Various improvements

This commit is contained in:
Ilya Laktyushin 2025-03-14 16:19:07 +04:00
parent fed338985d
commit 2effcb42b7
12 changed files with 310 additions and 130 deletions

View File

@ -14032,3 +14032,5 @@ Sorry for the inconvenience.";
"Privacy.Gifts.PremiumToast.Text" = "Subscribe to **Telegram Premium** to use this setting.";
"Privacy.Gifts.PremiumToast.Action" = "Open";
"Gift.Send.ErrorDisallowed" = "**%@** doesn't accept this kind of gifts.";

View File

@ -1059,7 +1059,7 @@ public protocol SharedAccountContext: AnyObject {
func makePremiumLimitController(context: AccountContext, subject: PremiumLimitSubject, count: Int32, forceDark: Bool, cancel: @escaping () -> Void, action: @escaping () -> Bool) -> ViewController
func makeStarsGiftController(context: AccountContext, birthdays: [EnginePeer.Id: TelegramBirthday]?, completion: @escaping (([EnginePeer.Id]) -> Void)) -> ViewController
func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (([EnginePeer.Id]) -> Void)?) -> ViewController
func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (([EnginePeer.Id]) -> Signal<Never, TransferStarGiftError>)?) -> ViewController
func makeGiftOptionsController(context: AccountContext, peerId: EnginePeer.Id, premiumOptions: [CachedPremiumGiftOption], hasBirthday: Bool, completion: (() -> Void)?) -> ViewController
func makePremiumPrivacyControllerController(context: AccountContext, subject: PremiumPrivacySubject, peerId: EnginePeer.Id) -> ViewController
func makePremiumBoostLevelsController(context: AccountContext, peerId: EnginePeer.Id, subject: BoostSubject, boostStatus: ChannelBoostStatus, myBoostStatus: MyBoostStatus, forceDark: Bool, openStats: (() -> Void)?) -> ViewController

View File

