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";
"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 makeStarsTransactionScreen(context: AccountContext, transaction: StarsContext.State.Transaction, peer: EnginePeer) -> ViewController
func makeStarsReceiptScreen(context: AccountContext, receipt: BotPaymentReceipt) -> ViewController
func makeStarsSubscriptionScreen(context: AccountContext, subscription: StarsContext.State.Subscription) -> ViewController
func makeStarsSubscriptionScreen(context: AccountContext, subscription: StarsContext.State.Subscription, update: @escaping (Bool) -> Void) -> ViewController
func makeStarsSubscriptionScreen(context: AccountContext, peer: EnginePeer, pricing: StarsSubscriptionPricing, importer: PeerInvitationImportersState.Importer, usdRate: Double) -> ViewController
func makeStarsStatisticsScreen(context: AccountContext, peerId: EnginePeer.Id, revenueContext: StarsRevenueStatsContext) -> ViewController
func makeStarsAmountScreen(context: AccountContext, initialValue: Int64?, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController

View File

@ -1225,6 +1225,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
var avatarBadgeBackground: ASImageNode?
let onlineNode: PeerOnlineMarkerNode
var avatarTimerBadge: AvatarBadgeView?
private var starView: StarView?
let pinnedIconNode: ASImageNode
var secretIconNode: ASImageNode?
var verifiedIconView: ComponentHostView<Empty>?
@ -1827,6 +1828,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
if let item = self.item, case .chatList = item.index {
self.onlineNode.setImage(PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .highlighted, voiceChat: self.onlineIsVoiceChat), color: nil, transition: transition)
self.starView?.setOutlineColor(item.presentationData.theme.chatList.itemHighlightedBackgroundColor, transition: transition)
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
@ -1845,12 +1847,16 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
if let item = self.item {
let onlineIcon: UIImage?
let effectiveBackgroundColor: UIColor
if item.isPinned {
onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .pinned, voiceChat: self.onlineIsVoiceChat)
effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor
} else {
onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .regular, voiceChat: self.onlineIsVoiceChat)
effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor
}
self.onlineNode.setImage(onlineIcon, color: nil, transition: transition)
self.starView?.setOutlineColor(effectiveBackgroundColor, transition: transition)
}
}
}
@ -2934,6 +2940,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
titleIconsWidth += currentMutedIconImage.size.width
}
var isSubscription = false
var isSecret = false
if !isPeerGroup {
if case let .chatList(index) = item.index, index.messageIndex.id.peerId.namespace == Namespaces.Peer.SecretChat {
@ -2978,6 +2985,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
break
}
} else if case let .chat(itemPeer) = contentPeer, let peer = itemPeer.chatMainPeer {
if peer.isSubscription {
isSubscription = true
}
if case let .peer(peerData) = item.content, peerData.customMessageListData?.hidePeerStatus == true {
currentCredibilityIconContent = nil
} else if case .savedMessagesChats = item.chatListLocation, peer.id == item.context.account.peerId {
@ -3635,15 +3645,39 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
transition.updateSublayerTransformScale(node: strongSelf.onlineNode, scale: (1.0 - onlineInlineNavigationFraction) * 1.0 + onlineInlineNavigationFraction * 0.00001)
let onlineIcon: UIImage?
let effectiveBackgroundColor: UIColor
if strongSelf.reallyHighlighted {
onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .highlighted, voiceChat: onlineIsVoiceChat)
effectiveBackgroundColor = item.presentationData.theme.chatList.itemHighlightedBackgroundColor
} else if case let .chatList(index) = item.index, index.pinningIndex != nil {
onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .pinned, voiceChat: onlineIsVoiceChat)
effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor
} else {
onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .regular, voiceChat: onlineIsVoiceChat)
effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor
}
strongSelf.onlineNode.setImage(onlineIcon, color: item.presentationData.theme.list.itemCheckColors.foregroundColor, transition: .immediate)
if isSubscription {
let starView: StarView
if let current = strongSelf.starView {
starView = current
} else {
starView = StarView()
strongSelf.starView = starView
strongSelf.view.addSubview(starView)
// strongSelf.mainContentContainerNode.view.addSubview(starView)
}
starView.outlineColor = effectiveBackgroundColor
let starSize = CGSize(width: 20.0, height: 20.0)
let starFrame = CGRect(origin: CGPoint(x: avatarFrame.maxX - starSize.width + 1.0, y: avatarFrame.maxY - starSize.height + 1.0), size: starSize)
transition.updateFrame(view: starView, frame: starFrame)
} else if let starView = strongSelf.starView {
strongSelf.starView = nil
starView.removeFromSuperview()
}
let autoremoveTimeoutFraction: CGFloat
if online {
autoremoveTimeoutFraction = 0.0
@ -4746,3 +4780,47 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
}
private class StarView: UIView {
let outline = SimpleLayer()
let foreground = SimpleLayer()
var outlineColor: UIColor = .white {
didSet {
self.outline.layerTintColor = self.outlineColor.cgColor
}
}
override init(frame: CGRect) {
self.outline.contents = UIImage(bundleImageName: "Premium/Stars/StarMediumOutline")?.cgImage
self.foreground.contents = UIImage(bundleImageName: "Premium/Stars/StarMedium")?.cgImage
super.init(frame: frame)
self.layer.addSublayer(self.outline)
self.layer.addSublayer(self.foreground)
}
required init?(coder: NSCoder) {
preconditionFailure()
}
func setOutlineColor(_ color: UIColor, transition: ContainedViewLayoutTransition) {
if case let .animated(duration, curve) = transition, color != self.outlineColor {
let snapshotLayer = SimpleLayer()
snapshotLayer.layerTintColor = self.outlineColor.cgColor
snapshotLayer.contents = self.outline.contents
snapshotLayer.frame = self.outline.bounds
self.layer.insertSublayer(snapshotLayer, above: self.outline)
snapshotLayer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false, completion: { [weak snapshotLayer] _ in
snapshotLayer?.removeFromSuperlayer()
})
}
self.outlineColor = color
}
override func layoutSubviews() {
self.outline.frame = self.bounds
self.foreground.frame = self.bounds
}
}

