Profile: Display gift and story collections (#6190)

Co-authored-by: Alexander Zinchuk <alx.zinchuk@gmail.com>
This commit is contained in:
Alexander Zinchuk 2025-09-09 21:10:31 +02:00
parent 4fa82cc83c
commit ca8260c309
34 changed files with 1158 additions and 40 deletions

View File

@ -9,6 +9,7 @@ import type {
ApiStarGiftAttribute, ApiStarGiftAttribute,
ApiStarGiftAttributeCounter, ApiStarGiftAttributeCounter,
ApiStarGiftAttributeId, ApiStarGiftAttributeId,
ApiStarGiftCollection,
ApiTypeResaleStarGifts, ApiTypeResaleStarGifts,
} from '../../types'; } from '../../types';
@ -287,3 +288,21 @@ GramJs.TypeStarGiftAttributeId[] {
} }
}); });
} }
export function buildApiStarGiftCollection(collection: GramJs.StarGiftCollection): ApiStarGiftCollection | undefined {
if (!collection) return undefined;
const { collectionId, title, icon, giftsCount, hash } = collection;
if (icon) {
addDocumentToLocalDb(icon);
}
return {
collectionId,
title,
icon: icon && buildStickerFromDocument(icon),
giftsCount,
hash: hash.toString(),
};
}

View File

@ -5,6 +5,7 @@ import type {
ApiMediaAreaCoordinates, ApiMediaAreaCoordinates,
ApiStealthMode, ApiStealthMode,
ApiStory, ApiStory,
ApiStoryAlbum,
ApiStoryForwardInfo, ApiStoryForwardInfo,
ApiStoryView, ApiStoryView,
ApiStoryViews, ApiStoryViews,
@ -14,8 +15,10 @@ import type {
} from '../../types'; } from '../../types';
import { buildCollectionByCallback, omitUndefined } from '../../../util/iteratees'; import { buildCollectionByCallback, omitUndefined } from '../../../util/iteratees';
import { buildPrivacyRules } from './common'; import { addDocumentToLocalDb } from '../helpers/localDb';
import { buildGeoPoint, buildMessageMediaContent, buildMessageTextContent } from './messageContent'; import { addPhotoToLocalDb } from '../helpers/localDb';
import { buildApiPhoto, buildPrivacyRules } from './common';
import { buildApiDocument, buildGeoPoint, buildMessageMediaContent, buildMessageTextContent } from './messageContent';
import { buildApiMessage } from './messages'; import { buildApiMessage } from './messages';
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers'; import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
import { buildApiReaction, buildReactionCount } from './reactions'; import { buildApiReaction, buildReactionCount } from './reactions';
@ -279,3 +282,23 @@ export function buildApiStoryForwardInfo(forwardHeader: GramJs.TypeStoryFwdHeade
isModified: modified, isModified: modified,
}; };
} }
export function buildApiStoryAlbum(album: GramJs.StoryAlbum): ApiStoryAlbum {
const {
albumId, title, iconPhoto, iconVideo,
} = album;
if (iconPhoto) {
addPhotoToLocalDb(iconPhoto);
}
if (iconVideo) {
addDocumentToLocalDb(iconVideo);
}
return {
albumId,
title,
iconPhoto: iconPhoto && iconPhoto instanceof GramJs.Photo ? buildApiPhoto(iconPhoto) : undefined,
iconVideo: iconVideo ? buildApiDocument(iconVideo) : undefined,
};
}

View File

