From 1419d396d3193bec63aa47f1399742c72baaf1b9 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Thu, 13 Feb 2025 14:27:50 +0100 Subject: [PATCH] Gifts: Support filter in profile (#5533) --- src/api/gramjs/methods/payments.ts | 21 +- src/assets/localization/fallback.strings | 9 + src/assets/tgs/SearchingDuck.tgs | Bin 0 -> 11826 bytes .../common/helpers/animatedAssets.ts | 2 + src/components/right/Profile.scss | 30 +++ src/components/right/Profile.tsx | 46 +++- src/components/right/RightHeader.tsx | 243 ++++++++++++++---- src/components/right/hooks/useProfileState.ts | 2 + src/components/ui/DropdownMenu.tsx | 4 +- src/config.ts | 12 + src/global/actions/api/stars.ts | 33 ++- src/global/actions/ui/payments.ts | 53 ++++ src/global/initialState.ts | 9 +- src/global/reducers/users.ts | 18 +- src/global/selectors/payments.ts | 35 +++ src/global/selectors/peers.ts | 14 +- src/global/types/actions.ts | 10 +- src/global/types/globalState.ts | 2 - src/global/types/tabState.ts | 7 + src/types/index.ts | 10 + src/types/language.d.ts | 9 + 21 files changed, 485 insertions(+), 84 deletions(-) create mode 100644 src/assets/tgs/SearchingDuck.tgs diff --git a/src/api/gramjs/methods/payments.ts b/src/api/gramjs/methods/payments.ts index b265eeb86..f65b5a7df 100644 --- a/src/api/gramjs/methods/payments.ts +++ b/src/api/gramjs/methods/payments.ts @@ -1,6 +1,9 @@ import BigInt from 'big-integer'; import { Api as GramJs } from '../../../lib/gramjs'; +import type { + GiftProfileFilterOptions, +} from '../../../types'; import type { ApiChat, ApiInputStorePaymentPurpose, @@ -451,16 +454,30 @@ export async function fetchSavedStarGifts({ peer, offset = '', limit, + filter, }: { peer: ApiPeer; offset?: string; limit?: number; + filter?: GiftProfileFilterOptions; }) { - const result = await invokeRequest(new GramJs.payments.GetSavedStarGifts({ + type GetSavedStarGiftsParams = ConstructorParameters[0]; + + const params : GetSavedStarGiftsParams = { peer: buildInputPeer(peer.id, peer.accessHash), offset, limit, - })); + ...(filter && { + sortByValue: filter.sortType === 'byValue' || undefined, + excludeUnlimited: !filter.shouldIncludeUnlimited || undefined, + excludeLimited: !filter.shouldIncludeLimited || undefined, + excludeUnique: !filter.shouldIncludeUnique || undefined, + excludeSaved: !filter.shouldIncludeDisplayed || undefined, + excludeUnsaved: !filter.shouldIncludeHidden || undefined, + } satisfies GetSavedStarGiftsParams), + }; + + const result = await invokeRequest(new GramJs.payments.GetSavedStarGifts(params)); if (!result) { return undefined; diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 403dbdfe7..ff57aecc7 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -1599,6 +1599,15 @@ "ViewButtonGiftUnique" = "VIEW COLLECTIBLE"; "AuthContinueOnThisLanguage" = "Continue in English"; "Share" = "Share"; +"GiftSortByDate" = "Sort by Date"; +"GiftSortByValue" = "Sort by Value"; +"GiftFilterUnlimited" = "Unlimited"; +"GiftFilterLimited" = "Limited"; +"GiftFilterUnique" = "Unique"; +"GiftFilterDisplayed" = "Displayed"; +"GiftFilterHidden" = "Hidden"; +"GiftSearchEmpty" = "No matching gifts"; +"GiftSearchReset" = "View All Gifts"; "CheckPasswordTitle" = "Enter Password"; "CheckPasswordPlaceholder" = "Password"; "CheckPasswordDescription" = "Please enter your password to continue."; diff --git a/src/assets/tgs/SearchingDuck.tgs b/src/assets/tgs/SearchingDuck.tgs new file mode 100644 index 0000000000000000000000000000000000000000..1cde51426d640fc5ed44f94df27a589eacc73062 GIT binary patch literal 11826 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 literal 0 HcmV?d00001 diff --git a/src/components/common/helpers/animatedAssets.ts b/src/components/common/helpers/animatedAssets.ts index 1642b2aa0..d368892db 100644 --- a/src/components/common/helpers/animatedAssets.ts +++ b/src/components/common/helpers/animatedAssets.ts @@ -20,6 +20,7 @@ import MonkeyPeek from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyPeek.tgs import MonkeyTracking from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyTracking.tgs'; import ReadTime from '../../../assets/tgs/ReadTime.tgs'; import Report from '../../../assets/tgs/Report.tgs'; +import SearchingDuck from '../../../assets/tgs/SearchingDuck.tgs'; import Congratulations from '../../../assets/tgs/settings/Congratulations.tgs'; import DiscussionGroups from '../../../assets/tgs/settings/DiscussionGroupsDucks.tgs'; import Experimental from '../../../assets/tgs/settings/Experimental.tgs'; @@ -64,4 +65,5 @@ export const LOCAL_TGS_URLS = { StarReactionEffect, StarReaction, Report, + SearchingDuck, }; diff --git a/src/components/right/Profile.scss b/src/components/right/Profile.scss index 50e79ba2b..6f00bf5fc 100644 --- a/src/components/right/Profile.scss +++ b/src/components/right/Profile.scss @@ -46,6 +46,36 @@ } } +.nothing-found-gifts { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + padding-top: 5rem; + height: 100%; + + .description { + color: var(--color-text-secondary); + font-size: 1rem; + font-weight: var(--font-weight-medium); + text-align: center; + margin-block: 1rem; + unicode-bidi: plaintext; + } + + .Link { + color: var(--color-links); + font-weight: var(--font-weight-medium); + transition: opacity 0.15s ease-in; + + &:active, + &:hover { + text-decoration: none; + opacity: 0.85; + } + } +} + .shared-media { display: flex; flex-direction: column-reverse; diff --git a/src/components/right/Profile.tsx b/src/components/right/Profile.tsx index 5669b4556..f2828f228 100644 --- a/src/components/right/Profile.tsx +++ b/src/components/right/Profile.tsx @@ -49,6 +49,7 @@ import { selectChatMessages, selectCurrentSharedMediaSearch, selectIsCurrentUserPremium, + selectIsGiftProfileFilterDefault, selectIsRightColumnShown, selectPeerStories, selectSimilarBotsIds, @@ -63,6 +64,7 @@ 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 { LOCAL_TGS_URLS } from '../common/helpers/animatedAssets'; import renderText from '../common/helpers/renderText'; import { getSenderName } from '../left/search/helpers/getSenderName'; @@ -79,6 +81,7 @@ import useProfileState from './hooks/useProfileState'; import useProfileViewportIds from './hooks/useProfileViewportIds'; import useTransitionFixes from './hooks/useTransitionFixes'; +import AnimatedIconWithPreview from '../common/AnimatedIconWithPreview'; import Audio from '../common/Audio'; import Document from '../common/Document'; import SavedGift from '../common/gift/SavedGift'; @@ -96,6 +99,7 @@ import MediaStory from '../story/MediaStory'; import Button from '../ui/Button'; import FloatingActionButton from '../ui/FloatingActionButton'; import InfiniteScroll from '../ui/InfiniteScroll'; +import Link from '../ui/Link'; import ListItem, { type MenuItemContextAction } from '../ui/ListItem'; import Spinner from '../ui/Spinner'; import TabList from '../ui/TabList'; @@ -155,6 +159,7 @@ type StateProps = { isSavedDialog?: boolean; forceScrollProfileTab?: boolean; isSynced?: boolean; + isNotDefaultGiftFilter?: boolean; }; type TabProps = { @@ -219,6 +224,7 @@ const Profile: FC = ({ forceScrollProfileTab, isSynced, onProfileStateChange, + isNotDefaultGiftFilter, }) => { const { setSharedMediaSearchType, @@ -237,6 +243,7 @@ const Profile: FC = ({ loadBotRecommendations, loadPreviewMedias, loadPeerSavedGifts, + resetGiftProfileFilter, } = getActions(); // eslint-disable-next-line no-null/no-null @@ -482,6 +489,10 @@ const Profile: FC = ({ setActiveTab(Math.min(newActiveTab, tabs.length - 1)); }, [hasMembersTab, activeTab, tabs]); + const handleResetGiftsFilter = useLastCallback(() => { + resetGiftProfileFilter(); + }); + useEffect(() => { if (!transitionRef.current || !IS_TOUCH_ENV) { return undefined; @@ -523,6 +534,28 @@ const Profile: FC = ({ }]; } + function renderNothingFoundGiftsWithFilter() { + return ( +
+ +
+ {lang('GiftSearchEmpty')} +
+ + {lang('GiftSearchReset')} + +
+ ); + } + function renderContent() { if (resultType === 'dialogs') { return ( @@ -535,7 +568,9 @@ const Profile: FC = ({ const forceRenderHiddenMembers = Boolean(resultType === 'members' && areMembersHidden); return ( -
+
{!noSpinner && !forceRenderHiddenMembers && } {forceRenderHiddenMembers && }
@@ -545,6 +580,10 @@ const Profile: FC = ({ if (viewportIds && !viewportIds?.length) { let text: string; + if (resultType === 'gifts' && isNotDefaultGiftFilter) { + return renderNothingFoundGiftsWithFilter(); + } + switch (resultType) { case 'members': text = areMembersHidden ? 'You have no access to group members list.' : 'No members found'; @@ -912,7 +951,9 @@ export default memo(withGlobal( const archiveStoryIds = peerStories?.archiveIds; const hasGiftsTab = Boolean(peerFullInfo?.starGiftCount) && !isSavedDialog; - const peerGifts = global.peers.giftsById[chatId]; + const peerGifts = selectTabState(global).savedGifts.giftsByPeerId[chatId]; + + const isNotDefaultGiftFilter = !selectIsGiftProfileFilterDefault(global); return { theme: selectTheme(global), @@ -952,6 +993,7 @@ export default memo(withGlobal( isTopicInfo, isSavedDialog, isSynced: global.isSynced, + isNotDefaultGiftFilter, limitSimilarPeers: selectPremiumLimit(global, 'recommendedChannels'), ...(hasMembersTab && members && { members, adminMembersById }), ...(hasCommonChatsTab && user && { commonChatIds: commonChats?.ids }), diff --git a/src/components/right/RightHeader.tsx b/src/components/right/RightHeader.tsx index 2e0b5460c..b35f72d35 100644 --- a/src/components/right/RightHeader.tsx +++ b/src/components/right/RightHeader.tsx @@ -1,10 +1,13 @@ import type { FC } from '../../lib/teact/teact'; -import React, { useEffect, useRef, useState } from '../../lib/teact/teact'; +import React, { + useEffect, useMemo, useRef, useState, +} from '../../lib/teact/teact'; import { getActions, withGlobal } from '../../global'; import type { ApiExportedInvite } from '../../api/types'; +import type { GiftProfileFilterOptions, ThreadId } from '../../types'; import { MAIN_THREAD_ID } from '../../api/types'; -import { ManagementScreens, ProfileState, type ThreadId } from '../../types'; +import { ManagementScreens, ProfileState } from '../../types'; import { ANIMATION_END_DELAY, SAVED_FOLDER_ID } from '../../config'; import { @@ -12,6 +15,8 @@ import { } from '../../global/helpers'; import { selectCanManage, + selectCanUseGiftProfileAdminFilter, + selectCanUseGiftProfileFilter, selectChat, selectChatFullInfo, selectCurrentGifSearch, @@ -28,12 +33,16 @@ import useCurrentOrPrev from '../../hooks/useCurrentOrPrev'; import useElectronDrag from '../../hooks/useElectronDrag'; import useFlag from '../../hooks/useFlag'; import { useFolderManagerForChatsCount } from '../../hooks/useFolderManager'; +import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; import useOldLang from '../../hooks/useOldLang'; import Icon from '../common/icons/Icon'; import Button from '../ui/Button'; import ConfirmDialog from '../ui/ConfirmDialog'; +import DropdownMenu from '../ui/DropdownMenu'; +import MenuItem from '../ui/MenuItem'; +import MenuSeparator from '../ui/MenuSeparator'; import SearchInput from '../ui/SearchInput'; import Transition from '../ui/Transition'; @@ -76,6 +85,9 @@ type StateProps = { shouldSkipHistoryAnimations?: boolean; isBot?: boolean; canEditBot?: boolean; + giftProfileFilter: GiftProfileFilterOptions; + canUseGiftFilter?: boolean; + canUseGiftAdminFilter?:boolean; isInsideTopic?: boolean; canEditTopic?: boolean; isSavedMessages?: boolean; @@ -86,6 +98,7 @@ const COLUMN_ANIMATION_DURATION = 450 + ANIMATION_END_DELAY; enum HeaderContent { Profile, MemberList, + GiftList, SharedMedia, StoryList, Search, @@ -161,6 +174,9 @@ const RightHeader: FC = ({ onClose, onScreenSelect, canEditBot, + giftProfileFilter, + canUseGiftFilter, + canUseGiftAdminFilter, }) => { const { setStickerSearchQuery, @@ -171,11 +187,21 @@ const RightHeader: FC = ({ setEditingExportedInvite, deleteExportedChatInvite, openEditTopicPanel, + updateGiftProfileFilter, } = getActions(); const [isDeleteDialogOpen, openDeleteDialog, closeDeleteDialog] = useFlag(); const { isMobile } = useAppLayout(); + const { + sortType: giftsSortType, + shouldIncludeUnlimited: shouldIncludeUnlimitedGifts, + shouldIncludeLimited: shouldIncludeLimitedGifts, + shouldIncludeUnique: shouldIncludeUniqueGifts, + shouldIncludeDisplayed: shouldIncludeDisplayedGifts, + shouldIncludeHidden: shouldIncludeHiddenGifts, + } = giftProfileFilter; + const foldersChatCount = useFolderManagerForChatsCount(); const handleEditInviteClick = useLastCallback(() => { @@ -226,7 +252,8 @@ const RightHeader: FC = ({ }, COLUMN_ANIMATION_DURATION); }, [isColumnOpen]); - const lang = useOldLang(); + const oldLang = useOldLang(); + const lang = useLang(); const contentKey = isProfile ? ( profileState === ProfileState.Profile ? ( HeaderContent.Profile @@ -234,6 +261,8 @@ const RightHeader: FC = ({ HeaderContent.SharedMedia ) : profileState === ProfileState.MemberList ? ( HeaderContent.MemberList + ) : profileState === ProfileState.GiftList ? ( + HeaderContent.GiftList ) : profileState === ProfileState.StoryList ? ( HeaderContent.StoryList ) : profileState === ProfileState.SavedDialogs ? ( @@ -309,24 +338,40 @@ const RightHeader: FC = ({ function getHeaderTitle() { if (isSavedMessages) { - return lang('SavedMessages'); + return oldLang('SavedMessages'); } if (isInsideTopic) { - return lang('AccDescrTopic'); + return oldLang('AccDescrTopic'); } if (isChannel) { - return lang('Channel.TitleInfo'); + return oldLang('Channel.TitleInfo'); } if (userId) { - return lang(isBot ? 'lng_info_bot_title' : 'lng_info_user_title'); + return oldLang(isBot ? 'lng_info_bot_title' : 'lng_info_user_title'); } - return lang('GroupInfo.Title'); + return oldLang('GroupInfo.Title'); } + const PrimaryLinkMenuButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => { + return ({ onTrigger, isOpen }) => ( + + ); + }, [isMobile, lang]); + function renderHeaderContent() { if (renderingContentKey === -1) { return undefined; @@ -334,48 +379,48 @@ const RightHeader: FC = ({ switch (renderingContentKey) { case HeaderContent.PollResults: - return

{lang('PollResults')}

; + return

{oldLang('PollResults')}

; case HeaderContent.AddingMembers: - return

{lang(isChannel ? 'ChannelAddSubscribers' : 'GroupAddMembers')}

; + return

{oldLang(isChannel ? 'ChannelAddSubscribers' : 'GroupAddMembers')}

; case HeaderContent.ManageInitial: - return

{lang('Edit')}

; + return

{oldLang('Edit')}

; case HeaderContent.ManageChatPrivacyType: - return

{lang(isChannel ? 'ChannelTypeHeader' : 'GroupTypeHeader')}

; + return

{oldLang(isChannel ? 'ChannelTypeHeader' : 'GroupTypeHeader')}

; case HeaderContent.ManageDiscussion: - return

{lang('Discussion')}

; + return

{oldLang('Discussion')}

; case HeaderContent.ManageChatAdministrators: - return

{lang('ChannelAdministrators')}

; + return

{oldLang('ChannelAdministrators')}

; case HeaderContent.ManageGroupRecentActions: - return

{lang('Group.Info.AdminLog')}

; + return

{oldLang('Group.Info.AdminLog')}

; case HeaderContent.ManageGroupAdminRights: - return

{lang('EditAdminRights')}

; + return

{oldLang('EditAdminRights')}

; case HeaderContent.ManageGroupNewAdminRights: - return

{lang('SetAsAdmin')}

; + return

{oldLang('SetAsAdmin')}

; case HeaderContent.ManageGroupPermissions: - return

{lang('ChannelPermissions')}

; + return

{oldLang('ChannelPermissions')}

; case HeaderContent.ManageGroupRemovedUsers: - return

{lang('BlockedUsers')}

; + return

{oldLang('BlockedUsers')}

; case HeaderContent.ManageChannelRemovedUsers: - return

{lang('ChannelBlockedUsers')}

; + return

{oldLang('ChannelBlockedUsers')}

; case HeaderContent.ManageGroupUserPermissionsCreate: - return

{lang('ChannelAddException')}

; + return

{oldLang('ChannelAddException')}

; case HeaderContent.ManageGroupUserPermissions: - return

{lang('UserRestrictions')}

; + return

{oldLang('UserRestrictions')}

; case HeaderContent.ManageInvites: - return

{lang('lng_group_invite_title')}

; + return

{oldLang('lng_group_invite_title')}

; case HeaderContent.ManageEditInvite: - return

{isEditingInvite ? lang('EditLink') : lang('NewLink')}

; + return

{isEditingInvite ? oldLang('EditLink') : oldLang('NewLink')}

; case HeaderContent.ManageInviteInfo: return ( <> -

{lang('InviteLink')}

+

{oldLang('InviteLink')}

{currentInviteInfo && !currentInviteInfo.isRevoked && ( @@ -599,6 +727,10 @@ export default withGlobal( const currentInviteInfo = chatId ? tabState.management.byChatId[chatId]?.inviteInfo?.invite : undefined; + const giftProfileFilter = tabState.savedGifts.filter; + const canUseGiftFilter = chatId ? selectCanUseGiftProfileFilter(global, chatId) : false; + const canUseGiftAdminFilter = chatId ? selectCanUseGiftProfileAdminFilter(global, chatId) : false; + return { canManage, canAddContact, @@ -616,6 +748,9 @@ export default withGlobal( isSavedMessages, shouldSkipHistoryAnimations: tabState.shouldSkipHistoryAnimations, canEditBot, + giftProfileFilter, + canUseGiftFilter, + canUseGiftAdminFilter, }; }, )(RightHeader); diff --git a/src/components/right/hooks/useProfileState.ts b/src/components/right/hooks/useProfileState.ts index 79612ccff..72ae54251 100644 --- a/src/components/right/hooks/useProfileState.ts +++ b/src/components/right/hooks/useProfileState.ts @@ -121,6 +121,8 @@ function getStateFromTabType(tabType: ProfileTabType) { switch (tabType) { case 'members': return ProfileState.MemberList; + case 'gifts': + return ProfileState.GiftList; case 'stories': return ProfileState.StoryList; case 'dialogs': diff --git a/src/components/ui/DropdownMenu.tsx b/src/components/ui/DropdownMenu.tsx index afcc3aab1..69adef39a 100644 --- a/src/components/ui/DropdownMenu.tsx +++ b/src/components/ui/DropdownMenu.tsx @@ -24,6 +24,7 @@ type OwnProps = { onTransitionEnd?: NoneToVoidFunction; onMouseEnterBackdrop?: (e: React.MouseEvent) => void; children: React.ReactNode; + autoClose?: boolean; }; const DropdownMenu: FC = ({ @@ -41,6 +42,7 @@ const DropdownMenu: FC = ({ onTransitionEnd, onMouseEnterBackdrop, onHide, + autoClose = true, }) => { // eslint-disable-next-line no-null/no-null const menuRef = useRef(null); @@ -110,7 +112,7 @@ const DropdownMenu: FC = ({ positionX={positionX} positionY={positionY} footer={footer} - autoClose + autoClose={autoClose} onClose={handleClose} onCloseAnimationEnd={onHide} onMouseEnterBackdrop={onMouseEnterBackdrop} diff --git a/src/config.ts b/src/config.ts index b7efaf536..d64b7576f 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,6 +1,9 @@ import type { ApiLimitType, ApiLimitTypeForPromo, ApiPremiumSection, ApiReactionEmoji, } from './api/types'; +import type { + GiftProfileFilterOptions, +} from './types'; export const APP_CODE_NAME = 'A'; export const APP_NAME = process.env.APP_NAME || `Telegram Web ${APP_CODE_NAME}`; @@ -427,3 +430,12 @@ export const PREMIUM_LIMITS_ORDER: ApiLimitTypeForPromo[] = [ 'dialogFiltersChats', 'recommendedChannels', ]; + +export const DEFAULT_GIFT_PROFILE_FILTER_OPTIONS : GiftProfileFilterOptions = { + sortType: 'byDate', + shouldIncludeUnlimited: true, + shouldIncludeLimited: true, + shouldIncludeUnique: true, + shouldIncludeDisplayed: true, + shouldIncludeHidden: true, +} as const; diff --git a/src/global/actions/api/stars.ts b/src/global/actions/api/stars.ts index 3d36c8723..3259c66ca 100644 --- a/src/global/actions/api/stars.ts +++ b/src/global/actions/api/stars.ts @@ -15,7 +15,10 @@ import { } from '../../reducers'; import { updateTabState } from '../../reducers/tabs'; import { + selectGiftProfileFilter, selectPeer, + selectPeerSavedGifts, + selectTabState, } from '../../selectors'; addActionHandler('loadStarStatus', async (global): Promise => { @@ -134,12 +137,14 @@ addActionHandler('loadStarGifts', async (global): Promise => { }); addActionHandler('loadPeerSavedGifts', async (global, actions, payload): Promise => { - const { peerId, shouldRefresh } = payload; + const { + peerId, shouldRefresh, tabId = getCurrentTabId(), + } = payload; const peer = selectPeer(global, peerId); if (!peer) return; - const currentGifts = global.peers.giftsById[peerId]; + const currentGifts = selectPeerSavedGifts(global, peerId, tabId); const localNextOffset = currentGifts?.nextOffset; if (!shouldRefresh && currentGifts && !localNextOffset) return; // Already loaded all @@ -147,6 +152,7 @@ addActionHandler('loadPeerSavedGifts', async (global, actions, payload): Promise const result = await callApi('fetchSavedStarGifts', { peer, offset: !shouldRefresh ? localNextOffset : '', + filter: selectGiftProfileFilter(global, peerId, tabId), }); if (!result) { @@ -157,7 +163,7 @@ addActionHandler('loadPeerSavedGifts', async (global, actions, payload): Promise const newGifts = currentGifts && !shouldRefresh ? currentGifts.gifts.concat(result.gifts) : result.gifts; - global = replacePeerSavedGifts(global, peerId, newGifts, result.nextOffset); + global = replacePeerSavedGifts(global, peerId, newGifts, result.nextOffset, tabId); setGlobal(global); }); @@ -216,14 +222,14 @@ addActionHandler('fulfillStarsSubscription', async (global, actions, payload): P }); addActionHandler('changeGiftVisibility', async (global, actions, payload): Promise => { - const { gift, shouldUnsave } = payload; + const { gift, shouldUnsave, tabId = getCurrentTabId() } = payload; const peerId = gift.type === 'user' ? global.currentUserId! : gift.chatId; const requestInputGift = getRequestInputSavedStarGift(global, gift); if (!requestInputGift) return; - const oldGifts = global.peers.giftsById[peerId]; + const oldGifts = selectTabState(global, tabId).savedGifts.giftsByPeerId[peerId]; if (oldGifts?.gifts?.length) { const newGifts = oldGifts.gifts.map((g) => { if (g.inputGift && areInputSavedGiftsEqual(g.inputGift, gift)) { @@ -234,7 +240,7 @@ addActionHandler('changeGiftVisibility', async (global, actions, payload): Promi } return g; }); - global = replacePeerSavedGifts(global, peerId, newGifts, oldGifts.nextOffset); + global = replacePeerSavedGifts(global, peerId, newGifts, oldGifts.nextOffset, tabId); setGlobal(global); } @@ -245,13 +251,17 @@ addActionHandler('changeGiftVisibility', async (global, actions, payload): Promi global = getGlobal(); if (!result) { - global = replacePeerSavedGifts(global, peerId, oldGifts.gifts, oldGifts.nextOffset); + global = replacePeerSavedGifts(global, peerId, oldGifts.gifts, oldGifts.nextOffset, tabId); setGlobal(global); return; } // Reload gift list to avoid issues with pagination - actions.loadPeerSavedGifts({ peerId, shouldRefresh: true }); + Object.values(global.byTabId).forEach((tabState) => { + if (selectPeerSavedGifts(global, peerId, tabId)) { + actions.loadPeerSavedGifts({ peerId, shouldRefresh: true, tabId: tabState.id }); + } + }); }); addActionHandler('convertGiftToStars', async (global, actions, payload): Promise => { @@ -268,7 +278,12 @@ addActionHandler('convertGiftToStars', async (global, actions, payload): Promise return; } - actions.loadPeerSavedGifts({ peerId: global.currentUserId!, shouldRefresh: true }); + const peerId = gift.type === 'user' ? global.currentUserId! : gift.chatId; + Object.values(global.byTabId).forEach((tabState) => { + if (selectPeerSavedGifts(global, peerId, tabId)) { + actions.loadPeerSavedGifts({ peerId, shouldRefresh: true, tabId: tabState.id }); + } + }); actions.openStarsBalanceModal({ tabId }); }); diff --git a/src/global/actions/ui/payments.ts b/src/global/actions/ui/payments.ts index 66c0778f2..1de0cd4a0 100644 --- a/src/global/actions/ui/payments.ts +++ b/src/global/actions/ui/payments.ts @@ -1,5 +1,6 @@ import type { ActionReturnType } from '../../types'; +import { DEFAULT_GIFT_PROFILE_FILTER_OPTIONS } from '../../../config'; import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { addActionHandler } from '../../index'; import { @@ -65,3 +66,55 @@ addActionHandler('closeGiftCodeModal', (global, actions, payload): ActionReturnT giftCodeModal: undefined, }, tabId); }); + +addActionHandler('updateGiftProfileFilter', (global, actions, payload): ActionReturnType => { + const { filter, tabId = getCurrentTabId() } = payload || {}; + const tabState = selectTabState(global, tabId); + + const prevFilter = tabState.savedGifts.filter; + let updatedFilter = { + ...prevFilter, + ...filter, + }; + + if (!updatedFilter.shouldIncludeUnlimited + && !updatedFilter.shouldIncludeLimited + && !updatedFilter.shouldIncludeUnique) { + updatedFilter = { + ...prevFilter, + shouldIncludeUnlimited: true, + shouldIncludeLimited: true, + shouldIncludeUnique: true, + ...filter, + }; + } + + if (!updatedFilter.shouldIncludeDisplayed && !updatedFilter.shouldIncludeHidden) { + updatedFilter = { + ...prevFilter, + shouldIncludeDisplayed: true, + shouldIncludeHidden: true, + ...filter, + }; + } + + return updateTabState(global, { + savedGifts: { + giftsByPeerId: {}, + filter: updatedFilter, + }, + }, tabId); +}); + +addActionHandler('resetGiftProfileFilter', (global, actions, payload): ActionReturnType => { + const { tabId = getCurrentTabId() } = payload || {}; + + return updateTabState(global, { + savedGifts: { + giftsByPeerId: {}, + filter: { + ...DEFAULT_GIFT_PROFILE_FILTER_OPTIONS, + }, + }, + }, tabId); +}); diff --git a/src/global/initialState.ts b/src/global/initialState.ts index 6f1219447..5d65c8361 100644 --- a/src/global/initialState.ts +++ b/src/global/initialState.ts @@ -5,6 +5,7 @@ import { NewChatMembersProgress } from '../types'; import { ANIMATION_LEVEL_DEFAULT, DARK_THEME_PATTERN_COLOR, + DEFAULT_GIFT_PROFILE_FILTER_OPTIONS, DEFAULT_MESSAGE_TEXT_SIZE_PX, DEFAULT_PATTERN_COLOR, DEFAULT_PLAYBACK_RATE, @@ -106,7 +107,6 @@ export const INITIAL_GLOBAL_STATE: GlobalState = { }, peers: { - giftsById: {}, profilePhotosById: {}, }, @@ -365,6 +365,13 @@ export const INITIAL_TAB_STATE: TabState = { byChatId: {}, }, + savedGifts: { + filter: { + ...DEFAULT_GIFT_PROFILE_FILTER_OPTIONS, + }, + giftsByPeerId: {}, + }, + storyViewer: { isMuted: true, isRibbonShown: false, diff --git a/src/global/reducers/users.ts b/src/global/reducers/users.ts index 2a11f24c8..72b205d76 100644 --- a/src/global/reducers/users.ts +++ b/src/global/reducers/users.ts @@ -329,20 +329,20 @@ export function replacePeerSavedGifts( peerId: string, gifts: ApiSavedStarGift[], nextOffset?: string, + ...[tabId = getCurrentTabId()]: TabArgs ): T { - global = { - ...global, - peers: { - ...global.peers, - giftsById: { - ...global.peers.giftsById, + const tabState = selectTabState(global, tabId); + + return updateTabState(global, { + savedGifts: { + ...tabState.savedGifts, + giftsByPeerId: { + ...tabState.savedGifts.giftsByPeerId, [peerId]: { gifts, nextOffset, }, }, }, - }; - - return global; + }, tabId); } diff --git a/src/global/selectors/payments.ts b/src/global/selectors/payments.ts index 431f1af5c..eccfc72bd 100644 --- a/src/global/selectors/payments.ts +++ b/src/global/selectors/payments.ts @@ -1,6 +1,12 @@ import type { GlobalState, TabArgs } from '../types'; +import { DEFAULT_GIFT_PROFILE_FILTER_OPTIONS } from '../../config'; +import arePropsShallowEqual from '../../util/arePropsShallowEqual'; import { getCurrentTabId } from '../../util/establishMultitabRole'; +import { + getHasAdminRight, isChatAdmin, isChatChannel, +} from '../helpers'; +import { selectChat } from './chats'; import { selectTabState } from './tabs'; export function selectPaymentInputInvoice( @@ -58,3 +64,32 @@ export function selectSmartGlocalCredentials( ) { return selectTabState(global, tabId).payment.smartGlocalCredentials; } + +export function selectCanUseGiftProfileAdminFilter( + global: T, peerId: string, +) { + const chat = selectChat(global, peerId); + return chat && isChatChannel(chat) && isChatAdmin(chat) && getHasAdminRight(chat, 'postMessages'); +} + +export function selectCanUseGiftProfileFilter( + global: T, peerId: string, +) { + const chat = selectChat(global, peerId); + return chat && isChatChannel(chat); +} + +export function selectGiftProfileFilter( + global: T, + peerId: string, + ...[tabId = getCurrentTabId()]: TabArgs +) { + return selectCanUseGiftProfileFilter(global, peerId) ? selectTabState(global, tabId).savedGifts.filter : undefined; +} + +export function selectIsGiftProfileFilterDefault( + global: T, + ...[tabId = getCurrentTabId()]: TabArgs +) { + return arePropsShallowEqual(selectTabState(global, tabId).savedGifts.filter, DEFAULT_GIFT_PROFILE_FILTER_OPTIONS); +} diff --git a/src/global/selectors/peers.ts b/src/global/selectors/peers.ts index 8dff5571e..72fe631dd 100644 --- a/src/global/selectors/peers.ts +++ b/src/global/selectors/peers.ts @@ -1,8 +1,10 @@ -import type { ApiPeer } from '../../api/types'; -import type { GlobalState } from '../types'; +import type { ApiPeer, ApiSavedGifts } from '../../api/types'; +import type { GlobalState, TabArgs } from '../types'; import { SERVICE_NOTIFICATIONS_USER_ID } from '../../config'; +import { getCurrentTabId } from '../../util/establishMultitabRole'; import { selectChat, selectChatFullInfo } from './chats'; +import { selectTabState } from './tabs'; import { selectBot, selectIsPremiumPurchaseBlocked, selectUser } from './users'; export function selectPeer(global: T, peerId: string): ApiPeer | undefined { @@ -22,3 +24,11 @@ export function selectCanGift(global: T, peerId: string) return Boolean(!selectIsPremiumPurchaseBlocked(global) && !bot && peerId !== SERVICE_NOTIFICATIONS_USER_ID && areStarGiftsAvailable); } + +export function selectPeerSavedGifts( + global: T, + peerId: string, + ...[tabId = getCurrentTabId()]: TabArgs +) : ApiSavedGifts { + return selectTabState(global, tabId).savedGifts.giftsByPeerId[peerId]; +} diff --git a/src/global/types/actions.ts b/src/global/types/actions.ts index 3a58fbf30..780708b2b 100644 --- a/src/global/types/actions.ts +++ b/src/global/types/actions.ts @@ -59,6 +59,7 @@ import type { CallSound, ChatListType, ConfettiParams, + GiftProfileFilterOptions, GlobalSearchContent, IAlbum, IAnchorPosition, @@ -2341,11 +2342,11 @@ export interface ActionPayloads { loadPeerSavedGifts: { peerId: string; shouldRefresh?: boolean; - }; + } & WithTabId; changeGiftVisibility: { gift: ApiInputSavedStarGift; shouldUnsave?: boolean; - }; + } & WithTabId; convertGiftToStars: { gift: ApiInputSavedStarGift; } & WithTabId; @@ -2369,6 +2370,11 @@ export interface ActionPayloads { } & WithTabId; closeSuggestedStatusModal: WithTabId | undefined; + updateGiftProfileFilter: { + filter: Partial; + } & WithTabId; + resetGiftProfileFilter: WithTabId | undefined; + // Invoice openInvoice: Exclude & WithTabId; diff --git a/src/global/types/globalState.ts b/src/global/types/globalState.ts index e772eadce..79d78d8a8 100644 --- a/src/global/types/globalState.ts +++ b/src/global/types/globalState.ts @@ -24,7 +24,6 @@ import type { ApiQuickReply, ApiReaction, ApiReactionKey, - ApiSavedGifts, ApiSavedReactionTag, ApiSession, ApiSponsoredMessage, @@ -176,7 +175,6 @@ export type GlobalState = { peers: { profilePhotosById: Record; - giftsById: Record; }; chats: { diff --git a/src/global/types/tabState.ts b/src/global/types/tabState.ts index 50830910a..6e18c6de7 100644 --- a/src/global/types/tabState.ts +++ b/src/global/types/tabState.ts @@ -32,6 +32,7 @@ import type { ApiPremiumSection, ApiReactionWithPaid, ApiReceiptRegular, + ApiSavedGifts, ApiSavedStarGift, ApiStarGift, ApiStarGiftAttribute, @@ -57,6 +58,7 @@ import type { ChatRequestedTranslations, ConfettiStyle, FocusDirection, + GiftProfileFilterOptions, GlobalSearchContent, IAlbum, IAnchorPosition, @@ -202,6 +204,11 @@ export type TabState = { byUsername: Record; }; + savedGifts: { + giftsByPeerId: Record; + filter: GiftProfileFilterOptions; + }; + globalSearch: { query?: string; minDate?: number; diff --git a/src/types/index.ts b/src/types/index.ts index 1028e8ef2..b9ecb696f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -417,6 +417,7 @@ export enum ProfileState { Profile, SharedMedia, MemberList, + GiftList, StoryList, SavedDialogs, } @@ -657,3 +658,12 @@ export type CallSound = ( export type BotAppPermissions = { geolocation?: boolean; }; + +export type GiftProfileFilterOptions = { + sortType: 'byDate' | 'byValue'; + shouldIncludeUnlimited: boolean; + shouldIncludeLimited: boolean; + shouldIncludeUnique: boolean; + shouldIncludeDisplayed: boolean; + shouldIncludeHidden: boolean; +}; diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 8f3f58d55..9ec85aa6e 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -1306,6 +1306,15 @@ export interface LangPair { 'ViewButtonGiftUnique': undefined; 'AuthContinueOnThisLanguage': undefined; 'Share': undefined; + 'GiftSortByDate': undefined; + 'GiftSortByValue': undefined; + 'GiftFilterUnlimited': undefined; + 'GiftFilterLimited': undefined; + 'GiftFilterUnique': undefined; + 'GiftFilterDisplayed': undefined; + 'GiftFilterHidden': undefined; + 'GiftSearchEmpty': undefined; + 'GiftSearchReset': undefined; 'CheckPasswordTitle': undefined; 'CheckPasswordPlaceholder': undefined; 'CheckPasswordDescription': undefined;