View File

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

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

View File

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

View File

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

View File

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

View File

@ -198,7 +198,7 @@ public func inviteRequestsController(context: AccountContext, updatedPresentatio
} else {
string = presentationData.strings.MemberRequests_UserAddedToGroup(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string
}
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .invitedToVoiceChat(context: context, peer: peer, text: string, action: nil, duration: 3), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .invitedToVoiceChat(context: context, peer: peer, title: nil, text: string, action: nil, duration: 3), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
})
}

View File

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

View File

@ -32,6 +32,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
let count: Int32
let peers: [EnginePeer]
let displayButton: Bool
let separateButtons: Bool
let displayImporters: Bool
let buttonColor: UIColor?
public let sectionId: ItemListSectionId
@ -49,6 +50,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
count: Int32,
peers: [EnginePeer],
displayButton: Bool,
separateButtons: Bool = false,
displayImporters: Bool,
buttonColor: UIColor?,
sectionId: ItemListSectionId,
@ -65,6 +67,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
self.count = count
self.peers = peers
self.displayButton = displayButton
self.separateButtons = separateButtons
self.displayImporters = displayImporters
self.buttonColor = buttonColor
self.sectionId = sectionId
@ -126,6 +129,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
private let addressButtonNode: HighlightTrackingButtonNode
private let addressButtonIconNode: ASImageNode
private var addressShimmerNode: ShimmerEffectNode?
private var copyButtonNode: SolidRoundedButtonNode?
private var shareButtonNode: SolidRoundedButtonNode?
private let avatarsButtonNode: HighlightTrackingButtonNode
@ -234,6 +238,11 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
}
}
}
self.copyButtonNode?.pressed = { [weak self] in
if let strongSelf = self, let item = strongSelf.item {
item.copyAction?()
}
}
self.shareButtonNode?.pressed = { [weak self] in
if let strongSelf = self, let item = strongSelf.item {
item.shareAction?()
@ -444,7 +453,31 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
strongSelf.addressButtonNode.isHidden = item.contextAction == nil
strongSelf.addressButtonIconNode.isHidden = item.contextAction == nil
var effectiveSeparateButtons = item.separateButtons
if let invite = item.invite, invitationAvailability(invite).isZero {
effectiveSeparateButtons = false
}
let copyButtonNode: SolidRoundedButtonNode
if let currentCopyButtonNode = strongSelf.copyButtonNode {
copyButtonNode = currentCopyButtonNode
} else {
let buttonTheme: SolidRoundedButtonTheme
if let buttonColor = item.buttonColor {
buttonTheme = SolidRoundedButtonTheme(backgroundColor: buttonColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor)
} else {
buttonTheme = SolidRoundedButtonTheme(theme: item.presentationData.theme)
}
copyButtonNode = SolidRoundedButtonNode(theme: buttonTheme, height: 50.0, cornerRadius: 11.0)
copyButtonNode.title = item.presentationData.strings.InviteLink_CopyShort
copyButtonNode.pressed = { [weak self] in
self?.item?.copyAction?()
}
strongSelf.addSubnode(copyButtonNode)
strongSelf.copyButtonNode = copyButtonNode
}
let shareButtonNode: SolidRoundedButtonNode
if let currentShareButtonNode = strongSelf.shareButtonNode {
shareButtonNode = currentShareButtonNode
@ -459,7 +492,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
if let invite = item.invite, invitationAvailability(invite).isZero {
shareButtonNode.title = item.presentationData.strings.InviteLink_ReactivateLink
} else {
shareButtonNode.title = item.presentationData.strings.InviteLink_Share
shareButtonNode.title = effectiveSeparateButtons ? item.presentationData.strings.InviteLink_ShareShort : item.presentationData.strings.InviteLink_Share
}
shareButtonNode.pressed = { [weak self] in
self?.item?.shareAction?()
@ -468,9 +501,19 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
strongSelf.shareButtonNode = shareButtonNode
}
let buttonWidth = contentSize.width - leftInset - rightInset
let buttonSpacing: CGFloat = 8.0
var buttonWidth = contentSize.width - leftInset - rightInset
var shareButtonOriginX = leftInset
if effectiveSeparateButtons {
buttonWidth = (buttonWidth - buttonSpacing) / 2.0
shareButtonOriginX = leftInset + buttonWidth + buttonSpacing
}
let _ = copyButtonNode.updateLayout(width: buttonWidth, transition: .immediate)
copyButtonNode.frame = CGRect(x: leftInset, y: verticalInset + fieldHeight + fieldSpacing, width: buttonWidth, height: buttonHeight)
let _ = shareButtonNode.updateLayout(width: buttonWidth, transition: .immediate)
shareButtonNode.frame = CGRect(x: leftInset, y: verticalInset + fieldHeight + fieldSpacing, width: buttonWidth, height: buttonHeight)
shareButtonNode.frame = CGRect(x: shareButtonOriginX, y: verticalInset + fieldHeight + fieldSpacing, width: buttonWidth, height: buttonHeight)
var totalWidth = invitedPeersLayout.size.width
var leftOrigin: CGFloat = floorToScreenPixels((params.width - invitedPeersLayout.size.width) / 2.0)
@ -498,9 +541,15 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
strongSelf.fieldButtonNode.isUserInteractionEnabled = item.invite != nil
strongSelf.addressButtonIconNode.alpha = item.invite != nil ? 1.0 : 0.0
strongSelf.copyButtonNode?.isUserInteractionEnabled = item.invite != nil
strongSelf.copyButtonNode?.alpha = item.invite != nil ? 1.0 : 0.4
strongSelf.copyButtonNode?.isHidden = !item.displayButton || !effectiveSeparateButtons
strongSelf.shareButtonNode?.isUserInteractionEnabled = item.invite != nil
strongSelf.shareButtonNode?.alpha = item.invite != nil ? 1.0 : 0.4
strongSelf.shareButtonNode?.isHidden = !item.displayButton
strongSelf.avatarsButtonNode.isHidden = !item.displayImporters
strongSelf.avatarsNode.isHidden = !item.displayImporters || item.invite == nil
strongSelf.invitedPeersNode.isHidden = !item.displayImporters || item.invite == nil

View File

@ -251,6 +251,7 @@ public enum ItemListPeerItemLabel {
case text(String, ItemListPeerItemLabelFont)
case disclosure(String)
case badge(String)
case attributedText(NSAttributedString)
}
public struct ItemListPeerItemSwitch {
@ -728,7 +729,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
private var avatarButton: HighlightTrackingButton?
private let titleNode: TextNode
private let labelNode: TextNode
private let labelNode: TextNodeWithEntities
private let labelBadgeNode: ASImageNode
private var labelArrowNode: ASImageNode?
private let statusNode: TextNode
@ -829,10 +830,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
self.statusNode.contentMode = .left
self.statusNode.contentsScale = UIScreen.main.scale
self.labelNode = TextNode()
self.labelNode.isUserInteractionEnabled = false
self.labelNode.contentMode = .left
self.labelNode.contentsScale = UIScreen.main.scale
self.labelNode = TextNodeWithEntities()
self.labelBadgeNode = ASImageNode()
self.labelBadgeNode.displayWithoutProcessing = true
@ -850,7 +848,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
self.containerNode.addSubnode(self.avatarNode)
self.containerNode.addSubnode(self.titleNode)
self.containerNode.addSubnode(self.statusNode)
self.containerNode.addSubnode(self.labelNode)
self.containerNode.addSubnode(self.labelNode.textNode)
self.peerPresenceManager = PeerPresenceStatusManager(update: { [weak self] in
if let strongSelf = self, let layoutParams = strongSelf.layoutParams {
@ -885,7 +883,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
public func asyncLayout() -> (_ item: ItemListPeerItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors, _ headerAtTop: Bool) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeStatusLayout = TextNode.asyncLayout(self.statusNode)
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let makeLabelLayout = TextNodeWithEntities.asyncLayout(self.labelNode)
let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode)
let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode)
@ -1156,42 +1154,49 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
editingOffset = 0.0
}
var labelMaximumNumberOfLines = 1
var labelInset: CGFloat = 0.0
var labelAlignment: NSTextAlignment = .natural
var updatedLabelArrowNode: ASImageNode?
switch item.label {
case .none:
break
case let .text(text, font):
let selectedFont: UIFont
switch font {
case .standard:
selectedFont = labelFont
case let .custom(value):
selectedFont = value
}
labelAttributedString = NSAttributedString(string: text, font: selectedFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
labelInset += 15.0
case let .disclosure(text):
if let currentLabelArrowNode = currentLabelArrowNode {
updatedLabelArrowNode = currentLabelArrowNode
} else {
let arrowNode = ASImageNode()
arrowNode.isLayerBacked = true
arrowNode.displayWithoutProcessing = true
arrowNode.displaysAsynchronously = false
arrowNode.image = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme)
updatedLabelArrowNode = arrowNode
}
labelInset += 40.0
labelAttributedString = NSAttributedString(string: text, font: labelDisclosureFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
case let .badge(text):
labelAttributedString = NSAttributedString(string: text, font: badgeFont, textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor)
labelInset += 15.0
case .none:
break
case let .attributedText(text):
labelAttributedString = text
labelInset += 15.0
labelMaximumNumberOfLines = 2
labelAlignment = .right
case let .text(text, font):
let selectedFont: UIFont
switch font {
case .standard:
selectedFont = labelFont
case let .custom(value):
selectedFont = value
}
labelAttributedString = NSAttributedString(string: text, font: selectedFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
labelInset += 15.0
case let .disclosure(text):
if let currentLabelArrowNode = currentLabelArrowNode {
updatedLabelArrowNode = currentLabelArrowNode
} else {
let arrowNode = ASImageNode()
arrowNode.isLayerBacked = true
arrowNode.displayWithoutProcessing = true
arrowNode.displaysAsynchronously = false
arrowNode.image = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme)
updatedLabelArrowNode = arrowNode
}
labelInset += 40.0
labelAttributedString = NSAttributedString(string: text, font: labelDisclosureFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
case let .badge(text):
labelAttributedString = NSAttributedString(string: text, font: badgeFont, textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor)
labelInset += 15.0
}
labelInset += reorderInset
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: labelAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 16.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: labelAttributedString, backgroundColor: nil, maximumNumberOfLines: labelMaximumNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 16.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: labelAlignment, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let constrainedTitleSize = CGSize(width: params.width - leftInset - 12.0 - editingOffset - rightInset - labelLayout.size.width - labelInset - titleIconsWidth, height: CGFloat.greatestFiniteMagnitude)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: constrainedTitleSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
@ -1351,9 +1356,10 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
let _ = titleApply()
let _ = statusApply()
let _ = labelApply()
strongSelf.labelNode.isHidden = labelAttributedString == nil
if case let .account(context) = item.context {
let _ = labelApply(TextNodeWithEntities.Arguments(context: context, cache: item.context.animationCache, renderer: item.context.animationRenderer, placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, attemptSynchronous: false))
}
strongSelf.labelNode.textNode.isHidden = labelAttributedString == nil
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
@ -1496,15 +1502,15 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
let labelFrame: CGRect
if case .badge = item.label {
labelFrame = CGRect(origin: CGPoint(x: revealOffset + params.width - rightLabelInset - badgeWidth + (badgeWidth - labelLayout.size.width) / 2.0, y: floor((contentSize.height - labelLayout.size.height) / 2.0) + 1.0), size: labelLayout.size)
strongSelf.labelNode.frame = labelFrame
strongSelf.labelNode.textNode.frame = labelFrame
} else {
labelFrame = CGRect(origin: CGPoint(x: revealOffset + params.width - labelLayout.size.width - rightLabelInset, y: floor((contentSize.height - labelLayout.size.height) / 2.0) + 1.0), size: labelLayout.size)
transition.updateFrame(node: strongSelf.labelNode, frame: labelFrame)
transition.updateFrame(node: strongSelf.labelNode.textNode, frame: labelFrame)
}
if let updateBadgeImage = updatedLabelBadgeImage {
if strongSelf.labelBadgeNode.supernode == nil {
strongSelf.containerNode.insertSubnode(strongSelf.labelBadgeNode, belowSubnode: strongSelf.labelNode)
strongSelf.containerNode.insertSubnode(strongSelf.labelBadgeNode, belowSubnode: strongSelf.labelNode.textNode)
}
strongSelf.labelBadgeNode.image = updateBadgeImage
}
@ -1853,16 +1859,16 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
}
let badgeDiameter: CGFloat = 20.0
let labelSize = self.labelNode.frame.size
let labelSize = self.labelNode.textNode.frame.size
let badgeWidth = max(badgeDiameter, labelSize.width + 10.0)
let labelFrame: CGRect
if case .badge = item.label {
labelFrame = CGRect(origin: CGPoint(x: offset + params.width - rightLabelInset - badgeWidth + (badgeWidth - labelSize.width) / 2.0, y: self.labelNode.frame.minY), size: labelSize)
labelFrame = CGRect(origin: CGPoint(x: offset + params.width - rightLabelInset - badgeWidth + (badgeWidth - labelSize.width) / 2.0, y: self.labelNode.textNode.frame.minY), size: labelSize)
} else {
labelFrame = CGRect(origin: CGPoint(x: offset + params.width - self.labelNode.bounds.size.width - rightLabelInset, y: self.labelNode.frame.minY), size: self.labelNode.bounds.size)
labelFrame = CGRect(origin: CGPoint(x: offset + params.width - self.labelNode.textNode.bounds.size.width - rightLabelInset, y: self.labelNode.textNode.frame.minY), size: self.labelNode.textNode.bounds.size)
}
transition.updateFrame(node: self.labelNode, frame: labelFrame)
transition.updateFrame(node: self.labelNode.textNode, frame: labelFrame)
transition.updateFrame(node: self.labelBadgeNode, frame: CGRect(origin: CGPoint(x: offset + params.width - rightLabelInset - badgeWidth, y: self.labelBadgeNode.frame.minY), size: CGSize(width: badgeWidth, height: badgeDiameter)))

View File

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

View File

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

View File

@ -1254,7 +1254,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController
} else {
text = strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string
}
strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: EnginePeer(participant.peer), text: text, action: nil, duration: 3), action: { _ in return false })
strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: EnginePeer(participant.peer), title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
}
} else {
if case let .channel(groupPeer) = groupPeer, let listenerLink = inviteLinks?.listenerLink, !groupPeer.hasPermission(.inviteMembers) {
@ -1362,7 +1362,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController
} else {
text = strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string
}
strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: text, action: nil, duration: 3), action: { _ in return false })
strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
}
}))
} else if case let .legacyGroup(groupPeer) = groupPeer {
@ -1430,7 +1430,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController
} else {
text = strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string
}
strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: text, action: nil, duration: 3), action: { _ in return false })
strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
}
}))
}
@ -2262,7 +2262,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController
return
}
let text = strongSelf.presentationData.strings.VoiceChat_PeerJoinedText(event.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string
strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: event.peer, text: text, action: nil, duration: 3), action: { _ in return false })
strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: event.peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
}
}))
@ -2277,7 +2277,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController
} else {
text = strongSelf.presentationData.strings.VoiceChat_DisplayAsSuccess(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string
}
strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: text, action: nil, duration: 3), action: { _ in return false })
strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
}))
self.stateVersionDisposable.set((self.call.stateVersion

View File

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

View File

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

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

View File

@ -905,7 +905,7 @@ private let starImage: UIImage? = {
context.clear(CGRect(origin: .zero, size: size))
if let image = UIImage(bundleImageName: "Premium/Stars/StarLarge"), let cgImage = image.cgImage {
context.draw(cgImage, in: CGRect(origin: .zero, size: size).insetBy(dx: 4.0, dy: 4.0), byTiling: false)
context.draw(cgImage, in: CGRect(origin: .zero, size: size).insetBy(dx: 1.0, dy: 1.0), byTiling: false)
}
})?.withRenderingMode(.alwaysTemplate)
}()

