From e454abd4f33436ce31822101853e2599c6587752 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Thu, 6 May 2021 01:47:52 +0300 Subject: [PATCH] Profile: Infinite scroll for members --- src/api/gramjs/methods/chats.ts | 35 ++++++++++------- src/api/gramjs/methods/index.ts | 2 +- src/components/right/Profile.tsx | 19 +++++++--- src/components/right/RightColumn.scss | 2 +- .../right/hooks/useProfileViewportIds.ts | 30 +++++++++++++-- src/components/ui/InfiniteScroll.tsx | 6 +-- src/config.ts | 7 ++-- src/global/types.ts | 2 +- src/modules/actions/api/chats.ts | 38 ++++++++++++++++++- src/modules/selectors/chats.ts | 4 +- 10 files changed, 110 insertions(+), 35 deletions(-) diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index e7ddba2a8..52747c329 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -12,7 +12,7 @@ import { ApiChatAdminRights, } from '../../types'; -import { DEBUG, ARCHIVED_FOLDER_ID, CHANNEL_MEMBERS_LIMIT } from '../../../config'; +import { DEBUG, ARCHIVED_FOLDER_ID, MEMBERS_LOAD_SLICE } from '../../../config'; import { invokeRequest, uploadFile } from './client'; import { buildApiChatFromDialog, @@ -156,11 +156,12 @@ export async function fetchChats({ export function fetchFullChat(chat: ApiChat) { const { id, accessHash, adminRights } = chat; + const input = buildInputEntity(id, accessHash); return input instanceof GramJs.InputChannel - ? getFullChannelInfo(input, adminRights) - : getFullChatInfo(input as number); + ? getFullChannelInfo(id, accessHash!, adminRights) + : getFullChatInfo(id); } export async function searchChats({ query }: { query: string }) { @@ -298,7 +299,9 @@ async function getFullChatInfo(chatId: number): Promise<{ fullInfo: ApiChatFullInfo; users?: ApiUser[]; } | undefined> { - const result = await invokeRequest(new GramJs.messages.GetFullChat({ chatId })); + const result = await invokeRequest(new GramJs.messages.GetFullChat({ + chatId: buildInputEntity(chatId) as number, + })); if (!result || !(result.fullChat instanceof GramJs.ChatFull)) { return undefined; @@ -328,10 +331,13 @@ async function getFullChatInfo(chatId: number): Promise<{ } async function getFullChannelInfo( - channel: GramJs.InputChannel, + id: number, + accessHash: string, adminRights?: ApiChatAdminRights, ) { - const result = await invokeRequest(new GramJs.channels.GetFullChannel({ channel })); + const result = await invokeRequest(new GramJs.channels.GetFullChannel({ + channel: buildInputEntity(id, accessHash) as GramJs.InputChannel, + })); if (!result || !(result.fullChat instanceof GramJs.ChannelFull)) { return undefined; @@ -354,12 +360,12 @@ async function getFullChannelInfo( ? exportedInvite.link : undefined; - const { members, users } = (canViewParticipants && await getChannelMembers(channel)) || {}; + const { members, users } = (canViewParticipants && await fetchMembers(id, accessHash)) || {}; const { members: kickedMembers, users: bannedUsers } = ( - canViewParticipants && adminRights && await getChannelMembers(channel, 'kicked') + canViewParticipants && adminRights && await fetchMembers(id, accessHash, 'kicked') ) || {}; const { members: adminMembers, users: adminUsers } = ( - canViewParticipants && adminRights && await getChannelMembers(channel, 'admin') + canViewParticipants && adminRights && await fetchMembers(id, accessHash, 'admin') ) || {}; return { @@ -785,9 +791,11 @@ export function toggleSignatures({ type ChannelMembersFilter = 'kicked' | 'admin' | 'recent'; -async function getChannelMembers( - channel: GramJs.InputChannel, +export async function fetchMembers( + chatId: number, + accessHash: string, memberFilter: ChannelMembersFilter = 'recent', + offset?: number, ) { let filter: GramJs.TypeChannelParticipantsFilter; @@ -804,9 +812,10 @@ async function getChannelMembers( } const result = await invokeRequest(new GramJs.channels.GetParticipants({ - channel, + channel: buildInputEntity(chatId, accessHash) as GramJs.InputChannel, filter, - limit: CHANNEL_MEMBERS_LIMIT, + offset, + limit: MEMBERS_LOAD_SLICE, })); if (!result || result instanceof GramJs.channels.ChannelParticipantsNotModified) { diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index 2596fc059..9456c7e0d 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -14,7 +14,7 @@ export { fetchChatFolders, editChatFolder, deleteChatFolder, fetchRecommendedChatFolders, getChatByUsername, togglePreHistoryHidden, updateChatDefaultBannedRights, updateChatMemberBannedRights, updateChatTitle, updateChatAbout, toggleSignatures, updateChatAdmin, fetchGroupsForDiscussion, setDiscussionGroup, - migrateChat, openChatByInvite, + migrateChat, openChatByInvite, fetchMembers, } from './chats'; export { diff --git a/src/components/right/Profile.tsx b/src/components/right/Profile.tsx index cbc07a021..58a3d9d69 100644 --- a/src/components/right/Profile.tsx +++ b/src/components/right/Profile.tsx @@ -14,10 +14,10 @@ import { MediaViewerOrigin, ProfileState, ProfileTabType, SharedMediaType, } from '../../types'; -import { SHARED_MEDIA_SLICE, SLIDE_TRANSITION_DURATION } from '../../config'; +import { MEMBERS_SLICE, SHARED_MEDIA_SLICE, SLIDE_TRANSITION_DURATION } from '../../config'; import { IS_TOUCH_ENV } from '../../util/environment'; import { - isChatAdmin, isChatChannel, isChatGroup, isChatPrivate, + isChatAdmin, isChatBasicGroup, isChatChannel, isChatGroup, isChatPrivate, } from '../../modules/helpers'; import { selectChatMessages, @@ -57,6 +57,7 @@ type OwnProps = { }; type StateProps = { + isBasicGroup?: boolean; isChannel?: boolean; resolvedUserId?: number; chatMessages?: Record; @@ -72,7 +73,7 @@ type StateProps = { }; type DispatchProps = Pick; @@ -89,6 +90,7 @@ const Profile: FC = ({ chatId, profileState, onProfileStateChange, + isBasicGroup, isChannel, resolvedUserId, chatMessages, @@ -102,6 +104,7 @@ const Profile: FC = ({ isRestricted, lastSyncTime, setLocalMediaSearchType, + loadMoreMembers, searchMediaMessagesLocal, openMediaViewer, openAudioPlayer, @@ -125,7 +128,7 @@ const Profile: FC = ({ const tabType = tabs[activeTab].type as ProfileTabType; const [resultType, viewportIds, getMore, noProfileInfo] = useProfileViewportIds( - isRightColumnShown, searchMediaMessagesLocal, tabType, mediaSearchType, members, + isRightColumnShown, loadMoreMembers, searchMediaMessagesLocal, tabType, mediaSearchType, members, usersById, chatMessages, foundIds, chatId, lastSyncTime, ); const activeKey = tabs.findIndex(({ type }) => type === resultType); @@ -306,8 +309,9 @@ const Profile: FC = ({ itemSelector={buildInfiniteScrollItemSelector(resultType)} items={viewportIds} cacheBuster={cacheBuster} - preloadBackwards={SHARED_MEDIA_SLICE} - isDisabled={tabType === 'members'} + sensitiveArea={500} + preloadBackwards={resultType === 'members' ? MEMBERS_SLICE : SHARED_MEDIA_SLICE} + isDisabled={resultType === 'members' && isBasicGroup} noFastList onLoadMore={getMore} onScroll={handleScroll} @@ -366,6 +370,7 @@ export default memo(withGlobal( const { byId: usersById } = global.users; const isGroup = chat && isChatGroup(chat); + const isBasicGroup = chat && isChatBasicGroup(chat); const isChannel = chat && isChatChannel(chat); const hasMembersTab = isGroup || (isChannel && isChatAdmin(chat!)); const members = chat && chat.fullInfo && chat.fullInfo.members; @@ -379,6 +384,7 @@ export default memo(withGlobal( } return { + isBasicGroup, isChannel, resolvedUserId, chatMessages, @@ -397,6 +403,7 @@ export default memo(withGlobal( }, (setGlobal, actions): DispatchProps => pick(actions, [ 'setLocalMediaSearchType', + 'loadMoreMembers', 'searchMediaMessagesLocal', 'openMediaViewer', 'openAudioPlayer', diff --git a/src/components/right/RightColumn.scss b/src/components/right/RightColumn.scss index 0ee6b7540..60a4271be 100644 --- a/src/components/right/RightColumn.scss +++ b/src/components/right/RightColumn.scss @@ -17,7 +17,7 @@ // @optimization &:not(:hover) { - .Picker .chat-item-clickable:nth-child(n + 18) { + .chat-item-clickable:nth-child(n + 18) { display: none !important; } } diff --git a/src/components/right/hooks/useProfileViewportIds.ts b/src/components/right/hooks/useProfileViewportIds.ts index 60de4294d..e2df1fc0d 100644 --- a/src/components/right/hooks/useProfileViewportIds.ts +++ b/src/components/right/hooks/useProfileViewportIds.ts @@ -3,13 +3,14 @@ import { useMemo, useRef } from '../../../lib/teact/teact'; import { ApiChatMember, ApiMessage, ApiUser } from '../../../api/types'; import { ProfileTabType, SharedMediaType } from '../../../types'; -import { MESSAGE_SEARCH_SLICE, SHARED_MEDIA_SLICE } from '../../../config'; +import { MEMBERS_SLICE, MESSAGE_SEARCH_SLICE, SHARED_MEDIA_SLICE } from '../../../config'; import { getMessageContentIds, sortUserIds } from '../../../modules/helpers'; import useOnChange from '../../../hooks/useOnChange'; import useInfiniteScroll from '../../../hooks/useInfiniteScroll'; export default function useProfileViewportIds( isRightColumnShown: boolean, + loadMoreMembers: AnyToVoidFunction, searchMessages: AnyToVoidFunction, tabType: ProfileTabType, mediaSearchType?: SharedMediaType, @@ -30,6 +31,10 @@ export default function useProfileViewportIds( return sortUserIds(groupChatMembers.map(({ userId }) => userId), usersById); }, [groupChatMembers, usersById]); + const [memberViewportIds, getMoreMembers, noProfileInfoForMembers] = useInfiniteScrollForMembers( + resultType, loadMoreMembers, lastSyncTime, memberIds, + ); + const [mediaViewportIds, getMoreMedia, noProfileInfoForMedia] = useInfiniteScrollForSharedMedia( 'media', resultType, searchMessages, lastSyncTime, chatMessages, foundIds, ); @@ -52,8 +57,9 @@ export default function useProfileViewportIds( switch (resultType) { case 'members': - viewportIds = memberIds; - getMore = undefined; + viewportIds = memberViewportIds; + getMore = getMoreMembers; + noProfileInfo = noProfileInfoForMembers; break; case 'media': viewportIds = mediaViewportIds; @@ -80,6 +86,24 @@ export default function useProfileViewportIds( return [resultType, viewportIds, getMore, noProfileInfo] as const; } +function useInfiniteScrollForMembers( + currentResultType?: ProfileTabType, + handleLoadMore?: AnyToVoidFunction, + lastSyncTime?: number, + memberIds?: number[], +) { + const [viewportIds, getMore] = useInfiniteScroll( + lastSyncTime ? handleLoadMore : undefined, + memberIds, + undefined, + MEMBERS_SLICE, + ); + + const isOnTop = !viewportIds || !memberIds || viewportIds[0] === memberIds[0]; + + return [viewportIds, getMore, !isOnTop] as const; +} + function useInfiniteScrollForSharedMedia( forSharedMediaType: SharedMediaType, currentResultType?: ProfileTabType, diff --git a/src/components/ui/InfiniteScroll.tsx b/src/components/ui/InfiniteScroll.tsx index fe2cff7de..d2dffba9c 100644 --- a/src/components/ui/InfiniteScroll.tsx +++ b/src/components/ui/InfiniteScroll.tsx @@ -36,7 +36,7 @@ const InfiniteScroll: FC = ({ itemSelector = DEFAULT_LIST_SELECTOR, preloadBackwards = DEFAULT_PRELOAD_BACKWARDS, sensitiveArea = DEFAULT_SENSITIVE_AREA, - // Used to turn off restoring scroll position (e.g. for frequently re-ordered chat or user lists) + // Used to turn off preloading and restoring scroll position (e.g. for frequently re-ordered chat or user lists) isDisabled = false, noFastList, // Used to re-query `listItemElements` if rendering is delayed by transition @@ -70,7 +70,7 @@ const InfiniteScroll: FC = ({ // Initial preload useEffect(() => { - if (!loadMoreBackwards) { + if (isDisabled || !loadMoreBackwards) { return; } @@ -82,7 +82,7 @@ const InfiniteScroll: FC = ({ loadMoreBackwards(); } } - }, [items, loadMoreBackwards, preloadBackwards]); + }, [isDisabled, items, loadMoreBackwards, preloadBackwards]); // Restore `scrollTop` after adding items useLayoutEffect(() => { diff --git a/src/config.ts b/src/config.ts index 7c877233a..b206777b6 100644 --- a/src/config.ts +++ b/src/config.ts @@ -53,13 +53,14 @@ export const CHAT_LIST_LOAD_SLICE = 100; export const SHARED_MEDIA_SLICE = 42; export const MESSAGE_SEARCH_SLICE = 42; export const GLOBAL_SEARCH_SLICE = 20; -export const CHANNEL_MEMBERS_LIMIT = 30; +export const MEMBERS_SLICE = 30; +export const MEMBERS_LOAD_SLICE = 200; export const PINNED_MESSAGES_LIMIT = 50; export const BLOCKED_LIST_LIMIT = 100; export const PROFILE_PHOTOS_LIMIT = 40; -export const TOP_CHAT_MESSAGES_PRELOAD_LIMIT = 25; -export const ALL_CHATS_PRELOAD_DISABLED = false; +export const TOP_CHAT_MESSAGES_PRELOAD_LIMIT = 0; +export const ALL_CHATS_PRELOAD_DISABLED = true; export const ANIMATION_LEVEL_MIN = 0; export const ANIMATION_LEVEL_MED = 1; diff --git a/src/global/types.ts b/src/global/types.ts index 05c5b5391..20877b1f9 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -397,7 +397,7 @@ export type ActionTypes = ( 'joinChannel' | 'leaveChannel' | 'deleteChannel' | 'toggleChatPinned' | 'toggleChatArchived' | 'toggleChatUnread' | 'loadChatFolders' | 'loadRecommendedChatFolders' | 'editChatFolder' | 'addChatFolder' | 'deleteChatFolder' | 'updateChat' | 'toggleSignatures' | 'loadGroupsForDiscussion' | 'linkDiscussionGroup' | 'unlinkDiscussionGroup' | - 'loadProfilePhotos' | + 'loadProfilePhotos' | 'loadMoreMembers' | // messages 'loadViewportMessages' | 'selectMessage' | 'sendMessage' | 'cancelSendingMessage' | 'pinMessage' | 'deleteMessages' | 'markMessageListRead' | 'markMessagesRead' | 'loadMessage' | 'focusMessage' | 'focusLastMessage' | 'sendPollVote' | diff --git a/src/modules/actions/api/chats.ts b/src/modules/actions/api/chats.ts index 146e634f8..f0b90dbf5 100644 --- a/src/modules/actions/api/chats.ts +++ b/src/modules/actions/api/chats.ts @@ -52,7 +52,7 @@ const TMP_CHAT_ID = -1; const runThrottledForLoadChats = throttle((cb) => cb(), 1000, true); const runThrottledForLoadTopChats = throttle((cb) => cb(), 3000, true); -const runDebouncedForFetchFullChat = debounce((cb) => cb(), 500, false, true); +const runDebouncedForLoadFullChat = debounce((cb) => cb(), 500, false, true); addReducer('preloadTopChatMessages', (global, actions) => { (async () => { @@ -173,7 +173,7 @@ addReducer('loadFullChat', (global, actions, payload) => { if (force) { loadFullChat(chat); } else { - runDebouncedForFetchFullChat(() => loadFullChat(chat)); + runDebouncedForLoadFullChat(() => loadFullChat(chat)); } }); @@ -669,6 +669,40 @@ addReducer('unlinkDiscussionGroup', (global, actions, payload) => { })(); }); +addReducer('loadMoreMembers', (global) => { + (async () => { + const { chatId } = selectCurrentMessageList(global) || {}; + const chat = chatId ? selectChat(global, chatId) : undefined; + if (!chat) { + return; + } + + const offset = (chat.fullInfo && chat.fullInfo.members && chat.fullInfo.members.length) || undefined; + const result = await callApi('fetchMembers', chat.id, chat.accessHash!, 'recent', offset); + if (!result) { + return; + } + + const { members, users } = result; + if (!members || !members.length) { + return; + } + + global = getGlobal(); + global = addUsers(global, buildCollectionByKey(users, 'id')); + global = updateChat(global, chat.id, { + fullInfo: { + ...chat.fullInfo, + members: [ + ...((chat.fullInfo || {}).members || []), + ...(members || []), + ], + }, + }); + setGlobal(global); + })(); +}); + async function loadChats(listType: 'active' | 'archived', offsetId?: number, offsetDate?: number) { const result = await callApi('fetchChats', { limit: CHAT_LIST_LOAD_SLICE, diff --git a/src/modules/selectors/chats.ts b/src/modules/selectors/chats.ts index 5dd43ccfd..c0b1a77e0 100644 --- a/src/modules/selectors/chats.ts +++ b/src/modules/selectors/chats.ts @@ -5,7 +5,7 @@ import { getPrivateChatUserId, isChatChannel, isChatPrivate, isHistoryClearMessage, isUserBot, isUserOnline, } from '../helpers'; import { selectUser } from './users'; -import { ALL_FOLDER_ID, ARCHIVED_FOLDER_ID, CHANNEL_MEMBERS_LIMIT } from '../../config'; +import { ALL_FOLDER_ID, ARCHIVED_FOLDER_ID, MEMBERS_LOAD_SLICE } from '../../config'; export function selectChat(global: GlobalState, chatId: number): ApiChat | undefined { return global.chats.byId[chatId]; @@ -38,7 +38,7 @@ export function selectChatOnlineCount(global: GlobalState, chat: ApiChat) { return undefined; } - if (!chat.fullInfo.members || chat.fullInfo.members.length === CHANNEL_MEMBERS_LIMIT) { + if (!chat.fullInfo.members || chat.fullInfo.members.length === MEMBERS_LOAD_SLICE) { return chat.fullInfo.onlineCount; }