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':
default: {
const purpose = buildInputStorePaymentPurpose(invoice.purpose);

View File

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

View File

@ -694,7 +694,7 @@ export async function fetchStarGiftUpgradePreview({
return result.sampleAttributes.map(buildApiStarGiftAttribute).filter(Boolean);
}
export function upgradeGift({
export function upgradeStarGift({
inputSavedGift,
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({
inputGift,
password,

View File

@ -589,9 +589,16 @@ export type ApiInputInvoiceStarGiftUpgrade = {
shouldKeepOriginalDetails?: true;
};
export type ApiInputInvoiceStarGiftTransfer = {
type: 'stargiftTransfer';
inputSavedGift: ApiInputSavedStarGift;
recipientId: string;
};
export type ApiInputInvoice = ApiInputInvoiceMessage | ApiInputInvoiceSlug | ApiInputInvoiceGiveaway
| ApiInputInvoiceGiftCode | ApiInputInvoiceStars | ApiInputInvoiceStarsGift | ApiInputInvoiceStarGiftUpgrade
| ApiInputInvoiceStarsGiveaway | ApiInputInvoiceStarGift | ApiInputInvoiceChatInviteSubscription;
| ApiInputInvoiceGiftCode | ApiInputInvoiceStars | ApiInputInvoiceStarsGift
| ApiInputInvoiceStarsGiveaway | ApiInputInvoiceStarGift | ApiInputInvoiceChatInviteSubscription
| ApiInputInvoiceStarGiftUpgrade | ApiInputInvoiceStarGiftTransfer;
/* Used for Invoice request */
export type ApiRequestInputInvoiceMessage = {
@ -641,6 +648,13 @@ export type ApiRequestInputInvoiceStarGiftUpgrade = {
shouldKeepOriginalDetails?: true;
};
export type ApiRequestInputInvoiceStarGiftTransfer = {
type: 'stargiftTransfer';
inputSavedGift: ApiRequestInputSavedStarGift;
recipient: ApiPeer;
};
export type ApiRequestInputInvoice = ApiRequestInputInvoiceMessage | ApiRequestInputInvoiceSlug
| 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.";
"GiftHideNameDescriptionChannel" = "You can hide your name and message from all visitors of this channel except its admins.";
"GiftSend" = "Send a Gift for {amount}";
"GiftUnique" = "{title} #{number}";
"GiftInfoSent" = "Sent Gift";
"GiftInfoReceived" = "Received Gift";
"GiftInfoTitle" = "Gift";
@ -1436,7 +1437,15 @@
"GiftInfoViewUpgraded" = "View Upgraded Gift";
"GiftInfoUpgradeBadge" = "upgrade";
"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";
"GiftUpgradeUniqueDescription" = "Turn your gift into a unique collectible that you can transfer or auction.";
"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 GiftInfoModal } from '../components/modals/gift/info/GiftInfoModal';
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 {
filterChatsByName,
filterUsersByName,
getCanPostInChat,
isDeletedUser,
} 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 sortChatIds from './helpers/sortChatIds';
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
import useOldLang from '../../hooks/useOldLang';
import ChatOrUserPicker from './pickers/ChatOrUserPicker';
@ -55,7 +55,6 @@ const RecipientPicker: FC<OwnProps & StateProps> = ({
onClose,
onCloseAnimationEnd,
}) => {
const lang = useOldLang();
const [search, setSearch] = useState('');
const ids = useMemo(() => {
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
const global = getGlobal();
const usersById = global.users.byId;
const chatsById = global.chats.byId;
const chatFullInfoById = global.chats.fullInfoById;
const chatIds = [
const peerIds = [
...(activeListIds || []),
...((search && archivedListIds) || []),
].filter((id) => {
const chat = chatsById[id];
const user = usersById[id];
const chat = selectChat(global, id);
const user = selectUser(global, id);
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(
unique([
...(currentUserId ? [currentUserId] : []),
...filterChatsByName(lang, chatIds, chatsById, search, currentUserId),
...(contactIds && filter.includes('users') ? filterUsersByName(contactIds, usersById, search) : []),
]),
filterPeersByQuery({
ids: unique([
...(currentUserId ? [currentUserId] : []),
...peerIds,
...(contactIds || []),
]),
query: search,
}),
undefined,
priorityIds,
currentUserId,
);
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)!;

View File

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

View File

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

View File

@ -5,7 +5,8 @@ import { getActions, withGlobal } from '../../../global';
import type { ApiUser, ApiUserStatus } from '../../../api/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 useHistoryBack from '../../../hooks/useHistoryBack';
@ -61,7 +62,7 @@ const ContactList: FC<OwnProps & StateProps> = ({
return undefined;
}
const filteredIds = filterUsersByName(contactIds, usersById, filter);
const filteredIds = filterPeersByQuery({ ids: contactIds, query: filter, type: 'user' });
return sortUserIds(filteredIds, 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 { 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 { unique } from '../../../util/iteratees';
import sortChatIds from '../../common/helpers/sortChatIds';
@ -63,7 +64,8 @@ const NewChatStep1: FC<OwnProps & StateProps> = ({
const displayedIds = useMemo(() => {
// No need for expensive global updates on users, so we avoid them
const usersById = getGlobal().users.byId;
const foundContactIds = localContactIds ? filterUsersByName(localContactIds, usersById, searchQuery) : [];
const foundContactIds = localContactIds
? filterPeersByQuery({ ids: localContactIds, query: searchQuery, type: 'user' }) : [];
return sortChatIds(
unique([

View File

@ -3,12 +3,12 @@ import React, {
memo, useCallback, useMemo, useRef,
useState,
} from '../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../global';
import { getActions, withGlobal } from '../../../global';
import { LoadMoreDirection } from '../../../types';
import { SLIDE_TRANSITION_DURATION } from '../../../config';
import { filterUsersByName } from '../../../global/helpers';
import { filterPeersByQuery } from '../../../global/helpers/peers';
import { selectTabState } from '../../../global/selectors';
import { throttle } from '../../../util/schedulers';
@ -58,8 +58,7 @@ const BotAppResults: FC<OwnProps & StateProps> = ({
const recentSet = new Set(recentBotIds);
const withoutRecent = foundIds.filter((id) => !recentSet.has(id));
const usersById = getGlobal().users.byId;
return filterUsersByName(withoutRecent, usersById, searchQuery);
return filterPeersByQuery({ ids: withoutRecent, query: searchQuery, type: 'user' });
}, [foundIds, recentBotIds, searchQuery]);
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 {
filterChatsByName,
filterUsersByName,
isChatChannel,
} from '../../../global/helpers';
import { filterPeersByQuery } from '../../../global/helpers/peers';
import { selectSimilarChannelIds, selectTabState } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import { getOrderedIds } from '../../../util/folderManager';
@ -221,7 +220,6 @@ const ChatResults: FC<OwnProps & StateProps> = ({
}
// No need for expensive global updates, so we avoid them
const usersById = getGlobal().users.byId;
const chatsById = getGlobal().chats.byId;
const orderedChatIds = getOrderedIds(ALL_FOLDER_ID) ?? [];
@ -230,7 +228,7 @@ const ChatResults: FC<OwnProps & StateProps> = ({
const chat = chatsById[id];
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;
@ -239,9 +237,9 @@ const ChatResults: FC<OwnProps & StateProps> = ({
...(contactIds || []),
];
const localContactIds = filterUsersByName(
contactIdsWithMe, usersById, searchQuery, currentUserId, oldLang('SavedMessages'),
);
const localContactIds = filterPeersByQuery({
ids: contactIdsWithMe, query: searchQuery, type: 'user',
});
const localPeerIds = [
...localContactIds,
@ -252,7 +250,7 @@ const ChatResults: FC<OwnProps & StateProps> = ({
...sortChatIds(localPeerIds, undefined, currentUserId ? [currentUserId] : undefined),
...sortChatIds(accountPeerIds || []),
]);
}, [searchQuery, oldLang, currentUserId, contactIds, accountPeerIds, isChannelList]);
}, [searchQuery, currentUserId, contactIds, accountPeerIds, isChannelList]);
useHorizontalScroll(chatSelectionRef, !localResults.length || isChannelList, true);

View File

@ -7,7 +7,8 @@ import { getActions, withGlobal } from '../../../global';
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 { unique } from '../../../util/iteratees';
@ -57,7 +58,7 @@ const BlockUserModal: FC<OwnProps & StateProps> = ({
return contactId !== currentUserId && !blockedIds.includes(contactId);
}));
return filterUsersByName(availableContactIds, usersById, search)
return filterPeersByQuery({ ids: availableContactIds, query: search, type: 'user' })
.sort((firstId, secondId) => {
const firstName = getUserFullName(usersById[firstId]) || '';
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 {
filterChatsByName, isChatChannel, isDeletedUser,
isChatChannel, isDeletedUser,
} from '../../../global/helpers';
import { filterPeersByQuery } from '../../../global/helpers/peers';
import { unique } from '../../../util/iteratees';
import { CUSTOM_PEER_PREMIUM } from '../../../util/objects/customPeer';
import { getPrivacyKey } from './helpers/privacy';
@ -122,16 +123,16 @@ const SettingsPrivacyVisibilityExceptionList: FC<OwnProps & StateProps> = ({
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
if (searchQuery) return filteredChats;
return unique([
...selectedContactIds,
...filterChatsByName(oldLang, chatIds, chatsById, searchQuery),
...chatIds,
]);
}, [folderAllOrderedIds, folderArchivedOrderedIds, selectedContactIds, oldLang, searchQuery, currentUserId]);
}, [folderAllOrderedIds, folderArchivedOrderedIds, selectedContactIds, searchQuery, currentUserId]);
const handleSelectedCategoriesChange = useCallback((value: CustomPeerType[]) => {
setNewSelectedCategoryTypes(value);

View File

@ -2,12 +2,12 @@ import type { FC } from '../../../../lib/teact/teact';
import React, {
memo, useEffect, useMemo, useState,
} from '../../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../../global';
import { getActions, withGlobal } from '../../../../global';
import type { FolderEditDispatch, FoldersState } from '../../../../hooks/reducers/useFoldersReducer';
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 { unique } from '../../../../util/iteratees';
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]);
const displayedIds = useMemo(() => {
// No need for expensive global updates on chats, so we avoid them
const chatsById = getGlobal().chats.byId;
const chatIds = [...folderAllOrderedIds || [], ...folderArchivedOrderedIds || []];
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) => {
dispatch({

View File

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

View File

@ -6,10 +6,10 @@ import { getActions, getGlobal, withGlobal } from '../../../global';
import type { ApiChatMember } from '../../../api/types';
import {
filterUsersByName,
isUserBot,
sortUserIds,
} from '../../../global/helpers';
import { filterPeersByQuery } from '../../../global/helpers/peers';
import { selectChatFullInfo } from '../../../global/selectors';
import { unique } from '../../../util/iteratees';
import sortChatIds from '../../common/helpers/sortChatIds';
@ -74,9 +74,10 @@ const GiveawayUserPickerModal = ({
const displayedMemberIds = useMemo(() => {
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];
if (!user) {
return true;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,8 +10,9 @@ import type {
import { NewChatMembersProgress } from '../../types';
import {
filterUsersByName, isChatChannel, isUserBot,
isChatChannel, isUserBot,
} from '../../global/helpers';
import { filterPeersByQuery } from '../../global/helpers/peers';
import { selectChat, selectChatFullInfo, selectTabState } from '../../global/selectors';
import { unique } from '../../util/iteratees';
import sortChatIds from '../common/helpers/sortChatIds';
@ -83,14 +84,18 @@ const AddChatMembers: FC<OwnProps & StateProps> = ({
const displayedIds = useMemo(() => {
// No need for expensive global updates on users, so we avoid them
const usersById = getGlobal().users.byId;
const filteredContactIds = localContactIds ? filterUsersByName(localContactIds, usersById, searchQuery) : [];
return sortChatIds(
unique([
...filteredContactIds,
const filteredIds = filterPeersByQuery({
ids: unique([
...(localContactIds || []),
...(localUserIds || []),
...(globalUserIds || []),
]).filter((userId) => {
]),
query: searchQuery,
type: 'user',
});
return sortChatIds(
filteredIds.filter((userId) => {
const user = usersById[userId];
// 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 {
filterUsersByName, getHasAdminRight, isChatBasicGroup,
getHasAdminRight, isChatBasicGroup,
isChatChannel, isUserBot, isUserRightBanned, sortUserIds,
} from '../../../global/helpers';
import { filterPeersByQuery } from '../../../global/helpers/peers';
import { selectChat, selectChatFullInfo, selectTabState } from '../../../global/selectors';
import { unique } from '../../../util/iteratees';
import sortChatIds from '../../common/helpers/sortChatIds';
@ -121,7 +122,7 @@ const ManageGroupMembers: FC<OwnProps & StateProps> = ({
const shouldUseSearchResults = Boolean(searchQuery);
const listedIds = !shouldUseSearchResults
? memberIds
: (localContactIds ? filterUsersByName(localContactIds, usersById, searchQuery) : []);
: (localContactIds ? filterPeersByQuery({ ids: localContactIds, query: searchQuery, type: 'user' }) : []);
return sortChatIds(
unique([

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import type {
ApiInputInvoice, ApiInputInvoiceStarGift, ApiInputInvoiceStarGiftUpgrade, ApiRequestInputInvoice,
ApiInputInvoice, ApiInputInvoiceStarGift, ApiRequestInputInvoice,
} from '../../../api/types';
import type { ApiCredentials } from '../../../components/payment/PaymentModal';
import type { RegularLangFnParameters } from '../../../util/localization';
@ -977,7 +977,7 @@ addActionHandler('upgradeGift', (global, actions, payload): ActionReturnType =>
actions.closeGiftInfoModal({ tabId });
if (!upgradeStars) {
callApi('upgradeGift', {
callApi('upgradeStarGift', {
inputSavedGift: requestSavedGift,
shouldKeepOriginalDetails: shouldKeepOriginalDetails || undefined,
});
@ -985,7 +985,7 @@ addActionHandler('upgradeGift', (global, actions, payload): ActionReturnType =>
return;
}
const invoice: ApiInputInvoiceStarGiftUpgrade = {
const invoice: ApiInputInvoice = {
type: 'stargiftUpgrade',
inputSavedGift: gift,
shouldKeepOriginalDetails: shouldKeepOriginalDetails || undefined,
@ -994,6 +994,46 @@ addActionHandler('upgradeGift', (global, actions, payload): ActionReturnType =>
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>(
global: T, inputInvoice: ApiInputInvoice, price: number,
...[tabId = getCurrentTabId()]: TabArgs<T>

View File

@ -2,7 +2,8 @@ import type { ActionReturnType } from '../../types';
import { PaymentStep } from '../../../types';
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 {
addBlockedUser,
@ -21,7 +22,12 @@ import {
updateThreadInfos,
} from '../../reducers';
import { updateTabState } from '../../reducers/tabs';
import { selectPeerStories, selectPeerStory, selectTabState } from '../../selectors';
import {
selectPeer,
selectPeerStories,
selectPeerStory,
selectTabState,
} from '../../selectors';
addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
switch (update['@type']) {
@ -235,7 +241,44 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
isWaitingForStarGiftUpgrade: undefined,
}, 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 * as langProvider from '../../../util/oldLangProvider';
import { addTabStateResetterAction } from '../../helpers/meta';
import { getPrizeStarsTransactionFromGiveaway, getStarsTransactionFromGift } from '../../helpers/payments';
import { addActionHandler } from '../../index';
import {
@ -63,13 +64,7 @@ addActionHandler('openGiftRecipientPicker', (global, actions, payload): ActionRe
}, tabId);
});
addActionHandler('closeGiftRecipientPicker', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
isGiftRecipientPickerOpen: undefined,
}, tabId);
});
addTabStateResetterAction('closeGiftRecipientPicker', 'isGiftRecipientPickerOpen');
addActionHandler('openStarsGiftingPickerModal', (global, actions, payload): ActionReturnType => {
const {
@ -83,13 +78,7 @@ addActionHandler('openStarsGiftingPickerModal', (global, actions, payload): Acti
}, tabId);
});
addActionHandler('closeStarsGiftingPickerModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
starsGiftingPickerModal: undefined,
}, tabId);
});
addTabStateResetterAction('closeStarsGiftingPickerModal', 'starsGiftingPickerModal');
addActionHandler('openPrizeStarsTransactionFromGiveaway', (global, actions, payload): ActionReturnType => {
const {
@ -148,13 +137,7 @@ addActionHandler('openStarsBalanceModal', (global, actions, payload): ActionRetu
}, tabId);
});
addActionHandler('closeStarsBalanceModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
starsBalanceModal: undefined,
}, tabId);
});
addTabStateResetterAction('closeStarsBalanceModal', 'starsBalanceModal');
addActionHandler('closeStarsPaymentModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
@ -193,13 +176,7 @@ addActionHandler('openStarsTransactionFromGift', (global, actions, payload): Act
return openStarsTransactionModal(global, transaction, tabId);
});
addActionHandler('closeStarsTransactionModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
starsTransactionModal: undefined,
}, tabId);
});
addTabStateResetterAction('closeStarsTransactionModal', 'starsTransactionModal');
addActionHandler('openStarsSubscriptionModal', (global, actions, payload): ActionReturnType => {
const { subscription, tabId = getCurrentTabId() } = payload;
@ -211,20 +188,9 @@ addActionHandler('openStarsSubscriptionModal', (global, actions, payload): Actio
}, tabId);
});
addActionHandler('closeStarsSubscriptionModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
addTabStateResetterAction('closeStarsSubscriptionModal', 'starsSubscriptionModal');
return updateTabState(global, {
starsSubscriptionModal: undefined,
}, tabId);
});
addActionHandler('closeGiftModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
giftModal: undefined,
}, tabId);
});
addTabStateResetterAction('closeGiftModal', 'giftModal');
addActionHandler('closeStarsGiftModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
@ -287,21 +253,9 @@ addActionHandler('openGiftInfoModal', (global, actions, payload): ActionReturnTy
}, tabId);
});
addActionHandler('closeGiftInfoModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
addTabStateResetterAction('closeGiftInfoModal', 'giftInfoModal');
return updateTabState(global, {
giftInfoModal: undefined,
}, tabId);
});
addActionHandler('closeGiftUpgradeModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
giftUpgradeModal: undefined,
}, tabId);
});
addTabStateResetterAction('closeGiftUpgradeModal', 'giftUpgradeModal');
addActionHandler('openGiftWithdrawModal', (global, actions, payload): ActionReturnType => {
const { gift, tabId = getCurrentTabId() } = payload || {};
@ -313,13 +267,7 @@ addActionHandler('openGiftWithdrawModal', (global, actions, payload): ActionRetu
}, tabId);
});
addActionHandler('closeGiftWithdrawModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
giftWithdrawModal: undefined,
}, tabId);
});
addTabStateResetterAction('closeGiftWithdrawModal', 'giftWithdrawModal');
addActionHandler('clearGiftWithdrawError', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
@ -334,3 +282,15 @@ addActionHandler('clearGiftWithdrawError', (global, actions, payload): ActionRet
},
}, 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,
} from '../../config';
import { formatDateToString, formatTime } from '../../util/dates/dateFormat';
import { prepareSearchWordsForNeedle } from '../../util/searchWords';
import { getGlobal } from '..';
import { isSystemBot } from './bots';
import { getMainUsername, getUserFirstOrLastName } from './users';
@ -395,36 +394,6 @@ export function getMessageSenderName(lang: OldLangFn, chatId: string, sender?: A
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) {
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;
}

View File

@ -1,6 +1,12 @@
import type { ApiChat, ApiPeer, ApiUser } from '../../api/types';
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 {
return 'title' in peer;
@ -10,6 +16,44 @@ export function isApiPeerUser(peer: ApiPeer): peer is ApiUser {
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) {
if (isApiPeerChat(peer)) {
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 { orderBy } from '../../util/iteratees';
import { formatPhoneNumber } from '../../util/phoneNumber';
import { prepareSearchWordsForNeedle } from '../../util/searchWords';
import { getServerTime, getServerTimeOffset } from '../../util/serverTime';
export function getUserFirstOrLastName(user?: ApiUser) {
@ -239,31 +238,6 @@ export function sortUserIds(
}, '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) {
return userOrChat.usernames?.find((u) => u.isActive)?.username;
}

View File

@ -14,13 +14,18 @@ type ProjectActionTypes =
type ProjectActionNames = keyof ProjectActionTypes;
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.
// There are three types of actions:
// 1. With tabId, which is made required when calling action from another action handler
// 2. Without payload (= undefined), hence made the payload not required
// 3. With payload, hence made the payload required
export type RequiredGlobalActions = {
[ActionName in ProjectActionNames]: 'tabId' extends keyof Helper<ProjectActionTypes[ActionName], undefined> ? ((
[ActionName in ProjectActionNames]: ActionName extends TabStateActionNames ? ((
payload: ProjectActionTypes[ActionName] & { tabId: number },
options?: ActionOptions,
) => void) :

View File

@ -3,9 +3,10 @@ import type { GlobalState, TabArgs } from '../types';
import { SERVICE_NOTIFICATIONS_USER_ID } from '../../config';
import { getCurrentTabId } from '../../util/establishMultitabRole';
import { isDeletedUser } from '../helpers';
import { selectChat, selectChatFullInfo } from './chats';
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 {
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 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
&& areStarGiftsAvailable);
return selectChatFullInfo(global, peerId)?.areStarGiftsAvailable;
}
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 { TabState } from './tabState';
type WithTabId = { tabId?: number };
export type WithTabId = { tabId?: number };
export interface ActionPayloads {
// system
@ -2308,6 +2308,7 @@ export interface ActionPayloads {
} & WithTabId;
closeGiftModal: WithTabId | undefined;
sendStarGift: StarGiftInfo & WithTabId;
openGiftInfoModalFromMessage: {
chatId: string;
messageId: number;
@ -2319,6 +2320,7 @@ export interface ActionPayloads {
gift: ApiStarGift;
}) & WithTabId;
closeGiftInfoModal: WithTabId | undefined;
openGiftUpgradeModal: {
giftId: string;
peerId?: string;
@ -2330,6 +2332,7 @@ export interface ActionPayloads {
shouldKeepOriginalDetails?: boolean;
upgradeStars?: number;
} & WithTabId;
openGiftWithdrawModal: {
gift: ApiSavedStarGift;
} & WithTabId;
@ -2339,6 +2342,17 @@ export interface ActionPayloads {
gift: ApiInputSavedStarGift;
password: string;
} & WithTabId;
openGiftTransferModal: {
gift: ApiSavedStarGift;
} & WithTabId;
transferGift: {
gift: ApiInputSavedStarGift;
transferStars?: number;
recipientId: string;
} & WithTabId;
closeGiftTransferModal: WithTabId | undefined;
loadPeerSavedGifts: {
peerId: string;
shouldRefresh?: boolean;

View File

@ -722,6 +722,10 @@ export type TabState = {
gift: ApiSavedStarGift | ApiStarGift;
};
giftTransferModal?: {
gift: ApiSavedStarGift;
};
giftUpgradeModal?: {
sampleAttributes: ApiStarGiftAttribute[];
recipientId?: string;
@ -748,4 +752,5 @@ export type TabState = {
};
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;
'GiftInfoUpgradeBadge': undefined;
'GiftInfoUpgradeForFree': undefined;
'GiftInfoWithdraw': undefined;
'GiftInfoTransfer': undefined;
'GiftTransferTitle': undefined;
'GiftTransferTON': undefined;
'GiftTransferConfirmButtonFree': undefined;
'GiftUpgradeUniqueTitle': undefined;
'GiftUpgradeUniqueDescription': undefined;
'GiftUpgradeTransferableTitle': undefined;
@ -1672,6 +1675,10 @@ export interface LangPairWithVariables<V extends unknown = LangVariable> {
'GiftSend': {
'amount': V;
};
'GiftUnique': {
'title': V;
'number': V;
};
'GiftInfoPeerDescriptionFreeUpgradeOut': {
'peer': V;
};
@ -1718,6 +1725,25 @@ export interface LangPairWithVariables<V extends unknown = LangVariable> {
'date': 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': {
'peer': V;
};

View File

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