Profile: Infinite scroll for members

This commit is contained in:
Alexander Zinchuk 2021-05-06 01:47:52 +03:00
parent 1984318297
commit e454abd4f3
10 changed files with 110 additions and 35 deletions

View File

@ -12,7 +12,7 @@ import {
ApiChatAdminRights, ApiChatAdminRights,
} from '../../types'; } 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 { invokeRequest, uploadFile } from './client';
import { import {
buildApiChatFromDialog, buildApiChatFromDialog,
@ -156,11 +156,12 @@ export async function fetchChats({
export function fetchFullChat(chat: ApiChat) { export function fetchFullChat(chat: ApiChat) {
const { id, accessHash, adminRights } = chat; const { id, accessHash, adminRights } = chat;
const input = buildInputEntity(id, accessHash); const input = buildInputEntity(id, accessHash);
return input instanceof GramJs.InputChannel return input instanceof GramJs.InputChannel
? getFullChannelInfo(input, adminRights) ? getFullChannelInfo(id, accessHash!, adminRights)
: getFullChatInfo(input as number); : getFullChatInfo(id);
} }
export async function searchChats({ query }: { query: string }) { export async function searchChats({ query }: { query: string }) {
@ -298,7 +299,9 @@ async function getFullChatInfo(chatId: number): Promise<{
fullInfo: ApiChatFullInfo; fullInfo: ApiChatFullInfo;
users?: ApiUser[]; users?: ApiUser[];
} | undefined> { } | 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)) { if (!result || !(result.fullChat instanceof GramJs.ChatFull)) {
return undefined; return undefined;
@ -328,10 +331,13 @@ async function getFullChatInfo(chatId: number): Promise<{
} }
async function getFullChannelInfo( async function getFullChannelInfo(
channel: GramJs.InputChannel, id: number,
accessHash: string,
adminRights?: ApiChatAdminRights, 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)) { if (!result || !(result.fullChat instanceof GramJs.ChannelFull)) {
return undefined; return undefined;
@ -354,12 +360,12 @@ async function getFullChannelInfo(
? exportedInvite.link ? exportedInvite.link
: undefined; : undefined;
const { members, users } = (canViewParticipants && await getChannelMembers(channel)) || {}; const { members, users } = (canViewParticipants && await fetchMembers(id, accessHash)) || {};
const { members: kickedMembers, users: bannedUsers } = ( const { members: kickedMembers, users: bannedUsers } = (
canViewParticipants && adminRights && await getChannelMembers(channel, 'kicked') canViewParticipants && adminRights && await fetchMembers(id, accessHash, 'kicked')
) || {}; ) || {};
const { members: adminMembers, users: adminUsers } = ( const { members: adminMembers, users: adminUsers } = (
canViewParticipants && adminRights && await getChannelMembers(channel, 'admin') canViewParticipants && adminRights && await fetchMembers(id, accessHash, 'admin')
) || {}; ) || {};
return { return {
@ -785,9 +791,11 @@ export function toggleSignatures({
type ChannelMembersFilter = 'kicked' | 'admin' | 'recent'; type ChannelMembersFilter = 'kicked' | 'admin' | 'recent';
async function getChannelMembers( export async function fetchMembers(
channel: GramJs.InputChannel, chatId: number,
accessHash: string,
memberFilter: ChannelMembersFilter = 'recent', memberFilter: ChannelMembersFilter = 'recent',
offset?: number,
) { ) {
let filter: GramJs.TypeChannelParticipantsFilter; let filter: GramJs.TypeChannelParticipantsFilter;
@ -804,9 +812,10 @@ async function getChannelMembers(
} }
const result = await invokeRequest(new GramJs.channels.GetParticipants({ const result = await invokeRequest(new GramJs.channels.GetParticipants({
channel, channel: buildInputEntity(chatId, accessHash) as GramJs.InputChannel,
filter, filter,
limit: CHANNEL_MEMBERS_LIMIT, offset,
limit: MEMBERS_LOAD_SLICE,
})); }));
if (!result || result instanceof GramJs.channels.ChannelParticipantsNotModified) { if (!result || result instanceof GramJs.channels.ChannelParticipantsNotModified) {

View File

@ -14,7 +14,7 @@ export {
fetchChatFolders, editChatFolder, deleteChatFolder, fetchRecommendedChatFolders, fetchChatFolders, editChatFolder, deleteChatFolder, fetchRecommendedChatFolders,
getChatByUsername, togglePreHistoryHidden, updateChatDefaultBannedRights, updateChatMemberBannedRights, getChatByUsername, togglePreHistoryHidden, updateChatDefaultBannedRights, updateChatMemberBannedRights,
updateChatTitle, updateChatAbout, toggleSignatures, updateChatAdmin, fetchGroupsForDiscussion, setDiscussionGroup, updateChatTitle, updateChatAbout, toggleSignatures, updateChatAdmin, fetchGroupsForDiscussion, setDiscussionGroup,
migrateChat, openChatByInvite, migrateChat, openChatByInvite, fetchMembers,
} from './chats'; } from './chats';
export { export {

View File

@ -14,10 +14,10 @@ import {
MediaViewerOrigin, ProfileState, ProfileTabType, SharedMediaType, MediaViewerOrigin, ProfileState, ProfileTabType, SharedMediaType,
} from '../../types'; } 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 { IS_TOUCH_ENV } from '../../util/environment';
import { import {
isChatAdmin, isChatChannel, isChatGroup, isChatPrivate, isChatAdmin, isChatBasicGroup, isChatChannel, isChatGroup, isChatPrivate,
} from '../../modules/helpers'; } from '../../modules/helpers';
import { import {
selectChatMessages, selectChatMessages,
@ -57,6 +57,7 @@ type OwnProps = {
}; };
type StateProps = { type StateProps = {
isBasicGroup?: boolean;
isChannel?: boolean; isChannel?: boolean;
resolvedUserId?: number; resolvedUserId?: number;
chatMessages?: Record<number, ApiMessage>; chatMessages?: Record<number, ApiMessage>;
@ -72,7 +73,7 @@ type StateProps = {
}; };
type DispatchProps = Pick<GlobalActions, ( type DispatchProps = Pick<GlobalActions, (
'setLocalMediaSearchType' | 'searchMediaMessagesLocal' | 'openMediaViewer' | 'setLocalMediaSearchType' | 'loadMoreMembers' | 'searchMediaMessagesLocal' | 'openMediaViewer' |
'openAudioPlayer' | 'openUserInfo' | 'focusMessage' | 'loadProfilePhotos' 'openAudioPlayer' | 'openUserInfo' | 'focusMessage' | 'loadProfilePhotos'
)>; )>;
@ -89,6 +90,7 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
chatId, chatId,
profileState, profileState,
onProfileStateChange, onProfileStateChange,
isBasicGroup,
isChannel, isChannel,
resolvedUserId, resolvedUserId,
chatMessages, chatMessages,
@ -102,6 +104,7 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
isRestricted, isRestricted,
lastSyncTime, lastSyncTime,
setLocalMediaSearchType, setLocalMediaSearchType,
loadMoreMembers,
searchMediaMessagesLocal, searchMediaMessagesLocal,
openMediaViewer, openMediaViewer,
openAudioPlayer, openAudioPlayer,
@ -125,7 +128,7 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
const tabType = tabs[activeTab].type as ProfileTabType; const tabType = tabs[activeTab].type as ProfileTabType;
const [resultType, viewportIds, getMore, noProfileInfo] = useProfileViewportIds( const [resultType, viewportIds, getMore, noProfileInfo] = useProfileViewportIds(
isRightColumnShown, searchMediaMessagesLocal, tabType, mediaSearchType, members, isRightColumnShown, loadMoreMembers, searchMediaMessagesLocal, tabType, mediaSearchType, members,
usersById, chatMessages, foundIds, chatId, lastSyncTime, usersById, chatMessages, foundIds, chatId, lastSyncTime,
); );
const activeKey = tabs.findIndex(({ type }) => type === resultType); const activeKey = tabs.findIndex(({ type }) => type === resultType);
@ -306,8 +309,9 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
itemSelector={buildInfiniteScrollItemSelector(resultType)} itemSelector={buildInfiniteScrollItemSelector(resultType)}
items={viewportIds} items={viewportIds}
cacheBuster={cacheBuster} cacheBuster={cacheBuster}
preloadBackwards={SHARED_MEDIA_SLICE} sensitiveArea={500}
isDisabled={tabType === 'members'} preloadBackwards={resultType === 'members' ? MEMBERS_SLICE : SHARED_MEDIA_SLICE}
isDisabled={resultType === 'members' && isBasicGroup}
noFastList noFastList
onLoadMore={getMore} onLoadMore={getMore}
onScroll={handleScroll} onScroll={handleScroll}
@ -366,6 +370,7 @@ export default memo(withGlobal<OwnProps>(
const { byId: usersById } = global.users; const { byId: usersById } = global.users;
const isGroup = chat && isChatGroup(chat); const isGroup = chat && isChatGroup(chat);
const isBasicGroup = chat && isChatBasicGroup(chat);
const isChannel = chat && isChatChannel(chat); const isChannel = chat && isChatChannel(chat);
const hasMembersTab = isGroup || (isChannel && isChatAdmin(chat!)); const hasMembersTab = isGroup || (isChannel && isChatAdmin(chat!));
const members = chat && chat.fullInfo && chat.fullInfo.members; const members = chat && chat.fullInfo && chat.fullInfo.members;
@ -379,6 +384,7 @@ export default memo(withGlobal<OwnProps>(
} }
return { return {
isBasicGroup,
isChannel, isChannel,
resolvedUserId, resolvedUserId,
chatMessages, chatMessages,
@ -397,6 +403,7 @@ export default memo(withGlobal<OwnProps>(
}, },
(setGlobal, actions): DispatchProps => pick(actions, [ (setGlobal, actions): DispatchProps => pick(actions, [
'setLocalMediaSearchType', 'setLocalMediaSearchType',
'loadMoreMembers',
'searchMediaMessagesLocal', 'searchMediaMessagesLocal',
'openMediaViewer', 'openMediaViewer',
'openAudioPlayer', 'openAudioPlayer',

View File

@ -17,7 +17,7 @@
// @optimization // @optimization
&:not(:hover) { &:not(:hover) {
.Picker .chat-item-clickable:nth-child(n + 18) { .chat-item-clickable:nth-child(n + 18) {
display: none !important; display: none !important;
} }
} }

View File

@ -3,13 +3,14 @@ import { useMemo, useRef } from '../../../lib/teact/teact';
import { ApiChatMember, ApiMessage, ApiUser } from '../../../api/types'; import { ApiChatMember, ApiMessage, ApiUser } from '../../../api/types';
import { ProfileTabType, SharedMediaType } from '../../../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 { getMessageContentIds, sortUserIds } from '../../../modules/helpers';
import useOnChange from '../../../hooks/useOnChange'; import useOnChange from '../../../hooks/useOnChange';
import useInfiniteScroll from '../../../hooks/useInfiniteScroll'; import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
export default function useProfileViewportIds( export default function useProfileViewportIds(
isRightColumnShown: boolean, isRightColumnShown: boolean,
loadMoreMembers: AnyToVoidFunction,
searchMessages: AnyToVoidFunction, searchMessages: AnyToVoidFunction,
tabType: ProfileTabType, tabType: ProfileTabType,
mediaSearchType?: SharedMediaType, mediaSearchType?: SharedMediaType,
@ -30,6 +31,10 @@ export default function useProfileViewportIds(
return sortUserIds(groupChatMembers.map(({ userId }) => userId), usersById); return sortUserIds(groupChatMembers.map(({ userId }) => userId), usersById);
}, [groupChatMembers, usersById]); }, [groupChatMembers, usersById]);
const [memberViewportIds, getMoreMembers, noProfileInfoForMembers] = useInfiniteScrollForMembers(
resultType, loadMoreMembers, lastSyncTime, memberIds,
);
const [mediaViewportIds, getMoreMedia, noProfileInfoForMedia] = useInfiniteScrollForSharedMedia( const [mediaViewportIds, getMoreMedia, noProfileInfoForMedia] = useInfiniteScrollForSharedMedia(
'media', resultType, searchMessages, lastSyncTime, chatMessages, foundIds, 'media', resultType, searchMessages, lastSyncTime, chatMessages, foundIds,
); );
@ -52,8 +57,9 @@ export default function useProfileViewportIds(
switch (resultType) { switch (resultType) {
case 'members': case 'members':
viewportIds = memberIds; viewportIds = memberViewportIds;
getMore = undefined; getMore = getMoreMembers;
noProfileInfo = noProfileInfoForMembers;
break; break;
case 'media': case 'media':
viewportIds = mediaViewportIds; viewportIds = mediaViewportIds;
@ -80,6 +86,24 @@ export default function useProfileViewportIds(
return [resultType, viewportIds, getMore, noProfileInfo] as const; 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( function useInfiniteScrollForSharedMedia(
forSharedMediaType: SharedMediaType, forSharedMediaType: SharedMediaType,
currentResultType?: ProfileTabType, currentResultType?: ProfileTabType,

View File

@ -36,7 +36,7 @@ const InfiniteScroll: FC<OwnProps> = ({
itemSelector = DEFAULT_LIST_SELECTOR, itemSelector = DEFAULT_LIST_SELECTOR,
preloadBackwards = DEFAULT_PRELOAD_BACKWARDS, preloadBackwards = DEFAULT_PRELOAD_BACKWARDS,
sensitiveArea = DEFAULT_SENSITIVE_AREA, 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, isDisabled = false,
noFastList, noFastList,
// Used to re-query `listItemElements` if rendering is delayed by transition // Used to re-query `listItemElements` if rendering is delayed by transition
@ -70,7 +70,7 @@ const InfiniteScroll: FC<OwnProps> = ({
// Initial preload // Initial preload
useEffect(() => { useEffect(() => {
if (!loadMoreBackwards) { if (isDisabled || !loadMoreBackwards) {
return; return;
} }
@ -82,7 +82,7 @@ const InfiniteScroll: FC<OwnProps> = ({
loadMoreBackwards(); loadMoreBackwards();
} }
} }
}, [items, loadMoreBackwards, preloadBackwards]); }, [isDisabled, items, loadMoreBackwards, preloadBackwards]);
// Restore `scrollTop` after adding items // Restore `scrollTop` after adding items
useLayoutEffect(() => { useLayoutEffect(() => {

View File

@ -53,13 +53,14 @@ export const CHAT_LIST_LOAD_SLICE = 100;
export const SHARED_MEDIA_SLICE = 42; export const SHARED_MEDIA_SLICE = 42;
export const MESSAGE_SEARCH_SLICE = 42; export const MESSAGE_SEARCH_SLICE = 42;
export const GLOBAL_SEARCH_SLICE = 20; 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 PINNED_MESSAGES_LIMIT = 50;
export const BLOCKED_LIST_LIMIT = 100; export const BLOCKED_LIST_LIMIT = 100;
export const PROFILE_PHOTOS_LIMIT = 40; export const PROFILE_PHOTOS_LIMIT = 40;
export const TOP_CHAT_MESSAGES_PRELOAD_LIMIT = 25; export const TOP_CHAT_MESSAGES_PRELOAD_LIMIT = 0;
export const ALL_CHATS_PRELOAD_DISABLED = false; export const ALL_CHATS_PRELOAD_DISABLED = true;
export const ANIMATION_LEVEL_MIN = 0; export const ANIMATION_LEVEL_MIN = 0;
export const ANIMATION_LEVEL_MED = 1; export const ANIMATION_LEVEL_MED = 1;

View File

@ -397,7 +397,7 @@ export type ActionTypes = (
'joinChannel' | 'leaveChannel' | 'deleteChannel' | 'toggleChatPinned' | 'toggleChatArchived' | 'toggleChatUnread' | 'joinChannel' | 'leaveChannel' | 'deleteChannel' | 'toggleChatPinned' | 'toggleChatArchived' | 'toggleChatUnread' |
'loadChatFolders' | 'loadRecommendedChatFolders' | 'editChatFolder' | 'addChatFolder' | 'deleteChatFolder' | 'loadChatFolders' | 'loadRecommendedChatFolders' | 'editChatFolder' | 'addChatFolder' | 'deleteChatFolder' |
'updateChat' | 'toggleSignatures' | 'loadGroupsForDiscussion' | 'linkDiscussionGroup' | 'unlinkDiscussionGroup' | 'updateChat' | 'toggleSignatures' | 'loadGroupsForDiscussion' | 'linkDiscussionGroup' | 'unlinkDiscussionGroup' |
'loadProfilePhotos' | 'loadProfilePhotos' | 'loadMoreMembers' |
// messages // messages
'loadViewportMessages' | 'selectMessage' | 'sendMessage' | 'cancelSendingMessage' | 'pinMessage' | 'deleteMessages' | 'loadViewportMessages' | 'selectMessage' | 'sendMessage' | 'cancelSendingMessage' | 'pinMessage' | 'deleteMessages' |
'markMessageListRead' | 'markMessagesRead' | 'loadMessage' | 'focusMessage' | 'focusLastMessage' | 'sendPollVote' | 'markMessageListRead' | 'markMessagesRead' | 'loadMessage' | 'focusMessage' | 'focusLastMessage' | 'sendPollVote' |

View File

@ -52,7 +52,7 @@ const TMP_CHAT_ID = -1;
const runThrottledForLoadChats = throttle((cb) => cb(), 1000, true); const runThrottledForLoadChats = throttle((cb) => cb(), 1000, true);
const runThrottledForLoadTopChats = throttle((cb) => cb(), 3000, 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) => { addReducer('preloadTopChatMessages', (global, actions) => {
(async () => { (async () => {
@ -173,7 +173,7 @@ addReducer('loadFullChat', (global, actions, payload) => {
if (force) { if (force) {
loadFullChat(chat); loadFullChat(chat);
} else { } 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) { async function loadChats(listType: 'active' | 'archived', offsetId?: number, offsetDate?: number) {
const result = await callApi('fetchChats', { const result = await callApi('fetchChats', {
limit: CHAT_LIST_LOAD_SLICE, limit: CHAT_LIST_LOAD_SLICE,

View File

@ -5,7 +5,7 @@ import {
getPrivateChatUserId, isChatChannel, isChatPrivate, isHistoryClearMessage, isUserBot, isUserOnline, getPrivateChatUserId, isChatChannel, isChatPrivate, isHistoryClearMessage, isUserBot, isUserOnline,
} from '../helpers'; } from '../helpers';
import { selectUser } from './users'; 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 { export function selectChat(global: GlobalState, chatId: number): ApiChat | undefined {
return global.chats.byId[chatId]; return global.chats.byId[chatId];
@ -38,7 +38,7 @@ export function selectChatOnlineCount(global: GlobalState, chat: ApiChat) {
return undefined; 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; return chat.fullInfo.onlineCount;
} }