@ -177,6 +177,7 @@ public enum BotPaymentFormRequestError {
case generic
case alreadyActive
case noPaymentNeeded
case disallowedStarGift
}
extension BotPaymentInvoice {
@ -473,6 +474,8 @@ func _internal_fetchBotPaymentForm(accountPeerId: PeerId, postbox: Postbox, netw
|> `catch` { error -> Signal<Api.payments.PaymentForm, BotPaymentFormRequestError> in
if error.errorDescription == "NO_PAYMENT_NEEDED" {
return .fail(.noPaymentNeeded)
} else if error.errorDescription == "USER_DISALLOWED_STARGIFTS" {
return .fail(.disallowedStarGift)
}
return .fail(.generic)
}
@ -635,6 +638,7 @@ public enum SendBotPaymentFormError {
case paymentFailed
case alreadyPaid
case starGiftOutOfStock
case disallowedStarGift
}
public enum SendBotPaymentResult {

View File

@ -766,7 +766,7 @@ func _internal_updateStarGiftsPinnedToTop(account: Account, peerId: EnginePeer.I
public enum TransferStarGiftError {
case generic
case disallowed
case disallowedStarGift
}
func _internal_transferStarGift(account: Account, prepaid: Bool, reference: StarGiftReference, peerId: EnginePeer.Id) -> Signal<Never, TransferStarGiftError> {
@ -783,7 +783,10 @@ func _internal_transferStarGift(account: Account, prepaid: Bool, reference: Star
}
if prepaid {
return account.network.request(Api.functions.payments.transferStarGift(stargift: starGift, toId: inputPeer))
|> mapError { _ -> TransferStarGiftError in
|> mapError { error -> TransferStarGiftError in
if error.errorDescription == "USER_DISALLOWED_STARGIFTS" {
return .disallowedStarGift
}
return .generic
}
|> mapToSignal { updates -> Signal<Void, TransferStarGiftError> in
@ -798,6 +801,8 @@ func _internal_transferStarGift(account: Account, prepaid: Bool, reference: Star
|> `catch` { error -> Signal<BotPaymentForm?, TransferStarGiftError> in
if case .noPaymentNeeded = error {
return .single(nil)
} else if case .disallowedStarGift = error {
return .fail(.disallowedStarGift)
}
return .fail(.generic)
}
@ -1335,16 +1340,15 @@ private final class ProfileGiftsContextImpl {
self.pushState()
}
func transferStarGift(prepaid: Bool, reference: StarGiftReference, peerId: EnginePeer.Id) {
self.actionDisposable.set(
_internal_transferStarGift(account: self.account, prepaid: prepaid, reference: reference, peerId: peerId).startStrict()
)
func transferStarGift(prepaid: Bool, reference: StarGiftReference, peerId: EnginePeer.Id) -> Signal<Never, TransferStarGiftError> {
if let count = self.count {
self.count = max(0, count - 1)
}
self.gifts.removeAll(where: { $0.reference == reference })
self.filteredGifts.removeAll(where: { $0.reference == reference })
self.pushState()
return _internal_transferStarGift(account: self.account, prepaid: prepaid, reference: reference, peerId: peerId)
}
func upgradeStarGift(formId: Int64?, reference: StarGiftReference, keepOriginalInfo: Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError> {
@ -1703,9 +1707,17 @@ public final class ProfileGiftsContext {
}
}
public func transferStarGift(prepaid: Bool, reference: StarGiftReference, peerId: EnginePeer.Id) {
self.impl.with { impl in
impl.transferStarGift(prepaid: prepaid, reference: reference, peerId: peerId)
public func transferStarGift(prepaid: Bool, reference: StarGiftReference, peerId: EnginePeer.Id) -> Signal<Never, TransferStarGiftError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.transferStarGift(prepaid: prepaid, reference: reference, peerId: peerId).start(error: { error in
subscriber.putError(error)
}, completed: {
subscriber.putCompletion()
}))
}
return disposable
}
}

View File

@ -513,6 +513,9 @@ final class GiftOptionsScreenComponent: Component {
}
let context = component.context
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var dismissAlertImpl: (() -> Void)?
let alertController = giftTransferAlertController(
context: context,
gift: transferGift,
@ -521,65 +524,102 @@ final class GiftOptionsScreenComponent: Component {
navigationController: mainController.navigationController as? NavigationController,
commit: { [weak self, weak mainController] in
let proceed: (Bool) -> Void = { waitForTopUp in
var errorImpl: ((TransferStarGiftError) -> Void)?
var completedImpl: (() -> Void)?
if waitForTopUp, let starsContext = context.starsContext {
let _ = (starsContext.onUpdate
|> deliverOnMainQueue).start(next: {
let _ = (context.engine.payments.transferStarGift(prepaid: gift.transferStars == 0, reference: reference, peerId: peer.id)
|> deliverOnMainQueue).start()
|> deliverOnMainQueue).start(error: { error in
errorImpl?(error)
}, completed: {
completedImpl?()
})
})
} else {
let _ = (context.engine.payments.transferStarGift(prepaid: gift.transferStars == 0, reference: reference, peerId: peer.id)
|> deliverOnMainQueue).start()
|> deliverOnMainQueue).start(error: { error in
errorImpl?(error)
}, completed: {
completedImpl?()
})
}
guard let controller = mainController, let navigationController = controller.navigationController as? NavigationController else {
return
}
if peer.id.namespace == Namespaces.Peer.CloudChannel {
var controllers = navigationController.viewControllers
controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) }
var foundController = false
for controller in controllers.reversed() {
if let controller = controller as? PeerInfoScreen, controller.peerId == component.peerId {
foundController = true
break
errorImpl = { [weak navigationController] error in
guard let navigationController else {
return
}
dismissAlertImpl?()
var errorText: String?
switch error {
case .disallowedStarGift:
errorText = presentationData.strings.Gift_Send_ErrorDisallowed(peer.compactDisplayTitle).string
default:
errorText = presentationData.strings.Gift_Send_ErrorUnknown
}
if let errorText = errorText {
let alertController = textAlertController(context: context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})], parseMarkdown: true)
if let lastController = navigationController.viewControllers.last as? ViewController {
lastController.present(alertController, in: .window(.root))
}
}
if !foundController {
if let controller = context.sharedContext.makePeerInfoController(
context: context,
updatedPresentationData: nil,
peer: peer._asPeer(),
mode: .gifts,
avatarInitiallyExpanded: false,
fromChat: false,
requestsContext: nil
) {
controllers.append(controller)
}
}
navigationController.setViewControllers(controllers, animated: true)
} else {
var controllers = navigationController.viewControllers
controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) && !($0 is PeerInfoScreen) && !($0 is ContactSelectionController) }
var foundController = false
for controller in controllers.reversed() {
if let chatController = controller as? ChatController, case .peer(id: component.peerId) = chatController.chatLocation {
chatController.hintPlayNextOutgoingGift()
foundController = true
break
}
}
if !foundController {
let chatController = component.context.sharedContext.makeChatController(context: component.context, chatLocation: .peer(id: component.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil)
chatController.hintPlayNextOutgoingGift()
controllers.append(chatController)
}
navigationController.setViewControllers(controllers, animated: true)
}
if let completion = component.completion {
completion()
completedImpl = {
dismissAlertImpl?()
if peer.id.namespace == Namespaces.Peer.CloudChannel {
var controllers = navigationController.viewControllers
controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) }
var foundController = false
for controller in controllers.reversed() {
if let controller = controller as? PeerInfoScreen, controller.peerId == component.peerId {
foundController = true
break
}
}
if !foundController {
if let controller = context.sharedContext.makePeerInfoController(
context: context,
updatedPresentationData: nil,
peer: peer._asPeer(),
mode: .gifts,
avatarInitiallyExpanded: false,
fromChat: false,
requestsContext: nil
) {
controllers.append(controller)
}
}
navigationController.setViewControllers(controllers, animated: true)
} else {
var controllers = navigationController.viewControllers
controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) && !($0 is PeerInfoScreen) && !($0 is ContactSelectionController) }
var foundController = false
for controller in controllers.reversed() {
if let chatController = controller as? ChatController, case .peer(id: component.peerId) = chatController.chatLocation {
chatController.hintPlayNextOutgoingGift()
foundController = true
break
}
}
if !foundController {
let chatController = component.context.sharedContext.makeChatController(context: component.context, chatLocation: .peer(id: component.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil)
chatController.hintPlayNextOutgoingGift()
controllers.append(chatController)
}
navigationController.setViewControllers(controllers, animated: true)
}
if let completion = component.completion {
completion()
}
}
}
@ -610,6 +650,10 @@ final class GiftOptionsScreenComponent: Component {
}
)
controller.present(alertController, in: .window(.root))
dismissAlertImpl = { [weak alertController] in
alertController?.dismissAnimated()
}
}
func update(component: GiftOptionsScreenComponent, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {

View File

@ -465,12 +465,14 @@ final class GiftSetupScreenComponent: Component {
switch error {
case .starGiftOutOfStock:
errorText = presentationData.strings.Gift_Send_ErrorOutOfStock
case .disallowedStarGift:
errorText = presentationData.strings.Gift_Send_ErrorDisallowed(self.peerMap[peerId]?.compactDisplayTitle ?? "").string
default:
errorText = presentationData.strings.Gift_Send_ErrorUnknown
}
if let errorText = errorText {
let alertController = textAlertController(context: component.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
let alertController = textAlertController(context: component.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})], parseMarkdown: true)
controller.present(alertController, in: .window(.root))
}
})

View File

@ -50,6 +50,7 @@ swift_library(
"//submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent",
"//submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent",
"//submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController",
"//submodules/ActivityIndicator",
],
visibility = [
"//visibility:public",

View File

@ -13,6 +13,7 @@ import AvatarNode
import Markdown
import GiftItemComponent
import ChatMessagePaymentAlertController
import ActivityIndicator
private final class GiftTransferAlertContentNode: AlertContentNode {
private let context: AccountContext
@ -31,9 +32,19 @@ private final class GiftTransferAlertContentNode: AlertContentNode {
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private var activityIndicator: ActivityIndicator?
private var validLayout: CGSize?
var inProgress = false {
didSet {
if let size = self.validLayout {
let _ = self.updateLayout(size: size, transition: .immediate)
}
}
}
override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
@ -248,6 +259,23 @@ private final class GiftTransferAlertContentNode: AlertContentNode {
nodeIndex += 1
}
if self.inProgress {
let activityIndicator: ActivityIndicator
if let current = self.activityIndicator {
activityIndicator = current
} else {
activityIndicator = ActivityIndicator(type: .custom(self.presentationTheme.list.freeInputField.controlColor, 18.0, 1.5, false))
self.addSubnode(activityIndicator)
}
if let actionNode = self.actionNodes.first {
actionNode.isHidden = true
let indicatorSize = CGSize(width: 22.0, height: 22.0)
transition.updateFrame(node: activityIndicator, frame: CGRect(origin: CGPoint(x: actionNode.frame.minX + floor((actionNode.frame.width - indicatorSize.width) / 2.0), y: actionNode.frame.minY + floor((actionNode.frame.height - indicatorSize.height) / 2.0)), size: indicatorSize))
}
}
return resultSize
}
}
@ -274,17 +302,18 @@ public func giftTransferAlertController(
buttonText = strings.Gift_Transfer_Confirmation_TransferFree
}
var contentNode: GiftTransferAlertContentNode?
var dismissImpl: ((Bool) -> Void)?
let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: buttonText, action: {
dismissImpl?(true)
let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: buttonText, action: { [weak contentNode] in
contentNode?.inProgress = true
commit()
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?(true)
})]
let contentNode = GiftTransferAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, gift: gift, peer: peer, title: title, text: text, actions: actions)
contentNode = GiftTransferAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, gift: gift, peer: peer, title: title, text: text, actions: actions)
let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode, navigationController: navigationController, showBalance: transferStars > 0)
let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode!, navigationController: navigationController, showBalance: transferStars > 0)
dismissImpl = { [weak controller] animated in
if animated {
controller?.dismissAnimated()

View File

@ -2415,7 +2415,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
case upgradePreview([StarGift.UniqueGift.Attribute], String)
case wearPreview(StarGift.UniqueGift)
var arguments: (peerId: EnginePeer.Id?, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, reference: StarGiftReference?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, transferStars: Int64?, canExportDate: Int32?, upgradeMessageId: Int32?)? {
var arguments: (peerId: EnginePeer.Id?, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, reference: StarGiftReference?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, pinnedToTop: Bool?, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, transferStars: Int64?, canExportDate: Int32?, upgradeMessageId: Int32?)? {
switch self {
case let .message(message):
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction {
@ -2427,7 +2427,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
} else {
reference = .message(messageId: message.id)
}
return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, nil, nil, upgradeMessageId)
return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, canUpgrade, upgradeStars, nil, nil, upgradeMessageId)
case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, peerId, senderId, savedId):
var reference: StarGiftReference
if let peerId, let savedId {
@ -2447,19 +2447,19 @@ public class GiftViewScreen: ViewControllerComponentContainer {
} else {
incoming = message.flags.contains(.Incoming)
}
return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, false, false, false, nil, transferStars, canExportDate, nil)
return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, nil, false, false, false, nil, transferStars, canExportDate, nil)
default:
return nil
}
}
case let .uniqueGift(gift), let .wearPreview(gift):
return (nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, false, false, false, nil, nil, nil, nil)
return (nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, nil, nil, nil, nil)
case let .profileGift(peerId, gift):
var messageId: EngineMessage.Id?
if case let .message(messageIdValue) = gift.reference {
messageId = messageIdValue
}
return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, messageId, gift.reference, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, gift.canExportDate, nil)
return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, messageId, gift.reference, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, gift.pinnedToTop, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, gift.canExportDate, nil)
case .soldOutGift:
return nil
case .upgradePreview:
@ -2488,8 +2488,9 @@ public class GiftViewScreen: ViewControllerComponentContainer {
forceDark: Bool = false,
updateSavedToProfile: ((StarGiftReference, Bool) -> Void)? = nil,
convertToStars: (() -> Void)? = nil,
transferGift: ((Bool, EnginePeer.Id) -> Void)? = nil,
transferGift: ((Bool, EnginePeer.Id) -> Signal<Never, TransferStarGiftError>)? = nil,
upgradeGift: ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)? = nil,
togglePinnedToTop: ((Bool) -> Bool)? = nil,
shareStory: ((StarGift.UniqueGift) -> Void)? = nil
) {
self.context = context
@ -2822,19 +2823,19 @@ public class GiftViewScreen: ViewControllerComponentContainer {
}
let controller = context.sharedContext.makePremiumGiftController(context: context, source: .starGiftTransfer(birthdays, reference, gift, transferStars, arguments.canExportDate, showSelf), completion: { peerIds in
guard let peerId = peerIds.first else {
return
return .complete()
}
if let transferGift {
transferGift(transferStars == 0, peerId)
} else {
let _ = (context.engine.payments.transferStarGift(prepaid: transferStars == 0, reference: reference, peerId: peerId)
|> deliverOnMainQueue).start()
}
Queue.mainQueue().after(1.0, {
Queue.mainQueue().after(1.5, {
if transferStars > 0 {
context.starsContext?.load(force: true)
}
})
if let transferGift {
return transferGift(transferStars == 0, peerId)
} else {
return (context.engine.payments.transferStarGift(prepaid: transferStars == 0, reference: reference, peerId: peerId)
|> deliverOnMainQueue)
}
})
navigationController.pushViewController(controller)
})
@ -2979,6 +2980,37 @@ public class GiftViewScreen: ViewControllerComponentContainer {
return
}
var items: [ContextMenuItem] = []
let strings = presentationData.strings
if let _ = arguments.reference, case .unique = arguments.gift, let togglePinnedToTop, let pinnedToTop = arguments.pinnedToTop {
items.append(.action(ContextMenuActionItem(text: pinnedToTop ? strings.PeerInfo_Gifts_Context_Unpin : strings.PeerInfo_Gifts_Context_Pin , icon: { theme in generateTintedImage(image: UIImage(bundleImageName: pinnedToTop ? "Chat/Context Menu/Unpin" : "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in
c?.dismiss(completion: { [weak self] in
guard let self else {
return
}
let pinnedToTop = !pinnedToTop
if togglePinnedToTop(pinnedToTop) {
if pinnedToTop {
self.dismissAnimated()
} else {
let toastText = strings.PeerInfo_Gifts_ToastUnpinned_Text
self.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_toastunpin", scale: 0.06, colors: [:], title: nil, text: toastText, customUndoText: nil, timeout: 5), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
if case let .profileGift(peerId, gift) = self.subject {
self.subject = .profileGift(peerId, gift.withPinnedToTop(false))
}
}
} else {
var maxPinnedCount: Int = 6
if let value = context.currentAppConfiguration.with({ $0 }).data?["stargifts_pinned_to_top_limit"] as? Double {
maxPinnedCount = Int(value)
}
self.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: strings.PeerInfo_Gifts_ToastPinLimit_Text(Int32(maxPinnedCount)), timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
}
})
})))
}
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_CopyLink, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, _ in

View File

@ -4855,9 +4855,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
},
transferGift: { [weak profileGifts] prepaid, peerId in
guard let profileGifts, let reference = gift.reference else {
return
return .complete()
}
profileGifts.transferStarGift(prepaid: prepaid, reference: reference, peerId: peerId)
return profileGifts.transferStarGift(prepaid: prepaid, reference: reference, peerId: peerId)
},
upgradeGift: { [weak profileGifts] formId, keepOriginalInfo in
guard let profileGifts, let reference = gift.reference else {

View File

@ -518,9 +518,9 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
},
transferGift: { [weak self] prepaid, peerId in
guard let self, let reference = product.reference else {
return
return .complete()
}
self.profileGifts.transferStarGift(prepaid: prepaid, reference: reference, peerId: peerId)
return self.profileGifts.transferStarGift(prepaid: prepaid, reference: reference, peerId: peerId)
},
upgradeGift: { [weak self] formId, keepOriginalInfo in
guard let self, let reference = product.reference else {
@ -528,6 +528,27 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
}
return self.profileGifts.upgradeStarGift(formId: formId, reference: reference, keepOriginalInfo: keepOriginalInfo)
},
togglePinnedToTop: { [weak self] pinnedToTop in
guard let self else {
return false
}
if pinnedToTop && self.pinnedReferences.count >= self.maxPinnedCount {
return false
}
if let reference = product.reference {
self.profileGifts.updateStarGiftPinnedToTop(reference: reference, pinnedToTop: pinnedToTop)
if pinnedToTop {
let _ = self.scrollToTop()
Queue.mainQueue().after(0.35) {
let toastTitle = params.presentationData.strings.PeerInfo_Gifts_ToastPinned_Title
let toastText = params.presentationData.strings.PeerInfo_Gifts_ToastPinned_Text
self.parentController?.present(UndoOverlayController(presentationData: params.presentationData, content: .universal(animation: "anim_toastpin", scale: 0.06, colors: [:], title: toastTitle, text: toastText, customUndoText: nil, timeout: 5), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
}
}
}
return true
},
shareStory: { [weak self] uniqueGift in
guard let self, let parentController = self.parentController else {
return
@ -977,7 +998,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
let pinnedToTop = !gift.pinnedToTop
if pinnedToTop && self.pinnedReferences.count >= self.maxPinnedCount {
self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.PeerInfo_Gifts_ToastPinLimit_Text(Int32(self.maxPinnedCount)), timeout: nil, customUndoText: nil), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: strings.PeerInfo_Gifts_ToastPinLimit_Text(Int32(self.maxPinnedCount)), timeout: nil, customUndoText: nil), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
return
}
@ -989,10 +1010,10 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
let toastText: String
if !pinnedToTop {
toastTitle = nil
toastText = presentationData.strings.PeerInfo_Gifts_ToastUnpinned_Text
toastText = strings.PeerInfo_Gifts_ToastUnpinned_Text
} else {
toastTitle = presentationData.strings.PeerInfo_Gifts_ToastPinned_Title
toastText = presentationData.strings.PeerInfo_Gifts_ToastPinned_Text
toastTitle = strings.PeerInfo_Gifts_ToastPinned_Title
toastText = strings.PeerInfo_Gifts_ToastPinned_Text
}
self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: !pinnedToTop ? "anim_toastunpin" : "anim_toastpin", scale: 0.06, colors: [:], title: toastTitle, text: toastText, customUndoText: nil, timeout: 5), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
})
@ -1200,14 +1221,14 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
let transferStars = gift.transferStars ?? 0
let controller = context.sharedContext.makePremiumGiftController(context: context, source: .starGiftTransfer(birthdays, reference, uniqueGift, transferStars, gift.canExportDate, showSelf), completion: { [weak self] peerIds in
guard let self, let peerId = peerIds.first else {
return
return .complete()
}
self.profileGifts.transferStarGift(prepaid: transferStars == 0, reference: reference, peerId: peerId)
Queue.mainQueue().after(1.0, {
Queue.mainQueue().after(1.5, {
if transferStars > 0 {
context.starsContext?.load(force: true)
}
})
return self.profileGifts.transferStarGift(prepaid: transferStars == 0, reference: reference, peerId: peerId)
})
self.parentController?.push(controller)
})

View File

@ -2674,7 +2674,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return controller
}
public func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (([EnginePeer.Id]) -> Void)?) -> ViewController {
public func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (([EnginePeer.Id]) -> Signal<Never, TransferStarGiftError>)?) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var presentExportAlertImpl: (() -> Void)?
@ -2912,6 +2912,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
guard let controller, case let .starGiftTransfer(_, _, gift, transferStars, _, _) = source else {
return
}
var dismissAlertImpl: (() -> Void)?
let alertController = giftTransferAlertController(
context: context,
gift: gift,
@ -2920,61 +2921,89 @@ public final class SharedAccountContextImpl: SharedAccountContext {
navigationController: controller.navigationController as? NavigationController,
commit: { [weak controller] in
let proceed: (Bool) -> Void = { waitForTopUp in
completion?([peer.id])
guard let controller, let navigationController = controller.navigationController as? NavigationController else {
return
}
var controllers = navigationController.viewControllers
controllers = controllers.filter { !($0 is ContactSelectionController) }
if !isChannelGift {
if peer.id.namespace == Namespaces.Peer.CloudChannel {
if let controller = context.sharedContext.makePeerInfoController(
context: context,
updatedPresentationData: nil,
peer: peer._asPeer(),
mode: .gifts,
avatarInitiallyExpanded: false,
fromChat: false,
requestsContext: nil
) {
controllers.append(controller)
if let completion {
let _ = (completion([peer.id])
|> deliverOnMainQueue).startStandalone(error: { [weak navigationController] error in
guard let navigationController else {
return
}
} else {
var foundController = false
for controller in controllers.reversed() {
if let chatController = controller as? ChatController, case .peer(id: peer.id) = chatController.chatLocation {
chatController.hintPlayNextOutgoingGift()
foundController = true
break
dismissAlertImpl?()
var errorText: String?
switch error {
case .disallowedStarGift:
errorText = presentationData.strings.Gift_Send_ErrorDisallowed(peer.compactDisplayTitle).string
default:
errorText = presentationData.strings.Gift_Send_ErrorUnknown
}
if let errorText = errorText {
let alertController = textAlertController(context: context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})], parseMarkdown: true)
if let lastController = navigationController.viewControllers.last as? ViewController {
lastController.present(alertController, in: .window(.root))
}
}
if !foundController {
let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.default), params: nil)
chatController.hintPlayNextOutgoingGift()
controllers.append(chatController)
}, completed: { [weak navigationController] in
guard let navigationController else {
return
}
}
}
navigationController.setViewControllers(controllers, animated: true)
Queue.mainQueue().after(0.3) {
let tooltipController = UndoOverlayController(
presentationData: presentationData,
content: .forward(savedMessages: false, text: presentationData.strings.Gift_Transfer_Success("\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))", peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string),
elevatedLayout: false,
action: { _ in return true }
)
if let lastController = controllers.last as? ViewController {
lastController.present(tooltipController, in: .window(.root))
}
Queue.mainQueue().after(0.5) {
dismissAlertImpl?()
var controllers = navigationController.viewControllers
controllers = controllers.filter { !($0 is GiftViewScreen) }
navigationController.setViewControllers(controllers, animated: false)
}
controllers = controllers.filter { !($0 is ContactSelectionController) }
if !isChannelGift {
if peer.id.namespace == Namespaces.Peer.CloudChannel {
if let controller = context.sharedContext.makePeerInfoController(
context: context,
updatedPresentationData: nil,
peer: peer._asPeer(),
mode: .gifts,
avatarInitiallyExpanded: false,
fromChat: false,
requestsContext: nil
) {
controllers.append(controller)
}
} else {
var foundController = false
for controller in controllers.reversed() {
if let chatController = controller as? ChatController, case .peer(id: peer.id) = chatController.chatLocation {
chatController.hintPlayNextOutgoingGift()
foundController = true
break
}
}
if !foundController {
let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.default), params: nil)
chatController.hintPlayNextOutgoingGift()
controllers.append(chatController)
}
}
}
navigationController.setViewControllers(controllers, animated: true)
Queue.mainQueue().after(0.3) {
let tooltipController = UndoOverlayController(
presentationData: presentationData,
content: .forward(savedMessages: false, text: presentationData.strings.Gift_Transfer_Success("\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))", peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string),
elevatedLayout: false,
action: { _ in return true }
)
if let lastController = navigationController.viewControllers.last as? ViewController {
lastController.present(tooltipController, in: .window(.root))
}
Queue.mainQueue().after(0.5) {
var controllers = navigationController.viewControllers
controllers = controllers.filter { !($0 is GiftViewScreen) }
navigationController.setViewControllers(controllers, animated: false)
}
}
})
}
}
@ -3005,6 +3034,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}
)
controller.present(alertController, in: .window(.root))
dismissAlertImpl = { [weak alertController] in
alertController?.dismissAnimated()
}
}
return controller