Support gift transfer (#5555)

This commit is contained in:
zubiden 2025-02-13 14:27:52 +01:00 committed by Alexander Zinchuk
parent 1419d396d3
commit 45fc7aac7c
56 changed files with 875 additions and 329 deletions

View File

@ -682,6 +682,13 @@ export function buildInputInvoice(invoice: ApiRequestInputInvoice) {
}); });
} }
case 'stargiftTransfer': {
return new GramJs.InputInvoiceStarGiftTransfer({
stargift: buildInputSavedStarGift(invoice.inputSavedGift),
toId: buildInputPeer(invoice.recipient.id, invoice.recipient.accessHash),
});
}
case 'giveaway': case 'giveaway':
default: { default: {
const purpose = buildInputStorePaymentPurpose(invoice.purpose); const purpose = buildInputStorePaymentPurpose(invoice.purpose);

View File

@ -620,12 +620,12 @@ async function getFullChannelInfo(
? exportedInvite.link ? exportedInvite.link
: undefined; : undefined;
const { members, userStatusesById } = (canViewParticipants && await fetchMembers(id, accessHash)) || {}; const { members, userStatusesById } = (canViewParticipants && await fetchMembers({ chat })) || {};
const { members: kickedMembers, userStatusesById: bannedStatusesById } = ( const { members: kickedMembers, userStatusesById: bannedStatusesById } = (
canViewParticipants && adminRights && await fetchMembers(id, accessHash, 'kicked') canViewParticipants && adminRights && await fetchMembers({ chat, memberFilter: 'kicked' })
) || {}; ) || {};
const { members: adminMembers, userStatusesById: adminStatusesById } = ( const { members: adminMembers, userStatusesById: adminStatusesById } = (
canViewParticipants && await fetchMembers(id, accessHash, 'admin') canViewParticipants && await fetchMembers({ chat, memberFilter: 'admin' })
) || {}; ) || {};
const botCommands = botInfo ? buildApiChatBotCommands(botInfo) : undefined; const botCommands = botInfo ? buildApiChatBotCommands(botInfo) : undefined;
const memberInfoRequest = !chat.isNotJoined && chat.type === 'chatTypeChannel' const memberInfoRequest = !chat.isNotJoined && chat.type === 'chatTypeChannel'
@ -1285,35 +1285,44 @@ export function toggleSignatures({
type ChannelMembersFilter = type ChannelMembersFilter =
'kicked' 'kicked'
| 'admin' | 'admin'
| 'recent'; | 'recent'
| 'search';
export async function fetchMembers( export async function fetchMembers({
chatId: string, chat,
accessHash: string, memberFilter = 'recent',
memberFilter: ChannelMembersFilter = 'recent', offset,
offset?: number, query = '',
) { } : {
chat: ApiChat;
memberFilter?: ChannelMembersFilter;
offset?: number;
query?: string;
}) {
let filter: GramJs.TypeChannelParticipantsFilter; let filter: GramJs.TypeChannelParticipantsFilter;
switch (memberFilter) { switch (memberFilter) {
case 'kicked': case 'kicked':
filter = new GramJs.ChannelParticipantsKicked({ q: '' }); filter = new GramJs.ChannelParticipantsKicked({ q: query });
break; break;
case 'admin': case 'admin':
filter = new GramJs.ChannelParticipantsAdmins(); filter = new GramJs.ChannelParticipantsAdmins();
break; break;
case 'search':
filter = new GramJs.ChannelParticipantsSearch({ q: query });
break;
default: default:
filter = new GramJs.ChannelParticipantsRecent(); filter = new GramJs.ChannelParticipantsRecent();
break; break;
} }
const result = await invokeRequest(new GramJs.channels.GetParticipants({ const result = await invokeRequest(new GramJs.channels.GetParticipants({
channel: buildInputEntity(chatId, accessHash) as GramJs.InputChannel, channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel,
filter, filter,
offset, offset,
limit: MEMBERS_LOAD_SLICE, limit: MEMBERS_LOAD_SLICE,
}), { }), {
abortControllerChatId: chatId, abortControllerChatId: chat.id,
}); });
if (!result || result instanceof GramJs.channels.ChannelParticipantsNotModified) { if (!result || result instanceof GramJs.channels.ChannelParticipantsNotModified) {

View File

@ -694,7 +694,7 @@ export async function fetchStarGiftUpgradePreview({
return result.sampleAttributes.map(buildApiStarGiftAttribute).filter(Boolean); return result.sampleAttributes.map(buildApiStarGiftAttribute).filter(Boolean);
} }
export function upgradeGift({ export function upgradeStarGift({
inputSavedGift, inputSavedGift,
shouldKeepOriginalDetails, shouldKeepOriginalDetails,
}: { }: {
@ -709,6 +709,21 @@ export function upgradeGift({
}); });
} }
export function transferStarGift({
inputSavedGift,
toPeer,
}: {
inputSavedGift: ApiRequestInputSavedStarGift;
toPeer: ApiPeer;
}) {
return invokeRequest(new GramJs.payments.TransferStarGift({
stargift: buildInputSavedStarGift(inputSavedGift),
toId: buildInputPeer(toPeer.id, toPeer.accessHash),
}), {
shouldReturnTrue: true,
});
}
export async function fetchStarGiftWithdrawalUrl({ export async function fetchStarGiftWithdrawalUrl({
inputGift, inputGift,
password, password,

View File

@ -589,9 +589,16 @@ export type ApiInputInvoiceStarGiftUpgrade = {
shouldKeepOriginalDetails?: true; shouldKeepOriginalDetails?: true;
}; };
export type ApiInputInvoiceStarGiftTransfer = {
type: 'stargiftTransfer';
inputSavedGift: ApiInputSavedStarGift;
recipientId: string;
};
export type ApiInputInvoice = ApiInputInvoiceMessage | ApiInputInvoiceSlug | ApiInputInvoiceGiveaway export type ApiInputInvoice = ApiInputInvoiceMessage | ApiInputInvoiceSlug | ApiInputInvoiceGiveaway
| ApiInputInvoiceGiftCode | ApiInputInvoiceStars | ApiInputInvoiceStarsGift | ApiInputInvoiceStarGiftUpgrade | ApiInputInvoiceGiftCode | ApiInputInvoiceStars | ApiInputInvoiceStarsGift
| ApiInputInvoiceStarsGiveaway | ApiInputInvoiceStarGift | ApiInputInvoiceChatInviteSubscription; | ApiInputInvoiceStarsGiveaway | ApiInputInvoiceStarGift | ApiInputInvoiceChatInviteSubscription
| ApiInputInvoiceStarGiftUpgrade | ApiInputInvoiceStarGiftTransfer;
/* Used for Invoice request */ /* Used for Invoice request */
export type ApiRequestInputInvoiceMessage = { export type ApiRequestInputInvoiceMessage = {
@ -641,6 +648,13 @@ export type ApiRequestInputInvoiceStarGiftUpgrade = {
shouldKeepOriginalDetails?: true; shouldKeepOriginalDetails?: true;
}; };
export type ApiRequestInputInvoiceStarGiftTransfer = {
type: 'stargiftTransfer';
inputSavedGift: ApiRequestInputSavedStarGift;
recipient: ApiPeer;
};
export type ApiRequestInputInvoice = ApiRequestInputInvoiceMessage | ApiRequestInputInvoiceSlug export type ApiRequestInputInvoice = ApiRequestInputInvoiceMessage | ApiRequestInputInvoiceSlug
| ApiRequestInputInvoiceGiveaway | ApiRequestInputInvoiceStars | ApiRequestInputInvoiceStarsGiveaway | ApiRequestInputInvoiceGiveaway | ApiRequestInputInvoiceStars | ApiRequestInputInvoiceStarsGiveaway
| ApiRequestInputInvoiceChatInviteSubscription | ApiRequestInputInvoiceStarGift | ApiRequestInputInvoiceStarGiftUpgrade; | ApiRequestInputInvoiceChatInviteSubscription | ApiRequestInputInvoiceStarGift | ApiRequestInputInvoiceStarGiftUpgrade
| ApiRequestInputInvoiceStarGiftTransfer;

View File

@ -1380,6 +1380,7 @@
"GiftHideNameDescription" = "You can hide your name and message from visitors to {receiver}'s profile. {receiver} will still see your name and message."; "GiftHideNameDescription" = "You can hide your name and message from visitors to {receiver}'s profile. {receiver} will still see your name and message.";
"GiftHideNameDescriptionChannel" = "You can hide your name and message from all visitors of this channel except its admins."; "GiftHideNameDescriptionChannel" = "You can hide your name and message from all visitors of this channel except its admins.";
"GiftSend" = "Send a Gift for {amount}"; "GiftSend" = "Send a Gift for {amount}";
"GiftUnique" = "{title} #{number}";
"GiftInfoSent" = "Sent Gift"; "GiftInfoSent" = "Sent Gift";
"GiftInfoReceived" = "Received Gift"; "GiftInfoReceived" = "Received Gift";
"GiftInfoTitle" = "Gift"; "GiftInfoTitle" = "Gift";
@ -1436,7 +1437,15 @@
"GiftInfoViewUpgraded" = "View Upgraded Gift"; "GiftInfoViewUpgraded" = "View Upgraded Gift";
"GiftInfoUpgradeBadge" = "upgrade"; "GiftInfoUpgradeBadge" = "upgrade";
"GiftInfoUpgradeForFree" = "Upgrade For Free"; "GiftInfoUpgradeForFree" = "Upgrade For Free";
"GiftInfoWithdraw" = "Withdraw"; "GiftInfoTransfer" = "Transfer";
"GiftTransferTitle" = "Transfer";
"GiftTransferTON" = "Send via Blockchain";
"GiftTransferTONBlocked" = "unlocks in {time}";
"GiftTransferConfirmDescription" = "Do you want to transfer ownership of **{gift}** to **{peer}** for **{amount}**?";
"GiftTransferConfirmDescriptionFree" = "Do you want to transfer ownership of **{gift}** to **{peer}**?";
"GiftTransferConfirmButton" = "Transfer for {amount}";
"GiftTransferConfirmButtonFree" = "Transfer";
"GiftTransferSuccessMessage" = "You have successfully gifted {gift} to {peer}.";
"GiftUpgradeUniqueTitle" = "Unique"; "GiftUpgradeUniqueTitle" = "Unique";
"GiftUpgradeUniqueDescription" = "Turn your gift into a unique collectible that you can transfer or auction."; "GiftUpgradeUniqueDescription" = "Turn your gift into a unique collectible that you can transfer or auction.";
"GiftUpgradeTransferableTitle" = "Transferable"; "GiftUpgradeTransferableTitle" = "Transferable";

View File

@ -9,4 +9,5 @@ export { default as GiftModal } from '../components/modals/gift/GiftModal';
export { default as GiftRecipientPicker } from '../components/modals/gift/recipient/GiftRecipientPicker'; export { default as GiftRecipientPicker } from '../components/modals/gift/recipient/GiftRecipientPicker';
export { default as GiftInfoModal } from '../components/modals/gift/info/GiftInfoModal'; export { default as GiftInfoModal } from '../components/modals/gift/info/GiftInfoModal';
export { default as GiftUpgradeModal } from '../components/modals/gift/upgrade/GiftUpgradeModal'; export { default as GiftUpgradeModal } from '../components/modals/gift/upgrade/GiftUpgradeModal';
export { default as GiftWithdrawModal } from '../components/modals/gift/fragment/GiftWithdrawModal'; export { default as GiftWithdrawModal } from '../components/modals/gift/withdraw/GiftWithdrawModal';
export { default as GiftTransferModal } from '../components/modals/gift/transfer/GiftTransferModal';

View File

@ -7,17 +7,17 @@ import type { ThreadId } from '../../types';
import { API_CHAT_TYPES } from '../../config'; import { API_CHAT_TYPES } from '../../config';
import { import {
filterChatsByName,
filterUsersByName,
getCanPostInChat, getCanPostInChat,
isDeletedUser, isDeletedUser,
} from '../../global/helpers'; } from '../../global/helpers';
import { filterChatIdsByType } from '../../global/selectors'; import { filterPeersByQuery } from '../../global/helpers/peers';
import {
filterChatIdsByType, selectChat, selectChatFullInfo, selectUser,
} from '../../global/selectors';
import { unique } from '../../util/iteratees'; import { unique } from '../../util/iteratees';
import sortChatIds from './helpers/sortChatIds'; import sortChatIds from './helpers/sortChatIds';
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev'; import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
import useOldLang from '../../hooks/useOldLang';
import ChatOrUserPicker from './pickers/ChatOrUserPicker'; import ChatOrUserPicker from './pickers/ChatOrUserPicker';
@ -55,7 +55,6 @@ const RecipientPicker: FC<OwnProps & StateProps> = ({
onClose, onClose,
onCloseAnimationEnd, onCloseAnimationEnd,
}) => { }) => {
const lang = useOldLang();
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const ids = useMemo(() => { const ids = useMemo(() => {
if (!isOpen) return undefined; if (!isOpen) return undefined;
@ -67,34 +66,36 @@ const RecipientPicker: FC<OwnProps & StateProps> = ({
// No need for expensive global updates on users, so we avoid them // No need for expensive global updates on users, so we avoid them
const global = getGlobal(); const global = getGlobal();
const usersById = global.users.byId;
const chatsById = global.chats.byId;
const chatFullInfoById = global.chats.fullInfoById;
const chatIds = [ const peerIds = [
...(activeListIds || []), ...(activeListIds || []),
...((search && archivedListIds) || []), ...((search && archivedListIds) || []),
].filter((id) => { ].filter((id) => {
const chat = chatsById[id]; const chat = selectChat(global, id);
const user = usersById[id]; const user = selectUser(global, id);
if (user && isDeletedUser(user)) return false; if (user && isDeletedUser(user)) return false;
return chat && getCanPostInChat(chat, undefined, undefined, chatFullInfoById[id]); const chatFullInfo = selectChatFullInfo(global, id);
return chat && chatFullInfo && getCanPostInChat(chat, undefined, undefined, chatFullInfo);
}); });
const sorted = sortChatIds( const sorted = sortChatIds(
unique([ filterPeersByQuery({
...(currentUserId ? [currentUserId] : []), ids: unique([
...filterChatsByName(lang, chatIds, chatsById, search, currentUserId), ...(currentUserId ? [currentUserId] : []),
...(contactIds && filter.includes('users') ? filterUsersByName(contactIds, usersById, search) : []), ...peerIds,
]), ...(contactIds || []),
]),
query: search,
}),
undefined, undefined,
priorityIds, priorityIds,
currentUserId, currentUserId,
); );
return filterChatIdsByType(global, sorted, filter); return filterChatIdsByType(global, sorted, filter);
}, [pinnedIds, currentUserId, activeListIds, search, archivedListIds, lang, contactIds, filter, isOpen]); }, [pinnedIds, currentUserId, activeListIds, search, archivedListIds, contactIds, filter, isOpen]);
const renderingIds = useCurrentOrPrev(ids, true)!; const renderingIds = useCurrentOrPrev(ids, true)!;

View File

@ -31,18 +31,18 @@ import PickerItem from './PickerItem';
import styles from './PickerStyles.module.scss'; import styles from './PickerStyles.module.scss';
type SingleModeProps = { type SingleModeProps<CategoryType extends string> = {
allowMultiple?: false; allowMultiple?: false;
itemInputType?: 'radio'; itemInputType?: 'radio';
selectedId?: string; selectedId?: string;
selectedIds?: never; // Help TS to throw an error if this is passed selectedIds?: never; // Help TS to throw an error if this is passed
selectedCategory?: CustomPeerType; selectedCategory?: CategoryType;
selectedCategories?: never; selectedCategories?: never;
onSelectedCategoryChange?: (category: CustomPeerType) => void; onSelectedCategoryChange?: (category: CategoryType) => void;
onSelectedIdChange?: (id: string) => void; onSelectedIdChange?: (id: string) => void;
}; };
type MultipleModeProps = { type MultipleModeProps<CategoryType extends string> = {
allowMultiple: true; allowMultiple: true;
itemInputType: 'checkbox'; itemInputType: 'checkbox';
selectedId?: never; selectedId?: never;
@ -50,14 +50,14 @@ type MultipleModeProps = {
lockedSelectedIds?: string[]; lockedSelectedIds?: string[];
lockedUnselectedIds?: string[]; lockedUnselectedIds?: string[];
selectedCategory?: never; selectedCategory?: never;
selectedCategories?: CustomPeerType[]; selectedCategories?: CategoryType[];
onSelectedCategoriesChange?: (categories: CustomPeerType[]) => void; onSelectedCategoriesChange?: (categories: CategoryType[]) => void;
onSelectedIdsChange?: (Ids: string[]) => void; onSelectedIdsChange?: (Ids: string[]) => void;
}; };
type OwnProps = { type OwnProps<CategoryType extends string> = {
className?: string; className?: string;
categories?: UniqueCustomPeer[]; categories?: UniqueCustomPeer<CategoryType>[];
itemIds: string[]; itemIds: string[];
lockedUnselectedSubtitle?: string; lockedUnselectedSubtitle?: string;
filterValue?: string; filterValue?: string;
@ -73,11 +73,12 @@ type OwnProps = {
isViewOnly?: boolean; isViewOnly?: boolean;
withStatus?: boolean; withStatus?: boolean;
withPeerTypes?: boolean; withPeerTypes?: boolean;
withPeerUsernames?: boolean;
withDefaultPadding?: boolean; withDefaultPadding?: boolean;
onFilterChange?: (value: string) => void; onFilterChange?: (value: string) => void;
onDisabledClick?: (id: string, isSelected: boolean) => void; onDisabledClick?: (id: string, isSelected: boolean) => void;
onLoadMore?: () => void; onLoadMore?: () => void;
} & (SingleModeProps | MultipleModeProps); } & (SingleModeProps<CategoryType> | MultipleModeProps<CategoryType>);
// Focus slows down animation, also it breaks transition layout in Chrome // Focus slows down animation, also it breaks transition layout in Chrome
const FOCUS_DELAY_MS = 500; const FOCUS_DELAY_MS = 500;
@ -87,7 +88,7 @@ const ALWAYS_FULL_ITEMS_COUNT = 5;
const ITEM_CLASS_NAME = 'PeerPickerItem'; const ITEM_CLASS_NAME = 'PeerPickerItem';
const PeerPicker = ({ const PeerPicker = <CategoryType extends string = CustomPeerType>({
className, className,
categories, categories,
itemIds, itemIds,
@ -106,12 +107,13 @@ const PeerPicker = ({
itemInputType, itemInputType,
withStatus, withStatus,
withPeerTypes, withPeerTypes,
withPeerUsernames,
withDefaultPadding, withDefaultPadding,
onFilterChange, onFilterChange,
onDisabledClick, onDisabledClick,
onLoadMore, onLoadMore,
...optionalProps ...optionalProps
}: OwnProps) => { }: OwnProps<CategoryType>) => {
const lang = useOldLang(); const lang = useOldLang();
const allowMultiple = optionalProps.allowMultiple; const allowMultiple = optionalProps.allowMultiple;
@ -268,7 +270,16 @@ const PeerPicker = ({
function getSubtitle() { function getSubtitle() {
if (isAlwaysUnselected) return [lockedUnselectedSubtitle]; if (isAlwaysUnselected) return [lockedUnselectedSubtitle];
if (withStatus && peer) { if (!peer) return undefined;
if (withPeerUsernames) {
const username = peer.usernames?.[0]?.username;
if (username) {
return [`@${username}`];
}
}
if (withStatus) {
if (isApiPeerChat(peer)) { if (isApiPeerChat(peer)) {
return [getGroupStatus(lang, peer)]; return [getGroupStatus(lang, peer)];
} }
@ -279,10 +290,12 @@ const PeerPicker = ({
buildClassName(isUserOnline(peer, userStatus, true) && styles.onlineStatus), buildClassName(isUserOnline(peer, userStatus, true) && styles.onlineStatus),
]; ];
} }
if (withPeerTypes && peer) {
if (withPeerTypes) {
const langKey = getPeerTypeKey(peer); const langKey = getPeerTypeKey(peer);
return langKey && [lang(langKey)]; return langKey && [lang(langKey)];
} }
return undefined; return undefined;
} }
@ -316,7 +329,7 @@ const PeerPicker = ({
}, [ }, [
categoriesByType, forceShowSelf, isViewOnly, itemClassName, itemInputType, lang, lockedSelectedIdsSet, categoriesByType, forceShowSelf, isViewOnly, itemClassName, itemInputType, lang, lockedSelectedIdsSet,
lockedUnselectedIdsSet, lockedUnselectedSubtitle, onDisabledClick, selectedCategories, selectedIds, lockedUnselectedIdsSet, lockedUnselectedSubtitle, onDisabledClick, selectedCategories, selectedIds,
withPeerTypes, withStatus, withPeerTypes, withStatus, withPeerUsernames,
]); ]);
const beforeChildren = useMemo(() => { const beforeChildren = useMemo(() => {

View File

@ -34,11 +34,12 @@
.pickerCategoryTitle { .pickerCategoryTitle {
color: var(--color-text-secondary); color: var(--color-text-secondary);
padding-inline: 0.5rem; padding-inline: 0.5rem;
margin-bottom: 0.5rem;
font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium);
&:not(:first-child) { &:not(:first-child) {
border-top: 1px solid var(--color-borders); border-top: 1px solid var(--color-borders);
padding-top: 0.75rem; padding-top: 0.5rem;
margin-top: 0.375rem; margin-top: 0.375rem;
} }
} }

View File

@ -5,7 +5,8 @@ import { getActions, withGlobal } from '../../../global';
import type { ApiUser, ApiUserStatus } from '../../../api/types'; import type { ApiUser, ApiUserStatus } from '../../../api/types';
import { StoryViewerOrigin } from '../../../types'; import { StoryViewerOrigin } from '../../../types';
import { filterUsersByName, sortUserIds } from '../../../global/helpers'; import { sortUserIds } from '../../../global/helpers';
import { filterPeersByQuery } from '../../../global/helpers/peers';
import useAppLayout from '../../../hooks/useAppLayout'; import useAppLayout from '../../../hooks/useAppLayout';
import useHistoryBack from '../../../hooks/useHistoryBack'; import useHistoryBack from '../../../hooks/useHistoryBack';
@ -61,7 +62,7 @@ const ContactList: FC<OwnProps & StateProps> = ({
return undefined; return undefined;
} }
const filteredIds = filterUsersByName(contactIds, usersById, filter); const filteredIds = filterPeersByQuery({ ids: contactIds, query: filter, type: 'user' });
return sortUserIds(filteredIds, usersById, userStatusesById); return sortUserIds(filteredIds, usersById, userStatusesById);
}, [contactIds, filter, usersById, userStatusesById]); }, [contactIds, filter, usersById, userStatusesById]);

View File

@ -2,7 +2,8 @@ import type { FC } from '../../../lib/teact/teact';
import React, { memo, useCallback, useMemo } from '../../../lib/teact/teact'; import React, { memo, useCallback, useMemo } from '../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../global'; import { getActions, getGlobal, withGlobal } from '../../../global';
import { filterUsersByName, isUserBot } from '../../../global/helpers'; import { isUserBot } from '../../../global/helpers';
import { filterPeersByQuery } from '../../../global/helpers/peers';
import { selectTabState } from '../../../global/selectors'; import { selectTabState } from '../../../global/selectors';
import { unique } from '../../../util/iteratees'; import { unique } from '../../../util/iteratees';
import sortChatIds from '../../common/helpers/sortChatIds'; import sortChatIds from '../../common/helpers/sortChatIds';
@ -63,7 +64,8 @@ const NewChatStep1: FC<OwnProps & StateProps> = ({
const displayedIds = useMemo(() => { const displayedIds = useMemo(() => {
// No need for expensive global updates on users, so we avoid them // No need for expensive global updates on users, so we avoid them
const usersById = getGlobal().users.byId; const usersById = getGlobal().users.byId;
const foundContactIds = localContactIds ? filterUsersByName(localContactIds, usersById, searchQuery) : []; const foundContactIds = localContactIds
? filterPeersByQuery({ ids: localContactIds, query: searchQuery, type: 'user' }) : [];
return sortChatIds( return sortChatIds(
unique([ unique([

View File

@ -3,12 +3,12 @@ import React, {
memo, useCallback, useMemo, useRef, memo, useCallback, useMemo, useRef,
useState, useState,
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import { LoadMoreDirection } from '../../../types'; import { LoadMoreDirection } from '../../../types';
import { SLIDE_TRANSITION_DURATION } from '../../../config'; import { SLIDE_TRANSITION_DURATION } from '../../../config';
import { filterUsersByName } from '../../../global/helpers'; import { filterPeersByQuery } from '../../../global/helpers/peers';
import { selectTabState } from '../../../global/selectors'; import { selectTabState } from '../../../global/selectors';
import { throttle } from '../../../util/schedulers'; import { throttle } from '../../../util/schedulers';
@ -58,8 +58,7 @@ const BotAppResults: FC<OwnProps & StateProps> = ({
const recentSet = new Set(recentBotIds); const recentSet = new Set(recentBotIds);
const withoutRecent = foundIds.filter((id) => !recentSet.has(id)); const withoutRecent = foundIds.filter((id) => !recentSet.has(id));
const usersById = getGlobal().users.byId; return filterPeersByQuery({ ids: withoutRecent, query: searchQuery, type: 'user' });
return filterUsersByName(withoutRecent, usersById, searchQuery);
}, [foundIds, recentBotIds, searchQuery]); }, [foundIds, recentBotIds, searchQuery]);
const handleChatClick = useLastCallback((id: string) => { const handleChatClick = useLastCallback((id: string) => {

View File

@ -10,10 +10,9 @@ import { LoadMoreDirection } from '../../../types';
import { ALL_FOLDER_ID, GLOBAL_SUGGESTED_CHANNELS_ID } from '../../../config'; import { ALL_FOLDER_ID, GLOBAL_SUGGESTED_CHANNELS_ID } from '../../../config';
import { import {
filterChatsByName,
filterUsersByName,
isChatChannel, isChatChannel,
} from '../../../global/helpers'; } from '../../../global/helpers';
import { filterPeersByQuery } from '../../../global/helpers/peers';
import { selectSimilarChannelIds, selectTabState } from '../../../global/selectors'; import { selectSimilarChannelIds, selectTabState } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { getOrderedIds } from '../../../util/folderManager'; import { getOrderedIds } from '../../../util/folderManager';
@ -221,7 +220,6 @@ const ChatResults: FC<OwnProps & StateProps> = ({
} }
// No need for expensive global updates, so we avoid them // No need for expensive global updates, so we avoid them
const usersById = getGlobal().users.byId;
const chatsById = getGlobal().chats.byId; const chatsById = getGlobal().chats.byId;
const orderedChatIds = getOrderedIds(ALL_FOLDER_ID) ?? []; const orderedChatIds = getOrderedIds(ALL_FOLDER_ID) ?? [];
@ -230,7 +228,7 @@ const ChatResults: FC<OwnProps & StateProps> = ({
const chat = chatsById[id]; const chat = chatsById[id];
return chat && isChatChannel(chat); return chat && isChatChannel(chat);
}); });
const localChatIds = filterChatsByName(oldLang, filteredChatIds, chatsById, searchQuery, currentUserId); const localChatIds = filterPeersByQuery({ ids: filteredChatIds, query: searchQuery, type: 'chat' });
if (isChannelList) return localChatIds; if (isChannelList) return localChatIds;
@ -239,9 +237,9 @@ const ChatResults: FC<OwnProps & StateProps> = ({
...(contactIds || []), ...(contactIds || []),
]; ];
const localContactIds = filterUsersByName( const localContactIds = filterPeersByQuery({
contactIdsWithMe, usersById, searchQuery, currentUserId, oldLang('SavedMessages'), ids: contactIdsWithMe, query: searchQuery, type: 'user',
); });
const localPeerIds = [ const localPeerIds = [
...localContactIds, ...localContactIds,
@ -252,7 +250,7 @@ const ChatResults: FC<OwnProps & StateProps> = ({
...sortChatIds(localPeerIds, undefined, currentUserId ? [currentUserId] : undefined), ...sortChatIds(localPeerIds, undefined, currentUserId ? [currentUserId] : undefined),
...sortChatIds(accountPeerIds || []), ...sortChatIds(accountPeerIds || []),
]); ]);
}, [searchQuery, oldLang, currentUserId, contactIds, accountPeerIds, isChannelList]); }, [searchQuery, currentUserId, contactIds, accountPeerIds, isChannelList]);
useHorizontalScroll(chatSelectionRef, !localResults.length || isChannelList, true); useHorizontalScroll(chatSelectionRef, !localResults.length || isChannelList, true);

View File

@ -7,7 +7,8 @@ import { getActions, withGlobal } from '../../../global';
import type { ApiUser } from '../../../api/types'; import type { ApiUser } from '../../../api/types';
import { filterUsersByName, getUserFullName } from '../../../global/helpers'; import { getUserFullName } from '../../../global/helpers';
import { filterPeersByQuery } from '../../../global/helpers/peers';
import { selectTabState } from '../../../global/selectors'; import { selectTabState } from '../../../global/selectors';
import { unique } from '../../../util/iteratees'; import { unique } from '../../../util/iteratees';
@ -57,7 +58,7 @@ const BlockUserModal: FC<OwnProps & StateProps> = ({
return contactId !== currentUserId && !blockedIds.includes(contactId); return contactId !== currentUserId && !blockedIds.includes(contactId);
})); }));
return filterUsersByName(availableContactIds, usersById, search) return filterPeersByQuery({ ids: availableContactIds, query: search, type: 'user' })
.sort((firstId, secondId) => { .sort((firstId, secondId) => {
const firstName = getUserFullName(usersById[firstId]) || ''; const firstName = getUserFullName(usersById[firstId]) || '';
const secondName = getUserFullName(usersById[secondId]) || ''; const secondName = getUserFullName(usersById[secondId]) || '';

View File

@ -11,8 +11,9 @@ import { SettingsScreens } from '../../../types';
import { ALL_FOLDER_ID, ARCHIVED_FOLDER_ID, SERVICE_NOTIFICATIONS_USER_ID } from '../../../config'; import { ALL_FOLDER_ID, ARCHIVED_FOLDER_ID, SERVICE_NOTIFICATIONS_USER_ID } from '../../../config';
import { import {
filterChatsByName, isChatChannel, isDeletedUser, isChatChannel, isDeletedUser,
} from '../../../global/helpers'; } from '../../../global/helpers';
import { filterPeersByQuery } from '../../../global/helpers/peers';
import { unique } from '../../../util/iteratees'; import { unique } from '../../../util/iteratees';
import { CUSTOM_PEER_PREMIUM } from '../../../util/objects/customPeer'; import { CUSTOM_PEER_PREMIUM } from '../../../util/objects/customPeer';
import { getPrivacyKey } from './helpers/privacy'; import { getPrivacyKey } from './helpers/privacy';
@ -122,16 +123,16 @@ const SettingsPrivacyVisibilityExceptionList: FC<OwnProps & StateProps> = ({
return chatId !== currentUserId && chatId !== SERVICE_NOTIFICATIONS_USER_ID && !isChannel && !isDeleted; return chatId !== currentUserId && chatId !== SERVICE_NOTIFICATIONS_USER_ID && !isChannel && !isDeleted;
}); });
const filteredChats = filterChatsByName(oldLang, chatIds, chatsById, searchQuery); const filteredChats = filterPeersByQuery({ ids: chatIds, query: searchQuery });
// Show only relevant items // Show only relevant items
if (searchQuery) return filteredChats; if (searchQuery) return filteredChats;
return unique([ return unique([
...selectedContactIds, ...selectedContactIds,
...filterChatsByName(oldLang, chatIds, chatsById, searchQuery), ...chatIds,
]); ]);
}, [folderAllOrderedIds, folderArchivedOrderedIds, selectedContactIds, oldLang, searchQuery, currentUserId]); }, [folderAllOrderedIds, folderArchivedOrderedIds, selectedContactIds, searchQuery, currentUserId]);
const handleSelectedCategoriesChange = useCallback((value: CustomPeerType[]) => { const handleSelectedCategoriesChange = useCallback((value: CustomPeerType[]) => {
setNewSelectedCategoryTypes(value); setNewSelectedCategoryTypes(value);

View File

@ -2,12 +2,12 @@ import type { FC } from '../../../../lib/teact/teact';
import React, { import React, {
memo, useEffect, useMemo, useState, memo, useEffect, useMemo, useState,
} from '../../../../lib/teact/teact'; } from '../../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../../global'; import { getActions, withGlobal } from '../../../../global';
import type { FolderEditDispatch, FoldersState } from '../../../../hooks/reducers/useFoldersReducer'; import type { FolderEditDispatch, FoldersState } from '../../../../hooks/reducers/useFoldersReducer';
import { ALL_FOLDER_ID, ARCHIVED_FOLDER_ID } from '../../../../config'; import { ALL_FOLDER_ID, ARCHIVED_FOLDER_ID } from '../../../../config';
import { filterChatsByName } from '../../../../global/helpers'; import { filterPeersByQuery } from '../../../../global/helpers/peers';
import { selectCurrentLimit } from '../../../../global/selectors/limits'; import { selectCurrentLimit } from '../../../../global/selectors/limits';
import { unique } from '../../../../util/iteratees'; import { unique } from '../../../../util/iteratees';
import { CUSTOM_PEER_EXCLUDED_CHAT_TYPES, CUSTOM_PEER_INCLUDED_CHAT_TYPES } from '../../../../util/objects/customPeer'; import { CUSTOM_PEER_EXCLUDED_CHAT_TYPES, CUSTOM_PEER_INCLUDED_CHAT_TYPES } from '../../../../util/objects/customPeer';
@ -67,14 +67,11 @@ const SettingsFoldersChatFilters: FC<OwnProps & StateProps> = ({
}, [isActive]); }, [isActive]);
const displayedIds = useMemo(() => { const displayedIds = useMemo(() => {
// No need for expensive global updates on chats, so we avoid them
const chatsById = getGlobal().chats.byId;
const chatIds = [...folderAllOrderedIds || [], ...folderArchivedOrderedIds || []]; const chatIds = [...folderAllOrderedIds || [], ...folderArchivedOrderedIds || []];
return unique([ return unique([
...filterChatsByName(lang, chatIds, chatsById, chatFilter), ...filterPeersByQuery({ ids: chatIds, query: chatFilter, type: 'chat' }),
]); ]);
}, [folderAllOrderedIds, folderArchivedOrderedIds, lang, chatFilter]); }, [folderAllOrderedIds, folderArchivedOrderedIds, chatFilter]);
const handleFilterChange = useLastCallback((newFilter: string) => { const handleFilterChange = useLastCallback((newFilter: string) => {
dispatch({ dispatch({

View File

@ -4,8 +4,9 @@ import React, {
import { getActions, getGlobal } from '../../../global'; import { getActions, getGlobal } from '../../../global';
import { import {
filterChatsByName, isChatChannel, isChatPublic, isChatSuperGroup, isChatChannel, isChatPublic, isChatSuperGroup,
} from '../../../global/helpers'; } from '../../../global/helpers';
import { filterPeersByQuery } from '../../../global/helpers/peers';
import { unique } from '../../../util/iteratees'; import { unique } from '../../../util/iteratees';
import sortChatIds from '../../common/helpers/sortChatIds'; import sortChatIds from '../../common/helpers/sortChatIds';
@ -61,13 +62,12 @@ const GiveawayChannelPickerModal = ({
}, [giveawayChatId]); }, [giveawayChatId]);
const displayedChannelIds = useMemo(() => { const displayedChannelIds = useMemo(() => {
const chatsById = getGlobal().chats.byId; const foundChannelIds = channelIds ? filterPeersByQuery({ ids: channelIds, query: searchQuery, type: 'chat' }) : [];
const foundChannelIds = channelIds ? filterChatsByName(lang, channelIds, chatsById, searchQuery) : [];
return sortChatIds(foundChannelIds, return sortChatIds(foundChannelIds,
false, false,
selectedIds); selectedIds);
}, [channelIds, lang, searchQuery, selectedIds]); }, [channelIds, searchQuery, selectedIds]);
const handleSelectedChannelIdsChange = useLastCallback((newSelectedIds: string[]) => { const handleSelectedChannelIdsChange = useLastCallback((newSelectedIds: string[]) => {
const chatsById = getGlobal().chats.byId; const chatsById = getGlobal().chats.byId;

View File

@ -6,10 +6,10 @@ import { getActions, getGlobal, withGlobal } from '../../../global';
import type { ApiChatMember } from '../../../api/types'; import type { ApiChatMember } from '../../../api/types';
import { import {
filterUsersByName,
isUserBot, isUserBot,
sortUserIds, sortUserIds,
} from '../../../global/helpers'; } from '../../../global/helpers';
import { filterPeersByQuery } from '../../../global/helpers/peers';
import { selectChatFullInfo } from '../../../global/selectors'; import { selectChatFullInfo } from '../../../global/selectors';
import { unique } from '../../../util/iteratees'; import { unique } from '../../../util/iteratees';
import sortChatIds from '../../common/helpers/sortChatIds'; import sortChatIds from '../../common/helpers/sortChatIds';
@ -74,9 +74,10 @@ const GiveawayUserPickerModal = ({
const displayedMemberIds = useMemo(() => { const displayedMemberIds = useMemo(() => {
const usersById = getGlobal().users.byId; const usersById = getGlobal().users.byId;
const filteredContactIds = memberIds ? filterUsersByName(memberIds, usersById, searchQuery) : []; const filteredUserIds = memberIds
? filterPeersByQuery({ ids: memberIds, query: searchQuery, type: 'user' }) : [];
return sortChatIds(unique(filteredContactIds).filter((userId) => { return sortChatIds(unique(filteredUserIds).filter((userId) => {
const user = usersById[userId]; const user = usersById[userId];
if (!user) { if (!user) {
return true; return true;

View File

@ -7,8 +7,9 @@ import { getActions, getGlobal, withGlobal } from '../../../global';
import { SERVICE_NOTIFICATIONS_USER_ID } from '../../../config'; import { SERVICE_NOTIFICATIONS_USER_ID } from '../../../config';
import { import {
filterUsersByName, isDeletedUser, isUserBot, isDeletedUser, isUserBot,
} from '../../../global/helpers'; } from '../../../global/helpers';
import { filterPeersByQuery } from '../../../global/helpers/peers';
import { unique } from '../../../util/iteratees'; import { unique } from '../../../util/iteratees';
import sortChatIds from '../../common/helpers/sortChatIds'; import sortChatIds from '../../common/helpers/sortChatIds';
@ -46,15 +47,17 @@ const StarsGiftingPickerModal: FC<OwnProps & StateProps> = ({
const displayedUserIds = useMemo(() => { const displayedUserIds = useMemo(() => {
const usersById = getGlobal().users.byId; const usersById = getGlobal().users.byId;
const combinedIds = [ const combinedIds = unique([
...(userIds || []), ...(userIds || []),
...(activeListIds || []), ...(activeListIds || []),
...(archivedListIds || []), ...(archivedListIds || []),
]; ]);
const filteredContactIds = filterUsersByName(combinedIds, usersById, searchQuery); const filteredUserIds = filterPeersByQuery({
ids: combinedIds, query: searchQuery, type: 'user',
});
return sortChatIds(unique(filteredContactIds).filter((id) => { return sortChatIds(filteredUserIds.filter((id) => {
const user = usersById[id]; const user = usersById[id];
if (!user) { if (!user) {

View File

@ -7,7 +7,8 @@ import type { Signal } from '../../../../util/signals';
import { ApiMessageEntityTypes } from '../../../../api/types'; import { ApiMessageEntityTypes } from '../../../../api/types';
import { requestNextMutation } from '../../../../lib/fasterdom/fasterdom'; import { requestNextMutation } from '../../../../lib/fasterdom/fasterdom';
import { filterUsersByName, getMainUsername, getUserFirstOrLastName } from '../../../../global/helpers'; import { getMainUsername, getUserFirstOrLastName } from '../../../../global/helpers';
import { filterPeersByQuery } from '../../../../global/helpers/peers';
import focusEditableElement from '../../../../util/focusEditableElement'; import focusEditableElement from '../../../../util/focusEditableElement';
import { pickTruthy, unique } from '../../../../util/iteratees'; import { pickTruthy, unique } from '../../../../util/iteratees';
import { getCaretPosition, getHtmlBeforeSelection, setCaretPosition } from '../../../../util/selection'; import { getCaretPosition, getHtmlBeforeSelection, setCaretPosition } from '../../../../util/selection';
@ -82,10 +83,14 @@ export default function useMentionTooltip(
}, []); }, []);
const filter = usernameTag.substring(1); const filter = usernameTag.substring(1);
const filteredIds = filterUsersByName(unique([ const filteredIds = filterPeersByQuery({
...((getWithInlineBots() && topInlineBotIds) || []), ids: unique([
...(memberIds || []), ...((getWithInlineBots() && topInlineBotIds) || []),
]), usersById, filter); ...(memberIds || []),
]),
query: filter,
type: 'user',
});
setFilteredUsers(Object.values(pickTruthy(usersById, filteredIds))); setFilteredUsers(Object.values(pickTruthy(usersById, filteredIds)));
}, [currentUserId, groupChatMembers, topInlineBotIds, getUsernameTag, getWithInlineBots]); }, [currentUserId, groupChatMembers, topInlineBotIds, getUsernameTag, getWithInlineBots]);

View File

@ -15,11 +15,12 @@ import ChatInviteModal from './chatInvite/ChatInviteModal.async';
import ChatlistModal from './chatlist/ChatlistModal.async'; import ChatlistModal from './chatlist/ChatlistModal.async';
import CollectibleInfoModal from './collectible/CollectibleInfoModal.async'; import CollectibleInfoModal from './collectible/CollectibleInfoModal.async';
import EmojiStatusAccessModal from './emojiStatusAccess/EmojiStatusAccessModal.async'; import EmojiStatusAccessModal from './emojiStatusAccess/EmojiStatusAccessModal.async';
import GiftWithdrawModal from './gift/fragment/GiftWithdrawModal.async';
import PremiumGiftModal from './gift/GiftModal.async'; import PremiumGiftModal from './gift/GiftModal.async';
import GiftInfoModal from './gift/info/GiftInfoModal.async'; import GiftInfoModal from './gift/info/GiftInfoModal.async';
import GiftRecipientPicker from './gift/recipient/GiftRecipientPicker.async'; import GiftRecipientPicker from './gift/recipient/GiftRecipientPicker.async';
import GiftTransferModal from './gift/transfer/GiftTransferModal.async';
import GiftUpgradeModal from './gift/upgrade/GiftUpgradeModal.async'; import GiftUpgradeModal from './gift/upgrade/GiftUpgradeModal.async';
import GiftWithdrawModal from './gift/withdraw/GiftWithdrawModal.async';
import GiftCodeModal from './giftcode/GiftCodeModal.async'; import GiftCodeModal from './giftcode/GiftCodeModal.async';
import InviteViaLinkModal from './inviteViaLink/InviteViaLinkModal.async'; import InviteViaLinkModal from './inviteViaLink/InviteViaLinkModal.async';
import LocationAccessModal from './locationAccess/LocationAccessModal.async'; import LocationAccessModal from './locationAccess/LocationAccessModal.async';
@ -69,7 +70,8 @@ type ModalKey = keyof Pick<TabState,
'aboutAdsModal' | 'aboutAdsModal' |
'giftUpgradeModal' | 'giftUpgradeModal' |
'monetizationVerificationModal' | 'monetizationVerificationModal' |
'giftWithdrawModal' 'giftWithdrawModal' |
'giftTransferModal'
>; >;
type StateProps = { type StateProps = {
@ -115,6 +117,7 @@ const MODALS: ModalRegistry = {
giftUpgradeModal: GiftUpgradeModal, giftUpgradeModal: GiftUpgradeModal,
monetizationVerificationModal: VerificationMonetizationModal, monetizationVerificationModal: VerificationMonetizationModal,
giftWithdrawModal: GiftWithdrawModal, giftWithdrawModal: GiftWithdrawModal,
giftTransferModal: GiftTransferModal,
}; };
const MODAL_KEYS = Object.keys(MODALS) as ModalKey[]; const MODAL_KEYS = Object.keys(MODALS) as ModalKey[];
const MODAL_ENTRIES = Object.entries(MODALS) as Entries<ModalRegistry>; const MODAL_ENTRIES = Object.entries(MODALS) as Entries<ModalRegistry>;

View File

@ -72,7 +72,7 @@ const GiftInfoModal = ({
openGiftUpgradeModal, openGiftUpgradeModal,
showNotification, showNotification,
openChatWithDraft, openChatWithDraft,
openGiftWithdrawModal, openGiftTransferModal,
} = getActions(); } = getActions();
const [isConvertConfirmOpen, openConvertConfirm, closeConvertConfirm] = useFlag(); const [isConvertConfirmOpen, openConvertConfirm, closeConvertConfirm] = useFlag();
@ -128,9 +128,9 @@ const GiftInfoModal = ({
handleClose(); handleClose();
}); });
const handleWithdraw = useLastCallback(() => { const handleTransfer = useLastCallback(() => {
if (savedGift?.gift.type !== 'starGiftUnique') return; if (savedGift?.gift.type !== 'starGiftUnique') return;
openGiftWithdrawModal({ gift: savedGift }); openGiftTransferModal({ gift: savedGift });
}); });
const handleFocusUpgraded = useLastCallback(() => { const handleFocusUpgraded = useLastCallback(() => {
@ -298,8 +298,8 @@ const GiftInfoModal = ({
{lang('Share')} {lang('Share')}
</MenuItem> </MenuItem>
{canUpdate && isUniqueGift && ( {canUpdate && isUniqueGift && (
<MenuItem icon="diamond" onClick={handleWithdraw}> <MenuItem icon="diamond" onClick={handleTransfer}>
{lang('GiftInfoWithdraw')} {lang('GiftInfoTransfer')}
</MenuItem> </MenuItem>
)} )}
</DropdownMenu> </DropdownMenu>

View File

@ -1,12 +1,10 @@
import type { FC } from '../../../../lib/teact/teact';
import React, { import React, {
memo, useMemo, useState, memo, useMemo, useState,
} from '../../../../lib/teact/teact'; } from '../../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../../global'; import { getActions, getGlobal, withGlobal } from '../../../../global';
import { import { filterPeersByQuery } from '../../../../global/helpers/peers';
filterUsersByName, isUserBot, import { selectCanGift } from '../../../../global/selectors';
} from '../../../../global/helpers';
import { unique } from '../../../../util/iteratees'; import { unique } from '../../../../util/iteratees';
import sortChatIds from '../../../common/helpers/sortChatIds'; import sortChatIds from '../../../common/helpers/sortChatIds';
@ -24,15 +22,14 @@ export type OwnProps = {
interface StateProps { interface StateProps {
currentUserId?: string; currentUserId?: string;
userSelectionLimit?: number;
userIds?: string[]; userIds?: string[];
} }
const GiftRecipientPicker: FC<OwnProps & StateProps> = ({ const GiftRecipientPicker = ({
modal, modal,
currentUserId, currentUserId,
userIds, userIds,
}) => { }: OwnProps & StateProps) => {
const { closeGiftRecipientPicker, openGiftModal } = getActions(); const { closeGiftRecipientPicker, openGiftModal } = getActions();
const oldLang = useOldLang(); const oldLang = useOldLang();
@ -41,17 +38,12 @@ const GiftRecipientPicker: FC<OwnProps & StateProps> = ({
const [searchQuery, setSearchQuery] = useState<string>(''); const [searchQuery, setSearchQuery] = useState<string>('');
const displayedUserIds = useMemo(() => { const displayedUserIds = useMemo(() => {
const usersById = getGlobal().users.byId; const global = getGlobal();
const idsWithSelf = userIds ? userIds.concat(currentUserId!) : undefined; const idsWithSelf = userIds ? userIds.concat(currentUserId!) : undefined;
const filteredContactIds = idsWithSelf ? filterUsersByName(idsWithSelf, usersById, searchQuery) : []; const filteredPeerIds = idsWithSelf ? filterPeersByQuery({ ids: idsWithSelf, query: searchQuery }) : [];
return sortChatIds(unique(filteredContactIds).filter((userId) => { return sortChatIds(unique(filteredPeerIds).filter((peerId) => {
const user = usersById[userId]; return selectCanGift(global, peerId);
if (!user) {
return true;
}
return !isUserBot(user);
}), undefined, [currentUserId!]); }), undefined, [currentUserId!]);
}, [currentUserId, searchQuery, userIds]); }, [currentUserId, searchQuery, userIds]);
@ -92,6 +84,5 @@ export default memo(withGlobal<OwnProps>((global): StateProps => {
return { return {
currentUserId, currentUserId,
userIds: global.contactList?.userIds, userIds: global.contactList?.userIds,
userSelectionLimit: global.appConfig?.giveawayAddPeersMax,
}; };
})(GiftRecipientPicker)); })(GiftRecipientPicker));

View File

@ -0,0 +1,18 @@
import type { FC } from '../../../../lib/teact/teact';
import React from '../../../../lib/teact/teact';
import type { OwnProps } from './GiftTransferModal';
import { Bundles } from '../../../../util/moduleLoader';
import useModuleLoader from '../../../../hooks/useModuleLoader';
const GiftTransferModalAsync: FC<OwnProps> = (props) => {
const { modal } = props;
const GiftTransferModal = useModuleLoader(Bundles.Stars, 'GiftTransferModal', !modal);
// eslint-disable-next-line react/jsx-props-no-spreading
return GiftTransferModal ? <GiftTransferModal {...props} /> : undefined;
};
export default GiftTransferModalAsync;

View File

@ -0,0 +1,31 @@
.header {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
margin-top: 0.5rem;
margin-bottom: 1rem;
}
.giftPreview {
width: 4rem;
height: 4rem;
position: relative;
display: grid;
place-items: center;
overflow: hidden;
border-radius: 0.625rem;
}
.backdrop {
position: absolute;
inset: 0;
}
.arrow {
font-size: 2rem;
color: var(--color-text-secondary);
}

View File

@ -0,0 +1,232 @@
import React, {
memo, useEffect, useMemo, useState,
} from '../../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../../global';
import type { ApiStarGiftUnique } from '../../../../api/types';
import type { TabState } from '../../../../global/types';
import type { UniqueCustomPeer } from '../../../../types';
import { ALL_FOLDER_ID } from '../../../../config';
import { getPeerTitle } from '../../../../global/helpers';
import { selectCanGift, selectPeer } from '../../../../global/selectors';
import { unique } from '../../../../util/iteratees';
import { formatStarsAsIcon, formatStarsAsText } from '../../../../util/localization/format';
import { MEMO_EMPTY_ARRAY } from '../../../../util/memo';
import { getGiftAttributes } from '../../../common/helpers/gifts';
import { REM } from '../../../common/helpers/mediaDimensions';
import sortChatIds from '../../../common/helpers/sortChatIds';
import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev';
import { useFolderManagerForOrderedIds } from '../../../../hooks/useFolderManager';
import useLang from '../../../../hooks/useLang';
import useLastCallback from '../../../../hooks/useLastCallback';
import usePeerSearch from '../../../../hooks/usePeerSearch';
import AnimatedIconFromSticker from '../../../common/AnimatedIconFromSticker';
import Avatar from '../../../common/Avatar';
import Icon from '../../../common/icons/Icon';
import PeerPicker from '../../../common/pickers/PeerPicker';
import PickerModal from '../../../common/pickers/PickerModal';
import RadialPatternBackground from '../../../common/profile/RadialPatternBackground';
import ConfirmDialog from '../../../ui/ConfirmDialog';
import styles from './GiftTransferModal.module.scss';
export type OwnProps = {
modal: TabState['giftTransferModal'];
};
type StateProps = {
contactIds?: string[];
currentUserId?: string;
};
type Categories = 'withdraw';
const AVATAR_SIZE = 4 * REM;
const GIFT_STICKER_SIZE = 3 * REM;
const GiftTransferModal = ({
modal, contactIds, currentUserId,
}: OwnProps & StateProps) => {
const { closeGiftTransferModal, openGiftWithdrawModal, transferGift } = getActions();
const isOpen = Boolean(modal);
const lang = useLang();
const [searchQuery, setSearchQuery] = useState<string>('');
const renderingModal = useCurrentOrPrev(modal);
const uniqueGift = renderingModal?.gift?.gift as ApiStarGiftUnique;
const giftAttributes = uniqueGift && getGiftAttributes(uniqueGift);
const [selectedId, setSelectedId] = useState<string | undefined>();
const renderingSelectedPeerId = useCurrentOrPrev(selectedId);
const renderingSelectedPeer = useMemo(() => {
const global = getGlobal();
return renderingSelectedPeerId ? selectPeer(global, renderingSelectedPeerId) : undefined;
}, [renderingSelectedPeerId]);
const orderedChatIds = useFolderManagerForOrderedIds(ALL_FOLDER_ID);
const sortedLocalIds = useMemo(() => {
return unique([
...(contactIds || []),
...(orderedChatIds || []),
]);
}, [contactIds, orderedChatIds]);
const { result: foundIds, currentResultsQuery } = usePeerSearch({
query: searchQuery,
defaultValue: sortedLocalIds,
});
const isLoading = currentResultsQuery !== searchQuery;
const categories = useMemo(() => {
if (currentResultsQuery) return MEMO_EMPTY_ARRAY;
return [{
type: 'withdraw',
isCustomPeer: true,
avatarIcon: 'toncoin',
peerColorId: 5,
title: lang('GiftTransferTON'),
}] satisfies UniqueCustomPeer<Categories>[];
}, [lang, currentResultsQuery]);
const handleCategoryChange = useLastCallback((category: Categories) => {
if (category !== 'withdraw') return;
openGiftWithdrawModal({
gift: renderingModal!.gift,
});
closeGiftTransferModal();
});
const displayIds = useMemo(() => {
if (isLoading) return MEMO_EMPTY_ARRAY;
const global = getGlobal();
return sortChatIds((foundIds || []).filter((peerId) => (
peerId !== currentUserId && selectCanGift(global, peerId)
)),
false);
}, [isLoading, foundIds, currentUserId]);
const closeConfirmModal = useLastCallback(() => {
setSelectedId(undefined);
});
useEffect(() => {
if (!isOpen) {
setSelectedId(undefined);
}
}, [isOpen]);
const handleTransfer = useLastCallback(() => {
if (!renderingModal?.gift.inputGift) return;
transferGift({
gift: renderingModal.gift.inputGift,
recipientId: renderingSelectedPeerId!,
transferStars: renderingModal.gift.transferStars,
});
closeConfirmModal();
closeGiftTransferModal();
});
return (
<PickerModal
isOpen={isOpen}
onClose={closeGiftTransferModal}
title={lang('GiftTransferTitle')}
hasCloseButton
shouldAdaptToSearch
withFixedHeight
ignoreFreeze
>
<PeerPicker<Categories>
itemIds={displayIds}
categories={categories}
onSelectedCategoryChange={handleCategoryChange}
withDefaultPadding
withPeerUsernames
isSearchable
noScrollRestore
isLoading={isLoading}
filterValue={searchQuery}
filterPlaceholder={lang('Search')}
onFilterChange={setSearchQuery}
onSelectedIdChange={setSelectedId}
/>
{giftAttributes && (
<ConfirmDialog
isOpen={Boolean(selectedId)}
noDefaultTitle
onClose={closeConfirmModal}
confirmLabel={renderingModal?.gift.transferStars
? lang(
'GiftTransferConfirmButton',
{ amount: formatStarsAsIcon(lang, renderingModal.gift.transferStars, { asFont: true }) },
{ withNodes: true },
) : lang('GiftTransferConfirmButtonFree')}
confirmHandler={handleTransfer}
>
<div className={styles.header}>
<div className={styles.giftPreview}>
<RadialPatternBackground
className={styles.backdrop}
backgroundColors={[giftAttributes.backdrop!.centerColor, giftAttributes.backdrop!.edgeColor]}
patternColor={giftAttributes.backdrop?.patternColor}
patternIcon={giftAttributes.pattern?.sticker}
/>
<AnimatedIconFromSticker
className={styles.sticker}
size={GIFT_STICKER_SIZE}
sticker={giftAttributes.model?.sticker}
/>
</div>
<Icon name="next" className={styles.arrow} />
<Avatar
peer={renderingSelectedPeer}
size={AVATAR_SIZE}
className={styles.avatar}
/>
</div>
<p>
{renderingModal?.gift.transferStars
? lang('GiftTransferConfirmDescription', {
gift: lang('GiftUnique', { title: uniqueGift.title, number: uniqueGift.number }),
amount: formatStarsAsText(lang, renderingModal.gift.transferStars),
peer: getPeerTitle(lang, renderingSelectedPeer!),
}, {
withNodes: true,
withMarkdown: true,
})
: lang('GiftTransferConfirmDescriptionFree', {
gift: lang('GiftUnique', { title: uniqueGift.title, number: uniqueGift.number }),
peer: getPeerTitle(lang, renderingSelectedPeer!),
}, {
withNodes: true,
withMarkdown: true,
})}
</p>
</ConfirmDialog>
)}
</PickerModal>
);
};
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const { contactList, currentUserId } = global;
return {
contactIds: contactList?.userIds,
currentUserId,
};
},
)(GiftTransferModal));

View File

@ -5,11 +5,11 @@ import React, {
useMemo, useMemo,
useState, useState,
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import { LoadMoreDirection } from '../../../types'; import { LoadMoreDirection } from '../../../types';
import { filterUsersByName } from '../../../global/helpers'; import { filterPeersByQuery } from '../../../global/helpers/peers';
import { selectTabState } from '../../../global/selectors'; import { selectTabState } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { throttle } from '../../../util/schedulers'; import { throttle } from '../../../util/schedulers';
@ -57,8 +57,7 @@ const MoreAppsTabContent: FC<OwnProps & StateProps> = ({
const filteredFoundIds = useMemo(() => { const filteredFoundIds = useMemo(() => {
if (!foundIds) return []; if (!foundIds) return [];
const usersById = getGlobal().users.byId; return filterPeersByQuery({ ids: foundIds, query: searchQuery, type: 'user' });
return filterUsersByName(foundIds, usersById, searchQuery);
}, [foundIds, searchQuery]); }, [foundIds, searchQuery]);
const handleLoadMore = useCallback(({ direction }: { direction: LoadMoreDirection }) => { const handleLoadMore = useCallback(({ direction }: { direction: LoadMoreDirection }) => {

View File

@ -10,8 +10,9 @@ import type {
import { NewChatMembersProgress } from '../../types'; import { NewChatMembersProgress } from '../../types';
import { import {
filterUsersByName, isChatChannel, isUserBot, isChatChannel, isUserBot,
} from '../../global/helpers'; } from '../../global/helpers';
import { filterPeersByQuery } from '../../global/helpers/peers';
import { selectChat, selectChatFullInfo, selectTabState } from '../../global/selectors'; import { selectChat, selectChatFullInfo, selectTabState } from '../../global/selectors';
import { unique } from '../../util/iteratees'; import { unique } from '../../util/iteratees';
import sortChatIds from '../common/helpers/sortChatIds'; import sortChatIds from '../common/helpers/sortChatIds';
@ -83,14 +84,18 @@ const AddChatMembers: FC<OwnProps & StateProps> = ({
const displayedIds = useMemo(() => { const displayedIds = useMemo(() => {
// No need for expensive global updates on users, so we avoid them // No need for expensive global updates on users, so we avoid them
const usersById = getGlobal().users.byId; const usersById = getGlobal().users.byId;
const filteredContactIds = localContactIds ? filterUsersByName(localContactIds, usersById, searchQuery) : []; const filteredIds = filterPeersByQuery({
ids: unique([
return sortChatIds( ...(localContactIds || []),
unique([
...filteredContactIds,
...(localUserIds || []), ...(localUserIds || []),
...(globalUserIds || []), ...(globalUserIds || []),
]).filter((userId) => { ]),
query: searchQuery,
type: 'user',
});
return sortChatIds(
filteredIds.filter((userId) => {
const user = usersById[userId]; const user = usersById[userId];
// The user can be added to the chat if the following conditions are met: // The user can be added to the chat if the following conditions are met:

View File

@ -8,9 +8,10 @@ import type { ApiChatMember, ApiUserStatus } from '../../../api/types';
import { ManagementScreens, NewChatMembersProgress } from '../../../types'; import { ManagementScreens, NewChatMembersProgress } from '../../../types';
import { import {
filterUsersByName, getHasAdminRight, isChatBasicGroup, getHasAdminRight, isChatBasicGroup,
isChatChannel, isUserBot, isUserRightBanned, sortUserIds, isChatChannel, isUserBot, isUserRightBanned, sortUserIds,
} from '../../../global/helpers'; } from '../../../global/helpers';
import { filterPeersByQuery } from '../../../global/helpers/peers';
import { selectChat, selectChatFullInfo, selectTabState } from '../../../global/selectors'; import { selectChat, selectChatFullInfo, selectTabState } from '../../../global/selectors';
import { unique } from '../../../util/iteratees'; import { unique } from '../../../util/iteratees';
import sortChatIds from '../../common/helpers/sortChatIds'; import sortChatIds from '../../common/helpers/sortChatIds';
@ -121,7 +122,7 @@ const ManageGroupMembers: FC<OwnProps & StateProps> = ({
const shouldUseSearchResults = Boolean(searchQuery); const shouldUseSearchResults = Boolean(searchQuery);
const listedIds = !shouldUseSearchResults const listedIds = !shouldUseSearchResults
? memberIds ? memberIds
: (localContactIds ? filterUsersByName(localContactIds, usersById, searchQuery) : []); : (localContactIds ? filterPeersByQuery({ ids: localContactIds, query: searchQuery, type: 'user' }) : []);
return sortChatIds( return sortChatIds(
unique([ unique([

View File

@ -19,6 +19,7 @@ import useInterval from '../../../hooks/schedulers/useInterval';
import useFlag from '../../../hooks/useFlag'; import useFlag from '../../../hooks/useFlag';
import useForceUpdate from '../../../hooks/useForceUpdate'; import useForceUpdate from '../../../hooks/useForceUpdate';
import useHistoryBack from '../../../hooks/useHistoryBack'; import useHistoryBack from '../../../hooks/useHistoryBack';
import useLang from '../../../hooks/useLang';
import useOldLang from '../../../hooks/useOldLang'; import useOldLang from '../../../hooks/useOldLang';
import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview'; import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview';
@ -72,7 +73,8 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
setOpenedInviteInfo, setOpenedInviteInfo,
} = getActions(); } = getActions();
const lang = useOldLang(); const lang = useLang();
const oldLang = useOldLang();
const [isDeleteRevokeAllDialogOpen, openDeleteRevokeAllDialog, closeDeleteRevokeAllDialog] = useFlag(); const [isDeleteRevokeAllDialogOpen, openDeleteRevokeAllDialog, closeDeleteRevokeAllDialog] = useFlag();
const [isRevokeDialogOpen, openRevokeDialog, closeRevokeDialog] = useFlag(); const [isRevokeDialogOpen, openRevokeDialog, closeRevokeDialog] = useFlag();
@ -174,9 +176,9 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
const copyLink = useCallback((link: string) => { const copyLink = useCallback((link: string) => {
copyTextToClipboard(link); copyTextToClipboard(link);
showNotification({ showNotification({
message: lang('LinkCopied'), message: oldLang('LinkCopied'),
}); });
}, [lang, showNotification]); }, [oldLang, showNotification]);
const prepareUsageText = (invite: ApiExportedInvite) => { const prepareUsageText = (invite: ApiExportedInvite) => {
const { const {
@ -184,34 +186,34 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
} = invite; } = invite;
let text = ''; let text = '';
if (!isRevoked && usageLimit && usage < usageLimit) { if (!isRevoked && usageLimit && usage < usageLimit) {
text = lang('CanJoin', usageLimit - usage); text = oldLang('CanJoin', usageLimit - usage);
} else if (usage) { } else if (usage) {
text = lang('PeopleJoined', usage); text = oldLang('PeopleJoined', usage);
} else { } else {
text = lang('NoOneJoined'); text = oldLang('NoOneJoined');
} }
if (isRevoked) { if (isRevoked) {
text += ` ${BULLET} ${lang('Revoked')}`; text += ` ${BULLET} ${oldLang('Revoked')}`;
return text; return text;
} }
if (requested) { if (requested) {
text += ` ${BULLET} ${lang('JoinRequests', requested)}`; text += ` ${BULLET} ${oldLang('JoinRequests', requested)}`;
} }
if (usageLimit !== undefined && usage === usageLimit) { if (usageLimit !== undefined && usage === usageLimit) {
text += ` ${BULLET} ${lang('LinkLimitReached')}`; text += ` ${BULLET} ${oldLang('LinkLimitReached')}`;
} else if (expireDate) { } else if (expireDate) {
const diff = (expireDate - getServerTime()) * 1000; const diff = expireDate - getServerTime();
text += ` ${BULLET} `; text += ` ${BULLET} `;
if (diff > 0) { if (diff > 0) {
text += lang('InviteLink.ExpiresIn', formatCountdown(lang, diff)); text += oldLang('InviteLink.ExpiresIn', formatCountdown(lang, diff));
} else { } else {
text += lang('InviteLink.Expired'); text += oldLang('InviteLink.Expired');
} }
} else if (isPermanent) { } else if (isPermanent) {
text += ` ${BULLET} ${lang('Permanent')}`; text += ` ${BULLET} ${oldLang('Permanent')}`;
} }
return text; return text;
@ -239,14 +241,14 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
const prepareContextActions = (invite: ApiExportedInvite) => { const prepareContextActions = (invite: ApiExportedInvite) => {
const actions: MenuItemContextAction[] = []; const actions: MenuItemContextAction[] = [];
actions.push({ actions.push({
title: lang('Copy'), title: oldLang('Copy'),
icon: 'copy', icon: 'copy',
handler: () => copyLink(invite.link), handler: () => copyLink(invite.link),
}); });
if (!invite.isPermanent && !invite.isRevoked) { if (!invite.isPermanent && !invite.isRevoked) {
actions.push({ actions.push({
title: lang('Edit'), title: oldLang('Edit'),
icon: 'edit', icon: 'edit',
handler: () => editInvite(invite), handler: () => editInvite(invite),
}); });
@ -254,14 +256,14 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
if (!invite.isRevoked) { if (!invite.isRevoked) {
actions.push({ actions.push({
title: lang('RevokeButton'), title: oldLang('RevokeButton'),
icon: 'delete', icon: 'delete',
handler: () => askToRevoke(invite), handler: () => askToRevoke(invite),
destructive: true, destructive: true,
}); });
} else { } else {
actions.push({ actions.push({
title: lang('DeleteLink'), title: oldLang('DeleteLink'),
icon: 'delete', icon: 'delete',
handler: () => askToDelete(invite), handler: () => askToDelete(invite),
destructive: true, destructive: true,
@ -279,7 +281,7 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
size={STICKER_SIZE_INVITES} size={STICKER_SIZE_INVITES}
className="section-icon" className="section-icon"
/> />
<p className="section-help">{isChannel ? lang('PrimaryLinkHelpChannel') : lang('PrimaryLinkHelp')}</p> <p className="section-help">{isChannel ? oldLang('PrimaryLinkHelpChannel') : oldLang('PrimaryLinkHelp')}</p>
</div> </div>
{primaryInviteLink && ( {primaryInviteLink && (
<div className="section"> <div className="section">
@ -288,13 +290,13 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
link={primaryInviteLink} link={primaryInviteLink}
withShare withShare
onRevoke={!chat?.usernames ? handlePrimaryRevoke : undefined} onRevoke={!chat?.usernames ? handlePrimaryRevoke : undefined}
title={chat?.usernames ? lang('PublicLink') : lang('lng_create_permanent_link_title')} title={chat?.usernames ? oldLang('PublicLink') : oldLang('lng_create_permanent_link_title')}
/> />
</div> </div>
)} )}
<div className="section" teactFastList> <div className="section" teactFastList>
<Button isText key="create" className="create-link" onClick={handleCreateNewClick}> <Button isText key="create" className="create-link" onClick={handleCreateNewClick}>
{lang('CreateNewLink')} {oldLang('CreateNewLink')}
</Button> </Button>
{(!temporalInvites || !temporalInvites.length) && <NothingFound text="No links found" key="nothing" />} {(!temporalInvites || !temporalInvites.length) && <NothingFound text="No links found" key="nothing" />}
{temporalInvites?.map((invite) => ( {temporalInvites?.map((invite) => (
@ -313,18 +315,18 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
</span> </span>
</ListItem> </ListItem>
))} ))}
<p className="section-help hint" key="links-hint">{lang('ManageLinksInfoHelp')}</p> <p className="section-help hint" key="links-hint">{oldLang('ManageLinksInfoHelp')}</p>
</div> </div>
{revokedExportedInvites && Boolean(revokedExportedInvites.length) && ( {revokedExportedInvites && Boolean(revokedExportedInvites.length) && (
<div className="section" teactFastList> <div className="section" teactFastList>
<p className="section-help" key="title">{lang('RevokedLinks')}</p> <p className="section-help" key="title">{oldLang('RevokedLinks')}</p>
<ListItem <ListItem
icon="delete" icon="delete"
destructive destructive
key="delete" key="delete"
onClick={openDeleteRevokeAllDialog} onClick={openDeleteRevokeAllDialog}
> >
<span className="title">{lang('DeleteAllRevokedLinks')}</span> <span className="title">{oldLang('DeleteAllRevokedLinks')}</span>
</ListItem> </ListItem>
{revokedExportedInvites?.map((invite) => ( {revokedExportedInvites?.map((invite) => (
<ListItem <ListItem
@ -348,28 +350,28 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
<ConfirmDialog <ConfirmDialog
isOpen={isDeleteRevokeAllDialogOpen} isOpen={isDeleteRevokeAllDialogOpen}
onClose={closeDeleteRevokeAllDialog} onClose={closeDeleteRevokeAllDialog}
title={lang('DeleteAllRevokedLinks')} title={oldLang('DeleteAllRevokedLinks')}
text={lang('DeleteAllRevokedLinkHelp')} text={oldLang('DeleteAllRevokedLinkHelp')}
confirmIsDestructive confirmIsDestructive
confirmLabel={lang('DeleteAll')} confirmLabel={oldLang('DeleteAll')}
confirmHandler={handleDeleteAllRevoked} confirmHandler={handleDeleteAllRevoked}
/> />
<ConfirmDialog <ConfirmDialog
isOpen={isRevokeDialogOpen} isOpen={isRevokeDialogOpen}
onClose={closeRevokeDialog} onClose={closeRevokeDialog}
title={lang('RevokeLink')} title={oldLang('RevokeLink')}
text={lang('RevokeAlert')} text={oldLang('RevokeAlert')}
confirmIsDestructive confirmIsDestructive
confirmLabel={lang('RevokeButton')} confirmLabel={oldLang('RevokeButton')}
confirmHandler={handleRevoke} confirmHandler={handleRevoke}
/> />
<ConfirmDialog <ConfirmDialog
isOpen={isDeleteDialogOpen} isOpen={isDeleteDialogOpen}
onClose={closeDeleteDialog} onClose={closeDeleteDialog}
title={lang('DeleteLink')} title={oldLang('DeleteLink')}
text={lang('DeleteLinkHelp')} text={oldLang('DeleteLinkHelp')}
confirmIsDestructive confirmIsDestructive
confirmLabel={lang('Delete')} confirmLabel={oldLang('Delete')}
confirmHandler={handleDelete} confirmHandler={handleDelete}
/> />
</div> </div>

View File

@ -3,11 +3,11 @@ import React, {
memo, useCallback, memo, useCallback,
useMemo, useState, useMemo, useState,
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { ApiChat, ApiChatMember } from '../../../api/types'; import type { ApiChat, ApiChatMember } from '../../../api/types';
import { filterUsersByName } from '../../../global/helpers'; import { filterPeersByQuery } from '../../../global/helpers/peers';
import { selectChatFullInfo } from '../../../global/selectors'; import { selectChatFullInfo } from '../../../global/selectors';
import useOldLang from '../../../hooks/useOldLang'; import useOldLang from '../../../hooks/useOldLang';
@ -49,10 +49,7 @@ const RemoveGroupUserModal: FC<OwnProps & StateProps> = ({
return acc; return acc;
}, []); }, []);
// No need for expensive global updates on users, so we avoid them return filterPeersByQuery({ ids: availableMemberIds, query: search, type: 'user' });
const usersById = getGlobal().users.byId;
return filterUsersByName(availableMemberIds, usersById, search);
}, [chatMembers, currentUserId, search]); }, [chatMembers, currentUserId, search]);
const handleRemoveUser = useCallback((userId: string) => { const handleRemoveUser = useCallback((userId: string) => {

View File

@ -278,7 +278,6 @@ function StorySettings({
id="deny-list" id="deny-list"
contactListIds={contactListIds} contactListIds={contactListIds}
currentUserId={currentUserId} currentUserId={currentUserId}
usersById={usersById}
selectedIds={selectedBlockedIds} selectedIds={selectedBlockedIds}
onSelect={handleDenyUserIdsChange} onSelect={handleDenyUserIdsChange}
/> />
@ -291,7 +290,6 @@ function StorySettings({
contactListIds={contactListIds} contactListIds={contactListIds}
lockedIds={lockedIds} lockedIds={lockedIds}
currentUserId={currentUserId} currentUserId={currentUserId}
usersById={usersById}
selectedIds={privacy?.allowUserIds} selectedIds={privacy?.allowUserIds}
onSelect={handleAllowUserIdsChange} onSelect={handleAllowUserIdsChange}
/> />

View File

@ -1,8 +1,6 @@
import React, { memo, useMemo, useState } from '../../../lib/teact/teact'; import React, { memo, useMemo, useState } from '../../../lib/teact/teact';
import type { ApiUser } from '../../../api/types'; import { filterPeersByQuery } from '../../../global/helpers/peers';
import { filterUsersByName } from '../../../global/helpers';
import { unique } from '../../../util/iteratees'; import { unique } from '../../../util/iteratees';
import { MEMO_EMPTY_ARRAY } from '../../../util/memo'; import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
@ -16,7 +14,6 @@ interface OwnProps {
currentUserId: string; currentUserId: string;
selectedIds?: string[]; selectedIds?: string[];
lockedIds?: string[]; lockedIds?: string[];
usersById: Record<string, ApiUser>;
onSelect: (selectedIds: string[]) => void; onSelect: (selectedIds: string[]) => void;
} }
@ -24,7 +21,6 @@ function AllowDenyList({
id, id,
contactListIds, contactListIds,
currentUserId, currentUserId,
usersById,
selectedIds, selectedIds,
lockedIds, lockedIds,
onSelect, onSelect,
@ -34,8 +30,8 @@ function AllowDenyList({
const [searchQuery, setSearchQuery] = useState<string>(''); const [searchQuery, setSearchQuery] = useState<string>('');
const displayedIds = useMemo(() => { const displayedIds = useMemo(() => {
const contactIds = (contactListIds || []).filter((userId) => userId !== currentUserId); const contactIds = (contactListIds || []).filter((userId) => userId !== currentUserId);
return unique(filterUsersByName([...selectedIds || [], ...contactIds], usersById, searchQuery)); return unique(filterPeersByQuery({ ids: [...selectedIds || [], ...contactIds], query: searchQuery, type: 'user' }));
}, [contactListIds, currentUserId, searchQuery, selectedIds, usersById]); }, [contactListIds, currentUserId, searchQuery, selectedIds]);
return ( return (
<PeerPicker <PeerPicker

View File

@ -5,7 +5,7 @@ import { getActions } from '../../../global';
import type { ApiUser } from '../../../api/types'; import type { ApiUser } from '../../../api/types';
import { filterUsersByName } from '../../../global/helpers'; import { filterPeersByQuery } from '../../../global/helpers/peers';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { unique } from '../../../util/iteratees'; import { unique } from '../../../util/iteratees';
@ -43,8 +43,8 @@ function CloseFriends({
const displayedIds = useMemo(() => { const displayedIds = useMemo(() => {
const contactIds = (contactListIds || []).filter((id) => id !== currentUserId); const contactIds = (contactListIds || []).filter((id) => id !== currentUserId);
return unique(filterUsersByName([...closeFriendIds, ...contactIds], usersById, searchQuery)); return unique(filterPeersByQuery({ ids: [...closeFriendIds, ...contactIds], query: searchQuery, type: 'user' }));
}, [closeFriendIds, contactListIds, currentUserId, searchQuery, usersById]); }, [closeFriendIds, contactListIds, currentUserId, searchQuery]);
useEffectWithPrevDeps(([prevIsActive]) => { useEffectWithPrevDeps(([prevIsActive]) => {
if (!prevIsActive && isActive) { if (!prevIsActive && isActive) {

View File

@ -14,10 +14,11 @@ import Modal from './Modal';
type OwnProps = { type OwnProps = {
isOpen: boolean; isOpen: boolean;
title?: string; title?: string;
noDefaultTitle?: boolean;
header?: TeactNode; header?: TeactNode;
textParts?: TextPart; textParts?: TextPart;
text?: string; text?: string;
confirmLabel?: string; confirmLabel?: TeactNode;
confirmIsDestructive?: boolean; confirmIsDestructive?: boolean;
isConfirmDisabled?: boolean; isConfirmDisabled?: boolean;
isOnlyConfirm?: boolean; isOnlyConfirm?: boolean;
@ -32,6 +33,7 @@ type OwnProps = {
const ConfirmDialog: FC<OwnProps> = ({ const ConfirmDialog: FC<OwnProps> = ({
isOpen, isOpen,
title, title,
noDefaultTitle,
header, header,
text, text,
textParts, textParts,
@ -60,7 +62,7 @@ const ConfirmDialog: FC<OwnProps> = ({
return ( return (
<Modal <Modal
className={buildClassName('confirm', className)} className={buildClassName('confirm', className)}
title={(title || lang('Telegram'))} title={(title || (!noDefaultTitle ? lang('Telegram') : undefined))}
header={header} header={header}
isOpen={isOpen} isOpen={isOpen}
onClose={onClose} onClose={onClose}

View File

@ -42,6 +42,7 @@ export type OwnProps = {
dialogRef?: React.RefObject<HTMLDivElement>; dialogRef?: React.RefObject<HTMLDivElement>;
isLowStackPriority?: boolean; isLowStackPriority?: boolean;
dialogContent?: React.ReactNode; dialogContent?: React.ReactNode;
ignoreFreeze?: boolean;
onClose: () => void; onClose: () => void;
onCloseAnimationEnd?: () => void; onCloseAnimationEnd?: () => void;
onEnter?: () => void; onEnter?: () => void;

View File

@ -1941,7 +1941,7 @@ addActionHandler('loadMoreMembers', async (global, actions, payload): Promise<vo
const offset = selectChatFullInfo(global, chat.id)?.members?.length; const offset = selectChatFullInfo(global, chat.id)?.members?.length;
if (offset !== undefined && chat.membersCount !== undefined && offset >= chat.membersCount) return; if (offset !== undefined && chat.membersCount !== undefined && offset >= chat.membersCount) return;
const result = await callApi('fetchMembers', chat.id, chat.accessHash!, 'recent', offset); const result = await callApi('fetchMembers', { chat, offset });
if (!result) { if (!result) {
return; return;
} }

View File

@ -1,5 +1,5 @@
import type { import type {
ApiInputInvoice, ApiInputInvoiceStarGift, ApiInputInvoiceStarGiftUpgrade, ApiRequestInputInvoice, ApiInputInvoice, ApiInputInvoiceStarGift, ApiRequestInputInvoice,
} from '../../../api/types'; } from '../../../api/types';
import type { ApiCredentials } from '../../../components/payment/PaymentModal'; import type { ApiCredentials } from '../../../components/payment/PaymentModal';
import type { RegularLangFnParameters } from '../../../util/localization'; import type { RegularLangFnParameters } from '../../../util/localization';
@ -977,7 +977,7 @@ addActionHandler('upgradeGift', (global, actions, payload): ActionReturnType =>
actions.closeGiftInfoModal({ tabId }); actions.closeGiftInfoModal({ tabId });
if (!upgradeStars) { if (!upgradeStars) {
callApi('upgradeGift', { callApi('upgradeStarGift', {
inputSavedGift: requestSavedGift, inputSavedGift: requestSavedGift,
shouldKeepOriginalDetails: shouldKeepOriginalDetails || undefined, shouldKeepOriginalDetails: shouldKeepOriginalDetails || undefined,
}); });
@ -985,7 +985,7 @@ addActionHandler('upgradeGift', (global, actions, payload): ActionReturnType =>
return; return;
} }
const invoice: ApiInputInvoiceStarGiftUpgrade = { const invoice: ApiInputInvoice = {
type: 'stargiftUpgrade', type: 'stargiftUpgrade',
inputSavedGift: gift, inputSavedGift: gift,
shouldKeepOriginalDetails: shouldKeepOriginalDetails || undefined, shouldKeepOriginalDetails: shouldKeepOriginalDetails || undefined,
@ -994,6 +994,46 @@ addActionHandler('upgradeGift', (global, actions, payload): ActionReturnType =>
payInputStarInvoice(global, invoice, upgradeStars, tabId); payInputStarInvoice(global, invoice, upgradeStars, tabId);
}); });
addActionHandler('transferGift', (global, actions, payload): ActionReturnType => {
const {
gift, recipientId, transferStars, tabId = getCurrentTabId(),
} = payload;
const peer = selectChat(global, recipientId);
const requestSavedGift = getRequestInputSavedStarGift(global, gift);
if (!peer || !requestSavedGift) {
return;
}
global = updateTabState(global, {
isWaitingForStarGiftTransfer: true,
}, tabId);
setGlobal(global);
global = getGlobal();
actions.closeGiftTransferModal({ tabId });
actions.closeGiftInfoModal({ tabId });
if (!transferStars) {
callApi('transferStarGift', {
inputSavedGift: requestSavedGift,
toPeer: peer,
});
return;
}
const invoice: ApiInputInvoice = {
type: 'stargiftTransfer',
inputSavedGift: gift,
recipientId,
};
payInputStarInvoice(global, invoice, transferStars, tabId);
});
async function payInputStarInvoice<T extends GlobalState>( async function payInputStarInvoice<T extends GlobalState>(
global: T, inputInvoice: ApiInputInvoice, price: number, global: T, inputInvoice: ApiInputInvoice, price: number,
...[tabId = getCurrentTabId()]: TabArgs<T> ...[tabId = getCurrentTabId()]: TabArgs<T>

View File

@ -2,7 +2,8 @@ import type { ActionReturnType } from '../../types';
import { PaymentStep } from '../../../types'; import { PaymentStep } from '../../../types';
import { SERVICE_NOTIFICATIONS_USER_ID } from '../../../config'; import { SERVICE_NOTIFICATIONS_USER_ID } from '../../../config';
import { applyLangPackDifference, requestLangPackDifference } from '../../../util/localization'; import { applyLangPackDifference, getTranslationFn, requestLangPackDifference } from '../../../util/localization';
import { getPeerTitle } from '../../helpers';
import { addActionHandler, setGlobal } from '../../index'; import { addActionHandler, setGlobal } from '../../index';
import { import {
addBlockedUser, addBlockedUser,
@ -21,7 +22,12 @@ import {
updateThreadInfos, updateThreadInfos,
} from '../../reducers'; } from '../../reducers';
import { updateTabState } from '../../reducers/tabs'; import { updateTabState } from '../../reducers/tabs';
import { selectPeerStories, selectPeerStory, selectTabState } from '../../selectors'; import {
selectPeer,
selectPeerStories,
selectPeerStory,
selectTabState,
} from '../../selectors';
addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
switch (update['@type']) { switch (update['@type']) {
@ -235,7 +241,44 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
isWaitingForStarGiftUpgrade: undefined, isWaitingForStarGiftUpgrade: undefined,
}, tabId); }, tabId);
} }
if (tabState.isWaitingForStarGiftTransfer) {
const chatId = update.message.chatId;
const receiver = chatId ? selectPeer(global, chatId) : undefined;
if (receiver) {
actions.focusMessage({
chatId: receiver.id,
messageId: update.message.id!,
tabId,
});
actions.showNotification({
message: {
key: 'GiftTransferSuccessMessage',
variables: {
gift: {
key: 'GiftUnique',
variables: {
title: actionStarGift.gift.title,
number: actionStarGift.gift.number,
},
},
peer: getPeerTitle(getTranslationFn(), receiver),
},
},
tabId,
});
}
actions.requestConfetti({ withStars: true, tabId });
global = updateTabState(global, {
isWaitingForStarGiftTransfer: undefined,
}, tabId);
}
}); });
setGlobal(global);
} }
} }

View File

@ -3,6 +3,7 @@ import type { ActionReturnType } from '../../types';
import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { getCurrentTabId } from '../../../util/establishMultitabRole';
import * as langProvider from '../../../util/oldLangProvider'; import * as langProvider from '../../../util/oldLangProvider';
import { addTabStateResetterAction } from '../../helpers/meta';
import { getPrizeStarsTransactionFromGiveaway, getStarsTransactionFromGift } from '../../helpers/payments'; import { getPrizeStarsTransactionFromGiveaway, getStarsTransactionFromGift } from '../../helpers/payments';
import { addActionHandler } from '../../index'; import { addActionHandler } from '../../index';
import { import {
@ -63,13 +64,7 @@ addActionHandler('openGiftRecipientPicker', (global, actions, payload): ActionRe
}, tabId); }, tabId);
}); });
addActionHandler('closeGiftRecipientPicker', (global, actions, payload): ActionReturnType => { addTabStateResetterAction('closeGiftRecipientPicker', 'isGiftRecipientPickerOpen');
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
isGiftRecipientPickerOpen: undefined,
}, tabId);
});
addActionHandler('openStarsGiftingPickerModal', (global, actions, payload): ActionReturnType => { addActionHandler('openStarsGiftingPickerModal', (global, actions, payload): ActionReturnType => {
const { const {
@ -83,13 +78,7 @@ addActionHandler('openStarsGiftingPickerModal', (global, actions, payload): Acti
}, tabId); }, tabId);
}); });
addActionHandler('closeStarsGiftingPickerModal', (global, actions, payload): ActionReturnType => { addTabStateResetterAction('closeStarsGiftingPickerModal', 'starsGiftingPickerModal');
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
starsGiftingPickerModal: undefined,
}, tabId);
});
addActionHandler('openPrizeStarsTransactionFromGiveaway', (global, actions, payload): ActionReturnType => { addActionHandler('openPrizeStarsTransactionFromGiveaway', (global, actions, payload): ActionReturnType => {
const { const {
@ -148,13 +137,7 @@ addActionHandler('openStarsBalanceModal', (global, actions, payload): ActionRetu
}, tabId); }, tabId);
}); });
addActionHandler('closeStarsBalanceModal', (global, actions, payload): ActionReturnType => { addTabStateResetterAction('closeStarsBalanceModal', 'starsBalanceModal');
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
starsBalanceModal: undefined,
}, tabId);
});
addActionHandler('closeStarsPaymentModal', (global, actions, payload): ActionReturnType => { addActionHandler('closeStarsPaymentModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {}; const { tabId = getCurrentTabId() } = payload || {};
@ -193,13 +176,7 @@ addActionHandler('openStarsTransactionFromGift', (global, actions, payload): Act
return openStarsTransactionModal(global, transaction, tabId); return openStarsTransactionModal(global, transaction, tabId);
}); });
addActionHandler('closeStarsTransactionModal', (global, actions, payload): ActionReturnType => { addTabStateResetterAction('closeStarsTransactionModal', 'starsTransactionModal');
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
starsTransactionModal: undefined,
}, tabId);
});
addActionHandler('openStarsSubscriptionModal', (global, actions, payload): ActionReturnType => { addActionHandler('openStarsSubscriptionModal', (global, actions, payload): ActionReturnType => {
const { subscription, tabId = getCurrentTabId() } = payload; const { subscription, tabId = getCurrentTabId() } = payload;
@ -211,20 +188,9 @@ addActionHandler('openStarsSubscriptionModal', (global, actions, payload): Actio
}, tabId); }, tabId);
}); });
addActionHandler('closeStarsSubscriptionModal', (global, actions, payload): ActionReturnType => { addTabStateResetterAction('closeStarsSubscriptionModal', 'starsSubscriptionModal');
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, { addTabStateResetterAction('closeGiftModal', 'giftModal');
starsSubscriptionModal: undefined,
}, tabId);
});
addActionHandler('closeGiftModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
giftModal: undefined,
}, tabId);
});
addActionHandler('closeStarsGiftModal', (global, actions, payload): ActionReturnType => { addActionHandler('closeStarsGiftModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {}; const { tabId = getCurrentTabId() } = payload || {};
@ -287,21 +253,9 @@ addActionHandler('openGiftInfoModal', (global, actions, payload): ActionReturnTy
}, tabId); }, tabId);
}); });
addActionHandler('closeGiftInfoModal', (global, actions, payload): ActionReturnType => { addTabStateResetterAction('closeGiftInfoModal', 'giftInfoModal');
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, { addTabStateResetterAction('closeGiftUpgradeModal', 'giftUpgradeModal');
giftInfoModal: undefined,
}, tabId);
});
addActionHandler('closeGiftUpgradeModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
giftUpgradeModal: undefined,
}, tabId);
});
addActionHandler('openGiftWithdrawModal', (global, actions, payload): ActionReturnType => { addActionHandler('openGiftWithdrawModal', (global, actions, payload): ActionReturnType => {
const { gift, tabId = getCurrentTabId() } = payload || {}; const { gift, tabId = getCurrentTabId() } = payload || {};
@ -313,13 +267,7 @@ addActionHandler('openGiftWithdrawModal', (global, actions, payload): ActionRetu
}, tabId); }, tabId);
}); });
addActionHandler('closeGiftWithdrawModal', (global, actions, payload): ActionReturnType => { addTabStateResetterAction('closeGiftWithdrawModal', 'giftWithdrawModal');
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
giftWithdrawModal: undefined,
}, tabId);
});
addActionHandler('clearGiftWithdrawError', (global, actions, payload): ActionReturnType => { addActionHandler('clearGiftWithdrawError', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {}; const { tabId = getCurrentTabId() } = payload || {};
@ -334,3 +282,15 @@ addActionHandler('clearGiftWithdrawError', (global, actions, payload): ActionRet
}, },
}, tabId); }, tabId);
}); });
addActionHandler('openGiftTransferModal', (global, actions, payload): ActionReturnType => {
const { gift, tabId = getCurrentTabId() } = payload;
return updateTabState(global, {
giftTransferModal: {
gift,
},
}, tabId);
});
addTabStateResetterAction('closeGiftTransferModal', 'giftTransferModal');

View File

@ -22,7 +22,6 @@ import {
VERIFICATION_CODES_USER_ID, VERIFICATION_CODES_USER_ID,
} from '../../config'; } from '../../config';
import { formatDateToString, formatTime } from '../../util/dates/dateFormat'; import { formatDateToString, formatTime } from '../../util/dates/dateFormat';
import { prepareSearchWordsForNeedle } from '../../util/searchWords';
import { getGlobal } from '..'; import { getGlobal } from '..';
import { isSystemBot } from './bots'; import { isSystemBot } from './bots';
import { getMainUsername, getUserFirstOrLastName } from './users'; import { getMainUsername, getUserFirstOrLastName } from './users';
@ -395,36 +394,6 @@ export function getMessageSenderName(lang: OldLangFn, chatId: string, sender?: A
return getUserFirstOrLastName(sender); return getUserFirstOrLastName(sender);
} }
export function filterChatsByName(
lang: OldLangFn,
chatIds: string[],
chatsById: Record<string, ApiChat>,
query?: string,
currentUserId?: string,
) {
if (!query) {
return chatIds;
}
const searchWords = prepareSearchWordsForNeedle(query);
return chatIds.filter((id) => {
const chat = chatsById[id];
if (!chat) {
return false;
}
const isSelf = id === currentUserId;
const translatedTitle = getChatTitle(lang, chat, isSelf);
if (isSelf) {
// Search both "Saved Messages" and user title
return searchWords(translatedTitle) || searchWords(chat.title);
}
return searchWords(translatedTitle) || Boolean(chat.usernames?.find(({ username }) => searchWords(username)));
});
}
export function isChatPublic(chat: ApiChat) { export function isChatPublic(chat: ApiChat) {
return chat.usernames?.some(({ isActive }) => isActive); return chat.usernames?.some(({ isActive }) => isActive);
} }

View File

@ -0,0 +1,18 @@
import type { ActionReturnType, TabState } from '../types';
import { getCurrentTabId } from '../../util/establishMultitabRole';
import { updateTabState } from '../reducers/tabs';
import { addActionHandler, type TabStateActionNames } from '..';
export function addTabStateResetterAction<ActionName extends TabStateActionNames>(
name: ActionName, key: keyof TabState,
) {
// @ts-ignore
addActionHandler(name, (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
[key]: undefined,
}, tabId);
});
}

View File

@ -188,6 +188,19 @@ export function getRequestInputInvoice<T extends GlobalState>(
}; };
} }
if (inputInvoice.type === 'stargiftTransfer') {
const { inputSavedGift, recipientId } = inputInvoice;
const savedGift = getRequestInputSavedStarGift(global, inputSavedGift);
const peer = selectPeer(global, recipientId);
if (!savedGift || !peer) return undefined;
return {
type: 'stargiftTransfer',
inputSavedGift: savedGift,
recipient: peer,
};
}
return undefined; return undefined;
} }

View File

@ -1,6 +1,12 @@
import type { ApiChat, ApiPeer, ApiUser } from '../../api/types'; import type { ApiChat, ApiPeer, ApiUser } from '../../api/types';
import { SERVICE_NOTIFICATIONS_USER_ID } from '../../config'; import { SERVICE_NOTIFICATIONS_USER_ID } from '../../config';
import { getTranslationFn } from '../../util/localization';
import { prepareSearchWordsForNeedle } from '../../util/searchWords';
import { selectChat, selectPeer, selectUser } from '../selectors';
import { getGlobal } from '..';
import { getChatTitle } from './chats';
import { getPeerFullTitle } from './messages';
export function isApiPeerChat(peer: ApiPeer): peer is ApiChat { export function isApiPeerChat(peer: ApiPeer): peer is ApiChat {
return 'title' in peer; return 'title' in peer;
@ -10,6 +16,44 @@ export function isApiPeerUser(peer: ApiPeer): peer is ApiUser {
return !isApiPeerChat(peer); return !isApiPeerChat(peer);
} }
export function filterPeersByQuery({
ids,
query,
type = 'peer',
} : {
ids: string[];
query: string | undefined;
type?: 'chat' | 'user' | 'peer';
}) {
if (!query) {
return ids;
}
const global = getGlobal();
const lang = getTranslationFn();
const searchWords = prepareSearchWordsForNeedle(query);
const selectorFn = type === 'chat' ? selectChat : type === 'user' ? selectUser : selectPeer;
return ids.filter((id) => {
const peer = selectorFn(global, id);
if (!peer) {
return false;
}
const localizedTitle = isApiPeerChat(peer)
? getChatTitle(lang, peer)
: id === global.currentUserId ? lang('SavedMessages') : undefined;
const isFoundInLocalized = localizedTitle ? searchWords(localizedTitle) : undefined;
const name = getPeerFullTitle(lang, peer);
return isFoundInLocalized
|| (name && searchWords(name))
|| Boolean(peer.usernames?.find(({ username }) => searchWords(username)));
});
}
export function getPeerTypeKey(peer: ApiPeer) { export function getPeerTypeKey(peer: ApiPeer) {
if (isApiPeerChat(peer)) { if (isApiPeerChat(peer)) {
if (peer.type === 'chatTypeBasicGroup' || peer.type === 'chatTypeSuperGroup') { if (peer.type === 'chatTypeBasicGroup' || peer.type === 'chatTypeSuperGroup') {

View File

@ -6,7 +6,6 @@ import { formatFullDate, formatTime } from '../../util/dates/dateFormat';
import { DAY } from '../../util/dates/units'; import { DAY } from '../../util/dates/units';
import { orderBy } from '../../util/iteratees'; import { orderBy } from '../../util/iteratees';
import { formatPhoneNumber } from '../../util/phoneNumber'; import { formatPhoneNumber } from '../../util/phoneNumber';
import { prepareSearchWordsForNeedle } from '../../util/searchWords';
import { getServerTime, getServerTimeOffset } from '../../util/serverTime'; import { getServerTime, getServerTimeOffset } from '../../util/serverTime';
export function getUserFirstOrLastName(user?: ApiUser) { export function getUserFirstOrLastName(user?: ApiUser) {
@ -239,31 +238,6 @@ export function sortUserIds(
}, 'desc'); }, 'desc');
} }
export function filterUsersByName(
userIds: string[],
usersById: Record<string, ApiUser>,
query?: string,
currentUserId?: string,
savedMessagesLang?: string,
) {
if (!query) {
return userIds;
}
const searchWords = prepareSearchWordsForNeedle(query);
return userIds.filter((id) => {
const user = usersById[id];
if (!user) {
return false;
}
const name = id === currentUserId ? savedMessagesLang : getUserFullName(user);
return (name && searchWords(name)) || Boolean(user.usernames?.find(({ username }) => searchWords(username)));
});
}
export function getMainUsername(userOrChat: ApiPeer) { export function getMainUsername(userOrChat: ApiPeer) {
return userOrChat.usernames?.find((u) => u.isActive)?.username; return userOrChat.usernames?.find((u) => u.isActive)?.username;
} }

View File

@ -14,13 +14,18 @@ type ProjectActionTypes =
type ProjectActionNames = keyof ProjectActionTypes; type ProjectActionNames = keyof ProjectActionTypes;
type Helper<T, E> = Exclude<T, E> extends never ? {} : Exclude<T, E>; type Helper<T, E> = Exclude<T, E> extends never ? {} : Exclude<T, E>;
export type TabStateActionNames = {
[ActionName in ProjectActionNames]:
'tabId' extends keyof Helper<ProjectActionTypes[ActionName], undefined> ? ActionName : never
}[ProjectActionNames];
// `Required` actions are called from actions to ensure the `tabId` is always provided if needed. // `Required` actions are called from actions to ensure the `tabId` is always provided if needed.
// There are three types of actions: // There are three types of actions:
// 1. With tabId, which is made required when calling action from another action handler // 1. With tabId, which is made required when calling action from another action handler
// 2. Without payload (= undefined), hence made the payload not required // 2. Without payload (= undefined), hence made the payload not required
// 3. With payload, hence made the payload required // 3. With payload, hence made the payload required
export type RequiredGlobalActions = { export type RequiredGlobalActions = {
[ActionName in ProjectActionNames]: 'tabId' extends keyof Helper<ProjectActionTypes[ActionName], undefined> ? (( [ActionName in ProjectActionNames]: ActionName extends TabStateActionNames ? ((
payload: ProjectActionTypes[ActionName] & { tabId: number }, payload: ProjectActionTypes[ActionName] & { tabId: number },
options?: ActionOptions, options?: ActionOptions,
) => void) : ) => void) :

View File

@ -3,9 +3,10 @@ import type { GlobalState, TabArgs } from '../types';
import { SERVICE_NOTIFICATIONS_USER_ID } from '../../config'; import { SERVICE_NOTIFICATIONS_USER_ID } from '../../config';
import { getCurrentTabId } from '../../util/establishMultitabRole'; import { getCurrentTabId } from '../../util/establishMultitabRole';
import { isDeletedUser } from '../helpers';
import { selectChat, selectChatFullInfo } from './chats'; import { selectChat, selectChatFullInfo } from './chats';
import { selectTabState } from './tabs'; import { selectTabState } from './tabs';
import { selectBot, selectIsPremiumPurchaseBlocked, selectUser } from './users'; import { selectBot, selectUser } from './users';
export function selectPeer<T extends GlobalState>(global: T, peerId: string): ApiPeer | undefined { export function selectPeer<T extends GlobalState>(global: T, peerId: string): ApiPeer | undefined {
return selectUser(global, peerId) || selectChat(global, peerId); return selectUser(global, peerId) || selectChat(global, peerId);
@ -19,10 +20,11 @@ export function selectCanGift<T extends GlobalState>(global: T, peerId: string)
const bot = selectBot(global, peerId); const bot = selectBot(global, peerId);
const user = selectUser(global, peerId); const user = selectUser(global, peerId);
const areStarGiftsAvailable = selectChatFullInfo(global, peerId)?.areStarGiftsAvailable || user; if (user) {
return !bot && peerId !== SERVICE_NOTIFICATIONS_USER_ID && !isDeletedUser(user);
}
return Boolean(!selectIsPremiumPurchaseBlocked(global) && !bot && peerId !== SERVICE_NOTIFICATIONS_USER_ID return selectChatFullInfo(global, peerId)?.areStarGiftsAvailable;
&& areStarGiftsAvailable);
} }
export function selectPeerSavedGifts<T extends GlobalState>( export function selectPeerSavedGifts<T extends GlobalState>(

View File

@ -92,7 +92,7 @@ import type { WebApp, WebAppModalStateType, WebAppOutboundEvent } from '../../ty
import type { DownloadableMedia } from '../helpers'; import type { DownloadableMedia } from '../helpers';
import type { TabState } from './tabState'; import type { TabState } from './tabState';
type WithTabId = { tabId?: number }; export type WithTabId = { tabId?: number };
export interface ActionPayloads { export interface ActionPayloads {
// system // system
@ -2308,6 +2308,7 @@ export interface ActionPayloads {
} & WithTabId; } & WithTabId;
closeGiftModal: WithTabId | undefined; closeGiftModal: WithTabId | undefined;
sendStarGift: StarGiftInfo & WithTabId; sendStarGift: StarGiftInfo & WithTabId;
openGiftInfoModalFromMessage: { openGiftInfoModalFromMessage: {
chatId: string; chatId: string;
messageId: number; messageId: number;
@ -2319,6 +2320,7 @@ export interface ActionPayloads {
gift: ApiStarGift; gift: ApiStarGift;
}) & WithTabId; }) & WithTabId;
closeGiftInfoModal: WithTabId | undefined; closeGiftInfoModal: WithTabId | undefined;
openGiftUpgradeModal: { openGiftUpgradeModal: {
giftId: string; giftId: string;
peerId?: string; peerId?: string;
@ -2330,6 +2332,7 @@ export interface ActionPayloads {
shouldKeepOriginalDetails?: boolean; shouldKeepOriginalDetails?: boolean;
upgradeStars?: number; upgradeStars?: number;
} & WithTabId; } & WithTabId;
openGiftWithdrawModal: { openGiftWithdrawModal: {
gift: ApiSavedStarGift; gift: ApiSavedStarGift;
} & WithTabId; } & WithTabId;
@ -2339,6 +2342,17 @@ export interface ActionPayloads {
gift: ApiInputSavedStarGift; gift: ApiInputSavedStarGift;
password: string; password: string;
} & WithTabId; } & WithTabId;
openGiftTransferModal: {
gift: ApiSavedStarGift;
} & WithTabId;
transferGift: {
gift: ApiInputSavedStarGift;
transferStars?: number;
recipientId: string;
} & WithTabId;
closeGiftTransferModal: WithTabId | undefined;
loadPeerSavedGifts: { loadPeerSavedGifts: {
peerId: string; peerId: string;
shouldRefresh?: boolean; shouldRefresh?: boolean;

View File

@ -722,6 +722,10 @@ export type TabState = {
gift: ApiSavedStarGift | ApiStarGift; gift: ApiSavedStarGift | ApiStarGift;
}; };
giftTransferModal?: {
gift: ApiSavedStarGift;
};
giftUpgradeModal?: { giftUpgradeModal?: {
sampleAttributes: ApiStarGiftAttribute[]; sampleAttributes: ApiStarGiftAttribute[];
recipientId?: string; recipientId?: string;
@ -748,4 +752,5 @@ export type TabState = {
}; };
isWaitingForStarGiftUpgrade?: true; isWaitingForStarGiftUpgrade?: true;
isWaitingForStarGiftTransfer?: true;
}; };

View File

@ -0,0 +1,66 @@
import { useState } from '../lib/teact/teact';
import type { ApiChat } from '../api/types';
import { callApi } from '../api/gramjs';
import useAsync from './useAsync';
import useDebouncedMemo from './useDebouncedMemo';
import useLastCallback from './useLastCallback';
const DEBOUNCE_TIMEOUT = 300;
export async function peerGlobalSearch(query: string) {
const searchResult = await callApi('searchChats', { query });
if (!searchResult) return undefined;
const ids = [...searchResult.accountResultIds, ...searchResult.globalResultIds];
return ids;
}
export function prepareChatMemberSearch(chat: ApiChat) {
return async (query: string) => {
const searchResult = await callApi('fetchMembers', {
chat,
memberFilter: 'search',
query,
});
return searchResult?.members?.map((member) => member.userId) || [];
};
}
export default function usePeerSearch({
query,
queryFn = peerGlobalSearch,
defaultValue,
debounceTimeout = DEBOUNCE_TIMEOUT,
isDisabled,
}: {
query: string;
queryFn?: (query: string) => Promise<string[] | undefined>;
defaultValue?: string[];
debounceTimeout?: number;
isDisabled?: boolean;
}) {
const debouncedQuery = useDebouncedMemo(() => query, debounceTimeout, [query]);
const [currentResultsQuery, setCurrentResultsQuery] = useState<string>('');
const searchQuery = !query ? query : debouncedQuery; // Ignore debounce if query is empty
const queryCallback = useLastCallback(queryFn);
const result = useAsync(async () => {
if (!searchQuery || isDisabled) {
setCurrentResultsQuery('');
return Promise.resolve(defaultValue);
}
const answer = await queryCallback(searchQuery);
setCurrentResultsQuery(searchQuery);
return answer;
}, [searchQuery, defaultValue, queryCallback, isDisabled], defaultValue);
return {
...result,
currentResultsQuery,
};
}

View File

@ -1197,7 +1197,10 @@ export interface LangPair {
'GiftInfoViewUpgraded': undefined; 'GiftInfoViewUpgraded': undefined;
'GiftInfoUpgradeBadge': undefined; 'GiftInfoUpgradeBadge': undefined;
'GiftInfoUpgradeForFree': undefined; 'GiftInfoUpgradeForFree': undefined;
'GiftInfoWithdraw': undefined; 'GiftInfoTransfer': undefined;
'GiftTransferTitle': undefined;
'GiftTransferTON': undefined;
'GiftTransferConfirmButtonFree': undefined;
'GiftUpgradeUniqueTitle': undefined; 'GiftUpgradeUniqueTitle': undefined;
'GiftUpgradeUniqueDescription': undefined; 'GiftUpgradeUniqueDescription': undefined;
'GiftUpgradeTransferableTitle': undefined; 'GiftUpgradeTransferableTitle': undefined;
@ -1672,6 +1675,10 @@ export interface LangPairWithVariables<V extends unknown = LangVariable> {
'GiftSend': { 'GiftSend': {
'amount': V; 'amount': V;
}; };
'GiftUnique': {
'title': V;
'number': V;
};
'GiftInfoPeerDescriptionFreeUpgradeOut': { 'GiftInfoPeerDescriptionFreeUpgradeOut': {
'peer': V; 'peer': V;
}; };
@ -1718,6 +1725,25 @@ export interface LangPairWithVariables<V extends unknown = LangVariable> {
'date': V; 'date': V;
'text': V; 'text': V;
}; };
'GiftTransferTONBlocked': {
'time': V;
};
'GiftTransferConfirmDescription': {
'gift': V;
'peer': V;
'amount': V;
};
'GiftTransferConfirmDescriptionFree': {
'gift': V;
'peer': V;
};
'GiftTransferConfirmButton': {
'amount': V;
};
'GiftTransferSuccessMessage': {
'gift': V;
'peer': V;
};
'GiftPeerUpgradeText': { 'GiftPeerUpgradeText': {
'peer': V; 'peer': V;
}; };

View File

@ -3,6 +3,7 @@ import type { TimeFormat } from '../../types';
import type { LangFn } from '../localization'; import type { LangFn } from '../localization';
import withCache from '../withCache'; import withCache from '../withCache';
import { getDays } from './units';
const WEEKDAYS_FULL = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; const WEEKDAYS_FULL = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const MONTHS_FULL = [ const MONTHS_FULL = [
@ -86,22 +87,26 @@ export function formatMonthAndYear(lang: OldLangFn, date: Date, isShort = false)
} }
export function formatCountdown( export function formatCountdown(
lang: OldLangFn, lang: LangFn,
msLeft: number, secondsLeft: number,
) { ) {
const days = Math.floor(msLeft / MILLISECONDS_IN_DAY); const days = getDays(secondsLeft);
if (msLeft < 0) { if (secondsLeft < 0) {
return 0; return 0;
} else if (days < 1) { } else if (days < 1) {
return formatMediaDuration(msLeft / 1000); return formatMediaDuration(secondsLeft);
} else if (days < 7) { } else if (days < 7) {
return lang('Days', days); const count = days;
return lang('Days', { count }, { pluralValue: count });
} else if (days < 30) { } else if (days < 30) {
return lang('Weeks', Math.floor(days / 7)); const count = Math.floor(days / 7);
return lang('Weeks', { count }, { pluralValue: count });
} else if (days < 365) { } else if (days < 365) {
return lang('Months', Math.floor(days / 30)); const count = Math.floor(days / 30);
return lang('Months', { count }, { pluralValue: count });
} else { } else {
return lang('Years', Math.floor(days / 365)); const count = Math.floor(days / 365);
return lang('Years', { count }, { pluralValue: count });
} }
} }

View File

@ -1,9 +1,17 @@
import { type FC, type Props, useRef } from '../../lib/teact/teact'; import { type FC, type Props, useRef } from '../../lib/teact/teact';
export default function freezeWhenClosed<T extends FC>(Component: T) { type InjectProps<T extends FC, P extends Props> = FC<Parameters<T>[0] & P>;
type OwnProps = {
ignoreFreeze?: boolean;
};
export default function freezeWhenClosed<T extends FC>(Component: T): InjectProps<T, OwnProps> {
function ComponentWrapper(props: Props) { function ComponentWrapper(props: Props) {
const newProps = useRef(props); const newProps = useRef(props);
if (props.ignoreFreeze) return Component(props);
if (props.isOpen) { if (props.isOpen) {
newProps.current = props; newProps.current = props;
} else { } else {