Gifts: Support filter in profile (#5533)

This commit is contained in:
Alexander Zinchuk 2025-02-13 14:27:50 +01:00
parent 0ac730a043
commit 1419d396d3
21 changed files with 485 additions and 84 deletions

View File

@ -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<typeof GramJs.payments.GetSavedStarGifts>[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;

View File

@ -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.";

Binary file not shown.

View File

@ -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,
};

View File

@ -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;

View File

@ -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<OwnProps & StateProps> = ({
forceScrollProfileTab,
isSynced,
onProfileStateChange,
isNotDefaultGiftFilter,
}) => {
const {
setSharedMediaSearchType,
@ -237,6 +243,7 @@ const Profile: FC<OwnProps & StateProps> = ({
loadBotRecommendations,
loadPreviewMedias,
loadPeerSavedGifts,
resetGiftProfileFilter,
} = getActions();
// eslint-disable-next-line no-null/no-null
@ -482,6 +489,10 @@ const Profile: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
}];
}
function renderNothingFoundGiftsWithFilter() {
return (
<div className="nothing-found-gifts">
<AnimatedIconWithPreview
size={160}
tgsUrl={LOCAL_TGS_URLS.SearchingDuck}
nonInteractive
noLoop
/>
<div className="description">
{lang('GiftSearchEmpty')}
</div>
<Link
className="date"
onClick={handleResetGiftsFilter}
>
{lang('GiftSearchReset')}
</Link>
</div>
);
}
function renderContent() {
if (resultType === 'dialogs') {
return (
@ -535,7 +568,9 @@ const Profile: FC<OwnProps & StateProps> = ({
const forceRenderHiddenMembers = Boolean(resultType === 'members' && areMembersHidden);
return (
<div className="content empty-list">
<div
className="content empty-list"
>
{!noSpinner && !forceRenderHiddenMembers && <Spinner />}
{forceRenderHiddenMembers && <NothingFound text="You have no access to group members list." />}
</div>
@ -545,6 +580,10 @@ const Profile: FC<OwnProps & StateProps> = ({
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<OwnProps>(
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<OwnProps>(
isTopicInfo,
isSavedDialog,
isSynced: global.isSynced,
isNotDefaultGiftFilter,
limitSimilarPeers: selectPremiumLimit(global, 'recommendedChannels'),
...(hasMembersTab && members && { members, adminMembersById }),
...(hasCommonChatsTab && user && { commonChatIds: commonChats?.ids }),

View File

@ -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<OwnProps & StateProps> = ({
onClose,
onScreenSelect,
canEditBot,
giftProfileFilter,
canUseGiftFilter,
canUseGiftAdminFilter,
}) => {
const {
setStickerSearchQuery,
@ -171,11 +187,21 @@ const RightHeader: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
}, 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<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
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 }) => (
<Button
round
ripple={!isMobile}
size="smaller"
color="translucent"
className={isOpen ? 'active' : ''}
onClick={onTrigger}
ariaLabel={lang('AccDescrOpenMenu2')}
>
<Icon name="more" />
</Button>
);
}, [isMobile, lang]);
function renderHeaderContent() {
if (renderingContentKey === -1) {
return undefined;
@ -334,48 +379,48 @@ const RightHeader: FC<OwnProps & StateProps> = ({
switch (renderingContentKey) {
case HeaderContent.PollResults:
return <h3 className="title">{lang('PollResults')}</h3>;
return <h3 className="title">{oldLang('PollResults')}</h3>;
case HeaderContent.AddingMembers:
return <h3 className="title">{lang(isChannel ? 'ChannelAddSubscribers' : 'GroupAddMembers')}</h3>;
return <h3 className="title">{oldLang(isChannel ? 'ChannelAddSubscribers' : 'GroupAddMembers')}</h3>;
case HeaderContent.ManageInitial:
return <h3 className="title">{lang('Edit')}</h3>;
return <h3 className="title">{oldLang('Edit')}</h3>;
case HeaderContent.ManageChatPrivacyType:
return <h3 className="title">{lang(isChannel ? 'ChannelTypeHeader' : 'GroupTypeHeader')}</h3>;
return <h3 className="title">{oldLang(isChannel ? 'ChannelTypeHeader' : 'GroupTypeHeader')}</h3>;
case HeaderContent.ManageDiscussion:
return <h3 className="title">{lang('Discussion')}</h3>;
return <h3 className="title">{oldLang('Discussion')}</h3>;
case HeaderContent.ManageChatAdministrators:
return <h3 className="title">{lang('ChannelAdministrators')}</h3>;
return <h3 className="title">{oldLang('ChannelAdministrators')}</h3>;
case HeaderContent.ManageGroupRecentActions:
return <h3 className="title">{lang('Group.Info.AdminLog')}</h3>;
return <h3 className="title">{oldLang('Group.Info.AdminLog')}</h3>;
case HeaderContent.ManageGroupAdminRights:
return <h3 className="title">{lang('EditAdminRights')}</h3>;
return <h3 className="title">{oldLang('EditAdminRights')}</h3>;
case HeaderContent.ManageGroupNewAdminRights:
return <h3 className="title">{lang('SetAsAdmin')}</h3>;
return <h3 className="title">{oldLang('SetAsAdmin')}</h3>;
case HeaderContent.ManageGroupPermissions:
return <h3 className="title">{lang('ChannelPermissions')}</h3>;
return <h3 className="title">{oldLang('ChannelPermissions')}</h3>;
case HeaderContent.ManageGroupRemovedUsers:
return <h3 className="title">{lang('BlockedUsers')}</h3>;
return <h3 className="title">{oldLang('BlockedUsers')}</h3>;
case HeaderContent.ManageChannelRemovedUsers:
return <h3 className="title">{lang('ChannelBlockedUsers')}</h3>;
return <h3 className="title">{oldLang('ChannelBlockedUsers')}</h3>;
case HeaderContent.ManageGroupUserPermissionsCreate:
return <h3 className="title">{lang('ChannelAddException')}</h3>;
return <h3 className="title">{oldLang('ChannelAddException')}</h3>;
case HeaderContent.ManageGroupUserPermissions:
return <h3 className="title">{lang('UserRestrictions')}</h3>;
return <h3 className="title">{oldLang('UserRestrictions')}</h3>;
case HeaderContent.ManageInvites:
return <h3 className="title">{lang('lng_group_invite_title')}</h3>;
return <h3 className="title">{oldLang('lng_group_invite_title')}</h3>;
case HeaderContent.ManageEditInvite:
return <h3 className="title">{isEditingInvite ? lang('EditLink') : lang('NewLink')}</h3>;
return <h3 className="title">{isEditingInvite ? oldLang('EditLink') : oldLang('NewLink')}</h3>;
case HeaderContent.ManageInviteInfo:
return (
<>
<h3 className="title">{lang('InviteLink')}</h3>
<h3 className="title">{oldLang('InviteLink')}</h3>
<section className="tools">
{currentInviteInfo && !currentInviteInfo.isRevoked && (
<Button
round
color="translucent"
size="smaller"
ariaLabel={lang('Edit')}
ariaLabel={oldLang('Edit')}
onClick={handleEditInviteClick}
>
<Icon name="edit" />
@ -387,7 +432,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
round
color="danger"
size="smaller"
ariaLabel={lang('Delete')}
ariaLabel={oldLang('Delete')}
onClick={openDeleteDialog}
>
<Icon name="delete" />
@ -395,10 +440,10 @@ const RightHeader: FC<OwnProps & StateProps> = ({
<ConfirmDialog
isOpen={isDeleteDialogOpen}
onClose={closeDeleteDialog}
title={lang('DeleteLink')}
text={lang('DeleteLinkHelp')}
title={oldLang('DeleteLink')}
text={oldLang('DeleteLinkHelp')}
confirmIsDestructive
confirmLabel={lang('Delete')}
confirmLabel={oldLang('Delete')}
confirmHandler={handleDeleteInviteClick}
/>
</>
@ -407,14 +452,14 @@ const RightHeader: FC<OwnProps & StateProps> = ({
</>
);
case HeaderContent.ManageJoinRequests:
return <h3 className="title">{isChannel ? lang('SubscribeRequests') : lang('MemberRequests')}</h3>;
return <h3 className="title">{isChannel ? oldLang('SubscribeRequests') : oldLang('MemberRequests')}</h3>;
case HeaderContent.ManageGroupAddAdmins:
return <h3 className="title">{lang('Channel.Management.AddModerator')}</h3>;
return <h3 className="title">{oldLang('Channel.Management.AddModerator')}</h3>;
case HeaderContent.StickerSearch:
return (
<SearchInput
value={stickerSearchQuery}
placeholder={lang('SearchStickersHint')}
placeholder={oldLang('SearchStickersHint')}
autoFocusSearch
onChange={handleStickerSearchQueryChange}
/>
@ -423,43 +468,125 @@ const RightHeader: FC<OwnProps & StateProps> = ({
return (
<SearchInput
value={gifSearchQuery}
placeholder={lang('SearchGifsTitle')}
placeholder={oldLang('SearchGifsTitle')}
autoFocusSearch
onChange={handleGifSearchQueryChange}
/>
);
case HeaderContent.Statistics:
return <h3 className="title">{lang(isChannel ? 'ChannelStats.Title' : 'GroupStats.Title')}</h3>;
return <h3 className="title">{oldLang(isChannel ? 'ChannelStats.Title' : 'GroupStats.Title')}</h3>;
case HeaderContent.MessageStatistics:
return <h3 className="title">{lang('Stats.MessageTitle')}</h3>;
return <h3 className="title">{oldLang('Stats.MessageTitle')}</h3>;
case HeaderContent.StoryStatistics:
return <h3 className="title">{lang('Stats.StoryTitle')}</h3>;
return <h3 className="title">{oldLang('Stats.StoryTitle')}</h3>;
case HeaderContent.BoostStatistics:
return <h3 className="title">{lang('Boosts')}</h3>;
return <h3 className="title">{oldLang('Boosts')}</h3>;
case HeaderContent.MonetizationStatistics:
return <h3 className="title">{lang('lng_channel_earn_title')}</h3>;
return <h3 className="title">{oldLang('lng_channel_earn_title')}</h3>;
case HeaderContent.SharedMedia:
return <h3 className="title">{lang('SharedMedia')}</h3>;
return <h3 className="title">{oldLang('SharedMedia')}</h3>;
case HeaderContent.ManageChannelSubscribers:
return <h3 className="title">{lang('ChannelSubscribers')}</h3>;
return <h3 className="title">{oldLang('ChannelSubscribers')}</h3>;
case HeaderContent.MemberList:
case HeaderContent.ManageGroupMembers:
return <h3 className="title">{lang('GroupMembers')}</h3>;
return <h3 className="title">{oldLang('GroupMembers')}</h3>;
case HeaderContent.StoryList:
return <h3 className="title">{lang(isSelf ? 'Settings.MyStories' : 'PeerInfo.PaneStories')}</h3>;
return <h3 className="title">{oldLang(isSelf ? 'Settings.MyStories' : 'PeerInfo.PaneStories')}</h3>;
case HeaderContent.SavedDialogs:
return (
<div className="header">
<h3 className="title">{lang('SavedMessagesTab')}</h3>
<div className="subtitle">{lang('Chats', foldersChatCount[SAVED_FOLDER_ID])}</div>
<h3 className="title">{oldLang('SavedMessagesTab')}</h3>
<div className="subtitle">{oldLang('Chats', foldersChatCount[SAVED_FOLDER_ID])}</div>
</div>
);
case HeaderContent.ManageReactions:
return <h3 className="title">{lang('Reactions')}</h3>;
return <h3 className="title">{oldLang('Reactions')}</h3>;
case HeaderContent.CreateTopic:
return <h3 className="title">{lang('NewTopic')}</h3>;
return <h3 className="title">{oldLang('NewTopic')}</h3>;
case HeaderContent.EditTopic:
return <h3 className="title">{lang('EditTopic')}</h3>;
return <h3 className="title">{oldLang('EditTopic')}</h3>;
case HeaderContent.GiftList:
return (
<>
<h3 className="title">{lang('ProfileTabGifts')}</h3>
{canUseGiftFilter && (
<section className="tools">
<DropdownMenu
trigger={PrimaryLinkMenuButton}
positionX="right"
autoClose={false}
>
<MenuItem
icon={giftsSortType === 'byDate' ? 'calendar-filter' : 'cash-circle'}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => updateGiftProfileFilter(
{ filter: { sortType: giftsSortType === 'byDate' ? 'byValue' : 'byDate' } },
)}
>
{lang(giftsSortType === 'byDate' ? 'GiftSortByDate' : 'GiftSortByValue')}
</MenuItem>
<MenuSeparator />
<MenuItem
icon={shouldIncludeUnlimitedGifts ? 'check' : 'placeholder'}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => updateGiftProfileFilter(
{ filter: { shouldIncludeUnlimited: !shouldIncludeUnlimitedGifts } },
)}
>
{lang('GiftFilterUnlimited')}
</MenuItem>
<MenuItem
icon={shouldIncludeLimitedGifts ? 'check' : 'placeholder'}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => updateGiftProfileFilter(
{ filter: { shouldIncludeLimited: !shouldIncludeLimitedGifts } },
)}
>
{lang('GiftFilterLimited')}
</MenuItem>
<MenuItem
icon={shouldIncludeUniqueGifts ? 'check' : 'placeholder'}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => updateGiftProfileFilter(
{ filter: { shouldIncludeUnique: !shouldIncludeUniqueGifts } },
)}
>
{lang('GiftFilterUnique')}
</MenuItem>
{canUseGiftAdminFilter && (
<>
<MenuSeparator />
<MenuItem
icon={shouldIncludeDisplayedGifts ? 'check' : 'placeholder'}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => updateGiftProfileFilter(
{ filter: { shouldIncludeDisplayed: !shouldIncludeDisplayedGifts } },
)}
>
{lang('GiftFilterDisplayed')}
</MenuItem>
<MenuItem
icon={shouldIncludeHiddenGifts ? 'check' : 'placeholder'}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => updateGiftProfileFilter(
{ filter: { shouldIncludeHidden: !shouldIncludeHiddenGifts } },
)}
>
{lang('GiftFilterHidden')}
</MenuItem>
</>
)}
</DropdownMenu>
</section>
)}
</>
);
default:
return (
<>
@ -472,7 +599,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
round
color="translucent"
size="smaller"
ariaLabel={lang('AddContact')}
ariaLabel={oldLang('AddContact')}
onClick={handleAddContact}
>
<Icon name="add-user" />
@ -483,7 +610,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
round
color="translucent"
size="smaller"
ariaLabel={lang('Edit')}
ariaLabel={oldLang('Edit')}
onClick={handleToggleManagement}
>
<Icon name="edit" />
@ -494,7 +621,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
round
color="translucent"
size="smaller"
ariaLabel={lang('Edit')}
ariaLabel={oldLang('Edit')}
onClick={handleToggleManagement}
>
<Icon name="edit" />
@ -505,7 +632,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
round
color="translucent"
size="smaller"
ariaLabel={lang('EditTopic')}
ariaLabel={oldLang('EditTopic')}
onClick={toggleEditTopic}
>
<Icon name="edit" />
@ -516,7 +643,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
round
color="translucent"
size="smaller"
ariaLabel={lang('Statistics')}
ariaLabel={oldLang('Statistics')}
onClick={handleToggleStatistics}
>
<Icon name="stats" />
@ -531,6 +658,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
const isBackButton = isMobile || (
!isSavedMessages && (
contentKey === HeaderContent.SharedMedia
|| contentKey === HeaderContent.GiftList
|| contentKey === HeaderContent.MemberList
|| contentKey === HeaderContent.StoryList
|| contentKey === HeaderContent.AddingMembers
@ -558,7 +686,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
color="translucent"
size="smaller"
onClick={handleClose}
ariaLabel={isBackButton ? lang('Common.Back') : lang('Common.Close')}
ariaLabel={isBackButton ? oldLang('Common.Back') : oldLang('Common.Close')}
>
<div className={buttonClassName} />
</Button>
@ -599,6 +727,10 @@ export default withGlobal<OwnProps>(
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<OwnProps>(
isSavedMessages,
shouldSkipHistoryAnimations: tabState.shouldSkipHistoryAnimations,
canEditBot,
giftProfileFilter,
canUseGiftFilter,
canUseGiftAdminFilter,
};
},
)(RightHeader);

View File

@ -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':

View File

@ -24,6 +24,7 @@ type OwnProps = {
onTransitionEnd?: NoneToVoidFunction;
onMouseEnterBackdrop?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
children: React.ReactNode;
autoClose?: boolean;
};
const DropdownMenu: FC<OwnProps> = ({
@ -41,6 +42,7 @@ const DropdownMenu: FC<OwnProps> = ({
onTransitionEnd,
onMouseEnterBackdrop,
onHide,
autoClose = true,
}) => {
// eslint-disable-next-line no-null/no-null
const menuRef = useRef<HTMLDivElement>(null);
@ -110,7 +112,7 @@ const DropdownMenu: FC<OwnProps> = ({
positionX={positionX}
positionY={positionY}
footer={footer}
autoClose
autoClose={autoClose}
onClose={handleClose}
onCloseAnimationEnd={onHide}
onMouseEnterBackdrop={onMouseEnterBackdrop}

View File

@ -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;

View File

@ -15,7 +15,10 @@ import {
} from '../../reducers';
import { updateTabState } from '../../reducers/tabs';
import {
selectGiftProfileFilter,
selectPeer,
selectPeerSavedGifts,
selectTabState,
} from '../../selectors';
addActionHandler('loadStarStatus', async (global): Promise<void> => {
@ -134,12 +137,14 @@ addActionHandler('loadStarGifts', async (global): Promise<void> => {
});
addActionHandler('loadPeerSavedGifts', async (global, actions, payload): Promise<void> => {
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<void> => {
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<void> => {
@ -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 });
});

View File

@ -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);
});

View File

@ -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,

View File

@ -329,20 +329,20 @@ export function replacePeerSavedGifts<T extends GlobalState>(
peerId: string,
gifts: ApiSavedStarGift[],
nextOffset?: string,
...[tabId = getCurrentTabId()]: TabArgs<T>
): 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);
}

View File

@ -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<T extends GlobalState>(
@ -58,3 +64,32 @@ export function selectSmartGlocalCredentials<T extends GlobalState>(
) {
return selectTabState(global, tabId).payment.smartGlocalCredentials;
}
export function selectCanUseGiftProfileAdminFilter<T extends GlobalState>(
global: T, peerId: string,
) {
const chat = selectChat(global, peerId);
return chat && isChatChannel(chat) && isChatAdmin(chat) && getHasAdminRight(chat, 'postMessages');
}
export function selectCanUseGiftProfileFilter<T extends GlobalState>(
global: T, peerId: string,
) {
const chat = selectChat(global, peerId);
return chat && isChatChannel(chat);
}
export function selectGiftProfileFilter<T extends GlobalState>(
global: T,
peerId: string,
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
return selectCanUseGiftProfileFilter(global, peerId) ? selectTabState(global, tabId).savedGifts.filter : undefined;
}
export function selectIsGiftProfileFilterDefault<T extends GlobalState>(
global: T,
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
return arePropsShallowEqual(selectTabState(global, tabId).savedGifts.filter, DEFAULT_GIFT_PROFILE_FILTER_OPTIONS);
}

View File

@ -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<T extends GlobalState>(global: T, peerId: string): ApiPeer | undefined {
@ -22,3 +24,11 @@ export function selectCanGift<T extends GlobalState>(global: T, peerId: string)
return Boolean(!selectIsPremiumPurchaseBlocked(global) && !bot && peerId !== SERVICE_NOTIFICATIONS_USER_ID
&& areStarGiftsAvailable);
}
export function selectPeerSavedGifts<T extends GlobalState>(
global: T,
peerId: string,
...[tabId = getCurrentTabId()]: TabArgs<T>
) : ApiSavedGifts {
return selectTabState(global, tabId).savedGifts.giftsByPeerId[peerId];
}

View File

@ -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<GiftProfileFilterOptions>;
} & WithTabId;
resetGiftProfileFilter: WithTabId | undefined;
// Invoice
openInvoice: Exclude<ApiInputInvoice, ApiInputInvoiceStarGift> & WithTabId;

View File

@ -24,7 +24,6 @@ import type {
ApiQuickReply,
ApiReaction,
ApiReactionKey,
ApiSavedGifts,
ApiSavedReactionTag,
ApiSession,
ApiSponsoredMessage,
@ -176,7 +175,6 @@ export type GlobalState = {
peers: {
profilePhotosById: Record<string, ApiPeerPhotos>;
giftsById: Record<string, ApiSavedGifts>;
};
chats: {

View File

@ -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<string, false | InlineBotSettings>;
};
savedGifts: {
giftsByPeerId: Record<string, ApiSavedGifts>;
filter: GiftProfileFilterOptions;
};
globalSearch: {
query?: string;
minDate?: number;

View File

@ -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;
};

View File

@ -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;