Profile: Common Chats tab (#1547)

This commit is contained in:
Alexander Zinchuk 2021-11-17 17:49:52 +03:00
parent aefd2f1cce
commit becfab6fe7
15 changed files with 167 additions and 31 deletions

View File

@ -416,7 +416,7 @@ export function buildInputReportReason(reason: ApiReportReason) {
return undefined;
}
function buildMtpPeerId(id: string, type: 'user' | 'chat' | 'channel') {
export function buildMtpPeerId(id: string, type: 'user' | 'chat' | 'channel') {
// Workaround for old-fashioned IDs stored locally
if (typeof id === 'number') {
return BigInt(Math.abs(id));

View File

@ -1,6 +1,6 @@
import { Api as GramJs } from '../../lib/gramjs';
import localDb from './localDb';
import { getApiChatIdFromMtpPeer } from './apiBuilders/peers';
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './apiBuilders/peers';
export function resolveMessageApiChatId(mtpMessage: GramJs.TypeMessage) {
if (!(mtpMessage instanceof GramJs.Message || mtpMessage instanceof GramJs.MessageService)) {
@ -41,3 +41,9 @@ export function addPhotoToLocalDb(photo: GramJs.TypePhoto) {
localDb.photos[String(photo.id)] = photo;
}
}
export function addChatToLocalDb(chat: GramJs.TypeChat) {
if (chat instanceof GramJs.Chat || chat instanceof GramJs.Channel) {
localDb.chats[buildApiPeerId(chat.id, chat instanceof GramJs.Chat ? 'chat' : 'channel')] = chat;
}
}

View File

@ -39,7 +39,7 @@ import {
buildChatBannedRights,
buildChatAdminRights,
} from '../gramjsBuilders';
import { addMessageToLocalDb } from '../helpers';
import { addChatToLocalDb, addMessageToLocalDb } from '../helpers';
import { buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
const MAX_INT_32 = 2 ** 31 - 1;
@ -1075,11 +1075,7 @@ function updateLocalDb(result: (
}
if ('chats' in result) {
result.chats.forEach((chat) => {
if (chat instanceof GramJs.Chat || chat instanceof GramJs.Channel) {
localDb.chats[buildApiPeerId(chat.id, chat instanceof GramJs.Chat ? 'chat' : 'channel')] = chat;
}
});
result.chats.forEach(addChatToLocalDb);
}
if ('messages' in result) {

View File

@ -27,7 +27,7 @@ export {
export {
fetchFullUser, fetchNearestCountry, fetchTopUsers, fetchContactList, fetchUsers,
addContact, updateContact, deleteUser, fetchProfilePhotos,
addContact, updateContact, deleteUser, fetchProfilePhotos, fetchCommonChats,
} from './users';
export {

View File

@ -4,19 +4,21 @@ import {
OnApiUpdate, ApiUser, ApiChat, ApiPhoto,
} from '../../types';
import { PROFILE_PHOTOS_LIMIT } from '../../../config';
import { COMMON_CHATS_LIMIT, PROFILE_PHOTOS_LIMIT } from '../../../config';
import { invokeRequest } from './client';
import { searchMessagesLocal } from './messages';
import {
buildInputEntity,
buildInputPeer,
buildInputContact,
buildMtpPeerId,
getEntityTypeById,
} from '../gramjsBuilders';
import { buildApiUser, buildApiUserFromFull } from '../apiBuilders/users';
import { buildApiChatFromPreview } from '../apiBuilders/chats';
import { buildApiPhoto } from '../apiBuilders/common';
import localDb from '../localDb';
import { addPhotoToLocalDb } from '../helpers';
import { addChatToLocalDb, addPhotoToLocalDb } from '../helpers';
import { buildApiPeerId } from '../apiBuilders/peers';
let onUpdate: OnApiUpdate;
@ -54,6 +56,34 @@ export async function fetchFullUser({
});
}
export async function fetchCommonChats(id: string, accessHash?: string, maxId?: string) {
const commonChats = await invokeRequest(new GramJs.messages.GetCommonChats({
userId: buildInputEntity(id, accessHash) as GramJs.InputUser,
maxId: maxId ? buildMtpPeerId(maxId, getEntityTypeById(maxId)) : undefined,
limit: COMMON_CHATS_LIMIT,
}));
if (!commonChats) {
return undefined;
}
updateLocalDb(commonChats);
const chatIds: string[] = [];
const chats: ApiChat[] = [];
commonChats.chats.forEach((mtpChat) => {
const chat = buildApiChatFromPreview(mtpChat);
if (chat) {
chats.push(chat);
chatIds.push(chat.id);
}
});
return { chats, chatIds, isFullyLoaded: chatIds.length < COMMON_CHATS_LIMIT };
}
export async function fetchNearestCountry() {
const dcInfo = await invokeRequest(new GramJs.help.GetNearestDc());
@ -217,6 +247,12 @@ export async function fetchProfilePhotos(user?: ApiUser, chat?: ApiChat) {
};
}
function updateLocalDb(result: (GramJs.photos.Photos | GramJs.photos.PhotosSlice)) {
result.photos.forEach(addPhotoToLocalDb);
function updateLocalDb(result: (GramJs.photos.Photos | GramJs.photos.PhotosSlice | GramJs.messages.Chats)) {
if ('chats' in result) {
result.chats.forEach(addChatToLocalDb);
}
if ('photos' in result) {
result.photos.forEach(addPhotoToLocalDb);
}
}

View File

@ -18,6 +18,11 @@ export interface ApiUser {
photos?: ApiPhoto[];
botPlaceholder?: string;
canBeInvitedToGroup?: boolean;
commonChats?: {
ids: string[];
maxId: string;
isFullyLoaded: boolean;
};
// Obtained from GetFullUser / UserFullInfo
fullInfo?: ApiUserFullInfo;

View File

@ -129,6 +129,7 @@
}
}
&.commonChats-list,
&.members-list {
padding: 0.5rem 1rem;

View File

@ -7,7 +7,8 @@ import {
ApiMessage,
ApiChatMember,
ApiUser,
MAIN_THREAD_ID, ApiChat,
ApiChat,
MAIN_THREAD_ID,
} from '../../api/types';
import { GlobalActions } from '../../global/types';
import {
@ -31,6 +32,7 @@ import {
selectIsRightColumnShown,
selectTheme,
selectActiveDownloadIds,
selectUser,
} from '../../modules/selectors';
import { pick } from '../../util/iteratees';
import { captureEvents, SwipeDirection } from '../../util/captureEvents';
@ -57,6 +59,7 @@ 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';
@ -75,11 +78,13 @@ type StateProps = {
chatMessages?: Record<number, ApiMessage>;
foundIds?: number[];
mediaSearchType?: SharedMediaType;
hasCommonChatsTab?: boolean;
hasMembersTab?: boolean;
areMembersHidden?: boolean;
canAddMembers?: boolean;
canDeleteMembers?: boolean;
members?: ApiChatMember[];
commonChatIds?: string[];
chatsById: Record<string, ApiChat>;
usersById: Record<string, ApiUser>;
isRightColumnShown: boolean;
@ -90,8 +95,9 @@ type StateProps = {
};
type DispatchProps = Pick<GlobalActions, (
'setLocalMediaSearchType' | 'loadMoreMembers' | 'searchMediaMessagesLocal' | 'openMediaViewer' |
'openAudioPlayer' | 'openUserInfo' | 'focusMessage' | 'loadProfilePhotos' | 'setNewChatMembersDialogState'
'setLocalMediaSearchType' | 'loadMoreMembers' | 'searchMediaMessagesLocal' | 'openMediaViewer' | 'loadCommonChats' |
'openAudioPlayer' | 'openUserInfo' | 'focusMessage' | 'loadProfilePhotos' | 'setNewChatMembersDialogState' |
'openChat'
)>;
const TABS = [
@ -115,10 +121,12 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
chatMessages,
foundIds,
mediaSearchType,
hasCommonChatsTab,
hasMembersTab,
areMembersHidden,
canAddMembers,
canDeleteMembers,
commonChatIds,
members,
usersById,
chatsById,
@ -129,6 +137,8 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
serverTimeOffset,
setLocalMediaSearchType,
loadMoreMembers,
loadCommonChats,
openChat,
searchMediaMessagesLocal,
openMediaViewer,
openAudioPlayer,
@ -150,12 +160,15 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
type: 'members', title: isChannel ? 'ChannelSubscribers' : 'GroupMembers',
}] : []),
...TABS,
]), [hasMembersTab, isChannel]);
...(hasCommonChatsTab ? [{
type: 'commonChats', title: 'SharedGroupsTab2',
}] : []),
]), [hasCommonChatsTab, hasMembersTab, isChannel]);
const tabType = tabs[activeTab].type as ProfileTabType;
const [resultType, viewportIds, getMore, noProfileInfo] = useProfileViewportIds(
isRightColumnShown, loadMoreMembers, searchMediaMessagesLocal, tabType, mediaSearchType, members,
usersById, chatMessages, foundIds, chatId, lastSyncTime, serverTimeOffset,
isRightColumnShown, loadMoreMembers, loadCommonChats, searchMediaMessagesLocal, tabType, mediaSearchType, members,
commonChatIds, usersById, chatsById, chatMessages, foundIds, chatId, lastSyncTime, serverTimeOffset,
);
const activeKey = tabs.findIndex(({ type }) => type === resultType);
@ -273,6 +286,9 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
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;
@ -373,6 +389,17 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
<PrivateChatInfo userId={id} forceShowSelf />
</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>
))
) : undefined}
</div>
);
@ -474,11 +501,16 @@ export default memo(withGlobal<OwnProps>(
const activeDownloadIds = selectActiveDownloadIds(global, chatId);
let resolvedUserId;
let user;
if (userId) {
resolvedUserId = userId;
} else if (isUserId(chatId)) {
resolvedUserId = chatId;
}
const hasCommonChatsTab = Boolean(resolvedUserId);
if (resolvedUserId) {
user = selectUser(global, resolvedUserId);
}
return {
theme: selectTheme(global),
@ -487,6 +519,7 @@ export default memo(withGlobal<OwnProps>(
chatMessages,
foundIds,
mediaSearchType,
hasCommonChatsTab,
hasMembersTab,
areMembersHidden,
canAddMembers,
@ -500,6 +533,7 @@ export default memo(withGlobal<OwnProps>(
usersById,
chatsById,
...(hasMembersTab && members && { members }),
...(hasCommonChatsTab && user && { commonChatIds: user.commonChats?.ids }),
};
},
(setGlobal, actions): DispatchProps => pick(actions, [
@ -512,5 +546,7 @@ export default memo(withGlobal<OwnProps>(
'focusMessage',
'loadProfilePhotos',
'setNewChatMembersDialogState',
'loadCommonChats',
'openChat',
]),
)(Profile));

View File

@ -1,21 +1,26 @@
import { useMemo, useRef } from '../../../lib/teact/teact';
import { ApiChatMember, ApiMessage, ApiUser } from '../../../api/types';
import {
ApiChat, ApiChatMember, ApiMessage, ApiUser,
} from '../../../api/types';
import { ProfileTabType, SharedMediaType } from '../../../types';
import { MEMBERS_SLICE, MESSAGE_SEARCH_SLICE, SHARED_MEDIA_SLICE } from '../../../config';
import { getMessageContentIds, sortUserIds } from '../../../modules/helpers';
import { getMessageContentIds, sortChatIds, sortUserIds } from '../../../modules/helpers';
import useOnChange from '../../../hooks/useOnChange';
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
export default function useProfileViewportIds(
isRightColumnShown: boolean,
loadMoreMembers: AnyToVoidFunction,
loadCommonChats: AnyToVoidFunction,
searchMessages: AnyToVoidFunction,
tabType: ProfileTabType,
mediaSearchType?: SharedMediaType,
groupChatMembers?: ApiChatMember[],
commonChatIds?: string[],
usersById?: Record<string, ApiUser>,
chatsById?: Record<string, ApiChat>,
chatMessages?: Record<number, ApiMessage>,
foundIds?: number[],
chatId?: string,
@ -32,10 +37,22 @@ export default function useProfileViewportIds(
return sortUserIds(groupChatMembers.map(({ userId }) => userId), usersById, undefined, serverTimeOffset);
}, [groupChatMembers, serverTimeOffset, usersById]);
const [memberViewportIds, getMoreMembers, noProfileInfoForMembers] = useInfiniteScrollForMembers(
const chatIds = useMemo(() => {
if (!commonChatIds || !chatsById) {
return undefined;
}
return sortChatIds(commonChatIds, chatsById, true);
}, [chatsById, commonChatIds]);
const [memberViewportIds, getMoreMembers, noProfileInfoForMembers] = useInfiniteScrollForLoadableItems(
resultType, loadMoreMembers, lastSyncTime, memberIds,
);
const [commonChatViewportIds, getMoreCommonChats, noProfileInfoForCommonChats] = useInfiniteScrollForLoadableItems(
resultType, loadCommonChats, lastSyncTime, chatIds,
);
const [mediaViewportIds, getMoreMedia, noProfileInfoForMedia] = useInfiniteScrollForSharedMedia(
'media', resultType, searchMessages, lastSyncTime, chatMessages, foundIds,
);
@ -66,6 +83,11 @@ export default function useProfileViewportIds(
getMore = getMoreMembers;
noProfileInfo = noProfileInfoForMembers;
break;
case 'commonChats':
viewportIds = commonChatViewportIds;
getMore = getMoreCommonChats;
noProfileInfo = noProfileInfoForCommonChats;
break;
case 'media':
viewportIds = mediaViewportIds;
getMore = getMoreMedia;
@ -96,20 +118,20 @@ export default function useProfileViewportIds(
return [resultType, viewportIds, getMore, noProfileInfo] as const;
}
function useInfiniteScrollForMembers(
function useInfiniteScrollForLoadableItems(
currentResultType?: ProfileTabType,
handleLoadMore?: AnyToVoidFunction,
lastSyncTime?: number,
memberIds?: string[],
itemIds?: string[],
) {
const [viewportIds, getMore] = useInfiniteScroll(
lastSyncTime ? handleLoadMore : undefined,
memberIds,
itemIds,
undefined,
MEMBERS_SLICE,
);
const isOnTop = !viewportIds || !memberIds || viewportIds[0] === memberIds[0];
const isOnTop = !viewportIds || !itemIds || viewportIds[0] === itemIds[0];
return [viewportIds, getMore, !isOnTop] as const;
}

View File

@ -57,6 +57,7 @@ export const PINNED_MESSAGES_LIMIT = 50;
export const BLOCKED_LIST_LIMIT = 100;
export const PROFILE_PHOTOS_LIMIT = 40;
export const PROFILE_SENSITIVE_AREA = 500;
export const COMMON_CHATS_LIMIT = 100;
export const TOP_CHAT_MESSAGES_PRELOAD_LIMIT = 20;
export const ALL_CHATS_PRELOAD_DISABLED = false;

View File

@ -505,7 +505,7 @@ export type ActionTypes = (
// users
'loadFullUser' | 'openUserInfo' | 'loadNearestCountry' | 'loadTopUsers' | 'loadContactList' |
'loadCurrentUser' | 'updateProfile' | 'checkUsername' | 'addContact' | 'updateContact' |
'deleteUser' | 'loadUser' | 'setUserSearchQuery' |
'deleteUser' | 'loadUser' | 'setUserSearchQuery' | 'loadCommonChats' |
// chat creation
'createChannel' | 'createGroupChat' | 'resetChatCreation' |
// settings

View File

@ -1026,6 +1026,7 @@ messages.saveDraft#bc39e14b flags:# no_webpage:flags.1?true reply_to_msg_id:flag
messages.getAllDrafts#6a3f8d65 = Updates;
messages.readFeaturedStickers#5b118126 id:Vector<long> = Bool;
messages.getRecentStickers#9da9403b flags:# attached:flags.0?true hash:long = messages.RecentStickers;
messages.getCommonChats#e40ca104 user_id:InputUser max_id:long limit:int = messages.Chats;
messages.getWebPage#32ca8f91 url:string hash:int = WebPage;
messages.toggleDialogPin#a731e257 flags:# pinned:flags.0?true peer:InputDialogPeer = Bool;
messages.getPinnedDialogs#d6b94df2 folder_id:int = messages.PeerDialogs;

View File

@ -1027,6 +1027,7 @@ messages.saveDraft#bc39e14b flags:# no_webpage:flags.1?true reply_to_msg_id:flag
messages.getAllDrafts#6a3f8d65 = Updates;
messages.readFeaturedStickers#5b118126 id:Vector<long> = Bool;
messages.getRecentStickers#9da9403b flags:# attached:flags.0?true hash:long = messages.RecentStickers;
messages.getCommonChats#e40ca104 user_id:InputUser max_id:long limit:int = messages.Chats;
messages.getWebPage#32ca8f91 url:string hash:int = WebPage;
messages.toggleDialogPin#a731e257 flags:# pinned:flags.0?true peer:InputDialogPeer = Bool;
messages.getPinnedDialogs#d6b94df2 folder_id:int = messages.PeerDialogs;

View File

@ -6,10 +6,10 @@ import { ApiUser } from '../../../api/types';
import { ManagementProgress } from '../../../types';
import { debounce, throttle } from '../../../util/schedulers';
import { buildCollectionByKey, pick } from '../../../util/iteratees';
import { isUserId } from '../../helpers';
import { buildCollectionByKey, pick, unique } from '../../../util/iteratees';
import { isUserBot, isUserId } from '../../helpers';
import { callApi } from '../../../api/gramjs';
import { selectChat, selectUser } from '../../selectors';
import { selectChat, selectCurrentMessageList, selectUser } from '../../selectors';
import {
addChats, addUsers, updateChat, updateManagementProgress, updateUser, updateUsers,
updateUserSearch, updateUserSearchFetchingStatus,
@ -67,6 +67,37 @@ addReducer('loadCurrentUser', () => {
void callApi('fetchCurrentUser');
});
addReducer('loadCommonChats', (global) => {
const { chatId } = selectCurrentMessageList(global) || {};
const user = chatId ? selectUser(global, chatId) : undefined;
if (!user || isUserBot(user) || user.commonChats?.isFullyLoaded) {
return;
}
(async () => {
const maxId = user.commonChats?.maxId;
const result = await callApi('fetchCommonChats', user.id, user.accessHash!, maxId);
if (!result) {
return;
}
const { chats, chatIds, isFullyLoaded } = result;
global = getGlobal();
if (chats.length) {
global = addChats(global, buildCollectionByKey(chats, 'id'));
}
global = updateUser(global, user.id, {
commonChats: {
maxId: chatIds.length ? chatIds[chatIds.length - 1] : '0',
ids: unique((user.commonChats?.ids || []).concat(chatIds)),
isFullyLoaded,
},
});
setGlobal(global);
})();
});
addReducer('updateContact', (global, actions, payload) => {
const {
userId, isMuted, firstName, lastName,

View File

@ -284,7 +284,7 @@ export enum NewChatMembersProgress {
Loading,
}
export type ProfileTabType = 'members' | 'media' | 'documents' | 'links' | 'audio' | 'voice';
export type ProfileTabType = 'members' | 'commonChats' | 'media' | 'documents' | 'links' | 'audio' | 'voice';
export type SharedMediaType = 'media' | 'documents' | 'links' | 'audio' | 'voice';
export type ApiPrivacyKey = 'phoneNumber' | 'lastSeen' | 'profilePhoto' | 'forwards' | 'chatInvite';
export type PrivacyVisibility = 'everybody' | 'contacts' | 'nonContacts' | 'nobody';