Stars subscriptions

This commit is contained in:
Ilya Laktyushin 2024-08-06 22:40:17 +02:00
parent 4a0d46047c
commit df311bb022
36 changed files with 767 additions and 239 deletions

View File

@ -12676,3 +12676,9 @@ Sorry for the inconvenience.";
"Stickers.CreateSticker" = "Create\nSticker"; "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.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";

View File

@ -1007,7 +1007,8 @@ public protocol SharedAccountContext: AnyObject {
func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController
func makeStarsTransactionScreen(context: AccountContext, transaction: StarsContext.State.Transaction, peer: EnginePeer) -> ViewController func makeStarsTransactionScreen(context: AccountContext, transaction: StarsContext.State.Transaction, peer: EnginePeer) -> ViewController
func makeStarsReceiptScreen(context: AccountContext, receipt: BotPaymentReceipt) -> ViewController func makeStarsReceiptScreen(context: AccountContext, receipt: BotPaymentReceipt) -> ViewController
func makeStarsSubscriptionScreen(context: AccountContext, subscription: StarsContext.State.Subscription) -> ViewController func makeStarsSubscriptionScreen(context: AccountContext, subscription: StarsContext.State.Subscription, update: @escaping (Bool) -> Void) -> ViewController
func makeStarsSubscriptionScreen(context: AccountContext, peer: EnginePeer, pricing: StarsSubscriptionPricing, importer: PeerInvitationImportersState.Importer, usdRate: Double) -> ViewController
func makeStarsStatisticsScreen(context: AccountContext, peerId: EnginePeer.Id, revenueContext: StarsRevenueStatsContext) -> ViewController func makeStarsStatisticsScreen(context: AccountContext, peerId: EnginePeer.Id, revenueContext: StarsRevenueStatsContext) -> ViewController
func makeStarsAmountScreen(context: AccountContext, initialValue: Int64?, completion: @escaping (Int64) -> Void) -> ViewController func makeStarsAmountScreen(context: AccountContext, initialValue: Int64?, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController

View File

@ -1225,6 +1225,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
var avatarBadgeBackground: ASImageNode? var avatarBadgeBackground: ASImageNode?
let onlineNode: PeerOnlineMarkerNode let onlineNode: PeerOnlineMarkerNode
var avatarTimerBadge: AvatarBadgeView? var avatarTimerBadge: AvatarBadgeView?
private var starView: StarView?
let pinnedIconNode: ASImageNode let pinnedIconNode: ASImageNode
var secretIconNode: ASImageNode? var secretIconNode: ASImageNode?
var verifiedIconView: ComponentHostView<Empty>? var verifiedIconView: ComponentHostView<Empty>?
@ -1827,6 +1828,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
if let item = self.item, case .chatList = item.index { 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.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 { } else {
if self.highlightedBackgroundNode.supernode != nil { if self.highlightedBackgroundNode.supernode != nil {
@ -1845,12 +1847,16 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
if let item = self.item { if let item = self.item {
let onlineIcon: UIImage? let onlineIcon: UIImage?
let effectiveBackgroundColor: UIColor
if item.isPinned { if item.isPinned {
onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .pinned, voiceChat: self.onlineIsVoiceChat) onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .pinned, voiceChat: self.onlineIsVoiceChat)
effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor
} else { } else {
onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .regular, voiceChat: self.onlineIsVoiceChat) 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.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 titleIconsWidth += currentMutedIconImage.size.width
} }
var isSubscription = false
var isSecret = false var isSecret = false
if !isPeerGroup { if !isPeerGroup {
if case let .chatList(index) = item.index, index.messageIndex.id.peerId.namespace == Namespaces.Peer.SecretChat { if case let .chatList(index) = item.index, index.messageIndex.id.peerId.namespace == Namespaces.Peer.SecretChat {
@ -2978,6 +2985,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
break break
} }
} else if case let .chat(itemPeer) = contentPeer, let peer = itemPeer.chatMainPeer { } 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 { if case let .peer(peerData) = item.content, peerData.customMessageListData?.hidePeerStatus == true {
currentCredibilityIconContent = nil currentCredibilityIconContent = nil
} else if case .savedMessagesChats = item.chatListLocation, peer.id == item.context.account.peerId { } 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) transition.updateSublayerTransformScale(node: strongSelf.onlineNode, scale: (1.0 - onlineInlineNavigationFraction) * 1.0 + onlineInlineNavigationFraction * 0.00001)
let onlineIcon: UIImage? let onlineIcon: UIImage?
let effectiveBackgroundColor: UIColor
if strongSelf.reallyHighlighted { if strongSelf.reallyHighlighted {
onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .highlighted, voiceChat: onlineIsVoiceChat) 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 { } else if case let .chatList(index) = item.index, index.pinningIndex != nil {
onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .pinned, voiceChat: onlineIsVoiceChat) onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .pinned, voiceChat: onlineIsVoiceChat)
effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor
} else { } else {
onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .regular, voiceChat: onlineIsVoiceChat) 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) 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 let autoremoveTimeoutFraction: CGFloat
if online { if online {
autoremoveTimeoutFraction = 0.0 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
}
}

View File

@ -90,7 +90,7 @@ public enum ChatListNotice: Equatable {
case birthdayPremiumGift(peers: [EnginePeer], birthdays: [EnginePeer.Id: TelegramBirthday]) case birthdayPremiumGift(peers: [EnginePeer], birthdays: [EnginePeer.Id: TelegramBirthday])
case reviewLogin(newSessionReview: NewSessionReview, totalCount: Int) case reviewLogin(newSessionReview: NewSessionReview, totalCount: Int)
case premiumGrace case premiumGrace
case starsSubscriptionLowBalance case starsSubscriptionLowBalance(amount: Int64)
} }
enum ChatListNodeEntry: Comparable, Identifiable { enum ChatListNodeEntry: Comparable, Identifiable {

View File

@ -262,10 +262,10 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode {
okButtonLayout = makeOkButtonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.ChatList_SessionReview_PanelConfirm, font: titleFont, textColor: item.theme.list.itemAccentColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0))) 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))) cancelButtonLayout = makeCancelButtonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.ChatList_SessionReview_PanelReject, font: titleFont, textColor: item.theme.list.itemDestructiveColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0)))
case .starsSubscriptionLowBalance: case let .starsSubscriptionLowBalance(amount):
let titleStringValue = NSMutableAttributedString(attributedString: NSAttributedString(string: "5 Stars needed for Astro Paws", font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor)) let titleStringValue = NSMutableAttributedString(attributedString: NSAttributedString(string: "⭐️ \(amount) Stars needed for your subscriptions", font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor))
titleString = titleStringValue titleString = titleStringValue
textString = NSAttributedString(string: "Insufficient funds to cover your subscription.", font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) textString = NSAttributedString(string: "Insufficient funds to cover your subscriptions.", font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
} }
var leftInset: CGFloat = sideInset var leftInset: CGFloat = sideInset

View File

@ -79,7 +79,7 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
case subscriptionFeeToggle(PresentationTheme, String, Bool, Bool) case subscriptionFeeToggle(PresentationTheme, String, Bool, Bool)
case subscriptionFee(PresentationTheme, String, Bool, Int64?) case subscriptionFee(PresentationTheme, String, Bool, Int64?, String)
case subscriptionFeeInfo(PresentationTheme, String) case subscriptionFeeInfo(PresentationTheme, String)
case requestApproval(PresentationTheme, String, Bool, Bool) case requestApproval(PresentationTheme, String, Bool, Bool)
@ -182,8 +182,8 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .subscriptionFee(lhsTheme, lhsText, lhsValue, lhsEnabled): case let .subscriptionFee(lhsTheme, lhsText, lhsValue, lhsEnabled, lhsLabel):
if case let .subscriptionFee(rhsTheme, rhsText, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled { if case let .subscriptionFee(rhsTheme, rhsText, rhsValue, rhsEnabled, rhsLabel) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled, lhsLabel == rhsLabel {
return true return true
} else { } else {
return false return false
@ -288,7 +288,6 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
}, action: {}) }, action: {})
case let .titleInfo(_, text): case let .titleInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .subscriptionFeeToggle(_, text, value, enabled): case let .subscriptionFeeToggle(_, text, value, enabled):
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateState { state in arguments.updateState { state in
@ -302,13 +301,13 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
return updatedState return updatedState
} }
}) })
case let .subscriptionFee(_, placeholder, enabled, value): case let .subscriptionFee(_, placeholder, enabled, value, label):
let title = NSMutableAttributedString(string: "⭐️", font: Font.semibold(18.0), textColor: .white) let title = NSMutableAttributedString(string: "⭐️", font: Font.semibold(18.0), textColor: .white)
if let range = title.string.range(of: "⭐️") { 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(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)) title.addAttribute(.baselineOffset, value: -1.0, range: NSRange(range, in: title.string))
} }
return ItemListSingleLineInputItem(context: arguments.context, presentationData: presentationData, title: title, text: value.flatMap { "\($0)" } ?? "", placeholder: placeholder, type: .number, spacing: 3.0, enabled: enabled, sectionId: self.section, textUpdated: { text in return ItemListSingleLineInputItem(context: arguments.context, presentationData: presentationData, title: title, text: value.flatMap { "\($0)" } ?? "", placeholder: placeholder, label: label, type: .number, spacing: 3.0, enabled: enabled, sectionId: self.section, textUpdated: { text in
arguments.updateState { state in arguments.updateState { state in
var updatedState = state var updatedState = state
if let value = Int64(text) { if let value = Int64(text) {
@ -318,7 +317,7 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
} }
return updatedState return updatedState
} }
}, action: {}) }, action: {})
case let .subscriptionFeeInfo(_, text): case let .subscriptionFeeInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
case let .requestApproval(_, text, value, enabled): case let .requestApproval(_, text, value, enabled):
@ -458,7 +457,7 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
} }
} }
private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: InviteLinkEditControllerState, isGroup: Bool, isPublic: Bool, presentationData: PresentationData) -> [InviteLinksEditEntry] { private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: InviteLinkEditControllerState, isGroup: Bool, isPublic: Bool, presentationData: PresentationData, starsState: StarsRevenueStats?) -> [InviteLinksEditEntry] {
var entries: [InviteLinksEditEntry] = [] var entries: [InviteLinksEditEntry] = []
entries.append(.titleHeader(presentationData.theme, presentationData.strings.InviteLink_Create_LinkNameTitle.uppercased())) entries.append(.titleHeader(presentationData.theme, presentationData.strings.InviteLink_Create_LinkNameTitle.uppercased()))
@ -471,7 +470,11 @@ private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state:
//TODO:localize //TODO:localize
entries.append(.subscriptionFeeToggle(presentationData.theme, "Require Monthly Fee", state.subscriptionEnabled, isEditingEnabled)) entries.append(.subscriptionFeeToggle(presentationData.theme, "Require Monthly Fee", state.subscriptionEnabled, isEditingEnabled))
if state.subscriptionEnabled { if state.subscriptionEnabled {
entries.append(.subscriptionFee(presentationData.theme, "Stars amount per month", isEditingEnabled, state.subscriptionFee)) var label: String = ""
if let subscriptionFee, subscriptionFee > 0, let starsState {
label = formatTonUsdValue(state.subscriptionFee, divide: false, rate: starsState.usdRate, dateTimeFormat: presentationData.dateTimeFormat)
}
entries.append(.subscriptionFee(presentationData.theme, "Stars amount per month", isEditingEnabled, state.subscriptionFee, label))
} }
let infoText: String let infoText: String
if let _ = invite, state.subscriptionEnabled { if let _ = invite, state.subscriptionEnabled {
@ -545,7 +548,7 @@ private struct InviteLinkEditControllerState: Equatable {
var updating = false var updating = false
} }
public func inviteLinkEditController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id, invite: ExportedInvitation?, completion: ((ExportedInvitation?) -> Void)? = nil) -> ViewController { public func inviteLinkEditController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id, invite: ExportedInvitation?, starsState: StarsRevenueStats? = nil, completion: ((ExportedInvitation?) -> Void)? = nil) -> ViewController {
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
let actionsDisposable = DisposableSet() let actionsDisposable = DisposableSet()
@ -759,7 +762,7 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio
} }
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(invite == nil ? presentationData.strings.InviteLink_Create_Title : presentationData.strings.InviteLink_Create_EditTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(invite == nil ? presentationData.strings.InviteLink_Create_Title : presentationData.strings.InviteLink_Create_EditTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: inviteLinkEditControllerEntries(invite: invite, state: state, isGroup: isGroup, isPublic: isPublic, presentationData: presentationData), style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: animateChanges) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: inviteLinkEditControllerEntries(invite: invite, state: state, isGroup: isGroup, isPublic: isPublic, presentationData: presentationData, starsState: starsState), style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: animateChanges)
return (controllerState, (listState, arguments)) return (controllerState, (listState, arguments))
} }

