Giveaway improvements

This commit is contained in:
Ilya Laktyushin 2023-10-08 14:12:39 +04:00
parent d1f2b29370
commit 713336a13f
97 changed files with 3941 additions and 1182 deletions
Telegram/Telegram-iOS/en.lproj
submodules
AccountContext/Sources
AttachmentUI
ChatListUI/Sources/Node
ComponentFlow/Source/Components
Components/LottieAnimationComponent/Sources
InAppPurchaseManager/Sources
ItemListUI/Sources
LegacyComponents
LegacyMediaPickerUI
LegacyUI
MediaPickerUI/Sources
PremiumUI
StatisticsUI/Sources
TelegramApi/Sources
TelegramCore/Sources
TelegramPresentationData/Sources/Resources
TelegramStringFormatting/Sources
TelegramUI
WebSearchUI/Sources
WebUI/Sources

@ -10095,3 +10095,8 @@ Sorry for the inconvenience.";
"PremiumGift.LabelRecipients_1" = "1 recipient";
"PremiumGift.LabelRecipients_any" = "%d recipients";
"Message.Giveaway" = "Giveaway";
"GiftLink.LinkSharedToChat" = "Gift link forwarded to **%@**";
"GiftLink.LinkSharedToSavedMessages" = "Gift link forwarded to **Saved Messages**";

@ -910,6 +910,8 @@ public protocol SharedAccountContext: AnyObject {
func makeInstalledStickerPacksController(context: AccountContext, mode: InstalledStickerPacksControllerMode, forceTheme: PresentationTheme?) -> ViewController
func makeChannelStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peerId: EnginePeer.Id, boosts: Bool, boostStatus: ChannelBoostStatus?, statsDatacenterId: Int32) -> ViewController
func makeDebugSettingsController(context: AccountContext?) -> ViewController?
func navigateToCurrentCall()

@ -0,0 +1,50 @@
import Foundation
import UIKit
public struct AttachmentMainButtonState {
public enum Background {
case color(UIColor)
case premium
}
public enum Progress: Equatable {
case none
case side
case center
}
public enum Font: Equatable {
case regular
case bold
}
public let text: String?
public let font: Font
public let background: Background
public let textColor: UIColor
public let isVisible: Bool
public let progress: Progress
public let isEnabled: Bool
public init(
text: String?,
font: Font,
background: Background,
textColor: UIColor,
isVisible: Bool,
progress: Progress,
isEnabled: Bool
) {
self.text = text
self.font = font
self.background = background
self.textColor = textColor
self.isVisible = isVisible
self.progress = progress
self.isEnabled = isEnabled
}
public static var initial: AttachmentMainButtonState {
return AttachmentMainButtonState(text: nil, font: .bold, background: .color(.clear), textColor: .clear, isVisible: false, progress: .none, isEnabled: false)
}
}

@ -37,6 +37,8 @@ swift_library(
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
"//submodules/ShimmerEffect:ShimmerEffect",
"//submodules/TextFormat:TextFormat",
"//submodules/TelegramUI/Components/LegacyMessageInputPanel",
"//submodules/TelegramUI/Components/LegacyMessageInputPanelInputView",
],
visibility = [
"//visibility:public",

@ -11,7 +11,8 @@ import AccountContext
import TelegramStringFormatting
import UIKitRuntimeUtils
import MediaResources
import AttachmentTextInputPanelNode
import LegacyMessageInputPanel
import LegacyMessageInputPanelInputView
public enum AttachmentButtonType: Equatable {
case gallery
@ -184,7 +185,7 @@ public class AttachmentController: ViewController {
private let initialButton: AttachmentButtonType
private let fromMenu: Bool
private let hasTextInput: Bool
private let makeEntityInputView: () -> AttachmentTextInputPanelInputView?
private let makeEntityInputView: () -> LegacyMessageInputPanelInputView?
public var animateAppearance: Bool = false
public var willDismiss: () -> Void = {}
@ -209,7 +210,7 @@ public class AttachmentController: ViewController {
private let dim: ASDisplayNode
private let shadowNode: ASImageNode
fileprivate let container: AttachmentContainer
private let makeEntityInputView: () -> AttachmentTextInputPanelInputView?
private let makeEntityInputView: () -> LegacyMessageInputPanelInputView?
let panel: AttachmentPanel
private var currentType: AttachmentButtonType?
@ -279,7 +280,7 @@ public class AttachmentController: ViewController {
private let wrapperNode: ASDisplayNode
init(controller: AttachmentController, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) {
init(controller: AttachmentController, makeEntityInputView: @escaping () -> LegacyMessageInputPanelInputView?) {
self.controller = controller
self.makeEntityInputView = makeEntityInputView
@ -910,7 +911,7 @@ public class AttachmentController: ViewController {
public var getSourceRect: (() -> CGRect?)?
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, chatLocation: ChatLocation?, isScheduledMessages: Bool = false, buttons: [AttachmentButtonType], initialButton: AttachmentButtonType = .gallery, fromMenu: Bool = false, hasTextInput: Bool = true, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView? = { return nil}) {
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, chatLocation: ChatLocation?, isScheduledMessages: Bool = false, buttons: [AttachmentButtonType], initialButton: AttachmentButtonType = .gallery, fromMenu: Bool = false, hasTextInput: Bool = true, makeEntityInputView: @escaping () -> LegacyMessageInputPanelInputView? = { return nil}) {
self.context = context
self.updatedPresentationData = updatedPresentationData
self.chatLocation = chatLocation

@ -19,6 +19,8 @@ import MediaResources
import MultilineTextComponent
import ShimmerEffect
import TextFormat
import LegacyMessageInputPanel
import LegacyMessageInputPanelInputView
private let buttonSize = CGSize(width: 88.0, height: 49.0)
private let smallButtonWidth: CGFloat = 69.0
@ -375,54 +377,6 @@ private final class LoadingProgressNode: ASDisplayNode {
}
}
public struct AttachmentMainButtonState {
public enum Background {
case color(UIColor)
case premium
}
public enum Progress: Equatable {
case none
case side
case center
}
public enum Font: Equatable {
case regular
case bold
}
public let text: String?
public let font: Font
public let background: Background
public let textColor: UIColor
public let isVisible: Bool
public let progress: Progress
public let isEnabled: Bool
public init(
text: String?,
font: Font,
background: Background,
textColor: UIColor,
isVisible: Bool,
progress: Progress,
isEnabled: Bool
) {
self.text = text
self.font = font
self.background = background
self.textColor = textColor
self.isVisible = isVisible
self.progress = progress
self.isEnabled = isEnabled
}
static var initial: AttachmentMainButtonState {
return AttachmentMainButtonState(text: nil, font: .bold, background: .color(.clear), textColor: .clear, isVisible: false, progress: .none, isEnabled: false)
}
}
private final class MainButtonNode: HighlightTrackingButtonNode {
private var state: AttachmentMainButtonState
private var size: CGSize?
@ -734,7 +688,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
private var presentationInterfaceState: ChatPresentationInterfaceState
private var interfaceInteraction: ChatPanelInterfaceInteraction?
private let makeEntityInputView: () -> AttachmentTextInputPanelInputView?
private let makeEntityInputView: () -> LegacyMessageInputPanelInputView?
private let containerNode: ASDisplayNode
private let backgroundNode: NavigationBackgroundNode
@ -742,7 +696,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
private let separatorNode: ASDisplayNode
private var buttonViews: [Int: ComponentHostView<Empty>] = [:]
private var textInputPanelNode: AttachmentTextInputPanelNode?
private var textInputPanelNode: LegacyMessageInputPanelNode?
private var progressNode: LoadingProgressNode?
private var mainButtonNode: MainButtonNode
@ -776,7 +730,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
var mainButtonPressed: () -> Void = { }
init(context: AccountContext, chatLocation: ChatLocation?, isScheduledMessages: Bool, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) {
init(context: AccountContext, chatLocation: ChatLocation?, isScheduledMessages: Bool, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, makeEntityInputView: @escaping () -> LegacyMessageInputPanelInputView?) {
self.context = context
self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
self.isScheduledMessages = isScheduledMessages
@ -937,7 +891,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
})
}
if let textInputPanelNode = strongSelf.textInputPanelNode {
textInputPanelNode.ensureFocused()
textInputPanelNode.activateInput()
}
strongSelf.updateChatPresentationInterfaceState(animated: true, { state in
return state.updatedInputMode({ _ in return inputMode }).updatedInterfaceState({
@ -950,52 +904,53 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
}
}, reportPeerIrrelevantGeoLocation: {
}, displaySlowmodeTooltip: { _, _ in
}, displaySendMessageOptions: { [weak self] node, gesture in
guard let strongSelf = self, let textInputPanelNode = strongSelf.textInputPanelNode else {
return
}
textInputPanelNode.loadTextInputNodeIfNeeded()
guard let textInputNode = textInputPanelNode.textInputNode, let peerId = chatLocation?.peerId else {
return
}
var hasEntityKeyboard = false
if case .media = strongSelf.presentationInterfaceState.inputMode {
hasEntityKeyboard = true
}
let _ = (strongSelf.context.account.viewTracker.peerView(peerId)
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak self] peerView in
guard let strongSelf = self, let peer = peerViewMainPeer(peerView) else {
return
}
var sendWhenOnlineAvailable = false
if let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence, case let .present(until) = presence.status {
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
if currentTime > until {
sendWhenOnlineAvailable = true
}
}
if peer.id.namespace == Namespaces.Peer.CloudUser && peer.id.id._internalGetInt64Value() == 777000 {
sendWhenOnlineAvailable = false
}
let controller = ChatSendMessageActionSheetController(context: strongSelf.context, peerId: strongSelf.presentationInterfaceState.chatLocation.peerId, forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds, hasEntityKeyboard: hasEntityKeyboard, gesture: gesture, sourceSendButton: node, textInputNode: textInputNode, attachment: true, canSendWhenOnline: sendWhenOnlineAvailable, completion: {
}, sendMessage: { [weak textInputPanelNode] mode in
switch mode {
case .generic:
textInputPanelNode?.sendMessage(.generic)
case .silently:
textInputPanelNode?.sendMessage(.silent)
case .whenOnline:
textInputPanelNode?.sendMessage(.whenOnline)
}
}, schedule: { [weak textInputPanelNode] in
textInputPanelNode?.sendMessage(.schedule)
})
controller.emojiViewProvider = textInputPanelNode.emojiViewProvider
strongSelf.presentInGlobalOverlay(controller)
})
}, displaySendMessageOptions: { node, gesture in
let _ = node
let _ = gesture
// guard let strongSelf = self, let textInputPanelNode = strongSelf.textInputPanelNode else {
// return
// }
// guard let textInputNode = textInputPanelNode.textInputNode, let peerId = chatLocation?.peerId else {
// return
// }
//
// var hasEntityKeyboard = false
// if case .media = strongSelf.presentationInterfaceState.inputMode {
// hasEntityKeyboard = true
// }
// let _ = (strongSelf.context.account.viewTracker.peerView(peerId)
// |> take(1)
// |> deliverOnMainQueue).startStandalone(next: { [weak self] peerView in
// guard let strongSelf = self, let peer = peerViewMainPeer(peerView) else {
// return
// }
// var sendWhenOnlineAvailable = false
// if let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence, case let .present(until) = presence.status {
// let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
// if currentTime > until {
// sendWhenOnlineAvailable = true
// }
// }
// if peer.id.namespace == Namespaces.Peer.CloudUser && peer.id.id._internalGetInt64Value() == 777000 {
// sendWhenOnlineAvailable = false
// }
//
// let controller = ChatSendMessageActionSheetController(context: strongSelf.context, peerId: strongSelf.presentationInterfaceState.chatLocation.peerId, forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds, hasEntityKeyboard: hasEntityKeyboard, gesture: gesture, sourceSendButton: node, textInputNode: textInputNode, attachment: true, canSendWhenOnline: sendWhenOnlineAvailable, completion: {
// }, sendMessage: { [weak textInputPanelNode] mode in
// switch mode {
// case .generic:
// textInputPanelNode?.sendMessage(.generic)
// case .silently:
// textInputPanelNode?.sendMessage(.silent)
// case .whenOnline:
// textInputPanelNode?.sendMessage(.whenOnline)
// }
// }, schedule: { [weak textInputPanelNode] in
// textInputPanelNode?.sendMessage(.schedule)
// })
// controller.emojiViewProvider = textInputPanelNode.emojiViewProvider
// strongSelf.presentInGlobalOverlay(controller)
// })
}, openScheduledMessages: {
}, openPeersNearby: {
}, displaySearchResultsTooltip: { _, _ in
@ -1075,10 +1030,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
}
func updateCaption(_ caption: NSAttributedString) {
if !caption.string.isEmpty {
self.loadTextNodeIfNeeded()
}
self.updateChatPresentationInterfaceState(animated: false, { $0.updatedInterfaceState { $0.withUpdatedComposeInputState(ChatTextInputState(inputText: caption))} })
self.textInputPanelNode?.setCaption(caption)
}
private func updateChatPresentationInterfaceState(animated: Bool = true, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) {
@ -1086,16 +1038,16 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
}
private func updateChatPresentationInterfaceState(transition: ContainedViewLayoutTransition, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion externalCompletion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) {
let presentationInterfaceState = f(self.presentationInterfaceState)
let updateInputTextState = self.presentationInterfaceState.interfaceState.effectiveInputState != presentationInterfaceState.interfaceState.effectiveInputState
self.presentationInterfaceState = presentationInterfaceState
if let textInputPanelNode = self.textInputPanelNode, updateInputTextState {
textInputPanelNode.updateInputTextState(presentationInterfaceState.interfaceState.effectiveInputState, animated: transition.isAnimated)
self.textUpdated(presentationInterfaceState.interfaceState.effectiveInputState.inputText)
}
// let presentationInterfaceState = f(self.presentationInterfaceState)
// let updateInputTextState = self.presentationInterfaceState.interfaceState.effectiveInputState != presentationInterfaceState.interfaceState.effectiveInputState
//
// self.presentationInterfaceState = presentationInterfaceState
//
// if let textInputPanelNode = self.textInputPanelNode, updateInputTextState {
// textInputPanelNode.updateInputTextState(presentationInterfaceState.interfaceState.effectiveInputState, animated: transition.isAnimated)
//
// self.textUpdated(presentationInterfaceState.interfaceState.effectiveInputState.inputText)
// }
}
func updateSelectedIndex(_ index: Int) {
@ -1250,32 +1202,52 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
private func loadTextNodeIfNeeded() {
if let _ = self.textInputPanelNode {
} else {
let textInputPanelNode = AttachmentTextInputPanelNode(context: self.context, presentationInterfaceState: self.presentationInterfaceState, isAttachment: true, isScheduledMessages: self.isScheduledMessages, presentController: { [weak self] c in
if let strongSelf = self {
strongSelf.present(c)
}
}, makeEntityInputView: self.makeEntityInputView)
textInputPanelNode.interfaceInteraction = self.interfaceInteraction
textInputPanelNode.sendMessage = { [weak self] mode in
if let strongSelf = self {
strongSelf.sendMessagePressed(mode)
}
}
textInputPanelNode.focusUpdated = { [weak self] focus in
if let strongSelf = self, focus {
strongSelf.beganTextEditing()
}
}
textInputPanelNode.updateHeight = { [weak self] _ in
if let strongSelf = self {
strongSelf.requestLayout()
}
}
let textInputPanelNode = LegacyMessageInputPanelNode(
context: self.context,
chatLocation: self.presentationInterfaceState.chatLocation,
isScheduledMessages: self.isScheduledMessages,
present: { [weak self] c in
if let strongSelf = self {
strongSelf.present(c)
}
},
presentInGlobalOverlay: { [weak self] c in
if let strongSelf = self {
strongSelf.present(c)
}
},
makeEntityInputView: self.makeEntityInputView
)
self.addSubnode(textInputPanelNode)
self.textInputPanelNode = textInputPanelNode
textInputPanelNode.alpha = self.isSelecting ? 1.0 : 0.0
textInputPanelNode.isUserInteractionEnabled = self.isSelecting
// let textInputPanelNode = AttachmentTextInputPanelNode(context: self.context, presentationInterfaceState: self.presentationInterfaceState, isAttachment: true, isScheduledMessages: self.isScheduledMessages, presentController: { [weak self] c in
// if let strongSelf = self {
// strongSelf.present(c)
// }
// }, makeEntityInputView: self.makeEntityInputView)
// textInputPanelNode.interfaceInteraction = self.interfaceInteraction
// textInputPanelNode.sendMessage = { [weak self] mode in
// if let strongSelf = self {
// strongSelf.sendMessagePressed(mode)
// }
// }
// textInputPanelNode.focusUpdated = { [weak self] focus in
// if let strongSelf = self, focus {
// strongSelf.beganTextEditing()
// }
// }
// textInputPanelNode.updateHeight = { [weak self] _ in
// if let strongSelf = self {
// strongSelf.requestLayout()
// }
// }
// self.addSubnode(textInputPanelNode)
// self.textInputPanelNode = textInputPanelNode
//
// textInputPanelNode.alpha = self.isSelecting ? 1.0 : 0.0
// textInputPanelNode.isUserInteractionEnabled = self.isSelecting
}
}
@ -1448,7 +1420,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
if isSelecting {
self.loadTextNodeIfNeeded()
} else {
self.textInputPanelNode?.ensureUnfocused()
let _ = self.textInputPanelNode?.dismissInput()
}
var textPanelHeight: CGFloat = 0.0
if let textInputPanelNode = self.textInputPanelNode {
@ -1458,7 +1430,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
if textInputPanelNode.frame.width.isZero {
panelTransition = .immediate
}
let panelHeight = textInputPanelNode.updateLayout(width: layout.size.width, leftInset: insets.left + layout.safeInsets.left, rightInset: insets.right + layout.safeInsets.right, bottomInset: 0.0, additionalSideInsets: UIEdgeInsets(), maxHeight: layout.size.height / 2.0, isSecondary: false, transition: panelTransition, interfaceState: self.presentationInterfaceState, metrics: layout.metrics, isMediaInputExpanded: false)
let panelHeight = textInputPanelNode.updateLayout(width: layout.size.width, leftInset: insets.left + layout.safeInsets.left, rightInset: insets.right + layout.safeInsets.right, bottomInset: 0.0, keyboardHeight: layout.inputHeight ?? 0.0, additionalSideInsets: UIEdgeInsets(), maxHeight: layout.size.height / 2.0, isSecondary: false, transition: panelTransition, metrics: layout.metrics, isMediaInputExpanded: false)
let panelFrame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: panelHeight)
if textInputPanelNode.frame.width.isZero {
textInputPanelNode.frame = panelFrame

@ -305,6 +305,8 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
} else {
messageText = strings.Notification_Story
}
case _ as TelegramMediaGiveaway:
messageText = strings.Message_Giveaway
default:
break
}

@ -11,11 +11,13 @@ public final class List<ChildEnvironment: Equatable>: CombinedComponent {
private let items: [AnyComponentWithIdentity<ChildEnvironment>]
private let direction: Direction
private let centerAlignment: Bool
private let appear: Transition.Appear
public init(_ items: [AnyComponentWithIdentity<ChildEnvironment>], direction: Direction = .vertical, appear: Transition.Appear = .default()) {
public init(_ items: [AnyComponentWithIdentity<ChildEnvironment>], direction: Direction = .vertical, centerAlignment: Bool = false, appear: Transition.Appear = .default()) {
self.items = items
self.direction = direction
self.centerAlignment = centerAlignment
self.appear = appear
}
@ -26,6 +28,9 @@ public final class List<ChildEnvironment: Equatable>: CombinedComponent {
if lhs.direction != rhs.direction {
return false
}
if lhs.centerAlignment != rhs.centerAlignment {
return false
}
return true
}
@ -42,6 +47,10 @@ public final class List<ChildEnvironment: Equatable>: CombinedComponent {
transition: context.transition
)
}
let maxWidth: CGFloat = updatedChildren.reduce(CGFloat(0.0)) { partialResult, child in
return max(partialResult, child.size.width)
}
var nextOrigin: CGFloat = 0.0
for child in updatedChildren {
@ -51,7 +60,13 @@ public final class List<ChildEnvironment: Equatable>: CombinedComponent {
position = CGPoint(x: nextOrigin + child.size.width / 2.0, y: child.size.height / 2.0)
nextOrigin += child.size.width
case .vertical:
position = CGPoint(x: child.size.width / 2.0, y: nextOrigin + child.size.height / 2.0)
let originX: CGFloat
if context.component.centerAlignment {
originX = maxWidth / 2.0
} else {
originX = child.size.width / 2.0
}
position = CGPoint(x: originX, y: nextOrigin + child.size.height / 2.0)
nextOrigin += child.size.height
}
context.add(child
@ -63,8 +78,14 @@ public final class List<ChildEnvironment: Equatable>: CombinedComponent {
switch context.component.direction {
case .horizontal:
return CGSize(width: min(context.availableSize.width, nextOrigin), height: context.availableSize.height)
case.vertical:
return CGSize(width: context.availableSize.width, height: min(context.availableSize.height, nextOrigin))
case .vertical:
let width: CGFloat
if context.component.centerAlignment {
width = maxWidth
} else {
width = context.availableSize.width
}
return CGSize(width: width, height: min(context.availableSize.height, nextOrigin))
}
}
}

@ -224,6 +224,9 @@ public final class LottieAnimationComponent: Component {
if updateColors, let animationView = self.animationView {
if let value = component.colors["__allcolors__"] {
for keypath in animationView.allKeypaths(predicate: { $0.keys.last == "Colors" }) {
animationView.setValueProvider(GradientValueProvider([value.lottieColorValue, value.lottieColorValue]), keypath: AnimationKeypath(keypath: keypath))
}
for keypath in animationView.allKeypaths(predicate: { $0.keys.last == "Color" }) {
animationView.setValueProvider(ColorValueProvider(value.lottieColorValue), keypath: AnimationKeypath(keypath: keypath))
}

@ -18,7 +18,15 @@ private let productIdentifiers = [
"org.telegram.telegramPremium.threeMonths.code_x1",
"org.telegram.telegramPremium.sixMonths.code_x1",
"org.telegram.telegramPremium.twelveMonths.code_x1"
"org.telegram.telegramPremium.twelveMonths.code_x1",
"org.telegram.telegramPremium.threeMonths.code_x5",
"org.telegram.telegramPremium.sixMonths.code_x5",
"org.telegram.telegramPremium.twelveMonths.code_x5",
"org.telegram.telegramPremium.threeMonths.code_x10",
"org.telegram.telegramPremium.sixMonths.code_x10",
"org.telegram.telegramPremium.twelveMonths.code_x10"
]
private extension NSDecimalNumber {
@ -253,7 +261,7 @@ public final class InAppPurchaseManager: NSObject {
}
}
public func buyProduct(_ product: Product, purpose: AppStoreTransactionPurpose) -> Signal<PurchaseState, PurchaseError> {
public func buyProduct(_ product: Product, quantity: Int32 = 1, purpose: AppStoreTransactionPurpose) -> Signal<PurchaseState, PurchaseError> {
if !self.canMakePayments {
return .fail(.cantMakePayments)
}
@ -266,7 +274,7 @@ public final class InAppPurchaseManager: NSObject {
let payment = SKMutablePayment(product: product.skProduct)
payment.applicationUsername = accountPeerId
payment.quantity = purpose.quantity
payment.quantity = Int(quantity)
SKPaymentQueue.default().add(payment)
let productIdentifier = payment.productIdentifier
@ -564,6 +572,8 @@ private final class PendingInAppPurchaseState: Codable {
case peer
case peers
case boostPeer
case additionalPeerIds
case onlyNewSubscribers
case randomId
case untilDate
}
@ -582,7 +592,7 @@ private final class PendingInAppPurchaseState: Codable {
case restore
case gift(peerId: EnginePeer.Id)
case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?)
case giveaway(boostPeer: EnginePeer.Id, randomId: Int64, untilDate: Int32)
case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], onlyNewSubscribers: Bool, randomId: Int64, untilDate: Int32)
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
@ -607,6 +617,8 @@ private final class PendingInAppPurchaseState: Codable {
case .giveaway:
self = .giveaway(
boostPeer: EnginePeer.Id(try container.decode(Int64.self, forKey: .boostPeer)),
additionalPeerIds: try container.decode([Int64].self, forKey: .randomId).map { EnginePeer.Id($0) },
onlyNewSubscribers: try container.decode(Bool.self, forKey: .onlyNewSubscribers),
randomId: try container.decode(Int64.self, forKey: .randomId),
untilDate: try container.decode(Int32.self, forKey: .untilDate)
)
@ -632,9 +644,11 @@ private final class PendingInAppPurchaseState: Codable {
try container.encode(PurposeType.giftCode.rawValue, forKey: .type)
try container.encode(peerIds.map { $0.toInt64() }, forKey: .peers)
try container.encodeIfPresent(boostPeer?.toInt64(), forKey: .boostPeer)
case let .giveaway(boostPeer, randomId, untilDate):
case let .giveaway(boostPeer, additionalPeerIds, onlyNewSubscribers, randomId, untilDate):
try container.encode(PurposeType.giveaway.rawValue, forKey: .type)
try container.encode(boostPeer.toInt64(), forKey: .boostPeer)
try container.encode(additionalPeerIds.map { $0.toInt64() }, forKey: .additionalPeerIds)
try container.encode(onlyNewSubscribers, forKey: .onlyNewSubscribers)
try container.encode(randomId, forKey: .randomId)
try container.encode(untilDate, forKey: .untilDate)
}
@ -652,8 +666,8 @@ private final class PendingInAppPurchaseState: Codable {
self = .gift(peerId: peerId)
case let .giftCode(peerIds, boostPeer, _, _):
self = .giftCode(peerIds: peerIds, boostPeer: boostPeer)
case let .giveaway(boostPeer, randomId, untilDate, _, _):
self = .giveaway(boostPeer: boostPeer, randomId: randomId, untilDate: untilDate)
case let .giveaway(boostPeer, additionalPeerIds, onlyNewSubscribers, randomId, untilDate, _, _):
self = .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, onlyNewSubscribers: onlyNewSubscribers, randomId: randomId, untilDate: untilDate)
}
}
@ -670,19 +684,8 @@ private final class PendingInAppPurchaseState: Codable {
return .gift(peerId: peerId, currency: currency, amount: amount)
case let .giftCode(peerIds, boostPeer):
return .giftCode(peerIds: peerIds, boostPeer: boostPeer, currency: currency, amount: amount)
case let .giveaway(boostPeer, randomId, untilDate):
return .giveaway(boostPeer: boostPeer, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount)
}
}
var quantity: Int {
switch self {
case .subscription, .upgrade, .restore, .gift:
return 1
case let .giftCode(peerIds, _):
return peerIds.count
case .giveaway:
return 1
case let .giveaway(boostPeer, additionalPeerIds, onlyNewSubscribers, randomId, untilDate):
return .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, onlyNewSubscribers: onlyNewSubscribers, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount)
}
}
}

@ -17,3 +17,19 @@ open class ItemListControllerFooterItemNode: ASDisplayNode {
}
}
public protocol ItemListControllerHeaderItem {
func isEqual(to: ItemListControllerHeaderItem) -> Bool
func node(current: ItemListControllerHeaderItemNode?) -> ItemListControllerHeaderItemNode
}
open class ItemListControllerHeaderItemNode: ASDisplayNode {
open func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat {
return 0.0
}
open func updateContentOffset(_ contentOffset: CGFloat, transition: ContainedViewLayoutTransition) {
}
}

@ -127,6 +127,7 @@ private struct ItemListNodeTransition {
let emptyStateItem: ItemListControllerEmptyStateItem?
let searchItem: ItemListControllerSearch?
let toolbarItem: ItemListToolbarItem?
let headerItem: ItemListControllerHeaderItem?
let footerItem: ItemListControllerFooterItem?
let focusItemTag: ItemListItemTag?
let ensureVisibleItemTag: ItemListItemTag?
@ -146,6 +147,7 @@ public final class ItemListNodeState {
let emptyStateItem: ItemListControllerEmptyStateItem?
let searchItem: ItemListControllerSearch?
let toolbarItem: ItemListToolbarItem?
let headerItem: ItemListControllerHeaderItem?
let footerItem: ItemListControllerFooterItem?
let animateChanges: Bool
let crossfadeState: Bool
@ -154,13 +156,14 @@ public final class ItemListNodeState {
let ensureVisibleItemTag: ItemListItemTag?
let initialScrollToItem: ListViewScrollToItem?
public init<T: ItemListNodeEntry>(presentationData: ItemListPresentationData, entries: [T], style: ItemListStyle, focusItemTag: ItemListItemTag? = nil, ensureVisibleItemTag: ItemListItemTag? = nil, emptyStateItem: ItemListControllerEmptyStateItem? = nil, searchItem: ItemListControllerSearch? = nil, toolbarItem: ItemListToolbarItem? = nil, footerItem: ItemListControllerFooterItem? = nil, initialScrollToItem: ListViewScrollToItem? = nil, crossfadeState: Bool = false, animateChanges: Bool = true, scrollEnabled: Bool = true) {
public init<T: ItemListNodeEntry>(presentationData: ItemListPresentationData, entries: [T], style: ItemListStyle, focusItemTag: ItemListItemTag? = nil, ensureVisibleItemTag: ItemListItemTag? = nil, emptyStateItem: ItemListControllerEmptyStateItem? = nil, searchItem: ItemListControllerSearch? = nil, toolbarItem: ItemListToolbarItem? = nil, headerItem: ItemListControllerHeaderItem? = nil, footerItem: ItemListControllerFooterItem? = nil, initialScrollToItem: ListViewScrollToItem? = nil, crossfadeState: Bool = false, animateChanges: Bool = true, scrollEnabled: Bool = true) {
self.presentationData = presentationData
self.entries = entries.map { $0 }
self.style = style
self.emptyStateItem = emptyStateItem
self.searchItem = searchItem
self.toolbarItem = toolbarItem
self.headerItem = headerItem
self.footerItem = footerItem
self.crossfadeState = crossfadeState
self.animateChanges = animateChanges
@ -250,6 +253,9 @@ open class ItemListControllerNode: ASDisplayNode {
private var searchNode: ItemListControllerSearchNode?
private var toolbarItem: ItemListToolbarItem?
private var headerItem: ItemListControllerHeaderItem?
private var headerItemNode: ItemListControllerHeaderItemNode?
private var footerItem: ItemListControllerFooterItem?
private var footerItemNode: ItemListControllerFooterItemNode?
@ -375,9 +381,19 @@ open class ItemListControllerNode: ASDisplayNode {
} else {
transition = .immediate
}
strongSelf.navigationBar.updateBackgroundAlpha(min(30.0, value) / 30.0, transition: transition)
if let headerItemNode = strongSelf.headerItemNode {
headerItemNode.updateContentOffset(value, transition: transition)
strongSelf.navigationBar.updateBackgroundAlpha(0.0, transition: .immediate)
} else {
strongSelf.navigationBar.updateBackgroundAlpha(min(30.0, value) / 30.0, transition: transition)
}
case .unknown, .none:
strongSelf.navigationBar.updateBackgroundAlpha(1.0, transition: .immediate)
if let headerItemNode = strongSelf.headerItemNode {
headerItemNode.updateContentOffset(0.0, transition: .immediate)
strongSelf.navigationBar.updateBackgroundAlpha(0.0, transition: .immediate)
} else {
strongSelf.navigationBar.updateBackgroundAlpha(1.0, transition: .immediate)
}
}
strongSelf.previousContentOffset = offset
@ -425,7 +441,7 @@ open class ItemListControllerNode: ASDisplayNode {
scrollToItem = state.initialScrollToItem
}
return ItemListNodeTransition(theme: presentationData.theme, strings: presentationData.strings, entries: transition, updateStyle: updatedStyle, emptyStateItem: state.emptyStateItem, searchItem: state.searchItem, toolbarItem: state.toolbarItem, footerItem: state.footerItem, focusItemTag: state.focusItemTag, ensureVisibleItemTag: state.ensureVisibleItemTag, scrollToItem: scrollToItem, firstTime: previous == nil, animated: previous != nil && state.animateChanges, animateAlpha: previous != nil && state.animateChanges, crossfade: state.crossfadeState, mergedEntries: state.entries, scrollEnabled: state.scrollEnabled)
return ItemListNodeTransition(theme: presentationData.theme, strings: presentationData.strings, entries: transition, updateStyle: updatedStyle, emptyStateItem: state.emptyStateItem, searchItem: state.searchItem, toolbarItem: state.toolbarItem, headerItem: state.headerItem, footerItem: state.footerItem, focusItemTag: state.focusItemTag, ensureVisibleItemTag: state.ensureVisibleItemTag, scrollToItem: scrollToItem, firstTime: previous == nil, animated: previous != nil && state.animateChanges, animateAlpha: previous != nil && state.animateChanges, crossfade: state.crossfadeState, mergedEntries: state.entries, scrollEnabled: state.scrollEnabled)
})
|> deliverOnMainQueue).start(next: { [weak self] transition in
if let strongSelf = self {
@ -563,6 +579,12 @@ open class ItemListControllerNode: ASDisplayNode {
}
}
if let headerItemNode = self.headerItemNode {
let headerHeight = headerItemNode.updateLayout(layout: layout, transition: transition)
headerItemNode.frame = CGRect(origin: .zero, size: CGSize(width: layout.size.width, height: 56.0))
insets.top += headerHeight
}
if let footerItemNode = self.footerItemNode {
let footerHeight = footerItemNode.updateLayout(layout: layout, transition: transition)
insets.bottom += footerHeight
@ -855,6 +877,43 @@ open class ItemListControllerNode: ASDisplayNode {
self.emptyStateNode = nil
}
}
var updateHeaderItem = false
if let headerItem = self.headerItem, let updatedHeaderItem = transition.headerItem {
updateHeaderItem = !headerItem.isEqual(to: updatedHeaderItem)
} else if (self.headerItem != nil) != (transition.headerItem != nil) {
updateHeaderItem = true
}
if updateHeaderItem {
self.headerItem = transition.headerItem
if let headerItem = transition.headerItem {
let updatedNode = headerItem.node(current: self.headerItemNode)
if let headerItemNode = self.headerItemNode, updatedNode !== headerItemNode {
headerItemNode.removeFromSupernode()
}
if self.headerItemNode !== updatedNode {
self.headerItemNode = updatedNode
let headerHeight: CGFloat
if let validLayout = self.validLayout {
headerHeight = updatedNode.updateLayout(layout: validLayout.0, transition: .immediate)
} else {
headerHeight = 100.0
}
let _ = headerHeight
self.addSubnode(updatedNode)
}
} else if let headerItemNode = self.headerItemNode {
let headerHeight: CGFloat
if let validLayout = self.validLayout {
headerHeight = headerItemNode.updateLayout(layout: validLayout.0, transition: .immediate)
} else {
headerHeight = 100.0
}
let _ = headerHeight
headerItemNode.removeFromSupernode()
self.headerItemNode = nil
}
}
var updateFooterItem = false
if let footerItem = self.footerItem, let updatedFooterItem = transition.footerItem {
updateFooterItem = !footerItem.isEqual(to: updatedFooterItem)

@ -39,10 +39,11 @@ typedef enum
@property (nonatomic, readonly) UIColor *badgeTextColor;
@property (nonatomic, readonly) UIImage *sendIconImage;
@property (nonatomic, readonly) UIImage *doneIconImage;
@property (nonatomic, readonly) UIImage *scheduleIconImage;
@property (nonatomic, readonly) UIColor *maybeAccentColor;
+ (instancetype)palleteWithDark:(bool)dark backgroundColor:(UIColor *)backgroundColor selectionColor:(UIColor *)selectionColor separatorColor:(UIColor *)separatorColor textColor:(UIColor *)textColor secondaryTextColor:(UIColor *)secondaryTextColor accentColor:(UIColor *)accentColor destructiveColor:(UIColor *)destructiveColor barBackgroundColor:(UIColor *)barBackgroundColor barSeparatorColor:(UIColor *)barSeparatorColor navigationTitleColor:(UIColor *)navigationTitleColor badge:(UIImage *)badge badgeTextColor:(UIColor *)badgeTextColor sendIconImage:(UIImage *)sendIconImage doneIconImage:(UIImage *)doneIconImage maybeAccentColor:(UIColor *)maybeAccentColor;
+ (instancetype)palleteWithDark:(bool)dark backgroundColor:(UIColor *)backgroundColor selectionColor:(UIColor *)selectionColor separatorColor:(UIColor *)separatorColor textColor:(UIColor *)textColor secondaryTextColor:(UIColor *)secondaryTextColor accentColor:(UIColor *)accentColor destructiveColor:(UIColor *)destructiveColor barBackgroundColor:(UIColor *)barBackgroundColor barSeparatorColor:(UIColor *)barSeparatorColor navigationTitleColor:(UIColor *)navigationTitleColor badge:(UIImage *)badge badgeTextColor:(UIColor *)badgeTextColor sendIconImage:(UIImage *)sendIconImage doneIconImage:(UIImage *)doneIconImage scheduleIconImage:(UIImage *)scheduleIconImage maybeAccentColor:(UIColor *)maybeAccentColor;
@end

@ -37,7 +37,7 @@
@property (nonatomic, readonly) UIView *timerButton;
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context focusItem:(id<TGModernGalleryItem>)focusItem selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext hasSelectionPanel:(bool)hasSelectionPanel hasCameraButton:(bool)hasCameraButton recipientName:(NSString *)recipientName;
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context focusItem:(id<TGModernGalleryItem>)focusItem selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext hasSelectionPanel:(bool)hasSelectionPanel hasCameraButton:(bool)hasCameraButton recipientName:(NSString *)recipientName isScheduledMessages:(bool)isScheduledMessages;
- (void)setSelectedItemsModel:(TGMediaPickerGallerySelectedItemsModel *)selectedItemsModel;
- (void)setEditorTabPressed:(void (^)(TGPhotoEditorTab tab))editorTabPressed;

@ -46,7 +46,7 @@
@property (nonatomic, readonly) TGMediaSelectionContext *selectionContext;
@property (nonatomic, strong) id<TGPhotoPaintStickersContext> stickersContext;
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context items:(NSArray *)items focusItem:(id<TGModernGalleryItem>)focusItem selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext hasCaptions:(bool)hasCaptions allowCaptionEntities:(bool)allowCaptionEntities hasTimer:(bool)hasTimer onlyCrop:(bool)onlyCrop inhibitDocumentCaptions:(bool)inhibitDocumentCaptions hasSelectionPanel:(bool)hasSelectionPanel hasCamera:(bool)hasCamera recipientName:(NSString *)recipientName;
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context items:(NSArray *)items focusItem:(id<TGModernGalleryItem>)focusItem selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext hasCaptions:(bool)hasCaptions allowCaptionEntities:(bool)allowCaptionEntities hasTimer:(bool)hasTimer onlyCrop:(bool)onlyCrop inhibitDocumentCaptions:(bool)inhibitDocumentCaptions hasSelectionPanel:(bool)hasSelectionPanel hasCamera:(bool)hasCamera recipientName:(NSString *)recipientName isScheduledMessages:(bool)isScheduledMessages;
- (void)presentPhotoEditorForItem:(id<TGModernGalleryEditableItem>)item tab:(TGPhotoEditorTab)tab;
- (void)presentPhotoEditorForItem:(id<TGModernGalleryEditableItem>)item tab:(TGPhotoEditorTab)tab snapshots:(NSArray *)snapshots fromRect:(CGRect)fromRect;

@ -30,7 +30,8 @@ typedef enum
{
TGPhotoEditorDoneButtonSend,
TGPhotoEditorDoneButtonCheck,
TGPhotoEditorDoneButtonDone
TGPhotoEditorDoneButtonDone,
TGPhotoEditorDoneButtonSchedule
} TGPhotoEditorDoneButton;
@interface TGPhotoToolbarView : UIView

@ -654,7 +654,7 @@ NSString *const PGCameraAdjustingFocusKey = @"adjustingFocus";
if (self.cameraMode == PGCameraModeVideo || self.cameraMode == PGCameraModeSquareVideo || self.cameraMode == PGCameraModeSquareSwing)
return self.captureSession.videoDevice.torchActive;
return self.captureSession.videoDevice.flashActive;
return self.captureSession.imageOutput.isFlashScene;
}
- (bool)flashAvailable

@ -1536,7 +1536,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
}];
bool hasCamera = !self.inhibitMultipleCapture && (((_intent == TGCameraControllerGenericIntent || _intent == TGCameraControllerGenericPhotoOnlyIntent || _intent == TGCameraControllerGenericVideoOnlyIntent) && !_shortcut) || (_intent == TGCameraControllerPassportMultipleIntent));
TGMediaPickerGalleryModel *model = [[TGMediaPickerGalleryModel alloc] initWithContext:windowContext items:galleryItems focusItem:focusItem selectionContext:_items.count > 1 ? selectionContext : nil editingContext:editingContext hasCaptions:self.allowCaptions allowCaptionEntities:self.allowCaptionEntities hasTimer:self.hasTimer onlyCrop:_intent == TGCameraControllerPassportIntent || _intent == TGCameraControllerPassportIdIntent || _intent == TGCameraControllerPassportMultipleIntent inhibitDocumentCaptions:self.inhibitDocumentCaptions hasSelectionPanel:true hasCamera:hasCamera recipientName:self.recipientName];
TGMediaPickerGalleryModel *model = [[TGMediaPickerGalleryModel alloc] initWithContext:windowContext items:galleryItems focusItem:focusItem selectionContext:_items.count > 1 ? selectionContext : nil editingContext:editingContext hasCaptions:self.allowCaptions allowCaptionEntities:self.allowCaptionEntities hasTimer:self.hasTimer onlyCrop:_intent == TGCameraControllerPassportIntent || _intent == TGCameraControllerPassportIdIntent || _intent == TGCameraControllerPassportMultipleIntent inhibitDocumentCaptions:self.inhibitDocumentCaptions hasSelectionPanel:true hasCamera:hasCamera recipientName:self.recipientName isScheduledMessages:false];
model.inhibitMute = self.inhibitMute;
model.controller = galleryController;
model.stickersContext = self.stickersContext;

@ -1894,7 +1894,7 @@
@implementation TGMediaAssetsPallete
+ (instancetype)palleteWithDark:(bool)dark backgroundColor:(UIColor *)backgroundColor selectionColor:(UIColor *)selectionColor separatorColor:(UIColor *)separatorColor textColor:(UIColor *)textColor secondaryTextColor:(UIColor *)secondaryTextColor accentColor:(UIColor *)accentColor destructiveColor:(UIColor *)destructiveColor barBackgroundColor:(UIColor *)barBackgroundColor barSeparatorColor:(UIColor *)barSeparatorColor navigationTitleColor:(UIColor *)navigationTitleColor badge:(UIImage *)badge badgeTextColor:(UIColor *)badgeTextColor sendIconImage:(UIImage *)sendIconImage doneIconImage:(UIImage *)doneIconImage maybeAccentColor:(UIColor *)maybeAccentColor
+ (instancetype)palleteWithDark:(bool)dark backgroundColor:(UIColor *)backgroundColor selectionColor:(UIColor *)selectionColor separatorColor:(UIColor *)separatorColor textColor:(UIColor *)textColor secondaryTextColor:(UIColor *)secondaryTextColor accentColor:(UIColor *)accentColor destructiveColor:(UIColor *)destructiveColor barBackgroundColor:(UIColor *)barBackgroundColor barSeparatorColor:(UIColor *)barSeparatorColor navigationTitleColor:(UIColor *)navigationTitleColor badge:(UIImage *)badge badgeTextColor:(UIColor *)badgeTextColor sendIconImage:(UIImage *)sendIconImage doneIconImage:(UIImage *)doneIconImage scheduleIconImage:(UIImage *)scheduleIconImage maybeAccentColor:(UIColor *)maybeAccentColor
{
TGMediaAssetsPallete *pallete = [[TGMediaAssetsPallete alloc] init];
pallete->_isDark = dark;
@ -1912,6 +1912,7 @@
pallete->_badgeTextColor = badgeTextColor;
pallete->_sendIconImage = sendIconImage;
pallete->_doneIconImage = doneIconImage;
pallete->_scheduleIconImage = scheduleIconImage;
pallete->_maybeAccentColor = maybeAccentColor;
return pallete;
}

@ -125,7 +125,7 @@
@synthesize safeAreaInset = _safeAreaInset;
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context focusItem:(id<TGModernGalleryItem>)focusItem selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext hasSelectionPanel:(bool)hasSelectionPanel hasCameraButton:(bool)hasCameraButton recipientName:(NSString *)recipientName
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context focusItem:(id<TGModernGalleryItem>)focusItem selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext hasSelectionPanel:(bool)hasSelectionPanel hasCameraButton:(bool)hasCameraButton recipientName:(NSString *)recipientName isScheduledMessages:(bool)isScheduledMessages
{
self = [super initWithFrame:CGRectZero];
if (self != nil)
@ -392,13 +392,15 @@
_captionMixin.stickersContext = stickersContext;
[_captionMixin createInputPanelIfNeeded];
_portraitToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:TGPhotoEditorBackButtonBack doneButton:TGPhotoEditorDoneButtonSend solidBackground:false];
TGPhotoEditorDoneButton doneButton = isScheduledMessages ? TGPhotoEditorDoneButtonSchedule : TGPhotoEditorDoneButtonSend;
_portraitToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:TGPhotoEditorBackButtonBack doneButton:doneButton solidBackground:false];
_portraitToolbarView.cancelPressed = toolbarCancelPressed;
_portraitToolbarView.donePressed = toolbarDonePressed;
_portraitToolbarView.doneLongPressed = toolbarDoneLongPressed;
[_wrapperView addSubview:_portraitToolbarView];
_landscapeToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:TGPhotoEditorBackButtonBack doneButton:TGPhotoEditorDoneButtonSend solidBackground:false];
_landscapeToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:TGPhotoEditorBackButtonBack doneButton:doneButton solidBackground:false];
_landscapeToolbarView.cancelPressed = toolbarCancelPressed;
_landscapeToolbarView.donePressed = toolbarDonePressed;
_landscapeToolbarView.doneLongPressed = toolbarDoneLongPressed;

@ -39,6 +39,7 @@
bool _inhibitDocumentCaptions;
NSString *_recipientName;
bool _hasCamera;
bool _isScheduledMessages;
}
@property (nonatomic, weak) TGPhotoEditorController *editorController;
@ -47,7 +48,7 @@
@implementation TGMediaPickerGalleryModel
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context items:(NSArray *)items focusItem:(id<TGModernGalleryItem>)focusItem selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext hasCaptions:(bool)hasCaptions allowCaptionEntities:(bool)allowCaptionEntities hasTimer:(bool)hasTimer onlyCrop:(bool)onlyCrop inhibitDocumentCaptions:(bool)inhibitDocumentCaptions hasSelectionPanel:(bool)hasSelectionPanel hasCamera:(bool)hasCamera recipientName:(NSString *)recipientName
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context items:(NSArray *)items focusItem:(id<TGModernGalleryItem>)focusItem selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext hasCaptions:(bool)hasCaptions allowCaptionEntities:(bool)allowCaptionEntities hasTimer:(bool)hasTimer onlyCrop:(bool)onlyCrop inhibitDocumentCaptions:(bool)inhibitDocumentCaptions hasSelectionPanel:(bool)hasSelectionPanel hasCamera:(bool)hasCamera recipientName:(NSString *)recipientName isScheduledMessages:(bool)isScheduledMessages
{
self = [super init];
if (self != nil)
@ -68,6 +69,7 @@
_inhibitDocumentCaptions = inhibitDocumentCaptions;
_recipientName = recipientName;
_hasCamera = hasCamera;
_isScheduledMessages = isScheduledMessages;
__weak TGMediaPickerGalleryModel *weakSelf = self;
if (selectionContext != nil)
@ -177,7 +179,7 @@
if (_interfaceView == nil)
{
__weak TGMediaPickerGalleryModel *weakSelf = self;
_interfaceView = [[TGMediaPickerGalleryInterfaceView alloc] initWithContext:_context focusItem:_initialFocusItem selectionContext:_selectionContext editingContext:_editingContext stickersContext:_stickersContext hasSelectionPanel:_hasSelectionPanel hasCameraButton:_hasCamera recipientName:_recipientName];
_interfaceView = [[TGMediaPickerGalleryInterfaceView alloc] initWithContext:_context focusItem:_initialFocusItem selectionContext:_selectionContext editingContext:_editingContext stickersContext:_stickersContext hasSelectionPanel:_hasSelectionPanel hasCameraButton:_hasCamera recipientName:_recipientName isScheduledMessages:_isScheduledMessages];
_interfaceView.hasCaptions = _hasCaptions;
_interfaceView.allowCaptionEntities = _allowCaptionEntities;
_interfaceView.hasTimer = _hasTimer;

@ -84,7 +84,7 @@
NSArray *galleryItems = [self prepareGalleryItemsForFetchResult:fetchResult selectionContext:selectionContext editingContext:editingContext stickersContext:stickersContext asFile:asFile enumerationBlock:enumerationBlock];
TGMediaPickerGalleryModel *model = [[TGMediaPickerGalleryModel alloc] initWithContext:[_windowManager context] items:galleryItems focusItem:focusItem selectionContext:selectionContext editingContext:editingContext hasCaptions:hasCaptions allowCaptionEntities:allowCaptionEntities hasTimer:hasTimer onlyCrop:onlyCrop inhibitDocumentCaptions:inhibitDocumentCaptions hasSelectionPanel:true hasCamera:false recipientName:recipientName];
TGMediaPickerGalleryModel *model = [[TGMediaPickerGalleryModel alloc] initWithContext:[_windowManager context] items:galleryItems focusItem:focusItem selectionContext:selectionContext editingContext:editingContext hasCaptions:hasCaptions allowCaptionEntities:allowCaptionEntities hasTimer:hasTimer onlyCrop:onlyCrop inhibitDocumentCaptions:inhibitDocumentCaptions hasSelectionPanel:true hasCamera:false recipientName:recipientName isScheduledMessages:false];
_galleryModel = model;
model.stickersContext = stickersContext;
model.inhibitMute = inhibitMute;

@ -112,6 +112,9 @@
doneImage = pallete != nil ? pallete.doneIconImage : TGTintedImage([UIImage imageNamed:@"Editor/Commit"], [UIColor whiteColor]);
break;
}
case TGPhotoEditorDoneButtonSchedule:
doneImage = pallete != nil ? pallete.scheduleIconImage : TGComponentsImageNamed(@"PhotoPickerSendIcon");
break;
default:
{
doneImage = pallete != nil ? pallete.sendIconImage : TGComponentsImageNamed(@"PhotoPickerSendIcon");

@ -178,7 +178,7 @@
galleryItem.editingContext = editingContext;
galleryItem.stickersContext = stickersContext;
TGMediaPickerGalleryModel *model = [[TGMediaPickerGalleryModel alloc] initWithContext:windowContext items:@[galleryItem] focusItem:galleryItem selectionContext:nil editingContext:editingContext hasCaptions:true allowCaptionEntities:true hasTimer:false onlyCrop:false inhibitDocumentCaptions:false hasSelectionPanel:false hasCamera:false recipientName:recipientName];
TGMediaPickerGalleryModel *model = [[TGMediaPickerGalleryModel alloc] initWithContext:windowContext items:@[galleryItem] focusItem:galleryItem selectionContext:nil editingContext:editingContext hasCaptions:true allowCaptionEntities:true hasTimer:false onlyCrop:false inhibitDocumentCaptions:false hasSelectionPanel:false hasCamera:false recipientName:recipientName isScheduledMessages:false];
model.controller = galleryController;
model.stickersContext = stickersContext;

@ -27,7 +27,6 @@ swift_library(
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/StickerResources:StickerResources",
"//submodules/TextFormat:TextFormat",
"//submodules/AttachmentUI:AttachmentUI",
"//submodules/DrawingUI:DrawingUI",
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
"//submodules/TelegramUI/Components/MediaEditor",

@ -14,7 +14,6 @@ import MimeTypes
import LocalMediaResources
import LegacyUI
import TextFormat
import AttachmentUI
public func guessMimeTypeByFileExtension(_ ext: String) -> String {
return TGMimeTypeMap.mimeType(forExtension: ext) ?? "application/binary"
@ -60,64 +59,6 @@ public func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, co
}
}
public class LegacyAssetPickerContext: AttachmentMediaPickerContext {
private weak var controller: TGMediaAssetsController?
public var selectionCount: Signal<Int, NoError> {
return Signal { [weak self] subscriber in
let disposable = self?.controller?.selectionContext.selectionChangedSignal().start(next: { [weak self] value in
subscriber.putNext(Int(self?.controller?.selectionContext.count() ?? 0))
}, error: { _ in }, completed: { })
return ActionDisposable {
disposable?.dispose()
}
}
}
public var caption: Signal<NSAttributedString?, NoError> {
return Signal { [weak self] subscriber in
let disposable = self?.controller?.editingContext.forcedCaption().start(next: { caption in
if let caption = caption as? NSAttributedString {
subscriber.putNext(caption)
} else {
subscriber.putNext(nil)
}
}, error: { _ in }, completed: { })
return ActionDisposable {
disposable?.dispose()
}
}
}
public var loadingProgress: Signal<CGFloat?, NoError> {
return .single(nil)
}
public var mainButtonState: Signal<AttachmentMainButtonState?, NoError> {
return .single(nil)
}
public init(controller: TGMediaAssetsController) {
self.controller = controller
}
public func setCaption(_ caption: NSAttributedString) {
self.controller?.editingContext.setForcedCaption(caption, skipUpdate: true)
}
public func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode) {
self.controller?.send(mode == .silently, whenOnline: mode == .whenOnline)
}
public func schedule() {
self.controller?.schedule(false)
}
public func mainButtonAction() {
}
}
public func legacyAssetPicker(context: AccountContext, presentationData: PresentationData, editingMedia: Bool, fileMode: Bool, peer: Peer?, threadTitle: String?, saveEditedPhotos: Bool, allowGrouping: Bool, selectionLimit: Int) -> Signal<(LegacyComponentsContext) -> TGMediaAssetsController, Void> {
let isSecretChat = (peer?.id.namespace._internalGetInt32Value() ?? 0) == Namespaces.Peer.SecretChat._internalGetInt32Value()

@ -20,7 +20,6 @@ swift_library(
"//submodules/DeviceAccess:DeviceAccess",
"//submodules/LegacyComponents:LegacyComponents",
"//submodules/StickerResources:StickerResources",
"//submodules/AttachmentUI:AttachmentUI",
],
visibility = [
"//visibility:public",

@ -4,7 +4,6 @@ import Display
import SwiftSignalKit
import LegacyComponents
import TelegramPresentationData
import AttachmentUI
public enum LegacyControllerPresentation {
case custom
@ -387,7 +386,7 @@ public final class LegacyControllerContext: NSObject, LegacyComponentsContext {
let navigationBar = presentationTheme.rootController.navigationBar
let tabBar = presentationTheme.rootController.tabBar
return TGMediaAssetsPallete(dark: presentationTheme.overallDarkAppearance, backgroundColor: theme.plainBackgroundColor, selectionColor: theme.itemHighlightedBackgroundColor, separatorColor: theme.itemPlainSeparatorColor, textColor: theme.itemPrimaryTextColor, secondaryTextColor: theme.controlSecondaryColor, accentColor: theme.itemAccentColor, destructiveColor: theme.itemDestructiveColor, barBackgroundColor: navigationBar.opaqueBackgroundColor, barSeparatorColor: tabBar.separatorColor, navigationTitleColor: navigationBar.primaryTextColor, badge: generateStretchableFilledCircleImage(diameter: 22.0, color: navigationBar.accentTextColor), badgeTextColor: navigationBar.opaqueBackgroundColor, sendIconImage: PresentationResourcesChat.chatInputPanelSendButtonImage(presentationTheme), doneIconImage: PresentationResourcesChat.chatInputPanelApplyButtonImage(presentationTheme), maybeAccentColor: navigationBar.accentTextColor)
return TGMediaAssetsPallete(dark: presentationTheme.overallDarkAppearance, backgroundColor: theme.plainBackgroundColor, selectionColor: theme.itemHighlightedBackgroundColor, separatorColor: theme.itemPlainSeparatorColor, textColor: theme.itemPrimaryTextColor, secondaryTextColor: theme.controlSecondaryColor, accentColor: theme.itemAccentColor, destructiveColor: theme.itemDestructiveColor, barBackgroundColor: navigationBar.opaqueBackgroundColor, barSeparatorColor: tabBar.separatorColor, navigationTitleColor: navigationBar.primaryTextColor, badge: generateStretchableFilledCircleImage(diameter: 22.0, color: navigationBar.accentTextColor), badgeTextColor: navigationBar.opaqueBackgroundColor, sendIconImage: PresentationResourcesChat.chatInputPanelSendButtonImage(presentationTheme), doneIconImage: PresentationResourcesChat.chatInputPanelApplyButtonImage(presentationTheme), scheduleIconImage: PresentationResourcesChat.chatInputPanelScheduleButtonImage(presentationTheme), maybeAccentColor: navigationBar.accentTextColor)
}
public func checkButtonPallete() -> TGCheckButtonPallete! {
@ -403,7 +402,7 @@ public final class LegacyControllerContext: NSObject, LegacyComponentsContext {
}
}
open class LegacyController: ViewController, PresentableController, AttachmentContainable {
open class LegacyController: ViewController, PresentableController {
public private(set) var legacyController: UIViewController!
private let presentation: LegacyControllerPresentation
@ -439,18 +438,7 @@ open class LegacyController: ViewController, PresentableController, AttachmentCo
}
public var disposables = DisposableSet()
open var requestAttachmentMenuExpansion: () -> Void = {}
open var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in }
open var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in }
open var cancelPanGesture: () -> Void = { }
open var isContainerPanning: () -> Bool = { return false }
open var isContainerExpanded: () -> Bool = { return false }
public var mediaPickerContext: AttachmentMediaPickerContext? {
return nil
}
public init(presentation: LegacyControllerPresentation, theme: PresentationTheme? = nil, strings: PresentationStrings? = nil, initialLayout: ContainerViewLayout? = nil) {
self.sizeClass.set(SSignal.single(UIUserInterfaceSizeClass.compact.rawValue as NSNumber))
self.presentation = presentation

@ -290,7 +290,7 @@ private final class LegacyComponentsGlobalsProviderImpl: NSObject, LegacyCompone
let navigationBar = presentationTheme.rootController.navigationBar
let tabBar = presentationTheme.rootController.tabBar
return TGMediaAssetsPallete(dark: presentationTheme.overallDarkAppearance, backgroundColor: theme.plainBackgroundColor, selectionColor: theme.itemHighlightedBackgroundColor, separatorColor: theme.itemPlainSeparatorColor, textColor: theme.itemPrimaryTextColor, secondaryTextColor: theme.controlSecondaryColor, accentColor: theme.itemAccentColor, destructiveColor: theme.itemDestructiveColor, barBackgroundColor: navigationBar.opaqueBackgroundColor, barSeparatorColor: tabBar.separatorColor, navigationTitleColor: navigationBar.primaryTextColor, badge: generateStretchableFilledCircleImage(diameter: 22.0, color: navigationBar.accentTextColor), badgeTextColor: navigationBar.opaqueBackgroundColor, sendIconImage: PresentationResourcesChat.chatInputPanelSendButtonImage(presentationTheme), doneIconImage: PresentationResourcesChat.chatInputPanelApplyButtonImage(presentationTheme), maybeAccentColor: navigationBar.accentTextColor)
return TGMediaAssetsPallete(dark: presentationTheme.overallDarkAppearance, backgroundColor: theme.plainBackgroundColor, selectionColor: theme.itemHighlightedBackgroundColor, separatorColor: theme.itemPlainSeparatorColor, textColor: theme.itemPrimaryTextColor, secondaryTextColor: theme.controlSecondaryColor, accentColor: theme.itemAccentColor, destructiveColor: theme.itemDestructiveColor, barBackgroundColor: navigationBar.opaqueBackgroundColor, barSeparatorColor: tabBar.separatorColor, navigationTitleColor: navigationBar.primaryTextColor, badge: generateStretchableFilledCircleImage(diameter: 22.0, color: navigationBar.accentTextColor), badgeTextColor: navigationBar.opaqueBackgroundColor, sendIconImage: PresentationResourcesChat.chatInputPanelSendButtonImage(presentationTheme), doneIconImage: PresentationResourcesChat.chatInputPanelApplyButtonImage(presentationTheme), scheduleIconImage: PresentationResourcesChat.chatInputPanelScheduleButtonImage(presentationTheme), maybeAccentColor: navigationBar.accentTextColor)
}
func checkButtonPallete() -> TGCheckButtonPallete! {

@ -135,7 +135,7 @@ func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?,
}
}
let model = TGMediaPickerGalleryModel(context: legacyController.context, items: items, focus: focusItem, selectionContext: selectionContext, editingContext: editingContext, hasCaptions: true, allowCaptionEntities: true, hasTimer: hasTimer, onlyCrop: false, inhibitDocumentCaptions: false, hasSelectionPanel: true, hasCamera: false, recipientName: recipientName)!
let model = TGMediaPickerGalleryModel(context: legacyController.context, items: items, focus: focusItem, selectionContext: selectionContext, editingContext: editingContext, hasCaptions: true, allowCaptionEntities: true, hasTimer: hasTimer, onlyCrop: false, inhibitDocumentCaptions: false, hasSelectionPanel: true, hasCamera: false, recipientName: recipientName, isScheduledMessages: isScheduledMessages)!
model.stickersContext = paintStickersContext
controller.model = model
model.controller = controller

@ -99,12 +99,12 @@ swift_library(
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent",
"//submodules/AttachmentUI:AttachmentUI",
"//submodules/Components/BalancedTextComponent",
"//submodules/ItemListPeerItem:ItemListPeerItem",
"//submodules/ItemListPeerActionItem:ItemListPeerActionItem",
"//submodules/TelegramUI/Components/ItemListDatePickerItem:ItemListDatePickerItem",
"//submodules/TelegramUI/Components/ShareWithPeersScreen",
"//submodules/TelegramUI/Components/ButtonComponent",
],
visibility = [
"//visibility:public",

@ -18,6 +18,7 @@ import ItemListDatePickerItem
import ItemListPeerActionItem
import ShareWithPeersScreen
import InAppPurchaseManager
import UndoUI
private final class CreateGiveawayControllerArguments {
let context: AccountContext
@ -25,13 +26,15 @@ private final class CreateGiveawayControllerArguments {
let dismissInput: () -> Void
let openPeersSelection: () -> Void
let openChannelsSelection: () -> Void
let openPremiumIntro: () -> Void
init(context: AccountContext, updateState: @escaping ((CreateGiveawayControllerState) -> CreateGiveawayControllerState) -> Void, dismissInput: @escaping () -> Void, openPeersSelection: @escaping () -> Void, openChannelsSelection: @escaping () -> Void) {
init(context: AccountContext, updateState: @escaping ((CreateGiveawayControllerState) -> CreateGiveawayControllerState) -> Void, dismissInput: @escaping () -> Void, openPeersSelection: @escaping () -> Void, openChannelsSelection: @escaping () -> Void, openPremiumIntro: @escaping () -> Void) {
self.context = context
self.updateState = updateState
self.dismissInput = dismissInput
self.openPeersSelection = openPeersSelection
self.openChannelsSelection = openChannelsSelection
self.openPremiumIntro = openPremiumIntro
}
}
@ -63,12 +66,15 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
case createGiveaway(PresentationTheme, String, String, Bool)
case awardUsers(PresentationTheme, String, String, Bool)
case subscriptionsHeader(PresentationTheme, String)
case prepaidHeader(PresentationTheme, String)
case prepaid(PresentationTheme, String, String, Int32, Int32)
case subscriptionsHeader(PresentationTheme, String, String)
case subscriptions(PresentationTheme, Int32)
case subscriptionsInfo(PresentationTheme, String)
case channelsHeader(PresentationTheme, String)
case channel(Int32, PresentationTheme, EnginePeer, Int32)
case channel(Int32, PresentationTheme, EnginePeer, Int32?)
case channelAdd(PresentationTheme, String)
case channelsInfo(PresentationTheme, String)
@ -83,14 +89,14 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
case timeInfo(PresentationTheme, String)
case durationHeader(PresentationTheme, String)
case duration(Int32, PresentationTheme, String, String, String, String, String?, Bool)
case duration(Int32, PresentationTheme, Int32, String, String, String, String?, Bool)
case durationInfo(PresentationTheme, String)
var section: ItemListSectionId {
switch self {
case .header:
return CreateGiveawaySection.header.rawValue
case .createGiveaway, .awardUsers:
case .createGiveaway, .awardUsers, .prepaidHeader, .prepaid:
return CreateGiveawaySection.mode.rawValue
case .subscriptionsHeader, .subscriptions, .subscriptionsInfo:
return CreateGiveawaySection.subscriptions.rawValue
@ -113,16 +119,20 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
return 0
case .awardUsers:
return 1
case .subscriptionsHeader:
case .prepaidHeader:
return 2
case .subscriptions:
case .prepaid:
return 3
case .subscriptionsInfo:
case .subscriptionsHeader:
return 4
case .channelsHeader:
case .subscriptions:
return 5
case .subscriptionsInfo:
return 6
case .channelsHeader:
return 7
case let .channel(index, _, _, _):
return 6 + index
return 8 + index
case .channelAdd:
return 100
case .channelsInfo:
@ -172,8 +182,20 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
} else {
return false
}
case let .subscriptionsHeader(lhsTheme, lhsText):
if case let .subscriptionsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
case let .prepaidHeader(lhsTheme, lhsText):
if case let .prepaidHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .prepaid(lhsTheme, lhsText, lhsSubtext, lhsBoosts, lhsMonths):
if case let .prepaid(rhsTheme, rhsText, rhsSubtext, rhsBoosts, rhsMonths) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsSubtext == rhsSubtext, lhsBoosts == rhsBoosts, lhsMonths == rhsMonths {
return true
} else {
return false
}
case let .subscriptionsHeader(lhsTheme, lhsText, lhsAdditionalText):
if case let .subscriptionsHeader(rhsTheme, rhsText, rhsAdditionalText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsAdditionalText == rhsAdditionalText {
return true
} else {
return false
@ -269,8 +291,8 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
} else {
return false
}
case let .duration(lhsIndex, lhsTheme, lhsProductId, lhsTitle, lhsSubtitle, lhsLabel, lhsBadge, lhsIsSelected):
if case let .duration(rhsIndex, rhsTheme, rhsProductId, rhsTitle, rhsSubtitle, rhsLabel, rhsBadge, rhsIsSelected) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsProductId == rhsProductId, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsLabel == rhsLabel, lhsBadge == rhsBadge, lhsIsSelected == rhsIsSelected {
case let .duration(lhsIndex, lhsTheme, lhsMonths, lhsTitle, lhsSubtitle, lhsLabel, lhsBadge, lhsIsSelected):
if case let .duration(rhsIndex, rhsTheme, rhsMonths, rhsTitle, rhsSubtitle, rhsLabel, rhsBadge, rhsIsSelected) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsMonths == rhsMonths, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsLabel == rhsLabel, lhsBadge == rhsBadge, lhsIsSelected == rhsIsSelected {
return true
} else {
return false
@ -292,9 +314,9 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
let arguments = arguments as! CreateGiveawayControllerArguments
switch self {
case let .header(_, title, text):
return CreateGiveawayHeaderItem(theme: presentationData.theme, title: title, text: text, sectionId: self.section)
return ItemListTextItem(presentationData: presentationData, text: .plain(title + text), sectionId: self.section)
case let .createGiveaway(_, title, subtitle, isSelected):
return GiftModeItem(presentationData: presentationData, context: arguments.context, iconName: "Premium/Giveaway", title: title, subtitle: subtitle, label: nil, badge: nil, isSelected: isSelected, sectionId: self.section, action: {
return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: GiftOptionItem.Icon(color: .blue, name: "Premium/Giveaway"), title: title, subtitle: subtitle, isSelected: isSelected, sectionId: self.section, action: {
arguments.updateState { state in
var updatedState = state
updatedState.mode = .giveaway
@ -302,7 +324,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
}
})
case let .awardUsers(_, title, subtitle, isSelected):
return GiftModeItem(presentationData: presentationData, context: arguments.context, iconName: "Media Editor/Privacy/SelectedUsers", title: title, subtitle: subtitle, subtitleActive: true, label: nil, badge: nil, isSelected: isSelected, sectionId: self.section, action: {
return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: GiftOptionItem.Icon(color: .violet, name: "Media Editor/Privacy/SelectedUsers"), title: title, subtitle: subtitle, subtitleActive: true, isSelected: isSelected, sectionId: self.section, action: {
var openSelection = false
arguments.updateState { state in
var updatedState = state
@ -316,11 +338,26 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
arguments.openPeersSelection()
}
})
case let .subscriptionsHeader(_, text):
case let .prepaidHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .prepaid(_, title, subtitle, boosts, months):
let _ = boosts
let color: GiftOptionItem.Icon.Color
switch months {
case 3:
color = .green
case 6:
color = .blue
case 12:
color = .red
default:
color = .blue
}
return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: GiftOptionItem.Icon(color: color, name: "Premium/Giveaway"), title: title, titleFont: .bold, subtitle: subtitle, label: .boosts(boosts), sectionId: self.section, action: nil)
case let .subscriptionsHeader(_, text, additionalText):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: ItemListSectionHeaderAccessoryText(value: additionalText, color: .generic), sectionId: self.section)
case let .subscriptions(_, value):
let text = "\(value) Subscriptions / Boosts"
return SubscriptionsCountItem(theme: presentationData.theme, strings: presentationData.strings, text: text, value: value, range: 1 ..< 11, sectionId: self.section, updated: { value in
return SubscriptionsCountItem(theme: presentationData.theme, strings: presentationData.strings, value: value, sectionId: self.section, updated: { value in
arguments.updateState { state in
var updatedState = state
updatedState.subscriptions = value
@ -332,11 +369,11 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
case let .channelsHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .channel(_, _, peer, boosts):
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: presentationData.nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: .text("this channel will receive \(boosts) boosts", .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: peer.id != arguments.context.account.peerId, sectionId: self.section, action: {
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: presentationData.nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: boosts.flatMap { .text("this channel will receive \($0) boosts", .secondary) } ?? .none, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: peer.id != arguments.context.account.peerId, sectionId: self.section, action: {
// arguments.openPeer(peer)
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in })
case let .channelAdd(theme, text):
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.roundPlusIconImage(theme), title: text, alwaysPlain: false, hasSeparator: true, sectionId: self.section, height: .generic, color: .accent, editing: false, action: {
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.roundPlusIconImage(theme), title: text, alwaysPlain: false, hasSeparator: true, sectionId: self.section, height: .compactPeerList, color: .accent, editing: false, action: {
arguments.openChannelsSelection()
})
case let .channelsInfo(_, text):
@ -344,7 +381,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
case let .usersHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .usersAll(_, title, isSelected):
return GiftModeItem(presentationData: presentationData, context: arguments.context, title: title, subtitle: nil, label: nil, badge: nil, isSelected: isSelected, sectionId: self.section, action: {
return GiftOptionItem(presentationData: presentationData, context: arguments.context, title: title, subtitle: nil, isSelected: isSelected, sectionId: self.section, action: {
arguments.updateState { state in
var updatedState = state
updatedState.onlyNewEligible = false
@ -352,7 +389,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
}
})
case let .usersNew(_, title, isSelected):
return GiftModeItem(presentationData: presentationData, context: arguments.context, title: title, subtitle: nil, label: nil, badge: nil, isSelected: isSelected, sectionId: self.section, action: {
return GiftOptionItem(presentationData: presentationData, context: arguments.context, title: title, subtitle: nil, isSelected: isSelected, sectionId: self.section, action: {
arguments.updateState { state in
var updatedState = state
updatedState.onlyNewEligible = true
@ -390,16 +427,18 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .durationHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .duration(_, _, productId, title, subtitle, label, badge, isSelected):
return GiftModeItem(presentationData: presentationData, context: arguments.context, title: title, subtitle: subtitle, label: label, badge: badge, isSelected: isSelected, sectionId: self.section, action: {
case let .duration(_, _, months, title, subtitle, label, badge, isSelected):
return GiftOptionItem(presentationData: presentationData, context: arguments.context, title: title, subtitle: subtitle, subtitleFont: .small, label: .generic(label), badge: badge, isSelected: isSelected, sectionId: self.section, action: {
arguments.updateState { state in
var updatedState = state
updatedState.selectedProductId = productId
updatedState.selectedMonths = months
return updatedState
}
})
case let .durationInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in
arguments.openPremiumIntro()
})
}
}
}
@ -425,43 +464,50 @@ private struct PremiumGiftProduct: Equatable {
}
}
private func createGiveawayControllerEntries(state: CreateGiveawayControllerState, presentationData: PresentationData, peers: [EnginePeer.Id: EnginePeer], products: [PremiumGiftProduct]) -> [CreateGiveawayEntry] {
private func createGiveawayControllerEntries(peerId: EnginePeer.Id, subject: CreateGiveawaySubject, state: CreateGiveawayControllerState, presentationData: PresentationData, peers: [EnginePeer.Id: EnginePeer], products: [PremiumGiftProduct], defaultPrice: (Int64, NSDecimalNumber)) -> [CreateGiveawayEntry] {
var entries: [CreateGiveawayEntry] = []
entries.append(.header(presentationData.theme, "Boosts via Gifts", "Get more boosts for your channel by gifting\nPremium to your subscribers."))
entries.append(.createGiveaway(presentationData.theme, "Create Giveaway", "winners are chosen randomly", state.mode == .giveaway))
let recipientsText: String
if !state.peers.isEmpty {
var peerNamesArray: [String] = []
let peersCount = state.peers.count
for peerId in state.peers.prefix(2) {
if let peer = peers[peerId] {
peerNamesArray.append(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))
switch subject {
case .generic:
entries.append(.createGiveaway(presentationData.theme, "Create Giveaway", "winners are chosen randomly", state.mode == .giveaway))
let recipientsText: String
if !state.peers.isEmpty {
var peerNamesArray: [String] = []
let peersCount = state.peers.count
for peerId in state.peers.prefix(2) {
if let peer = peers[peerId] {
peerNamesArray.append(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))
}
}
let peerNames = String(peerNamesArray.map { $0 }.joined(separator: ", "))
if !peerNames.isEmpty {
recipientsText = peerNames
} else {
recipientsText = presentationData.strings.PremiumGift_LabelRecipients(Int32(peersCount))
}
}
let peerNames = String(peerNamesArray.map { $0 }.joined(separator: ", "))
if !peerNames.isEmpty {
recipientsText = peerNames
} else {
recipientsText = presentationData.strings.PremiumGift_LabelRecipients(Int32(peersCount))
recipientsText = "select recipients"
}
} else {
recipientsText = "select recipients"
entries.append(.awardUsers(presentationData.theme, "Award Specific Users", recipientsText, state.mode == .gift))
case let .prepaid(months, count):
entries.append(.prepaidHeader(presentationData.theme, "PREPAID GIVEAWAY"))
entries.append(.prepaid(presentationData.theme, "\(count) Telegram Premium", "\(months)-month subscriptions", count, months))
}
entries.append(.awardUsers(presentationData.theme, "Award Specific Users", recipientsText, state.mode == .gift))
if case .giveaway = state.mode {
entries.append(.subscriptionsHeader(presentationData.theme, "QUANTITY OF PRIZES / BOOSTS".uppercased()))
entries.append(.subscriptions(presentationData.theme, state.subscriptions))
entries.append(.subscriptionsInfo(presentationData.theme, "Choose how many Premium subscriptions to give away and boosts to receive."))
if case .generic = subject {
entries.append(.subscriptionsHeader(presentationData.theme, "QUANTITY OF PRIZES".uppercased(), "\(state.subscriptions) BOOSTS"))
entries.append(.subscriptions(presentationData.theme, state.subscriptions))
entries.append(.subscriptionsInfo(presentationData.theme, "Choose how many Premium subscriptions to give away and boosts to receive."))
}
entries.append(.channelsHeader(presentationData.theme, "CHANNELS INCLUDED IN THE GIVEAWAY".uppercased()))
var index: Int32 = 0
for peerId in state.channels {
if let peer = peers[peerId] {
entries.append(.channel(index, presentationData.theme, peer, state.subscriptions))
let channels = [peerId] + state.channels
for channelId in channels {
if let channel = peers[channelId] {
entries.append(.channel(index, presentationData.theme, channel, channel.id == peerId ? state.subscriptions : nil))
}
index += 1
}
@ -480,62 +526,54 @@ private func createGiveawayControllerEntries(state: CreateGiveawayControllerStat
}
entries.append(.timeInfo(presentationData.theme, "Choose when \(state.subscriptions) subscribers of your channel will be randomly selected to receive Telegram Premium."))
}
entries.append(.durationHeader(presentationData.theme, "DURATION OF PREMIUM SUBSCRIPTIONS".uppercased()))
let recipientCount: Int
switch state.mode {
case .giveaway:
recipientCount = Int(state.subscriptions)
case .gift:
recipientCount = state.peers.count
}
let shortestOptionPrice: (Int64, NSDecimalNumber)
if let product = products.last {
shortestOptionPrice = (Int64(Float(product.storeProduct.priceCurrencyAndAmount.amount) / Float(product.months)), product.storeProduct.priceValue.dividing(by: NSDecimalNumber(value: product.months)))
} else {
shortestOptionPrice = (1, NSDecimalNumber(decimal: 1))
}
var i: Int32 = 0
for product in products {
let giftTitle: String
if product.months == 12 {
giftTitle = presentationData.strings.Premium_Gift_Years(1)
} else {
giftTitle = presentationData.strings.Premium_Gift_Months(product.months)
if case .generic = subject {
entries.append(.durationHeader(presentationData.theme, "DURATION OF PREMIUM SUBSCRIPTIONS".uppercased()))
let recipientCount: Int
switch state.mode {
case .giveaway:
recipientCount = Int(state.subscriptions)
case .gift:
recipientCount = state.peers.count
}
let discountValue = Int((1.0 - Float(product.storeProduct.priceCurrencyAndAmount.amount) / Float(product.months) / Float(shortestOptionPrice.0)) * 100.0)
let discount: String?
if discountValue > 0 {
discount = "-\(discountValue)%"
} else {
discount = nil
var i: Int32 = 0
var existingMonths = Set<Int32>()
for product in products {
if existingMonths.contains(product.months) {
continue
}
existingMonths.insert(product.months)
let giftTitle: String
if product.months == 12 {
giftTitle = presentationData.strings.Premium_Gift_Years(1)
} else {
giftTitle = presentationData.strings.Premium_Gift_Months(product.months)
}
let discountValue = Int((1.0 - Float(product.storeProduct.priceCurrencyAndAmount.amount) / Float(product.months) / Float(defaultPrice.0)) * 100.0)
let discount: String?
if discountValue > 0 {
discount = "-\(discountValue)%"
} else {
discount = nil
}
let subtitle = "\(product.storeProduct.price) x \(recipientCount)"
let label = product.storeProduct.multipliedPrice(count: recipientCount)
let selectedMonths = state.selectedMonths ?? 12
let isSelected = product.months == selectedMonths
entries.append(.duration(i, presentationData.theme, product.months, giftTitle, subtitle, label, discount, isSelected))
i += 1
}
let subtitle = "\(product.storeProduct.price) x \(recipientCount)"
let label = product.storeProduct.multipliedPrice(count: recipientCount)
var isSelected = false
if let selectedProductId = state.selectedProductId {
isSelected = product.id == selectedProductId
} else if i == 0 {
isSelected = true
}
entries.append(.duration(i, presentationData.theme, product.id, giftTitle, subtitle, label, discount, isSelected))
i += 1
entries.append(.durationInfo(presentationData.theme, "You can review the list of features and terms of use for Telegram Premium [here]()."))
}
// entries.append(.duration(0, presentationData.theme, "3 Months", "$13.99 x \(state.subscriptions)", "$41.99", nil, true))
// entries.append(.duration(1, presentationData.theme, "6 Months", "$15.99 x \(state.subscriptions)", "$47.99", nil, false))
// entries.append(.duration(2, presentationData.theme, "1 Year", "$29.99 x \(state.subscriptions)", "$89.99", nil, false))
entries.append(.durationInfo(presentationData.theme, "You can review the list of features and terms of use for Telegram Premium [here]()."))
return entries
}
@ -549,18 +587,30 @@ private struct CreateGiveawayControllerState: Equatable {
var subscriptions: Int32
var channels: [EnginePeer.Id]
var peers: [EnginePeer.Id]
var selectedProductId: String?
var selectedMonths: Int32?
var onlyNewEligible: Bool
var time: Int32
var pickingTimeLimit = false
var updating = false
}
public func createGiveawayController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id, completion: (() -> Void)? = nil) -> ViewController {
public enum CreateGiveawaySubject {
case generic
case prepaid(months: Int32, count: Int32)
}
public func createGiveawayController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id, subject: CreateGiveawaySubject, completion: (() -> Void)? = nil) -> ViewController {
let actionsDisposable = DisposableSet()
let initialSubscriptions: Int32
if case let .prepaid(_, count) = subject {
initialSubscriptions = count
} else {
initialSubscriptions = 5
}
let expiryTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + 86400 * 5
let initialState: CreateGiveawayControllerState = CreateGiveawayControllerState(mode: .giveaway, subscriptions: 5, channels: [peerId], peers: [], onlyNewEligible: false, time: expiryTime)
let initialState: CreateGiveawayControllerState = CreateGiveawayControllerState(mode: .giveaway, subscriptions: initialSubscriptions, channels: [], peers: [], onlyNewEligible: false, time: expiryTime)
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
@ -573,6 +623,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
var buyActionImpl: (() -> Void)?
var openPeersSelectionImpl: (() -> Void)?
var openChannelsSelectionImpl: (() -> Void)?
var openPremiumIntroImpl: (() -> Void)?
var presentControllerImpl: ((ViewController) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
var dismissImpl: (() -> Void)?
@ -586,11 +637,13 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
openPeersSelectionImpl?()
}, openChannelsSelection: {
openChannelsSelectionImpl?()
}, openPremiumIntro: {
openPremiumIntroImpl?()
})
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
let products = combineLatest(
let productsAndDefaultPrice: Signal<([PremiumGiftProduct], (Int64, NSDecimalNumber)), NoError> = combineLatest(
.single([]) |> then(context.engine.payments.premiumGiftCodeOptions(peerId: peerId)),
context.inAppPurchaseManager?.availableProducts ?? .single([])
)
@ -601,7 +654,13 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
gifts.append(PremiumGiftProduct(giftOption: option, storeProduct: product))
}
}
return gifts
let defaultPrice: (Int64, NSDecimalNumber)
if let defaultProduct = products.first(where: { $0.id == "org.telegram.telegramPremium.monthly" }) {
defaultPrice = (defaultProduct.priceCurrencyAndAmount.amount, defaultProduct.priceValue)
} else {
defaultPrice = (1, NSDecimalNumber(value: 1))
}
return (gifts, defaultPrice)
}
let previousState = Atomic<CreateGiveawayControllerState?>(value: nil)
@ -610,7 +669,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
statePromise.get()
|> mapToSignal { state in
return context.engine.data.get(EngineDataMap(
Set(state.channels + state.peers).map {
Set([peerId] + state.channels + state.peers).map {
TelegramEngine.EngineData.Item.Peer.Peer(id: $0)
}
))
@ -618,24 +677,35 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
return (state, peers)
}
},
products
productsAndDefaultPrice
)
|> deliverOnMainQueue
|> map { presentationData, stateAndPeersMap, products -> (ItemListControllerState, (ItemListNodeState, Any)) in
|> map { presentationData, stateAndPeersMap, productsAndDefaultPrice -> (ItemListControllerState, (ItemListNodeState, Any)) in
var presentationData = presentationData
let (products, defaultPrice) = productsAndDefaultPrice
let updatedTheme = presentationData.theme.withModalBlocksBackground()
presentationData = presentationData.withUpdated(theme: updatedTheme)
let (state, peersMap) = stateAndPeersMap
let footerItem = CreateGiveawayFooterItem(theme: presentationData.theme, title: state.mode == .gift ? "Gift Premium" : "Start Giveaway", action: {
buyActionImpl?()
})
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
let headerItem = CreateGiveawayHeaderItem(theme: presentationData.theme, title: "Boosts via Gifts", text: "Get more boosts for your channel by gifting\nPremium to your subscribers.", cancel: {
dismissImpl?()
})
let badgeCount: Int32
switch state.mode {
case .giveaway:
badgeCount = state.subscriptions
case .gift:
badgeCount = Int32(state.peers.count)
}
let footerItem = CreateGiveawayFooterItem(theme: presentationData.theme, title: state.mode == .gift ? "Gift Premium" : "Start Giveaway", badgeCount: badgeCount, isLoading: state.updating, action: {
buyActionImpl?()
})
let leftNavigationButton = ItemListNavigationButton(content: .none, style: .regular, enabled: false, action: {})
let _ = productsValue.swap(products)
let previousState = previousState.swap(state)
@ -652,7 +722,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(""), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: createGiveawayControllerEntries(state: state, presentationData: presentationData, peers: peers, products: products), style: .blocks, emptyStateItem: nil, footerItem: footerItem, crossfadeState: false, animateChanges: animateChanges)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: createGiveawayControllerEntries(peerId: peerId, subject: subject, state: state, presentationData: presentationData, peers: peers, products: products, defaultPrice: defaultPrice), style: .blocks, emptyStateItem: nil, headerItem: headerItem, footerItem: footerItem, crossfadeState: false, animateChanges: animateChanges)
return (controllerState, (listState, arguments))
}
@ -678,17 +748,30 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
controller?.dismiss()
}
buyActionImpl = {
buyActionImpl = { [weak controller] in
let state = stateValue.with { $0 }
guard let products = productsValue.with({ $0 }) else {
guard let products = productsValue.with({ $0 }), !products.isEmpty else {
return
}
let selectedProduct: PremiumGiftProduct
if let selectedProductId = state.selectedProductId, let product = products.first(where: { $0.id == selectedProductId }) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var selectedProduct: PremiumGiftProduct?
let selectedMonths = state.selectedMonths ?? 12
if let product = products.first(where: { $0.months == selectedMonths && $0.giftOption.users == state.subscriptions }) {
selectedProduct = product
} else {
selectedProduct = products.first!
}
guard let selectedProduct else {
let alertController = textAlertController(context: context, title: "Reduce Quantity", text: "You can't acquire \(state.subscriptions) \(selectedMonths)-month subscriptions in the app. Do you want to reduce quantity to 25?", actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: "Reduce", action: {
updateState { state in
var updatedState = state
updatedState.subscriptions = 25
return updatedState
}
})], parseMarkdown: true)
presentControllerImpl?(alertController)
return
}
let (currency, amount) = selectedProduct.storeProduct.priceCurrencyAndAmount
@ -696,20 +779,64 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
let purpose: AppStoreTransactionPurpose
switch state.mode {
case .giveaway:
purpose = .giveaway(boostPeer: peerId, randomId: 1000, untilDate: state.time, currency: currency, amount: amount)
purpose = .giveaway(boostPeer: peerId, additionalPeerIds: state.channels.filter { $0 != peerId}, onlyNewSubscribers: state.onlyNewEligible, randomId: Int64.random(in: .min ..< .max), untilDate: state.time, currency: currency, amount: amount)
case .gift:
purpose = .giftCode(peerIds: state.peers, boostPeer: peerId, currency: currency, amount: amount)
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
updateState { state in
var updatedState = state
updatedState.updating = true
return updatedState
}
let _ = (context.engine.payments.canPurchasePremium(purpose: purpose)
|> deliverOnMainQueue).startStandalone(next: { available in
|> deliverOnMainQueue).startStandalone(next: { [weak controller] available in
if available, let inAppPurchaseManager = context.inAppPurchaseManager {
let _ = (inAppPurchaseManager.buyProduct(selectedProduct.storeProduct, purpose: purpose)
|> deliverOnMainQueue).startStandalone(next: { status in
let _ = (inAppPurchaseManager.buyProduct(selectedProduct.storeProduct, quantity: selectedProduct.giftOption.storeQuantity, purpose: purpose)
|> deliverOnMainQueue).startStandalone(next: { [weak controller] status in
if case .purchased = status {
dismissImpl?()
if let controller, let navigationController = controller.navigationController as? NavigationController {
var controllers = navigationController.viewControllers
var count = 0
for c in controllers.reversed() {
if c is PeerInfoScreen {
if case .giveaway = state.mode {
count += 1
}
break
} else {
count += 1
}
}
controllers.removeLast(count)
navigationController.setViewControllers(controllers, animated: true)
let title: String
let text: String
switch state.mode {
case .giveaway:
title = "Giveaway Created"
text = "Check your channel's [Statistics]() to see how this giveaway boosted your channel."
case .gift:
title = "Premium Subscriptions Gifted"
text = "Check your channel's [Statistics]() to see how gifts boosted your channel."
}
let tooltipController = UndoOverlayController(presentationData: presentationData, content: .premiumPaywall(title: title, text: text, customUndoText: nil, timeout: nil, linkAction: { [weak navigationController] _ in
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.StatsDatacenterId(id: peerId))
|> deliverOnMainQueue).startStandalone(next: { [weak navigationController] statsDatacenterId in
guard let statsDatacenterId else {
return
}
let statsController = context.sharedContext.makeChannelStatsController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, boosts: true, boostStatus: nil, statsDatacenterId: statsDatacenterId)
navigationController?.pushViewController(statsController)
})
}), elevatedLayout: false, action: { _ in
return true
})
(controllers.last as? ViewController)?.present(tooltipController, in: .current)
}
}
}, error: { error in
var errorText: String?
@ -732,10 +859,22 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
let alertController = textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
presentControllerImpl?(alertController)
}
updateState { state in
var updatedState = state
updatedState.updating = false
return updatedState
}
})
} else {
let alertController = textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Premium_Purchase_ErrorUnknown, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
presentControllerImpl?(alertController)
updateState { state in
var updatedState = state
updatedState.updating = false
return updatedState
}
}
})
}
@ -745,7 +884,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
let stateContext = ShareWithPeersScreen.StateContext(
context: context,
subject: .members(peerId: peerId),
subject: .members(peerId: peerId, searchQuery: nil),
initialPeerIds: Set(state.peers)
)
let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).startStandalone(next: { _ in
@ -757,6 +896,9 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
updateState { state in
var updatedState = state
updatedState.peers = privacy.additionallyIncludePeers
if updatedState.peers.isEmpty {
updatedState.mode = .giveaway
}
return updatedState
}
}
@ -766,17 +908,32 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
}
openChannelsSelectionImpl = {
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, requestPeerType: [ReplyMarkupButtonRequestPeerType.channel(ReplyMarkupButtonRequestPeerType.Channel(isCreator: false, hasUsername: nil, userAdminRights: TelegramChatAdminRights(rights: [.canChangeInfo]), botAdminRights: nil))]))
controller.peerSelected = { [weak controller] peer, _ in
updateState { state in
var updatedState = state
var channels = state.channels
channels.append(peer.id)
updatedState.channels = channels
return updatedState
}
controller?.dismiss()
}
let state = stateValue.with { $0 }
let stateContext = ShareWithPeersScreen.StateContext(
context: context,
subject: .channels(exclude: Set([peerId])),
initialPeerIds: Set(state.channels.filter { $0 != peerId })
)
let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).startStandalone(next: { _ in
let controller = ShareWithPeersScreen(
context: context,
initialPrivacy: EngineStoryPrivacy(base: .nobody, additionallyIncludePeers: state.peers),
stateContext: stateContext,
completion: { _, privacy ,_, _, _, _ in
updateState { state in
var updatedState = state
updatedState.channels = privacy.additionallyIncludePeers
return updatedState
}
}
)
pushControllerImpl?(controller)
})
}
openPremiumIntroImpl = {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil)
pushControllerImpl?(controller)
}

@ -2,25 +2,30 @@ import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import SolidRoundedButtonNode
import ButtonComponent
final class CreateGiveawayFooterItem: ItemListControllerFooterItem {
let theme: PresentationTheme
let title: String
let badgeCount: Int32
let isLoading: Bool
let action: () -> Void
init(theme: PresentationTheme, title: String, action: @escaping () -> Void) {
init(theme: PresentationTheme, title: String, badgeCount: Int32, isLoading: Bool, action: @escaping () -> Void) {
self.theme = theme
self.title = title
self.badgeCount = badgeCount
self.isLoading = isLoading
self.action = action
}
func isEqual(to: ItemListControllerFooterItem) -> Bool {
if let item = to as? CreateGiveawayFooterItem {
return self.theme === item.theme && self.title == item.title
return self.theme === item.theme && self.title == item.title && self.badgeCount == item.badgeCount && self.isLoading == item.isLoading
} else {
return false
}
@ -39,10 +44,11 @@ final class CreateGiveawayFooterItem: ItemListControllerFooterItem {
final class CreateGiveawayFooterItemNode: ItemListControllerFooterItemNode {
private let backgroundNode: NavigationBackgroundNode
private let separatorNode: ASDisplayNode
private let buttonNode: SolidRoundedButtonNode
private let button = ComponentView<Empty>()
private var validLayout: ContainerViewLayout?
private var currentIsLoading = false
var item: CreateGiveawayFooterItem {
didSet {
self.updateItem()
@ -58,26 +64,32 @@ final class CreateGiveawayFooterItemNode: ItemListControllerFooterItemNode {
self.backgroundNode = NavigationBackgroundNode(color: item.theme.rootController.tabBar.backgroundColor)
self.separatorNode = ASDisplayNode()
self.buttonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: .black, foregroundColor: .white), height: 50.0, cornerRadius: 11.0)
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.separatorNode)
self.addSubnode(self.buttonNode)
self.updateItem()
}
private func updateItem() {
self.backgroundNode.updateColor(color: self.item.theme.rootController.tabBar.backgroundColor, transition: .immediate)
self.separatorNode.backgroundColor = self.item.theme.rootController.tabBar.separatorColor
self.buttonNode.updateTheme(SolidRoundedButtonTheme(backgroundColor: self.item.theme.list.itemCheckColors.fillColor, foregroundColor: self.item.theme.list.itemCheckColors.foregroundColor))
self.buttonNode.title = self.item.title
self.buttonNode.pressed = { [weak self] in
self?.item.action()
}
// if self.item.isLoading != self.currentIsLoading {
// self.currentIsLoading = self.item.isLoading
//
// if self.currentIsLoading {
// self.buttonNode.transitionToProgress()
// } else {
// self.buttonNode.transitionFromProgress()
// }
// }
// self.buttonNode.pressed = { [weak self] in
// self?.item.action()
// }
}
override func updateBackgroundAlpha(_ alpha: CGFloat, transition: ContainedViewLayoutTransition) {
@ -86,16 +98,16 @@ final class CreateGiveawayFooterItemNode: ItemListControllerFooterItemNode {
}
override func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat {
let hadLayout = self.validLayout != nil
self.validLayout = layout
let buttonInset: CGFloat = 16.0
let buttonWidth = layout.size.width - layout.safeInsets.left - layout.safeInsets.right - buttonInset * 2.0
let buttonHeight = self.buttonNode.updateLayout(width: buttonWidth, transition: transition)
let inset: CGFloat = 9.0
let insets = layout.insets(options: [.input])
var panelHeight: CGFloat = buttonHeight + inset * 2.0
var panelHeight: CGFloat = 50.0 + inset * 2.0
let totalPanelHeight: CGFloat
if let inputHeight = layout.inputHeight, inputHeight > 0.0 {
totalPanelHeight = panelHeight + insets.bottom
@ -105,7 +117,53 @@ final class CreateGiveawayFooterItemNode: ItemListControllerFooterItemNode {
}
let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - totalPanelHeight), size: CGSize(width: layout.size.width, height: panelHeight))
transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + buttonInset, y: panelFrame.minY + inset), size: CGSize(width: buttonWidth, height: buttonHeight)))
var buttonTransition: Transition = .easeInOut(duration: 0.2)
if !hadLayout {
buttonTransition = .immediate
}
let buttonSize = self.button.update(
transition: buttonTransition,
component: AnyComponent(
ButtonComponent(
background: ButtonComponent.Background(
color: self.item.theme.list.itemCheckColors.fillColor,
foreground: self.item.theme.list.itemCheckColors.foregroundColor,
pressedColor: self.item.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(ButtonTextContentComponent(
text: self.item.title,
badge: Int(self.item.badgeCount),
textColor: self.item.theme.list.itemCheckColors.foregroundColor,
badgeBackground: self.item.theme.list.itemCheckColors.foregroundColor,
badgeForeground: self.item.theme.list.itemCheckColors.fillColor,
badgeStyle: .roundedRectangle,
badgeIconName: "Premium/BoostButtonIcon",
combinedAlignment: true
))
),
isEnabled: true,
displaysProgress: self.item.isLoading,
action: { [weak self] in
guard let self else {
return
}
self.item.action()
}
)
),
environment: {},
containerSize: CGSize(width: buttonWidth, height: 50.0)
)
if let view = self.button.view {
if view.superview == nil {
self.view.addSubview(view)
}
transition.updateFrame(view: view, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + buttonInset, y: panelFrame.minY + inset), size: buttonSize))
}
transition.updateFrame(node: self.backgroundNode, frame: panelFrame)
self.backgroundNode.update(size: panelFrame.size, transition: transition)

@ -9,52 +9,33 @@ import PresentationDataUtils
import Markdown
import ComponentFlow
final class CreateGiveawayHeaderItem: ListViewItem, ItemListItem {
final class CreateGiveawayHeaderItem: ItemListControllerHeaderItem {
let theme: PresentationTheme
let title: String
let text: String
let sectionId: ItemListSectionId
let cancel: () -> Void
init(theme: PresentationTheme, title: String, text: String, sectionId: ItemListSectionId) {
init(theme: PresentationTheme, title: String, text: String, cancel: @escaping () -> Void) {
self.theme = theme
self.title = title
self.text = text
self.sectionId = sectionId
self.cancel = cancel
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = CreateGiveawayHeaderItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
func isEqual(to: ItemListControllerHeaderItem) -> Bool {
if let item = to as? CreateGiveawayHeaderItem {
return self.theme === item.theme && self.title == item.title && self.text == item.text
} else {
return false
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
guard let nodeValue = node() as? CreateGiveawayHeaderItemNode else {
assertionFailure()
return
}
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
func node(current: ItemListControllerHeaderItemNode?) -> ItemListControllerHeaderItemNode {
if let current = current as? CreateGiveawayHeaderItemNode {
current.item = self
return current
} else {
return CreateGiveawayHeaderItemNode(item: self)
}
}
}
@ -62,31 +43,63 @@ final class CreateGiveawayHeaderItem: ListViewItem, ItemListItem {
private let titleFont = Font.semibold(20.0)
private let textFont = Font.regular(15.0)
class CreateGiveawayHeaderItemNode: ListViewItemNode {
private let titleNode: TextNode
private let textNode: TextNode
class CreateGiveawayHeaderItemNode: ItemListControllerHeaderItemNode {
private let backgroundNode: NavigationBackgroundNode
private let separatorNode: ASDisplayNode
private let titleNode: ImmediateTextNode
private let textNode: ImmediateTextNode
private let cancelNode: HighlightableButtonNode
private var hostView: ComponentHostView<Empty>?
private var params: (AnyComponent<Empty>, CGSize, ListViewItemNodeLayout)?
private var component: AnyComponent<Empty>?
private var validLayout: ContainerViewLayout?
private var item: CreateGiveawayHeaderItem?
fileprivate var item: CreateGiveawayHeaderItem {
didSet {
self.updateItem()
if let layout = self.validLayout {
let _ = self.updateLayout(layout: layout, transition: .immediate)
}
}
}
init() {
self.titleNode = TextNode()
init(item: CreateGiveawayHeaderItem) {
self.item = item
self.backgroundNode = NavigationBackgroundNode(color: item.theme.rootController.navigationBar.blurredBackgroundColor)
self.separatorNode = ASDisplayNode()
self.titleNode = ImmediateTextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.textNode = TextNode()
self.textNode = ImmediateTextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.contentMode = .left
self.textNode.contentsScale = UIScreen.main.scale
self.textNode.maximumNumberOfLines = 0
super.init(layerBacked: false, dynamicBounce: false)
self.cancelNode = HighlightableButtonNode()
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.separatorNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.cancelNode)
self.cancelNode.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside)
self.updateItem()
}
@objc private func cancelPressed() {
self.item.cancel()
}
override func didLoad() {
@ -94,76 +107,121 @@ class CreateGiveawayHeaderItemNode: ListViewItemNode {
let hostView = ComponentHostView<Empty>()
self.hostView = hostView
self.view.addSubview(hostView)
self.view.insertSubview(hostView, at: 0)
if let (component, containerSize, layout) = self.params {
if let layout = self.validLayout, let component = self.component {
let containerSize = CGSize(width: min(414.0, layout.size.width), height: 220.0)
let size = hostView.update(
transition: .immediate,
component: component,
environment: {},
containerSize: containerSize
)
hostView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - size.width) / 2.0), y: -121.0), size: size)
hostView.bounds = CGRect(origin: .zero, size: size)
}
}
func asyncLayout() -> (_ item: CreateGiveawayHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeTextLayout = TextNode.asyncLayout(self.textNode)
func updateItem() {
self.backgroundNode.updateColor(color: self.item.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate)
self.separatorNode.backgroundColor = self.item.theme.rootController.navigationBar.separatorColor
return { item, params, neighbors in
let topInset: CGFloat = 2.0
let leftInset: CGFloat = 24.0 + params.leftInset
let attributedTitle = NSAttributedString(string: self.item.title, font: titleFont, textColor: self.item.theme.list.itemPrimaryTextColor, paragraphAlignment: .center)
let attributedText = NSAttributedString(string: self.item.text, font: textFont, textColor: self.item.theme.list.freeTextColor, paragraphAlignment: .center)
let attributedTitle = NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor, paragraphAlignment: .center)
let attributedText = NSAttributedString(string: item.text, font: textFont, textColor: item.theme.list.freeTextColor, paragraphAlignment: .center)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedTitle, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let contentSize = CGSize(width: params.width, height: topInset + titleLayout.size.height + textLayout.size.height + 80.0)
let insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
let component = AnyComponent(PremiumStarComponent(isIntro: true, isVisible: true, hasIdleAnimations: true))
let containerSize = CGSize(width: min(414.0, layout.size.width), height: 220.0)
if let hostView = strongSelf.hostView {
let size = hostView.update(
transition: .immediate,
component: component,
environment: {},
containerSize: containerSize
)
hostView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - size.width) / 2.0), y: -121.0), size: size)
}
var origin: CGFloat = 78.0
let _ = titleApply()
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleLayout.size.width) / 2.0), y: origin), size: titleLayout.size)
origin += titleLayout.size.height + 10.0
let _ = textApply()
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - textLayout.size.width) / 2.0), y: origin), size: textLayout.size)
strongSelf.params = (component, containerSize, layout)
}
})
self.titleNode.attributedText = attributedTitle
self.textNode.attributedText = attributedText
self.cancelNode.setAttributedTitle(NSAttributedString(string: "Cancel", font: Font.regular(17.0), textColor: self.item.theme.rootController.navigationBar.accentTextColor), for: .normal)
}
override func updateContentOffset(_ contentOffset: CGFloat, transition: ContainedViewLayoutTransition) {
guard let layout = self.validLayout else {
return
}
let navigationHeight: CGFloat = 56.0
let statusBarHeight = layout.statusBarHeight ?? 0.0
let topInset : CGFloat = 0.0
let titleOffsetDelta = (topInset + 160.0) - (statusBarHeight + (navigationHeight - statusBarHeight) / 2.0)
let topContentOffset = contentOffset + max(0.0, min(1.0, contentOffset / titleOffsetDelta)) * 10.0
let titleOffset = topContentOffset
let fraction = max(0.0, min(1.0, titleOffset / titleOffsetDelta))
let titleScale = 1.0 - fraction * 0.18
let topPanelAlpha = min(20.0, max(0.0, contentOffset - 95.0)) / 20.0
transition.updateAlpha(node: self.backgroundNode, alpha: topPanelAlpha)
transition.updateAlpha(node: self.separatorNode, alpha: topPanelAlpha)
let starPosition = CGPoint(
x: layout.size.width / 2.0,
y: -contentOffset + 80.0
)
if let view = self.hostView {
transition.updatePosition(layer: view.layer, position: starPosition)
}
let titlePosition = CGPoint(
x: layout.size.width / 2.0,
y: max(topInset + 170.0 - titleOffset, statusBarHeight + (navigationHeight - statusBarHeight) / 2.0)
)
transition.updatePosition(node: self.titleNode, position: titlePosition)
transition.updateTransformScale(node: self.titleNode, scale: titleScale)
let textPosition = CGPoint(
x: layout.size.width / 2.0,
y: -contentOffset + 212.0
)
transition.updatePosition(node: self.textNode, position: textPosition)
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
override func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat {
let leftInset: CGFloat = 24.0
let navigationBarHeight: CGFloat = 56.0
let constrainedSize = CGSize(width: layout.size.width - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude)
let titleSize = self.titleNode.updateLayout(constrainedSize)
let textSize = self.textNode.updateLayout(constrainedSize)
let cancelSize = self.cancelNode.measure(constrainedSize)
transition.updateFrame(node: self.cancelNode, frame: CGRect(origin: CGPoint(x: 16.0 + layout.safeInsets.left, y: floorToScreenPixels((navigationBarHeight - cancelSize.height) / 2.0)), size: cancelSize))
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: .zero, size: CGSize(width: layout.size.width, height: navigationBarHeight)))
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel)))
self.backgroundNode.update(size: CGSize(width: layout.size.width, height: navigationBarHeight), transition: transition)
let component = AnyComponent(PremiumStarComponent(isIntro: true, isVisible: true, hasIdleAnimations: true))
let containerSize = CGSize(width: min(414.0, layout.size.width), height: 220.0)
if let hostView = self.hostView {
let size = hostView.update(
transition: .immediate,
component: component,
environment: {},
containerSize: containerSize
)
hostView.bounds = CGRect(origin: .zero, size: size)
}
self.titleNode.bounds = CGRect(origin: .zero, size: titleSize)
self.textNode.bounds = CGRect(origin: .zero, size: textSize)
let contentHeight = titleSize.height + textSize.height + 128.0
self.component = component
self.validLayout = layout
return contentHeight
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if let hostView = self.hostView, hostView.frame.contains(point) {
return true
} else {
return super.point(inside: point, with: event)
}
}
}

@ -11,25 +11,72 @@ import PresentationDataUtils
import AccountContext
import AvatarNode
public final class GiftModeItem: ListViewItem, ItemListItem {
public final class GiftOptionItem: ListViewItem, ItemListItem {
public struct Icon: Equatable {
public enum Color {
case blue
case green
case red
case violet
}
public let color: Color
public let name: String
public init(
color: Color,
name: String
) {
self.color = color
self.name = name
}
}
public enum Font {
case regular
case bold
}
public enum SubtitleFont {
case regular
case small
}
public enum Label {
case generic(String)
case boosts(Int32)
var string: String {
switch self {
case let .generic(value):
return value
case let .boosts(value):
return "\(value)"
}
}
}
let presentationData: ItemListPresentationData
let context: AccountContext
let iconName: String?
let icon: Icon?
let title: String
let titleFont: Font
let subtitle: String?
let subtitleFont: SubtitleFont
let subtitleActive: Bool
let label: String?
let label: Label?
let badge: String?
let isSelected: Bool
let isSelected: Bool?
public let sectionId: ItemListSectionId
let action: (() -> Void)?
public init(presentationData: ItemListPresentationData, context: AccountContext, iconName: String? = nil, title: String, subtitle: String?, subtitleActive: Bool = false, label: String?, badge: String?, isSelected: Bool, sectionId: ItemListSectionId, action: (() -> Void)?) {
public init(presentationData: ItemListPresentationData, context: AccountContext, icon: Icon? = nil, title: String, titleFont: Font = .regular, subtitle: String?, subtitleFont: SubtitleFont = .regular, subtitleActive: Bool = false, label: Label? = nil, badge: String? = nil, isSelected: Bool? = nil, sectionId: ItemListSectionId, action: (() -> Void)?) {
self.presentationData = presentationData
self.iconName = iconName
self.icon = icon
self.context = context
self.title = title
self.titleFont = titleFont
self.subtitle = subtitle
self.subtitleFont = subtitleFont
self.subtitleActive = subtitleActive
self.label = label
self.badge = badge
@ -40,7 +87,7 @@ public final class GiftModeItem: ListViewItem, ItemListItem {
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = GiftModeItemNode()
let node = GiftOptionItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
@ -56,7 +103,7 @@ public final class GiftModeItem: ListViewItem, ItemListItem {
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? GiftModeItemNode {
if let nodeValue = node() as? GiftOptionItemNode {
let makeLayout = nodeValue.asyncLayout()
var animated = true
@ -76,7 +123,9 @@ public final class GiftModeItem: ListViewItem, ItemListItem {
}
}
public var selectable: Bool = true
public var selectable: Bool {
return self.action != nil
}
public func selected(listView: ListView){
listView.clearHighlightAnimated(true)
@ -84,7 +133,7 @@ public final class GiftModeItem: ListViewItem, ItemListItem {
}
}
class GiftModeItemNode: ItemListRevealOptionsItemNode {
class GiftOptionItemNode: ItemListRevealOptionsItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
@ -96,14 +145,18 @@ class GiftModeItemNode: ItemListRevealOptionsItemNode {
return self.containerNode
}
fileprivate var avatarNode: ASImageNode?
fileprivate var iconNode: ASImageNode?
private let titleNode: TextNode
private let statusNode: TextNode
private var statusArrowNode: ASImageNode?
private var labelBackgroundNode: ASImageNode?
private let labelNode: TextNode
private var labelIconNode: ASImageNode?
private let badgeTextNode: TextNode
private var badgeBackgroundNode: ASImageNode?
private var layoutParams: (GiftModeItem, ListViewItemLayoutParams, ItemListNeighbors)?
private var layoutParams: (GiftOptionItem, ListViewItemLayoutParams, ItemListNeighbors)?
private var selectableControlNode: ItemListSelectableControlNode?
@ -172,17 +225,31 @@ class GiftModeItemNode: ItemListRevealOptionsItemNode {
item.action?()
}
func asyncLayout() -> (_ item: GiftModeItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
func asyncLayout() -> (_ item: GiftOptionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeStatusLayout = TextNode.asyncLayout(self.statusNode)
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let makeBadgeLayout = TextNode.asyncLayout(self.badgeTextNode)
let selectableControlLayout = ItemListSelectableControlNode.asyncLayout(self.selectableControlNode)
let currentItem = self.layoutParams?.0
return { item, params, neighbors in
let titleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 17.0 / 17.0))
let statusFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0))
let titleFont: UIFont
switch item.titleFont {
case .regular:
titleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 17.0 / 17.0))
case .bold:
titleFont = Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize * 17.0 / 17.0))
}
let statusFont: UIFont
switch item.subtitleFont {
case .regular:
statusFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
case .small:
statusFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 13.0 / 17.0))
}
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
@ -193,16 +260,28 @@ class GiftModeItemNode: ItemListRevealOptionsItemNode {
let titleAttributedString = NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)
let statusAttributedString = NSAttributedString(string: item.subtitle ?? "", font: statusFont, textColor: item.subtitleActive ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemSecondaryTextColor)
let labelAttributedString = NSAttributedString(string: item.label ?? "", font: titleFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
let badgeAttributedString = NSAttributedString(string: item.badge ?? "", font: Font.with(size: 14.0, design: .round, weight: .semibold), textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor)
let leftInset: CGFloat = 17.0 + params.leftInset
var avatarInset: CGFloat = 0.0
if let _ = item.iconName {
avatarInset += 40.0
let labelColor: UIColor
let labelFont: UIFont
if let label = item.label, case .boosts = label {
labelColor = item.presentationData.theme.list.itemAccentColor
labelFont = Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
} else {
labelColor = item.presentationData.theme.list.itemSecondaryTextColor
labelFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 17.0 / 17.0))
}
let verticalInset: CGFloat = 11.0
let labelAttributedString = NSAttributedString(string: item.label?.string ?? "", font: labelFont, textColor: labelColor)
let leftInset: CGFloat = 14.0 + params.leftInset
var avatarInset: CGFloat = 0.0
if let _ = item.icon {
avatarInset += 48.0
}
let verticalInset: CGFloat = 10.0
let titleSpacing: CGFloat = 2.0
let insets = itemListNeighborsGroupedInsets(neighbors, params)
@ -211,9 +290,11 @@ class GiftModeItemNode: ItemListRevealOptionsItemNode {
var selectableControlSizeAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)?
var editingOffset: CGFloat = 0.0
let sizeAndApply = selectableControlLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, item.isSelected, false)
selectableControlSizeAndApply = sizeAndApply
editingOffset = sizeAndApply.0
if let isSelected = item.isSelected {
let sizeAndApply = selectableControlLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, isSelected, false)
selectableControlSizeAndApply = sizeAndApply
editingOffset = sizeAndApply.0
}
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: labelAttributedString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: .greatestFiniteMagnitude)))
@ -222,6 +303,8 @@ class GiftModeItemNode: ItemListRevealOptionsItemNode {
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: textConstrainedWidth, height: .greatestFiniteMagnitude)))
let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: textConstrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (badgeLayout, badgeApply) = makeBadgeLayout(TextNodeLayoutArguments(attributedString: badgeAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: textConstrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + statusLayout.size.height)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
@ -249,30 +332,35 @@ class GiftModeItemNode: ItemListRevealOptionsItemNode {
}
let iconSize = CGSize(width: 40.0, height: 40.0)
if let iconName = item.iconName {
if let icon = item.icon {
let iconNode: ASImageNode
if let current = strongSelf.avatarNode {
if let current = strongSelf.iconNode {
iconNode = current
} else {
iconNode = ASImageNode()
iconNode.displaysAsynchronously = false
strongSelf.addSubnode(iconNode)
strongSelf.avatarNode = iconNode
strongSelf.iconNode = iconNode
}
let colors: [UIColor]
if iconName.contains("away") {
colors = [UIColor(rgb: 0x4faaff), UIColor(rgb: 0x017aff)]
} else {
colors = [UIColor(rgb: 0xc36eff), UIColor(rgb: 0x8c61fa)]
switch icon.color {
case .blue:
colors = [UIColor(rgb: 0x2a9ef1), UIColor(rgb: 0x71d4fc)]
case .green:
colors = [UIColor(rgb: 0x54cb68), UIColor(rgb: 0xa0de7e)]
case .red:
colors = [UIColor(rgb: 0xff516a), UIColor(rgb: 0xff885e)]
case .violet:
colors = [UIColor(rgb: 0xd569ec), UIColor(rgb: 0xe0a2f3)]
}
if iconNode.image == nil {
iconNode.image = generateAvatarImage(size: iconSize, icon: generateTintedImage(image: UIImage(bundleImageName: iconName), color: .white), iconScale: 1.0, cornerRadius: 20.0, color: .blue, customColors: colors.reversed())
iconNode.image = generateAvatarImage(size: iconSize, icon: generateTintedImage(image: UIImage(bundleImageName: icon.name), color: .white), iconScale: 1.0, cornerRadius: 20.0, color: .blue, customColors: colors)
}
let iconFrame = CGRect(origin: CGPoint(x: leftInset + 38.0, y: floorToScreenPixels((layout.contentSize.height - iconSize.height) / 2.0)), size: iconSize)
let iconFrame = CGRect(origin: CGPoint(x: leftInset - 3.0 + editingOffset, y: floorToScreenPixels((layout.contentSize.height - iconSize.height) / 2.0)), size: iconSize)
iconNode.frame = iconFrame
}
@ -304,6 +392,7 @@ class GiftModeItemNode: ItemListRevealOptionsItemNode {
let _ = titleApply()
let _ = statusApply()
let _ = labelApply()
let _ = badgeApply()
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
@ -332,7 +421,7 @@ class GiftModeItemNode: ItemListRevealOptionsItemNode {
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset + editingOffset
bottomStripeInset = leftInset + editingOffset + avatarInset
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
@ -350,9 +439,91 @@ class GiftModeItemNode: ItemListRevealOptionsItemNode {
transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)))
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)))
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + editingOffset + avatarInset, y: verticalInset), size: titleLayout.size))
transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + editingOffset + avatarInset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: statusLayout.size))
transition.updateFrame(node: strongSelf.labelNode, frame: CGRect(origin: CGPoint(x: layoutSize.width - rightInset - labelLayout.size.width - 18.0, y: floorToScreenPixels((layout.contentSize.height - labelLayout.size.height) / 2.0)), size: labelLayout.size))
let titleVerticalOriginY: CGFloat
if statusLayout.size.height > 0.0 {
titleVerticalOriginY = verticalInset
} else {
titleVerticalOriginY = floorToScreenPixels((contentSize.height - titleLayout.size.height) / 2.0)
}
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + editingOffset + avatarInset, y: titleVerticalOriginY), size: titleLayout.size))
var badgeOffset: CGFloat = 0.0
if badgeLayout.size.width > 0.0 {
let badgeFrame = CGRect(origin: CGPoint(x: leftInset + editingOffset + avatarInset + 2.0, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: badgeLayout.size)
let badgeBackgroundFrame = badgeFrame.insetBy(dx: -2.0, dy: -2.0)
let badgeBackgroundNode: ASImageNode
if let current = strongSelf.badgeBackgroundNode {
badgeBackgroundNode = current
} else {
badgeBackgroundNode = ASImageNode()
badgeBackgroundNode.displaysAsynchronously = false
badgeBackgroundNode.image = generateStretchableFilledCircleImage(radius: 5.0, color: item.presentationData.theme.list.itemCheckColors.fillColor)
strongSelf.badgeBackgroundNode = badgeBackgroundNode
strongSelf.containerNode.addSubnode(badgeBackgroundNode)
strongSelf.containerNode.addSubnode(strongSelf.badgeTextNode)
}
transition.updateFrame(node: badgeBackgroundNode, frame: badgeBackgroundFrame)
transition.updateFrame(node: strongSelf.badgeTextNode, frame: badgeFrame)
badgeOffset = badgeLayout.size.width + 10.0
}
transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + editingOffset + avatarInset + badgeOffset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: statusLayout.size))
if let label = item.label, case .boosts = label {
let backgroundNode: ASImageNode
let iconNode: ASImageNode
if let currentBackground = strongSelf.labelBackgroundNode, let currentIcon = strongSelf.labelIconNode {
backgroundNode = currentBackground
iconNode = currentIcon
} else {
backgroundNode = ASImageNode()
backgroundNode.displaysAsynchronously = false
backgroundNode.image = generateStretchableFilledCircleImage(radius: 13.0, color: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.1))
strongSelf.containerNode.insertSubnode(backgroundNode, at: 1)
iconNode = ASImageNode()
iconNode.displaysAsynchronously = false
iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Premium/BoostChannel"), color: item.presentationData.theme.list.itemAccentColor)
strongSelf.containerNode.addSubnode(iconNode)
strongSelf.labelBackgroundNode = backgroundNode
strongSelf.labelIconNode = backgroundNode
}
if let icon = iconNode.image {
let labelFrame = CGRect(origin: CGPoint(x: layoutSize.width - rightInset - labelLayout.size.width - 21.0, y: floorToScreenPixels((layout.contentSize.height - labelLayout.size.height) / 2.0)), size: labelLayout.size)
let iconFrame = CGRect(origin: CGPoint(x: labelFrame.minX - icon.size.width - 2.0, y: labelFrame.minY - 1.0), size: icon.size)
let totalFrame = CGRect(x: iconFrame.minX - 7.0, y: labelFrame.minY - 4.0, width: iconFrame.width + labelFrame.width + 18.0, height: 26.0)
transition.updateFrame(node: backgroundNode, frame: totalFrame)
transition.updateFrame(node: strongSelf.labelNode, frame: labelFrame)
transition.updateFrame(node: iconNode, frame: iconFrame)
}
} else {
transition.updateFrame(node: strongSelf.labelNode, frame: CGRect(origin: CGPoint(x: layoutSize.width - rightInset - labelLayout.size.width - 18.0, y: floorToScreenPixels((layout.contentSize.height - labelLayout.size.height) / 2.0)), size: labelLayout.size))
}
if item.subtitleActive {
let statusArrowNode: ASImageNode
if let current = strongSelf.statusArrowNode {
statusArrowNode = current
} else {
statusArrowNode = ASImageNode()
statusArrowNode.displaysAsynchronously = false
statusArrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: item.presentationData.theme.list.itemAccentColor)
strongSelf.statusArrowNode = statusArrowNode
strongSelf.containerNode.addSubnode(statusArrowNode)
}
if let arrowSize = statusArrowNode.image?.size {
transition.updateFrame(node: statusArrowNode, frame: CGRect(origin: CGPoint(x: leftInset + editingOffset + avatarInset + statusLayout.size.width + 4.0, y: strongSelf.titleNode.frame.maxY + titleSpacing + 4.0), size: arrowSize))
}
} else if let statusArrowNode = strongSelf.statusArrowNode {
strongSelf.statusArrowNode = nil
statusArrowNode.removeFromSupernode()
}
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: strongSelf.backgroundNode.frame.height + UIScreenPixel + UIScreenPixel))

@ -29,19 +29,28 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
let action: () -> Void
let cancel: () -> Void
let openPeer: (EnginePeer) -> Void
let openMessage: (EngineMessage.Id) -> Void
let copyLink: (String) -> Void
let shareLink: (String) -> Void
init(
context: AccountContext,
giftCode: PremiumGiftCodeInfo,
action: @escaping () -> Void,
cancel: @escaping () -> Void,
openPeer: @escaping (EnginePeer) -> Void
openPeer: @escaping (EnginePeer) -> Void,
openMessage: @escaping (EngineMessage.Id) -> Void,
copyLink: @escaping (String) -> Void,
shareLink: @escaping (String) -> Void
) {
self.context = context
self.giftCode = giftCode
self.action = action
self.cancel = cancel
self.openPeer = openPeer
self.openMessage = openMessage
self.copyLink = copyLink
self.shareLink = shareLink
}
static func ==(lhs: PremiumGiftCodeSheetContent, rhs: PremiumGiftCodeSheetContent) -> Bool {
@ -210,7 +219,7 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
LinkButtonContentComponent(theme: environment.theme, text: link)
),
action: {
UIPasteboard.general.string = link
component.copyLink(link)
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
@ -231,6 +240,7 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
content: AnyComponent(PeerCellComponent(context: context.component.context, textColor: tableLinkColor, peer: fromPeer)),
action: {
if let peer = fromPeer {
component.cancel()
component.openPeer(peer)
}
}
@ -247,6 +257,7 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
content: AnyComponent(PeerCellComponent(context: context.component.context, textColor: tableLinkColor, peer: toPeer)),
action: {
if let peer = toPeer {
component.cancel()
component.openPeer(peer)
}
}
@ -268,12 +279,21 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
)
))
let giftReason = giftCode.isGiveaway ? "Giveaway" : "Gift"
let giftReason = giftCode.isGiveaway ? "Giveaway" : "You were selected by the channel"
tableItems.append(.init(
id: "reason",
title: "Reason",
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: giftReason, font: tableFont, textColor: tableLinkColor)))
Button(
content: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: giftReason, font: tableFont, textColor: giftCode.messageId != nil ? tableLinkColor : tableTextColor)))),
isEnabled: true,
action: {
component.cancel()
if let messageId = giftCode.messageId {
component.openMessage(messageId)
}
}
)
)
))
tableItems.append(.init(
@ -298,7 +318,18 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
text: .markdown(text: additionalText, attributes: markdownAttributes),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.1
lineSpacing: 0.1,
highlightColor: linkColor.withAlphaComponent(0.2),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { attributes, _ in
component.shareLink("https://t.me/giftcode/\(giftCode.slug)")
}
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
@ -383,19 +414,28 @@ private final class PremiumGiftCodeSheetComponent: CombinedComponent {
let action: () -> Void
let cancel: () -> Void
let openPeer: (EnginePeer) -> Void
let openMessage: (EngineMessage.Id) -> Void
let copyLink: (String) -> Void
let shareLink: (String) -> Void
init(
context: AccountContext,
giftCode: PremiumGiftCodeInfo,
action: @escaping () -> Void,
cancel: @escaping () -> Void,
openPeer: @escaping (EnginePeer) -> Void
openPeer: @escaping (EnginePeer) -> Void,
openMessage: @escaping (EngineMessage.Id) -> Void,
copyLink: @escaping (String) -> Void,
shareLink: @escaping (String) -> Void
) {
self.context = context
self.giftCode = giftCode
self.action = action
self.cancel = cancel
self.openPeer = openPeer
self.openMessage = openMessage
self.copyLink = copyLink
self.shareLink = shareLink
}
static func ==(lhs: PremiumGiftCodeSheetComponent, rhs: PremiumGiftCodeSheetComponent) -> Bool {
@ -429,7 +469,10 @@ private final class PremiumGiftCodeSheetComponent: CombinedComponent {
}
})
},
openPeer: context.component.openPeer
openPeer: context.component.openPeer,
openMessage: context.component.openMessage,
copyLink: context.component.copyLink,
shareLink: context.component.shareLink
)),
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
animateOut: animateOut
@ -481,13 +524,28 @@ public class PremiumGiftCodeScreen: ViewControllerComponentContainer {
forceDark: Bool = false,
cancel: @escaping () -> Void = {},
action: @escaping () -> Void,
openPeer: @escaping (EnginePeer) -> Void = { _ in }
openPeer: @escaping (EnginePeer) -> Void = { _ in },
openMessage: @escaping (EngineMessage.Id) -> Void = { _ in },
shareLink: @escaping (String) -> Void = { _ in }
) {
self.context = context
super.init(context: context, component: PremiumGiftCodeSheetComponent(context: context, giftCode: giftCode, action: action, cancel: cancel, openPeer: openPeer), navigationBarAppearance: .none, statusBarStyle: .ignore, theme: forceDark ? .dark : .default)
var copyLinkImpl: ((String) -> Void)?
super.init(context: context, component: PremiumGiftCodeSheetComponent(context: context, giftCode: giftCode, action: action, cancel: cancel, openPeer: openPeer, openMessage: openMessage, copyLink: { link in
copyLinkImpl?(link)
}, shareLink: shareLink), navigationBarAppearance: .none, statusBarStyle: .ignore, theme: forceDark ? .dark : .default)
self.navigationPresentation = .flatModal
copyLinkImpl = { [weak self] link in
UIPasteboard.general.string = link
guard let self else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, position: .top, action: { _ in return true }), in: .window(.root))
}
}
required public init(coder aDecoder: NSCoder) {

@ -19,7 +19,6 @@ import InAppPurchaseManager
import ConfettiEffect
import TextFormat
import UniversalMediaPlayer
import AttachmentUI
public enum PremiumGiftSource: Equatable {
case profile
@ -674,6 +673,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
let _ = (self.context.engine.payments.canPurchasePremium(purpose: purpose)
|> deliverOnMainQueue).start(next: { [weak self] available in
if let strongSelf = self {
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
if available {
strongSelf.paymentDisposable.set((inAppPurchaseManager.buyProduct(product.storeProduct, purpose: purpose)
|> deliverOnMainQueue).start(next: { [weak self] status in
@ -693,7 +693,6 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
strongSelf.updateInProgress(false)
strongSelf.updated(transition: .immediate)
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
var errorText: String?
switch error {
case .generic:
@ -992,7 +991,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
}
}
public final class PremiumGiftScreen: ViewControllerComponentContainer, AttachmentContainable {
open class PremiumGiftScreen: ViewControllerComponentContainer {
fileprivate let context: AccountContext
private var didSetReady = false
@ -1005,7 +1004,9 @@ public final class PremiumGiftScreen: ViewControllerComponentContainer, Attachme
public weak var containerView: UIView?
public var animationColor: UIColor?
fileprivate let mainButtonStatePromise = Promise<AttachmentMainButtonState?>(nil)
public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in }
public let mainButtonStatePromise = Promise<AttachmentMainButtonState?>(nil)
private let mainButtonActionSlot = ActionSlot<Void>()
public init(context: AccountContext, peerId: PeerId, options: [CachedPremiumGiftOption], source: PremiumGiftSource, pushController: @escaping (ViewController) -> Void, completion: @escaping () -> Void) {
@ -1092,55 +1093,7 @@ public final class PremiumGiftScreen: ViewControllerComponentContainer, Attachme
}
}
@objc fileprivate func mainButtonPressed() {
@objc public func mainButtonPressed() {
self.mainButtonActionSlot.invoke(Void())
}
public var requestAttachmentMenuExpansion: () -> Void = {}
public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in }
public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in }
public var cancelPanGesture: () -> Void = { }
public var isContainerPanning: () -> Bool = { return false }
public var isContainerExpanded: () -> Bool = { return false }
public var mediaPickerContext: AttachmentMediaPickerContext? {
return PremiumGiftContext(controller: self)
}
}
private final class PremiumGiftContext: AttachmentMediaPickerContext {
private weak var controller: PremiumGiftScreen?
var selectionCount: Signal<Int, NoError> {
return .single(0)
}
var caption: Signal<NSAttributedString?, NoError> {
return .single(nil)
}
public var loadingProgress: Signal<CGFloat?, NoError> {
return .single(nil)
}
public var mainButtonState: Signal<AttachmentMainButtonState?, NoError> {
return self.controller?.mainButtonStatePromise.get() ?? .single(nil)
}
init(controller: PremiumGiftScreen) {
self.controller = controller
}
func setCaption(_ caption: NSAttributedString) {
}
func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode) {
}
func schedule() {
}
func mainButtonAction() {
self.controller?.mainButtonPressed()
}
}

@ -12,18 +12,14 @@ import PresentationDataUtils
final class SubscriptionsCountItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let strings: PresentationStrings
let text: String
let value: Int32
let range: Range<Int32>?
let sectionId: ItemListSectionId
let updated: (Int32) -> Void
init(theme: PresentationTheme, strings: PresentationStrings, text: String, value: Int32, range: Range<Int32>?, sectionId: ItemListSectionId, updated: @escaping (Int32) -> Void) {
init(theme: PresentationTheme, strings: PresentationStrings, value: Int32, sectionId: ItemListSectionId, updated: @escaping (Int32) -> Void) {
self.theme = theme
self.strings = strings
self.text = text
self.value = value
self.range = range
self.sectionId = sectionId
self.updated = updated
}
@ -68,9 +64,13 @@ private final class SubscriptionsCountItemNode: ListViewItemNode {
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let minTextNode: TextNode
private let maxTextNode: TextNode
private let textNode: TextNode
private let label1TextNode: TextNode
private let label3TextNode: TextNode
private let label5TextNode: TextNode
private let label7TextNode: TextNode
private let label10TextNode: TextNode
private let label25TextNode: TextNode
private let label50TextNode: TextNode
private var sliderView: TGPhotoEditorSliderView?
private var item: SubscriptionsCountItem?
@ -88,23 +88,43 @@ private final class SubscriptionsCountItemNode: ListViewItemNode {
self.maskNode = ASImageNode()
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = false
self.label1TextNode = TextNode()
self.label1TextNode.isUserInteractionEnabled = false
self.label1TextNode.displaysAsynchronously = false
self.minTextNode = TextNode()
self.minTextNode.isUserInteractionEnabled = false
self.minTextNode.displaysAsynchronously = false
self.label3TextNode = TextNode()
self.label3TextNode.isUserInteractionEnabled = false
self.label3TextNode.displaysAsynchronously = false
self.maxTextNode = TextNode()
self.maxTextNode.isUserInteractionEnabled = false
self.maxTextNode.displaysAsynchronously = false
self.label5TextNode = TextNode()
self.label5TextNode.isUserInteractionEnabled = false
self.label5TextNode.displaysAsynchronously = false
self.label7TextNode = TextNode()
self.label7TextNode.isUserInteractionEnabled = false
self.label7TextNode.displaysAsynchronously = false
self.label10TextNode = TextNode()
self.label10TextNode.isUserInteractionEnabled = false
self.label10TextNode.displaysAsynchronously = false
self.label25TextNode = TextNode()
self.label25TextNode.isUserInteractionEnabled = false
self.label25TextNode.displaysAsynchronously = false
self.label50TextNode = TextNode()
self.label50TextNode.isUserInteractionEnabled = false
self.label50TextNode.displaysAsynchronously = false
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.textNode)
self.addSubnode(self.minTextNode)
self.addSubnode(self.maxTextNode)
self.addSubnode(self.label1TextNode)
self.addSubnode(self.label3TextNode)
self.addSubnode(self.label5TextNode)
self.addSubnode(self.label7TextNode)
self.addSubnode(self.label10TextNode)
self.addSubnode(self.label25TextNode)
self.addSubnode(self.label50TextNode)
}
override func didLoad() {
@ -114,13 +134,34 @@ private final class SubscriptionsCountItemNode: ListViewItemNode {
sliderView.enablePanHandling = true
sliderView.trackCornerRadius = 2.0
sliderView.lineSize = 4.0
sliderView.dotSize = 5.0
sliderView.dotSize = 8.0
sliderView.minimumValue = 0.0
sliderView.maximumValue = 10.0
sliderView.maximumValue = 6.0
sliderView.startValue = 0.0
sliderView.positionsCount = 7
sliderView.useLinesForPositions = true
sliderView.disablesInteractiveTransitionGestureRecognizer = true
if let item = self.item, let params = self.layoutParams {
sliderView.value = CGFloat(item.value)
var mappedValue: Int32 = 0
switch Int(item.value) {
case 1:
mappedValue = 0
case 3:
mappedValue = 1
case 5:
mappedValue = 2
case 7:
mappedValue = 3
case 10:
mappedValue = 4
case 25:
mappedValue = 5
case 50:
mappedValue = 6
default:
mappedValue = 0
}
sliderView.value = CGFloat(mappedValue)
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.itemSwitchColors.frameColor
sliderView.startColor = item.theme.list.itemSwitchColors.frameColor
@ -137,9 +178,13 @@ private final class SubscriptionsCountItemNode: ListViewItemNode {
func asyncLayout() -> (_ item: SubscriptionsCountItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
let makeTextLayout = TextNode.asyncLayout(self.textNode)
let makeMinTextLayout = TextNode.asyncLayout(self.minTextNode)
let makeMaxTextLayout = TextNode.asyncLayout(self.maxTextNode)
let makeLabel1TextLayout = TextNode.asyncLayout(self.label1TextNode)
let makeLabel3TextLayout = TextNode.asyncLayout(self.label3TextNode)
let makeLabel5TextLayout = TextNode.asyncLayout(self.label5TextNode)
let makeLabel7TextLayout = TextNode.asyncLayout(self.label7TextNode)
let makeLabel10TextLayout = TextNode.asyncLayout(self.label10TextNode)
let makeLabel25TextLayout = TextNode.asyncLayout(self.label25TextNode)
let makeLabel50TextLayout = TextNode.asyncLayout(self.label50TextNode)
return { item, params, neighbors in
var themeUpdated = false
@ -151,13 +196,19 @@ private final class SubscriptionsCountItemNode: ListViewItemNode {
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let (label1TextLayout, label1TextApply) = makeLabel1TextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "1", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let range = item.range ?? (1 ..< 11)
let (label3TextLayout, label3TextApply) = makeLabel3TextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "3", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let (minTextLayout, minTextApply) = makeMinTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "\(range.lowerBound)", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let (label5TextLayout, label5TextApply) = makeLabel5TextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "5", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let (maxTextLayout, maxTextApply) = makeMaxTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "\(range.upperBound - 1)", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let (label7TextLayout, label7TextApply) = makeLabel7TextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "7", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let (label10TextLayout, label10TextApply) = makeLabel10TextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "10", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let (label25TextLayout, label25TextApply) = makeLabel25TextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "25", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let (label50TextLayout, label50TextApply) = makeLabel50TextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "50", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
contentSize = CGSize(width: params.width, height: 88.0)
insets = itemListNeighborsGroupedInsets(neighbors, params)
@ -218,14 +269,31 @@ private final class SubscriptionsCountItemNode: ListViewItemNode {
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
let _ = textApply()
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floor((params.width - textLayout.size.width) / 2.0), y: 12.0), size: textLayout.size)
let _ = label1TextApply()
let _ = label3TextApply()
let _ = label5TextApply()
let _ = label7TextApply()
let _ = label10TextApply()
let _ = label25TextApply()
let _ = label50TextApply()
let _ = minTextApply()
strongSelf.minTextNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 16.0, y: 16.0), size: minTextLayout.size)
let textNodes: [(TextNode, CGSize)] = [
(strongSelf.label1TextNode, label1TextLayout.size),
(strongSelf.label3TextNode, label3TextLayout.size),
(strongSelf.label5TextNode, label5TextLayout.size),
(strongSelf.label7TextNode, label7TextLayout.size),
(strongSelf.label10TextNode, label10TextLayout.size),
(strongSelf.label25TextNode, label25TextLayout.size),
(strongSelf.label50TextNode, label50TextLayout.size)
]
let _ = maxTextApply()
strongSelf.maxTextNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 16.0 - maxTextLayout.size.width, y: 16.0), size: maxTextLayout.size)
let delta = (params.width - params.leftInset - params.rightInset - 20.0 * 2.0) / CGFloat(textNodes.count - 1)
for i in 0 ..< textNodes.count {
let (textNode, textSize) = textNodes[i]
let position = params.leftInset + 20.0 + delta * CGFloat(i)
textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(position - textSize.width / 2.0), y: 15.0), size: textSize)
}
if let sliderView = strongSelf.sliderView {
if themeUpdated {
@ -237,6 +305,29 @@ private final class SubscriptionsCountItemNode: ListViewItemNode {
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 37.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0))
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
var mappedValue: Int32 = 0
switch Int(item.value) {
case 1:
mappedValue = 0
case 3:
mappedValue = 1
case 5:
mappedValue = 2
case 7:
mappedValue = 3
case 10:
mappedValue = 4
case 25:
mappedValue = 5
case 50:
mappedValue = 6
default:
mappedValue = 0
}
if Int32(sliderView.value) != mappedValue {
sliderView.value = CGFloat(mappedValue)
}
}
}
})
@ -255,6 +346,27 @@ private final class SubscriptionsCountItemNode: ListViewItemNode {
guard let sliderView = self.sliderView else {
return
}
self.item?.updated(Int32(sliderView.value))
var mappedValue: Int32 = 1
switch Int(sliderView.value) {
case 0:
mappedValue = 1
case 1:
mappedValue = 3
case 2:
mappedValue = 5
case 3:
mappedValue = 7
case 4:
mappedValue = 10
case 5:
mappedValue = 25
case 6:
mappedValue = 50
default:
mappedValue = 1
}
self.item?.updated(Int32(mappedValue))
}
}

@ -34,8 +34,9 @@ private final class ChannelStatsControllerArguments {
let openPeer: (EnginePeer) -> Void
let expandBoosters: () -> Void
let openGifts: () -> Void
let createPrepaidGiveaway: (Int32, Int32) -> Void
init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>, openMessage: @escaping (MessageId) -> Void, contextAction: @escaping (MessageId, ASDisplayNode, ContextGesture?) -> Void, copyBoostLink: @escaping (String) -> Void, shareBoostLink: @escaping (String) -> Void, openPeer: @escaping (EnginePeer) -> Void, expandBoosters: @escaping () -> Void, openGifts: @escaping () -> Void) {
init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>, openMessage: @escaping (MessageId) -> Void, contextAction: @escaping (MessageId, ASDisplayNode, ContextGesture?) -> Void, copyBoostLink: @escaping (String) -> Void, shareBoostLink: @escaping (String) -> Void, openPeer: @escaping (EnginePeer) -> Void, expandBoosters: @escaping () -> Void, openGifts: @escaping () -> Void, createPrepaidGiveaway: @escaping (Int32, Int32) -> Void) {
self.context = context
self.loadDetailedGraph = loadDetailedGraph
self.openMessageStats = openMessage
@ -45,6 +46,7 @@ private final class ChannelStatsControllerArguments {
self.openPeer = openPeer
self.expandBoosters = expandBoosters
self.openGifts = openGifts
self.createPrepaidGiveaway = createPrepaidGiveaway
}
}
@ -62,6 +64,7 @@ private enum StatsSection: Int32 {
case instantPageInteractions
case boostLevel
case boostOverview
case boostPrepaid
case boosters
case boostLink
case gifts
@ -106,6 +109,10 @@ private enum StatsEntry: ItemListNodeEntry {
case boostOverviewTitle(PresentationTheme, String)
case boostOverview(PresentationTheme, ChannelBoostStatus)
case boostPrepaidTitle(PresentationTheme, String)
case boostPrepaid(Int32, PresentationTheme, String, String, Int32, Int32)
case boostPrepaidInfo(PresentationTheme, String)
case boostersTitle(PresentationTheme, String)
case boostersPlaceholder(PresentationTheme, String)
case booster(Int32, PresentationTheme, PresentationDateTimeFormat, EnginePeer, Int32)
@ -147,6 +154,8 @@ private enum StatsEntry: ItemListNodeEntry {
return StatsSection.boostLevel.rawValue
case .boostOverviewTitle, .boostOverview:
return StatsSection.boostOverview.rawValue
case .boostPrepaidTitle, .boostPrepaid, .boostPrepaidInfo:
return StatsSection.boostPrepaid.rawValue
case .boostersTitle, .boostersPlaceholder, .booster, .boostersExpand, .boostersInfo:
return StatsSection.boosters.rawValue
case .boostLinkTitle, .boostLink, .boostLinkInfo:
@ -208,12 +217,18 @@ private enum StatsEntry: ItemListNodeEntry {
return 2001
case .boostOverview:
return 2002
case .boostersTitle:
case .boostPrepaidTitle:
return 2003
case let .boostPrepaid(index, _, _, _, _, _):
return 2004 + index
case .boostPrepaidInfo:
return 2100
case .boostersTitle:
return 2101
case .boostersPlaceholder:
return 2004
return 2102
case let .booster(index, _, _, _, _):
return 2005 + index
return 2103 + index
case .boostersExpand:
return 10000
case .boostersInfo:
@ -383,6 +398,24 @@ private enum StatsEntry: ItemListNodeEntry {
} else {
return false
}
case let .boostPrepaidTitle(lhsTheme, lhsText):
if case let .boostPrepaidTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .boostPrepaid(lhsIndex, lhsTheme, lhsTitle, lhsSubtitle, lhsMonths, lhsCount):
if case let .boostPrepaid(rhsIndex, rhsTheme, rhsTitle, rhsSubtitle, rhsMonths, rhsCount) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsMonths == rhsMonths, lhsCount == rhsCount {
return true
} else {
return false
}
case let .boostPrepaidInfo(lhsTheme, lhsText):
if case let .boostPrepaidInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .boostersTitle(lhsTheme, lhsText):
if case let .boostersTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
@ -466,12 +499,14 @@ private enum StatsEntry: ItemListNodeEntry {
let .postsTitle(_, text),
let .instantPageInteractionsTitle(_, text),
let .boostOverviewTitle(_, text),
let .boostPrepaidTitle(_, text),
let .boostersTitle(_, text),
let .boostLinkTitle(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .boostersInfo(_, text),
case let .boostPrepaidInfo(_, text),
let .boostersInfo(_, text),
let .boostLinkInfo(_, text),
let .giftsInfo(_, text):
let .giftsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
case let .overview(_, stats):
return StatsOverviewItem(presentationData: presentationData, stats: stats, sectionId: self.section, style: .blocks)
@ -523,9 +558,24 @@ private enum StatsEntry: ItemListNodeEntry {
case let .boostersPlaceholder(_, text):
return ItemListPlaceholderItem(theme: presentationData.theme, text: text, sectionId: self.section, style: .blocks)
case let .gifts(theme, title):
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(theme), title: title, sectionId: self.section, editing: false, action: {
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.addBoostsIcon(theme), title: title, sectionId: self.section, editing: false, action: {
arguments.openGifts()
})
case let .boostPrepaid(_, _, title, subtitle, months, count):
let color: GiftOptionItem.Icon.Color
switch months {
case 3:
color = .green
case 6:
color = .blue
case 12:
color = .red
default:
color = .blue
}
return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: GiftOptionItem.Icon(color: color, name: "Premium/Giveaway"), title: title, titleFont: .bold, subtitle: subtitle, label: .boosts(count), sectionId: self.section, action: {
arguments.createPrepaidGiveaway(months, count)
})
}
}
}
@ -650,6 +700,12 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p
entries.append(.boostOverviewTitle(presentationData.theme, presentationData.strings.Stats_Boosts_OverviewHeader))
entries.append(.boostOverview(presentationData.theme, boostData))
//TODO:localize
entries.append(.boostPrepaidTitle(presentationData.theme, "PREPAID GIVEAWAYS"))
entries.append(.boostPrepaid(0, presentationData.theme, "70 Telegram Premium", "3-month subscriptions", 3, 70))
entries.append(.boostPrepaid(1, presentationData.theme, "200 Telegram Premium", "6-month subscriptions", 6, 200))
entries.append(.boostPrepaidInfo(presentationData.theme, "Select a giveaway you already paid for to set it up."))
let boostersTitle: String
let boostersPlaceholder: String?
let boostersFooter: String?
@ -812,7 +868,11 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
updateState { $0.withUpdatedBoostersExpanded(true) }
},
openGifts: {
let controller = createGiveawayController(context: context, peerId: peerId)
let controller = createGiveawayController(context: context, peerId: peerId, subject: .generic)
pushImpl?(controller)
},
createPrepaidGiveaway: { months, count in
let controller = createGiveawayController(context: context, peerId: peerId, subject: .prepaid(months: months, count: count))
pushImpl?(controller)
})

@ -211,7 +211,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1815593308] = { return Api.DocumentAttribute.parse_documentAttributeImageSize($0) }
dict[1662637586] = { return Api.DocumentAttribute.parse_documentAttributeSticker($0) }
dict[-745541182] = { return Api.DocumentAttribute.parse_documentAttributeVideo($0) }
dict[-40996577] = { return Api.DraftMessage.parse_draftMessage($0) }
dict[-1783606645] = { return Api.DraftMessage.parse_draftMessage($0) }
dict[453805082] = { return Api.DraftMessage.parse_draftMessageEmpty($0) }
dict[-1764723459] = { return Api.EmailVerification.parse_emailVerificationApple($0) }
dict[-1842457175] = { return Api.EmailVerification.parse_emailVerificationCode($0) }
@ -325,6 +325,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-457104426] = { return Api.InputGeoPoint.parse_inputGeoPointEmpty($0) }
dict[-659913713] = { return Api.InputGroupCall.parse_inputGroupCall($0) }
dict[-977967015] = { return Api.InputInvoice.parse_inputInvoiceMessage($0) }
dict[-1734841331] = { return Api.InputInvoice.parse_inputInvoicePremiumGiftCode($0) }
dict[-1020867857] = { return Api.InputInvoice.parse_inputInvoiceSlug($0) }
dict[-122978821] = { return Api.InputMedia.parse_inputMediaContact($0) }
dict[-428884101] = { return Api.InputMedia.parse_inputMediaDice($0) }
@ -385,7 +386,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-380694650] = { return Api.InputPrivacyRule.parse_inputPrivacyValueDisallowChatParticipants($0) }
dict[195371015] = { return Api.InputPrivacyRule.parse_inputPrivacyValueDisallowContacts($0) }
dict[-1877932953] = { return Api.InputPrivacyRule.parse_inputPrivacyValueDisallowUsers($0) }
dict[-1672247580] = { return Api.InputReplyTo.parse_inputReplyToMessage($0) }
dict[121554949] = { return Api.InputReplyTo.parse_inputReplyToMessage($0) }
dict[363917955] = { return Api.InputReplyTo.parse_inputReplyToStory($0) }
dict[1399317950] = { return Api.InputSecureFile.parse_inputSecureFile($0) }
dict[859091184] = { return Api.InputSecureFile.parse_inputSecureFileUploaded($0) }
@ -406,7 +407,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1251549527] = { return Api.InputStickeredMedia.parse_inputStickeredMediaPhoto($0) }
dict[1634697192] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentGiftPremium($0) }
dict[-1551868097] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiftCode($0) }
dict[-566640558] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiveaway($0) }
dict[-381016791] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiveaway($0) }
dict[-1502273946] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumSubscription($0) }
dict[1012306921] = { return Api.InputTheme.parse_inputTheme($0) }
dict[-175567375] = { return Api.InputTheme.parse_inputThemeSlug($0) }
@ -534,7 +535,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-38694904] = { return Api.MessageMedia.parse_messageMediaGame($0) }
dict[1457575028] = { return Api.MessageMedia.parse_messageMediaGeo($0) }
dict[-1186937242] = { return Api.MessageMedia.parse_messageMediaGeoLive($0) }
dict[1202724576] = { return Api.MessageMedia.parse_messageMediaGiveaway($0) }
dict[1116825468] = { return Api.MessageMedia.parse_messageMediaGiveaway($0) }
dict[-156940077] = { return Api.MessageMedia.parse_messageMediaInvoice($0) }
dict[1766936791] = { return Api.MessageMedia.parse_messageMediaPhoto($0) }
dict[1272375192] = { return Api.MessageMedia.parse_messageMediaPoll($0) }
@ -549,7 +550,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[182649427] = { return Api.MessageRange.parse_messageRange($0) }
dict[1328256121] = { return Api.MessageReactions.parse_messageReactions($0) }
dict[-2083123262] = { return Api.MessageReplies.parse_messageReplies($0) }
dict[-1495959709] = { return Api.MessageReplyHeader.parse_messageReplyHeader($0) }
dict[1029445267] = { return Api.MessageReplyHeader.parse_messageReplyHeader($0) }
dict[-1667711039] = { return Api.MessageReplyHeader.parse_messageReplyStoryHeader($0) }
dict[1163625789] = { return Api.MessageViews.parse_messageViews($0) }
dict[975236280] = { return Api.MessagesFilter.parse_inputMessagesFilterChatPhotos($0) }
@ -659,7 +660,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[2061444128] = { return Api.PollResults.parse_pollResults($0) }
dict[1558266229] = { return Api.PopularContact.parse_popularContact($0) }
dict[512535275] = { return Api.PostAddress.parse_postAddress($0) }
dict[-713473172] = { return Api.PremiumGiftCodeOption.parse_premiumGiftCodeOption($0) }
dict[629052971] = { return Api.PremiumGiftCodeOption.parse_premiumGiftCodeOption($0) }
dict[1958953753] = { return Api.PremiumGiftOption.parse_premiumGiftOption($0) }
dict[1596792306] = { return Api.PremiumSubscriptionOption.parse_premiumSubscriptionOption($0) }
dict[-1534675103] = { return Api.PrivacyKey.parse_privacyKeyAbout($0) }
@ -1149,8 +1150,10 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1218005070] = { return Api.messages.VotesList.parse_votesList($0) }
dict[-44166467] = { return Api.messages.WebPage.parse_webPage($0) }
dict[1042605427] = { return Api.payments.BankCardData.parse_bankCardData($0) }
dict[-9426548] = { return Api.payments.CheckedGiftCode.parse_checkedGiftCode($0) }
dict[-1222446760] = { return Api.payments.CheckedGiftCode.parse_checkedGiftCode($0) }
dict[-1362048039] = { return Api.payments.ExportedInvoice.parse_exportedInvoice($0) }
dict[2054937690] = { return Api.payments.GiveawayInfo.parse_giveawayInfo($0) }
dict[952312868] = { return Api.payments.GiveawayInfo.parse_giveawayInfoResults($0) }
dict[-1610250415] = { return Api.payments.PaymentForm.parse_paymentForm($0) }
dict[1891958275] = { return Api.payments.PaymentReceipt.parse_paymentReceipt($0) }
dict[1314881805] = { return Api.payments.PaymentResult.parse_paymentResult($0) }
@ -2034,6 +2037,8 @@ public extension Api {
_1.serialize(buffer, boxed)
case let _1 as Api.payments.ExportedInvoice:
_1.serialize(buffer, boxed)
case let _1 as Api.payments.GiveawayInfo:
_1.serialize(buffer, boxed)
case let _1 as Api.payments.PaymentForm:
_1.serialize(buffer, boxed)
case let _1 as Api.payments.PaymentReceipt:

@ -1,17 +1,24 @@
public extension Api {
indirect enum InputReplyTo: TypeConstructorDescription {
case inputReplyToMessage(flags: Int32, replyToMsgId: Int32, topMsgId: Int32?)
case inputReplyToMessage(flags: Int32, replyToMsgId: Int32, topMsgId: Int32?, replyToPeerId: Api.InputPeer?, quoteText: String?, quoteEntities: [Api.MessageEntity]?)
case inputReplyToStory(userId: Api.InputUser, storyId: Int32)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .inputReplyToMessage(let flags, let replyToMsgId, let topMsgId):
case .inputReplyToMessage(let flags, let replyToMsgId, let topMsgId, let replyToPeerId, let quoteText, let quoteEntities):
if boxed {
buffer.appendInt32(-1672247580)
buffer.appendInt32(121554949)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt32(replyToMsgId, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {serializeInt32(topMsgId!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 1) != 0 {replyToPeerId!.serialize(buffer, true)}
if Int(flags) & Int(1 << 2) != 0 {serializeString(quoteText!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261)
buffer.appendInt32(Int32(quoteEntities!.count))
for item in quoteEntities! {
item.serialize(buffer, true)
}}
break
case .inputReplyToStory(let userId, let storyId):
if boxed {
@ -25,8 +32,8 @@ public extension Api {
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .inputReplyToMessage(let flags, let replyToMsgId, let topMsgId):
return ("inputReplyToMessage", [("flags", flags as Any), ("replyToMsgId", replyToMsgId as Any), ("topMsgId", topMsgId as Any)])
case .inputReplyToMessage(let flags, let replyToMsgId, let topMsgId, let replyToPeerId, let quoteText, let quoteEntities):
return ("inputReplyToMessage", [("flags", flags as Any), ("replyToMsgId", replyToMsgId as Any), ("topMsgId", topMsgId as Any), ("replyToPeerId", replyToPeerId as Any), ("quoteText", quoteText as Any), ("quoteEntities", quoteEntities as Any)])
case .inputReplyToStory(let userId, let storyId):
return ("inputReplyToStory", [("userId", userId as Any), ("storyId", storyId as Any)])
}
@ -39,11 +46,24 @@ public extension Api {
_2 = reader.readInt32()
var _3: Int32?
if Int(_1!) & Int(1 << 0) != 0 {_3 = reader.readInt32() }
var _4: Api.InputPeer?
if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() {
_4 = Api.parse(reader, signature: signature) as? Api.InputPeer
} }
var _5: String?
if Int(_1!) & Int(1 << 2) != 0 {_5 = parseString(reader) }
var _6: [Api.MessageEntity]?
if Int(_1!) & Int(1 << 3) != 0 {if let _ = reader.readInt32() {
_6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self)
} }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil
if _c1 && _c2 && _c3 {
return Api.InputReplyTo.inputReplyToMessage(flags: _1!, replyToMsgId: _2!, topMsgId: _3)
let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil
let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil
let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 {
return Api.InputReplyTo.inputReplyToMessage(flags: _1!, replyToMsgId: _2!, topMsgId: _3, replyToPeerId: _4, quoteText: _5, quoteEntities: _6)
}
else {
return nil
@ -580,7 +600,7 @@ public extension Api {
indirect enum InputStorePaymentPurpose: TypeConstructorDescription {
case inputStorePaymentGiftPremium(userId: Api.InputUser, currency: String, amount: Int64)
case inputStorePaymentPremiumGiftCode(flags: Int32, users: [Api.InputUser], boostPeer: Api.InputPeer?, currency: String, amount: Int64)
case inputStorePaymentPremiumGiveaway(flags: Int32, boostPeer: Api.InputPeer, randomId: Int64, untilDate: Int32, currency: String, amount: Int64)
case inputStorePaymentPremiumGiveaway(flags: Int32, boostPeer: Api.InputPeer, additionalPeers: [Api.InputPeer]?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64)
case inputStorePaymentPremiumSubscription(flags: Int32)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
@ -607,12 +627,17 @@ public extension Api {
serializeString(currency, buffer: buffer, boxed: false)
serializeInt64(amount, buffer: buffer, boxed: false)
break
case .inputStorePaymentPremiumGiveaway(let flags, let boostPeer, let randomId, let untilDate, let currency, let amount):
case .inputStorePaymentPremiumGiveaway(let flags, let boostPeer, let additionalPeers, let randomId, let untilDate, let currency, let amount):
if boxed {
buffer.appendInt32(-566640558)
buffer.appendInt32(-381016791)
}
serializeInt32(flags, buffer: buffer, boxed: false)
boostPeer.serialize(buffer, true)
if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261)
buffer.appendInt32(Int32(additionalPeers!.count))
for item in additionalPeers! {
item.serialize(buffer, true)
}}
serializeInt64(randomId, buffer: buffer, boxed: false)
serializeInt32(untilDate, buffer: buffer, boxed: false)
serializeString(currency, buffer: buffer, boxed: false)
@ -633,8 +658,8 @@ public extension Api {
return ("inputStorePaymentGiftPremium", [("userId", userId as Any), ("currency", currency as Any), ("amount", amount as Any)])
case .inputStorePaymentPremiumGiftCode(let flags, let users, let boostPeer, let currency, let amount):
return ("inputStorePaymentPremiumGiftCode", [("flags", flags as Any), ("users", users as Any), ("boostPeer", boostPeer as Any), ("currency", currency as Any), ("amount", amount as Any)])
case .inputStorePaymentPremiumGiveaway(let flags, let boostPeer, let randomId, let untilDate, let currency, let amount):
return ("inputStorePaymentPremiumGiveaway", [("flags", flags as Any), ("boostPeer", boostPeer as Any), ("randomId", randomId as Any), ("untilDate", untilDate as Any), ("currency", currency as Any), ("amount", amount as Any)])
case .inputStorePaymentPremiumGiveaway(let flags, let boostPeer, let additionalPeers, let randomId, let untilDate, let currency, let amount):
return ("inputStorePaymentPremiumGiveaway", [("flags", flags as Any), ("boostPeer", boostPeer as Any), ("additionalPeers", additionalPeers as Any), ("randomId", randomId as Any), ("untilDate", untilDate as Any), ("currency", currency as Any), ("amount", amount as Any)])
case .inputStorePaymentPremiumSubscription(let flags):
return ("inputStorePaymentPremiumSubscription", [("flags", flags as Any)])
}
@ -693,22 +718,27 @@ public extension Api {
if let signature = reader.readInt32() {
_2 = Api.parse(reader, signature: signature) as? Api.InputPeer
}
var _3: Int64?
_3 = reader.readInt64()
var _4: Int32?
_4 = reader.readInt32()
var _5: String?
_5 = parseString(reader)
var _6: Int64?
_6 = reader.readInt64()
var _3: [Api.InputPeer]?
if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() {
_3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputPeer.self)
} }
var _4: Int64?
_4 = reader.readInt64()
var _5: Int32?
_5 = reader.readInt32()
var _6: String?
_6 = parseString(reader)
var _7: Int64?
_7 = reader.readInt64()
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil
let _c4 = _4 != nil
let _c5 = _5 != nil
let _c6 = _6 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 {
return Api.InputStorePaymentPurpose.inputStorePaymentPremiumGiveaway(flags: _1!, boostPeer: _2!, randomId: _3!, untilDate: _4!, currency: _5!, amount: _6!)
let _c7 = _7 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 {
return Api.InputStorePaymentPurpose.inputStorePaymentPremiumGiveaway(flags: _1!, boostPeer: _2!, additionalPeers: _3, randomId: _4!, untilDate: _5!, currency: _6!, amount: _7!)
}
else {
return nil

@ -741,7 +741,7 @@ public extension Api {
case messageMediaGame(game: Api.Game)
case messageMediaGeo(geo: Api.GeoPoint)
case messageMediaGeoLive(flags: Int32, geo: Api.GeoPoint, heading: Int32?, period: Int32, proximityNotificationRadius: Int32?)
case messageMediaGiveaway(channels: [Int64], quantity: Int32, months: Int32, untilDate: Int32)
case messageMediaGiveaway(flags: Int32, channels: [Int64], quantity: Int32, months: Int32, untilDate: Int32)
case messageMediaInvoice(flags: Int32, title: String, description: String, photo: Api.WebDocument?, receiptMsgId: Int32?, currency: String, totalAmount: Int64, startParam: String, extendedMedia: Api.MessageExtendedMedia?)
case messageMediaPhoto(flags: Int32, photo: Api.Photo?, ttlSeconds: Int32?)
case messageMediaPoll(poll: Api.Poll, results: Api.PollResults)
@ -806,10 +806,11 @@ public extension Api {
serializeInt32(period, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 1) != 0 {serializeInt32(proximityNotificationRadius!, buffer: buffer, boxed: false)}
break
case .messageMediaGiveaway(let channels, let quantity, let months, let untilDate):
case .messageMediaGiveaway(let flags, let channels, let quantity, let months, let untilDate):
if boxed {
buffer.appendInt32(1202724576)
buffer.appendInt32(1116825468)
}
serializeInt32(flags, buffer: buffer, boxed: false)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(channels.count))
for item in channels {
@ -899,8 +900,8 @@ public extension Api {
return ("messageMediaGeo", [("geo", geo as Any)])
case .messageMediaGeoLive(let flags, let geo, let heading, let period, let proximityNotificationRadius):
return ("messageMediaGeoLive", [("flags", flags as Any), ("geo", geo as Any), ("heading", heading as Any), ("period", period as Any), ("proximityNotificationRadius", proximityNotificationRadius as Any)])
case .messageMediaGiveaway(let channels, let quantity, let months, let untilDate):
return ("messageMediaGiveaway", [("channels", channels as Any), ("quantity", quantity as Any), ("months", months as Any), ("untilDate", untilDate as Any)])
case .messageMediaGiveaway(let flags, let channels, let quantity, let months, let untilDate):
return ("messageMediaGiveaway", [("flags", flags as Any), ("channels", channels as Any), ("quantity", quantity as Any), ("months", months as Any), ("untilDate", untilDate as Any)])
case .messageMediaInvoice(let flags, let title, let description, let photo, let receiptMsgId, let currency, let totalAmount, let startParam, let extendedMedia):
return ("messageMediaInvoice", [("flags", flags as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("receiptMsgId", receiptMsgId as Any), ("currency", currency as Any), ("totalAmount", totalAmount as Any), ("startParam", startParam as Any), ("extendedMedia", extendedMedia as Any)])
case .messageMediaPhoto(let flags, let photo, let ttlSeconds):
@ -1034,22 +1035,25 @@ public extension Api {
}
}
public static func parse_messageMediaGiveaway(_ reader: BufferReader) -> MessageMedia? {
var _1: [Int64]?
var _1: Int32?
_1 = reader.readInt32()
var _2: [Int64]?
if let _ = reader.readInt32() {
_1 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self)
_2 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self)
}
var _2: Int32?
_2 = reader.readInt32()
var _3: Int32?
_3 = reader.readInt32()
var _4: Int32?
_4 = reader.readInt32()
var _5: Int32?
_5 = reader.readInt32()
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = _4 != nil
if _c1 && _c2 && _c3 && _c4 {
return Api.MessageMedia.messageMediaGiveaway(channels: _1!, quantity: _2!, months: _3!, untilDate: _4!)
let _c5 = _5 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 {
return Api.MessageMedia.messageMediaGiveaway(flags: _1!, channels: _2!, quantity: _3!, months: _4!, untilDate: _5!)
}
else {
return nil

@ -322,19 +322,26 @@ public extension Api {
}
public extension Api {
enum MessageReplyHeader: TypeConstructorDescription {
case messageReplyHeader(flags: Int32, replyToMsgId: Int32, replyToPeerId: Api.Peer?, replyToTopId: Int32?)
case messageReplyHeader(flags: Int32, replyToMsgId: Int32?, replyToPeerId: Api.Peer?, replyHeader: Api.MessageFwdHeader?, replyToTopId: Int32?, quoteText: String?, quoteEntities: [Api.MessageEntity]?)
case messageReplyStoryHeader(userId: Int64, storyId: Int32)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .messageReplyHeader(let flags, let replyToMsgId, let replyToPeerId, let replyToTopId):
case .messageReplyHeader(let flags, let replyToMsgId, let replyToPeerId, let replyHeader, let replyToTopId, let quoteText, let quoteEntities):
if boxed {
buffer.appendInt32(-1495959709)
buffer.appendInt32(1029445267)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt32(replyToMsgId, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 4) != 0 {serializeInt32(replyToMsgId!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 0) != 0 {replyToPeerId!.serialize(buffer, true)}
if Int(flags) & Int(1 << 5) != 0 {replyHeader!.serialize(buffer, true)}
if Int(flags) & Int(1 << 1) != 0 {serializeInt32(replyToTopId!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 6) != 0 {serializeString(quoteText!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 7) != 0 {buffer.appendInt32(481674261)
buffer.appendInt32(Int32(quoteEntities!.count))
for item in quoteEntities! {
item.serialize(buffer, true)
}}
break
case .messageReplyStoryHeader(let userId, let storyId):
if boxed {
@ -348,8 +355,8 @@ public extension Api {
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .messageReplyHeader(let flags, let replyToMsgId, let replyToPeerId, let replyToTopId):
return ("messageReplyHeader", [("flags", flags as Any), ("replyToMsgId", replyToMsgId as Any), ("replyToPeerId", replyToPeerId as Any), ("replyToTopId", replyToTopId as Any)])
case .messageReplyHeader(let flags, let replyToMsgId, let replyToPeerId, let replyHeader, let replyToTopId, let quoteText, let quoteEntities):
return ("messageReplyHeader", [("flags", flags as Any), ("replyToMsgId", replyToMsgId as Any), ("replyToPeerId", replyToPeerId as Any), ("replyHeader", replyHeader as Any), ("replyToTopId", replyToTopId as Any), ("quoteText", quoteText as Any), ("quoteEntities", quoteEntities as Any)])
case .messageReplyStoryHeader(let userId, let storyId):
return ("messageReplyStoryHeader", [("userId", userId as Any), ("storyId", storyId as Any)])
}
@ -359,19 +366,32 @@ public extension Api {
var _1: Int32?
_1 = reader.readInt32()
var _2: Int32?
_2 = reader.readInt32()
if Int(_1!) & Int(1 << 4) != 0 {_2 = reader.readInt32() }
var _3: Api.Peer?
if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() {
_3 = Api.parse(reader, signature: signature) as? Api.Peer
} }
var _4: Int32?
if Int(_1!) & Int(1 << 1) != 0 {_4 = reader.readInt32() }
var _4: Api.MessageFwdHeader?
if Int(_1!) & Int(1 << 5) != 0 {if let signature = reader.readInt32() {
_4 = Api.parse(reader, signature: signature) as? Api.MessageFwdHeader
} }
var _5: Int32?
if Int(_1!) & Int(1 << 1) != 0 {_5 = reader.readInt32() }
var _6: String?
if Int(_1!) & Int(1 << 6) != 0 {_6 = parseString(reader) }
var _7: [Api.MessageEntity]?
if Int(_1!) & Int(1 << 7) != 0 {if let _ = reader.readInt32() {
_7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self)
} }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c2 = (Int(_1!) & Int(1 << 4) == 0) || _2 != nil
let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil
let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil
if _c1 && _c2 && _c3 && _c4 {
return Api.MessageReplyHeader.messageReplyHeader(flags: _1!, replyToMsgId: _2!, replyToPeerId: _3, replyToTopId: _4)
let _c4 = (Int(_1!) & Int(1 << 5) == 0) || _4 != nil
let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil
let _c6 = (Int(_1!) & Int(1 << 6) == 0) || _6 != nil
let _c7 = (Int(_1!) & Int(1 << 7) == 0) || _7 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 {
return Api.MessageReplyHeader.messageReplyHeader(flags: _1!, replyToMsgId: _2, replyToPeerId: _3, replyHeader: _4, replyToTopId: _5, quoteText: _6, quoteEntities: _7)
}
else {
return nil

@ -752,26 +752,29 @@ public extension Api {
}
public extension Api {
enum PremiumGiftCodeOption: TypeConstructorDescription {
case premiumGiftCodeOption(flags: Int32, users: Int32, months: Int32, storeProduct: String?)
case premiumGiftCodeOption(flags: Int32, users: Int32, months: Int32, storeProduct: String?, storeQuantity: Int32?, currency: String, amount: Int64)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .premiumGiftCodeOption(let flags, let users, let months, let storeProduct):
case .premiumGiftCodeOption(let flags, let users, let months, let storeProduct, let storeQuantity, let currency, let amount):
if boxed {
buffer.appendInt32(-713473172)
buffer.appendInt32(629052971)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt32(users, buffer: buffer, boxed: false)
serializeInt32(months, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {serializeString(storeProduct!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 1) != 0 {serializeInt32(storeQuantity!, buffer: buffer, boxed: false)}
serializeString(currency, buffer: buffer, boxed: false)
serializeInt64(amount, buffer: buffer, boxed: false)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .premiumGiftCodeOption(let flags, let users, let months, let storeProduct):
return ("premiumGiftCodeOption", [("flags", flags as Any), ("users", users as Any), ("months", months as Any), ("storeProduct", storeProduct as Any)])
case .premiumGiftCodeOption(let flags, let users, let months, let storeProduct, let storeQuantity, let currency, let amount):
return ("premiumGiftCodeOption", [("flags", flags as Any), ("users", users as Any), ("months", months as Any), ("storeProduct", storeProduct as Any), ("storeQuantity", storeQuantity as Any), ("currency", currency as Any), ("amount", amount as Any)])
}
}
@ -784,12 +787,21 @@ public extension Api {
_3 = reader.readInt32()
var _4: String?
if Int(_1!) & Int(1 << 0) != 0 {_4 = parseString(reader) }
var _5: Int32?
if Int(_1!) & Int(1 << 1) != 0 {_5 = reader.readInt32() }
var _6: String?
_6 = parseString(reader)
var _7: Int64?
_7 = reader.readInt64()
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil
if _c1 && _c2 && _c3 && _c4 {
return Api.PremiumGiftCodeOption.premiumGiftCodeOption(flags: _1!, users: _2!, months: _3!, storeProduct: _4)
let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil
let _c6 = _6 != nil
let _c7 = _7 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 {
return Api.PremiumGiftCodeOption.premiumGiftCodeOption(flags: _1!, users: _2!, months: _3!, storeProduct: _4, storeQuantity: _5, currency: _6!, amount: _7!)
}
else {
return nil

@ -538,16 +538,17 @@ public extension Api.payments {
}
public extension Api.payments {
enum CheckedGiftCode: TypeConstructorDescription {
case checkedGiftCode(flags: Int32, fromId: Api.Peer, toId: Int64?, date: Int32, months: Int32, usedDate: Int32?, chats: [Api.Chat], users: [Api.User])
case checkedGiftCode(flags: Int32, fromId: Api.Peer, giveawayMsgId: Int32?, toId: Int64?, date: Int32, months: Int32, usedDate: Int32?, chats: [Api.Chat], users: [Api.User])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .checkedGiftCode(let flags, let fromId, let toId, let date, let months, let usedDate, let chats, let users):
case .checkedGiftCode(let flags, let fromId, let giveawayMsgId, let toId, let date, let months, let usedDate, let chats, let users):
if boxed {
buffer.appendInt32(-9426548)
buffer.appendInt32(-1222446760)
}
serializeInt32(flags, buffer: buffer, boxed: false)
fromId.serialize(buffer, true)
if Int(flags) & Int(1 << 3) != 0 {serializeInt32(giveawayMsgId!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 0) != 0 {serializeInt64(toId!, buffer: buffer, boxed: false)}
serializeInt32(date, buffer: buffer, boxed: false)
serializeInt32(months, buffer: buffer, boxed: false)
@ -568,8 +569,8 @@ public extension Api.payments {
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .checkedGiftCode(let flags, let fromId, let toId, let date, let months, let usedDate, let chats, let users):
return ("checkedGiftCode", [("flags", flags as Any), ("fromId", fromId as Any), ("toId", toId as Any), ("date", date as Any), ("months", months as Any), ("usedDate", usedDate as Any), ("chats", chats as Any), ("users", users as Any)])
case .checkedGiftCode(let flags, let fromId, let giveawayMsgId, let toId, let date, let months, let usedDate, let chats, let users):
return ("checkedGiftCode", [("flags", flags as Any), ("fromId", fromId as Any), ("giveawayMsgId", giveawayMsgId as Any), ("toId", toId as Any), ("date", date as Any), ("months", months as Any), ("usedDate", usedDate as Any), ("chats", chats as Any), ("users", users as Any)])
}
}
@ -580,32 +581,35 @@ public extension Api.payments {
if let signature = reader.readInt32() {
_2 = Api.parse(reader, signature: signature) as? Api.Peer
}
var _3: Int64?
if Int(_1!) & Int(1 << 0) != 0 {_3 = reader.readInt64() }
var _4: Int32?
_4 = reader.readInt32()
var _3: Int32?
if Int(_1!) & Int(1 << 3) != 0 {_3 = reader.readInt32() }
var _4: Int64?
if Int(_1!) & Int(1 << 0) != 0 {_4 = reader.readInt64() }
var _5: Int32?
_5 = reader.readInt32()
var _6: Int32?
if Int(_1!) & Int(1 << 1) != 0 {_6 = reader.readInt32() }
var _7: [Api.Chat]?
_6 = reader.readInt32()
var _7: Int32?
if Int(_1!) & Int(1 << 1) != 0 {_7 = reader.readInt32() }
var _8: [Api.Chat]?
if let _ = reader.readInt32() {
_7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
_8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
}
var _8: [Api.User]?
var _9: [Api.User]?
if let _ = reader.readInt32() {
_8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
_9 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil
let _c4 = _4 != nil
let _c3 = (Int(_1!) & Int(1 << 3) == 0) || _3 != nil
let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil
let _c5 = _5 != nil
let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil
let _c7 = _7 != nil
let _c6 = _6 != nil
let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil
let _c8 = _8 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 {
return Api.payments.CheckedGiftCode.checkedGiftCode(flags: _1!, fromId: _2!, toId: _3, date: _4!, months: _5!, usedDate: _6, chats: _7!, users: _8!)
let _c9 = _9 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 {
return Api.payments.CheckedGiftCode.checkedGiftCode(flags: _1!, fromId: _2!, giveawayMsgId: _3, toId: _4, date: _5!, months: _6!, usedDate: _7, chats: _8!, users: _9!)
}
else {
return nil
@ -650,6 +654,86 @@ public extension Api.payments {
}
}
public extension Api.payments {
enum GiveawayInfo: TypeConstructorDescription {
case giveawayInfo(flags: Int32, joinedTooEarlyDate: Int32?, adminDisallowedChatId: Int64?)
case giveawayInfoResults(flags: Int32, giftCodeSlug: String?, finishDate: Int32, winnersCount: Int32, activatedCount: Int32)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .giveawayInfo(let flags, let joinedTooEarlyDate, let adminDisallowedChatId):
if boxed {
buffer.appendInt32(2054937690)
}
serializeInt32(flags, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 1) != 0 {serializeInt32(joinedTooEarlyDate!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 2) != 0 {serializeInt64(adminDisallowedChatId!, buffer: buffer, boxed: false)}
break
case .giveawayInfoResults(let flags, let giftCodeSlug, let finishDate, let winnersCount, let activatedCount):
if boxed {
buffer.appendInt32(952312868)
}
serializeInt32(flags, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {serializeString(giftCodeSlug!, buffer: buffer, boxed: false)}
serializeInt32(finishDate, buffer: buffer, boxed: false)
serializeInt32(winnersCount, buffer: buffer, boxed: false)
serializeInt32(activatedCount, buffer: buffer, boxed: false)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .giveawayInfo(let flags, let joinedTooEarlyDate, let adminDisallowedChatId):
return ("giveawayInfo", [("flags", flags as Any), ("joinedTooEarlyDate", joinedTooEarlyDate as Any), ("adminDisallowedChatId", adminDisallowedChatId as Any)])
case .giveawayInfoResults(let flags, let giftCodeSlug, let finishDate, let winnersCount, let activatedCount):
return ("giveawayInfoResults", [("flags", flags as Any), ("giftCodeSlug", giftCodeSlug as Any), ("finishDate", finishDate as Any), ("winnersCount", winnersCount as Any), ("activatedCount", activatedCount as Any)])
}
}
public static func parse_giveawayInfo(_ reader: BufferReader) -> GiveawayInfo? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Int32?
if Int(_1!) & Int(1 << 1) != 0 {_2 = reader.readInt32() }
var _3: Int64?
if Int(_1!) & Int(1 << 2) != 0 {_3 = reader.readInt64() }
let _c1 = _1 != nil
let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil
let _c3 = (Int(_1!) & Int(1 << 2) == 0) || _3 != nil
if _c1 && _c2 && _c3 {
return Api.payments.GiveawayInfo.giveawayInfo(flags: _1!, joinedTooEarlyDate: _2, adminDisallowedChatId: _3)
}
else {
return nil
}
}
public static func parse_giveawayInfoResults(_ reader: BufferReader) -> GiveawayInfo? {
var _1: Int32?
_1 = reader.readInt32()
var _2: String?
if Int(_1!) & Int(1 << 0) != 0 {_2 = parseString(reader) }
var _3: Int32?
_3 = reader.readInt32()
var _4: Int32?
_4 = reader.readInt32()
var _5: Int32?
_5 = reader.readInt32()
let _c1 = _1 != nil
let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil
let _c3 = _3 != nil
let _c4 = _4 != nil
let _c5 = _5 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 {
return Api.payments.GiveawayInfo.giveawayInfoResults(flags: _1!, giftCodeSlug: _2, finishDate: _3!, winnersCount: _4!, activatedCount: _5!)
}
else {
return nil
}
}
}
}
public extension Api.payments {
enum PaymentForm: TypeConstructorDescription {
case paymentForm(flags: Int32, formId: Int64, botId: Int64, title: String, description: String, photo: Api.WebDocument?, invoice: Api.Invoice, providerId: Int64, url: String, nativeProvider: String?, nativeParams: Api.DataJSON?, additionalMethods: [Api.PaymentFormMethod]?, savedInfo: Api.PaymentRequestedInfo?, savedCredentials: [Api.PaymentSavedCredentials]?, users: [Api.User])
@ -1538,129 +1622,3 @@ public extension Api.photos {
}
}
public extension Api.stats {
enum BroadcastStats: TypeConstructorDescription {
case broadcastStats(period: Api.StatsDateRangeDays, followers: Api.StatsAbsValueAndPrev, viewsPerPost: Api.StatsAbsValueAndPrev, sharesPerPost: Api.StatsAbsValueAndPrev, enabledNotifications: Api.StatsPercentValue, growthGraph: Api.StatsGraph, followersGraph: Api.StatsGraph, muteGraph: Api.StatsGraph, topHoursGraph: Api.StatsGraph, interactionsGraph: Api.StatsGraph, ivInteractionsGraph: Api.StatsGraph, viewsBySourceGraph: Api.StatsGraph, newFollowersBySourceGraph: Api.StatsGraph, languagesGraph: Api.StatsGraph, recentMessageInteractions: [Api.MessageInteractionCounters])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .broadcastStats(let period, let followers, let viewsPerPost, let sharesPerPost, let enabledNotifications, let growthGraph, let followersGraph, let muteGraph, let topHoursGraph, let interactionsGraph, let ivInteractionsGraph, let viewsBySourceGraph, let newFollowersBySourceGraph, let languagesGraph, let recentMessageInteractions):
if boxed {
buffer.appendInt32(-1107852396)
}
period.serialize(buffer, true)
followers.serialize(buffer, true)
viewsPerPost.serialize(buffer, true)
sharesPerPost.serialize(buffer, true)
enabledNotifications.serialize(buffer, true)
growthGraph.serialize(buffer, true)
followersGraph.serialize(buffer, true)
muteGraph.serialize(buffer, true)
topHoursGraph.serialize(buffer, true)
interactionsGraph.serialize(buffer, true)
ivInteractionsGraph.serialize(buffer, true)
viewsBySourceGraph.serialize(buffer, true)
newFollowersBySourceGraph.serialize(buffer, true)
languagesGraph.serialize(buffer, true)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(recentMessageInteractions.count))
for item in recentMessageInteractions {
item.serialize(buffer, true)
}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .broadcastStats(let period, let followers, let viewsPerPost, let sharesPerPost, let enabledNotifications, let growthGraph, let followersGraph, let muteGraph, let topHoursGraph, let interactionsGraph, let ivInteractionsGraph, let viewsBySourceGraph, let newFollowersBySourceGraph, let languagesGraph, let recentMessageInteractions):
return ("broadcastStats", [("period", period as Any), ("followers", followers as Any), ("viewsPerPost", viewsPerPost as Any), ("sharesPerPost", sharesPerPost as Any), ("enabledNotifications", enabledNotifications as Any), ("growthGraph", growthGraph as Any), ("followersGraph", followersGraph as Any), ("muteGraph", muteGraph as Any), ("topHoursGraph", topHoursGraph as Any), ("interactionsGraph", interactionsGraph as Any), ("ivInteractionsGraph", ivInteractionsGraph as Any), ("viewsBySourceGraph", viewsBySourceGraph as Any), ("newFollowersBySourceGraph", newFollowersBySourceGraph as Any), ("languagesGraph", languagesGraph as Any), ("recentMessageInteractions", recentMessageInteractions as Any)])
}
}
public static func parse_broadcastStats(_ reader: BufferReader) -> BroadcastStats? {
var _1: Api.StatsDateRangeDays?
if let signature = reader.readInt32() {
_1 = Api.parse(reader, signature: signature) as? Api.StatsDateRangeDays
}
var _2: Api.StatsAbsValueAndPrev?
if let signature = reader.readInt32() {
_2 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev
}
var _3: Api.StatsAbsValueAndPrev?
if let signature = reader.readInt32() {
_3 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev
}
var _4: Api.StatsAbsValueAndPrev?
if let signature = reader.readInt32() {
_4 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev
}
var _5: Api.StatsPercentValue?
if let signature = reader.readInt32() {
_5 = Api.parse(reader, signature: signature) as? Api.StatsPercentValue
}
var _6: Api.StatsGraph?
if let signature = reader.readInt32() {
_6 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _7: Api.StatsGraph?
if let signature = reader.readInt32() {
_7 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _8: Api.StatsGraph?
if let signature = reader.readInt32() {
_8 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _9: Api.StatsGraph?
if let signature = reader.readInt32() {
_9 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _10: Api.StatsGraph?
if let signature = reader.readInt32() {
_10 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _11: Api.StatsGraph?
if let signature = reader.readInt32() {
_11 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _12: Api.StatsGraph?
if let signature = reader.readInt32() {
_12 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _13: Api.StatsGraph?
if let signature = reader.readInt32() {
_13 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _14: Api.StatsGraph?
if let signature = reader.readInt32() {
_14 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _15: [Api.MessageInteractionCounters]?
if let _ = reader.readInt32() {
_15 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageInteractionCounters.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = _4 != nil
let _c5 = _5 != nil
let _c6 = _6 != nil
let _c7 = _7 != nil
let _c8 = _8 != nil
let _c9 = _9 != nil
let _c10 = _10 != nil
let _c11 = _11 != nil
let _c12 = _12 != nil
let _c13 = _13 != nil
let _c14 = _14 != nil
let _c15 = _15 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 {
return Api.stats.BroadcastStats.broadcastStats(period: _1!, followers: _2!, viewsPerPost: _3!, sharesPerPost: _4!, enabledNotifications: _5!, growthGraph: _6!, followersGraph: _7!, muteGraph: _8!, topHoursGraph: _9!, interactionsGraph: _10!, ivInteractionsGraph: _11!, viewsBySourceGraph: _12!, newFollowersBySourceGraph: _13!, languagesGraph: _14!, recentMessageInteractions: _15!)
}
else {
return nil
}
}
}
}

@ -1,3 +1,129 @@
public extension Api.stats {
enum BroadcastStats: TypeConstructorDescription {
case broadcastStats(period: Api.StatsDateRangeDays, followers: Api.StatsAbsValueAndPrev, viewsPerPost: Api.StatsAbsValueAndPrev, sharesPerPost: Api.StatsAbsValueAndPrev, enabledNotifications: Api.StatsPercentValue, growthGraph: Api.StatsGraph, followersGraph: Api.StatsGraph, muteGraph: Api.StatsGraph, topHoursGraph: Api.StatsGraph, interactionsGraph: Api.StatsGraph, ivInteractionsGraph: Api.StatsGraph, viewsBySourceGraph: Api.StatsGraph, newFollowersBySourceGraph: Api.StatsGraph, languagesGraph: Api.StatsGraph, recentMessageInteractions: [Api.MessageInteractionCounters])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .broadcastStats(let period, let followers, let viewsPerPost, let sharesPerPost, let enabledNotifications, let growthGraph, let followersGraph, let muteGraph, let topHoursGraph, let interactionsGraph, let ivInteractionsGraph, let viewsBySourceGraph, let newFollowersBySourceGraph, let languagesGraph, let recentMessageInteractions):
if boxed {
buffer.appendInt32(-1107852396)
}
period.serialize(buffer, true)
followers.serialize(buffer, true)
viewsPerPost.serialize(buffer, true)
sharesPerPost.serialize(buffer, true)
enabledNotifications.serialize(buffer, true)
growthGraph.serialize(buffer, true)
followersGraph.serialize(buffer, true)
muteGraph.serialize(buffer, true)
topHoursGraph.serialize(buffer, true)
interactionsGraph.serialize(buffer, true)
ivInteractionsGraph.serialize(buffer, true)
viewsBySourceGraph.serialize(buffer, true)
newFollowersBySourceGraph.serialize(buffer, true)
languagesGraph.serialize(buffer, true)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(recentMessageInteractions.count))
for item in recentMessageInteractions {
item.serialize(buffer, true)
}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .broadcastStats(let period, let followers, let viewsPerPost, let sharesPerPost, let enabledNotifications, let growthGraph, let followersGraph, let muteGraph, let topHoursGraph, let interactionsGraph, let ivInteractionsGraph, let viewsBySourceGraph, let newFollowersBySourceGraph, let languagesGraph, let recentMessageInteractions):
return ("broadcastStats", [("period", period as Any), ("followers", followers as Any), ("viewsPerPost", viewsPerPost as Any), ("sharesPerPost", sharesPerPost as Any), ("enabledNotifications", enabledNotifications as Any), ("growthGraph", growthGraph as Any), ("followersGraph", followersGraph as Any), ("muteGraph", muteGraph as Any), ("topHoursGraph", topHoursGraph as Any), ("interactionsGraph", interactionsGraph as Any), ("ivInteractionsGraph", ivInteractionsGraph as Any), ("viewsBySourceGraph", viewsBySourceGraph as Any), ("newFollowersBySourceGraph", newFollowersBySourceGraph as Any), ("languagesGraph", languagesGraph as Any), ("recentMessageInteractions", recentMessageInteractions as Any)])
}
}
public static func parse_broadcastStats(_ reader: BufferReader) -> BroadcastStats? {
var _1: Api.StatsDateRangeDays?
if let signature = reader.readInt32() {
_1 = Api.parse(reader, signature: signature) as? Api.StatsDateRangeDays
}
var _2: Api.StatsAbsValueAndPrev?
if let signature = reader.readInt32() {
_2 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev
}
var _3: Api.StatsAbsValueAndPrev?
if let signature = reader.readInt32() {
_3 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev
}
var _4: Api.StatsAbsValueAndPrev?
if let signature = reader.readInt32() {
_4 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev
}
var _5: Api.StatsPercentValue?
if let signature = reader.readInt32() {
_5 = Api.parse(reader, signature: signature) as? Api.StatsPercentValue
}
var _6: Api.StatsGraph?
if let signature = reader.readInt32() {
_6 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _7: Api.StatsGraph?
if let signature = reader.readInt32() {
_7 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _8: Api.StatsGraph?
if let signature = reader.readInt32() {
_8 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _9: Api.StatsGraph?
if let signature = reader.readInt32() {
_9 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _10: Api.StatsGraph?
if let signature = reader.readInt32() {
_10 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _11: Api.StatsGraph?
if let signature = reader.readInt32() {
_11 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _12: Api.StatsGraph?
if let signature = reader.readInt32() {
_12 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _13: Api.StatsGraph?
if let signature = reader.readInt32() {
_13 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _14: Api.StatsGraph?
if let signature = reader.readInt32() {
_14 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _15: [Api.MessageInteractionCounters]?
if let _ = reader.readInt32() {
_15 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageInteractionCounters.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = _4 != nil
let _c5 = _5 != nil
let _c6 = _6 != nil
let _c7 = _7 != nil
let _c8 = _8 != nil
let _c9 = _9 != nil
let _c10 = _10 != nil
let _c11 = _11 != nil
let _c12 = _12 != nil
let _c13 = _13 != nil
let _c14 = _14 != nil
let _c15 = _15 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 {
return Api.stats.BroadcastStats.broadcastStats(period: _1!, followers: _2!, viewsPerPost: _3!, sharesPerPost: _4!, enabledNotifications: _5!, growthGraph: _6!, followersGraph: _7!, muteGraph: _8!, topHoursGraph: _9!, interactionsGraph: _10!, ivInteractionsGraph: _11!, viewsBySourceGraph: _12!, newFollowersBySourceGraph: _13!, languagesGraph: _14!, recentMessageInteractions: _15!)
}
else {
return nil
}
}
}
}
public extension Api.stats {
enum MegagroupStats: TypeConstructorDescription {
case megagroupStats(period: Api.StatsDateRangeDays, members: Api.StatsAbsValueAndPrev, messages: Api.StatsAbsValueAndPrev, viewers: Api.StatsAbsValueAndPrev, posters: Api.StatsAbsValueAndPrev, growthGraph: Api.StatsGraph, membersGraph: Api.StatsGraph, newMembersBySourceGraph: Api.StatsGraph, languagesGraph: Api.StatsGraph, messagesGraph: Api.StatsGraph, actionsGraph: Api.StatsGraph, topHoursGraph: Api.StatsGraph, weekdaysGraph: Api.StatsGraph, topPosters: [Api.StatsGroupTopPoster], topAdmins: [Api.StatsGroupTopAdmin], topInviters: [Api.StatsGroupTopInviter], users: [Api.User])
@ -1326,59 +1452,3 @@ public extension Api.updates {
}
}
public extension Api.upload {
enum CdnFile: TypeConstructorDescription {
case cdnFile(bytes: Buffer)
case cdnFileReuploadNeeded(requestToken: Buffer)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .cdnFile(let bytes):
if boxed {
buffer.appendInt32(-1449145777)
}
serializeBytes(bytes, buffer: buffer, boxed: false)
break
case .cdnFileReuploadNeeded(let requestToken):
if boxed {
buffer.appendInt32(-290921362)
}
serializeBytes(requestToken, buffer: buffer, boxed: false)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .cdnFile(let bytes):
return ("cdnFile", [("bytes", bytes as Any)])
case .cdnFileReuploadNeeded(let requestToken):
return ("cdnFileReuploadNeeded", [("requestToken", requestToken as Any)])
}
}
public static func parse_cdnFile(_ reader: BufferReader) -> CdnFile? {
var _1: Buffer?
_1 = parseBytes(reader)
let _c1 = _1 != nil
if _c1 {
return Api.upload.CdnFile.cdnFile(bytes: _1!)
}
else {
return nil
}
}
public static func parse_cdnFileReuploadNeeded(_ reader: BufferReader) -> CdnFile? {
var _1: Buffer?
_1 = parseBytes(reader)
let _c1 = _1 != nil
if _c1 {
return Api.upload.CdnFile.cdnFileReuploadNeeded(requestToken: _1!)
}
else {
return nil
}
}
}
}

@ -1,3 +1,59 @@
public extension Api.upload {
enum CdnFile: TypeConstructorDescription {
case cdnFile(bytes: Buffer)
case cdnFileReuploadNeeded(requestToken: Buffer)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .cdnFile(let bytes):
if boxed {
buffer.appendInt32(-1449145777)
}
serializeBytes(bytes, buffer: buffer, boxed: false)
break
case .cdnFileReuploadNeeded(let requestToken):
if boxed {
buffer.appendInt32(-290921362)
}
serializeBytes(requestToken, buffer: buffer, boxed: false)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .cdnFile(let bytes):
return ("cdnFile", [("bytes", bytes as Any)])
case .cdnFileReuploadNeeded(let requestToken):
return ("cdnFileReuploadNeeded", [("requestToken", requestToken as Any)])
}
}
public static func parse_cdnFile(_ reader: BufferReader) -> CdnFile? {
var _1: Buffer?
_1 = parseBytes(reader)
let _c1 = _1 != nil
if _c1 {
return Api.upload.CdnFile.cdnFile(bytes: _1!)
}
else {
return nil
}
}
public static func parse_cdnFileReuploadNeeded(_ reader: BufferReader) -> CdnFile? {
var _1: Buffer?
_1 = parseBytes(reader)
let _c1 = _1 != nil
if _c1 {
return Api.upload.CdnFile.cdnFileReuploadNeeded(requestToken: _1!)
}
else {
return nil
}
}
}
}
public extension Api.upload {
enum File: TypeConstructorDescription {
case file(type: Api.storage.FileType, mtime: Int32, bytes: Buffer)

@ -6447,12 +6447,11 @@ public extension Api.functions.messages {
}
}
public extension Api.functions.messages {
static func saveDraft(flags: Int32, replyToMsgId: Int32?, topMsgId: Int32?, peer: Api.InputPeer, message: String, entities: [Api.MessageEntity]?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
static func saveDraft(flags: Int32, replyTo: Api.InputReplyTo?, peer: Api.InputPeer, message: String, entities: [Api.MessageEntity]?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer()
buffer.appendInt32(-1271718337)
buffer.appendInt32(1688404588)
serializeInt32(flags, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {serializeInt32(replyToMsgId!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 2) != 0 {serializeInt32(topMsgId!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 0) != 0 {replyTo!.serialize(buffer, true)}
peer.serialize(buffer, true)
serializeString(message, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261)
@ -6460,7 +6459,7 @@ public extension Api.functions.messages {
for item in entities! {
item.serialize(buffer, true)
}}
return (FunctionDescription(name: "messages.saveDraft", parameters: [("flags", String(describing: flags)), ("replyToMsgId", String(describing: replyToMsgId)), ("topMsgId", String(describing: topMsgId)), ("peer", String(describing: peer)), ("message", String(describing: message)), ("entities", String(describing: entities))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
return (FunctionDescription(name: "messages.saveDraft", parameters: [("flags", String(describing: flags)), ("replyTo", String(describing: replyTo)), ("peer", String(describing: peer)), ("message", String(describing: message)), ("entities", String(describing: entities))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
let reader = BufferReader(buffer)
var result: Api.Bool?
if let signature = reader.readInt32() {
@ -7562,6 +7561,22 @@ public extension Api.functions.payments {
})
}
}
public extension Api.functions.payments {
static func getGiveawayInfo(peer: Api.InputPeer, msgId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.payments.GiveawayInfo>) {
let buffer = Buffer()
buffer.appendInt32(-198994907)
peer.serialize(buffer, true)
serializeInt32(msgId, buffer: buffer, boxed: false)
return (FunctionDescription(name: "payments.getGiveawayInfo", parameters: [("peer", String(describing: peer)), ("msgId", String(describing: msgId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.GiveawayInfo? in
let reader = BufferReader(buffer)
var result: Api.payments.GiveawayInfo?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.payments.GiveawayInfo
}
return result
})
}
}
public extension Api.functions.payments {
static func getPaymentForm(flags: Int32, invoice: Api.InputInvoice, themeParams: Api.DataJSON?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.payments.PaymentForm>) {
let buffer = Buffer()

@ -1,16 +1,16 @@
public extension Api {
enum DraftMessage: TypeConstructorDescription {
case draftMessage(flags: Int32, replyToMsgId: Int32?, message: String, entities: [Api.MessageEntity]?, date: Int32)
case draftMessage(flags: Int32, replyTo: Api.MessageReplyHeader?, message: String, entities: [Api.MessageEntity]?, date: Int32)
case draftMessageEmpty(flags: Int32, date: Int32?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .draftMessage(let flags, let replyToMsgId, let message, let entities, let date):
case .draftMessage(let flags, let replyTo, let message, let entities, let date):
if boxed {
buffer.appendInt32(-40996577)
buffer.appendInt32(-1783606645)
}
serializeInt32(flags, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {serializeInt32(replyToMsgId!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 4) != 0 {replyTo!.serialize(buffer, true)}
serializeString(message, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261)
buffer.appendInt32(Int32(entities!.count))
@ -31,8 +31,8 @@ public extension Api {
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .draftMessage(let flags, let replyToMsgId, let message, let entities, let date):
return ("draftMessage", [("flags", flags as Any), ("replyToMsgId", replyToMsgId as Any), ("message", message as Any), ("entities", entities as Any), ("date", date as Any)])
case .draftMessage(let flags, let replyTo, let message, let entities, let date):
return ("draftMessage", [("flags", flags as Any), ("replyTo", replyTo as Any), ("message", message as Any), ("entities", entities as Any), ("date", date as Any)])
case .draftMessageEmpty(let flags, let date):
return ("draftMessageEmpty", [("flags", flags as Any), ("date", date as Any)])
}
@ -41,8 +41,10 @@ public extension Api {
public static func parse_draftMessage(_ reader: BufferReader) -> DraftMessage? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Int32?
if Int(_1!) & Int(1 << 0) != 0 {_2 = reader.readInt32() }
var _2: Api.MessageReplyHeader?
if Int(_1!) & Int(1 << 4) != 0 {if let signature = reader.readInt32() {
_2 = Api.parse(reader, signature: signature) as? Api.MessageReplyHeader
} }
var _3: String?
_3 = parseString(reader)
var _4: [Api.MessageEntity]?
@ -52,12 +54,12 @@ public extension Api {
var _5: Int32?
_5 = reader.readInt32()
let _c1 = _1 != nil
let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil
let _c2 = (Int(_1!) & Int(1 << 4) == 0) || _2 != nil
let _c3 = _3 != nil
let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil
let _c5 = _5 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 {
return Api.DraftMessage.draftMessage(flags: _1!, replyToMsgId: _2, message: _3!, entities: _4, date: _5!)
return Api.DraftMessage.draftMessage(flags: _1!, replyTo: _2, message: _3!, entities: _4, date: _5!)
}
else {
return nil

@ -209,6 +209,7 @@ public extension Api {
public extension Api {
indirect enum InputInvoice: TypeConstructorDescription {
case inputInvoiceMessage(peer: Api.InputPeer, msgId: Int32)
case inputInvoicePremiumGiftCode(purpose: Api.InputStorePaymentPurpose, option: Api.PremiumGiftCodeOption)
case inputInvoiceSlug(slug: String)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
@ -220,6 +221,13 @@ public extension Api {
peer.serialize(buffer, true)
serializeInt32(msgId, buffer: buffer, boxed: false)
break
case .inputInvoicePremiumGiftCode(let purpose, let option):
if boxed {
buffer.appendInt32(-1734841331)
}
purpose.serialize(buffer, true)
option.serialize(buffer, true)
break
case .inputInvoiceSlug(let slug):
if boxed {
buffer.appendInt32(-1020867857)
@ -233,6 +241,8 @@ public extension Api {
switch self {
case .inputInvoiceMessage(let peer, let msgId):
return ("inputInvoiceMessage", [("peer", peer as Any), ("msgId", msgId as Any)])
case .inputInvoicePremiumGiftCode(let purpose, let option):
return ("inputInvoicePremiumGiftCode", [("purpose", purpose as Any), ("option", option as Any)])
case .inputInvoiceSlug(let slug):
return ("inputInvoiceSlug", [("slug", slug as Any)])
}
@ -254,6 +264,24 @@ public extension Api {
return nil
}
}
public static func parse_inputInvoicePremiumGiftCode(_ reader: BufferReader) -> InputInvoice? {
var _1: Api.InputStorePaymentPurpose?
if let signature = reader.readInt32() {
_1 = Api.parse(reader, signature: signature) as? Api.InputStorePaymentPurpose
}
var _2: Api.PremiumGiftCodeOption?
if let signature = reader.readInt32() {
_2 = Api.parse(reader, signature: signature) as? Api.PremiumGiftCodeOption
}
let _c1 = _1 != nil
let _c2 = _2 != nil
if _c1 && _c2 {
return Api.InputInvoice.inputInvoicePremiumGiftCode(purpose: _1!, option: _2!)
}
else {
return nil
}
}
public static func parse_inputInvoiceSlug(_ reader: BufferReader) -> InputInvoice? {
var _1: String?
_1 = parseString(reader)

@ -387,8 +387,12 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI
case let .messageMediaStory(flags, peerId, id, _):
let isMention = (flags & (1 << 1)) != 0
return (TelegramMediaStory(storyId: StoryId(peerId: peerId.peerId, id: id), isMention: isMention), nil, nil, nil)
case let .messageMediaGiveaway(channels, quantity, months, untilDate):
return (TelegramMediaGiveaway(channelPeerIds: channels.map { PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value($0)) }, quantity: quantity, months: months, untilDate: untilDate), nil, nil, nil)
case let .messageMediaGiveaway(apiFlags, channels, quantity, months, untilDate):
var flags: TelegramMediaGiveaway.Flags = []
if (apiFlags & (1 << 0)) != 0 {
flags.insert(.onlyNewSubscribers)
}
return (TelegramMediaGiveaway(flags: flags, channelPeerIds: channels.map { PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value($0)) }, quantity: quantity, months: months, untilDate: untilDate), nil, nil, nil)
}
}
@ -582,7 +586,7 @@ extension StoreMessage {
threadId = makeMessageThreadId(threadIdValue)
}
}
attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId))
attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: 0), threadMessageId: threadMessageId))
case let .messageReplyStoryHeader(userId, storyId):
attributes.append(ReplyStoryAttribute(storyId: StoryId(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), id: storyId)))
}

@ -1,6 +1,16 @@
import Postbox
public final class TelegramMediaGiveaway: Media, Equatable {
public struct Flags: OptionSet {
public var rawValue: Int32
public init(rawValue: Int32) {
self.rawValue = rawValue
}
public static let onlyNewSubscribers = Flags(rawValue: 1 << 0)
}
public var id: MediaId? {
return nil
}
@ -8,12 +18,14 @@ public final class TelegramMediaGiveaway: Media, Equatable {
return self.channelPeerIds
}
public let flags: Flags
public let channelPeerIds: [PeerId]
public let quantity: Int32
public let months: Int32
public let untilDate: Int32
public init(channelPeerIds: [PeerId], quantity: Int32, months: Int32, untilDate: Int32) {
public init(flags: Flags, channelPeerIds: [PeerId], quantity: Int32, months: Int32, untilDate: Int32) {
self.flags = flags
self.channelPeerIds = channelPeerIds
self.quantity = quantity
self.months = months
@ -21,6 +33,7 @@ public final class TelegramMediaGiveaway: Media, Equatable {
}
public init(decoder: PostboxDecoder) {
self.flags = Flags(rawValue: decoder.decodeInt32ForKey("flg", orElse: 0))
self.channelPeerIds = decoder.decodeInt64ArrayForKey("cns").map { PeerId($0) }
self.quantity = decoder.decodeInt32ForKey("qty", orElse: 0)
self.months = decoder.decodeInt32ForKey("mts", orElse: 0)
@ -28,6 +41,7 @@ public final class TelegramMediaGiveaway: Media, Equatable {
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt32(self.flags.rawValue, forKey: "flg")
encoder.encodeInt64Array(self.channelPeerIds.map { $0.toInt64() }, forKey: "cns")
encoder.encodeInt32(self.quantity, forKey: "qty")
encoder.encodeInt32(self.months, forKey: "mts")
@ -42,6 +56,9 @@ public final class TelegramMediaGiveaway: Media, Equatable {
guard let other = other as? TelegramMediaGiveaway else {
return false
}
if self.flags != other.flags {
return false
}
if self.channelPeerIds != other.channelPeerIds {
return false
}

@ -17,6 +17,7 @@ public enum EngineMedia: Equatable {
case webFile(TelegramMediaWebFile)
case webpage(TelegramMediaWebpage)
case story(TelegramMediaStory)
case giveaway(TelegramMediaGiveaway)
}
public extension EngineMedia {
@ -50,6 +51,8 @@ public extension EngineMedia {
return webpage.id
case let .story(story):
return story.id
case let .giveaway(giveaway):
return giveaway.id
}
}
}
@ -85,6 +88,8 @@ public extension EngineMedia {
self = .webpage(webpage)
case let story as TelegramMediaStory:
self = .story(story)
case let giveaway as TelegramMediaGiveaway:
self = .giveaway(giveaway)
default:
preconditionFailure()
}
@ -120,6 +125,8 @@ public extension EngineMedia {
return webpage
case let .story(story):
return story
case let .giveaway(giveaway):
return giveaway
}
}
}

@ -16,7 +16,7 @@ public enum AppStoreTransactionPurpose {
case restore
case gift(peerId: EnginePeer.Id, currency: String, amount: Int64)
case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?, currency: String, amount: Int64)
case giveaway(boostPeer: EnginePeer.Id, randomId: Int64, untilDate: Int32, currency: String, amount: Int64)
case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], onlyNewSubscribers: Bool, randomId: Int64, untilDate: Int32, currency: String, amount: Int64)
}
private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTransactionPurpose) -> Signal<Api.InputStorePaymentPurpose, NoError> {
@ -59,14 +59,27 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran
return .inputStorePaymentPremiumGiftCode(flags: flags, users: apiInputUsers, boostPeer: apiBoostPeer, currency: currency, amount: amount)
}
case let .giveaway(boostPeerId, randomId, untilDate, currency, amount):
return account.postbox.loadedPeerWithId(boostPeerId)
|> mapToSignal { peer in
guard let apiBoostPeer = apiInputPeer(peer) else {
case let .giveaway(boostPeerId, additionalPeerIds, onlyNewSubscribers, randomId, untilDate, currency, amount):
return account.postbox.transaction { transaction -> Signal<Api.InputStorePaymentPurpose, NoError> in
guard let peer = transaction.getPeer(boostPeerId), let apiBoostPeer = apiInputPeer(peer) else {
return .complete()
}
return .single(.inputStorePaymentPremiumGiveaway(flags: 0, boostPeer: apiBoostPeer, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount))
var flags: Int32 = 0
if onlyNewSubscribers {
flags |= (1 << 0)
}
var additionalPeers: [Api.InputPeer] = []
if !additionalPeerIds.isEmpty {
flags |= (1 << 1)
for peerId in additionalPeerIds {
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
additionalPeers.append(inputPeer)
}
}
}
return .single(.inputStorePaymentPremiumGiveaway(flags: flags, boostPeer: apiBoostPeer, additionalPeers: additionalPeers, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount))
}
|> switchToLatest
}
}

@ -6,6 +6,7 @@ import TelegramApi
public struct PremiumGiftCodeInfo: Equatable {
public let slug: String
public let fromPeerId: EnginePeer.Id
public let messageId: EngineMessage.Id?
public let toPeerId: EnginePeer.Id?
public let date: Int32
public let months: Int32
@ -18,16 +19,19 @@ public struct PremiumGiftCodeOption: Codable, Equatable {
case users
case months
case storeProductId
case storeQuantity
}
public let users: Int32
public let months: Int32
public let storeProductId: String?
public let storeQuantity: Int32
public init(users: Int32, months: Int32, storeProductId: String?) {
public init(users: Int32, months: Int32, storeProductId: String?, storeQuantity: Int32) {
self.users = users
self.months = months
self.storeProductId = storeProductId
self.storeQuantity = storeQuantity
}
public init(from decoder: Decoder) throws {
@ -35,6 +39,7 @@ public struct PremiumGiftCodeOption: Codable, Equatable {
self.users = try container.decode(Int32.self, forKey: .users)
self.months = try container.decode(Int32.self, forKey: .months)
self.storeProductId = try container.decodeIfPresent(String.self, forKey: .storeProductId)
self.storeQuantity = try container.decodeIfPresent(Int32.self, forKey: .storeQuantity) ?? 1
}
public func encode(to encoder: Encoder) throws {
@ -42,6 +47,74 @@ public struct PremiumGiftCodeOption: Codable, Equatable {
try container.encode(self.users, forKey: .users)
try container.encode(self.months, forKey: .months)
try container.encodeIfPresent(self.storeProductId, forKey: .storeProductId)
try container.encode(self.storeQuantity, forKey: .storeQuantity)
}
}
public enum PremiumGiveawayInfo: Equatable {
public enum OngoingStatus: Equatable {
public enum DisallowReason: Equatable {
case joinedTooEarly(Int32)
case channelAdmin(EnginePeer.Id)
}
case notQualified
case notAllowed(DisallowReason)
case participating
case almostOver
}
public enum ResultStatus: Equatable {
case notWon
case won(slug: String)
case refunded
}
case ongoing(status: OngoingStatus)
case finished(status: ResultStatus, finishDate: Int32, winnersCount: Int32, activatedCount: Int32)
}
func _internal_getPremiumGiveawayInfo(account: Account, peerId: EnginePeer.Id, messageId: EngineMessage.Id) -> Signal<PremiumGiveawayInfo?, NoError> {
return account.postbox.loadedPeerWithId(peerId)
|> mapToSignal { peer in
guard let inputPeer = apiInputPeer(peer) else {
return .complete()
}
return account.network.request(Api.functions.payments.getGiveawayInfo(peer: inputPeer, msgId: messageId.id))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.payments.GiveawayInfo?, NoError> in
return .single(nil)
}
|> map { result -> PremiumGiveawayInfo? in
if let result {
switch result {
case let .giveawayInfo(flags, joinedTooEarlyDate, adminDisallowedChatId):
if (flags & (1 << 3)) != 0 {
return .ongoing(status: .almostOver)
} else if (flags & (1 << 0)) != 0 {
return .ongoing(status: .participating)
} else if let joinedTooEarlyDate = joinedTooEarlyDate {
return .ongoing(status: .notAllowed(.joinedTooEarly(joinedTooEarlyDate)))
} else if let adminDisallowedChatId = adminDisallowedChatId {
return .ongoing(status: .notAllowed(.channelAdmin(EnginePeer.Id(namespace: Namespaces.Peer.CloudChannel, id: EnginePeer.Id.Id._internalFromInt64Value(adminDisallowedChatId)))))
} else {
return .ongoing(status: .notQualified)
}
case let .giveawayInfoResults(flags, giftCodeSlug, finishDate, winnersCount, activatedCount):
let status: PremiumGiveawayInfo.ResultStatus
if let giftCodeSlug = giftCodeSlug {
status = .won(slug: giftCodeSlug)
} else if (flags & (1 << 1)) != 0 {
status = .refunded
} else {
status = .notWon
}
return .finished(status: status, finishDate: finishDate, winnersCount: winnersCount, activatedCount: activatedCount)
}
} else {
return nil
}
}
}
}
@ -76,7 +149,7 @@ func _internal_checkPremiumGiftCode(account: Account, slug: String) -> Signal<Pr
|> mapToSignal { result -> Signal<PremiumGiftCodeInfo?, NoError> in
if let result = result {
switch result {
case let .checkedGiftCode(_, _, _, _, _, _, chats, users):
case let .checkedGiftCode(_, _, _, _, _, _, _, chats, users):
return account.postbox.transaction { transaction in
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: parsedPeers)
@ -107,8 +180,8 @@ func _internal_applyPremiumGiftCode(account: Account, slug: String) -> Signal<Ne
extension PremiumGiftCodeOption {
init(apiGiftCodeOption: Api.PremiumGiftCodeOption) {
switch apiGiftCodeOption {
case let .premiumGiftCodeOption(_, users, months, storeProduct):
self.init(users: users, months: months, storeProductId: storeProduct)
case let .premiumGiftCodeOption(_, users, months, storeProduct, storeQuantity, _, _):
self.init(users: users, months: months, storeProductId: storeProduct, storeQuantity: storeQuantity ?? 1)
}
}
}
@ -116,9 +189,10 @@ extension PremiumGiftCodeOption {
extension PremiumGiftCodeInfo {
init(apiCheckedGiftCode: Api.payments.CheckedGiftCode, slug: String) {
switch apiCheckedGiftCode {
case let .checkedGiftCode(flags, fromId, toId, date, months, usedDate, _, _):
case let .checkedGiftCode(flags, fromId, giveawayMsgId, toId, date, months, usedDate, _, _):
self.slug = slug
self.fromPeerId = fromId.peerId
self.messageId = giveawayMsgId.flatMap { EngineMessage.Id(peerId: fromId.peerId, namespace: Namespaces.Message.Cloud, id: $0) }
self.toPeerId = toId.flatMap { EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value($0)) }
self.date = date
self.months = months

@ -57,5 +57,9 @@ public extension TelegramEngine {
public func premiumGiftCodeOptions(peerId: EnginePeer.Id) -> Signal<[PremiumGiftCodeOption], NoError> {
return _internal_premiumGiftCodeOptions(account: self.account, peerId: peerId)
}
public func premiumGiveawayInfo(peerId: EnginePeer.Id, messageId: EngineMessage.Id) -> Signal<PremiumGiveawayInfo?, NoError> {
return _internal_getPremiumGiveawayInfo(account: self.account, peerId: peerId, messageId: messageId)
}
}
}

@ -51,7 +51,7 @@ public func _internal_recentlySearchedPeers(postbox: Postbox) -> Signal<[Recentl
keys.append(contentsOf: peerIds.map({ .peer(peerId: $0, components: .all) }))
return postbox.combinedView(keys: keys)
|> map { view -> [RecentlySearchedPeer] in
|> mapToSignal { view -> Signal<[RecentlySearchedPeer], NoError> in
var result: [RecentlySearchedPeer] = []
var unreadCounts: [PeerId: Int32] = [:]
if let unreadCountsView = view.views[unreadCountsKey] as? UnreadMessageCountsView {
@ -62,6 +62,7 @@ public func _internal_recentlySearchedPeers(postbox: Postbox) -> Signal<[Recentl
}
}
var migratedPeerIds: [EnginePeer.Id: EnginePeer.Id] = [:]
for peerId in peerIds {
if let peerView = view.views[.peer(peerId: peerId, components: .all)] as? PeerView {
var presence: TelegramUserPresence?
@ -79,6 +80,10 @@ public func _internal_recentlySearchedPeers(postbox: Postbox) -> Signal<[Recentl
unreadCount = 0
}
}
if let group = peer as? TelegramGroup, let migrationReference = group.migrationReference {
migratedPeerIds = [group.id: migrationReference.peerId]
}
}
var subpeerSummary: RecentlySearchedPeerSubpeerSummary?
@ -91,7 +96,20 @@ public func _internal_recentlySearchedPeers(postbox: Postbox) -> Signal<[Recentl
}
}
return result
if !migratedPeerIds.isEmpty {
return postbox.transaction { transaction -> Signal<[RecentlySearchedPeer], NoError> in
for (previousPeerId, updatedPeerId) in migratedPeerIds {
transaction.removeOrderedItemListItem(collectionId: Namespaces.OrderedItemList.RecentlySearchedPeerIds, itemId: RecentPeerItemId(previousPeerId).rawValue)
if let entry = CodableEntry(RecentPeerItem(rating: 0.0)) {
transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.RecentlySearchedPeerIds, item: OrderedItemListEntry(id: RecentPeerItemId(updatedPeerId).rawValue, contents: entry), removeTailIfCountExceeds: 20)
}
}
return .complete()
}
|> switchToLatest
} else {
return .single(result)
}
}
}
}

@ -70,6 +70,7 @@ public enum PresentationResourceKey: Int32 {
case itemListImageIcon
case itemListCloudIcon
case itemListTopicArrowIcon
case itemListAddBoostsIcon
case itemListVoiceCallIcon
case itemListVideoCallIcon

@ -276,6 +276,12 @@ public struct PresentationResourcesItemList {
})
}
public static func addBoostsIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.itemListAddBoostsIcon.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Premium/AddBoosts"), color: theme.list.itemAccentColor)
})
}
public static func cornersImage(_ theme: PresentationTheme, top: Bool, bottom: Bool) -> UIImage? {
if !top && !bottom {
return nil

@ -899,7 +899,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
}
case .giftCode:
attributedString = NSAttributedString(string: "Gift code", font: titleFont, textColor: primaryTextColor)
attributedString = NSAttributedString(string: "Gift Link", font: titleFont, textColor: primaryTextColor)
case .unknown:
attributedString = nil
}

@ -340,6 +340,8 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/AccessoryPanelNode",
"//submodules/TelegramUI/Components/Chat/ForwardAccessoryPanelNode",
"//submodules/TelegramUI/Components/LegacyMessageInputPanel",
"//submodules/StatisticsUI",
"//submodules/TelegramUI/Components/PremiumGiftAttachmentScreen",
] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
"//build-system:ios_sim_arm64": [],

@ -14,6 +14,7 @@ swift_library(
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/ActivityIndicator",
"//submodules/Components/BundleIconComponent",
],
visibility = [
"//visibility:public",

@ -4,16 +4,20 @@ import Display
import ComponentFlow
import AnimatedTextComponent
import ActivityIndicator
import BundleIconComponent
public final class ButtonBadgeComponent: Component {
let fillColor: UIColor
let style: ButtonTextContentComponent.BadgeStyle
let content: AnyComponent<Empty>
public init(
fillColor: UIColor,
style: ButtonTextContentComponent.BadgeStyle,
content: AnyComponent<Empty>
) {
self.fillColor = fillColor
self.style = style
self.content = content
}
@ -21,6 +25,9 @@ public final class ButtonBadgeComponent: Component {
if lhs.fillColor != rhs.fillColor {
return false
}
if lhs.style != rhs.style {
return false
}
if lhs.content != rhs.content {
return false
}
@ -46,7 +53,13 @@ public final class ButtonBadgeComponent: Component {
}
public func update(component: ButtonBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let height: CGFloat = 20.0
let height: CGFloat
switch component.style {
case .round:
height = 20.0
case .roundedRectangle:
height = 18.0
}
let contentInset: CGFloat = 10.0
let themeUpdated = self.component?.fillColor != component.fillColor
@ -71,7 +84,12 @@ public final class ButtonBadgeComponent: Component {
}
if themeUpdated || backgroundFrame.height != self.backgroundView.image?.size.height {
self.backgroundView.image = generateStretchableFilledCircleImage(diameter: backgroundFrame.height, color: component.fillColor)
switch component.style {
case .round:
self.backgroundView.image = generateStretchableFilledCircleImage(diameter: backgroundFrame.height, color: component.fillColor)
case .roundedRectangle:
self.backgroundView.image = generateFilledRoundedRectImage(size: CGSize(width: height, height: height), cornerRadius: 4.0, color: component.fillColor)?.stretchableImage(withLeftCapWidth: Int(height / 2.0), topCapHeight: Int(height / 2.0))
}
}
return backgroundFrame.size
@ -88,11 +106,18 @@ public final class ButtonBadgeComponent: Component {
}
public final class ButtonTextContentComponent: Component {
public enum BadgeStyle {
case round
case roundedRectangle
}
public let text: String
public let badge: Int
public let textColor: UIColor
public let badgeBackground: UIColor
public let badgeForeground: UIColor
public let badgeStyle: BadgeStyle
public let badgeIconName: String?
public let combinedAlignment: Bool
public init(
@ -101,6 +126,8 @@ public final class ButtonTextContentComponent: Component {
textColor: UIColor,
badgeBackground: UIColor,
badgeForeground: UIColor,
badgeStyle: BadgeStyle = .round,
badgeIconName: String? = nil,
combinedAlignment: Bool = false
) {
self.text = text
@ -108,6 +135,8 @@ public final class ButtonTextContentComponent: Component {
self.textColor = textColor
self.badgeBackground = badgeBackground
self.badgeForeground = badgeForeground
self.badgeStyle = badgeStyle
self.badgeIconName = badgeIconName
self.combinedAlignment = combinedAlignment
}
@ -127,6 +156,12 @@ public final class ButtonTextContentComponent: Component {
if lhs.badgeForeground != rhs.badgeForeground {
return false
}
if lhs.badgeStyle != rhs.badgeStyle {
return false
}
if lhs.badgeIconName != rhs.badgeIconName {
return false
}
if lhs.combinedAlignment != rhs.combinedAlignment {
return false
}
@ -158,7 +193,10 @@ public final class ButtonTextContentComponent: Component {
self.component = component
self.componentState = state
let badgeSpacing: CGFloat = 6.0
var badgeSpacing: CGFloat = 6.0
if component.badgeIconName != nil {
badgeSpacing += 4.0
}
let contentSize = self.content.update(
transition: .immediate,
@ -182,17 +220,34 @@ public final class ButtonTextContentComponent: Component {
badge = ComponentView()
self.badge = badge
}
var badgeContent: [AnyComponentWithIdentity<Empty>] = []
if let badgeIconName = component.badgeIconName {
badgeContent.append(AnyComponentWithIdentity(
id: "icon",
component: AnyComponent(BundleIconComponent(
name: badgeIconName,
tintColor: component.badgeForeground
)))
)
}
badgeContent.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(AnimatedTextComponent(
font: Font.with(size: 15.0, design: .round, weight: .semibold, traits: .monospacedNumbers),
color: component.badgeForeground,
items: [
AnimatedTextComponent.Item(id: AnyHashable(0), content: .number(component.badge, minDigits: 0))
]
)))
)
badgeSize = badge.update(
transition: badgeTransition,
component: AnyComponent(ButtonBadgeComponent(
fillColor: component.badgeBackground,
content: AnyComponent(AnimatedTextComponent(
font: Font.semibold(15.0),
color: component.badgeForeground,
items: [
AnimatedTextComponent.Item(id: AnyHashable(0), content: .number(component.badge, minDigits: 0))
]
))
style: component.badgeStyle,
content: AnyComponent(HStack(badgeContent, spacing: 2.0))
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)

@ -8,19 +8,22 @@ public final class CameraButton: Component {
let tag: AnyObject?
let isEnabled: Bool
let action: () -> Void
let longTapAction: (() -> Void)?
public init(
content: AnyComponentWithIdentity<Empty>,
minSize: CGSize? = nil,
tag: AnyObject? = nil,
isEnabled: Bool = true,
action: @escaping () -> Void
action: @escaping () -> Void,
longTapAction: (() -> Void)? = nil
) {
self.content = content
self.minSize = minSize
self.tag = tag
self.isEnabled = isEnabled
self.action = action
self.longTapAction = longTapAction
}
public func tagged(_ tag: AnyObject) -> CameraButton {
@ -29,7 +32,8 @@ public final class CameraButton: Component {
minSize: self.minSize,
tag: tag,
isEnabled: self.isEnabled,
action: self.action
action: self.action,
longTapAction: self.longTapAction
)
}
@ -73,6 +77,8 @@ public final class CameraButton: Component {
}
transition.setScale(view: self, scale: scale)
}
private var longTapGestureRecognizer: UILongPressGestureRecognizer?
public override init(frame: CGRect) {
self.contentView = ComponentHostView<Empty>()
@ -86,6 +92,10 @@ public final class CameraButton: Component {
self.addSubview(self.contentView)
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
let longTapGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongPress))
self.longTapGestureRecognizer = longTapGestureRecognizer
self.addGestureRecognizer(longTapGestureRecognizer)
}
required init?(coder: NSCoder) {
@ -102,6 +112,10 @@ public final class CameraButton: Component {
return false
}
@objc private func handleLongPress() {
self.component?.longTapAction?()
}
@objc private func pressed() {
self.component?.action()
}
@ -159,6 +173,7 @@ public final class CameraButton: Component {
self.updateScale(transition: transition)
self.isEnabled = component.isEnabled
self.longTapGestureRecognizer?.isEnabled = component.longTapAction != nil
self.contentView.frame = CGRect(origin: CGPoint(x: floor((size.width - contentSize.width) / 2.0), y: floor((size.height - contentSize.height) / 2.0)), size: contentSize)

@ -67,7 +67,8 @@ fragment half4 cameraBlobFragment(RasterizerData in[[stage_in]],
float t = AARadius / resolution.y;
float cAlpha = 1.0 - primaryParameters.y;
float cAlpha = min(1.0, 1.0 - primaryParameters.y);
float minColor = min(1.0, 1.0 + primaryParameters.y);
float bound = primaryParameters.x + 0.05;
if (abs(offset) > bound) {
cAlpha = mix(0.0, 1.0, min(1.0, (abs(offset) - bound) * 2.4));
@ -75,5 +76,5 @@ fragment half4 cameraBlobFragment(RasterizerData in[[stage_in]],
float c = smoothstep(t, -t, map(uv, primaryParameters, primaryOffset, secondaryParameters, secondaryOffset));
return half4(c, max(cAlpha, 0.231), max(cAlpha, 0.188), c);
return half4(min(minColor, c), min(minColor, max(cAlpha, 0.231)), min(minColor, max(cAlpha, 0.188)), c);
}

@ -36,36 +36,47 @@ private struct CameraState: Equatable {
case holding
case handsFree
}
enum FlashTint {
case white
case yellow
case blue
}
let mode: CameraMode
let position: Camera.Position
let flashMode: Camera.FlashMode
let flashModeDidChange: Bool
let flashTint: FlashTint
let recording: Recording
let duration: Double
let isDualCameraEnabled: Bool
func updatedMode(_ mode: CameraMode) -> CameraState {
return CameraState(mode: mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled)
return CameraState(mode: mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled)
}
func updatedPosition(_ position: Camera.Position) -> CameraState {
return CameraState(mode: self.mode, position: position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled)
return CameraState(mode: self.mode, position: position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled)
}
func updatedFlashMode(_ flashMode: Camera.FlashMode) -> CameraState {
return CameraState(mode: self.mode, position: self.position, flashMode: flashMode, flashModeDidChange: self.flashMode != flashMode, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled)
return CameraState(mode: self.mode, position: self.position, flashMode: flashMode, flashModeDidChange: self.flashMode != flashMode, flashTint: self.flashTint, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled)
}
func updatedFlashTint(_ flashTint: FlashTint) -> CameraState {
return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: flashTint, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled)
}
func updatedRecording(_ recording: Recording) -> CameraState {
return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled)
return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, recording: recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled)
}
func updatedDuration(_ duration: Double) -> CameraState {
return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: self.recording, duration: duration, isDualCameraEnabled: self.isDualCameraEnabled)
return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, recording: self.recording, duration: duration, isDualCameraEnabled: self.isDualCameraEnabled)
}
func updatedIsDualCameraEnabled(_ isDualCameraEnabled: Bool) -> CameraState {
return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: self.recording, duration: self.duration, isDualCameraEnabled: isDualCameraEnabled)
return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, recording: self.recording, duration: self.duration, isDualCameraEnabled: isDualCameraEnabled)
}
}
@ -162,6 +173,7 @@ private final class CameraScreenComponent: CombinedComponent {
enum ImageKey: Hashable {
case cancel
case flip
case flashImage
}
private var cachedImages: [ImageKey: UIImage] = [:]
func image(_ key: ImageKey) -> UIImage {
@ -171,9 +183,21 @@ private final class CameraScreenComponent: CombinedComponent {
var image: UIImage
switch key {
case .cancel:
image = UIImage(bundleImageName: "Camera/CloseIcon")!
image = UIImage(bundleImageName: "Camera/CloseIcon")!.withRenderingMode(.alwaysTemplate)
case .flip:
image = UIImage(bundleImageName: "Camera/FlipIcon")!
image = UIImage(bundleImageName: "Camera/FlipIcon")!.withRenderingMode(.alwaysTemplate)
case .flashImage:
image = generateImage(CGSize(width: 393.0, height: 852.0), rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
var locations: [CGFloat] = [0.0, 0.2, 0.6, 1.0]
let colors: [CGColor] = [UIColor(rgb: 0xffffff, alpha: 0.25).cgColor, UIColor(rgb: 0xffffff, alpha: 0.25).cgColor, UIColor(rgb: 0xffffff, alpha: 1.0).cgColor, UIColor(rgb: 0xffffff, alpha: 1.0).cgColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0 - 10.0)
context.drawRadialGradient(gradient, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: size.width, options: .drawsAfterEndLocation)
})!.withRenderingMode(.alwaysTemplate)
}
cachedImages[key] = image
return image
@ -589,7 +613,7 @@ private final class CameraScreenComponent: CombinedComponent {
static var body: Body {
let placeholder = Child(PlaceholderComponent.self)
let frontFlash = Child(Rectangle.self)
let frontFlash = Child(Image.self)
let cancelButton = Child(CameraButton.self)
let captureControls = Child(CaptureControlsComponent.self)
let zoomControl = Child(ZoomComponent.self)
@ -689,19 +713,29 @@ private final class CameraScreenComponent: CombinedComponent {
// )
}
if case .none = component.cameraState.recording {
} else if case .front = component.cameraState.position {
var controlsTintColor: UIColor = .white
if case .front = component.cameraState.position, case .on = component.cameraState.flashMode {
let flashTintColor: UIColor
switch component.cameraState.flashTint {
case .white:
flashTintColor = .white
case .yellow:
flashTintColor = UIColor(rgb: 0xffed8c)
case .blue:
flashTintColor = UIColor(rgb: 0x8cdfff)
}
let frontFlash = frontFlash.update(
component: Rectangle(color: UIColor(white: 1.0, alpha: 0.6)),
availableSize: CGSize(width: availableSize.width, height: previewHeight),
component: Image(image: state.image(.flashImage), tintColor: flashTintColor),
availableSize: availableSize,
transition: .easeInOut(duration: 0.2)
)
context.add(frontFlash
.position(CGPoint(x: context.availableSize.width / 2.0, y: environment.safeInsets.top + previewHeight / 2.0))
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
controlsTintColor = .black
}
let shutterState: ShutterButtonState
@ -737,6 +771,7 @@ private final class CameraScreenComponent: CombinedComponent {
isTablet: isTablet,
hasAppeared: component.hasAppeared && hasAllRequiredAccess,
hasAccess: hasAllRequiredAccess,
tintColor: controlsTintColor,
shutterState: shutterState,
lastGalleryAsset: state.lastGalleryAsset,
tag: captureControlsTag,
@ -823,6 +858,7 @@ private final class CameraScreenComponent: CombinedComponent {
component: AnyComponent(
Image(
image: state.image(.cancel),
tintColor: controlsTintColor,
size: CGSize(width: 40.0, height: 40.0)
)
)
@ -867,7 +903,7 @@ private final class CameraScreenComponent: CombinedComponent {
range: nil,
waitForCompletion: false
),
colors: [:],
colors: ["__allcolors__": controlsTintColor],
size: CGSize(width: 40.0, height: 40.0)
)
)
@ -878,7 +914,7 @@ private final class CameraScreenComponent: CombinedComponent {
component: AnyComponent(
BundleIconComponent(
name: "Camera/FlashOffIcon",
tintColor: nil
tintColor: controlsTintColor
)
)
)
@ -909,7 +945,10 @@ private final class CameraScreenComponent: CombinedComponent {
content: AnyComponentWithIdentity(
id: "dual",
component: AnyComponent(
DualIconComponent(isSelected: component.cameraState.isDualCameraEnabled)
DualIconComponent(
isSelected: component.cameraState.isDualCameraEnabled,
tintColor: controlsTintColor
)
)
),
action: { [weak state] in
@ -938,7 +977,8 @@ private final class CameraScreenComponent: CombinedComponent {
component: AnyComponent(
FlipButtonContentComponent(
action: animateFlipAction,
maskFrame: .zero
maskFrame: .zero,
tintColor: controlsTintColor
)
)
),
@ -971,7 +1011,7 @@ private final class CameraScreenComponent: CombinedComponent {
let durationString = String(format: "%02d:%02d", (duration / 60) % 60, duration % 60)
let timeLabel = timeLabel.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(string: durationString, font: Font.with(size: 21.0, design: .camera), textColor: .white)),
text: .plain(NSAttributedString(string: durationString, font: Font.with(size: 21.0, design: .camera), textColor: controlsTintColor)),
horizontalAlignment: .center,
textShadowColor: UIColor(rgb: 0x000000, alpha: 0.2)
),
@ -1021,7 +1061,10 @@ private final class CameraScreenComponent: CombinedComponent {
}
if let hintText {
let hintLabel = hintLabel.update(
component: HintLabelComponent(text: hintText),
component: HintLabelComponent(
text: hintText,
tintColor: controlsTintColor
),
availableSize: availableSize,
transition: .immediate
)
@ -1045,6 +1088,7 @@ private final class CameraScreenComponent: CombinedComponent {
component: ModeComponent(
isTablet: isTablet,
strings: environment.strings,
tintColor: controlsTintColor,
availableModes: [.photo, .video],
currentMode: component.cameraState.mode,
updatedMode: { [weak state] mode in
@ -1358,6 +1402,7 @@ public class CameraScreen: ViewController {
position: cameraFrontPosition ? .front : .back,
flashMode: .off,
flashModeDidChange: false,
flashTint: .white,
recording: .none,
duration: 0.0,
isDualCameraEnabled: isDualCameraEnabled
@ -2820,17 +2865,23 @@ private final class DualIconComponent: Component {
typealias EnvironmentType = Empty
let isSelected: Bool
let tintColor: UIColor
init(
isSelected: Bool
isSelected: Bool,
tintColor: UIColor
) {
self.isSelected = isSelected
self.tintColor = tintColor
}
static func ==(lhs: DualIconComponent, rhs: DualIconComponent) -> Bool {
if lhs.isSelected != rhs.isSelected {
return false
}
if lhs.tintColor != rhs.tintColor {
return false
}
return true
}
@ -2849,7 +2900,7 @@ private final class DualIconComponent: Component {
if let image = UIImage(bundleImageName: "Camera/DualIcon"), let cgImage = image.cgImage {
context.draw(cgImage, in: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - image.size.width) / 2.0), y: floorToScreenPixels((size.height - image.size.height) / 2.0) - 1.0), size: image.size))
}
})
})?.withRenderingMode(.alwaysTemplate)
let selectedImage = generateImage(CGSize(width: 36.0, height: 36.0), rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
@ -2861,7 +2912,7 @@ private final class DualIconComponent: Component {
context.clip(to: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - image.size.width) / 2.0), y: floorToScreenPixels((size.height - image.size.height) / 2.0) - 1.0), size: image.size), mask: cgImage)
context.fill(CGRect(origin: .zero, size: size))
}
})
})?.withRenderingMode(.alwaysTemplate)
self.iconView.image = image
self.iconView.highlightedImage = selectedImage
@ -2886,6 +2937,8 @@ private final class DualIconComponent: Component {
self.iconView.frame = CGRect(origin: .zero, size: size)
self.iconView.isHighlighted = component.isSelected
self.iconView.tintColor = component.tintColor
return size
}
}

@ -31,6 +31,7 @@ private extension SimpleShapeLayer {
private final class ShutterButtonContentComponent: Component {
let isTablet: Bool
let hasAppeared: Bool
let tintColor: UIColor
let shutterState: ShutterButtonState
let blobState: ShutterBlobView.BlobState
let highlightedAction: ActionSlot<Bool>
@ -40,6 +41,7 @@ private final class ShutterButtonContentComponent: Component {
init(
isTablet: Bool,
hasAppeared: Bool,
tintColor: UIColor,
shutterState: ShutterButtonState,
blobState: ShutterBlobView.BlobState,
highlightedAction: ActionSlot<Bool>,
@ -48,6 +50,7 @@ private final class ShutterButtonContentComponent: Component {
) {
self.isTablet = isTablet
self.hasAppeared = hasAppeared
self.tintColor = tintColor
self.shutterState = shutterState
self.blobState = blobState
self.highlightedAction = highlightedAction
@ -62,6 +65,9 @@ private final class ShutterButtonContentComponent: Component {
if lhs.hasAppeared != rhs.hasAppeared {
return false
}
if lhs.tintColor != rhs.tintColor {
return false
}
if lhs.shutterState != rhs.shutterState {
return false
}
@ -156,7 +162,7 @@ private final class ShutterButtonContentComponent: Component {
}
}
}
let innerColor: UIColor
let innerSize: CGSize
let ringSize: CGSize
@ -164,7 +170,7 @@ private final class ShutterButtonContentComponent: Component {
var recordingProgress: Float?
switch component.shutterState {
case .generic, .disabled:
innerColor = .white
innerColor = component.tintColor
innerSize = CGSize(width: 60.0, height: 60.0)
ringSize = CGSize(width: 68.0, height: 68.0)
case .video:
@ -188,7 +194,7 @@ private final class ShutterButtonContentComponent: Component {
}
self.ringLayer.fillColor = UIColor.clear.cgColor
self.ringLayer.strokeColor = UIColor.white.cgColor
self.ringLayer.strokeColor = component.tintColor.cgColor
self.ringLayer.lineWidth = ringWidth
let ringPath = CGPath(
ellipseIn: CGRect(
@ -204,7 +210,7 @@ private final class ShutterButtonContentComponent: Component {
self.ringLayer.position = CGPoint(x: maximumShutterSize.width / 2.0, y: maximumShutterSize.height / 2.0)
if let blobView = self.blobView {
blobView.updateState(component.blobState, transition: transition)
blobView.updateState(component.blobState, tintColor: innerColor, transition: transition)
if component.isTablet {
blobView.bounds = CGRect(origin: .zero, size: CGSize(width: maximumShutterSize.width, height: 440.0))
} else {
@ -247,14 +253,16 @@ private final class ShutterButtonContentComponent: Component {
final class FlipButtonContentComponent: Component {
private let action: ActionSlot<Void>
private let maskFrame: CGRect
private let tintColor: UIColor
init(action: ActionSlot<Void>, maskFrame: CGRect) {
init(action: ActionSlot<Void>, maskFrame: CGRect, tintColor: UIColor) {
self.action = action
self.maskFrame = maskFrame
self.tintColor = tintColor
}
static func ==(lhs: FlipButtonContentComponent, rhs: FlipButtonContentComponent) -> Bool {
return lhs.maskFrame == rhs.maskFrame
return lhs.maskFrame == rhs.maskFrame && lhs.tintColor == rhs.tintColor
}
final class View: UIView {
@ -280,7 +288,7 @@ final class FlipButtonContentComponent: Component {
self.maskLayer.masksToBounds = true
self.maskLayer.cornerRadius = 16.0
self.icon.contents = UIImage(bundleImageName: "Camera/FlipIcon")?.cgImage
self.icon.contents = UIImage(bundleImageName: "Camera/FlipIcon")?.withRenderingMode(.alwaysTemplate).cgImage
self.darkIcon.contents = UIImage(bundleImageName: "Camera/FlipIcon")?.cgImage
self.darkIcon.layerTintColor = UIColor.black.cgColor
}
@ -326,6 +334,8 @@ final class FlipButtonContentComponent: Component {
let size = CGSize(width: 48.0, height: 48.0)
self.icon.layerTintColor = component.tintColor.cgColor
self.icon.position = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
self.icon.bounds = CGRect(origin: .zero, size: size)
@ -350,13 +360,15 @@ final class FlipButtonContentComponent: Component {
final class LockContentComponent: Component {
private let maskFrame: CGRect
private let tintColor: UIColor
init(maskFrame: CGRect) {
init(maskFrame: CGRect, tintColor: UIColor) {
self.maskFrame = maskFrame
self.tintColor = tintColor
}
static func ==(lhs: LockContentComponent, rhs: LockContentComponent) -> Bool {
return lhs.maskFrame == rhs.maskFrame
return lhs.maskFrame == rhs.maskFrame && lhs.tintColor == rhs.tintColor
}
final class View: UIView {
@ -383,7 +395,7 @@ final class LockContentComponent: Component {
self.maskLayer.masksToBounds = true
self.maskLayer.cornerRadius = 24.0
self.icon.contents = UIImage(bundleImageName: "Camera/LockIcon")?.cgImage
self.icon.contents = UIImage(bundleImageName: "Camera/LockIcon")?.withRenderingMode(.alwaysTemplate).cgImage
self.darkIcon.contents = UIImage(bundleImageName: "Camera/LockedIcon")?.cgImage
self.darkIcon.layerTintColor = UIColor.black.cgColor
}
@ -397,6 +409,8 @@ final class LockContentComponent: Component {
let size = CGSize(width: 30.0, height: 30.0)
self.icon.layerTintColor = component.tintColor.cgColor
self.icon.position = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
self.icon.bounds = CGRect(origin: .zero, size: size)
@ -447,6 +461,7 @@ final class CaptureControlsComponent: Component {
let isTablet: Bool
let hasAppeared: Bool
let hasAccess: Bool
let tintColor: UIColor
let shutterState: ShutterButtonState
let lastGalleryAsset: PHAsset?
let tag: AnyObject?
@ -465,6 +480,7 @@ final class CaptureControlsComponent: Component {
isTablet: Bool,
hasAppeared: Bool,
hasAccess: Bool,
tintColor: UIColor,
shutterState: ShutterButtonState,
lastGalleryAsset: PHAsset?,
tag: AnyObject?,
@ -482,6 +498,7 @@ final class CaptureControlsComponent: Component {
self.isTablet = isTablet
self.hasAppeared = hasAppeared
self.hasAccess = hasAccess
self.tintColor = tintColor
self.shutterState = shutterState
self.lastGalleryAsset = lastGalleryAsset
self.tag = tag
@ -507,6 +524,9 @@ final class CaptureControlsComponent: Component {
if lhs.hasAccess != rhs.hasAccess {
return false
}
if lhs.tintColor != rhs.tintColor {
return false
}
if lhs.shutterState != rhs.shutterState {
return false
}
@ -578,8 +598,7 @@ final class CaptureControlsComponent: Component {
private let shutterHightlightedAction = ActionSlot<Bool>()
private let lockImage = UIImage(bundleImageName: "Camera/LockIcon")
private let zoomImage = UIImage(bundleImageName: "Camera/ZoomIcon")
private let zoomImage = UIImage(bundleImageName: "Camera/ZoomIcon")?.withRenderingMode(.alwaysTemplate)
private var didFlip = false
@ -946,8 +965,10 @@ final class CaptureControlsComponent: Component {
transition.setBounds(view: galleryButtonView, bounds: CGRect(origin: .zero, size: galleryButtonFrame.size))
transition.setPosition(view: galleryButtonView, position: galleryButtonFrame.center)
let normalAlpha = component.tintColor.rgb == 0xffffff ? 1.0 : 0.6
transition.setScale(view: galleryButtonView, scale: isRecording || isTransitioning ? 0.1 : 1.0)
transition.setAlpha(view: galleryButtonView, alpha: isRecording || isTransitioning ? 0.0 : 1.0)
transition.setAlpha(view: galleryButtonView, alpha: isRecording || isTransitioning ? 0.0 : normalAlpha)
}
if !component.isTablet && component.hasAccess {
@ -963,7 +984,8 @@ final class CaptureControlsComponent: Component {
component: AnyComponent(
FlipButtonContentComponent(
action: component.flipAnimationAction,
maskFrame: flipButtonMaskFrame
maskFrame: flipButtonMaskFrame,
tintColor: component.tintColor
)
)
),
@ -1011,6 +1033,7 @@ final class CaptureControlsComponent: Component {
ShutterButtonContentComponent(
isTablet: component.isTablet,
hasAppeared: component.hasAppeared,
tintColor: component.tintColor,
shutterState: component.shutterState,
blobState: blobState,
highlightedAction: self.shutterHightlightedAction,
@ -1072,6 +1095,7 @@ final class CaptureControlsComponent: Component {
component: AnyComponent(
Image(
image: self.zoomImage,
tintColor: component.tintColor,
size: hintIconSize
)
),
@ -1117,7 +1141,8 @@ final class CaptureControlsComponent: Component {
id: "lock",
component: AnyComponent(
LockContentComponent(
maskFrame: lockMaskFrame
maskFrame: lockMaskFrame,
tintColor: component.tintColor
)
)
),

@ -21,6 +21,7 @@ private let buttonSize = CGSize(width: 55.0, height: 44.0)
final class ModeComponent: Component {
let isTablet: Bool
let strings: PresentationStrings
let tintColor: UIColor
let availableModes: [CameraMode]
let currentMode: CameraMode
let updatedMode: (CameraMode) -> Void
@ -29,6 +30,7 @@ final class ModeComponent: Component {
init(
isTablet: Bool,
strings: PresentationStrings,
tintColor: UIColor,
availableModes: [CameraMode],
currentMode: CameraMode,
updatedMode: @escaping (CameraMode) -> Void,
@ -36,6 +38,7 @@ final class ModeComponent: Component {
) {
self.isTablet = isTablet
self.strings = strings
self.tintColor = tintColor
self.availableModes = availableModes
self.currentMode = currentMode
self.updatedMode = updatedMode
@ -49,6 +52,9 @@ final class ModeComponent: Component {
if lhs.strings !== rhs.strings {
return false
}
if lhs.tintColor != rhs.tintColor {
return false
}
if lhs.availableModes != rhs.availableModes {
return false
}
@ -82,8 +88,17 @@ final class ModeComponent: Component {
self.pressed()
}
func update(value: String, selected: Bool) {
self.setAttributedTitle(NSAttributedString(string: value.uppercased(), font: Font.with(size: 14.0, design: .camera, weight: .semibold), textColor: selected ? UIColor(rgb: 0xf8d74a) : .white, paragraphAlignment: .center), for: .normal)
func update(value: String, selected: Bool, tintColor: UIColor) {
let accentColor: UIColor
let normalColor: UIColor
if tintColor.rgb == 0xffffff {
accentColor = UIColor(rgb: 0xf8d74a)
normalColor = .white
} else {
accentColor = tintColor
normalColor = tintColor.withAlphaComponent(0.5)
}
self.setAttributedTitle(NSAttributedString(string: value.uppercased(), font: Font.with(size: 14.0, design: .camera, weight: .semibold), textColor: selected ? accentColor : normalColor, paragraphAlignment: .center), for: .normal)
}
}
@ -152,7 +167,7 @@ final class ModeComponent: Component {
updatedMode(mode)
}
itemView.update(value: mode.title(strings: component.strings), selected: mode == component.currentMode)
itemView.update(value: mode.title(strings: component.strings), selected: mode == component.currentMode, tintColor: component.tintColor)
itemView.bounds = CGRect(origin: .zero, size: itemFrame.size)
if isTablet {
@ -199,17 +214,23 @@ final class ModeComponent: Component {
final class HintLabelComponent: Component {
let text: String
let tintColor: UIColor
init(
text: String
text: String,
tintColor: UIColor
) {
self.text = text
self.tintColor = tintColor
}
static func ==(lhs: HintLabelComponent, rhs: HintLabelComponent) -> Bool {
if lhs.text != rhs.text {
return false
}
if lhs.tintColor != rhs.tintColor {
return false
}
return true
}
@ -247,7 +268,7 @@ final class HintLabelComponent: Component {
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: component.text.uppercased(), font: Font.with(size: 14.0, design: .camera, weight: .semibold), textColor: .white)),
text: .plain(NSAttributedString(string: component.text.uppercased(), font: Font.with(size: 14.0, design: .camera, weight: .semibold), textColor: component.tintColor)),
horizontalAlignment: .center
)
),

@ -64,7 +64,6 @@ private final class AnimatableProperty<T: Interpolatable> {
}
func tick(timestamp: Double) -> Bool {
guard let animation = self.animation, case let .curve(duration, curve) = animation.animation else {
return false
}
@ -163,10 +162,14 @@ final class ShutterBlobView: UIView {
}
}
var primaryRedness: CGFloat {
func primaryRedness(tintColor: UIColor) -> CGFloat {
switch self {
case .generic:
return 0.0
if tintColor.rgb == 0x000000 {
return -1.0
} else {
return 0.0
}
default:
return 1.0
}
@ -290,14 +293,14 @@ final class ShutterBlobView: UIView {
self.displayLink?.invalidate()
}
func updateState(_ state: BlobState, transition: Transition = .immediate) {
func updateState(_ state: BlobState, tintColor: UIColor, transition: Transition = .immediate) {
guard self.state != state else {
return
}
self.state = state
self.primarySize.update(value: state.primarySize, transition: transition)
self.primaryRedness.update(value: state.primaryRedness, transition: transition)
self.primaryRedness.update(value: state.primaryRedness(tintColor: tintColor), transition: transition)
self.primaryCornerRadius.update(value: state.primaryCornerRadius, transition: transition)
self.secondarySize.update(value: state.secondarySize, transition: transition)
self.secondaryRedness.update(value: state.secondaryRedness, transition: transition)
@ -453,7 +456,6 @@ final class ShutterBlobView: UIView {
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6, instanceCount: 1)
renderEncoder.endEncoding()
var storedDrawable: MetalImageLayer.Drawable? = drawable
commandBuffer.addCompletedHandler { _ in
DispatchQueue.main.async {

@ -20,7 +20,6 @@ import AudioToolbox
import UndoUI
import ContextUI
import GalleryUI
import AttachmentTextInputPanelNode
import TelegramPresentationData
import TelegramNotices
import StickerPeekUI
@ -2081,7 +2080,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent
}
}
public final class EntityInputView: UIInputView, LegacyMessageInputPanelInputView, AttachmentTextInputPanelInputView, UIInputViewAudioFeedback {
public final class EntityInputView: UIInputView, LegacyMessageInputPanelInputView, UIInputViewAudioFeedback {
private let context: AccountContext
public var insertText: ((NSAttributedString) -> Void)?

@ -106,6 +106,12 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
self.currentIsVideo = isVideo
}
public func activateInput() {
if let view = self.inputPanel.view as? MessageInputPanelComponent.View {
view.activateInput()
}
}
public func dismissInput() -> Bool {
if let view = self.inputPanel.view as? MessageInputPanelComponent.View {
if view.canDeactivateInput() {

@ -0,0 +1,25 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "PremiumGiftAttachmentScreen",
module_name = "PremiumGiftAttachmentScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/AccountContext",
"//submodules/AttachmentUI",
"//submodules/PremiumUI",
],
visibility = [
"//visibility:public",
],
)

@ -0,0 +1,58 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import AccountContext
import PremiumUI
import AttachmentUI
public class PremiumGiftAttachmentScreen: PremiumGiftScreen, AttachmentContainable {
public var requestAttachmentMenuExpansion: () -> Void = {}
public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in }
public var cancelPanGesture: () -> Void = { }
public var isContainerPanning: () -> Bool = { return false }
public var isContainerExpanded: () -> Bool = { return false }
public var mediaPickerContext: AttachmentMediaPickerContext? {
return PremiumGiftContext(controller: self)
}
}
private final class PremiumGiftContext: AttachmentMediaPickerContext {
private weak var controller: PremiumGiftScreen?
var selectionCount: Signal<Int, NoError> {
return .single(0)
}
var caption: Signal<NSAttributedString?, NoError> {
return .single(nil)
}
public var loadingProgress: Signal<CGFloat?, NoError> {
return .single(nil)
}
public var mainButtonState: Signal<AttachmentMainButtonState?, NoError> {
return self.controller?.mainButtonStatePromise.get() ?? .single(nil)
}
init(controller: PremiumGiftScreen) {
self.controller = controller
}
func setCaption(_ caption: NSAttributedString) {
}
func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode) {
}
func schedule() {
}
func mainButtonAction() {
self.controller?.mainButtonPressed()
}
}

@ -520,7 +520,7 @@ final class ShareWithPeersScreenComponent: Component {
if translation.y > 100.0 || velocity.y > 10.0 {
controller.requestDismiss()
if case .members = component.stateContext.subject {
} else if case .channels = component.stateContext.subject {
} else {
Queue.mainQueue().justDispatch {
controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .spring))
@ -826,6 +826,8 @@ final class ShareWithPeersScreenComponent: Component {
if case .members = component.stateContext.subject {
return
} else if case .channels = component.stateContext.subject {
return
}
var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset
@ -962,6 +964,8 @@ final class ShareWithPeersScreenComponent: Component {
} else if section.id == 1 {
if case .members = component.stateContext.subject {
sectionTitle = "SUBSCRIBERS"
} else if case .channels = component.stateContext.subject {
sectionTitle = "CHANNELS"
} else {
sectionTitle = environment.strings.Story_Privacy_ContactsHeader
}
@ -1395,9 +1399,13 @@ final class ShareWithPeersScreenComponent: Component {
let subtitle: String?
if case let .legacyGroup(group) = peer {
subtitle = environment.strings.Conversation_StatusMembers(Int32(group.participantCount))
} else if case .channel = peer {
} else if case let .channel(channel) = peer {
if let count = stateValue.participants[peer.id] {
subtitle = environment.strings.Conversation_StatusMembers(Int32(count))
if case .broadcast = channel.info {
subtitle = environment.strings.Conversation_StatusSubscribers(Int32(count))
} else {
subtitle = environment.strings.Conversation_StatusMembers(Int32(count))
}
} else {
subtitle = nil
}
@ -1430,27 +1438,60 @@ final class ShareWithPeersScreenComponent: Component {
selectionState: .editing(isSelected: isSelected, isTinted: false),
hasNext: true,
action: { [weak self] peer in
guard let self else {
guard let self, let environment = self.environment, let controller = environment.controller() as? ShareWithPeersScreen else {
return
}
if peer.id.isGroupOrChannel {
self.toggleGroupPeer(peer)
} else {
let update = {
let transition = Transition(animation: .curve(duration: 0.35, curve: .spring))
self.state?.updated(transition: transition)
if self.searchStateContext != nil {
if let navigationTextFieldView = self.navigationTextField.view as? TokenListTextField.View {
navigationTextFieldView.clearText()
}
}
}
let togglePeer = {
if let index = self.selectedPeers.firstIndex(of: peer.id) {
self.selectedPeers.remove(at: index)
self.updateSelectedGroupPeers()
} else {
self.selectedPeers.append(peer.id)
}
update()
}
let transition = Transition(animation: .curve(duration: 0.35, curve: .spring))
self.state?.updated(transition: transition)
if self.searchStateContext != nil {
if let navigationTextFieldView = self.navigationTextField.view as? TokenListTextField.View {
navigationTextFieldView.clearText()
if peer.id.isGroupOrChannel {
if case .channels = component.stateContext.subject, self.selectedPeers.count >= 10 {
return
}
if case .channels = component.stateContext.subject {
if case let .channel(channel) = peer, channel.addressName == nil, !self.selectedPeers.contains(peer.id) {
let alertController = textAlertController(
context: component.context,
forceTheme: environment.theme,
title: "Channel is Private",
text: "Are you sure you want to add a private channel? Users won't be able to join it without an invite link.",
actions: [
TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}),
TextAlertAction(type: .defaultAction, title: "Add", action: {
togglePeer()
})
]
)
controller.present(alertController, in: .window(.root))
} else {
togglePeer()
}
} else {
self.toggleGroupPeer(peer)
update()
}
} else {
if case .members = component.stateContext.subject, self.selectedPeers.count >= 10 {
return
}
togglePeer()
}
}
)),
@ -1644,7 +1685,7 @@ final class ShareWithPeersScreenComponent: Component {
}
let fadeTransition = Transition.easeInOut(duration: 0.25)
if let searchStateContext = self.searchStateContext, case let .search(query, _) = searchStateContext.subject, let value = searchStateContext.stateValue, value.peers.isEmpty {
if let searchStateContext = self.searchStateContext, case let .contactsSearch(query, _) = searchStateContext.subject, let value = searchStateContext.stateValue, value.peers.isEmpty {
let sideInset: CGFloat = 44.0
let emptyAnimationHeight = 148.0
let topInset: CGFloat = topOffset + itemLayout.containerInset + 40.0
@ -1762,7 +1803,7 @@ final class ShareWithPeersScreenComponent: Component {
if let controller = self.environment?.controller() as? ShareWithPeersScreen {
if case .members = component.stateContext.subject {
} else if case .channels = component.stateContext.subject {
} else {
controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .easeInOut))
}
@ -1828,6 +1869,8 @@ final class ShareWithPeersScreenComponent: Component {
self.currentHasChannels = hasChannels
} else if case .members = component.stateContext.subject {
self.dismissPanGesture?.isEnabled = false
} else if case .channels = component.stateContext.subject {
self.dismissPanGesture?.isEnabled = false
}
let environment = environment[ViewControllerComponentContainer.Environment.self].value
@ -1975,6 +2018,8 @@ final class ShareWithPeersScreenComponent: Component {
switch component.stateContext.subject {
case .members:
placeholder = "Search Subscribers"
case .channels:
placeholder = "Search Channels"
case .chats:
placeholder = environment.strings.Story_Privacy_SearchChats
default:
@ -2015,10 +2060,10 @@ final class ShareWithPeersScreenComponent: Component {
if component.initialPrivacy.base == .closeFriends || component.initialPrivacy.base == .contacts {
onlyContacts = true
}
if let searchStateContext = self.searchStateContext, searchStateContext.subject == .search(query: self.navigationTextFieldState.text, onlyContacts: onlyContacts) {
if let searchStateContext = self.searchStateContext, searchStateContext.subject == .contactsSearch(query: self.navigationTextFieldState.text, onlyContacts: onlyContacts) {
} else {
self.searchStateDisposable?.dispose()
let searchStateContext = ShareWithPeersScreen.StateContext(context: component.context, subject: .search(query: self.navigationTextFieldState.text, onlyContacts: onlyContacts), editing: false)
let searchStateContext = ShareWithPeersScreen.StateContext(context: component.context, subject: .contactsSearch(query: self.navigationTextFieldState.text, onlyContacts: onlyContacts), editing: false)
var applyState = false
self.searchStateDisposable = (searchStateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
guard let self else {
@ -2043,6 +2088,8 @@ final class ShareWithPeersScreenComponent: Component {
transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize))
if case .members = component.stateContext.subject {
self.dimView .isHidden = true
} else if case .channels = component.stateContext.subject {
self.dimView .isHidden = true
} else {
self.dimView .isHidden = false
}
@ -2065,7 +2112,7 @@ final class ShareWithPeersScreenComponent: Component {
containerSize: CGSize(width: itemsContainerWidth, height: 1000.0)
)
var isContactsSearch = false
if let searchStateContext = self.searchStateContext, case .search(_, true) = searchStateContext.subject {
if let searchStateContext = self.searchStateContext, case .contactsSearch(_, true) = searchStateContext.subject {
isContactsSearch = true
}
let peerItemSize = self.peerTemplateItem.update(
@ -2218,6 +2265,7 @@ final class ShareWithPeersScreenComponent: Component {
var containerInset: CGFloat = environment.statusBarHeight
if case .members = component.stateContext.subject {
} else if case .channels = component.stateContext.subject {
} else {
containerInset += 10.0
}
@ -2274,6 +2322,7 @@ final class ShareWithPeersScreenComponent: Component {
var actionButtonTitle = environment.strings.Story_Privacy_SaveList
let title: String
var subtitle: String?
switch component.stateContext.subject {
case .peers:
title = environment.strings.Story_Privacy_PostStoryAs
@ -2301,15 +2350,40 @@ final class ShareWithPeersScreenComponent: Component {
case .everyone:
title = environment.strings.Story_Privacy_ExcludedPeople
}
case .search:
case .contactsSearch:
title = ""
case .members:
title = "Gift Premium"
actionButtonTitle = "Save Recipients"
subtitle = "select up to 10 subscribers"
case .channels:
title = "Add Channels"
actionButtonTitle = "Save Channels"
subtitle = "select up to 10 channels"
}
let titleComponent: AnyComponent<Empty>
if let subtitle {
titleComponent = AnyComponent(
List([
AnyComponentWithIdentity(
id: "title",
component: AnyComponent(Text(text: title, font: Font.semibold(17.0), color: environment.theme.rootController.navigationBar.primaryTextColor))
),
AnyComponentWithIdentity(
id: "subtitle",
component: AnyComponent(Text(text: subtitle, font: Font.regular(13.0), color: environment.theme.rootController.navigationBar.secondaryTextColor))
)
],
centerAlignment: true)
)
} else {
titleComponent = AnyComponent(Text(text: title, font: Font.semibold(17.0), color: environment.theme.rootController.navigationBar.primaryTextColor))
}
let navigationTitleSize = self.navigationTitle.update(
transition: .immediate,
component: AnyComponent(Text(text: title, font: Font.semibold(17.0), color: environment.theme.rootController.navigationBar.primaryTextColor)),
component: titleComponent,
environment: {},
containerSize: CGSize(width: containerWidth - navigationButtonsWidth, height: navigationHeight)
)
@ -2344,6 +2418,8 @@ final class ShareWithPeersScreenComponent: Component {
var inset: CGFloat
if case .members = component.stateContext.subject {
inset = 1000.0
} else if case .channels = component.stateContext.subject {
inset = 1000.0
} else if case let .stories(editing) = component.stateContext.subject {
if editing {
inset = 351.0
@ -2644,6 +2720,7 @@ final class ShareWithPeersScreenComponent: Component {
var scrollClippingInset: CGFloat = 0.0
if case .members = component.stateContext.subject {
} else if case .channels = component.stateContext.subject {
} else {
scrollClippingInset = 10.0
}
@ -2865,6 +2942,8 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
var theme: ViewControllerComponentContainer.Theme = .dark
if case .members = stateContext.subject {
theme = .default
} else if case .channels = stateContext.subject {
theme = .default
}
super.init(context: context, component: ShareWithPeersScreenComponent(
context: context,
@ -2887,6 +2966,9 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
if case .members = stateContext.subject {
self.navigationPresentation = .modal
self.isCustomModal = false
} else if case .channels = stateContext.subject {
self.navigationPresentation = .modal
self.isCustomModal = false
} else {
self.navigationPresentation = .flatModal
}

@ -46,8 +46,9 @@ public extension ShareWithPeersScreen {
case stories(editing: Bool)
case chats(blocked: Bool)
case contacts(base: EngineStoryPrivacy.Base)
case search(query: String, onlyContacts: Bool)
case members(peerId: EnginePeer.Id)
case contactsSearch(query: String, onlyContacts: Bool)
case members(peerId: EnginePeer.Id, searchQuery: String?)
case channels(exclude: Set<EnginePeer.Id>)
}
var stateValue: State?
@ -431,7 +432,7 @@ public extension ShareWithPeersScreen {
self.readySubject.set(true)
})
case let .search(query, onlyContacts):
case let .contactsSearch(query, onlyContacts):
let signal: Signal<([EngineRenderedPeer], [EnginePeer.Id: Optional<EnginePeer.Presence>], [EnginePeer.Id: Optional<Int>]), NoError>
if onlyContacts {
signal = combineLatest(
@ -508,18 +509,16 @@ public extension ShareWithPeersScreen {
self.readySubject.set(true)
})
case let .members(peerId):
case let .members(peerId, searchQuery):
let membersState = Promise<ChannelMemberListState>()
let contactsState = Promise<ChannelMemberListState>()
let disposableAndLoadMoreControl: (Disposable, PeerChannelMemberCategoryControl?)
disposableAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.recent(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, updated: { state in
disposableAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.recent(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: searchQuery, updated: { state in
membersState.set(.single(state))
})
let contactsDisposableAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.contacts(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: nil, updated: { state in
let contactsDisposableAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.contacts(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: searchQuery, updated: { state in
contactsState.set(.single(state))
})
@ -585,6 +584,81 @@ public extension ShareWithPeersScreen {
self.stateDisposable = combinedDisposable
self.listControl = disposableAndLoadMoreControl.1
case let .channels(excludePeerIds):
self.stateDisposable = (combineLatest(
context.engine.messages.chatList(group: .root, count: 500) |> take(1),
context.engine.data.get(EngineDataMap(Array(self.initialPeerIds).map(TelegramEngine.EngineData.Item.Peer.Peer.init)))
)
|> mapToSignal { chatList, initialPeers -> Signal<(EngineChatList, [EnginePeer.Id: Optional<EnginePeer>], [EnginePeer.Id: Optional<Int>]), NoError> in
return context.engine.data.subscribe(
EngineDataMap(chatList.items.map(\.renderedPeer.peerId).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init))
)
|> map { participantCountMap -> (EngineChatList, [EnginePeer.Id: Optional<EnginePeer>], [EnginePeer.Id: Optional<Int>]) in
return (chatList, initialPeers, participantCountMap)
}
}
|> deliverOnMainQueue).start(next: { [weak self] chatList, initialPeers, participantCounts in
guard let self else {
return
}
var participants: [EnginePeer.Id: Int] = [:]
for (key, value) in participantCounts {
if let value {
participants[key] = value
}
}
var existingIds = Set<EnginePeer.Id>()
var selectedPeers: [EnginePeer] = []
for item in chatList.items.reversed() {
if let peer = item.renderedPeer.peer {
if self.initialPeerIds.contains(peer.id) {
selectedPeers.append(peer)
existingIds.insert(peer.id)
}
}
}
for peerId in self.initialPeerIds {
if !existingIds.contains(peerId), let maybePeer = initialPeers[peerId], let peer = maybePeer {
selectedPeers.append(peer)
existingIds.insert(peerId)
}
}
var peers: [EnginePeer] = []
peers = chatList.items.filter { peer in
if let peer = peer.renderedPeer.peer {
if excludePeerIds.contains(peer.id) {
return false
}
if self.initialPeerIds.contains(peer.id) {
return false
}
if case let .channel(channel) = peer, case .broadcast = channel.info {
return true
}
return false
} else {
return false
}
}.reversed().compactMap { $0.renderedPeer.peer }
for peer in peers {
existingIds.insert(peer.id)
}
peers.insert(contentsOf: selectedPeers, at: 0)
let state = State(
peers: peers,
participants: participants
)
self.stateValue = state
self.stateSubject.set(.single(state))
self.readySubject.set(true)
})
}
}

@ -91,6 +91,7 @@ swift_library(
"//submodules/TelegramUI/Components/TabSelectorComponent",
"//submodules/TelegramUI/Components/OptionButtonComponent",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
"//submodules/Components/BalancedTextComponent",
"//submodules/AnimatedCountLabelNode",
"//submodules/StickerResources",
],

@ -395,6 +395,9 @@ private final class StoryContainerScreenComponent: Component {
private var didDisplayReactionTooltip: Bool = false
private let interactionGuide = ComponentView<Empty>()
private var isDisplayingInteractionGuide: Bool = true
override init(frame: CGRect) {
self.backgroundLayer = SimpleLayer()
self.backgroundLayer.backgroundColor = UIColor.black.cgColor
@ -890,6 +893,10 @@ private final class StoryContainerScreenComponent: Component {
self.didAnimateIn = true
self.state?.updated(transition: .immediate)
}
if let view = self.interactionGuide.view as? StoryInteractionGuideComponent.View {
view.animateIn(transitionIn: transitionIn)
}
} else {
self.didAnimateIn = true
self.state?.updated(transition: .immediate)
@ -929,7 +936,7 @@ private final class StoryContainerScreenComponent: Component {
})
})
}
func animateOut(completion: @escaping () -> Void) {
self.isAnimatingOut = true
@ -1290,6 +1297,9 @@ private final class StoryContainerScreenComponent: Component {
if self.pendingNavigationToItemId != nil {
isProgressPaused = true
}
if self.isDisplayingInteractionGuide {
isProgressPaused = true
}
var contentDerivedBottomInset: CGFloat = environment.safeInsets.bottom
@ -1698,6 +1708,35 @@ private final class StoryContainerScreenComponent: Component {
controller.presentationContext.containerLayoutUpdated(subLayout, transition: transition.containedViewLayoutTransition)
}
if self.isDisplayingInteractionGuide {
let _ = self.interactionGuide.update(
transition: .immediate,
component: AnyComponent(
StoryInteractionGuideComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
action: { [weak self] in
self?.isDisplayingInteractionGuide = false
}
)
),
environment: {},
containerSize: availableSize
)
if let view = self.interactionGuide.view {
if view.superview == nil {
self.addSubview(view)
}
view.layer.zPosition = 1000.0
view.frame = CGRect(origin: .zero, size: availableSize)
}
} else if let view = self.interactionGuide.view as? StoryInteractionGuideComponent.View, view.superview != nil {
view.animateOut(completion: {
view.removeFromSuperview()
})
}
return availableSize
}
}

@ -0,0 +1,333 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import MultilineTextComponent
import BalancedTextComponent
import LottieComponent
final class StoryInteractionGuideComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let action: () -> Void
init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
action: @escaping () -> Void
) {
self.context = context
self.theme = theme
self.strings = strings
self.action = action
}
static func ==(lhs: StoryInteractionGuideComponent, rhs: StoryInteractionGuideComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
return true
}
final class View: UIView {
private var component: StoryInteractionGuideComponent?
private weak var state: EmptyComponentState?
private let effectView: UIVisualEffectView
private let containerView = UIView()
private let titleLabel = ComponentView<Empty>()
private let descriptionLabel = ComponentView<Empty>()
private let guideItems = ComponentView<Empty>()
private let proceedButton = ComponentView<Empty>()
override init(frame: CGRect) {
self.effectView = UIVisualEffectView(effect: nil)
super.init(frame: frame)
self.addSubview(self.effectView)
self.addSubview(self.containerView)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.handleTap)))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func handleTap() {
if let component = self.component {
component.action()
}
}
func animateIn(transitionIn: StoryContainerScreen.TransitionIn) {
UIView.animate(withDuration: 0.2) {
self.effectView.effect = UIBlurEffect(style: .dark)
}
self.containerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.containerView.layer.animateScale(from: 0.85, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
}
func animateOut(completion: @escaping () -> Void) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
completion()
})
self.containerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
self.containerView.layer.animateScale(from: 1.0, to: 1.1, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
}
func update(component: StoryInteractionGuideComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
let sideInset: CGFloat = 48.0
let titleSize = self.titleLabel.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "Watching Stories", font: Font.semibold(20.0), textColor: .white, paragraphAlignment: .center)))),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height)
)
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0), y: 146.0), size: titleSize)
if let view = self.titleLabel.view {
if view.superview == nil {
self.containerView.addSubview(view)
}
view.frame = titleFrame
}
let textSize = self.descriptionLabel.update(
transition: .immediate,
component: AnyComponent(BalancedTextComponent(text: .plain(NSAttributedString(string: "You can use these gestures to control playback.", font: Font.regular(15.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.6), paragraphAlignment: .center)), maximumNumberOfLines: 0, lineSpacing: 0.2)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height)
)
let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - textSize.width) / 2.0), y: titleFrame.maxY + 7.0), size: textSize)
if let view = self.descriptionLabel.view {
if view.superview == nil {
self.containerView.addSubview(view)
}
view.frame = textFrame
}
let items: [AnyComponentWithIdentity<Empty>] = [
AnyComponentWithIdentity(
id: "forward",
component: AnyComponent(
GuideItemComponent(
context: component.context,
title: "Go forward",
text: "Tap the screen",
animationName: "story_forward"
)
)
),
AnyComponentWithIdentity(
id: "pause",
component: AnyComponent(
GuideItemComponent(
context: component.context,
title: "Pause and Seek",
text: "Hold and move sideways",
animationName: "story_pause"
)
)
),
AnyComponentWithIdentity(
id: "back",
component: AnyComponent(
GuideItemComponent(
context: component.context,
title: "Go back",
text: "Tap the left edge",
animationName: "story_back"
)
)
),
AnyComponentWithIdentity(
id: "move",
component: AnyComponent(
GuideItemComponent(
context: component.context,
title: "Move between stories",
text: "Swipe left or right",
animationName: "story_move"
)
)
)
]
let itemsSize = self.guideItems.update(
transition: .immediate,
component: AnyComponent(List(items)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height)
)
let itemsFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - itemsSize.width) / 2.0), y: textFrame.maxY + 40.0), size: itemsSize)
if let view = self.guideItems.view {
if view.superview == nil {
self.containerView.addSubview(view)
}
view.frame = itemsFrame
}
let buttonSize = self.proceedButton.update(
transition: .immediate,
component: AnyComponent(Button(
content: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "Tap to keep watching", font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center)))),
action: { [weak self] in
self?.handleTap()
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height)
)
let buttonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - buttonSize.width) / 2.0), y: itemsFrame.maxY + 57.0), size: buttonSize)
if let view = self.proceedButton.view {
if view.superview == nil {
self.containerView.addSubview(view)
}
view.frame = buttonFrame
}
let bounds = CGRect(origin: .zero, size: availableSize)
self.effectView.frame = bounds
self.containerView.frame = bounds
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class GuideItemComponent: Component {
let context: AccountContext
let title: String
let text: String
let animationName: String
init(
context: AccountContext,
title: String,
text: String,
animationName: String
) {
self.context = context
self.title = title
self.text = text
self.animationName = animationName
}
static func ==(lhs: GuideItemComponent, rhs: GuideItemComponent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.animationName != rhs.animationName {
return false
}
return true
}
final class View: UIView {
private var component: GuideItemComponent?
private weak var state: EmptyComponentState?
private let animation = ComponentView<Empty>()
private let titleLabel = ComponentView<Empty>()
private let descriptionLabel = ComponentView<Empty>()
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: GuideItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
let sideInset: CGFloat = 48.0
let size = CGSize(width: availableSize.width, height: 90.0)
let originX = availableSize.width / 2.0 - 110.0
let animationSize = self.animation.update(
transition: .immediate,
component: AnyComponent(
LottieComponent(
content: LottieComponent.AppBundleContent(name: component.animationName),
color: .white,
placeholderColor: nil,
startingPosition: .begin,
size: CGSize(width: 60.0, height: 60.0),
renderingScale: UIScreen.main.scale,
loop: true
)
),
environment: {},
containerSize: availableSize
)
let animationFrame = CGRect(origin: CGPoint(x: originX - 11.0, y: 15.0), size: animationSize)
if let view = self.animation.view {
if view.superview == nil {
self.addSubview(view)
}
view.frame = animationFrame
}
let titleSize = self.titleLabel.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .natural)))),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height)
)
let titleFrame = CGRect(origin: CGPoint(x: originX + 60.0, y: 25.0), size: titleSize)
if let view = self.titleLabel.view {
if view.superview == nil {
self.addSubview(view)
}
view.frame = titleFrame
}
let textSize = self.descriptionLabel.update(
transition: .immediate,
component: AnyComponent(BalancedTextComponent(text: .plain(NSAttributedString(string: component.text, font: Font.regular(15.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.6), paragraphAlignment: .natural)), maximumNumberOfLines: 0)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height)
)
let textFrame = CGRect(origin: CGPoint(x: originX + 60.0, y: titleFrame.maxY + 2.0), size: textSize)
if let view = self.descriptionLabel.view {
if view.superview == nil {
self.addSubview(view)
}
view.frame = textFrame
}
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

@ -1918,7 +1918,7 @@ final class StoryItemSetContainerSendMessage {
present(controller, mediaPickerContext)
}
private func presentOldMediaPicker(view: StoryItemSetContainerComponent.View, peer: EnginePeer, replyMessageId: EngineMessage.Id?, replyToStoryId: StoryId?, fileMode: Bool, editingMedia: Bool, present: @escaping (AttachmentContainable, AttachmentMediaPickerContext) -> Void, completion: @escaping ([Any], Bool, Int32) -> Void) {
private func presentOldMediaPicker(view: StoryItemSetContainerComponent.View, peer: EnginePeer, replyMessageId: EngineMessage.Id?, replyToStoryId: StoryId?, fileMode: Bool, editingMedia: Bool, push: @escaping (ViewController) -> Void, completion: @escaping ([Any], Bool, Int32) -> Void) {
guard let component = view.component else {
return
}
@ -2047,14 +2047,14 @@ final class StoryItemSetContainerSendMessage {
}
}
view.endEditing(true)
present(legacyController, LegacyAssetPickerContext(controller: controller))
push(legacyController)
}
})
})
}
private func presentFileGallery(view: StoryItemSetContainerComponent.View, peer: EnginePeer, replyMessageId: EngineMessage.Id?, replyToStoryId: StoryId?, editingMessage: Bool = false) {
self.presentOldMediaPicker(view: view, peer: peer, replyMessageId: replyMessageId, replyToStoryId: replyToStoryId, fileMode: true, editingMedia: editingMessage, present: { [weak view] c, _ in
self.presentOldMediaPicker(view: view, peer: peer, replyMessageId: replyMessageId, replyToStoryId: replyToStoryId, fileMode: true, editingMedia: editingMessage, push: { [weak view] c in
view?.component?.controller()?.push(c)
}, completion: { [weak self, weak view] signals, silentPosting, scheduleTime in
guard let self, let view else {

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

@ -0,0 +1,185 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 11.000000 0.696762 cm
0.000000 0.000000 0.000000 scn
9.816118 24.143660 m
9.159256 24.247375 l
9.816118 24.143660 l
h
9.408499 17.298222 m
10.472980 24.039946 l
9.159256 24.247375 l
8.094773 17.505651 l
9.408499 17.298222 l
h
7.454219 25.239820 m
-0.372424 13.800879 l
0.725235 13.049850 l
8.551878 24.488791 l
7.454219 25.239820 l
h
1.001714 11.195682 m
4.259832 11.195682 l
4.259832 12.525682 l
1.001714 12.525682 l
1.001714 11.195682 l
h
4.590733 10.808434 m
3.526251 4.066710 l
4.839975 3.859280 l
5.904458 10.601005 l
4.590733 10.808434 l
h
6.545013 2.866837 m
14.371655 14.305777 l
13.273996 15.056806 l
5.447354 3.617867 l
6.545013 2.866837 l
h
12.997517 16.910973 m
9.739399 16.910973 l
9.739399 15.580973 l
12.997517 15.580973 l
12.997517 16.910973 l
h
14.371655 14.305777 m
15.127728 15.410808 14.336444 16.910973 12.997517 16.910973 c
12.997517 15.580973 l
13.266914 15.580973 13.426117 15.279137 13.273996 15.056806 c
14.371655 14.305777 l
h
3.526251 4.066710 m
3.249377 2.313175 5.542558 1.401711 6.545013 2.866837 c
5.447354 3.617867 l
5.245656 3.323076 4.784269 3.506472 4.839975 3.859280 c
3.526251 4.066710 l
h
4.259832 11.195682 m
4.465684 11.195682 4.622838 11.011766 4.590733 10.808434 c
5.904458 10.601005 l
6.064025 11.611597 5.282945 12.525682 4.259832 12.525682 c
4.259832 11.195682 l
h
-0.372424 13.800879 m
-1.128497 12.695848 -0.337213 11.195682 1.001714 11.195682 c
1.001714 12.525682 l
0.732317 12.525682 0.573114 12.827518 0.725235 13.049850 c
-0.372424 13.800879 l
h
10.472980 24.039946 m
10.749854 25.793478 8.456675 26.704947 7.454219 25.239820 c
8.551878 24.488791 l
8.753575 24.783579 9.214963 24.600183 9.159256 24.247375 c
10.472980 24.039946 l
h
8.094773 17.505651 m
7.935206 16.495058 8.716287 15.580973 9.739399 15.580973 c
9.739399 16.910973 l
9.533547 16.910973 9.376393 17.094889 9.408499 17.298222 c
8.094773 17.505651 l
h
f
n
Q
q
0.000000 1.000000 -1.000000 0.000000 4.330002 8.000000 cm
0.000000 0.000000 0.000000 scn
0.665000 1.330002 m
0.665000 1.697271 0.367269 1.995002 0.000000 1.995002 c
-0.367269 1.995002 -0.665000 1.697271 -0.665000 1.330002 c
0.665000 1.330002 l
h
-0.665000 -6.669998 m
-0.665000 -7.037268 -0.367269 -7.334998 0.000000 -7.334998 c
0.367269 -7.334998 0.665000 -7.037268 0.665000 -6.669998 c
-0.665000 -6.669998 l
h
-0.665000 1.330002 m
-0.665000 -6.669998 l
0.665000 -6.669998 l
0.665000 1.330002 l
-0.665000 1.330002 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 6.500000 2.669998 cm
0.000000 0.000000 0.000000 scn
1.165000 9.330002 m
1.165000 9.697271 0.867269 9.995002 0.500000 9.995002 c
0.132731 9.995002 -0.165000 9.697271 -0.165000 9.330002 c
1.165000 9.330002 l
h
-0.165000 1.330002 m
-0.165000 0.962732 0.132731 0.665002 0.500000 0.665002 c
0.867269 0.665002 1.165000 0.962732 1.165000 1.330002 c
-0.165000 1.330002 l
h
-0.165000 9.330002 m
-0.165000 1.330002 l
1.165000 1.330002 l
1.165000 9.330002 l
-0.165000 9.330002 l
h
f
n
Q
endstream
endobj
3 0 obj
2982
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000003072 00000 n
0000003095 00000 n
0000003268 00000 n
0000003342 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
3401
%%EOF

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

@ -0,0 +1,79 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 -0.388103 cm
1.000000 1.000000 1.000000 scn
4.868845 7.330381 m
4.598474 7.330381 4.392062 7.571941 4.434230 7.839005 c
5.022154 11.562523 l
5.095322 12.025919 4.489317 12.266791 4.224403 11.879610 c
0.077619 5.818924 l
-0.122184 5.526905 0.086922 5.130463 0.440753 5.130463 c
2.264232 5.130463 l
2.534604 5.130463 2.741015 4.888903 2.698848 4.621839 c
2.110924 0.898320 l
2.037756 0.434924 2.643760 0.194052 2.908674 0.581233 c
7.055459 6.641919 l
7.255262 6.933939 7.046156 7.330381 6.692324 7.330381 c
4.868845 7.330381 l
h
f*
n
Q
endstream
endobj
3 0 obj
610
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 7.133078 11.684636 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000000700 00000 n
0000000722 00000 n
0000000894 00000 n
0000000968 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1027
%%EOF

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -76,6 +76,7 @@ import ChatSendMessageActionUI
import ChatTextLinkEditUI
import WebUI
import PremiumUI
import PremiumGiftAttachmentScreen
import ImageTransparency
import StickerPackPreviewUI
import TextNodeWithEntities
@ -747,6 +748,204 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
case .full:
break
}
} else if let giveaway = media as? TelegramMediaGiveaway {
//TODO:localize
var peerName = ""
if let peerId = giveaway.channelPeerIds.first, let peer = message.peers[peerId] {
peerName = EnginePeer(peer).compactDisplayTitle
}
var signal = strongSelf.context.engine.payments.premiumGiveawayInfo(peerId: message.id.peerId, messageId: message.id)
let disposable: MetaDisposable
if let current = strongSelf.bankCardDisposable {
disposable = current
} else {
disposable = MetaDisposable()
strongSelf.bankCardDisposable = disposable
}
var cancelImpl: (() -> Void)?
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let progressSignal = Signal<Never, NoError> { subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.startStrict()
signal = signal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
cancelImpl = {
disposable.set(nil)
}
disposable.set((signal
|> deliverOnMainQueue).startStrict(next: { [weak self] info in
if let strongSelf = self, let info = info {
let date = stringForDate(timestamp: giveaway.untilDate, strings: strongSelf.presentationData.strings)
let startDate = stringForDate(timestamp: message.timestamp, strings: strongSelf.presentationData.strings)
let title: String
let text: String
var warning: String?
var dismissImpl: (() -> Void)?
var actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {
dismissImpl?()
})]
switch info {
case let .ongoing(status):
title = "About This Giveaway"
let intro: String
if case .almostOver = status {
intro = "The giveaway was sponsored by the admins of **\(peerName)**, who acquired **\(giveaway.quantity) Telegram Premium** subscriptions for **\(giveaway.months)** months for its followers."
} else {
intro = "The giveaway is sponsored by the admins of **\(peerName)**, who acquired **\(giveaway.quantity) Telegram Premium** subscriptions for **\(giveaway.months)** months for its followers."
}
let ending: String
if case .almostOver = status {
if giveaway.flags.contains(.onlyNewSubscribers) {
if giveaway.channelPeerIds.count > 1 {
ending = "On **\(date)**, Telegram automatically selected **\(giveaway.quantity)** random users that joined **\(peerName)** and other listed channels after **\(startDate)**."
} else {
ending = "On **\(date)**, Telegram automatically selected **\(giveaway.quantity)** random users that joined **\(peerName)** after **\(startDate)**."
}
} else {
if giveaway.channelPeerIds.count > 1 {
ending = "On **\(date)**, Telegram automatically selected **\(giveaway.quantity)** random subscribers of **\(peerName)** and other listed channels."
} else {
ending = "On **\(date)**, Telegram automatically selected **\(giveaway.quantity)** random subscribers of **\(peerName)**."
}
}
} else {
if giveaway.flags.contains(.onlyNewSubscribers) {
if giveaway.channelPeerIds.count > 1 {
ending = "On **\(date)**, Telegram will automatically select **\(giveaway.quantity)** random users that joined **\(peerName)** and **\(giveaway.channelPeerIds.count - 1)** other listed channels after **\(startDate)**."
} else {
ending = "On **\(date)**, Telegram will automatically select **\(giveaway.quantity)** random users that joined **\(peerName)** after **\(startDate)**."
}
} else {
if giveaway.channelPeerIds.count > 1 {
ending = "On **\(date)**, Telegram will automatically select **\(giveaway.quantity)** random subscribers of **\(peerName)** and **\(giveaway.channelPeerIds.count - 1)** other listed channels."
} else {
ending = "On **\(date)**, Telegram will automatically select **\(giveaway.quantity)** random subscribers of **\(peerName)**."
}
}
}
var participation: String
switch status {
case .notQualified:
if giveaway.channelPeerIds.count > 1 {
participation = "To take part in this giveaway please join the channel **\(peerName)** (**\(giveaway.channelPeerIds.count - 1)** other listed channels) before **\(date)**."
} else {
participation = "To take part in this giveaway please join the channel **\(peerName)** before **\(date)**."
}
case let .notAllowed(reason):
switch reason {
case let .joinedTooEarly(joinedOn):
let joinDate = stringForDate(timestamp: joinedOn, strings: strongSelf.presentationData.strings)
participation = "You are not eligible to participate in this giveaway, because you joined this channel on **\(joinDate)**, which is before the contest started."
case let .channelAdmin(adminId):
let _ = adminId
participation = "You are not eligible to participate in this giveaway, because you are an admin of participating channel (**\(peerName)**)."
}
case .participating:
if giveaway.channelPeerIds.count > 1 {
participation = "You are participating in this giveaway, because you have joined the channel **\(peerName)** (**\(giveaway.channelPeerIds.count - 1)** other listed channels)."
} else {
participation = "You are participating in this giveaway, because you have joined the channel **\(peerName)**."
}
case .almostOver:
participation = "The giveaway is over, preparing results."
}
if !participation.isEmpty {
participation = "\n\n\(participation)"
}
text = "\(intro)\n\n\(ending)\(participation)"
case let .finished(status, finishDate, _, activatedCount):
let date = stringForDate(timestamp: finishDate, strings: strongSelf.presentationData.strings)
title = "Giveaway Ended"
let intro = "The giveaway was sponsored by the admins of **\(peerName)**, who acquired **\(giveaway.quantity) Telegram Premium** subscriptions for **\(giveaway.months)** months for its followers."
var ending: String
if giveaway.flags.contains(.onlyNewSubscribers) {
if giveaway.channelPeerIds.count > 1 {
ending = "On **\(date)**, Telegram automatically selected **\(giveaway.quantity)** random users that joined **\(peerName)** and other listed channels after **\(startDate)**."
} else {
ending = "On **\(date)**, Telegram automatically selected **\(giveaway.quantity)** random users that joined **\(peerName)** after **\(startDate)**."
}
} else {
if giveaway.channelPeerIds.count > 1 {
ending = "On **\(date)**, Telegram automatically selected **\(giveaway.quantity)** random subscribers of **\(peerName)** and other listed channels."
} else {
ending = "On **\(date)**, Telegram automatically selected **\(giveaway.quantity)** random subscribers of **\(peerName)**."
}
}
if activatedCount > 0 {
ending += " \(activatedCount) of the winners already used their gift links."
}
var result: String
switch status {
case .refunded:
result = ""
warning = "The channel cancelled the prizes by reversing the payment for them."
actions = [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Close, action: {
dismissImpl?()
})]
case .notWon:
result = "\n\nYou didn't win a prize in this giveaway."
case let .won(slug):
result = "\n\nYou won a prize in this giveaway. 🏆"
let _ = slug
actions = [TextAlertAction(type: .defaultAction, title: "View My Prize", action: {
dismissImpl?()
}), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
dismissImpl?()
})]
}
text = "\(intro)\n\n\(ending)\(result)"
}
let alertController = giveawayInfoAlertController(
context: strongSelf.context,
updatedPresentationData: strongSelf.updatedPresentationData,
title: title,
text: text,
warning: warning,
actions: actions
)
strongSelf.present(alertController, in: .window(.root))
dismissImpl = { [weak alertController] in
alertController?.dismissAnimated()
}
}
}))
return true
} else if let action = media as? TelegramMediaAction {
if !displayVoiceMessageDiscardAlert() {
return false
@ -4021,9 +4220,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
})
} else {
strongSelf.presentOldMediaPicker(fileMode: false, editingMedia: true, present: { [weak self] c, _ in
self?.effectiveNavigationController?.pushViewController(c)
}, completion: { signals, _, _ in
strongSelf.presentOldMediaPicker(fileMode: false, editingMedia: true, completion: { signals, _, _ in
self?.interfaceInteraction?.setupEditMessage(messageId, { _ in })
self?.editMessageMediaWithLegacySignals(signals)
})
@ -13806,7 +14003,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
case .gift:
let premiumGiftOptions = strongSelf.presentationInterfaceState.premiumGiftOptions
if !premiumGiftOptions.isEmpty {
let controller = PremiumGiftScreen(context: context, peerId: peer.id, options: premiumGiftOptions, source: .attachMenu, pushController: { [weak self] c in
let controller = PremiumGiftAttachmentScreen(context: context, peerId: peer.id, options: premiumGiftOptions, source: .attachMenu, pushController: { [weak self] c in
if let strongSelf = self {
strongSelf.push(c)
}
@ -13985,9 +14182,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
let controller = legacyAttachmentMenu(context: strongSelf.context, peer: peer, threadTitle: strongSelf.threadInfo?.title, chatLocation: strongSelf.chatLocation, editMediaOptions: menuEditMediaOptions, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, canSendPolls: canSendPolls, updatedPresentationData: strongSelf.updatedPresentationData, parentController: legacyController, recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, initialCaption: inputText, openGallery: {
self?.presentOldMediaPicker(fileMode: false, editingMedia: editMediaOptions != nil, present: { [weak self] c, _ in
self?.effectiveNavigationController?.pushViewController(c)
}, completion: { signals, silentPosting, scheduleTime in
self?.presentOldMediaPicker(fileMode: false, editingMedia: editMediaOptions != nil, completion: { signals, silentPosting, scheduleTime in
if !inputText.string.isEmpty {
strongSelf.clearInputText()
}
@ -14179,9 +14374,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
private func presentFileGallery(editingMessage: Bool = false) {
self.presentOldMediaPicker(fileMode: true, editingMedia: editingMessage, present: { [weak self] c, _ in
self?.effectiveNavigationController?.pushViewController(c)
}, completion: { [weak self] signals, silentPosting, scheduleTime in
self.presentOldMediaPicker(fileMode: true, editingMedia: editingMessage, completion: { [weak self] signals, silentPosting, scheduleTime in
if editingMessage {
self?.editMessageMediaWithLegacySignals(signals)
} else {
@ -14388,7 +14581,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
present(controller, mediaPickerContext)
}
private func presentOldMediaPicker(fileMode: Bool, editingMedia: Bool, present: @escaping (AttachmentContainable, AttachmentMediaPickerContext) -> Void, completion: @escaping ([Any], Bool, Int32) -> Void) {
private func presentOldMediaPicker(fileMode: Bool, editingMedia: Bool, completion: @escaping ([Any], Bool, Int32) -> Void) {
let engine = self.context.engine
let _ = (self.context.sharedContext.accountManager.transaction { transaction -> Signal<(GeneratedMediaStoreSettings, EngineConfiguration.SearchBots), NoError> in
let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self)
@ -14496,7 +14689,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}
strongSelf.chatDisplayNode.dismissInput()
present(legacyController, LegacyAssetPickerContext(controller: controller))
strongSelf.effectiveNavigationController?.pushViewController(legacyController)
}
})
})
@ -17695,13 +17888,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
let dismissWebAppContollers: () -> Void = {
if let currentWebAppController = strongSelf.currentWebAppController {
strongSelf.currentWebAppController = nil
currentWebAppController.dismiss(animated: true, completion: nil)
} else if let currentWebAppController = strongSelf.currentMenuWebAppController {
strongSelf.currentMenuWebAppController = nil
currentWebAppController.dismiss(animated: true, completion: nil)
}
// if let currentWebAppController = strongSelf.currentWebAppController {
// strongSelf.currentWebAppController = nil
// currentWebAppController.dismiss(animated: true, completion: nil)
// } else if let currentWebAppController = strongSelf.currentMenuWebAppController {
// strongSelf.currentMenuWebAppController = nil
// currentWebAppController.dismiss(animated: true, completion: nil)
// }
}
switch navigation {
@ -17751,7 +17944,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
commit()
case let .withBotApp(botAppStart):
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId.id))
|> deliverOnMainQueue).startStrict(next: { [weak self] peer in
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
if let strongSelf = self, let peer {
strongSelf.presentBotApp(botApp: botAppStart.botApp, botPeer: peer, payload: botAppStart.payload, concealed: concealed, commit: {
dismissWebAppContollers()

@ -194,20 +194,21 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
months = monthsValue
text = item.presentationData.strings.Notification_PremiumGift_Subtitle(item.presentationData.strings.Notification_PremiumGift_Months(months)).string
case let .giftCode(_, fromGiveaway, channelId, monthsValue):
giftSize.width += 34.0
giftSize.height += 84.0
textSpacing += 13.0
title = "Congratulations!"
var peerName = ""
if let channelId, let channel = item.message.peers[channelId] {
peerName = EnginePeer(channel).compactDisplayTitle
}
if fromGiveaway {
giftSize.width += 34.0
giftSize.height += 84.0
textSpacing += 20.0
title = "Congratulations!"
var peerName = ""
if let channelId, let channel = item.message.peers[channelId] {
peerName = EnginePeer(channel).compactDisplayTitle
}
text = "You won a prize in a giveaway organized by **\(peerName)**.\n\nYour prize is a **Telegram Premium** subscription for **\(monthsValue)** months."
} else {
text = item.presentationData.strings.Notification_PremiumGift_Subtitle(item.presentationData.strings.Notification_PremiumGift_Months(months)).string
text = "You've received a gift from **\(peerName)**.\n\nYour gift is a **Telegram Premium** subscription for **\(monthsValue)** months."
}
months = monthsValue
buttonTitle = "Open Gift Link"
hasServiceMessage = false

@ -14,6 +14,7 @@ import Markdown
import ShimmerEffect
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import AvatarNode
private let titleFont = Font.medium(15.0)
private let textFont = Font.regular(13.0)
@ -34,9 +35,13 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
private let dateTitleNode: TextNode
private let dateTextNode: TextNode
private let badgeBackgroundNode: ASImageNode
private let badgeTextNode: TextNode
private var giveaway: TelegramMediaGiveaway?
private let buttonNode: ChatMessageAttachedContentButtonNode
private let channelButton: PeerButtonNode
override var visibility: ListViewItemNodeVisibility {
didSet {
@ -76,7 +81,13 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
self.dateTitleNode = TextNode()
self.dateTextNode = TextNode()
self.badgeBackgroundNode = ASImageNode()
self.badgeBackgroundNode.displaysAsynchronously = false
self.badgeTextNode = TextNode()
self.buttonNode = ChatMessageAttachedContentButtonNode()
self.channelButton = PeerButtonNode()
super.init()
@ -87,7 +98,10 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
self.addSubnode(self.dateTitleNode)
self.addSubnode(self.dateTextNode)
self.addSubnode(self.buttonNode)
self.addSubnode(self.channelButton)
self.addSubnode(self.animationNode)
self.addSubnode(self.badgeBackgroundNode)
self.addSubnode(self.badgeTextNode)
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
@ -145,9 +159,15 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
let makeDateTitleLayout = TextNode.asyncLayout(self.dateTitleNode)
let makeDateTextLayout = TextNode.asyncLayout(self.dateTextNode)
let makeBadgeTextLayout = TextNode.asyncLayout(self.badgeTextNode)
let makeButtonLayout = ChatMessageAttachedContentButtonNode.asyncLayout(self.buttonNode)
let makeChannelLayout = PeerButtonNode.asyncLayout(self.channelButton)
let currentItem = self.item
return { item, layoutConstants, _, _, constrainedSize, _ in
var giveaway: TelegramMediaGiveaway?
for media in item.message.media {
@ -156,13 +176,27 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
}
}
var themeUpdated = false
if currentItem?.presentationData.theme.theme !== item.presentationData.theme.theme {
themeUpdated = true
}
var incoming = item.message.effectivelyIncoming(item.context.account.peerId)
if case .forwardedMessages = item.associatedData.subject {
incoming = false
}
let textColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.primaryTextColor : item.presentationData.theme.theme.chat.message.outgoing.primaryTextColor
let accentColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.accentTextColor : item.presentationData.theme.theme.chat.message.outgoing.accentTextColor
var updatedBadgeImage: UIImage?
if themeUpdated {
updatedBadgeImage = generateStretchableFilledCircleImage(diameter: 21.0, color: accentColor, strokeColor: .white, strokeWidth: 1.0 + UIScreenPixel, backgroundColor: nil)
}
let badgeString = NSAttributedString(string: "X\(giveaway?.quantity ?? 1)", font: Font.with(size: 10.0, design: .round , weight: .bold, traits: .monospacedNumbers), textColor: .white)
//TODO:localize
let prizeTitleString = NSAttributedString(string: "Giveaway Prizes", font: titleFont, textColor: textColor)
var prizeTextString: NSAttributedString?
if let giveaway {
@ -177,19 +211,40 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
}
let participantsTitleString = NSAttributedString(string: "Participants", font: titleFont, textColor: textColor)
let participantsTextString = NSAttributedString(string: "All subscribers of this channel:", font: textFont, textColor: textColor)
let participantsText: String
if let giveaway {
if giveaway.flags.contains(.onlyNewSubscribers) {
if giveaway.channelPeerIds.count > 1 {
participantsText = "All users who join the channels below after this date:"
} else {
participantsText = "All users who join this channel below after this date:"
}
} else {
if giveaway.channelPeerIds.count > 1 {
participantsText = "All subscribers of the channels below:"
} else {
participantsText = "All subscribers of this channel:"
}
}
} else {
participantsText = ""
}
let participantsTextString = NSAttributedString(string: participantsText, font: textFont, textColor: textColor)
let dateTitleString = NSAttributedString(string: "Winners Selection Date", font: titleFont, textColor: textColor)
var dateTextString: NSAttributedString?
if let giveaway {
dateTextString = NSAttributedString(string: stringForFullDate(timestamp: giveaway.untilDate, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat), font: textFont, textColor: textColor)
}
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
let sideInsets = layoutConstants.text.bubbleInsets.right * 2.0
let maxTextWidth = min(200.0, max(1.0, constrainedSize.width - 7.0 - sideInsets))
let (badgeTextLayout, badgeTextApply) = makeBadgeTextLayout(TextNodeLayoutArguments(attributedString: badgeString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (prizeTitleLayout, prizeTitleApply) = makePrizeTitleLayout(TextNodeLayoutArguments(attributedString: prizeTitleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (prizeTextLayout, prizeTextApply) = makePrizeTextLayout(TextNodeLayoutArguments(attributedString: prizeTextString, backgroundColor: nil, maximumNumberOfLines: 5, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
@ -291,7 +346,7 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
titleHighlightedColor = bubbleColors.fill[0]
}
let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, nil, nil, false, "HOW DOES IT WORK?", titleColor, titleHighlightedColor, false)
let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, nil, nil, false, "LEARN MORE", titleColor, titleHighlightedColor, false)
let months = giveaway?.months ?? 0
let animationName: String
@ -319,15 +374,26 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
maxContentWidth = max(maxContentWidth, buttonWidth)
maxContentWidth += 30.0
var channelPeer: EnginePeer?
if let channelPeerId = giveaway?.channelPeerIds.first, let peer = item.message.peers[channelPeerId] {
channelPeer = EnginePeer(peer)
}
let (_, continueChannelLayout) = makeChannelLayout(item.context, maxContentWidth - 30.0, channelPeer, accentColor, accentColor.withAlphaComponent(0.1))
let contentWidth = maxContentWidth + layoutConstants.text.bubbleInsets.right * 2.0
return (contentWidth, { boundingWidth in
let (buttonSize, buttonApply) = continueLayout(boundingWidth - layoutConstants.text.bubbleInsets.right * 2.0)
let buttonSpacing: CGFloat = 4.0
let (channelButtonSize, channelButtonApply) = continueChannelLayout(boundingWidth - layoutConstants.text.bubbleInsets.right * 2.0)
let statusSizeAndApply = statusSuggestedWidthAndContinue?.1(boundingWidth - sideInsets)
var layoutSize = CGSize(width: contentWidth, height: 49.0 + prizeTitleLayout.size.height + prizeTextLayout.size.height + participantsTitleLayout.size.height + participantsTextLayout.size.height + dateTitleLayout.size.height + dateTextLayout.size.height + buttonSize.height + buttonSpacing + 100.0)
var layoutSize = CGSize(width: contentWidth, height: 49.0 + prizeTitleLayout.size.height + prizeTextLayout.size.height + participantsTitleLayout.size.height + participantsTextLayout.size.height + dateTitleLayout.size.height + dateTextLayout.size.height + buttonSize.height + buttonSpacing + 120.0)
layoutSize.height += channelButtonSize.height
if let statusSizeAndApply = statusSizeAndApply {
layoutSize.height += statusSizeAndApply.0.height - 4.0
}
@ -344,6 +410,8 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.updateVisibility()
let _ = badgeTextApply()
let _ = prizeTitleApply()
let _ = prizeTextApply()
@ -353,6 +421,7 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
let _ = dateTitleApply()
let _ = dateTextApply()
let _ = channelButtonApply()
let _ = buttonApply()
let smallSpacing: CGFloat = 2.0
@ -361,10 +430,18 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
var originY: CGFloat = 0.0
let iconSize = CGSize(width: 140.0, height: 140.0)
strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - iconSize.width) / 2.0), y: originY - 50.0), size: iconSize)
strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - iconSize.width) / 2.0), y: originY - 40.0), size: iconSize)
strongSelf.animationNode.updateLayout(size: iconSize)
originY += 95.0
let badgeTextFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - badgeTextLayout.size.width) / 2.0) + 1.0, y: originY + 88.0), size: badgeTextLayout.size)
strongSelf.badgeTextNode.frame = badgeTextFrame
strongSelf.badgeBackgroundNode.frame = badgeTextFrame.insetBy(dx: -6.0, dy: -5.0).offsetBy(dx: -1.0, dy: 0.0)
if let updatedBadgeImage {
strongSelf.badgeBackgroundNode.image = updatedBadgeImage
}
originY += 112.0
strongSelf.prizeTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - prizeTitleLayout.size.width) / 2.0), y: originY), size: prizeTitleLayout.size)
originY += prizeTitleLayout.size.height + smallSpacing
strongSelf.prizeTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - prizeTextLayout.size.width) / 2.0), y: originY), size: prizeTextLayout.size)
@ -373,7 +450,10 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.participantsTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - participantsTitleLayout.size.width) / 2.0), y: originY), size: participantsTitleLayout.size)
originY += participantsTitleLayout.size.height + smallSpacing
strongSelf.participantsTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - participantsTextLayout.size.width) / 2.0), y: originY), size: participantsTextLayout.size)
originY += participantsTextLayout.size.height + largeSpacing
originY += participantsTextLayout.size.height + smallSpacing * 2.0 + 3.0
strongSelf.channelButton.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - channelButtonSize.width) / 2.0), y: originY), size: channelButtonSize)
originY += channelButtonSize.height + largeSpacing
strongSelf.dateTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - dateTitleLayout.size.width) / 2.0), y: originY), size: dateTitleLayout.size)
originY += dateTitleLayout.size.height + smallSpacing
@ -416,47 +496,6 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
}
private func updateVisibility() {
// guard let item = self.item else {
// return
// }
//
// let isPlaying = self.visibilityStatus == true
// if self.isPlaying != isPlaying {
// self.isPlaying = isPlaying
// self.animationNode.visibility = isPlaying
// }
//
// if isPlaying && self.setupTimestamp == nil {
// self.setupTimestamp = CACurrentMediaTime()
// }
//
// if isPlaying {
// var alreadySeen = true
//
// if item.message.flags.contains(.Incoming) {
// if let unreadRange = item.controllerInteraction.unreadMessageRange[UnreadMessageRangeKey(peerId: item.message.id.peerId, namespace: item.message.id.namespace)] {
// if unreadRange.contains(item.message.id.id) {
// alreadySeen = false
// }
// }
// } else {
// if item.controllerInteraction.playNextOutgoingGift && !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) {
// alreadySeen = false
// }
// }
//
// if !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) {
// item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id)
// self.animationNode.playOnce()
// }
//
// if !alreadySeen && self.animationNode.isPlaying {
// item.controllerInteraction.playNextOutgoingGift = false
// Queue.mainQueue().after(1.0) {
// item.controllerInteraction.animateDiceSuccess(false, true)
// }
// }
// }
}
private var absoluteRect: (CGRect, CGSize)?
@ -516,3 +555,102 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
return super.hitTest(point, with: event)
}
}
private final class PeerButtonNode: HighlightTrackingButtonNode {
private let backgroundNode: ASImageNode
private let textNode: TextNode
private let avatarNode: AvatarNode
var currentBackgroundColor: UIColor?
var pressed: (() -> Void)?
init() {
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
self.backgroundNode = ASImageNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.displaysAsynchronously = false
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 14.0))
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.textNode)
self.addSubnode(self.avatarNode)
self.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.layer.removeAnimation(forKey: "opacity")
strongSelf.alpha = 0.4
} else {
strongSelf.alpha = 1.0
strongSelf.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
}
@objc func buttonPressed() {
self.pressed?()
}
static func asyncLayout(_ current: PeerButtonNode?) -> (_ context: AccountContext, _ width: CGFloat, _ peer: EnginePeer?, _ titleColor: UIColor, _ backgroundColor: UIColor) -> (CGFloat, (CGFloat) -> (CGSize, () -> PeerButtonNode)) {
let maybeMakeTextLayout = (current?.textNode).flatMap(TextNode.asyncLayout)
return { context, width, peer, titleColor, backgroundColor in
let targetNode: PeerButtonNode
if let current = current {
targetNode = current
} else {
targetNode = PeerButtonNode()
}
let makeTextLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode)
if let maybeMakeTextLayout = maybeMakeTextLayout {
makeTextLayout = maybeMakeTextLayout
} else {
makeTextLayout = TextNode.asyncLayout(targetNode.textNode)
}
let inset: CGFloat = 1.0
let avatarSize = CGSize(width: 22.0, height: 22.0)
let spacing: CGFloat = 5.0
let (textSize, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: peer?.compactDisplayTitle ?? "", font: Font.medium(14.0), textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(1.0, width - avatarSize.width - (spacing + inset) * 2.0), height: CGFloat.greatestFiniteMagnitude), alignment: .left, cutout: nil, insets: UIEdgeInsets()))
let refinedWidth = avatarSize.width + textSize.size.width + (spacing + inset) * 2.0
return (refinedWidth, { _ in
return (CGSize(width: refinedWidth, height: 24.0), {
let _ = textApply()
let backgroundFrame = CGRect(origin: .zero, size: CGSize(width: refinedWidth, height: 24.0))
let textFrame = CGRect(origin: CGPoint(x: inset + avatarSize.width + spacing, y: floorToScreenPixels((backgroundFrame.height - textSize.size.height) / 2.0)), size: textSize.size)
targetNode.backgroundNode.frame = backgroundFrame
if targetNode.currentBackgroundColor != backgroundColor {
targetNode.currentBackgroundColor = backgroundColor
targetNode.backgroundNode.image = generateStretchableFilledCircleImage(radius: 12.0, color: backgroundColor, backgroundColor: nil)
}
targetNode.avatarNode.setPeer(
context: context,
theme: context.sharedContext.currentPresentationData.with({ $0 }).theme,
peer: peer,
synchronousLoad: false
)
targetNode.avatarNode.frame = CGRect(origin: CGPoint(x: inset, y: inset), size: avatarSize)
targetNode.textNode.frame = textFrame
return targetNode
})
})
}
}
}

@ -0,0 +1,246 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TextFormat
import AccountContext
import AlertUI
import PresentationDataUtils
import Markdown
private final class GiveawayInfoAlertContentNode: AlertContentNode {
private let title: String
private let text: String
private let warning: String?
private let titleNode: ASTextNode
private let textNode: ASTextNode
fileprivate let warningBackgroundNode: ASImageNode
fileprivate let warningTextNode: ImmediateTextNode
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private var validLayout: CGSize?
public var theme: PresentationTheme
public override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
public init(theme: AlertControllerTheme, ptheme: PresentationTheme, title: String, text: String, warning: String?, actions: [TextAlertAction]) {
self.theme = ptheme
self.title = title
self.text = text
self.warning = warning
self.titleNode = ASTextNode()
self.titleNode.maximumNumberOfLines = 0
self.textNode = ASTextNode()
self.textNode.maximumNumberOfLines = 0
self.warningBackgroundNode = ASImageNode()
self.warningBackgroundNode.displaysAsynchronously = false
self.warningTextNode = ImmediateTextNode()
self.warningTextNode.maximumNumberOfLines = 0
self.warningTextNode.lineSpacing = 0.1
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.warningBackgroundNode)
self.addSubnode(self.warningTextNode)
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.updateTheme(theme)
}
public override func updateTheme(_ theme: AlertControllerTheme) {
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor)
let bold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor)
let attributedText = parseMarkdownIntoAttributedString(self.text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center)
self.textNode.attributedText = attributedText
self.warningTextNode.attributedText = NSAttributedString(string: self.warning ?? "", font: Font.semibold(13.0), textColor: theme.destructiveColor, paragraphAlignment: .center)
self.warningBackgroundNode.image = generateStretchableFilledCircleImage(radius: 5.0, color: theme.destructiveColor.withAlphaComponent(0.1))
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
public override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width, 270.0)
let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
let titleSize = self.titleNode.measure(measureSize)
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
origin.y += titleSize.height + 4.0
let textSize = self.textNode.measure(measureSize)
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
origin.y += textSize.height + 6.0
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
effectiveActionLayout = .vertical
}
switch effectiveActionLayout {
case .horizontal:
minActionsWidth += actionTitleSize.width + actionTitleInsets
case .vertical:
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
}
if "".isEmpty {
effectiveActionLayout = .vertical
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
var contentWidth = max(titleSize.width, minActionsWidth)
contentWidth = max(contentWidth, 234.0)
var actionsHeight: CGFloat = 0.0
switch effectiveActionLayout {
case .horizontal:
actionsHeight = actionButtonHeight
case .vertical:
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
}
let resultWidth = contentWidth + insets.left + insets.right
var warningHeight: CGFloat = 0.0
if let _ = self.warning {
let warningSize = self.warningTextNode.updateLayout(measureSize)
let warningFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - warningSize.width) / 2.0), y: origin.y + 20.0), size: warningSize)
transition.updateFrame(node: self.warningTextNode, frame: warningFrame)
transition.updateFrame(node: self.warningBackgroundNode, frame: warningFrame.insetBy(dx: -8.0, dy: -8.0))
warningHeight += warningSize.height + 26.0
}
let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + 8.0 + actionsHeight + warningHeight + insets.top + insets.bottom)
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
var actionOffset: CGFloat = 0.0
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
switch effectiveActionLayout {
case .horizontal:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
case .vertical:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
switch effectiveActionLayout {
case .horizontal:
if nodeIndex == self.actionNodes.count - 1 {
currentActionWidth = resultSize.width - actionOffset
} else {
currentActionWidth = actionWidth
}
case .vertical:
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
switch effectiveActionLayout {
case .horizontal:
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += currentActionWidth
case .vertical:
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
return resultSize
}
}
func giveawayInfoAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, title: String, text: String, warning: String?, actions: [TextAlertAction]) -> AlertController {
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
let contentNode = GiveawayInfoAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, title: title, text: text, warning: warning, actions: actions)
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller] presentationData in
controller?.theme = AlertControllerTheme(presentationData: presentationData)
})
controller.dismissed = { _ in
presentationDataDisposable.dispose()
}
return controller
}

@ -961,11 +961,50 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
|> deliverOnMainQueue).startStandalone(next: { giftCode in
if let giftCode {
var dismissImpl: (() -> Void)?
let controller = PremiumGiftCodeScreen(context: context, giftCode: giftCode, forceDark: forceDark, action: {
dismissImpl?()
let controller = PremiumGiftCodeScreen(
context: context,
giftCode: giftCode,
forceDark: forceDark,
action: {
dismissImpl?()
let _ = context.engine.payments.applyPremiumGiftCode(slug: slug).startStandalone()
})
let _ = context.engine.payments.applyPremiumGiftCode(slug: slug).startStandalone()
},
openPeer: { peer in
openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil))
},
openMessage: { messageId in
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId))
|> deliverOnMainQueue).startStandalone(next: { peer in
guard let peer else {
return
}
openPeer(peer, .chat(textInputState: nil, subject: .message(id: .id(messageId), highlight: true, timecode: nil), peekData: nil))
})
},
shareLink: { link in
let messages: [EnqueueMessage] = [.message(text: link, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]
let peerSelectionController = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled], multipleSelection: false, selectForumThreads: true))
peerSelectionController.peerSelected = { [weak peerSelectionController] peer, threadId in
if let _ = peerSelectionController {
Queue.mainQueue().after(0.88) {
HapticFeedback().success()
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
(navigationController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: true, text: peer.id == context.account.peerId ? presentationData.strings.GiftLink_LinkSharedToSavedMessages : presentationData.strings.GiftLink_LinkSharedToChat(peer.compactDisplayTitle).string), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .window(.root))
let _ = (enqueueMessages(account: context.account, peerId: peer.id, messages: messages)
|> deliverOnMainQueue).startStandalone()
if let peerSelectionController = peerSelectionController {
peerSelectionController.dismiss()
}
}
}
navigationController?.pushViewController(peerSelectionController)
}
)
dismissImpl = { [weak controller] in
controller?.dismiss()
}

@ -42,6 +42,7 @@ import PeerInfoStoryGridScreen
import TelegramAccountAuxiliaryMethods
import PeerSelectionController
import LegacyMessageInputPanel
import StatisticsUI
private final class AccountUserInterfaceInUseContext {
let subscribers = Bag<(Bool) -> Void>()
@ -1807,6 +1808,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
public func makeInstalledStickerPacksController(context: AccountContext, mode: InstalledStickerPacksControllerMode, forceTheme: PresentationTheme?) -> ViewController {
return installedStickerPacksController(context: context, mode: mode, forceTheme: forceTheme)
}
public func makeChannelStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peerId: EnginePeer.Id, boosts: Bool, boostStatus: ChannelBoostStatus?, statsDatacenterId: Int32) -> ViewController {
return channelStatsController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, section: boosts ? .boosts : .stats, boostStatus: nil, statsDatacenterId: statsDatacenterId)
}
}
private func peerInfoControllerImpl(context: AccountContext, updatedPresentationData: (PresentationData, Signal<PresentationData, NoError>)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, requestsContext: PeerInvitationImportersContext? = nil) -> ViewController? {

@ -90,9 +90,11 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: EnginePeer.Id?, n
controller.present(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in
openResolvedPeerImpl(peer, .chat(textInputState: nil, subject: nil, peekData: peekData))
}, parentNavigationController: controller.navigationController as? NavigationController), in: .window(.root))
case .boost:
case .boost, .chatFolder:
if let navigationController = controller.navigationController as? NavigationController {
openResolvedUrlImpl(result, context: context, urlContext: peerId.flatMap { .chat(peerId: $0, updatedPresentationData: nil) } ?? .generic, navigationController: navigationController, forceExternal: false, openPeer: { _, _ in }, sendFile: nil, sendSticker: nil, joinVoiceChat: nil, present: { c, a in }, dismissInput: {}, contentContext: nil)
openResolvedUrlImpl(result, context: context, urlContext: peerId.flatMap { .chat(peerId: $0, updatedPresentationData: nil) } ?? .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigateToPeer in
openResolvedPeerImpl(peer, navigateToPeer)
}, sendFile: nil, sendSticker: nil, joinVoiceChat: nil, present: { c, a in }, dismissInput: {}, contentContext: nil)
}
default:
break

@ -338,7 +338,7 @@ func presentLegacyWebSearchGallery(context: AccountContext, peer: EnginePeer?, t
let (items, focusItem) = galleryItems(account: context.account, results: results, current: current, selectionContext: selectionContext, editingContext: editingContext)
let model = TGMediaPickerGalleryModel(context: legacyController.context, items: items, focus: focusItem, selectionContext: selectionContext, editingContext: editingContext, hasCaptions: false, allowCaptionEntities: true, hasTimer: false, onlyCrop: false, inhibitDocumentCaptions: false, hasSelectionPanel: false, hasCamera: false, recipientName: recipientName)!
let model = TGMediaPickerGalleryModel(context: legacyController.context, items: items, focus: focusItem, selectionContext: selectionContext, editingContext: editingContext, hasCaptions: false, allowCaptionEntities: true, hasTimer: false, onlyCrop: false, inhibitDocumentCaptions: false, hasSelectionPanel: false, hasCamera: false, recipientName: recipientName, isScheduledMessages: false)!
model.stickersContext = paintStickersContext
controller.model = model
model.controller = controller

@ -225,12 +225,9 @@ public struct WebAppParameters {
}
public func generateWebAppThemeParams(_ presentationTheme: PresentationTheme) -> [String: Any] {
var backgroundColor = presentationTheme.list.plainBackgroundColor.rgb
var secondaryBackgroundColor = presentationTheme.list.blocksBackgroundColor.rgb
if presentationTheme.list.blocksBackgroundColor.rgb == presentationTheme.list.plainBackgroundColor.rgb {
backgroundColor = presentationTheme.list.modalPlainBackgroundColor.rgb
secondaryBackgroundColor = presentationTheme.list.plainBackgroundColor.rgb
}
let backgroundColor = presentationTheme.list.plainBackgroundColor.rgb
let secondaryBackgroundColor = presentationTheme.list.blocksBackgroundColor.rgb
return [
"bg_color": Int32(bitPattern: backgroundColor),
"secondary_bg_color": Int32(bitPattern: secondaryBackgroundColor),
@ -238,7 +235,13 @@ public func generateWebAppThemeParams(_ presentationTheme: PresentationTheme) ->
"hint_color": Int32(bitPattern: presentationTheme.list.itemSecondaryTextColor.rgb),
"link_color": Int32(bitPattern: presentationTheme.list.itemAccentColor.rgb),
"button_color": Int32(bitPattern: presentationTheme.list.itemCheckColors.fillColor.rgb),
"button_text_color": Int32(bitPattern: presentationTheme.list.itemCheckColors.foregroundColor.rgb)
"button_text_color": Int32(bitPattern: presentationTheme.list.itemCheckColors.foregroundColor.rgb),
"header_bg_color": Int32(bitPattern: presentationTheme.rootController.navigationBar.opaqueBackgroundColor.rgb),
"accent_text_color": Int32(bitPattern: presentationTheme.list.itemAccentColor.rgb),
"section_bg_color": Int32(bitPattern: presentationTheme.list.itemBlocksBackgroundColor.rgb),
"section_header_text_color": Int32(bitPattern: presentationTheme.list.freeTextColor.rgb),
"subtitle_text_color": Int32(bitPattern: presentationTheme.list.itemSecondaryTextColor.rgb),
"destructive_text_color": Int32(bitPattern: presentationTheme.list.itemDestructiveColor.rgb)
]
}
@ -295,13 +298,13 @@ public final class WebAppController: ViewController, AttachmentContainable {
self.topOverscrollNode = ASDisplayNode()
super.init()
if self.presentationData.theme.list.plainBackgroundColor.rgb == 0x000000 {
self.backgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor
} else {
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
}
let webView = WebAppWebView()
webView.alpha = 0.0
webView.navigationDelegate = self
@ -768,7 +771,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
case "web_app_open_tg_link":
if let json = json, let path = json["path_full"] as? String {
controller.openUrl("https://t.me\(path)", false, { [weak controller] in
controller?.dismiss()
let _ = controller
// controller?.dismiss()
})
}
case "web_app_open_invoice":
@ -990,6 +994,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
self.invokeCustomMethod(requestId: requestId, method: method, params: paramsString ?? "{}")
}
case "web_app_setup_settings_button":
if let json = json, let isVisible = json["is_visible"] as? Bool {
self.controller?.hasSettings = isVisible
}
default:
break
}
@ -1009,12 +1017,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
let color: UIColor?
var primaryTextColor: UIColor?
var secondaryTextColor: UIColor?
var backgroundColor = self.presentationData.theme.list.plainBackgroundColor
var secondaryBackgroundColor = self.presentationData.theme.list.blocksBackgroundColor
if self.presentationData.theme.list.blocksBackgroundColor.rgb == self.presentationData.theme.list.plainBackgroundColor.rgb {
backgroundColor = self.presentationData.theme.list.modalPlainBackgroundColor
secondaryBackgroundColor = self.presentationData.theme.list.plainBackgroundColor
}
let backgroundColor = self.presentationData.theme.list.plainBackgroundColor
let secondaryBackgroundColor = self.presentationData.theme.list.blocksBackgroundColor
if let headerColor = self.headerColor {
color = headerColor
let textColor = headerColor.lightness > 0.5 ? UIColor(rgb: 0x000000) : UIColor(rgb: 0xffffff)
@ -1342,6 +1346,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
fileprivate let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
private var presentationDataDisposable: Disposable?
private var hasSettings = false
public var openUrl: (String, Bool, @escaping () -> Void) -> Void = { _, _, _ in }
public var getNavigationController: () -> NavigationController? = { return nil }
public var completion: () -> Void = {}
@ -1363,7 +1369,11 @@ public final class WebAppController: ViewController, AttachmentContainable {
self.threadId = threadId
self.updatedPresentationData = updatedPresentationData
self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
var presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
let updatedTheme = presentationData.theme.withModalBlocksBackground()
presentationData = presentationData.withUpdated(theme: updatedTheme)
self.presentationData = presentationData
self.cancelButtonNode = WebAppCancelButtonNode(theme: self.presentationData.theme, strings: self.presentationData.strings)
@ -1399,6 +1409,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData)
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
let updatedTheme = presentationData.theme.withModalBlocksBackground()
let presentationData = presentationData.withUpdated(theme: updatedTheme)
strongSelf.presentationData = presentationData
strongSelf.updateNavigationBarTheme(transition: .immediate)
@ -1469,11 +1481,11 @@ public final class WebAppController: ViewController, AttachmentContainable {
let peerId = self.peerId
let botId = self.botId
let url = self.url
let forceHasSettings = self.forceHasSettings
let source = self.source
let hasSettings = self.hasSettings
let items = context.engine.messages.attachMenuBots()
|> take(1)
|> map { [weak self] attachMenuBots -> ContextController.Items in
@ -1481,16 +1493,15 @@ public final class WebAppController: ViewController, AttachmentContainable {
let attachMenuBot = attachMenuBots.first(where: { $0.peer.id == botId && !$0.flags.contains(.notActivated) })
let hasSettings: Bool
if url == nil {
if forceHasSettings {
hasSettings = true
} else {
hasSettings = attachMenuBot?.flags.contains(.hasSettings) == true
}
} else {
hasSettings = forceHasSettings
}
// if url == nil {
// if forceHasSettings {
// hasSettings = true
// } else {
// hasSettings = attachMenuBot?.flags.contains(.hasSettings) == true
// }
// } else {
// hasSettings = forceHasSettings
// }
if hasSettings {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.WebApp_Settings, icon: { theme in

@ -88,9 +88,9 @@ final class WebAppWebView: WKWebView {
return point.x > 30.0
}
self.allowsBackForwardNavigationGestures = false
// if #available(iOS 16.4, *) {
// self.isInspectable = true
// }
if #available(iOS 16.4, *) {
self.isInspectable = true
}
handleScriptMessageImpl = { [weak self] message in
if let strongSelf = self {