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.Text" = "Subscribe to **Telegram Premium** to use this setting.";
"Privacy.Gifts.PremiumToast.Action" = "Open"; "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 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 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 makeGiftOptionsController(context: AccountContext, peerId: EnginePeer.Id, premiumOptions: [CachedPremiumGiftOption], hasBirthday: Bool, completion: (() -> Void)?) -> ViewController
func makePremiumPrivacyControllerController(context: AccountContext, subject: PremiumPrivacySubject, peerId: EnginePeer.Id) -> 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 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 generic
case alreadyActive case alreadyActive
case noPaymentNeeded case noPaymentNeeded
case disallowedStarGift
} }
extension BotPaymentInvoice { extension BotPaymentInvoice {
@ -473,6 +474,8 @@ func _internal_fetchBotPaymentForm(accountPeerId: PeerId, postbox: Postbox, netw
|> `catch` { error -> Signal<Api.payments.PaymentForm, BotPaymentFormRequestError> in |> `catch` { error -> Signal<Api.payments.PaymentForm, BotPaymentFormRequestError> in
if error.errorDescription == "NO_PAYMENT_NEEDED" { if error.errorDescription == "NO_PAYMENT_NEEDED" {
return .fail(.noPaymentNeeded) return .fail(.noPaymentNeeded)
} else if error.errorDescription == "USER_DISALLOWED_STARGIFTS" {
return .fail(.disallowedStarGift)
} }
return .fail(.generic) return .fail(.generic)
} }
@ -635,6 +638,7 @@ public enum SendBotPaymentFormError {
case paymentFailed case paymentFailed
case alreadyPaid case alreadyPaid
case starGiftOutOfStock case starGiftOutOfStock
case disallowedStarGift
} }
public enum SendBotPaymentResult { public enum SendBotPaymentResult {

View File

@ -766,7 +766,7 @@ func _internal_updateStarGiftsPinnedToTop(account: Account, peerId: EnginePeer.I
public enum TransferStarGiftError { public enum TransferStarGiftError {
case generic case generic
case disallowed case disallowedStarGift
} }
func _internal_transferStarGift(account: Account, prepaid: Bool, reference: StarGiftReference, peerId: EnginePeer.Id) -> Signal<Never, TransferStarGiftError> { 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 { if prepaid {
return account.network.request(Api.functions.payments.transferStarGift(stargift: starGift, toId: inputPeer)) 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 return .generic
} }
|> mapToSignal { updates -> Signal<Void, TransferStarGiftError> in |> 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 |> `catch` { error -> Signal<BotPaymentForm?, TransferStarGiftError> in
if case .noPaymentNeeded = error { if case .noPaymentNeeded = error {
return .single(nil) return .single(nil)
} else if case .disallowedStarGift = error {
return .fail(.disallowedStarGift)
} }
return .fail(.generic) return .fail(.generic)
} }
@ -1335,16 +1340,15 @@ private final class ProfileGiftsContextImpl {
self.pushState() self.pushState()
} }
func transferStarGift(prepaid: Bool, reference: StarGiftReference, peerId: EnginePeer.Id) { func transferStarGift(prepaid: Bool, reference: StarGiftReference, peerId: EnginePeer.Id) -> Signal<Never, TransferStarGiftError> {
self.actionDisposable.set(
_internal_transferStarGift(account: self.account, prepaid: prepaid, reference: reference, peerId: peerId).startStrict()
)
if let count = self.count { if let count = self.count {
self.count = max(0, count - 1) self.count = max(0, count - 1)
} }
self.gifts.removeAll(where: { $0.reference == reference }) self.gifts.removeAll(where: { $0.reference == reference })
self.filteredGifts.removeAll(where: { $0.reference == reference }) self.filteredGifts.removeAll(where: { $0.reference == reference })
self.pushState() 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> { 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) { public func transferStarGift(prepaid: Bool, reference: StarGiftReference, peerId: EnginePeer.Id) -> Signal<Never, TransferStarGiftError> {
self.impl.with { impl in return Signal { subscriber in
impl.transferStarGift(prepaid: prepaid, reference: reference, peerId: peerId) 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 context = component.context
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var dismissAlertImpl: (() -> Void)?
let alertController = giftTransferAlertController( let alertController = giftTransferAlertController(
context: context, context: context,
gift: transferGift, gift: transferGift,
@ -521,65 +524,102 @@ final class GiftOptionsScreenComponent: Component {
navigationController: mainController.navigationController as? NavigationController, navigationController: mainController.navigationController as? NavigationController,
commit: { [weak self, weak mainController] in commit: { [weak self, weak mainController] in
let proceed: (Bool) -> Void = { waitForTopUp in let proceed: (Bool) -> Void = { waitForTopUp in
var errorImpl: ((TransferStarGiftError) -> Void)?
var completedImpl: (() -> Void)?
if waitForTopUp, let starsContext = context.starsContext { if waitForTopUp, let starsContext = context.starsContext {
let _ = (starsContext.onUpdate let _ = (starsContext.onUpdate
|> deliverOnMainQueue).start(next: { |> deliverOnMainQueue).start(next: {
let _ = (context.engine.payments.transferStarGift(prepaid: gift.transferStars == 0, reference: reference, peerId: peer.id) 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 { } else {
let _ = (context.engine.payments.transferStarGift(prepaid: gift.transferStars == 0, reference: reference, peerId: peer.id) 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 { guard let controller = mainController, let navigationController = controller.navigationController as? NavigationController else {
return return
} }
if peer.id.namespace == Namespaces.Peer.CloudChannel { errorImpl = { [weak navigationController] error in
var controllers = navigationController.viewControllers guard let navigationController else {
controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) } return
var foundController = false }
for controller in controllers.reversed() { dismissAlertImpl?()
if let controller = controller as? PeerInfoScreen, controller.peerId == component.peerId {
foundController = true var errorText: String?
break 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)) 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 { 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 { switch error {
case .starGiftOutOfStock: case .starGiftOutOfStock:
errorText = presentationData.strings.Gift_Send_ErrorOutOfStock errorText = presentationData.strings.Gift_Send_ErrorOutOfStock
case .disallowedStarGift:
errorText = presentationData.strings.Gift_Send_ErrorDisallowed(self.peerMap[peerId]?.compactDisplayTitle ?? "").string
default: default:
errorText = presentationData.strings.Gift_Send_ErrorUnknown errorText = presentationData.strings.Gift_Send_ErrorUnknown
} }
if let errorText = errorText { 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)) controller.present(alertController, in: .window(.root))
} }
}) })

View File

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

View File

@ -13,6 +13,7 @@ import AvatarNode
import Markdown import Markdown
import GiftItemComponent import GiftItemComponent
import ChatMessagePaymentAlertController import ChatMessagePaymentAlertController
import ActivityIndicator
private final class GiftTransferAlertContentNode: AlertContentNode { private final class GiftTransferAlertContentNode: AlertContentNode {
private let context: AccountContext private let context: AccountContext
@ -31,9 +32,19 @@ private final class GiftTransferAlertContentNode: AlertContentNode {
private let actionNodesSeparator: ASDisplayNode private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode] private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode] private let actionVerticalSeparators: [ASDisplayNode]
private var activityIndicator: ActivityIndicator?
private var validLayout: CGSize? private var validLayout: CGSize?
var inProgress = false {
didSet {
if let size = self.validLayout {
let _ = self.updateLayout(size: size, transition: .immediate)
}
}
}
override var dismissOnOutsideTap: Bool { override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled return self.isUserInteractionEnabled
} }
@ -248,6 +259,23 @@ private final class GiftTransferAlertContentNode: AlertContentNode {
nodeIndex += 1 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 return resultSize
} }
} }
@ -274,17 +302,18 @@ public func giftTransferAlertController(
buttonText = strings.Gift_Transfer_Confirmation_TransferFree buttonText = strings.Gift_Transfer_Confirmation_TransferFree
} }
var contentNode: GiftTransferAlertContentNode?
var dismissImpl: ((Bool) -> Void)? var dismissImpl: ((Bool) -> Void)?
let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: buttonText, action: { let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: buttonText, action: { [weak contentNode] in
dismissImpl?(true) contentNode?.inProgress = true
commit() commit()
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?(true) 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 dismissImpl = { [weak controller] animated in
if animated { if animated {
controller?.dismissAnimated() controller?.dismissAnimated()

View File

@ -2415,7 +2415,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
case upgradePreview([StarGift.UniqueGift.Attribute], String) case upgradePreview([StarGift.UniqueGift.Attribute], String)
case wearPreview(StarGift.UniqueGift) 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 { switch self {
case let .message(message): case let .message(message):
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction { if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction {
@ -2427,7 +2427,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
} else { } else {
reference = .message(messageId: message.id) 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): case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, peerId, senderId, savedId):
var reference: StarGiftReference var reference: StarGiftReference
if let peerId, let savedId { if let peerId, let savedId {
@ -2447,19 +2447,19 @@ public class GiftViewScreen: ViewControllerComponentContainer {
} else { } else {
incoming = message.flags.contains(.Incoming) 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: default:
return nil return nil
} }
} }
case let .uniqueGift(gift), let .wearPreview(gift): 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): case let .profileGift(peerId, gift):
var messageId: EngineMessage.Id? var messageId: EngineMessage.Id?
if case let .message(messageIdValue) = gift.reference { if case let .message(messageIdValue) = gift.reference {
messageId = messageIdValue 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: case .soldOutGift:
return nil return nil
case .upgradePreview: case .upgradePreview:
@ -2488,8 +2488,9 @@ public class GiftViewScreen: ViewControllerComponentContainer {
forceDark: Bool = false, forceDark: Bool = false,
updateSavedToProfile: ((StarGiftReference, Bool) -> Void)? = nil, updateSavedToProfile: ((StarGiftReference, Bool) -> Void)? = nil,
convertToStars: (() -> 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, upgradeGift: ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)? = nil,
togglePinnedToTop: ((Bool) -> Bool)? = nil,
shareStory: ((StarGift.UniqueGift) -> Void)? = nil shareStory: ((StarGift.UniqueGift) -> Void)? = nil
) { ) {
self.context = context 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 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 { guard let peerId = peerIds.first else {
return return .complete()
} }
if let transferGift { Queue.mainQueue().after(1.5, {
transferGift(transferStars == 0, peerId)
} else {
let _ = (context.engine.payments.transferStarGift(prepaid: transferStars == 0, reference: reference, peerId: peerId)
|> deliverOnMainQueue).start()
}
Queue.mainQueue().after(1.0, {
if transferStars > 0 { if transferStars > 0 {
context.starsContext?.load(force: true) 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) navigationController.pushViewController(controller)
}) })
@ -2979,6 +2980,37 @@ public class GiftViewScreen: ViewControllerComponentContainer {
return return
} }
var items: [ContextMenuItem] = [] 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 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) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, _ in }, action: { [weak self] c, _ in

View File

@ -4855,9 +4855,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}, },
transferGift: { [weak profileGifts] prepaid, peerId in transferGift: { [weak profileGifts] prepaid, peerId in
guard let profileGifts, let reference = gift.reference else { 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 upgradeGift: { [weak profileGifts] formId, keepOriginalInfo in
guard let profileGifts, let reference = gift.reference else { 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 transferGift: { [weak self] prepaid, peerId in
guard let self, let reference = product.reference else { 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 upgradeGift: { [weak self] formId, keepOriginalInfo in
guard let self, let reference = product.reference else { 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) 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 shareStory: { [weak self] uniqueGift in
guard let self, let parentController = self.parentController else { guard let self, let parentController = self.parentController else {
return return
@ -977,7 +998,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
let pinnedToTop = !gift.pinnedToTop let pinnedToTop = !gift.pinnedToTop
if pinnedToTop && self.pinnedReferences.count >= self.maxPinnedCount { 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 return
} }
@ -989,10 +1010,10 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
let toastText: String let toastText: String
if !pinnedToTop { if !pinnedToTop {
toastTitle = nil toastTitle = nil
toastText = presentationData.strings.PeerInfo_Gifts_ToastUnpinned_Text toastText = strings.PeerInfo_Gifts_ToastUnpinned_Text
} else { } else {
toastTitle = presentationData.strings.PeerInfo_Gifts_ToastPinned_Title toastTitle = strings.PeerInfo_Gifts_ToastPinned_Title
toastText = presentationData.strings.PeerInfo_Gifts_ToastPinned_Text 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)) 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 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 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 { 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.5, {
Queue.mainQueue().after(1.0, {
if transferStars > 0 { if transferStars > 0 {
context.starsContext?.load(force: true) context.starsContext?.load(force: true)
} }
}) })
return self.profileGifts.transferStarGift(prepaid: transferStars == 0, reference: reference, peerId: peerId)
}) })
self.parentController?.push(controller) self.parentController?.push(controller)
}) })

View File

@ -2674,7 +2674,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return controller 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 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var presentExportAlertImpl: (() -> Void)? var presentExportAlertImpl: (() -> Void)?
@ -2912,6 +2912,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
guard let controller, case let .starGiftTransfer(_, _, gift, transferStars, _, _) = source else { guard let controller, case let .starGiftTransfer(_, _, gift, transferStars, _, _) = source else {
return return
} }
var dismissAlertImpl: (() -> Void)?
let alertController = giftTransferAlertController( let alertController = giftTransferAlertController(
context: context, context: context,
gift: gift, gift: gift,
@ -2920,61 +2921,89 @@ public final class SharedAccountContextImpl: SharedAccountContext {
navigationController: controller.navigationController as? NavigationController, navigationController: controller.navigationController as? NavigationController,
commit: { [weak controller] in commit: { [weak controller] in
let proceed: (Bool) -> Void = { waitForTopUp in let proceed: (Bool) -> Void = { waitForTopUp in
completion?([peer.id])
guard let controller, let navigationController = controller.navigationController as? NavigationController else { guard let controller, let navigationController = controller.navigationController as? NavigationController else {
return return
} }
var controllers = navigationController.viewControllers
controllers = controllers.filter { !($0 is ContactSelectionController) }
if !isChannelGift { if let completion {
if peer.id.namespace == Namespaces.Peer.CloudChannel { let _ = (completion([peer.id])
if let controller = context.sharedContext.makePeerInfoController( |> deliverOnMainQueue).startStandalone(error: { [weak navigationController] error in
context: context, guard let navigationController else {
updatedPresentationData: nil, return
peer: peer._asPeer(),
mode: .gifts,
avatarInitiallyExpanded: false,
fromChat: false,
requestsContext: nil
) {
controllers.append(controller)
} }
} else { dismissAlertImpl?()
var foundController = false
for controller in controllers.reversed() { var errorText: String?
if let chatController = controller as? ChatController, case .peer(id: peer.id) = chatController.chatLocation { switch error {
chatController.hintPlayNextOutgoingGift() case .disallowedStarGift:
foundController = true errorText = presentationData.strings.Gift_Send_ErrorDisallowed(peer.compactDisplayTitle).string
break 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 { }, completed: { [weak navigationController] in
let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.default), params: nil) guard let navigationController else {
chatController.hintPlayNextOutgoingGift() return
controllers.append(chatController)
} }
} dismissAlertImpl?()
}
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) {
var controllers = navigationController.viewControllers var controllers = navigationController.viewControllers
controllers = controllers.filter { !($0 is GiftViewScreen) } controllers = controllers.filter { !($0 is ContactSelectionController) }
navigationController.setViewControllers(controllers, animated: false) 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)) controller.present(alertController, in: .window(.root))
dismissAlertImpl = { [weak alertController] in
alertController?.dismissAnimated()
}
} }
return controller return controller