Profile: add similar channels tab (#4104)
This commit is contained in:
parent
20d8087d87
commit
d2b834d1a6
@ -15,9 +15,21 @@ import localDb from '../localDb';
|
||||
import { buildJson } from './misc';
|
||||
|
||||
type LimitType = 'default' | 'premium';
|
||||
type Limit = 'upload_max_fileparts' | 'stickers_faved_limit' | 'saved_gifs_limit' | 'dialog_filters_chats_limit' |
|
||||
'dialog_filters_limit' | 'dialogs_folder_pinned_limit' | 'dialogs_pinned_limit' | 'caption_length_limit' |
|
||||
'channels_limit' | 'channels_public_limit' | 'about_length_limit' | 'chatlist_invites_limit' | 'chatlist_joined_limit';
|
||||
type Limit =
|
||||
| 'upload_max_fileparts'
|
||||
| 'stickers_faved_limit'
|
||||
| 'saved_gifs_limit'
|
||||
| 'dialog_filters_chats_limit'
|
||||
| 'dialog_filters_limit'
|
||||
| 'dialogs_folder_pinned_limit'
|
||||
| 'dialogs_pinned_limit'
|
||||
| 'caption_length_limit'
|
||||
| 'channels_limit'
|
||||
| 'channels_public_limit'
|
||||
| 'about_length_limit'
|
||||
| 'chatlist_invites_limit'
|
||||
| 'chatlist_joined_limit'
|
||||
| 'recommended_channels_limit';
|
||||
type LimitKey = `${Limit}_${LimitType}`;
|
||||
type LimitsConfig = Record<LimitKey, number>;
|
||||
|
||||
@ -111,6 +123,7 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
|
||||
aboutLength: getLimit(appConfig, 'about_length_limit', 'aboutLength'),
|
||||
chatlistInvites: getLimit(appConfig, 'chatlist_invites_limit', 'chatlistInvites'),
|
||||
chatlistJoined: getLimit(appConfig, 'chatlist_joined_limit', 'chatlistJoined'),
|
||||
recommendedChannels: getLimit(appConfig, 'recommended_channels_limit', 'recommendedChannels'),
|
||||
},
|
||||
hash,
|
||||
areStoriesHidden: appConfig.stories_all_hidden,
|
||||
|
||||
@ -1895,6 +1895,22 @@ export function setViewForumAsMessages({ chat, isEnabled }: { chat: ApiChat; isE
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchChannelRecommendations({ chat }: { chat: ApiChat }) {
|
||||
const { id, accessHash } = chat;
|
||||
const channel = buildInputEntity(id, accessHash);
|
||||
|
||||
const result = await invokeRequest(new GramJs.channels.GetChannelRecommendations({
|
||||
channel: channel as GramJs.InputChannel,
|
||||
}));
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
updateLocalDb(result);
|
||||
|
||||
return result?.chats.map((_chat) => buildApiChatFromPreview(_chat)).filter(Boolean);
|
||||
}
|
||||
|
||||
function handleUserPrivacyRestrictedUpdates(updates: GramJs.TypeUpdates) {
|
||||
if (!(updates instanceof GramJs.Updates) && !(updates instanceof GramJs.UpdatesCombined)) {
|
||||
return undefined;
|
||||
|
||||
@ -24,6 +24,7 @@ export {
|
||||
editTopic, toggleForum, fetchTopicById, createTopic, toggleParticipantsHidden, checkChatlistInvite,
|
||||
joinChatlistInvite, createChalistInvite, editChatlistInvite, deleteChatlistInvite, fetchChatlistInvites,
|
||||
fetchLeaveChatlistSuggestions, leaveChatlist, togglePeerTranslations, setViewForumAsMessages,
|
||||
fetchChannelRecommendations,
|
||||
} from './chats';
|
||||
|
||||
export {
|
||||
|
||||
@ -98,6 +98,7 @@ const LIMITS_ORDER: ApiLimitTypeWithoutUpload[] = [
|
||||
'captionLength',
|
||||
'dialogFilters',
|
||||
'dialogFiltersChats',
|
||||
'recommendedChannels',
|
||||
];
|
||||
|
||||
const LIMITS_TITLES: Record<ApiLimitTypeWithoutUpload, string> = {
|
||||
@ -110,6 +111,7 @@ const LIMITS_TITLES: Record<ApiLimitTypeWithoutUpload, string> = {
|
||||
captionLength: 'CaptionsLimitTitle',
|
||||
dialogFilters: 'FoldersLimitTitle',
|
||||
dialogFiltersChats: 'ChatPerFolderLimitTitle',
|
||||
recommendedChannels: 'SimilarChannelsLimitTitle',
|
||||
};
|
||||
|
||||
const LIMITS_DESCRIPTIONS: Record<ApiLimitTypeWithoutUpload, string> = {
|
||||
@ -122,6 +124,7 @@ const LIMITS_DESCRIPTIONS: Record<ApiLimitTypeWithoutUpload, string> = {
|
||||
captionLength: 'CaptionsLimitSubtitle',
|
||||
dialogFilters: 'FoldersLimitSubtitle',
|
||||
dialogFiltersChats: 'ChatPerFolderLimitSubtitle',
|
||||
recommendedChannels: 'SimilarChannelsLimitSubtitle',
|
||||
};
|
||||
|
||||
const BORDER_THRESHOLD = 20;
|
||||
|
||||
@ -121,6 +121,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.similarChannels-list,
|
||||
&.commonChats-list,
|
||||
&.members-list {
|
||||
padding: 0.5rem;
|
||||
@ -133,5 +134,30 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.similarChannels-list {
|
||||
.ListItem.blured {
|
||||
filter: opacity(0.8);
|
||||
}
|
||||
|
||||
.show-more-channels {
|
||||
width: calc(100% - 1rem);
|
||||
margin: 0 auto;
|
||||
margin-top: -1.8125rem;
|
||||
z-index: 1;
|
||||
border-radius: var(--border-radius-default-small);
|
||||
box-shadow: 0px 0px 1rem 1rem white;
|
||||
|
||||
.icon {
|
||||
margin-left: 0.625rem;
|
||||
}
|
||||
}
|
||||
|
||||
.more-similar {
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,15 +34,20 @@ import {
|
||||
selectChatFullInfo,
|
||||
selectChatMessages,
|
||||
selectCurrentMediaSearch,
|
||||
selectIsCurrentUserPremium,
|
||||
selectIsRightColumnShown,
|
||||
selectPeerFullInfo,
|
||||
selectPeerStories,
|
||||
selectSimilarChannelIds,
|
||||
selectTabState,
|
||||
selectTheme,
|
||||
selectUser,
|
||||
} from '../../global/selectors';
|
||||
import { selectPremiumLimit } from '../../global/selectors/limits';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { captureEvents, SwipeDirection } from '../../util/captureEvents';
|
||||
import { IS_TOUCH_ENV } from '../../util/windowEnvironment';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
import { getSenderName } from '../left/search/helpers/getSenderName';
|
||||
|
||||
import usePeerStoriesPolling from '../../hooks/polling/usePeerStoriesPolling';
|
||||
@ -66,6 +71,7 @@ import PrivateChatInfo from '../common/PrivateChatInfo';
|
||||
import ProfileInfo from '../common/ProfileInfo';
|
||||
import WebLink from '../common/WebLink';
|
||||
import MediaStory from '../story/MediaStory';
|
||||
import Button from '../ui/Button';
|
||||
import FloatingActionButton from '../ui/FloatingActionButton';
|
||||
import InfiniteScroll from '../ui/InfiniteScroll';
|
||||
import ListItem, { type MenuItemContextAction } from '../ui/ListItem';
|
||||
@ -113,6 +119,9 @@ type StateProps = {
|
||||
isChatProtected?: boolean;
|
||||
nextProfileTab?: ProfileTabType;
|
||||
shouldWarnAboutSvg?: boolean;
|
||||
similarChannels?: string[];
|
||||
isCurrentUserPremium?: boolean;
|
||||
limitSimilarChannels: number;
|
||||
};
|
||||
|
||||
const TABS = [
|
||||
@ -158,6 +167,9 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
isChatProtected,
|
||||
nextProfileTab,
|
||||
shouldWarnAboutSvg,
|
||||
similarChannels,
|
||||
isCurrentUserPremium,
|
||||
limitSimilarChannels,
|
||||
}) => {
|
||||
const {
|
||||
setLocalMediaSearchType,
|
||||
@ -172,6 +184,8 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
setNewChatMembersDialogState,
|
||||
loadPeerPinnedStories,
|
||||
loadStoriesArchive,
|
||||
openPremiumModal,
|
||||
fetchChannelRecommendations,
|
||||
} = getActions();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
@ -192,7 +206,19 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
// in forum topics. Return it when it's fixed on the server side.
|
||||
...(!topicId ? [{ type: 'voice', title: 'SharedVoiceTab2' }] : []),
|
||||
...(hasCommonChatsTab ? [{ type: 'commonChats', title: 'SharedGroupsTab2' }] : []),
|
||||
]), [chatId, currentUserId, hasCommonChatsTab, hasMembersTab, hasStoriesTab, isChannel, topicId]);
|
||||
...(isChannel && similarChannels?.length
|
||||
? [{ type: 'similarChannels', title: 'SimilarChannelsTab' }]
|
||||
: []),
|
||||
]), [
|
||||
chatId,
|
||||
currentUserId,
|
||||
hasCommonChatsTab,
|
||||
hasMembersTab,
|
||||
hasStoriesTab,
|
||||
isChannel,
|
||||
topicId,
|
||||
similarChannels,
|
||||
]);
|
||||
|
||||
const initialTab = useMemo(() => {
|
||||
if (!nextProfileTab) {
|
||||
@ -213,6 +239,12 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
setActiveTab(index);
|
||||
}, [nextProfileTab, tabs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isChannel) {
|
||||
fetchChannelRecommendations({ chatId });
|
||||
}
|
||||
}, [chatId, isChannel]);
|
||||
|
||||
const renderingActiveTab = activeTab > tabs.length - 1 ? tabs.length - 1 : activeTab;
|
||||
const tabType = tabs[renderingActiveTab].type as ProfileTabType;
|
||||
const handleLoadPeerStories = useCallback(({ offsetId }: { offsetId: number }) => {
|
||||
@ -240,6 +272,7 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
topicId,
|
||||
storyIds,
|
||||
archiveStoryIds,
|
||||
similarChannels,
|
||||
);
|
||||
const isFirstTab = (hasStoriesTab && resultType === 'stories')
|
||||
|| resultType === 'members'
|
||||
@ -512,6 +545,35 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
<GroupChatInfo chatId={id} />
|
||||
</ListItem>
|
||||
))
|
||||
) : 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',
|
||||
)}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => openChat({ id: channelId })}
|
||||
>
|
||||
<GroupChatInfo avatarSize="large" chatId={channelId} withFullInfo />
|
||||
</ListItem>
|
||||
))}
|
||||
{!isCurrentUserPremium && (
|
||||
<>
|
||||
{/* eslint-disable-next-line react/jsx-no-bind */}
|
||||
<Button className="show-more-channels" size="smaller" onClick={() => openPremiumModal()}>
|
||||
{lang('UnlockSimilar')}
|
||||
<i className="icon icon-unlock-badge" />
|
||||
</Button>
|
||||
<div className="more-similar">
|
||||
{renderText(lang('MoreSimilarText', limitSimilarChannels), ['simple_markdown'])}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : undefined}
|
||||
</div>
|
||||
);
|
||||
@ -604,6 +666,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
&& (getHasAdminRight(chat, 'inviteUsers') || !isUserRightBanned(chat, 'inviteUsers') || chat.isCreator);
|
||||
const canDeleteMembers = hasMembersTab && chat && (getHasAdminRight(chat, 'banUsers') || chat.isCreator);
|
||||
const activeDownloads = selectActiveDownloads(global, chatId);
|
||||
const similarChannels = selectSimilarChannelIds(global, chatId);
|
||||
const isCurrentUserPremium = selectIsCurrentUserPremium(global);
|
||||
|
||||
let hasCommonChatsTab;
|
||||
let resolvedUserId;
|
||||
@ -648,6 +712,9 @@ export default memo(withGlobal<OwnProps>(
|
||||
isChatProtected: chat?.isProtected,
|
||||
nextProfileTab: selectTabState(global).nextProfileTab,
|
||||
shouldWarnAboutSvg: global.settings.byKey.shouldWarnAboutSvg,
|
||||
similarChannels,
|
||||
isCurrentUserPremium,
|
||||
limitSimilarChannels: selectPremiumLimit(global, 'recommendedChannels'),
|
||||
...(hasMembersTab && members && { members, adminMembersById }),
|
||||
...(hasCommonChatsTab && user && { commonChatIds: user.commonChats?.ids }),
|
||||
};
|
||||
|
||||
@ -29,6 +29,7 @@ export default function useProfileViewportIds(
|
||||
topicId?: number,
|
||||
storyIds?: number[],
|
||||
archiveStoryIds?: number[],
|
||||
similarChannels?: string[],
|
||||
) {
|
||||
const resultType = tabType === 'members' || !mediaSearchType ? tabType : mediaSearchType;
|
||||
|
||||
@ -142,6 +143,9 @@ export default function useProfileViewportIds(
|
||||
getMore = getMoreStoriesArchive;
|
||||
noProfileInfo = noProfileInfoForStoriesArchive;
|
||||
break;
|
||||
case 'similarChannels':
|
||||
viewportIds = similarChannels;
|
||||
break;
|
||||
}
|
||||
|
||||
return [resultType, viewportIds, getMore, noProfileInfo] as const;
|
||||
|
||||
@ -337,4 +337,5 @@ export const DEFAULT_LIMITS: Record<ApiLimitType, readonly [number, number]> = {
|
||||
aboutLength: [70, 140],
|
||||
chatlistInvites: [3, 100],
|
||||
chatlistJoined: [2, 20],
|
||||
recommendedChannels: [10, 100],
|
||||
};
|
||||
|
||||
@ -51,6 +51,7 @@ import {
|
||||
addChatMembers,
|
||||
addChats,
|
||||
addMessages,
|
||||
addSimilarChannels,
|
||||
addUsers,
|
||||
addUserStatuses,
|
||||
addUsersToRestrictedInviteList,
|
||||
@ -2517,6 +2518,29 @@ addActionHandler('setViewForumAsMessages', (global, actions, payload): ActionRet
|
||||
void callApi('setViewForumAsMessages', { chat, isEnabled });
|
||||
});
|
||||
|
||||
addActionHandler('fetchChannelRecommendations', async (global, actions, payload): Promise<void> => {
|
||||
const { chatId } = payload;
|
||||
const chat = selectChat(global, chatId);
|
||||
|
||||
if (!chat) {
|
||||
return;
|
||||
}
|
||||
|
||||
const similarChannels = await callApi('fetchChannelRecommendations', {
|
||||
chat,
|
||||
});
|
||||
|
||||
if (!similarChannels) {
|
||||
return;
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
global = addChats(global, buildCollectionByKey(similarChannels, 'id'));
|
||||
global = addSimilarChannels(global, chatId, similarChannels.map((channel) => channel.id));
|
||||
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
async function loadChats(
|
||||
listType: 'active' | 'archived',
|
||||
offsetId?: string,
|
||||
|
||||
@ -205,6 +205,10 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
|
||||
cached.stories.orderedPeerIds = initialState.stories.orderedPeerIds;
|
||||
}
|
||||
|
||||
if (!cached.chats.similarChannelsById) {
|
||||
cached.chats.similarChannelsById = initialState.chats.similarChannelsById;
|
||||
}
|
||||
|
||||
// Clear old color storage to optimize cache size
|
||||
if (untypedCached?.appConfig?.peerColors) {
|
||||
untypedCached.appConfig.peerColors = undefined;
|
||||
@ -373,6 +377,7 @@ function reduceChats<T extends GlobalState>(global: T): GlobalState['chats'] {
|
||||
|
||||
return {
|
||||
...global.chats,
|
||||
similarChannelsById: {},
|
||||
isFullyLoaded: {},
|
||||
byId: pick(global.chats.byId, idsToSave),
|
||||
fullInfoById: pick(global.chats.fullInfoById, idsToSave),
|
||||
|
||||
@ -105,6 +105,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
|
||||
totalCount: {},
|
||||
byId: {},
|
||||
fullInfoById: {},
|
||||
similarChannelsById: {},
|
||||
},
|
||||
|
||||
messages: {
|
||||
|
||||
@ -401,3 +401,20 @@ export function deleteTopic<T extends GlobalState>(
|
||||
|
||||
return global;
|
||||
}
|
||||
|
||||
export function addSimilarChannels<T extends GlobalState>(
|
||||
global: T,
|
||||
chatId: string,
|
||||
similarChannels: string[],
|
||||
) {
|
||||
return {
|
||||
...global,
|
||||
chats: {
|
||||
...global.chats,
|
||||
similarChannelsById: {
|
||||
...global.chats.similarChannelsById,
|
||||
[chatId]: similarChannels,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -316,3 +316,10 @@ export function selectRequestedChatTranslationLanguage<T extends GlobalState>(
|
||||
|
||||
return requestedTranslations.byChatId[chatId]?.toLanguage;
|
||||
}
|
||||
|
||||
export function selectSimilarChannelIds<T extends GlobalState>(
|
||||
global: T,
|
||||
chatId: string,
|
||||
): string[] | undefined {
|
||||
return global.chats.similarChannelsById[chatId];
|
||||
}
|
||||
|
||||
@ -169,13 +169,23 @@ export interface ServiceNotification {
|
||||
isDeleted?: boolean;
|
||||
}
|
||||
|
||||
export type ApiLimitType = (
|
||||
'uploadMaxFileparts' | 'stickersFaved' | 'savedGifs' | 'dialogFiltersChats' | 'dialogFilters' | 'dialogFolderPinned' |
|
||||
'captionLength' | 'channels' | 'channelsPublic' | 'aboutLength' | 'chatlistInvites' | 'chatlistJoined'
|
||||
);
|
||||
export type ApiLimitType =
|
||||
| 'uploadMaxFileparts'
|
||||
| 'stickersFaved'
|
||||
| 'savedGifs'
|
||||
| 'dialogFiltersChats'
|
||||
| 'dialogFilters'
|
||||
| 'dialogFolderPinned'
|
||||
| 'captionLength'
|
||||
| 'channels'
|
||||
| 'channelsPublic'
|
||||
| 'aboutLength'
|
||||
| 'chatlistInvites'
|
||||
| 'chatlistJoined'
|
||||
| 'recommendedChannels';
|
||||
|
||||
export type ApiLimitTypeWithModal = Exclude<ApiLimitType, (
|
||||
'captionLength' | 'aboutLength' | 'stickersFaved' | 'savedGifs'
|
||||
'captionLength' | 'aboutLength' | 'stickersFaved' | 'savedGifs' | 'recommendedChannels'
|
||||
)>;
|
||||
|
||||
export type TranslatedMessage = {
|
||||
@ -771,6 +781,7 @@ export type GlobalState = {
|
||||
forDiscussionIds?: string[];
|
||||
// Obtained from GetFullChat / GetFullChannel
|
||||
fullInfoById: Record<string, ApiChatFullInfo>;
|
||||
similarChannelsById: Record<string, string[]>;
|
||||
};
|
||||
|
||||
messages: {
|
||||
@ -1759,6 +1770,9 @@ export interface ActionPayloads {
|
||||
fetchChat: {
|
||||
chatId: string;
|
||||
};
|
||||
fetchChannelRecommendations: {
|
||||
chatId: string;
|
||||
};
|
||||
updateChatMutedState: {
|
||||
chatId: string;
|
||||
isMuted?: boolean;
|
||||
|
||||
@ -1443,6 +1443,7 @@ channels.deleteTopicHistory#34435f2d channel:InputChannel top_msg_id:int = messa
|
||||
channels.toggleParticipantsHidden#6a6e7854 channel:InputChannel enabled:Bool = Updates;
|
||||
channels.clickSponsoredMessage#18afbc93 channel:InputChannel random_id:bytes = Bool;
|
||||
channels.toggleViewForumAsMessages#9738bb15 channel:InputChannel enabled:Bool = Updates;
|
||||
channels.getChannelRecommendations#83b70d97 channel:InputChannel = messages.Chats;
|
||||
bots.canSendMessage#1359f4e6 bot:InputUser = Bool;
|
||||
bots.allowSendMessage#f132e3ef bot:InputUser = Updates;
|
||||
bots.invokeWebViewCustomMethod#87fc5e7 bot:InputUser custom_method:string params:DataJSON = DataJSON;
|
||||
|
||||
@ -214,6 +214,7 @@
|
||||
"channels.toggleUsername",
|
||||
"channels.viewSponsoredMessage",
|
||||
"channels.getSponsoredMessages",
|
||||
"channels.getChannelRecommendations",
|
||||
"bots.canSendMessage",
|
||||
"bots.allowSendMessage",
|
||||
"bots.invokeWebViewCustomMethod",
|
||||
|
||||
@ -355,9 +355,17 @@ export enum NewChatMembersProgress {
|
||||
Loading,
|
||||
}
|
||||
|
||||
export type ProfileTabType = (
|
||||
'members' | 'commonChats' | 'media' | 'documents' | 'links' | 'audio' | 'voice' | 'stories' | 'storiesArchive'
|
||||
);
|
||||
export type ProfileTabType =
|
||||
| 'members'
|
||||
| 'commonChats'
|
||||
| 'media'
|
||||
| 'documents'
|
||||
| 'links'
|
||||
| 'audio'
|
||||
| 'voice'
|
||||
| 'stories'
|
||||
| 'storiesArchive'
|
||||
| 'similarChannels';
|
||||
export type SharedMediaType = 'media' | 'documents' | 'links' | 'audio' | 'voice';
|
||||
export type ApiPrivacyKey = 'phoneNumber' | 'addByPhone' | 'lastSeen' | 'profilePhoto' | 'voiceMessages' |
|
||||
'forwards' | 'chatInvite' | 'phoneCall' | 'phoneP2P' | 'bio';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user