View File

@ -215,7 +215,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
case let .mainLinkHeader(_, text): case let .mainLinkHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .mainLink(_, invite, peers, importersCount, isPublic): 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 { if let invite = invite {
arguments.copyLink(invite) arguments.copyLink(invite)
} }
@ -268,7 +268,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
} }
} }
private func inviteLinkListControllerEntries(presentationData: PresentationData, exportedInvitation: EngineExportedPeerInvitation?, peer: EnginePeer?, invites: [ExportedInvitation]?, revokedInvites: [ExportedInvitation]?, importers: PeerInvitationImportersState?, creators: [ExportedInvitationCreator], admin: ExportedInvitationCreator?, tick: Int32) -> [InviteLinksListEntry] { private func inviteLinkListControllerEntries(presentationData: PresentationData, exportedInvitation: EngineExportedPeerInvitation?, peer: EnginePeer?, invites: [ExportedInvitation]?, revokedInvites: [ExportedInvitation]?, importers: PeerInvitationImportersState?, creators: [ExportedInvitationCreator], admin: ExportedInvitationCreator?, tick: Int32, starsState: StarsRevenueStats?) -> [InviteLinksListEntry] {
var entries: [InviteLinksListEntry] = [] var entries: [InviteLinksListEntry] = []
if admin == nil { if admin == nil {
@ -393,12 +393,12 @@ private struct InviteLinkListControllerState: Equatable {
var revokingPrivateLink: Bool var revokingPrivateLink: Bool
} }
public func inviteLinkListController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id, admin: ExportedInvitationCreator?) -> ViewController { public func inviteLinkListController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id, admin: ExportedInvitationCreator?, starsRevenueContext: StarsRevenueStatsContext? = nil) -> ViewController {
var pushControllerImpl: ((ViewController) -> Void)? var pushControllerImpl: ((ViewController) -> Void)?
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
var presentInGlobalOverlayImpl: ((ViewController) -> Void)? var presentInGlobalOverlayImpl: ((ViewController) -> Void)?
var navigationController: (() -> NavigationController?)? var navigationController: (() -> NavigationController?)?
var dismissTooltipsImpl: (() -> Void)? var dismissTooltipsImpl: (() -> Void)?
let actionsDisposable = DisposableSet() let actionsDisposable = DisposableSet()
@ -409,6 +409,9 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio
statePromise.set(stateValue.modify { f($0) }) statePromise.set(stateValue.modify { f($0) })
} }
let starsContext: StarsRevenueStatsContext = starsRevenueContext ?? context.engine.payments.peerStarsRevenueContext(peerId: peerId)
let starsStats = Atomic<StarsRevenueStats?>(value: nil)
let revokeLinkDisposable = MetaDisposable() let revokeLinkDisposable = MetaDisposable()
actionsDisposable.add(revokeLinkDisposable) actionsDisposable.add(revokeLinkDisposable)
@ -487,7 +490,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio
} }
presentControllerImpl?(shareController, nil) presentControllerImpl?(shareController, nil)
}, openMainLink: { invite in }, 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) pushControllerImpl?(controller)
}, copyLink: { invite in }, copyLink: { invite in
UIPasteboard.general.string = invite.link UIPasteboard.general.string = invite.link
@ -604,7 +607,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio
let contextController = ContextController(presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) let contextController = ContextController(presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
presentInGlobalOverlayImpl?(contextController) presentInGlobalOverlayImpl?(contextController)
}, createLink: { }, 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 { if let invite = invite {
invitesContext.add(invite) invitesContext.add(invite)
} }
@ -613,7 +616,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio
pushControllerImpl?(controller) pushControllerImpl?(controller)
}, openLink: { invite in }, openLink: { invite in
if let invite = invite { 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) pushControllerImpl?(controller)
} }
}, linkContextAction: { invite, canEdit, node, gesture in }, linkContextAction: { invite, canEdit, node, gesture in
@ -730,7 +733,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio
}, action: { _, f in }, action: { _, f in
f(.default) 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 let invite = invite {
if invite.isRevoked { if invite.isRevoked {
invitesContext.remove(invite) invitesContext.remove(invite)
@ -897,12 +900,14 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio
invitesContext.state, invitesContext.state,
revokedInvitesContext.state, revokedInvitesContext.state,
creators, 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 previousInvites = previousInvites.swap(invites)
let previousRevokedInvites = previousRevokedInvites.swap(revokedInvites) let previousRevokedInvites = previousRevokedInvites.swap(revokedInvites)
let previousCreators = previousCreators.swap(creators) let previousCreators = previousCreators.swap(creators)
let _ = starsStats.swap(starsState.stats)
var crossfade = false var crossfade = false
if (previousInvites?.hasLoadedOnce ?? false) != (invites.hasLoadedOnce) { 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 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)) return (controllerState, (listState, arguments))
} }

View File

@ -48,14 +48,24 @@ private var subscriptionLinkIcon: UIImage? = {
class InviteLinkViewInteraction { class InviteLinkViewInteraction {
let context: AccountContext let context: AccountContext
let openPeer: (EnginePeer.Id) -> Void let openPeer: (EnginePeer.Id) -> Void
let openSubscription: (StarsSubscriptionPricing, PeerInvitationImportersState.Importer) -> Void
let copyLink: (ExportedInvitation) -> Void let copyLink: (ExportedInvitation) -> Void
let shareLink: (ExportedInvitation) -> Void let shareLink: (ExportedInvitation) -> Void
let editLink: (ExportedInvitation) -> Void let editLink: (ExportedInvitation) -> Void
let contextAction: (ExportedInvitation, ASDisplayNode, ContextGesture?) -> 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.context = context
self.openPeer = openPeer self.openPeer = openPeer
self.openSubscription = openSubscription
self.copyLink = copyLink self.copyLink = copyLink
self.shareLink = shareLink self.shareLink = shareLink
self.editLink = editLink self.editLink = editLink
@ -93,7 +103,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
case requestHeader(PresentationTheme, String, String, Bool) case requestHeader(PresentationTheme, String, String, Bool)
case request(Int32, PresentationTheme, PresentationDateTimeFormat, EnginePeer, Int32, Bool) case request(Int32, PresentationTheme, PresentationDateTimeFormat, EnginePeer, Int32, Bool)
case importerHeader(PresentationTheme, String, String, 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 { var stableId: InviteLinkViewEntryId {
switch self { switch self {
@ -113,7 +123,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
return .request(peer.id) return .request(peer.id)
case .importerHeader: case .importerHeader:
return .importerHeader return .importerHeader
case let .importer(_, _, _, peer, _, _, _): case let .importer(_, _, _, peer, _, _, _, _, _):
return .importer(peer.id) return .importer(peer.id)
} }
} }
@ -168,8 +178,8 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
} else { } else {
return false return false
} }
case let .importer(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsPeer, lhsDate, lhsJoinedViaFolderLink, lhsLoading): case let .importer(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsPeer, lhsDate, lhsJoinedViaFolderLink, lhsLoading, lhsImporter, lhsPricing):
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 { 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 return true
} else { } else {
return false return false
@ -237,11 +247,11 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
case .importer: case .importer:
return true return true
} }
case let .importer(lhsIndex, _, _, _, _, _, _): case let .importer(lhsIndex, _, _, _, _, _, _, _, _):
switch rhs { switch rhs {
case .link, .subscriptionHeader, .subscriptionPricing, .creatorHeader, .creator, .importerHeader, .request, .requestHeader: case .link, .subscriptionHeader, .subscriptionPricing, .creatorHeader, .creator, .importerHeader, .request, .requestHeader:
return false return false
case let .importer(rhsIndex, _, _, _, _, _, _): case let .importer(rhsIndex, _, _, _, _, _, _, _, _):
return lhsIndex < rhsIndex return lhsIndex < rhsIndex
} }
} }
@ -250,7 +260,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
func item(account: Account, presentationData: PresentationData, interaction: InviteLinkViewInteraction) -> ListViewItem { func item(account: Account, presentationData: PresentationData, interaction: InviteLinkViewInteraction) -> ListViewItem {
switch self { switch self {
case let .link(_, invite): 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) interaction.copyLink(invite)
}, shareAction: { }, shareAction: {
if invitationAvailability(invite).isZero { if invitationAvailability(invite).isZero {
@ -290,15 +300,35 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
additionalText = .none additionalText = .none
} }
return SectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title, additionalText: additionalText) 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 let dateString: String
if joinedViaFolderLink { if joinedViaFolderLink {
dateString = presentationData.strings.InviteLink_LabelJoinedViaFolder dateString = presentationData.strings.InviteLink_LabelJoinedViaFolder
} else { } else {
dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat) dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat)
} }
return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .peerList, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: peer.id != account.peerId, sectionId: 0, action: {
interaction.openPeer(peer.id) let label: ItemListPeerItemLabel
if let pricing {
//TODO:localize
let text = NSMutableAttributedString()
text.append(NSAttributedString(string: "⭐️\(pricing.amount)\n", font: Font.semibold(17.0), textColor: presentationData.theme.list.itemPrimaryTextColor))
text.append(NSAttributedString(string: "per month", font: Font.regular(13.0), textColor: presentationData.theme.list.itemSecondaryTextColor))
if let range = text.string.range(of: "⭐️") {
text.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: text.string))
text.addAttribute(NSAttributedString.Key.font, value: Font.semibold(15.0), range: NSRange(range, in: text.string))
text.addAttribute(.baselineOffset, value: 3.5, range: NSRange(range, in: text.string))
}
label = .attributedText(text)
} else {
label = .none
}
return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .peerList, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: label, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: peer.id != account.peerId, sectionId: 0, action: {
if let importer, let pricing {
interaction.openSubscription(pricing, importer)
} else {
interaction.openPeer(peer.id)
}
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, style: .plain, tag: nil, shimmering: loading ? ItemListPeerItemShimmering(alternationIndex: 0) : nil) }, 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): case let .request(_, _, dateTimeFormat, peer, date, loading):
let dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat) let dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat)
@ -351,18 +381,20 @@ public final class InviteLinkViewController: ViewController {
private let invitationsContext: PeerExportedInvitationsContext? private let invitationsContext: PeerExportedInvitationsContext?
private let revokedInvitationsContext: PeerExportedInvitationsContext? private let revokedInvitationsContext: PeerExportedInvitationsContext?
private let importersContext: PeerInvitationImportersContext? private let importersContext: PeerInvitationImportersContext?
private let starsState: StarsRevenueStats?
private var presentationData: PresentationData private var presentationData: PresentationData
private var presentationDataDisposable: Disposable? private var presentationDataDisposable: Disposable?
fileprivate var presentationDataPromise = Promise<PresentationData>() fileprivate var presentationDataPromise = Promise<PresentationData>()
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id, invite: ExportedInvitation, invitationsContext: PeerExportedInvitationsContext?, revokedInvitationsContext: PeerExportedInvitationsContext?, importersContext: PeerInvitationImportersContext?) { public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id, invite: ExportedInvitation, invitationsContext: PeerExportedInvitationsContext?, revokedInvitationsContext: PeerExportedInvitationsContext?, importersContext: PeerInvitationImportersContext?, starsState: StarsRevenueStats? = nil) {
self.context = context self.context = context
self.peerId = peerId self.peerId = peerId
self.invite = invite self.invite = invite
self.invitationsContext = invitationsContext self.invitationsContext = invitationsContext
self.revokedInvitationsContext = revokedInvitationsContext self.revokedInvitationsContext = revokedInvitationsContext
self.importersContext = importersContext self.importersContext = importersContext
self.starsState = starsState
self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
@ -550,14 +582,25 @@ public final class InviteLinkViewController: ViewController {
self.interaction = InviteLinkViewInteraction(context: context, openPeer: { [weak self] peerId in self.interaction = InviteLinkViewInteraction(context: context, openPeer: { [weak self] peerId in
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).start(next: { peer in |> deliverOnMainQueue).start(next: { peer in
guard let peer = peer else { guard let peer else {
return return
} }
if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), keepStack: .always)) 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 }, copyLink: { [weak self] invite in
UIPasteboard.general.string = invite.link UIPasteboard.general.string = invite.link
@ -766,7 +809,7 @@ public final class InviteLinkViewController: ViewController {
}))) })))
} }
} }
let contextController = ContextController(presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) 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) self?.controller?.presentInGlobalOverlay(contextController)
}) })
@ -791,6 +834,8 @@ public final class InviteLinkViewController: ViewController {
context.account.postbox.loadedPeerWithId(adminId) context.account.postbox.loadedPeerWithId(adminId)
) |> deliverOnMainQueue).start(next: { [weak self] presentationData, state, requestsState, creatorPeer in ) |> deliverOnMainQueue).start(next: { [weak self] presentationData, state, requestsState, creatorPeer in
if let strongSelf = self { if let strongSelf = self {
let usdRate = strongSelf.controller?.starsState?.usdRate
var entries: [InviteLinkViewEntry] = [] var entries: [InviteLinkViewEntry] = []
entries.append(.link(presentationData.theme, invite)) entries.append(.link(presentationData.theme, invite))
@ -802,7 +847,12 @@ public final class InviteLinkViewController: ViewController {
var subtitle = "No one joined yet" var subtitle = "No one joined yet"
if state.count > 0 { if state.count > 0 {
title += " x \(state.count)" title += " x \(state.count)"
subtitle = "You get approximately $\(Float(pricing.amount * Int64(state.count)) * 0.01) monthly" if let usdRate {
let usdValue = formatTonUsdValue(pricing.amount * Int64(state.count), divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat)
subtitle = "You get approximately \(usdValue) monthly"
} else {
subtitle = ""
}
} }
entries.append(.subscriptionPricing(presentationData.theme, title, subtitle)) entries.append(.subscriptionPricing(presentationData.theme, title, subtitle))
} }
@ -856,14 +906,14 @@ public final class InviteLinkViewController: ViewController {
loading = true 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) 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 { 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 { } else {
count = min(4, Int32(state.importers.count)) count = min(4, Int32(state.importers.count))
loading = false loading = false
for importer in state.importers { for importer in state.importers {
if let peer = importer.peer.peer { 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 index += 1
} }
@ -953,7 +1003,7 @@ public final class InviteLinkViewController: ViewController {
let revokedInvitationsContext = parentController.revokedInvitationsContext let revokedInvitationsContext = parentController.revokedInvitationsContext
if let navigationController = navigationController { if let navigationController = navigationController {
let updatedPresentationData = (self.presentationData, parentController.presentationDataPromise.get()) 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 let invite = invite {
if invite.isRevoked { if invite.isRevoked {
invitationsContext?.remove(invite) invitationsContext?.remove(invite)

View File

@ -198,7 +198,7 @@ public func inviteRequestsController(context: AccountContext, updatedPresentatio
} else { } else {
string = presentationData.strings.MemberRequests_UserAddedToGroup(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string 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)
}) })
} }

View File

@ -45,7 +45,7 @@ private enum ItemBackgroundColor: Equatable {
case .blue: case .blue:
return (UIColor(rgb: 0x00b5f7), UIColor(rgb: 0x00b2f6), UIColor(rgb: 0xa7f4ff)) return (UIColor(rgb: 0x00b5f7), UIColor(rgb: 0x00b2f6), UIColor(rgb: 0xa7f4ff))
case .green: case .green:
return (UIColor(rgb: 0x4aca62), UIColor(rgb: 0x43c85c), UIColor(rgb: 0xc5ffe6)) return (UIColor(rgb: 0x31b73b), UIColor(rgb: 0x88d93b), UIColor(rgb: 0xc5ffe6))
case .yellow: case .yellow:
return (UIColor(rgb: 0xf8a953), UIColor(rgb: 0xf7a64e), UIColor(rgb: 0xfeffd7)) return (UIColor(rgb: 0xf8a953), UIColor(rgb: 0xf7a64e), UIColor(rgb: 0xfeffd7))
case .red: case .red:
@ -208,7 +208,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode {
self.iconBackgroundNode = ASDisplayNode() self.iconBackgroundNode = ASDisplayNode()
self.iconBackgroundNode.setLayerBlock { () -> CALayer in self.iconBackgroundNode.setLayerBlock { () -> CALayer in
return CAShapeLayer() return CAGradientLayer()
} }
self.iconNode = ASImageNode() self.iconNode = ASImageNode()
@ -283,8 +283,11 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode {
public override func didLoad() { public override func didLoad() {
super.didLoad() super.didLoad()
if let shapeLayer = self.iconBackgroundNode.layer as? CAShapeLayer { self.iconBackgroundNode.cornerRadius = 20.0
shapeLayer.path = UIBezierPath(ovalIn: CGRect(x: 0.0, y: 0.0, width: 40.0, height: 40.0)).cgPath if let iconBackgroundLayer = self.iconBackgroundNode.layer as? CAGradientLayer {
iconBackgroundLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
iconBackgroundLayer.endPoint = CGPoint(x: 0.0, y: 1.0)
iconBackgroundLayer.type = .axial
} }
} }
@ -344,17 +347,24 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode {
transitionFraction = 0.0 transitionFraction = 0.0
} }
let topColor = color.colors.top let colors = color.colors
let nextTopColor = nextColor.colors.top let nextColors = nextColor.colors
let iconColor: UIColor let topIconColor: UIColor
let bottomIconColor: UIColor
if let _ = item.invite { if let _ = item.invite {
if case .blue = color { if case .green = color, item.invite?.pricing != nil {
iconColor = item.presentationData.theme.list.itemAccentColor topIconColor = color.colors.bottom
bottomIconColor = color.colors.top
} else if case .blue = color {
topIconColor = item.presentationData.theme.list.itemAccentColor
bottomIconColor = topIconColor
} else { } else {
iconColor = nextTopColor.mixedWith(topColor, alpha: transitionFraction) topIconColor = nextColors.top.mixedWith(colors.top, alpha: transitionFraction)
bottomIconColor = topIconColor
} }
} else { } else {
iconColor = item.presentationData.theme.list.mediaPlaceholderColor topIconColor = item.presentationData.theme.list.mediaPlaceholderColor
bottomIconColor = topIconColor
} }
let inviteLink = item.invite?.link?.replacingOccurrences(of: "https://", with: "") ?? "" let inviteLink = item.invite?.link?.replacingOccurrences(of: "https://", with: "") ?? ""
@ -400,7 +410,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode {
if let range = text.string.range(of: "⭐️") { 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(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(NSAttributedString.Key.font, value: Font.semibold(15.0), range: NSRange(range, in: text.string))
text.addAttribute(.baselineOffset, value: 2.5, range: NSRange(range, in: text.string)) text.addAttribute(.baselineOffset, value: 3.5, range: NSRange(range, in: text.string))
} }
pricingAttributedText = text pricingAttributedText = text
} }
@ -526,8 +536,11 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode {
} }
strongSelf.contextSourceNode.contentRect = extractedRect strongSelf.contextSourceNode.contentRect = extractedRect
if let layer = strongSelf.iconBackgroundNode.layer as? CAShapeLayer { if let iconBackgroundLayer = strongSelf.iconBackgroundNode.layer as? CAGradientLayer {
layer.fillColor = iconColor.cgColor iconBackgroundLayer.colors = [
topIconColor.cgColor,
bottomIconColor.cgColor
]
} }
if let _ = updatedTheme { if let _ = updatedTheme {
@ -633,7 +646,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.timerNode = timerNode strongSelf.timerNode = timerNode
strongSelf.offsetContainerNode.addSubnode(timerNode) strongSelf.offsetContainerNode.addSubnode(timerNode)
} }
timerNode.update(color: iconColor, value: timerValue) timerNode.update(color: topIconColor, value: timerValue)
} else if let timerNode = strongSelf.timerNode { } else if let timerNode = strongSelf.timerNode {
strongSelf.timerNode = nil strongSelf.timerNode = nil
timerNode.removeFromSupernode() timerNode.removeFromSupernode()

View File

@ -32,6 +32,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
let count: Int32 let count: Int32
let peers: [EnginePeer] let peers: [EnginePeer]
let displayButton: Bool let displayButton: Bool
let separateButtons: Bool
let displayImporters: Bool let displayImporters: Bool
let buttonColor: UIColor? let buttonColor: UIColor?
public let sectionId: ItemListSectionId public let sectionId: ItemListSectionId
@ -49,6 +50,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
count: Int32, count: Int32,
peers: [EnginePeer], peers: [EnginePeer],
displayButton: Bool, displayButton: Bool,
separateButtons: Bool = false,
displayImporters: Bool, displayImporters: Bool,
buttonColor: UIColor?, buttonColor: UIColor?,
sectionId: ItemListSectionId, sectionId: ItemListSectionId,
@ -65,6 +67,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
self.count = count self.count = count
self.peers = peers self.peers = peers
self.displayButton = displayButton self.displayButton = displayButton
self.separateButtons = separateButtons
self.displayImporters = displayImporters self.displayImporters = displayImporters
self.buttonColor = buttonColor self.buttonColor = buttonColor
self.sectionId = sectionId self.sectionId = sectionId
@ -126,6 +129,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
private let addressButtonNode: HighlightTrackingButtonNode private let addressButtonNode: HighlightTrackingButtonNode
private let addressButtonIconNode: ASImageNode private let addressButtonIconNode: ASImageNode
private var addressShimmerNode: ShimmerEffectNode? private var addressShimmerNode: ShimmerEffectNode?
private var copyButtonNode: SolidRoundedButtonNode?
private var shareButtonNode: SolidRoundedButtonNode? private var shareButtonNode: SolidRoundedButtonNode?
private let avatarsButtonNode: HighlightTrackingButtonNode 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 self.shareButtonNode?.pressed = { [weak self] in
if let strongSelf = self, let item = strongSelf.item { if let strongSelf = self, let item = strongSelf.item {
item.shareAction?() item.shareAction?()
@ -444,7 +453,31 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
strongSelf.addressButtonNode.isHidden = item.contextAction == nil strongSelf.addressButtonNode.isHidden = item.contextAction == nil
strongSelf.addressButtonIconNode.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 let shareButtonNode: SolidRoundedButtonNode
if let currentShareButtonNode = strongSelf.shareButtonNode { if let currentShareButtonNode = strongSelf.shareButtonNode {
shareButtonNode = currentShareButtonNode shareButtonNode = currentShareButtonNode
@ -459,7 +492,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
if let invite = item.invite, invitationAvailability(invite).isZero { if let invite = item.invite, invitationAvailability(invite).isZero {
shareButtonNode.title = item.presentationData.strings.InviteLink_ReactivateLink shareButtonNode.title = item.presentationData.strings.InviteLink_ReactivateLink
} else { } 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 shareButtonNode.pressed = { [weak self] in
self?.item?.shareAction?() self?.item?.shareAction?()
@ -468,9 +501,19 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
strongSelf.shareButtonNode = shareButtonNode 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) 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 totalWidth = invitedPeersLayout.size.width
var leftOrigin: CGFloat = floorToScreenPixels((params.width - invitedPeersLayout.size.width) / 2.0) 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.fieldButtonNode.isUserInteractionEnabled = item.invite != nil
strongSelf.addressButtonIconNode.alpha = item.invite != nil ? 1.0 : 0.0 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?.isUserInteractionEnabled = item.invite != nil
strongSelf.shareButtonNode?.alpha = item.invite != nil ? 1.0 : 0.4 strongSelf.shareButtonNode?.alpha = item.invite != nil ? 1.0 : 0.4
strongSelf.shareButtonNode?.isHidden = !item.displayButton strongSelf.shareButtonNode?.isHidden = !item.displayButton
strongSelf.avatarsButtonNode.isHidden = !item.displayImporters strongSelf.avatarsButtonNode.isHidden = !item.displayImporters
strongSelf.avatarsNode.isHidden = !item.displayImporters || item.invite == nil strongSelf.avatarsNode.isHidden = !item.displayImporters || item.invite == nil
strongSelf.invitedPeersNode.isHidden = !item.displayImporters || item.invite == nil strongSelf.invitedPeersNode.isHidden = !item.displayImporters || item.invite == nil

View File

@ -251,6 +251,7 @@ public enum ItemListPeerItemLabel {
case text(String, ItemListPeerItemLabelFont) case text(String, ItemListPeerItemLabelFont)
case disclosure(String) case disclosure(String)
case badge(String) case badge(String)
case attributedText(NSAttributedString)
} }
public struct ItemListPeerItemSwitch { public struct ItemListPeerItemSwitch {
@ -728,7 +729,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
private var avatarButton: HighlightTrackingButton? private var avatarButton: HighlightTrackingButton?
private let titleNode: TextNode private let titleNode: TextNode
private let labelNode: TextNode private let labelNode: TextNodeWithEntities
private let labelBadgeNode: ASImageNode private let labelBadgeNode: ASImageNode
private var labelArrowNode: ASImageNode? private var labelArrowNode: ASImageNode?
private let statusNode: TextNode private let statusNode: TextNode
@ -829,10 +830,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
self.statusNode.contentMode = .left self.statusNode.contentMode = .left
self.statusNode.contentsScale = UIScreen.main.scale self.statusNode.contentsScale = UIScreen.main.scale
self.labelNode = TextNode() self.labelNode = TextNodeWithEntities()
self.labelNode.isUserInteractionEnabled = false
self.labelNode.contentMode = .left
self.labelNode.contentsScale = UIScreen.main.scale
self.labelBadgeNode = ASImageNode() self.labelBadgeNode = ASImageNode()
self.labelBadgeNode.displayWithoutProcessing = true self.labelBadgeNode.displayWithoutProcessing = true
@ -850,7 +848,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
self.containerNode.addSubnode(self.avatarNode) self.containerNode.addSubnode(self.avatarNode)
self.containerNode.addSubnode(self.titleNode) self.containerNode.addSubnode(self.titleNode)
self.containerNode.addSubnode(self.statusNode) self.containerNode.addSubnode(self.statusNode)
self.containerNode.addSubnode(self.labelNode) self.containerNode.addSubnode(self.labelNode.textNode)
self.peerPresenceManager = PeerPresenceStatusManager(update: { [weak self] in self.peerPresenceManager = PeerPresenceStatusManager(update: { [weak self] in
if let strongSelf = self, let layoutParams = strongSelf.layoutParams { 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) { public func asyncLayout() -> (_ item: ItemListPeerItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors, _ headerAtTop: Bool) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeStatusLayout = TextNode.asyncLayout(self.statusNode) 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 editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode)
let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode) let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode)
@ -1156,42 +1154,49 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
editingOffset = 0.0 editingOffset = 0.0
} }
var labelMaximumNumberOfLines = 1
var labelInset: CGFloat = 0.0 var labelInset: CGFloat = 0.0
var labelAlignment: NSTextAlignment = .natural
var updatedLabelArrowNode: ASImageNode? var updatedLabelArrowNode: ASImageNode?
switch item.label { switch item.label {
case .none: case .none:
break break
case let .text(text, font): case let .attributedText(text):
let selectedFont: UIFont labelAttributedString = text
switch font { labelInset += 15.0
case .standard: labelMaximumNumberOfLines = 2
selectedFont = labelFont labelAlignment = .right
case let .custom(value): case let .text(text, font):
selectedFont = value let selectedFont: UIFont
} switch font {
labelAttributedString = NSAttributedString(string: text, font: selectedFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) case .standard:
labelInset += 15.0 selectedFont = labelFont
case let .disclosure(text): case let .custom(value):
if let currentLabelArrowNode = currentLabelArrowNode { selectedFont = value
updatedLabelArrowNode = currentLabelArrowNode }
} else { labelAttributedString = NSAttributedString(string: text, font: selectedFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
let arrowNode = ASImageNode() labelInset += 15.0
arrowNode.isLayerBacked = true case let .disclosure(text):
arrowNode.displayWithoutProcessing = true if let currentLabelArrowNode = currentLabelArrowNode {
arrowNode.displaysAsynchronously = false updatedLabelArrowNode = currentLabelArrowNode
arrowNode.image = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme) } else {
updatedLabelArrowNode = arrowNode let arrowNode = ASImageNode()
} arrowNode.isLayerBacked = true
labelInset += 40.0 arrowNode.displayWithoutProcessing = true
labelAttributedString = NSAttributedString(string: text, font: labelDisclosureFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) arrowNode.displaysAsynchronously = false
case let .badge(text): arrowNode.image = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme)
labelAttributedString = NSAttributedString(string: text, font: badgeFont, textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor) updatedLabelArrowNode = arrowNode
labelInset += 15.0 }
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 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 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())) 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 _ = titleApply()
let _ = statusApply() let _ = statusApply()
let _ = labelApply() 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.isHidden = labelAttributedString == nil }
strongSelf.labelNode.textNode.isHidden = labelAttributedString == nil
if strongSelf.backgroundNode.supernode == nil { if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
@ -1496,15 +1502,15 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
let labelFrame: CGRect let labelFrame: CGRect
if case .badge = item.label { 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) 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 { } 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) 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 let updateBadgeImage = updatedLabelBadgeImage {
if strongSelf.labelBadgeNode.supernode == nil { 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 strongSelf.labelBadgeNode.image = updateBadgeImage
} }
@ -1853,16 +1859,16 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
} }
let badgeDiameter: CGFloat = 20.0 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 badgeWidth = max(badgeDiameter, labelSize.width + 10.0)
let labelFrame: CGRect let labelFrame: CGRect
if case .badge = item.label { 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 { } 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))) 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)))

View File

@ -50,6 +50,7 @@ public class ItemListSingleLineInputItem: ListViewItem, ItemListItem {
let title: NSAttributedString let title: NSAttributedString
let text: String let text: String
let placeholder: String let placeholder: String
let label: String?
let type: ItemListSingleLineInputItemType let type: ItemListSingleLineInputItemType
let returnKeyType: UIReturnKeyType let returnKeyType: UIReturnKeyType
let alignment: ItemListSingleLineInputAlignment let alignment: ItemListSingleLineInputAlignment
@ -68,12 +69,13 @@ public class ItemListSingleLineInputItem: ListViewItem, ItemListItem {
let cleared: (() -> Void)? let cleared: (() -> Void)?
public let tag: ItemListItemTag? public let tag: ItemListItemTag?
public init(context: AccountContext? = nil, presentationData: ItemListPresentationData, title: NSAttributedString, text: String, placeholder: String, type: ItemListSingleLineInputItemType = .regular(capitalization: true, autocorrection: true), returnKeyType: UIReturnKeyType = .`default`, alignment: ItemListSingleLineInputAlignment = .default, spacing: CGFloat = 0.0, clearType: ItemListSingleLineInputClearType = .none, maxLength: Int = 0, enabled: Bool = true, selectAllOnFocus: Bool = false, secondaryStyle: Bool = false, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> String)? = nil, updatedFocus: ((Bool) -> Void)? = nil, action: @escaping () -> Void, cleared: (() -> Void)? = nil) { public init(context: AccountContext? = nil, presentationData: ItemListPresentationData, title: NSAttributedString, text: String, placeholder: String, label: String? = nil, type: ItemListSingleLineInputItemType = .regular(capitalization: true, autocorrection: true), returnKeyType: UIReturnKeyType = .`default`, alignment: ItemListSingleLineInputAlignment = .default, spacing: CGFloat = 0.0, clearType: ItemListSingleLineInputClearType = .none, maxLength: Int = 0, enabled: Bool = true, selectAllOnFocus: Bool = false, secondaryStyle: Bool = false, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> String)? = nil, updatedFocus: ((Bool) -> Void)? = nil, action: @escaping () -> Void, cleared: (() -> Void)? = nil) {
self.context = context self.context = context
self.presentationData = presentationData self.presentationData = presentationData
self.title = title self.title = title
self.text = text self.text = text
self.placeholder = placeholder self.placeholder = placeholder
self.label = label
self.type = type self.type = type
self.returnKeyType = returnKeyType self.returnKeyType = returnKeyType
self.alignment = alignment self.alignment = alignment

View File

@ -239,6 +239,7 @@ final class StarsTransactionItemNode: ListViewItemNode, ItemListItemNode {
itemSubtitle = peer.displayTitle(strings: item.presentationData.strings, displayOrder: .firstLast) itemSubtitle = peer.displayTitle(strings: item.presentationData.strings, displayOrder: .firstLast)
} else { } else {
if let _ = item.transaction.subscriptionPeriod { if let _ = item.transaction.subscriptionPeriod {
//TODO:localize
itemSubtitle = "Monthly subscription fee" itemSubtitle = "Monthly subscription fee"
} else { } else {
itemSubtitle = nil itemSubtitle = nil
@ -276,9 +277,15 @@ final class StarsTransactionItemNode: ListViewItemNode, ItemListItemNode {
} }
itemLabel = NSAttributedString(string: labelString, font: Font.medium(fontBaseDisplaySize), textColor: labelString.hasPrefix("-") ? item.presentationData.theme.list.itemDestructiveColor : item.presentationData.theme.list.itemDisclosureActions.constructive.fillColor) 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) itemDate = stringForMediumCompactDate(timestamp: item.transaction.date, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat)
if item.transaction.flags.contains(.isRefund) { if item.transaction.flags.contains(.isRefund) {
itemDate += " \(item.presentationData.strings.Stars_Intro_Transaction_Refund)" 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<Empty>] = [] var titleComponents: [AnyComponentWithIdentity<Empty>] = []
@ -309,7 +316,7 @@ final class StarsTransactionItemNode: ListViewItemNode, ItemListItemNode {
text: .plain(NSAttributedString( text: .plain(NSAttributedString(
string: itemDate, string: itemDate,
font: Font.regular(floor(fontBaseDisplaySize * 14.0 / 17.0)), font: Font.regular(floor(fontBaseDisplaySize * 14.0 / 17.0)),
textColor: item.presentationData.theme.list.itemSecondaryTextColor textColor: itemDateColor
)), )),
maximumNumberOfLines: 1 maximumNumberOfLines: 1
))) )))

View File

@ -1254,7 +1254,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController
} else { } else {
text = strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string 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 { } else {
if case let .channel(groupPeer) = groupPeer, let listenerLink = inviteLinks?.listenerLink, !groupPeer.hasPermission(.inviteMembers) { if case let .channel(groupPeer) = groupPeer, let listenerLink = inviteLinks?.listenerLink, !groupPeer.hasPermission(.inviteMembers) {
@ -1362,7 +1362,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController
} else { } else {
text = strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string 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 { } else if case let .legacyGroup(groupPeer) = groupPeer {
@ -1430,7 +1430,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController
} else { } else {
text = strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string 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 return
} }
let text = strongSelf.presentationData.strings.VoiceChat_PeerJoinedText(event.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string 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 { } else {
text = strongSelf.presentationData.strings.VoiceChat_DisplayAsSuccess(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string 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 self.stateVersionDisposable.set((self.call.stateVersion

View File

@ -956,10 +956,6 @@ private final class StarsSubscriptionsContextImpl {
updatedState.isLoading = false updatedState.isLoading = false
updatedState.canLoadMore = self.nextOffset != nil updatedState.canLoadMore = self.nextOffset != nil
self.updateState(updatedState) self.updateState(updatedState)
if updatedState.canLoadMore {
self.loadMore()
}
})) }))
} }
@ -967,6 +963,22 @@ private final class StarsSubscriptionsContextImpl {
self._state = state self._state = state
self._statePromise.set(.single(state)) self._statePromise.set(.single(state))
} }
func updateSubscription(id: String, cancel: Bool) {
var updatedState = self._state
if let index = updatedState.subscriptions.firstIndex(where: { $0.id == id }) {
let subscription = updatedState.subscriptions[index]
var updatedFlags = subscription.flags
if cancel {
updatedFlags.insert(.isCancelled)
} else {
updatedFlags.remove(.isCancelled)
}
let updatedSubscription = StarsContext.State.Subscription(flags: updatedFlags, id: subscription.id, peer: subscription.peer, untilDate: subscription.untilDate, pricing: subscription.pricing)
updatedState.subscriptions[index] = updatedSubscription
}
self.updateState(updatedState)
}
} }
public final class StarsSubscriptionsContext { public final class StarsSubscriptionsContext {
@ -1007,6 +1019,12 @@ public final class StarsSubscriptionsContext {
return StarsSubscriptionsContextImpl(account: account, starsContext: starsContext) return StarsSubscriptionsContextImpl(account: account, starsContext: starsContext)
}) })
} }
public func updateSubscription(id: String, cancel: Bool) {
self.impl.with {
$0.updateSubscription(id: id, cancel: cancel)
}
}
} }
@ -1203,7 +1221,7 @@ func _internal_updateStarsSubscription(account: Account, peerId: EnginePeer.Id,
return .complete() return .complete()
} }
let flags: Int32 = (1 << 0) let flags: Int32 = (1 << 0)
return account.network.request(Api.functions.payments.changeStarsSubscription(flags: flags, peer: inputPeer, subscriptionId: subscriptionId, canceled: .boolTrue)) return account.network.request(Api.functions.payments.changeStarsSubscription(flags: flags, peer: inputPeer, subscriptionId: subscriptionId, canceled: cancel ? .boolTrue : .boolFalse))
|> mapError { _ -> UpdateStarsSubsciptionError in |> mapError { _ -> UpdateStarsSubsciptionError in
return .generic return .generic
} }

