Stars subscriptions

This commit is contained in:
Ilya Laktyushin 2024-08-01 09:53:28 +02:00
parent dc68eab568
commit bc454cfa93
22 changed files with 545 additions and 138 deletions

View File

@ -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)

View File

@ -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 {

View File

@ -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

View File

@ -59,6 +59,7 @@ swift_library(
"//submodules/QrCodeUI:QrCodeUI",
"//submodules/PromptUI",
"//submodules/TelegramUI/Components/ItemListDatePickerItem:ItemListDatePickerItem",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
],
visibility = [
"//visibility:public",

View File

@ -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
}

View File

@ -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)

View File

@ -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))

View File

@ -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))

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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 {

View File

@ -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?()

View File

@ -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 {

View File

@ -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

View File

@ -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! {

View File

@ -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 {

View File

@ -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

View File

@ -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
}

View File

@ -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<Empty>] = []
let fontBaseDisplaySize = 17.0
var subscriptionsItems: [AnyComponentWithIdentity<Empty>] = []
if let starsState = self.starsState {
for subscription in starsState.subscriptions {
var titleComponents: [AnyComponentWithIdentity<Empty>] = []
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

View File

@ -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

View File

@ -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<BotCheckoutController.InputData?>()
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))