View File

@ -9232,7 +9232,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
case .fallback:
(strongSelf.controller?.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: nil, text: strongSelf.presentationData.strings.Privacy_ProfilePhoto_PublicPhotoSuccess, round: true, undoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
case .custom:
strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_SuccessPhotoText(peer.compactDisplayTitle).string, action: nil, duration: 5), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, title: nil, text: strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_SuccessPhotoText(peer.compactDisplayTitle).string, action: nil, duration: 5), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
let _ = (strongSelf.context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, peerId: strongSelf.peerId, fetch: peerInfoProfilePhotos(context: strongSelf.context, peerId: strongSelf.peerId)) |> ignoreValues).startStandalone()
case .suggest:
@ -9469,7 +9469,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
case .fallback:
(strongSelf.controller?.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: nil, text: strongSelf.presentationData.strings.Privacy_ProfilePhoto_PublicVideoSuccess, round: true, undoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
case .custom:
strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_SuccessVideoText(peer.compactDisplayTitle).string, action: nil, duration: 5), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, title: nil, text: strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_SuccessVideoText(peer.compactDisplayTitle).string, action: nil, duration: 5), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
let _ = (strongSelf.context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, peerId: strongSelf.peerId, fetch: peerInfoProfilePhotos(context: strongSelf.context, peerId: strongSelf.peerId)) |> ignoreValues).startStandalone()
case .suggest:

