2025-09-30 16:52:27 +02:00

1297 lines
42 KiB
TypeScript

import type { FC } from '@teact';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from '@teact';
import { getActions, getGlobal, withGlobal } from '../../global';
import type {
ApiBotPreviewMedia,
ApiChat,
ApiChatMember,
ApiMessage,
ApiSavedStarGift,
ApiStarGiftCollection,
ApiStoryAlbum,
ApiTypeStory,
ApiUser,
ApiUserStatus,
} from '../../api/types';
import type { ProfileCollectionKey } from '../../global/selectors/payments';
import type { TabState } from '../../global/types';
import type { AnimationLevel, ProfileState, ProfileTabType, SharedMediaType, ThemeKey, ThreadId } from '../../types';
import type { RegularLangKey } from '../../types/language';
import { MAIN_THREAD_ID } from '../../api/types';
import { AudioOrigin, MediaViewerOrigin, NewChatMembersProgress } from '../../types';
import { MEMBERS_SLICE, PROFILE_SENSITIVE_AREA, SHARED_MEDIA_SLICE, SLIDE_TRANSITION_DURATION } from '../../config';
import { selectActiveGiftsCollectionId } from '../../global/selectors/payments';
const CONTENT_PANEL_SHOW_DELAY = 300;
import {
getHasAdminRight,
getIsDownloading,
getIsSavedDialog,
getMessageDocument,
isChatAdmin,
isChatChannel,
isChatGroup,
isUserBot,
isUserRightBanned,
} from '../../global/helpers';
import { getSavedGiftKey } from '../../global/helpers/stars';
import {
selectActiveDownloads,
selectChat,
selectChatFullInfo,
selectChatMessages,
selectCurrentSharedMediaSearch,
selectIsChatRestricted,
selectIsCurrentUserPremium,
selectIsRightColumnShown,
selectMonoforumChannel,
selectPeerStories,
selectSimilarBotsIds,
selectSimilarChannelIds,
selectTabState,
selectTheme,
selectUser,
selectUserCommonChats,
selectUserFullInfo,
} from '../../global/selectors';
import { selectPremiumLimit } from '../../global/selectors/limits';
import { selectMessageDownloadableMedia } from '../../global/selectors/media';
import { selectSharedSettings } from '../../global/selectors/sharedState';
import { selectActiveStoriesCollectionId } from '../../global/selectors/stories';
import {
VTT_PROFILE_GIFTS,
VTT_RIGHT_PROFILE_COLLAPSE,
VTT_RIGHT_PROFILE_EXPAND,
} from '../../util/animations/viewTransitionTypes.ts';
import { areDeepEqual } from '../../util/areDeepEqual';
import { IS_TOUCH_ENV } from '../../util/browser/windowEnvironment';
import buildClassName from '../../util/buildClassName';
import { captureEvents, SwipeDirection } from '../../util/captureEvents';
import { isUserId } from '../../util/entities/ids';
import { resolveTransitionName } from '../../util/resolveTransitionName.ts';
import { LOCAL_TGS_URLS } from '../common/helpers/animatedAssets';
import renderText from '../common/helpers/renderText';
import { getSenderName } from '../left/search/helpers/getSenderName';
import { useViewTransition } from '../../hooks/animations/useViewTransition';
import { useVtn } from '../../hooks/animations/useVtn.ts';
import usePeerStoriesPolling from '../../hooks/polling/usePeerStoriesPolling';
import useTopOverscroll from '../../hooks/scroll/useTopOverscroll.tsx';
import useCacheBuster from '../../hooks/useCacheBuster';
import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
import useFlag from '../../hooks/useFlag';
import { useIntersectionObserver } from '../../hooks/useIntersectionObserver';
import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang';
import useSyncEffect from '../../hooks/useSyncEffect';
import useAsyncRendering from './hooks/useAsyncRendering';
import useProfileState from './hooks/useProfileState';
import useProfileViewportIds from './hooks/useProfileViewportIds';
import useTransitionFixes from './hooks/useTransitionFixes';
import AnimatedIconWithPreview from '../common/AnimatedIconWithPreview';
import Audio from '../common/Audio';
import Document from '../common/Document';
import SavedGift from '../common/gift/SavedGift';
import GroupChatInfo from '../common/GroupChatInfo';
import Icon from '../common/icons/Icon';
import Media from '../common/Media';
import NothingFound from '../common/NothingFound';
import PreviewMedia from '../common/PreviewMedia';
import PrivateChatInfo from '../common/PrivateChatInfo';
import ChatExtra from '../common/profile/ChatExtra';
import ProfileInfo from '../common/profile/ProfileInfo.tsx';
import WebLink from '../common/WebLink';
import ChatList from '../left/main/ChatList';
import MediaStory from '../story/MediaStory';
import Button from '../ui/Button';
import FloatingActionButton from '../ui/FloatingActionButton';
import InfiniteScroll from '../ui/InfiniteScroll';
import Link from '../ui/Link';
import ListItem, { type MenuItemContextAction } from '../ui/ListItem';
import Spinner from '../ui/Spinner';
import TabList from '../ui/TabList';
import Transition from '../ui/Transition';
import DeleteMemberModal from './DeleteMemberModal';
import StarGiftCollectionList from './gifts/StarGiftCollectionList';
import StoryAlbumList from './stories/StoryAlbumList';
import './Profile.scss';
type OwnProps = {
chatId: string;
threadId?: ThreadId;
profileState: ProfileState;
isMobile?: boolean;
isActive: boolean;
onProfileStateChange: (state: ProfileState) => void;
};
type StateProps = {
monoforumChannel?: ApiChat;
theme: ThemeKey;
isChannel?: boolean;
isBot?: boolean;
currentUserId?: string;
messagesById?: Record<number, ApiMessage>;
foundIds?: number[];
mediaSearchType?: SharedMediaType;
hasCommonChatsTab?: boolean;
hasStoriesTab?: boolean;
hasMembersTab?: boolean;
hasPreviewMediaTab?: boolean;
hasGiftsTab?: boolean;
gifts?: ApiSavedStarGift[];
storyAlbums?: ApiStoryAlbum[];
giftCollections?: ApiStarGiftCollection[];
areMembersHidden?: boolean;
canAddMembers?: boolean;
canDeleteMembers?: boolean;
members?: ApiChatMember[];
adminMembersById?: Record<string, ApiChatMember>;
commonChatIds?: string[];
storyIds?: number[];
pinnedStoryIds?: number[];
archiveStoryIds?: number[];
storyByIds?: Record<number, ApiTypeStory>;
selectedStoryAlbumId: ProfileCollectionKey;
activeCollectionId: ProfileCollectionKey;
giftsFilter?: any;
chatsById: Record<string, ApiChat>;
usersById: Record<string, ApiUser>;
userStatusesById: Record<string, ApiUserStatus>;
isRightColumnShown: boolean;
isRestricted?: boolean;
activeDownloads: TabState['activeDownloads'];
isChatProtected?: boolean;
nextProfileTab?: ProfileTabType;
animationLevel: AnimationLevel;
shouldWarnAboutFiles?: boolean;
similarChannels?: string[];
similarBots?: string[];
botPreviewMedia?: ApiBotPreviewMedia[];
isCurrentUserPremium?: boolean;
limitSimilarPeers: number;
isTopicInfo?: boolean;
isSavedDialog?: boolean;
forceScrollProfileTab?: boolean;
isSynced?: boolean;
hasAvatar?: boolean;
};
type TabProps = {
type: ProfileTabType;
key: RegularLangKey;
};
const TABS: TabProps[] = [
{ type: 'media', key: 'ProfileTabMedia' },
{ type: 'documents', key: 'ProfileTabFiles' },
{ type: 'links', key: 'ProfileTabLinks' },
{ type: 'audio', key: 'ProfileTabMusic' },
];
const HIDDEN_RENDER_DELAY = 1000;
const INTERSECTION_THROTTLE = 500;
const Profile: FC<OwnProps & StateProps> = ({
chatId,
isActive,
threadId,
profileState,
theme,
monoforumChannel,
isChannel,
isBot,
currentUserId,
messagesById,
foundIds,
storyIds,
pinnedStoryIds,
archiveStoryIds,
storyByIds,
selectedStoryAlbumId,
activeCollectionId,
giftsFilter,
mediaSearchType,
hasCommonChatsTab,
hasStoriesTab,
hasMembersTab,
hasPreviewMediaTab,
hasGiftsTab,
gifts,
storyAlbums,
giftCollections,
botPreviewMedia,
areMembersHidden,
canAddMembers,
canDeleteMembers,
commonChatIds,
members,
adminMembersById,
usersById,
userStatusesById,
chatsById,
isRightColumnShown,
isRestricted,
activeDownloads,
isChatProtected,
nextProfileTab,
animationLevel,
shouldWarnAboutFiles,
similarChannels,
similarBots,
isCurrentUserPremium,
limitSimilarPeers,
isTopicInfo,
isSavedDialog,
forceScrollProfileTab,
isSynced,
hasAvatar,
onProfileStateChange,
}) => {
const {
setSharedMediaSearchType,
loadMoreMembers,
loadCommonChats,
openChat,
searchSharedMediaMessages,
openMediaViewer,
openAudioPlayer,
focusMessage,
setNewChatMembersDialogState,
loadPeerProfileStories,
loadStoriesArchive,
openPremiumModal,
loadChannelRecommendations,
loadBotRecommendations,
loadPreviewMedias,
loadPeerSavedGifts,
resetGiftProfileFilter,
loadStarGiftCollections,
loadStoryAlbums,
resetSelectedStoryAlbum,
} = getActions();
const containerRef = useRef<HTMLDivElement>();
const transitionRef = useRef<HTMLDivElement>();
const oldLang = useOldLang();
const lang = useLang();
const [deletingUserId, setDeletingUserId] = useState<string | undefined>();
const [isGiftTransitionEnabled, enableGiftTransition, disableGiftTransition] = useFlag();
const profileId = isSavedDialog ? String(threadId) : chatId;
const isSavedMessages = profileId === currentUserId && !isSavedDialog;
const [isProfileExpanded, expandProfile, collapseProfile] = useFlag();
const [restoreContentHeightKey, setRestoreContentHeightKey] = useState(0);
const tabs = useMemo(() => {
const arr: TabProps[] = [];
if (isSavedMessages && !isSavedDialog) {
arr.push({ type: 'dialogs', key: 'ProfileTabSavedDialogs' });
}
if (hasStoriesTab) {
arr.push({ type: 'stories', key: 'ProfileTabStories' });
}
if (hasStoriesTab && isSavedMessages) {
arr.push({ type: 'storiesArchive', key: 'ProfileTabStoriesArchive' });
}
if (hasGiftsTab) {
arr.push({ type: 'gifts', key: 'ProfileTabGifts' });
}
if (hasMembersTab) {
arr.push({ type: 'members', key: isChannel ? 'ProfileTabSubscribers' : 'ProfileTabMembers' });
}
if (hasPreviewMediaTab) {
arr.push({ type: 'previewMedia', key: 'ProfileTabBotPreview' });
}
arr.push(...TABS);
// Voice messages filter currently does not work in forum topics. Return it when it's fixed on the server side.
if (!isTopicInfo) {
arr.push({ type: 'voice', key: 'ProfileTabVoice' });
}
if (hasCommonChatsTab) {
arr.push({ type: 'commonChats', key: 'ProfileTabSharedGroups' });
}
if (isChannel && similarChannels?.length) {
arr.push({ type: 'similarChannels', key: 'ProfileTabSimilarChannels' });
}
if (isBot && similarBots?.length) {
arr.push({ type: 'similarBots', key: 'ProfileTabSimilarBots' });
}
return arr.map((tab) => ({
type: tab.type,
title: lang(tab.key),
}));
}, [
isSavedMessages, isSavedDialog, hasStoriesTab, hasGiftsTab, hasMembersTab, hasPreviewMediaTab, isTopicInfo,
hasCommonChatsTab, isChannel, isBot, similarChannels?.length, similarBots?.length, lang,
]);
const initialTab = useMemo(() => {
if (!nextProfileTab) {
return 0;
}
const index = tabs.findIndex(({ type }) => type === nextProfileTab);
return index === -1 ? 0 : index;
}, [nextProfileTab, tabs]);
const [allowAutoScrollToTabs, startAutoScrollToTabsIfNeeded, stopAutoScrollToTabs] = useFlag(false);
const [activeTab, setActiveTab] = useState(initialTab);
useEffect(() => {
if (!nextProfileTab) return;
const index = tabs.findIndex(({ type }) => type === nextProfileTab);
if (index === -1) return;
setActiveTab(index);
}, [nextProfileTab, tabs]);
const handleSwitchTab = useCallback((index: number) => {
startAutoScrollToTabsIfNeeded();
setActiveTab(index);
}, []);
useEffect(() => {
if (hasPreviewMediaTab && !botPreviewMedia) {
loadPreviewMedias({ botId: chatId });
}
}, [chatId, botPreviewMedia, hasPreviewMediaTab]);
useEffect(() => {
if (isChannel && !similarChannels && isSynced) {
loadChannelRecommendations({ chatId });
}
}, [chatId, isChannel, similarChannels, isSynced]);
useEffect(() => {
if (isBot && !similarBots && isSynced) {
loadBotRecommendations({ userId: chatId });
}
}, [chatId, isBot, similarBots, isSynced]);
useEffect(() => {
resetSelectedStoryAlbum();
}, [chatId]);
useSyncEffect(() => {
enableGiftTransition();
}, [giftsFilter]);
useSyncEffect(() => {
disableGiftTransition();
}, [gifts]);
useEffect(() => {
if (hasGiftsTab && isSynced) {
loadStarGiftCollections({ peerId: chatId });
loadStoryAlbums({ peerId: chatId });
}
}, [chatId, hasGiftsTab, isSynced]);
const [renderingGifts, setRenderingGifts] = useState(gifts);
const { startViewTransition } = useViewTransition();
const { createVtnStyle } = useVtn();
const giftIds = useMemo(() => renderingGifts?.map((gift) => getSavedGiftKey(gift)), [renderingGifts]);
const renderingActiveTab = activeTab > tabs.length - 1 ? tabs.length - 1 : activeTab;
const tabType = tabs[renderingActiveTab].type;
const handleLoadCommonChats = useCallback(() => {
loadCommonChats({ userId: chatId });
}, [chatId]);
const handleLoadPeerStories = useCallback(({ offsetId }: { offsetId: number }) => {
loadPeerProfileStories({ peerId: chatId, offsetId });
}, [chatId]);
const handleLoadStoriesArchive = useCallback(({ offsetId }: { offsetId: number }) => {
loadStoriesArchive({ peerId: chatId, offsetId });
}, [chatId]);
const handleLoadGifts = useCallback(() => {
loadPeerSavedGifts({ peerId: chatId });
}, [chatId]);
const handleLoadMoreMembers = useCallback(() => {
loadMoreMembers({ chatId });
}, [chatId, loadMoreMembers]);
useEffectWithPrevDeps(([prevGifts]) => {
if (areDeepEqual(gifts, prevGifts)) {
return;
}
if (!gifts || !prevGifts || !isGiftTransitionEnabled) {
setRenderingGifts(gifts);
return;
}
const prevGiftIds = prevGifts.map((gift) => getSavedGiftKey(gift));
const newGiftIds = gifts.map((gift) => getSavedGiftKey(gift));
const hasOrderChanged = prevGiftIds.some((id, index) => id !== newGiftIds[index]);
if (hasOrderChanged) {
startViewTransition(VTT_PROFILE_GIFTS, () => {
setRenderingGifts(gifts);
});
} else {
setRenderingGifts(gifts);
}
}, [gifts, startViewTransition, isGiftTransitionEnabled]);
const [resultType, viewportIds, getMore, noProfileInfo] = useProfileViewportIds({
loadMoreMembers: handleLoadMoreMembers,
searchMessages: searchSharedMediaMessages,
loadStories: handleLoadPeerStories,
loadStoriesArchive: handleLoadStoriesArchive,
loadMoreGifts: handleLoadGifts,
loadCommonChats: handleLoadCommonChats,
tabType,
mediaSearchType,
groupChatMembers: members,
commonChatIds,
usersById,
userStatusesById,
chatsById,
chatMessages: messagesById,
foundIds,
threadId,
storyIds,
giftIds,
pinnedStoryIds,
archiveStoryIds,
similarChannels,
similarBots,
});
const isFirstTab = (isSavedMessages && resultType === 'dialogs')
|| (hasStoriesTab && resultType === 'stories')
|| resultType === 'members'
|| (!hasMembersTab && resultType === 'media');
const activeKey = tabs.findIndex(({ type }) => type === resultType);
const [isGiftCollectionsShowed, markGiftCollectionsShowed, unmarkGiftCollectionsShowed] = useFlag(false);
const [isStoryAlbumsShowed, markStoryAlbumsShowed, unmarkStoryAlbums] = useFlag(false);
const hasGiftsCollections = giftCollections && giftCollections.length > 0;
const hasStoryAlbums = storyAlbums && storyAlbums.length > 0;
const isGiftsResult = resultType === 'gifts';
const isStoriesResult = resultType === 'stories';
const shouldShowContentPanel = (isGiftsResult && hasGiftsCollections) || (isStoriesResult && hasStoryAlbums);
useEffect(() => {
if (hasGiftsCollections) {
setTimeout(() => {
markGiftCollectionsShowed();
}, CONTENT_PANEL_SHOW_DELAY);
} else {
unmarkGiftCollectionsShowed();
}
if (hasStoryAlbums) {
setTimeout(() => {
markStoryAlbumsShowed();
}, CONTENT_PANEL_SHOW_DELAY);
} else {
unmarkStoryAlbums();
}
}, [hasGiftsCollections, hasStoryAlbums, markGiftCollectionsShowed, markStoryAlbumsShowed]);
usePeerStoriesPolling(resultType === 'members' ? viewportIds as string[] : undefined);
const handleStopAutoScrollToTabs = useLastCallback(() => {
stopAutoScrollToTabs();
});
const handleExpandProfile = useLastCallback(() => {
if (isProfileExpanded) return;
startViewTransition(VTT_RIGHT_PROFILE_EXPAND, () => {
expandProfile();
});
});
const handleCollapseProfile = useLastCallback(() => {
if (!isProfileExpanded) return;
startViewTransition(VTT_RIGHT_PROFILE_COLLAPSE, () => {
collapseProfile();
});
});
const { handleScroll } = useProfileState({
containerRef,
tabType: resultType,
profileState,
forceScrollProfileTab,
allowAutoScrollToTabs,
onProfileStateChange,
handleStopAutoScrollToTabs,
});
const { applyTransitionFix, releaseTransitionFix } = useTransitionFixes(containerRef);
const [cacheBuster, resetCacheBuster] = useCacheBuster();
const { observe: observeIntersectionForMedia } = useIntersectionObserver({
rootRef: containerRef,
throttleMs: INTERSECTION_THROTTLE,
});
const handleTransitionStop = useLastCallback(() => {
releaseTransitionFix();
resetCacheBuster();
});
const handleNewMemberDialogOpen = useLastCallback(() => {
setNewChatMembersDialogState({ newChatMembersProgress: NewChatMembersProgress.InProgress });
});
// Update search type when switching tabs or forum topics
useEffect(() => {
setSharedMediaSearchType({ mediaType: tabType as SharedMediaType });
}, [setSharedMediaSearchType, tabType, threadId]);
const handleSelectMedia = useLastCallback((messageId: number) => {
openMediaViewer({
chatId: profileId,
threadId: MAIN_THREAD_ID,
messageId,
origin: MediaViewerOrigin.SharedMedia,
});
});
const handleSelectPreviewMedia = useLastCallback((index: number) => {
openMediaViewer({
standaloneMedia: botPreviewMedia?.flatMap((item) => item?.content.photo
|| item?.content.video).filter(Boolean),
origin: MediaViewerOrigin.PreviewMedia,
mediaIndex: index,
});
});
const handlePlayAudio = useLastCallback((messageId: number) => {
openAudioPlayer({ chatId: profileId, messageId });
});
const handleMemberClick = useLastCallback((id: string) => {
openChat({ id });
});
const handleMessageFocus = useLastCallback((message: ApiMessage) => {
focusMessage({ chatId: message.chatId, messageId: message.id });
});
const handleDeleteMembersModalClose = useLastCallback(() => {
setDeletingUserId(undefined);
});
useEffectWithPrevDeps(([prevHasMemberTabs]) => {
if (prevHasMemberTabs === undefined || activeTab === 0 || prevHasMemberTabs === hasMembersTab) {
return;
}
const newActiveTab = activeTab + (hasMembersTab ? 1 : -1);
setActiveTab(Math.min(newActiveTab, tabs.length - 1));
}, [hasMembersTab, activeTab, tabs]);
const handleResetGiftsFilter = useLastCallback(() => {
resetGiftProfileFilter({ peerId: chatId });
});
useTopOverscroll(
containerRef, handleExpandProfile, handleCollapseProfile, !hasAvatar,
);
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 && !botPreviewMedia) || (!gifts?.length && resultType === 'gifts')) {
renderingDelay = SLIDE_TRANSITION_DURATION;
}
const canRenderContent = useAsyncRendering([chatId, threadId, resultType,
renderingActiveTab, activeCollectionId, selectedStoryAlbumId], renderingDelay);
function getMemberContextAction(memberId: string): MenuItemContextAction[] | undefined {
return memberId === currentUserId || !canDeleteMembers ? undefined : [{
title: oldLang('lng_context_remove_from_group'),
icon: 'stop',
handler: () => {
setDeletingUserId(memberId);
},
}];
}
function renderNothingFoundGiftsWithFilter() {
return (
<div className="nothing-found-gifts">
<AnimatedIconWithPreview
size={160}
tgsUrl={LOCAL_TGS_URLS.SearchingDuck}
nonInteractive
noLoop
/>
<div className="description">
{lang('GiftSearchEmpty')}
</div>
<Link
className="date"
onClick={handleResetGiftsFilter}
>
{lang('GiftSearchReset')}
</Link>
</div>
);
}
function renderContent() {
if (resultType === 'dialogs') {
return (
<ChatList className="saved-dialogs" folderType="saved" isActive />
);
}
const noContent = (!viewportIds && !botPreviewMedia) || !canRenderContent || !messagesById;
const noSpinner = isFirstTab && !canRenderContent;
return (
<div>
{renderCategories()}
{renderSpinnerOrContent(noContent, noSpinner)}
</div>
);
}
function renderCategories() {
if (resultType === 'gifts') {
return (
<div
className={buildClassName(
'contentCategoriesPanel',
!shouldShowContentPanel && 'hiddenPanel',
isGiftCollectionsShowed && 'noTransition',
)}
>
<StarGiftCollectionList peerId={chatId} />
</div>
);
}
if (resultType === 'stories') {
return (
<div
className={buildClassName(
'contentCategoriesPanel',
!shouldShowContentPanel && 'hiddenPanel',
isStoryAlbumsShowed && 'noTransition',
)}
>
<StoryAlbumList peerId={chatId} />
</div>
);
}
return undefined;
}
function renderSpinnerOrContentBase(noContent: boolean, noSpinner: boolean) {
if (noContent) {
const forceRenderHiddenMembers = Boolean(resultType === 'members' && areMembersHidden);
return (
<div
className="content empty-list"
>
{!noSpinner && !forceRenderHiddenMembers && <Spinner />}
{forceRenderHiddenMembers && <NothingFound text={lang('ChatMemberListNoAccess')} />}
</div>
);
}
const isViewportIdsEmpty = viewportIds && !viewportIds?.length;
if (isViewportIdsEmpty && resultType === 'gifts') {
return renderNothingFoundGiftsWithFilter();
}
if (isViewportIdsEmpty) {
let text: string;
switch (resultType) {
case 'members':
text = areMembersHidden ? lang('ChatMemberListNoAccess') : lang('NoMembersFound');
break;
case 'commonChats':
text = oldLang('NoGroupsInCommon');
break;
case 'documents':
text = oldLang('lng_media_file_empty');
break;
case 'links':
text = oldLang('lng_media_link_empty');
break;
case 'audio':
text = oldLang('lng_media_song_empty');
break;
case 'voice':
text = oldLang('lng_media_audio_empty');
break;
case 'stories':
text = oldLang('StoryList.SavedEmptyState.Title');
break;
case 'storiesArchive':
text = oldLang('StoryList.ArchivedEmptyState.Title');
break;
default:
text = oldLang('SharedMedia.EmptyTitle');
}
return (
<div className="content empty-list">
<NothingFound text={text} />
</div>
);
}
if (!messagesById) {
// A TypeScript assertion, should never be really reached
return;
}
const noTransition = resultType === 'gifts' ? isGiftCollectionsShowed
: resultType === 'stories' ? isStoryAlbumsShowed : false;
return (
<div
className={buildClassName(
`content ${resultType}-list`,
shouldShowContentPanel && 'showContentPanel',
noTransition && 'noTransition',
)}
dir={oldLang.isRtl && resultType === 'media' ? 'rtl' : undefined}
teactFastList
>
{resultType === 'media' ? (
(viewportIds as number[]).map((id) => messagesById[id] && (
<Media
key={id}
message={messagesById[id]}
isProtected={isChatProtected || messagesById[id].isProtected}
observeIntersection={observeIntersectionForMedia}
onClick={handleSelectMedia}
/>
))
) : (resultType === 'stories' || resultType === 'storiesArchive') ? (
(viewportIds as number[]).map((id, i) => storyByIds?.[id] && (
<MediaStory
teactOrderKey={i}
key={`${resultType}_${id}`}
story={storyByIds[id]}
isArchive={resultType === 'storiesArchive'}
/>
))
) : resultType === 'documents' ? (
(viewportIds as number[]).map((id) => messagesById[id] && (
<Document
key={id}
document={getMessageDocument(messagesById[id])!}
withDate
smaller
className="scroll-item"
isDownloading={getIsDownloading(activeDownloads, getMessageDocument(messagesById[id])!)}
observeIntersection={observeIntersectionForMedia}
onDateClick={handleMessageFocus}
message={messagesById[id]}
shouldWarnAboutFiles={shouldWarnAboutFiles}
/>
))
) : resultType === 'links' ? (
(viewportIds as number[]).map((id) => messagesById[id] && (
<WebLink
key={id}
message={messagesById[id]}
isProtected={isChatProtected || messagesById[id].isProtected}
observeIntersection={observeIntersectionForMedia}
onMessageClick={handleMessageFocus}
/>
))
) : resultType === 'audio' ? (
(viewportIds as number[]).map((id) => messagesById[id] && (
<Audio
key={id}
theme={theme}
message={messagesById[id]}
origin={AudioOrigin.SharedMedia}
date={messagesById[id].date}
className="scroll-item"
onPlay={handlePlayAudio}
onDateClick={handleMessageFocus}
canDownload={!isChatProtected && !messagesById[id].isProtected}
isDownloading={getIsDownloading(activeDownloads, messagesById[id].content.audio!)}
/>
))
) : resultType === 'voice' ? (
(viewportIds as number[]).map((id) => {
const global = getGlobal();
const message = messagesById[id];
if (!message) return undefined;
const media = selectMessageDownloadableMedia(global, message)!;
return messagesById[id] && (
<Audio
key={id}
theme={theme}
message={message}
senderTitle={getSenderName(oldLang, message, chatsById, usersById)}
origin={AudioOrigin.SharedMedia}
date={message.date}
className="scroll-item"
onPlay={handlePlayAudio}
onDateClick={handleMessageFocus}
canDownload={!isChatProtected && !message.isProtected}
isDownloading={getIsDownloading(activeDownloads, media)}
/>
);
})
) : resultType === 'members' ? (
(viewportIds as string[]).map((id, i) => (
<ListItem
key={id}
teactOrderKey={i}
className="chat-item-clickable contact-list-item scroll-item small-icon"
onClick={() => handleMemberClick(id)}
contextActions={getMemberContextAction(id)}
>
<PrivateChatInfo userId={id} adminMember={adminMembersById?.[id]} forceShowSelf withStory />
</ListItem>
))
) : resultType === 'commonChats' ? (
(viewportIds as string[]).map((id, i) => (
<ListItem
key={id}
teactOrderKey={i}
className="chat-item-clickable scroll-item small-icon"
onClick={() => openChat({ id })}
>
<GroupChatInfo chatId={id} />
</ListItem>
))
) : resultType === 'previewMedia' ? (
botPreviewMedia!.map((media, i) => (
<PreviewMedia
key={media.date}
media={media}
isProtected={isChatProtected}
observeIntersection={observeIntersectionForMedia}
onClick={handleSelectPreviewMedia}
index={i}
/>
))
) : resultType === 'similarChannels' ? (
<div key={resultType}>
{(viewportIds as string[]).map((channelId, i) => (
<ListItem
key={channelId}
teactOrderKey={i}
className={buildClassName(
'chat-item-clickable search-result',
!isCurrentUserPremium && i === similarChannels!.length - 1 && 'blured',
)}
onClick={() => openChat({ id: channelId })}
>
<GroupChatInfo avatarSize="large" chatId={channelId} withFullInfo />
</ListItem>
))}
{!isCurrentUserPremium && (
<>
{}
<Button className="show-more-channels" onClick={() => openPremiumModal()}>
{oldLang('UnlockSimilar')}
<Icon name="unlock-badge" />
</Button>
<div className="more-similar">
{renderText(oldLang('MoreSimilarText', limitSimilarPeers), ['simple_markdown'])}
</div>
</>
)}
</div>
) : resultType === 'similarBots' ? (
<div key={resultType}>
{(viewportIds as string[]).map((userId, i) => (
<ListItem
key={userId}
teactOrderKey={i}
className={buildClassName(
'chat-item-clickable search-result',
!isCurrentUserPremium && i === similarBots!.length - 1 && 'blured',
)}
onClick={() => openChat({ id: userId })}
>
{isUserId(userId) ? (
<PrivateChatInfo
userId={userId}
avatarSize="medium"
/>
) : (
<GroupChatInfo
chatId={userId}
avatarSize="medium"
/>
)}
</ListItem>
))}
{!isCurrentUserPremium && (
<>
{}
<Button className="show-more-bots" onClick={() => openPremiumModal()}>
{lang('UnlockMoreSimilarBots')}
<Icon name="unlock-badge" />
</Button>
<div className="more-similar">
{renderText(lang('MoreSimilarBotsDescription', { count: limitSimilarPeers }, {
withNodes: true,
withMarkdown: true,
pluralValue: limitSimilarPeers,
}))}
</div>
</>
)}
</div>
) : resultType === 'gifts' ? (
(renderingGifts?.map((gift) => {
return (
<SavedGift
peerId={chatId}
key={getSavedGiftKey(gift)}
className="saved-gift"
style={createVtnStyle(getSavedGiftKey(gift))}
gift={gift}
observeIntersection={observeIntersectionForMedia}
/>
);
}))
) : undefined}
</div>
);
}
const shouldUseTransitionForContent = resultType === 'stories' || resultType === 'gifts';
const contentTransitionKey = (() => {
if (resultType === 'stories') {
return selectedStoryAlbumId === 'all' ? 0 : selectedStoryAlbumId;
}
if (resultType === 'gifts') {
return activeCollectionId === 'all' ? 0 : activeCollectionId;
}
return 0;
})();
const handleOnStop = useLastCallback(() => {
setRestoreContentHeightKey(restoreContentHeightKey + 1);
});
function renderProfileInfo(peerId: string, isReady: boolean) {
return (
<div className="profile-info">
<ProfileInfo
isExpanded={isProfileExpanded}
peerId={peerId}
canPlayVideo={isReady}
isForMonoforum={Boolean(monoforumChannel)}
/>
<ChatExtra
chatOrUserId={profileId}
isSavedDialog={isSavedDialog}
style={createVtnStyle('chatExtra')}
/>
</div>
);
}
function renderSpinnerOrContent(noContent: boolean, noSpinner: boolean) {
const baseContent = renderSpinnerOrContentBase(noContent, noSpinner);
const isSpinner = noContent && !noSpinner;
if (shouldUseTransitionForContent) {
return (
<Transition
className={`${resultType}-list`}
activeKey={contentTransitionKey}
name={resolveTransitionName('slideOptimized', animationLevel, undefined, oldLang.isRtl)}
shouldCleanup
shouldRestoreHeight
restoreHeightKey={restoreContentHeightKey}
contentSelector=".Transition > .Transition_slide-active > .content"
>
<Transition
activeKey={isSpinner ? 0 : 1}
name="fade"
shouldCleanup
shouldRestoreHeight
restoreHeightKey={restoreContentHeightKey}
contentSelector=".content"
onStop={handleOnStop}
>
{baseContent}
</Transition>
</Transition>
);
}
return (
<Transition
activeKey={isSpinner ? 0 : 1}
name="fade"
shouldCleanup
shouldRestoreHeight
>
{baseContent}
</Transition>
);
}
const activeListSelector = `.shared-media-transition > .Transition_slide-active`;
const itemSelector = !shouldUseTransitionForContent
? `${activeListSelector} .${resultType}-list > .scroll-item`
/* eslint-disable @stylistic/max-len */
: `${activeListSelector} > .Transition > .Transition_slide-active > .Transition > .Transition_slide-active > .gifts-list > .scroll-item`;
return (
<InfiniteScroll
ref={containerRef}
className="Profile custom-scroll"
itemSelector={itemSelector}
items={canRenderContent ? viewportIds : undefined}
cacheBuster={cacheBuster}
sensitiveArea={PROFILE_SENSITIVE_AREA}
preloadBackwards={canRenderContent ? (resultType === 'members' ? MEMBERS_SLICE : SHARED_MEDIA_SLICE) : 0}
// To prevent scroll jumps caused by reordering member list
noScrollRestoreOnTop
noFastList
onLoadMore={getMore}
onScroll={handleScroll}
>
{!noProfileInfo && !isSavedMessages && (
renderProfileInfo(
monoforumChannel?.id || profileId,
isRightColumnShown && canRenderContent,
)
)}
{!isRestricted && (
<div
className="shared-media"
style={createVtnStyle('sharedMedia')}
>
<Transition
ref={transitionRef}
name={resolveTransitionName('slideOptimized', animationLevel, undefined, oldLang.isRtl)}
activeKey={activeKey}
renderCount={tabs.length}
shouldRestoreHeight
className="shared-media-transition"
onStart={applyTransitionFix}
onStop={handleTransitionStop}
restoreHeightKey={shouldUseTransitionForContent ? restoreContentHeightKey : undefined}
contentSelector={shouldUseTransitionForContent
? '.Transition > .Transition_slide-active > .Transition > .Transition_slide-active > .content'
: undefined}
>
{renderContent()}
</Transition>
<TabList activeTab={renderingActiveTab} tabs={tabs} onSwitchTab={handleSwitchTab} />
</div>
)}
{canAddMembers && (
<FloatingActionButton
className={buildClassName(!isActive && 'hidden')}
isShown={canRenderContent}
onClick={handleNewMemberDialogOpen}
ariaLabel={oldLang('lng_channel_add_users')}
>
<Icon name="add-user-filled" />
</FloatingActionButton>
)}
{canDeleteMembers && (
<DeleteMemberModal
isOpen={Boolean(deletingUserId)}
userId={deletingUserId}
onClose={handleDeleteMembersModalClose}
/>
)}
</InfiniteScroll>
);
};
export default memo(withGlobal<OwnProps>(
(global, {
chatId, threadId, isMobile,
}): Complete<StateProps> => {
const user = selectUser(global, chatId);
const chat = selectChat(global, chatId);
const chatFullInfo = selectChatFullInfo(global, chatId);
const userFullInfo = selectUserFullInfo(global, chatId);
const messagesById = selectChatMessages(global, chatId);
const { animationLevel, shouldWarnAboutFiles } = selectSharedSettings(global);
const { currentType: mediaSearchType, resultsByType } = selectCurrentSharedMediaSearch(global) || {};
const { foundIds } = (resultsByType && mediaSearchType && resultsByType[mediaSearchType]) || {};
const isTopicInfo = Boolean(chat?.isForum && threadId && threadId !== MAIN_THREAD_ID);
const { byId: usersById, statusesById: userStatusesById } = global.users;
const { byId: chatsById } = global.chats;
const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
const isGroup = chat && isChatGroup(chat);
const isChannel = chat && isChatChannel(chat);
const isBot = user && isUserBot(user);
const hasMembersTab = !isTopicInfo && !isSavedDialog
&& (isGroup || (isChannel && isChatAdmin(chat))) && !chat?.isMonoforum;
const members = chatFullInfo?.members;
const adminMembersById = chatFullInfo?.adminMembersById;
const areMembersHidden = hasMembersTab && chat
&& (chat.isForbidden || (chatFullInfo && !chatFullInfo.canViewMembers));
const canAddMembers = hasMembersTab && chat
&& (getHasAdminRight(chat, 'inviteUsers') || (!isChannel && !isUserRightBanned(chat, 'inviteUsers'))
|| chat.isCreator);
const canDeleteMembers = hasMembersTab && chat && (getHasAdminRight(chat, 'banUsers') || chat.isCreator);
const activeDownloads = selectActiveDownloads(global);
const { similarChannelIds } = selectSimilarChannelIds(global, chatId) || {};
const { similarBotsIds } = selectSimilarBotsIds(global, chatId) || {};
const isCurrentUserPremium = selectIsCurrentUserPremium(global);
const peer = user || chat;
const peerFullInfo = userFullInfo || chatFullInfo;
const hasCommonChatsTab = user && !user.isSelf && !isUserBot(user) && !isSavedDialog
&& Boolean(userFullInfo?.commonChatsCount);
const commonChats = selectUserCommonChats(global, chatId);
const hasPreviewMediaTab = userFullInfo?.botInfo?.hasPreviewMedia;
const botPreviewMedia = global.users.previewMediaByBotId[chatId];
const hasStoriesTab = peer && (user?.isSelf || (!peer.areStoriesHidden && peerFullInfo?.hasPinnedStories))
&& !isSavedDialog;
const peerStories = hasStoriesTab ? selectPeerStories(global, peer.id) : undefined;
const tabState = selectTabState(global);
const { nextProfileTab, forceScrollProfileTab, savedGifts } = tabState;
const selectedStoryAlbumId = selectActiveStoriesCollectionId(global);
const storyIds = selectedStoryAlbumId !== 'all'
? peerStories?.idsByAlbumId?.[selectedStoryAlbumId]?.ids
: peerStories?.profileIds;
const pinnedStoryIds = peerStories?.pinnedIds;
const storyByIds = peerStories?.byId;
const archiveStoryIds = peerStories?.archiveIds;
const hasGiftsTab = Boolean(peerFullInfo?.starGiftCount) && !isSavedDialog;
const activeCollectionId = selectActiveGiftsCollectionId(global, chatId);
const peerGifts = savedGifts.collectionsByPeerId[chatId]?.[activeCollectionId];
const storyAlbums = global.stories.albumsByPeerId?.[chatId];
const giftCollections = global.starGiftCollections?.byPeerId?.[chatId];
const monoforumChannel = selectMonoforumChannel(global, chatId);
const isRestricted = chat && selectIsChatRestricted(global, chat.id);
const hasAvatar = Boolean(peer?.avatarPhotoId);
return {
theme: selectTheme(global),
isChannel,
isBot,
messagesById,
foundIds,
mediaSearchType,
hasCommonChatsTab,
hasStoriesTab,
hasMembersTab,
hasPreviewMediaTab,
areMembersHidden,
canAddMembers,
canDeleteMembers,
currentUserId: global.currentUserId,
isRightColumnShown: selectIsRightColumnShown(global, isMobile),
isRestricted,
activeDownloads,
usersById,
userStatusesById,
chatsById,
storyIds,
hasGiftsTab,
gifts: peerGifts?.gifts,
storyAlbums,
giftCollections,
pinnedStoryIds,
archiveStoryIds,
storyByIds,
selectedStoryAlbumId,
activeCollectionId,
giftsFilter: savedGifts.filter,
isChatProtected: chat?.isProtected,
nextProfileTab,
forceScrollProfileTab,
animationLevel,
shouldWarnAboutFiles,
similarChannels: similarChannelIds,
similarBots: similarBotsIds,
botPreviewMedia,
isCurrentUserPremium,
isTopicInfo,
isSavedDialog,
isSynced: global.isSynced,
limitSimilarPeers: selectPremiumLimit(global, 'recommendedChannels'),
members: hasMembersTab ? members : undefined,
adminMembersById: hasMembersTab ? adminMembersById : undefined,
commonChatIds: commonChats?.ids,
monoforumChannel,
hasAvatar,
};
},
)(Profile));