View File

@ -510,6 +510,10 @@ public extension EnginePeer {
var isPremium: Bool { var isPremium: Bool {
return self._asPeer().isPremium return self._asPeer().isPremium
} }
var isSubscription: Bool {
return self._asPeer().isSubscription
}
var isService: Bool { var isService: Bool {
if case let .user(peer) = self { if case let .user(peer) = self {

View File

@ -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 { var isCloseFriend: Bool {
switch self { switch self {
case let user as TelegramUser: case let user as TelegramUser:

View File

@ -905,7 +905,7 @@ private let starImage: UIImage? = {
context.clear(CGRect(origin: .zero, size: size)) context.clear(CGRect(origin: .zero, size: size))
if let image = UIImage(bundleImageName: "Premium/Stars/StarLarge"), let cgImage = image.cgImage { 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) })?.withRenderingMode(.alwaysTemplate)
}() }()

View File

@ -9232,7 +9232,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
case .fallback: 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) (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: 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() 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: case .suggest:
@ -9469,7 +9469,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
case .fallback: 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) (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: 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() 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: case .suggest:

View File

@ -828,22 +828,35 @@ public final class StarsImageComponent: Component {
if let _ = component.icon { if let _ = component.icon {
let smallIconView: UIImageView let smallIconView: UIImageView
if let current = self.smallIconView { let smallIconOutlineView: UIImageView
if let current = self.smallIconView, let currentOutline = self.smallIconOutlineView {
smallIconView = current smallIconView = current
smallIconOutlineView = currentOutline
} else { } else {
smallIconOutlineView = UIImageView()
containerNode.view.addSubview(smallIconOutlineView)
self.smallIconOutlineView = smallIconOutlineView
smallIconView = UIImageView() smallIconView = UIImageView()
containerNode.view.addSubview(smallIconView) containerNode.view.addSubview(smallIconView)
self.smallIconView = smallIconView
smallIconOutlineView.image = UIImage(bundleImageName: "Premium/Stars/TransactionStarOutline")?.withRenderingMode(.alwaysTemplate)
smallIconView.image = UIImage(bundleImageName: "Premium/Stars/TransactionStar")
} }
smallIconView.image = UIImage(bundleImageName: "Premium/Stars/MockBigStar") smallIconOutlineView.tintColor = component.backgroundColor
if let icon = smallIconView.image { 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) let smallIconFrame = CGRect(origin: CGPoint(x: imageFrame.maxX - icon.size.width, y: imageFrame.maxY - icon.size.height), size: icon.size)
smallIconView.frame = smallIconFrame smallIconView.frame = smallIconFrame
smallIconOutlineView.frame = smallIconFrame
} }
} else if let smallIconView = self.smallIconView { } else if let smallIconView = self.smallIconView, let smallIconOutlineView = self.smallIconOutlineView {
self.smallIconView = nil self.smallIconView = nil
smallIconView.removeFromSuperview() smallIconView.removeFromSuperview()
self.smallIconOutlineView = nil
smallIconOutlineView.removeFromSuperview()
} }
if let _ = component.action { if let _ = component.action {

View File

@ -36,6 +36,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
let openMedia: ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void let openMedia: ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void
let openAppExamples: () -> Void let openAppExamples: () -> Void
let copyTransactionId: (String) -> Void let copyTransactionId: (String) -> Void
let updateSubscription: (StarsTransactionScreen.SubscriptionAction) -> Void
init( init(
context: AccountContext, context: AccountContext,
@ -45,7 +46,8 @@ private final class StarsTransactionSheetContent: CombinedComponent {
openMessage: @escaping (EngineMessage.Id) -> Void, openMessage: @escaping (EngineMessage.Id) -> Void,
openMedia: @escaping ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void, openMedia: @escaping ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void,
openAppExamples: @escaping () -> Void, openAppExamples: @escaping () -> Void,
copyTransactionId: @escaping (String) -> Void copyTransactionId: @escaping (String) -> Void,
updateSubscription: @escaping (StarsTransactionScreen.SubscriptionAction) -> Void
) { ) {
self.context = context self.context = context
self.subject = subject self.subject = subject
@ -55,6 +57,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
self.openMedia = openMedia self.openMedia = openMedia
self.openAppExamples = openAppExamples self.openAppExamples = openAppExamples
self.copyTransactionId = copyTransactionId self.copyTransactionId = copyTransactionId
self.updateSubscription = updateSubscription
} }
static func ==(lhs: StarsTransactionSheetContent, rhs: StarsTransactionSheetContent) -> Bool { static func ==(lhs: StarsTransactionSheetContent, rhs: StarsTransactionSheetContent) -> Bool {
@ -96,6 +99,8 @@ private final class StarsTransactionSheetContent: CombinedComponent {
peerIds.append(message.id.peerId) peerIds.append(message.id.peerId)
case let .subscription(subscription): case let .subscription(subscription):
peerIds.append(subscription.peer.id) peerIds.append(subscription.peer.id)
case let .importer(_, _, importer, _):
peerIds.append(importer.peer.peerId)
} }
self.disposable = (context.engine.data.get( self.disposable = (context.engine.data.get(
@ -138,10 +143,11 @@ private final class StarsTransactionSheetContent: CombinedComponent {
let description = Child(MultilineTextComponent.self) let description = Child(MultilineTextComponent.self)
let table = Child(TableComponent.self) let table = Child(TableComponent.self)
let additional = Child(BalancedTextComponent.self) let additional = Child(BalancedTextComponent.self)
let status = Child(BalancedTextComponent.self)
let button = Child(SolidRoundedButtonComponent.self) let button = Child(SolidRoundedButtonComponent.self)
let refundBackgound = Child(RoundedRectangle.self) let transactionStatusBackgound = Child(RoundedRectangle.self)
let refundText = Child(MultilineTextComponent.self) let transactionStatusText = Child(MultilineTextComponent.self)
let spaceRegex = try? NSRegularExpression(pattern: "\\[(.*?)\\]", options: []) let spaceRegex = try? NSRegularExpression(pattern: "\\[(.*?)\\]", options: [])
@ -182,8 +188,11 @@ private final class StarsTransactionSheetContent: CombinedComponent {
let titleText: String let titleText: String
let amountText: String let amountText: String
var descriptionText: String var descriptionText: String
let additionalText: String let additionalText = strings.Stars_Transaction_Terms
let buttonText: String var buttonText: String? = strings.Common_OK
var buttonIsDestructive = false
var statusText: String?
var statusIsDestructive = false
let count: Int64 let count: Int64
var countIsGeneric = false var countIsGeneric = false
@ -196,13 +205,28 @@ private final class StarsTransactionSheetContent: CombinedComponent {
let transactionPeer: StarsContext.State.Transaction.Peer? let transactionPeer: StarsContext.State.Transaction.Peer?
var media: [AnyMediaReference] = [] var media: [AnyMediaReference] = []
var photo: TelegramMediaWebFile? var photo: TelegramMediaWebFile?
var isRefund = false var transactionStatus: (String, UIColor)? = nil
var isGift = false var isGift = false
var isSubscription = false var isSubscription = false
var isSubscriber = false
var isSubscriptionFee = false var isSubscriptionFee = false
var isCancelled = false
var delayedCloseOnOpenPeer = true var delayedCloseOnOpenPeer = true
switch subject { 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): case let .subscription(subscription):
titleText = "Subscription" titleText = "Subscription"
descriptionText = "" descriptionText = ""
@ -214,6 +238,17 @@ private final class StarsTransactionSheetContent: CombinedComponent {
toPeer = subscription.peer toPeer = subscription.peer
transactionPeer = .peer(subscription.peer) transactionPeer = .peer(subscription.peer)
isSubscription = true 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): case let .transaction(transaction, parentPeer):
if let _ = transaction.subscriptionPeriod { if let _ = transaction.subscriptionPeriod {
//TODO:localize //TODO:localize
@ -325,7 +360,12 @@ private final class StarsTransactionSheetContent: CombinedComponent {
transactionPeer = transaction.peer transactionPeer = transaction.peer
media = transaction.media.map { AnyMediaReference.starsTransaction(transaction: StarsTransactionReference(peerId: parentPeer.id, id: transaction.id, isRefund: transaction.flags.contains(.isRefund)), media: $0) } media = transaction.media.map { AnyMediaReference.starsTransaction(transaction: StarsTransactionReference(peerId: parentPeer.id, id: transaction.id, isRefund: transaction.flags.contains(.isRefund)), media: $0) }
photo = transaction.photo photo = transaction.photo
isRefund = transaction.flags.contains(.isRefund)
if transaction.flags.contains(.isRefund) {
transactionStatus = (strings.Stars_Transaction_Refund, theme.list.itemDisclosureActions.constructive.fillColor)
} else if transaction.flags.contains(.isPending) {
transactionStatus = (strings.Monetization_Transaction_Pending, theme.list.itemDisclosureActions.warning.fillColor)
}
} }
case let .receipt(receipt): case let .receipt(receipt):
titleText = receipt.invoiceMedia.title titleText = receipt.invoiceMedia.title
@ -386,7 +426,10 @@ private final class StarsTransactionSheetContent: CombinedComponent {
let formattedAmount = presentationStringsFormattedNumber(abs(Int32(count)), dateTimeFormat.groupingSeparator) let formattedAmount = presentationStringsFormattedNumber(abs(Int32(count)), dateTimeFormat.groupingSeparator)
let countColor: UIColor let countColor: UIColor
if countIsGeneric { if isSubscription || isSubscriber {
amountText = "\(formattedAmount) / month"
countColor = theme.list.itemSecondaryTextColor
} else if countIsGeneric {
amountText = "\(formattedAmount)" amountText = "\(formattedAmount)"
countColor = theme.list.itemPrimaryTextColor countColor = theme.list.itemPrimaryTextColor
} else if count < 0 { } else if count < 0 {
@ -396,8 +439,6 @@ private final class StarsTransactionSheetContent: CombinedComponent {
amountText = "+ \(formattedAmount)" amountText = "+ \(formattedAmount)"
countColor = theme.list.itemDisclosureActions.constructive.fillColor countColor = theme.list.itemDisclosureActions.constructive.fillColor
} }
additionalText = strings.Stars_Transaction_Terms
buttonText = strings.Common_OK
let title = title.update( let title = title.update(
component: MultilineTextComponent( component: MultilineTextComponent(
@ -429,7 +470,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
} else { } else {
imageSubject = .none imageSubject = .none
} }
if isSubscription || isSubscriptionFee { if isSubscription || isSubscriber || isSubscriptionFee {
imageIcon = .star imageIcon = .star
} else { } else {
imageIcon = nil imageIcon = nil
@ -450,7 +491,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
transition: .immediate 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( let amount = amount.update(
component: BalancedTextComponent( component: BalancedTextComponent(
text: .plain(amountAttributedText), text: .plain(amountAttributedText),
@ -500,9 +541,17 @@ private final class StarsTransactionSheetContent: CombinedComponent {
) )
)) ))
} else if let toPeer { } 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( tableItems.append(.init(
id: "to", id: "to",
title: count < 0 || countIsGeneric ? strings.Stars_Transaction_To : strings.Stars_Transaction_From, title: title,
component: AnyComponent( component: AnyComponent(
Button( Button(
content: AnyComponent( content: AnyComponent(
@ -594,9 +643,25 @@ private final class StarsTransactionSheetContent: CombinedComponent {
)) ))
} }
let dateTitle: String
if isSubscription {
if isCancelled {
if date > Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) {
dateTitle = "Expires"
} else {
dateTitle = "Expired"
}
} else {
dateTitle = "Renews"
}
} else if isSubscriber {
dateTitle = "Subscribed"
} else {
dateTitle = strings.Stars_Transaction_Date
}
tableItems.append(.init( tableItems.append(.init(
id: "date", id: "date",
title: strings.Stars_Transaction_Date, title: dateTitle,
component: AnyComponent( component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
) )
@ -615,6 +680,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
let boldTextFont = Font.semibold(15.0) let boldTextFont = Font.semibold(15.0)
let textColor = theme.actionSheet.secondaryTextColor let textColor = theme.actionSheet.secondaryTextColor
let linkColor = theme.actionSheet.controlAccentColor 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 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) return (TelegramTextAttributes.URL, contents)
}) })
@ -623,7 +689,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
text: .markdown(text: additionalText, attributes: markdownAttributes), text: .markdown(text: additionalText, attributes: markdownAttributes),
horizontalAlignment: .center, horizontalAlignment: .center,
maximumNumberOfLines: 0, maximumNumberOfLines: 0,
lineSpacing: 0.1, lineSpacing: 0.2,
highlightColor: linkColor.withAlphaComponent(0.2), highlightColor: linkColor.withAlphaComponent(0.2),
highlightAction: { attributes in highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
@ -643,28 +709,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate 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 context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: 31.0 + 125.0)) .position(CGPoint(x: context.availableSize.width / 2.0, y: 31.0 + 125.0))
) )
@ -684,8 +729,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme) state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme)
} }
let textFont = Font.regular(15.0) let textColor = countOnTop && !isSubscriber ? theme.list.itemPrimaryTextColor : textColor
let textColor = countOnTop ? 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 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) return (TelegramTextAttributes.URL, contents)
}) })
@ -728,21 +772,21 @@ private final class StarsTransactionSheetContent: CombinedComponent {
let amountSpacing: CGFloat = 1.0 let amountSpacing: CGFloat = 1.0
var totalAmountWidth: CGFloat = amount.size.width + amountSpacing + amountStar.size.width var totalAmountWidth: CGFloat = amount.size.width + amountSpacing + amountStar.size.width
var amountOriginX: CGFloat = floor(context.availableSize.width - totalAmountWidth) / 2.0 var amountOriginX: CGFloat = floor(context.availableSize.width - totalAmountWidth) / 2.0
if isRefund { if let (statusText, statusColor) = transactionStatus {
let refundText = refundText.update( let refundText = transactionStatusText.update(
component: MultilineTextComponent( component: MultilineTextComponent(
text: .plain(NSAttributedString( text: .plain(NSAttributedString(
string: strings.Stars_Transaction_Refund, string: statusText,
font: Font.medium(14.0), font: Font.medium(14.0),
textColor: theme.list.itemDisclosureActions.constructive.fillColor textColor: statusColor
)) ))
), ),
availableSize: context.availableSize, availableSize: context.availableSize,
transition: .immediate transition: .immediate
) )
let refundBackground = refundBackgound.update( let refundBackground = transactionStatusBackgound.update(
component: RoundedRectangle( component: RoundedRectangle(
color: theme.list.itemDisclosureActions.constructive.fillColor.withAlphaComponent(0.1), color: statusColor.withAlphaComponent(0.1),
cornerRadius: 6.0 cornerRadius: 6.0
), ),
availableSize: CGSize(width: refundText.size.width + 10.0, height: refundText.size.height + 4.0), availableSize: CGSize(width: refundText.size.width + 10.0, height: refundText.size.height + 4.0),
@ -766,11 +810,22 @@ private final class StarsTransactionSheetContent: CombinedComponent {
} else { } else {
originY += amount.size.height + 20.0 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 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 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 context.add(table
@ -783,16 +838,66 @@ private final class StarsTransactionSheetContent: CombinedComponent {
) )
originY += additional.size.height + 23.0 originY += additional.size.height + 23.0
let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: button.size) if let statusText {
context.add(button originY += 7.0
.position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY)) 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 context.add(closeButton
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - closeButton.size.width, y: 28.0)) .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 return contentSize
} }
@ -809,6 +914,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent {
let openMedia: ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void let openMedia: ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void
let openAppExamples: () -> Void let openAppExamples: () -> Void
let copyTransactionId: (String) -> Void let copyTransactionId: (String) -> Void
let updateSubscription: (StarsTransactionScreen.SubscriptionAction) -> Void
init( init(
context: AccountContext, context: AccountContext,
@ -817,7 +923,8 @@ private final class StarsTransactionSheetComponent: CombinedComponent {
openMessage: @escaping (EngineMessage.Id) -> Void, openMessage: @escaping (EngineMessage.Id) -> Void,
openMedia: @escaping ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void, openMedia: @escaping ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void,
openAppExamples: @escaping () -> Void, openAppExamples: @escaping () -> Void,
copyTransactionId: @escaping (String) -> Void copyTransactionId: @escaping (String) -> Void,
updateSubscription: @escaping (StarsTransactionScreen.SubscriptionAction) -> Void
) { ) {
self.context = context self.context = context
self.subject = subject self.subject = subject
@ -826,6 +933,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent {
self.openMedia = openMedia self.openMedia = openMedia
self.openAppExamples = openAppExamples self.openAppExamples = openAppExamples
self.copyTransactionId = copyTransactionId self.copyTransactionId = copyTransactionId
self.updateSubscription = updateSubscription
} }
static func ==(lhs: StarsTransactionSheetComponent, rhs: StarsTransactionSheetComponent) -> Bool { static func ==(lhs: StarsTransactionSheetComponent, rhs: StarsTransactionSheetComponent) -> Bool {
@ -869,7 +977,8 @@ private final class StarsTransactionSheetComponent: CombinedComponent {
openMessage: context.component.openMessage, openMessage: context.component.openMessage,
openMedia: context.component.openMedia, openMedia: context.component.openMedia,
openAppExamples: context.component.openAppExamples, openAppExamples: context.component.openAppExamples,
copyTransactionId: context.component.copyTransactionId copyTransactionId: context.component.copyTransactionId,
updateSubscription: context.component.updateSubscription
)), )),
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
followContentSizeChanges: true, followContentSizeChanges: true,
@ -936,11 +1045,17 @@ private final class StarsTransactionSheetComponent: CombinedComponent {
} }
public class StarsTransactionScreen: ViewControllerComponentContainer { public class StarsTransactionScreen: ViewControllerComponentContainer {
enum SubscriptionAction {
case cancel
case renew
}
public enum Subject: Equatable { public enum Subject: Equatable {
case transaction(StarsContext.State.Transaction, EnginePeer) case transaction(StarsContext.State.Transaction, EnginePeer)
case receipt(BotPaymentReceipt) case receipt(BotPaymentReceipt)
case gift(EngineMessage) case gift(EngineMessage)
case subscription(StarsContext.State.Subscription) case subscription(StarsContext.State.Subscription)
case importer(EnginePeer, StarsSubscriptionPricing, PeerInvitationImportersState.Importer, Double)
} }
private let context: AccountContext private let context: AccountContext
@ -951,7 +1066,8 @@ public class StarsTransactionScreen: ViewControllerComponentContainer {
public init( public init(
context: AccountContext, context: AccountContext,
subject: StarsTransactionScreen.Subject, subject: StarsTransactionScreen.Subject,
forceDark: Bool = false forceDark: Bool = false,
updateSubscription: @escaping (Bool) -> Void = { _ in }
) { ) {
self.context = context self.context = context
@ -960,6 +1076,8 @@ public class StarsTransactionScreen: ViewControllerComponentContainer {
var openMediaImpl: (([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void)? var openMediaImpl: (([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void)?
var openAppExamplesImpl: (() -> Void)? var openAppExamplesImpl: (() -> Void)?
var copyTransactionIdImpl: ((String) -> Void)? var copyTransactionIdImpl: ((String) -> Void)?
var updateSubscriptionImpl: ((StarsTransactionScreen.SubscriptionAction) -> Void)?
super.init( super.init(
context: context, context: context,
component: StarsTransactionSheetComponent( component: StarsTransactionSheetComponent(
@ -979,6 +1097,9 @@ public class StarsTransactionScreen: ViewControllerComponentContainer {
}, },
copyTransactionId: { transactionId in copyTransactionId: { transactionId in
copyTransactionIdImpl?(transactionId) copyTransactionIdImpl?(transactionId)
},
updateSubscription: { action in
updateSubscriptionImpl?(action)
} }
), ),
navigationBarAppearance: .none, navigationBarAppearance: .none,
@ -1090,6 +1211,30 @@ public class StarsTransactionScreen: ViewControllerComponentContainer {
HapticFeedback().tap() 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) { required public init(coder aDecoder: NSCoder) {

View File

@ -268,9 +268,15 @@ final class StarsTransactionsListPanelComponent: Component {
} }
itemLabel = NSAttributedString(string: labelString, font: Font.medium(fontBaseDisplaySize), textColor: labelString.hasPrefix("-") ? environment.theme.list.itemDestructiveColor : environment.theme.list.itemDisclosureActions.constructive.fillColor) 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) itemDate = stringForMediumCompactDate(timestamp: item.date, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat)
if item.flags.contains(.isRefund) { if item.flags.contains(.isRefund) {
itemDate += " \(environment.strings.Stars_Intro_Transaction_Refund)" 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<Empty>] = [] var titleComponents: [AnyComponentWithIdentity<Empty>] = []
@ -301,7 +307,7 @@ final class StarsTransactionsListPanelComponent: Component {
text: .plain(NSAttributedString( text: .plain(NSAttributedString(
string: itemDate, string: itemDate,
font: Font.regular(floor(fontBaseDisplaySize * 14.0 / 17.0)), font: Font.regular(floor(fontBaseDisplaySize * 14.0 / 17.0)),
textColor: environment.theme.list.itemSecondaryTextColor textColor: itemDateColor
)), )),
maximumNumberOfLines: 1 maximumNumberOfLines: 1
))) )))

View File

@ -27,6 +27,7 @@ final class StarsTransactionsScreenComponent: Component {
let context: AccountContext let context: AccountContext
let starsContext: StarsContext let starsContext: StarsContext
let subscriptionsContext: StarsSubscriptionsContext
let openTransaction: (StarsContext.State.Transaction) -> Void let openTransaction: (StarsContext.State.Transaction) -> Void
let openSubscription: (StarsContext.State.Subscription) -> Void let openSubscription: (StarsContext.State.Subscription) -> Void
let buy: () -> Void let buy: () -> Void
@ -35,6 +36,7 @@ final class StarsTransactionsScreenComponent: Component {
init( init(
context: AccountContext, context: AccountContext,
starsContext: StarsContext, starsContext: StarsContext,
subscriptionsContext: StarsSubscriptionsContext,
openTransaction: @escaping (StarsContext.State.Transaction) -> Void, openTransaction: @escaping (StarsContext.State.Transaction) -> Void,
openSubscription: @escaping (StarsContext.State.Subscription) -> Void, openSubscription: @escaping (StarsContext.State.Subscription) -> Void,
buy: @escaping () -> Void, buy: @escaping () -> Void,
@ -42,6 +44,7 @@ final class StarsTransactionsScreenComponent: Component {
) { ) {
self.context = context self.context = context
self.starsContext = starsContext self.starsContext = starsContext
self.subscriptionsContext = subscriptionsContext
self.openTransaction = openTransaction self.openTransaction = openTransaction
self.openSubscription = openSubscription self.openSubscription = openSubscription
self.buy = buy self.buy = buy
@ -122,7 +125,6 @@ final class StarsTransactionsScreenComponent: Component {
private var previousBalance: Int64? private var previousBalance: Int64?
private var subscriptionsContext: StarsSubscriptionsContext?
private var subscriptionsStateDisposable: Disposable? private var subscriptionsStateDisposable: Disposable?
private var subscriptionsState: StarsSubscriptionsContext.State? private var subscriptionsState: StarsSubscriptionsContext.State?
@ -312,9 +314,7 @@ final class StarsTransactionsScreenComponent: Component {
} }
}) })
let subscriptionsContext = component.context.engine.payments.peerStarsSubscriptionsContext(starsContext: component.starsContext) self.subscriptionsStateDisposable = (component.subscriptionsContext.state
self.subscriptionsContext = subscriptionsContext
self.subscriptionsStateDisposable = (subscriptionsContext.state
|> deliverOnMainQueue).start(next: { [weak self] state in |> deliverOnMainQueue).start(next: { [weak self] state in
guard let self else { guard let self else {
return return
@ -614,10 +614,21 @@ final class StarsTransactionsScreenComponent: Component {
))) )))
) )
//TODO:localize //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( titleComponents.append(
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString( text: .plain(NSAttributedString(
string: "renews on \(stringForDateWithoutYear(date: Date(timeIntervalSince1970: Double(subscription.untilDate)), strings: environment.strings))", string: dateText,
font: Font.regular(floor(fontBaseDisplaySize * 15.0 / 17.0)), font: Font.regular(floor(fontBaseDisplaySize * 15.0 / 17.0)),
textColor: environment.theme.list.itemSecondaryTextColor textColor: environment.theme.list.itemSecondaryTextColor
)), )),
@ -626,15 +637,15 @@ final class StarsTransactionsScreenComponent: Component {
) )
let labelComponent: AnyComponentWithIdentity<Empty> let labelComponent: AnyComponentWithIdentity<Empty>
if "".isEmpty { if subscription.flags.contains(.isCancelled) {
labelComponent = AnyComponentWithIdentity(id: "cancelledLabel", component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: "cancelled", font: Font.regular(floor(fontBaseDisplaySize * 13.0 / 17.0)), textColor: environment.theme.list.itemDestructiveColor)))
))
} else {
let itemLabel = NSAttributedString(string: "\(subscription.pricing.amount)", font: Font.medium(fontBaseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor) let 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) 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))) labelComponent = AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(text: itemLabel, subtext: itemSublabel)))
} else {
labelComponent = AnyComponentWithIdentity(id: "cancelledLabel", component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: "cancelled", font: Font.regular(floor(fontBaseDisplaySize * 13.0 / 17.0)), textColor: environment.theme.list.itemDestructiveColor)))
))
} }
subscriptionsItems.append(AnyComponentWithIdentity( subscriptionsItems.append(AnyComponentWithIdentity(
@ -657,6 +668,38 @@ final class StarsTransactionsScreenComponent: Component {
) )
)) ))
} }
if subscriptionsState.canLoadMore {
subscriptionsItems.append(AnyComponentWithIdentity(
id: "showMore",
component: AnyComponent(
ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(Text(
text: "Show More",
font: Font.regular(17.0),
color: environment.theme.list.itemAccentColor
)),
leftIcon: .custom(
AnyComponentWithIdentity(
id: "icon",
component: AnyComponent(Image(
image: PresentationResourcesItemList.downArrowImage(environment.theme),
size: CGSize(width: 30.0, height: 30.0)
))
),
false
),
accessory: nil,
action: { _ in
},
highlighting: .default,
updateIsHighlighted: { view, _ in
})
)
))
}
} }
if !subscriptionsItems.isEmpty { if !subscriptionsItems.isEmpty {
@ -837,6 +880,7 @@ final class StarsTransactionsScreenComponent: Component {
public final class StarsTransactionsScreen: ViewControllerComponentContainer { public final class StarsTransactionsScreen: ViewControllerComponentContainer {
private let context: AccountContext private let context: AccountContext
private let starsContext: StarsContext private let starsContext: StarsContext
private let subscriptionsContext: StarsSubscriptionsContext
private let options = Promise<[StarsTopUpOption]>() private let options = Promise<[StarsTopUpOption]>()
@ -844,6 +888,8 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer {
self.context = context self.context = context
self.starsContext = starsContext self.starsContext = starsContext
self.subscriptionsContext = context.engine.payments.peerStarsSubscriptionsContext(starsContext: starsContext)
var buyImpl: (() -> Void)? var buyImpl: (() -> Void)?
var giftImpl: (() -> Void)? var giftImpl: (() -> Void)?
var openTransactionImpl: ((StarsContext.State.Transaction) -> Void)? var openTransactionImpl: ((StarsContext.State.Transaction) -> Void)?
@ -851,6 +897,7 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer {
super.init(context: context, component: StarsTransactionsScreenComponent( super.init(context: context, component: StarsTransactionsScreenComponent(
context: context, context: context,
starsContext: starsContext, starsContext: starsContext,
subscriptionsContext: self.subscriptionsContext,
openTransaction: { transaction in openTransaction: { transaction in
openTransactionImpl?(transaction) openTransactionImpl?(transaction)
}, },
@ -887,7 +934,12 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer {
guard let self else { guard let self else {
return return
} }
let controller = context.sharedContext.makeStarsSubscriptionScreen(context: context, subscription: subscription) let controller = context.sharedContext.makeStarsSubscriptionScreen(context: context, subscription: subscription, update: { [weak self] cancel in
guard let self else {
return
}
self.subscriptionsContext.updateSubscription(id: subscription.id, cancel: cancel)
})
self.push(controller) self.push(controller)
} }

View File

@ -262,7 +262,7 @@ private final class SheetContent: CombinedComponent {
var contentSize = CGSize(width: context.availableSize.width, height: 18.0) var contentSize = CGSize(width: context.availableSize.width, height: 18.0)
let background = background.update( 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), availableSize: CGSize(width: context.availableSize.width, height: 1000.0),
transition: .immediate transition: .immediate
) )
@ -270,7 +270,6 @@ private final class SheetContent: CombinedComponent {
.position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0)) .position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0))
) )
var isSubscription = false
let subject: StarsImageComponent.Subject let subject: StarsImageComponent.Subject
if !component.extendedMedia.isEmpty { if !component.extendedMedia.isEmpty {
subject = .extendedMedia(component.extendedMedia) subject = .extendedMedia(component.extendedMedia)
@ -283,13 +282,19 @@ private final class SheetContent: CombinedComponent {
} else { } else {
subject = .none subject = .none
} }
var isSubscription = false
if case .starsChatSubscription = context.component.source {
isSubscription = true
}
let star = star.update( let star = star.update(
component: StarsImageComponent( component: StarsImageComponent(
context: component.context, context: component.context,
subject: subject, subject: subject,
theme: theme, theme: theme,
diameter: 90.0, 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), availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0),
transition: context.transition transition: context.transition
@ -324,10 +329,9 @@ private final class SheetContent: CombinedComponent {
contentSize.height += 126.0 contentSize.height += 126.0
let titleString: String let titleString: String
if case .starsChatSubscription = context.component.source { if isSubscription {
//TODO:localize //TODO:localize
titleString = "Subscribe to the Channel" titleString = "Subscribe to the Channel"
isSubscription = true
} else { } else {
titleString = strings.Stars_Transfer_Title titleString = strings.Stars_Transfer_Title
} }
@ -588,12 +592,26 @@ private final class SheetContent: CombinedComponent {
let info = info.update( let info = info.update(
component: BalancedTextComponent( component: BalancedTextComponent(
text: .markdown( text: .markdown(
text: "By subscribing you agree to the [Terms of Service]()", text: strings.Stars_Subscription_Terms,
attributes: termsMarkdownAttributes attributes: termsMarkdownAttributes
), ),
horizontalAlignment: .center, horizontalAlignment: .center,
maximumNumberOfLines: 0, maximumNumberOfLines: 0,
lineSpacing: 0.2 lineSpacing: 0.2,
highlightColor: linkColor.withAlphaComponent(0.2),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { [weak controller] attributes, _ in
if let controller, let navigationController = controller.navigationController as? NavigationController {
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_Subscription_Terms_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
}
}
), ),
availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height), availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height),
transition: .immediate transition: .immediate

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "StarOutline.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "StarTransaction.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "StarTransactionOutline.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -3989,7 +3989,7 @@ extension ChatControllerImpl {
if let strongSelf = self { if let strongSelf = self {
HapticFeedback().impact() 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 { guard let strongSelf = self else {
return true return true
} }

View File

@ -87,34 +87,35 @@ func chatHistoryEntriesForView(
if (associatedData.subject?.isService ?? false) { if (associatedData.subject?.isService ?? false) {
} else { } else {
if case let .peer(peerId) = location, case let cachedData = cachedData as? CachedChannelData, let invitedOn = cachedData?.invitedOn { // if case let .peer(peerId) = location, case let cachedData = cachedData as? CachedChannelData, let invitedOn = cachedData?.invitedOn {
joinMessage = Message( // joinMessage = Message(
stableId: UInt32.max - 1000, // stableId: UInt32.max - 1000,
stableVersion: 0, // stableVersion: 0,
id: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: 0), // id: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: 0),
globallyUniqueId: nil, // globallyUniqueId: nil,
groupingKey: nil, // groupingKey: nil,
groupInfo: nil, // groupInfo: nil,
threadId: nil, // threadId: nil,
timestamp: invitedOn, // timestamp: invitedOn,
flags: [.Incoming], // flags: [.Incoming],
tags: [], // tags: [],
globalTags: [], // globalTags: [],
localTags: [], // localTags: [],
customTags: [], // customTags: [],
forwardInfo: nil, // forwardInfo: nil,
author: channelPeer, // author: channelPeer,
text: "", // text: "",
attributes: [], // attributes: [],
media: [TelegramMediaAction(action: .joinedByRequest)], // media: [TelegramMediaAction(action: .joinedByRequest)],
peers: SimpleDictionary<PeerId, Peer>(), // peers: SimpleDictionary<PeerId, Peer>(),
associatedMessages: SimpleDictionary<MessageId, Message>(), // associatedMessages: SimpleDictionary<MessageId, Message>(),
associatedMessageIds: [], // associatedMessageIds: [],
associatedMedia: [:], // associatedMedia: [:],
associatedThreadInfo: nil, // associatedThreadInfo: nil,
associatedStories: [:] // associatedStories: [:]
) // )
} else if let peer = channelPeer as? TelegramChannel, case .broadcast = peer.info, case .member = peer.participationStatus, !peer.flags.contains(.isCreator) { // } else
if let peer = channelPeer as? TelegramChannel, case .broadcast = peer.info, case .member = peer.participationStatus, !peer.flags.contains(.isCreator) {
joinMessage = Message( joinMessage = Message(
stableId: UInt32.max - 1000, stableId: UInt32.max - 1000,
stableVersion: 0, stableVersion: 0,

View File

@ -2750,8 +2750,12 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return StarsTransactionScreen(context: context, subject: .receipt(receipt)) return StarsTransactionScreen(context: context, subject: .receipt(receipt))
} }
public func makeStarsSubscriptionScreen(context: AccountContext, subscription: StarsContext.State.Subscription) -> ViewController { public func makeStarsSubscriptionScreen(context: AccountContext, subscription: StarsContext.State.Subscription, update: @escaping (Bool) -> Void) -> ViewController {
return StarsTransactionScreen(context: context, subject: .subscription(subscription)) 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 { public func makeStarsStatisticsScreen(context: AccountContext, peerId: EnginePeer.Id, revenueContext: StarsRevenueStatsContext) -> ViewController {

View File

@ -22,7 +22,7 @@ public enum UndoOverlayContent {
case chatRemovedFromFolder(chatTitle: String, folderTitle: String) case chatRemovedFromFolder(chatTitle: String, folderTitle: String)
case messagesUnpinned(title: String, text: String, undo: Bool, isHidden: Bool) case messagesUnpinned(title: String, text: String, undo: Bool, isHidden: Bool)
case setProximityAlert(title: String, text: String, cancelled: 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 linkCopied(text: String)
case banned(text: String) case banned(text: String)
case importedMessage(text: String) case importedMessage(text: String)

View File

@ -652,19 +652,21 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
displayUndo = false displayUndo = false
self.originalRemainingSeconds = 3 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.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 15.0))
self.iconNode = nil self.iconNode = nil
self.iconCheckNode = nil self.iconCheckNode = nil
self.animationNode = nil self.animationNode = nil
self.animatedStickerNode = 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 body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
let bold = MarkdownAttributeSet(font: Font.semibold(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) 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) self.avatarNode?.setPeer(context: context, theme: presentationData.theme, peer: peer, overrideImage: nil, emptyColor: presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: true)
if let action = action { if let action = action {