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) nodeInteraction?.openPremiumGift(birthdays)
case .reviewLogin: case .reviewLogin:
break break
case .starsSubscriptionLowBalance:
break
} }
case .hide: case .hide:
nodeInteraction?.dismissNotice(notice) nodeInteraction?.dismissNotice(notice)
@ -1085,6 +1087,8 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
nodeInteraction?.openPremiumGift(birthdays) nodeInteraction?.openPremiumGift(birthdays)
case .reviewLogin: case .reviewLogin:
break break
case .starsSubscriptionLowBalance:
break
} }
case .hide: case .hide:
nodeInteraction?.dismissNotice(notice) nodeInteraction?.dismissNotice(notice)

View File

@ -90,6 +90,7 @@ public enum ChatListNotice: Equatable {
case birthdayPremiumGift(peers: [EnginePeer], birthdays: [EnginePeer.Id: TelegramBirthday]) case birthdayPremiumGift(peers: [EnginePeer], birthdays: [EnginePeer.Id: TelegramBirthday])
case reviewLogin(newSessionReview: NewSessionReview, totalCount: Int) case reviewLogin(newSessionReview: NewSessionReview, totalCount: Int)
case premiumGrace case premiumGrace
case starsSubscriptionLowBalance
} }
enum ChatListNodeEntry: Comparable, Identifiable { 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))) okButtonLayout = makeOkButtonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.ChatList_SessionReview_PanelConfirm, font: titleFont, textColor: item.theme.list.itemAccentColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0)))
cancelButtonLayout = makeCancelButtonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.ChatList_SessionReview_PanelReject, font: titleFont, textColor: item.theme.list.itemDestructiveColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0))) cancelButtonLayout = makeCancelButtonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.ChatList_SessionReview_PanelReject, font: titleFont, textColor: item.theme.list.itemDestructiveColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0)))
case .starsSubscriptionLowBalance:
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 var leftInset: CGFloat = sideInset

View File

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

View File

