From ea1e9e797f755aee18a43b71301b67b8ce2987f4 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 22 Jul 2024 20:45:32 +0400 Subject: [PATCH 01/12] Stars subscriptions API [skip ci] --- .../Sources/InviteLinkEditController.swift | 8 +- .../Sources/InviteLinkListController.swift | 8 +- .../Sources/InviteLinkViewController.swift | 8 +- .../Sources/ItemListInviteLinkItem.swift | 6 +- .../Sources/ChannelStatsController.swift | 2 +- submodules/TelegramApi/Sources/Api0.swift | 15 +- submodules/TelegramApi/Sources/Api23.swift | 114 +++++- submodules/TelegramApi/Sources/Api33.swift | 62 +-- submodules/TelegramApi/Sources/Api36.swift | 41 +- submodules/TelegramApi/Sources/Api4.swift | 24 +- submodules/TelegramApi/Sources/Api6.swift | 20 +- submodules/TelegramApi/Sources/Api9.swift | 20 + .../Sources/ApiUtils/ExportedInvitation.swift | 21 +- .../SyncCore_ExportedInvitation.swift | 16 +- .../Payments/BotPaymentForm.swift | 5 +- .../TelegramEngine/Payments/Stars.swift | 366 ++++++++++++++++-- .../Payments/TelegramEnginePayments.swift | 12 +- .../Peers/InvitationLinks.swift | 13 +- .../TelegramEngine/Peers/JoinLink.swift | 6 +- .../Peers/TelegramEnginePeers.swift | 4 +- .../ChatRecentActionsHistoryTransition.swift | 23 +- 21 files changed, 639 insertions(+), 155 deletions(-) diff --git a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift index 951a5e4b4a..e137d9e099 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift @@ -1,4 +1,4 @@ -import Foundation +wimport Foundation import UIKit import AsyncDisplayKit import Display @@ -460,7 +460,7 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio let actionsDisposable = DisposableSet() let initialState: InviteLinkEditControllerState - if let invite = invite, case let .link(_, title, _, requestApproval, _, _, _, _, expireDate, usageLimit, count, _) = invite { + if let invite = invite, case let .link(_, title, _, requestApproval, _, _, _, _, expireDate, usageLimit, count, _, _) = invite { var usageLimit = usageLimit if let limit = usageLimit, let count = count, count > 0 { usageLimit = limit - count @@ -593,7 +593,7 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio let requestNeeded = state.requestApproval && !isPublic if invite == nil { - let _ = (context.engine.peers.createPeerExportedInvitation(peerId: peerId, title: title, expireDate: expireDate, usageLimit: requestNeeded ? 0 : usageLimit, requestNeeded: requestNeeded) + let _ = (context.engine.peers.createPeerExportedInvitation(peerId: peerId, title: title, expireDate: expireDate, usageLimit: requestNeeded ? 0 : usageLimit, requestNeeded: requestNeeded, subscriptionPricing: nil) |> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic)) |> deliverOnMainQueue).start(next: { invite in completion?(invite) @@ -606,7 +606,7 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio } presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) }) - } else if let initialInvite = invite, case let .link(link, _, _, initialRequestApproval, _, _, _, _, initialExpireDate, initialUsageLimit, _, _) = initialInvite { + } else if let initialInvite = invite, case let .link(link, _, _, initialRequestApproval, _, _, _, _, initialExpireDate, initialUsageLimit, _, _, _) = initialInvite { if initialExpireDate == expireDate && initialUsageLimit == usageLimit && initialRequestApproval == requestNeeded { completion?(initialInvite) dismissImpl?() diff --git a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift index c37972d0af..c59db516a9 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift @@ -284,7 +284,7 @@ private func inviteLinkListControllerEntries(presentationData: PresentationData, let mainInvite: ExportedInvitation? var isPublic = false if let peer = peer, let address = peer.addressName, !address.isEmpty && admin == nil { - mainInvite = .link(link: "t.me/\(address)", title: nil, isPermanent: true, requestApproval: false, isRevoked: false, adminId: EnginePeer.Id(0), date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil, requestedCount: nil) + mainInvite = .link(link: "t.me/\(address)", title: nil, isPermanent: true, requestApproval: false, isRevoked: false, adminId: EnginePeer.Id(0), date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil, requestedCount: nil, pricing: nil) isPublic = true } else if let invites = invites, let invite = invites.first(where: { $0.isPermanent && !$0.isRevoked }) { mainInvite = invite @@ -299,7 +299,7 @@ private func inviteLinkListControllerEntries(presentationData: PresentationData, let importersCount: Int32 if let count = importers?.count { importersCount = count - } else if let mainInvite = mainInvite, case let .link(_, _, _, _, _, _, _, _, _, _, count, _) = mainInvite, let count = count { + } else if let mainInvite = mainInvite, case let .link(_, _, _, _, _, _, _, _, _, _, count, _, _) = mainInvite, let count = count { importersCount = count } else { importersCount = 0 @@ -338,7 +338,7 @@ private func inviteLinkListControllerEntries(presentationData: PresentationData, if let additionalInvites = additionalInvites { var index: Int32 = 0 for invite in additionalInvites { - if case let .link(_, _, _, _, _, _, _, _, expireDate, _, _, _) = invite { + if case let .link(_, _, _, _, _, _, _, _, expireDate, _, _, _, _) = invite { entries.append(.link(index, presentationData.theme, invite, canEditLinks, expireDate != nil ? tick : nil)) index += 1 } @@ -533,7 +533,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio }) }))) - if case let .link(_, _, _, _, _, adminId, _, _, _, _, _, _) = invite, adminId.toInt64() != 0 { + if case let .link(_, _, _, _, _, adminId, _, _, _, _, _, _, _) = invite, adminId.toInt64() != 0 { items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextRevoke, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor) }, action: { _, f in diff --git a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift index d69e30ea2b..b7a4929a4a 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift @@ -425,7 +425,7 @@ public final class InviteLinkViewController: ViewController { self.controller = controller self.importersContext = importersContext ?? context.engine.peers.peerInvitationImporters(peerId: peerId, subject: .invite(invite: invite, requested: false)) - if case let .link(_, _, _, requestApproval, _, _, _, _, _, _, _, _) = invite, requestApproval { + if case let .link(_, _, _, requestApproval, _, _, _, _, _, _, _, _, _) = invite, requestApproval { self.requestsContext = context.engine.peers.peerInvitationImporters(peerId: peerId, subject: .invite(invite: invite, requested: true)) } else { self.requestsContext = nil @@ -568,7 +568,7 @@ public final class InviteLinkViewController: ViewController { } var creatorIsBot: Signal - if case let .link(_, _, _, _, _, adminId, _, _, _, _, _, _) = invite { + if case let .link(_, _, _, _, _, adminId, _, _, _, _, _, _, _) = invite { creatorIsBot = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: adminId)) |> map { peer -> Bool in if let peer, case let .user(user) = peer, user.botInfo != nil { @@ -716,7 +716,7 @@ public final class InviteLinkViewController: ViewController { requestsState = .single(PeerInvitationImportersState.Empty) } - if case let .link(_, _, _, _, _, adminId, date, _, _, usageLimit, _, _) = invite { + if case let .link(_, _, _, _, _, adminId, date, _, _, usageLimit, _, _, _) = invite { self.disposable = (combineLatest( self.presentationDataPromise.get(), self.importersContext.state, @@ -1002,7 +1002,7 @@ public final class InviteLinkViewController: ViewController { var subtitleText = "" var subtitleColor = self.presentationData.theme.list.itemSecondaryTextColor - if case let .link(_, title, _, _, isRevoked, _, _, _, expireDate, usageLimit, count, _) = self.invite { + if case let .link(_, title, _, _, isRevoked, _, _, _, expireDate, usageLimit, count, _, _) = self.invite { if isRevoked { subtitleText = self.presentationData.strings.InviteLink_Revoked } else if let usageLimit = usageLimit, let count = count, count >= usageLimit { diff --git a/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift b/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift index 1abb2b9da8..acf81eea96 100644 --- a/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift @@ -9,7 +9,7 @@ import ShimmerEffect import TelegramCore func invitationAvailability(_ invite: ExportedInvitation) -> CGFloat { - if case let .link(_, _, _, _, isRevoked, _, date, startDate, expireDate, usageLimit, count, _) = invite { + if case let .link(_, _, _, _, isRevoked, _, date, startDate, expireDate, usageLimit, count, _, _) = invite { if isRevoked { return 0.0 } @@ -299,7 +299,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { let color: ItemBackgroundColor let nextColor: ItemBackgroundColor let transitionFraction: CGFloat - if let invite = item.invite, case let .link(_, _, _, _, isRevoked, _, _, _, expireDate, usageLimit, _, _) = invite { + if let invite = item.invite, case let .link(_, _, _, _, isRevoked, _, _, _, expireDate, usageLimit, _, _, _) = invite { if isRevoked { color = .gray nextColor = .gray @@ -346,7 +346,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { var timerValue: TimerNode.Value? - if let invite = item.invite, case let .link(_, title, _, _, _, _, date, startDate, expireDate, usageLimit, count, requestedCount) = invite { + if let invite = item.invite, case let .link(_, title, _, _, _, _, date, startDate, expireDate, usageLimit, count, requestedCount, _) = invite { if let title = title, !title.isEmpty { titleText = title } diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index a294b33a05..9b3f183527 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -1024,7 +1024,7 @@ private enum StatsEntry: ItemListNodeEntry { case let .boostOverview(_, stats, isGroup): return StatsOverviewItem(context: arguments.context, presentationData: presentationData, isGroup: isGroup, stats: stats, sectionId: self.section, style: .blocks) case let .boostLink(_, link): - let invite: ExportedInvitation = .link(link: link, title: nil, isPermanent: false, requestApproval: false, isRevoked: false, adminId: PeerId(0), date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil, requestedCount: nil) + let invite: ExportedInvitation = .link(link: link, title: nil, isPermanent: false, requestApproval: false, isRevoked: false, adminId: PeerId(0), date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil, requestedCount: nil, pricing: nil) return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, count: 0, peers: [], displayButton: true, displayImporters: false, buttonColor: nil, sectionId: self.section, style: .blocks, copyAction: { arguments.copyBoostLink(link) }, shareAction: { diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 42d3ec2a58..1476843b49 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -201,7 +201,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1626209256] = { return Api.ChatBannedRights.parse_chatBannedRights($0) } dict[-1146407795] = { return Api.ChatFull.parse_channelFull($0) } dict[640893467] = { return Api.ChatFull.parse_chatFull($0) } - dict[-840897472] = { return Api.ChatInvite.parse_chatInvite($0) } + dict[-1965998484] = { return Api.ChatInvite.parse_chatInvite($0) } dict[1516793212] = { return Api.ChatInvite.parse_chatInviteAlready($0) } dict[1634294960] = { return Api.ChatInvite.parse_chatInvitePeek($0) } dict[-1940201511] = { return Api.ChatInviteImporter.parse_chatInviteImporter($0) } @@ -273,7 +273,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1038136962] = { return Api.EncryptedFile.parse_encryptedFileEmpty($0) } dict[-317144808] = { return Api.EncryptedMessage.parse_encryptedMessage($0) } dict[594758406] = { return Api.EncryptedMessage.parse_encryptedMessageService($0) } - dict[179611673] = { return Api.ExportedChatInvite.parse_chatInviteExported($0) } + dict[-1812799720] = { return Api.ExportedChatInvite.parse_chatInviteExported($0) } dict[-317687113] = { return Api.ExportedChatInvite.parse_chatInvitePublicJoinRequests($0) } dict[206668204] = { return Api.ExportedChatlistInvite.parse_exportedChatlistInvite($0) } dict[1103040667] = { return Api.ExportedContactToken.parse_exportedContactToken($0) } @@ -370,6 +370,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1210199983] = { return Api.InputGeoPoint.parse_inputGeoPoint($0) } dict[-457104426] = { return Api.InputGeoPoint.parse_inputGeoPointEmpty($0) } dict[-659913713] = { return Api.InputGroupCall.parse_inputGroupCall($0) } + dict[887591921] = { return Api.InputInvoice.parse_inputInvoiceChatInviteSubscription($0) } dict[-977967015] = { return Api.InputInvoice.parse_inputInvoiceMessage($0) } dict[-1734841331] = { return Api.InputInvoice.parse_inputInvoicePremiumGiftCode($0) } dict[-1020867857] = { return Api.InputInvoice.parse_inputInvoiceSlug($0) } @@ -879,8 +880,10 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1124938064] = { return Api.SponsoredMessageReportOption.parse_sponsoredMessageReportOption($0) } dict[1577421297] = { return Api.StarsGiftOption.parse_starsGiftOption($0) } dict[2033461574] = { return Api.StarsRevenueStatus.parse_starsRevenueStatus($0) } + dict[-797707802] = { return Api.StarsSubscription.parse_starsSubscription($0) } + dict[88173912] = { return Api.StarsSubscriptionPricing.parse_starsSubscriptionPricing($0) } dict[198776256] = { return Api.StarsTopupOption.parse_starsTopupOption($0) } - dict[766853519] = { return Api.StarsTransaction.parse_starsTransaction($0) } + dict[455361027] = { return Api.StarsTransaction.parse_starsTransaction($0) } dict[-670195363] = { return Api.StarsTransactionPeer.parse_starsTransactionPeer($0) } dict[1617438738] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerAds($0) } dict[-1269320843] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerAppStore($0) } @@ -1325,7 +1328,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[961445665] = { return Api.payments.StarsRevenueAdsAccountUrl.parse_starsRevenueAdsAccountUrl($0) } dict[-919881925] = { return Api.payments.StarsRevenueStats.parse_starsRevenueStats($0) } dict[497778871] = { return Api.payments.StarsRevenueWithdrawalUrl.parse_starsRevenueWithdrawalUrl($0) } - dict[-1930105248] = { return Api.payments.StarsStatus.parse_starsStatus($0) } + dict[-2064727699] = { return Api.payments.StarsStatus.parse_starsStatus($0) } dict[-784000893] = { return Api.payments.ValidatedRequestedInfo.parse_validatedRequestedInfo($0) } dict[541839704] = { return Api.phone.ExportedGroupCallInvite.parse_exportedGroupCallInvite($0) } dict[-1636664659] = { return Api.phone.GroupCall.parse_groupCall($0) } @@ -1996,6 +1999,10 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.StarsRevenueStatus: _1.serialize(buffer, boxed) + case let _1 as Api.StarsSubscription: + _1.serialize(buffer, boxed) + case let _1 as Api.StarsSubscriptionPricing: + _1.serialize(buffer, boxed) case let _1 as Api.StarsTopupOption: _1.serialize(buffer, boxed) case let _1 as Api.StarsTransaction: diff --git a/submodules/TelegramApi/Sources/Api23.swift b/submodules/TelegramApi/Sources/Api23.swift index 0d646cf4e4..6c490c574f 100644 --- a/submodules/TelegramApi/Sources/Api23.swift +++ b/submodules/TelegramApi/Sources/Api23.swift @@ -670,6 +670,102 @@ public extension Api { } } +public extension Api { + enum StarsSubscription: TypeConstructorDescription { + case starsSubscription(flags: Int32, id: String, peer: Api.Peer, untilDate: Int32, pricing: Api.StarsSubscriptionPricing) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .starsSubscription(let flags, let id, let peer, let untilDate, let pricing): + if boxed { + buffer.appendInt32(-797707802) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(id, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + serializeInt32(untilDate, buffer: buffer, boxed: false) + pricing.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .starsSubscription(let flags, let id, let peer, let untilDate, let pricing): + return ("starsSubscription", [("flags", flags as Any), ("id", id as Any), ("peer", peer as Any), ("untilDate", untilDate as Any), ("pricing", pricing as Any)]) + } + } + + public static func parse_starsSubscription(_ reader: BufferReader) -> StarsSubscription? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Api.Peer? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.Peer + } + var _4: Int32? + _4 = reader.readInt32() + var _5: Api.StarsSubscriptionPricing? + if let signature = reader.readInt32() { + _5 = Api.parse(reader, signature: signature) as? Api.StarsSubscriptionPricing + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.StarsSubscription.starsSubscription(flags: _1!, id: _2!, peer: _3!, untilDate: _4!, pricing: _5!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum StarsSubscriptionPricing: TypeConstructorDescription { + case starsSubscriptionPricing(period: Int32, amount: Int64) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .starsSubscriptionPricing(let period, let amount): + if boxed { + buffer.appendInt32(88173912) + } + serializeInt32(period, buffer: buffer, boxed: false) + serializeInt64(amount, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .starsSubscriptionPricing(let period, let amount): + return ("starsSubscriptionPricing", [("period", period as Any), ("amount", amount as Any)]) + } + } + + public static func parse_starsSubscriptionPricing(_ reader: BufferReader) -> StarsSubscriptionPricing? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.StarsSubscriptionPricing.starsSubscriptionPricing(period: _1!, amount: _2!) + } + else { + return nil + } + } + + } +} public extension Api { enum StarsTopupOption: TypeConstructorDescription { case starsTopupOption(flags: Int32, stars: Int64, storeProduct: String?, currency: String, amount: Int64) @@ -724,13 +820,13 @@ public extension Api { } public extension Api { enum StarsTransaction: TypeConstructorDescription { - case starsTransaction(flags: Int32, id: String, stars: Int64, date: Int32, peer: Api.StarsTransactionPeer, title: String?, description: String?, photo: Api.WebDocument?, transactionDate: Int32?, transactionUrl: String?, botPayload: Buffer?, msgId: Int32?, extendedMedia: [Api.MessageMedia]?) + case starsTransaction(flags: Int32, id: String, stars: Int64, date: Int32, peer: Api.StarsTransactionPeer, title: String?, description: String?, photo: Api.WebDocument?, transactionDate: Int32?, transactionUrl: String?, botPayload: Buffer?, msgId: Int32?, extendedMedia: [Api.MessageMedia]?, subscriptionPeriod: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia): + case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia, let subscriptionPeriod): if boxed { - buffer.appendInt32(766853519) + buffer.appendInt32(455361027) } serializeInt32(flags, buffer: buffer, boxed: false) serializeString(id, buffer: buffer, boxed: false) @@ -749,14 +845,15 @@ public extension Api { for item in extendedMedia! { item.serialize(buffer, true) }} + if Int(flags) & Int(1 << 11) != 0 {serializeInt32(subscriptionPeriod!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia): - return ("starsTransaction", [("flags", flags as Any), ("id", id as Any), ("stars", stars as Any), ("date", date as Any), ("peer", peer as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("transactionDate", transactionDate as Any), ("transactionUrl", transactionUrl as Any), ("botPayload", botPayload as Any), ("msgId", msgId as Any), ("extendedMedia", extendedMedia as Any)]) + case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia, let subscriptionPeriod): + return ("starsTransaction", [("flags", flags as Any), ("id", id as Any), ("stars", stars as Any), ("date", date as Any), ("peer", peer as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("transactionDate", transactionDate as Any), ("transactionUrl", transactionUrl as Any), ("botPayload", botPayload as Any), ("msgId", msgId as Any), ("extendedMedia", extendedMedia as Any), ("subscriptionPeriod", subscriptionPeriod as Any)]) } } @@ -793,6 +890,8 @@ public extension Api { if Int(_1!) & Int(1 << 9) != 0 {if let _ = reader.readInt32() { _13 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageMedia.self) } } + var _14: Int32? + if Int(_1!) & Int(1 << 11) != 0 {_14 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -806,8 +905,9 @@ public extension Api { let _c11 = (Int(_1!) & Int(1 << 7) == 0) || _11 != nil let _c12 = (Int(_1!) & Int(1 << 8) == 0) || _12 != nil let _c13 = (Int(_1!) & Int(1 << 9) == 0) || _13 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 { - return Api.StarsTransaction.starsTransaction(flags: _1!, id: _2!, stars: _3!, date: _4!, peer: _5!, title: _6, description: _7, photo: _8, transactionDate: _9, transactionUrl: _10, botPayload: _11, msgId: _12, extendedMedia: _13) + let _c14 = (Int(_1!) & Int(1 << 11) == 0) || _14 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 { + return Api.StarsTransaction.starsTransaction(flags: _1!, id: _2!, stars: _3!, date: _4!, peer: _5!, title: _6, description: _7, photo: _8, transactionDate: _9, transactionUrl: _10, botPayload: _11, msgId: _12, extendedMedia: _13, subscriptionPeriod: _14) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api33.swift b/submodules/TelegramApi/Sources/Api33.swift index 508a3b6a35..8d89b93a23 100644 --- a/submodules/TelegramApi/Sources/Api33.swift +++ b/submodules/TelegramApi/Sources/Api33.swift @@ -1246,21 +1246,27 @@ public extension Api.payments { } public extension Api.payments { enum StarsStatus: TypeConstructorDescription { - case starsStatus(flags: Int32, balance: Int64, history: [Api.StarsTransaction], nextOffset: String?, chats: [Api.Chat], users: [Api.User]) + case starsStatus(flags: Int32, balance: Int64, subscriptions: [Api.StarsSubscription]?, subscriptionsNextOffset: String?, history: [Api.StarsTransaction]?, nextOffset: String?, chats: [Api.Chat], users: [Api.User]) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .starsStatus(let flags, let balance, let history, let nextOffset, let chats, let users): + case .starsStatus(let flags, let balance, let subscriptions, let subscriptionsNextOffset, let history, let nextOffset, let chats, let users): if boxed { - buffer.appendInt32(-1930105248) + buffer.appendInt32(-2064727699) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(balance, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(history.count)) - for item in history { + if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(subscriptions!.count)) + for item in subscriptions! { item.serialize(buffer, true) - } + }} + if Int(flags) & Int(1 << 2) != 0 {serializeString(subscriptionsNextOffset!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(history!.count)) + for item in history! { + item.serialize(buffer, true) + }} if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} buffer.appendInt32(481674261) buffer.appendInt32(Int32(chats.count)) @@ -1278,8 +1284,8 @@ public extension Api.payments { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .starsStatus(let flags, let balance, let history, let nextOffset, let chats, let users): - return ("starsStatus", [("flags", flags as Any), ("balance", balance as Any), ("history", history as Any), ("nextOffset", nextOffset as Any), ("chats", chats as Any), ("users", users as Any)]) + case .starsStatus(let flags, let balance, let subscriptions, let subscriptionsNextOffset, let history, let nextOffset, let chats, let users): + return ("starsStatus", [("flags", flags as Any), ("balance", balance as Any), ("subscriptions", subscriptions as Any), ("subscriptionsNextOffset", subscriptionsNextOffset as Any), ("history", history as Any), ("nextOffset", nextOffset as Any), ("chats", chats as Any), ("users", users as Any)]) } } @@ -1288,28 +1294,36 @@ public extension Api.payments { _1 = reader.readInt32() var _2: Int64? _2 = reader.readInt64() - var _3: [Api.StarsTransaction]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarsTransaction.self) - } + var _3: [Api.StarsSubscription]? + if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarsSubscription.self) + } } var _4: String? - if Int(_1!) & Int(1 << 0) != 0 {_4 = parseString(reader) } - var _5: [Api.Chat]? + if Int(_1!) & Int(1 << 2) != 0 {_4 = parseString(reader) } + var _5: [Api.StarsTransaction]? + if Int(_1!) & Int(1 << 3) != 0 {if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarsTransaction.self) + } } + var _6: String? + if Int(_1!) & Int(1 << 0) != 0 {_6 = parseString(reader) } + var _7: [Api.Chat]? if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) } - var _6: [Api.User]? + var _8: [Api.User]? if let _ = reader.readInt32() { - _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.payments.StarsStatus.starsStatus(flags: _1!, balance: _2!, history: _3!, nextOffset: _4, chats: _5!, users: _6!) + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return Api.payments.StarsStatus.starsStatus(flags: _1!, balance: _2!, subscriptions: _3, subscriptionsNextOffset: _4, history: _5, nextOffset: _6, chats: _7!, users: _8!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api36.swift b/submodules/TelegramApi/Sources/Api36.swift index fbd9e0b3b2..16fdd263a6 100644 --- a/submodules/TelegramApi/Sources/Api36.swift +++ b/submodules/TelegramApi/Sources/Api36.swift @@ -5439,15 +5439,16 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func exportChatInvite(flags: Int32, peer: Api.InputPeer, expireDate: Int32?, usageLimit: Int32?, title: String?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func exportChatInvite(flags: Int32, peer: Api.InputPeer, expireDate: Int32?, usageLimit: Int32?, title: String?, subscriptionPricing: Api.StarsSubscriptionPricing?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-1607670315) + buffer.appendInt32(-1537876336) serializeInt32(flags, buffer: buffer, boxed: false) peer.serialize(buffer, true) if Int(flags) & Int(1 << 0) != 0 {serializeInt32(expireDate!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 1) != 0 {serializeInt32(usageLimit!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 4) != 0 {serializeString(title!, buffer: buffer, boxed: false)} - return (FunctionDescription(name: "messages.exportChatInvite", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("expireDate", String(describing: expireDate)), ("usageLimit", String(describing: usageLimit)), ("title", String(describing: title))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.ExportedChatInvite? in + if Int(flags) & Int(1 << 5) != 0 {subscriptionPricing!.serialize(buffer, true)} + return (FunctionDescription(name: "messages.exportChatInvite", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("expireDate", String(describing: expireDate)), ("usageLimit", String(describing: usageLimit)), ("title", String(describing: title)), ("subscriptionPricing", String(describing: subscriptionPricing))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.ExportedChatInvite? in let reader = BufferReader(buffer) var result: Api.ExportedChatInvite? if let signature = reader.readInt32() { @@ -8735,6 +8736,24 @@ public extension Api.functions.payments { }) } } +public extension Api.functions.payments { + static func changeStarsSubscription(flags: Int32, peer: Api.InputPeer, subscriptionId: String, canceled: Api.Bool?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-948500360) + serializeInt32(flags, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + serializeString(subscriptionId, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {canceled!.serialize(buffer, true)} + return (FunctionDescription(name: "payments.changeStarsSubscription", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("subscriptionId", String(describing: subscriptionId)), ("canceled", String(describing: canceled))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.payments { static func checkGiftCode(slug: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -8954,6 +8973,22 @@ public extension Api.functions.payments { }) } } +public extension Api.functions.payments { + static func getStarsSubscriptions(peer: Api.InputPeer, offset: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-2145547570) + peer.serialize(buffer, true) + serializeString(offset, buffer: buffer, boxed: false) + return (FunctionDescription(name: "payments.getStarsSubscriptions", parameters: [("peer", String(describing: peer)), ("offset", String(describing: offset))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.StarsStatus? in + let reader = BufferReader(buffer) + var result: Api.payments.StarsStatus? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.payments.StarsStatus + } + return result + }) + } +} public extension Api.functions.payments { static func getStarsTopupOptions() -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Api.StarsTopupOption]>) { let buffer = Buffer() diff --git a/submodules/TelegramApi/Sources/Api4.swift b/submodules/TelegramApi/Sources/Api4.swift index 09f23bb178..c934d3962d 100644 --- a/submodules/TelegramApi/Sources/Api4.swift +++ b/submodules/TelegramApi/Sources/Api4.swift @@ -1280,15 +1280,15 @@ public extension Api { } public extension Api { indirect enum ChatInvite: TypeConstructorDescription { - case chatInvite(flags: Int32, title: String, about: String?, photo: Api.Photo, participantsCount: Int32, participants: [Api.User]?, color: Int32) + case chatInvite(flags: Int32, title: String, about: String?, photo: Api.Photo, participantsCount: Int32, participants: [Api.User]?, color: Int32, subscriptionPricing: Api.StarsSubscriptionPricing?, subscriptionFormId: Int64?) case chatInviteAlready(chat: Api.Chat) case chatInvitePeek(chat: Api.Chat, expires: Int32) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .chatInvite(let flags, let title, let about, let photo, let participantsCount, let participants, let color): + case .chatInvite(let flags, let title, let about, let photo, let participantsCount, let participants, let color, let subscriptionPricing, let subscriptionFormId): if boxed { - buffer.appendInt32(-840897472) + buffer.appendInt32(-1965998484) } serializeInt32(flags, buffer: buffer, boxed: false) serializeString(title, buffer: buffer, boxed: false) @@ -1301,6 +1301,8 @@ public extension Api { item.serialize(buffer, true) }} serializeInt32(color, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 10) != 0 {subscriptionPricing!.serialize(buffer, true)} + if Int(flags) & Int(1 << 10) != 0 {serializeInt64(subscriptionFormId!, buffer: buffer, boxed: false)} break case .chatInviteAlready(let chat): if boxed { @@ -1320,8 +1322,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .chatInvite(let flags, let title, let about, let photo, let participantsCount, let participants, let color): - return ("chatInvite", [("flags", flags as Any), ("title", title as Any), ("about", about as Any), ("photo", photo as Any), ("participantsCount", participantsCount as Any), ("participants", participants as Any), ("color", color as Any)]) + case .chatInvite(let flags, let title, let about, let photo, let participantsCount, let participants, let color, let subscriptionPricing, let subscriptionFormId): + return ("chatInvite", [("flags", flags as Any), ("title", title as Any), ("about", about as Any), ("photo", photo as Any), ("participantsCount", participantsCount as Any), ("participants", participants as Any), ("color", color as Any), ("subscriptionPricing", subscriptionPricing as Any), ("subscriptionFormId", subscriptionFormId as Any)]) case .chatInviteAlready(let chat): return ("chatInviteAlready", [("chat", chat as Any)]) case .chatInvitePeek(let chat, let expires): @@ -1348,6 +1350,12 @@ public extension Api { } } var _7: Int32? _7 = reader.readInt32() + var _8: Api.StarsSubscriptionPricing? + if Int(_1!) & Int(1 << 10) != 0 {if let signature = reader.readInt32() { + _8 = Api.parse(reader, signature: signature) as? Api.StarsSubscriptionPricing + } } + var _9: Int64? + if Int(_1!) & Int(1 << 10) != 0 {_9 = reader.readInt64() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = (Int(_1!) & Int(1 << 5) == 0) || _3 != nil @@ -1355,8 +1363,10 @@ public extension Api { let _c5 = _5 != nil let _c6 = (Int(_1!) & Int(1 << 4) == 0) || _6 != nil let _c7 = _7 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { - return Api.ChatInvite.chatInvite(flags: _1!, title: _2!, about: _3, photo: _4!, participantsCount: _5!, participants: _6, color: _7!) + let _c8 = (Int(_1!) & Int(1 << 10) == 0) || _8 != nil + let _c9 = (Int(_1!) & Int(1 << 10) == 0) || _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.ChatInvite.chatInvite(flags: _1!, title: _2!, about: _3, photo: _4!, participantsCount: _5!, participants: _6, color: _7!, subscriptionPricing: _8, subscriptionFormId: _9) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api6.swift b/submodules/TelegramApi/Sources/Api6.swift index 177e11efde..f5894fb213 100644 --- a/submodules/TelegramApi/Sources/Api6.swift +++ b/submodules/TelegramApi/Sources/Api6.swift @@ -1012,14 +1012,14 @@ public extension Api { } public extension Api { enum ExportedChatInvite: TypeConstructorDescription { - case chatInviteExported(flags: Int32, link: String, adminId: Int64, date: Int32, startDate: Int32?, expireDate: Int32?, usageLimit: Int32?, usage: Int32?, requested: Int32?, title: String?) + case chatInviteExported(flags: Int32, link: String, adminId: Int64, date: Int32, startDate: Int32?, expireDate: Int32?, usageLimit: Int32?, usage: Int32?, requested: Int32?, title: String?, subscriptionPricing: Api.StarsSubscriptionPricing?) case chatInvitePublicJoinRequests public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .chatInviteExported(let flags, let link, let adminId, let date, let startDate, let expireDate, let usageLimit, let usage, let requested, let title): + case .chatInviteExported(let flags, let link, let adminId, let date, let startDate, let expireDate, let usageLimit, let usage, let requested, let title, let subscriptionPricing): if boxed { - buffer.appendInt32(179611673) + buffer.appendInt32(-1812799720) } serializeInt32(flags, buffer: buffer, boxed: false) serializeString(link, buffer: buffer, boxed: false) @@ -1031,6 +1031,7 @@ public extension Api { if Int(flags) & Int(1 << 3) != 0 {serializeInt32(usage!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 7) != 0 {serializeInt32(requested!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 8) != 0 {serializeString(title!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 9) != 0 {subscriptionPricing!.serialize(buffer, true)} break case .chatInvitePublicJoinRequests: if boxed { @@ -1043,8 +1044,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .chatInviteExported(let flags, let link, let adminId, let date, let startDate, let expireDate, let usageLimit, let usage, let requested, let title): - return ("chatInviteExported", [("flags", flags as Any), ("link", link as Any), ("adminId", adminId as Any), ("date", date as Any), ("startDate", startDate as Any), ("expireDate", expireDate as Any), ("usageLimit", usageLimit as Any), ("usage", usage as Any), ("requested", requested as Any), ("title", title as Any)]) + case .chatInviteExported(let flags, let link, let adminId, let date, let startDate, let expireDate, let usageLimit, let usage, let requested, let title, let subscriptionPricing): + return ("chatInviteExported", [("flags", flags as Any), ("link", link as Any), ("adminId", adminId as Any), ("date", date as Any), ("startDate", startDate as Any), ("expireDate", expireDate as Any), ("usageLimit", usageLimit as Any), ("usage", usage as Any), ("requested", requested as Any), ("title", title as Any), ("subscriptionPricing", subscriptionPricing as Any)]) case .chatInvitePublicJoinRequests: return ("chatInvitePublicJoinRequests", []) } @@ -1071,6 +1072,10 @@ public extension Api { if Int(_1!) & Int(1 << 7) != 0 {_9 = reader.readInt32() } var _10: String? if Int(_1!) & Int(1 << 8) != 0 {_10 = parseString(reader) } + var _11: Api.StarsSubscriptionPricing? + if Int(_1!) & Int(1 << 9) != 0 {if let signature = reader.readInt32() { + _11 = Api.parse(reader, signature: signature) as? Api.StarsSubscriptionPricing + } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -1081,8 +1086,9 @@ public extension Api { let _c8 = (Int(_1!) & Int(1 << 3) == 0) || _8 != nil let _c9 = (Int(_1!) & Int(1 << 7) == 0) || _9 != nil let _c10 = (Int(_1!) & Int(1 << 8) == 0) || _10 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { - return Api.ExportedChatInvite.chatInviteExported(flags: _1!, link: _2!, adminId: _3!, date: _4!, startDate: _5, expireDate: _6, usageLimit: _7, usage: _8, requested: _9, title: _10) + let _c11 = (Int(_1!) & Int(1 << 9) == 0) || _11 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 { + return Api.ExportedChatInvite.chatInviteExported(flags: _1!, link: _2!, adminId: _3!, date: _4!, startDate: _5, expireDate: _6, usageLimit: _7, usage: _8, requested: _9, title: _10, subscriptionPricing: _11) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api9.swift b/submodules/TelegramApi/Sources/Api9.swift index 85225552db..3e68195c3f 100644 --- a/submodules/TelegramApi/Sources/Api9.swift +++ b/submodules/TelegramApi/Sources/Api9.swift @@ -1004,6 +1004,7 @@ public extension Api { } public extension Api { indirect enum InputInvoice: TypeConstructorDescription { + case inputInvoiceChatInviteSubscription(hash: String) case inputInvoiceMessage(peer: Api.InputPeer, msgId: Int32) case inputInvoicePremiumGiftCode(purpose: Api.InputStorePaymentPurpose, option: Api.PremiumGiftCodeOption) case inputInvoiceSlug(slug: String) @@ -1011,6 +1012,12 @@ public extension Api { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { + case .inputInvoiceChatInviteSubscription(let hash): + if boxed { + buffer.appendInt32(887591921) + } + serializeString(hash, buffer: buffer, boxed: false) + break case .inputInvoiceMessage(let peer, let msgId): if boxed { buffer.appendInt32(-977967015) @@ -1042,6 +1049,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { + case .inputInvoiceChatInviteSubscription(let hash): + return ("inputInvoiceChatInviteSubscription", [("hash", hash as Any)]) case .inputInvoiceMessage(let peer, let msgId): return ("inputInvoiceMessage", [("peer", peer as Any), ("msgId", msgId as Any)]) case .inputInvoicePremiumGiftCode(let purpose, let option): @@ -1053,6 +1062,17 @@ public extension Api { } } + public static func parse_inputInvoiceChatInviteSubscription(_ reader: BufferReader) -> InputInvoice? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.InputInvoice.inputInvoiceChatInviteSubscription(hash: _1!) + } + else { + return nil + } + } public static func parse_inputInvoiceMessage(_ reader: BufferReader) -> InputInvoice? { var _1: Api.InputPeer? if let signature = reader.readInt32() { diff --git a/submodules/TelegramCore/Sources/ApiUtils/ExportedInvitation.swift b/submodules/TelegramCore/Sources/ApiUtils/ExportedInvitation.swift index ace0b15845..67e92ed228 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ExportedInvitation.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ExportedInvitation.swift @@ -6,8 +6,8 @@ import TelegramApi extension ExportedInvitation { init(apiExportedInvite: Api.ExportedChatInvite) { switch apiExportedInvite { - case let .chatInviteExported(flags, link, adminId, date, startDate, expireDate, usageLimit, usage, requested, title): - self = .link(link: link, title: title, isPermanent: (flags & (1 << 5)) != 0, requestApproval: (flags & (1 << 6)) != 0, isRevoked: (flags & (1 << 0)) != 0, adminId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(adminId)), date: date, startDate: startDate, expireDate: expireDate, usageLimit: usageLimit, count: usage, requestedCount: requested) + case let .chatInviteExported(flags, link, adminId, date, startDate, expireDate, usageLimit, usage, requested, title, pricing): + self = .link(link: link, title: title, isPermanent: (flags & (1 << 5)) != 0, requestApproval: (flags & (1 << 6)) != 0, isRevoked: (flags & (1 << 0)) != 0, adminId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(adminId)), date: date, startDate: startDate, expireDate: expireDate, usageLimit: usageLimit, count: usage, requestedCount: requested, pricing: pricing.flatMap { StarsSubscriptionPricing(apiStarsSubscriptionPricing: $0) }) case .chatInvitePublicJoinRequests: self = .publicJoinRequest } @@ -17,7 +17,7 @@ extension ExportedInvitation { public extension ExportedInvitation { var link: String? { switch self { - case let .link(link, _, _, _, _, _, _, _, _, _, _, _): + case let .link(link, _, _, _, _, _, _, _, _, _, _, _, _): return link case .publicJoinRequest: return nil @@ -26,7 +26,7 @@ public extension ExportedInvitation { var date: Int32? { switch self { - case let .link(_, _, _, _, _, _, date, _, _, _, _, _): + case let .link(_, _, _, _, _, _, date, _, _, _, _, _, _): return date case .publicJoinRequest: return nil @@ -35,7 +35,7 @@ public extension ExportedInvitation { var isPermanent: Bool { switch self { - case let .link(_, _, isPermanent, _, _, _, _, _, _, _, _, _): + case let .link(_, _, isPermanent, _, _, _, _, _, _, _, _, _, _): return isPermanent case .publicJoinRequest: return false @@ -44,10 +44,19 @@ public extension ExportedInvitation { var isRevoked: Bool { switch self { - case let .link(_, _, _, _, isRevoked, _, _, _, _, _, _, _): + case let .link(_, _, _, _, isRevoked, _, _, _, _, _, _, _, _): return isRevoked case .publicJoinRequest: return false } } + + var pricing: StarsSubscriptionPricing? { + switch self { + case let .link(_, _, _, _, _, _, _, _, _, _, _, _, pricing): + return pricing + case .publicJoinRequest: + return nil + } + } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ExportedInvitation.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ExportedInvitation.swift index f10d5c7861..6c61a31ba4 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ExportedInvitation.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ExportedInvitation.swift @@ -1,7 +1,7 @@ import Postbox public enum ExportedInvitation: Codable, Equatable { - case link(link: String, title: String?, isPermanent: Bool, requestApproval: Bool, isRevoked: Bool, adminId: PeerId, date: Int32, startDate: Int32?, expireDate: Int32?, usageLimit: Int32?, count: Int32?, requestedCount: Int32?) + case link(link: String, title: String?, isPermanent: Bool, requestApproval: Bool, isRevoked: Bool, adminId: PeerId, date: Int32, startDate: Int32?, expireDate: Int32?, usageLimit: Int32?, count: Int32?, requestedCount: Int32?, pricing: StarsSubscriptionPricing?) case publicJoinRequest public init(from decoder: Decoder) throws { @@ -21,8 +21,9 @@ public enum ExportedInvitation: Codable, Equatable { let usageLimit = try container.decodeIfPresent(Int32.self, forKey: "usageLimit") let count = try container.decodeIfPresent(Int32.self, forKey: "count") let requestedCount = try? container.decodeIfPresent(Int32.self, forKey: "requestedCount") + let pricing = try? container.decodeIfPresent(StarsSubscriptionPricing.self, forKey: "pricing") - self = .link(link: link, title: title, isPermanent: isPermanent, requestApproval: requestApproval, isRevoked: isRevoked, adminId: adminId, date: date, startDate: startDate, expireDate: expireDate, usageLimit: usageLimit, count: count, requestedCount: requestedCount) + self = .link(link: link, title: title, isPermanent: isPermanent, requestApproval: requestApproval, isRevoked: isRevoked, adminId: adminId, date: date, startDate: startDate, expireDate: expireDate, usageLimit: usageLimit, count: count, requestedCount: requestedCount, pricing: pricing) } else { self = .publicJoinRequest } @@ -32,7 +33,7 @@ public enum ExportedInvitation: Codable, Equatable { var container = encoder.container(keyedBy: StringCodingKey.self) switch self { - case let .link(link, title, isPermanent, requestApproval, isRevoked, adminId, date, startDate, expireDate, usageLimit, count, requestedCount): + case let .link(link, title, isPermanent, requestApproval, isRevoked, adminId, date, startDate, expireDate, usageLimit, count, requestedCount, pricing): let type: Int32 = 0 try container.encode(type, forKey: "t") try container.encode(link, forKey: "l") @@ -47,6 +48,7 @@ public enum ExportedInvitation: Codable, Equatable { try container.encodeIfPresent(usageLimit, forKey: "usageLimit") try container.encodeIfPresent(count, forKey: "count") try container.encodeIfPresent(requestedCount, forKey: "requestedCount") + try container.encodeIfPresent(pricing, forKey: "pricing") case .publicJoinRequest: let type: Int32 = 1 try container.encode(type, forKey: "t") @@ -55,8 +57,8 @@ public enum ExportedInvitation: Codable, Equatable { public static func ==(lhs: ExportedInvitation, rhs: ExportedInvitation) -> Bool { switch lhs { - case let .link(link, title, isPermanent, requestApproval, isRevoked, adminId, date, startDate, expireDate, usageLimit, count, requestedCount): - if case .link(link, title, isPermanent, requestApproval, isRevoked, adminId, date, startDate, expireDate, usageLimit, count, requestedCount) = rhs { + case let .link(link, title, isPermanent, requestApproval, isRevoked, adminId, date, startDate, expireDate, usageLimit, count, requestedCount, pricing): + if case .link(link, title, isPermanent, requestApproval, isRevoked, adminId, date, startDate, expireDate, usageLimit, count, requestedCount, pricing) = rhs { return true } else { return false @@ -72,8 +74,8 @@ public enum ExportedInvitation: Codable, Equatable { public func withUpdated(isRevoked: Bool) -> ExportedInvitation { switch self { - case let .link(link, title, isPermanent, requestApproval, _, adminId, date, startDate, expireDate, usageLimit, count, requestedCount): - return .link(link: link, title: title, isPermanent: isPermanent, requestApproval: requestApproval, isRevoked: isRevoked, adminId: adminId, date: date, startDate: startDate, expireDate: expireDate, usageLimit: usageLimit, count: count, requestedCount: requestedCount) + case let .link(link, title, isPermanent, requestApproval, _, adminId, date, startDate, expireDate, usageLimit, count, requestedCount, pricing): + return .link(link: link, title: title, isPermanent: isPermanent, requestApproval: requestApproval, isRevoked: isRevoked, adminId: adminId, date: date, startDate: startDate, expireDate: expireDate, usageLimit: usageLimit, count: count, requestedCount: requestedCount, pricing: pricing) case .publicJoinRequest: return .publicJoinRequest } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index d5175d928d..d8768a0297 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -11,6 +11,7 @@ public enum BotPaymentInvoiceSource { case giftCode(users: [PeerId], currency: String, amount: Int64, option: PremiumGiftCodeOption) case stars(option: StarsTopUpOption) case starsGift(peerId: EnginePeer.Id, count: Int64, currency: String, amount: Int64) + case starsChatSubscription(hash: String) } public struct BotPaymentInvoiceFields: OptionSet { @@ -314,6 +315,8 @@ func _internal_parseInputInvoice(transaction: Transaction, source: BotPaymentInv return nil } return .inputInvoiceStars(purpose: .inputStorePaymentStarsGift(userId: inputUser, stars: count, currency: currency, amount: amount)) + case let .starsChatSubscription(hash): + return .inputInvoiceChatInviteSubscription(hash: hash) } } @@ -612,7 +615,7 @@ func _internal_sendBotPaymentForm(account: Account, formId: Int64, source: BotPa receiptMessageId = id } } - case .giftCode, .stars, .starsGift: + case .giftCode, .stars, .starsGift, .starsChatSubscription: receiptMessageId = nil } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index 0bf8b5549c..2398e7a42e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -147,8 +147,10 @@ func _internal_starsGiftOptions(account: Account, peerId: EnginePeer.Id?) -> Sig struct InternalStarsStatus { let balance: Int64 + let subscriptions: [StarsContext.State.Subscription] + let nextSubscriptionsOffset: String? let transactions: [StarsContext.State.Transaction] - let nextOffset: String? + let nextTransactionsOffset: String? } private enum RequestStarsStateError { @@ -187,17 +189,25 @@ private func _internal_requestStarsState(account: Account, peerId: EnginePeer.Id |> mapToSignal { result -> Signal in return account.postbox.transaction { transaction -> InternalStarsStatus in switch result { - case let .starsStatus(_, balance, history, nextOffset, chats, users): + case let .starsStatus(_, balance, _, _, transactions, nextTransactionsOffset, chats, users): let peers = AccumulatedPeers(chats: chats, users: users) updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: peers) - + var parsedTransactions: [StarsContext.State.Transaction] = [] - for entry in history { - if let parsedTransaction = StarsContext.State.Transaction(apiTransaction: entry, peerId: peerId != account.peerId ? peerId : nil, transaction: transaction) { - parsedTransactions.append(parsedTransaction) + if let transactions { + for entry in transactions { + if let parsedTransaction = StarsContext.State.Transaction(apiTransaction: entry, peerId: peerId != account.peerId ? peerId : nil, transaction: transaction) { + parsedTransactions.append(parsedTransaction) + } } } - return InternalStarsStatus(balance: balance, transactions: parsedTransactions, nextOffset: nextOffset) + return InternalStarsStatus( + balance: balance, + subscriptions: [], + nextSubscriptionsOffset: nil, + transactions: parsedTransactions, + nextTransactionsOffset: nextTransactionsOffset + ) } } |> castError(RequestStarsStateError.self) @@ -205,6 +215,51 @@ private func _internal_requestStarsState(account: Account, peerId: EnginePeer.Id } } +private enum RequestStarsSubscriptionsError { + case generic +} + +private func _internal_requestStarsSubscriptions(account: Account, peerId: EnginePeer.Id, offset: String) -> Signal { + return account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(peerId) + } + |> castError(RequestStarsSubscriptionsError.self) + |> mapToSignal { peer -> Signal in + guard let peer, let inputPeer = apiInputPeer(peer) else { + return .fail(.generic) + } + return account.network.request(Api.functions.payments.getStarsSubscriptions(peer: inputPeer, offset: offset)) + |> retryRequest + |> castError(RequestStarsSubscriptionsError.self) + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction -> InternalStarsStatus in + switch result { + case let .starsStatus(_, balance, subscriptions, subscriptionsNextOffset, _, _, chats, users): + let peers = AccumulatedPeers(chats: chats, users: users) + updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: peers) + + var parsedSubscriptions: [StarsContext.State.Subscription] = [] + if let subscriptions { + for entry in subscriptions { + if let parsedSubscription = StarsContext.State.Subscription(apiSubscription: entry, transaction: transaction) { + parsedSubscriptions.append(parsedSubscription) + } + } + } + return InternalStarsStatus( + balance: balance, + subscriptions: parsedSubscriptions, + nextSubscriptionsOffset: subscriptionsNextOffset, + transactions: [], + nextTransactionsOffset: nil + ) + } + } + |> castError(RequestStarsSubscriptionsError.self) + } + } +} + private final class StarsContextImpl { private let account: Account fileprivate let peerId: EnginePeer.Id @@ -214,7 +269,6 @@ private final class StarsContextImpl { var state: Signal { return self._statePromise.get() } - private var nextOffset: String? private let disposable = MetaDisposable() private var updateDisposable: Disposable? @@ -235,7 +289,7 @@ private final class StarsContextImpl { guard let self, let state = self._state, let balance = balances[peerId] else { return } - self.updateState(StarsContext.State(flags: [], balance: balance, transactions: state.transactions, canLoadMore: nextOffset != nil, isLoading: false)) + self.updateState(StarsContext.State(flags: [], balance: balance, subscriptions: state.subscriptions, canLoadMoreSubscriptions: state.canLoadMoreSubscriptions, transactions: state.transactions, canLoadMoreTransactions: state.canLoadMoreTransactions, isLoading: false)) self.load(force: true) }) } @@ -261,8 +315,7 @@ private final class StarsContextImpl { guard let self else { return } - self.updateState(StarsContext.State(flags: [], balance: status.balance, transactions: status.transactions, canLoadMore: status.nextOffset != nil, isLoading: false)) - self.nextOffset = status.nextOffset + self.updateState(StarsContext.State(flags: [], balance: status.balance, subscriptions: status.subscriptions, canLoadMoreSubscriptions: status.nextSubscriptionsOffset != nil, transactions: status.transactions, canLoadMoreTransactions: status.nextTransactionsOffset != nil, isLoading: false)) }, error: { [weak self] _ in guard let self else { return @@ -280,32 +333,14 @@ private final class StarsContextImpl { var transactions = state.transactions transactions.insert(.init(flags: [.isLocal], id: "\(arc4random())", count: balance, date: Int32(Date().timeIntervalSince1970), peer: .appStore, title: nil, description: nil, photo: nil, transactionDate: nil, transactionUrl: nil, paidMessageId: nil, media: []), at: 0) - self.updateState(StarsContext.State(flags: [.isPendingBalance], balance: state.balance + balance, transactions: transactions, canLoadMore: state.canLoadMore, isLoading: state.isLoading)) + self.updateState(StarsContext.State(flags: [.isPendingBalance], balance: state.balance + balance, subscriptions: state.subscriptions, canLoadMoreSubscriptions: state.canLoadMoreSubscriptions, transactions: transactions, canLoadMoreTransactions: state.canLoadMoreTransactions, isLoading: state.isLoading)) } fileprivate func updateBalance(_ balance: Int64, transactions: [StarsContext.State.Transaction]?) { guard let state = self._state else { return } - self.updateState(StarsContext.State(flags: [], balance: balance, transactions: transactions ?? state.transactions, canLoadMore: state.canLoadMore, isLoading: state.isLoading)) - } - - func loadMore() { - assert(Queue.mainQueue().isCurrent()) - - guard let currentState = self._state, let nextOffset = self.nextOffset else { - return - } - - self._state?.isLoading = true - - self.disposable.set((_internal_requestStarsState(account: self.account, peerId: self.peerId, mode: .all, offset: nextOffset, limit: 10) - |> deliverOnMainQueue).start(next: { [weak self] status in - if let self { - self.updateState(StarsContext.State(flags: [], balance: status.balance, transactions: currentState.transactions + status.transactions, canLoadMore: status.nextOffset != nil, isLoading: false)) - self.nextOffset = status.nextOffset - } - })) + self.updateState(StarsContext.State(flags: [], balance: balance, subscriptions: state.subscriptions, canLoadMoreSubscriptions: state.canLoadMoreSubscriptions, transactions: transactions ?? state.transactions, canLoadMoreTransactions: state.canLoadMoreTransactions, isLoading: state.isLoading)) } private func updateState(_ state: StarsContext.State) { @@ -317,7 +352,7 @@ private final class StarsContextImpl { private extension StarsContext.State.Transaction { init?(apiTransaction: Api.StarsTransaction, peerId: EnginePeer.Id?, transaction: Transaction) { switch apiTransaction { - case let .starsTransaction(apiFlags, id, stars, date, transactionPeer, title, description, photo, transactionDate, transactionUrl, _, messageId, extendedMedia): + case let .starsTransaction(apiFlags, id, stars, date, transactionPeer, title, description, photo, transactionDate, transactionUrl, _, messageId, extendedMedia, subscriptionPeriod): let parsedPeer: StarsContext.State.Transaction.Peer var paidMessageId: MessageId? switch transactionPeer { @@ -362,11 +397,28 @@ private extension StarsContext.State.Transaction { } let media = extendedMedia.flatMap({ $0.compactMap { textMediaAndExpirationTimerFromApiMedia($0, PeerId(0)).media } }) ?? [] + let _ = subscriptionPeriod self.init(flags: flags, id: id, count: stars, date: date, peer: parsedPeer, title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), transactionDate: transactionDate, transactionUrl: transactionUrl, paidMessageId: paidMessageId, media: media) } } } +private extension StarsContext.State.Subscription { + init?(apiSubscription: Api.StarsSubscription, transaction: Transaction) { + switch apiSubscription { + case let .starsSubscription(apiFlags, id, apiPeer, untilDate, pricing): + guard let peer = transaction.getPeer(apiPeer.peerId) else { + return nil + } + var flags: Flags = [] + if (apiFlags & (1 << 0)) != 0 { + flags.insert(.isCancelled) + } + self.init(flags: flags, id: id, peer: EnginePeer(peer), untilDate: untilDate, pricing: StarsSubscriptionPricing(apiStarsSubscriptionPricing: pricing)) + } + } +} + public final class StarsContext { public struct State: Equatable { public struct Transaction: Equatable { @@ -476,6 +528,57 @@ public final class StarsContext { } } + public struct Subscription: Equatable { + public struct Flags: OptionSet { + public var rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public static let isCancelled = Flags(rawValue: 1 << 0) + } + + public let flags: Flags + public let id: String + public let peer: EnginePeer + public let untilDate: Int32 + public let pricing: StarsSubscriptionPricing + + public init( + flags: Flags, + id: String, + peer: EnginePeer, + untilDate: Int32, + pricing: StarsSubscriptionPricing + ) { + self.flags = flags + self.id = id + self.peer = peer + self.untilDate = untilDate + self.pricing = pricing + } + + public static func == (lhs: Subscription, rhs: Subscription) -> Bool { + if lhs.flags != rhs.flags { + return false + } + if lhs.id != rhs.id { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.untilDate != rhs.untilDate { + return false + } + if lhs.pricing != rhs.pricing { + return false + } + return true + } + } + public struct Flags: OptionSet { public var rawValue: Int32 @@ -488,15 +591,19 @@ public final class StarsContext { public var flags: Flags public var balance: Int64 + public var subscriptions: [Subscription] + public var canLoadMoreSubscriptions: Bool public var transactions: [Transaction] - public var canLoadMore: Bool + public var canLoadMoreTransactions: Bool public var isLoading: Bool - init(flags: Flags, balance: Int64, transactions: [Transaction], canLoadMore: Bool, isLoading: Bool) { + init(flags: Flags, balance: Int64, subscriptions: [Subscription], canLoadMoreSubscriptions: Bool, transactions: [Transaction], canLoadMoreTransactions: Bool, isLoading: Bool) { self.flags = flags self.balance = balance + self.subscriptions = subscriptions + self.canLoadMoreSubscriptions = canLoadMoreSubscriptions self.transactions = transactions - self.canLoadMore = canLoadMore + self.canLoadMoreTransactions = canLoadMoreTransactions self.isLoading = isLoading } @@ -510,7 +617,10 @@ public final class StarsContext { if lhs.transactions != rhs.transactions { return false } - if lhs.canLoadMore != rhs.canLoadMore { + if lhs.subscriptions != rhs.subscriptions { + return false + } + if lhs.canLoadMoreTransactions != rhs.canLoadMoreTransactions { return false } if lhs.isLoading != rhs.isLoading { @@ -569,11 +679,6 @@ public final class StarsContext { } } - public func loadMore() { - self.impl.with { - $0.loadMore() - } - } init(account: Account) { self.impl = QueueLocalObject(queue: Queue.mainQueue(), generate: { @@ -690,7 +795,7 @@ private final class StarsTransactionsContextImpl { guard let self else { return } - self.nextOffset = status.nextOffset + self.nextOffset = status.nextTransactionsOffset var updatedState = self._state updatedState.transactions = nextOffset.isEmpty ? status.transactions : updatedState.transactions + status.transactions @@ -769,6 +874,112 @@ public final class StarsTransactionsContext { } } +private final class StarsSubscriptionsContextImpl { + private let account: Account + private weak var starsContext: StarsContext? + + private var _state: StarsSubscriptionsContext.State + private let _statePromise = Promise() + var state: Signal { + return self._statePromise.get() + } + private var nextOffset: String? = "" + + private let disposable = MetaDisposable() + private var stateDisposable: Disposable? + + init(account: Account, starsContext: StarsContext) { + assert(Queue.mainQueue().isCurrent()) + + self.account = account + self.starsContext = starsContext + + let currentSubscriptions = starsContext.currentState?.subscriptions ?? [] + let canLoadMore = starsContext.currentState?.canLoadMoreSubscriptions ?? true + + self._state = StarsSubscriptionsContext.State(subscriptions: currentSubscriptions, canLoadMore: canLoadMore, isLoading: false) + self._statePromise.set(.single(self._state)) + } + + deinit { + assert(Queue.mainQueue().isCurrent()) + self.disposable.dispose() + self.stateDisposable?.dispose() + } + + func loadMore() { + assert(Queue.mainQueue().isCurrent()) + + guard !self._state.isLoading, let nextOffset = self.nextOffset else { + return + } + + var updatedState = self._state + updatedState.isLoading = true + self.updateState(updatedState) + + self.disposable.set((_internal_requestStarsSubscriptions(account: self.account, peerId: self.account.peerId, offset: nextOffset) + |> deliverOnMainQueue).start(next: { [weak self] status in + guard let self else { + return + } + self.nextOffset = status.nextSubscriptionsOffset + + var updatedState = self._state + updatedState.subscriptions = nextOffset.isEmpty ? status.subscriptions : updatedState.subscriptions + status.subscriptions + updatedState.isLoading = false + updatedState.canLoadMore = self.nextOffset != nil + self.updateState(updatedState) + })) + } + + private func updateState(_ state: StarsSubscriptionsContext.State) { + self._state = state + self._statePromise.set(.single(state)) + } +} + +public final class StarsSubscriptionsContext { + public struct State: Equatable { + public var subscriptions: [StarsContext.State.Subscription] + public var canLoadMore: Bool + public var isLoading: Bool + + init(subscriptions: [StarsContext.State.Subscription], canLoadMore: Bool, isLoading: Bool) { + self.subscriptions = subscriptions + self.canLoadMore = canLoadMore + self.isLoading = isLoading + } + } + + fileprivate let impl: QueueLocalObject + + public var state: Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.state.start(next: { value in + subscriber.putNext(value) + })) + } + return disposable + } + } + + public func loadMore() { + self.impl.with { + $0.loadMore() + } + } + + init(account: Account, starsContext: StarsContext) { + self.impl = QueueLocalObject(queue: Queue.mainQueue(), generate: { + return StarsSubscriptionsContextImpl(account: account, starsContext: starsContext) + }) + } +} + + func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: BotPaymentInvoiceSource) -> Signal { return account.postbox.transaction { transaction -> Api.InputInvoice? in return _internal_parseInputInvoice(transaction: transaction, source: source) @@ -819,6 +1030,8 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot } case .giftCode, .stars, .starsGift: receiptMessageId = nil + case .starsChatSubscription: + receiptMessageId = nil } } } @@ -889,7 +1102,7 @@ func _internal_getStarsTransaction(accountPeerId: PeerId, postbox: Postbox, netw } |> mapToSignal { result -> Signal in return postbox.transaction { transaction -> StarsContext.State.Transaction? in - guard let result, case let .starsStatus(_, _, transactions, _, chats, users) = result, let matchingTransaction = transactions.first else { + guard let result, case let .starsStatus(_, _, _, _, transactions, _, chats, users) = result, let matchingTransaction = transactions?.first else { return nil } let peers = AccumulatedPeers(chats: chats, users: users) @@ -900,3 +1113,70 @@ func _internal_getStarsTransaction(accountPeerId: PeerId, postbox: Postbox, netw } } } + +public struct StarsSubscriptionPricing: Codable, Equatable { + private enum CodingKeys: String, CodingKey { + case period + case amount + } + + public let period: Int32 + public let amount: Int64 + + public init(period: Int32, amount: Int64) { + self.period = period + self.amount = amount + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.period = try container.decode(Int32.self, forKey: .period) + self.amount = try container.decode(Int64.self, forKey: .amount) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.period, forKey: .period) + try container.encode(self.amount, forKey: .amount) + } + + public static let monthPeriod = 2592000 + public static let testPeriod = 300 +} + +extension StarsSubscriptionPricing { + init(apiStarsSubscriptionPricing: Api.StarsSubscriptionPricing) { + switch apiStarsSubscriptionPricing { + case let .starsSubscriptionPricing(period, amount): + self = .init(period: period, amount: amount) + } + } + + var apiStarsSubscriptionPricing: Api.StarsSubscriptionPricing { + return .starsSubscriptionPricing(period: self.period, amount: self.amount) + } +} + +public enum CancelStarsSubsciptionError { + case generic +} + +func _internal_cancelStarsSubscription(account: Account, subscriptionId: String, reason: String) -> Signal { + return account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(account.peerId).flatMap(apiInputPeer) + } + |> castError(CancelStarsSubsciptionError.self) + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer else { + return .complete() + } + let flags: Int32 = (1 << 0) + return account.network.request(Api.functions.payments.changeStarsSubscription(flags: flags, peer: inputPeer, subscriptionId: subscriptionId, canceled: .boolTrue)) + |> mapError { _ -> CancelStarsSubsciptionError in + return .generic + } + |> ignoreValues + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index 062e457ea8..03df3c56c7 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -5,11 +5,11 @@ import Postbox public extension TelegramEngine { final class Payments { private let account: Account - + init(account: Account) { self.account = account } - + public func getBankCardInfo(cardNumber: String) -> Signal { return _internal_getBankCardInfo(account: self.account, cardNumber: cardNumber) } @@ -25,7 +25,7 @@ public extension TelegramEngine { public func validateBotPaymentForm(saveInfo: Bool, source: BotPaymentInvoiceSource, formInfo: BotPaymentRequestedInfo) -> Signal { return _internal_validateBotPaymentForm(account: self.account, saveInfo: saveInfo, source: source, formInfo: formInfo) } - + public func sendBotPaymentForm(source: BotPaymentInvoiceSource, formId: Int64, validatedInfoId: String?, shippingOptionId: String?, tipAmount: Int64?, credentials: BotPaymentCredentials) -> Signal { return _internal_sendBotPaymentForm(account: self.account, formId: formId, source: source, validatedInfoId: validatedInfoId, shippingOptionId: shippingOptionId, tipAmount: tipAmount, credentials: credentials) } @@ -33,7 +33,7 @@ public extension TelegramEngine { public func requestBotPaymentReceipt(messageId: MessageId) -> Signal { return _internal_requestBotPaymentReceipt(account: self.account, messageId: messageId) } - + public func clearBotPaymentInfo(info: BotPaymentInfo) -> Signal { return _internal_clearBotPaymentInfo(network: self.account.network, info: info) } @@ -89,5 +89,9 @@ public extension TelegramEngine { public func sendStarsPaymentForm(formId: Int64, source: BotPaymentInvoiceSource) -> Signal { return _internal_sendStarsPaymentForm(account: self.account, formId: formId, source: source) } + + public func cancelStarsSubscription(subscriptionId: String, reason: String) -> Signal { + return _internal_cancelStarsSubscription(account: self.account, subscriptionId: subscriptionId, reason: reason) + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/InvitationLinks.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/InvitationLinks.swift index c8c86a29cf..62a7d6ebdd 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/InvitationLinks.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/InvitationLinks.swift @@ -44,7 +44,7 @@ func _internal_revokePersistentPeerExportedInvitation(account: Account, peerId: if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { let flags: Int32 = (1 << 2) if let _ = peer as? TelegramChannel { - return account.network.request(Api.functions.messages.exportChatInvite(flags: flags, peer: inputPeer, expireDate: nil, usageLimit: nil, title: nil)) + return account.network.request(Api.functions.messages.exportChatInvite(flags: flags, peer: inputPeer, expireDate: nil, usageLimit: nil, title: nil, subscriptionPricing: nil)) |> retryRequest |> mapToSignal { result -> Signal in return account.postbox.transaction { transaction -> ExportedInvitation? in @@ -61,7 +61,7 @@ func _internal_revokePersistentPeerExportedInvitation(account: Account, peerId: } } } else if let _ = peer as? TelegramGroup { - return account.network.request(Api.functions.messages.exportChatInvite(flags: flags, peer: inputPeer, expireDate: nil, usageLimit: nil, title: nil)) + return account.network.request(Api.functions.messages.exportChatInvite(flags: flags, peer: inputPeer, expireDate: nil, usageLimit: nil, title: nil, subscriptionPricing: nil)) |> retryRequest |> mapToSignal { result -> Signal in return account.postbox.transaction { transaction -> ExportedInvitation? in @@ -90,7 +90,7 @@ public enum CreatePeerExportedInvitationError { case generic } -func _internal_createPeerExportedInvitation(account: Account, peerId: PeerId, title: String?, expireDate: Int32?, usageLimit: Int32?, requestNeeded: Bool?) -> Signal { +func _internal_createPeerExportedInvitation(account: Account, peerId: PeerId, title: String?, expireDate: Int32?, usageLimit: Int32?, requestNeeded: Bool?, subscriptionPricing: StarsSubscriptionPricing?) -> Signal { return account.postbox.transaction { transaction -> Signal in if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { var flags: Int32 = 0 @@ -106,7 +106,10 @@ func _internal_createPeerExportedInvitation(account: Account, peerId: PeerId, ti if let _ = title { flags |= (1 << 4) } - return account.network.request(Api.functions.messages.exportChatInvite(flags: flags, peer: inputPeer, expireDate: expireDate, usageLimit: usageLimit, title: title)) + if let _ = subscriptionPricing { + flags |= (1 << 5) + } + return account.network.request(Api.functions.messages.exportChatInvite(flags: flags, peer: inputPeer, expireDate: expireDate, usageLimit: usageLimit, title: title, subscriptionPricing: subscriptionPricing?.apiStarsSubscriptionPricing)) |> mapError { _ in return CreatePeerExportedInvitationError.generic } |> map { result -> ExportedInvitation? in return ExportedInvitation(apiExportedInvite: result) @@ -817,7 +820,7 @@ private final class PeerInvitationImportersContextImpl { var link: String? var count: Int32 = 0 - if let invite = invite, case let .link(inviteLink, _, _, _, _, _, _, _, _, _, inviteCount, _) = invite { + if let invite = invite, case let .link(inviteLink, _, _, _, _, _, _, _, _, _, inviteCount, _, _) = invite { link = inviteLink if let inviteCount = inviteCount { count = inviteCount diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift index be5f03b5a7..26bc0705df 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift @@ -48,6 +48,8 @@ public enum ExternalJoiningChatState { public let participantsCount: Int32 public let participants: [EnginePeer]? public let nameColor: PeerNameColor? + public let subscriptionPricing: StarsSubscriptionPricing? + public let subscriptionFormId: Int64? } case invite(Invite) @@ -106,10 +108,10 @@ func _internal_joinLinkInformation(_ hash: String, account: Account) -> Signal mapToSignal { result -> Signal in if let result = result { switch result { - case let .chatInvite(flags, title, about, invitePhoto, participantsCount, participants, nameColor): + case let .chatInvite(flags, title, about, invitePhoto, participantsCount, participants, nameColor, subscriptionPricing, subscriptionFormId): let photo = telegramMediaImageFromApiPhoto(invitePhoto).flatMap({ smallestImageRepresentation($0.representations) }) let flags: ExternalJoiningChatState.Invite.Flags = .init(isChannel: (flags & (1 << 0)) != 0, isBroadcast: (flags & (1 << 1)) != 0, isPublic: (flags & (1 << 2)) != 0, isMegagroup: (flags & (1 << 3)) != 0, requestNeeded: (flags & (1 << 6)) != 0, isVerified: (flags & (1 << 7)) != 0, isScam: (flags & (1 << 8)) != 0, isFake: (flags & (1 << 9)) != 0) - return .single(.invite(ExternalJoiningChatState.Invite(flags: flags, title: title, about: about, photoRepresentation: photo, participantsCount: participantsCount, participants: participants?.map({ EnginePeer(TelegramUser(user: $0)) }), nameColor: PeerNameColor(rawValue: nameColor)))) + return .single(.invite(ExternalJoiningChatState.Invite(flags: flags, title: title, about: about, photoRepresentation: photo, participantsCount: participantsCount, participants: participants?.map({ EnginePeer(TelegramUser(user: $0)) }), nameColor: PeerNameColor(rawValue: nameColor), subscriptionPricing: subscriptionPricing.flatMap { StarsSubscriptionPricing(apiStarsSubscriptionPricing: $0) }, subscriptionFormId: subscriptionFormId))) case let .chatInviteAlready(chat): if let peer = parseTelegramGroupOrChannel(chat: chat) { return account.postbox.transaction({ (transaction) -> ExternalJoiningChatState in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 948d724634..41578acbb3 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -705,8 +705,8 @@ public extension TelegramEngine { return _internal_checkPeerChatServiceActions(postbox: self.account.postbox, peerId: peerId) } - public func createPeerExportedInvitation(peerId: PeerId, title: String?, expireDate: Int32?, usageLimit: Int32?, requestNeeded: Bool?) -> Signal { - return _internal_createPeerExportedInvitation(account: self.account, peerId: peerId, title: title, expireDate: expireDate, usageLimit: usageLimit, requestNeeded: requestNeeded) + public func createPeerExportedInvitation(peerId: PeerId, title: String?, expireDate: Int32?, usageLimit: Int32?, requestNeeded: Bool?, subscriptionPricing: StarsSubscriptionPricing?) -> Signal { + return _internal_createPeerExportedInvitation(account: self.account, peerId: peerId, title: title, expireDate: expireDate, usageLimit: usageLimit, requestNeeded: requestNeeded, subscriptionPricing: subscriptionPricing) } public func editPeerExportedInvitation(peerId: PeerId, link: String, title: String?, expireDate: Int32?, usageLimit: Int32?, requestNeeded: Bool?) -> Signal { diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift index 7126d2d620..1137aa250b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift @@ -1460,7 +1460,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { var text: String = "" var entities: [MessageTextEntity] = [] - let rawText: PresentationStrings.FormattedString = self.presentationData.strings.Channel_AdminLog_DeletedInviteLink(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", invite.link_?.replacingOccurrences(of: "https://", with: "") ?? "") + let rawText: PresentationStrings.FormattedString = self.presentationData.strings.Channel_AdminLog_DeletedInviteLink(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", invite.link?.replacingOccurrences(of: "https://", with: "") ?? "") appendAttributedText(text: rawText, generateEntities: { index in if index == 0, let author = author { @@ -1486,7 +1486,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { var text: String = "" var entities: [MessageTextEntity] = [] - let rawText: PresentationStrings.FormattedString = self.presentationData.strings.Channel_AdminLog_RevokedInviteLink(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", invite.link_?.replacingOccurrences(of: "https://", with: "") ?? "") + let rawText: PresentationStrings.FormattedString = self.presentationData.strings.Channel_AdminLog_RevokedInviteLink(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", invite.link?.replacingOccurrences(of: "https://", with: "") ?? "") appendAttributedText(text: rawText, generateEntities: { index in if index == 0, let author = author { @@ -1512,7 +1512,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { var text: String = "" var entities: [MessageTextEntity] = [] - let rawText: PresentationStrings.FormattedString = self.presentationData.strings.Channel_AdminLog_EditedInviteLink(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", updatedInvite.link_?.replacingOccurrences(of: "https://", with: "") ?? "") + let rawText: PresentationStrings.FormattedString = self.presentationData.strings.Channel_AdminLog_EditedInviteLink(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", updatedInvite.link?.replacingOccurrences(of: "https://", with: "") ?? "") appendAttributedText(text: rawText, generateEntities: { index in if index == 0, let author = author { @@ -1540,9 +1540,9 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let rawText: PresentationStrings.FormattedString if joinedViaFolderLink { - rawText = self.presentationData.strings.Channel_AdminLog_JoinedViaFolderInviteLink(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", invite.link_?.replacingOccurrences(of: "https://", with: "") ?? "") + rawText = self.presentationData.strings.Channel_AdminLog_JoinedViaFolderInviteLink(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", invite.link?.replacingOccurrences(of: "https://", with: "") ?? "") } else { - rawText = self.presentationData.strings.Channel_AdminLog_JoinedViaInviteLink(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", invite.link_?.replacingOccurrences(of: "https://", with: "") ?? "") + rawText = self.presentationData.strings.Channel_AdminLog_JoinedViaInviteLink(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", invite.link?.replacingOccurrences(of: "https://", with: "") ?? "") } appendAttributedText(text: rawText, generateEntities: { index in @@ -1709,7 +1709,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let rawText: PresentationStrings.FormattedString switch invite { - case let .link(link, _, _, _, _, _, _, _, _, _, _, _): + case let .link(link, _, _, _, _, _, _, _, _, _, _, _, _): rawText = self.presentationData.strings.Channel_AdminLog_JoinedViaRequest(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", link.replacingOccurrences(of: "https://", with: ""), approver.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "") case .publicJoinRequest: rawText = self.presentationData.strings.Channel_AdminLog_JoinedViaPublicRequest(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", approver.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "") @@ -2326,14 +2326,3 @@ func chatRecentActionsHistoryPreparedTransition(from fromEntries: [ChatRecentAct return ChatRecentActionsHistoryTransition(filteredEntries: toEntries, type: type, deletions: deletions, insertions: insertions, updates: updates, canLoadEarlier: canLoadEarlier, displayingResults: displayingResults, searchResultsState: searchResultsState, synchronous: !toggledDeletedMessageIds.isEmpty, isEmpty: toEntries.isEmpty) } - -private extension ExportedInvitation { - var link_: String? { - switch self { - case let .link(link, _, _, _, _, _, _, _, _, _, _, _): - return link - case .publicJoinRequest: - return nil - } - } -} From bc454cfa93911366710876d38429866c64452313 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 1 Aug 2024 09:53:28 +0200 Subject: [PATCH 02/12] Stars subscriptions --- .../Sources/Node/ChatListNode.swift | 4 + .../Sources/Node/ChatListNodeEntries.swift | 1 + .../Sources/Node/ChatListNoticeItem.swift | 4 + submodules/InviteLinksUI/BUILD | 1 + .../Sources/InviteLinkEditController.swift | 247 +++++++++++++----- .../Sources/InviteLinkListController.swift | 4 +- .../Sources/InviteLinkViewController.swift | 98 ++++++- .../Sources/ItemListInviteLinkItem.swift | 49 +++- .../Items/ItemListDisclosureItem.swift | 49 +++- .../Items/ItemListSingleLineInputItem.swift | 44 +++- .../Sources/ResetPasswordController.swift | 6 +- .../ChangePhoneNumberCodeController.swift | 6 +- .../ProxyServerSettingsController.swift | 8 +- .../CreatePasswordController.swift | 6 +- .../TwoStepVerificationUnlockController.swift | 6 +- .../Sources/State/Serialization.swift | 2 +- .../TelegramEngine/Payments/Stars.swift | 16 +- .../Sources/StarsTransactionScreen.swift | 22 +- .../StarsTransactionsListPanelComponent.swift | 3 + .../Sources/StarsTransactionsScreen.swift | 41 ++- .../Sources/StarsWithdrawalScreen.swift | 10 +- .../TelegramUI/Sources/OpenResolvedUrl.swift | 56 +++- 22 files changed, 545 insertions(+), 138 deletions(-) diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 628025c9fc..e16451c4c9 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -747,6 +747,8 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL nodeInteraction?.openPremiumGift(birthdays) case .reviewLogin: break + case .starsSubscriptionLowBalance: + break } case .hide: nodeInteraction?.dismissNotice(notice) @@ -1085,6 +1087,8 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL nodeInteraction?.openPremiumGift(birthdays) case .reviewLogin: break + case .starsSubscriptionLowBalance: + break } case .hide: nodeInteraction?.dismissNotice(notice) diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift index 6ed8117ebf..953ba56328 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift @@ -90,6 +90,7 @@ public enum ChatListNotice: Equatable { case birthdayPremiumGift(peers: [EnginePeer], birthdays: [EnginePeer.Id: TelegramBirthday]) case reviewLogin(newSessionReview: NewSessionReview, totalCount: Int) case premiumGrace + case starsSubscriptionLowBalance } enum ChatListNodeEntry: Comparable, Identifiable { diff --git a/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift b/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift index b3f88f9b8c..cb7ca09b4e 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift @@ -262,6 +262,10 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode { okButtonLayout = makeOkButtonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.ChatList_SessionReview_PanelConfirm, font: titleFont, textColor: item.theme.list.itemAccentColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0))) cancelButtonLayout = makeCancelButtonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.ChatList_SessionReview_PanelReject, font: titleFont, textColor: item.theme.list.itemDestructiveColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0))) + case .starsSubscriptionLowBalance: + let titleStringValue = NSMutableAttributedString(attributedString: NSAttributedString(string: "5 Stars needed for Astro Paws", font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor)) + titleString = titleStringValue + textString = NSAttributedString(string: "Insufficient funds to cover your subscription.", font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) } var leftInset: CGFloat = sideInset diff --git a/submodules/InviteLinksUI/BUILD b/submodules/InviteLinksUI/BUILD index 1e3b637e4d..37f6192719 100644 --- a/submodules/InviteLinksUI/BUILD +++ b/submodules/InviteLinksUI/BUILD @@ -59,6 +59,7 @@ swift_library( "//submodules/QrCodeUI:QrCodeUI", "//submodules/PromptUI", "//submodules/TelegramUI/Components/ItemListDatePickerItem:ItemListDatePickerItem", + "//submodules/TelegramUI/Components/TextNodeWithEntities", ], visibility = [ "//visibility:public", diff --git a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift index fe5c4a8123..d988513a91 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift @@ -17,6 +17,7 @@ import ContextUI import TelegramStringFormatting import UndoUI import ItemListDatePickerItem +import TextFormat private final class InviteLinkEditControllerArguments { let context: AccountContext @@ -36,6 +37,7 @@ private final class InviteLinkEditControllerArguments { private enum InviteLinksEditSection: Int32 { case title + case subscriptionFee case requestApproval case time case usage @@ -75,18 +77,23 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { case title(PresentationTheme, String, String) case titleInfo(PresentationTheme, String) - case requestApproval(PresentationTheme, String, Bool) + + case subscriptionFeeToggle(PresentationTheme, String, Bool, Bool) + case subscriptionFee(PresentationTheme, String, Bool, Int64?) + case subscriptionFeeInfo(PresentationTheme, String) + + case requestApproval(PresentationTheme, String, Bool, Bool) case requestApprovalInfo(PresentationTheme, String) case timeHeader(PresentationTheme, String) - case timePicker(PresentationTheme, InviteLinkTimeLimit) - case timeExpiryDate(PresentationTheme, PresentationDateTimeFormat, Int32?, Bool) - case timeCustomPicker(PresentationTheme, PresentationDateTimeFormat, Int32?, Bool, Bool) + case timePicker(PresentationTheme, InviteLinkTimeLimit, Bool) + case timeExpiryDate(PresentationTheme, PresentationDateTimeFormat, Int32?, Bool, Bool) + case timeCustomPicker(PresentationTheme, PresentationDateTimeFormat, Int32?, Bool, Bool, Bool) case timeInfo(PresentationTheme, String) case usageHeader(PresentationTheme, String) - case usagePicker(PresentationTheme, PresentationDateTimeFormat, InviteLinkUsageLimit) - case usageCustomPicker(PresentationTheme, Int32?, Bool, Bool) + case usagePicker(PresentationTheme, PresentationDateTimeFormat, InviteLinkUsageLimit, Bool) + case usageCustomPicker(PresentationTheme, Int32?, Bool, Bool, Bool) case usageInfo(PresentationTheme, String) case revoke(PresentationTheme, String) @@ -95,6 +102,8 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { switch self { case .titleHeader, .title, .titleInfo: return InviteLinksEditSection.title.rawValue + case .subscriptionFeeToggle, .subscriptionFee, .subscriptionFeeInfo: + return InviteLinksEditSection.subscriptionFee.rawValue case .requestApproval, .requestApprovalInfo: return InviteLinksEditSection.requestApproval.rawValue case .timeHeader, .timePicker, .timeExpiryDate, .timeCustomPicker, .timeInfo: @@ -114,30 +123,36 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { return 1 case .titleInfo: return 2 - case .requestApproval: + case .subscriptionFeeToggle: return 3 - case .requestApprovalInfo: + case .subscriptionFee: return 4 - case .timeHeader: + case .subscriptionFeeInfo: return 5 - case .timePicker: + case .requestApproval: return 6 - case .timeExpiryDate: + case .requestApprovalInfo: return 7 - case .timeCustomPicker: + case .timeHeader: return 8 - case .timeInfo: + case .timePicker: return 9 - case .usageHeader: + case .timeExpiryDate: return 10 - case .usagePicker: + case .timeCustomPicker: return 11 - case .usageCustomPicker: + case .timeInfo: return 12 - case .usageInfo: + case .usageHeader: return 13 - case .revoke: + case .usagePicker: return 14 + case .usageCustomPicker: + return 15 + case .usageInfo: + return 16 + case .revoke: + return 17 } } @@ -161,8 +176,26 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { } else { return false } - case let .requestApproval(lhsTheme, lhsText, lhsValue): - if case let .requestApproval(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + case let .subscriptionFeeToggle(lhsTheme, lhsText, lhsValue, lhsEnabled): + if case let .subscriptionFeeToggle(rhsTheme, rhsText, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled { + return true + } else { + return false + } + case let .subscriptionFee(lhsTheme, lhsText, lhsValue, lhsEnabled): + if case let .subscriptionFee(rhsTheme, rhsText, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled { + return true + } else { + return false + } + case let .subscriptionFeeInfo(lhsTheme, lhsText): + if case let .subscriptionFeeInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .requestApproval(lhsTheme, lhsText, lhsValue, lhsEnabled): + if case let .requestApproval(rhsTheme, rhsText, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled { return true } else { return false @@ -179,20 +212,20 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { } else { return false } - case let .timePicker(lhsTheme, lhsValue): - if case let .timePicker(rhsTheme, rhsValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue { + case let .timePicker(lhsTheme, lhsValue, lhsEnabled): + if case let .timePicker(rhsTheme, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue, lhsEnabled == rhsEnabled { return true } else { return false } - case let .timeExpiryDate(lhsTheme, lhsDateTimeFormat, lhsDate, lhsActive): - if case let .timeExpiryDate(rhsTheme, rhsDateTimeFormat, rhsDate, rhsActive) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsDate == rhsDate, lhsActive == rhsActive { + case let .timeExpiryDate(lhsTheme, lhsDateTimeFormat, lhsDate, lhsActive, lhsEnabled): + if case let .timeExpiryDate(rhsTheme, rhsDateTimeFormat, rhsDate, rhsActive, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsDate == rhsDate, lhsActive == rhsActive, lhsEnabled == rhsEnabled { return true } else { return false } - case let .timeCustomPicker(lhsTheme, lhsDateTimeFormat, lhsDate, lhsDisplayingDateSelection, lhsDisplayingTimeSelection): - if case let .timeCustomPicker(rhsTheme, rhsDateTimeFormat, rhsDate, rhsDisplayingDateSelection, rhsDisplayingTimeSelection) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsDate == rhsDate, lhsDisplayingDateSelection == rhsDisplayingDateSelection, lhsDisplayingTimeSelection == rhsDisplayingTimeSelection { + case let .timeCustomPicker(lhsTheme, lhsDateTimeFormat, lhsDate, lhsDisplayingDateSelection, lhsDisplayingTimeSelection, lhsEnabled): + if case let .timeCustomPicker(rhsTheme, rhsDateTimeFormat, rhsDate, rhsDisplayingDateSelection, rhsDisplayingTimeSelection, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsDate == rhsDate, lhsDisplayingDateSelection == rhsDisplayingDateSelection, lhsDisplayingTimeSelection == rhsDisplayingTimeSelection, lhsEnabled == rhsEnabled { return true } else { return false @@ -209,14 +242,14 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { } else { return false } - case let .usagePicker(lhsTheme, lhsDateTimeFormat, lhsValue): - if case let .usagePicker(rhsTheme, rhsDateTimeFormat, rhsValue) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsValue == rhsValue { + case let .usagePicker(lhsTheme, lhsDateTimeFormat, lhsValue, lhsEnabled): + if case let .usagePicker(rhsTheme, rhsDateTimeFormat, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsValue == rhsValue, lhsEnabled == rhsEnabled { return true } else { return false } - case let .usageCustomPicker(lhsTheme, lhsValue, lhsFocused, lhsCustomValue): - if case let .usageCustomPicker(rhsTheme, rhsValue, rhsFocused, rhsCustomValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue, lhsFocused == rhsFocused, lhsCustomValue == rhsCustomValue { + case let .usageCustomPicker(lhsTheme, lhsValue, lhsFocused, lhsCustomValue, lhsEnabled): + if case let .usageCustomPicker(rhsTheme, rhsValue, rhsFocused, rhsCustomValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue, lhsFocused == rhsFocused, lhsCustomValue == rhsCustomValue, lhsEnabled == rhsEnabled { return true } else { return false @@ -246,7 +279,7 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { case let .titleHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .title(_, placeholder, value): - return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: value, placeholder: placeholder, maxLength: 32, sectionId: self.section, textUpdated: { value in + return ItemListSingleLineInputItem(context: arguments.context, presentationData: presentationData, title: NSAttributedString(), text: value, placeholder: placeholder, maxLength: 32, sectionId: self.section, textUpdated: { value in arguments.updateState { state in var updatedState = state updatedState.title = value @@ -255,8 +288,41 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { }, action: {}) case let .titleInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) - case let .requestApproval(_, text, value): - return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in + + case let .subscriptionFeeToggle(_, text, value, enabled): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in + arguments.updateState { state in + var updatedState = state + updatedState.subscriptionEnabled = value + if value { + updatedState.requestApproval = false + } else { + updatedState.subscriptionFee = nil + } + return updatedState + } + }) + case let .subscriptionFee(_, placeholder, enabled, value): + let title = NSMutableAttributedString(string: "⭐️", font: Font.semibold(18.0), textColor: .white) + if let range = title.string.range(of: "⭐️") { + title.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: title.string)) + title.addAttribute(.baselineOffset, value: -1.0, range: NSRange(range, in: title.string)) + } + return ItemListSingleLineInputItem(context: arguments.context, presentationData: presentationData, title: title, text: value.flatMap { "\($0)" } ?? "", placeholder: placeholder, type: .number, spacing: 3.0, enabled: enabled, sectionId: self.section, textUpdated: { text in + arguments.updateState { state in + var updatedState = state + if let value = Int64(text) { + updatedState.subscriptionFee = value + } else { + updatedState.subscriptionFee = nil + } + return updatedState + } + }, action: {}) + case let .subscriptionFeeInfo(_, text): + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) + case let .requestApproval(_, text, value, enabled): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in arguments.updateState { state in var updatedState = state updatedState.requestApproval = value @@ -267,8 +333,8 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .timeHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .timePicker(_, value): - return ItemListInviteLinkTimeLimitItem(theme: presentationData.theme, strings: presentationData.strings, value: value, enabled: true, sectionId: self.section, updated: { value in + case let .timePicker(_, value, enabled): + return ItemListInviteLinkTimeLimitItem(theme: presentationData.theme, strings: presentationData.strings, value: value, enabled: enabled, sectionId: self.section, updated: { value in arguments.updateState({ state in var updatedState = state if value != updatedState.time { @@ -279,14 +345,14 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { return updatedState }) }) - case let .timeExpiryDate(theme, dateTimeFormat, value, active): + case let .timeExpiryDate(theme, dateTimeFormat, value, active, enabled): let text: String if let value = value { text = stringForMediumDate(timestamp: value, strings: presentationData.strings, dateTimeFormat: dateTimeFormat) } else { text = presentationData.strings.InviteLink_Create_TimeLimitExpiryDateNever } - return ItemListDisclosureItem(presentationData: presentationData, title: presentationData.strings.InviteLink_Create_TimeLimitExpiryDate, label: text, labelStyle: active ? .coloredText(theme.list.itemAccentColor) : .text, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: presentationData.strings.InviteLink_Create_TimeLimitExpiryDate, enabled: enabled, label: text, labelStyle: active ? .coloredText(theme.list.itemAccentColor) : .text, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: { arguments.dismissInput() arguments.updateState { state in var updatedState = state @@ -298,7 +364,8 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { return updatedState } }) - case let .timeCustomPicker(_, dateTimeFormat, date, displayingDateSelection, displayingTimeSelection): + case let .timeCustomPicker(_, dateTimeFormat, date, displayingDateSelection, displayingTimeSelection, enabled): + let _ = enabled let title = presentationData.strings.InviteLink_Create_TimeLimitExpiryTime return ItemListDatePickerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, date: date, title: title, displayingDateSelection: displayingDateSelection, displayingTimeSelection: displayingTimeSelection, sectionId: self.section, style: .blocks, toggleDateSelection: { arguments.updateState({ state in @@ -329,8 +396,8 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .usageHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .usagePicker(_, dateTimeFormat, value): - return ItemListInviteLinkUsageLimitItem(theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: dateTimeFormat, value: value, enabled: true, sectionId: self.section, updated: { value in + case let .usagePicker(_, dateTimeFormat, value, enabled): + return ItemListInviteLinkUsageLimitItem(theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: dateTimeFormat, value: value, enabled: enabled, sectionId: self.section, updated: { value in arguments.dismissInput() arguments.updateState({ state in var updatedState = state @@ -342,14 +409,14 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { return updatedState }) }) - case let .usageCustomPicker(theme, value, focused, customValue): + case let .usageCustomPicker(theme, value, focused, customValue, enabled): let text: String if let value = value, value != 0 { text = String(value) } else { text = focused ? "" : presentationData.strings.InviteLink_Create_UsersLimitNumberOfUsersUnlimited } - return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: presentationData.strings.InviteLink_Create_UsersLimitNumberOfUsers, textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: "", type: .number, alignment: .right, selectAllOnFocus: true, secondaryStyle: !customValue, tag: InviteLinksEditEntryTag.usage, sectionId: self.section, textUpdated: { updatedText in + return ItemListSingleLineInputItem(context: arguments.context, presentationData: presentationData, title: NSAttributedString(string: presentationData.strings.InviteLink_Create_UsersLimitNumberOfUsers, textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: "", type: .number, alignment: .right, enabled: enabled, selectAllOnFocus: true, secondaryStyle: !customValue, tag: InviteLinksEditEntryTag.usage, sectionId: self.section, textUpdated: { updatedText in arguments.updateState { state in var updatedState = state if updatedText.isEmpty { @@ -398,19 +465,40 @@ private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: entries.append(.title(presentationData.theme, presentationData.strings.InviteLink_Create_LinkName, state.title)) entries.append(.titleInfo(presentationData.theme, presentationData.strings.InviteLink_Create_LinkNameInfo)) - if !isPublic { - entries.append(.requestApproval(presentationData.theme, presentationData.strings.InviteLink_Create_RequestApproval, state.requestApproval)) - var requestApprovalInfoText = presentationData.strings.InviteLink_Create_RequestApprovalOffInfoChannel - if state.requestApproval { - requestApprovalInfoText = isGroup ? presentationData.strings.InviteLink_Create_RequestApprovalOnInfoGroup : presentationData.strings.InviteLink_Create_RequestApprovalOnInfoChannel + let isEditingEnabled = invite?.pricing == nil + let isSubscription = state.subscriptionEnabled + if !isGroup { + //TODO:localize + entries.append(.subscriptionFeeToggle(presentationData.theme, "Require Monthly Fee", state.subscriptionEnabled, isEditingEnabled)) + if state.subscriptionEnabled { + entries.append(.subscriptionFee(presentationData.theme, "Stars amount per month", isEditingEnabled, state.subscriptionFee)) + } + let infoText: String + if let _ = invite, state.subscriptionEnabled { + infoText = "If you need to change the subscription fee, create a new invite link with a different price." } else { - requestApprovalInfoText = isGroup ? presentationData.strings.InviteLink_Create_RequestApprovalOnInfoGroup : presentationData.strings.InviteLink_Create_RequestApprovalOffInfoChannel + infoText = "Charge a subscription fee from people joining your channel via this link. [Learn More >]()" + } + entries.append(.subscriptionFeeInfo(presentationData.theme, infoText)) + } + + if !isPublic { + entries.append(.requestApproval(presentationData.theme, presentationData.strings.InviteLink_Create_RequestApproval, state.requestApproval, isEditingEnabled && !isSubscription)) + var requestApprovalInfoText = presentationData.strings.InviteLink_Create_RequestApprovalOffInfoChannel + if isSubscription { + requestApprovalInfoText = "You can't enable admin approval for links that require a monthly fee." + } else { + if state.requestApproval { + requestApprovalInfoText = isGroup ? presentationData.strings.InviteLink_Create_RequestApprovalOnInfoGroup : presentationData.strings.InviteLink_Create_RequestApprovalOnInfoChannel + } else { + requestApprovalInfoText = isGroup ? presentationData.strings.InviteLink_Create_RequestApprovalOnInfoGroup : presentationData.strings.InviteLink_Create_RequestApprovalOffInfoChannel + } } entries.append(.requestApprovalInfo(presentationData.theme, requestApprovalInfoText)) } entries.append(.timeHeader(presentationData.theme, presentationData.strings.InviteLink_Create_TimeLimit.uppercased())) - entries.append(.timePicker(presentationData.theme, state.time)) + entries.append(.timePicker(presentationData.theme, state.time, isEditingEnabled)) let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) var time: Int32? @@ -419,21 +507,21 @@ private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: } else if let value = state.time.value { time = currentTime + value } - entries.append(.timeExpiryDate(presentationData.theme, presentationData.dateTimeFormat, time, state.pickingExpiryDate || state.pickingExpiryTime)) + entries.append(.timeExpiryDate(presentationData.theme, presentationData.dateTimeFormat, time, state.pickingExpiryDate || state.pickingExpiryTime, isEditingEnabled)) if state.pickingExpiryDate || state.pickingExpiryTime { - entries.append(.timeCustomPicker(presentationData.theme, presentationData.dateTimeFormat, time, state.pickingExpiryDate, state.pickingExpiryTime)) + entries.append(.timeCustomPicker(presentationData.theme, presentationData.dateTimeFormat, time, state.pickingExpiryDate, state.pickingExpiryTime, isEditingEnabled)) } entries.append(.timeInfo(presentationData.theme, presentationData.strings.InviteLink_Create_TimeLimitInfo)) if !state.requestApproval || isPublic { entries.append(.usageHeader(presentationData.theme, presentationData.strings.InviteLink_Create_UsersLimit.uppercased())) - entries.append(.usagePicker(presentationData.theme, presentationData.dateTimeFormat, state.usage)) + entries.append(.usagePicker(presentationData.theme, presentationData.dateTimeFormat, state.usage, isEditingEnabled)) var customValue = false if case .custom = state.usage { customValue = true } - entries.append(.usageCustomPicker(presentationData.theme, state.usage.value, state.pickingUsageLimit, customValue)) + entries.append(.usageCustomPicker(presentationData.theme, state.usage.value, state.pickingUsageLimit, customValue, isEditingEnabled)) entries.append(.usageInfo(presentationData.theme, presentationData.strings.InviteLink_Create_UsersLimitInfo)) } @@ -449,6 +537,8 @@ private struct InviteLinkEditControllerState: Equatable { var usage: InviteLinkUsageLimit var time: InviteLinkTimeLimit var requestApproval = false + var subscriptionEnabled = false + var subscriptionFee: Int64? var pickingExpiryDate = false var pickingExpiryTime = false var pickingUsageLimit = false @@ -460,7 +550,7 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio let actionsDisposable = DisposableSet() let initialState: InviteLinkEditControllerState - if let invite = invite, case let .link(_, title, _, requestApproval, _, _, _, _, expireDate, usageLimit, count, _, _) = invite { + if let invite = invite, case let .link(_, title, _, requestApproval, _, _, _, _, expireDate, usageLimit, count, _, pricing) = invite { var usageLimit = usageLimit if let limit = usageLimit, let count = count, count > 0 { usageLimit = limit - count @@ -478,9 +568,9 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio timeLimit = .unlimited } - initialState = InviteLinkEditControllerState(title: title ?? "", usage: InviteLinkUsageLimit(value: usageLimit), time: timeLimit, requestApproval: requestApproval, pickingExpiryDate: false, pickingExpiryTime: false, pickingUsageLimit: false) + initialState = InviteLinkEditControllerState(title: title ?? "", usage: InviteLinkUsageLimit(value: usageLimit), time: timeLimit, requestApproval: requestApproval, subscriptionEnabled: pricing != nil, subscriptionFee: pricing?.amount, pickingExpiryDate: false, pickingExpiryTime: false, pickingUsageLimit: false) } else { - initialState = InviteLinkEditControllerState(title: "", usage: .unlimited, time: .unlimited, requestApproval: false, pickingExpiryDate: false, pickingExpiryTime: false, pickingUsageLimit: false) + initialState = InviteLinkEditControllerState(title: "", usage: .unlimited, time: .unlimited, requestApproval: false, subscriptionEnabled: false, subscriptionFee: nil, pickingExpiryDate: false, pickingExpiryTime: false, pickingUsageLimit: false) } let statePromise = ValuePromise(initialState, ignoreRepeated: true) @@ -570,14 +660,21 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio dismissImpl?() }) - let rightNavigationButton = ItemListNavigationButton(content: .text(invite == nil ? presentationData.strings.Common_Create : presentationData.strings.Common_Save), style: state.updating ? .activity : .bold, enabled: true, action: { + var doneIsEnabled = true + if state.subscriptionEnabled { + if (state.subscriptionFee ?? 0) == 0 { + doneIsEnabled = false + } + } + + let rightNavigationButton = ItemListNavigationButton(content: .text(invite == nil ? presentationData.strings.Common_Create : presentationData.strings.Common_Save), style: state.updating ? .activity : .bold, enabled: doneIsEnabled, action: { updateState { state in var updatedState = state updatedState.updating = true return updatedState } - let expireDate: Int32? + var expireDate: Int32? if case let .custom(value) = state.time { expireDate = value } else if let value = state.time.value { @@ -589,11 +686,20 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio let titleString = state.title.trimmingCharacters(in: .whitespacesAndNewlines) let title = titleString.isEmpty ? nil : titleString - let usageLimit = state.usage.value - let requestNeeded = state.requestApproval && !isPublic + var usageLimit = state.usage.value + var requestNeeded: Bool? = state.requestApproval && !isPublic if invite == nil { - let _ = (context.engine.peers.createPeerExportedInvitation(peerId: peerId, title: title, expireDate: expireDate, usageLimit: requestNeeded ? 0 : usageLimit, requestNeeded: requestNeeded, subscriptionPricing: nil) + let subscriptionPricing: StarsSubscriptionPricing? + if let subscriptionFee = state.subscriptionFee { + subscriptionPricing = StarsSubscriptionPricing( + period: context.account.testingEnvironment ? StarsSubscriptionPricing.testPeriod : StarsSubscriptionPricing.monthPeriod, + amount: subscriptionFee + ) + } else { + subscriptionPricing = nil + } + let _ = (context.engine.peers.createPeerExportedInvitation(peerId: peerId, title: title, expireDate: expireDate, usageLimit: requestNeeded == true ? 0 : usageLimit, requestNeeded: requestNeeded, subscriptionPricing: subscriptionPricing) |> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic)) |> deliverOnMainQueue).start(next: { invite in completion?(invite) @@ -606,13 +712,24 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio } presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) }) - } else if let initialInvite = invite, case let .link(link, _, _, initialRequestApproval, _, _, _, _, initialExpireDate, initialUsageLimit, _, _, _) = initialInvite { - if initialExpireDate == expireDate && initialUsageLimit == usageLimit && initialRequestApproval == requestNeeded { + } else if let initialInvite = invite, case let .link(link, initialTitle, _, initialRequestApproval, _, _, _, _, initialExpireDate, initialUsageLimit, _, _, _) = initialInvite { + if (initialExpireDate ?? 0) == expireDate && (initialUsageLimit ?? 0) == usageLimit && initialRequestApproval == requestNeeded && (initialTitle ?? "") == title { completion?(initialInvite) dismissImpl?() return } - let _ = (context.engine.peers.editPeerExportedInvitation(peerId: peerId, link: link, title: title, expireDate: expireDate, usageLimit: requestNeeded ? 0 : usageLimit, requestNeeded: requestNeeded) + + if (initialExpireDate ?? 0) == expireDate { + expireDate = nil + } + if (initialUsageLimit ?? 0) == usageLimit { + usageLimit = nil + } + if initialRequestApproval == requestNeeded { + requestNeeded = nil + } + + let _ = (context.engine.peers.editPeerExportedInvitation(peerId: peerId, link: link, title: title, expireDate: expireDate, usageLimit: requestNeeded == true ? 0 : usageLimit, requestNeeded: requestNeeded) |> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic)) |> deliverOnMainQueue).start(next: { invite in completion?(invite) @@ -630,7 +747,7 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio let previousState = previousState.swap(state) var animateChanges = false - if let previousState = previousState, previousState.pickingExpiryDate != state.pickingExpiryDate || previousState.pickingExpiryTime != state.pickingExpiryTime || previousState.requestApproval != state.requestApproval { + if let previousState = previousState, previousState.pickingExpiryDate != state.pickingExpiryDate || previousState.pickingExpiryTime != state.pickingExpiryTime || previousState.requestApproval != state.requestApproval || previousState.subscriptionEnabled != state.subscriptionEnabled { animateChanges = true } diff --git a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift index c59db516a9..1088f93cf0 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift @@ -239,7 +239,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry { arguments.createLink() }) case let .link(_, _, invite, canEdit, _): - return ItemListInviteLinkItem(presentationData: presentationData, invite: invite, share: false, sectionId: self.section, style: .blocks) { invite in + return ItemListInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, share: false, sectionId: self.section, style: .blocks) { invite in arguments.openLink(invite) } contextAction: { invite, node, gesture in arguments.linkContextAction(invite, canEdit, node, gesture) @@ -253,7 +253,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry { arguments.deleteAllRevokedLinks() }) case let .revokedLink(_, _, invite): - return ItemListInviteLinkItem(presentationData: presentationData, invite: invite, share: false, sectionId: self.section, style: .blocks) { invite in + return ItemListInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, share: false, sectionId: self.section, style: .blocks) { invite in arguments.openLink(invite) } contextAction: { invite, node, gesture in arguments.linkContextAction(invite, false, node, gesture) diff --git a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift index b7a4929a4a..803cb4793b 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift @@ -20,6 +20,30 @@ import PresentationDataUtils import DirectionalPanGesture import UndoUI import QrCodeUI +import TextFormat + +private var subscriptionLinkIcon: UIImage? = { + return generateImage(CGSize(width: 40.0, height: 40.0), contextGenerator: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.clear(bounds) + + let pathBounds = CGRect(origin: .zero, size: CGSize(width: 40.0, height: 40.0)) + context.addPath(CGPath(ellipseIn: pathBounds, transform: nil)) + context.clip() + + var locations: [CGFloat] = [1.0, 0.0] + let colors: [CGColor] = [UIColor(rgb: 0x87d93b).cgColor, UIColor(rgb: 0x31b73b).cgColor] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + + if let image = generateTintedImage(image: UIImage(bundleImageName: "Item List/SubscriptionLink"), color: .white), let cgImage = image.cgImage { + context.draw(cgImage, in: pathBounds) + } + }) +}() class InviteLinkViewInteraction { let context: AccountContext @@ -50,6 +74,8 @@ private struct InviteLinkViewTransaction { private enum InviteLinkViewEntryId: Hashable { case link + case subscriptionHeader + case subscriptionPricing case creatorHeader case creator case requestHeader @@ -60,6 +86,8 @@ private enum InviteLinkViewEntryId: Hashable { private enum InviteLinkViewEntry: Comparable, Identifiable { case link(PresentationTheme, ExportedInvitation) + case subscriptionHeader(PresentationTheme, String) + case subscriptionPricing(PresentationTheme, String, String) case creatorHeader(PresentationTheme, String) case creator(PresentationTheme, PresentationDateTimeFormat, EnginePeer, Int32) case requestHeader(PresentationTheme, String, String, Bool) @@ -71,6 +99,10 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { switch self { case .link: return .link + case .subscriptionHeader: + return .subscriptionHeader + case .subscriptionPricing: + return .subscriptionPricing case .creatorHeader: return .creatorHeader case .creator: @@ -94,6 +126,18 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { } else { return false } + case let .subscriptionHeader(lhsTheme, lhsTitle): + if case let .subscriptionHeader(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle { + return true + } else { + return false + } + case let .subscriptionPricing(lhsTheme, lhsTitle, lhsSubtitle): + if case let .subscriptionPricing(rhsTheme, rhsTitle, rhsSubtitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle { + return true + } else { + return false + } case let .creatorHeader(lhsTheme, lhsTitle): if case let .creatorHeader(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle { return true @@ -139,33 +183,47 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { switch rhs { case .link: return false + case .subscriptionHeader, .subscriptionPricing, .creatorHeader, .creator, .requestHeader, .request, .importerHeader, .importer: + return true + } + case .subscriptionHeader: + switch rhs { + case .link, .subscriptionHeader: + return false + case .subscriptionPricing, .creatorHeader, .creator, .requestHeader, .request, .importerHeader, .importer: + return true + } + case .subscriptionPricing: + switch rhs { + case .link, .subscriptionHeader, .subscriptionPricing: + return false case .creatorHeader, .creator, .requestHeader, .request, .importerHeader, .importer: return true } case .creatorHeader: switch rhs { - case .link, .creatorHeader: + case .link, .subscriptionHeader, .subscriptionPricing, .creatorHeader: return false case .creator, .requestHeader, .request, .importerHeader, .importer: return true } case .creator: switch rhs { - case .link, .creatorHeader, .creator: + case .link, .subscriptionHeader, .subscriptionPricing, .creatorHeader, .creator: return false case .requestHeader, .request, .importerHeader, .importer: return true } case .requestHeader: switch rhs { - case .link, .creatorHeader, .creator, .requestHeader: + case .link, .subscriptionHeader, .subscriptionPricing, .creatorHeader, .creator, .requestHeader: return false case .request, .importerHeader, .importer: return true } case let .request(lhsIndex, _, _, _, _, _): switch rhs { - case .link, .creatorHeader, .creator, .requestHeader: + case .link, .subscriptionHeader, .subscriptionPricing, .creatorHeader, .creator, .requestHeader: return false case let .request(rhsIndex, _, _, _, _, _): return lhsIndex < rhsIndex @@ -174,14 +232,14 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { } case .importerHeader: switch rhs { - case .link, .creatorHeader, .creator, .requestHeader, .request, .importerHeader: + case .link, .subscriptionHeader, .subscriptionPricing, .creatorHeader, .creator, .requestHeader, .request, .importerHeader: return false case .importer: return true } case let .importer(lhsIndex, _, _, _, _, _, _): switch rhs { - case .link, .creatorHeader, .creator, .importerHeader, .request, .requestHeader: + case .link, .subscriptionHeader, .subscriptionPricing, .creatorHeader, .creator, .importerHeader, .request, .requestHeader: return false case let .importer(rhsIndex, _, _, _, _, _, _): return lhsIndex < rhsIndex @@ -204,13 +262,22 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { interaction.contextAction(invite, node, gesture) }, viewAction: { }) + case let .subscriptionHeader(_, title): + return SectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title) + case let .subscriptionPricing(_, title, subtitle): + let attributedTitle = NSMutableAttributedString(string: title, font: Font.semibold(presentationData.listsFontSize.itemListBaseFontSize), textColor: presentationData.theme.list.itemPrimaryTextColor) + if let range = attributedTitle.string.range(of: "⭐️") { + attributedTitle.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: attributedTitle.string)) + attributedTitle.addAttribute(.baselineOffset, value: -1.0, range: NSRange(range, in: attributedTitle.string)) + } + return ItemListDisclosureItem(presentationData: ItemListPresentationData(presentationData), icon: subscriptionLinkIcon, context: interaction.context, title: "", attributedTitle: attributedTitle, enabled: false, label: subtitle, labelStyle: .detailText, sectionId: 0, style: .plain, disclosureStyle: .none, noInsets: true, action: nil, clearHighlightAutomatically: true, tag: nil, shimmeringIndex: nil) case let .creatorHeader(_, title): return SectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title) case let .creator(_, dateTimeFormat, peer, date): let dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat) - return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .generic, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: peer.id != account.peerId, sectionId: 0, action: { + return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .peerList, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: peer.id != account.peerId, sectionId: 0, action: { interaction.openPeer(peer.id) - }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, tag: nil) + }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, style: .plain, tag: nil) case let .importerHeader(_, title, subtitle, expired), let .requestHeader(_, title, subtitle, expired): let additionalText: SectionHeaderAdditionalText if !subtitle.isEmpty { @@ -230,14 +297,14 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { } else { dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat) } - return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .generic, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: peer.id != account.peerId, sectionId: 0, action: { + return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .peerList, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: peer.id != account.peerId, sectionId: 0, action: { interaction.openPeer(peer.id) - }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, tag: nil, shimmering: loading ? ItemListPeerItemShimmering(alternationIndex: 0) : nil) + }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, style: .plain, tag: nil, shimmering: loading ? ItemListPeerItemShimmering(alternationIndex: 0) : nil) case let .request(_, _, dateTimeFormat, peer, date, loading): let dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat) - return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .generic, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: peer.id != account.peerId, sectionId: 0, action: { + return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .peerList, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: peer.id != account.peerId, sectionId: 0, action: { interaction.openPeer(peer.id) - }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, tag: nil, shimmering: loading ? ItemListPeerItemShimmering(alternationIndex: 0) : nil) + }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, style: .plain, tag: nil, shimmering: loading ? ItemListPeerItemShimmering(alternationIndex: 0) : nil) } } } @@ -727,6 +794,13 @@ public final class InviteLinkViewController: ViewController { var entries: [InviteLinkViewEntry] = [] entries.append(.link(presentationData.theme, invite)) + + if let pricing = invite.pricing { + //TODO:localize + entries.append(.subscriptionHeader(presentationData.theme, "SUBSCRIPTION FEE")) + entries.append(.subscriptionPricing(presentationData.theme, "⭐️\(pricing.amount) / month x \(state.count)", "You get approximately $\(Float(pricing.amount * Int64(state.count)) * 0.01) monthly")) + } + entries.append(.creatorHeader(presentationData.theme, presentationData.strings.InviteLink_CreatedBy.uppercased())) entries.append(.creator(presentationData.theme, presentationData.dateTimeFormat, EnginePeer(creatorPeer), date)) diff --git a/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift b/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift index acf81eea96..32ee4b6cc2 100644 --- a/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift @@ -7,6 +7,9 @@ import TelegramPresentationData import ItemListUI import ShimmerEffect import TelegramCore +import TextNodeWithEntities +import AccountContext +import TextFormat func invitationAvailability(_ invite: ExportedInvitation) -> CGFloat { if case let .link(_, _, _, _, isRevoked, _, date, startDate, expireDate, usageLimit, count, _, _) = invite { @@ -54,6 +57,7 @@ private enum ItemBackgroundColor: Equatable { } public class ItemListInviteLinkItem: ListViewItem, ItemListItem { + let context: AccountContext let presentationData: ItemListPresentationData let invite: ExportedInvitation? let share: Bool @@ -64,6 +68,7 @@ public class ItemListInviteLinkItem: ListViewItem, ItemListItem { public let tag: ItemListItemTag? public init( + context: AccountContext, presentationData: ItemListPresentationData, invite: ExportedInvitation?, share: Bool, @@ -73,6 +78,7 @@ public class ItemListInviteLinkItem: ListViewItem, ItemListItem { contextAction: ((ExportedInvitation, ASDisplayNode, ContextGesture?) -> Void)?, tag: ItemListItemTag? = nil ) { + self.context = context self.presentationData = presentationData self.invite = invite self.share = share @@ -170,6 +176,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { private let titleNode: TextNode private let subtitleNode: TextNode + private let pricingNode: TextNodeWithEntities private var placeholderNode: ShimmerEffectNode? private var absoluteLocation: (CGRect, CGSize)? @@ -218,6 +225,8 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { self.subtitleNode.isUserInteractionEnabled = false self.subtitleNode.contentMode = .left self.subtitleNode.contentsScale = UIScreen.main.scale + + self.pricingNode = TextNodeWithEntities() self.highlightedBackgroundNode = ASDisplayNode() self.highlightedBackgroundNode.isLayerBacked = true @@ -237,6 +246,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { self.offsetContainerNode.addSubnode(self.iconNode) self.offsetContainerNode.addSubnode(self.titleNode) self.offsetContainerNode.addSubnode(self.subtitleNode) + self.offsetContainerNode.addSubnode(self.pricingNode.textNode) self.containerNode.activated = { [weak self] gesture, _ in guard let strongSelf = self, let item = strongSelf.layoutParams?.0, let invite = item.invite, let contextAction = item.contextAction else { @@ -266,6 +276,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { self?.extractedBackgroundImageNode.image = nil } }) + transition.updateAlpha(node: strongSelf.pricingNode.textNode, alpha: isExtracted ? 0.0 : 1.0) } } @@ -280,6 +291,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { public func asyncLayout() -> (_ item: ItemListInviteLinkItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors, _ firstWithHeader: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) + let makePricingLayout = TextNodeWithEntities.asyncLayout(self.pricingNode) let currentItem = self.layoutParams?.0 @@ -299,14 +311,19 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { let color: ItemBackgroundColor let nextColor: ItemBackgroundColor let transitionFraction: CGFloat - if let invite = item.invite, case let .link(_, _, _, _, isRevoked, _, _, _, expireDate, usageLimit, _, _, _) = invite { + if let invite = item.invite, case let .link(_, _, _, _, isRevoked, _, _, _, expireDate, usageLimit, _, _, pricing) = invite { if isRevoked { color = .gray nextColor = .gray transitionFraction = 0.0 } else if expireDate == nil && usageLimit == nil { - color = .blue - nextColor = .blue + if let _ = pricing { + color = .green + nextColor = .green + } else { + color = .blue + nextColor = .blue + } transitionFraction = 0.0 } else if availability >= 0.5 { color = .green @@ -343,10 +360,10 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { let inviteLink = item.invite?.link?.replacingOccurrences(of: "https://", with: "") ?? "" var titleText = inviteLink var subtitleText: String = "" + var pricingAttributedText: NSMutableAttributedString? var timerValue: TimerNode.Value? - - if let invite = item.invite, case let .link(_, title, _, _, _, _, date, startDate, expireDate, usageLimit, count, requestedCount, _) = invite { + if let invite = item.invite, case let .link(_, title, _, _, _, _, date, startDate, expireDate, usageLimit, count, requestedCount, subscriptionPricing) = invite { if let title = title, !title.isEmpty { titleText = title } @@ -375,6 +392,19 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { subtitleText += item.presentationData.strings.MemberRequests_PeopleRequestedShort(requestedCount) } + if let subscriptionPricing { + //TODO:localize + let text = NSMutableAttributedString() + text.append(NSAttributedString(string: "⭐️\(subscriptionPricing.amount)\n", font: Font.semibold(17.0), textColor: item.presentationData.theme.list.itemPrimaryTextColor)) + text.append(NSAttributedString(string: "per month", font: Font.regular(13.0), textColor: item.presentationData.theme.list.itemSecondaryTextColor)) + if let range = text.string.range(of: "⭐️") { + text.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: text.string)) + text.addAttribute(NSAttributedString.Key.font, value: Font.semibold(15.0), range: NSRange(range, in: text.string)) + text.addAttribute(.baselineOffset, value: 2.5, range: NSRange(range, in: text.string)) + } + pricingAttributedText = text + } + if invite.isRevoked { if !subtitleText.isEmpty { subtitleText += " • " @@ -443,6 +473,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (pricingLayout, pricingApply) = makePricingLayout(TextNodeLayoutArguments(attributedString: pricingAttributedText, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .right, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) let titleSpacing: CGFloat = 1.0 @@ -505,13 +536,18 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { strongSelf.backgroundNode.backgroundColor = itemBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor - strongSelf.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor) + if let _ = item.invite?.pricing { + strongSelf.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Item List/SubscriptionLink"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor) + } else { + strongSelf.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Item List/InviteLink"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor) + } } let transition = ContainedViewLayoutTransition.immediate let _ = titleApply() let _ = subtitleApply() + let _ = pricingApply(TextNodeWithEntities.Arguments(context: item.context, cache: item.context.animationCache, renderer: item.context.animationRenderer, placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, attemptSynchronous: false)) switch item.style { case .plain: @@ -607,6 +643,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size)) transition.updateFrame(node: strongSelf.subtitleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: verticalInset + titleLayout.size.height + titleSpacing), size: subtitleLayout.size)) + transition.updateFrame(node: strongSelf.pricingNode.textNode, frame: CGRect(origin: CGPoint(x: layout.contentSize.width - rightInset - pricingLayout.size.width, y: floorToScreenPixels((layout.contentSize.height - pricingLayout.size.height) / 2.0)), size: pricingLayout.size)) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentSize.height + UIScreenPixel + UIScreenPixel)) diff --git a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift index d830858c1f..cd8408de51 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift @@ -8,6 +8,7 @@ import ShimmerEffect import AvatarNode import TelegramCore import AccountContext +import TextNodeWithEntities private let avatarFont = avatarPlaceholderFont(size: 16.0) @@ -64,12 +65,13 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem { public let sectionId: ItemListSectionId let style: ItemListStyle let disclosureStyle: ItemListDisclosureStyle + let noInsets: Bool let action: (() -> Void)? let clearHighlightAutomatically: Bool public let tag: ItemListItemTag? public let shimmeringIndex: Int? - public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, context: AccountContext? = nil, iconPeer: EnginePeer? = nil, title: String, attributedTitle: NSAttributedString? = nil, enabled: Bool = true, titleColor: ItemListDisclosureItemTitleColor = .primary, titleFont: ItemListDisclosureItemTitleFont = .regular, titleIcon: UIImage? = nil, label: String, attributedLabel: NSAttributedString? = nil, labelStyle: ItemListDisclosureLabelStyle = .text, additionalDetailLabel: String? = nil, additionalDetailLabelColor: ItemListDisclosureItemDetailLabelColor = .generic, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, action: (() -> Void)?, clearHighlightAutomatically: Bool = true, tag: ItemListItemTag? = nil, shimmeringIndex: Int? = nil) { + public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, context: AccountContext? = nil, iconPeer: EnginePeer? = nil, title: String, attributedTitle: NSAttributedString? = nil, enabled: Bool = true, titleColor: ItemListDisclosureItemTitleColor = .primary, titleFont: ItemListDisclosureItemTitleFont = .regular, titleIcon: UIImage? = nil, label: String, attributedLabel: NSAttributedString? = nil, labelStyle: ItemListDisclosureLabelStyle = .text, additionalDetailLabel: String? = nil, additionalDetailLabelColor: ItemListDisclosureItemDetailLabelColor = .generic, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, noInsets: Bool = false, action: (() -> Void)?, clearHighlightAutomatically: Bool = true, tag: ItemListItemTag? = nil, shimmeringIndex: Int? = nil) { self.presentationData = presentationData self.icon = icon self.context = context @@ -88,6 +90,7 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem { self.sectionId = sectionId self.style = style self.disclosureStyle = disclosureStyle + self.noInsets = noInsets self.action = action self.clearHighlightAutomatically = clearHighlightAutomatically self.tag = tag @@ -151,7 +154,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { var avatarNode: AvatarNode? let iconNode: ASImageNode - let titleNode: TextNode + let titleNode: TextNodeWithEntities let titleIconNode: ASImageNode public let labelNode: TextNode var additionalDetailLabelNode: TextNode? @@ -196,8 +199,8 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { self.iconNode.isLayerBacked = true self.iconNode.displaysAsynchronously = false - self.titleNode = TextNode() - self.titleNode.isUserInteractionEnabled = false + self.titleNode = TextNodeWithEntities() + self.titleNode.textNode.isUserInteractionEnabled = false self.titleIconNode = ASImageNode() self.titleIconNode.displayWithoutProcessing = true @@ -224,7 +227,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { super.init(layerBacked: false, dynamicBounce: false) - self.addSubnode(self.titleNode) + self.addSubnode(self.titleNode.textNode) self.addSubnode(self.labelNode) self.addSubnode(self.arrowNode) @@ -252,7 +255,8 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { } public func asyncLayout() -> (_ item: ItemListDisclosureItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { - let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeTitleLayout = TextNode.asyncLayout(self.titleNode.textNode) + let makeTitleWithEntitiesLayout = TextNodeWithEntities.asyncLayout(self.titleNode) let makeLabelLayout = TextNode.asyncLayout(self.labelNode) let makeAdditionalDetailLabelLayout = TextNode.asyncLayout(self.additionalDetailLabelNode) @@ -329,14 +333,14 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { } let contentSize: CGSize - let insets: UIEdgeInsets + var insets: UIEdgeInsets let separatorHeight = UIScreenPixel let itemBackgroundColor: UIColor let itemSeparatorColor: UIColor var leftInset = 16.0 + params.leftInset if item.icon != nil { - leftInset += 43.0 + leftInset += item.noInsets ? 49.0 : 43.0 } else if item.iconPeer != nil { leftInset += 46.0 } @@ -370,7 +374,11 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { maxTitleWidth -= 12.0 } - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: item.attributedTitle ?? NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: item.attributedTitle != nil ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let titleArguments = TextNodeLayoutArguments(attributedString: item.attributedTitle ?? NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: item.attributedTitle != nil ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()) + let (titleLayoutAndApply) = item.context == nil ? makeTitleLayout(titleArguments) : nil + let (titleWithEntitiesLayoutAndApply) = item.context != nil ? makeTitleWithEntitiesLayout(titleArguments) : nil + + let titleLayout: TextNodeLayout = (titleWithEntitiesLayoutAndApply?.0 ?? titleLayoutAndApply?.0)! let detailFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) @@ -455,6 +463,10 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor contentSize = CGSize(width: params.width, height: height) insets = itemListNeighborsPlainInsets(neighbors) + if item.noInsets { + insets.top = 0.0 + insets.bottom = 0.0 + } case .blocks: itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor @@ -531,8 +543,21 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { strongSelf.backgroundNode.backgroundColor = itemBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor } + + if let titleWithEntitiesApply = titleWithEntitiesLayoutAndApply?.1, let context = item.context { + let _ = titleWithEntitiesApply( + TextNodeWithEntities.Arguments( + context: context, + cache: context.animationCache, + renderer: context.animationRenderer, + placeholderColor: item.presentationData.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12), + attemptSynchronous: false + ) + ) + } else if let titleApply = titleLayoutAndApply?.1 { + let _ = titleApply() + } - let _ = titleApply() let _ = labelApply() switch item.style { @@ -607,7 +632,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { } let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - centralContentHeight) / 2.0)), size: titleLayout.size) - strongSelf.titleNode.frame = titleFrame + strongSelf.titleNode.textNode.frame = titleFrame if let updateBadgeImage = updatedLabelBadgeImage { if strongSelf.labelBadgeNode.supernode == nil { @@ -746,7 +771,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { let titleLineWidth: CGFloat = (shimmeringIndex % 2 == 0) ? 120.0 : 80.0 let lineDiameter: CGFloat = 8.0 - let titleFrame = strongSelf.titleNode.frame + let titleFrame = strongSelf.titleNode.textNode.frame shapes.append(.roundedRectLine(startPoint: CGPoint(x: titleFrame.minX, y: titleFrame.minY + floor((titleFrame.height - lineDiameter) / 2.0)), width: titleLineWidth, diameter: lineDiameter)) shimmerNode.update(backgroundColor: item.presentationData.theme.list.itemBlocksBackgroundColor, foregroundColor: item.presentationData.theme.list.mediaPlaceholderColor, shimmeringColor: item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: contentSize) diff --git a/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift b/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift index 5e5fcb35f6..dfbc841be4 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift @@ -4,6 +4,8 @@ import Display import AsyncDisplayKit import SwiftSignalKit import TelegramPresentationData +import TextNodeWithEntities +import AccountContext private let validIdentifierSet: CharacterSet = { var set = CharacterSet(charactersIn: "a".unicodeScalars.first! ... "z".unicodeScalars.first!) @@ -43,6 +45,7 @@ public enum ItemListSingleLineInputAlignment { } public class ItemListSingleLineInputItem: ListViewItem, ItemListItem { + let context: AccountContext? let presentationData: ItemListPresentationData let title: NSAttributedString let text: String @@ -65,7 +68,8 @@ public class ItemListSingleLineInputItem: ListViewItem, ItemListItem { let cleared: (() -> Void)? public let tag: ItemListItemTag? - public init(presentationData: ItemListPresentationData, title: NSAttributedString, text: String, placeholder: String, type: ItemListSingleLineInputItemType = .regular(capitalization: true, autocorrection: true), returnKeyType: UIReturnKeyType = .`default`, alignment: ItemListSingleLineInputAlignment = .default, spacing: CGFloat = 0.0, clearType: ItemListSingleLineInputClearType = .none, maxLength: Int = 0, enabled: Bool = true, selectAllOnFocus: Bool = false, secondaryStyle: Bool = false, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> String)? = nil, updatedFocus: ((Bool) -> Void)? = nil, action: @escaping () -> Void, cleared: (() -> Void)? = nil) { + public init(context: AccountContext? = nil, presentationData: ItemListPresentationData, title: NSAttributedString, text: String, placeholder: String, type: ItemListSingleLineInputItemType = .regular(capitalization: true, autocorrection: true), returnKeyType: UIReturnKeyType = .`default`, alignment: ItemListSingleLineInputAlignment = .default, spacing: CGFloat = 0.0, clearType: ItemListSingleLineInputClearType = .none, maxLength: Int = 0, enabled: Bool = true, selectAllOnFocus: Bool = false, secondaryStyle: Bool = false, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> String)? = nil, updatedFocus: ((Bool) -> Void)? = nil, action: @escaping () -> Void, cleared: (() -> Void)? = nil) { + self.context = context self.presentationData = presentationData self.title = title self.text = text @@ -130,7 +134,7 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg private let bottomStripeNode: ASDisplayNode private let maskNode: ASImageNode - private let titleNode: TextNode + private let titleNode: TextNodeWithEntities private let measureTitleSizeNode: TextNode private let textNode: TextFieldNode private let clearIconNode: ASImageNode @@ -154,7 +158,7 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg self.maskNode = ASImageNode() - self.titleNode = TextNode() + self.titleNode = TextNodeWithEntities() self.measureTitleSizeNode = TextNode() self.textNode = TextFieldNode() @@ -167,7 +171,7 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg super.init(layerBacked: false, dynamicBounce: false) - self.addSubnode(self.titleNode) + self.addSubnode(self.titleNode.textNode) self.addSubnode(self.textNode) self.addSubnode(self.clearIconNode) self.addSubnode(self.clearButtonNode) @@ -209,7 +213,8 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg } public func asyncLayout() -> (_ item: ItemListSingleLineInputItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { - let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeTitleLayout = TextNode.asyncLayout(self.titleNode.textNode) + let makeTitleWithEntitiesLayout = TextNodeWithEntities.asyncLayout(self.titleNode) let makeMeasureTitleSizeLayout = TextNode.asyncLayout(self.measureTitleSizeNode) let currentItem = self.item @@ -241,15 +246,22 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg } let titleString = NSMutableAttributedString(attributedString: item.title) - titleString.removeAttribute(NSAttributedString.Key.font, range: NSMakeRange(0, titleString.length)) + if !item.title.string.isSingleEmoji { + titleString.removeAttribute(NSAttributedString.Key.font, range: NSMakeRange(0, titleString.length)) + } titleString.addAttributes([NSAttributedString.Key.font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize)], range: NSMakeRange(0, titleString.length)) - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 32.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let titleArguments = TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 32.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()) + + let (titleLayoutAndApply) = item.context == nil ? makeTitleLayout(titleArguments) : nil + let (titleWithEntitiesLayoutAndApply) = item.context != nil ? makeTitleWithEntitiesLayout(titleArguments) : nil + + let titleLayout: TextNodeLayout = (titleWithEntitiesLayoutAndApply?.0 ?? titleLayoutAndApply?.0)! let (measureTitleLayout, measureTitleSizeApply) = makeMeasureTitleSizeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "A", font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize)), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 32.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let separatorHeight = UIScreenPixel - + let contentSize = CGSize(width: params.width, height: max(titleLayout.size.height, measureTitleLayout.size.height) + 22.0) let insets = itemListNeighborsGroupedInsets(neighbors, params) @@ -280,8 +292,20 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg strongSelf.textNode.textField.textColor = item.secondaryStyle ? item.presentationData.theme.list.itemSecondaryTextColor : item.presentationData.theme.list.itemPrimaryTextColor } - let _ = titleApply() - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((layout.contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) + if let titleWithEntitiesApply = titleWithEntitiesLayoutAndApply?.1, let context = item.context { + let _ = titleWithEntitiesApply( + TextNodeWithEntities.Arguments( + context: context, + cache: context.animationCache, + renderer: context.animationRenderer, + placeholderColor: item.presentationData.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12), + attemptSynchronous: false + ) + ) + } else if let titleApply = titleLayoutAndApply?.1 { + let _ = titleApply() + } + strongSelf.titleNode.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((layout.contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) let _ = measureTitleSizeApply() diff --git a/submodules/PasswordSetupUI/Sources/ResetPasswordController.swift b/submodules/PasswordSetupUI/Sources/ResetPasswordController.swift index f42594e4f0..fd12587163 100644 --- a/submodules/PasswordSetupUI/Sources/ResetPasswordController.swift +++ b/submodules/PasswordSetupUI/Sources/ResetPasswordController.swift @@ -11,10 +11,12 @@ import AlertUI import PresentationDataUtils private final class ResetPasswordControllerArguments { + let context: AccountContext let updateCodeText: (String) -> Void let openHelp: () -> Void - init(updateCodeText: @escaping (String) -> Void, openHelp: @escaping () -> Void) { + init(context: AccountContext, updateCodeText: @escaping (String) -> Void, openHelp: @escaping () -> Void) { + self.context = context self.updateCodeText = updateCodeText self.openHelp = openHelp } @@ -128,7 +130,7 @@ public func resetPasswordController(context: AccountContext, emailPattern: Strin let saveDisposable = MetaDisposable() actionsDisposable.add(saveDisposable) - let arguments = ResetPasswordControllerArguments(updateCodeText: { updatedText in + let arguments = ResetPasswordControllerArguments(context: context, updateCodeText: { updatedText in updateState { state in var state = state state.code = updatedText diff --git a/submodules/SettingsUI/Sources/ChangePhoneNumberCodeController.swift b/submodules/SettingsUI/Sources/ChangePhoneNumberCodeController.swift index 59c2896d0c..199624f520 100644 --- a/submodules/SettingsUI/Sources/ChangePhoneNumberCodeController.swift +++ b/submodules/SettingsUI/Sources/ChangePhoneNumberCodeController.swift @@ -15,10 +15,12 @@ import AuthorizationUtils import PhoneNumberFormat private final class ChangePhoneNumberCodeControllerArguments { + let context: AccountContext let updateEntryText: (String) -> Void let next: () -> Void - init(updateEntryText: @escaping (String) -> Void, next: @escaping () -> Void) { + init(context: AccountContext, updateEntryText: @escaping (String) -> Void, next: @escaping () -> Void) { + self.context = context self.updateEntryText = updateEntryText self.next = next } @@ -290,7 +292,7 @@ func changePhoneNumberCodeController(context: AccountContext, phoneNumber: Strin } } - let arguments = ChangePhoneNumberCodeControllerArguments(updateEntryText: { updatedText in + let arguments = ChangePhoneNumberCodeControllerArguments(context: context, updateEntryText: { updatedText in var initiateCheck = false updateState { state in if state.codeText.count < 5 && updatedText.count == 5 { diff --git a/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift index 67ddd69b04..aa3aa8cd7c 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift @@ -26,7 +26,7 @@ private func shareLink(for server: ProxyServerSettings) -> String { return link } -private final class proxyServerSettingsControllerArguments { +private final class ProxyServerSettingsControllerArguments { let updateState: ((ProxyServerSettingsControllerState) -> ProxyServerSettingsControllerState) -> Void let share: () -> Void let usePasteboardSettings: () -> Void @@ -113,7 +113,7 @@ private enum ProxySettingsEntry: ItemListNodeEntry { } func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { - let arguments = arguments as! proxyServerSettingsControllerArguments + let arguments = arguments as! ProxyServerSettingsControllerArguments switch self { case let .usePasteboardSettings(_, title): return ItemListActionItem(presentationData: presentationData, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { @@ -158,7 +158,7 @@ private enum ProxySettingsEntry: ItemListNodeEntry { case let .credentialsHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .credentialsUsername(_, _, placeholder, text): - return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: text, placeholder: placeholder, sectionId: self.section, textUpdated: { value in + return ItemListSingleLineInputItem(context: nil, presentationData: presentationData, title: NSAttributedString(), text: text, placeholder: placeholder, sectionId: self.section, textUpdated: { value in arguments.updateState { current in var state = current state.username = value @@ -306,7 +306,7 @@ func proxyServerSettingsController(sharedContext: SharedAccountContext, context: var shareImpl: (() -> Void)? - let arguments = proxyServerSettingsControllerArguments(updateState: { f in + let arguments = ProxyServerSettingsControllerArguments(updateState: { f in updateState(f) }, share: { shareImpl?() diff --git a/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift b/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift index 67bb0ba0e3..e085ebf7e6 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift @@ -18,12 +18,14 @@ private enum CreatePasswordField { } private final class CreatePasswordControllerArguments { + let context: AccountContext let updateFieldText: (CreatePasswordField, String) -> Void let selectNextInputItem: (CreatePasswordEntryTag) -> Void let save: () -> Void let cancelEmailConfirmation: () -> Void - init(updateFieldText: @escaping (CreatePasswordField, String) -> Void, selectNextInputItem: @escaping (CreatePasswordEntryTag) -> Void, save: @escaping () -> Void, cancelEmailConfirmation: @escaping () -> Void) { + init(context: AccountContext, updateFieldText: @escaping (CreatePasswordField, String) -> Void, selectNextInputItem: @escaping (CreatePasswordEntryTag) -> Void, save: @escaping () -> Void, cancelEmailConfirmation: @escaping () -> Void) { + self.context = context self.updateFieldText = updateFieldText self.selectNextInputItem = selectNextInputItem self.save = save @@ -321,7 +323,7 @@ func createPasswordController(context: AccountContext, createPasswordContext: Cr } } - let arguments = CreatePasswordControllerArguments(updateFieldText: { field, updatedText in + let arguments = CreatePasswordControllerArguments(context: context, updateFieldText: { field, updatedText in updateState { state in var state = state switch field { diff --git a/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift b/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift index ac7aa8d89e..f8efe84b9d 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift @@ -16,6 +16,7 @@ import PasswordSetupUI import Markdown private final class TwoStepVerificationUnlockSettingsControllerArguments { + let context: AccountContext let updatePasswordText: (String) -> Void let checkPassword: () -> Void let openForgotPassword: () -> Void @@ -28,7 +29,8 @@ private final class TwoStepVerificationUnlockSettingsControllerArguments { let declinePasswordReset: () -> Void let resetPassword: () -> Void - init(updatePasswordText: @escaping (String) -> Void, checkPassword: @escaping () -> Void, openForgotPassword: @escaping () -> Void, openSetupPassword: @escaping () -> Void, openDisablePassword: @escaping () -> Void, openSetupEmail: @escaping () -> Void, openResetPendingEmail: @escaping () -> Void, updateEmailCode: @escaping (String) -> Void, openConfirmEmail: @escaping () -> Void, declinePasswordReset: @escaping () -> Void, resetPassword: @escaping () -> Void) { + init(context: AccountContext, updatePasswordText: @escaping (String) -> Void, checkPassword: @escaping () -> Void, openForgotPassword: @escaping () -> Void, openSetupPassword: @escaping () -> Void, openDisablePassword: @escaping () -> Void, openSetupEmail: @escaping () -> Void, openResetPendingEmail: @escaping () -> Void, updateEmailCode: @escaping (String) -> Void, openConfirmEmail: @escaping () -> Void, declinePasswordReset: @escaping () -> Void, resetPassword: @escaping () -> Void) { + self.context = context self.updatePasswordText = updatePasswordText self.checkPassword = checkPassword self.openForgotPassword = openForgotPassword @@ -423,7 +425,7 @@ public func twoStepVerificationUnlockSettingsController(context: AccountContext, }) } - let arguments = TwoStepVerificationUnlockSettingsControllerArguments(updatePasswordText: { updatedText in + let arguments = TwoStepVerificationUnlockSettingsControllerArguments(context: context, updatePasswordText: { updatedText in updateState { state in var state = state state.passwordText = updatedText diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index 9bbff0d1df..9ca64a5e9e 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 185 + return 186 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index 7fd173a0f2..2a7d56c02d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -341,7 +341,7 @@ private final class StarsContextImpl { return } var transactions = state.transactions - transactions.insert(.init(flags: [.isLocal], id: "\(arc4random())", count: balance, date: Int32(Date().timeIntervalSince1970), peer: .appStore, title: nil, description: nil, photo: nil, transactionDate: nil, transactionUrl: nil, paidMessageId: nil, media: []), at: 0) + transactions.insert(.init(flags: [.isLocal], id: "\(arc4random())", count: balance, date: Int32(Date().timeIntervalSince1970), peer: .appStore, title: nil, description: nil, photo: nil, transactionDate: nil, transactionUrl: nil, paidMessageId: nil, media: [], subscriptionPeriod: nil), at: 0) self.updateState(StarsContext.State(flags: [.isPendingBalance], balance: state.balance + balance, subscriptions: state.subscriptions, canLoadMoreSubscriptions: state.canLoadMoreSubscriptions, transactions: transactions, canLoadMoreTransactions: state.canLoadMoreTransactions, isLoading: state.isLoading)) } @@ -408,7 +408,7 @@ private extension StarsContext.State.Transaction { let media = extendedMedia.flatMap({ $0.compactMap { textMediaAndExpirationTimerFromApiMedia($0, PeerId(0)).media } }) ?? [] let _ = subscriptionPeriod - self.init(flags: flags, id: id, count: stars, date: date, peer: parsedPeer, title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), transactionDate: transactionDate, transactionUrl: transactionUrl, paidMessageId: paidMessageId, media: media) + self.init(flags: flags, id: id, count: stars, date: date, peer: parsedPeer, title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), transactionDate: transactionDate, transactionUrl: transactionUrl, paidMessageId: paidMessageId, media: media, subscriptionPeriod: subscriptionPeriod) } } } @@ -474,6 +474,7 @@ public final class StarsContext { public let transactionUrl: String? public let paidMessageId: MessageId? public let media: [Media] + public let subscriptionPeriod: Int32? public init( flags: Flags, @@ -487,7 +488,8 @@ public final class StarsContext { transactionDate: Int32?, transactionUrl: String?, paidMessageId: MessageId?, - media: [Media] + media: [Media], + subscriptionPeriod: Int32? ) { self.flags = flags self.id = id @@ -501,6 +503,7 @@ public final class StarsContext { self.transactionUrl = transactionUrl self.paidMessageId = paidMessageId self.media = media + self.subscriptionPeriod = subscriptionPeriod } public static func == (lhs: Transaction, rhs: Transaction) -> Bool { @@ -540,6 +543,9 @@ public final class StarsContext { if !areMediaArraysEqual(lhs.media, rhs.media) { return false } + if lhs.subscriptionPeriod != rhs.subscriptionPeriod { + return false + } return true } } @@ -1160,8 +1166,8 @@ public struct StarsSubscriptionPricing: Codable, Equatable { try container.encode(self.amount, forKey: .amount) } - public static let monthPeriod = 2592000 - public static let testPeriod = 300 + public static let monthPeriod: Int32 = 2592000 + public static let testPeriod: Int32 = 300 } extension StarsSubscriptionPricing { diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index ee53a897b4..9702ce0582 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -203,7 +203,27 @@ private final class StarsTransactionSheetContent: CombinedComponent { var delayedCloseOnOpenPeer = true switch subject { case let .transaction(transaction, parentPeer): - if transaction.flags.contains(.isGift) { + if let _ = transaction.subscriptionPeriod { + //TODO:localize + titleText = "Monthly Subscription Fee" + descriptionText = "" + count = transaction.count + countOnTop = false + transactionId = transaction.id + via = nil + messageId = nil + date = transaction.date + if case let .peer(peer) = transaction.peer { + toPeer = peer + } else { + toPeer = nil + } + transactionPeer = transaction.peer + media = [] + photo = nil + isRefund = false + isGift = false + } else if transaction.flags.contains(.isGift) { titleText = strings.Stars_Gift_Received_Title descriptionText = strings.Stars_Gift_Received_Text count = transaction.count diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift index 61a909dff3..bd8f3b37bd 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift @@ -219,6 +219,9 @@ final class StarsTransactionsListPanelComponent: Component { itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) if item.flags.contains(.isGift) { itemSubtitle = environment.strings.Stars_Intro_Transaction_Gift_Title + } else if let _ = item.subscriptionPeriod { + //TODO:localize + itemSubtitle = "Monthly subscription fee" } else { itemSubtitle = nil } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index e6329ee119..bd4c837481 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -18,6 +18,8 @@ import ListSectionComponent import BundleIconComponent import TextFormat import UndoUI +import ListActionItemComponent +import StarsAvatarComponent final class StarsTransactionsScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -574,7 +576,44 @@ final class StarsTransactionsScreenComponent: Component { contentHeight += balanceSize.height contentHeight += 44.0 - let subscriptionsItems: [AnyComponentWithIdentity] = [] + let fontBaseDisplaySize = 17.0 + var subscriptionsItems: [AnyComponentWithIdentity] = [] + if let starsState = self.starsState { + for subscription in starsState.subscriptions { + var titleComponents: [AnyComponentWithIdentity] = [] + titleComponents.append( + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: subscription.peer.compactDisplayTitle, + font: Font.semibold(fontBaseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))) + ) + let itemLabel = NSAttributedString(string: "\(subscription.pricing.amount)", font: Font.medium(fontBaseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor) + + subscriptionsItems.append(AnyComponentWithIdentity( + id: subscription.id, + component: AnyComponent( + ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack(titleComponents, alignment: .left, spacing: 2.0)), + contentInsets: UIEdgeInsets(top: 9.0, left: 0.0, bottom: 8.0, right: 0.0), + leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: component.context, theme: environment.theme, peer: .peer(subscription.peer), photo: nil, media: [], backgroundColor: environment.theme.list.plainBackgroundColor))), false), + icon: nil, + accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(text: itemLabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))), + action: { [weak self] _ in + guard let self, let _ = self.component else { + return + } + + } + ) + ) + )) + } + } if !subscriptionsItems.isEmpty { //TODO:localize diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift index 0270432457..fdf612d778 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift @@ -55,7 +55,7 @@ private final class SheetContent: CombinedComponent { let background = Child(RoundedRectangle.self) let closeButton = Child(Button.self) let title = Child(Text.self) - let urlSection = Child(ListSectionComponent.self) + let amountSection = Child(ListSectionComponent.self) let button = Child(ButtonComponent.self) let balanceTitle = Child(MultilineTextComponent.self) let balanceValue = Child(MultilineTextComponent.self) @@ -246,7 +246,7 @@ private final class SheetContent: CombinedComponent { amountFooter = nil } - let urlSection = urlSection.update( + let amountSection = amountSection.update( component: ListSectionComponent( theme: theme, header: AnyComponent(MultilineTextComponent( @@ -283,12 +283,12 @@ private final class SheetContent: CombinedComponent { availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), transition: context.transition ) - context.add(urlSection - .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + urlSection.size.height / 2.0)) + context.add(amountSection + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + amountSection.size.height / 2.0)) .clipsToBounds(true) .cornerRadius(10.0) ) - contentSize.height += urlSection.size.height + contentSize.height += amountSection.size.height contentSize.height += 32.0 let buttonString: String diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 167e85acec..49c597b45b 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -250,12 +250,7 @@ func openResolvedUrlImpl( present(controller, nil) case let .instantView(webpage, anchor): let sourceLocation = InstantPageSourceLocation(userLocation: .other, peerType: .channel) - let pageController: ViewController - if context.sharedContext.immediateExperimentalUISettings.browserExperiment { - pageController = BrowserScreen(context: context, subject: .instantPage(webPage: webpage, anchor: anchor, sourceLocation: sourceLocation)) - } else { - pageController = InstantPageController(context: context, webPage: webpage, sourceLocation: sourceLocation, anchor: anchor) - } + let pageController = BrowserScreen(context: context, subject: .instantPage(webPage: webpage, anchor: anchor, sourceLocation: sourceLocation)) navigationController?.pushViewController(pageController) case let .join(link): dismissInput() @@ -288,6 +283,55 @@ func openResolvedUrlImpl( openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil)) case let .peek(peer, deadline): openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: ChatPeekTimeout(deadline: deadline, linkData: link))) + case let .invite(invite): + if let subscriptionPricing = invite.subscriptionPricing, let subscriptionFormId = invite.subscriptionFormId, let starsContext = context.starsContext { + let inputData = Promise() + var photo: [TelegramMediaImageRepresentation] = [] + if let photoRepresentation = invite.photoRepresentation { + photo.append(photoRepresentation) + } + let channel = TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(0)), accessHash: .genericPublic(0), title: invite.title, username: nil, photo: photo, creationDate: 0, version: 0, participationStatus: .left, info: .broadcast(TelegramChannelBroadcastInfo(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: invite.nameColor, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil, subscriptionUntilDate: nil) + let invoice = TelegramMediaInvoice(title: "", description: "", photo: nil, receiptMessageId: nil, currency: "XTR", totalAmount: subscriptionPricing.amount, startParam: "", extendedMedia: nil, flags: [], version: 0) + + inputData.set(.single(BotCheckoutController.InputData( + form: BotPaymentForm( + id: subscriptionFormId, + canSaveCredentials: false, + passwordMissing: false, + invoice: BotPaymentInvoice(isTest: false, requestedFields: [], currency: "XTR", prices: [BotPaymentPrice(label: "", amount: subscriptionPricing.amount)], tip: nil, termsInfo: nil), + paymentBotId: channel.id, + providerId: nil, + url: nil, + nativeProvider: nil, + savedInfo: nil, + savedCredentials: [], + additionalPaymentMethods: [] + ), + validatedFormInfo: nil, + botPeer: EnginePeer(channel) + ))) + + let starsInputData = combineLatest( + inputData.get(), + starsContext.state + ) + |> map { data, state -> (StarsContext.State, BotPaymentForm, EnginePeer?)? in + if let data, let state { + return (state, data.form, data.botPeer) + } else { + return nil + } + } + let _ = (starsInputData |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue).start(next: { _ in + let controller = context.sharedContext.makeStarsTransferScreen(context: context, starsContext: starsContext, invoice: invoice, source: .starsChatSubscription(hash: link), extendedMedia: [], inputData: starsInputData, completion: { _ in + }) + navigationController?.pushViewController(controller) + }) + } else { + present(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in + openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: peekData)) + }, parentNavigationController: navigationController, resolvedState: resolvedState), nil) + } default: present(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: peekData)) From b069c1ee5e5efb53363193f24a450086550af8d6 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 1 Aug 2024 09:56:06 +0200 Subject: [PATCH 03/12] Stars subscriptions --- .../Sources/StarsTransferScreen.swift | 72 ++++++++++++++---- .../InviteLink.imageset/Contents.json | 12 +++ .../InviteLink.imageset/linklink_40.pdf | Bin 0 -> 1437 bytes .../SubscriptionLink.imageset/Contents.json | 12 +++ .../SubscriptionLink.imageset/cashlink_40.pdf | Bin 0 -> 2705 bytes 5 files changed, 83 insertions(+), 13 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Item List/InviteLink.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Item List/InviteLink.imageset/linklink_40.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Item List/SubscriptionLink.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Item List/SubscriptionLink.imageset/cashlink_40.pdf diff --git a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift index 84cdd6589f..1fd60bd068 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift @@ -248,6 +248,7 @@ private final class SheetContent: CombinedComponent { let balanceTitle = Child(MultilineTextComponent.self) let balanceValue = Child(MultilineTextComponent.self) let balanceIcon = Child(BundleIconComponent.self) + let info = Child(BalancedTextComponent.self) return { context in let environment = context.environment[EnvironmentType.self] @@ -269,6 +270,7 @@ private final class SheetContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0)) ) + var isSubscription = false let subject: StarsImageComponent.Subject if !component.extendedMedia.isEmpty { subject = .extendedMedia(component.extendedMedia) @@ -321,8 +323,17 @@ private final class SheetContent: CombinedComponent { contentSize.height += 126.0 + let titleString: String + if case .starsChatSubscription = context.component.source { + //TODO:localize + titleString = "Subscribe to the Channel" + isSubscription = true + } else { + titleString = strings.Stars_Transfer_Title + } + let title = title.update( - component: Text(text: strings.Stars_Transfer_Title, font: Font.bold(24.0), color: theme.list.itemPrimaryTextColor), + component: Text(text: titleString, font: Font.bold(24.0), color: theme.list.itemPrimaryTextColor), availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height), transition: .immediate ) @@ -342,7 +353,9 @@ private final class SheetContent: CombinedComponent { let amount = component.invoice.totalAmount let infoText: String - if !component.extendedMedia.isEmpty { + if case .starsChatSubscription = context.component.source { + infoText = "Do you want to subscribe to **\(state.botPeer?.compactDisplayTitle ?? "")** for **\(strings.Stars_Transfer_Info_Stars(Int32(amount)))** per month?" + } else if !component.extendedMedia.isEmpty { var description: String = "" var photoCount: Int32 = 0 var videoCount: Int32 = 0 @@ -446,7 +459,12 @@ private final class SheetContent: CombinedComponent { } let amountString = presentationStringsFormattedNumber(Int32(amount), presentationData.dateTimeFormat.groupingSeparator) - let buttonAttributedString = NSMutableAttributedString(string: "\(strings.Stars_Transfer_Pay) # \(amountString)", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + let buttonAttributedString: NSMutableAttributedString + if case .starsChatSubscription = component.source { + buttonAttributedString = NSMutableAttributedString(string: "Subscribe", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + } else { + buttonAttributedString = NSMutableAttributedString(string: "\(strings.Stars_Transfer_Pay) # \(amountString)", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + } if let range = buttonAttributedString.string.range(of: "#"), let starImage = state.cachedStarImage?.0 { buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string)) buttonAttributedString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xffffff), range: NSRange(range, in: buttonAttributedString.string)) @@ -506,8 +524,13 @@ private final class SheetContent: CombinedComponent { }, completion: { [weak controller] success in if success { let presentationData = accountContext.sharedContext.currentPresentationData.with { $0 } + var title = presentationData.strings.Stars_Transfer_PurchasedTitle let text: String - if let _ = component.invoice.extendedMedia { + if isSubscription { + //TODO:localize + title = "Subscription successful!" + text = "\(presentationData.strings.Stars_Transfer_Purchased_Stars(Int32(invoice.totalAmount))) transferred to \(botTitle)." + } else if let _ = component.invoice.extendedMedia { text = presentationData.strings.Stars_Transfer_UnlockedText( presentationData.strings.Stars_Transfer_Purchased_Stars(Int32(invoice.totalAmount))).string } else { text = presentationData.strings.Stars_Transfer_PurchasedText(invoice.title, botTitle, presentationData.strings.Stars_Transfer_Purchased_Stars(Int32(invoice.totalAmount))).string @@ -518,18 +541,11 @@ private final class SheetContent: CombinedComponent { if let lastController = navigationController.viewControllers.last as? ViewController { let resultController = UndoOverlayController( presentationData: presentationData, -// content: .image( -// image: UIImage(bundleImageName: "Premium/Stars/StarLarge")!, -// title: presentationData.strings.Stars_Transfer_PurchasedTitle, -// text: text, -// round: false, -// undoText: nil -// ), content: .universal( animation: "StarsSend", scale: 0.066, colors: [:], - title: presentationData.strings.Stars_Transfer_PurchasedTitle, + title: title, text: text, customUndoText: nil, timeout: nil @@ -559,6 +575,36 @@ private final class SheetContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0)) ) contentSize.height += button.size.height + + if isSubscription { + contentSize.height += 14.0 + + let termsTextFont = Font.regular(13.0) + let termsTextColor = theme.actionSheet.secondaryTextColor + let termsLinkColor = theme.actionSheet.controlAccentColor + let termsMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: termsTextFont, textColor: termsTextColor), bold: MarkdownAttributeSet(font: termsTextFont, textColor: termsTextColor), link: MarkdownAttributeSet(font: termsTextFont, textColor: termsLinkColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + let info = info.update( + component: BalancedTextComponent( + text: .markdown( + text: "By subscribing you agree to the [Terms of Service]()", + attributes: termsMarkdownAttributes + ), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ), + availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height), + transition: .immediate + ) + context.add(info + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + info.size.height / 2.0)) + ) + contentSize.height += info.size.height + + } + contentSize.height += 48.0 return contentSize @@ -681,7 +727,7 @@ public final class StarsTransferScreen: ViewControllerComponentContainer { starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, - extendedMedia: [TelegramExtendedMedia], + extendedMedia: [TelegramExtendedMedia] = [], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void ) { diff --git a/submodules/TelegramUI/Images.xcassets/Item List/InviteLink.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Item List/InviteLink.imageset/Contents.json new file mode 100644 index 0000000000..3ec814d17a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Item List/InviteLink.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "linklink_40.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Item List/InviteLink.imageset/linklink_40.pdf b/submodules/TelegramUI/Images.xcassets/Item List/InviteLink.imageset/linklink_40.pdf new file mode 100644 index 0000000000000000000000000000000000000000..c2be8a1a21eb38d217f45452f8b9f073b848d022 GIT binary patch literal 1437 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 fKP?z>&@X~2=>8KRf(+KDagh3kC Date: Fri, 2 Aug 2024 12:34:08 +0200 Subject: [PATCH 04/12] Stars subscriptions --- .../Telegram-iOS/en.lproj/Localizable.strings | 2 + .../Sources/InviteLinkListController.swift | 2 +- .../Sources/InviteLinkViewController.swift | 8 +++- .../Sources/StarsTransactionItem.swift | 8 +++- .../Sources/StarsImageComponent.swift | 42 ++++++++++++++++ .../Sources/StarsTransactionScreen.swift | 48 +++++++++++-------- 6 files changed, 87 insertions(+), 23 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 04c4abfc6c..2fdf3ec924 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12674,3 +12674,5 @@ Sorry for the inconvenience."; "MediaPicker.CreateSticker" = "Create a sticker from a photo"; "Stickers.CreateSticker" = "Create\nSticker"; + +"InviteLink.CreateNewInfo" = "You can create additional invite links that are limited by time, number of users, or require a paid subscription."; diff --git a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift index 1088f93cf0..84f0daa5ef 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift @@ -351,7 +351,7 @@ private func inviteLinkListControllerEntries(presentationData: PresentationData, } } if admin == nil { - entries.append(.linksInfo(presentationData.theme, presentationData.strings.InviteLink_CreateInfo)) + entries.append(.linksInfo(presentationData.theme, presentationData.strings.InviteLink_CreateNewInfo)) } if let revokedInvites = revokedInvites { diff --git a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift index 803cb4793b..a5f19c7534 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift @@ -798,7 +798,13 @@ public final class InviteLinkViewController: ViewController { if let pricing = invite.pricing { //TODO:localize entries.append(.subscriptionHeader(presentationData.theme, "SUBSCRIPTION FEE")) - entries.append(.subscriptionPricing(presentationData.theme, "⭐️\(pricing.amount) / month x \(state.count)", "You get approximately $\(Float(pricing.amount * Int64(state.count)) * 0.01) monthly")) + var title = "⭐️\(pricing.amount) / month" + var subtitle = "No one joined yet" + if state.count > 0 { + title += " x \(state.count)" + subtitle = "You get approximately $\(Float(pricing.amount * Int64(state.count)) * 0.01) monthly" + } + entries.append(.subscriptionPricing(presentationData.theme, title, subtitle)) } entries.append(.creatorHeader(presentationData.theme, presentationData.strings.InviteLink_CreatedBy.uppercased())) diff --git a/submodules/StatisticsUI/Sources/StarsTransactionItem.swift b/submodules/StatisticsUI/Sources/StarsTransactionItem.swift index 6a225c18dc..43c3daf13a 100644 --- a/submodules/StatisticsUI/Sources/StarsTransactionItem.swift +++ b/submodules/StatisticsUI/Sources/StarsTransactionItem.swift @@ -231,15 +231,19 @@ final class StarsTransactionItemNode: ListViewItemNode, ItemListItemNode { var itemDate: String switch item.transaction.peer { case let .peer(peer): - if !item.transaction.media.isEmpty { + if !item.transaction.media.isEmpty { itemTitle = item.presentationData.strings.Stars_Intro_Transaction_MediaPurchase itemSubtitle = peer.displayTitle(strings: item.presentationData.strings, displayOrder: .firstLast) } else if let title = item.transaction.title { itemTitle = title itemSubtitle = peer.displayTitle(strings: item.presentationData.strings, displayOrder: .firstLast) } else { + if let _ = item.transaction.subscriptionPeriod { + itemSubtitle = "Monthly subscription fee" + } else { + itemSubtitle = nil + } itemTitle = peer.displayTitle(strings: item.presentationData.strings, displayOrder: .firstLast) - itemSubtitle = nil } case .appStore: itemTitle = item.presentationData.strings.Stars_Intro_Transaction_AppleTopUp_Title diff --git a/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift b/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift index 6c1e5dc948..aa48532c32 100644 --- a/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift @@ -297,11 +297,16 @@ public final class StarsImageComponent: Component { } } + public enum Icon { + case star + } + public let context: AccountContext public let subject: Subject public let theme: PresentationTheme public let diameter: CGFloat public let backgroundColor: UIColor + public let icon: Icon? public let action: ((@escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void)? public init( @@ -310,6 +315,7 @@ public final class StarsImageComponent: Component { theme: PresentationTheme, diameter: CGFloat, backgroundColor: UIColor, + icon: Icon? = nil, action: ((@escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void)? = nil ) { self.context = context @@ -317,6 +323,7 @@ public final class StarsImageComponent: Component { self.theme = theme self.diameter = diameter self.backgroundColor = backgroundColor + self.icon = icon self.action = action } @@ -336,6 +343,9 @@ public final class StarsImageComponent: Component { if lhs.backgroundColor != rhs.backgroundColor { return false } + if lhs.icon != rhs.icon { + return false + } return true } @@ -353,6 +363,8 @@ public final class StarsImageComponent: Component { private var avatarNode: ImageNode? private var iconBackgroundView: UIImageView? private var iconView: UIImageView? + private var smallIconOutlineView: UIImageView? + private var smallIconView: UIImageView? private var dustNode: MediaDustNode? private var button: UIControl? @@ -814,6 +826,36 @@ public final class StarsImageComponent: Component { animationNode.updateLayout(size: animationFrame.size) } + if let _ = component.icon { + let smallIconView: UIImageView + let smallIconOutlineView: UIImageView + if let current = self.smallIconView, let currentOutline = self.smallIconOutlineView { + smallIconView = current + smallIconOutlineView = currentOutline + } else { + smallIconOutlineView = UIImageView() + containerNode.view.addSubview(smallIconOutlineView) + + smallIconView = UIImageView() + containerNode.view.addSubview(smallIconView) + } + + smallIconView.image = UIImage(bundleImageName: "Premium/Stars/BalanceStar") + if smallIconOutlineView.image == nil { + smallIconOutlineView.image = generateTintedImage(image: smallIconView.image, color: .white)?.withRenderingMode(.alwaysTemplate) + } + smallIconOutlineView.tintColor = component.backgroundColor + + if let icon = smallIconView.image { + let smallIconFrame = CGRect(origin: CGPoint(x: imageFrame.maxX - icon.size.width - 5.0, y: imageFrame.maxY - icon.size.height - 5.0), size: icon.size) + smallIconView.frame = smallIconFrame + smallIconOutlineView.frame = smallIconFrame.insetBy(dx: -3.0 + UIScreenPixel, dy: -3.0 + UIScreenPixel) + } + } else if let smallIconView = self.smallIconView { + self.smallIconView = nil + smallIconView.removeFromSuperview() + } + if let _ = component.action { if self.button == nil { let button = UIControl(frame: imageFrame) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index 9702ce0582..1119a23904 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -97,6 +97,8 @@ private final class StarsTransactionSheetContent: CombinedComponent { peerIds.append(receipt.botPaymentId) case let .gift(message): peerIds.append(message.id.peerId) + case let .subscription(subscription): + peerIds.append(subscription.peer.id) } self.disposable = (context.engine.data.get( @@ -195,17 +197,30 @@ private final class StarsTransactionSheetContent: CombinedComponent { let messageId: EngineMessage.Id? let toPeer: EnginePeer? let transactionPeer: StarsContext.State.Transaction.Peer? - let media: [AnyMediaReference] - let photo: TelegramMediaWebFile? - let isRefund: Bool - let isGift: Bool + var media: [AnyMediaReference] = [] + var photo: TelegramMediaWebFile? + var isRefund = false + var isGift = false + var isSubscription = false + var isSubscriptionFee = false var delayedCloseOnOpenPeer = true switch subject { + case let .subscription(subscription): + titleText = "Subscription" + descriptionText = "" + count = subscription.pricing.amount + transactionId = nil + date = subscription.untilDate + via = nil + messageId = nil + toPeer = subscription.peer + transactionPeer = .peer(subscription.peer) + isSubscription = true case let .transaction(transaction, parentPeer): if let _ = transaction.subscriptionPeriod { //TODO:localize - titleText = "Monthly Subscription Fee" + titleText = "Monthly subscription fee" descriptionText = "" count = transaction.count countOnTop = false @@ -219,10 +234,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { toPeer = nil } transactionPeer = transaction.peer - media = [] - photo = nil - isRefund = false - isGift = false + isSubscriptionFee = true } else if transaction.flags.contains(.isGift) { titleText = strings.Stars_Gift_Received_Title descriptionText = strings.Stars_Gift_Received_Text @@ -238,9 +250,6 @@ private final class StarsTransactionSheetContent: CombinedComponent { toPeer = nil } transactionPeer = transaction.peer - media = [] - photo = nil - isRefund = false isGift = true } else { switch transaction.peer { @@ -319,7 +328,6 @@ private final class StarsTransactionSheetContent: CombinedComponent { transactionPeer = transaction.peer media = transaction.media.map { AnyMediaReference.starsTransaction(transaction: StarsTransactionReference(peerId: parentPeer.id, id: transaction.id, isRefund: transaction.flags.contains(.isRefund)), media: $0) } photo = transaction.photo - isGift = false isRefund = transaction.flags.contains(.isRefund) } case let .receipt(receipt): @@ -336,10 +344,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { toPeer = nil } transactionPeer = nil - media = [] photo = receipt.invoiceMedia.photo - isRefund = false - isGift = false delayedCloseOnOpenPeer = false case let .gift(message): let incoming = message.flags.contains(.Incoming) @@ -365,9 +370,6 @@ private final class StarsTransactionSheetContent: CombinedComponent { toPeer = state.peerMap[message.id.peerId] } transactionPeer = nil - media = [] - photo = nil - isRefund = false isGift = true delayedCloseOnOpenPeer = false } @@ -416,6 +418,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { ) let imageSubject: StarsImageComponent.Subject + let imageIcon: StarsImageComponent.Icon? if isGift { imageSubject = .gift(count) } else if !media.isEmpty { @@ -429,6 +432,11 @@ private final class StarsTransactionSheetContent: CombinedComponent { } else { imageSubject = .none } + if isSubscription || isSubscriptionFee { + imageIcon = .star + } else { + imageIcon = nil + } let star = star.update( component: StarsImageComponent( context: component.context, @@ -436,6 +444,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { theme: theme, diameter: 90.0, backgroundColor: theme.actionSheet.opaqueItemBackgroundColor, + icon: imageIcon, action: !media.isEmpty ? { transitionNode, addToTransitionSurface in component.openMedia(media.map { $0.media }, transitionNode, addToTransitionSurface) } : nil @@ -939,6 +948,7 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { case transaction(StarsContext.State.Transaction, EnginePeer) case receipt(BotPaymentReceipt) case gift(EngineMessage) + case subscription(StarsContext.State.Subscription) } private let context: AccountContext From b564eff46cc570e221f97afc4c17816b11f5c809 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 2 Aug 2024 15:51:05 +0200 Subject: [PATCH 05/12] Stars subscriptions --- .../TelegramEngine/Payments/Stars.swift | 6 +++ .../Payments/TelegramEnginePayments.swift | 4 ++ .../Sources/StarsAvatarComponent.swift | 37 +++++++++++++++++-- .../Sources/StarsImageComponent.swift | 16 ++------ .../Sources/StarsTransactionsScreen.swift | 35 ++++++++++++++++-- 5 files changed, 78 insertions(+), 20 deletions(-) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index 2a7d56c02d..cf5743da41 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -923,6 +923,8 @@ private final class StarsSubscriptionsContextImpl { self._state = StarsSubscriptionsContext.State(subscriptions: currentSubscriptions, canLoadMore: canLoadMore, isLoading: false) self._statePromise.set(.single(self._state)) + + self.loadMore() } deinit { @@ -954,6 +956,10 @@ private final class StarsSubscriptionsContextImpl { updatedState.isLoading = false updatedState.canLoadMore = self.nextOffset != nil self.updateState(updatedState) + + if updatedState.canLoadMore { + self.loadMore() + } })) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index 528e60957c..022720b8ef 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -86,6 +86,10 @@ public extension TelegramEngine { return StarsTransactionsContext(account: self.account, subject: subject, mode: mode) } + public func peerStarsSubscriptionsContext(starsContext: StarsContext) -> StarsSubscriptionsContext { + return StarsSubscriptionsContext(account: self.account, starsContext: starsContext) + } + public func sendStarsPaymentForm(formId: Int64, source: BotPaymentInvoiceSource) -> Signal { return _internal_sendStarsPaymentForm(account: self.account, formId: formId, source: source) } diff --git a/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift index 679bb8df66..189be9a0a5 100644 --- a/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift @@ -309,17 +309,23 @@ public final class StarsAvatarComponent: Component { public final class StarsLabelComponent: CombinedComponent { let text: NSAttributedString + let subtext: NSAttributedString? public init( - text: NSAttributedString + text: NSAttributedString, + subtext: NSAttributedString? = nil ) { self.text = text + self.subtext = subtext } public static func ==(lhs: StarsLabelComponent, rhs: StarsLabelComponent) -> Bool { if lhs.text != rhs.text { return false } + if lhs.subtext != rhs.subtext { + return false + } return true } @@ -336,6 +342,15 @@ public final class StarsLabelComponent: CombinedComponent { transition: context.transition ) + let subtext: _UpdatedChildComponent? = nil +// if let sublabel = component.subtext { +// subtext = text.update( +// component: MultilineTextComponent(text: .plain(sublabel)), +// availableSize: CGSize(width: 100.0, height: 40.0), +// transition: context.transition +// ) +// } + let iconSize = CGSize(width: 20.0, height: 20.0) let icon = icon.update( component: BundleIconComponent( @@ -348,13 +363,27 @@ public final class StarsLabelComponent: CombinedComponent { let spacing: CGFloat = 3.0 let totalWidth = text.size.width + spacing + iconSize.width - let size = CGSize(width: totalWidth, height: iconSize.height) + var size = CGSize(width: totalWidth, height: iconSize.height) + let firstLineSize = size.height + if let _ = subtext { + size.height += 20.0 + } + + let iconPosition: CGFloat + let textPosition: CGFloat + if let _ = component.subtext { + iconPosition = iconSize.width / 2.0 + textPosition = totalWidth - text.size.width / 2.0 + } else { + textPosition = text.size.width / 2.0 + iconPosition = totalWidth - iconSize.width / 2.0 + } context.add(text - .position(CGPoint(x: text.size.width / 2.0, y: size.height / 2.0)) + .position(CGPoint(x: textPosition, y: firstLineSize / 2.0)) ) context.add(icon - .position(CGPoint(x: totalWidth - iconSize.width / 2.0, y: size.height / 2.0 - UIScreenPixel)) + .position(CGPoint(x: iconPosition, y: firstLineSize / 2.0 - UIScreenPixel)) ) return size } diff --git a/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift b/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift index aa48532c32..1783c702ff 100644 --- a/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift @@ -828,28 +828,18 @@ public final class StarsImageComponent: Component { if let _ = component.icon { let smallIconView: UIImageView - let smallIconOutlineView: UIImageView - if let current = self.smallIconView, let currentOutline = self.smallIconOutlineView { + if let current = self.smallIconView { smallIconView = current - smallIconOutlineView = currentOutline } else { - smallIconOutlineView = UIImageView() - containerNode.view.addSubview(smallIconOutlineView) - smallIconView = UIImageView() containerNode.view.addSubview(smallIconView) } - smallIconView.image = UIImage(bundleImageName: "Premium/Stars/BalanceStar") - if smallIconOutlineView.image == nil { - smallIconOutlineView.image = generateTintedImage(image: smallIconView.image, color: .white)?.withRenderingMode(.alwaysTemplate) - } - smallIconOutlineView.tintColor = component.backgroundColor + smallIconView.image = UIImage(bundleImageName: "Premium/Stars/MockBigStar") if let icon = smallIconView.image { - let smallIconFrame = CGRect(origin: CGPoint(x: imageFrame.maxX - icon.size.width - 5.0, y: imageFrame.maxY - icon.size.height - 5.0), size: icon.size) + let smallIconFrame = CGRect(origin: CGPoint(x: imageFrame.maxX - icon.size.width, y: imageFrame.maxY - icon.size.height), size: icon.size) smallIconView.frame = smallIconFrame - smallIconOutlineView.frame = smallIconFrame.insetBy(dx: -3.0 + UIScreenPixel, dy: -3.0 + UIScreenPixel) } } else if let smallIconView = self.smallIconView { self.smallIconView = nil diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index bd4c837481..2b07523b20 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -118,6 +118,10 @@ final class StarsTransactionsScreenComponent: Component { private var previousBalance: Int64? + private var subscriptionsContext: StarsSubscriptionsContext? + private var subscriptionsStateDisposable: Disposable? + private var subscriptionsState: StarsSubscriptionsContext.State? + private var allTransactionsContext: StarsTransactionsContext? private var incomingTransactionsContext: StarsTransactionsContext? private var outgoingTransactionsContext: StarsTransactionsContext? @@ -303,6 +307,20 @@ final class StarsTransactionsScreenComponent: Component { self.state?.updated() } }) + + let subscriptionsContext = component.context.engine.payments.peerStarsSubscriptionsContext(starsContext: component.starsContext) + self.subscriptionsContext = subscriptionsContext + self.subscriptionsStateDisposable = (subscriptionsContext.state + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let self else { + return + } + self.subscriptionsState = state + + if !self.isUpdating { + self.state?.updated() + } + }) } var wasLockedAtPanels = false @@ -578,8 +596,8 @@ final class StarsTransactionsScreenComponent: Component { let fontBaseDisplaySize = 17.0 var subscriptionsItems: [AnyComponentWithIdentity] = [] - if let starsState = self.starsState { - for subscription in starsState.subscriptions { + if let subscriptionsState = self.subscriptionsState { + for subscription in subscriptionsState.subscriptions { var titleComponents: [AnyComponentWithIdentity] = [] titleComponents.append( AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( @@ -591,7 +609,18 @@ final class StarsTransactionsScreenComponent: Component { maximumNumberOfLines: 1 ))) ) + titleComponents.append( + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "renews on 2 Aug", + font: Font.regular(floor(fontBaseDisplaySize * 15.0 / 17.0)), + textColor: environment.theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 1 + ))) + ) let itemLabel = NSAttributedString(string: "\(subscription.pricing.amount)", font: Font.medium(fontBaseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor) + let itemSublabel = NSAttributedString(string: "per month", font: Font.regular(floor(fontBaseDisplaySize * 14.0 / 17.0)), textColor: environment.theme.list.itemPrimaryTextColor) subscriptionsItems.append(AnyComponentWithIdentity( id: subscription.id, @@ -602,7 +631,7 @@ final class StarsTransactionsScreenComponent: Component { contentInsets: UIEdgeInsets(top: 9.0, left: 0.0, bottom: 8.0, right: 0.0), leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: component.context, theme: environment.theme, peer: .peer(subscription.peer), photo: nil, media: [], backgroundColor: environment.theme.list.plainBackgroundColor))), false), icon: nil, - accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(text: itemLabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))), + accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(text: itemLabel, subtext: itemSublabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))), action: { [weak self] _ in guard let self, let _ = self.component else { return From df311bb022b24e1fb5c9c290c458bffea3bedc79 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 6 Aug 2024 22:40:17 +0200 Subject: [PATCH 06/12] Stars subscriptions --- .../Telegram-iOS/en.lproj/Localizable.strings | 6 + .../Sources/AccountContext.swift | 3 +- .../Sources/Node/ChatListItem.swift | 78 ++++++ .../Sources/Node/ChatListNodeEntries.swift | 2 +- .../Sources/Node/ChatListNoticeItem.swift | 6 +- .../Sources/InviteLinkEditController.swift | 25 +- .../Sources/InviteLinkListController.swift | 27 +- .../Sources/InviteLinkViewController.swift | 88 ++++-- .../Sources/InviteRequestsController.swift | 2 +- .../Sources/ItemListInviteLinkItem.swift | 43 +-- .../ItemListPermanentInviteLinkItem.swift | 57 +++- .../Sources/ItemListPeerItem.swift | 98 +++---- .../Items/ItemListSingleLineInputItem.swift | 4 +- .../Sources/StarsTransactionItem.swift | 9 +- .../Sources/VoiceChatController.swift | 10 +- .../TelegramEngine/Payments/Stars.swift | 28 +- .../Sources/TelegramEngine/Peers/Peer.swift | 4 + .../Sources/Utils/PeerUtils.swift | 9 + .../Sources/EmojiTextAttachmentView.swift | 2 +- .../Sources/PeerInfoScreen.swift | 4 +- .../Sources/StarsImageComponent.swift | 19 +- .../Sources/StarsTransactionScreen.swift | 255 ++++++++++++++---- .../StarsTransactionsListPanelComponent.swift | 8 +- .../Sources/StarsTransactionsScreen.swift | 74 ++++- .../Sources/StarsTransferScreen.swift | 32 ++- .../StarMediumOutline.imageset/Contents.json | 12 + .../StarOutline.pdf | Bin 0 -> 16644 bytes .../TransactionStar.imageset/Contents.json | 12 + .../StarTransaction.pdf | Bin 0 -> 7031 bytes .../Contents.json | 12 + .../StarTransactionOutline.pdf | Bin 0 -> 17106 bytes .../Chat/ChatControllerLoadDisplayNode.swift | 2 +- .../Sources/ChatHistoryEntriesForView.swift | 57 ++-- .../Sources/SharedAccountContext.swift | 8 +- .../Sources/UndoOverlayController.swift | 2 +- .../Sources/UndoOverlayControllerNode.swift | 8 +- 36 files changed, 767 insertions(+), 239 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/StarMediumOutline.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/StarMediumOutline.imageset/StarOutline.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/TransactionStar.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/TransactionStar.imageset/StarTransaction.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/TransactionStarOutline.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/TransactionStarOutline.imageset/StarTransactionOutline.pdf diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 2fdf3ec924..96632f4872 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12676,3 +12676,9 @@ Sorry for the inconvenience."; "Stickers.CreateSticker" = "Create\nSticker"; "InviteLink.CreateNewInfo" = "You can create additional invite links that are limited by time, number of users, or require a paid subscription."; + +"InviteLink.CopyShort" = "Copy"; +"InviteLink.ShareShort" = "Share"; + +"Stars.Subscription.Terms" = "By subscribing you agree to the [Terms of Service]()."; +"Stars.Subscription.Terms_URL" = "https://telegram.org/tos/stars"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 0bee003248..8293c75c9f 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1007,7 +1007,8 @@ public protocol SharedAccountContext: AnyObject { func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController func makeStarsTransactionScreen(context: AccountContext, transaction: StarsContext.State.Transaction, peer: EnginePeer) -> ViewController func makeStarsReceiptScreen(context: AccountContext, receipt: BotPaymentReceipt) -> ViewController - func makeStarsSubscriptionScreen(context: AccountContext, subscription: StarsContext.State.Subscription) -> ViewController + func makeStarsSubscriptionScreen(context: AccountContext, subscription: StarsContext.State.Subscription, update: @escaping (Bool) -> Void) -> ViewController + func makeStarsSubscriptionScreen(context: AccountContext, peer: EnginePeer, pricing: StarsSubscriptionPricing, importer: PeerInvitationImportersState.Importer, usdRate: Double) -> ViewController func makeStarsStatisticsScreen(context: AccountContext, peerId: EnginePeer.Id, revenueContext: StarsRevenueStatsContext) -> ViewController func makeStarsAmountScreen(context: AccountContext, initialValue: Int64?, completion: @escaping (Int64) -> Void) -> ViewController func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 901fd837c3..cdd46823e7 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -1225,6 +1225,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { var avatarBadgeBackground: ASImageNode? let onlineNode: PeerOnlineMarkerNode var avatarTimerBadge: AvatarBadgeView? + private var starView: StarView? let pinnedIconNode: ASImageNode var secretIconNode: ASImageNode? var verifiedIconView: ComponentHostView? @@ -1827,6 +1828,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { if let item = self.item, case .chatList = item.index { self.onlineNode.setImage(PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .highlighted, voiceChat: self.onlineIsVoiceChat), color: nil, transition: transition) + self.starView?.setOutlineColor(item.presentationData.theme.chatList.itemHighlightedBackgroundColor, transition: transition) } } else { if self.highlightedBackgroundNode.supernode != nil { @@ -1845,12 +1847,16 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { if let item = self.item { let onlineIcon: UIImage? + let effectiveBackgroundColor: UIColor if item.isPinned { onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .pinned, voiceChat: self.onlineIsVoiceChat) + effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor } else { onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .regular, voiceChat: self.onlineIsVoiceChat) + effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor } self.onlineNode.setImage(onlineIcon, color: nil, transition: transition) + self.starView?.setOutlineColor(effectiveBackgroundColor, transition: transition) } } } @@ -2934,6 +2940,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { titleIconsWidth += currentMutedIconImage.size.width } + var isSubscription = false var isSecret = false if !isPeerGroup { if case let .chatList(index) = item.index, index.messageIndex.id.peerId.namespace == Namespaces.Peer.SecretChat { @@ -2978,6 +2985,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { break } } else if case let .chat(itemPeer) = contentPeer, let peer = itemPeer.chatMainPeer { + if peer.isSubscription { + isSubscription = true + } if case let .peer(peerData) = item.content, peerData.customMessageListData?.hidePeerStatus == true { currentCredibilityIconContent = nil } else if case .savedMessagesChats = item.chatListLocation, peer.id == item.context.account.peerId { @@ -3635,15 +3645,39 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { transition.updateSublayerTransformScale(node: strongSelf.onlineNode, scale: (1.0 - onlineInlineNavigationFraction) * 1.0 + onlineInlineNavigationFraction * 0.00001) let onlineIcon: UIImage? + let effectiveBackgroundColor: UIColor if strongSelf.reallyHighlighted { onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .highlighted, voiceChat: onlineIsVoiceChat) + effectiveBackgroundColor = item.presentationData.theme.chatList.itemHighlightedBackgroundColor } else if case let .chatList(index) = item.index, index.pinningIndex != nil { onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .pinned, voiceChat: onlineIsVoiceChat) + effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor } else { onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .regular, voiceChat: onlineIsVoiceChat) + effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor } strongSelf.onlineNode.setImage(onlineIcon, color: item.presentationData.theme.list.itemCheckColors.foregroundColor, transition: .immediate) + if isSubscription { + let starView: StarView + if let current = strongSelf.starView { + starView = current + } else { + starView = StarView() + strongSelf.starView = starView + strongSelf.view.addSubview(starView) +// strongSelf.mainContentContainerNode.view.addSubview(starView) + } + starView.outlineColor = effectiveBackgroundColor + + let starSize = CGSize(width: 20.0, height: 20.0) + let starFrame = CGRect(origin: CGPoint(x: avatarFrame.maxX - starSize.width + 1.0, y: avatarFrame.maxY - starSize.height + 1.0), size: starSize) + transition.updateFrame(view: starView, frame: starFrame) + } else if let starView = strongSelf.starView { + strongSelf.starView = nil + starView.removeFromSuperview() + } + let autoremoveTimeoutFraction: CGFloat if online { autoremoveTimeoutFraction = 0.0 @@ -4746,3 +4780,47 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } } } + +private class StarView: UIView { + let outline = SimpleLayer() + let foreground = SimpleLayer() + + var outlineColor: UIColor = .white { + didSet { + self.outline.layerTintColor = self.outlineColor.cgColor + } + } + + override init(frame: CGRect) { + self.outline.contents = UIImage(bundleImageName: "Premium/Stars/StarMediumOutline")?.cgImage + self.foreground.contents = UIImage(bundleImageName: "Premium/Stars/StarMedium")?.cgImage + + super.init(frame: frame) + + self.layer.addSublayer(self.outline) + self.layer.addSublayer(self.foreground) + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + func setOutlineColor(_ color: UIColor, transition: ContainedViewLayoutTransition) { + if case let .animated(duration, curve) = transition, color != self.outlineColor { + let snapshotLayer = SimpleLayer() + snapshotLayer.layerTintColor = self.outlineColor.cgColor + snapshotLayer.contents = self.outline.contents + snapshotLayer.frame = self.outline.bounds + self.layer.insertSublayer(snapshotLayer, above: self.outline) + snapshotLayer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false, completion: { [weak snapshotLayer] _ in + snapshotLayer?.removeFromSuperlayer() + }) + } + self.outlineColor = color + } + + override func layoutSubviews() { + self.outline.frame = self.bounds + self.foreground.frame = self.bounds + } +} diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift index 953ba56328..a3b38bca0e 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift @@ -90,7 +90,7 @@ public enum ChatListNotice: Equatable { case birthdayPremiumGift(peers: [EnginePeer], birthdays: [EnginePeer.Id: TelegramBirthday]) case reviewLogin(newSessionReview: NewSessionReview, totalCount: Int) case premiumGrace - case starsSubscriptionLowBalance + case starsSubscriptionLowBalance(amount: Int64) } enum ChatListNodeEntry: Comparable, Identifiable { diff --git a/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift b/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift index cb7ca09b4e..c41a4c936e 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift @@ -262,10 +262,10 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode { okButtonLayout = makeOkButtonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.ChatList_SessionReview_PanelConfirm, font: titleFont, textColor: item.theme.list.itemAccentColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0))) cancelButtonLayout = makeCancelButtonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.ChatList_SessionReview_PanelReject, font: titleFont, textColor: item.theme.list.itemDestructiveColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0))) - case .starsSubscriptionLowBalance: - let titleStringValue = NSMutableAttributedString(attributedString: NSAttributedString(string: "5 Stars needed for Astro Paws", font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor)) + case let .starsSubscriptionLowBalance(amount): + let titleStringValue = NSMutableAttributedString(attributedString: NSAttributedString(string: "⭐️ \(amount) Stars needed for your subscriptions", font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor)) titleString = titleStringValue - textString = NSAttributedString(string: "Insufficient funds to cover your subscription.", font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) + textString = NSAttributedString(string: "Insufficient funds to cover your subscriptions.", font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) } var leftInset: CGFloat = sideInset diff --git a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift index d988513a91..4886e17e44 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift @@ -79,7 +79,7 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { case subscriptionFeeToggle(PresentationTheme, String, Bool, Bool) - case subscriptionFee(PresentationTheme, String, Bool, Int64?) + case subscriptionFee(PresentationTheme, String, Bool, Int64?, String) case subscriptionFeeInfo(PresentationTheme, String) case requestApproval(PresentationTheme, String, Bool, Bool) @@ -182,8 +182,8 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { } else { return false } - case let .subscriptionFee(lhsTheme, lhsText, lhsValue, lhsEnabled): - if case let .subscriptionFee(rhsTheme, rhsText, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled { + case let .subscriptionFee(lhsTheme, lhsText, lhsValue, lhsEnabled, lhsLabel): + if case let .subscriptionFee(rhsTheme, rhsText, rhsValue, rhsEnabled, rhsLabel) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled, lhsLabel == rhsLabel { return true } else { return false @@ -288,7 +288,6 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { }, action: {}) case let .titleInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) - case let .subscriptionFeeToggle(_, text, value, enabled): return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in arguments.updateState { state in @@ -302,13 +301,13 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { return updatedState } }) - case let .subscriptionFee(_, placeholder, enabled, value): + case let .subscriptionFee(_, placeholder, enabled, value, label): let title = NSMutableAttributedString(string: "⭐️", font: Font.semibold(18.0), textColor: .white) if let range = title.string.range(of: "⭐️") { title.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: title.string)) title.addAttribute(.baselineOffset, value: -1.0, range: NSRange(range, in: title.string)) } - return ItemListSingleLineInputItem(context: arguments.context, presentationData: presentationData, title: title, text: value.flatMap { "\($0)" } ?? "", placeholder: placeholder, type: .number, spacing: 3.0, enabled: enabled, sectionId: self.section, textUpdated: { text in + return ItemListSingleLineInputItem(context: arguments.context, presentationData: presentationData, title: title, text: value.flatMap { "\($0)" } ?? "", placeholder: placeholder, label: label, type: .number, spacing: 3.0, enabled: enabled, sectionId: self.section, textUpdated: { text in arguments.updateState { state in var updatedState = state if let value = Int64(text) { @@ -318,7 +317,7 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { } return updatedState } - }, action: {}) + }, action: {}) case let .subscriptionFeeInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) case let .requestApproval(_, text, value, enabled): @@ -458,7 +457,7 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { } } -private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: InviteLinkEditControllerState, isGroup: Bool, isPublic: Bool, presentationData: PresentationData) -> [InviteLinksEditEntry] { +private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: InviteLinkEditControllerState, isGroup: Bool, isPublic: Bool, presentationData: PresentationData, starsState: StarsRevenueStats?) -> [InviteLinksEditEntry] { var entries: [InviteLinksEditEntry] = [] entries.append(.titleHeader(presentationData.theme, presentationData.strings.InviteLink_Create_LinkNameTitle.uppercased())) @@ -471,7 +470,11 @@ private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: //TODO:localize entries.append(.subscriptionFeeToggle(presentationData.theme, "Require Monthly Fee", state.subscriptionEnabled, isEditingEnabled)) if state.subscriptionEnabled { - entries.append(.subscriptionFee(presentationData.theme, "Stars amount per month", isEditingEnabled, state.subscriptionFee)) + var label: String = "" + if let subscriptionFee, subscriptionFee > 0, let starsState { + label = formatTonUsdValue(state.subscriptionFee, divide: false, rate: starsState.usdRate, dateTimeFormat: presentationData.dateTimeFormat) + } + entries.append(.subscriptionFee(presentationData.theme, "Stars amount per month", isEditingEnabled, state.subscriptionFee, label)) } let infoText: String if let _ = invite, state.subscriptionEnabled { @@ -545,7 +548,7 @@ private struct InviteLinkEditControllerState: Equatable { var updating = false } -public func inviteLinkEditController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id, invite: ExportedInvitation?, completion: ((ExportedInvitation?) -> Void)? = nil) -> ViewController { +public func inviteLinkEditController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id, invite: ExportedInvitation?, starsState: StarsRevenueStats? = nil, completion: ((ExportedInvitation?) -> Void)? = nil) -> ViewController { var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? let actionsDisposable = DisposableSet() @@ -759,7 +762,7 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(invite == nil ? presentationData.strings.InviteLink_Create_Title : presentationData.strings.InviteLink_Create_EditTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: inviteLinkEditControllerEntries(invite: invite, state: state, isGroup: isGroup, isPublic: isPublic, presentationData: presentationData), style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: animateChanges) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: inviteLinkEditControllerEntries(invite: invite, state: state, isGroup: isGroup, isPublic: isPublic, presentationData: presentationData, starsState: starsState), style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: animateChanges) return (controllerState, (listState, arguments)) } diff --git a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift index 84f0daa5ef..2473129fac 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift @@ -215,7 +215,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry { case let .mainLinkHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .mainLink(_, invite, peers, importersCount, isPublic): - return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, count: importersCount, peers: peers, displayButton: true, displayImporters: !isPublic, buttonColor: nil, sectionId: self.section, style: .blocks, copyAction: { + return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, count: importersCount, peers: peers, displayButton: true, separateButtons: true, displayImporters: !isPublic, buttonColor: nil, sectionId: self.section, style: .blocks, copyAction: { if let invite = invite { arguments.copyLink(invite) } @@ -268,7 +268,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry { } } -private func inviteLinkListControllerEntries(presentationData: PresentationData, exportedInvitation: EngineExportedPeerInvitation?, peer: EnginePeer?, invites: [ExportedInvitation]?, revokedInvites: [ExportedInvitation]?, importers: PeerInvitationImportersState?, creators: [ExportedInvitationCreator], admin: ExportedInvitationCreator?, tick: Int32) -> [InviteLinksListEntry] { +private func inviteLinkListControllerEntries(presentationData: PresentationData, exportedInvitation: EngineExportedPeerInvitation?, peer: EnginePeer?, invites: [ExportedInvitation]?, revokedInvites: [ExportedInvitation]?, importers: PeerInvitationImportersState?, creators: [ExportedInvitationCreator], admin: ExportedInvitationCreator?, tick: Int32, starsState: StarsRevenueStats?) -> [InviteLinksListEntry] { var entries: [InviteLinksListEntry] = [] if admin == nil { @@ -393,12 +393,12 @@ private struct InviteLinkListControllerState: Equatable { var revokingPrivateLink: Bool } -public func inviteLinkListController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id, admin: ExportedInvitationCreator?) -> ViewController { +public func inviteLinkListController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id, admin: ExportedInvitationCreator?, starsRevenueContext: StarsRevenueStatsContext? = nil) -> ViewController { var pushControllerImpl: ((ViewController) -> Void)? var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? var presentInGlobalOverlayImpl: ((ViewController) -> Void)? var navigationController: (() -> NavigationController?)? - + var dismissTooltipsImpl: (() -> Void)? let actionsDisposable = DisposableSet() @@ -409,6 +409,9 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio statePromise.set(stateValue.modify { f($0) }) } + let starsContext: StarsRevenueStatsContext = starsRevenueContext ?? context.engine.payments.peerStarsRevenueContext(peerId: peerId) + let starsStats = Atomic(value: nil) + let revokeLinkDisposable = MetaDisposable() actionsDisposable.add(revokeLinkDisposable) @@ -487,7 +490,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio } presentControllerImpl?(shareController, nil) }, openMainLink: { invite in - let controller = InviteLinkViewController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, invite: invite, invitationsContext: nil, revokedInvitationsContext: revokedInvitesContext, importersContext: nil) + let controller = InviteLinkViewController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, invite: invite, invitationsContext: nil, revokedInvitationsContext: revokedInvitesContext, importersContext: nil, starsState: starsStats.with { $0 }) pushControllerImpl?(controller) }, copyLink: { invite in UIPasteboard.general.string = invite.link @@ -604,7 +607,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio let contextController = ContextController(presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) presentInGlobalOverlayImpl?(contextController) }, createLink: { - let controller = inviteLinkEditController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, invite: nil, completion: { invite in + let controller = inviteLinkEditController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, invite: nil, starsState: starsStats.with( { $0 }), completion: { invite in if let invite = invite { invitesContext.add(invite) } @@ -613,7 +616,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio pushControllerImpl?(controller) }, openLink: { invite in if let invite = invite { - let controller = InviteLinkViewController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, invite: invite, invitationsContext: invitesContext, revokedInvitationsContext: revokedInvitesContext, importersContext: nil) + let controller = InviteLinkViewController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, invite: invite, invitationsContext: invitesContext, revokedInvitationsContext: revokedInvitesContext, importersContext: nil, starsState: starsStats.with { $0 }) pushControllerImpl?(controller) } }, linkContextAction: { invite, canEdit, node, gesture in @@ -730,7 +733,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio }, action: { _, f in f(.default) - let controller = inviteLinkEditController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, invite: invite, completion: { invite in + let controller = inviteLinkEditController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, invite: invite, starsState: starsStats.with( { $0 }), completion: { invite in if let invite = invite { if invite.isRevoked { invitesContext.remove(invite) @@ -897,12 +900,14 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio invitesContext.state, revokedInvitesContext.state, creators, - timerPromise.get() + timerPromise.get(), + starsContext.state ) - |> map { presentationData, exportedInvitation, peer, importersContext, importers, invites, revokedInvites, creators, tick -> (ItemListControllerState, (ItemListNodeState, Any)) in + |> map { presentationData, exportedInvitation, peer, importersContext, importers, invites, revokedInvites, creators, tick, starsState -> (ItemListControllerState, (ItemListNodeState, Any)) in let previousInvites = previousInvites.swap(invites) let previousRevokedInvites = previousRevokedInvites.swap(revokedInvites) let previousCreators = previousCreators.swap(creators) + let _ = starsStats.swap(starsState.stats) var crossfade = false if (previousInvites?.hasLoadedOnce ?? false) != (invites.hasLoadedOnce) { @@ -928,7 +933,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: inviteLinkListControllerEntries(presentationData: presentationData, exportedInvitation: exportedInvitation, peer: peer, invites: invites.hasLoadedOnce ? invites.invitations : nil, revokedInvites: revokedInvites.hasLoadedOnce ? revokedInvites.invitations : nil, importers: importers, creators: creators, admin: admin, tick: tick), style: .blocks, emptyStateItem: nil, crossfadeState: crossfade, animateChanges: animateChanges) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: inviteLinkListControllerEntries(presentationData: presentationData, exportedInvitation: exportedInvitation, peer: peer, invites: invites.hasLoadedOnce ? invites.invitations : nil, revokedInvites: revokedInvites.hasLoadedOnce ? revokedInvites.invitations : nil, importers: importers, creators: creators, admin: admin, tick: tick, starsState: starsState.stats), style: .blocks, emptyStateItem: nil, crossfadeState: crossfade, animateChanges: animateChanges) return (controllerState, (listState, arguments)) } diff --git a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift index a5f19c7534..2ffa88c3f8 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift @@ -48,14 +48,24 @@ private var subscriptionLinkIcon: UIImage? = { class InviteLinkViewInteraction { let context: AccountContext let openPeer: (EnginePeer.Id) -> Void + let openSubscription: (StarsSubscriptionPricing, PeerInvitationImportersState.Importer) -> Void let copyLink: (ExportedInvitation) -> Void let shareLink: (ExportedInvitation) -> Void let editLink: (ExportedInvitation) -> Void let contextAction: (ExportedInvitation, ASDisplayNode, ContextGesture?) -> Void - init(context: AccountContext, openPeer: @escaping (EnginePeer.Id) -> Void, copyLink: @escaping (ExportedInvitation) -> Void, shareLink: @escaping (ExportedInvitation) -> Void, editLink: @escaping (ExportedInvitation) -> Void, contextAction: @escaping (ExportedInvitation, ASDisplayNode, ContextGesture?) -> Void) { + init( + context: AccountContext, + openPeer: @escaping (EnginePeer.Id) -> Void, + openSubscription: @escaping (StarsSubscriptionPricing, PeerInvitationImportersState.Importer) -> Void, + copyLink: @escaping (ExportedInvitation) -> Void, + shareLink: @escaping (ExportedInvitation) -> Void, + editLink: @escaping (ExportedInvitation) -> Void, + contextAction: @escaping (ExportedInvitation, ASDisplayNode, ContextGesture?) -> Void + ) { self.context = context self.openPeer = openPeer + self.openSubscription = openSubscription self.copyLink = copyLink self.shareLink = shareLink self.editLink = editLink @@ -93,7 +103,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { case requestHeader(PresentationTheme, String, String, Bool) case request(Int32, PresentationTheme, PresentationDateTimeFormat, EnginePeer, Int32, Bool) case importerHeader(PresentationTheme, String, String, Bool) - case importer(Int32, PresentationTheme, PresentationDateTimeFormat, EnginePeer, Int32, Bool, Bool) + case importer(Int32, PresentationTheme, PresentationDateTimeFormat, EnginePeer, Int32, Bool, Bool, PeerInvitationImportersState.Importer?, StarsSubscriptionPricing?) var stableId: InviteLinkViewEntryId { switch self { @@ -113,7 +123,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { return .request(peer.id) case .importerHeader: return .importerHeader - case let .importer(_, _, _, peer, _, _, _): + case let .importer(_, _, _, peer, _, _, _, _, _): return .importer(peer.id) } } @@ -168,8 +178,8 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { } else { return false } - case let .importer(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsPeer, lhsDate, lhsJoinedViaFolderLink, lhsLoading): - if case let .importer(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsPeer, rhsDate, rhsJoinedViaFolderLink, rhsLoading) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsPeer == rhsPeer, lhsDate == rhsDate, lhsJoinedViaFolderLink == rhsJoinedViaFolderLink, lhsLoading == rhsLoading { + case let .importer(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsPeer, lhsDate, lhsJoinedViaFolderLink, lhsLoading, lhsImporter, lhsPricing): + if case let .importer(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsPeer, rhsDate, rhsJoinedViaFolderLink, rhsLoading, rhsImporter, rhsPricing) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsPeer == rhsPeer, lhsDate == rhsDate, lhsJoinedViaFolderLink == rhsJoinedViaFolderLink, lhsLoading == rhsLoading, lhsImporter == rhsImporter, lhsPricing == rhsPricing { return true } else { return false @@ -237,11 +247,11 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { case .importer: return true } - case let .importer(lhsIndex, _, _, _, _, _, _): + case let .importer(lhsIndex, _, _, _, _, _, _, _, _): switch rhs { case .link, .subscriptionHeader, .subscriptionPricing, .creatorHeader, .creator, .importerHeader, .request, .requestHeader: return false - case let .importer(rhsIndex, _, _, _, _, _, _): + case let .importer(rhsIndex, _, _, _, _, _, _, _, _): return lhsIndex < rhsIndex } } @@ -250,7 +260,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { func item(account: Account, presentationData: PresentationData, interaction: InviteLinkViewInteraction) -> ListViewItem { switch self { case let .link(_, invite): - return ItemListPermanentInviteLinkItem(context: interaction.context, presentationData: ItemListPresentationData(presentationData), invite: invite, count: 0, peers: [], displayButton: !invite.isRevoked, displayImporters: false, buttonColor: nil, sectionId: 0, style: .plain, copyAction: { + return ItemListPermanentInviteLinkItem(context: interaction.context, presentationData: ItemListPresentationData(presentationData), invite: invite, count: 0, peers: [], displayButton: !invite.isRevoked, separateButtons: true, displayImporters: false, buttonColor: nil, sectionId: 0, style: .plain, copyAction: { interaction.copyLink(invite) }, shareAction: { if invitationAvailability(invite).isZero { @@ -290,15 +300,35 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { additionalText = .none } return SectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title, additionalText: additionalText) - case let .importer(_, _, dateTimeFormat, peer, date, joinedViaFolderLink, loading): + case let .importer(_, _, dateTimeFormat, peer, date, joinedViaFolderLink, loading, importer, pricing): let dateString: String if joinedViaFolderLink { dateString = presentationData.strings.InviteLink_LabelJoinedViaFolder } else { dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat) } - return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .peerList, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: peer.id != account.peerId, sectionId: 0, action: { - interaction.openPeer(peer.id) + + let label: ItemListPeerItemLabel + if let pricing { + //TODO:localize + let text = NSMutableAttributedString() + text.append(NSAttributedString(string: "⭐️\(pricing.amount)\n", font: Font.semibold(17.0), textColor: presentationData.theme.list.itemPrimaryTextColor)) + text.append(NSAttributedString(string: "per month", font: Font.regular(13.0), textColor: presentationData.theme.list.itemSecondaryTextColor)) + if let range = text.string.range(of: "⭐️") { + text.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: text.string)) + text.addAttribute(NSAttributedString.Key.font, value: Font.semibold(15.0), range: NSRange(range, in: text.string)) + text.addAttribute(.baselineOffset, value: 3.5, range: NSRange(range, in: text.string)) + } + label = .attributedText(text) + } else { + label = .none + } + return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .peerList, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: label, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: peer.id != account.peerId, sectionId: 0, action: { + if let importer, let pricing { + interaction.openSubscription(pricing, importer) + } else { + interaction.openPeer(peer.id) + } }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, style: .plain, tag: nil, shimmering: loading ? ItemListPeerItemShimmering(alternationIndex: 0) : nil) case let .request(_, _, dateTimeFormat, peer, date, loading): let dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat) @@ -351,18 +381,20 @@ public final class InviteLinkViewController: ViewController { private let invitationsContext: PeerExportedInvitationsContext? private let revokedInvitationsContext: PeerExportedInvitationsContext? private let importersContext: PeerInvitationImportersContext? + private let starsState: StarsRevenueStats? private var presentationData: PresentationData private var presentationDataDisposable: Disposable? fileprivate var presentationDataPromise = Promise() - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id, invite: ExportedInvitation, invitationsContext: PeerExportedInvitationsContext?, revokedInvitationsContext: PeerExportedInvitationsContext?, importersContext: PeerInvitationImportersContext?) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id, invite: ExportedInvitation, invitationsContext: PeerExportedInvitationsContext?, revokedInvitationsContext: PeerExportedInvitationsContext?, importersContext: PeerInvitationImportersContext?, starsState: StarsRevenueStats? = nil) { self.context = context self.peerId = peerId self.invite = invite self.invitationsContext = invitationsContext self.revokedInvitationsContext = revokedInvitationsContext self.importersContext = importersContext + self.starsState = starsState self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } @@ -550,14 +582,25 @@ public final class InviteLinkViewController: ViewController { self.interaction = InviteLinkViewInteraction(context: context, openPeer: { [weak self] peerId in let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).start(next: { peer in - guard let peer = peer else { + guard let peer else { return } - if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), keepStack: .always)) } }) + }, openSubscription: { [weak self] pricing, importer in + guard let controller = self?.controller else { + return + } + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> deliverOnMainQueue).start(next: { peer in + guard let peer else { + return + } + let subscriptionController = context.sharedContext.makeStarsSubscriptionScreen(context: context, peer: peer, pricing: pricing, importer: importer, usdRate: controller.starsState?.usdRate ?? 0.0) + self?.controller?.push(subscriptionController) + }) }, copyLink: { [weak self] invite in UIPasteboard.general.string = invite.link @@ -766,7 +809,7 @@ public final class InviteLinkViewController: ViewController { }))) } } - + let contextController = ContextController(presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) self?.controller?.presentInGlobalOverlay(contextController) }) @@ -791,6 +834,8 @@ public final class InviteLinkViewController: ViewController { context.account.postbox.loadedPeerWithId(adminId) ) |> deliverOnMainQueue).start(next: { [weak self] presentationData, state, requestsState, creatorPeer in if let strongSelf = self { + let usdRate = strongSelf.controller?.starsState?.usdRate + var entries: [InviteLinkViewEntry] = [] entries.append(.link(presentationData.theme, invite)) @@ -802,7 +847,12 @@ public final class InviteLinkViewController: ViewController { var subtitle = "No one joined yet" if state.count > 0 { title += " x \(state.count)" - subtitle = "You get approximately $\(Float(pricing.amount * Int64(state.count)) * 0.01) monthly" + if let usdRate { + let usdValue = formatTonUsdValue(pricing.amount * Int64(state.count), divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat) + subtitle = "You get approximately \(usdValue) monthly" + } else { + subtitle = "" + } } entries.append(.subscriptionPricing(presentationData.theme, title, subtitle)) } @@ -856,14 +906,14 @@ public final class InviteLinkViewController: ViewController { loading = true let fakeUser = TelegramUser(id: EnginePeer.Id(namespace: .max, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil) for i in 0 ..< count { - entries.append(.importer(Int32(i), presentationData.theme, presentationData.dateTimeFormat, EnginePeer.user(fakeUser), 0, false, true)) + entries.append(.importer(Int32(i), presentationData.theme, presentationData.dateTimeFormat, EnginePeer.user(fakeUser), 0, false, true, nil, nil)) } } else { count = min(4, Int32(state.importers.count)) loading = false for importer in state.importers { if let peer = importer.peer.peer { - entries.append(.importer(index, presentationData.theme, presentationData.dateTimeFormat, EnginePeer(peer), importer.date, importer.joinedViaFolderLink, false)) + entries.append(.importer(index, presentationData.theme, presentationData.dateTimeFormat, EnginePeer(peer), importer.date, importer.joinedViaFolderLink, false, importer, invite.pricing)) } index += 1 } @@ -953,7 +1003,7 @@ public final class InviteLinkViewController: ViewController { let revokedInvitationsContext = parentController.revokedInvitationsContext if let navigationController = navigationController { let updatedPresentationData = (self.presentationData, parentController.presentationDataPromise.get()) - let controller = inviteLinkEditController(context: self.context, updatedPresentationData: updatedPresentationData, peerId: self.peerId, invite: self.invite, completion: { [weak self] invite in + let controller = inviteLinkEditController(context: self.context, updatedPresentationData: updatedPresentationData, peerId: self.peerId, invite: self.invite, starsState: self.controller?.starsState, completion: { [weak self] invite in if let invite = invite { if invite.isRevoked { invitationsContext?.remove(invite) diff --git a/submodules/InviteLinksUI/Sources/InviteRequestsController.swift b/submodules/InviteLinksUI/Sources/InviteRequestsController.swift index 2c5f76a94b..2b592ce5ab 100644 --- a/submodules/InviteLinksUI/Sources/InviteRequestsController.swift +++ b/submodules/InviteLinksUI/Sources/InviteRequestsController.swift @@ -198,7 +198,7 @@ public func inviteRequestsController(context: AccountContext, updatedPresentatio } else { string = presentationData.strings.MemberRequests_UserAddedToGroup(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string } - presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .invitedToVoiceChat(context: context, peer: peer, text: string, action: nil, duration: 3), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .invitedToVoiceChat(context: context, peer: peer, title: nil, text: string, action: nil, duration: 3), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) }) } diff --git a/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift b/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift index 32ee4b6cc2..e305a37971 100644 --- a/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift @@ -45,7 +45,7 @@ private enum ItemBackgroundColor: Equatable { case .blue: return (UIColor(rgb: 0x00b5f7), UIColor(rgb: 0x00b2f6), UIColor(rgb: 0xa7f4ff)) case .green: - return (UIColor(rgb: 0x4aca62), UIColor(rgb: 0x43c85c), UIColor(rgb: 0xc5ffe6)) + return (UIColor(rgb: 0x31b73b), UIColor(rgb: 0x88d93b), UIColor(rgb: 0xc5ffe6)) case .yellow: return (UIColor(rgb: 0xf8a953), UIColor(rgb: 0xf7a64e), UIColor(rgb: 0xfeffd7)) case .red: @@ -208,7 +208,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { self.iconBackgroundNode = ASDisplayNode() self.iconBackgroundNode.setLayerBlock { () -> CALayer in - return CAShapeLayer() + return CAGradientLayer() } self.iconNode = ASImageNode() @@ -283,8 +283,11 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { public override func didLoad() { super.didLoad() - if let shapeLayer = self.iconBackgroundNode.layer as? CAShapeLayer { - shapeLayer.path = UIBezierPath(ovalIn: CGRect(x: 0.0, y: 0.0, width: 40.0, height: 40.0)).cgPath + self.iconBackgroundNode.cornerRadius = 20.0 + if let iconBackgroundLayer = self.iconBackgroundNode.layer as? CAGradientLayer { + iconBackgroundLayer.startPoint = CGPoint(x: 0.0, y: 0.0) + iconBackgroundLayer.endPoint = CGPoint(x: 0.0, y: 1.0) + iconBackgroundLayer.type = .axial } } @@ -344,17 +347,24 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { transitionFraction = 0.0 } - let topColor = color.colors.top - let nextTopColor = nextColor.colors.top - let iconColor: UIColor + let colors = color.colors + let nextColors = nextColor.colors + let topIconColor: UIColor + let bottomIconColor: UIColor if let _ = item.invite { - if case .blue = color { - iconColor = item.presentationData.theme.list.itemAccentColor + if case .green = color, item.invite?.pricing != nil { + topIconColor = color.colors.bottom + bottomIconColor = color.colors.top + } else if case .blue = color { + topIconColor = item.presentationData.theme.list.itemAccentColor + bottomIconColor = topIconColor } else { - iconColor = nextTopColor.mixedWith(topColor, alpha: transitionFraction) + topIconColor = nextColors.top.mixedWith(colors.top, alpha: transitionFraction) + bottomIconColor = topIconColor } } else { - iconColor = item.presentationData.theme.list.mediaPlaceholderColor + topIconColor = item.presentationData.theme.list.mediaPlaceholderColor + bottomIconColor = topIconColor } let inviteLink = item.invite?.link?.replacingOccurrences(of: "https://", with: "") ?? "" @@ -400,7 +410,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { if let range = text.string.range(of: "⭐️") { text.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: text.string)) text.addAttribute(NSAttributedString.Key.font, value: Font.semibold(15.0), range: NSRange(range, in: text.string)) - text.addAttribute(.baselineOffset, value: 2.5, range: NSRange(range, in: text.string)) + text.addAttribute(.baselineOffset, value: 3.5, range: NSRange(range, in: text.string)) } pricingAttributedText = text } @@ -526,8 +536,11 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { } strongSelf.contextSourceNode.contentRect = extractedRect - if let layer = strongSelf.iconBackgroundNode.layer as? CAShapeLayer { - layer.fillColor = iconColor.cgColor + if let iconBackgroundLayer = strongSelf.iconBackgroundNode.layer as? CAGradientLayer { + iconBackgroundLayer.colors = [ + topIconColor.cgColor, + bottomIconColor.cgColor + ] } if let _ = updatedTheme { @@ -633,7 +646,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { strongSelf.timerNode = timerNode strongSelf.offsetContainerNode.addSubnode(timerNode) } - timerNode.update(color: iconColor, value: timerValue) + timerNode.update(color: topIconColor, value: timerValue) } else if let timerNode = strongSelf.timerNode { strongSelf.timerNode = nil timerNode.removeFromSupernode() diff --git a/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift b/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift index 4c0dee3641..35dc5ed1b8 100644 --- a/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift @@ -32,6 +32,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem { let count: Int32 let peers: [EnginePeer] let displayButton: Bool + let separateButtons: Bool let displayImporters: Bool let buttonColor: UIColor? public let sectionId: ItemListSectionId @@ -49,6 +50,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem { count: Int32, peers: [EnginePeer], displayButton: Bool, + separateButtons: Bool = false, displayImporters: Bool, buttonColor: UIColor?, sectionId: ItemListSectionId, @@ -65,6 +67,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem { self.count = count self.peers = peers self.displayButton = displayButton + self.separateButtons = separateButtons self.displayImporters = displayImporters self.buttonColor = buttonColor self.sectionId = sectionId @@ -126,6 +129,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem private let addressButtonNode: HighlightTrackingButtonNode private let addressButtonIconNode: ASImageNode private var addressShimmerNode: ShimmerEffectNode? + private var copyButtonNode: SolidRoundedButtonNode? private var shareButtonNode: SolidRoundedButtonNode? private let avatarsButtonNode: HighlightTrackingButtonNode @@ -234,6 +238,11 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem } } } + self.copyButtonNode?.pressed = { [weak self] in + if let strongSelf = self, let item = strongSelf.item { + item.copyAction?() + } + } self.shareButtonNode?.pressed = { [weak self] in if let strongSelf = self, let item = strongSelf.item { item.shareAction?() @@ -444,7 +453,31 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem strongSelf.addressButtonNode.isHidden = item.contextAction == nil strongSelf.addressButtonIconNode.isHidden = item.contextAction == nil - + + var effectiveSeparateButtons = item.separateButtons + if let invite = item.invite, invitationAvailability(invite).isZero { + effectiveSeparateButtons = false + } + + let copyButtonNode: SolidRoundedButtonNode + if let currentCopyButtonNode = strongSelf.copyButtonNode { + copyButtonNode = currentCopyButtonNode + } else { + let buttonTheme: SolidRoundedButtonTheme + if let buttonColor = item.buttonColor { + buttonTheme = SolidRoundedButtonTheme(backgroundColor: buttonColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor) + } else { + buttonTheme = SolidRoundedButtonTheme(theme: item.presentationData.theme) + } + copyButtonNode = SolidRoundedButtonNode(theme: buttonTheme, height: 50.0, cornerRadius: 11.0) + copyButtonNode.title = item.presentationData.strings.InviteLink_CopyShort + copyButtonNode.pressed = { [weak self] in + self?.item?.copyAction?() + } + strongSelf.addSubnode(copyButtonNode) + strongSelf.copyButtonNode = copyButtonNode + } + let shareButtonNode: SolidRoundedButtonNode if let currentShareButtonNode = strongSelf.shareButtonNode { shareButtonNode = currentShareButtonNode @@ -459,7 +492,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem if let invite = item.invite, invitationAvailability(invite).isZero { shareButtonNode.title = item.presentationData.strings.InviteLink_ReactivateLink } else { - shareButtonNode.title = item.presentationData.strings.InviteLink_Share + shareButtonNode.title = effectiveSeparateButtons ? item.presentationData.strings.InviteLink_ShareShort : item.presentationData.strings.InviteLink_Share } shareButtonNode.pressed = { [weak self] in self?.item?.shareAction?() @@ -468,9 +501,19 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem strongSelf.shareButtonNode = shareButtonNode } - let buttonWidth = contentSize.width - leftInset - rightInset + let buttonSpacing: CGFloat = 8.0 + var buttonWidth = contentSize.width - leftInset - rightInset + var shareButtonOriginX = leftInset + if effectiveSeparateButtons { + buttonWidth = (buttonWidth - buttonSpacing) / 2.0 + shareButtonOriginX = leftInset + buttonWidth + buttonSpacing + } + + let _ = copyButtonNode.updateLayout(width: buttonWidth, transition: .immediate) + copyButtonNode.frame = CGRect(x: leftInset, y: verticalInset + fieldHeight + fieldSpacing, width: buttonWidth, height: buttonHeight) + let _ = shareButtonNode.updateLayout(width: buttonWidth, transition: .immediate) - shareButtonNode.frame = CGRect(x: leftInset, y: verticalInset + fieldHeight + fieldSpacing, width: buttonWidth, height: buttonHeight) + shareButtonNode.frame = CGRect(x: shareButtonOriginX, y: verticalInset + fieldHeight + fieldSpacing, width: buttonWidth, height: buttonHeight) var totalWidth = invitedPeersLayout.size.width var leftOrigin: CGFloat = floorToScreenPixels((params.width - invitedPeersLayout.size.width) / 2.0) @@ -498,9 +541,15 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem strongSelf.fieldButtonNode.isUserInteractionEnabled = item.invite != nil strongSelf.addressButtonIconNode.alpha = item.invite != nil ? 1.0 : 0.0 + + strongSelf.copyButtonNode?.isUserInteractionEnabled = item.invite != nil + strongSelf.copyButtonNode?.alpha = item.invite != nil ? 1.0 : 0.4 + strongSelf.copyButtonNode?.isHidden = !item.displayButton || !effectiveSeparateButtons + strongSelf.shareButtonNode?.isUserInteractionEnabled = item.invite != nil strongSelf.shareButtonNode?.alpha = item.invite != nil ? 1.0 : 0.4 strongSelf.shareButtonNode?.isHidden = !item.displayButton + strongSelf.avatarsButtonNode.isHidden = !item.displayImporters strongSelf.avatarsNode.isHidden = !item.displayImporters || item.invite == nil strongSelf.invitedPeersNode.isHidden = !item.displayImporters || item.invite == nil diff --git a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift index f3e178a2fe..d4e18d0bbe 100644 --- a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift +++ b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift @@ -251,6 +251,7 @@ public enum ItemListPeerItemLabel { case text(String, ItemListPeerItemLabelFont) case disclosure(String) case badge(String) + case attributedText(NSAttributedString) } public struct ItemListPeerItemSwitch { @@ -728,7 +729,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo private var avatarButton: HighlightTrackingButton? private let titleNode: TextNode - private let labelNode: TextNode + private let labelNode: TextNodeWithEntities private let labelBadgeNode: ASImageNode private var labelArrowNode: ASImageNode? private let statusNode: TextNode @@ -829,10 +830,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo self.statusNode.contentMode = .left self.statusNode.contentsScale = UIScreen.main.scale - self.labelNode = TextNode() - self.labelNode.isUserInteractionEnabled = false - self.labelNode.contentMode = .left - self.labelNode.contentsScale = UIScreen.main.scale + self.labelNode = TextNodeWithEntities() self.labelBadgeNode = ASImageNode() self.labelBadgeNode.displayWithoutProcessing = true @@ -850,7 +848,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo self.containerNode.addSubnode(self.avatarNode) self.containerNode.addSubnode(self.titleNode) self.containerNode.addSubnode(self.statusNode) - self.containerNode.addSubnode(self.labelNode) + self.containerNode.addSubnode(self.labelNode.textNode) self.peerPresenceManager = PeerPresenceStatusManager(update: { [weak self] in if let strongSelf = self, let layoutParams = strongSelf.layoutParams { @@ -885,7 +883,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo public func asyncLayout() -> (_ item: ItemListPeerItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors, _ headerAtTop: Bool) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeStatusLayout = TextNode.asyncLayout(self.statusNode) - let makeLabelLayout = TextNode.asyncLayout(self.labelNode) + let makeLabelLayout = TextNodeWithEntities.asyncLayout(self.labelNode) let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode) let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode) @@ -1156,42 +1154,49 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo editingOffset = 0.0 } + var labelMaximumNumberOfLines = 1 var labelInset: CGFloat = 0.0 + var labelAlignment: NSTextAlignment = .natural var updatedLabelArrowNode: ASImageNode? switch item.label { - case .none: - break - case let .text(text, font): - let selectedFont: UIFont - switch font { - case .standard: - selectedFont = labelFont - case let .custom(value): - selectedFont = value - } - labelAttributedString = NSAttributedString(string: text, font: selectedFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) - labelInset += 15.0 - case let .disclosure(text): - if let currentLabelArrowNode = currentLabelArrowNode { - updatedLabelArrowNode = currentLabelArrowNode - } else { - let arrowNode = ASImageNode() - arrowNode.isLayerBacked = true - arrowNode.displayWithoutProcessing = true - arrowNode.displaysAsynchronously = false - arrowNode.image = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme) - updatedLabelArrowNode = arrowNode - } - labelInset += 40.0 - labelAttributedString = NSAttributedString(string: text, font: labelDisclosureFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) - case let .badge(text): - labelAttributedString = NSAttributedString(string: text, font: badgeFont, textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor) - labelInset += 15.0 + case .none: + break + case let .attributedText(text): + labelAttributedString = text + labelInset += 15.0 + labelMaximumNumberOfLines = 2 + labelAlignment = .right + case let .text(text, font): + let selectedFont: UIFont + switch font { + case .standard: + selectedFont = labelFont + case let .custom(value): + selectedFont = value + } + labelAttributedString = NSAttributedString(string: text, font: selectedFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) + labelInset += 15.0 + case let .disclosure(text): + if let currentLabelArrowNode = currentLabelArrowNode { + updatedLabelArrowNode = currentLabelArrowNode + } else { + let arrowNode = ASImageNode() + arrowNode.isLayerBacked = true + arrowNode.displayWithoutProcessing = true + arrowNode.displaysAsynchronously = false + arrowNode.image = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme) + updatedLabelArrowNode = arrowNode + } + labelInset += 40.0 + labelAttributedString = NSAttributedString(string: text, font: labelDisclosureFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) + case let .badge(text): + labelAttributedString = NSAttributedString(string: text, font: badgeFont, textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor) + labelInset += 15.0 } labelInset += reorderInset - let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: labelAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 16.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: labelAttributedString, backgroundColor: nil, maximumNumberOfLines: labelMaximumNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 16.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: labelAlignment, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) let constrainedTitleSize = CGSize(width: params.width - leftInset - 12.0 - editingOffset - rightInset - labelLayout.size.width - labelInset - titleIconsWidth, height: CGFloat.greatestFiniteMagnitude) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: constrainedTitleSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) @@ -1351,9 +1356,10 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo let _ = titleApply() let _ = statusApply() - let _ = labelApply() - - strongSelf.labelNode.isHidden = labelAttributedString == nil + if case let .account(context) = item.context { + let _ = labelApply(TextNodeWithEntities.Arguments(context: context, cache: item.context.animationCache, renderer: item.context.animationRenderer, placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, attemptSynchronous: false)) + } + strongSelf.labelNode.textNode.isHidden = labelAttributedString == nil if strongSelf.backgroundNode.supernode == nil { strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) @@ -1496,15 +1502,15 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo let labelFrame: CGRect if case .badge = item.label { labelFrame = CGRect(origin: CGPoint(x: revealOffset + params.width - rightLabelInset - badgeWidth + (badgeWidth - labelLayout.size.width) / 2.0, y: floor((contentSize.height - labelLayout.size.height) / 2.0) + 1.0), size: labelLayout.size) - strongSelf.labelNode.frame = labelFrame + strongSelf.labelNode.textNode.frame = labelFrame } else { labelFrame = CGRect(origin: CGPoint(x: revealOffset + params.width - labelLayout.size.width - rightLabelInset, y: floor((contentSize.height - labelLayout.size.height) / 2.0) + 1.0), size: labelLayout.size) - transition.updateFrame(node: strongSelf.labelNode, frame: labelFrame) + transition.updateFrame(node: strongSelf.labelNode.textNode, frame: labelFrame) } if let updateBadgeImage = updatedLabelBadgeImage { if strongSelf.labelBadgeNode.supernode == nil { - strongSelf.containerNode.insertSubnode(strongSelf.labelBadgeNode, belowSubnode: strongSelf.labelNode) + strongSelf.containerNode.insertSubnode(strongSelf.labelBadgeNode, belowSubnode: strongSelf.labelNode.textNode) } strongSelf.labelBadgeNode.image = updateBadgeImage } @@ -1853,16 +1859,16 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo } let badgeDiameter: CGFloat = 20.0 - let labelSize = self.labelNode.frame.size + let labelSize = self.labelNode.textNode.frame.size let badgeWidth = max(badgeDiameter, labelSize.width + 10.0) let labelFrame: CGRect if case .badge = item.label { - labelFrame = CGRect(origin: CGPoint(x: offset + params.width - rightLabelInset - badgeWidth + (badgeWidth - labelSize.width) / 2.0, y: self.labelNode.frame.minY), size: labelSize) + labelFrame = CGRect(origin: CGPoint(x: offset + params.width - rightLabelInset - badgeWidth + (badgeWidth - labelSize.width) / 2.0, y: self.labelNode.textNode.frame.minY), size: labelSize) } else { - labelFrame = CGRect(origin: CGPoint(x: offset + params.width - self.labelNode.bounds.size.width - rightLabelInset, y: self.labelNode.frame.minY), size: self.labelNode.bounds.size) + labelFrame = CGRect(origin: CGPoint(x: offset + params.width - self.labelNode.textNode.bounds.size.width - rightLabelInset, y: self.labelNode.textNode.frame.minY), size: self.labelNode.textNode.bounds.size) } - transition.updateFrame(node: self.labelNode, frame: labelFrame) + transition.updateFrame(node: self.labelNode.textNode, frame: labelFrame) transition.updateFrame(node: self.labelBadgeNode, frame: CGRect(origin: CGPoint(x: offset + params.width - rightLabelInset - badgeWidth, y: self.labelBadgeNode.frame.minY), size: CGSize(width: badgeWidth, height: badgeDiameter))) diff --git a/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift b/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift index dfbc841be4..4aaaf7df8d 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift @@ -50,6 +50,7 @@ public class ItemListSingleLineInputItem: ListViewItem, ItemListItem { let title: NSAttributedString let text: String let placeholder: String + let label: String? let type: ItemListSingleLineInputItemType let returnKeyType: UIReturnKeyType let alignment: ItemListSingleLineInputAlignment @@ -68,12 +69,13 @@ public class ItemListSingleLineInputItem: ListViewItem, ItemListItem { let cleared: (() -> Void)? public let tag: ItemListItemTag? - public init(context: AccountContext? = nil, presentationData: ItemListPresentationData, title: NSAttributedString, text: String, placeholder: String, type: ItemListSingleLineInputItemType = .regular(capitalization: true, autocorrection: true), returnKeyType: UIReturnKeyType = .`default`, alignment: ItemListSingleLineInputAlignment = .default, spacing: CGFloat = 0.0, clearType: ItemListSingleLineInputClearType = .none, maxLength: Int = 0, enabled: Bool = true, selectAllOnFocus: Bool = false, secondaryStyle: Bool = false, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> String)? = nil, updatedFocus: ((Bool) -> Void)? = nil, action: @escaping () -> Void, cleared: (() -> Void)? = nil) { + public init(context: AccountContext? = nil, presentationData: ItemListPresentationData, title: NSAttributedString, text: String, placeholder: String, label: String? = nil, type: ItemListSingleLineInputItemType = .regular(capitalization: true, autocorrection: true), returnKeyType: UIReturnKeyType = .`default`, alignment: ItemListSingleLineInputAlignment = .default, spacing: CGFloat = 0.0, clearType: ItemListSingleLineInputClearType = .none, maxLength: Int = 0, enabled: Bool = true, selectAllOnFocus: Bool = false, secondaryStyle: Bool = false, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> String)? = nil, updatedFocus: ((Bool) -> Void)? = nil, action: @escaping () -> Void, cleared: (() -> Void)? = nil) { self.context = context self.presentationData = presentationData self.title = title self.text = text self.placeholder = placeholder + self.label = label self.type = type self.returnKeyType = returnKeyType self.alignment = alignment diff --git a/submodules/StatisticsUI/Sources/StarsTransactionItem.swift b/submodules/StatisticsUI/Sources/StarsTransactionItem.swift index 43c3daf13a..aadd16c894 100644 --- a/submodules/StatisticsUI/Sources/StarsTransactionItem.swift +++ b/submodules/StatisticsUI/Sources/StarsTransactionItem.swift @@ -239,6 +239,7 @@ final class StarsTransactionItemNode: ListViewItemNode, ItemListItemNode { itemSubtitle = peer.displayTitle(strings: item.presentationData.strings, displayOrder: .firstLast) } else { if let _ = item.transaction.subscriptionPeriod { + //TODO:localize itemSubtitle = "Monthly subscription fee" } else { itemSubtitle = nil @@ -276,9 +277,15 @@ final class StarsTransactionItemNode: ListViewItemNode, ItemListItemNode { } itemLabel = NSAttributedString(string: labelString, font: Font.medium(fontBaseDisplaySize), textColor: labelString.hasPrefix("-") ? item.presentationData.theme.list.itemDestructiveColor : item.presentationData.theme.list.itemDisclosureActions.constructive.fillColor) + var itemDateColor = item.presentationData.theme.list.itemSecondaryTextColor itemDate = stringForMediumCompactDate(timestamp: item.transaction.date, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat) if item.transaction.flags.contains(.isRefund) { itemDate += " – \(item.presentationData.strings.Stars_Intro_Transaction_Refund)" + } else if item.transaction.flags.contains(.isPending) { + itemDate += " – \(item.presentationData.strings.Monetization_Transaction_Pending)" + } else if item.transaction.flags.contains(.isFailed) { + itemDate += " – \(item.presentationData.strings.Monetization_Transaction_Failed)" + itemDateColor = item.presentationData.theme.list.itemDestructiveColor } var titleComponents: [AnyComponentWithIdentity] = [] @@ -309,7 +316,7 @@ final class StarsTransactionItemNode: ListViewItemNode, ItemListItemNode { text: .plain(NSAttributedString( string: itemDate, font: Font.regular(floor(fontBaseDisplaySize * 14.0 / 17.0)), - textColor: item.presentationData.theme.list.itemSecondaryTextColor + textColor: itemDateColor )), maximumNumberOfLines: 1 ))) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index b4a2ac7239..3f8d31e7b1 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -1254,7 +1254,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController } else { text = strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string } - strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: EnginePeer(participant.peer), text: text, action: nil, duration: 3), action: { _ in return false }) + strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: EnginePeer(participant.peer), title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) } } else { if case let .channel(groupPeer) = groupPeer, let listenerLink = inviteLinks?.listenerLink, !groupPeer.hasPermission(.inviteMembers) { @@ -1362,7 +1362,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController } else { text = strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string } - strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: text, action: nil, duration: 3), action: { _ in return false }) + strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) } })) } else if case let .legacyGroup(groupPeer) = groupPeer { @@ -1430,7 +1430,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController } else { text = strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string } - strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: text, action: nil, duration: 3), action: { _ in return false }) + strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) } })) } @@ -2262,7 +2262,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController return } let text = strongSelf.presentationData.strings.VoiceChat_PeerJoinedText(event.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string - strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: event.peer, text: text, action: nil, duration: 3), action: { _ in return false }) + strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: event.peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) } })) @@ -2277,7 +2277,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController } else { text = strongSelf.presentationData.strings.VoiceChat_DisplayAsSuccess(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string } - strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: text, action: nil, duration: 3), action: { _ in return false }) + strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) })) self.stateVersionDisposable.set((self.call.stateVersion diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index cf5743da41..c92d845045 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -956,10 +956,6 @@ private final class StarsSubscriptionsContextImpl { updatedState.isLoading = false updatedState.canLoadMore = self.nextOffset != nil self.updateState(updatedState) - - if updatedState.canLoadMore { - self.loadMore() - } })) } @@ -967,6 +963,22 @@ private final class StarsSubscriptionsContextImpl { self._state = state self._statePromise.set(.single(state)) } + + func updateSubscription(id: String, cancel: Bool) { + var updatedState = self._state + if let index = updatedState.subscriptions.firstIndex(where: { $0.id == id }) { + let subscription = updatedState.subscriptions[index] + var updatedFlags = subscription.flags + if cancel { + updatedFlags.insert(.isCancelled) + } else { + updatedFlags.remove(.isCancelled) + } + let updatedSubscription = StarsContext.State.Subscription(flags: updatedFlags, id: subscription.id, peer: subscription.peer, untilDate: subscription.untilDate, pricing: subscription.pricing) + updatedState.subscriptions[index] = updatedSubscription + } + self.updateState(updatedState) + } } public final class StarsSubscriptionsContext { @@ -1007,6 +1019,12 @@ public final class StarsSubscriptionsContext { return StarsSubscriptionsContextImpl(account: account, starsContext: starsContext) }) } + + public func updateSubscription(id: String, cancel: Bool) { + self.impl.with { + $0.updateSubscription(id: id, cancel: cancel) + } + } } @@ -1203,7 +1221,7 @@ func _internal_updateStarsSubscription(account: Account, peerId: EnginePeer.Id, return .complete() } let flags: Int32 = (1 << 0) - return account.network.request(Api.functions.payments.changeStarsSubscription(flags: flags, peer: inputPeer, subscriptionId: subscriptionId, canceled: .boolTrue)) + return account.network.request(Api.functions.payments.changeStarsSubscription(flags: flags, peer: inputPeer, subscriptionId: subscriptionId, canceled: cancel ? .boolTrue : .boolFalse)) |> mapError { _ -> UpdateStarsSubsciptionError in return .generic } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift index 9fd972a675..1f67bde1b4 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift @@ -510,6 +510,10 @@ public extension EnginePeer { var isPremium: Bool { return self._asPeer().isPremium } + + var isSubscription: Bool { + return self._asPeer().isSubscription + } var isService: Bool { if case let .user(peer) = self { diff --git a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift index e51dc63acd..99b15c30c6 100644 --- a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift @@ -191,6 +191,15 @@ public extension Peer { } } + var isSubscription: Bool { + switch self { + case let channel as TelegramChannel: + return channel.subscriptionUntilDate != nil + default: + return false + } + } + var isCloseFriend: Bool { switch self { case let user as TelegramUser: diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index 6442a231f7..4d91e53e12 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -905,7 +905,7 @@ private let starImage: UIImage? = { context.clear(CGRect(origin: .zero, size: size)) if let image = UIImage(bundleImageName: "Premium/Stars/StarLarge"), let cgImage = image.cgImage { - context.draw(cgImage, in: CGRect(origin: .zero, size: size).insetBy(dx: 4.0, dy: 4.0), byTiling: false) + context.draw(cgImage, in: CGRect(origin: .zero, size: size).insetBy(dx: 1.0, dy: 1.0), byTiling: false) } })?.withRenderingMode(.alwaysTemplate) }() diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 98b14e1ed8..cabc8f1b89 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -9232,7 +9232,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro case .fallback: (strongSelf.controller?.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: nil, text: strongSelf.presentationData.strings.Privacy_ProfilePhoto_PublicPhotoSuccess, round: true, undoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) case .custom: - strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_SuccessPhotoText(peer.compactDisplayTitle).string, action: nil, duration: 5), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, title: nil, text: strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_SuccessPhotoText(peer.compactDisplayTitle).string, action: nil, duration: 5), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) let _ = (strongSelf.context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, peerId: strongSelf.peerId, fetch: peerInfoProfilePhotos(context: strongSelf.context, peerId: strongSelf.peerId)) |> ignoreValues).startStandalone() case .suggest: @@ -9469,7 +9469,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro case .fallback: (strongSelf.controller?.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: nil, text: strongSelf.presentationData.strings.Privacy_ProfilePhoto_PublicVideoSuccess, round: true, undoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) case .custom: - strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_SuccessVideoText(peer.compactDisplayTitle).string, action: nil, duration: 5), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, title: nil, text: strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_SuccessVideoText(peer.compactDisplayTitle).string, action: nil, duration: 5), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) let _ = (strongSelf.context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, peerId: strongSelf.peerId, fetch: peerInfoProfilePhotos(context: strongSelf.context, peerId: strongSelf.peerId)) |> ignoreValues).startStandalone() case .suggest: diff --git a/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift b/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift index 1783c702ff..72e92b950f 100644 --- a/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift @@ -828,22 +828,35 @@ public final class StarsImageComponent: Component { if let _ = component.icon { let smallIconView: UIImageView - if let current = self.smallIconView { + let smallIconOutlineView: UIImageView + if let current = self.smallIconView, let currentOutline = self.smallIconOutlineView { smallIconView = current + smallIconOutlineView = currentOutline } else { + smallIconOutlineView = UIImageView() + containerNode.view.addSubview(smallIconOutlineView) + self.smallIconOutlineView = smallIconOutlineView + smallIconView = UIImageView() containerNode.view.addSubview(smallIconView) + self.smallIconView = smallIconView + + smallIconOutlineView.image = UIImage(bundleImageName: "Premium/Stars/TransactionStarOutline")?.withRenderingMode(.alwaysTemplate) + smallIconView.image = UIImage(bundleImageName: "Premium/Stars/TransactionStar") } - smallIconView.image = UIImage(bundleImageName: "Premium/Stars/MockBigStar") + smallIconOutlineView.tintColor = component.backgroundColor if let icon = smallIconView.image { let smallIconFrame = CGRect(origin: CGPoint(x: imageFrame.maxX - icon.size.width, y: imageFrame.maxY - icon.size.height), size: icon.size) smallIconView.frame = smallIconFrame + smallIconOutlineView.frame = smallIconFrame } - } else if let smallIconView = self.smallIconView { + } else if let smallIconView = self.smallIconView, let smallIconOutlineView = self.smallIconOutlineView { self.smallIconView = nil smallIconView.removeFromSuperview() + self.smallIconOutlineView = nil + smallIconOutlineView.removeFromSuperview() } if let _ = component.action { diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index 4b53650c0c..21e5cf1eec 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -36,6 +36,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { let openMedia: ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void let openAppExamples: () -> Void let copyTransactionId: (String) -> Void + let updateSubscription: (StarsTransactionScreen.SubscriptionAction) -> Void init( context: AccountContext, @@ -45,7 +46,8 @@ private final class StarsTransactionSheetContent: CombinedComponent { openMessage: @escaping (EngineMessage.Id) -> Void, openMedia: @escaping ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void, openAppExamples: @escaping () -> Void, - copyTransactionId: @escaping (String) -> Void + copyTransactionId: @escaping (String) -> Void, + updateSubscription: @escaping (StarsTransactionScreen.SubscriptionAction) -> Void ) { self.context = context self.subject = subject @@ -55,6 +57,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { self.openMedia = openMedia self.openAppExamples = openAppExamples self.copyTransactionId = copyTransactionId + self.updateSubscription = updateSubscription } static func ==(lhs: StarsTransactionSheetContent, rhs: StarsTransactionSheetContent) -> Bool { @@ -96,6 +99,8 @@ private final class StarsTransactionSheetContent: CombinedComponent { peerIds.append(message.id.peerId) case let .subscription(subscription): peerIds.append(subscription.peer.id) + case let .importer(_, _, importer, _): + peerIds.append(importer.peer.peerId) } self.disposable = (context.engine.data.get( @@ -138,10 +143,11 @@ private final class StarsTransactionSheetContent: CombinedComponent { let description = Child(MultilineTextComponent.self) let table = Child(TableComponent.self) let additional = Child(BalancedTextComponent.self) + let status = Child(BalancedTextComponent.self) let button = Child(SolidRoundedButtonComponent.self) - let refundBackgound = Child(RoundedRectangle.self) - let refundText = Child(MultilineTextComponent.self) + let transactionStatusBackgound = Child(RoundedRectangle.self) + let transactionStatusText = Child(MultilineTextComponent.self) let spaceRegex = try? NSRegularExpression(pattern: "\\[(.*?)\\]", options: []) @@ -182,8 +188,11 @@ private final class StarsTransactionSheetContent: CombinedComponent { let titleText: String let amountText: String var descriptionText: String - let additionalText: String - let buttonText: String + let additionalText = strings.Stars_Transaction_Terms + var buttonText: String? = strings.Common_OK + var buttonIsDestructive = false + var statusText: String? + var statusIsDestructive = false let count: Int64 var countIsGeneric = false @@ -196,13 +205,28 @@ private final class StarsTransactionSheetContent: CombinedComponent { let transactionPeer: StarsContext.State.Transaction.Peer? var media: [AnyMediaReference] = [] var photo: TelegramMediaWebFile? - var isRefund = false + var transactionStatus: (String, UIColor)? = nil var isGift = false var isSubscription = false + var isSubscriber = false var isSubscriptionFee = false + var isCancelled = false var delayedCloseOnOpenPeer = true switch subject { + case let .importer(peer, pricing, importer, usdRate): + let usdValue = formatTonUsdValue(pricing.amount, divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat) + titleText = "Subscription" + descriptionText = "appx. \(usdValue) per month" + count = pricing.amount + countOnTop = true + transactionId = nil + date = importer.date + via = nil + messageId = nil + toPeer = importer.peer.peer.flatMap(EnginePeer.init) + transactionPeer = .peer(peer) + isSubscriber = true case let .subscription(subscription): titleText = "Subscription" descriptionText = "" @@ -214,6 +238,17 @@ private final class StarsTransactionSheetContent: CombinedComponent { toPeer = subscription.peer transactionPeer = .peer(subscription.peer) isSubscription = true + + if subscription.flags.contains(.isCancelled) { + statusText = "You have cancelled your subscription" + statusIsDestructive = true + buttonText = "Renew Subscription" + isCancelled = true + } else { + statusText = "If you cancel now, you can still access your subscription until \(stringForMediumDate(timestamp: subscription.untilDate, strings: strings, dateTimeFormat: dateTimeFormat, withTime: false))" + buttonText = "Cancel Subscription" + buttonIsDestructive = true + } case let .transaction(transaction, parentPeer): if let _ = transaction.subscriptionPeriod { //TODO:localize @@ -325,7 +360,12 @@ private final class StarsTransactionSheetContent: CombinedComponent { transactionPeer = transaction.peer media = transaction.media.map { AnyMediaReference.starsTransaction(transaction: StarsTransactionReference(peerId: parentPeer.id, id: transaction.id, isRefund: transaction.flags.contains(.isRefund)), media: $0) } photo = transaction.photo - isRefund = transaction.flags.contains(.isRefund) + + if transaction.flags.contains(.isRefund) { + transactionStatus = (strings.Stars_Transaction_Refund, theme.list.itemDisclosureActions.constructive.fillColor) + } else if transaction.flags.contains(.isPending) { + transactionStatus = (strings.Monetization_Transaction_Pending, theme.list.itemDisclosureActions.warning.fillColor) + } } case let .receipt(receipt): titleText = receipt.invoiceMedia.title @@ -386,7 +426,10 @@ private final class StarsTransactionSheetContent: CombinedComponent { let formattedAmount = presentationStringsFormattedNumber(abs(Int32(count)), dateTimeFormat.groupingSeparator) let countColor: UIColor - if countIsGeneric { + if isSubscription || isSubscriber { + amountText = "\(formattedAmount) / month" + countColor = theme.list.itemSecondaryTextColor + } else if countIsGeneric { amountText = "\(formattedAmount)" countColor = theme.list.itemPrimaryTextColor } else if count < 0 { @@ -396,8 +439,6 @@ private final class StarsTransactionSheetContent: CombinedComponent { amountText = "+ \(formattedAmount)" countColor = theme.list.itemDisclosureActions.constructive.fillColor } - additionalText = strings.Stars_Transaction_Terms - buttonText = strings.Common_OK let title = title.update( component: MultilineTextComponent( @@ -429,7 +470,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { } else { imageSubject = .none } - if isSubscription || isSubscriptionFee { + if isSubscription || isSubscriber || isSubscriptionFee { imageIcon = .star } else { imageIcon = nil @@ -450,7 +491,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { transition: .immediate ) - let amountAttributedText = NSMutableAttributedString(string: amountText, font: Font.semibold(17.0), textColor: countColor) + let amountAttributedText = NSMutableAttributedString(string: amountText, font: isSubscription || isSubscriber ? Font.regular(17.0) : Font.semibold(17.0), textColor: countColor) let amount = amount.update( component: BalancedTextComponent( text: .plain(amountAttributedText), @@ -500,9 +541,17 @@ private final class StarsTransactionSheetContent: CombinedComponent { ) )) } else if let toPeer { + let title: String + if isSubscription { + title = "Subscription" + } else if isSubscriber { + title = "Subscriber" + } else { + title = count < 0 || countIsGeneric ? strings.Stars_Transaction_To : strings.Stars_Transaction_From + } tableItems.append(.init( id: "to", - title: count < 0 || countIsGeneric ? strings.Stars_Transaction_To : strings.Stars_Transaction_From, + title: title, component: AnyComponent( Button( content: AnyComponent( @@ -594,9 +643,25 @@ private final class StarsTransactionSheetContent: CombinedComponent { )) } + let dateTitle: String + if isSubscription { + if isCancelled { + if date > Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) { + dateTitle = "Expires" + } else { + dateTitle = "Expired" + } + } else { + dateTitle = "Renews" + } + } else if isSubscriber { + dateTitle = "Subscribed" + } else { + dateTitle = strings.Stars_Transaction_Date + } tableItems.append(.init( id: "date", - title: strings.Stars_Transaction_Date, + title: dateTitle, component: AnyComponent( MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) ) @@ -615,6 +680,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { let boldTextFont = Font.semibold(15.0) let textColor = theme.actionSheet.secondaryTextColor let linkColor = theme.actionSheet.controlAccentColor + let destructiveColor = theme.actionSheet.destructiveActionTextColor let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) }) @@ -623,7 +689,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { text: .markdown(text: additionalText, attributes: markdownAttributes), horizontalAlignment: .center, maximumNumberOfLines: 0, - lineSpacing: 0.1, + lineSpacing: 0.2, highlightColor: linkColor.withAlphaComponent(0.2), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { @@ -643,28 +709,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), transition: .immediate ) - - let button = button.update( - component: SolidRoundedButtonComponent( - title: buttonText, - theme: SolidRoundedButtonComponent.Theme(theme: theme), - font: .bold, - fontSize: 17.0, - height: 50.0, - cornerRadius: 10.0, - gloss: false, - iconName: nil, - animationName: nil, - iconPosition: .left, - isLoading: state.inProgress, - action: { - component.cancel(true) - } - ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), - transition: context.transition - ) - + context.add(title .position(CGPoint(x: context.availableSize.width / 2.0, y: 31.0 + 125.0)) ) @@ -684,8 +729,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme) } - let textFont = Font.regular(15.0) - let textColor = countOnTop ? theme.list.itemPrimaryTextColor : textColor + let textColor = countOnTop && !isSubscriber ? theme.list.itemPrimaryTextColor : textColor let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) }) @@ -728,21 +772,21 @@ private final class StarsTransactionSheetContent: CombinedComponent { let amountSpacing: CGFloat = 1.0 var totalAmountWidth: CGFloat = amount.size.width + amountSpacing + amountStar.size.width var amountOriginX: CGFloat = floor(context.availableSize.width - totalAmountWidth) / 2.0 - if isRefund { - let refundText = refundText.update( + if let (statusText, statusColor) = transactionStatus { + let refundText = transactionStatusText.update( component: MultilineTextComponent( text: .plain(NSAttributedString( - string: strings.Stars_Transaction_Refund, + string: statusText, font: Font.medium(14.0), - textColor: theme.list.itemDisclosureActions.constructive.fillColor + textColor: statusColor )) ), availableSize: context.availableSize, transition: .immediate ) - let refundBackground = refundBackgound.update( + let refundBackground = transactionStatusBackgound.update( component: RoundedRectangle( - color: theme.list.itemDisclosureActions.constructive.fillColor.withAlphaComponent(0.1), + color: statusColor.withAlphaComponent(0.1), cornerRadius: 6.0 ), availableSize: CGSize(width: refundText.size.width + 10.0, height: refundText.size.height + 4.0), @@ -766,11 +810,22 @@ private final class StarsTransactionSheetContent: CombinedComponent { } else { originY += amount.size.height + 20.0 } + + let amountLabelOriginX: CGFloat + let amountStarOriginX: CGFloat + if isSubscription || isSubscriber { + amountStarOriginX = amountOriginX + amountStar.size.width / 2.0 + amountLabelOriginX = amountOriginX + amountStar.size.width + amountSpacing + amount.size.width / 2.0 + } else { + amountLabelOriginX = amountOriginX + amount.size.width / 2.0 + amountStarOriginX = amountOriginX + amount.size.width + amountSpacing + amountStar.size.width / 2.0 + } + context.add(amount - .position(CGPoint(x: amountOriginX + amount.size.width / 2.0, y: amountOrigin + amount.size.height / 2.0)) + .position(CGPoint(x: amountLabelOriginX, y: amountOrigin + amount.size.height / 2.0)) ) context.add(amountStar - .position(CGPoint(x: amountOriginX + amount.size.width + amountSpacing + amountStar.size.width / 2.0, y: amountOrigin + amountStar.size.height / 2.0)) + .position(CGPoint(x: amountStarOriginX, y: amountOrigin + amountStar.size.height / 2.0 - UIScreenPixel)) ) context.add(table @@ -783,16 +838,66 @@ private final class StarsTransactionSheetContent: CombinedComponent { ) originY += additional.size.height + 23.0 - let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: button.size) - context.add(button - .position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY)) - ) + if let statusText { + originY += 7.0 + let status = status.update( + component: BalancedTextComponent( + text: .plain(NSAttributedString(string: statusText, font: textFont, textColor: statusIsDestructive ? destructiveColor : textColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.1 + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(status + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + status.size.height / 2.0)) + ) + originY += status.size.height + (statusIsDestructive ? 23.0 : 13.0) + } + + if let buttonText { + let button = button.update( + component: SolidRoundedButtonComponent( + title: buttonText, + theme: buttonIsDestructive ? SolidRoundedButtonComponent.Theme(backgroundColor: .clear, foregroundColor: destructiveColor) : SolidRoundedButtonComponent.Theme(theme: theme), + font: buttonIsDestructive ? .regular : .bold, + fontSize: 17.0, + height: 50.0, + cornerRadius: 10.0, + gloss: false, + iconName: nil, + animationName: nil, + iconPosition: .left, + isLoading: state.inProgress, + action: { + component.cancel(true) + + if isSubscription { + if buttonIsDestructive { + component.updateSubscription(.cancel) + } else { + component.updateSubscription(.renew) + } + } + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + transition: context.transition + ) + + let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: button.size) + context.add(button + .position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY)) + ) + originY += button.size.height + } context.add(closeButton .position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - closeButton.size.width, y: 28.0)) ) - let contentSize = CGSize(width: context.availableSize.width, height: buttonFrame.maxY + 5.0 + environment.safeInsets.bottom) + let contentSize = CGSize(width: context.availableSize.width, height: originY + 5.0 + environment.safeInsets.bottom) return contentSize } @@ -809,6 +914,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent { let openMedia: ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void let openAppExamples: () -> Void let copyTransactionId: (String) -> Void + let updateSubscription: (StarsTransactionScreen.SubscriptionAction) -> Void init( context: AccountContext, @@ -817,7 +923,8 @@ private final class StarsTransactionSheetComponent: CombinedComponent { openMessage: @escaping (EngineMessage.Id) -> Void, openMedia: @escaping ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void, openAppExamples: @escaping () -> Void, - copyTransactionId: @escaping (String) -> Void + copyTransactionId: @escaping (String) -> Void, + updateSubscription: @escaping (StarsTransactionScreen.SubscriptionAction) -> Void ) { self.context = context self.subject = subject @@ -826,6 +933,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent { self.openMedia = openMedia self.openAppExamples = openAppExamples self.copyTransactionId = copyTransactionId + self.updateSubscription = updateSubscription } static func ==(lhs: StarsTransactionSheetComponent, rhs: StarsTransactionSheetComponent) -> Bool { @@ -869,7 +977,8 @@ private final class StarsTransactionSheetComponent: CombinedComponent { openMessage: context.component.openMessage, openMedia: context.component.openMedia, openAppExamples: context.component.openAppExamples, - copyTransactionId: context.component.copyTransactionId + copyTransactionId: context.component.copyTransactionId, + updateSubscription: context.component.updateSubscription )), backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), followContentSizeChanges: true, @@ -936,11 +1045,17 @@ private final class StarsTransactionSheetComponent: CombinedComponent { } public class StarsTransactionScreen: ViewControllerComponentContainer { + enum SubscriptionAction { + case cancel + case renew + } + public enum Subject: Equatable { case transaction(StarsContext.State.Transaction, EnginePeer) case receipt(BotPaymentReceipt) case gift(EngineMessage) case subscription(StarsContext.State.Subscription) + case importer(EnginePeer, StarsSubscriptionPricing, PeerInvitationImportersState.Importer, Double) } private let context: AccountContext @@ -951,7 +1066,8 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { public init( context: AccountContext, subject: StarsTransactionScreen.Subject, - forceDark: Bool = false + forceDark: Bool = false, + updateSubscription: @escaping (Bool) -> Void = { _ in } ) { self.context = context @@ -960,6 +1076,8 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { var openMediaImpl: (([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void)? var openAppExamplesImpl: (() -> Void)? var copyTransactionIdImpl: ((String) -> Void)? + var updateSubscriptionImpl: ((StarsTransactionScreen.SubscriptionAction) -> Void)? + super.init( context: context, component: StarsTransactionSheetComponent( @@ -979,6 +1097,9 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { }, copyTransactionId: { transactionId in copyTransactionIdImpl?(transactionId) + }, + updateSubscription: { action in + updateSubscriptionImpl?(action) } ), navigationBarAppearance: .none, @@ -1090,6 +1211,30 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { HapticFeedback().tap() } + + updateSubscriptionImpl = { [weak self] action in + guard let self, case let .subscription(subscription) = subject, let navigationController = self.navigationController as? NavigationController else { + return + } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + updateSubscription(action == .cancel) + + let title: String + let text: String + switch action { + case .cancel: + title = "Subscription cancelled" + text = "You will still have access top [\(subscription.peer.compactDisplayTitle)]() until \(stringForMediumDate(timestamp: subscription.untilDate, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat))." + case .renew: + title = "Subscription renewed" + text = "You renewed your subscription to [\(subscription.peer.compactDisplayTitle)]()." + } + + let controller = UndoOverlayController(presentationData: presentationData, content: .invitedToVoiceChat(context: context, peer: subscription.peer, title: title, text: text, action: nil, duration: 3.0), elevatedLayout: false, position: .bottom, action: { _ in return true }) + Queue.mainQueue().after(0.6) { + navigationController.presentOverlay(controller: controller) + } + } } required public init(coder aDecoder: NSCoder) { diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift index bd8f3b37bd..e54ee86672 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift @@ -268,9 +268,15 @@ final class StarsTransactionsListPanelComponent: Component { } itemLabel = NSAttributedString(string: labelString, font: Font.medium(fontBaseDisplaySize), textColor: labelString.hasPrefix("-") ? environment.theme.list.itemDestructiveColor : environment.theme.list.itemDisclosureActions.constructive.fillColor) + var itemDateColor = environment.theme.list.itemSecondaryTextColor itemDate = stringForMediumCompactDate(timestamp: item.date, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat) if item.flags.contains(.isRefund) { itemDate += " – \(environment.strings.Stars_Intro_Transaction_Refund)" + } else if item.flags.contains(.isPending) { + itemDate += " – \(environment.strings.Monetization_Transaction_Pending)" + } else if item.flags.contains(.isFailed) { + itemDate += " – \(environment.strings.Monetization_Transaction_Failed)" + itemDateColor = environment.theme.list.itemDestructiveColor } var titleComponents: [AnyComponentWithIdentity] = [] @@ -301,7 +307,7 @@ final class StarsTransactionsListPanelComponent: Component { text: .plain(NSAttributedString( string: itemDate, font: Font.regular(floor(fontBaseDisplaySize * 14.0 / 17.0)), - textColor: environment.theme.list.itemSecondaryTextColor + textColor: itemDateColor )), maximumNumberOfLines: 1 ))) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index 84cfb0c7ae..27cbe07ea0 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -27,6 +27,7 @@ final class StarsTransactionsScreenComponent: Component { let context: AccountContext let starsContext: StarsContext + let subscriptionsContext: StarsSubscriptionsContext let openTransaction: (StarsContext.State.Transaction) -> Void let openSubscription: (StarsContext.State.Subscription) -> Void let buy: () -> Void @@ -35,6 +36,7 @@ final class StarsTransactionsScreenComponent: Component { init( context: AccountContext, starsContext: StarsContext, + subscriptionsContext: StarsSubscriptionsContext, openTransaction: @escaping (StarsContext.State.Transaction) -> Void, openSubscription: @escaping (StarsContext.State.Subscription) -> Void, buy: @escaping () -> Void, @@ -42,6 +44,7 @@ final class StarsTransactionsScreenComponent: Component { ) { self.context = context self.starsContext = starsContext + self.subscriptionsContext = subscriptionsContext self.openTransaction = openTransaction self.openSubscription = openSubscription self.buy = buy @@ -122,7 +125,6 @@ final class StarsTransactionsScreenComponent: Component { private var previousBalance: Int64? - private var subscriptionsContext: StarsSubscriptionsContext? private var subscriptionsStateDisposable: Disposable? private var subscriptionsState: StarsSubscriptionsContext.State? @@ -312,9 +314,7 @@ final class StarsTransactionsScreenComponent: Component { } }) - let subscriptionsContext = component.context.engine.payments.peerStarsSubscriptionsContext(starsContext: component.starsContext) - self.subscriptionsContext = subscriptionsContext - self.subscriptionsStateDisposable = (subscriptionsContext.state + self.subscriptionsStateDisposable = (component.subscriptionsContext.state |> deliverOnMainQueue).start(next: { [weak self] state in guard let self else { return @@ -614,10 +614,21 @@ final class StarsTransactionsScreenComponent: Component { ))) ) //TODO:localize + let dateText: String + let dateValue = stringForDateWithoutYear(date: Date(timeIntervalSince1970: Double(subscription.untilDate)), strings: environment.strings) + if subscription.flags.contains(.isCancelled) { + if subscription.untilDate > Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) { + dateText = "expires on \(dateValue)" + } else { + dateText = "expired on \(dateValue)" + } + } else { + dateText = "renews on \(dateValue)" + } titleComponents.append( AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: "renews on \(stringForDateWithoutYear(date: Date(timeIntervalSince1970: Double(subscription.untilDate)), strings: environment.strings))", + string: dateText, font: Font.regular(floor(fontBaseDisplaySize * 15.0 / 17.0)), textColor: environment.theme.list.itemSecondaryTextColor )), @@ -626,15 +637,15 @@ final class StarsTransactionsScreenComponent: Component { ) let labelComponent: AnyComponentWithIdentity - if "".isEmpty { + if subscription.flags.contains(.isCancelled) { + labelComponent = AnyComponentWithIdentity(id: "cancelledLabel", component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: "cancelled", font: Font.regular(floor(fontBaseDisplaySize * 13.0 / 17.0)), textColor: environment.theme.list.itemDestructiveColor))) + )) + } else { let itemLabel = NSAttributedString(string: "\(subscription.pricing.amount)", font: Font.medium(fontBaseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor) let itemSublabel = NSAttributedString(string: "per month", font: Font.regular(floor(fontBaseDisplaySize * 13.0 / 17.0)), textColor: environment.theme.list.itemSecondaryTextColor) labelComponent = AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(text: itemLabel, subtext: itemSublabel))) - } else { - labelComponent = AnyComponentWithIdentity(id: "cancelledLabel", component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: "cancelled", font: Font.regular(floor(fontBaseDisplaySize * 13.0 / 17.0)), textColor: environment.theme.list.itemDestructiveColor))) - )) } subscriptionsItems.append(AnyComponentWithIdentity( @@ -657,6 +668,38 @@ final class StarsTransactionsScreenComponent: Component { ) )) } + if subscriptionsState.canLoadMore { + subscriptionsItems.append(AnyComponentWithIdentity( + id: "showMore", + component: AnyComponent( + ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(Text( + text: "Show More", + font: Font.regular(17.0), + color: environment.theme.list.itemAccentColor + )), + leftIcon: .custom( + AnyComponentWithIdentity( + id: "icon", + component: AnyComponent(Image( + image: PresentationResourcesItemList.downArrowImage(environment.theme), + size: CGSize(width: 30.0, height: 30.0) + )) + ), + false + ), + accessory: nil, + action: { _ in + + }, + highlighting: .default, + updateIsHighlighted: { view, _ in + + }) + ) + )) + } } if !subscriptionsItems.isEmpty { @@ -837,6 +880,7 @@ final class StarsTransactionsScreenComponent: Component { public final class StarsTransactionsScreen: ViewControllerComponentContainer { private let context: AccountContext private let starsContext: StarsContext + private let subscriptionsContext: StarsSubscriptionsContext private let options = Promise<[StarsTopUpOption]>() @@ -844,6 +888,8 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer { self.context = context self.starsContext = starsContext + self.subscriptionsContext = context.engine.payments.peerStarsSubscriptionsContext(starsContext: starsContext) + var buyImpl: (() -> Void)? var giftImpl: (() -> Void)? var openTransactionImpl: ((StarsContext.State.Transaction) -> Void)? @@ -851,6 +897,7 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer { super.init(context: context, component: StarsTransactionsScreenComponent( context: context, starsContext: starsContext, + subscriptionsContext: self.subscriptionsContext, openTransaction: { transaction in openTransactionImpl?(transaction) }, @@ -887,7 +934,12 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer { guard let self else { return } - let controller = context.sharedContext.makeStarsSubscriptionScreen(context: context, subscription: subscription) + let controller = context.sharedContext.makeStarsSubscriptionScreen(context: context, subscription: subscription, update: { [weak self] cancel in + guard let self else { + return + } + self.subscriptionsContext.updateSubscription(id: subscription.id, cancel: cancel) + }) self.push(controller) } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift index 0767b02d7e..4824033937 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift @@ -262,7 +262,7 @@ private final class SheetContent: CombinedComponent { var contentSize = CGSize(width: context.availableSize.width, height: 18.0) let background = background.update( - component: RoundedRectangle(color: theme.list.blocksBackgroundColor, cornerRadius: 8.0), + component: RoundedRectangle(color: theme.actionSheet.opaqueItemBackgroundColor, cornerRadius: 8.0), availableSize: CGSize(width: context.availableSize.width, height: 1000.0), transition: .immediate ) @@ -270,7 +270,6 @@ private final class SheetContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0)) ) - var isSubscription = false let subject: StarsImageComponent.Subject if !component.extendedMedia.isEmpty { subject = .extendedMedia(component.extendedMedia) @@ -283,13 +282,19 @@ private final class SheetContent: CombinedComponent { } else { subject = .none } + + var isSubscription = false + if case .starsChatSubscription = context.component.source { + isSubscription = true + } let star = star.update( component: StarsImageComponent( context: component.context, subject: subject, theme: theme, diameter: 90.0, - backgroundColor: theme.list.blocksBackgroundColor + backgroundColor: theme.actionSheet.opaqueItemBackgroundColor, + icon: isSubscription ? .star : nil ), availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0), transition: context.transition @@ -324,10 +329,9 @@ private final class SheetContent: CombinedComponent { contentSize.height += 126.0 let titleString: String - if case .starsChatSubscription = context.component.source { + if isSubscription { //TODO:localize titleString = "Subscribe to the Channel" - isSubscription = true } else { titleString = strings.Stars_Transfer_Title } @@ -588,12 +592,26 @@ private final class SheetContent: CombinedComponent { let info = info.update( component: BalancedTextComponent( text: .markdown( - text: "By subscribing you agree to the [Terms of Service]()", + text: strings.Stars_Subscription_Terms, attributes: termsMarkdownAttributes ), horizontalAlignment: .center, maximumNumberOfLines: 0, - lineSpacing: 0.2 + lineSpacing: 0.2, + highlightColor: linkColor.withAlphaComponent(0.2), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { [weak controller] attributes, _ in + if let controller, let navigationController = controller.navigationController as? NavigationController { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_Subscription_Terms_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) + } + } ), availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height), transition: .immediate diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarMediumOutline.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarMediumOutline.imageset/Contents.json new file mode 100644 index 0000000000..02b779594f --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarMediumOutline.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "StarOutline.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarMediumOutline.imageset/StarOutline.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarMediumOutline.imageset/StarOutline.pdf new file mode 100644 index 0000000000000000000000000000000000000000..22b2d4a5d3013c36e8e3305c3dab2e5e654262f3 GIT binary patch literal 16644 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 SvBS*%iIoLFPA)1h_J0AE`J<%( literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/TransactionStar.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Stars/TransactionStar.imageset/Contents.json new file mode 100644 index 0000000000..f6d2b394da --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Stars/TransactionStar.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "StarTransaction.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/TransactionStar.imageset/StarTransaction.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Stars/TransactionStar.imageset/StarTransaction.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4836b64fc5a13961147680ffe75e17d02645bb9e GIT binary patch literal 7031 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/TransactionStarOutline.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Stars/TransactionStarOutline.imageset/Contents.json new file mode 100644 index 0000000000..8b71ceb553 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Stars/TransactionStarOutline.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "StarTransactionOutline.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/TransactionStarOutline.imageset/StarTransactionOutline.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Stars/TransactionStarOutline.imageset/StarTransactionOutline.pdf new file mode 100644 index 0000000000000000000000000000000000000000..bd3a7e4f21b28186fbfaf539308ebe01419e862a GIT binary patch literal 17106 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index 6e98ac5a67..62abaaa005 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -3989,7 +3989,7 @@ extension ChatControllerImpl { if let strongSelf = self { HapticFeedback().impact() - strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.Conversation_SendMesageAsPremiumInfo, action: strongSelf.presentationData.strings.EmojiInput_PremiumEmojiToast_Action, duration: 3), elevatedLayout: false, action: { [weak self] action in + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, title: nil, text: strongSelf.presentationData.strings.Conversation_SendMesageAsPremiumInfo, action: strongSelf.presentationData.strings.EmojiInput_PremiumEmojiToast_Action, duration: 3), elevatedLayout: false, action: { [weak self] action in guard let strongSelf = self else { return true } diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift index d11043b5ae..0c8f47b758 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift @@ -87,34 +87,35 @@ func chatHistoryEntriesForView( if (associatedData.subject?.isService ?? false) { } else { - if case let .peer(peerId) = location, case let cachedData = cachedData as? CachedChannelData, let invitedOn = cachedData?.invitedOn { - joinMessage = Message( - stableId: UInt32.max - 1000, - stableVersion: 0, - id: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: 0), - globallyUniqueId: nil, - groupingKey: nil, - groupInfo: nil, - threadId: nil, - timestamp: invitedOn, - flags: [.Incoming], - tags: [], - globalTags: [], - localTags: [], - customTags: [], - forwardInfo: nil, - author: channelPeer, - text: "", - attributes: [], - media: [TelegramMediaAction(action: .joinedByRequest)], - peers: SimpleDictionary(), - associatedMessages: SimpleDictionary(), - associatedMessageIds: [], - associatedMedia: [:], - associatedThreadInfo: nil, - associatedStories: [:] - ) - } else if let peer = channelPeer as? TelegramChannel, case .broadcast = peer.info, case .member = peer.participationStatus, !peer.flags.contains(.isCreator) { +// if case let .peer(peerId) = location, case let cachedData = cachedData as? CachedChannelData, let invitedOn = cachedData?.invitedOn { +// joinMessage = Message( +// stableId: UInt32.max - 1000, +// stableVersion: 0, +// id: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: 0), +// globallyUniqueId: nil, +// groupingKey: nil, +// groupInfo: nil, +// threadId: nil, +// timestamp: invitedOn, +// flags: [.Incoming], +// tags: [], +// globalTags: [], +// localTags: [], +// customTags: [], +// forwardInfo: nil, +// author: channelPeer, +// text: "", +// attributes: [], +// media: [TelegramMediaAction(action: .joinedByRequest)], +// peers: SimpleDictionary(), +// associatedMessages: SimpleDictionary(), +// associatedMessageIds: [], +// associatedMedia: [:], +// associatedThreadInfo: nil, +// associatedStories: [:] +// ) +// } else + if let peer = channelPeer as? TelegramChannel, case .broadcast = peer.info, case .member = peer.participationStatus, !peer.flags.contains(.isCreator) { joinMessage = Message( stableId: UInt32.max - 1000, stableVersion: 0, diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index a59f91300e..08a1292e89 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2750,8 +2750,12 @@ public final class SharedAccountContextImpl: SharedAccountContext { return StarsTransactionScreen(context: context, subject: .receipt(receipt)) } - public func makeStarsSubscriptionScreen(context: AccountContext, subscription: StarsContext.State.Subscription) -> ViewController { - return StarsTransactionScreen(context: context, subject: .subscription(subscription)) + public func makeStarsSubscriptionScreen(context: AccountContext, subscription: StarsContext.State.Subscription, update: @escaping (Bool) -> Void) -> ViewController { + return StarsTransactionScreen(context: context, subject: .subscription(subscription), updateSubscription: update) + } + + public func makeStarsSubscriptionScreen(context: AccountContext, peer: EnginePeer, pricing: StarsSubscriptionPricing, importer: PeerInvitationImportersState.Importer, usdRate: Double) -> ViewController { + return StarsTransactionScreen(context: context, subject: .importer(peer, pricing, importer, usdRate)) } public func makeStarsStatisticsScreen(context: AccountContext, peerId: EnginePeer.Id, revenueContext: StarsRevenueStatsContext) -> ViewController { diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index 030e3c3a58..1172c49219 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -22,7 +22,7 @@ public enum UndoOverlayContent { case chatRemovedFromFolder(chatTitle: String, folderTitle: String) case messagesUnpinned(title: String, text: String, undo: Bool, isHidden: Bool) case setProximityAlert(title: String, text: String, cancelled: Bool) - case invitedToVoiceChat(context: AccountContext, peer: EnginePeer, text: String, action: String?, duration: Double) + case invitedToVoiceChat(context: AccountContext, peer: EnginePeer, title: String?, text: String, action: String?, duration: Double) case linkCopied(text: String) case banned(text: String) case importedMessage(text: String) diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index d9398de8a4..c20ed6ecee 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -652,19 +652,21 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { displayUndo = false self.originalRemainingSeconds = 3 - case let .invitedToVoiceChat(context, peer, text, action, duration): + case let .invitedToVoiceChat(context, peer, title, text, action, duration): self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 15.0)) self.iconNode = nil self.iconCheckNode = nil self.animationNode = nil self.animatedStickerNode = nil + self.titleNode.attributedText = NSAttributedString(string: title ?? "", font: Font.semibold(14.0), textColor: .white) + let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) - let link = MarkdownAttributeSet(font: Font.regular(14.0), textColor: undoTextColor) + let link = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: undoTextColor) let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return nil }), textAlignment: .natural) - self.textNode.attributedText = attributedText + self.textNode.attributedText = attributedText self.avatarNode?.setPeer(context: context, theme: presentationData.theme, peer: peer, overrideImage: nil, emptyColor: presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: true) if let action = action { From c504c1d70e4f456a7ab9e87fc944f95fc9ebf1d8 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 6 Aug 2024 23:12:35 +0200 Subject: [PATCH 07/12] Fix build --- .../InviteLinksUI/Sources/InviteLinkEditController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift index 4886e17e44..4cec283b1b 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift @@ -471,8 +471,8 @@ private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: entries.append(.subscriptionFeeToggle(presentationData.theme, "Require Monthly Fee", state.subscriptionEnabled, isEditingEnabled)) if state.subscriptionEnabled { var label: String = "" - if let subscriptionFee, subscriptionFee > 0, let starsState { - label = formatTonUsdValue(state.subscriptionFee, divide: false, rate: starsState.usdRate, dateTimeFormat: presentationData.dateTimeFormat) + if let subscriptionFee = state.subscriptionFee, subscriptionFee > 0, let starsState { + label = formatTonUsdValue(subscriptionFee, divide: false, rate: starsState.usdRate, dateTimeFormat: presentationData.dateTimeFormat) } entries.append(.subscriptionFee(presentationData.theme, "Stars amount per month", isEditingEnabled, state.subscriptionFee, label)) } From c400ccda2440a01a3d634317fd6f8e58736ecb22 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 7 Aug 2024 14:58:25 +0200 Subject: [PATCH 08/12] Various fixes --- .../Sources/AccountContext.swift | 1 + .../Sources/InviteLinkEditController.swift | 88 ++++++++++++++++--- .../Items/ItemListSingleLineInputItem.swift | 12 +++ .../TelegramEngine/Payments/Stars.swift | 3 + .../Payments/TelegramEnginePayments.swift | 4 - .../Sources/StarsTransferScreen.swift | 34 +++++-- .../TelegramUI/Sources/OpenResolvedUrl.swift | 3 +- .../Sources/SharedAccountContext.swift | 4 + 8 files changed, 128 insertions(+), 21 deletions(-) diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 8293c75c9f..2f4efe6f2f 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1005,6 +1005,7 @@ public protocol SharedAccountContext: AnyObject { func makeStarsTransactionsScreen(context: AccountContext, starsContext: StarsContext) -> ViewController func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [Any], purpose: StarsPurchasePurpose, completion: @escaping (Int64) -> Void) -> ViewController func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController + func makeStarsSubscriptionTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, link: String, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, navigateToPeer: @escaping (EnginePeer) -> Void) -> ViewController func makeStarsTransactionScreen(context: AccountContext, transaction: StarsContext.State.Transaction, peer: EnginePeer) -> ViewController func makeStarsReceiptScreen(context: AccountContext, receipt: BotPaymentReceipt) -> ViewController func makeStarsSubscriptionScreen(context: AccountContext, subscription: StarsContext.State.Subscription, update: @escaping (Bool) -> Void) -> ViewController diff --git a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift index 4cec283b1b..f1b96e8970 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift @@ -22,13 +22,25 @@ import TextFormat private final class InviteLinkEditControllerArguments { let context: AccountContext let updateState: ((InviteLinkEditControllerState) -> InviteLinkEditControllerState) -> Void + let focusOnItem: (InviteLinksEditEntryTag) -> Void + let errorWithItem: (InviteLinksEditEntryTag) -> Void let scrollToUsage: () -> Void let dismissInput: () -> Void let revoke: () -> Void - init(context: AccountContext, updateState: @escaping ((InviteLinkEditControllerState) -> InviteLinkEditControllerState) -> Void, scrollToUsage: @escaping () -> Void, dismissInput: @escaping () -> Void, revoke: @escaping () -> Void) { + init( + context: AccountContext, + updateState: @escaping ((InviteLinkEditControllerState) -> InviteLinkEditControllerState) -> Void, + focusOnItem: @escaping (InviteLinksEditEntryTag) -> Void, + errorWithItem: @escaping (InviteLinksEditEntryTag) -> Void, + scrollToUsage: @escaping () -> Void, + dismissInput: @escaping () -> Void, + revoke: @escaping () -> Void) + { self.context = context self.updateState = updateState + self.focusOnItem = focusOnItem + self.errorWithItem = errorWithItem self.scrollToUsage = scrollToUsage self.dismissInput = dismissInput self.revoke = revoke @@ -45,6 +57,7 @@ private enum InviteLinksEditSection: Int32 { } private enum InviteLinksEditEntryTag: ItemListItemTag { + case subscriptionFee case usage func isEqual(to other: ItemListItemTag) -> Bool { @@ -79,7 +92,7 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { case subscriptionFeeToggle(PresentationTheme, String, Bool, Bool) - case subscriptionFee(PresentationTheme, String, Bool, Int64?, String) + case subscriptionFee(PresentationTheme, String, Bool, Int64?, String, Int64?) case subscriptionFeeInfo(PresentationTheme, String) case requestApproval(PresentationTheme, String, Bool, Bool) @@ -182,8 +195,8 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { } else { return false } - case let .subscriptionFee(lhsTheme, lhsText, lhsValue, lhsEnabled, lhsLabel): - if case let .subscriptionFee(rhsTheme, rhsText, rhsValue, rhsEnabled, rhsLabel) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled, lhsLabel == rhsLabel { + case let .subscriptionFee(lhsTheme, lhsText, lhsValue, lhsEnabled, lhsLabel, lhsMaxValue): + if case let .subscriptionFee(rhsTheme, rhsText, rhsValue, rhsEnabled, rhsLabel, rhsMaxValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled, lhsLabel == rhsLabel, lhsMaxValue == rhsMaxValue { return true } else { return false @@ -300,17 +313,26 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { } return updatedState } + if value { + Queue.mainQueue().after(0.1) { + arguments.focusOnItem(.subscriptionFee) + } + } }) - case let .subscriptionFee(_, placeholder, enabled, value, label): + case let .subscriptionFee(_, placeholder, enabled, value, label, maxValue): let title = NSMutableAttributedString(string: "⭐️", font: Font.semibold(18.0), textColor: .white) if let range = title.string.range(of: "⭐️") { title.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: title.string)) title.addAttribute(.baselineOffset, value: -1.0, range: NSRange(range, in: title.string)) } - return ItemListSingleLineInputItem(context: arguments.context, presentationData: presentationData, title: title, text: value.flatMap { "\($0)" } ?? "", placeholder: placeholder, label: label, type: .number, spacing: 3.0, enabled: enabled, sectionId: self.section, textUpdated: { text in + return ItemListSingleLineInputItem(context: arguments.context, presentationData: presentationData, title: title, text: value.flatMap { "\($0)" } ?? "", placeholder: placeholder, label: label, type: .number, spacing: 3.0, enabled: enabled, tag: InviteLinksEditEntryTag.subscriptionFee, sectionId: self.section, textUpdated: { text in arguments.updateState { state in var updatedState = state - if let value = Int64(text) { + if var value = Int64(text) { + if let maxValue, value > maxValue { + value = maxValue + arguments.errorWithItem(.subscriptionFee) + } updatedState.subscriptionFee = value } else { updatedState.subscriptionFee = nil @@ -457,7 +479,7 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { } } -private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: InviteLinkEditControllerState, isGroup: Bool, isPublic: Bool, presentationData: PresentationData, starsState: StarsRevenueStats?) -> [InviteLinksEditEntry] { +private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: InviteLinkEditControllerState, isGroup: Bool, isPublic: Bool, presentationData: PresentationData, starsState: StarsRevenueStats?, configuration: StarsSubscriptionConfiguration) -> [InviteLinksEditEntry] { var entries: [InviteLinksEditEntry] = [] entries.append(.titleHeader(presentationData.theme, presentationData.strings.InviteLink_Create_LinkNameTitle.uppercased())) @@ -472,9 +494,9 @@ private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: if state.subscriptionEnabled { var label: String = "" if let subscriptionFee = state.subscriptionFee, subscriptionFee > 0, let starsState { - label = formatTonUsdValue(subscriptionFee, divide: false, rate: starsState.usdRate, dateTimeFormat: presentationData.dateTimeFormat) + label = "≈\(formatTonUsdValue(subscriptionFee, divide: false, rate: starsState.usdRate, dateTimeFormat: presentationData.dateTimeFormat)) / month" } - entries.append(.subscriptionFee(presentationData.theme, "Stars amount per month", isEditingEnabled, state.subscriptionFee, label)) + entries.append(.subscriptionFee(presentationData.theme, "Stars amount per month", isEditingEnabled, state.subscriptionFee, label, configuration.maxFee)) } let infoText: String if let _ = invite, state.subscriptionEnabled { @@ -585,9 +607,15 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio var dismissImpl: (() -> Void)? var dismissInputImpl: (() -> Void)? var scrollToUsageImpl: (() -> Void)? + var focusImpl: ((InviteLinksEditEntryTag) -> Void)? + var errorImpl: ((InviteLinksEditEntryTag) -> Void)? let arguments = InviteLinkEditControllerArguments(context: context, updateState: { f in updateState(f) + }, focusOnItem: { tag in + focusImpl?(tag) + }, errorWithItem: { tag in + errorImpl?(tag) }, scrollToUsage: { scrollToUsageImpl?() }, dismissInput: { @@ -648,6 +676,8 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData + let configuration = StarsSubscriptionConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + let previousState = Atomic(value: nil) let signal = combineLatest( presentationData, @@ -762,7 +792,7 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(invite == nil ? presentationData.strings.InviteLink_Create_Title : presentationData.strings.InviteLink_Create_EditTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: inviteLinkEditControllerEntries(invite: invite, state: state, isGroup: isGroup, isPublic: isPublic, presentationData: presentationData, starsState: starsState), style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: animateChanges) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: inviteLinkEditControllerEntries(invite: invite, state: state, isGroup: isGroup, isPublic: isPublic, presentationData: presentationData, starsState: starsState, configuration: configuration), style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: animateChanges) return (controllerState, (listState, arguments)) } @@ -806,5 +836,41 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio dismissImpl = { [weak controller] in controller?.dismiss() } + focusImpl = { [weak controller] targetTag in + controller?.forEachItemNode { itemNode in + if let itemNode = itemNode as? ItemListSingleLineInputItemNode, let tag = itemNode.tag, tag.isEqual(to: targetTag) { + itemNode.focus() + } + } + } + let hapticFeedback = HapticFeedback() + errorImpl = { [weak controller] targetTag in + hapticFeedback.error() + controller?.forEachItemNode { itemNode in + if let itemNode = itemNode as? ItemListSingleLineInputItemNode, let tag = itemNode.tag, tag.isEqual(to: targetTag) { + itemNode.animateError() + } + } + } return controller } + +private struct StarsSubscriptionConfiguration { + static var defaultValue: StarsSubscriptionConfiguration { + return StarsSubscriptionConfiguration(maxFee: 2500) + } + + let maxFee: Int64? + + fileprivate init(maxFee: Int64?) { + self.maxFee = maxFee + } + + public static func with(appConfiguration: AppConfiguration) -> StarsSubscriptionConfiguration { + if let data = appConfiguration.data, let value = data["stars_subscription_amount_max"] as? Double { + return StarsSubscriptionConfiguration(maxFee: Int64(value)) + } else { + return .defaultValue + } + } +} diff --git a/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift b/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift index 4aaaf7df8d..81dc1224b2 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift @@ -141,6 +141,7 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg private let textNode: TextFieldNode private let clearIconNode: ASImageNode private let clearButtonNode: HighlightableButtonNode + private let labelNode: TextNode private var item: ItemListSingleLineInputItem? @@ -171,12 +172,17 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg self.clearButtonNode = HighlightableButtonNode() + self.labelNode = TextNode() + self.labelNode.isUserInteractionEnabled = false + super.init(layerBacked: false, dynamicBounce: false) self.addSubnode(self.titleNode.textNode) self.addSubnode(self.textNode) self.addSubnode(self.clearIconNode) self.addSubnode(self.clearButtonNode) + self.addSubnode(self.textNode) + self.addSubnode(self.labelNode) self.clearButtonNode.addTarget(self, action: #selector(self.clearButtonPressed), forControlEvents: .touchUpInside) self.clearButtonNode.highligthedChanged = { [weak self] highlighted in @@ -218,6 +224,7 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg let makeTitleLayout = TextNode.asyncLayout(self.titleNode.textNode) let makeTitleWithEntitiesLayout = TextNodeWithEntities.asyncLayout(self.titleNode) let makeMeasureTitleSizeLayout = TextNode.asyncLayout(self.measureTitleSizeNode) + let makeLabelLayout = TextNode.asyncLayout(self.labelNode) let currentItem = self.item @@ -262,6 +269,8 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg let (measureTitleLayout, measureTitleSizeApply) = makeMeasureTitleSizeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "A", font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize)), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 32.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label ?? "", font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 32.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let separatorHeight = UIScreenPixel let contentSize = CGSize(width: params.width, height: max(titleLayout.size.height, measureTitleLayout.size.height) + 22.0) @@ -310,6 +319,7 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg strongSelf.titleNode.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((layout.contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) let _ = measureTitleSizeApply() + let _ = labelApply() let secureEntry: Bool let capitalizationType: UITextAutocapitalizationType @@ -379,6 +389,8 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset + titleLayout.size.width + item.spacing, y: 0.0), size: CGSize(width: max(1.0, params.width - (leftInset + rightInset + titleLayout.size.width + item.spacing)), height: layout.contentSize.height - 2.0)) + strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: layoutSize.width - rightInset - labelLayout.size.width, y: floorToScreenPixels((layout.contentSize.height - labelLayout.size.height) / 2.0)), size: labelLayout.size) + switch item.alignment { case .default: strongSelf.textNode.textField.textAlignment = .natural diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index c92d845045..72c24c4b94 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -911,6 +911,7 @@ private final class StarsSubscriptionsContextImpl { private let disposable = MetaDisposable() private var stateDisposable: Disposable? + private let updateDisposable = MetaDisposable() init(account: Account, starsContext: StarsContext) { assert(Queue.mainQueue().isCurrent()) @@ -931,6 +932,7 @@ private final class StarsSubscriptionsContextImpl { assert(Queue.mainQueue().isCurrent()) self.disposable.dispose() self.stateDisposable?.dispose() + self.updateDisposable.dispose() } func loadMore() { @@ -978,6 +980,7 @@ private final class StarsSubscriptionsContextImpl { updatedState.subscriptions[index] = updatedSubscription } self.updateState(updatedState) + self.updateDisposable.set(_internal_updateStarsSubscription(account: self.account, peerId: self.account.peerId, subscriptionId: id, cancel: cancel).startStrict()) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index 022720b8ef..14fc327d3f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -93,9 +93,5 @@ public extension TelegramEngine { public func sendStarsPaymentForm(formId: Int64, source: BotPaymentInvoiceSource) -> Signal { return _internal_sendStarsPaymentForm(account: self.account, formId: formId, source: source) } - - public func updateStarsSubscription(peerId: EnginePeer.Id, subscriptionId: String, cancel: Bool) -> Signal { - return _internal_updateStarsSubscription(account: self.account, peerId: peerId, subscriptionId: subscriptionId, cancel: cancel) - } } } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift index 4824033937..b0dc0a709e 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift @@ -29,6 +29,7 @@ private final class SheetContent: CombinedComponent { let source: BotPaymentInvoiceSource let extendedMedia: [TelegramExtendedMedia] let inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError> + let navigateToPeer: (EnginePeer) -> Void let dismiss: () -> Void init( @@ -38,6 +39,7 @@ private final class SheetContent: CombinedComponent { source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, + navigateToPeer: @escaping (EnginePeer) -> Void, dismiss: @escaping () -> Void ) { self.context = context @@ -46,6 +48,7 @@ private final class SheetContent: CombinedComponent { self.source = source self.extendedMedia = extendedMedia self.inputData = inputData + self.navigateToPeer = navigateToPeer self.dismiss = dismiss } @@ -77,6 +80,7 @@ private final class SheetContent: CombinedComponent { private var peerDisposable: Disposable? private(set) var balance: Int64? private(set) var form: BotPaymentForm? + private(set) var navigateToPeer: (EnginePeer) -> Void private var stateDisposable: Disposable? @@ -96,13 +100,15 @@ private final class SheetContent: CombinedComponent { source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], invoice: TelegramMediaInvoice, - inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError> + inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, + navigateToPeer: @escaping (EnginePeer) -> Void ) { self.context = context self.starsContext = starsContext self.source = source self.extendedMedia = extendedMedia self.invoice = invoice + self.navigateToPeer = navigateToPeer super.init() @@ -159,6 +165,7 @@ private final class SheetContent: CombinedComponent { return } + let navigateToPeer = self.navigateToPeer let action = { [weak self] in guard let self else { return @@ -167,8 +174,19 @@ private final class SheetContent: CombinedComponent { self.updated() let _ = (self.context.engine.payments.sendStarsPaymentForm(formId: form.id, source: self.source) - |> deliverOnMainQueue).start(next: { _ in + |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let self else { + return + } completion(true) + if case let .starsChatSubscription(link) = self.source { + let _ = (self.context.engine.peers.joinLinkInformation(link) + |> deliverOnMainQueue).startStandalone(next: { result in + if case let .alreadyJoined(peer) = result { + navigateToPeer(peer) + } + }) + } }, error: { [weak self] error in guard let self else { return @@ -235,7 +253,7 @@ private final class SheetContent: CombinedComponent { } func makeState() -> State { - return State(context: self.context, starsContext: self.starsContext, source: self.source, extendedMedia: self.extendedMedia, invoice: self.invoice, inputData: self.inputData) + return State(context: self.context, starsContext: self.starsContext, source: self.source, extendedMedia: self.extendedMedia, invoice: self.invoice, inputData: self.inputData, navigateToPeer: self.navigateToPeer) } static var body: Body { @@ -639,6 +657,7 @@ private final class StarsTransferSheetComponent: CombinedComponent { private let source: BotPaymentInvoiceSource private let extendedMedia: [TelegramExtendedMedia] private let inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError> + private let navigateToPeer: (EnginePeer) -> Void init( context: AccountContext, @@ -646,7 +665,8 @@ private final class StarsTransferSheetComponent: CombinedComponent { invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], - inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError> + inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, + navigateToPeer: @escaping (EnginePeer) -> Void ) { self.context = context self.starsContext = starsContext @@ -654,6 +674,7 @@ private final class StarsTransferSheetComponent: CombinedComponent { self.source = source self.extendedMedia = extendedMedia self.inputData = inputData + self.navigateToPeer = navigateToPeer } static func ==(lhs: StarsTransferSheetComponent, rhs: StarsTransferSheetComponent) -> Bool { @@ -687,6 +708,7 @@ private final class StarsTransferSheetComponent: CombinedComponent { source: context.component.source, extendedMedia: context.component.extendedMedia, inputData: context.component.inputData, + navigateToPeer: context.component.navigateToPeer, dismiss: { animateOut.invoke(Action { _ in if let controller = controller() { @@ -747,6 +769,7 @@ public final class StarsTransferScreen: ViewControllerComponentContainer { source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia] = [], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, + navigateToPeer: @escaping (EnginePeer) -> Void = { _ in }, completion: @escaping (Bool) -> Void ) { self.context = context @@ -761,7 +784,8 @@ public final class StarsTransferScreen: ViewControllerComponentContainer { invoice: invoice, source: source, extendedMedia: extendedMedia, - inputData: inputData + inputData: inputData, + navigateToPeer: navigateToPeer ), navigationBarAppearance: .none, statusBarStyle: .ignore, diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index ca19a40983..7cd2153142 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -323,7 +323,8 @@ func openResolvedUrlImpl( } } let _ = (starsInputData |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue).start(next: { _ in - let controller = context.sharedContext.makeStarsTransferScreen(context: context, starsContext: starsContext, invoice: invoice, source: .starsChatSubscription(hash: link), extendedMedia: [], inputData: starsInputData, completion: { _ in + let controller = context.sharedContext.makeStarsSubscriptionTransferScreen(context: context, starsContext: starsContext, invoice: invoice, link: link, inputData: starsInputData, navigateToPeer: { peer in + openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil)) }) navigationController?.pushViewController(controller) }) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index f6df7e2a9f..5ce56c2bc8 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2743,6 +2743,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { return StarsTransferScreen(context: context, starsContext: starsContext, invoice: invoice, source: source, extendedMedia: extendedMedia, inputData: inputData, completion: completion) } + public func makeStarsSubscriptionTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, link: String, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, navigateToPeer: @escaping (EnginePeer) -> Void) -> ViewController { + return StarsTransferScreen(context: context, starsContext: starsContext, invoice: invoice, source: .starsChatSubscription(hash: link), extendedMedia: [], inputData: inputData, navigateToPeer: navigateToPeer, completion: { _ in }) + } + public func makeStarsTransactionScreen(context: AccountContext, transaction: StarsContext.State.Transaction, peer: EnginePeer) -> ViewController { return StarsTransactionScreen(context: context, subject: .transaction(transaction, peer)) } From f200b4fd4dfd372988d26091a0ea83dd305638e4 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Wed, 7 Aug 2024 10:13:45 -0300 Subject: [PATCH 09/12] - fulfillStars - joinchannel method update for paid subscriptions --- .../Payments/TelegramEnginePayments.swift | 3 + .../TelegramEngine/Peers/JoinChannel.swift | 64 +++++++++++-------- 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index 022720b8ef..12f5af0a97 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -97,5 +97,8 @@ public extension TelegramEngine { public func updateStarsSubscription(peerId: EnginePeer.Id, subscriptionId: String, cancel: Bool) -> Signal { return _internal_updateStarsSubscription(account: self.account, peerId: peerId, subscriptionId: subscriptionId, cancel: cancel) } + public func fulfillStarsSubscription(peerId: PeerId, subscriptionId: String) -> Signal { + return _internal_fulfillStarsSubscription(account: self.account, peerId: peerId, subscriptionId: subscriptionId) + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift index df78b66f04..94dacbdc8c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift @@ -17,29 +17,35 @@ func _internal_joinChannel(account: Account, peerId: PeerId, hash: String?) -> S |> take(1) |> castError(JoinChannelError.self) |> mapToSignal { peer -> Signal in - if let inputChannel = apiInputChannel(peer) { - let request: Signal - if let hash = hash { - request = account.network.request(Api.functions.messages.importChatInvite(hash: hash)) - } else { - request = account.network.request(Api.functions.channels.joinChannel(channel: inputChannel)) + + let request: Signal + if let hash = hash { + request = account.network.request(Api.functions.messages.importChatInvite(hash: hash)) + } else if let inputChannel = apiInputChannel(peer) { + request = account.network.request(Api.functions.channels.joinChannel(channel: inputChannel)) + } else { + request = .fail(.init()) + } + + return request + |> mapError { error -> JoinChannelError in + switch error.errorDescription { + case "CHANNELS_TOO_MUCH": + return .tooMuchJoined + case "USERS_TOO_MUCH": + return .tooMuchUsers + case "INVITE_REQUEST_SENT": + return .inviteRequestSent + default: + return .generic } - return request - |> mapError { error -> JoinChannelError in - switch error.errorDescription { - case "CHANNELS_TOO_MUCH": - return .tooMuchJoined - case "USERS_TOO_MUCH": - return .tooMuchUsers - case "INVITE_REQUEST_SENT": - return .inviteRequestSent - default: - return .generic - } - } - |> mapToSignal { updates -> Signal in - account.stateManager.addUpdates(updates) - + } + |> mapToSignal { updates -> Signal in + account.stateManager.addUpdates(updates) + + let channels = updates.chats.compactMap { parseTelegramGroupOrChannel(chat: $0) }.compactMap(apiInputChannel) + + if let inputChannel = channels.first { return account.network.request(Api.functions.channels.getParticipant(channel: inputChannel, participant: .inputPeerSelf)) |> map(Optional.init) |> `catch` { _ -> Signal in @@ -76,14 +82,16 @@ func _internal_joinChannel(account: Account, peerId: PeerId, hash: String?) -> S } |> castError(JoinChannelError.self) } + } else { + return .fail(.generic) } - |> afterCompleted { - if hash == nil { - let _ = _internal_requestRecommendedChannels(account: account, peerId: peerId, forceUpdate: true).startStandalone() - } + + + } + |> afterCompleted { + if hash == nil { + let _ = _internal_requestRecommendedChannels(account: account, peerId: peerId, forceUpdate: true).startStandalone() } - } else { - return .fail(.generic) } } } From e6c457e69dc7934caac3a251fd8be5bb1f323fb3 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Wed, 7 Aug 2024 11:05:23 -0300 Subject: [PATCH 10/12] - reaction flag for transaction --- .../TelegramCore/Sources/TelegramEngine/Payments/Stars.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index c92d845045..aaaaaa8520 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -405,6 +405,9 @@ private extension StarsContext.State.Transaction { if (apiFlags & (1 << 10)) != 0 { flags.insert(.isGift) } + if (apiFlags & (1 << 11)) != 0 { + flags.insert(.isReaction) + } let media = extendedMedia.flatMap({ $0.compactMap { textMediaAndExpirationTimerFromApiMedia($0, PeerId(0)).media } }) ?? [] let _ = subscriptionPeriod @@ -450,6 +453,7 @@ public final class StarsContext { public static let isPending = Flags(rawValue: 1 << 2) public static let isFailed = Flags(rawValue: 1 << 3) public static let isGift = Flags(rawValue: 1 << 4) + public static let isReaction = Flags(rawValue: 1 << 5) } public enum Peer: Equatable { From 7eed0dc7aa7f6fab35b621d58a79bacbd41021b3 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Wed, 7 Aug 2024 11:09:54 -0300 Subject: [PATCH 11/12] - fulfillStars --- .../TelegramEngine/Payments/TelegramEnginePayments.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index 14fc327d3f..a7292134b5 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -93,5 +93,9 @@ public extension TelegramEngine { public func sendStarsPaymentForm(formId: Int64, source: BotPaymentInvoiceSource) -> Signal { return _internal_sendStarsPaymentForm(account: self.account, formId: formId, source: source) } + + public func fulfillStarsSubscription(peerId: EnginePeer.Id, subscriptionId: String) -> Signal { + return _internal_fulfillStarsSubscription(account: self.account, peerId: peerId, subscriptionId: subscriptionId) + } } } From b62b39523acf07d7b3447c934ac8baccd0988631 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Wed, 7 Aug 2024 12:00:19 -0300 Subject: [PATCH 12/12] - load force for subscriptions --- .../TelegramEngine/Payments/Stars.swift | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index 4c0f1d5c12..1d6ebe1e5c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -986,6 +986,31 @@ private final class StarsSubscriptionsContextImpl { self.updateState(updatedState) self.updateDisposable.set(_internal_updateStarsSubscription(account: self.account, peerId: self.account.peerId, subscriptionId: id, cancel: cancel).startStrict()) } + + private var previousLoadTimestamp: Double? + func load(force: Bool) { + assert(Queue.mainQueue().isCurrent()) + + let currentTimestamp = CFAbsoluteTimeGetCurrent() + if let previousLoadTimestamp = self.previousLoadTimestamp, currentTimestamp - previousLoadTimestamp < 60 && !force { + return + } + self.previousLoadTimestamp = currentTimestamp + + self.disposable.set((_internal_requestStarsSubscriptions(account: self.account, peerId: self.account.peerId, offset: "", missingBalance: false) + |> deliverOnMainQueue).start(next: { [weak self] status in + guard let self else { + return + } + self.nextOffset = status.nextSubscriptionsOffset + + var updatedState = self._state + updatedState.subscriptions = status.subscriptions + updatedState.isLoading = false + updatedState.canLoadMore = self.nextOffset != nil + self.updateState(updatedState) + })) + } } public final class StarsSubscriptionsContext { @@ -1032,6 +1057,12 @@ public final class StarsSubscriptionsContext { $0.updateSubscription(id: id, cancel: cancel) } } + + public func load(force: Bool) { + self.impl.with { + $0.load(force: force) + } + } }