diff --git a/src/api/gramjs/gramjsBuilders/index.ts b/src/api/gramjs/gramjsBuilders/index.ts index f44885f4d..a64d4c23d 100644 --- a/src/api/gramjs/gramjsBuilders/index.ts +++ b/src/api/gramjs/gramjsBuilders/index.ts @@ -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); diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index fbfe2040b..4a06997c4 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -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) { diff --git a/src/api/gramjs/methods/payments.ts b/src/api/gramjs/methods/payments.ts index f65b5a7df..0513b6881 100644 --- a/src/api/gramjs/methods/payments.ts +++ b/src/api/gramjs/methods/payments.ts @@ -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, diff --git a/src/api/types/payments.ts b/src/api/types/payments.ts index ef0ae44b5..0313df5d1 100644 --- a/src/api/types/payments.ts +++ b/src/api/types/payments.ts @@ -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; diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index ff57aecc7..2103b28d2 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -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"; diff --git a/src/bundles/stars.ts b/src/bundles/stars.ts index c6437704f..fdef4b332 100644 --- a/src/bundles/stars.ts +++ b/src/bundles/stars.ts @@ -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'; diff --git a/src/components/common/RecipientPicker.tsx b/src/components/common/RecipientPicker.tsx index 90aca7a68..068bb2b85 100644 --- a/src/components/common/RecipientPicker.tsx +++ b/src/components/common/RecipientPicker.tsx @@ -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 = ({ onClose, onCloseAnimationEnd, }) => { - const lang = useOldLang(); const [search, setSearch] = useState(''); const ids = useMemo(() => { if (!isOpen) return undefined; @@ -67,34 +66,36 @@ const RecipientPicker: FC = ({ // 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)!; diff --git a/src/components/common/pickers/PeerPicker.tsx b/src/components/common/pickers/PeerPicker.tsx index 541c20e98..28e1682af 100644 --- a/src/components/common/pickers/PeerPicker.tsx +++ b/src/components/common/pickers/PeerPicker.tsx @@ -31,18 +31,18 @@ import PickerItem from './PickerItem'; import styles from './PickerStyles.module.scss'; -type SingleModeProps = { +type SingleModeProps = { 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 = { 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 = { className?: string; - categories?: UniqueCustomPeer[]; + categories?: UniqueCustomPeer[]; 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 | MultipleModeProps); // 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 = ({ className, categories, itemIds, @@ -106,12 +107,13 @@ const PeerPicker = ({ itemInputType, withStatus, withPeerTypes, + withPeerUsernames, withDefaultPadding, onFilterChange, onDisabledClick, onLoadMore, ...optionalProps -}: OwnProps) => { +}: OwnProps) => { 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(() => { diff --git a/src/components/common/pickers/PickerStyles.module.scss b/src/components/common/pickers/PickerStyles.module.scss index ecb059420..69bda0cf7 100644 --- a/src/components/common/pickers/PickerStyles.module.scss +++ b/src/components/common/pickers/PickerStyles.module.scss @@ -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; } } diff --git a/src/components/left/main/ContactList.tsx b/src/components/left/main/ContactList.tsx index 4b02162f0..b55706e1e 100644 --- a/src/components/left/main/ContactList.tsx +++ b/src/components/left/main/ContactList.tsx @@ -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 = ({ 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]); diff --git a/src/components/left/newChat/NewChatStep1.tsx b/src/components/left/newChat/NewChatStep1.tsx index 7ef6a5016..8f677db17 100644 --- a/src/components/left/newChat/NewChatStep1.tsx +++ b/src/components/left/newChat/NewChatStep1.tsx @@ -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 = ({ 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([ diff --git a/src/components/left/search/BotAppResults.tsx b/src/components/left/search/BotAppResults.tsx index e96b1b1a8..047f11abf 100644 --- a/src/components/left/search/BotAppResults.tsx +++ b/src/components/left/search/BotAppResults.tsx @@ -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 = ({ 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) => { diff --git a/src/components/left/search/ChatResults.tsx b/src/components/left/search/ChatResults.tsx index 969b82baf..d6895fdb5 100644 --- a/src/components/left/search/ChatResults.tsx +++ b/src/components/left/search/ChatResults.tsx @@ -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 = ({ } // 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 = ({ 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 = ({ ...(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 = ({ ...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); diff --git a/src/components/left/settings/BlockUserModal.tsx b/src/components/left/settings/BlockUserModal.tsx index e734db7c3..e1a0b6d0b 100644 --- a/src/components/left/settings/BlockUserModal.tsx +++ b/src/components/left/settings/BlockUserModal.tsx @@ -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 = ({ 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]) || ''; diff --git a/src/components/left/settings/SettingsPrivacyVisibilityExceptionList.tsx b/src/components/left/settings/SettingsPrivacyVisibilityExceptionList.tsx index 357bda0f2..9cd3a74eb 100644 --- a/src/components/left/settings/SettingsPrivacyVisibilityExceptionList.tsx +++ b/src/components/left/settings/SettingsPrivacyVisibilityExceptionList.tsx @@ -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 = ({ 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); diff --git a/src/components/left/settings/folders/SettingsFoldersChatFilters.tsx b/src/components/left/settings/folders/SettingsFoldersChatFilters.tsx index 726b6046a..6c2cc737e 100644 --- a/src/components/left/settings/folders/SettingsFoldersChatFilters.tsx +++ b/src/components/left/settings/folders/SettingsFoldersChatFilters.tsx @@ -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 = ({ }, [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({ diff --git a/src/components/main/premium/GiveawayChannelPickerModal.tsx b/src/components/main/premium/GiveawayChannelPickerModal.tsx index f83bf01fc..263c09b03 100644 --- a/src/components/main/premium/GiveawayChannelPickerModal.tsx +++ b/src/components/main/premium/GiveawayChannelPickerModal.tsx @@ -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; diff --git a/src/components/main/premium/GiveawayUserPickerModal.tsx b/src/components/main/premium/GiveawayUserPickerModal.tsx index 200cd395d..2c98a89f3 100644 --- a/src/components/main/premium/GiveawayUserPickerModal.tsx +++ b/src/components/main/premium/GiveawayUserPickerModal.tsx @@ -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; diff --git a/src/components/main/premium/StarsGiftingPickerModal.tsx b/src/components/main/premium/StarsGiftingPickerModal.tsx index b6650c543..852ceff6f 100644 --- a/src/components/main/premium/StarsGiftingPickerModal.tsx +++ b/src/components/main/premium/StarsGiftingPickerModal.tsx @@ -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 = ({ 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) { diff --git a/src/components/middle/composer/hooks/useMentionTooltip.ts b/src/components/middle/composer/hooks/useMentionTooltip.ts index c8e1541fa..dcf16fe59 100644 --- a/src/components/middle/composer/hooks/useMentionTooltip.ts +++ b/src/components/middle/composer/hooks/useMentionTooltip.ts @@ -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]); diff --git a/src/components/modals/ModalContainer.tsx b/src/components/modals/ModalContainer.tsx index 549603901..d5a3deafe 100644 --- a/src/components/modals/ModalContainer.tsx +++ b/src/components/modals/ModalContainer.tsx @@ -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; 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; diff --git a/src/components/modals/gift/info/GiftInfoModal.tsx b/src/components/modals/gift/info/GiftInfoModal.tsx index f7c131643..3e6a1b5ae 100644 --- a/src/components/modals/gift/info/GiftInfoModal.tsx +++ b/src/components/modals/gift/info/GiftInfoModal.tsx @@ -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')} {canUpdate && isUniqueGift && ( - - {lang('GiftInfoWithdraw')} + + {lang('GiftInfoTransfer')} )} diff --git a/src/components/modals/gift/recipient/GiftRecipientPicker.tsx b/src/components/modals/gift/recipient/GiftRecipientPicker.tsx index 648f6cfb6..09faa679e 100644 --- a/src/components/modals/gift/recipient/GiftRecipientPicker.tsx +++ b/src/components/modals/gift/recipient/GiftRecipientPicker.tsx @@ -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 = ({ +const GiftRecipientPicker = ({ modal, currentUserId, userIds, -}) => { +}: OwnProps & StateProps) => { const { closeGiftRecipientPicker, openGiftModal } = getActions(); const oldLang = useOldLang(); @@ -41,17 +38,12 @@ const GiftRecipientPicker: FC = ({ const [searchQuery, setSearchQuery] = useState(''); 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((global): StateProps => { return { currentUserId, userIds: global.contactList?.userIds, - userSelectionLimit: global.appConfig?.giveawayAddPeersMax, }; })(GiftRecipientPicker)); diff --git a/src/components/modals/gift/transfer/GiftTransferModal.async.tsx b/src/components/modals/gift/transfer/GiftTransferModal.async.tsx new file mode 100644 index 000000000..4b304722d --- /dev/null +++ b/src/components/modals/gift/transfer/GiftTransferModal.async.tsx @@ -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 = (props) => { + const { modal } = props; + const GiftTransferModal = useModuleLoader(Bundles.Stars, 'GiftTransferModal', !modal); + + // eslint-disable-next-line react/jsx-props-no-spreading + return GiftTransferModal ? : undefined; +}; + +export default GiftTransferModalAsync; diff --git a/src/components/modals/gift/transfer/GiftTransferModal.module.scss b/src/components/modals/gift/transfer/GiftTransferModal.module.scss new file mode 100644 index 000000000..257fe3841 --- /dev/null +++ b/src/components/modals/gift/transfer/GiftTransferModal.module.scss @@ -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); +} diff --git a/src/components/modals/gift/transfer/GiftTransferModal.tsx b/src/components/modals/gift/transfer/GiftTransferModal.tsx new file mode 100644 index 000000000..479dd5b45 --- /dev/null +++ b/src/components/modals/gift/transfer/GiftTransferModal.tsx @@ -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(''); + + const renderingModal = useCurrentOrPrev(modal); + const uniqueGift = renderingModal?.gift?.gift as ApiStarGiftUnique; + const giftAttributes = uniqueGift && getGiftAttributes(uniqueGift); + + const [selectedId, setSelectedId] = useState(); + + 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[]; + }, [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 ( + + + itemIds={displayIds} + categories={categories} + onSelectedCategoryChange={handleCategoryChange} + withDefaultPadding + withPeerUsernames + isSearchable + noScrollRestore + isLoading={isLoading} + filterValue={searchQuery} + filterPlaceholder={lang('Search')} + onFilterChange={setSearchQuery} + onSelectedIdChange={setSelectedId} + /> + {giftAttributes && ( + +
+
+ + +
+ + +
+

+ {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, + })} +

