From bc454cfa93911366710876d38429866c64452313 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 1 Aug 2024 09:53:28 +0200 Subject: [PATCH] Stars subscriptions --- .../Sources/Node/ChatListNode.swift | 4 + .../Sources/Node/ChatListNodeEntries.swift | 1 + .../Sources/Node/ChatListNoticeItem.swift | 4 + submodules/InviteLinksUI/BUILD | 1 + .../Sources/InviteLinkEditController.swift | 247 +++++++++++++----- .../Sources/InviteLinkListController.swift | 4 +- .../Sources/InviteLinkViewController.swift | 98 ++++++- .../Sources/ItemListInviteLinkItem.swift | 49 +++- .../Items/ItemListDisclosureItem.swift | 49 +++- .../Items/ItemListSingleLineInputItem.swift | 44 +++- .../Sources/ResetPasswordController.swift | 6 +- .../ChangePhoneNumberCodeController.swift | 6 +- .../ProxyServerSettingsController.swift | 8 +- .../CreatePasswordController.swift | 6 +- .../TwoStepVerificationUnlockController.swift | 6 +- .../Sources/State/Serialization.swift | 2 +- .../TelegramEngine/Payments/Stars.swift | 16 +- .../Sources/StarsTransactionScreen.swift | 22 +- .../StarsTransactionsListPanelComponent.swift | 3 + .../Sources/StarsTransactionsScreen.swift | 41 ++- .../Sources/StarsWithdrawalScreen.swift | 10 +- .../TelegramUI/Sources/OpenResolvedUrl.swift | 56 +++- 22 files changed, 545 insertions(+), 138 deletions(-) diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 628025c9fc..e16451c4c9 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -747,6 +747,8 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL nodeInteraction?.openPremiumGift(birthdays) case .reviewLogin: break + case .starsSubscriptionLowBalance: + break } case .hide: nodeInteraction?.dismissNotice(notice) @@ -1085,6 +1087,8 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL nodeInteraction?.openPremiumGift(birthdays) case .reviewLogin: break + case .starsSubscriptionLowBalance: + break } case .hide: nodeInteraction?.dismissNotice(notice) diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift index 6ed8117ebf..953ba56328 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift @@ -90,6 +90,7 @@ public enum ChatListNotice: Equatable { case birthdayPremiumGift(peers: [EnginePeer], birthdays: [EnginePeer.Id: TelegramBirthday]) case reviewLogin(newSessionReview: NewSessionReview, totalCount: Int) case premiumGrace + case starsSubscriptionLowBalance } enum ChatListNodeEntry: Comparable, Identifiable { diff --git a/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift b/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift index b3f88f9b8c..cb7ca09b4e 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift @@ -262,6 +262,10 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode { okButtonLayout = makeOkButtonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.ChatList_SessionReview_PanelConfirm, font: titleFont, textColor: item.theme.list.itemAccentColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0))) cancelButtonLayout = makeCancelButtonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.ChatList_SessionReview_PanelReject, font: titleFont, textColor: item.theme.list.itemDestructiveColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0))) + case .starsSubscriptionLowBalance: + let titleStringValue = NSMutableAttributedString(attributedString: NSAttributedString(string: "5 Stars needed for Astro Paws", font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor)) + titleString = titleStringValue + textString = NSAttributedString(string: "Insufficient funds to cover your subscription.", font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) } var leftInset: CGFloat = sideInset diff --git a/submodules/InviteLinksUI/BUILD b/submodules/InviteLinksUI/BUILD index 1e3b637e4d..37f6192719 100644 --- a/submodules/InviteLinksUI/BUILD +++ b/submodules/InviteLinksUI/BUILD @@ -59,6 +59,7 @@ swift_library( "//submodules/QrCodeUI:QrCodeUI", "//submodules/PromptUI", "//submodules/TelegramUI/Components/ItemListDatePickerItem:ItemListDatePickerItem", + "//submodules/TelegramUI/Components/TextNodeWithEntities", ], visibility = [ "//visibility:public", diff --git a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift index fe5c4a8123..d988513a91 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift @@ -17,6 +17,7 @@ import ContextUI import TelegramStringFormatting import UndoUI import ItemListDatePickerItem +import TextFormat private final class InviteLinkEditControllerArguments { let context: AccountContext @@ -36,6 +37,7 @@ private final class InviteLinkEditControllerArguments { private enum InviteLinksEditSection: Int32 { case title + case subscriptionFee case requestApproval case time case usage @@ -75,18 +77,23 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { case title(PresentationTheme, String, String) case titleInfo(PresentationTheme, String) - case requestApproval(PresentationTheme, String, Bool) + + case subscriptionFeeToggle(PresentationTheme, String, Bool, Bool) + case subscriptionFee(PresentationTheme, String, Bool, Int64?) + case subscriptionFeeInfo(PresentationTheme, String) + + case requestApproval(PresentationTheme, String, Bool, Bool) case requestApprovalInfo(PresentationTheme, String) case timeHeader(PresentationTheme, String) - case timePicker(PresentationTheme, InviteLinkTimeLimit) - case timeExpiryDate(PresentationTheme, PresentationDateTimeFormat, Int32?, Bool) - case timeCustomPicker(PresentationTheme, PresentationDateTimeFormat, Int32?, Bool, Bool) + case timePicker(PresentationTheme, InviteLinkTimeLimit, Bool) + case timeExpiryDate(PresentationTheme, PresentationDateTimeFormat, Int32?, Bool, Bool) + case timeCustomPicker(PresentationTheme, PresentationDateTimeFormat, Int32?, Bool, Bool, Bool) case timeInfo(PresentationTheme, String) case usageHeader(PresentationTheme, String) - case usagePicker(PresentationTheme, PresentationDateTimeFormat, InviteLinkUsageLimit) - case usageCustomPicker(PresentationTheme, Int32?, Bool, Bool) + case usagePicker(PresentationTheme, PresentationDateTimeFormat, InviteLinkUsageLimit, Bool) + case usageCustomPicker(PresentationTheme, Int32?, Bool, Bool, Bool) case usageInfo(PresentationTheme, String) case revoke(PresentationTheme, String) @@ -95,6 +102,8 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { switch self { case .titleHeader, .title, .titleInfo: return InviteLinksEditSection.title.rawValue + case .subscriptionFeeToggle, .subscriptionFee, .subscriptionFeeInfo: + return InviteLinksEditSection.subscriptionFee.rawValue case .requestApproval, .requestApprovalInfo: return InviteLinksEditSection.requestApproval.rawValue case .timeHeader, .timePicker, .timeExpiryDate, .timeCustomPicker, .timeInfo: @@ -114,30 +123,36 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { return 1 case .titleInfo: return 2 - case .requestApproval: + case .subscriptionFeeToggle: return 3 - case .requestApprovalInfo: + case .subscriptionFee: return 4 - case .timeHeader: + case .subscriptionFeeInfo: return 5 - case .timePicker: + case .requestApproval: return 6 - case .timeExpiryDate: + case .requestApprovalInfo: return 7 - case .timeCustomPicker: + case .timeHeader: return 8 - case .timeInfo: + case .timePicker: return 9 - case .usageHeader: + case .timeExpiryDate: return 10 - case .usagePicker: + case .timeCustomPicker: return 11 - case .usageCustomPicker: + case .timeInfo: return 12 - case .usageInfo: + case .usageHeader: return 13 - case .revoke: + case .usagePicker: return 14 + case .usageCustomPicker: + return 15 + case .usageInfo: + return 16 + case .revoke: + return 17 } } @@ -161,8 +176,26 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { } else { return false } - case let .requestApproval(lhsTheme, lhsText, lhsValue): - if case let .requestApproval(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + case let .subscriptionFeeToggle(lhsTheme, lhsText, lhsValue, lhsEnabled): + if case let .subscriptionFeeToggle(rhsTheme, rhsText, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled { + return true + } else { + return false + } + case let .subscriptionFee(lhsTheme, lhsText, lhsValue, lhsEnabled): + if case let .subscriptionFee(rhsTheme, rhsText, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled { + return true + } else { + return false + } + case let .subscriptionFeeInfo(lhsTheme, lhsText): + if case let .subscriptionFeeInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .requestApproval(lhsTheme, lhsText, lhsValue, lhsEnabled): + if case let .requestApproval(rhsTheme, rhsText, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled { return true } else { return false @@ -179,20 +212,20 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { } else { return false } - case let .timePicker(lhsTheme, lhsValue): - if case let .timePicker(rhsTheme, rhsValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue { + case let .timePicker(lhsTheme, lhsValue, lhsEnabled): + if case let .timePicker(rhsTheme, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue, lhsEnabled == rhsEnabled { return true } else { return false } - case let .timeExpiryDate(lhsTheme, lhsDateTimeFormat, lhsDate, lhsActive): - if case let .timeExpiryDate(rhsTheme, rhsDateTimeFormat, rhsDate, rhsActive) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsDate == rhsDate, lhsActive == rhsActive { + case let .timeExpiryDate(lhsTheme, lhsDateTimeFormat, lhsDate, lhsActive, lhsEnabled): + if case let .timeExpiryDate(rhsTheme, rhsDateTimeFormat, rhsDate, rhsActive, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsDate == rhsDate, lhsActive == rhsActive, lhsEnabled == rhsEnabled { return true } else { return false } - case let .timeCustomPicker(lhsTheme, lhsDateTimeFormat, lhsDate, lhsDisplayingDateSelection, lhsDisplayingTimeSelection): - if case let .timeCustomPicker(rhsTheme, rhsDateTimeFormat, rhsDate, rhsDisplayingDateSelection, rhsDisplayingTimeSelection) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsDate == rhsDate, lhsDisplayingDateSelection == rhsDisplayingDateSelection, lhsDisplayingTimeSelection == rhsDisplayingTimeSelection { + case let .timeCustomPicker(lhsTheme, lhsDateTimeFormat, lhsDate, lhsDisplayingDateSelection, lhsDisplayingTimeSelection, lhsEnabled): + if case let .timeCustomPicker(rhsTheme, rhsDateTimeFormat, rhsDate, rhsDisplayingDateSelection, rhsDisplayingTimeSelection, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsDate == rhsDate, lhsDisplayingDateSelection == rhsDisplayingDateSelection, lhsDisplayingTimeSelection == rhsDisplayingTimeSelection, lhsEnabled == rhsEnabled { return true } else { return false @@ -209,14 +242,14 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { } else { return false } - case let .usagePicker(lhsTheme, lhsDateTimeFormat, lhsValue): - if case let .usagePicker(rhsTheme, rhsDateTimeFormat, rhsValue) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsValue == rhsValue { + case let .usagePicker(lhsTheme, lhsDateTimeFormat, lhsValue, lhsEnabled): + if case let .usagePicker(rhsTheme, rhsDateTimeFormat, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsValue == rhsValue, lhsEnabled == rhsEnabled { return true } else { return false } - case let .usageCustomPicker(lhsTheme, lhsValue, lhsFocused, lhsCustomValue): - if case let .usageCustomPicker(rhsTheme, rhsValue, rhsFocused, rhsCustomValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue, lhsFocused == rhsFocused, lhsCustomValue == rhsCustomValue { + case let .usageCustomPicker(lhsTheme, lhsValue, lhsFocused, lhsCustomValue, lhsEnabled): + if case let .usageCustomPicker(rhsTheme, rhsValue, rhsFocused, rhsCustomValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue, lhsFocused == rhsFocused, lhsCustomValue == rhsCustomValue, lhsEnabled == rhsEnabled { return true } else { return false @@ -246,7 +279,7 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { case let .titleHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .title(_, placeholder, value): - return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: value, placeholder: placeholder, maxLength: 32, sectionId: self.section, textUpdated: { value in + return ItemListSingleLineInputItem(context: arguments.context, presentationData: presentationData, title: NSAttributedString(), text: value, placeholder: placeholder, maxLength: 32, sectionId: self.section, textUpdated: { value in arguments.updateState { state in var updatedState = state updatedState.title = value @@ -255,8 +288,41 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { }, action: {}) case let .titleInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) - case let .requestApproval(_, text, value): - return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in + + case let .subscriptionFeeToggle(_, text, value, enabled): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in + arguments.updateState { state in + var updatedState = state + updatedState.subscriptionEnabled = value + if value { + updatedState.requestApproval = false + } else { + updatedState.subscriptionFee = nil + } + return updatedState + } + }) + case let .subscriptionFee(_, placeholder, enabled, value): + let title = NSMutableAttributedString(string: "⭐️", font: Font.semibold(18.0), textColor: .white) + if let range = title.string.range(of: "⭐️") { + title.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: title.string)) + title.addAttribute(.baselineOffset, value: -1.0, range: NSRange(range, in: title.string)) + } + return ItemListSingleLineInputItem(context: arguments.context, presentationData: presentationData, title: title, text: value.flatMap { "\($0)" } ?? "", placeholder: placeholder, type: .number, spacing: 3.0, enabled: enabled, sectionId: self.section, textUpdated: { text in + arguments.updateState { state in + var updatedState = state + if let value = Int64(text) { + updatedState.subscriptionFee = value + } else { + updatedState.subscriptionFee = nil + } + return updatedState + } + }, action: {}) + case let .subscriptionFeeInfo(_, text): + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) + case let .requestApproval(_, text, value, enabled): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in arguments.updateState { state in var updatedState = state updatedState.requestApproval = value @@ -267,8 +333,8 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .timeHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .timePicker(_, value): - return ItemListInviteLinkTimeLimitItem(theme: presentationData.theme, strings: presentationData.strings, value: value, enabled: true, sectionId: self.section, updated: { value in + case let .timePicker(_, value, enabled): + return ItemListInviteLinkTimeLimitItem(theme: presentationData.theme, strings: presentationData.strings, value: value, enabled: enabled, sectionId: self.section, updated: { value in arguments.updateState({ state in var updatedState = state if value != updatedState.time { @@ -279,14 +345,14 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { return updatedState }) }) - case let .timeExpiryDate(theme, dateTimeFormat, value, active): + case let .timeExpiryDate(theme, dateTimeFormat, value, active, enabled): let text: String if let value = value { text = stringForMediumDate(timestamp: value, strings: presentationData.strings, dateTimeFormat: dateTimeFormat) } else { text = presentationData.strings.InviteLink_Create_TimeLimitExpiryDateNever } - return ItemListDisclosureItem(presentationData: presentationData, title: presentationData.strings.InviteLink_Create_TimeLimitExpiryDate, label: text, labelStyle: active ? .coloredText(theme.list.itemAccentColor) : .text, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: presentationData.strings.InviteLink_Create_TimeLimitExpiryDate, enabled: enabled, label: text, labelStyle: active ? .coloredText(theme.list.itemAccentColor) : .text, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: { arguments.dismissInput() arguments.updateState { state in var updatedState = state @@ -298,7 +364,8 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { return updatedState } }) - case let .timeCustomPicker(_, dateTimeFormat, date, displayingDateSelection, displayingTimeSelection): + case let .timeCustomPicker(_, dateTimeFormat, date, displayingDateSelection, displayingTimeSelection, enabled): + let _ = enabled let title = presentationData.strings.InviteLink_Create_TimeLimitExpiryTime return ItemListDatePickerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, date: date, title: title, displayingDateSelection: displayingDateSelection, displayingTimeSelection: displayingTimeSelection, sectionId: self.section, style: .blocks, toggleDateSelection: { arguments.updateState({ state in @@ -329,8 +396,8 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .usageHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .usagePicker(_, dateTimeFormat, value): - return ItemListInviteLinkUsageLimitItem(theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: dateTimeFormat, value: value, enabled: true, sectionId: self.section, updated: { value in + case let .usagePicker(_, dateTimeFormat, value, enabled): + return ItemListInviteLinkUsageLimitItem(theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: dateTimeFormat, value: value, enabled: enabled, sectionId: self.section, updated: { value in arguments.dismissInput() arguments.updateState({ state in var updatedState = state @@ -342,14 +409,14 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { return updatedState }) }) - case let .usageCustomPicker(theme, value, focused, customValue): + case let .usageCustomPicker(theme, value, focused, customValue, enabled): let text: String if let value = value, value != 0 { text = String(value) } else { text = focused ? "" : presentationData.strings.InviteLink_Create_UsersLimitNumberOfUsersUnlimited } - return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: presentationData.strings.InviteLink_Create_UsersLimitNumberOfUsers, textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: "", type: .number, alignment: .right, selectAllOnFocus: true, secondaryStyle: !customValue, tag: InviteLinksEditEntryTag.usage, sectionId: self.section, textUpdated: { updatedText in + return ItemListSingleLineInputItem(context: arguments.context, presentationData: presentationData, title: NSAttributedString(string: presentationData.strings.InviteLink_Create_UsersLimitNumberOfUsers, textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: "", type: .number, alignment: .right, enabled: enabled, selectAllOnFocus: true, secondaryStyle: !customValue, tag: InviteLinksEditEntryTag.usage, sectionId: self.section, textUpdated: { updatedText in arguments.updateState { state in var updatedState = state if updatedText.isEmpty { @@ -398,19 +465,40 @@ private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: entries.append(.title(presentationData.theme, presentationData.strings.InviteLink_Create_LinkName, state.title)) entries.append(.titleInfo(presentationData.theme, presentationData.strings.InviteLink_Create_LinkNameInfo)) - if !isPublic { - entries.append(.requestApproval(presentationData.theme, presentationData.strings.InviteLink_Create_RequestApproval, state.requestApproval)) - var requestApprovalInfoText = presentationData.strings.InviteLink_Create_RequestApprovalOffInfoChannel - if state.requestApproval { - requestApprovalInfoText = isGroup ? presentationData.strings.InviteLink_Create_RequestApprovalOnInfoGroup : presentationData.strings.InviteLink_Create_RequestApprovalOnInfoChannel + let isEditingEnabled = invite?.pricing == nil + let isSubscription = state.subscriptionEnabled + if !isGroup { + //TODO:localize + entries.append(.subscriptionFeeToggle(presentationData.theme, "Require Monthly Fee", state.subscriptionEnabled, isEditingEnabled)) + if state.subscriptionEnabled { + entries.append(.subscriptionFee(presentationData.theme, "Stars amount per month", isEditingEnabled, state.subscriptionFee)) + } + let infoText: String + if let _ = invite, state.subscriptionEnabled { + infoText = "If you need to change the subscription fee, create a new invite link with a different price." } else { - requestApprovalInfoText = isGroup ? presentationData.strings.InviteLink_Create_RequestApprovalOnInfoGroup : presentationData.strings.InviteLink_Create_RequestApprovalOffInfoChannel + infoText = "Charge a subscription fee from people joining your channel via this link. [Learn More >]()" + } + entries.append(.subscriptionFeeInfo(presentationData.theme, infoText)) + } + + if !isPublic { + entries.append(.requestApproval(presentationData.theme, presentationData.strings.InviteLink_Create_RequestApproval, state.requestApproval, isEditingEnabled && !isSubscription)) + var requestApprovalInfoText = presentationData.strings.InviteLink_Create_RequestApprovalOffInfoChannel + if isSubscription { + requestApprovalInfoText = "You can't enable admin approval for links that require a monthly fee." + } else { + if state.requestApproval { + requestApprovalInfoText = isGroup ? presentationData.strings.InviteLink_Create_RequestApprovalOnInfoGroup : presentationData.strings.InviteLink_Create_RequestApprovalOnInfoChannel + } else { + requestApprovalInfoText = isGroup ? presentationData.strings.InviteLink_Create_RequestApprovalOnInfoGroup : presentationData.strings.InviteLink_Create_RequestApprovalOffInfoChannel + } } entries.append(.requestApprovalInfo(presentationData.theme, requestApprovalInfoText)) } entries.append(.timeHeader(presentationData.theme, presentationData.strings.InviteLink_Create_TimeLimit.uppercased())) - entries.append(.timePicker(presentationData.theme, state.time)) + entries.append(.timePicker(presentationData.theme, state.time, isEditingEnabled)) let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) var time: Int32? @@ -419,21 +507,21 @@ private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: } else if let value = state.time.value { time = currentTime + value } - entries.append(.timeExpiryDate(presentationData.theme, presentationData.dateTimeFormat, time, state.pickingExpiryDate || state.pickingExpiryTime)) + entries.append(.timeExpiryDate(presentationData.theme, presentationData.dateTimeFormat, time, state.pickingExpiryDate || state.pickingExpiryTime, isEditingEnabled)) if state.pickingExpiryDate || state.pickingExpiryTime { - entries.append(.timeCustomPicker(presentationData.theme, presentationData.dateTimeFormat, time, state.pickingExpiryDate, state.pickingExpiryTime)) + entries.append(.timeCustomPicker(presentationData.theme, presentationData.dateTimeFormat, time, state.pickingExpiryDate, state.pickingExpiryTime, isEditingEnabled)) } entries.append(.timeInfo(presentationData.theme, presentationData.strings.InviteLink_Create_TimeLimitInfo)) if !state.requestApproval || isPublic { entries.append(.usageHeader(presentationData.theme, presentationData.strings.InviteLink_Create_UsersLimit.uppercased())) - entries.append(.usagePicker(presentationData.theme, presentationData.dateTimeFormat, state.usage)) + entries.append(.usagePicker(presentationData.theme, presentationData.dateTimeFormat, state.usage, isEditingEnabled)) var customValue = false if case .custom = state.usage { customValue = true } - entries.append(.usageCustomPicker(presentationData.theme, state.usage.value, state.pickingUsageLimit, customValue)) + entries.append(.usageCustomPicker(presentationData.theme, state.usage.value, state.pickingUsageLimit, customValue, isEditingEnabled)) entries.append(.usageInfo(presentationData.theme, presentationData.strings.InviteLink_Create_UsersLimitInfo)) } @@ -449,6 +537,8 @@ private struct InviteLinkEditControllerState: Equatable { var usage: InviteLinkUsageLimit var time: InviteLinkTimeLimit var requestApproval = false + var subscriptionEnabled = false + var subscriptionFee: Int64? var pickingExpiryDate = false var pickingExpiryTime = false var pickingUsageLimit = false @@ -460,7 +550,7 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio let actionsDisposable = DisposableSet() let initialState: InviteLinkEditControllerState - if let invite = invite, case let .link(_, title, _, requestApproval, _, _, _, _, expireDate, usageLimit, count, _, _) = invite { + if let invite = invite, case let .link(_, title, _, requestApproval, _, _, _, _, expireDate, usageLimit, count, _, pricing) = invite { var usageLimit = usageLimit if let limit = usageLimit, let count = count, count > 0 { usageLimit = limit - count @@ -478,9 +568,9 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio timeLimit = .unlimited } - initialState = InviteLinkEditControllerState(title: title ?? "", usage: InviteLinkUsageLimit(value: usageLimit), time: timeLimit, requestApproval: requestApproval, pickingExpiryDate: false, pickingExpiryTime: false, pickingUsageLimit: false) + initialState = InviteLinkEditControllerState(title: title ?? "", usage: InviteLinkUsageLimit(value: usageLimit), time: timeLimit, requestApproval: requestApproval, subscriptionEnabled: pricing != nil, subscriptionFee: pricing?.amount, pickingExpiryDate: false, pickingExpiryTime: false, pickingUsageLimit: false) } else { - initialState = InviteLinkEditControllerState(title: "", usage: .unlimited, time: .unlimited, requestApproval: false, pickingExpiryDate: false, pickingExpiryTime: false, pickingUsageLimit: false) + initialState = InviteLinkEditControllerState(title: "", usage: .unlimited, time: .unlimited, requestApproval: false, subscriptionEnabled: false, subscriptionFee: nil, pickingExpiryDate: false, pickingExpiryTime: false, pickingUsageLimit: false) } let statePromise = ValuePromise(initialState, ignoreRepeated: true) @@ -570,14 +660,21 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio dismissImpl?() }) - let rightNavigationButton = ItemListNavigationButton(content: .text(invite == nil ? presentationData.strings.Common_Create : presentationData.strings.Common_Save), style: state.updating ? .activity : .bold, enabled: true, action: { + var doneIsEnabled = true + if state.subscriptionEnabled { + if (state.subscriptionFee ?? 0) == 0 { + doneIsEnabled = false + } + } + + let rightNavigationButton = ItemListNavigationButton(content: .text(invite == nil ? presentationData.strings.Common_Create : presentationData.strings.Common_Save), style: state.updating ? .activity : .bold, enabled: doneIsEnabled, action: { updateState { state in var updatedState = state updatedState.updating = true return updatedState } - let expireDate: Int32? + var expireDate: Int32? if case let .custom(value) = state.time { expireDate = value } else if let value = state.time.value { @@ -589,11 +686,20 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio let titleString = state.title.trimmingCharacters(in: .whitespacesAndNewlines) let title = titleString.isEmpty ? nil : titleString - let usageLimit = state.usage.value - let requestNeeded = state.requestApproval && !isPublic + var usageLimit = state.usage.value + var requestNeeded: Bool? = state.requestApproval && !isPublic if invite == nil { - let _ = (context.engine.peers.createPeerExportedInvitation(peerId: peerId, title: title, expireDate: expireDate, usageLimit: requestNeeded ? 0 : usageLimit, requestNeeded: requestNeeded, subscriptionPricing: nil) + let subscriptionPricing: StarsSubscriptionPricing? + if let subscriptionFee = state.subscriptionFee { + subscriptionPricing = StarsSubscriptionPricing( + period: context.account.testingEnvironment ? StarsSubscriptionPricing.testPeriod : StarsSubscriptionPricing.monthPeriod, + amount: subscriptionFee + ) + } else { + subscriptionPricing = nil + } + let _ = (context.engine.peers.createPeerExportedInvitation(peerId: peerId, title: title, expireDate: expireDate, usageLimit: requestNeeded == true ? 0 : usageLimit, requestNeeded: requestNeeded, subscriptionPricing: subscriptionPricing) |> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic)) |> deliverOnMainQueue).start(next: { invite in completion?(invite) @@ -606,13 +712,24 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio } presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) }) - } else if let initialInvite = invite, case let .link(link, _, _, initialRequestApproval, _, _, _, _, initialExpireDate, initialUsageLimit, _, _, _) = initialInvite { - if initialExpireDate == expireDate && initialUsageLimit == usageLimit && initialRequestApproval == requestNeeded { + } else if let initialInvite = invite, case let .link(link, initialTitle, _, initialRequestApproval, _, _, _, _, initialExpireDate, initialUsageLimit, _, _, _) = initialInvite { + if (initialExpireDate ?? 0) == expireDate && (initialUsageLimit ?? 0) == usageLimit && initialRequestApproval == requestNeeded && (initialTitle ?? "") == title { completion?(initialInvite) dismissImpl?() return } - let _ = (context.engine.peers.editPeerExportedInvitation(peerId: peerId, link: link, title: title, expireDate: expireDate, usageLimit: requestNeeded ? 0 : usageLimit, requestNeeded: requestNeeded) + + if (initialExpireDate ?? 0) == expireDate { + expireDate = nil + } + if (initialUsageLimit ?? 0) == usageLimit { + usageLimit = nil + } + if initialRequestApproval == requestNeeded { + requestNeeded = nil + } + + let _ = (context.engine.peers.editPeerExportedInvitation(peerId: peerId, link: link, title: title, expireDate: expireDate, usageLimit: requestNeeded == true ? 0 : usageLimit, requestNeeded: requestNeeded) |> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic)) |> deliverOnMainQueue).start(next: { invite in completion?(invite) @@ -630,7 +747,7 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio let previousState = previousState.swap(state) var animateChanges = false - if let previousState = previousState, previousState.pickingExpiryDate != state.pickingExpiryDate || previousState.pickingExpiryTime != state.pickingExpiryTime || previousState.requestApproval != state.requestApproval { + if let previousState = previousState, previousState.pickingExpiryDate != state.pickingExpiryDate || previousState.pickingExpiryTime != state.pickingExpiryTime || previousState.requestApproval != state.requestApproval || previousState.subscriptionEnabled != state.subscriptionEnabled { animateChanges = true } diff --git a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift index c59db516a9..1088f93cf0 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift @@ -239,7 +239,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry { arguments.createLink() }) case let .link(_, _, invite, canEdit, _): - return ItemListInviteLinkItem(presentationData: presentationData, invite: invite, share: false, sectionId: self.section, style: .blocks) { invite in + return ItemListInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, share: false, sectionId: self.section, style: .blocks) { invite in arguments.openLink(invite) } contextAction: { invite, node, gesture in arguments.linkContextAction(invite, canEdit, node, gesture) @@ -253,7 +253,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry { arguments.deleteAllRevokedLinks() }) case let .revokedLink(_, _, invite): - return ItemListInviteLinkItem(presentationData: presentationData, invite: invite, share: false, sectionId: self.section, style: .blocks) { invite in + return ItemListInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, share: false, sectionId: self.section, style: .blocks) { invite in arguments.openLink(invite) } contextAction: { invite, node, gesture in arguments.linkContextAction(invite, false, node, gesture) diff --git a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift index b7a4929a4a..803cb4793b 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift @@ -20,6 +20,30 @@ import PresentationDataUtils import DirectionalPanGesture import UndoUI import QrCodeUI +import TextFormat + +private var subscriptionLinkIcon: UIImage? = { + return generateImage(CGSize(width: 40.0, height: 40.0), contextGenerator: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.clear(bounds) + + let pathBounds = CGRect(origin: .zero, size: CGSize(width: 40.0, height: 40.0)) + context.addPath(CGPath(ellipseIn: pathBounds, transform: nil)) + context.clip() + + var locations: [CGFloat] = [1.0, 0.0] + let colors: [CGColor] = [UIColor(rgb: 0x87d93b).cgColor, UIColor(rgb: 0x31b73b).cgColor] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + + if let image = generateTintedImage(image: UIImage(bundleImageName: "Item List/SubscriptionLink"), color: .white), let cgImage = image.cgImage { + context.draw(cgImage, in: pathBounds) + } + }) +}() class InviteLinkViewInteraction { let context: AccountContext @@ -50,6 +74,8 @@ private struct InviteLinkViewTransaction { private enum InviteLinkViewEntryId: Hashable { case link + case subscriptionHeader + case subscriptionPricing case creatorHeader case creator case requestHeader @@ -60,6 +86,8 @@ private enum InviteLinkViewEntryId: Hashable { private enum InviteLinkViewEntry: Comparable, Identifiable { case link(PresentationTheme, ExportedInvitation) + case subscriptionHeader(PresentationTheme, String) + case subscriptionPricing(PresentationTheme, String, String) case creatorHeader(PresentationTheme, String) case creator(PresentationTheme, PresentationDateTimeFormat, EnginePeer, Int32) case requestHeader(PresentationTheme, String, String, Bool) @@ -71,6 +99,10 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { switch self { case .link: return .link + case .subscriptionHeader: + return .subscriptionHeader + case .subscriptionPricing: + return .subscriptionPricing case .creatorHeader: return .creatorHeader case .creator: @@ -94,6 +126,18 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { } else { return false } + case let .subscriptionHeader(lhsTheme, lhsTitle): + if case let .subscriptionHeader(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle { + return true + } else { + return false + } + case let .subscriptionPricing(lhsTheme, lhsTitle, lhsSubtitle): + if case let .subscriptionPricing(rhsTheme, rhsTitle, rhsSubtitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle { + return true + } else { + return false + } case let .creatorHeader(lhsTheme, lhsTitle): if case let .creatorHeader(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle { return true @@ -139,33 +183,47 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { switch rhs { case .link: return false + case .subscriptionHeader, .subscriptionPricing, .creatorHeader, .creator, .requestHeader, .request, .importerHeader, .importer: + return true + } + case .subscriptionHeader: + switch rhs { + case .link, .subscriptionHeader: + return false + case .subscriptionPricing, .creatorHeader, .creator, .requestHeader, .request, .importerHeader, .importer: + return true + } + case .subscriptionPricing: + switch rhs { + case .link, .subscriptionHeader, .subscriptionPricing: + return false case .creatorHeader, .creator, .requestHeader, .request, .importerHeader, .importer: return true } case .creatorHeader: switch rhs { - case .link, .creatorHeader: + case .link, .subscriptionHeader, .subscriptionPricing, .creatorHeader: return false case .creator, .requestHeader, .request, .importerHeader, .importer: return true } case .creator: switch rhs { - case .link, .creatorHeader, .creator: + case .link, .subscriptionHeader, .subscriptionPricing, .creatorHeader, .creator: return false case .requestHeader, .request, .importerHeader, .importer: return true } case .requestHeader: switch rhs { - case .link, .creatorHeader, .creator, .requestHeader: + case .link, .subscriptionHeader, .subscriptionPricing, .creatorHeader, .creator, .requestHeader: return false case .request, .importerHeader, .importer: return true } case let .request(lhsIndex, _, _, _, _, _): switch rhs { - case .link, .creatorHeader, .creator, .requestHeader: + case .link, .subscriptionHeader, .subscriptionPricing, .creatorHeader, .creator, .requestHeader: return false case let .request(rhsIndex, _, _, _, _, _): return lhsIndex < rhsIndex @@ -174,14 +232,14 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { } case .importerHeader: switch rhs { - case .link, .creatorHeader, .creator, .requestHeader, .request, .importerHeader: + case .link, .subscriptionHeader, .subscriptionPricing, .creatorHeader, .creator, .requestHeader, .request, .importerHeader: return false case .importer: return true } case let .importer(lhsIndex, _, _, _, _, _, _): switch rhs { - case .link, .creatorHeader, .creator, .importerHeader, .request, .requestHeader: + case .link, .subscriptionHeader, .subscriptionPricing, .creatorHeader, .creator, .importerHeader, .request, .requestHeader: return false case let .importer(rhsIndex, _, _, _, _, _, _): return lhsIndex < rhsIndex @@ -204,13 +262,22 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { interaction.contextAction(invite, node, gesture) }, viewAction: { }) + case let .subscriptionHeader(_, title): + return SectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title) + case let .subscriptionPricing(_, title, subtitle): + let attributedTitle = NSMutableAttributedString(string: title, font: Font.semibold(presentationData.listsFontSize.itemListBaseFontSize), textColor: presentationData.theme.list.itemPrimaryTextColor) + if let range = attributedTitle.string.range(of: "⭐️") { + attributedTitle.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: attributedTitle.string)) + attributedTitle.addAttribute(.baselineOffset, value: -1.0, range: NSRange(range, in: attributedTitle.string)) + } + return ItemListDisclosureItem(presentationData: ItemListPresentationData(presentationData), icon: subscriptionLinkIcon, context: interaction.context, title: "", attributedTitle: attributedTitle, enabled: false, label: subtitle, labelStyle: .detailText, sectionId: 0, style: .plain, disclosureStyle: .none, noInsets: true, action: nil, clearHighlightAutomatically: true, tag: nil, shimmeringIndex: nil) case let .creatorHeader(_, title): return SectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title) case let .creator(_, dateTimeFormat, peer, date): let dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat) - return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .generic, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: peer.id != account.peerId, sectionId: 0, action: { + return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .peerList, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: peer.id != account.peerId, sectionId: 0, action: { interaction.openPeer(peer.id) - }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, tag: nil) + }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, style: .plain, tag: nil) case let .importerHeader(_, title, subtitle, expired), let .requestHeader(_, title, subtitle, expired): let additionalText: SectionHeaderAdditionalText if !subtitle.isEmpty { @@ -230,14 +297,14 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { } else { dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat) } - return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .generic, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: peer.id != account.peerId, sectionId: 0, action: { + return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .peerList, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: peer.id != account.peerId, sectionId: 0, action: { interaction.openPeer(peer.id) - }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, tag: nil, shimmering: loading ? ItemListPeerItemShimmering(alternationIndex: 0) : nil) + }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, style: .plain, tag: nil, shimmering: loading ? ItemListPeerItemShimmering(alternationIndex: 0) : nil) case let .request(_, _, dateTimeFormat, peer, date, loading): let dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat) - return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .generic, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: peer.id != account.peerId, sectionId: 0, action: { + return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .peerList, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: peer.id != account.peerId, sectionId: 0, action: { interaction.openPeer(peer.id) - }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, tag: nil, shimmering: loading ? ItemListPeerItemShimmering(alternationIndex: 0) : nil) + }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, style: .plain, tag: nil, shimmering: loading ? ItemListPeerItemShimmering(alternationIndex: 0) : nil) } } } @@ -727,6 +794,13 @@ public final class InviteLinkViewController: ViewController { var entries: [InviteLinkViewEntry] = [] entries.append(.link(presentationData.theme, invite)) + + if let pricing = invite.pricing { + //TODO:localize + entries.append(.subscriptionHeader(presentationData.theme, "SUBSCRIPTION FEE")) + entries.append(.subscriptionPricing(presentationData.theme, "⭐️\(pricing.amount) / month x \(state.count)", "You get approximately $\(Float(pricing.amount * Int64(state.count)) * 0.01) monthly")) + } + entries.append(.creatorHeader(presentationData.theme, presentationData.strings.InviteLink_CreatedBy.uppercased())) entries.append(.creator(presentationData.theme, presentationData.dateTimeFormat, EnginePeer(creatorPeer), date)) diff --git a/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift b/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift index acf81eea96..32ee4b6cc2 100644 --- a/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift @@ -7,6 +7,9 @@ import TelegramPresentationData import ItemListUI import ShimmerEffect import TelegramCore +import TextNodeWithEntities +import AccountContext +import TextFormat func invitationAvailability(_ invite: ExportedInvitation) -> CGFloat { if case let .link(_, _, _, _, isRevoked, _, date, startDate, expireDate, usageLimit, count, _, _) = invite { @@ -54,6 +57,7 @@ private enum ItemBackgroundColor: Equatable { } public class ItemListInviteLinkItem: ListViewItem, ItemListItem { + let context: AccountContext let presentationData: ItemListPresentationData let invite: ExportedInvitation? let share: Bool @@ -64,6 +68,7 @@ public class ItemListInviteLinkItem: ListViewItem, ItemListItem { public let tag: ItemListItemTag? public init( + context: AccountContext, presentationData: ItemListPresentationData, invite: ExportedInvitation?, share: Bool, @@ -73,6 +78,7 @@ public class ItemListInviteLinkItem: ListViewItem, ItemListItem { contextAction: ((ExportedInvitation, ASDisplayNode, ContextGesture?) -> Void)?, tag: ItemListItemTag? = nil ) { + self.context = context self.presentationData = presentationData self.invite = invite self.share = share @@ -170,6 +176,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { private let titleNode: TextNode private let subtitleNode: TextNode + private let pricingNode: TextNodeWithEntities private var placeholderNode: ShimmerEffectNode? private var absoluteLocation: (CGRect, CGSize)? @@ -218,6 +225,8 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { self.subtitleNode.isUserInteractionEnabled = false self.subtitleNode.contentMode = .left self.subtitleNode.contentsScale = UIScreen.main.scale + + self.pricingNode = TextNodeWithEntities() self.highlightedBackgroundNode = ASDisplayNode() self.highlightedBackgroundNode.isLayerBacked = true @@ -237,6 +246,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { self.offsetContainerNode.addSubnode(self.iconNode) self.offsetContainerNode.addSubnode(self.titleNode) self.offsetContainerNode.addSubnode(self.subtitleNode) + self.offsetContainerNode.addSubnode(self.pricingNode.textNode) self.containerNode.activated = { [weak self] gesture, _ in guard let strongSelf = self, let item = strongSelf.layoutParams?.0, let invite = item.invite, let contextAction = item.contextAction else { @@ -266,6 +276,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { self?.extractedBackgroundImageNode.image = nil } }) + transition.updateAlpha(node: strongSelf.pricingNode.textNode, alpha: isExtracted ? 0.0 : 1.0) } } @@ -280,6 +291,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { public func asyncLayout() -> (_ item: ItemListInviteLinkItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors, _ firstWithHeader: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) + let makePricingLayout = TextNodeWithEntities.asyncLayout(self.pricingNode) let currentItem = self.layoutParams?.0 @@ -299,14 +311,19 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { let color: ItemBackgroundColor let nextColor: ItemBackgroundColor let transitionFraction: CGFloat - if let invite = item.invite, case let .link(_, _, _, _, isRevoked, _, _, _, expireDate, usageLimit, _, _, _) = invite { + if let invite = item.invite, case let .link(_, _, _, _, isRevoked, _, _, _, expireDate, usageLimit, _, _, pricing) = invite { if isRevoked { color = .gray nextColor = .gray transitionFraction = 0.0 } else if expireDate == nil && usageLimit == nil { - color = .blue - nextColor = .blue + if let _ = pricing { + color = .green + nextColor = .green + } else { + color = .blue + nextColor = .blue + } transitionFraction = 0.0 } else if availability >= 0.5 { color = .green @@ -343,10 +360,10 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { let inviteLink = item.invite?.link?.replacingOccurrences(of: "https://", with: "") ?? "" var titleText = inviteLink var subtitleText: String = "" + var pricingAttributedText: NSMutableAttributedString? var timerValue: TimerNode.Value? - - if let invite = item.invite, case let .link(_, title, _, _, _, _, date, startDate, expireDate, usageLimit, count, requestedCount, _) = invite { + if let invite = item.invite, case let .link(_, title, _, _, _, _, date, startDate, expireDate, usageLimit, count, requestedCount, subscriptionPricing) = invite { if let title = title, !title.isEmpty { titleText = title } @@ -375,6 +392,19 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { subtitleText += item.presentationData.strings.MemberRequests_PeopleRequestedShort(requestedCount) } + if let subscriptionPricing { + //TODO:localize + let text = NSMutableAttributedString() + text.append(NSAttributedString(string: "⭐️\(subscriptionPricing.amount)\n", font: Font.semibold(17.0), textColor: item.presentationData.theme.list.itemPrimaryTextColor)) + text.append(NSAttributedString(string: "per month", font: Font.regular(13.0), textColor: item.presentationData.theme.list.itemSecondaryTextColor)) + if let range = text.string.range(of: "⭐️") { + text.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: text.string)) + text.addAttribute(NSAttributedString.Key.font, value: Font.semibold(15.0), range: NSRange(range, in: text.string)) + text.addAttribute(.baselineOffset, value: 2.5, range: NSRange(range, in: text.string)) + } + pricingAttributedText = text + } + if invite.isRevoked { if !subtitleText.isEmpty { subtitleText += " • " @@ -443,6 +473,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (pricingLayout, pricingApply) = makePricingLayout(TextNodeLayoutArguments(attributedString: pricingAttributedText, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .right, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) let titleSpacing: CGFloat = 1.0 @@ -505,13 +536,18 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { strongSelf.backgroundNode.backgroundColor = itemBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor - strongSelf.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor) + if let _ = item.invite?.pricing { + strongSelf.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Item List/SubscriptionLink"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor) + } else { + strongSelf.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Item List/InviteLink"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor) + } } let transition = ContainedViewLayoutTransition.immediate let _ = titleApply() let _ = subtitleApply() + let _ = pricingApply(TextNodeWithEntities.Arguments(context: item.context, cache: item.context.animationCache, renderer: item.context.animationRenderer, placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, attemptSynchronous: false)) switch item.style { case .plain: @@ -607,6 +643,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size)) transition.updateFrame(node: strongSelf.subtitleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: verticalInset + titleLayout.size.height + titleSpacing), size: subtitleLayout.size)) + transition.updateFrame(node: strongSelf.pricingNode.textNode, frame: CGRect(origin: CGPoint(x: layout.contentSize.width - rightInset - pricingLayout.size.width, y: floorToScreenPixels((layout.contentSize.height - pricingLayout.size.height) / 2.0)), size: pricingLayout.size)) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentSize.height + UIScreenPixel + UIScreenPixel)) diff --git a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift index d830858c1f..cd8408de51 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift @@ -8,6 +8,7 @@ import ShimmerEffect import AvatarNode import TelegramCore import AccountContext +import TextNodeWithEntities private let avatarFont = avatarPlaceholderFont(size: 16.0) @@ -64,12 +65,13 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem { public let sectionId: ItemListSectionId let style: ItemListStyle let disclosureStyle: ItemListDisclosureStyle + let noInsets: Bool let action: (() -> Void)? let clearHighlightAutomatically: Bool public let tag: ItemListItemTag? public let shimmeringIndex: Int? - public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, context: AccountContext? = nil, iconPeer: EnginePeer? = nil, title: String, attributedTitle: NSAttributedString? = nil, enabled: Bool = true, titleColor: ItemListDisclosureItemTitleColor = .primary, titleFont: ItemListDisclosureItemTitleFont = .regular, titleIcon: UIImage? = nil, label: String, attributedLabel: NSAttributedString? = nil, labelStyle: ItemListDisclosureLabelStyle = .text, additionalDetailLabel: String? = nil, additionalDetailLabelColor: ItemListDisclosureItemDetailLabelColor = .generic, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, action: (() -> Void)?, clearHighlightAutomatically: Bool = true, tag: ItemListItemTag? = nil, shimmeringIndex: Int? = nil) { + public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, context: AccountContext? = nil, iconPeer: EnginePeer? = nil, title: String, attributedTitle: NSAttributedString? = nil, enabled: Bool = true, titleColor: ItemListDisclosureItemTitleColor = .primary, titleFont: ItemListDisclosureItemTitleFont = .regular, titleIcon: UIImage? = nil, label: String, attributedLabel: NSAttributedString? = nil, labelStyle: ItemListDisclosureLabelStyle = .text, additionalDetailLabel: String? = nil, additionalDetailLabelColor: ItemListDisclosureItemDetailLabelColor = .generic, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, noInsets: Bool = false, action: (() -> Void)?, clearHighlightAutomatically: Bool = true, tag: ItemListItemTag? = nil, shimmeringIndex: Int? = nil) { self.presentationData = presentationData self.icon = icon self.context = context @@ -88,6 +90,7 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem { self.sectionId = sectionId self.style = style self.disclosureStyle = disclosureStyle + self.noInsets = noInsets self.action = action self.clearHighlightAutomatically = clearHighlightAutomatically self.tag = tag @@ -151,7 +154,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { var avatarNode: AvatarNode? let iconNode: ASImageNode - let titleNode: TextNode + let titleNode: TextNodeWithEntities let titleIconNode: ASImageNode public let labelNode: TextNode var additionalDetailLabelNode: TextNode? @@ -196,8 +199,8 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { self.iconNode.isLayerBacked = true self.iconNode.displaysAsynchronously = false - self.titleNode = TextNode() - self.titleNode.isUserInteractionEnabled = false + self.titleNode = TextNodeWithEntities() + self.titleNode.textNode.isUserInteractionEnabled = false self.titleIconNode = ASImageNode() self.titleIconNode.displayWithoutProcessing = true @@ -224,7 +227,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { super.init(layerBacked: false, dynamicBounce: false) - self.addSubnode(self.titleNode) + self.addSubnode(self.titleNode.textNode) self.addSubnode(self.labelNode) self.addSubnode(self.arrowNode) @@ -252,7 +255,8 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { } public func asyncLayout() -> (_ item: ItemListDisclosureItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { - let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeTitleLayout = TextNode.asyncLayout(self.titleNode.textNode) + let makeTitleWithEntitiesLayout = TextNodeWithEntities.asyncLayout(self.titleNode) let makeLabelLayout = TextNode.asyncLayout(self.labelNode) let makeAdditionalDetailLabelLayout = TextNode.asyncLayout(self.additionalDetailLabelNode) @@ -329,14 +333,14 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { } let contentSize: CGSize - let insets: UIEdgeInsets + var insets: UIEdgeInsets let separatorHeight = UIScreenPixel let itemBackgroundColor: UIColor let itemSeparatorColor: UIColor var leftInset = 16.0 + params.leftInset if item.icon != nil { - leftInset += 43.0 + leftInset += item.noInsets ? 49.0 : 43.0 } else if item.iconPeer != nil { leftInset += 46.0 } @@ -370,7 +374,11 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { maxTitleWidth -= 12.0 } - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: item.attributedTitle ?? NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: item.attributedTitle != nil ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let titleArguments = TextNodeLayoutArguments(attributedString: item.attributedTitle ?? NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: item.attributedTitle != nil ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()) + let (titleLayoutAndApply) = item.context == nil ? makeTitleLayout(titleArguments) : nil + let (titleWithEntitiesLayoutAndApply) = item.context != nil ? makeTitleWithEntitiesLayout(titleArguments) : nil + + let titleLayout: TextNodeLayout = (titleWithEntitiesLayoutAndApply?.0 ?? titleLayoutAndApply?.0)! let detailFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) @@ -455,6 +463,10 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor contentSize = CGSize(width: params.width, height: height) insets = itemListNeighborsPlainInsets(neighbors) + if item.noInsets { + insets.top = 0.0 + insets.bottom = 0.0 + } case .blocks: itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor @@ -531,8 +543,21 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { strongSelf.backgroundNode.backgroundColor = itemBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor } + + if let titleWithEntitiesApply = titleWithEntitiesLayoutAndApply?.1, let context = item.context { + let _ = titleWithEntitiesApply( + TextNodeWithEntities.Arguments( + context: context, + cache: context.animationCache, + renderer: context.animationRenderer, + placeholderColor: item.presentationData.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12), + attemptSynchronous: false + ) + ) + } else if let titleApply = titleLayoutAndApply?.1 { + let _ = titleApply() + } - let _ = titleApply() let _ = labelApply() switch item.style { @@ -607,7 +632,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { } let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - centralContentHeight) / 2.0)), size: titleLayout.size) - strongSelf.titleNode.frame = titleFrame + strongSelf.titleNode.textNode.frame = titleFrame if let updateBadgeImage = updatedLabelBadgeImage { if strongSelf.labelBadgeNode.supernode == nil { @@ -746,7 +771,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { let titleLineWidth: CGFloat = (shimmeringIndex % 2 == 0) ? 120.0 : 80.0 let lineDiameter: CGFloat = 8.0 - let titleFrame = strongSelf.titleNode.frame + let titleFrame = strongSelf.titleNode.textNode.frame shapes.append(.roundedRectLine(startPoint: CGPoint(x: titleFrame.minX, y: titleFrame.minY + floor((titleFrame.height - lineDiameter) / 2.0)), width: titleLineWidth, diameter: lineDiameter)) shimmerNode.update(backgroundColor: item.presentationData.theme.list.itemBlocksBackgroundColor, foregroundColor: item.presentationData.theme.list.mediaPlaceholderColor, shimmeringColor: item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: contentSize) diff --git a/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift b/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift index 5e5fcb35f6..dfbc841be4 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift @@ -4,6 +4,8 @@ import Display import AsyncDisplayKit import SwiftSignalKit import TelegramPresentationData +import TextNodeWithEntities +import AccountContext private let validIdentifierSet: CharacterSet = { var set = CharacterSet(charactersIn: "a".unicodeScalars.first! ... "z".unicodeScalars.first!) @@ -43,6 +45,7 @@ public enum ItemListSingleLineInputAlignment { } public class ItemListSingleLineInputItem: ListViewItem, ItemListItem { + let context: AccountContext? let presentationData: ItemListPresentationData let title: NSAttributedString let text: String @@ -65,7 +68,8 @@ public class ItemListSingleLineInputItem: ListViewItem, ItemListItem { let cleared: (() -> Void)? public let tag: ItemListItemTag? - public init(presentationData: ItemListPresentationData, title: NSAttributedString, text: String, placeholder: String, type: ItemListSingleLineInputItemType = .regular(capitalization: true, autocorrection: true), returnKeyType: UIReturnKeyType = .`default`, alignment: ItemListSingleLineInputAlignment = .default, spacing: CGFloat = 0.0, clearType: ItemListSingleLineInputClearType = .none, maxLength: Int = 0, enabled: Bool = true, selectAllOnFocus: Bool = false, secondaryStyle: Bool = false, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> String)? = nil, updatedFocus: ((Bool) -> Void)? = nil, action: @escaping () -> Void, cleared: (() -> Void)? = nil) { + public init(context: AccountContext? = nil, presentationData: ItemListPresentationData, title: NSAttributedString, text: String, placeholder: String, type: ItemListSingleLineInputItemType = .regular(capitalization: true, autocorrection: true), returnKeyType: UIReturnKeyType = .`default`, alignment: ItemListSingleLineInputAlignment = .default, spacing: CGFloat = 0.0, clearType: ItemListSingleLineInputClearType = .none, maxLength: Int = 0, enabled: Bool = true, selectAllOnFocus: Bool = false, secondaryStyle: Bool = false, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> String)? = nil, updatedFocus: ((Bool) -> Void)? = nil, action: @escaping () -> Void, cleared: (() -> Void)? = nil) { + self.context = context self.presentationData = presentationData self.title = title self.text = text @@ -130,7 +134,7 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg private let bottomStripeNode: ASDisplayNode private let maskNode: ASImageNode - private let titleNode: TextNode + private let titleNode: TextNodeWithEntities private let measureTitleSizeNode: TextNode private let textNode: TextFieldNode private let clearIconNode: ASImageNode @@ -154,7 +158,7 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg self.maskNode = ASImageNode() - self.titleNode = TextNode() + self.titleNode = TextNodeWithEntities() self.measureTitleSizeNode = TextNode() self.textNode = TextFieldNode() @@ -167,7 +171,7 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg super.init(layerBacked: false, dynamicBounce: false) - self.addSubnode(self.titleNode) + self.addSubnode(self.titleNode.textNode) self.addSubnode(self.textNode) self.addSubnode(self.clearIconNode) self.addSubnode(self.clearButtonNode) @@ -209,7 +213,8 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg } public func asyncLayout() -> (_ item: ItemListSingleLineInputItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { - let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeTitleLayout = TextNode.asyncLayout(self.titleNode.textNode) + let makeTitleWithEntitiesLayout = TextNodeWithEntities.asyncLayout(self.titleNode) let makeMeasureTitleSizeLayout = TextNode.asyncLayout(self.measureTitleSizeNode) let currentItem = self.item @@ -241,15 +246,22 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg } let titleString = NSMutableAttributedString(attributedString: item.title) - titleString.removeAttribute(NSAttributedString.Key.font, range: NSMakeRange(0, titleString.length)) + if !item.title.string.isSingleEmoji { + titleString.removeAttribute(NSAttributedString.Key.font, range: NSMakeRange(0, titleString.length)) + } titleString.addAttributes([NSAttributedString.Key.font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize)], range: NSMakeRange(0, titleString.length)) - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 32.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let titleArguments = TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 32.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()) + + let (titleLayoutAndApply) = item.context == nil ? makeTitleLayout(titleArguments) : nil + let (titleWithEntitiesLayoutAndApply) = item.context != nil ? makeTitleWithEntitiesLayout(titleArguments) : nil + + let titleLayout: TextNodeLayout = (titleWithEntitiesLayoutAndApply?.0 ?? titleLayoutAndApply?.0)! let (measureTitleLayout, measureTitleSizeApply) = makeMeasureTitleSizeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "A", font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize)), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 32.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let separatorHeight = UIScreenPixel - + let contentSize = CGSize(width: params.width, height: max(titleLayout.size.height, measureTitleLayout.size.height) + 22.0) let insets = itemListNeighborsGroupedInsets(neighbors, params) @@ -280,8 +292,20 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg strongSelf.textNode.textField.textColor = item.secondaryStyle ? item.presentationData.theme.list.itemSecondaryTextColor : item.presentationData.theme.list.itemPrimaryTextColor } - let _ = titleApply() - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((layout.contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) + if let titleWithEntitiesApply = titleWithEntitiesLayoutAndApply?.1, let context = item.context { + let _ = titleWithEntitiesApply( + TextNodeWithEntities.Arguments( + context: context, + cache: context.animationCache, + renderer: context.animationRenderer, + placeholderColor: item.presentationData.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12), + attemptSynchronous: false + ) + ) + } else if let titleApply = titleLayoutAndApply?.1 { + let _ = titleApply() + } + strongSelf.titleNode.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((layout.contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) let _ = measureTitleSizeApply() diff --git a/submodules/PasswordSetupUI/Sources/ResetPasswordController.swift b/submodules/PasswordSetupUI/Sources/ResetPasswordController.swift index f42594e4f0..fd12587163 100644 --- a/submodules/PasswordSetupUI/Sources/ResetPasswordController.swift +++ b/submodules/PasswordSetupUI/Sources/ResetPasswordController.swift @@ -11,10 +11,12 @@ import AlertUI import PresentationDataUtils private final class ResetPasswordControllerArguments { + let context: AccountContext let updateCodeText: (String) -> Void let openHelp: () -> Void - init(updateCodeText: @escaping (String) -> Void, openHelp: @escaping () -> Void) { + init(context: AccountContext, updateCodeText: @escaping (String) -> Void, openHelp: @escaping () -> Void) { + self.context = context self.updateCodeText = updateCodeText self.openHelp = openHelp } @@ -128,7 +130,7 @@ public func resetPasswordController(context: AccountContext, emailPattern: Strin let saveDisposable = MetaDisposable() actionsDisposable.add(saveDisposable) - let arguments = ResetPasswordControllerArguments(updateCodeText: { updatedText in + let arguments = ResetPasswordControllerArguments(context: context, updateCodeText: { updatedText in updateState { state in var state = state state.code = updatedText diff --git a/submodules/SettingsUI/Sources/ChangePhoneNumberCodeController.swift b/submodules/SettingsUI/Sources/ChangePhoneNumberCodeController.swift index 59c2896d0c..199624f520 100644 --- a/submodules/SettingsUI/Sources/ChangePhoneNumberCodeController.swift +++ b/submodules/SettingsUI/Sources/ChangePhoneNumberCodeController.swift @@ -15,10 +15,12 @@ import AuthorizationUtils import PhoneNumberFormat private final class ChangePhoneNumberCodeControllerArguments { + let context: AccountContext let updateEntryText: (String) -> Void let next: () -> Void - init(updateEntryText: @escaping (String) -> Void, next: @escaping () -> Void) { + init(context: AccountContext, updateEntryText: @escaping (String) -> Void, next: @escaping () -> Void) { + self.context = context self.updateEntryText = updateEntryText self.next = next } @@ -290,7 +292,7 @@ func changePhoneNumberCodeController(context: AccountContext, phoneNumber: Strin } } - let arguments = ChangePhoneNumberCodeControllerArguments(updateEntryText: { updatedText in + let arguments = ChangePhoneNumberCodeControllerArguments(context: context, updateEntryText: { updatedText in var initiateCheck = false updateState { state in if state.codeText.count < 5 && updatedText.count == 5 { diff --git a/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift index 67ddd69b04..aa3aa8cd7c 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift @@ -26,7 +26,7 @@ private func shareLink(for server: ProxyServerSettings) -> String { return link } -private final class proxyServerSettingsControllerArguments { +private final class ProxyServerSettingsControllerArguments { let updateState: ((ProxyServerSettingsControllerState) -> ProxyServerSettingsControllerState) -> Void let share: () -> Void let usePasteboardSettings: () -> Void @@ -113,7 +113,7 @@ private enum ProxySettingsEntry: ItemListNodeEntry { } func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { - let arguments = arguments as! proxyServerSettingsControllerArguments + let arguments = arguments as! ProxyServerSettingsControllerArguments switch self { case let .usePasteboardSettings(_, title): return ItemListActionItem(presentationData: presentationData, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { @@ -158,7 +158,7 @@ private enum ProxySettingsEntry: ItemListNodeEntry { case let .credentialsHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .credentialsUsername(_, _, placeholder, text): - return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: text, placeholder: placeholder, sectionId: self.section, textUpdated: { value in + return ItemListSingleLineInputItem(context: nil, presentationData: presentationData, title: NSAttributedString(), text: text, placeholder: placeholder, sectionId: self.section, textUpdated: { value in arguments.updateState { current in var state = current state.username = value @@ -306,7 +306,7 @@ func proxyServerSettingsController(sharedContext: SharedAccountContext, context: var shareImpl: (() -> Void)? - let arguments = proxyServerSettingsControllerArguments(updateState: { f in + let arguments = ProxyServerSettingsControllerArguments(updateState: { f in updateState(f) }, share: { shareImpl?() diff --git a/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift b/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift index 67bb0ba0e3..e085ebf7e6 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift @@ -18,12 +18,14 @@ private enum CreatePasswordField { } private final class CreatePasswordControllerArguments { + let context: AccountContext let updateFieldText: (CreatePasswordField, String) -> Void let selectNextInputItem: (CreatePasswordEntryTag) -> Void let save: () -> Void let cancelEmailConfirmation: () -> Void - init(updateFieldText: @escaping (CreatePasswordField, String) -> Void, selectNextInputItem: @escaping (CreatePasswordEntryTag) -> Void, save: @escaping () -> Void, cancelEmailConfirmation: @escaping () -> Void) { + init(context: AccountContext, updateFieldText: @escaping (CreatePasswordField, String) -> Void, selectNextInputItem: @escaping (CreatePasswordEntryTag) -> Void, save: @escaping () -> Void, cancelEmailConfirmation: @escaping () -> Void) { + self.context = context self.updateFieldText = updateFieldText self.selectNextInputItem = selectNextInputItem self.save = save @@ -321,7 +323,7 @@ func createPasswordController(context: AccountContext, createPasswordContext: Cr } } - let arguments = CreatePasswordControllerArguments(updateFieldText: { field, updatedText in + let arguments = CreatePasswordControllerArguments(context: context, updateFieldText: { field, updatedText in updateState { state in var state = state switch field { diff --git a/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift b/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift index ac7aa8d89e..f8efe84b9d 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift @@ -16,6 +16,7 @@ import PasswordSetupUI import Markdown private final class TwoStepVerificationUnlockSettingsControllerArguments { + let context: AccountContext let updatePasswordText: (String) -> Void let checkPassword: () -> Void let openForgotPassword: () -> Void @@ -28,7 +29,8 @@ private final class TwoStepVerificationUnlockSettingsControllerArguments { let declinePasswordReset: () -> Void let resetPassword: () -> Void - init(updatePasswordText: @escaping (String) -> Void, checkPassword: @escaping () -> Void, openForgotPassword: @escaping () -> Void, openSetupPassword: @escaping () -> Void, openDisablePassword: @escaping () -> Void, openSetupEmail: @escaping () -> Void, openResetPendingEmail: @escaping () -> Void, updateEmailCode: @escaping (String) -> Void, openConfirmEmail: @escaping () -> Void, declinePasswordReset: @escaping () -> Void, resetPassword: @escaping () -> Void) { + init(context: AccountContext, updatePasswordText: @escaping (String) -> Void, checkPassword: @escaping () -> Void, openForgotPassword: @escaping () -> Void, openSetupPassword: @escaping () -> Void, openDisablePassword: @escaping () -> Void, openSetupEmail: @escaping () -> Void, openResetPendingEmail: @escaping () -> Void, updateEmailCode: @escaping (String) -> Void, openConfirmEmail: @escaping () -> Void, declinePasswordReset: @escaping () -> Void, resetPassword: @escaping () -> Void) { + self.context = context self.updatePasswordText = updatePasswordText self.checkPassword = checkPassword self.openForgotPassword = openForgotPassword @@ -423,7 +425,7 @@ public func twoStepVerificationUnlockSettingsController(context: AccountContext, }) } - let arguments = TwoStepVerificationUnlockSettingsControllerArguments(updatePasswordText: { updatedText in + let arguments = TwoStepVerificationUnlockSettingsControllerArguments(context: context, updatePasswordText: { updatedText in updateState { state in var state = state state.passwordText = updatedText diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index 9bbff0d1df..9ca64a5e9e 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 185 + return 186 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index 7fd173a0f2..2a7d56c02d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -341,7 +341,7 @@ private final class StarsContextImpl { return } var transactions = state.transactions - transactions.insert(.init(flags: [.isLocal], id: "\(arc4random())", count: balance, date: Int32(Date().timeIntervalSince1970), peer: .appStore, title: nil, description: nil, photo: nil, transactionDate: nil, transactionUrl: nil, paidMessageId: nil, media: []), at: 0) + transactions.insert(.init(flags: [.isLocal], id: "\(arc4random())", count: balance, date: Int32(Date().timeIntervalSince1970), peer: .appStore, title: nil, description: nil, photo: nil, transactionDate: nil, transactionUrl: nil, paidMessageId: nil, media: [], subscriptionPeriod: nil), at: 0) self.updateState(StarsContext.State(flags: [.isPendingBalance], balance: state.balance + balance, subscriptions: state.subscriptions, canLoadMoreSubscriptions: state.canLoadMoreSubscriptions, transactions: transactions, canLoadMoreTransactions: state.canLoadMoreTransactions, isLoading: state.isLoading)) } @@ -408,7 +408,7 @@ private extension StarsContext.State.Transaction { let media = extendedMedia.flatMap({ $0.compactMap { textMediaAndExpirationTimerFromApiMedia($0, PeerId(0)).media } }) ?? [] let _ = subscriptionPeriod - self.init(flags: flags, id: id, count: stars, date: date, peer: parsedPeer, title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), transactionDate: transactionDate, transactionUrl: transactionUrl, paidMessageId: paidMessageId, media: media) + self.init(flags: flags, id: id, count: stars, date: date, peer: parsedPeer, title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), transactionDate: transactionDate, transactionUrl: transactionUrl, paidMessageId: paidMessageId, media: media, subscriptionPeriod: subscriptionPeriod) } } } @@ -474,6 +474,7 @@ public final class StarsContext { public let transactionUrl: String? public let paidMessageId: MessageId? public let media: [Media] + public let subscriptionPeriod: Int32? public init( flags: Flags, @@ -487,7 +488,8 @@ public final class StarsContext { transactionDate: Int32?, transactionUrl: String?, paidMessageId: MessageId?, - media: [Media] + media: [Media], + subscriptionPeriod: Int32? ) { self.flags = flags self.id = id @@ -501,6 +503,7 @@ public final class StarsContext { self.transactionUrl = transactionUrl self.paidMessageId = paidMessageId self.media = media + self.subscriptionPeriod = subscriptionPeriod } public static func == (lhs: Transaction, rhs: Transaction) -> Bool { @@ -540,6 +543,9 @@ public final class StarsContext { if !areMediaArraysEqual(lhs.media, rhs.media) { return false } + if lhs.subscriptionPeriod != rhs.subscriptionPeriod { + return false + } return true } } @@ -1160,8 +1166,8 @@ public struct StarsSubscriptionPricing: Codable, Equatable { try container.encode(self.amount, forKey: .amount) } - public static let monthPeriod = 2592000 - public static let testPeriod = 300 + public static let monthPeriod: Int32 = 2592000 + public static let testPeriod: Int32 = 300 } extension StarsSubscriptionPricing { diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index ee53a897b4..9702ce0582 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -203,7 +203,27 @@ private final class StarsTransactionSheetContent: CombinedComponent { var delayedCloseOnOpenPeer = true switch subject { case let .transaction(transaction, parentPeer): - if transaction.flags.contains(.isGift) { + if let _ = transaction.subscriptionPeriod { + //TODO:localize + titleText = "Monthly Subscription Fee" + descriptionText = "" + count = transaction.count + countOnTop = false + transactionId = transaction.id + via = nil + messageId = nil + date = transaction.date + if case let .peer(peer) = transaction.peer { + toPeer = peer + } else { + toPeer = nil + } + transactionPeer = transaction.peer + media = [] + photo = nil + isRefund = false + isGift = false + } else if transaction.flags.contains(.isGift) { titleText = strings.Stars_Gift_Received_Title descriptionText = strings.Stars_Gift_Received_Text count = transaction.count diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift index 61a909dff3..bd8f3b37bd 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift @@ -219,6 +219,9 @@ final class StarsTransactionsListPanelComponent: Component { itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) if item.flags.contains(.isGift) { itemSubtitle = environment.strings.Stars_Intro_Transaction_Gift_Title + } else if let _ = item.subscriptionPeriod { + //TODO:localize + itemSubtitle = "Monthly subscription fee" } else { itemSubtitle = nil } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index e6329ee119..bd4c837481 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -18,6 +18,8 @@ import ListSectionComponent import BundleIconComponent import TextFormat import UndoUI +import ListActionItemComponent +import StarsAvatarComponent final class StarsTransactionsScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -574,7 +576,44 @@ final class StarsTransactionsScreenComponent: Component { contentHeight += balanceSize.height contentHeight += 44.0 - let subscriptionsItems: [AnyComponentWithIdentity] = [] + let fontBaseDisplaySize = 17.0 + var subscriptionsItems: [AnyComponentWithIdentity] = [] + if let starsState = self.starsState { + for subscription in starsState.subscriptions { + var titleComponents: [AnyComponentWithIdentity] = [] + titleComponents.append( + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: subscription.peer.compactDisplayTitle, + font: Font.semibold(fontBaseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))) + ) + let itemLabel = NSAttributedString(string: "\(subscription.pricing.amount)", font: Font.medium(fontBaseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor) + + subscriptionsItems.append(AnyComponentWithIdentity( + id: subscription.id, + component: AnyComponent( + ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack(titleComponents, alignment: .left, spacing: 2.0)), + contentInsets: UIEdgeInsets(top: 9.0, left: 0.0, bottom: 8.0, right: 0.0), + leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: component.context, theme: environment.theme, peer: .peer(subscription.peer), photo: nil, media: [], backgroundColor: environment.theme.list.plainBackgroundColor))), false), + icon: nil, + accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(text: itemLabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))), + action: { [weak self] _ in + guard let self, let _ = self.component else { + return + } + + } + ) + ) + )) + } + } if !subscriptionsItems.isEmpty { //TODO:localize diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift index 0270432457..fdf612d778 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift @@ -55,7 +55,7 @@ private final class SheetContent: CombinedComponent { let background = Child(RoundedRectangle.self) let closeButton = Child(Button.self) let title = Child(Text.self) - let urlSection = Child(ListSectionComponent.self) + let amountSection = Child(ListSectionComponent.self) let button = Child(ButtonComponent.self) let balanceTitle = Child(MultilineTextComponent.self) let balanceValue = Child(MultilineTextComponent.self) @@ -246,7 +246,7 @@ private final class SheetContent: CombinedComponent { amountFooter = nil } - let urlSection = urlSection.update( + let amountSection = amountSection.update( component: ListSectionComponent( theme: theme, header: AnyComponent(MultilineTextComponent( @@ -283,12 +283,12 @@ private final class SheetContent: CombinedComponent { availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), transition: context.transition ) - context.add(urlSection - .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + urlSection.size.height / 2.0)) + context.add(amountSection + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + amountSection.size.height / 2.0)) .clipsToBounds(true) .cornerRadius(10.0) ) - contentSize.height += urlSection.size.height + contentSize.height += amountSection.size.height contentSize.height += 32.0 let buttonString: String diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 167e85acec..49c597b45b 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -250,12 +250,7 @@ func openResolvedUrlImpl( present(controller, nil) case let .instantView(webpage, anchor): let sourceLocation = InstantPageSourceLocation(userLocation: .other, peerType: .channel) - let pageController: ViewController - if context.sharedContext.immediateExperimentalUISettings.browserExperiment { - pageController = BrowserScreen(context: context, subject: .instantPage(webPage: webpage, anchor: anchor, sourceLocation: sourceLocation)) - } else { - pageController = InstantPageController(context: context, webPage: webpage, sourceLocation: sourceLocation, anchor: anchor) - } + let pageController = BrowserScreen(context: context, subject: .instantPage(webPage: webpage, anchor: anchor, sourceLocation: sourceLocation)) navigationController?.pushViewController(pageController) case let .join(link): dismissInput() @@ -288,6 +283,55 @@ func openResolvedUrlImpl( openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil)) case let .peek(peer, deadline): openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: ChatPeekTimeout(deadline: deadline, linkData: link))) + case let .invite(invite): + if let subscriptionPricing = invite.subscriptionPricing, let subscriptionFormId = invite.subscriptionFormId, let starsContext = context.starsContext { + let inputData = Promise() + var photo: [TelegramMediaImageRepresentation] = [] + if let photoRepresentation = invite.photoRepresentation { + photo.append(photoRepresentation) + } + let channel = TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(0)), accessHash: .genericPublic(0), title: invite.title, username: nil, photo: photo, creationDate: 0, version: 0, participationStatus: .left, info: .broadcast(TelegramChannelBroadcastInfo(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: invite.nameColor, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil, subscriptionUntilDate: nil) + let invoice = TelegramMediaInvoice(title: "", description: "", photo: nil, receiptMessageId: nil, currency: "XTR", totalAmount: subscriptionPricing.amount, startParam: "", extendedMedia: nil, flags: [], version: 0) + + inputData.set(.single(BotCheckoutController.InputData( + form: BotPaymentForm( + id: subscriptionFormId, + canSaveCredentials: false, + passwordMissing: false, + invoice: BotPaymentInvoice(isTest: false, requestedFields: [], currency: "XTR", prices: [BotPaymentPrice(label: "", amount: subscriptionPricing.amount)], tip: nil, termsInfo: nil), + paymentBotId: channel.id, + providerId: nil, + url: nil, + nativeProvider: nil, + savedInfo: nil, + savedCredentials: [], + additionalPaymentMethods: [] + ), + validatedFormInfo: nil, + botPeer: EnginePeer(channel) + ))) + + let starsInputData = combineLatest( + inputData.get(), + starsContext.state + ) + |> map { data, state -> (StarsContext.State, BotPaymentForm, EnginePeer?)? in + if let data, let state { + return (state, data.form, data.botPeer) + } else { + return nil + } + } + let _ = (starsInputData |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue).start(next: { _ in + let controller = context.sharedContext.makeStarsTransferScreen(context: context, starsContext: starsContext, invoice: invoice, source: .starsChatSubscription(hash: link), extendedMedia: [], inputData: starsInputData, completion: { _ in + }) + navigationController?.pushViewController(controller) + }) + } else { + present(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in + openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: peekData)) + }, parentNavigationController: navigationController, resolvedState: resolvedState), nil) + } default: present(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: peekData))