diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 91c7cd7a05..be5a4b03d9 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12675,3 +12675,13 @@ Sorry for the inconvenience."; "Channel.AdminLog.MessageToggleProfileSignaturesOn" = "%@ enabled admin profiles"; "Channel.AdminLog.MessageToggleProfileSignaturesOff" = "%@ disabled admin profiles"; + +"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 d7b3ecb81a..2f4efe6f2f 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1005,8 +1005,11 @@ 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 + 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/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 8041db6208..8e40330e08 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -275,12 +275,12 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.backgroundColor = presentationData.theme.list.plainBackgroundColor self.webView.backgroundColor = presentationData.theme.list.plainBackgroundColor - self.webView.isOpaque = false + self.webView.alpha = 0.0 self.webView.allowsBackForwardNavigationGestures = true self.webView.scrollView.delegate = self self.webView.scrollView.clipsToBounds = false -// self.webView.translatesAutoresizingMaskIntoConstraints = false + self.webView.navigationDelegate = self self.webView.uiDelegate = self self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.title), options: [], context: nil) @@ -615,6 +615,10 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } self.didSetupSearch = false } else if keyPath == "estimatedProgress" { + if self.webView.estimatedProgress >= 0.1 && self.webView.alpha.isZero { + self.webView.alpha = 1.0 + self.webView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } self.updateState { $0.withUpdatedEstimatedProgress(self.webView.estimatedProgress) } } else if keyPath == "canGoBack" { self.updateState { $0.withUpdatedCanGoBack(self.webView.canGoBack) } @@ -759,7 +763,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU decisionHandler(.allow) } } - + func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { if let _ = self.currentError { self.currentError = nil @@ -771,10 +775,9 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - self.updateState { - $0 - .withUpdatedBackList(webView.backForwardList.backList.map { BrowserContentState.HistoryItem(webItem: $0) }) - .withUpdatedForwardList(webView.backForwardList.forwardList.map { BrowserContentState.HistoryItem(webItem: $0) }) + self.updateState {$0 + .withUpdatedBackList(webView.backForwardList.backList.map { BrowserContentState.HistoryItem(webItem: $0) }) + .withUpdatedForwardList(webView.backForwardList.forwardList.map { BrowserContentState.HistoryItem(webItem: $0) }) } self.parseFavicon() } 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/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..a3b38bca0e 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(amount: Int64) } enum ChatListNodeEntry: Comparable, Identifiable { diff --git a/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift b/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift index b3f88f9b8c..c41a4c936e 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 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 subscriptions.", 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 951a5e4b4a..f1b96e8970 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift @@ -17,17 +17,30 @@ import ContextUI import TelegramStringFormatting import UndoUI import ItemListDatePickerItem +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 @@ -36,6 +49,7 @@ private final class InviteLinkEditControllerArguments { private enum InviteLinksEditSection: Int32 { case title + case subscriptionFee case requestApproval case time case usage @@ -43,6 +57,7 @@ private enum InviteLinksEditSection: Int32 { } private enum InviteLinksEditEntryTag: ItemListItemTag { + case subscriptionFee case usage func isEqual(to other: ItemListItemTag) -> Bool { @@ -75,18 +90,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?, String, 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 +115,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 +136,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 +189,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, 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 + } + 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 +225,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 +255,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 +292,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 +301,49 @@ 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 + } + if value { + Queue.mainQueue().after(0.1) { + arguments.focusOnItem(.subscriptionFee) + } + } + }) + 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, tag: InviteLinksEditEntryTag.subscriptionFee, sectionId: self.section, textUpdated: { text in + arguments.updateState { state in + var updatedState = state + if var value = Int64(text) { + if let maxValue, value > maxValue { + value = maxValue + arguments.errorWithItem(.subscriptionFee) + } + 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 +354,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 +366,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 +385,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 +417,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 +430,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 { @@ -391,26 +479,51 @@ 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?, configuration: StarsSubscriptionConfiguration) -> [InviteLinksEditEntry] { var entries: [InviteLinksEditEntry] = [] entries.append(.titleHeader(presentationData.theme, presentationData.strings.InviteLink_Create_LinkNameTitle.uppercased())) 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 { + var label: String = "" + if let subscriptionFee = state.subscriptionFee, subscriptionFee > 0, let starsState { + 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, configuration.maxFee)) + } + 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 +532,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,18 +562,20 @@ 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 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() 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 +593,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) @@ -492,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: { @@ -555,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, @@ -570,14 +693,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 +719,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) + 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 +745,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 +780,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 } @@ -642,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), 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)) } @@ -686,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/InviteLinksUI/Sources/InviteLinkListController.swift b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift index c37972d0af..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) } @@ -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) @@ -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 { @@ -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 } @@ -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 { @@ -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 @@ -533,7 +536,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 @@ -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 d69e30ea2b..2ffa88c3f8 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift @@ -20,18 +20,52 @@ 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 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 @@ -50,6 +84,8 @@ private struct InviteLinkViewTransaction { private enum InviteLinkViewEntryId: Hashable { case link + case subscriptionHeader + case subscriptionPricing case creatorHeader case creator case requestHeader @@ -60,17 +96,23 @@ 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) 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 { case .link: return .link + case .subscriptionHeader: + return .subscriptionHeader + case .subscriptionPricing: + return .subscriptionPricing case .creatorHeader: return .creatorHeader case .creator: @@ -81,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) } } @@ -94,6 +136,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 @@ -124,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 @@ -139,33 +193,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,16 +242,16 @@ 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, _, _, _, _, _, _): + 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, _, _, _, _, _, _): + case let .importer(rhsIndex, _, _, _, _, _, _, _, _): return lhsIndex < rhsIndex } } @@ -192,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 { @@ -204,13 +272,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 { @@ -223,21 +300,41 @@ 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: .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: { - interaction.openPeer(peer.id) - }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, tag: nil, shimmering: loading ? ItemListPeerItemShimmering(alternationIndex: 0) : nil) + + 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) - 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) } } } @@ -284,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 } @@ -425,7 +524,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 @@ -483,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 @@ -568,7 +678,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 { @@ -699,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) }) @@ -716,7 +826,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, @@ -724,9 +834,29 @@ 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)) + + if let pricing = invite.pricing { + //TODO:localize + entries.append(.subscriptionHeader(presentationData.theme, "SUBSCRIPTION FEE")) + var title = "⭐️\(pricing.amount) / month" + var subtitle = "No one joined yet" + if state.count > 0 { + title += " x \(state.count)" + 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)) + } + entries.append(.creatorHeader(presentationData.theme, presentationData.strings.InviteLink_CreatedBy.uppercased())) entries.append(.creator(presentationData.theme, presentationData.dateTimeFormat, EnginePeer(creatorPeer), date)) @@ -776,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 } @@ -873,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) @@ -1002,7 +1132,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/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 1abb2b9da8..e305a37971 100644 --- a/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift @@ -7,9 +7,12 @@ 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 { + if case let .link(_, _, _, _, isRevoked, _, date, startDate, expireDate, usageLimit, count, _, _) = invite { if isRevoked { return 0.0 } @@ -42,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: @@ -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)? @@ -201,7 +208,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { self.iconBackgroundNode = ASDisplayNode() self.iconBackgroundNode.setLayerBlock { () -> CALayer in - return CAShapeLayer() + return CAGradientLayer() } self.iconNode = ASImageNode() @@ -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,20 +276,25 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { self?.extractedBackgroundImageNode.image = nil } }) + transition.updateAlpha(node: strongSelf.pricingNode.textNode, alpha: isExtracted ? 0.0 : 1.0) } } 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 } } 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 +314,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 @@ -327,26 +347,33 @@ 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: "") ?? "" 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 +402,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: 3.5, range: NSRange(range, in: text.string)) + } + pricingAttributedText = text + } + if invite.isRevoked { if !subtitleText.isEmpty { subtitleText += " • " @@ -443,6 +483,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 @@ -495,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 { @@ -505,13 +549,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: @@ -597,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() @@ -607,6 +656,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/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/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..81dc1224b2 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,10 +45,12 @@ public enum ItemListSingleLineInputAlignment { } public class ItemListSingleLineInputItem: ListViewItem, ItemListItem { + let context: AccountContext? let presentationData: ItemListPresentationData let title: NSAttributedString let text: String let placeholder: String + let label: String? let type: ItemListSingleLineInputItemType let returnKeyType: UIReturnKeyType let alignment: ItemListSingleLineInputAlignment @@ -65,11 +69,13 @@ 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, 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 @@ -130,11 +136,12 @@ 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 private let clearButtonNode: HighlightableButtonNode + private let labelNode: TextNode private var item: ItemListSingleLineInputItem? @@ -154,7 +161,7 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg self.maskNode = ASImageNode() - self.titleNode = TextNode() + self.titleNode = TextNodeWithEntities() self.measureTitleSizeNode = TextNode() self.textNode = TextFieldNode() @@ -165,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) + 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 @@ -209,8 +221,10 @@ 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 makeLabelLayout = TextNode.asyncLayout(self.labelNode) let currentItem = self.item @@ -241,15 +255,24 @@ 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 (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) let insets = itemListNeighborsGroupedInsets(neighbors, params) @@ -280,10 +303,23 @@ 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() + let _ = labelApply() let secureEntry: Bool let capitalizationType: UITextAutocapitalizationType @@ -353,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/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/PeerInfoUI/Sources/ChannelAdminController.swift b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift index 914ed416ac..a97ea37c15 100644 --- a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift @@ -580,7 +580,7 @@ private func canEditAdminRights(accountPeerId: EnginePeer.Id, channelPeer: Engin switch initialParticipant { case .creator: return false - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): if let adminInfo = adminInfo { return adminInfo.canBeEditedByAccountPeer || adminInfo.promotedBy == accountPeerId } else { @@ -703,7 +703,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s let currentRightsFlags: TelegramChatAdminRightsFlags if let updatedFlags = state.updatedFlags { currentRightsFlags = updatedFlags - } else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminRights, _, _) = initialParticipant, let adminRights = maybeAdminRights { + } else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminRights, _, _, _) = initialParticipant, let adminRights = maybeAdminRights { currentRightsFlags = adminRights.rights.rights } else if let initialParticipant = initialParticipant, case let .creator(_, maybeAdminRights, _) = initialParticipant, let adminRights = maybeAdminRights { currentRightsFlags = adminRights.rights.rights @@ -761,7 +761,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s } } else { if case let .user(adminPeer) = adminPeer, adminPeer.botInfo != nil, case .group = channel.info, invite, let channelPeer = channelPeer, canEditAdminRights(accountPeerId: accountPeerId, channelPeer: channelPeer, initialParticipant: initialParticipant) { - if let initialParticipant = initialParticipant, case let .member(_, _, adminInfo, _, _) = initialParticipant, adminInfo != nil { + if let initialParticipant = initialParticipant, case let .member(_, _, adminInfo, _, _, _) = initialParticipant, adminInfo != nil { } else { entries.append(.adminRights(presentationData.theme, presentationData.strings.Bot_AddToChat_Add_AdminRights, state.adminRights)) @@ -784,7 +784,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s let currentRightsFlags: TelegramChatAdminRightsFlags if let updatedFlags = state.updatedFlags { currentRightsFlags = updatedFlags - } else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminRights, _, _) = initialParticipant, let adminRights = maybeAdminRights { + } else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminRights, _, _, _) = initialParticipant, let adminRights = maybeAdminRights { currentRightsFlags = adminRights.rights.rights } else { currentRightsFlags = accountUserRightsFlags.subtracting(.canAddAdmins).subtracting(.canBeAnonymous) @@ -846,14 +846,14 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s canTransfer = true } - if let initialParticipant = initialParticipant, case let .member(_, _, adminInfo, _, _) = initialParticipant, admin.id != accountPeerId, adminInfo != nil { + if let initialParticipant = initialParticipant, case let .member(_, _, adminInfo, _, _, _) = initialParticipant, admin.id != accountPeerId, adminInfo != nil { if channel.flags.contains(.isCreator) { canDismiss = true } else { switch initialParticipant { case .creator: break - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): if let adminInfo = adminInfo { if adminInfo.promotedBy == accountPeerId || adminInfo.canBeEditedByAccountPeer { canDismiss = true @@ -862,7 +862,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s } } } - } else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminInfo, _, _) = initialParticipant, let adminInfo = maybeAdminInfo { + } else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminInfo, _, _, _) = initialParticipant, let adminInfo = maybeAdminInfo { var index = 0 rightsLoop: for right in rightsOrder { let enabled: Bool = false @@ -955,7 +955,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s entries.append(.rank(presentationData.theme, presentationData.strings, isCreator ? presentationData.strings.Group_EditAdmin_RankOwnerPlaceholder : presentationData.strings.Group_EditAdmin_RankAdminPlaceholder, currentRank ?? "", rankEnabled)) } else { if case let .user(adminPeer) = adminPeer, adminPeer.botInfo != nil, invite { - if let initialParticipant = initialParticipant, case let .member(_, _, adminRights, _, _) = initialParticipant, adminRights != nil { + if let initialParticipant = initialParticipant, case let .member(_, _, adminRights, _, _, _) = initialParticipant, adminRights != nil { } else { entries.append(.adminRights(presentationData.theme, presentationData.strings.Bot_AddToChat_Add_AdminRights, state.adminRights)) } @@ -988,7 +988,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s let currentRightsFlags: TelegramChatAdminRightsFlags if let updatedFlags = state.updatedFlags { currentRightsFlags = updatedFlags - } else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminRights, _, _) = initialParticipant, let adminRights = maybeAdminRights { + } else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminRights, _, _, _) = initialParticipant, let adminRights = maybeAdminRights { currentRightsFlags = adminRights.rights.rights.subtracting(.canAddAdmins).subtracting(.canBeAnonymous) } else { currentRightsFlags = accountUserRightsFlags.subtracting(.canAddAdmins).subtracting(.canBeAnonymous) @@ -1016,7 +1016,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s entries.append(.rankInfo(presentationData.theme, presentationData.strings.Group_EditAdmin_RankInfo(placeholder).string, invite)) } - if let initialParticipant = initialParticipant, case let .member(_, _, adminInfo, _, _) = initialParticipant, admin.id != accountPeerId, let adminInfo { + if let initialParticipant = initialParticipant, case let .member(_, _, adminInfo, _, _, _) = initialParticipant, admin.id != accountPeerId, let adminInfo { var canDismiss = false if accountIsCreator { canDismiss = true @@ -1319,7 +1319,7 @@ public func channelAdminController(context: AccountContext, updatedPresentationD case let .creator(_, adminInfo, rank): currentRank = rank currentFlags = adminInfo?.rights.rights ?? maskRightsFlags.subtracting(.canBeAnonymous) - case let .member(_, _, adminInfo, _, rank): + case let .member(_, _, adminInfo, _, rank, _): if updateFlags == nil { if adminInfo?.rights == nil { if channel.flags.contains(.isCreator) { diff --git a/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift b/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift index e967b8901a..c30f940220 100644 --- a/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift @@ -282,7 +282,7 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { switch participant.participant { case .creator: peerText = strings.Channel_Management_LabelOwner - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): if let adminInfo = adminInfo { if let peer = participant.peers[adminInfo.promotedBy] { if peer.id == participant.peer.id { @@ -444,14 +444,14 @@ private func channelAdminsControllerEntries(presentationData: PresentationData, switch lhs.participant { case .creator: lhsInvitedAt = Int32.min - case let .member(_, invitedAt, _, _, _): + case let .member(_, invitedAt, _, _, _, _): lhsInvitedAt = invitedAt } let rhsInvitedAt: Int32 switch rhs.participant { case .creator: rhsInvitedAt = Int32.min - case let .member(_, invitedAt, _, _, _): + case let .member(_, invitedAt, _, _, _, _): rhsInvitedAt = invitedAt } return lhsInvitedAt < rhsInvitedAt @@ -463,7 +463,7 @@ private func channelAdminsControllerEntries(presentationData: PresentationData, case .creator: canEdit = false canOpen = isGroup && peer.flags.contains(.isCreator) - case let .member(id, _, adminInfo, _, _): + case let .member(id, _, adminInfo, _, _, _): if id == accountPeerId { canEdit = false } else if let adminInfo = adminInfo { @@ -530,14 +530,14 @@ private func channelAdminsControllerEntries(presentationData: PresentationData, switch lhs.participant { case .creator: lhsInvitedAt = Int32.min - case let .member(_, invitedAt, _, _, _): + case let .member(_, invitedAt, _, _, _, _): lhsInvitedAt = invitedAt } let rhsInvitedAt: Int32 switch rhs.participant { case .creator: rhsInvitedAt = Int32.min - case let .member(_, invitedAt, _, _, _): + case let .member(_, invitedAt, _, _, _, _): rhsInvitedAt = invitedAt } return lhsInvitedAt < rhsInvitedAt @@ -552,7 +552,7 @@ private func channelAdminsControllerEntries(presentationData: PresentationData, } else { canEdit = false } - case let .member(id, _, adminInfo, _, _): + case let .member(id, _, adminInfo, _, _, _): if id == accountPeerId { editable = false } else if let adminInfo = adminInfo { @@ -763,7 +763,7 @@ public func channelAdminsController(context: AccountContext, updatedPresentation switch participant.participant { case .creator: return - case let .member(_, _, _, banInfo, _): + case let .member(_, _, _, banInfo, _, _): if let banInfo = banInfo { var canUnban = false if banInfo.restrictedBy != context.account.peerId { @@ -892,7 +892,7 @@ public func channelAdminsController(context: AccountContext, updatedPresentation var peers: [EnginePeer.Id: EnginePeer] = [:] peers[creator.id] = creator peers[peer.id] = peer - result.append(RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: .internal_groupSpecific), promotedBy: creator.id, canBeEditedByAccountPeer: creator.id == context.account.peerId), banInfo: nil, rank: nil), peer: peer._asPeer(), peers: peers.mapValues({ $0._asPeer() }))) + result.append(RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: .internal_groupSpecific), promotedBy: creator.id, canBeEditedByAccountPeer: creator.id == context.account.peerId), banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: peer._asPeer(), peers: peers.mapValues({ $0._asPeer() }))) case .member: break } diff --git a/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift b/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift index bca367086e..bdd377d175 100644 --- a/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift @@ -303,7 +303,7 @@ private func channelBannedMemberControllerEntries(presentationData: Presentation let currentRightsFlags: TelegramChatBannedRightsFlags if let updatedFlags = state.updatedFlags { currentRightsFlags = updatedFlags - } else if let initialParticipant = initialParticipant, case let .member(_, _, _, maybeBanInfo, _) = initialParticipant, let banInfo = maybeBanInfo { + } else if let initialParticipant = initialParticipant, case let .member(_, _, _, maybeBanInfo, _, _) = initialParticipant, let banInfo = maybeBanInfo { currentRightsFlags = banInfo.rights.flags } else { currentRightsFlags = defaultBannedRights.flags @@ -312,7 +312,7 @@ private func channelBannedMemberControllerEntries(presentationData: Presentation let currentTimeout: Int32 if let updatedTimeout = state.updatedTimeout { currentTimeout = updatedTimeout - } else if let initialParticipant = initialParticipant, case let .member(_, _, _, maybeBanInfo, _) = initialParticipant, let banInfo = maybeBanInfo { + } else if let initialParticipant = initialParticipant, case let .member(_, _, _, maybeBanInfo, _, _) = initialParticipant, let banInfo = maybeBanInfo { currentTimeout = banInfo.rights.untilDate } else { currentTimeout = Int32.max @@ -351,7 +351,7 @@ private func channelBannedMemberControllerEntries(presentationData: Presentation entries.append(.timeout(presentationData.theme, presentationData.strings.GroupPermission_Duration, currentTimeoutString)) - if let initialParticipant = initialParticipant, case let .member(_, _, _, banInfo?, _) = initialParticipant, let initialBannedBy = initialBannedBy { + if let initialParticipant = initialParticipant, case let .member(_, _, _, banInfo?, _, _) = initialParticipant, let initialBannedBy = initialBannedBy { entries.append(.exceptionInfo(presentationData.theme, presentationData.strings.GroupPermission_AddedInfo(initialBannedBy.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), stringForRelativeSymbolicTimestamp(strings: presentationData.strings, relativeTimestamp: banInfo.timestamp, relativeTo: state.referenceTimestamp, dateTimeFormat: presentationData.dateTimeFormat)).string)) entries.append(.delete(presentationData.theme, presentationData.strings.GroupPermission_Delete)) } @@ -363,7 +363,7 @@ private func channelBannedMemberControllerEntries(presentationData: Presentation let currentRightsFlags: TelegramChatBannedRightsFlags if let updatedFlags = state.updatedFlags { currentRightsFlags = updatedFlags - } else if let initialParticipant = initialParticipant, case let .member(_, _, _, maybeBanInfo, _) = initialParticipant, let banInfo = maybeBanInfo { + } else if let initialParticipant = initialParticipant, case let .member(_, _, _, maybeBanInfo, _, _) = initialParticipant, let banInfo = maybeBanInfo { currentRightsFlags = banInfo.rights.flags } else { currentRightsFlags = defaultBannedRightsFlags @@ -372,7 +372,7 @@ private func channelBannedMemberControllerEntries(presentationData: Presentation let currentTimeout: Int32 if let updatedTimeout = state.updatedTimeout { currentTimeout = updatedTimeout - } else if let initialParticipant = initialParticipant, case let .member(_, _, _, maybeBanInfo, _) = initialParticipant, let banInfo = maybeBanInfo { + } else if let initialParticipant = initialParticipant, case let .member(_, _, _, maybeBanInfo, _, _) = initialParticipant, let banInfo = maybeBanInfo { currentTimeout = banInfo.rights.untilDate } else { currentTimeout = Int32.max @@ -411,7 +411,7 @@ private func channelBannedMemberControllerEntries(presentationData: Presentation entries.append(.timeout(presentationData.theme, presentationData.strings.GroupPermission_Duration, currentTimeoutString)) - if let initialParticipant = initialParticipant, case let .member(_, _, _, banInfo?, _) = initialParticipant, let initialBannedBy = initialBannedBy { + if let initialParticipant = initialParticipant, case let .member(_, _, _, banInfo?, _, _) = initialParticipant, let initialBannedBy = initialBannedBy { entries.append(.exceptionInfo(presentationData.theme, presentationData.strings.GroupPermission_AddedInfo(initialBannedBy.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), stringForRelativeSymbolicTimestamp(strings: presentationData.strings, relativeTimestamp: banInfo.timestamp, relativeTo: state.referenceTimestamp, dateTimeFormat: presentationData.dateTimeFormat)).string)) entries.append(.delete(presentationData.theme, presentationData.strings.GroupPermission_Delete)) } @@ -462,7 +462,7 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen var effectiveRightsFlags: TelegramChatBannedRightsFlags if let updatedFlags = state.updatedFlags { effectiveRightsFlags = updatedFlags - } else if let initialParticipant = initialParticipant, case let .member(_, _, _, banInfo?, _) = initialParticipant { + } else if let initialParticipant = initialParticipant, case let .member(_, _, _, banInfo?, _, _) = initialParticipant { effectiveRightsFlags = banInfo.rights.flags } else { effectiveRightsFlags = defaultBannedRightsFlags @@ -671,7 +671,7 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen } if updateFlags == nil && updateTimeout == nil { - if case let .member(_, _, _, maybeBanInfo, _) = initialParticipant { + if case let .member(_, _, _, maybeBanInfo, _, _) = initialParticipant { if maybeBanInfo == nil { updateFlags = defaultBannedRightsFlags updateTimeout = Int32.max @@ -683,7 +683,7 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen let currentRightsFlags: TelegramChatBannedRightsFlags if let updatedFlags = updateFlags { currentRightsFlags = updatedFlags - } else if case let .member(_, _, _, maybeBanInfo, _) = initialParticipant, let banInfo = maybeBanInfo { + } else if case let .member(_, _, _, maybeBanInfo, _, _) = initialParticipant, let banInfo = maybeBanInfo { currentRightsFlags = banInfo.rights.flags } else { currentRightsFlags = defaultBannedRightsFlags @@ -692,7 +692,7 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen let currentTimeout: Int32 if let updateTimeout = updateTimeout { currentTimeout = updateTimeout - } else if case let .member(_, _, _, maybeBanInfo, _) = initialParticipant, let banInfo = maybeBanInfo { + } else if case let .member(_, _, _, maybeBanInfo, _, _) = initialParticipant, let banInfo = maybeBanInfo { currentTimeout = banInfo.rights.untilDate } else { currentTimeout = Int32.max @@ -724,7 +724,7 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen } var previousRights: TelegramChatBannedRights? - if let initialParticipant = initialParticipant, case let .member(_, _, _, banInfo, _) = initialParticipant, banInfo != nil { + if let initialParticipant = initialParticipant, case let .member(_, _, _, banInfo, _, _) = initialParticipant, banInfo != nil { previousRights = banInfo?.rights } @@ -825,7 +825,7 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen } let title: String - if let initialParticipant = initialParticipant, case let .member(_, _, _, banInfo, _) = initialParticipant, banInfo != nil { + if let initialParticipant = initialParticipant, case let .member(_, _, _, banInfo, _, _) = initialParticipant, banInfo != nil { title = presentationData.strings.GroupPermission_Title } else { title = presentationData.strings.GroupPermission_NewTitle diff --git a/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift b/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift index b028a7f806..6ee7bdeeb8 100644 --- a/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift @@ -160,7 +160,7 @@ private enum ChannelBlacklistEntry: ItemListNodeEntry { case let .peerItem(_, strings, dateTimeFormat, nameDisplayOrder, _, participant, editing, enabled): var text: ItemListPeerItemText = .none switch participant.participant { - case let .member(_, _, _, banInfo, _): + case let .member(_, _, _, banInfo, _, _): if let banInfo = banInfo, let peer = participant.peers[banInfo.restrictedBy] { text = .text(strings.Channel_Management_RemovedBy(EnginePeer(peer).displayTitle(strings: strings, displayOrder: nameDisplayOrder)).string, .secondary) } @@ -306,7 +306,7 @@ public func channelBlacklistController(context: AccountContext, updatedPresentat switch participant.participant { case .creator: return - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): if let adminInfo = adminInfo, adminInfo.promotedBy != context.account.peerId { presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Channel_Members_AddBannedErrorAdmin, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) return diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift b/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift index aa97ed2b2f..6f6e2f63f5 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift @@ -511,7 +511,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon case .creator: canPromote = false canRestrict = false - case let .member(_, _, adminRights, bannedRights, _): + case let .member(_, _, adminRights, bannedRights, _, _): if channel.hasPermission(.addAdmins) { canPromote = true } else { @@ -740,7 +740,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon case .creator: canPromote = false canRestrict = false - case let .member(_, _, adminRights, bannedRights, _): + case let .member(_, _, adminRights, bannedRights, _, _): if channel.hasPermission(.addAdmins) { canPromote = true } else { @@ -778,7 +778,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon switch participant.participant { case .creator: label = presentationData.strings.Channel_Management_LabelOwner - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): if adminInfo != nil { label = presentationData.strings.Channel_Management_LabelEditor } @@ -809,7 +809,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon switch participant.participant { case .creator: label = presentationData.strings.Channel_Management_LabelOwner - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): if let adminInfo = adminInfo { if let peer = participant.peers[adminInfo.promotedBy] { if peer.id == participant.peer.id { @@ -822,7 +822,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon } case .searchBanned: switch participant.participant { - case let .member(_, _, _, banInfo, _): + case let .member(_, _, _, banInfo, _, _): if let banInfo = banInfo { var exceptionsString = "" let sendMediaRights = banSendMediaSubList().map { $0.0 } @@ -844,7 +844,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon } case .searchKicked: switch participant.participant { - case let .member(_, _, _, banInfo, _): + case let .member(_, _, _, banInfo, _, _): if let banInfo = banInfo, let peer = participant.peers[banInfo.restrictedBy] { label = presentationData.strings.Channel_Management_RemovedBy(EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string } @@ -972,11 +972,11 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon peers[creator.id] = creator } peers[peer.id] = EnginePeer(peer) - renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: TelegramChatAdminRightsFlags.peerSpecific(peer: .legacyGroup(group))), promotedBy: creatorPeer?.id ?? context.account.peerId, canBeEditedByAccountPeer: creatorPeer?.id == context.account.peerId), banInfo: nil, rank: nil), peer: peer, peers: peers.mapValues({ $0._asPeer() })) + renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: TelegramChatAdminRightsFlags.peerSpecific(peer: .legacyGroup(group))), promotedBy: creatorPeer?.id ?? context.account.peerId, canBeEditedByAccountPeer: creatorPeer?.id == context.account.peerId), banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: peer, peers: peers.mapValues({ $0._asPeer() })) case .member: var peers: [EnginePeer.Id: EnginePeer] = [:] peers[peer.id] = EnginePeer(peer) - renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil), peer: peer, peers: peers.mapValues({ $0._asPeer() })) + renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: peer, peers: peers.mapValues({ $0._asPeer() })) } matchingMembers.append(renderedParticipant) } @@ -1057,7 +1057,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon switch participant.participant { case .creator: label = presentationData.strings.Channel_Management_LabelOwner - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): if adminInfo != nil { label = presentationData.strings.Channel_Management_LabelEditor } @@ -1073,7 +1073,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon switch participant.participant { case .creator: label = presentationData.strings.Channel_Management_LabelOwner - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): if let adminInfo = adminInfo { if let peer = participant.peers[adminInfo.promotedBy] { if peer.id == participant.peer.id { @@ -1086,7 +1086,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon } case .searchBanned: switch participant.participant { - case let .member(_, _, _, banInfo, _): + case let .member(_, _, _, banInfo, _, _): if let banInfo = banInfo { var exceptionsString = "" let sendMediaRights = banSendMediaSubList().map { $0.0 } @@ -1108,7 +1108,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon } case .searchKicked: switch participant.participant { - case let .member(_, _, _, banInfo, _): + case let .member(_, _, _, banInfo, _, _): if let banInfo = banInfo, let peer = participant.peers[banInfo.restrictedBy] { label = presentationData.strings.Channel_Management_RemovedBy(EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string } diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift b/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift index 5b399fe10a..c85e72cd03 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift @@ -399,11 +399,11 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { var peers: [EnginePeer.Id: EnginePeer] = [:] peers[creator.id] = creator peers[peer.id] = EnginePeer(peer) - renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: TelegramChatAdminRightsFlags.peerSpecific(peer: EnginePeer(mainPeer))), promotedBy: creator.id, canBeEditedByAccountPeer: creator.id == context.account.peerId), banInfo: nil, rank: nil), peer: peer, peers: peers.mapValues({ $0._asPeer() }), presences: peerView.peerPresences) + renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: TelegramChatAdminRightsFlags.peerSpecific(peer: EnginePeer(mainPeer))), promotedBy: creator.id, canBeEditedByAccountPeer: creator.id == context.account.peerId), banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: peer, peers: peers.mapValues({ $0._asPeer() }), presences: peerView.peerPresences) case .member: var peers: [EnginePeer.Id: EnginePeer] = [:] peers[peer.id] = EnginePeer(peer) - renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil), peer: peer, peers: peers.mapValues({ $0._asPeer() }), presences: peerView.peerPresences) + renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: peer, peers: peers.mapValues({ $0._asPeer() }), presences: peerView.peerPresences) } entries.append(.peer(index, renderedParticipant, ContactsPeerItemEditing(editable: false, editing: false, revealed: false), label, enabled, false, false)) diff --git a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift index e26fd8e287..59bee118b4 100644 --- a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift @@ -381,7 +381,7 @@ private enum ChannelPermissionsEntry: ItemListNodeEntry { case let .peerItem(_, strings, dateTimeFormat, nameDisplayOrder, _, participant, editing, enabled, canOpen, defaultBannedRights): var text: ItemListPeerItemText = .none switch participant.participant { - case let .member(_, _, _, banInfo, _): + case let .member(_, _, _, banInfo, _, _): var exceptionsString = "" if let banInfo = banInfo { let sendMediaRights = banSendMediaSubList().map { $0.0 } @@ -937,7 +937,7 @@ public func channelPermissionsController(context: AccountContext, updatedPresent switch participant.participant { case .creator: return - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): if let adminInfo = adminInfo, adminInfo.promotedBy != context.account.peerId { presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Channel_Members_AddBannedErrorAdmin, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) return 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/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index 94e9ed572d..cedad73715 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -315,10 +315,10 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, ASScrollView let selfPeer: EnginePeer = .user(TelegramUser(id: self.context.account.peerId, accessHash: nil, firstName: nil, lastName: nil, 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)) let peer1: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_1_Name, lastName: nil, 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)) let peer2: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(2)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_2_Name, lastName: nil, 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)) - let peer3: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(3)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil)) + let peer3: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(3)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil, subscriptionUntilDate: nil)) let peer3Author: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_AuthorName, lastName: nil, 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)) let peer4: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_4_Name, lastName: nil, 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)) - let peer5: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(5)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_5_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .broadcast(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil)) + let peer5: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(5)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_5_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .broadcast(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil, subscriptionUntilDate: nil)) let peer6: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt64Value(5)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_6_Name, lastName: nil, 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)) let timestamp = self.referenceTimestamp diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index 336cf3e8b6..70804382b8 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -464,10 +464,10 @@ final class ThemePreviewControllerNode: ASDisplayNode, ASScrollViewDelegate { let selfPeer: EnginePeer = .user(TelegramUser(id: self.context.account.peerId, accessHash: nil, firstName: nil, lastName: nil, 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)) let peer1: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_1_Name, lastName: nil, 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)) let peer2: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(2)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_2_Name, lastName: nil, 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)) - let peer3: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(3)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil)) + let peer3: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(3)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil, subscriptionUntilDate: nil)) let peer3Author: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_AuthorName, lastName: nil, 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)) let peer4: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_4_Name, lastName: nil, 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)) - let peer5: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(5)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_5_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .broadcast(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil)) + let peer5: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(5)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_5_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .broadcast(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil, subscriptionUntilDate: nil)) let peer6: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt64Value(5)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_6_Name, lastName: nil, 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)) let peer7: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(6)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_7_Name, lastName: nil, 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)) diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index 13650ccf00..ca1656d854 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/StatisticsUI/Sources/GroupStatsController.swift b/submodules/StatisticsUI/Sources/GroupStatsController.swift index c56e2c81ff..0bff3fe232 100644 --- a/submodules/StatisticsUI/Sources/GroupStatsController.swift +++ b/submodules/StatisticsUI/Sources/GroupStatsController.swift @@ -686,7 +686,7 @@ private func canEditAdminRights(accountPeerId: EnginePeer.Id, channelPeer: Engin switch initialParticipant { case .creator: return false - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): if let adminInfo = adminInfo { return adminInfo.canBeEditedByAccountPeer || adminInfo.promotedBy == accountPeerId } else { diff --git a/submodules/StatisticsUI/Sources/StarsTransactionItem.swift b/submodules/StatisticsUI/Sources/StarsTransactionItem.swift index 6a225c18dc..aadd16c894 100644 --- a/submodules/StatisticsUI/Sources/StarsTransactionItem.swift +++ b/submodules/StatisticsUI/Sources/StarsTransactionItem.swift @@ -231,15 +231,20 @@ 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 { + //TODO:localize + 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 @@ -272,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] = [] @@ -305,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/ApiUtils/ApiGroupOrChannel.swift b/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift index 657ef472d9..396a271ecc 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift @@ -61,7 +61,7 @@ func parseTelegramGroupOrChannel(chat: Api.Chat) -> Peer? { return TelegramGroup(id: PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(id)), title: "", photo: [], participantCount: 0, role: .member, membership: .Removed, flags: [], defaultBannedRights: nil, migrationReference: nil, creationDate: 0, version: 0) case let .chatForbidden(id, title): return TelegramGroup(id: PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(id)), title: title, photo: [], participantCount: 0, role: .member, membership: .Removed, flags: [], defaultBannedRights: nil, migrationReference: nil, creationDate: 0, version: 0) - case let .channel(flags, flags2, id, accessHash, title, username, photo, date, restrictionReason, adminRights, bannedRights, defaultBannedRights, _, usernames, _, color, profileColor, emojiStatus, boostLevel, _): + case let .channel(flags, flags2, id, accessHash, title, username, photo, date, restrictionReason, adminRights, bannedRights, defaultBannedRights, _, usernames, _, color, profileColor, emojiStatus, boostLevel, subscriptionUntilDate): let isMin = (flags & (1 << 12)) != 0 let participationStatus: TelegramChannelParticipationStatus @@ -176,7 +176,7 @@ func parseTelegramGroupOrChannel(chat: Api.Chat) -> Peer? { } } - return TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id)), accessHash: accessHashValue, title: title, username: username, photo: imageRepresentationsForApiChatPhoto(photo), creationDate: date, version: 0, participationStatus: participationStatus, info: info, flags: channelFlags, restrictionInfo: restrictionInfo, adminRights: adminRights.flatMap(TelegramChatAdminRights.init), bannedRights: bannedRights.flatMap(TelegramChatBannedRights.init), defaultBannedRights: defaultBannedRights.flatMap(TelegramChatBannedRights.init), usernames: usernames?.map(TelegramPeerUsername.init(apiUsername:)) ?? [], storiesHidden: storiesHidden, nameColor: nameColorIndex.flatMap { PeerNameColor(rawValue: $0) }, backgroundEmojiId: backgroundEmojiId, profileColor: profileColorIndex.flatMap { PeerNameColor(rawValue: $0) }, profileBackgroundEmojiId: profileBackgroundEmojiId, emojiStatus: emojiStatus.flatMap(PeerEmojiStatus.init(apiStatus:)), approximateBoostLevel: boostLevel) + return TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id)), accessHash: accessHashValue, title: title, username: username, photo: imageRepresentationsForApiChatPhoto(photo), creationDate: date, version: 0, participationStatus: participationStatus, info: info, flags: channelFlags, restrictionInfo: restrictionInfo, adminRights: adminRights.flatMap(TelegramChatAdminRights.init), bannedRights: bannedRights.flatMap(TelegramChatBannedRights.init), defaultBannedRights: defaultBannedRights.flatMap(TelegramChatBannedRights.init), usernames: usernames?.map(TelegramPeerUsername.init(apiUsername:)) ?? [], storiesHidden: storiesHidden, nameColor: nameColorIndex.flatMap { PeerNameColor(rawValue: $0) }, backgroundEmojiId: backgroundEmojiId, profileColor: profileColorIndex.flatMap { PeerNameColor(rawValue: $0) }, profileBackgroundEmojiId: profileBackgroundEmojiId, emojiStatus: emojiStatus.flatMap(PeerEmojiStatus.init(apiStatus:)), approximateBoostLevel: boostLevel, subscriptionUntilDate: subscriptionUntilDate) case let .channelForbidden(flags, id, accessHash, title, untilDate): let info: TelegramChannelInfo if (flags & Int32(1 << 8)) != 0 { @@ -185,7 +185,7 @@ func parseTelegramGroupOrChannel(chat: Api.Chat) -> Peer? { info = .broadcast(TelegramChannelBroadcastInfo(flags: [])) } - return TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id)), accessHash: .personal(accessHash), title: title, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .kicked, info: info, flags: TelegramChannelFlags(), restrictionInfo: nil, adminRights: nil, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: untilDate ?? Int32.max), defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil) + return TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id)), accessHash: .personal(accessHash), title: title, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .kicked, info: info, flags: TelegramChannelFlags(), restrictionInfo: nil, adminRights: nil, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: untilDate ?? Int32.max), defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil, subscriptionUntilDate: nil) } } @@ -193,7 +193,7 @@ func mergeGroupOrChannel(lhs: Peer?, rhs: Api.Chat) -> Peer? { switch rhs { case .chat, .chatEmpty, .chatForbidden, .channelForbidden: return parseTelegramGroupOrChannel(chat: rhs) - case let .channel(flags, flags2, _, accessHash, title, username, photo, _, _, _, _, defaultBannedRights, _, usernames, _, color, profileColor, emojiStatus, boostLevel, _): + case let .channel(flags, flags2, _, accessHash, title, username, photo, _, _, _, _, defaultBannedRights, _, usernames, _, color, profileColor, emojiStatus, boostLevel, subscriptionUntilDate): let isMin = (flags & (1 << 12)) != 0 if accessHash != nil && !isMin { return parseTelegramGroupOrChannel(chat: rhs) @@ -257,7 +257,7 @@ func mergeGroupOrChannel(lhs: Peer?, rhs: Api.Chat) -> Peer? { let parsedEmojiStatus = emojiStatus.flatMap(PeerEmojiStatus.init(apiStatus:)) - return TelegramChannel(id: lhs.id, accessHash: lhs.accessHash, title: title, username: username, photo: imageRepresentationsForApiChatPhoto(photo), creationDate: lhs.creationDate, version: lhs.version, participationStatus: lhs.participationStatus, info: info, flags: channelFlags, restrictionInfo: lhs.restrictionInfo, adminRights: lhs.adminRights, bannedRights: lhs.bannedRights, defaultBannedRights: defaultBannedRights.flatMap(TelegramChatBannedRights.init), usernames: usernames?.map(TelegramPeerUsername.init(apiUsername:)) ?? [], storiesHidden: storiesHidden, nameColor: nameColorIndex.flatMap { PeerNameColor(rawValue: $0) }, backgroundEmojiId: backgroundEmojiId, profileColor: profileColorIndex.flatMap { PeerNameColor(rawValue: $0) }, profileBackgroundEmojiId: profileBackgroundEmojiId, emojiStatus: parsedEmojiStatus, approximateBoostLevel: boostLevel) + return TelegramChannel(id: lhs.id, accessHash: lhs.accessHash, title: title, username: username, photo: imageRepresentationsForApiChatPhoto(photo), creationDate: lhs.creationDate, version: lhs.version, participationStatus: lhs.participationStatus, info: info, flags: channelFlags, restrictionInfo: lhs.restrictionInfo, adminRights: lhs.adminRights, bannedRights: lhs.bannedRights, defaultBannedRights: defaultBannedRights.flatMap(TelegramChatBannedRights.init), usernames: usernames?.map(TelegramPeerUsername.init(apiUsername:)) ?? [], storiesHidden: storiesHidden, nameColor: nameColorIndex.flatMap { PeerNameColor(rawValue: $0) }, backgroundEmojiId: backgroundEmojiId, profileColor: profileColorIndex.flatMap { PeerNameColor(rawValue: $0) }, profileBackgroundEmojiId: profileBackgroundEmojiId, emojiStatus: parsedEmojiStatus, approximateBoostLevel: boostLevel, subscriptionUntilDate: subscriptionUntilDate) } else { return parseTelegramGroupOrChannel(chat: rhs) } @@ -311,6 +311,6 @@ func mergeChannel(lhs: TelegramChannel?, rhs: TelegramChannel) -> TelegramChanne let storiesHidden: Bool? = rhs.storiesHidden ?? lhs.storiesHidden - return TelegramChannel(id: lhs.id, accessHash: accessHash, title: rhs.title, username: rhs.username, photo: rhs.photo, creationDate: rhs.creationDate, version: rhs.version, participationStatus: lhs.participationStatus, info: info, flags: channelFlags, restrictionInfo: rhs.restrictionInfo, adminRights: rhs.adminRights, bannedRights: rhs.bannedRights, defaultBannedRights: rhs.defaultBannedRights, usernames: rhs.usernames, storiesHidden: storiesHidden, nameColor: rhs.nameColor, backgroundEmojiId: rhs.backgroundEmojiId, profileColor: rhs.profileColor, profileBackgroundEmojiId: rhs.profileBackgroundEmojiId, emojiStatus: rhs.emojiStatus, approximateBoostLevel: rhs.approximateBoostLevel) + return TelegramChannel(id: lhs.id, accessHash: accessHash, title: rhs.title, username: rhs.username, photo: rhs.photo, creationDate: rhs.creationDate, version: rhs.version, participationStatus: lhs.participationStatus, info: info, flags: channelFlags, restrictionInfo: rhs.restrictionInfo, adminRights: rhs.adminRights, bannedRights: rhs.bannedRights, defaultBannedRights: rhs.defaultBannedRights, usernames: rhs.usernames, storiesHidden: storiesHidden, nameColor: rhs.nameColor, backgroundEmojiId: rhs.backgroundEmojiId, profileColor: rhs.profileColor, profileBackgroundEmojiId: rhs.profileBackgroundEmojiId, emojiStatus: rhs.emojiStatus, approximateBoostLevel: rhs.approximateBoostLevel, subscriptionUntilDate: rhs.subscriptionUntilDate) } diff --git a/submodules/TelegramCore/Sources/ApiUtils/CachedChannelParticipants.swift b/submodules/TelegramCore/Sources/ApiUtils/CachedChannelParticipants.swift index 1ad822c3ad..2adf13b334 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/CachedChannelParticipants.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/CachedChannelParticipants.swift @@ -72,13 +72,13 @@ public struct ChannelParticipantBannedInfo: PostboxCoding, Equatable { public enum ChannelParticipant: PostboxCoding, Equatable { case creator(id: PeerId, adminInfo: ChannelParticipantAdminInfo?, rank: String?) - case member(id: PeerId, invitedAt: Int32, adminInfo: ChannelParticipantAdminInfo?, banInfo: ChannelParticipantBannedInfo?, rank: String?) + case member(id: PeerId, invitedAt: Int32, adminInfo: ChannelParticipantAdminInfo?, banInfo: ChannelParticipantBannedInfo?, rank: String?, subscriptionUntilDate: Int32?) public var peerId: PeerId { switch self { case let .creator(id, _, _): return id - case let .member(id, _, _, _, _): + case let .member(id, _, _, _, _, _): return id } } @@ -87,15 +87,15 @@ public enum ChannelParticipant: PostboxCoding, Equatable { switch self { case let .creator(_, _, rank): return rank - case let .member(_, _, _, _, rank): + case let .member(_, _, _, _, rank, _): return rank } } public static func ==(lhs: ChannelParticipant, rhs: ChannelParticipant) -> Bool { switch lhs { - case let .member(lhsId, lhsInvitedAt, lhsAdminInfo, lhsBanInfo, lhsRank): - if case let .member(rhsId, rhsInvitedAt, rhsAdminInfo, rhsBanInfo, rhsRank) = rhs { + case let .member(lhsId, lhsInvitedAt, lhsAdminInfo, lhsBanInfo, lhsRank, lhsSubscriptionUntilDate): + if case let .member(rhsId, rhsInvitedAt, rhsAdminInfo, rhsBanInfo, rhsRank, rhsSubscriptionUntilDate) = rhs { if lhsId != rhsId { return false } @@ -111,6 +111,9 @@ public enum ChannelParticipant: PostboxCoding, Equatable { if lhsRank != rhsRank { return false } + if lhsSubscriptionUntilDate != rhsSubscriptionUntilDate { + return false + } return true } else { return false @@ -127,17 +130,17 @@ public enum ChannelParticipant: PostboxCoding, Equatable { public init(decoder: PostboxDecoder) { switch decoder.decodeInt32ForKey("r", orElse: 0) { case ChannelParticipantValue.member.rawValue: - self = .member(id: PeerId(decoder.decodeInt64ForKey("i", orElse: 0)), invitedAt: decoder.decodeInt32ForKey("t", orElse: 0), adminInfo: decoder.decodeObjectForKey("ai", decoder: { ChannelParticipantAdminInfo(decoder: $0) }) as? ChannelParticipantAdminInfo, banInfo: decoder.decodeObjectForKey("bi", decoder: { ChannelParticipantBannedInfo(decoder: $0) }) as? ChannelParticipantBannedInfo, rank: decoder.decodeOptionalStringForKey("rank")) + self = .member(id: PeerId(decoder.decodeInt64ForKey("i", orElse: 0)), invitedAt: decoder.decodeInt32ForKey("t", orElse: 0), adminInfo: decoder.decodeObjectForKey("ai", decoder: { ChannelParticipantAdminInfo(decoder: $0) }) as? ChannelParticipantAdminInfo, banInfo: decoder.decodeObjectForKey("bi", decoder: { ChannelParticipantBannedInfo(decoder: $0) }) as? ChannelParticipantBannedInfo, rank: decoder.decodeOptionalStringForKey("rank"), subscriptionUntilDate: decoder.decodeOptionalInt32ForKey("subscriptionUntilDate")) case ChannelParticipantValue.creator.rawValue: self = .creator(id: PeerId(decoder.decodeInt64ForKey("i", orElse: 0)), adminInfo: decoder.decodeObjectForKey("ai", decoder: { ChannelParticipantAdminInfo(decoder: $0) }) as? ChannelParticipantAdminInfo, rank: decoder.decodeOptionalStringForKey("rank")) default: - self = .member(id: PeerId(decoder.decodeInt64ForKey("i", orElse: 0)), invitedAt: decoder.decodeInt32ForKey("t", orElse: 0), adminInfo: nil, banInfo: nil, rank: nil) + self = .member(id: PeerId(decoder.decodeInt64ForKey("i", orElse: 0)), invitedAt: decoder.decodeInt32ForKey("t", orElse: 0), adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil) } } public func encode(_ encoder: PostboxEncoder) { switch self { - case let .member(id, invitedAt, adminInfo, banInfo, rank): + case let .member(id, invitedAt, adminInfo, banInfo, rank, subscriptionUntilDate): encoder.encodeInt32(ChannelParticipantValue.member.rawValue, forKey: "r") encoder.encodeInt64(id.toInt64(), forKey: "i") encoder.encodeInt32(invitedAt, forKey: "t") @@ -156,6 +159,11 @@ public enum ChannelParticipant: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "rank") } + if let subscriptionUntilDate = subscriptionUntilDate { + encoder.encodeInt32(subscriptionUntilDate, forKey: "subscriptionUntilDate") + } else { + encoder.encodeNil(forKey: "subscriptionUntilDate") + } case let .creator(id, adminInfo, rank): encoder.encodeInt32(ChannelParticipantValue.creator.rawValue, forKey: "r") encoder.encodeInt64(id.toInt64(), forKey: "i") @@ -197,20 +205,20 @@ public final class CachedChannelParticipants: PostboxCoding, Equatable { extension ChannelParticipant { init(apiParticipant: Api.ChannelParticipant) { switch apiParticipant { - case let .channelParticipant(_, userId, date, _): - self = .member(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), invitedAt: date, adminInfo: nil, banInfo: nil, rank: nil) + case let .channelParticipant(_, userId, date, subscriptionUntilDate): + self = .member(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), invitedAt: date, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: subscriptionUntilDate) case let .channelParticipantCreator(_, userId, adminRights, rank): self = .creator(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(apiAdminRights: adminRights) ?? TelegramChatAdminRights(rights: []), promotedBy: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), canBeEditedByAccountPeer: true), rank: rank) case let .channelParticipantBanned(flags, userId, restrictedBy, date, bannedRights): let hasLeft = (flags & (1 << 0)) != 0 let banInfo = ChannelParticipantBannedInfo(rights: TelegramChatBannedRights(apiBannedRights: bannedRights), restrictedBy: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(restrictedBy)), timestamp: date, isMember: !hasLeft) - self = .member(id: userId.peerId, invitedAt: date, adminInfo: nil, banInfo: banInfo, rank: nil) + self = .member(id: userId.peerId, invitedAt: date, adminInfo: nil, banInfo: banInfo, rank: nil, subscriptionUntilDate: nil) case let .channelParticipantAdmin(flags, userId, _, promotedBy, date, adminRights, rank: rank): - self = .member(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), invitedAt: date, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(apiAdminRights: adminRights) ?? TelegramChatAdminRights(rights: []), promotedBy: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(promotedBy)), canBeEditedByAccountPeer: (flags & (1 << 0)) != 0), banInfo: nil, rank: rank) - case let .channelParticipantSelf(_, userId, _, date, _): - self = .member(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), invitedAt: date, adminInfo: nil, banInfo: nil, rank: nil) + self = .member(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), invitedAt: date, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(apiAdminRights: adminRights) ?? TelegramChatAdminRights(rights: []), promotedBy: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(promotedBy)), canBeEditedByAccountPeer: (flags & (1 << 0)) != 0), banInfo: nil, rank: rank, subscriptionUntilDate: nil) + case let .channelParticipantSelf(_, userId, _, date, subscriptionUntilDate): + self = .member(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), invitedAt: date, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: subscriptionUntilDate) case let .channelParticipantLeft(userId): - self = .member(id: userId.peerId, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil) + self = .member(id: userId.peerId, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil) } } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/ExportedInvitation.swift b/submodules/TelegramCore/Sources/ApiUtils/ExportedInvitation.swift index 163c9d5890..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/Suggestions.swift b/submodules/TelegramCore/Sources/Suggestions.swift index 3e00ee7a83..ba765fa57e 100644 --- a/submodules/TelegramCore/Sources/Suggestions.swift +++ b/submodules/TelegramCore/Sources/Suggestions.swift @@ -16,6 +16,7 @@ public enum ServerProvidedSuggestion: String { case setupBirthday = "BIRTHDAY_SETUP" case todayBirthdays = "BIRTHDAY_CONTACTS_TODAY" case gracePremium = "PREMIUM_GRACE" + case starsSubscriptionLowBalance = "STARS_SUBSCRIPTION_LOW_BALANCE" } private var dismissedSuggestionsPromise = ValuePromise<[AccountRecordId: Set]>([:]) 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/SyncCore/SyncCore_TelegramChannel.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift index 7b863fd86f..d20dab8bcf 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift @@ -174,6 +174,7 @@ public final class TelegramChannel: Peer, Equatable { public let profileBackgroundEmojiId: Int64? public let emojiStatus: PeerEmojiStatus? public let approximateBoostLevel: Int32? + public let subscriptionUntilDate: Int32? public var indexName: PeerIndexNameRepresentation { var addressNames = self.usernames.map { $0.username } @@ -239,7 +240,8 @@ public final class TelegramChannel: Peer, Equatable { profileColor: PeerNameColor?, profileBackgroundEmojiId: Int64?, emojiStatus: PeerEmojiStatus?, - approximateBoostLevel: Int32? + approximateBoostLevel: Int32?, + subscriptionUntilDate: Int32? ) { self.id = id self.accessHash = accessHash @@ -263,6 +265,7 @@ public final class TelegramChannel: Peer, Equatable { self.profileBackgroundEmojiId = profileBackgroundEmojiId self.emojiStatus = emojiStatus self.approximateBoostLevel = approximateBoostLevel + self.subscriptionUntilDate = subscriptionUntilDate } public init(decoder: PostboxDecoder) { @@ -298,6 +301,7 @@ public final class TelegramChannel: Peer, Equatable { self.profileBackgroundEmojiId = decoder.decodeOptionalInt64ForKey("pgem") self.emojiStatus = decoder.decode(PeerEmojiStatus.self, forKey: "emjs") self.approximateBoostLevel = decoder.decodeOptionalInt32ForKey("abl") + self.subscriptionUntilDate = decoder.decodeOptionalInt32ForKey("sud") } public func encode(_ encoder: PostboxEncoder) { @@ -389,6 +393,12 @@ public final class TelegramChannel: Peer, Equatable { } else { encoder.encodeNil(forKey: "abl") } + + if let subscriptionUntilDate = self.subscriptionUntilDate { + encoder.encodeInt32(subscriptionUntilDate, forKey: "sud") + } else { + encoder.encodeNil(forKey: "sud") + } } public func isEqual(_ other: Peer) -> Bool { @@ -447,51 +457,57 @@ public final class TelegramChannel: Peer, Equatable { if lhs.approximateBoostLevel != rhs.approximateBoostLevel { return false } - + if lhs.subscriptionUntilDate != rhs.subscriptionUntilDate { + return false + } return true } public func withUpdatedAddressName(_ addressName: String?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: addressName, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: addressName, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel, subscriptionUntilDate: self.subscriptionUntilDate) } public func withUpdatedAddressNames(_ addressNames: [TelegramPeerUsername]) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: addressNames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: addressNames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel, subscriptionUntilDate: self.subscriptionUntilDate) } public func withUpdatedDefaultBannedRights(_ defaultBannedRights: TelegramChatBannedRights?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel, subscriptionUntilDate: self.subscriptionUntilDate) } public func withUpdatedFlags(_ flags: TelegramChannelFlags) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel, subscriptionUntilDate: self.subscriptionUntilDate) } public func withUpdatedStoriesHidden(_ storiesHidden: Bool?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel, subscriptionUntilDate: self.subscriptionUntilDate) } public func withUpdatedNameColor(_ nameColor: PeerNameColor?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel, subscriptionUntilDate: self.subscriptionUntilDate) } public func withUpdatedBackgroundEmojiId(_ backgroundEmojiId: Int64?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel, subscriptionUntilDate: self.subscriptionUntilDate) } public func withUpdatedProfileColor(_ profileColor: PeerNameColor?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel, subscriptionUntilDate: self.subscriptionUntilDate) } public func withUpdatedProfileBackgroundEmojiId(_ profileBackgroundEmojiId: Int64?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel, subscriptionUntilDate: self.subscriptionUntilDate) } public func withUpdatedEmojiStatus(_ emojiStatus: PeerEmojiStatus?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: emojiStatus, approximateBoostLevel: self.approximateBoostLevel) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: emojiStatus, approximateBoostLevel: self.approximateBoostLevel, subscriptionUntilDate: self.subscriptionUntilDate) } public func withUpdatedApproximateBoostLevel(_ approximateBoostLevel: Int32?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: approximateBoostLevel) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: approximateBoostLevel, subscriptionUntilDate: self.subscriptionUntilDate) + } + + public func withUpdatedSubscriptionUntilDate(_ subscriptionUntilDate: Int32?) -> TelegramChannel { + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel, subscriptionUntilDate: subscriptionUntilDate) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index 4c6c5e094d..db6451d672 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -48,13 +48,13 @@ extension EnginePeerCachedInfoItem: Equatable where T: Equatable { public enum EngineChannelParticipant: Equatable { case creator(id: EnginePeer.Id, adminInfo: ChannelParticipantAdminInfo?, rank: String?) - case member(id: EnginePeer.Id, invitedAt: Int32, adminInfo: ChannelParticipantAdminInfo?, banInfo: ChannelParticipantBannedInfo?, rank: String?) + case member(id: EnginePeer.Id, invitedAt: Int32, adminInfo: ChannelParticipantAdminInfo?, banInfo: ChannelParticipantBannedInfo?, rank: String?, subscriptionUntilDate: Int32?) public var peerId: EnginePeer.Id { switch self { case let .creator(id, _, _): return id - case let .member(id, _, _, _, _): + case let .member(id, _, _, _, _, _): return id } } @@ -65,8 +65,8 @@ public extension EngineChannelParticipant { switch participant { case let .creator(id, adminInfo, rank): self = .creator(id: id, adminInfo: adminInfo, rank: rank) - case let .member(id, invitedAt, adminInfo, banInfo, rank): - self = .member(id: id, invitedAt: invitedAt, adminInfo: adminInfo, banInfo: banInfo, rank: rank) + case let .member(id, invitedAt, adminInfo, banInfo, rank, subscriptionUntilDate): + self = .member(id: id, invitedAt: invitedAt, adminInfo: adminInfo, banInfo: banInfo, rank: rank, subscriptionUntilDate: subscriptionUntilDate) } } @@ -74,8 +74,8 @@ public extension EngineChannelParticipant { switch self { case let .creator(id, adminInfo, rank): return .creator(id: id, adminInfo: adminInfo, rank: rank) - case let .member(id, invitedAt, adminInfo, banInfo, rank): - return .member(id: id, invitedAt: invitedAt, adminInfo: adminInfo, banInfo: banInfo, rank: rank) + case let .member(id, invitedAt, adminInfo, banInfo, rank, subscriptionUntilDate): + return .member(id: id, invitedAt: invitedAt, adminInfo: adminInfo, banInfo: banInfo, rank: rank, subscriptionUntilDate: subscriptionUntilDate) } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift index 4c5b712b09..f42b4dd4fb 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift @@ -215,7 +215,8 @@ private class AdMessagesHistoryContextImpl { profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, - approximateBoostLevel: nil + approximateBoostLevel: nil, + subscriptionUntilDate: nil ) messagePeers[author.id] = author 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 ee33e92627..1d6ebe1e5c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -147,15 +147,18 @@ func _internal_starsGiftOptions(account: Account, peerId: EnginePeer.Id?) -> Sig struct InternalStarsStatus { let balance: Int64 + let subscriptionsMissingBalance: Int64? + let subscriptions: [StarsContext.State.Subscription] + let nextSubscriptionsOffset: String? let transactions: [StarsContext.State.Transaction] - let nextOffset: String? + let nextTransactionsOffset: String? } private enum RequestStarsStateError { case generic } -private func _internal_requestStarsState(account: Account, peerId: EnginePeer.Id, mode: StarsTransactionsContext.Mode, offset: String?, limit: Int32) -> Signal { +private func _internal_requestStarsState(account: Account, peerId: EnginePeer.Id, mode: StarsTransactionsContext.Mode, subscriptionId: String?, offset: String?, limit: Int32) -> Signal { return account.postbox.transaction { transaction -> Peer? in return transaction.getPeer(peerId) } @@ -176,7 +179,10 @@ private func _internal_requestStarsState(account: Account, peerId: EnginePeer.Id default: break } - signal = account.network.request(Api.functions.payments.getStarsTransactions(flags: flags, subscriptionId: nil, peer: inputPeer, offset: offset, limit: limit)) + if let _ = subscriptionId { + flags = 1 << 3 + } + signal = account.network.request(Api.functions.payments.getStarsTransactions(flags: flags, subscriptionId: subscriptionId, peer: inputPeer, offset: offset, limit: limit)) } else { signal = account.network.request(Api.functions.payments.getStarsStatus(peer: inputPeer)) } @@ -187,17 +193,26 @@ 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, _, _, subscriptionsMissingBalance, 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, + subscriptionsMissingBalance: subscriptionsMissingBalance, + subscriptions: [], + nextSubscriptionsOffset: nil, + transactions: parsedTransactions, + nextTransactionsOffset: nextTransactionsOffset + ) } } |> castError(RequestStarsStateError.self) @@ -205,6 +220,56 @@ 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, missingBalance: Bool) -> 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) + } + var flags: Int32 = 0 + if missingBalance { + flags |= (1 << 0) + } + return account.network.request(Api.functions.payments.getStarsSubscriptions(flags: flags, 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, subscriptionsMissingBalance, _, _, 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, + subscriptionsMissingBalance: subscriptionsMissingBalance, + 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 +279,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 +299,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) }) } @@ -256,13 +320,12 @@ private final class StarsContextImpl { } self.previousLoadTimestamp = currentTimestamp - self.disposable.set((_internal_requestStarsState(account: self.account, peerId: self.peerId, mode: .all, offset: nil, limit: 5) + self.disposable.set((_internal_requestStarsState(account: self.account, peerId: self.peerId, mode: .all, subscriptionId: nil, offset: nil, limit: 5) |> deliverOnMainQueue).start(next: { [weak self] status in 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 @@ -278,34 +341,16 @@ 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, 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 +362,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 { @@ -360,9 +405,35 @@ 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 } }) ?? [] - 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) + 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, subscriptionPeriod: subscriptionPeriod) + } + } +} + +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) + } + if (apiFlags & (1 << 1)) != 0 { + flags.insert(.canRefulfill) + } + if (apiFlags & (1 << 2)) != 0 { + flags.insert(.missingBalance) + } + self.init(flags: flags, id: id, peer: EnginePeer(peer), untilDate: untilDate, pricing: StarsSubscriptionPricing(apiStarsSubscriptionPricing: pricing)) } } } @@ -382,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 { @@ -406,6 +478,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, @@ -419,7 +492,8 @@ public final class StarsContext { transactionDate: Int32?, transactionUrl: String?, paidMessageId: MessageId?, - media: [Media] + media: [Media], + subscriptionPeriod: Int32? ) { self.flags = flags self.id = id @@ -433,6 +507,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 { @@ -472,6 +547,62 @@ public final class StarsContext { if !areMediaArraysEqual(lhs.media, rhs.media) { return false } + if lhs.subscriptionPeriod != rhs.subscriptionPeriod { + return false + } + return true + } + } + + 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 static let canRefulfill = Flags(rawValue: 1 << 1) + public static let missingBalance = Flags(rawValue: 1 << 2) + } + + 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 } } @@ -488,15 +619,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 +645,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 +707,6 @@ public final class StarsContext { } } - public func loadMore() { - self.impl.with { - $0.loadMore() - } - } init(account: Account) { self.impl = QueueLocalObject(queue: Queue.mainQueue(), generate: { @@ -685,12 +818,12 @@ private final class StarsTransactionsContextImpl { updatedState.isLoading = true self.updateState(updatedState) - self.disposable.set((_internal_requestStarsState(account: self.account, peerId: self.peerId, mode: self.mode, offset: nextOffset, limit: self.nextOffset == "" ? 25 : 50) + self.disposable.set((_internal_requestStarsState(account: self.account, peerId: self.peerId, mode: self.mode, subscriptionId: nil, offset: nextOffset, limit: self.nextOffset == "" ? 25 : 50) |> deliverOnMainQueue).start(next: { [weak self] status in 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 +902,170 @@ 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? + private let updateDisposable = MetaDisposable() + + 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)) + + self.loadMore() + } + + deinit { + assert(Queue.mainQueue().isCurrent()) + self.disposable.dispose() + self.stateDisposable?.dispose() + self.updateDisposable.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, missingBalance: false) + |> 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)) + } + + 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) + 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 { + 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) + }) + } + + public func updateSubscription(id: String, cancel: Bool) { + self.impl.with { + $0.updateSubscription(id: id, cancel: cancel) + } + } + + public func load(force: Bool) { + self.impl.with { + $0.load(force: force) + } + } +} + + 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 +1116,8 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot } case .giftCode, .stars, .starsGift: receiptMessageId = nil + case .starsChatSubscription: + receiptMessageId = nil } } } @@ -900,3 +1199,91 @@ 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: Int32 = 2592000 + public static let testPeriod: Int32 = 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 UpdateStarsSubsciptionError { + case generic +} + +func _internal_updateStarsSubscription(account: Account, peerId: EnginePeer.Id, subscriptionId: String, cancel: Bool) -> Signal { + return account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(peerId).flatMap(apiInputPeer) + } + |> castError(UpdateStarsSubsciptionError.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: cancel ? .boolTrue : .boolFalse)) + |> mapError { _ -> UpdateStarsSubsciptionError in + return .generic + } + |> ignoreValues + } +} + +public enum FulfillStarsSubsciptionError { + case generic +} + +func _internal_fulfillStarsSubscription(account: Account, peerId: EnginePeer.Id, subscriptionId: String) -> Signal { + return account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(peerId).flatMap(apiInputPeer) + } + |> castError(FulfillStarsSubsciptionError.self) + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer else { + return .complete() + } + return account.network.request(Api.functions.payments.fulfillStarsSubscription(peer: inputPeer, subscriptionId: subscriptionId)) + |> mapError { _ -> FulfillStarsSubsciptionError in + return .generic + } + |> ignoreValues + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index 062e457ea8..a7292134b5 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) } @@ -86,8 +86,16 @@ 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) } + + public func fulfillStarsSubscription(peerId: EnginePeer.Id, subscriptionId: String) -> Signal { + return _internal_fulfillStarsSubscription(account: self.account, peerId: peerId, subscriptionId: subscriptionId) + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift index dfbda21313..27ad703429 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift @@ -153,10 +153,10 @@ func _internal_addChannelMember(account: Account, peerId: PeerId, memberId: Peer if let peer = transaction.getPeer(peerId), let memberPeer = transaction.getPeer(memberId), let inputUser = apiInputUser(memberPeer) { if let channel = peer as? TelegramChannel, let inputChannel = apiInputChannel(channel) { let updatedParticipant: ChannelParticipant - if let currentParticipant = currentParticipant, case let .member(_, invitedAt, adminInfo, _, rank) = currentParticipant { - updatedParticipant = ChannelParticipant.member(id: memberId, invitedAt: invitedAt, adminInfo: adminInfo, banInfo: nil, rank: rank) + if let currentParticipant = currentParticipant, case let .member(_, invitedAt, adminInfo, _, rank, subscriptionUntilDate) = currentParticipant { + updatedParticipant = ChannelParticipant.member(id: memberId, invitedAt: invitedAt, adminInfo: adminInfo, banInfo: nil, rank: rank, subscriptionUntilDate: subscriptionUntilDate) } else { - updatedParticipant = ChannelParticipant.member(id: memberId, invitedAt: Int32(Date().timeIntervalSince1970), adminInfo: nil, banInfo: nil, rank: nil) + updatedParticipant = ChannelParticipant.member(id: memberId, invitedAt: Int32(Date().timeIntervalSince1970), adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil) } return account.network.request(Api.functions.channels.inviteToChannel(channel: inputChannel, users: [inputUser])) |> `catch` { error -> Signal in @@ -208,7 +208,7 @@ func _internal_addChannelMember(account: Account, peerId: PeerId, memberId: Peer switch currentParticipant { case .creator: break - case let .member(_, _, _, banInfo, _): + case let .member(_, _, _, banInfo, _, _): if let banInfo = banInfo { wasBanned = true wasMember = !banInfo.rights.flags.contains(.banReadMessages) @@ -235,7 +235,7 @@ func _internal_addChannelMember(account: Account, peerId: PeerId, memberId: Peer if let presence = transaction.getPeerPresence(peerId: memberPeer.id) { presences[memberPeer.id] = presence } - if case let .member(_, _, maybeAdminInfo, _, _) = updatedParticipant { + if case let .member(_, _, maybeAdminInfo, _, _, _) = updatedParticipant { if let adminInfo = maybeAdminInfo { if let peer = transaction.getPeer(adminInfo.promotedBy) { peers[peer.id] = peer diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift index ddeea363e8..84e2522453 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift @@ -10,14 +10,14 @@ func _internal_updateChannelMemberBannedRights(account: Account, peerId: PeerId, return account.postbox.transaction { transaction -> Signal<(ChannelParticipant?, RenderedChannelParticipant?, Bool), NoError> in if let peer = transaction.getPeer(peerId), let inputChannel = apiInputChannel(peer), let _ = transaction.getPeer(account.peerId), let memberPeer = transaction.getPeer(memberId), let inputPeer = apiInputPeer(memberPeer) { let updatedParticipant: ChannelParticipant - if let currentParticipant = currentParticipant, case let .member(_, invitedAt, _, currentBanInfo, _) = currentParticipant { + if let currentParticipant = currentParticipant, case let .member(_, invitedAt, _, currentBanInfo, _, subscriptionUntilDate) = currentParticipant { let banInfo: ChannelParticipantBannedInfo? if let rights = rights, !rights.flags.isEmpty { banInfo = ChannelParticipantBannedInfo(rights: rights, restrictedBy: currentBanInfo?.restrictedBy ?? account.peerId, timestamp: currentBanInfo?.timestamp ?? Int32(Date().timeIntervalSince1970), isMember: currentBanInfo?.isMember ?? true) } else { banInfo = nil } - updatedParticipant = ChannelParticipant.member(id: memberId, invitedAt: invitedAt, adminInfo: nil, banInfo: banInfo, rank: nil) + updatedParticipant = ChannelParticipant.member(id: memberId, invitedAt: invitedAt, adminInfo: nil, banInfo: banInfo, rank: nil, subscriptionUntilDate: subscriptionUntilDate) } else { let banInfo: ChannelParticipantBannedInfo? if let rights = rights, !rights.flags.isEmpty { @@ -25,7 +25,7 @@ func _internal_updateChannelMemberBannedRights(account: Account, peerId: PeerId, } else { banInfo = nil } - updatedParticipant = ChannelParticipant.member(id: memberId, invitedAt: Int32(Date().timeIntervalSince1970), adminInfo: nil, banInfo: banInfo, rank: nil) + updatedParticipant = ChannelParticipant.member(id: memberId, invitedAt: Int32(Date().timeIntervalSince1970), adminInfo: nil, banInfo: banInfo, rank: nil, subscriptionUntilDate: nil) } let apiRights: Api.ChatBannedRights @@ -48,7 +48,7 @@ func _internal_updateChannelMemberBannedRights(account: Account, peerId: PeerId, switch currentParticipant { case .creator: break - case let .member(_, _, adminInfo, banInfo, _): + case let .member(_, _, adminInfo, banInfo, _, _): if let _ = adminInfo { wasAdmin = true } @@ -131,7 +131,7 @@ func _internal_updateChannelMemberBannedRights(account: Account, peerId: PeerId, if let presence = transaction.getPeerPresence(peerId: memberPeer.id) { presences[memberPeer.id] = presence } - if case let .member(_, _, _, maybeBanInfo, _) = updatedParticipant, let banInfo = maybeBanInfo { + if case let .member(_, _, _, maybeBanInfo, _, _) = updatedParticipant, let banInfo = maybeBanInfo { if let peer = transaction.getPeer(banInfo.restrictedBy) { peers[peer.id] = peer } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift index b650134dae..2394f8f535 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift @@ -86,7 +86,7 @@ func _internal_updateChannelOwnership(account: Account, channelId: PeerId, membe let flags: TelegramChatAdminRightsFlags = TelegramChatAdminRightsFlags.peerSpecific(peer: .channel(channel)) let updatedParticipant = ChannelParticipant.creator(id: user.id, adminInfo: nil, rank: currentParticipant?.rank) - let updatedPreviousCreator = ChannelParticipant.member(id: accountUser.id, invitedAt: Int32(Date().timeIntervalSince1970), adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: flags), promotedBy: accountUser.id, canBeEditedByAccountPeer: false), banInfo: nil, rank: currentCreator?.rank) + let updatedPreviousCreator = ChannelParticipant.member(id: accountUser.id, invitedAt: Int32(Date().timeIntervalSince1970), adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: flags), promotedBy: accountUser.id, canBeEditedByAccountPeer: false), banInfo: nil, rank: currentCreator?.rank, subscriptionUntilDate: nil) let checkPassword = _internal_twoStepAuthData(account.network) |> mapError { error -> ChannelOwnershipTransferError in @@ -152,7 +152,7 @@ func _internal_updateChannelOwnership(account: Account, channelId: PeerId, membe switch currentParticipant { case .creator: wasAdmin = true - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): if let _ = adminInfo { wasAdmin = true } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/InvitationLinks.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/InvitationLinks.swift index 0aa9bc75a1..62a7d6ebdd 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/InvitationLinks.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/InvitationLinks.swift @@ -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, subscriptionPricing: nil)) + 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/JoinChannel.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift index c597192842..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 @@ -64,7 +70,7 @@ func _internal_joinChannel(account: Account, peerId: PeerId, hash: String?) -> S case let .channelParticipant(participant, _, _): updatedParticipant = ChannelParticipant(apiParticipant: participant) } - if case let .member(_, _, maybeAdminInfo, _, _) = updatedParticipant { + if case let .member(_, _, maybeAdminInfo, _, _, _) = updatedParticipant { if let adminInfo = maybeAdminInfo { if let peer = transaction.getPeer(adminInfo.promotedBy) { peers[peer.id] = peer @@ -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) } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift index a23e3c443a..a73977553b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift @@ -39,6 +39,7 @@ public enum ExternalJoiningChatState { public let isVerified: Bool public let isScam: Bool public let isFake: Bool + public let canRefulfillSubscription: Bool } public let flags: Flags @@ -48,6 +49,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 +109,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)))) + 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, canRefulfillSubscription: (flags & (1 << 11)) != 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), 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/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/TelegramEngine/Peers/PeerAdmins.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift index 5df0981456..2a3dca7538 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift @@ -158,14 +158,14 @@ func _internal_updateChannelAdminRights(account: Account, peerId: PeerId, adminI if let peer = transaction.getPeer(peerId), let adminPeer = transaction.getPeer(adminId), let inputUser = apiInputUser(adminPeer) { if let channel = peer as? TelegramChannel, let inputChannel = apiInputChannel(channel) { let updatedParticipant: ChannelParticipant - if let currentParticipant = currentParticipant, case let .member(_, invitedAt, currentAdminInfo, _, _) = currentParticipant { + if let currentParticipant = currentParticipant, case let .member(_, invitedAt, currentAdminInfo, _, _, subscriptionUntilDate) = currentParticipant { let adminInfo: ChannelParticipantAdminInfo? if let rights = rights { adminInfo = ChannelParticipantAdminInfo(rights: rights, promotedBy: currentAdminInfo?.promotedBy ?? account.peerId, canBeEditedByAccountPeer: true) } else { adminInfo = nil } - updatedParticipant = .member(id: adminId, invitedAt: invitedAt, adminInfo: adminInfo, banInfo: nil, rank: rank) + updatedParticipant = .member(id: adminId, invitedAt: invitedAt, adminInfo: adminInfo, banInfo: nil, rank: rank, subscriptionUntilDate: subscriptionUntilDate) } else if let currentParticipant = currentParticipant, case .creator = currentParticipant { let adminInfo: ChannelParticipantAdminInfo? if let rights = rights { @@ -181,7 +181,7 @@ func _internal_updateChannelAdminRights(account: Account, peerId: PeerId, adminI } else { adminInfo = nil } - updatedParticipant = .member(id: adminId, invitedAt: Int32(Date().timeIntervalSince1970), adminInfo: adminInfo, banInfo: nil, rank: rank) + updatedParticipant = .member(id: adminId, invitedAt: Int32(Date().timeIntervalSince1970), adminInfo: adminInfo, banInfo: nil, rank: rank, subscriptionUntilDate: nil) } return account.network.request(Api.functions.channels.editAdmin(channel: inputChannel, userId: inputUser, adminRights: rights?.apiAdminRights ?? .chatAdminRights(flags: 0), rank: rank ?? "")) |> map { [$0] } @@ -225,7 +225,7 @@ func _internal_updateChannelAdminRights(account: Account, peerId: PeerId, adminI switch currentParticipant { case .creator: wasAdmin = true - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): if let _ = adminInfo { wasAdmin = true } @@ -248,7 +248,7 @@ func _internal_updateChannelAdminRights(account: Account, peerId: PeerId, adminI if let presence = transaction.getPeerPresence(peerId: adminPeer.id) { presences[adminPeer.id] = presence } - if case let .member(_, _, maybeAdminInfo, _, _) = updatedParticipant, let adminInfo = maybeAdminInfo { + if case let .member(_, _, maybeAdminInfo, _, _, _) = updatedParticipant, let adminInfo = maybeAdminInfo { if let peer = transaction.getPeer(adminInfo.promotedBy) { peers[peer.id] = peer } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 765304b395..18577aff3e 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/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index 119177fab8..822d12aae3 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -450,7 +450,6 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee let participants = CachedGroupParticipants(apiParticipants: chatFullParticipants) let autoremoveTimeout: CachedPeerAutoremoveTimeout = .known(CachedPeerAutoremoveTimeout.Value(chatTtlPeriod)) - var invitedBy: PeerId? if let participants = participants { 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/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift index 439be5c6bf..8259e27965 100644 --- a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift +++ b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift @@ -531,7 +531,7 @@ private final class AdminUserActionsSheetComponent: Component { allowedParticipantRights = [] allowedMediaRights = [] break loop - case let .member(_, _, adminInfo, banInfo, _): + case let .member(_, _, adminInfo, banInfo, _, _): if adminInfo != nil { (allowedParticipantRights, allowedMediaRights) = rightsFromBannedRights([]) break loop @@ -625,7 +625,7 @@ private final class AdminUserActionsSheetComponent: Component { switch peer.participant { case .creator: canBanEveryone = false - case let .member(_, _, adminInfo, banInfo, _): + case let .member(_, _, adminInfo, banInfo, _, _): let _ = banInfo if let adminInfo { if channel.flags.contains(.isCreator) { diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsFilterController.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsFilterController.swift index f593ba2378..7a597f83b0 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsFilterController.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsFilterController.swift @@ -442,7 +442,7 @@ public func channelRecentActionsFilterController(context: AccountContext, update antiSpamBotPeerPromise.set(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: antiSpamBotId)) |> map { peer in if let peer = peer, case let .user(user) = peer { - return RenderedChannelParticipant(participant: .member(id: user.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil), peer: user) + return RenderedChannelParticipant(participant: .member(id: user.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: user) } else { return nil } diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift index f85b2ae55f..ea5afffba0 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift @@ -87,7 +87,7 @@ private func filterOriginalMessageFlags(_ message: Message) -> Message { private func filterMessageChannelPeer(_ peer: Peer) -> Peer { if let peer = peer as? TelegramChannel { - return TelegramChannel(id: peer.id, accessHash: peer.accessHash, title: peer.title, username: peer.username, photo: peer.photo, creationDate: peer.creationDate, version: peer.version, participationStatus: peer.participationStatus, info: .group(TelegramChannelGroupInfo(flags: [])), flags: peer.flags, restrictionInfo: peer.restrictionInfo, adminRights: peer.adminRights, bannedRights: peer.bannedRights, defaultBannedRights: peer.defaultBannedRights, usernames: peer.usernames, storiesHidden: peer.storiesHidden, nameColor: peer.nameColor, backgroundEmojiId: peer.backgroundEmojiId, profileColor: peer.profileColor, profileBackgroundEmojiId: peer.profileBackgroundEmojiId, emojiStatus: peer.emojiStatus, approximateBoostLevel: peer.approximateBoostLevel) + return TelegramChannel(id: peer.id, accessHash: peer.accessHash, title: peer.title, username: peer.username, photo: peer.photo, creationDate: peer.creationDate, version: peer.version, participationStatus: peer.participationStatus, info: .group(TelegramChannelGroupInfo(flags: [])), flags: peer.flags, restrictionInfo: peer.restrictionInfo, adminRights: peer.adminRights, bannedRights: peer.bannedRights, defaultBannedRights: peer.defaultBannedRights, usernames: peer.usernames, storiesHidden: peer.storiesHidden, nameColor: peer.nameColor, backgroundEmojiId: peer.backgroundEmojiId, profileColor: peer.profileColor, profileBackgroundEmojiId: peer.profileBackgroundEmojiId, emojiStatus: peer.emojiStatus, approximateBoostLevel: peer.approximateBoostLevel, subscriptionUntilDate: peer.subscriptionUntilDate) } return peer } @@ -704,8 +704,8 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { isBroadcast = false } - if case let .member(_, _, _, prevBanInfo, _) = prev.participant { - if case let .member(_, _, _, newBanInfo, _) = new.participant { + if case let .member(_, _, _, prevBanInfo, _, _) = prev.participant { + if case let .member(_, _, _, newBanInfo, _, _) = new.participant { let newFlags = newBanInfo?.rights.flags ?? [] var addedRights = newBanInfo?.rights.flags ?? [] @@ -890,8 +890,8 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } } - } else if case let .member(_, _, prevAdminRights, _, prevRank) = prev.participant { - if case let .member(_, _, newAdminRights, _, newRank) = new.participant { + } else if case let .member(_, _, prevAdminRights, _, prevRank, _) = prev.participant { + if case let .member(_, _, newAdminRights, _, newRank, _) = new.participant { var prevFlags = prevAdminRights?.rights.rights ?? [] var newFlags = newAdminRights?.rights.rights ?? [] @@ -1474,7 +1474,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 { @@ -1500,7 +1500,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 { @@ -1526,7 +1526,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 { @@ -1554,9 +1554,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 @@ -1725,7 +1725,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) ?? "") @@ -2342,14 +2342,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 - } - } -} 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/EntityKeyboard/Sources/EmojiKeyboardItemLayer.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiKeyboardItemLayer.swift index 7824028732..e20af0a7ee 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiKeyboardItemLayer.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiKeyboardItemLayer.swift @@ -336,7 +336,8 @@ public final class EmojiKeyboardItemLayer: MultiAnimationRenderTarget { func update( content: EmojiPagerContentComponent.ItemContent, - theme: PresentationTheme + theme: PresentationTheme, + strings: PresentationStrings ) { var themeUpdated = false if self.theme !== theme { @@ -376,14 +377,38 @@ public final class EmojiKeyboardItemLayer: MultiAnimationRenderTarget { UIGraphicsPushContext(context) context.setFillColor(color.withMultipliedAlpha(0.2).cgColor) - context.fillEllipse(in: CGRect(origin: .zero, size: size).insetBy(dx: 8.0, dy: 8.0)) + + context.addPath(UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 21.0).cgPath) + context.fillPath() context.setFillColor(color.cgColor) - let plusSize = CGSize(width: 4.5, height: 31.5) - context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.width) / 2.0), y: floorToScreenPixels((size.height - plusSize.height) / 2.0), width: plusSize.width, height: plusSize.height), cornerRadius: plusSize.width / 2.0).cgPath) - context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.height) / 2.0), y: floorToScreenPixels((size.height - plusSize.width) / 2.0), width: plusSize.height, height: plusSize.width), cornerRadius: plusSize.width / 2.0).cgPath) + let plusSize = CGSize(width: 3.5, height: 28.0) + context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.width) / 2.0), y: floorToScreenPixels((size.height - plusSize.height) / 2.0), width: plusSize.width, height: plusSize.height).offsetBy(dx: 0.0, dy: -17.0), cornerRadius: plusSize.width / 2.0).cgPath) + context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.height) / 2.0), y: floorToScreenPixels((size.height - plusSize.width) / 2.0), width: plusSize.height, height: plusSize.width).offsetBy(dx: 0.0, dy: -17.0), cornerRadius: plusSize.width / 2.0).cgPath) context.fillPath() + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + + let string = strings.Stickers_CreateSticker + var lineOriginY = size.height / 2.0 - 18.0 + let components = string.components(separatedBy: "\n") + for component in components { + context.saveGState() + let attributedString = NSAttributedString(string: component, attributes: [NSAttributedString.Key.font: Font.medium(17.0), NSAttributedString.Key.foregroundColor: color]) + + let line = CTLineCreateWithAttributedString(attributedString) + let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds) + + let lineOrigin = CGPoint(x: floorToScreenPixels((size.width - lineBounds.size.width) / 2.0), y: lineOriginY) + context.textPosition = lineOrigin + CTLineDraw(line, context) + + lineOriginY -= lineBounds.height + 6.0 + context.restoreGState() + } + UIGraphicsPopContext() }) } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index db64a3829f..4a780344e5 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -3510,7 +3510,7 @@ public final class EmojiPagerContentComponent: Component { } if case .icon = item.content { - itemLayer.update(content: item.content, theme: keyboardChildEnvironment.theme) + itemLayer.update(content: item.content, theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings) } itemLayer.update( diff --git a/submodules/TelegramUI/Components/NotificationExceptionsScreen/Sources/NotificationExceptionsScreen.swift b/submodules/TelegramUI/Components/NotificationExceptionsScreen/Sources/NotificationExceptionsScreen.swift index 472def405a..6cf94f4b03 100644 --- a/submodules/TelegramUI/Components/NotificationExceptionsScreen/Sources/NotificationExceptionsScreen.swift +++ b/submodules/TelegramUI/Components/NotificationExceptionsScreen/Sources/NotificationExceptionsScreen.swift @@ -332,7 +332,7 @@ private func notificationsPeerCategoryEntries(peerId: EnginePeer.Id, notificatio } } existingThreadIds.insert(value.threadId) - entries.append(.exception(Int32(index), presentationData.dateTimeFormat, presentationData.nameDisplayOrder, .channel(TelegramChannel(id: peerId, accessHash: nil, title: "", username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(TelegramChannelGroupInfo(flags: [])), flags: [.isForum], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil)), value.threadId, value.info, title, value.notificationSettings._asNotificationSettings(), state.editing, state.revealedThreadId == value.threadId)) + entries.append(.exception(Int32(index), presentationData.dateTimeFormat, presentationData.nameDisplayOrder, .channel(TelegramChannel(id: peerId, accessHash: nil, title: "", username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(TelegramChannelGroupInfo(flags: [])), flags: [.isForum], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil, subscriptionUntilDate: nil)), value.threadId, value.info, title, value.notificationSettings._asNotificationSettings(), state.editing, state.revealedThreadId == value.threadId)) index += 1 } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index 5ad6ea1af0..8575757a73 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -1958,7 +1958,7 @@ func availableActionsForMemberOfPeer(accountPeerId: PeerId, peer: Peer?, member: switch channelMember.participant { case .creator: break - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): if let adminInfo = adminInfo { if adminInfo.promotedBy == accountPeerId { if !channel.flags.contains(.isGigagroup) { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoMembers.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoMembers.swift index 801d58db58..81845f8a85 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoMembers.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoMembers.swift @@ -55,7 +55,7 @@ enum PeerInfoMember: Equatable { switch participant.participant { case .creator: return .creator - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): if adminInfo != nil { return .admin } else { @@ -75,7 +75,7 @@ enum PeerInfoMember: Equatable { switch participant.participant { case let .creator(_, _, rank): return rank - case let .member(_, _, _, _, rank): + case let .member(_, _, _, _, rank, _): return rank } case .legacyGroupMember: diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index c7cb6ffd85..115495afae 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -9223,7 +9223,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: @@ -9460,7 +9460,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/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift index 0ce8b9d798..23c029753a 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift @@ -957,7 +957,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate let selfPeer: EnginePeer = .user(TelegramUser(id: self.context.account.peerId, accessHash: nil, firstName: nil, lastName: nil, 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)) let peer1: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_1_Name, lastName: nil, 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)) let peer2: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(2)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_2_Name, lastName: nil, 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)) - let peer3: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(3)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil)) + let peer3: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(3)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil, subscriptionUntilDate: nil)) let peer3Author: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_AuthorName, lastName: nil, 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)) let peer4: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_4_Name, lastName: nil, 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)) diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift index 8b9f6ccbe8..9047c66cc4 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift @@ -539,7 +539,7 @@ public extension ShareWithPeersScreen { continue } - if case let .member(_, date, _, _, _) = participant.participant { + if case let .member(_, date, _, _, _, _) = participant.participant { invitedAt[participant.peer.id] = date } else { continue @@ -557,7 +557,7 @@ public extension ShareWithPeersScreen { continue } - if case let .member(_, date, _, _, _) = participant.participant { + if case let .member(_, date, _, _, _, _) = participant.participant { invitedAt[participant.peer.id] = date } else { continue diff --git a/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift index 78e45051c9..e05bba66bd 100644 --- a/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift @@ -309,22 +309,29 @@ 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 } public static var body: Body { let text = Child(MultilineTextComponent.self) + let subLabel = Child(MultilineTextComponent.self) let icon = Child(BundleIconComponent.self) return { context in @@ -332,30 +339,59 @@ public final class StarsLabelComponent: CombinedComponent { let text = text.update( component: MultilineTextComponent(text: .plain(component.text)), - availableSize: CGSize(width: 100.0, height: 40.0), + availableSize: CGSize(width: 140.0, height: 40.0), transition: context.transition ) - let iconSize = CGSize(width: 24.0, height: 24.0) + + var subtext: _UpdatedChildComponent? = nil + if let sublabel = component.subtext { + subtext = subLabel.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( - name: "Premium/Stars/StarLarge", + name: "Premium/Stars/StarMedium", tintColor: nil ), availableSize: iconSize, transition: context.transition ) - let spacing: CGFloat = 3.0 + let spacing: CGFloat = 0.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 += subtext.size.height + } + + 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)) ) + if let subtext { + context.add(subtext + .position(CGPoint(x: size.width - subtext.size.width / 2.0, y: firstLineSize + subtext.size.height / 2.0)) + ) + } return size } } diff --git a/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift b/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift index 6c1e5dc948..72e92b950f 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,39 @@ 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) + 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") + } + + 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, let smallIconOutlineView = self.smallIconOutlineView { + self.smallIconView = nil + smallIconView.removeFromSuperview() + self.smallIconOutlineView = nil + smallIconOutlineView.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 618c0ab57b..21e5cf1eec 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -30,34 +30,34 @@ private final class StarsTransactionSheetContent: CombinedComponent { let context: AccountContext let subject: StarsTransactionScreen.Subject - let action: () -> Void let cancel: (Bool) -> Void let openPeer: (EnginePeer) -> Void let openMessage: (EngineMessage.Id) -> Void 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, subject: StarsTransactionScreen.Subject, - action: @escaping () -> Void, cancel: @escaping (Bool) -> Void, openPeer: @escaping (EnginePeer) -> Void, 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 - self.action = action self.cancel = cancel self.openPeer = openPeer self.openMessage = openMessage self.openMedia = openMedia self.openAppExamples = openAppExamples self.copyTransactionId = copyTransactionId + self.updateSubscription = updateSubscription } static func ==(lhs: StarsTransactionSheetContent, rhs: StarsTransactionSheetContent) -> Bool { @@ -97,6 +97,10 @@ 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) + case let .importer(_, _, importer, _): + peerIds.append(importer.peer.peerId) } self.disposable = (context.engine.data.get( @@ -139,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: []) @@ -183,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 @@ -195,15 +203,71 @@ 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 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 = "" + count = subscription.pricing.amount + transactionId = nil + date = subscription.untilDate + via = nil + messageId = nil + 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 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 + isSubscriptionFee = true + } else if transaction.flags.contains(.isGift) { titleText = strings.Stars_Gift_Received_Title descriptionText = strings.Stars_Gift_Received_Text count = transaction.count @@ -218,9 +282,6 @@ private final class StarsTransactionSheetContent: CombinedComponent { toPeer = nil } transactionPeer = transaction.peer - media = [] - photo = nil - isRefund = false isGift = true } else { switch transaction.peer { @@ -299,8 +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 - isGift = false - 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 @@ -316,10 +381,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) @@ -345,9 +407,6 @@ private final class StarsTransactionSheetContent: CombinedComponent { toPeer = state.peerMap[message.id.peerId] } transactionPeer = nil - media = [] - photo = nil - isRefund = false isGift = true delayedCloseOnOpenPeer = false } @@ -367,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 { @@ -377,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( @@ -396,6 +456,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { ) let imageSubject: StarsImageComponent.Subject + let imageIcon: StarsImageComponent.Icon? if isGift { imageSubject = .gift(count) } else if !media.isEmpty { @@ -409,6 +470,11 @@ private final class StarsTransactionSheetContent: CombinedComponent { } else { imageSubject = .none } + if isSubscription || isSubscriber || isSubscriptionFee { + imageIcon = .star + } else { + imageIcon = nil + } let star = star.update( component: StarsImageComponent( context: component.context, @@ -416,6 +482,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 @@ -424,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), @@ -474,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( @@ -568,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))) ) @@ -589,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) }) @@ -597,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)] { @@ -617,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)) ) @@ -658,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) }) @@ -702,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), @@ -740,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 @@ -757,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 } @@ -778,31 +909,31 @@ private final class StarsTransactionSheetComponent: CombinedComponent { let context: AccountContext let subject: StarsTransactionScreen.Subject - let action: () -> Void let openPeer: (EnginePeer) -> Void let openMessage: (EngineMessage.Id) -> Void 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, subject: StarsTransactionScreen.Subject, - action: @escaping () -> Void, openPeer: @escaping (EnginePeer) -> Void, 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 - self.action = action self.openPeer = openPeer self.openMessage = openMessage self.openMedia = openMedia self.openAppExamples = openAppExamples self.copyTransactionId = copyTransactionId + self.updateSubscription = updateSubscription } static func ==(lhs: StarsTransactionSheetComponent, rhs: StarsTransactionSheetComponent) -> Bool { @@ -830,7 +961,6 @@ private final class StarsTransactionSheetComponent: CombinedComponent { content: AnyComponent(StarsTransactionSheetContent( context: context.component.context, subject: context.component.subject, - action: context.component.action, cancel: { animate in if animate { if let controller = controller() as? StarsTransactionScreen { @@ -847,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, @@ -914,10 +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 @@ -929,7 +1067,7 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { context: AccountContext, subject: StarsTransactionScreen.Subject, forceDark: Bool = false, - action: @escaping () -> Void + updateSubscription: @escaping (Bool) -> Void = { _ in } ) { self.context = context @@ -938,12 +1076,13 @@ 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( context: context, subject: subject, - action: action, openPeer: { peerId in openPeerImpl?(peerId) }, @@ -958,6 +1097,9 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { }, copyTransactionId: { transactionId in copyTransactionIdImpl?(transactionId) + }, + updateSubscription: { action in + updateSubscriptionImpl?(action) } ), navigationBarAppearance: .none, @@ -1069,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 61a909dff3..e54ee86672 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 } @@ -265,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] = [] @@ -298,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 d0ff4ecdb2..27cbe07ea0 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -18,26 +18,35 @@ import ListSectionComponent import BundleIconComponent import TextFormat import UndoUI +import ListActionItemComponent +import StarsAvatarComponent +import TelegramStringFormatting final class StarsTransactionsScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let starsContext: StarsContext + let subscriptionsContext: StarsSubscriptionsContext let openTransaction: (StarsContext.State.Transaction) -> Void + let openSubscription: (StarsContext.State.Subscription) -> Void let buy: () -> Void let gift: () -> Void init( context: AccountContext, starsContext: StarsContext, + subscriptionsContext: StarsSubscriptionsContext, openTransaction: @escaping (StarsContext.State.Transaction) -> Void, + openSubscription: @escaping (StarsContext.State.Subscription) -> Void, buy: @escaping () -> Void, gift: @escaping () -> Void ) { self.context = context self.starsContext = starsContext + self.subscriptionsContext = subscriptionsContext self.openTransaction = openTransaction + self.openSubscription = openSubscription self.buy = buy self.gift = gift } @@ -116,6 +125,9 @@ final class StarsTransactionsScreenComponent: Component { private var previousBalance: Int64? + private var subscriptionsStateDisposable: Disposable? + private var subscriptionsState: StarsSubscriptionsContext.State? + private var allTransactionsContext: StarsTransactionsContext? private var incomingTransactionsContext: StarsTransactionsContext? private var outgoingTransactionsContext: StarsTransactionsContext? @@ -301,6 +313,18 @@ final class StarsTransactionsScreenComponent: Component { self.state?.updated() } }) + + self.subscriptionsStateDisposable = (component.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 @@ -574,7 +598,109 @@ final class StarsTransactionsScreenComponent: Component { contentHeight += balanceSize.height contentHeight += 44.0 - let subscriptionsItems: [AnyComponentWithIdentity] = [] + let fontBaseDisplaySize = 17.0 + var subscriptionsItems: [AnyComponentWithIdentity] = [] + if let subscriptionsState = self.subscriptionsState { + for subscription in subscriptionsState.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 + ))) + ) + //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: dateText, + font: Font.regular(floor(fontBaseDisplaySize * 15.0 / 17.0)), + textColor: environment.theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 1 + ))) + ) + + let labelComponent: AnyComponentWithIdentity + 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))) + } + + 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: labelComponent, insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))), + action: { [weak self] _ in + guard let self, let component = self.component else { + return + } + component.openSubscription(subscription) + } + ) + ) + )) + } + 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 { //TODO:localize @@ -754,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]>() @@ -761,15 +888,22 @@ 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)? + var openSubscriptionImpl: ((StarsContext.State.Subscription) -> Void)? super.init(context: context, component: StarsTransactionsScreenComponent( context: context, starsContext: starsContext, + subscriptionsContext: self.subscriptionsContext, openTransaction: { transaction in openTransactionImpl?(transaction) }, + openSubscription: { subscription in + openSubscriptionImpl?(subscription) + }, buy: { buyImpl?() }, @@ -796,6 +930,19 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer { }) } + openSubscriptionImpl = { [weak self] subscription in + guard let self else { + return + } + 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) + } + buyImpl = { [weak self] in guard let self else { return diff --git a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift index 913a0eff7e..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 { @@ -248,6 +266,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] @@ -261,7 +280,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 ) @@ -281,13 +300,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 @@ -321,8 +346,16 @@ private final class SheetContent: CombinedComponent { contentSize.height += 126.0 + let titleString: String + if isSubscription { + //TODO:localize + titleString = "Subscribe to the Channel" + } 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 +375,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 +481,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 +546,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 +563,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 +597,50 @@ 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: strings.Stars_Subscription_Terms, + attributes: termsMarkdownAttributes + ), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + 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 + ) + 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 @@ -575,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, @@ -582,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 @@ -590,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 { @@ -623,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() { @@ -681,8 +767,9 @@ public final class StarsTransferScreen: ViewControllerComponentContainer { starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, - extendedMedia: [TelegramExtendedMedia], + extendedMedia: [TelegramExtendedMedia] = [], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, + navigateToPeer: @escaping (EnginePeer) -> Void = { _ in }, completion: @escaping (Bool) -> Void ) { self.context = context @@ -697,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/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift index 0270432457..7a03e2b91c 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,19 +283,19 @@ 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 if case .paidMedia = component.mode { buttonString = environment.strings.Stars_PaidContent_Create } else if let amount = state.amount { - buttonString = "\(environment.strings.Stars_Withdraw_Withdraw) # \(amount)" + buttonString = "\(environment.strings.Stars_Withdraw_Withdraw) # \(presentationStringsFormattedNumber(Int32(amount), environment.dateTimeFormat.groupingSeparator))" } else { buttonString = environment.strings.Stars_Withdraw_Withdraw } 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 0000000000..c2be8a1a21 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Item List/InviteLink.imageset/linklink_40.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Item List/SubscriptionLink.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Item List/SubscriptionLink.imageset/Contents.json new file mode 100644 index 0000000000..4092df77ca --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Item List/SubscriptionLink.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "cashlink_40.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Item List/SubscriptionLink.imageset/cashlink_40.pdf b/submodules/TelegramUI/Images.xcassets/Item List/SubscriptionLink.imageset/cashlink_40.pdf new file mode 100644 index 0000000000..b3ceedb679 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Item List/SubscriptionLink.imageset/cashlink_40.pdf differ 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 0000000000..22b2d4a5d3 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarMediumOutline.imageset/StarOutline.pdf differ 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 0000000000..4836b64fc5 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Stars/TransactionStar.imageset/StarTransaction.pdf differ 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 0000000000..bd3a7e4f21 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Stars/TransactionStarOutline.imageset/StarTransactionOutline.pdf differ 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/Chat/ChatControllerOpenWebApp.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift index 5d89917c5c..a39806ddb5 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift @@ -163,7 +163,7 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u var botId = peer.id var botName = botName var botAddress = "" - var botVerified = false + var botVerified = peer.isVerified if case let .inline(bot) = source { isInline = true botId = bot.id diff --git a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift index fbad3ba82c..ef5d3f69b1 100644 --- a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift +++ b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift @@ -196,7 +196,7 @@ extension ChatControllerImpl { restrictedBy: self.context.account.peerId, timestamp: 0, isMember: false - ), rank: nil) + ), rank: nil, subscriptionUntilDate: nil) } let peer = author @@ -207,7 +207,7 @@ extension ChatControllerImpl { switch participant { case .creator: break - case let .member(_, _, _, banInfo, _): + case let .member(_, _, _, banInfo, _, _): if let banInfo { initialUserBannedRights[participant.peerId] = InitialBannedRights(value: banInfo.rights) } else { @@ -294,7 +294,7 @@ extension ChatControllerImpl { restrictedBy: self.context.account.peerId, timestamp: 0, isMember: false - ), rank: nil) + ), rank: nil, subscriptionUntilDate: nil) } let _ = (self.context.engine.data.get( @@ -312,7 +312,7 @@ extension ChatControllerImpl { switch participant { case .creator: break - case let .member(_, _, _, banInfo, _): + case let .member(_, _, _, banInfo, _, _): if let banInfo { initialUserBannedRights[participant.peerId] = InitialBannedRights(value: banInfo.rights) } else { 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/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index c92885e83d..7cd2153142 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -283,6 +283,56 @@ 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.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) + }) + } 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)) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 01749d1680..5ce56c2bc8 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2743,12 +2743,24 @@ 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), action: {}) + return StarsTransactionScreen(context: context, subject: .transaction(transaction, peer)) } public func makeStarsReceiptScreen(context: AccountContext, receipt: BotPaymentReceipt) -> ViewController { - return StarsTransactionScreen(context: context, subject: .receipt(receipt), action: {}) + return StarsTransactionScreen(context: context, subject: .receipt(receipt)) + } + + 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 { @@ -2764,7 +2776,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { } public func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController { - return StarsTransactionScreen(context: context, subject: .gift(message), action: {}) + return StarsTransactionScreen(context: context, subject: .gift(message)) } public func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal { diff --git a/submodules/TemporaryCachedPeerDataManager/Sources/ChannelMemberCategoryListContext.swift b/submodules/TemporaryCachedPeerDataManager/Sources/ChannelMemberCategoryListContext.swift index 6d8b21210a..581a55b288 100644 --- a/submodules/TemporaryCachedPeerDataManager/Sources/ChannelMemberCategoryListContext.swift +++ b/submodules/TemporaryCachedPeerDataManager/Sources/ChannelMemberCategoryListContext.swift @@ -18,7 +18,7 @@ public extension ChannelParticipant { switch self { case .creator: return nil - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): return adminInfo } } @@ -27,7 +27,7 @@ public extension ChannelParticipant { switch self { case .creator: return nil - case let .member(_, _, _, banInfo, _): + case let .member(_, _, _, banInfo, _, _): return banInfo } } @@ -36,7 +36,7 @@ public extension ChannelParticipant { switch self { case .creator: return false - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): if let adminInfo = adminInfo { if adminInfo.promotedBy != peerId { return false @@ -92,7 +92,7 @@ private extension CachedChannelAdminRank { } else { self = .owner } - case let .member(_, _, _, _, rank): + case let .member(_, _, _, _, rank, _): if let rank = rank { self = .custom(rank) } else { @@ -366,7 +366,7 @@ private final class ChannelMemberSingleCategoryListContext: ChannelMemberCategor switch self.category { case let .admins(query): if let updated = updated, (query == nil || updated.peer.indexName.matchesByTokens(query!)) { - if case let .member(_, _, adminInfo, _, _) = updated.participant, adminInfo == nil { + if case let .member(_, _, adminInfo, _, _, _) = updated.participant, adminInfo == nil { loop: for i in 0 ..< list.count { if list[i].peer.id == updated.peer.id { list.remove(at: i) diff --git a/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift b/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift index 005634bca0..4c25ab81ff 100644 --- a/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift +++ b/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift @@ -321,7 +321,7 @@ public final class PeerChannelMemberCategoriesContextsManager { self.impl.with { impl in for (contextPeerId, context) in impl.contexts { if contextPeerId == peerId { - context.replayUpdates([(.member(id: memberId, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil), nil, nil)]) + context.replayUpdates([(.member(id: memberId, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil), nil, nil)]) } } } 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 {