+
+ )} +
+ ); +}; + +export default memo(withGlobal( + (global): StateProps => { + const { contactList, currentUserId } = global; + + return { + contactIds: contactList?.userIds, + currentUserId, + }; + }, +)(GiftTransferModal)); diff --git a/src/components/modals/gift/fragment/GiftWithdrawModal.async.tsx b/src/components/modals/gift/withdraw/GiftWithdrawModal.async.tsx similarity index 100% rename from src/components/modals/gift/fragment/GiftWithdrawModal.async.tsx rename to src/components/modals/gift/withdraw/GiftWithdrawModal.async.tsx diff --git a/src/components/modals/gift/fragment/GiftWithdrawModal.module.scss b/src/components/modals/gift/withdraw/GiftWithdrawModal.module.scss similarity index 100% rename from src/components/modals/gift/fragment/GiftWithdrawModal.module.scss rename to src/components/modals/gift/withdraw/GiftWithdrawModal.module.scss diff --git a/src/components/modals/gift/fragment/GiftWithdrawModal.tsx b/src/components/modals/gift/withdraw/GiftWithdrawModal.tsx similarity index 100% rename from src/components/modals/gift/fragment/GiftWithdrawModal.tsx rename to src/components/modals/gift/withdraw/GiftWithdrawModal.tsx diff --git a/src/components/modals/webApp/MoreAppsTabContent.tsx b/src/components/modals/webApp/MoreAppsTabContent.tsx index 34dbecc45..e335122ad 100644 --- a/src/components/modals/webApp/MoreAppsTabContent.tsx +++ b/src/components/modals/webApp/MoreAppsTabContent.tsx @@ -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 = ({ 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 }) => { diff --git a/src/components/right/AddChatMembers.tsx b/src/components/right/AddChatMembers.tsx index f37ee1a38..5c645fea0 100644 --- a/src/components/right/AddChatMembers.tsx +++ b/src/components/right/AddChatMembers.tsx @@ -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 = ({ 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: diff --git a/src/components/right/management/ManageGroupMembers.tsx b/src/components/right/management/ManageGroupMembers.tsx index 28e7f6d2e..966aad986 100644 --- a/src/components/right/management/ManageGroupMembers.tsx +++ b/src/components/right/management/ManageGroupMembers.tsx @@ -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 = ({ const shouldUseSearchResults = Boolean(searchQuery); const listedIds = !shouldUseSearchResults ? memberIds - : (localContactIds ? filterUsersByName(localContactIds, usersById, searchQuery) : []); + : (localContactIds ? filterPeersByQuery({ ids: localContactIds, query: searchQuery, type: 'user' }) : []); return sortChatIds( unique([ diff --git a/src/components/right/management/ManageInvites.tsx b/src/components/right/management/ManageInvites.tsx index d1a6c1b31..ea43070dd 100644 --- a/src/components/right/management/ManageInvites.tsx +++ b/src/components/right/management/ManageInvites.tsx @@ -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 = ({ 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 = ({ 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 = ({ } = 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 = ({ 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 = ({ 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 = ({ size={STICKER_SIZE_INVITES} className="section-icon" /> -

{isChannel ? lang('PrimaryLinkHelpChannel') : lang('PrimaryLinkHelp')}

+

{isChannel ? oldLang('PrimaryLinkHelpChannel') : oldLang('PrimaryLinkHelp')}

{primaryInviteLink && (
@@ -288,13 +290,13 @@ const ManageInvites: FC = ({ 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')} />
)}
{(!temporalInvites || !temporalInvites.length) && } {temporalInvites?.map((invite) => ( @@ -313,18 +315,18 @@ const ManageInvites: FC = ({ ))} -

{lang('ManageLinksInfoHelp')}

+

{oldLang('ManageLinksInfoHelp')}

{revokedExportedInvites && Boolean(revokedExportedInvites.length) && (
-

{lang('RevokedLinks')}

+

{oldLang('RevokedLinks')}

- {lang('DeleteAllRevokedLinks')} + {oldLang('DeleteAllRevokedLinks')} {revokedExportedInvites?.map((invite) => ( = ({
diff --git a/src/components/right/management/RemoveGroupUserModal.tsx b/src/components/right/management/RemoveGroupUserModal.tsx index d2c437ee4..afe6d5b62 100644 --- a/src/components/right/management/RemoveGroupUserModal.tsx +++ b/src/components/right/management/RemoveGroupUserModal.tsx @@ -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 = ({ 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) => { diff --git a/src/components/story/StorySettings.tsx b/src/components/story/StorySettings.tsx index 317fcb64c..d326ca9a2 100644 --- a/src/components/story/StorySettings.tsx +++ b/src/components/story/StorySettings.tsx @@ -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} /> diff --git a/src/components/story/privacy/AllowDenyList.tsx b/src/components/story/privacy/AllowDenyList.tsx index 56abaf833..3719b2108 100644 --- a/src/components/story/privacy/AllowDenyList.tsx +++ b/src/components/story/privacy/AllowDenyList.tsx @@ -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; 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(''); 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 ( { 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) { diff --git a/src/components/ui/ConfirmDialog.tsx b/src/components/ui/ConfirmDialog.tsx index 215c1484c..14a884680 100644 --- a/src/components/ui/ConfirmDialog.tsx +++ b/src/components/ui/ConfirmDialog.tsx @@ -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 = ({ isOpen, title, + noDefaultTitle, header, text, textParts, @@ -60,7 +62,7 @@ const ConfirmDialog: FC = ({ return ( ; isLowStackPriority?: boolean; dialogContent?: React.ReactNode; + ignoreFreeze?: boolean; onClose: () => void; onCloseAnimationEnd?: () => void; onEnter?: () => void; diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index 0622a3a8a..c39973c45 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -1941,7 +1941,7 @@ addActionHandler('loadMoreMembers', async (global, actions, payload): Promise= chat.membersCount) return; - const result = await callApi('fetchMembers', chat.id, chat.accessHash!, 'recent', offset); + const result = await callApi('fetchMembers', { chat, offset }); if (!result) { return; } diff --git a/src/global/actions/api/payments.ts b/src/global/actions/api/payments.ts index ab7f37340..17279c0c4 100644 --- a/src/global/actions/api/payments.ts +++ b/src/global/actions/api/payments.ts @@ -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( global: T, inputInvoice: ApiInputInvoice, price: number, ...[tabId = getCurrentTabId()]: TabArgs diff --git a/src/global/actions/apiUpdaters/misc.ts b/src/global/actions/apiUpdaters/misc.ts index f163ae89d..59826af8a 100644 --- a/src/global/actions/apiUpdaters/misc.ts +++ b/src/global/actions/apiUpdaters/misc.ts @@ -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); } } diff --git a/src/global/actions/ui/stars.ts b/src/global/actions/ui/stars.ts index 0c992eece..335078b48 100644 --- a/src/global/actions/ui/stars.ts +++ b/src/global/actions/ui/stars.ts @@ -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'); diff --git a/src/global/helpers/chats.ts b/src/global/helpers/chats.ts index 40f26713e..4ee8820bc 100644 --- a/src/global/helpers/chats.ts +++ b/src/global/helpers/chats.ts @@ -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, - 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); } diff --git a/src/global/helpers/meta.ts b/src/global/helpers/meta.ts new file mode 100644 index 000000000..9360d1b6f --- /dev/null +++ b/src/global/helpers/meta.ts @@ -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( + name: ActionName, key: keyof TabState, +) { + // @ts-ignore + addActionHandler(name, (global, actions, payload): ActionReturnType => { + const { tabId = getCurrentTabId() } = payload || {}; + + return updateTabState(global, { + [key]: undefined, + }, tabId); + }); +} diff --git a/src/global/helpers/payments.ts b/src/global/helpers/payments.ts index 1fa31db6b..83292b82e 100644 --- a/src/global/helpers/payments.ts +++ b/src/global/helpers/payments.ts @@ -188,6 +188,19 @@ export function getRequestInputInvoice( }; } + 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; } diff --git a/src/global/helpers/peers.ts b/src/global/helpers/peers.ts index e1911a357..7fff07ddd 100644 --- a/src/global/helpers/peers.ts +++ b/src/global/helpers/peers.ts @@ -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') { diff --git a/src/global/helpers/users.ts b/src/global/helpers/users.ts index fadf7c72f..97523713d 100644 --- a/src/global/helpers/users.ts +++ b/src/global/helpers/users.ts @@ -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, - 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; } diff --git a/src/global/index.ts b/src/global/index.ts index 22b191797..ad760a2d7 100644 --- a/src/global/index.ts +++ b/src/global/index.ts @@ -14,13 +14,18 @@ type ProjectActionTypes = type ProjectActionNames = keyof ProjectActionTypes; type Helper = Exclude extends never ? {} : Exclude; + +export type TabStateActionNames = { + [ActionName in ProjectActionNames]: + 'tabId' extends keyof Helper ? 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 ? (( + [ActionName in ProjectActionNames]: ActionName extends TabStateActionNames ? (( payload: ProjectActionTypes[ActionName] & { tabId: number }, options?: ActionOptions, ) => void) : diff --git a/src/global/selectors/peers.ts b/src/global/selectors/peers.ts index 72fe631dd..4c5f6cd8a 100644 --- a/src/global/selectors/peers.ts +++ b/src/global/selectors/peers.ts @@ -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(global: T, peerId: string): ApiPeer | undefined { return selectUser(global, peerId) || selectChat(global, peerId); @@ -19,10 +20,11 @@ export function selectCanGift(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( diff --git a/src/global/types/actions.ts b/src/global/types/actions.ts index 780708b2b..d30a55461 100644 --- a/src/global/types/actions.ts +++ b/src/global/types/actions.ts @@ -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; diff --git a/src/global/types/tabState.ts b/src/global/types/tabState.ts index 6e18c6de7..9654888a9 100644 --- a/src/global/types/tabState.ts +++ b/src/global/types/tabState.ts @@ -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; }; diff --git a/src/hooks/usePeerSearch.ts b/src/hooks/usePeerSearch.ts new file mode 100644 index 000000000..a3f021b67 --- /dev/null +++ b/src/hooks/usePeerSearch.ts @@ -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; + defaultValue?: string[]; + debounceTimeout?: number; + isDisabled?: boolean; +}) { + const debouncedQuery = useDebouncedMemo(() => query, debounceTimeout, [query]); + const [currentResultsQuery, setCurrentResultsQuery] = useState(''); + 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, + }; +} diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 9ec85aa6e..f5aafd356 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -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 { 'GiftSend': { 'amount': V; }; + 'GiftUnique': { + 'title': V; + 'number': V; + }; 'GiftInfoPeerDescriptionFreeUpgradeOut': { 'peer': V; }; @@ -1718,6 +1725,25 @@ export interface LangPairWithVariables { '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; }; diff --git a/src/util/dates/dateFormat.ts b/src/util/dates/dateFormat.ts index eb954694a..2169c243a 100644 --- a/src/util/dates/dateFormat.ts +++ b/src/util/dates/dateFormat.ts @@ -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 }); } } diff --git a/src/util/hoc/freezeWhenClosed.ts b/src/util/hoc/freezeWhenClosed.ts index fa299036f..2a9186ae8 100644 --- a/src/util/hoc/freezeWhenClosed.ts +++ b/src/util/hoc/freezeWhenClosed.ts @@ -1,9 +1,17 @@ import { type FC, type Props, useRef } from '../../lib/teact/teact'; -export default function freezeWhenClosed(Component: T) { +type InjectProps = FC[0] & P>; + +type OwnProps = { + ignoreFreeze?: boolean; +}; + +export default function freezeWhenClosed(Component: T): InjectProps { function ComponentWrapper(props: Props) { const newProps = useRef(props); + if (props.ignoreFreeze) return Component(props); + if (props.isOpen) { newProps.current = props; } else {