View File

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

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

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

View File

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

View File

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

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 {
HapticFeedback().impact()
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.Conversation_SendMesageAsPremiumInfo, action: strongSelf.presentationData.strings.EmojiInput_PremiumEmojiToast_Action, duration: 3), elevatedLayout: false, action: { [weak self] action in
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, title: nil, text: strongSelf.presentationData.strings.Conversation_SendMesageAsPremiumInfo, action: strongSelf.presentationData.strings.EmojiInput_PremiumEmojiToast_Action, duration: 3), elevatedLayout: false, action: { [weak self] action in
guard let strongSelf = self else {
return true
}

View File

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

View File

@ -2750,8 +2750,12 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return StarsTransactionScreen(context: context, subject: .receipt(receipt))
}
public func makeStarsSubscriptionScreen(context: AccountContext, subscription: StarsContext.State.Subscription) -> ViewController {
return StarsTransactionScreen(context: context, subject: .subscription(subscription))
public func makeStarsSubscriptionScreen(context: AccountContext, subscription: StarsContext.State.Subscription, update: @escaping (Bool) -> Void) -> ViewController {
return StarsTransactionScreen(context: context, subject: .subscription(subscription), updateSubscription: update)
}
public func makeStarsSubscriptionScreen(context: AccountContext, peer: EnginePeer, pricing: StarsSubscriptionPricing, importer: PeerInvitationImportersState.Importer, usdRate: Double) -> ViewController {
return StarsTransactionScreen(context: context, subject: .importer(peer, pricing, importer, usdRate))
}
public func makeStarsStatisticsScreen(context: AccountContext, peerId: EnginePeer.Id, revenueContext: StarsRevenueStatsContext) -> ViewController {

View File

@ -22,7 +22,7 @@ public enum UndoOverlayContent {
case chatRemovedFromFolder(chatTitle: String, folderTitle: String)
case messagesUnpinned(title: String, text: String, undo: Bool, isHidden: Bool)
case setProximityAlert(title: String, text: String, cancelled: Bool)
case invitedToVoiceChat(context: AccountContext, peer: EnginePeer, text: String, action: String?, duration: Double)
case invitedToVoiceChat(context: AccountContext, peer: EnginePeer, title: String?, text: String, action: String?, duration: Double)
case linkCopied(text: String)
case banned(text: String)
case importedMessage(text: String)

View File

@ -652,19 +652,21 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
displayUndo = false
self.originalRemainingSeconds = 3
case let .invitedToVoiceChat(context, peer, text, action, duration):
case let .invitedToVoiceChat(context, peer, title, text, action, duration):
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 15.0))
self.iconNode = nil
self.iconCheckNode = nil
self.animationNode = nil
self.animatedStickerNode = nil
self.titleNode.attributedText = NSAttributedString(string: title ?? "", font: Font.semibold(14.0), textColor: .white)
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
let link = MarkdownAttributeSet(font: Font.regular(14.0), textColor: undoTextColor)
let link = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: undoTextColor)
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return nil }), textAlignment: .natural)
self.textNode.attributedText = attributedText
self.textNode.attributedText = attributedText
self.avatarNode?.setPeer(context: context, theme: presentationData.theme, peer: peer, overrideImage: nil, emptyColor: presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: true)
if let action = action {