@ -13,7 +13,7 @@ import type {
import { buildApiChatFromPreview } from '../apiBuilders/chats'; import { buildApiChatFromPreview } from '../apiBuilders/chats';
import { buildApiResaleGifts, buildApiSavedStarGift, buildApiStarGift, import { buildApiResaleGifts, buildApiSavedStarGift, buildApiStarGift,
buildApiStarGiftAttribute, buildInputResaleGiftsAttributes } from '../apiBuilders/gifts'; buildApiStarGiftAttribute, buildApiStarGiftCollection, buildInputResaleGiftsAttributes } from '../apiBuilders/gifts';
import { import {
buildApiCurrencyAmount, buildApiCurrencyAmount,
buildApiStarsGiftOptions, buildApiStarsGiftOptions,
@ -112,11 +112,13 @@ export async function fetchSavedStarGifts({
offset = DEFAULT_PRIMITIVES.STRING, offset = DEFAULT_PRIMITIVES.STRING,
limit = DEFAULT_PRIMITIVES.INT, limit = DEFAULT_PRIMITIVES.INT,
filter, filter,
collectionId,
}: { }: {
peer: ApiPeer; peer: ApiPeer;
offset?: string; offset?: string;
limit?: number; limit?: number;
filter?: GiftProfileFilterOptions; filter?: GiftProfileFilterOptions;
collectionId?: number;
}) { }) {
type GetSavedStarGiftsParams = ConstructorParameters<typeof GramJs.payments.GetSavedStarGifts>[0]; type GetSavedStarGiftsParams = ConstructorParameters<typeof GramJs.payments.GetSavedStarGifts>[0];
@ -124,6 +126,7 @@ export async function fetchSavedStarGifts({
peer: buildInputPeer(peer.id, peer.accessHash), peer: buildInputPeer(peer.id, peer.accessHash),
offset, offset,
limit, limit,
collectionId,
...(filter && { ...(filter && {
sortByValue: filter.sortType === 'byValue' || undefined, sortByValue: filter.sortType === 'byValue' || undefined,
excludeUnlimited: !filter.shouldIncludeUnlimited || undefined, excludeUnlimited: !filter.shouldIncludeUnlimited || undefined,
@ -476,3 +479,24 @@ export async function fetchStarGiftWithdrawalUrl({
return undefined; return undefined;
} }
export async function fetchStarGiftCollections({
peer,
hash,
}: {
peer: ApiPeer;
hash?: string;
}) {
const result = await invokeRequest(new GramJs.payments.GetStarGiftCollections({
peer: buildInputPeer(peer.id, peer.accessHash),
hash: hash ? bigInt(hash) : DEFAULT_PRIMITIVES.BIGINT,
}));
if (!result || result instanceof GramJs.payments.StarGiftCollectionsNotModified) {
return undefined;
}
return {
collections: result.collections.map(buildApiStarGiftCollection).filter(Boolean),
};
}

View File

@ -7,6 +7,7 @@ import type {
ApiPeerStories, ApiPeerStories,
ApiReaction, ApiReaction,
ApiStealthMode, ApiStealthMode,
ApiStoryAlbum,
ApiTypeStory, ApiTypeStory,
} from '../../types'; } from '../../types';
@ -19,6 +20,7 @@ import {
buildApiPeerStories, buildApiPeerStories,
buildApiStealthMode, buildApiStealthMode,
buildApiStory, buildApiStory,
buildApiStoryAlbum,
buildApiStoryView, buildApiStoryView,
buildApiStoryViews, buildApiStoryViews,
} from '../apiBuilders/stories'; } from '../apiBuilders/stories';
@ -463,3 +465,63 @@ export function activateStealthMode({
shouldReturnTrue: true, shouldReturnTrue: true,
}); });
} }
export async function fetchAlbums({
peer,
}: {
peer: ApiPeer;
}): Promise<ApiStoryAlbum[] | undefined> {
const result = await invokeRequest(new GramJs.stories.GetAlbums({
peer: buildInputPeer(peer.id, peer.accessHash),
hash: DEFAULT_PRIMITIVES.BIGINT,
}));
if (!result || result instanceof GramJs.stories.AlbumsNotModified) {
return undefined;
}
return result.albums.map(buildApiStoryAlbum);
}
export async function fetchAlbumStories({
peer,
albumId,
offset = 0,
limit = STORY_LIST_LIMIT,
}: {
peer: ApiPeer;
albumId: number;
offset?: number;
limit?: number;
}): Promise<{
stories: Record<number, ApiTypeStory>;
pinnedIds?: number[];
count: number;
} | undefined> {
const result = await invokeRequest(new GramJs.stories.GetAlbumStories({
peer: buildInputPeer(peer.id, peer.accessHash),
albumId,
offset,
limit,
}));
if (!result) {
return undefined;
}
const stories = buildCollectionByCallback(result.stories, (story) => (
[story.id, buildApiStory(peer.id, story)]
));
result.stories.forEach((story) => {
if (story && story instanceof GramJs.StoryItem) {
addStoryToLocalDb(story, peer.id);
}
});
return {
stories,
pinnedIds: result.pinnedToTop,
count: result.count,
};
}

View File

@ -282,6 +282,14 @@ export interface ApiDisallowedGiftsSettings {
shouldDisallowPremiumGifts?: true; shouldDisallowPremiumGifts?: true;
} }
export interface ApiStarGiftCollection {
collectionId: number;
title: string;
icon?: ApiSticker;
giftsCount: number;
hash: string;
}
export interface ApiStarsRating { export interface ApiStarsRating {
level: number; level: number;
currentLevelStars: number; currentLevelStars: number;

View File

@ -1,6 +1,8 @@
import type { import type {
ApiDocument,
ApiGeoPoint, ApiGeoPoint,
ApiMessage, ApiMessage,
ApiPhoto,
ApiReaction, ApiReaction,
ApiReactionCount, ApiReactionCount,
ApiSticker, ApiSticker,
@ -69,6 +71,10 @@ export type ApiPeerStories = {
isArchiveFullyLoaded?: boolean; isArchiveFullyLoaded?: boolean;
lastUpdatedAt?: number; lastUpdatedAt?: number;
lastReadId?: number; lastReadId?: number;
idsByAlbumId?: Record<number, {
ids: number[];
isFullyLoaded?: boolean;
}>; // Story IDs grouped by album ID with loading state
}; };
export type ApiMessageStoryData = { export type ApiMessageStoryData = {
@ -184,3 +190,10 @@ export type ApiMediaAreaUniqueGift = {
export type ApiMediaArea = ApiMediaAreaVenue | ApiMediaAreaGeoPoint | ApiMediaAreaSuggestedReaction export type ApiMediaArea = ApiMediaAreaVenue | ApiMediaAreaGeoPoint | ApiMediaAreaSuggestedReaction
| ApiMediaAreaChannelPost | ApiMediaAreaUrl | ApiMediaAreaWeather | ApiMediaAreaUniqueGift; | ApiMediaAreaChannelPost | ApiMediaAreaUrl | ApiMediaAreaWeather | ApiMediaAreaUniqueGift;
export type ApiStoryAlbum = {
albumId: number;
title: string;
iconPhoto?: ApiPhoto;
iconVideo?: ApiDocument;
};

View File

@ -2235,6 +2235,8 @@
"PublicPostsPremiumFeatureSubtitle" = "Global search is a Premium feature."; "PublicPostsPremiumFeatureSubtitle" = "Global search is a Premium feature.";
"PublicPostsSubscribeToPremium" = "Subscribe to Premium"; "PublicPostsSubscribeToPremium" = "Subscribe to Premium";
"NotificationPaidExtraSearch" = "{stars} spent on extra search."; "NotificationPaidExtraSearch" = "{stars} spent on extra search.";
"PostsSearchTransaction" = "Posts Search";
"AllStoriesCategory" = "All stories";
"PostsSearchTransaction" = "Public Post Search"; "PostsSearchTransaction" = "Public Post Search";
"TitleRating" = "Rating"; "TitleRating" = "Rating";
"RatingReflectsActivity" = "This rating reflects {name}'s activity on Telegram. It is based on:"; "RatingReflectsActivity" = "This rating reflects {name}'s activity on Telegram. It is based on:";
@ -2258,3 +2260,4 @@
"ErrorFocusInaccessibleMessage" = "Unfortunately, you can't access this message. You aren't a member of the chat where it was posted."; "ErrorFocusInaccessibleMessage" = "Unfortunately, you can't access this message. You aren't a member of the chat where it was posted.";
"ContextMenuHintMouse" = "To edit or reply, close this menu. Then right click on a message."; "ContextMenuHintMouse" = "To edit or reply, close this menu. Then right click on a message.";
"ContextMenuHintTouch" = "To edit or reply, close this menu. Then long tap on a message."; "ContextMenuHintTouch" = "To edit or reply, close this menu. Then long tap on a message.";

View File

@ -0,0 +1,22 @@
.item {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0.375rem 0.75rem;
font-weight: var(--font-weight-medium);
white-space: nowrap;
transition: opacity 0.15s;
&:hover {
opacity: 0.85;
}
}
.icon {
margin-right: 0.375rem;
}

View File

@ -0,0 +1,46 @@
import { memo } from '../../lib/teact/teact';
import type { ApiSticker } from '../../api/types';
import useLastCallback from '../../hooks/useLastCallback';
import AnimatedIconFromSticker from './AnimatedIconFromSticker';
import styles from './AnimatedTabItem.module.scss';
type OwnProps = {
id: string;
title: string;
sticker?: ApiSticker;
onClick?: (id: string) => void;
};
const AnimatedTabItem = ({
id,
title,
sticker,
onClick,
}: OwnProps) => {
const handleClick = useLastCallback(() => {
onClick?.(id);
});
return (
<div
className={styles.item}
onClick={handleClick}
>
{sticker && (
<AnimatedIconFromSticker
className={styles.icon}
sticker={sticker}
size={20}
forcePreview
/>
)}
{title}
</div>
);
};
export default memo(AnimatedTabItem);

View File

@ -0,0 +1,72 @@
.container,
.clipPathContainer {
display: flex;
flex-shrink: 0;
flex-wrap: nowrap;
gap: var(--tab-gap-size, 0.25rem);
align-items: flex-end;
justify-content: flex-start;
padding-inline: 0.5rem;
font-size: 0.9375rem;
font-weight: var(--font-weight-medium);
}
.container {
user-select: none;
position: relative;
z-index: 1;
overflow-x: auto;
color: var(--color-text-secondary);
opacity: 0;
transition: background-color 150ms, opacity 150ms;
mask-image: linear-gradient(to right, transparent, black 0.5rem, black calc(100% - 0.5rem), transparent);
&::-webkit-scrollbar {
height: 0;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0);
// `box-shadow` prevents repaint on macOS when hovering out of scrollable container
box-shadow: 0 0 1px rgba(255, 255, 255, 0.01);
}
&.isVisible {
opacity: 1;
}
}
.clipPathContainer {
// Hardware acceleration for clip-path animations
will-change: clip-path;
// Use GPU compositing for better performance
isolation: isolate;
position: absolute;
z-index: 3;
right: 0;
left: 0;
// Optimize for animations
contain: layout style paint;
overflow: hidden;
width: fit-content;
color: var(--color-primary);
background-color: var(--color-interactive-element-hover);
transition: var(--slide-transition) clip-path;
&.noAnimation {
transition: none;
}
}

View File

@ -0,0 +1,114 @@
import { memo, useEffect, useRef, useState } from '../../lib/teact/teact';
import type { ApiSticker } from '../../api/types';
import type { AnimationLevel } from '../../types';
import buildClassName from '../../util/buildClassName';
import useHorizontalScroll from '../../hooks/useHorizontalScroll';
import useLastCallback from '../../hooks/useLastCallback';
import useResizeObserver from '../../hooks/useResizeObserver';
import AnimatedTabItem from './AnimatedTabItem';
import styles from './AnimatedTabList.module.scss';
export type TabItem = {
id: string;
title: string;
sticker?: ApiSticker;
};
type OwnProps = {
items: TabItem[];
selectedItemId?: string;
className?: string;
animationLevel?: AnimationLevel;
onItemSelect?: (itemId: string) => void;
};
const AnimatedTabList = ({
items,
selectedItemId,
animationLevel = 1,
onItemSelect,
className,
}: OwnProps) => {
const containerRef = useRef<HTMLDivElement>();
const clipPathContainerRef = useRef<HTMLDivElement>();
const selectedIndex = items.findIndex((item) => item.id === selectedItemId) || 0;
const [clipPath, setClipPath] = useState<string>('');
const shouldAnimate = animationLevel > 0;
useHorizontalScroll(containerRef, !items.length, true);
const updateClipPath = useLastCallback(() => {
const clipPathContainer = clipPathContainerRef.current;
const activeTab = selectedIndex >= 0 && clipPathContainer?.childNodes[selectedIndex] as HTMLElement | null;
if (clipPathContainer && activeTab && clipPathContainer.offsetWidth > 0) {
const { offsetLeft, offsetWidth } = activeTab;
const containerWidth = clipPathContainer.offsetWidth;
const left = (offsetLeft / containerWidth * 100).toFixed(1);
const right = ((containerWidth - (offsetLeft + offsetWidth)) / containerWidth * 100).toFixed(1);
const newClipPath = `inset(0 ${right}% 0 ${left}% round 1rem)`;
setClipPath(newClipPath);
}
});
useEffect(() => {
updateClipPath();
}, [selectedIndex, items]);
useResizeObserver(clipPathContainerRef, updateClipPath);
if (!items.length) return undefined;
return (
<div
ref={containerRef}
className={
buildClassName(
styles.container,
'no-scrollbar',
className,
clipPath && styles.isVisible,
)
}
>
{items.map((item) => (
<AnimatedTabItem
key={item.id}
id={item.id}
title={item.title}
sticker={item.sticker}
onClick={onItemSelect}
/>
))}
<div
ref={clipPathContainerRef}
className={buildClassName(
styles.clipPathContainer,
'clip-path-container',
!shouldAnimate && styles.noAnimation,
)}
style={clipPath ? `clip-path: ${clipPath}` : undefined}
aria-hidden
>
{items.map((item, i) => (
<AnimatedTabItem
key={item.id}
id={item.id}
title={item.title}
sticker={item.sticker}
onClick={onItemSelect}
/>
))}
</div>
</div>
);
};
export default memo(AnimatedTabList);

View File

@ -101,7 +101,20 @@
height: 100% !important; height: 100% !important;
} }
.shared-media-transition {
overflow: hidden;
}
.content { .content {
transition: transform 0.3s;
&.showContentPanel {
transform: translateY(3rem);
padding-bottom: 3.5rem !important;
}
&.noTransition {
transition: none;
}
&.empty-list { &.empty-list {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
@ -210,4 +223,22 @@
} }
} }
} }
.contentCategoriesPanel {
position: absolute;
top: 0;
right: 0;
left: 0;
transition: transform 0.3s, opacity 0.3s;
&.hiddenPanel {
transform: translateY(-100%);
opacity: 0;
}
&.noTransition {
transition: none;
}
}
} }

View File

@ -8,6 +8,8 @@ import type {
ApiChatMember, ApiChatMember,
ApiMessage, ApiMessage,
ApiSavedStarGift, ApiSavedStarGift,
ApiStarGiftCollection,
ApiStoryAlbum,
ApiTypeStory, ApiTypeStory,
ApiUser, ApiUser,
ApiUserStatus, ApiUserStatus,
@ -19,6 +21,8 @@ import { MAIN_THREAD_ID } from '../../api/types';
import { AudioOrigin, MediaViewerOrigin, NewChatMembersProgress } from '../../types'; import { AudioOrigin, MediaViewerOrigin, NewChatMembersProgress } from '../../types';
import { MEMBERS_SLICE, PROFILE_SENSITIVE_AREA, SHARED_MEDIA_SLICE, SLIDE_TRANSITION_DURATION } from '../../config'; import { MEMBERS_SLICE, PROFILE_SENSITIVE_AREA, SHARED_MEDIA_SLICE, SLIDE_TRANSITION_DURATION } from '../../config';
const CONTENT_PANEL_SHOW_DELAY = 300;
import { import {
getHasAdminRight, getHasAdminRight,
getIsDownloading, getIsDownloading,
@ -53,6 +57,7 @@ import {
import { selectPremiumLimit } from '../../global/selectors/limits'; import { selectPremiumLimit } from '../../global/selectors/limits';
import { selectMessageDownloadableMedia } from '../../global/selectors/media'; import { selectMessageDownloadableMedia } from '../../global/selectors/media';
import { selectSharedSettings } from '../../global/selectors/sharedState'; import { selectSharedSettings } from '../../global/selectors/sharedState';
import { areDeepEqual } from '../../util/areDeepEqual';
import { IS_TOUCH_ENV } from '../../util/browser/windowEnvironment'; import { IS_TOUCH_ENV } from '../../util/browser/windowEnvironment';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { captureEvents, SwipeDirection } from '../../util/captureEvents'; import { captureEvents, SwipeDirection } from '../../util/captureEvents';
@ -71,6 +76,7 @@ import { useIntersectionObserver } from '../../hooks/useIntersectionObserver';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang'; import useOldLang from '../../hooks/useOldLang';
import useSyncEffect from '../../hooks/useSyncEffect';
import useAsyncRendering from './hooks/useAsyncRendering'; import useAsyncRendering from './hooks/useAsyncRendering';
import useProfileState from './hooks/useProfileState'; import useProfileState from './hooks/useProfileState';
import useProfileViewportIds from './hooks/useProfileViewportIds'; import useProfileViewportIds from './hooks/useProfileViewportIds';
@ -100,6 +106,8 @@ import Spinner from '../ui/Spinner';
import TabList from '../ui/TabList'; import TabList from '../ui/TabList';
import Transition from '../ui/Transition'; import Transition from '../ui/Transition';
import DeleteMemberModal from './DeleteMemberModal'; import DeleteMemberModal from './DeleteMemberModal';
import StarGiftCollectionList from './gifts/StarGiftCollectionList';
import StoryAlbumList from './stories/StoryAlbumList';
import './Profile.scss'; import './Profile.scss';
@ -127,6 +135,8 @@ type StateProps = {
hasPreviewMediaTab?: boolean; hasPreviewMediaTab?: boolean;
hasGiftsTab?: boolean; hasGiftsTab?: boolean;
gifts?: ApiSavedStarGift[]; gifts?: ApiSavedStarGift[];
storyAlbums?: ApiStoryAlbum[];
giftCollections?: ApiStarGiftCollection[];
areMembersHidden?: boolean; areMembersHidden?: boolean;
canAddMembers?: boolean; canAddMembers?: boolean;
canDeleteMembers?: boolean; canDeleteMembers?: boolean;
@ -137,6 +147,9 @@ type StateProps = {
pinnedStoryIds?: number[]; pinnedStoryIds?: number[];
archiveStoryIds?: number[]; archiveStoryIds?: number[];
storyByIds?: Record<number, ApiTypeStory>; storyByIds?: Record<number, ApiTypeStory>;
selectedStoryAlbumId?: number;
activeCollectionId?: number;
giftsFilter?: any;
chatsById: Record<string, ApiChat>; chatsById: Record<string, ApiChat>;
usersById: Record<string, ApiUser>; usersById: Record<string, ApiUser>;
userStatusesById: Record<string, ApiUserStatus>; userStatusesById: Record<string, ApiUserStatus>;
@ -189,6 +202,9 @@ const Profile: FC<OwnProps & StateProps> = ({
pinnedStoryIds, pinnedStoryIds,
archiveStoryIds, archiveStoryIds,
storyByIds, storyByIds,
selectedStoryAlbumId,
activeCollectionId,
giftsFilter,
mediaSearchType, mediaSearchType,
hasCommonChatsTab, hasCommonChatsTab,
hasStoriesTab, hasStoriesTab,
@ -196,6 +212,8 @@ const Profile: FC<OwnProps & StateProps> = ({
hasPreviewMediaTab, hasPreviewMediaTab,
hasGiftsTab, hasGiftsTab,
gifts, gifts,
storyAlbums,
giftCollections,
botPreviewMedia, botPreviewMedia,
areMembersHidden, areMembersHidden,
canAddMembers, canAddMembers,
@ -241,6 +259,9 @@ const Profile: FC<OwnProps & StateProps> = ({
loadPreviewMedias, loadPreviewMedias,
loadPeerSavedGifts, loadPeerSavedGifts,
resetGiftProfileFilter, resetGiftProfileFilter,
loadStarGiftCollections,
loadStoryAlbums,
resetSelectedStoryAlbum,
} = getActions(); } = getActions();
const containerRef = useRef<HTMLDivElement>(); const containerRef = useRef<HTMLDivElement>();
@ -250,10 +271,13 @@ const Profile: FC<OwnProps & StateProps> = ({
const lang = useLang(); const lang = useLang();
const [deletingUserId, setDeletingUserId] = useState<string | undefined>(); const [deletingUserId, setDeletingUserId] = useState<string | undefined>();
const [isViewTransitionEnabled, enableViewTransition, disableViewTransition] = useFlag();
const profileId = isSavedDialog ? String(threadId) : chatId; const profileId = isSavedDialog ? String(threadId) : chatId;
const isSavedMessages = profileId === currentUserId && !isSavedDialog; const isSavedMessages = profileId === currentUserId && !isSavedDialog;
const [restoreContentHeightKey, setRestoreContentHeightKey] = useState(0);
const tabs = useMemo(() => { const tabs = useMemo(() => {
const arr: TabProps[] = []; const arr: TabProps[] = [];
if (isSavedMessages && !isSavedDialog) { if (isSavedMessages && !isSavedDialog) {
@ -352,6 +376,25 @@ const Profile: FC<OwnProps & StateProps> = ({
} }
}, [chatId, isBot, similarBots, isSynced]); }, [chatId, isBot, similarBots, isSynced]);
useEffect(() => {
resetSelectedStoryAlbum();
}, [chatId]);
useSyncEffect(() => {
enableViewTransition();
}, [giftsFilter]);
useSyncEffect(() => {
disableViewTransition();
}, [gifts]);
useEffect(() => {
if (hasGiftsTab && isSynced) {
loadStarGiftCollections({ peerId: chatId });
loadStoryAlbums({ peerId: chatId });
}
}, [chatId, hasGiftsTab, isSynced]);
const [renderingGifts, setRenderingGifts] = useState(gifts); const [renderingGifts, setRenderingGifts] = useState(gifts);
const { startViewTransition, shouldApplyVtn } = useViewTransition(); const { startViewTransition, shouldApplyVtn } = useViewTransition();
@ -371,12 +414,17 @@ const Profile: FC<OwnProps & StateProps> = ({
const handleLoadGifts = useCallback(() => { const handleLoadGifts = useCallback(() => {
loadPeerSavedGifts({ peerId: chatId }); loadPeerSavedGifts({ peerId: chatId });
}, [chatId]); }, [chatId]);
const handleLoadMoreMembers = useCallback(() => { const handleLoadMoreMembers = useCallback(() => {
loadMoreMembers({ chatId }); loadMoreMembers({ chatId });
}, [chatId, loadMoreMembers]); }, [chatId, loadMoreMembers]);
useEffectWithPrevDeps(([prevGifts]) => { useEffectWithPrevDeps(([prevGifts]) => {
if (!gifts || !prevGifts) { if (areDeepEqual(gifts, prevGifts)) {
return;
}
if (!gifts || !prevGifts || !isViewTransitionEnabled) {
setRenderingGifts(gifts); setRenderingGifts(gifts);
return; return;
} }
@ -385,14 +433,14 @@ const Profile: FC<OwnProps & StateProps> = ({
const newGiftIds = gifts.map((gift) => getSavedGiftKey(gift)); const newGiftIds = gifts.map((gift) => getSavedGiftKey(gift));
const hasOrderChanged = prevGiftIds.some((id, index) => id !== newGiftIds[index]); const hasOrderChanged = prevGiftIds.some((id, index) => id !== newGiftIds[index]);
if (hasOrderChanged) { if (hasOrderChanged && animationLevel > 0) {
startViewTransition(() => { startViewTransition(() => {
setRenderingGifts(gifts); setRenderingGifts(gifts);
}); });
} else { } else {
setRenderingGifts(gifts); setRenderingGifts(gifts);
} }
}, [gifts, startViewTransition]); }, [gifts, startViewTransition, animationLevel, isViewTransitionEnabled]);
const [resultType, viewportIds, getMore, noProfileInfo] = useProfileViewportIds({ const [resultType, viewportIds, getMore, noProfileInfo] = useProfileViewportIds({
loadMoreMembers: handleLoadMoreMembers, loadMoreMembers: handleLoadMoreMembers,
@ -418,12 +466,40 @@ const Profile: FC<OwnProps & StateProps> = ({
similarChannels, similarChannels,
similarBots, similarBots,
}); });
const isFirstTab = (isSavedMessages && resultType === 'dialogs') const isFirstTab = (isSavedMessages && resultType === 'dialogs')
|| (hasStoriesTab && resultType === 'stories') || (hasStoriesTab && resultType === 'stories')
|| resultType === 'members' || resultType === 'members'
|| (!hasMembersTab && resultType === 'media'); || (!hasMembersTab && resultType === 'media');
const activeKey = tabs.findIndex(({ type }) => type === resultType); 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); usePeerStoriesPolling(resultType === 'members' ? viewportIds as string[] : undefined);
const handleStopAutoScrollToTabs = useLastCallback(() => { const handleStopAutoScrollToTabs = useLastCallback(() => {
@ -583,16 +659,48 @@ const Profile: FC<OwnProps & StateProps> = ({
const noContent = (!viewportIds && !botPreviewMedia) || !canRenderContent || !messagesById; const noContent = (!viewportIds && !botPreviewMedia) || !canRenderContent || !messagesById;
const noSpinner = isFirstTab && !canRenderContent; const noSpinner = isFirstTab && !canRenderContent;
const isSpinner = noContent && !noSpinner;
return ( return (
<Transition activeKey={isSpinner ? 0 : 1} name="fade" shouldCleanup> <div>
{renderCategories()}
{renderSpinnerOrContent(noContent, noSpinner)} {renderSpinnerOrContent(noContent, noSpinner)}
</Transition> </div>
); );
} }
function renderSpinnerOrContent(noContent: boolean, noSpinner: boolean) { 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) { if (noContent) {
const forceRenderHiddenMembers = Boolean(resultType === 'members' && areMembersHidden); const forceRenderHiddenMembers = Boolean(resultType === 'members' && areMembersHidden);
@ -656,9 +764,15 @@ const Profile: FC<OwnProps & StateProps> = ({
return; return;
} }
const noTransition = resultType === 'gifts' ? isGiftCollectionsShowed
: resultType === 'stories' ? isStoryAlbumsShowed : false;
return ( return (
<div <div
className={`content ${resultType}-list`} className={buildClassName(
`content ${resultType}-list`,
shouldShowContentPanel && 'showContentPanel',
noTransition && 'noTransition',
)}
dir={oldLang.isRtl && resultType === 'media' ? 'rtl' : undefined} dir={oldLang.isRtl && resultType === 'media' ? 'rtl' : undefined}
teactFastList teactFastList
> >
@ -869,8 +983,69 @@ const Profile: FC<OwnProps & StateProps> = ({
); );
} }
const activeListSelector = `.shared-media-transition > .Transition_slide-active .${resultType}-list`; const shouldUseTransitionForContent = resultType === 'stories' || resultType === 'gifts';
const itemSelector = `${activeListSelector} > .scroll-item`; const contentTransitionKey = (() => {
if (resultType === 'stories') {
return selectedStoryAlbumId || 0;
}
if (resultType === 'gifts') {
return activeCollectionId || 0;
}
return 0;
})();
const handleOnStop = useLastCallback(() => {
setRestoreContentHeightKey(restoreContentHeightKey + 1);
});
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 ( return (
<InfiniteScroll <InfiniteScroll
@ -908,6 +1083,10 @@ const Profile: FC<OwnProps & StateProps> = ({
className="shared-media-transition" className="shared-media-transition"
onStart={applyTransitionFix} onStart={applyTransitionFix}
onStop={handleTransitionStop} onStop={handleTransitionStop}
restoreHeightKey={shouldUseTransitionForContent ? restoreContentHeightKey : undefined}
contentSelector={shouldUseTransitionForContent
? '.Transition > .Transition_slide-active > .Transition > .Transition_slide-active > .content'
: undefined}
> >
{renderContent()} {renderContent()}
</Transition> </Transition>
@ -998,13 +1177,21 @@ export default memo(withGlobal<OwnProps>(
const hasStoriesTab = peer && (user?.isSelf || (!peer.areStoriesHidden && peerFullInfo?.hasPinnedStories)) const hasStoriesTab = peer && (user?.isSelf || (!peer.areStoriesHidden && peerFullInfo?.hasPinnedStories))
&& !isSavedDialog; && !isSavedDialog;
const peerStories = hasStoriesTab ? selectPeerStories(global, peer.id) : undefined; const peerStories = hasStoriesTab ? selectPeerStories(global, peer.id) : undefined;
const storyIds = peerStories?.profileIds; const tabState = selectTabState(global);
const { selectedStoryAlbumId, nextProfileTab, forceScrollProfileTab, savedGifts } = tabState;
const storyIds = selectedStoryAlbumId
? peerStories?.idsByAlbumId?.[selectedStoryAlbumId]?.ids
: peerStories?.profileIds;
const pinnedStoryIds = peerStories?.pinnedIds; const pinnedStoryIds = peerStories?.pinnedIds;
const storyByIds = peerStories?.byId; const storyByIds = peerStories?.byId;
const archiveStoryIds = peerStories?.archiveIds; const archiveStoryIds = peerStories?.archiveIds;
const hasGiftsTab = Boolean(peerFullInfo?.starGiftCount) && !isSavedDialog; const hasGiftsTab = Boolean(peerFullInfo?.starGiftCount) && !isSavedDialog;
const peerGifts = selectTabState(global).savedGifts.giftsByPeerId[chatId]; const activeCollectionId = savedGifts.activeCollectionByPeerId[chatId];
const peerGifts = savedGifts.collectionsByPeerId[chatId]?.[activeCollectionId || 'all'];
const storyAlbums = global.stories.albumsByPeerId?.[chatId];
const giftCollections = global.starGiftCollections?.byPeerId?.[chatId];
const monoforumChannel = selectMonoforumChannel(global, chatId); const monoforumChannel = selectMonoforumChannel(global, chatId);
const isRestricted = chat && selectIsChatRestricted(global, chat.id); const isRestricted = chat && selectIsChatRestricted(global, chat.id);
@ -1033,12 +1220,17 @@ export default memo(withGlobal<OwnProps>(
storyIds, storyIds,
hasGiftsTab, hasGiftsTab,
gifts: peerGifts?.gifts, gifts: peerGifts?.gifts,
storyAlbums,
giftCollections,
pinnedStoryIds, pinnedStoryIds,
archiveStoryIds, archiveStoryIds,
storyByIds, storyByIds,
selectedStoryAlbumId,
activeCollectionId,
giftsFilter: savedGifts.filter,
isChatProtected: chat?.isProtected, isChatProtected: chat?.isProtected,
nextProfileTab: selectTabState(global).nextProfileTab, nextProfileTab,
forceScrollProfileTab: selectTabState(global).forceScrollProfileTab, forceScrollProfileTab,
animationLevel, animationLevel,
shouldWarnAboutFiles, shouldWarnAboutFiles,
similarChannels: similarChannelIds, similarChannels: similarChannelIds,

View File

@ -0,0 +1,3 @@
.tabList {
margin-top: 0.5rem;
}

View File

@ -0,0 +1,90 @@
import { memo, useMemo } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type { ApiStarGiftCollection } from '../../../api/types';
import type { AnimationLevel } from '../../../types';
import type { TabItem } from '../../common/AnimatedTabList';
import { selectActiveCollectionId } from '../../../global/selectors';
import { selectSharedSettings } from '../../../global/selectors/sharedState';
import buildClassName from '../../../util/buildClassName';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import AnimatedTabList from '../../common/AnimatedTabList';
import styles from './StarGiftCollectionList.module.scss';
type OwnProps = {
peerId: string;
className?: string;
};
type StateProps = {
collections?: ApiStarGiftCollection[];
activeCollectionId?: number;
animationLevel?: AnimationLevel;
};
const StarGiftCollectionList = ({
peerId,
className,
collections,
activeCollectionId,
animationLevel,
}: StateProps & OwnProps) => {
const { updateSelectedGiftCollection, resetSelectedGiftCollection } = getActions();
const lang = useLang();
const handleItemSelect = useLastCallback((itemId?: string) => {
if (itemId === 'all') {
resetSelectedGiftCollection({ peerId });
} else {
const collectionId = Number(itemId);
updateSelectedGiftCollection({ peerId, collectionId });
}
});
if (!collections || collections.length === 0) {
return undefined;
}
const items: TabItem[] = useMemo(() => [
{
id: 'all',
title: lang('AllGiftsCategory'),
},
...collections.map((collection) => ({
id: String(collection.collectionId),
title: collection.title,
sticker: collection.icon,
})),
], [collections, lang]);
const selectedItemId = activeCollectionId ? String(activeCollectionId) : 'all';
return (
<AnimatedTabList
items={items}
selectedItemId={selectedItemId}
animationLevel={animationLevel}
onItemSelect={handleItemSelect}
className={buildClassName(styles.tabList, className)}
/>
);
};
export default memo(withGlobal<OwnProps>(
(global, { peerId }): StateProps => {
const { starGiftCollections } = global;
const collections = starGiftCollections?.byPeerId?.[peerId];
const activeCollectionId = selectActiveCollectionId(global, peerId);
return {
collections,
activeCollectionId,
animationLevel: selectSharedSettings(global).animationLevel,
};
},
)(StarGiftCollectionList));

View File

@ -0,0 +1,3 @@
.tabList {
margin-block: 0.5rem;
}

View File

@ -0,0 +1,90 @@
import { memo, useMemo } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type { ApiStoryAlbum } from '../../../api/types';
import type { AnimationLevel } from '../../../types';
import type { TabItem } from '../../common/AnimatedTabList';
import { selectTabState } from '../../../global/selectors';
import { selectSharedSettings } from '../../../global/selectors/sharedState';
import buildClassName from '../../../util/buildClassName';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import AnimatedTabList from '../../common/AnimatedTabList';
import styles from './StoryAlbumList.module.scss';
type OwnProps = {
peerId: string;
className?: string;
};
type StateProps = {
albums?: ApiStoryAlbum[];
selectedAlbumId?: number;
animationLevel?: AnimationLevel;
};
const StoryAlbumList = ({
peerId,
className,
albums,
selectedAlbumId,
animationLevel,
}: StateProps & OwnProps) => {
const { selectStoryAlbum, resetSelectedStoryAlbum } = getActions();
const lang = useLang();
const handleItemSelect = useLastCallback((itemId: string) => {
if (itemId === 'all') {
resetSelectedStoryAlbum();
} else {
const albumId = Number(itemId);
selectStoryAlbum({ peerId, albumId });
}
});
if (!albums?.length) {
return undefined;
}
const items: TabItem[] = useMemo(() => [
{
id: 'all',
title: lang('AllStoriesCategory'),
},
...albums.map((album) => ({
id: String(album.albumId),
title: album.title,
})),
], [albums, lang]);
const selectedItemId = selectedAlbumId ? String(selectedAlbumId) : 'all';
return (
<AnimatedTabList
items={items}
selectedItemId={selectedItemId}
animationLevel={animationLevel}
onItemSelect={handleItemSelect}
className={buildClassName(styles.tabList, className)}
/>
);
};
export default memo(withGlobal<OwnProps>(
(global, { peerId }): StateProps => {
const { stories } = global;
const tabState = selectTabState(global);
const albums = stories?.albumsByPeerId?.[peerId];
const selectedAlbumId = tabState.selectedStoryAlbumId;
return {
albums,
selectedAlbumId,
animationLevel: selectSharedSettings(global).animationLevel,
};
},
)(StoryAlbumList));

View File

@ -29,8 +29,7 @@
&-slideOptimizedBackwards, &-slideOptimizedBackwards,
&-slideOptimizedRtl, &-slideOptimizedRtl,
&-slideOptimizedRtlBackwards { &-slideOptimizedRtlBackwards {
contain: strict; transform: translate3d(0, 0, 0);
#root & > .Transition_slide { #root & > .Transition_slide {
position: absolute; position: absolute;
top: 0; top: 0;

View File

@ -47,6 +47,8 @@ export type TransitionProps = {
onScroll?: NoneToVoidFunction; onScroll?: NoneToVoidFunction;
onMouseDown?: (e: React.MouseEvent<HTMLDivElement>) => void; onMouseDown?: (e: React.MouseEvent<HTMLDivElement>) => void;
children: React.ReactNode | ChildrenFn; children: React.ReactNode | ChildrenFn;
contentSelector?: string;
restoreHeightKey?: number;
}; };
const FALLBACK_ANIMATION_END = 1000; const FALLBACK_ANIMATION_END = 1000;
@ -89,6 +91,8 @@ function Transition({
onScroll, onScroll,
onMouseDown, onMouseDown,
children, children,
contentSelector,
restoreHeightKey,
}: TransitionProps) { }: TransitionProps) {
const currentKeyRef = useRef<number>(); const currentKeyRef = useRef<number>();
// No need for a container to update on change // No need for a container to update on change
@ -361,7 +365,10 @@ function Transition({
return; return;
} }
const { clientHeight, clientWidth } = activeElement || {}; const contentElement = contentSelector
? activeElement.querySelector<HTMLDivElement>(contentSelector) : activeElement;
const { clientHeight, clientWidth } = contentElement || activeElement || {};
if (!clientHeight || !clientWidth) { if (!clientHeight || !clientWidth) {
return; return;
} }
@ -373,7 +380,7 @@ function Transition({
flexBasis: `${clientHeight}px`, flexBasis: `${clientHeight}px`,
}); });
}); });
}, [shouldRestoreHeight, children]); }, [shouldRestoreHeight, children, restoreHeightKey, contentSelector]);
const asFastList = !renderCount; const asFastList = !renderCount;
const renders = rendersRef.current; const renders = rendersRef.current;

View File

@ -18,12 +18,14 @@ import {
appendStarsTransactions, appendStarsTransactions,
replacePeerSavedGifts, replacePeerSavedGifts,
updateChats, updateChats,
updatePeerStarGiftCollections,
updateStarsBalance, updateStarsBalance,
updateStarsSubscriptionLoading, updateStarsSubscriptionLoading,
updateUsers, updateUsers,
} from '../../reducers'; } from '../../reducers';
import { updateTabState } from '../../reducers/tabs'; import { updateTabState } from '../../reducers/tabs';
import { import {
selectActiveCollectionId,
selectGiftProfileFilter, selectGiftProfileFilter,
selectPeer, selectPeer,
selectPeerSavedGifts, selectPeerSavedGifts,
@ -301,17 +303,20 @@ addActionHandler('loadPeerSavedGifts', async (global, actions, payload): Promise
if (!shouldRefresh && currentGifts && !localNextOffset) return; // Already loaded all if (!shouldRefresh && currentGifts && !localNextOffset) return; // Already loaded all
const fetchingFilter = selectGiftProfileFilter(global, peerId, tabId); const fetchingFilter = selectGiftProfileFilter(global, peerId, tabId);
const fetchingCollectionId = selectActiveCollectionId(global, peerId, tabId);
const result = await callApi('fetchSavedStarGifts', { const result = await callApi('fetchSavedStarGifts', {
peer, peer,
offset: !shouldRefresh ? localNextOffset : '', offset: !shouldRefresh ? localNextOffset : '',
filter: fetchingFilter, filter: fetchingFilter,
collectionId: fetchingCollectionId ? Number(fetchingCollectionId) : undefined,
}); });
global = getGlobal(); global = getGlobal();
const currentFilter = selectGiftProfileFilter(global, peerId, tabId); const currentFilter = selectGiftProfileFilter(global, peerId, tabId);
const currentCollectionId = selectActiveCollectionId(global, peerId, tabId);
if (!result || currentFilter !== fetchingFilter) { if (!result || currentCollectionId !== fetchingCollectionId || currentFilter !== fetchingFilter) {
return; return;
} }
@ -395,7 +400,8 @@ addActionHandler('changeGiftVisibility', async (global, actions, payload): Promi
const requestInputGift = getRequestInputSavedStarGift(global, gift); const requestInputGift = getRequestInputSavedStarGift(global, gift);
if (!requestInputGift) return; if (!requestInputGift) return;
const oldGifts = selectTabState(global, tabId).savedGifts.giftsByPeerId[peerId]; const activeCollectionId = selectActiveCollectionId(global, peerId, tabId) || 'all';
const oldGifts = selectTabState(global, tabId).savedGifts.collectionsByPeerId[peerId]?.[activeCollectionId];
if (oldGifts?.gifts?.length) { if (oldGifts?.gifts?.length) {
const newGifts = oldGifts.gifts.map((g) => { const newGifts = oldGifts.gifts.map((g) => {
if (g.inputGift && areInputSavedGiftsEqual(g.inputGift, gift)) { if (g.inputGift && areInputSavedGiftsEqual(g.inputGift, gift)) {
@ -530,3 +536,25 @@ addActionHandler('updateStarGiftPrice', async (global, actions, payload): Promis
actions.reloadPeerSavedGifts({ peerId: global.currentUserId! }); actions.reloadPeerSavedGifts({ peerId: global.currentUserId! });
}); });
addActionHandler('loadStarGiftCollections', async (global, actions, payload): Promise<void> => {
const {
peerId,
hash,
} = payload;
const peer = selectPeer(global, peerId);
if (!peer) return;
const result = await callApi('fetchStarGiftCollections', {
peer,
hash,
});
if (!result) return;
global = getGlobal();
global = updatePeerStarGiftCollections(global, peerId, result.collections);
setGlobal(global);
});

View File

@ -301,13 +301,43 @@ addActionHandler('loadPeerStories', async (global, actions, payload): Promise<vo
addActionHandler('loadPeerProfileStories', async (global, actions, payload): Promise<void> => { addActionHandler('loadPeerProfileStories', async (global, actions, payload): Promise<void> => {
if (selectIsCurrentUserFrozen(global)) return; if (selectIsCurrentUserFrozen(global)) return;
const { peerId, offsetId } = payload; const { peerId, offsetId, tabId = getCurrentTabId() } = payload;
const peer = selectPeer(global, peerId); const peer = selectPeer(global, peerId);
let peerStories = selectPeerStories(global, peerId); let peerStories = selectPeerStories(global, peerId);
if (!peer || peerStories?.isFullyLoaded) { if (!peer || peerStories?.isFullyLoaded) {
return; return;
} }
const tabState = selectTabState(global, tabId);
const selectedAlbumId = tabState.selectedStoryAlbumId;
if (selectedAlbumId) {
let albumData = peerStories?.idsByAlbumId?.[selectedAlbumId];
if (albumData?.isFullyLoaded) {
return;
}
const result = await callApi('fetchAlbumStories', {
peer,
albumId: selectedAlbumId,
offset: offsetId || 0,
});
if (!result) {
return;
}
global = getGlobal();
global = addStoriesForPeer(global, peerId, result.stories, result.pinnedIds, false, selectedAlbumId);
peerStories = selectPeerStories(global, peerId);
albumData = peerStories?.idsByAlbumId?.[selectedAlbumId];
if (Object.values(result.stories).length === 0
|| (albumData?.ids?.length && albumData.ids.length >= result.count)) {
global = updatePeerStoriesFullyLoaded(global, peerId, true, false, selectedAlbumId);
}
setGlobal(global);
return;
}
const result = await callApi('fetchPeerProfileStories', { peer, offsetId }); const result = await callApi('fetchPeerProfileStories', { peer, offsetId });
if (!result) { if (!result) {
return; return;
@ -619,3 +649,66 @@ addActionHandler('activateStealthMode', (global, actions, payload): ActionReturn
callApi('activateStealthMode', { isForPast: isForPast || true, isForFuture: isForFuture || true }); callApi('activateStealthMode', { isForPast: isForPast || true, isForFuture: isForFuture || true });
}); });
addActionHandler('loadStoryAlbums', async (global, actions, payload): Promise<void> => {
const { peerId } = payload;
const peer = selectPeer(global, peerId);
if (!peer) return;
const albums = await callApi('fetchAlbums', { peer });
if (!albums) return;
global = getGlobal();
global = {
...global,
stories: {
...global.stories,
albumsByPeerId: {
...global.stories.albumsByPeerId,
[peerId]: albums,
},
},
};
setGlobal(global);
});
addActionHandler('selectStoryAlbum', (global, actions, payload): ActionReturnType => {
const { peerId, albumId, tabId = getCurrentTabId() } = payload;
if (albumId && peerId) {
global = updatePeerStoriesFullyLoaded(global, peerId, false);
}
global = updateTabState(global, {
selectedStoryAlbumId: albumId || undefined,
}, tabId);
setGlobal(global);
actions.loadPeerProfileStories({ peerId, tabId });
});
addActionHandler('loadAlbumStories', async (global, actions, payload): Promise<void> => {
const { peerId, albumId, offsetId } = payload;
const peer = selectPeer(global, peerId);
if (!peer) return;
const result = await callApi('fetchAlbumStories', {
peer,
albumId,
offset: offsetId || 0,
});
if (!result) return;
global = getGlobal();
global = addStoriesForPeer(global, peerId, result.stories, result.pinnedIds);
setGlobal(global);
});
addActionHandler('resetSelectedStoryAlbum', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
selectedStoryAlbumId: undefined,
}, tabId);
});

View File

@ -1,6 +1,8 @@
import type { ApiSavedGifts } from '../../../api/types';
import type { ActionReturnType } from '../../types'; import type { ActionReturnType } from '../../types';
import { DEFAULT_GIFT_PROFILE_FILTER_OPTIONS } from '../../../config'; import { DEFAULT_GIFT_PROFILE_FILTER_OPTIONS } from '../../../config';
import { selectActiveCollectionId } from '../../../global/selectors';
import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { addActionHandler, setGlobal } from '../../index'; import { addActionHandler, setGlobal } from '../../index';
import { import {
@ -98,11 +100,15 @@ addActionHandler('updateGiftProfileFilter', (global, actions, payload): ActionRe
}; };
} }
const activeCollectionId = selectActiveCollectionId(global, peerId, tabId) || 'all';
global = updateTabState(global, { global = updateTabState(global, {
savedGifts: { savedGifts: {
...tabState.savedGifts, ...tabState.savedGifts,
giftsByPeerId: { collectionsByPeerId: {
[peerId]: tabState.savedGifts.giftsByPeerId[peerId], [peerId]: {
[activeCollectionId]: tabState.savedGifts.collectionsByPeerId[peerId]?.[activeCollectionId],
} as Record<number | 'all', ApiSavedGifts>,
}, },
filter: updatedFilter, filter: updatedFilter,
}, },
@ -118,11 +124,15 @@ addActionHandler('resetGiftProfileFilter', (global, actions, payload): ActionRet
const { peerId, tabId = getCurrentTabId() } = payload || {}; const { peerId, tabId = getCurrentTabId() } = payload || {};
const tabState = selectTabState(global, tabId); const tabState = selectTabState(global, tabId);
const activeCollectionId = selectActiveCollectionId(global, peerId, tabId) || 'all';
global = updateTabState(global, { global = updateTabState(global, {
savedGifts: { savedGifts: {
...tabState.savedGifts, ...tabState.savedGifts,
giftsByPeerId: { collectionsByPeerId: {
[peerId]: tabState.savedGifts.giftsByPeerId[peerId], [peerId]: {
[activeCollectionId]: tabState.savedGifts.collectionsByPeerId[peerId]?.[activeCollectionId],
} as Record<number | 'all', ApiSavedGifts>,
}, },
filter: { filter: {
...DEFAULT_GIFT_PROFILE_FILTER_OPTIONS, ...DEFAULT_GIFT_PROFILE_FILTER_OPTIONS,

View File

@ -6,7 +6,7 @@ import { getCurrentTabId } from '../../../util/establishMultitabRole';
import * as langProvider from '../../../util/oldLangProvider'; import * as langProvider from '../../../util/oldLangProvider';
import { addTabStateResetterAction } from '../../helpers/meta'; import { addTabStateResetterAction } from '../../helpers/meta';
import { getPrizeStarsTransactionFromGiveaway, getStarsTransactionFromGift } from '../../helpers/payments'; import { getPrizeStarsTransactionFromGiveaway, getStarsTransactionFromGift } from '../../helpers/payments';
import { addActionHandler } from '../../index'; import { addActionHandler, setGlobal } from '../../index';
import { import {
clearStarPayment, openStarsTransactionModal, clearStarPayment, openStarsTransactionModal,
} from '../../reducers'; } from '../../reducers';
@ -344,3 +344,43 @@ addActionHandler('openGiftTransferModal', (global, actions, payload): ActionRetu
}); });
addTabStateResetterAction('closeGiftTransferModal', 'giftTransferModal'); addTabStateResetterAction('closeGiftTransferModal', 'giftTransferModal');
addActionHandler('updateSelectedGiftCollection', (global, actions, payload): ActionReturnType => {
const { peerId, collectionId, tabId = getCurrentTabId() } = payload;
const tabState = selectTabState(global, tabId);
global = updateTabState(global, {
savedGifts: {
...tabState.savedGifts,
activeCollectionByPeerId: {
...tabState.savedGifts.activeCollectionByPeerId,
[peerId]: collectionId,
},
},
}, tabId);
setGlobal(global);
actions.loadPeerSavedGifts({
peerId, shouldRefresh: true, tabId: tabState.id,
});
});
addActionHandler('resetSelectedGiftCollection', (global, actions, payload): ActionReturnType => {
const { peerId, tabId = getCurrentTabId() } = payload;
const tabState = selectTabState(global, tabId);
global = updateTabState(global, {
savedGifts: {
...tabState.savedGifts,
activeCollectionByPeerId: {
...tabState.savedGifts.activeCollectionByPeerId,
[peerId]: undefined,
},
},
}, tabId);
setGlobal(global);
actions.loadPeerSavedGifts({
peerId, shouldRefresh: true, tabId: tabState.id,
});
});

View File

@ -165,6 +165,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
stories: { stories: {
byPeerId: {}, byPeerId: {},
albumsByPeerId: {},
orderedPeerIds: { orderedPeerIds: {
archived: [], archived: [],
active: [], active: [],
@ -393,7 +394,8 @@ export const INITIAL_TAB_STATE: TabState = {
filter: { filter: {
...DEFAULT_GIFT_PROFILE_FILTER_OPTIONS, ...DEFAULT_GIFT_PROFILE_FILTER_OPTIONS,
}, },
giftsByPeerId: {}, collectionsByPeerId: {},
activeCollectionByPeerId: {},
}, },
resaleGifts: { resaleGifts: {

View File

@ -55,9 +55,10 @@ export function addStoriesForPeer<T extends GlobalState>(
newStories: Record<number, ApiTypeStory>, newStories: Record<number, ApiTypeStory>,
newPinnedIds?: number[], newPinnedIds?: number[],
addToArchive?: boolean, addToArchive?: boolean,
albumId?: number,
): T { ): T {
const { const {
byId, orderedIds, profileIds, archiveIds, pinnedIds, byId, orderedIds, profileIds, archiveIds, pinnedIds, idsByAlbumId,
} = global.stories.byPeerId[peerId] || {}; } = global.stories.byPeerId[peerId] || {};
const deletedIds = Object.keys(newStories).filter((id) => 'isDeleted' in newStories[Number(id)]).map(Number); const deletedIds = Object.keys(newStories).filter((id) => 'isDeleted' in newStories[Number(id)]).map(Number);
const updatedById = { ...byId, ...newStories }; const updatedById = { ...byId, ...newStories };
@ -65,7 +66,7 @@ export function addStoriesForPeer<T extends GlobalState>(
let updatedArchiveIds = [...(archiveIds || [])]; let updatedArchiveIds = [...(archiveIds || [])];
const updatedProfileIds = unique( const updatedProfileIds = unique(
[...(profileIds || [])].concat(Object.values(newStories).reduce((ids, story) => { [...(profileIds || [])].concat(Object.values(newStories).reduce((ids, story) => {
if ('isInProfile' in story && story.isInProfile) { if (('isInProfile' in story && story.isInProfile)) {
ids.push(story.id); ids.push(story.id);
} }
@ -87,6 +88,20 @@ export function addStoriesForPeer<T extends GlobalState>(
.filter((storyId) => !deletedIds.includes(storyId)); .filter((storyId) => !deletedIds.includes(storyId));
} }
const updatedIdsByAlbumId = { ...(idsByAlbumId || {}) };
if (albumId !== undefined) {
const newAlbumStoryIds = Object.keys(newStories).map(Number)
.filter((storyId) => !deletedIds.includes(storyId));
const existingAlbumData = updatedIdsByAlbumId[albumId];
const existingIds = existingAlbumData?.ids || [];
updatedIdsByAlbumId[albumId] = {
...updatedIdsByAlbumId[albumId],
ids: unique([...existingIds, ...newAlbumStoryIds]).sort((a, b) => b - a),
};
}
global = { global = {
...global, ...global,
stories: { stories: {
@ -100,6 +115,7 @@ export function addStoriesForPeer<T extends GlobalState>(
profileIds: updatedProfileIds, profileIds: updatedProfileIds,
pinnedIds: pinnedIds || newPinnedIds, pinnedIds: pinnedIds || newPinnedIds,
...(addToArchive && { archiveIds: updatedArchiveIds }), ...(addToArchive && { archiveIds: updatedArchiveIds }),
...(albumId !== undefined && { idsByAlbumId: updatedIdsByAlbumId }),
}, },
}, },
}, },
@ -137,7 +153,33 @@ export function updatePeerStoriesFullyLoaded<T extends GlobalState>(
peerId: string, peerId: string,
isFullyLoaded: boolean, isFullyLoaded: boolean,
isArchive?: boolean, isArchive?: boolean,
albumId?: number,
): T { ): T {
const { byPeerId } = global.stories;
const peerStories = byPeerId[peerId];
if (albumId !== undefined && peerStories?.idsByAlbumId?.[albumId]) {
return {
...global,
stories: {
...global.stories,
byPeerId: {
...byPeerId,
[peerId]: {
...peerStories,
idsByAlbumId: {
...peerStories.idsByAlbumId,
[albumId]: {
...peerStories.idsByAlbumId[albumId],
isFullyLoaded,
},
},
},
},
},
};
}
return { return {
...global, ...global,
stories: { stories: {

View File

@ -1,6 +1,7 @@
import type { import type {
ApiMissingInvitedUser, ApiMissingInvitedUser,
ApiSavedStarGift, ApiSavedStarGift,
ApiStarGiftCollection,
ApiUser, ApiUser,
ApiUserCommonChats, ApiUserCommonChats,
ApiUserFullInfo, ApiUserFullInfo,
@ -14,6 +15,7 @@ import { getCurrentTabId } from '../../util/establishMultitabRole';
import { omit, omitUndefined, unique } from '../../util/iteratees'; import { omit, omitUndefined, unique } from '../../util/iteratees';
import { MEMO_EMPTY_ARRAY } from '../../util/memo'; import { MEMO_EMPTY_ARRAY } from '../../util/memo';
import { getSavedGiftKey } from '../helpers/stars'; import { getSavedGiftKey } from '../helpers/stars';
import { selectActiveCollectionId } from '../selectors';
import { selectTabState } from '../selectors'; import { selectTabState } from '../selectors';
import { updateTabState } from './tabs'; import { updateTabState } from './tabs';
@ -344,16 +346,38 @@ export function replacePeerSavedGifts<T extends GlobalState>(
keyCounts.set(id, count + 1); keyCounts.set(id, count + 1);
}); });
const activeCollectionId = selectActiveCollectionId(global, peerId, tabId) || 'all';
return updateTabState(global, { return updateTabState(global, {
savedGifts: { savedGifts: {
...tabState.savedGifts, ...tabState.savedGifts,
giftsByPeerId: { collectionsByPeerId: {
...tabState.savedGifts.giftsByPeerId, ...tabState.savedGifts.collectionsByPeerId,
[peerId]: { [peerId]: {
gifts, ...tabState.savedGifts.collectionsByPeerId[peerId],
nextOffset, [activeCollectionId]: {
gifts,
nextOffset,
},
}, },
}, },
}, },
}, tabId); }, tabId);
} }
export function updatePeerStarGiftCollections<T extends GlobalState>(
global: T,
peerId: string,
collections: ApiStarGiftCollection[],
): T {
return {
...global,
starGiftCollections: {
...global.starGiftCollections,
byPeerId: {
...global.starGiftCollections?.byPeerId,
[peerId]: collections,
},
},
};
}

View File

@ -96,3 +96,11 @@ export function selectIsGiftProfileFilterDefault<T extends GlobalState>(
) { ) {
return arePropsShallowEqual(selectTabState(global, tabId).savedGifts.filter, DEFAULT_GIFT_PROFILE_FILTER_OPTIONS); return arePropsShallowEqual(selectTabState(global, tabId).savedGifts.filter, DEFAULT_GIFT_PROFILE_FILTER_OPTIONS);
} }
export function selectActiveCollectionId<T extends GlobalState>(
global: T,
peerId: string,
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
return selectTabState(global, tabId).savedGifts.activeCollectionByPeerId?.[peerId];
}

View File

@ -1,4 +1,4 @@
import type { ApiPeer, ApiSavedGifts } from '../../api/types'; import type { ApiPeer, ApiSavedGifts, ApiStarGiftCollection } from '../../api/types';
import type { GlobalState, TabArgs } from '../types'; import type { GlobalState, TabArgs } from '../types';
import { SERVICE_NOTIFICATIONS_USER_ID } from '../../config'; import { SERVICE_NOTIFICATIONS_USER_ID } from '../../config';
@ -32,8 +32,17 @@ export function selectPeerSavedGifts<T extends GlobalState>(
global: T, global: T,
peerId: string, peerId: string,
...[tabId = getCurrentTabId()]: TabArgs<T> ...[tabId = getCurrentTabId()]: TabArgs<T>
): ApiSavedGifts { ): ApiSavedGifts | undefined {
return selectTabState(global, tabId).savedGifts.giftsByPeerId[peerId]; const tabState = selectTabState(global, tabId);
const activeCollectionId = tabState.savedGifts.activeCollectionByPeerId[peerId] || 'all';
return tabState.savedGifts.collectionsByPeerId[peerId]?.[activeCollectionId];
}
export function selectPeerStarGiftCollections<T extends GlobalState>(
global: T,
peerId: string,
): ApiStarGiftCollection[] | undefined {
return global.starGiftCollections?.byPeerId[peerId];
} }
export function selectPeerPaidMessagesStars<T extends GlobalState>( export function selectPeerPaidMessagesStars<T extends GlobalState>(

View File

@ -1691,6 +1691,19 @@ export interface ActionPayloads {
isForPast?: boolean; isForPast?: boolean;
isForFuture?: boolean; isForFuture?: boolean;
} | undefined; } | undefined;
loadStoryAlbums: {
peerId: string;
};
selectStoryAlbum: {
peerId: string;
albumId?: number;
} & WithTabId;
loadAlbumStories: {
peerId: string;
albumId: number;
offsetId?: number;
};
resetSelectedStoryAlbum: WithTabId | undefined;
openBoostModal: { openBoostModal: {
chatId: string; chatId: string;
@ -2596,6 +2609,13 @@ export interface ActionPayloads {
recipientId: string; recipientId: string;
} & WithTabId; } & WithTabId;
closeGiftTransferModal: WithTabId | undefined; closeGiftTransferModal: WithTabId | undefined;
updateSelectedGiftCollection: {
peerId: string;
collectionId: number;
} & WithTabId;
resetSelectedGiftCollection: {
peerId: string;
} & WithTabId;
loadPeerSavedGifts: { loadPeerSavedGifts: {
peerId: string; peerId: string;
@ -2621,6 +2641,11 @@ export interface ActionPayloads {
price: ApiTypeCurrencyAmount; price: ApiTypeCurrencyAmount;
} & WithTabId; } & WithTabId;
loadStarGiftCollections: {
peerId: string;
hash?: string;
} & WithTabId;
openStarsGiftModal: ({ openStarsGiftModal: ({
chatId?: string; chatId?: string;
forUserId?: string; forUserId?: string;

View File

@ -30,12 +30,14 @@ import type {
ApiSavedReactionTag, ApiSavedReactionTag,
ApiSession, ApiSession,
ApiSponsoredMessage, ApiSponsoredMessage,
ApiStarGiftCollection,
ApiStarGiftRegular, ApiStarGiftRegular,
ApiStarsAmount, ApiStarsAmount,
ApiStarTopupOption, ApiStarTopupOption,
ApiStealthMode, ApiStealthMode,
ApiSticker, ApiSticker,
ApiStickerSet, ApiStickerSet,
ApiStoryAlbum,
ApiTimezone, ApiTimezone,
ApiTonAmount, ApiTonAmount,
ApiTranscription, ApiTranscription,
@ -253,6 +255,7 @@ export type GlobalState = {
archived: string[]; archived: string[];
}; };
stealthMode: ApiStealthMode; stealthMode: ApiStealthMode;
albumsByPeerId: Record<string, ApiStoryAlbum[]>;
}; };
groupCalls: { groupCalls: {
@ -307,6 +310,9 @@ export type GlobalState = {
byId: Record<string, ApiStarGiftRegular>; byId: Record<string, ApiStarGiftRegular>;
idsByCategory: Record<StarGiftCategory, string[]>; idsByCategory: Record<StarGiftCategory, string[]>;
}; };
starGiftCollections?: {
byPeerId: Record<string, ApiStarGiftCollection[]>;
};
stickers: { stickers: {
setsById: Record<string, ApiStickerSet>; setsById: Record<string, ApiStickerSet>;

View File

@ -218,7 +218,8 @@ export type TabState = {
}; };
savedGifts: { savedGifts: {
giftsByPeerId: Record<string, ApiSavedGifts>; collectionsByPeerId: Record<string, Record<number | 'all', ApiSavedGifts>>;
activeCollectionByPeerId: Record<string, number | undefined>;
filter: GiftProfileFilterOptions; filter: GiftProfileFilterOptions;
}; };
@ -342,6 +343,8 @@ export type TabState = {
}; };
}; };
selectedStoryAlbumId?: number;
mediaViewer: { mediaViewer: {
chatId?: string; chatId?: string;
threadId?: ThreadId; threadId?: ThreadId;

View File

@ -1796,6 +1796,7 @@ payments.getStarGiftWithdrawalUrl#d06e93a8 stargift:InputSavedStarGift password:
payments.toggleStarGiftsPinnedToTop#1513e7b0 peer:InputPeer stargift:Vector<InputSavedStarGift> = Bool; payments.toggleStarGiftsPinnedToTop#1513e7b0 peer:InputPeer stargift:Vector<InputSavedStarGift> = Bool;
payments.getResaleStarGifts#7a5fa236 flags:# sort_by_price:flags.1?true sort_by_num:flags.2?true attributes_hash:flags.0?long gift_id:long attributes:flags.3?Vector<StarGiftAttributeId> offset:string limit:int = payments.ResaleStarGifts; payments.getResaleStarGifts#7a5fa236 flags:# sort_by_price:flags.1?true sort_by_num:flags.2?true attributes_hash:flags.0?long gift_id:long attributes:flags.3?Vector<StarGiftAttributeId> offset:string limit:int = payments.ResaleStarGifts;
payments.updateStarGiftPrice#edbe6ccb stargift:InputSavedStarGift resell_amount:StarsAmount = Updates; payments.updateStarGiftPrice#edbe6ccb stargift:InputSavedStarGift resell_amount:StarsAmount = Updates;
payments.getStarGiftCollections#981b91dd peer:InputPeer hash:long = payments.StarGiftCollections;
phone.requestCall#42ff96ed flags:# video:flags.0?true user_id:InputUser random_id:int g_a_hash:bytes protocol:PhoneCallProtocol = phone.PhoneCall; phone.requestCall#42ff96ed flags:# video:flags.0?true user_id:InputUser random_id:int g_a_hash:bytes protocol:PhoneCallProtocol = phone.PhoneCall;
phone.acceptCall#3bd2b4a0 peer:InputPhoneCall g_b:bytes protocol:PhoneCallProtocol = phone.PhoneCall; phone.acceptCall#3bd2b4a0 peer:InputPhoneCall g_b:bytes protocol:PhoneCallProtocol = phone.PhoneCall;
phone.confirmCall#2efe1722 peer:InputPhoneCall g_a:bytes key_fingerprint:long protocol:PhoneCallProtocol = phone.PhoneCall; phone.confirmCall#2efe1722 peer:InputPhoneCall g_a:bytes key_fingerprint:long protocol:PhoneCallProtocol = phone.PhoneCall;
@ -1855,6 +1856,8 @@ stories.getPeerStories#2c4ada50 peer:InputPeer = stories.PeerStories;
stories.getPeerMaxIDs#535983c3 id:Vector<InputPeer> = Vector<int>; stories.getPeerMaxIDs#535983c3 id:Vector<InputPeer> = Vector<int>;
stories.togglePeerStoriesHidden#bd0415c4 peer:InputPeer hidden:Bool = Bool; stories.togglePeerStoriesHidden#bd0415c4 peer:InputPeer hidden:Bool = Bool;
stories.togglePinnedToTop#b297e9b peer:InputPeer id:Vector<int> = Bool; stories.togglePinnedToTop#b297e9b peer:InputPeer id:Vector<int> = Bool;
stories.getAlbums#25b3eac7 peer:InputPeer hash:long = stories.Albums;
stories.getAlbumStories#ac806d61 peer:InputPeer album_id:int offset:int limit:int = stories.Stories;
premium.getBoostsList#60f67660 flags:# gifts:flags.0?true peer:InputPeer offset:string limit:int = premium.BoostsList; premium.getBoostsList#60f67660 flags:# gifts:flags.0?true peer:InputPeer offset:string limit:int = premium.BoostsList;
premium.getMyBoosts#be77b4a = premium.MyBoosts; premium.getMyBoosts#be77b4a = premium.MyBoosts;
premium.applyBoost#6b7da746 flags:# slots:flags.0?Vector<int> peer:InputPeer = premium.MyBoosts; premium.applyBoost#6b7da746 flags:# slots:flags.0?Vector<int> peer:InputPeer = premium.MyBoosts;

View File

@ -330,6 +330,7 @@
"payments.toggleStarGiftsPinnedToTop", "payments.toggleStarGiftsPinnedToTop",
"payments.getResaleStarGifts", "payments.getResaleStarGifts",
"payments.updateStarGiftPrice", "payments.updateStarGiftPrice",
"payments.getStarGiftCollections",
"langpack.getLangPack", "langpack.getLangPack",
"langpack.getStrings", "langpack.getStrings",
"langpack.getLanguages", "langpack.getLanguages",
@ -404,6 +405,8 @@
"stories.getPeerStories", "stories.getPeerStories",
"stories.getStoriesViews", "stories.getStoriesViews",
"stories.togglePinnedToTop", "stories.togglePinnedToTop",
"stories.getAlbums",
"stories.getAlbumStories",
"premium.getBoostsStatus", "premium.getBoostsStatus",
"premium.getBoostersList", "premium.getBoostersList",
"premium.applyBoost", "premium.applyBoost",

View File

@ -1666,6 +1666,7 @@ export interface LangPair {
'PublicPostsPremiumFeatureSubtitle': undefined; 'PublicPostsPremiumFeatureSubtitle': undefined;
'PublicPostsSubscribeToPremium': undefined; 'PublicPostsSubscribeToPremium': undefined;
'PostsSearchTransaction': undefined; 'PostsSearchTransaction': undefined;
'AllStoriesCategory': undefined;
'TitleRating': undefined; 'TitleRating': undefined;
'RatingYourReflectsActivity': undefined; 'RatingYourReflectsActivity': undefined;
'RatingGiftsFromTelegram': undefined; 'RatingGiftsFromTelegram': undefined;