@ -17,6 +17,7 @@ import ContextUI
import TelegramStringFormatting import TelegramStringFormatting
import UndoUI import UndoUI
import ItemListDatePickerItem import ItemListDatePickerItem
import TextFormat
private final class InviteLinkEditControllerArguments { private final class InviteLinkEditControllerArguments {
let context: AccountContext let context: AccountContext
@ -36,6 +37,7 @@ private final class InviteLinkEditControllerArguments {
private enum InviteLinksEditSection: Int32 { private enum InviteLinksEditSection: Int32 {
case title case title
case subscriptionFee
case requestApproval case requestApproval
case time case time
case usage case usage
@ -75,18 +77,23 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
case title(PresentationTheme, String, String) case title(PresentationTheme, String, String)
case titleInfo(PresentationTheme, 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 requestApprovalInfo(PresentationTheme, String)
case timeHeader(PresentationTheme, String) case timeHeader(PresentationTheme, String)
case timePicker(PresentationTheme, InviteLinkTimeLimit) case timePicker(PresentationTheme, InviteLinkTimeLimit, Bool)
case timeExpiryDate(PresentationTheme, PresentationDateTimeFormat, Int32?, Bool) case timeExpiryDate(PresentationTheme, PresentationDateTimeFormat, Int32?, Bool, Bool)
case timeCustomPicker(PresentationTheme, PresentationDateTimeFormat, Int32?, Bool, Bool) case timeCustomPicker(PresentationTheme, PresentationDateTimeFormat, Int32?, Bool, Bool, Bool)
case timeInfo(PresentationTheme, String) case timeInfo(PresentationTheme, String)
case usageHeader(PresentationTheme, String) case usageHeader(PresentationTheme, String)
case usagePicker(PresentationTheme, PresentationDateTimeFormat, InviteLinkUsageLimit) case usagePicker(PresentationTheme, PresentationDateTimeFormat, InviteLinkUsageLimit, Bool)
case usageCustomPicker(PresentationTheme, Int32?, Bool, Bool) case usageCustomPicker(PresentationTheme, Int32?, Bool, Bool, Bool)
case usageInfo(PresentationTheme, String) case usageInfo(PresentationTheme, String)
case revoke(PresentationTheme, String) case revoke(PresentationTheme, String)
@ -95,6 +102,8 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
switch self { switch self {
case .titleHeader, .title, .titleInfo: case .titleHeader, .title, .titleInfo:
return InviteLinksEditSection.title.rawValue return InviteLinksEditSection.title.rawValue
case .subscriptionFeeToggle, .subscriptionFee, .subscriptionFeeInfo:
return InviteLinksEditSection.subscriptionFee.rawValue
case .requestApproval, .requestApprovalInfo: case .requestApproval, .requestApprovalInfo:
return InviteLinksEditSection.requestApproval.rawValue return InviteLinksEditSection.requestApproval.rawValue
case .timeHeader, .timePicker, .timeExpiryDate, .timeCustomPicker, .timeInfo: case .timeHeader, .timePicker, .timeExpiryDate, .timeCustomPicker, .timeInfo:
@ -114,30 +123,36 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
return 1 return 1
case .titleInfo: case .titleInfo:
return 2 return 2
case .requestApproval: case .subscriptionFeeToggle:
return 3 return 3
case .requestApprovalInfo: case .subscriptionFee:
return 4 return 4
case .timeHeader: case .subscriptionFeeInfo:
return 5 return 5
case .timePicker: case .requestApproval:
return 6 return 6
case .timeExpiryDate: case .requestApprovalInfo:
return 7 return 7
case .timeCustomPicker: case .timeHeader:
return 8 return 8
case .timeInfo: case .timePicker:
return 9 return 9
case .usageHeader: case .timeExpiryDate:
return 10 return 10
case .usagePicker: case .timeCustomPicker:
return 11 return 11
case .usageCustomPicker: case .timeInfo:
return 12 return 12
case .usageInfo: case .usageHeader:
return 13 return 13
case .revoke: case .usagePicker:
return 14 return 14
case .usageCustomPicker:
return 15
case .usageInfo:
return 16
case .revoke:
return 17
} }
} }
@ -161,8 +176,26 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .requestApproval(lhsTheme, lhsText, lhsValue): case let .subscriptionFeeToggle(lhsTheme, lhsText, lhsValue, lhsEnabled):
if case let .requestApproval(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { 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 return true
} else { } else {
return false return false
@ -179,20 +212,20 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .timePicker(lhsTheme, lhsValue): case let .timePicker(lhsTheme, lhsValue, lhsEnabled):
if case let .timePicker(rhsTheme, rhsValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue { if case let .timePicker(rhsTheme, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue, lhsEnabled == rhsEnabled {
return true return true
} else { } else {
return false return false
} }
case let .timeExpiryDate(lhsTheme, lhsDateTimeFormat, lhsDate, lhsActive): case let .timeExpiryDate(lhsTheme, lhsDateTimeFormat, lhsDate, lhsActive, lhsEnabled):
if case let .timeExpiryDate(rhsTheme, rhsDateTimeFormat, rhsDate, rhsActive) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsDate == rhsDate, lhsActive == rhsActive { if case let .timeExpiryDate(rhsTheme, rhsDateTimeFormat, rhsDate, rhsActive, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsDate == rhsDate, lhsActive == rhsActive, lhsEnabled == rhsEnabled {
return true return true
} else { } else {
return false return false
} }
case let .timeCustomPicker(lhsTheme, lhsDateTimeFormat, lhsDate, lhsDisplayingDateSelection, lhsDisplayingTimeSelection): case let .timeCustomPicker(lhsTheme, lhsDateTimeFormat, lhsDate, lhsDisplayingDateSelection, lhsDisplayingTimeSelection, lhsEnabled):
if case let .timeCustomPicker(rhsTheme, rhsDateTimeFormat, rhsDate, rhsDisplayingDateSelection, rhsDisplayingTimeSelection) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsDate == rhsDate, lhsDisplayingDateSelection == rhsDisplayingDateSelection, lhsDisplayingTimeSelection == rhsDisplayingTimeSelection { 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 return true
} else { } else {
return false return false
@ -209,14 +242,14 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .usagePicker(lhsTheme, lhsDateTimeFormat, lhsValue): case let .usagePicker(lhsTheme, lhsDateTimeFormat, lhsValue, lhsEnabled):
if case let .usagePicker(rhsTheme, rhsDateTimeFormat, rhsValue) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsValue == rhsValue { if case let .usagePicker(rhsTheme, rhsDateTimeFormat, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsValue == rhsValue, lhsEnabled == rhsEnabled {
return true return true
} else { } else {
return false return false
} }
case let .usageCustomPicker(lhsTheme, lhsValue, lhsFocused, lhsCustomValue): case let .usageCustomPicker(lhsTheme, lhsValue, lhsFocused, lhsCustomValue, lhsEnabled):
if case let .usageCustomPicker(rhsTheme, rhsValue, rhsFocused, rhsCustomValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue, lhsFocused == rhsFocused, lhsCustomValue == rhsCustomValue { if case let .usageCustomPicker(rhsTheme, rhsValue, rhsFocused, rhsCustomValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue, lhsFocused == rhsFocused, lhsCustomValue == rhsCustomValue, lhsEnabled == rhsEnabled {
return true return true
} else { } else {
return false return false
@ -246,7 +279,7 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
case let .titleHeader(_, text): case let .titleHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .title(_, placeholder, value): 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 arguments.updateState { state in
var updatedState = state var updatedState = state
updatedState.title = value updatedState.title = value
@ -255,8 +288,41 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
}, action: {}) }, action: {})
case let .titleInfo(_, text): case let .titleInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .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 arguments.updateState { state in
var updatedState = state var updatedState = state
updatedState.requestApproval = value updatedState.requestApproval = value
@ -267,8 +333,8 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .timeHeader(_, text): case let .timeHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .timePicker(_, value): case let .timePicker(_, value, enabled):
return ItemListInviteLinkTimeLimitItem(theme: presentationData.theme, strings: presentationData.strings, value: value, enabled: true, sectionId: self.section, updated: { value in return ItemListInviteLinkTimeLimitItem(theme: presentationData.theme, strings: presentationData.strings, value: value, enabled: enabled, sectionId: self.section, updated: { value in
arguments.updateState({ state in arguments.updateState({ state in
var updatedState = state var updatedState = state
if value != updatedState.time { if value != updatedState.time {
@ -279,14 +345,14 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
return updatedState return updatedState
}) })
}) })
case let .timeExpiryDate(theme, dateTimeFormat, value, active): case let .timeExpiryDate(theme, dateTimeFormat, value, active, enabled):
let text: String let text: String
if let value = value { if let value = value {
text = stringForMediumDate(timestamp: value, strings: presentationData.strings, dateTimeFormat: dateTimeFormat) text = stringForMediumDate(timestamp: value, strings: presentationData.strings, dateTimeFormat: dateTimeFormat)
} else { } else {
text = presentationData.strings.InviteLink_Create_TimeLimitExpiryDateNever 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.dismissInput()
arguments.updateState { state in arguments.updateState { state in
var updatedState = state var updatedState = state
@ -298,7 +364,8 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
return updatedState 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 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: { return ItemListDatePickerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, date: date, title: title, displayingDateSelection: displayingDateSelection, displayingTimeSelection: displayingTimeSelection, sectionId: self.section, style: .blocks, toggleDateSelection: {
arguments.updateState({ state in arguments.updateState({ state in
@ -329,8 +396,8 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .usageHeader(_, text): case let .usageHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .usagePicker(_, dateTimeFormat, value): case let .usagePicker(_, dateTimeFormat, value, enabled):
return ItemListInviteLinkUsageLimitItem(theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: dateTimeFormat, value: value, enabled: true, sectionId: self.section, updated: { value in return ItemListInviteLinkUsageLimitItem(theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: dateTimeFormat, value: value, enabled: enabled, sectionId: self.section, updated: { value in
arguments.dismissInput() arguments.dismissInput()
arguments.updateState({ state in arguments.updateState({ state in
var updatedState = state var updatedState = state
@ -342,14 +409,14 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
return updatedState return updatedState
}) })
}) })
case let .usageCustomPicker(theme, value, focused, customValue): case let .usageCustomPicker(theme, value, focused, customValue, enabled):
let text: String let text: String
if let value = value, value != 0 { if let value = value, value != 0 {
text = String(value) text = String(value)
} else { } else {
text = focused ? "" : presentationData.strings.InviteLink_Create_UsersLimitNumberOfUsersUnlimited 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 arguments.updateState { state in
var updatedState = state var updatedState = state
if updatedText.isEmpty { 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(.title(presentationData.theme, presentationData.strings.InviteLink_Create_LinkName, state.title))
entries.append(.titleInfo(presentationData.theme, presentationData.strings.InviteLink_Create_LinkNameInfo)) entries.append(.titleInfo(presentationData.theme, presentationData.strings.InviteLink_Create_LinkNameInfo))
if !isPublic { let isEditingEnabled = invite?.pricing == nil
entries.append(.requestApproval(presentationData.theme, presentationData.strings.InviteLink_Create_RequestApproval, state.requestApproval)) let isSubscription = state.subscriptionEnabled
var requestApprovalInfoText = presentationData.strings.InviteLink_Create_RequestApprovalOffInfoChannel if !isGroup {
if state.requestApproval { //TODO:localize
requestApprovalInfoText = isGroup ? presentationData.strings.InviteLink_Create_RequestApprovalOnInfoGroup : presentationData.strings.InviteLink_Create_RequestApprovalOnInfoChannel 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 { } 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(.requestApprovalInfo(presentationData.theme, requestApprovalInfoText))
} }
entries.append(.timeHeader(presentationData.theme, presentationData.strings.InviteLink_Create_TimeLimit.uppercased())) 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) let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
var time: Int32? var time: Int32?
@ -419,21 +507,21 @@ private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state:
} else if let value = state.time.value { } else if let value = state.time.value {
time = currentTime + 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 { 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)) entries.append(.timeInfo(presentationData.theme, presentationData.strings.InviteLink_Create_TimeLimitInfo))
if !state.requestApproval || isPublic { if !state.requestApproval || isPublic {
entries.append(.usageHeader(presentationData.theme, presentationData.strings.InviteLink_Create_UsersLimit.uppercased())) 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 var customValue = false
if case .custom = state.usage { if case .custom = state.usage {
customValue = true 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)) entries.append(.usageInfo(presentationData.theme, presentationData.strings.InviteLink_Create_UsersLimitInfo))
} }
@ -449,6 +537,8 @@ private struct InviteLinkEditControllerState: Equatable {
var usage: InviteLinkUsageLimit var usage: InviteLinkUsageLimit
var time: InviteLinkTimeLimit var time: InviteLinkTimeLimit
var requestApproval = false var requestApproval = false
var subscriptionEnabled = false
var subscriptionFee: Int64?
var pickingExpiryDate = false var pickingExpiryDate = false
var pickingExpiryTime = false var pickingExpiryTime = false
var pickingUsageLimit = false var pickingUsageLimit = false
@ -460,7 +550,7 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio
let actionsDisposable = DisposableSet() let actionsDisposable = DisposableSet()
let initialState: InviteLinkEditControllerState 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 var usageLimit = usageLimit
if let limit = usageLimit, let count = count, count > 0 { if let limit = usageLimit, let count = count, count > 0 {
usageLimit = limit - count usageLimit = limit - count
@ -478,9 +568,9 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio
timeLimit = .unlimited 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 { } 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) let statePromise = ValuePromise(initialState, ignoreRepeated: true)
@ -570,14 +660,21 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio
dismissImpl?() 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 updateState { state in
var updatedState = state var updatedState = state
updatedState.updating = true updatedState.updating = true
return updatedState return updatedState
} }
let expireDate: Int32? var expireDate: Int32?
if case let .custom(value) = state.time { if case let .custom(value) = state.time {
expireDate = value expireDate = value
} else if let value = state.time.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 titleString = state.title.trimmingCharacters(in: .whitespacesAndNewlines)
let title = titleString.isEmpty ? nil : titleString let title = titleString.isEmpty ? nil : titleString
let usageLimit = state.usage.value var usageLimit = state.usage.value
let requestNeeded = state.requestApproval && !isPublic var requestNeeded: Bool? = state.requestApproval && !isPublic
if invite == nil { 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)) |> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic))
|> deliverOnMainQueue).start(next: { invite in |> deliverOnMainQueue).start(next: { invite in
completion?(invite) 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) 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 { } else if let initialInvite = invite, case let .link(link, initialTitle, _, initialRequestApproval, _, _, _, _, initialExpireDate, initialUsageLimit, _, _, _) = initialInvite {
if initialExpireDate == expireDate && initialUsageLimit == usageLimit && initialRequestApproval == requestNeeded { if (initialExpireDate ?? 0) == expireDate && (initialUsageLimit ?? 0) == usageLimit && initialRequestApproval == requestNeeded && (initialTitle ?? "") == title {
completion?(initialInvite) completion?(initialInvite)
dismissImpl?() dismissImpl?()
return 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)) |> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic))
|> deliverOnMainQueue).start(next: { invite in |> deliverOnMainQueue).start(next: { invite in
completion?(invite) completion?(invite)
@ -630,7 +747,7 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio
let previousState = previousState.swap(state) let previousState = previousState.swap(state)
var animateChanges = false 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 animateChanges = true
} }

View File

@ -239,7 +239,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
arguments.createLink() arguments.createLink()
}) })
case let .link(_, _, invite, canEdit, _): 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) arguments.openLink(invite)
} contextAction: { invite, node, gesture in } contextAction: { invite, node, gesture in
arguments.linkContextAction(invite, canEdit, node, gesture) arguments.linkContextAction(invite, canEdit, node, gesture)
@ -253,7 +253,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
arguments.deleteAllRevokedLinks() arguments.deleteAllRevokedLinks()
}) })
case let .revokedLink(_, _, invite): 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) arguments.openLink(invite)
} contextAction: { invite, node, gesture in } contextAction: { invite, node, gesture in
arguments.linkContextAction(invite, false, node, gesture) arguments.linkContextAction(invite, false, node, gesture)

View File

@ -20,6 +20,30 @@ import PresentationDataUtils
import DirectionalPanGesture import DirectionalPanGesture
import UndoUI import UndoUI
import QrCodeUI 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 { class InviteLinkViewInteraction {
let context: AccountContext let context: AccountContext
@ -50,6 +74,8 @@ private struct InviteLinkViewTransaction {
private enum InviteLinkViewEntryId: Hashable { private enum InviteLinkViewEntryId: Hashable {
case link case link
case subscriptionHeader
case subscriptionPricing
case creatorHeader case creatorHeader
case creator case creator
case requestHeader case requestHeader
@ -60,6 +86,8 @@ private enum InviteLinkViewEntryId: Hashable {
private enum InviteLinkViewEntry: Comparable, Identifiable { private enum InviteLinkViewEntry: Comparable, Identifiable {
case link(PresentationTheme, ExportedInvitation) case link(PresentationTheme, ExportedInvitation)
case subscriptionHeader(PresentationTheme, String)
case subscriptionPricing(PresentationTheme, String, String)
case creatorHeader(PresentationTheme, String) case creatorHeader(PresentationTheme, String)
case creator(PresentationTheme, PresentationDateTimeFormat, EnginePeer, Int32) case creator(PresentationTheme, PresentationDateTimeFormat, EnginePeer, Int32)
case requestHeader(PresentationTheme, String, String, Bool) case requestHeader(PresentationTheme, String, String, Bool)
@ -71,6 +99,10 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
switch self { switch self {
case .link: case .link:
return .link return .link
case .subscriptionHeader:
return .subscriptionHeader
case .subscriptionPricing:
return .subscriptionPricing
case .creatorHeader: case .creatorHeader:
return .creatorHeader return .creatorHeader
case .creator: case .creator:
@ -94,6 +126,18 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
} else { } else {
return false 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): case let .creatorHeader(lhsTheme, lhsTitle):
if case let .creatorHeader(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle { if case let .creatorHeader(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle {
return true return true
@ -139,33 +183,47 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
switch rhs { switch rhs {
case .link: case .link:
return false 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: case .creatorHeader, .creator, .requestHeader, .request, .importerHeader, .importer:
return true return true
} }
case .creatorHeader: case .creatorHeader:
switch rhs { switch rhs {
case .link, .creatorHeader: case .link, .subscriptionHeader, .subscriptionPricing, .creatorHeader:
return false return false
case .creator, .requestHeader, .request, .importerHeader, .importer: case .creator, .requestHeader, .request, .importerHeader, .importer:
return true return true
} }
case .creator: case .creator:
switch rhs { switch rhs {
case .link, .creatorHeader, .creator: case .link, .subscriptionHeader, .subscriptionPricing, .creatorHeader, .creator:
return false return false
case .requestHeader, .request, .importerHeader, .importer: case .requestHeader, .request, .importerHeader, .importer:
return true return true
} }
case .requestHeader: case .requestHeader:
switch rhs { switch rhs {
case .link, .creatorHeader, .creator, .requestHeader: case .link, .subscriptionHeader, .subscriptionPricing, .creatorHeader, .creator, .requestHeader:
return false return false
case .request, .importerHeader, .importer: case .request, .importerHeader, .importer:
return true return true
} }
case let .request(lhsIndex, _, _, _, _, _): case let .request(lhsIndex, _, _, _, _, _):
switch rhs { switch rhs {
case .link, .creatorHeader, .creator, .requestHeader: case .link, .subscriptionHeader, .subscriptionPricing, .creatorHeader, .creator, .requestHeader:
return false return false
case let .request(rhsIndex, _, _, _, _, _): case let .request(rhsIndex, _, _, _, _, _):
return lhsIndex < rhsIndex return lhsIndex < rhsIndex
@ -174,14 +232,14 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
} }
case .importerHeader: case .importerHeader:
switch rhs { switch rhs {
case .link, .creatorHeader, .creator, .requestHeader, .request, .importerHeader: case .link, .subscriptionHeader, .subscriptionPricing, .creatorHeader, .creator, .requestHeader, .request, .importerHeader:
return false return false
case .importer: case .importer:
return true return true
} }
case let .importer(lhsIndex, _, _, _, _, _, _): case let .importer(lhsIndex, _, _, _, _, _, _):
switch rhs { switch rhs {
case .link, .creatorHeader, .creator, .importerHeader, .request, .requestHeader: case .link, .subscriptionHeader, .subscriptionPricing, .creatorHeader, .creator, .importerHeader, .request, .requestHeader:
return false return false
case let .importer(rhsIndex, _, _, _, _, _, _): case let .importer(rhsIndex, _, _, _, _, _, _):
return lhsIndex < rhsIndex return lhsIndex < rhsIndex
@ -204,13 +262,22 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
interaction.contextAction(invite, node, gesture) interaction.contextAction(invite, node, gesture)
}, viewAction: { }, 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): case let .creatorHeader(_, title):
return SectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title) return SectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title)
case let .creator(_, dateTimeFormat, peer, date): case let .creator(_, dateTimeFormat, peer, date):
let dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat) 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) 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): case let .importerHeader(_, title, subtitle, expired), let .requestHeader(_, title, subtitle, expired):
let additionalText: SectionHeaderAdditionalText let additionalText: SectionHeaderAdditionalText
if !subtitle.isEmpty { if !subtitle.isEmpty {
@ -230,14 +297,14 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
} else { } else {
dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat) dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat)
} }
return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .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) 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): case let .request(_, _, dateTimeFormat, peer, date, loading):
let dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat) let dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat)
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) 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] = [] var entries: [InviteLinkViewEntry] = []
entries.append(.link(presentationData.theme, invite)) 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(.creatorHeader(presentationData.theme, presentationData.strings.InviteLink_CreatedBy.uppercased()))
entries.append(.creator(presentationData.theme, presentationData.dateTimeFormat, EnginePeer(creatorPeer), date)) entries.append(.creator(presentationData.theme, presentationData.dateTimeFormat, EnginePeer(creatorPeer), date))

View File

@ -7,6 +7,9 @@ import TelegramPresentationData
import ItemListUI import ItemListUI
import ShimmerEffect import ShimmerEffect
import TelegramCore import TelegramCore
import TextNodeWithEntities
import AccountContext
import TextFormat
func invitationAvailability(_ invite: ExportedInvitation) -> CGFloat { func invitationAvailability(_ invite: ExportedInvitation) -> CGFloat {
if case let .link(_, _, _, _, isRevoked, _, date, startDate, expireDate, usageLimit, count, _, _) = invite { if case let .link(_, _, _, _, isRevoked, _, date, startDate, expireDate, usageLimit, count, _, _) = invite {
@ -54,6 +57,7 @@ private enum ItemBackgroundColor: Equatable {
} }
public class ItemListInviteLinkItem: ListViewItem, ItemListItem { public class ItemListInviteLinkItem: ListViewItem, ItemListItem {
let context: AccountContext
let presentationData: ItemListPresentationData let presentationData: ItemListPresentationData
let invite: ExportedInvitation? let invite: ExportedInvitation?
let share: Bool let share: Bool
@ -64,6 +68,7 @@ public class ItemListInviteLinkItem: ListViewItem, ItemListItem {
public let tag: ItemListItemTag? public let tag: ItemListItemTag?
public init( public init(
context: AccountContext,
presentationData: ItemListPresentationData, presentationData: ItemListPresentationData,
invite: ExportedInvitation?, invite: ExportedInvitation?,
share: Bool, share: Bool,
@ -73,6 +78,7 @@ public class ItemListInviteLinkItem: ListViewItem, ItemListItem {
contextAction: ((ExportedInvitation, ASDisplayNode, ContextGesture?) -> Void)?, contextAction: ((ExportedInvitation, ASDisplayNode, ContextGesture?) -> Void)?,
tag: ItemListItemTag? = nil tag: ItemListItemTag? = nil
) { ) {
self.context = context
self.presentationData = presentationData self.presentationData = presentationData
self.invite = invite self.invite = invite
self.share = share self.share = share
@ -170,6 +176,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode {
private let titleNode: TextNode private let titleNode: TextNode
private let subtitleNode: TextNode private let subtitleNode: TextNode
private let pricingNode: TextNodeWithEntities
private var placeholderNode: ShimmerEffectNode? private var placeholderNode: ShimmerEffectNode?
private var absoluteLocation: (CGRect, CGSize)? private var absoluteLocation: (CGRect, CGSize)?
@ -219,6 +226,8 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode {
self.subtitleNode.contentMode = .left self.subtitleNode.contentMode = .left
self.subtitleNode.contentsScale = UIScreen.main.scale self.subtitleNode.contentsScale = UIScreen.main.scale
self.pricingNode = TextNodeWithEntities()
self.highlightedBackgroundNode = ASDisplayNode() self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true self.highlightedBackgroundNode.isLayerBacked = true
@ -237,6 +246,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode {
self.offsetContainerNode.addSubnode(self.iconNode) self.offsetContainerNode.addSubnode(self.iconNode)
self.offsetContainerNode.addSubnode(self.titleNode) self.offsetContainerNode.addSubnode(self.titleNode)
self.offsetContainerNode.addSubnode(self.subtitleNode) self.offsetContainerNode.addSubnode(self.subtitleNode)
self.offsetContainerNode.addSubnode(self.pricingNode.textNode)
self.containerNode.activated = { [weak self] gesture, _ in 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 { 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 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) { public func asyncLayout() -> (_ item: ItemListInviteLinkItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors, _ firstWithHeader: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode)
let makePricingLayout = TextNodeWithEntities.asyncLayout(self.pricingNode)
let currentItem = self.layoutParams?.0 let currentItem = self.layoutParams?.0
@ -299,14 +311,19 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode {
let color: ItemBackgroundColor let color: ItemBackgroundColor
let nextColor: ItemBackgroundColor let nextColor: ItemBackgroundColor
let transitionFraction: CGFloat 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 { if isRevoked {
color = .gray color = .gray
nextColor = .gray nextColor = .gray
transitionFraction = 0.0 transitionFraction = 0.0
} else if expireDate == nil && usageLimit == nil { } else if expireDate == nil && usageLimit == nil {
color = .blue if let _ = pricing {
nextColor = .blue color = .green
nextColor = .green
} else {
color = .blue
nextColor = .blue
}
transitionFraction = 0.0 transitionFraction = 0.0
} else if availability >= 0.5 { } else if availability >= 0.5 {
color = .green color = .green
@ -343,10 +360,10 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode {
let inviteLink = item.invite?.link?.replacingOccurrences(of: "https://", with: "") ?? "" let inviteLink = item.invite?.link?.replacingOccurrences(of: "https://", with: "") ?? ""
var titleText = inviteLink var titleText = inviteLink
var subtitleText: String = "" var subtitleText: String = ""
var pricingAttributedText: NSMutableAttributedString?
var timerValue: TimerNode.Value? var timerValue: TimerNode.Value?
if let invite = item.invite, case let .link(_, title, _, _, _, _, date, startDate, expireDate, usageLimit, count, requestedCount, subscriptionPricing) = invite {
if let invite = item.invite, case let .link(_, title, _, _, _, _, date, startDate, expireDate, usageLimit, count, requestedCount, _) = invite {
if let title = title, !title.isEmpty { if let title = title, !title.isEmpty {
titleText = title titleText = title
} }
@ -375,6 +392,19 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode {
subtitleText += item.presentationData.strings.MemberRequests_PeopleRequestedShort(requestedCount) 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 invite.isRevoked {
if !subtitleText.isEmpty { if !subtitleText.isEmpty {
subtitleText += "" 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 (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 (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 let titleSpacing: CGFloat = 1.0
@ -505,13 +536,18 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor 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 transition = ContainedViewLayoutTransition.immediate
let _ = titleApply() let _ = titleApply()
let _ = subtitleApply() 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 { switch item.style {
case .plain: 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.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.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)) 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 AvatarNode
import TelegramCore import TelegramCore
import AccountContext import AccountContext
import TextNodeWithEntities
private let avatarFont = avatarPlaceholderFont(size: 16.0) private let avatarFont = avatarPlaceholderFont(size: 16.0)
@ -64,12 +65,13 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem {
public let sectionId: ItemListSectionId public let sectionId: ItemListSectionId
let style: ItemListStyle let style: ItemListStyle
let disclosureStyle: ItemListDisclosureStyle let disclosureStyle: ItemListDisclosureStyle
let noInsets: Bool
let action: (() -> Void)? let action: (() -> Void)?
let clearHighlightAutomatically: Bool let clearHighlightAutomatically: Bool
public let tag: ItemListItemTag? public let tag: ItemListItemTag?
public let shimmeringIndex: Int? 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.presentationData = presentationData
self.icon = icon self.icon = icon
self.context = context self.context = context
@ -88,6 +90,7 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem {
self.sectionId = sectionId self.sectionId = sectionId
self.style = style self.style = style
self.disclosureStyle = disclosureStyle self.disclosureStyle = disclosureStyle
self.noInsets = noInsets
self.action = action self.action = action
self.clearHighlightAutomatically = clearHighlightAutomatically self.clearHighlightAutomatically = clearHighlightAutomatically
self.tag = tag self.tag = tag
@ -151,7 +154,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
var avatarNode: AvatarNode? var avatarNode: AvatarNode?
let iconNode: ASImageNode let iconNode: ASImageNode
let titleNode: TextNode let titleNode: TextNodeWithEntities
let titleIconNode: ASImageNode let titleIconNode: ASImageNode
public let labelNode: TextNode public let labelNode: TextNode
var additionalDetailLabelNode: TextNode? var additionalDetailLabelNode: TextNode?
@ -196,8 +199,8 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
self.iconNode.isLayerBacked = true self.iconNode.isLayerBacked = true
self.iconNode.displaysAsynchronously = false self.iconNode.displaysAsynchronously = false
self.titleNode = TextNode() self.titleNode = TextNodeWithEntities()
self.titleNode.isUserInteractionEnabled = false self.titleNode.textNode.isUserInteractionEnabled = false
self.titleIconNode = ASImageNode() self.titleIconNode = ASImageNode()
self.titleIconNode.displayWithoutProcessing = true self.titleIconNode.displayWithoutProcessing = true
@ -224,7 +227,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
super.init(layerBacked: false, dynamicBounce: false) super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode) self.addSubnode(self.titleNode.textNode)
self.addSubnode(self.labelNode) self.addSubnode(self.labelNode)
self.addSubnode(self.arrowNode) self.addSubnode(self.arrowNode)
@ -252,7 +255,8 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
} }
public func asyncLayout() -> (_ item: ItemListDisclosureItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { 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 makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let makeAdditionalDetailLabelLayout = TextNode.asyncLayout(self.additionalDetailLabelNode) let makeAdditionalDetailLabelLayout = TextNode.asyncLayout(self.additionalDetailLabelNode)
@ -329,14 +333,14 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
} }
let contentSize: CGSize let contentSize: CGSize
let insets: UIEdgeInsets var insets: UIEdgeInsets
let separatorHeight = UIScreenPixel let separatorHeight = UIScreenPixel
let itemBackgroundColor: UIColor let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor let itemSeparatorColor: UIColor
var leftInset = 16.0 + params.leftInset var leftInset = 16.0 + params.leftInset
if item.icon != nil { if item.icon != nil {
leftInset += 43.0 leftInset += item.noInsets ? 49.0 : 43.0
} else if item.iconPeer != nil { } else if item.iconPeer != nil {
leftInset += 46.0 leftInset += 46.0
} }
@ -370,7 +374,11 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
maxTitleWidth -= 12.0 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)) 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 itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
contentSize = CGSize(width: params.width, height: height) contentSize = CGSize(width: params.width, height: height)
insets = itemListNeighborsPlainInsets(neighbors) insets = itemListNeighborsPlainInsets(neighbors)
if item.noInsets {
insets.top = 0.0
insets.bottom = 0.0
}
case .blocks: case .blocks:
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
@ -532,7 +544,20 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
} }
let _ = titleApply() 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 _ = labelApply() let _ = labelApply()
switch item.style { 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) 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 let updateBadgeImage = updatedLabelBadgeImage {
if strongSelf.labelBadgeNode.supernode == nil { 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 titleLineWidth: CGFloat = (shimmeringIndex % 2 == 0) ? 120.0 : 80.0
let lineDiameter: CGFloat = 8.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)) 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) 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 AsyncDisplayKit
import SwiftSignalKit import SwiftSignalKit
import TelegramPresentationData import TelegramPresentationData
import TextNodeWithEntities
import AccountContext
private let validIdentifierSet: CharacterSet = { private let validIdentifierSet: CharacterSet = {
var set = CharacterSet(charactersIn: "a".unicodeScalars.first! ... "z".unicodeScalars.first!) var set = CharacterSet(charactersIn: "a".unicodeScalars.first! ... "z".unicodeScalars.first!)
@ -43,6 +45,7 @@ public enum ItemListSingleLineInputAlignment {
} }
public class ItemListSingleLineInputItem: ListViewItem, ItemListItem { public class ItemListSingleLineInputItem: ListViewItem, ItemListItem {
let context: AccountContext?
let presentationData: ItemListPresentationData let presentationData: ItemListPresentationData
let title: NSAttributedString let title: NSAttributedString
let text: String let text: String
@ -65,7 +68,8 @@ public class ItemListSingleLineInputItem: ListViewItem, ItemListItem {
let cleared: (() -> Void)? let cleared: (() -> Void)?
public let tag: ItemListItemTag? 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.presentationData = presentationData
self.title = title self.title = title
self.text = text self.text = text
@ -130,7 +134,7 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg
private let bottomStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode private let maskNode: ASImageNode
private let titleNode: TextNode private let titleNode: TextNodeWithEntities
private let measureTitleSizeNode: TextNode private let measureTitleSizeNode: TextNode
private let textNode: TextFieldNode private let textNode: TextFieldNode
private let clearIconNode: ASImageNode private let clearIconNode: ASImageNode
@ -154,7 +158,7 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg
self.maskNode = ASImageNode() self.maskNode = ASImageNode()
self.titleNode = TextNode() self.titleNode = TextNodeWithEntities()
self.measureTitleSizeNode = TextNode() self.measureTitleSizeNode = TextNode()
self.textNode = TextFieldNode() self.textNode = TextFieldNode()
@ -167,7 +171,7 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg
super.init(layerBacked: false, dynamicBounce: false) super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode) self.addSubnode(self.titleNode.textNode)
self.addSubnode(self.textNode) self.addSubnode(self.textNode)
self.addSubnode(self.clearIconNode) self.addSubnode(self.clearIconNode)
self.addSubnode(self.clearButtonNode) self.addSubnode(self.clearButtonNode)
@ -209,7 +213,8 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg
} }
public func asyncLayout() -> (_ item: ItemListSingleLineInputItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { 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 makeMeasureTitleSizeLayout = TextNode.asyncLayout(self.measureTitleSizeNode)
let currentItem = self.item let currentItem = self.item
@ -241,10 +246,17 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg
} }
let titleString = NSMutableAttributedString(attributedString: item.title) 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)) 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 (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()))
@ -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 strongSelf.textNode.textField.textColor = item.secondaryStyle ? item.presentationData.theme.list.itemSecondaryTextColor : item.presentationData.theme.list.itemPrimaryTextColor
} }
let _ = titleApply() if let titleWithEntitiesApply = titleWithEntitiesLayoutAndApply?.1, let context = item.context {
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((layout.contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) let _ = titleWithEntitiesApply(
TextNodeWithEntities.Arguments(
context: context,
cache: context.animationCache,
renderer: context.animationRenderer,
placeholderColor: item.presentationData.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12),
attemptSynchronous: false
)
)
} else if let titleApply = titleLayoutAndApply?.1 {
let _ = titleApply()
}
strongSelf.titleNode.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((layout.contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size)
let _ = measureTitleSizeApply() let _ = measureTitleSizeApply()

View File

@ -11,10 +11,12 @@ import AlertUI
import PresentationDataUtils import PresentationDataUtils
private final class ResetPasswordControllerArguments { private final class ResetPasswordControllerArguments {
let context: AccountContext
let updateCodeText: (String) -> Void let updateCodeText: (String) -> Void
let openHelp: () -> 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.updateCodeText = updateCodeText
self.openHelp = openHelp self.openHelp = openHelp
} }
@ -128,7 +130,7 @@ public func resetPasswordController(context: AccountContext, emailPattern: Strin
let saveDisposable = MetaDisposable() let saveDisposable = MetaDisposable()
actionsDisposable.add(saveDisposable) actionsDisposable.add(saveDisposable)
let arguments = ResetPasswordControllerArguments(updateCodeText: { updatedText in let arguments = ResetPasswordControllerArguments(context: context, updateCodeText: { updatedText in
updateState { state in updateState { state in
var state = state var state = state
state.code = updatedText state.code = updatedText

View File

@ -15,10 +15,12 @@ import AuthorizationUtils
import PhoneNumberFormat import PhoneNumberFormat
private final class ChangePhoneNumberCodeControllerArguments { private final class ChangePhoneNumberCodeControllerArguments {
let context: AccountContext
let updateEntryText: (String) -> Void let updateEntryText: (String) -> Void
let next: () -> 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.updateEntryText = updateEntryText
self.next = next 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 var initiateCheck = false
updateState { state in updateState { state in
if state.codeText.count < 5 && updatedText.count == 5 { if state.codeText.count < 5 && updatedText.count == 5 {

View File

@ -26,7 +26,7 @@ private func shareLink(for server: ProxyServerSettings) -> String {
return link return link
} }
private final class proxyServerSettingsControllerArguments { private final class ProxyServerSettingsControllerArguments {
let updateState: ((ProxyServerSettingsControllerState) -> ProxyServerSettingsControllerState) -> Void let updateState: ((ProxyServerSettingsControllerState) -> ProxyServerSettingsControllerState) -> Void
let share: () -> Void let share: () -> Void
let usePasteboardSettings: () -> Void let usePasteboardSettings: () -> Void
@ -113,7 +113,7 @@ private enum ProxySettingsEntry: ItemListNodeEntry {
} }
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! proxyServerSettingsControllerArguments let arguments = arguments as! ProxyServerSettingsControllerArguments
switch self { switch self {
case let .usePasteboardSettings(_, title): case let .usePasteboardSettings(_, title):
return ItemListActionItem(presentationData: presentationData, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { 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): case let .credentialsHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .credentialsUsername(_, _, placeholder, text): 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 arguments.updateState { current in
var state = current var state = current
state.username = value state.username = value
@ -306,7 +306,7 @@ func proxyServerSettingsController(sharedContext: SharedAccountContext, context:
var shareImpl: (() -> Void)? var shareImpl: (() -> Void)?
let arguments = proxyServerSettingsControllerArguments(updateState: { f in let arguments = ProxyServerSettingsControllerArguments(updateState: { f in
updateState(f) updateState(f)
}, share: { }, share: {
shareImpl?() shareImpl?()

View File

@ -18,12 +18,14 @@ private enum CreatePasswordField {
} }
private final class CreatePasswordControllerArguments { private final class CreatePasswordControllerArguments {
let context: AccountContext
let updateFieldText: (CreatePasswordField, String) -> Void let updateFieldText: (CreatePasswordField, String) -> Void
let selectNextInputItem: (CreatePasswordEntryTag) -> Void let selectNextInputItem: (CreatePasswordEntryTag) -> Void
let save: () -> Void let save: () -> Void
let cancelEmailConfirmation: () -> 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.updateFieldText = updateFieldText
self.selectNextInputItem = selectNextInputItem self.selectNextInputItem = selectNextInputItem
self.save = save 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 updateState { state in
var state = state var state = state
switch field { switch field {

View File

@ -16,6 +16,7 @@ import PasswordSetupUI
import Markdown import Markdown
private final class TwoStepVerificationUnlockSettingsControllerArguments { private final class TwoStepVerificationUnlockSettingsControllerArguments {
let context: AccountContext
let updatePasswordText: (String) -> Void let updatePasswordText: (String) -> Void
let checkPassword: () -> Void let checkPassword: () -> Void
let openForgotPassword: () -> Void let openForgotPassword: () -> Void
@ -28,7 +29,8 @@ private final class TwoStepVerificationUnlockSettingsControllerArguments {
let declinePasswordReset: () -> Void let declinePasswordReset: () -> Void
let resetPassword: () -> 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.updatePasswordText = updatePasswordText
self.checkPassword = checkPassword self.checkPassword = checkPassword
self.openForgotPassword = openForgotPassword 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 updateState { state in
var state = state var state = state
state.passwordText = updatedText state.passwordText = updatedText

View File

@ -210,7 +210,7 @@ public class BoxedMessage: NSObject {
public class Serialization: NSObject, MTSerialization { public class Serialization: NSObject, MTSerialization {
public func currentLayer() -> UInt { public func currentLayer() -> UInt {
return 185 return 186
} }
public func parseMessage(_ data: Data!) -> Any! { public func parseMessage(_ data: Data!) -> Any! {

View File

@ -341,7 +341,7 @@ private final class StarsContextImpl {
return return
} }
var transactions = state.transactions 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)) 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 media = extendedMedia.flatMap({ $0.compactMap { textMediaAndExpirationTimerFromApiMedia($0, PeerId(0)).media } }) ?? []
let _ = subscriptionPeriod 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 transactionUrl: String?
public let paidMessageId: MessageId? public let paidMessageId: MessageId?
public let media: [Media] public let media: [Media]
public let subscriptionPeriod: Int32?
public init( public init(
flags: Flags, flags: Flags,
@ -487,7 +488,8 @@ public final class StarsContext {
transactionDate: Int32?, transactionDate: Int32?,
transactionUrl: String?, transactionUrl: String?,
paidMessageId: MessageId?, paidMessageId: MessageId?,
media: [Media] media: [Media],
subscriptionPeriod: Int32?
) { ) {
self.flags = flags self.flags = flags
self.id = id self.id = id
@ -501,6 +503,7 @@ public final class StarsContext {
self.transactionUrl = transactionUrl self.transactionUrl = transactionUrl
self.paidMessageId = paidMessageId self.paidMessageId = paidMessageId
self.media = media self.media = media
self.subscriptionPeriod = subscriptionPeriod
} }
public static func == (lhs: Transaction, rhs: Transaction) -> Bool { public static func == (lhs: Transaction, rhs: Transaction) -> Bool {
@ -540,6 +543,9 @@ public final class StarsContext {
if !areMediaArraysEqual(lhs.media, rhs.media) { if !areMediaArraysEqual(lhs.media, rhs.media) {
return false return false
} }
if lhs.subscriptionPeriod != rhs.subscriptionPeriod {
return false
}
return true return true
} }
} }
@ -1160,8 +1166,8 @@ public struct StarsSubscriptionPricing: Codable, Equatable {
try container.encode(self.amount, forKey: .amount) try container.encode(self.amount, forKey: .amount)
} }
public static let monthPeriod = 2592000 public static let monthPeriod: Int32 = 2592000
public static let testPeriod = 300 public static let testPeriod: Int32 = 300
} }
extension StarsSubscriptionPricing { extension StarsSubscriptionPricing {

View File

@ -203,7 +203,27 @@ private final class StarsTransactionSheetContent: CombinedComponent {
var delayedCloseOnOpenPeer = true var delayedCloseOnOpenPeer = true
switch subject { switch subject {
case let .transaction(transaction, parentPeer): 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 titleText = strings.Stars_Gift_Received_Title
descriptionText = strings.Stars_Gift_Received_Text descriptionText = strings.Stars_Gift_Received_Text
count = transaction.count count = transaction.count

View File

@ -219,6 +219,9 @@ final class StarsTransactionsListPanelComponent: Component {
itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)
if item.flags.contains(.isGift) { if item.flags.contains(.isGift) {
itemSubtitle = environment.strings.Stars_Intro_Transaction_Gift_Title itemSubtitle = environment.strings.Stars_Intro_Transaction_Gift_Title
} else if let _ = item.subscriptionPeriod {
//TODO:localize
itemSubtitle = "Monthly subscription fee"
} else { } else {
itemSubtitle = nil itemSubtitle = nil
} }

View File

@ -18,6 +18,8 @@ import ListSectionComponent
import BundleIconComponent import BundleIconComponent
import TextFormat import TextFormat
import UndoUI import UndoUI
import ListActionItemComponent
import StarsAvatarComponent
final class StarsTransactionsScreenComponent: Component { final class StarsTransactionsScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -574,7 +576,44 @@ final class StarsTransactionsScreenComponent: Component {
contentHeight += balanceSize.height contentHeight += balanceSize.height
contentHeight += 44.0 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 { if !subscriptionsItems.isEmpty {
//TODO:localize //TODO:localize

View File

@ -55,7 +55,7 @@ private final class SheetContent: CombinedComponent {
let background = Child(RoundedRectangle.self) let background = Child(RoundedRectangle.self)
let closeButton = Child(Button.self) let closeButton = Child(Button.self)
let title = Child(Text.self) let title = Child(Text.self)
let urlSection = Child(ListSectionComponent.self) let amountSection = Child(ListSectionComponent.self)
let button = Child(ButtonComponent.self) let button = Child(ButtonComponent.self)
let balanceTitle = Child(MultilineTextComponent.self) let balanceTitle = Child(MultilineTextComponent.self)
let balanceValue = Child(MultilineTextComponent.self) let balanceValue = Child(MultilineTextComponent.self)
@ -246,7 +246,7 @@ private final class SheetContent: CombinedComponent {
amountFooter = nil amountFooter = nil
} }
let urlSection = urlSection.update( let amountSection = amountSection.update(
component: ListSectionComponent( component: ListSectionComponent(
theme: theme, theme: theme,
header: AnyComponent(MultilineTextComponent( header: AnyComponent(MultilineTextComponent(
@ -283,12 +283,12 @@ private final class SheetContent: CombinedComponent {
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude),
transition: context.transition transition: context.transition
) )
context.add(urlSection context.add(amountSection
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + urlSection.size.height / 2.0)) .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + amountSection.size.height / 2.0))
.clipsToBounds(true) .clipsToBounds(true)
.cornerRadius(10.0) .cornerRadius(10.0)
) )
contentSize.height += urlSection.size.height contentSize.height += amountSection.size.height
contentSize.height += 32.0 contentSize.height += 32.0
let buttonString: String let buttonString: String

View File

@ -250,12 +250,7 @@ func openResolvedUrlImpl(
present(controller, nil) present(controller, nil)
case let .instantView(webpage, anchor): case let .instantView(webpage, anchor):
let sourceLocation = InstantPageSourceLocation(userLocation: .other, peerType: .channel) let sourceLocation = InstantPageSourceLocation(userLocation: .other, peerType: .channel)
let pageController: ViewController let pageController = BrowserScreen(context: context, subject: .instantPage(webPage: webpage, anchor: anchor, sourceLocation: sourceLocation))
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)
}
navigationController?.pushViewController(pageController) navigationController?.pushViewController(pageController)
case let .join(link): case let .join(link):
dismissInput() dismissInput()
@ -288,6 +283,55 @@ func openResolvedUrlImpl(
openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil)) openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil))
case let .peek(peer, deadline): case let .peek(peer, deadline):
openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: ChatPeekTimeout(deadline: deadline, linkData: link))) 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: default:
present(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in present(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in
openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: peekData)) openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: peekData))