Various improvements

This commit is contained in:
Ilya Laktyushin 2022-05-25 16:32:10 +04:00
parent ce03f9dac3
commit 21f06b62fc
24 changed files with 1894 additions and 622 deletions

View File

@ -7541,9 +7541,6 @@ Sorry for the inconvenience.";
"Premium.Stickers.Description" = "Unlock this sticker and more by subscribing to Telegram Premium.";
"Premium.Stickers.Proceed" = "Unlock Premium Stickers";
"Premium.Reactions.Description" = "Unlock additional reactions by subscribing to Telegram Premium.";
"Premium.Reactions.Proceed" = "Unlock Additional Reactions";
"AccessDenied.LocationPreciseDenied" = "To share your specific location in this chat, please go to Settings > Privacy > Location Services > Telegram and set Precise Location to On.";
"Chat.MultipleTypingPair" = "%@ and %@";
@ -7557,11 +7554,12 @@ Sorry for the inconvenience.";
"OldChannels.LeaveCommunities_1" = "Leave %@ Community";
"OldChannels.LeaveCommunities_any" = "Leave %@ Communities";
"Premium.FileTooLarge" = "File Too Large";
"Premium.LimitReached" = "Limit Reached";
"Premium.IncreaseLimit" = "Increase Limit";
"Premium.MaxFoldersCountText" = "You have reached the limit of **%@** folders. You can double the limit to **%@** folders by subscribing to **Telegram Premium**.";
"Premium.MaxChatsInFolderCountText" = "Sorry, you can't add more than **%@** chats to a folder. You can increase this limit to **%@** by upgrading to **Telegram Premium**.";
"Premium.MaxFoldersCountText" = "You have reached the limit of **%1$@** folders. You can double the limit to **%2$@** folders by subscribing to **Telegram Premium**.";
"Premium.MaxChatsInFolderCountText" = "Sorry, you can't add more than **%1$@** chats to a folder. You can increase this limit to **%2$@** by upgrading to **Telegram Premium**.";
"Premium.MaxFileSizeText" = "Double this limit to %@ per file by subscribing to **Telegram Premium**.";
"Premium.MaxPinsText" = "Sorry, you can't pin more than **%1$@** chats to the top. Unpin some of the currently pinned ones or subscribe to **Telegram Premium** to double the limit to **%2$@** chats.";
"Premium.MaxFavedStickersTitle" = "The Limit of %@ Stickers Reached";
@ -7573,6 +7571,12 @@ Sorry for the inconvenience.";
"Premium.Title" = "Telegram Premium";
"Premium.Description" = "Go **beyond the limits**, get **exclusive features** and support us by subscribing to **Telegram Premium**.";
"Premium.PersonalTitle" = "%@ is a subscriber of Telegram Premium";
"Premium.PersonalDescription" = "Owners of **Telegram Premium** accounts have exclusive access to multiple additional features.";
"Premium.SubscribedTitle" = "You are all set!";
"Premium.SubscribedDescription" = "Thank you for subsribing to **Telegram Premium**. Here's what is now unlocked.";
"Premium.DoubledLimits" = "Doubled Limits";
"Premium.DoubledLimitsInfo" = "Up to 1000 channels, 20 folders, 10 pins, 20 public links, 4 accounts and more.";
@ -7610,6 +7614,8 @@ Sorry for the inconvenience.";
"Premium.Terms" = "By purchasing a Premium subscription, you agree to our [Terms of Service](terms) and [Privacy Policy](privacy).";
"Premium.MoreAboutPremium" = "More About Premium";
"Conversation.CopyProtectionSavingDisabledSecret" = "Saving is restricted";
"Conversation.CopyProtectionForwardingDisabledSecret" = "Forwards are restricted";
@ -7622,3 +7628,5 @@ Sorry for the inconvenience.";
"Conversation.PremiumUploadFileTooLarge" = "File could not be sent, because it is larger than 4 GB.\n\nYou can send as many files as you like, but each must be smaller than 4 GB.";
"SponsoredMessageMenu.Hide" = "Hide";
"ChatListFolder.MaxChatsInFolder" = "Sorry, you can't add more than %d chats to a folder.";

View File

@ -244,6 +244,8 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
private var emojiViewProvider: ((String) -> UIView)?
private var maxCaptionLength: Int32?
public init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, isCaption: Bool = false, isAttachment: Bool = false, presentController: @escaping (ViewController) -> Void) {
self.context = context
self.presentationInterfaceState = presentationInterfaceState
@ -323,6 +325,23 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
}
self.updateSendButtonEnabled(isCaption || isAttachment, animated: false)
if self.isCaption || self.isAttachment {
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|> mapToSignal { peer -> Signal<Int32, NoError> in
if let peer = peer {
return self.context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.UserLimits.init(isPremium: peer.isPremium))
|> map { limits in
return limits.maxCaptionLengthCount
}
} else {
return .complete()
}
}
|> deliverOnMainQueue).start(next: { [weak self] maxCaptionLength in
self?.maxCaptionLength = maxCaptionLength
})
}
}
public var sendPressed: ((NSAttributedString?) -> Void)?
@ -931,8 +950,8 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
private func updateCounterTextNode(transition: ContainedViewLayoutTransition) {
let inputTextMaxLength: Int32?
if self.isCaption || self.isAttachment {
inputTextMaxLength = self.context.currentLimitsConfiguration.with { $0 }.maxMediaCaptionLength
if let maxCaptionLength = self.maxCaptionLength {
inputTextMaxLength = maxCaptionLength
} else {
inputTextMaxLength = nil
}
@ -1301,8 +1320,8 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
@objc func sendButtonPressed() {
let inputTextMaxLength: Int32?
if self.isCaption || self.isAttachment {
inputTextMaxLength = self.context.currentLimitsConfiguration.with { $0 }.maxMediaCaptionLength
if let maxCaptionLength = self.maxCaptionLength {
inputTextMaxLength = maxCaptionLength
} else {
inputTextMaxLength = nil
}

View File

@ -1461,7 +1461,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
tabContextGesture(id, sourceNode, gesture, true)
}
self.ready.set(self.chatListDisplayNode.containerNode.ready)
if case .group = self.groupId {
self.ready.set(self.chatListDisplayNode.containerNode.ready)
} else {
self.ready.set(.never())
}
self.displayNodeDidLoad()
}
@ -2060,10 +2064,19 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
strongSelf.chatListDisplayNode.containerNode.updateAvailableFilters(availableFilters, limit: filtersLimit)
if !strongSelf.initializedFilters && selectedEntryId != strongSelf.chatListDisplayNode.containerNode.currentItemFilter {
strongSelf.chatListDisplayNode.containerNode.switchToFilter(id: selectedEntryId, animated: false, completion: nil)
if !strongSelf.initializedFilters {
if selectedEntryId != strongSelf.chatListDisplayNode.containerNode.currentItemFilter {
strongSelf.chatListDisplayNode.containerNode.switchToFilter(id: selectedEntryId, animated: false, completion: { [weak self] in
if let strongSelf = self {
strongSelf.ready.set(strongSelf.chatListDisplayNode.containerNode.currentItemNode.ready)
}
})
} else {
strongSelf.ready.set(strongSelf.chatListDisplayNode.containerNode.currentItemNode.ready)
}
strongSelf.initializedFilters = true
}
strongSelf.initializedFilters = true
let isEmpty = resolvedItems.count <= 1 || displayTabsAtBottom
@ -3236,15 +3249,23 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let _ = (combineLatest(queue: .mainQueue(),
self.context.engine.peers.currentChatListFilters(),
chatListFilterItems(context: self.context)
|> take(1)
|> take(1),
context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId),
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false),
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true)
)
)
|> deliverOnMainQueue).start(next: { [weak self] presetList, filterItemsAndTotalCount in
|> deliverOnMainQueue).start(next: { [weak self] presetList, filterItemsAndTotalCount, result in
guard let strongSelf = self else {
return
}
let _ = strongSelf.context.engine.peers.markChatListFeaturedFiltersAsSeen().start()
let (accountPeer, limits, _) = result
let isPremium = accountPeer?.isPremium ?? false
let _ = strongSelf.context.engine.peers.markChatListFeaturedFiltersAsSeen().start()
let (_, filterItems) = filterItemsAndTotalCount
var items: [ContextMenuItem] = []
@ -3272,11 +3293,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
if !presetList.isEmpty {
items.append(.separator)
if presetList.count > 1 {
items.append(.separator)
}
var filterCount = 0
for case let .filter(id, title, _, data) in presetList {
let filterType = chatListFilterType(data)
var badge: ContextMenuActionBadge?
var isDisabled = false
if !isPremium && filterCount >= limits.maxFoldersCount {
isDisabled = true
}
for item in filterItems {
if item.0.id == id && item.1 != 0 {
badge = ContextMenuActionBadge(value: "\(item.1)", color: item.2 ? .accent : .inactive)
@ -3284,23 +3312,27 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
items.append(.action(ContextMenuActionItem(text: title, badge: badge, icon: { theme in
let imageName: String
switch filterType {
case .generic:
imageName = "Chat/Context Menu/List"
case .unmuted:
imageName = "Chat/Context Menu/Unmute"
case .unread:
imageName = "Chat/Context Menu/MarkAsUnread"
case .channels:
imageName = "Chat/Context Menu/Channels"
case .groups:
imageName = "Chat/Context Menu/Groups"
case .bots:
imageName = "Chat/Context Menu/Bots"
case .contacts:
imageName = "Chat/Context Menu/User"
case .nonContacts:
imageName = "Chat/Context Menu/UnknownUser"
if isDisabled {
imageName = "Chat/Context Menu/Lock"
} else {
switch filterType {
case .generic:
imageName = "Chat/Context Menu/List"
case .unmuted:
imageName = "Chat/Context Menu/Unmute"
case .unread:
imageName = "Chat/Context Menu/MarkAsUnread"
case .channels:
imageName = "Chat/Context Menu/Channels"
case .groups:
imageName = "Chat/Context Menu/Groups"
case .bots:
imageName = "Chat/Context Menu/Bots"
case .contacts:
imageName = "Chat/Context Menu/User"
case .nonContacts:
imageName = "Chat/Context Menu/UnknownUser"
}
}
return generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.contextMenu.primaryColor)
}, action: { _, f in
@ -3308,8 +3340,23 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let strongSelf = self else {
return
}
strongSelf.selectTab(id: .filter(id))
if isDisabled {
let context = strongSelf.context
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumLimitScreen(context: context, subject: .folders, count: strongSelf.tabContainerNode.filtersCount, action: {
let controller = PremiumIntroScreen(context: context, source: .folders)
replaceImpl?(controller)
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
strongSelf.push(controller)
} else {
strongSelf.selectTab(id: .filter(id))
}
})))
filterCount += 1
}
}

View File

@ -878,20 +878,28 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
let disposable = MetaDisposable()
self.pendingItemNode = (id, itemNode, disposable)
disposable.set((combineLatest(itemNode.listNode.ready, self.validLayoutReady)
|> filter { $0 && $1 }
disposable.set((itemNode.listNode.ready
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self, weak itemNode] _ in
guard let strongSelf = self, let itemNode = itemNode, itemNode === strongSelf.pendingItemNode?.1 else {
return
}
guard let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, isReorderingFilters, isEditing) = strongSelf.validLayout else {
return
}
strongSelf.pendingItemNode = nil
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.35, curve: .spring) : .immediate
guard let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, isReorderingFilters, isEditing) = strongSelf.validLayout else {
strongSelf.itemNodes[id] = itemNode
strongSelf.addSubnode(itemNode)
strongSelf.selectedId = id
strongSelf.applyItemNodeAsCurrent(id: id, itemNode: itemNode)
strongSelf.currentItemFilterUpdated?(strongSelf.currentItemFilter, strongSelf.transitionFraction, .immediate, false)
completion?()
return
}
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.35, curve: .spring) : .immediate
if let previousIndex = strongSelf.availableFilters.firstIndex(where: { $0.id == strongSelf.selectedId }), let index = strongSelf.availableFilters.firstIndex(where: { $0.id == id }) {
let previousId = strongSelf.selectedId
let offsetDirection: CGFloat = index < previousIndex ? 1.0 : -1.0

View File

@ -601,14 +601,22 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .chatSelection(title: presentationData.strings.ChatListFolder_IncludeChatsTitle, selectedChats: Set(filterData.includePeers.peers), additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories), chatListFilters: allFilters), options: [], filters: [], alwaysEnabled: true, limit: 100))
controller.navigationPresentation = .modal
let _ = (controller.result
|> take(1)
|> deliverOnMainQueue).start(next: { [weak controller] result in
let _ = combineLatest(
queue: Queue.mainQueue(),
controller.result |> take(1),
context.engine.data.get(
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false),
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true)
)
)
.start(next: { [weak controller] result, data in
guard case let .result(peerIds, additionalCategoryIds) = result else {
controller?.dismiss()
return
}
let (limits, premiumLimits) = data
var includePeers: [PeerId] = []
for peerId in peerIds {
switch peerId {
@ -620,6 +628,26 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
}
includePeers.sort()
if includePeers.count > limits.maxFolderChatsCount {
if includePeers.count > premiumLimits.maxFolderChatsCount {
let alertController = textAlertController(context: context, title: nil, text: presentationData.strings.ChatListFolder_MaxChatsInFolder(Int(premiumLimits.maxFolderChatsCount)).string, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
controller?.present(alertController, in: .window(.root))
return
}
var replaceImpl: ((ViewController) -> Void)?
let limitController = PremiumLimitScreen(context: context, subject: .chatsInFolder, count: Int32(includePeers.count), action: {
let introController = PremiumIntroScreen(context: context, source: .chatsPerFolder)
replaceImpl?(introController)
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
controller?.push(limitController)
return
}
var categories: ChatListFilterPeerCategories = []
for id in additionalCategoryIds {
if let index = categoryMapping.firstIndex(where: { $0.1.rawValue == id }) {

View File

@ -307,7 +307,7 @@ private final class ItemNode: ASDisplayNode {
self.badgeTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((badgeBackgroundFrame.width - badgeSize.width) / 2.0), y: floor((badgeBackgroundFrame.height - badgeSize.height) / 2.0)), size: badgeSize)
let width: CGFloat
if self.unreadCount == 0 || self.isReordering || self.isEditing {
if self.unreadCount == 0 || self.isReordering || self.isEditing || self.isDisabled {
if !self.isReordering {
self.badgeContainerNode.alpha = 0.0
}

View File

@ -2,13 +2,20 @@ import Foundation
import UIKit
public final class List<ChildEnvironment: Equatable>: CombinedComponent {
public enum Direction {
case horizontal
case vertical
}
public typealias EnvironmentType = ChildEnvironment
private let items: [AnyComponentWithIdentity<ChildEnvironment>]
private let direction: Direction
private let appear: Transition.Appear
public init(_ items: [AnyComponentWithIdentity<ChildEnvironment>], appear: Transition.Appear = .default()) {
public init(_ items: [AnyComponentWithIdentity<ChildEnvironment>], direction: Direction = .vertical, appear: Transition.Appear = .default()) {
self.items = items
self.direction = direction
self.appear = appear
}
@ -16,6 +23,9 @@ public final class List<ChildEnvironment: Equatable>: CombinedComponent {
if lhs.items != rhs.items {
return false
}
if lhs.direction != rhs.direction {
return false
}
return true
}
@ -35,11 +45,19 @@ public final class List<ChildEnvironment: Equatable>: CombinedComponent {
var nextOrigin: CGFloat = 0.0
for child in updatedChildren {
let position: CGPoint
switch context.component.direction {
case .horizontal:
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)
nextOrigin += child.size.height
}
context.add(child
.position(CGPoint(x: child.size.width / 2.0, y: nextOrigin + child.size.height / 2.0))
.position(position)
.appear(context.component.appear)
)
nextOrigin += child.size.height
}
return context.availableSize

View File

@ -71,9 +71,7 @@ public final class ComponentHostView<EnvironmentType>: UIView {
}
let isEnvironmentUpdated = context.erasedEnvironment.calculateIsUpdated()
if isEnvironmentUpdated {
context.erasedEnvironment._isUpdated = false
}
if !forceUpdate, !isEnvironmentUpdated, let currentComponent = self.currentComponent, let currentContainerSize = self.currentContainerSize, let currentSize = self.currentSize {
if currentContainerSize == containerSize && currentComponent == component {
@ -98,6 +96,10 @@ public final class ComponentHostView<EnvironmentType>: UIView {
transition.setFrame(view: componentView, frame: CGRect(origin: CGPoint(), size: updatedSize))
}
if isEnvironmentUpdated {
context.erasedEnvironment._isUpdated = false
}
self.isUpdating = false
return updatedSize

View File

@ -2437,6 +2437,10 @@ public final class ContextController: ViewController, StandalonePresentableContr
self.dismiss(result: .default, completion: completion)
}
public func dismissWithoutContent() {
self.dismiss(result: .dismissWithoutContent, completion: nil)
}
public func dismissNow() {
self.presentingViewController?.dismiss(animated: false, completion: nil)
self.dismissed?()

View File

@ -0,0 +1,44 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import ComponentFlow
import AccountContext
final class DemoComponent: Component {
public typealias EnvironmentType = DemoPageEnvironment
let context: AccountContext
public init(
context: AccountContext
) {
self.context = context
}
public static func ==(lhs: DemoComponent, rhs: DemoComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
return true
}
public final class View: UIView {
private var component: DemoComponent?
public func update(component: DemoComponent, availableSize: CGSize, transition: Transition) -> CGSize {
self.component = component
return availableSize
}
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<DemoPageEnvironment>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}

View File

@ -0,0 +1,892 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import PresentationDataUtils
import ComponentFlow
import ViewControllerComponent
import SheetComponent
import MultilineTextComponent
import BundleIconComponent
import SolidRoundedButtonComponent
import Markdown
private final class GradientBackgroundComponent: Component {
public let colors: [UIColor]
public init(
colors: [UIColor]
) {
self.colors = colors
}
public static func ==(lhs: GradientBackgroundComponent, rhs: GradientBackgroundComponent) -> Bool {
if lhs.colors != rhs.colors {
return false
}
return true
}
public final class View: UIView {
private let clipLayer: CALayer
private let gradientLayer: CAGradientLayer
private var component: GradientBackgroundComponent?
override init(frame: CGRect) {
self.clipLayer = CALayer()
self.clipLayer.cornerRadius = 10.0
self.clipLayer.masksToBounds = true
self.gradientLayer = CAGradientLayer()
super.init(frame: frame)
self.layer.addSublayer(self.clipLayer)
self.clipLayer.addSublayer(gradientLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: GradientBackgroundComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.clipLayer.frame = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: availableSize.height + 10.0))
self.gradientLayer.frame = CGRect(origin: .zero, size: availableSize)
var locations: [NSNumber] = []
let delta = 1.0 / CGFloat(component.colors.count - 1)
for i in 0 ..< component.colors.count {
locations.append((delta * CGFloat(i)) as NSNumber)
}
self.gradientLayer.locations = locations
self.gradientLayer.colors = component.colors.reversed().map { $0.cgColor }
self.gradientLayer.type = .radial
self.gradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0)
self.gradientLayer.endPoint = CGPoint(x: -2.0, y: 3.0)
self.component = component
self.setupGradientAnimations()
return availableSize
}
private func setupGradientAnimations() {
if let _ = self.gradientLayer.animation(forKey: "movement") {
} else {
let previousValue = self.gradientLayer.endPoint
let value: CGFloat
if previousValue.x < -0.5 {
value = 0.5
} else {
value = 2.0
}
let newValue = CGPoint(x: -value, y: 1.0 + value)
// let secondNewValue = CGPoint(x: 3.0 - value, y: -2.0 + value)
self.gradientLayer.endPoint = newValue
CATransaction.begin()
let animation = CABasicAnimation(keyPath: "endPoint")
animation.duration = 4.5
animation.fromValue = previousValue
animation.toValue = newValue
CATransaction.setCompletionBlock { [weak self] in
self?.setupGradientAnimations()
}
self.gradientLayer.add(animation, forKey: "movement")
// let secondPreviousValue = self.gradientLayer.startPoint
// let secondAnimation = CABasicAnimation(keyPath: "startPoint")
// secondAnimation.duration = 4.5
// secondAnimation.fromValue = secondPreviousValue
// secondAnimation.toValue = secondNewValue
//
// self.gradientLayer.add(secondAnimation, forKey: "movement2")
CATransaction.commit()
}
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public 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)
}
}
final class DemoPageEnvironment: Equatable {
public let isDisplaying: Bool
public init(isDisplaying: Bool) {
self.isDisplaying = isDisplaying
}
public static func ==(lhs: DemoPageEnvironment, rhs: DemoPageEnvironment) -> Bool {
if lhs.isDisplaying != rhs.isDisplaying {
return false
}
return true
}
}
private final class PageComponent<ChildEnvironment: Equatable>: CombinedComponent {
typealias EnvironmentType = ChildEnvironment
private let content: AnyComponent<ChildEnvironment>
private let title: String
private let text: String
private let textColor: UIColor
init(
content: AnyComponent<ChildEnvironment>,
title: String,
text: String,
textColor: UIColor
) {
self.content = content
self.title = title
self.text = text
self.textColor = textColor
}
static func ==(lhs: PageComponent<ChildEnvironment>, rhs: PageComponent<ChildEnvironment>) -> Bool {
if lhs.content != rhs.content {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.textColor != rhs.textColor {
return false
}
return true
}
static var body: Body {
let children = ChildMap(environment: ChildEnvironment.self, keyedBy: AnyHashable.self)
let title = Child(MultilineTextComponent.self)
let text = Child(MultilineTextComponent.self)
return { context in
let availableSize = context.availableSize
let component = context.component
let sideInset: CGFloat = 16.0 //+ environment.safeInsets.left
let textSideInset: CGFloat = 24.0 //+ environment.safeInsets.left
let textColor = component.textColor
let textFont = Font.regular(17.0)
let boldTextFont = Font.semibold(17.0)
let content = children["main"].update(
component: component.content,
environment: {
context.environment[ChildEnvironment.self]
},
availableSize: CGSize(width: availableSize.width, height: availableSize.width),
transition: context.transition
)
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.title,
font: boldTextFont,
textColor: component.textColor,
paragraphAlignment: .center
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: textColor), linkAttribute: { _ in
return nil
})
let text = text.update(
component: MultilineTextComponent(
text: .markdown(text: component.text, attributes: markdownAttributes),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.0
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: content.size.height + 40.0))
)
context.add(text
.position(CGPoint(x: context.availableSize.width / 2.0, y: content.size.height + 80.0))
)
context.add(content
.position(CGPoint(x: content.size.width / 2.0, y: content.size.height / 2.0))
)
return availableSize
}
}
}
private final class DemoPagerComponent: Component {
public final class Item: Equatable {
public let content: AnyComponentWithIdentity<DemoPageEnvironment>
public init(_ content: AnyComponentWithIdentity<DemoPageEnvironment>) {
self.content = content
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs.content != rhs.content {
return false
}
return true
}
}
public let items: [Item]
public let index: Int
public init(
items: [Item],
index: Int = 0
) {
self.items = items
self.index = index
}
public static func ==(lhs: DemoPagerComponent, rhs: DemoPagerComponent) -> Bool {
if lhs.items != rhs.items {
return false
}
return true
}
public final class View: UIView, UIScrollViewDelegate {
private let scrollView: UIScrollView
private var itemViews: [AnyHashable: ComponentHostView<DemoPageEnvironment>] = [:]
private var component: DemoPagerComponent?
override init(frame: CGRect) {
self.scrollView = UIScrollView(frame: frame)
self.scrollView.isPagingEnabled = true
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.alwaysBounceHorizontal = false
self.scrollView.bounces = false
self.scrollView.layer.cornerRadius = 10.0
super.init(frame: frame)
self.scrollView.delegate = self
self.addSubview(self.scrollView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let component = self.component else {
return
}
for item in component.items {
if let itemView = self.itemViews[item.content.id] {
let isDisplaying = itemView.frame.intersects(self.scrollView.bounds)
let environment = DemoPageEnvironment(isDisplaying: isDisplaying)
let _ = itemView.update(
transition: .immediate,
component: item.content.component,
environment: { environment },
containerSize: self.bounds.size
)
}
}
}
func update(component: DemoPagerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
var validIds: [AnyHashable] = []
let firstTime = self.itemViews.isEmpty
let contentSize = CGSize(width: availableSize.width * CGFloat(component.items.count), height: availableSize.height)
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
self.scrollView.frame = CGRect(origin: .zero, size: availableSize)
if firstTime {
self.scrollView.contentOffset = CGPoint(x: CGFloat(component.index) * availableSize.width, y: 0.0)
}
var i = 0
for item in component.items {
validIds.append(item.content.id)
let itemView: ComponentHostView<DemoPageEnvironment>
var itemTransition = transition
if let current = self.itemViews[item.content.id] {
itemView = current
} else {
itemTransition = transition.withAnimation(.none)
itemView = ComponentHostView<DemoPageEnvironment>()
self.itemViews[item.content.id] = itemView
self.scrollView.addSubview(itemView)
}
let isDisplaying = itemView.frame.intersects(self.scrollView.bounds)
let environment = DemoPageEnvironment(isDisplaying: isDisplaying)
let itemSize = itemView.update(
transition: itemTransition,
component: item.content.component,
environment: { environment },
containerSize: availableSize
)
itemView.frame = CGRect(origin: CGPoint(x: availableSize.width * CGFloat(i), y: 0.0), size: itemSize)
i += 1
}
var removeIds: [AnyHashable] = []
for (id, itemView) in self.itemViews {
if !validIds.contains(id) {
removeIds.append(id)
itemView.removeFromSuperview()
}
}
for id in removeIds {
self.itemViews.removeValue(forKey: id)
}
self.component = component
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public 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 DemoSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let subject: PremiumDemoScreen.Subject
let source: PremiumDemoScreen.Source
let action: () -> Void
let dismiss: () -> Void
init(context: AccountContext, subject: PremiumDemoScreen.Subject, source: PremiumDemoScreen.Source, action: @escaping () -> Void, dismiss: @escaping () -> Void) {
self.context = context
self.subject = subject
self.source = source
self.action = action
self.dismiss = dismiss
}
static func ==(lhs: DemoSheetContent, rhs: DemoSheetContent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.subject != rhs.subject {
return false
}
if lhs.source != rhs.source {
return false
}
return true
}
final class State: ComponentState {
private let context: AccountContext
var cachedCloseImage: UIImage?
var reactions: [AvailableReactions.Reaction]?
var stickers: [TelegramMediaFile]?
var reactionsDisposable: Disposable?
var stickersDisposable: Disposable?
init(context: AccountContext) {
self.context = context
super.init()
self.reactionsDisposable = (self.context.engine.stickers.availableReactions()
|> map { reactions -> [AvailableReactions.Reaction] in
if let reactions = reactions {
return reactions.reactions.filter { $0.isPremium }
} else {
return []
}
}
|> deliverOnMainQueue).start(next: { [weak self] reactions in
guard let strongSelf = self else {
return
}
strongSelf.reactions = reactions
strongSelf.updated(transition: .immediate)
})
self.stickersDisposable = (self.context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.PremiumStickers], namespaces: [Namespaces.ItemCollection.CloudDice], aroundIndex: nil, count: 100)
|> map { view -> [TelegramMediaFile] in
var result: [TelegramMediaFile] = []
if let premiumStickers = view.orderedItemListsViews.first {
for i in 0 ..< premiumStickers.items.count {
if let item = premiumStickers.items[i].contents.get(RecentMediaItem.self) {
result.append(item.media)
}
}
}
return result
}
|> deliverOnMainQueue).start(next: { [weak self] stickers in
guard let strongSelf = self else {
return
}
strongSelf.stickers = stickers
strongSelf.updated(transition: .immediate)
})
}
deinit {
self.reactionsDisposable?.dispose()
self.stickersDisposable?.dispose()
}
}
func makeState() -> State {
return State(context: self.context)
}
static var body: Body {
let closeButton = Child(Button.self)
let background = Child(GradientBackgroundComponent.self)
let pager = Child(DemoPagerComponent.self)
let button = Child(SolidRoundedButtonComponent.self)
let dots = Child(BundleIconComponent.self)
return { context in
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let component = context.component
let theme = environment.theme
let strings = environment.strings
let state = context.state
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let background = background.update(
component: GradientBackgroundComponent(colors: [
UIColor(rgb: 0x0077ff),
UIColor(rgb: 0x6b93ff),
UIColor(rgb: 0x8878ff),
UIColor(rgb: 0xe46ace)
]),
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.width),
transition: .immediate
)
context.add(background
.position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0))
)
let closeImage: UIImage
if let image = state.cachedCloseImage {
closeImage = image
} else {
closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.1), foregroundColor: UIColor(rgb: 0xffffff))!
state.cachedCloseImage = closeImage
}
if let reactions = state.reactions, let stickers = state.stickers {
let textColor = theme.actionSheet.primaryTextColor
let items: [DemoPagerComponent.Item] = [
DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.moreUpload,
component: AnyComponent(
PageComponent(
content: AnyComponent(DemoComponent(
context: component.context
)),
title: strings.Premium_UploadSize,
text: strings.Premium_UploadSizeInfo,
textColor: textColor
)
)
)
),
DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.fasterDownload,
component: AnyComponent(
PageComponent(
content: AnyComponent(DemoComponent(
context: component.context
)),
title: strings.Premium_FasterSpeed,
text: strings.Premium_FasterSpeedInfo,
textColor: textColor
)
)
)
),
DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.voiceToText,
component: AnyComponent(
PageComponent(
content: AnyComponent(DemoComponent(
context: component.context
)),
title: strings.Premium_VoiceToText,
text: strings.Premium_VoiceToTextInfo,
textColor: textColor
)
)
)
),
DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.noAds,
component: AnyComponent(
PageComponent(
content: AnyComponent(DemoComponent(
context: component.context
)),
title: strings.Premium_NoAds,
text: strings.Premium_NoAdsInfo,
textColor: textColor
)
)
)
),
DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.uniqueReactions,
component: AnyComponent(
PageComponent(
content: AnyComponent(
ReactionsCarouselComponent(
context: component.context,
theme: environment.theme,
reactions: reactions
)
),
title: strings.Premium_Reactions,
text: strings.Premium_ReactionsInfo,
textColor: textColor
)
)
)
),
DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.premiumStickers,
component: AnyComponent(
PageComponent(
content: AnyComponent(
StickersCarouselComponent(
context: component.context,
stickers: stickers
)
),
title: strings.Premium_Stickers,
text: strings.Premium_StickersInfo,
textColor: textColor
)
)
)
),
DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.advancedChatManagement,
component: AnyComponent(
PageComponent(
content: AnyComponent(DemoComponent(
context: component.context
)),
title: strings.Premium_ChatManagement,
text: strings.Premium_ChatManagementInfo,
textColor: textColor
)
)
)
),
DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.profileBadge,
component: AnyComponent(
PageComponent(
content: AnyComponent(DemoComponent(
context: component.context
)),
title: strings.Premium_Badge,
text: strings.Premium_BadgeInfo,
textColor: textColor
)
)
)
),
DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.animatedUserpics,
component: AnyComponent(
PageComponent(
content: AnyComponent(DemoComponent(
context: component.context
)),
title: strings.Premium_Avatar,
text: strings.Premium_AvatarInfo,
textColor: textColor
)
)
)
)
]
let index = items.firstIndex(where: { (component.subject as AnyHashable) == $0.content.id }) ?? 0
let pager = pager.update(
component: DemoPagerComponent(
items: items,
index: index
),
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.width + 154.0),
transition: .immediate
)
context.add(pager
.position(CGPoint(x: context.availableSize.width / 2.0, y: pager.size.height / 2.0))
)
}
let closeButton = closeButton.update(
component: Button(
content: AnyComponent(Image(image: closeImage)),
action: { [weak component] in
component?.dismiss()
}
),
availableSize: CGSize(width: 30.0, height: 30.0),
transition: .immediate
)
context.add(closeButton
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - closeButton.size.width, y: 28.0))
)
let buttonText: String
switch component.source {
case let .intro(price):
buttonText = strings.Premium_SubscribeFor(price ?? "").string
case .other:
buttonText = strings.Premium_MoreAboutPremium
}
let button = button.update(
component: SolidRoundedButtonComponent(
title: buttonText,
theme: SolidRoundedButtonComponent.Theme(
backgroundColor: .black,
backgroundColors: [
UIColor(rgb: 0x0077ff),
UIColor(rgb: 0x6b93ff),
UIColor(rgb: 0x8878ff),
UIColor(rgb: 0xe46ace)
],
foregroundColor: .white
),
font: .bold,
fontSize: 17.0,
height: 50.0,
cornerRadius: 10.0,
gloss: true,
iconPosition: .right,
action: { [weak component] in
guard let component = component else {
return
}
component.dismiss()
component.action()
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
transition: context.transition
)
let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: context.availableSize.width + 154.0 + 20.0), size: button.size)
context.add(button
.position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY))
)
let dots = dots.update(
component: BundleIconComponent(name: "Components/Dots", tintColor: nil),
availableSize: CGSize(width: 110.0, height: 20.0),
transition: .immediate
)
context.add(dots
.position(CGPoint(x: context.availableSize.width / 2.0, y: buttonFrame.minY - dots.size.height - 18.0))
)
let contentSize = CGSize(width: context.availableSize.width, height: buttonFrame.maxY + 5.0 + environment.safeInsets.bottom)
return contentSize
}
}
}
private final class DemoSheetComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let subject: PremiumDemoScreen.Subject
let source: PremiumDemoScreen.Source
let action: () -> Void
init(context: AccountContext, subject: PremiumDemoScreen.Subject, source: PremiumDemoScreen.Source, action: @escaping () -> Void) {
self.context = context
self.subject = subject
self.source = source
self.action = action
}
static func ==(lhs: DemoSheetComponent, rhs: DemoSheetComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.subject != rhs.subject {
return false
}
if lhs.source != rhs.source {
return false
}
return true
}
static var body: Body {
let sheet = Child(SheetComponent<EnvironmentType>.self)
let animateOut = StoredActionSlot(Action<Void>.self)
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(DemoSheetContent(
context: context.component.context,
subject: context.component.subject,
source: context.component.source,
action: context.component.action,
dismiss: {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
}
)),
backgroundColor: environment.theme.actionSheet.opaqueItemBackgroundColor,
animateOut: animateOut
),
environment: {
environment
SheetComponentEnvironment(
isDisplaying: environment.value.isVisible,
dismiss: {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
}
)
},
availableSize: context.availableSize,
transition: context.transition
)
context.add(sheet
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
return context.availableSize
}
}
}
public class PremiumDemoScreen: ViewControllerComponentContainer {
public enum Subject {
case moreUpload
case fasterDownload
case voiceToText
case noAds
case uniqueReactions
case premiumStickers
case advancedChatManagement
case profileBadge
case animatedUserpics
}
public enum Source: Equatable {
case intro(String?)
case other
}
var disposed: () -> Void = {}
public init(context: AccountContext, subject: PremiumDemoScreen.Subject, source: PremiumDemoScreen.Source = .other, action: @escaping () -> Void) {
super.init(context: context, component: DemoSheetComponent(context: context, subject: subject, source: source, action: action), navigationBarAppearance: .none)
self.navigationPresentation = .flatModal
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.disposed()
}
public override func viewDidLoad() {
super.viewDidLoad()
self.view.disablesInteractiveModalDismiss = true
}
}

View File

@ -718,15 +718,26 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
typealias EnvironmentType = (ViewControllerComponentContainer.Environment, ScrollChildEnvironment)
let context: AccountContext
let price: String?
let present: (ViewController) -> Void
let buy: () -> Void
let updateIsFocused: (Bool) -> Void
init(context: AccountContext) {
init(context: AccountContext, price: String?, present: @escaping (ViewController) -> Void, buy: @escaping () -> Void, updateIsFocused: @escaping (Bool) -> Void) {
self.context = context
self.price = price
self.present = present
self.buy = buy
self.updateIsFocused = updateIsFocused
}
static func ==(lhs: PremiumIntroScreenContentComponent, rhs: PremiumIntroScreenContentComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.price != rhs.price {
return false
}
return true
}
@ -862,6 +873,11 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
var items: [SectionGroupComponent.Item] = []
let accountContext = context.component.context
let present = context.component.present
let buy = context.component.buy
let updateIsFocused = context.component.updateIsFocused
let price = context.component.price
var i = 0
for perk in state.configuration.perks {
let iconBackgroundColors = gradientColors[i]
@ -884,7 +900,48 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
)
),
action: {
var demoSubject: PremiumDemoScreen.Subject
switch perk {
case .doubleLimits:
return
case .moreUpload:
demoSubject = .moreUpload
case .fasterDownload:
demoSubject = .fasterDownload
case .voiceToText:
demoSubject = .voiceToText
case .noAds:
demoSubject = .noAds
case .uniqueReactions:
demoSubject = .uniqueReactions
case .premiumStickers:
demoSubject = .premiumStickers
case .advancedChatManagement:
demoSubject = .advancedChatManagement
case .profileBadge:
demoSubject = .profileBadge
case .animatedUserpics:
demoSubject = .animatedUserpics
}
var dismissImpl: (() -> Void)?
let controller = PremiumDemoScreen(
context: accountContext,
subject: demoSubject,
source: .intro(price),
action: {
dismissImpl?()
buy()
}
)
controller.disposed = {
updateIsFocused(false)
}
present(controller)
dismissImpl = { [weak controller] in
controller?.dismiss(animated: true, completion: nil)
}
updateIsFocused(true)
}
))
i += 1
@ -901,241 +958,6 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude),
transition: context.transition
)
//
// let section = section.update(
// component: SectionGroupComponent(
// items: [
// SectionGroupComponent.Item(
// AnyComponentWithIdentity(
// id: "limits",
// component: AnyComponent(
// PerkComponent(
// iconName: "Premium/Perk/Limits",
// iconBackgroundColors: [
// UIColor(rgb: 0xF28528),
// UIColor(rgb: 0xEF7633)
// ],
// title: strings.Premium_DoubledLimits,
// titleColor: titleColor,
// subtitle: strings.Premium_DoubledLimitsInfo,
// subtitleColor: subtitleColor,
// arrowColor: arrowColor
// )
// )
// ),
// action: {
//
// }
// ),
// SectionGroupComponent.Item(
// AnyComponentWithIdentity(
// id: "upload",
// component: AnyComponent(
// PerkComponent(
// iconName: "Premium/Perk/Upload",
// iconBackgroundColors: [
// UIColor(rgb: 0xEA5F43),
// UIColor(rgb: 0xE7504E)
// ],
// title: strings.Premium_UploadSize,
// titleColor: titleColor,
// subtitle: strings.Premium_UploadSizeInfo,
// subtitleColor: subtitleColor,
// arrowColor: arrowColor
// )
// )
// ),
// action: {
//
// }
// ),
// SectionGroupComponent.Item(
// AnyComponentWithIdentity(
// id: "speed",
// component: AnyComponent(
// PerkComponent(
// iconName: "Premium/Perk/Speed",
// iconBackgroundColors: [
// UIColor(rgb: 0xDE4768),
// UIColor(rgb: 0xD54D82)
// ],
// title: strings.Premium_FasterSpeed,
// titleColor: titleColor,
// subtitle: strings.Premium_FasterSpeedInfo,
// subtitleColor: subtitleColor,
// arrowColor: arrowColor
// )
// )
// ),
// action: {
//
// }
// ),
// SectionGroupComponent.Item(
// AnyComponentWithIdentity(
// id: "voice",
// component: AnyComponent(
// PerkComponent(
// iconName: "Premium/Perk/Voice",
// iconBackgroundColors: [
// UIColor(rgb: 0xDE4768),
// UIColor(rgb: 0xD54D82)
// ],
// title: strings.Premium_VoiceToText,
// titleColor: titleColor,
// subtitle: strings.Premium_VoiceToTextInfo,
// subtitleColor: subtitleColor,
// arrowColor: arrowColor
// )
// )
// ),
// action: {
//
// }
// ),
// SectionGroupComponent.Item(
// AnyComponentWithIdentity(
// id: "noAds",
// component: AnyComponent(
// PerkComponent(
// iconName: "Premium/Perk/NoAds",
// iconBackgroundColors: [
// UIColor(rgb: 0xC654A8),
// UIColor(rgb: 0xBE5AC2)
// ],
// title: strings.Premium_NoAds,
// titleColor: titleColor,
// subtitle: strings.Premium_NoAdsInfo,
// subtitleColor: subtitleColor,
// arrowColor: arrowColor
// )
// )
// ),
// action: {
//
// }
// ),
// SectionGroupComponent.Item(
// AnyComponentWithIdentity(
// id: "reactions",
// component: AnyComponent(
// PerkComponent(
// iconName: "Premium/Perk/Reactions",
// iconBackgroundColors: [
// UIColor(rgb: 0xAF62E9),
// UIColor(rgb: 0xA668FF)
// ],
// title: strings.Premium_Reactions,
// titleColor: titleColor,
// subtitle: strings.Premium_ReactionsInfo,
// subtitleColor: subtitleColor,
// arrowColor: arrowColor
// )
// )
// ),
// action: {
//
// }
// ),
// SectionGroupComponent.Item(
// AnyComponentWithIdentity(
// id: "stickers",
// component: AnyComponent(
// PerkComponent(
// iconName: "Premium/Perk/Stickers",
// iconBackgroundColors: [
// UIColor(rgb: 0x9674FF),
// UIColor(rgb: 0x8C7DFF)
// ],
// title: strings.Premium_Stickers,
// titleColor: titleColor,
// subtitle: strings.Premium_StickersInfo,
// subtitleColor: subtitleColor,
// arrowColor: arrowColor
// )
// )
// ),
// action: {
//
// }
// ),
// SectionGroupComponent.Item(
// AnyComponentWithIdentity(
// id: "chat",
// component: AnyComponent(
// PerkComponent(
// iconName: "Premium/Perk/Chat",
// iconBackgroundColors: [
// UIColor(rgb: 0x9674FF),
// UIColor(rgb: 0x8C7DFF)
// ],
// title: strings.Premium_ChatManagement,
// titleColor: titleColor,
// subtitle: strings.Premium_ChatManagementInfo,
// subtitleColor: subtitleColor,
// arrowColor: arrowColor
// )
// )
// ),
// action: {
//
// }
// ),
// SectionGroupComponent.Item(
// AnyComponentWithIdentity(
// id: "badge",
// component: AnyComponent(
// PerkComponent(
// iconName: "Premium/Perk/Badge",
// iconBackgroundColors: [
// UIColor(rgb: 0x7B88FF),
// UIColor(rgb: 0x7091FF)
// ],
// title: strings.Premium_Badge,
// titleColor: titleColor,
// subtitle: strings.Premium_BadgeInfo,
// subtitleColor: subtitleColor,
// arrowColor: arrowColor
// )
// )
// ),
// action: {
//
// }
// ),
// SectionGroupComponent.Item(
// AnyComponentWithIdentity(
// id: "avatar",
// component: AnyComponent(
// PerkComponent(
// iconName: "Premium/Perk/Avatar",
// iconBackgroundColors: [
// UIColor(rgb: 0x609DFF),
// UIColor(rgb: 0x56A5FF)
// ],
// title: strings.Premium_Avatar,
// titleColor: titleColor,
// subtitle: strings.Premium_AvatarInfo,
// subtitleColor: subtitleColor,
// arrowColor: arrowColor
// )
// )
// ),
// action: {
//
// }
// ),
// ],
// backgroundColor: environment.theme.list.itemBlocksBackgroundColor,
// selectionColor: environment.theme.list.itemHighlightedBackgroundColor,
// separatorColor: environment.theme.list.itemBlocksSeparatorColor
// ),
// environment: {},
// availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude),
// transition: context.transition
// )
context.add(section
.position(CGPoint(x: availableWidth / 2.0, y: size.height + section.size.height / 2.0))
.clipsToBounds(true)
@ -1315,11 +1137,13 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
let context: AccountContext
let updateInProgress: (Bool) -> Void
let present: (ViewController) -> Void
let completion: () -> Void
init(context: AccountContext, updateInProgress: @escaping (Bool) -> Void, completion: @escaping () -> Void) {
init(context: AccountContext, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void) {
self.context = context
self.updateInProgress = updateInProgress
self.present = present
self.completion = completion
}
@ -1338,6 +1162,8 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
var topContentOffset: CGFloat?
var bottomContentOffset: CGFloat?
var hasIdleAnimations = true
var inProgress = false
var premiumProduct: InAppPurchaseManager.Product?
private var disposable: Disposable?
@ -1398,6 +1224,11 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
}
}))
}
func updateIsFocused(_ isFocused: Bool) {
self.hasIdleAnimations = !isFocused
self.updated(transition: .immediate)
}
}
func makeState() -> State {
@ -1427,7 +1258,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
}
let star = star.update(
component: PremiumStarComponent(isVisible: starIsVisible),
component: PremiumStarComponent(isVisible: starIsVisible, hasIdleAnimations: state.hasIdleAnimations),
availableSize: CGSize(width: min(390.0, context.availableSize.width), height: 220.0),
transition: context.transition
)
@ -1504,7 +1335,14 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
let scrollContent = scrollContent.update(
component: ScrollComponent<EnvironmentType>(
content: AnyComponent(PremiumIntroScreenContentComponent(
context: context.component.context
context: context.component.context,
price: state.premiumProduct?.price,
present: context.component.present,
buy: { [weak state] in
state?.buy()
}, updateIsFocused: { [weak state] isFocused in
state?.updateIsFocused(isFocused)
}
)),
contentInsets: UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: bottomPanel.size.height, right: 0.0),
contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in
@ -1610,12 +1448,16 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
self.context = context
var updateInProgressImpl: ((Bool) -> Void)?
var presentImpl: ((ViewController) -> Void)?
var completionImpl: (() -> Void)?
super.init(context: context, component: PremiumIntroScreenComponent(
context: context,
updateInProgress: { inProgress in
updateInProgressImpl?(inProgress)
},
present: { c in
presentImpl?(c)
},
completion: {
completionImpl?()
}
@ -1639,6 +1481,10 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
}
}
presentImpl = { [weak self] c in
self?.push(c)
}
completionImpl = { [weak self] in
if let strongSelf = self {
strongSelf.view.addSubview(ConfettiView(frame: strongSelf.view.bounds))

View File

@ -16,16 +16,16 @@ import BundleIconComponent
import SolidRoundedButtonComponent
import Markdown
private func generateCloseButtonImage(theme: PresentationTheme) -> UIImage? {
func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor(rgb: 0x808084, alpha: 0.1).cgColor)
context.setFillColor(backgroundColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.setLineWidth(2.0)
context.setLineCap(.round)
context.setStrokeColor(theme.actionSheet.inputClearButtonColor.cgColor)
context.setStrokeColor(foregroundColor.cgColor)
context.move(to: CGPoint(x: 10.0, y: 10.0))
context.addLine(to: CGPoint(x: 20.0, y: 20.0))
@ -143,7 +143,7 @@ private class PremiumLimitAnimationComponent: Component {
self.badgeCountLabel = RollingLabel()
self.badgeCountLabel.font = Font.with(size: 24.0, design: .round, weight: .semibold, traits: [])
self.badgeCountLabel.textColor = .white
self.badgeCountLabel.text(num: 0)
self.badgeCountLabel.configure(with: "0")
super.init(frame: frame)
@ -203,8 +203,8 @@ private class PremiumLimitAnimationComponent: Component {
self.badgeView.layer.add(rotateAnimation, forKey: "appearance2")
self.badgeView.layer.add(returnAnimation, forKey: "appearance3")
if let badgeText = component.badgeText, let num = Int(badgeText) {
self.badgeCountLabel.text(num: num)
if let badgeText = component.badgeText {
self.badgeCountLabel.configure(with: badgeText)
}
}
@ -251,7 +251,18 @@ private class PremiumLimitAnimationComponent: Component {
self.badgeMaskArrowView.frame = CGRect(origin: CGPoint(x: (badgeSize.width - 44.0) / 2.0, y: badgeSize.height - 12.0), size: CGSize(width: 44.0, height: 12.0))
self.badgeView.bounds = CGRect(origin: .zero, size: badgeSize)
self.badgeView.center = CGPoint(x: 3.0 + (availableSize.width - 6.0) * component.badgePosition, y: 82.0)
if component.badgePosition > 1.0 - .ulpOfOne {
let offset = badgeWidth / 2.0 - 16.0
self.badgeView.center = CGPoint(x: 3.0 + (availableSize.width - 6.0) * component.badgePosition - offset, y: 82.0)
self.badgeMaskArrowView.frame = self.badgeMaskArrowView.frame.offsetBy(dx: offset - 18.0, dy: 0.0)
} else {
self.badgeView.center = CGPoint(x: 3.0 + (availableSize.width - 6.0) * component.badgePosition, y: 82.0)
if self.badgeView.frame.maxX > availableSize.width {
let delta = self.badgeView.frame.maxX - availableSize.width - 6.0
self.badgeView.center = self.badgeView.center.offsetBy(dx: -delta, dy: 0.0)
}
}
self.badgeForeground.bounds = CGRect(origin: CGPoint(), size: CGSize(width: badgeSize.width * 3.0, height: badgeSize.height))
if self.badgeForeground.animation(forKey: "movement") == nil {
self.badgeForeground.position = CGPoint(x: badgeSize.width * 3.0 / 2.0 - self.badgeForeground.frame.width * 0.35, y: badgeSize.height / 2.0)
@ -616,7 +627,7 @@ private final class LimitSheetContent: CombinedComponent {
if let (image, theme) = state.cachedCloseImage, theme === environment.theme {
closeImage = image
} else {
closeImage = generateCloseButtonImage(theme: theme)!
closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: theme.actionSheet.inputClearButtonColor)!
state.cachedCloseImage = (closeImage, theme)
}
@ -634,6 +645,7 @@ private final class LimitSheetContent: CombinedComponent {
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - closeButton.size.width, y: 28.0))
)
var titleText = strings.Premium_LimitReached
let iconName: String
let badgeText: String
let string: String
@ -669,20 +681,21 @@ private final class LimitSheetContent: CombinedComponent {
premiumValue = "\(premiumLimit)"
badgePosition = CGFloat(component.count) / CGFloat(premiumLimit)
case .files:
let limit = Int64(state.limits.maxUploadFileParts) * 512 * 1024
let premiumLimit = Int64(state.limits.maxUploadFileParts) * 512 * 1024
let limit = Int64(state.limits.maxUploadFileParts) * 512 * 1024 + 1024 * 1024 * 100
let premiumLimit = Int64(state.premiumLimits.maxUploadFileParts) * 512 * 1024 + 1024 * 1024 * 100
iconName = "Premium/File"
badgeText = dataSizeString(limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))
string = strings.Premium_MaxFileSizeText(dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))).string
defaultValue = ""
premiumValue = dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))
badgePosition = 0.5
titleText = strings.Premium_FileTooLarge
}
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: strings.Premium_LimitReached,
string: titleText,
font: Font.semibold(17.0),
textColor: theme.actionSheet.primaryTextColor,
paragraphAlignment: .center

View File

@ -1,272 +0,0 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import PresentationDataUtils
import SolidRoundedButtonNode
import AppBundle
public final class PremiumReactionsScreen: ViewController {
private let context: AccountContext
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
private let reactions: [AvailableReactions.Reaction]
public var proceed: (() -> Void)?
private class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate {
private weak var controller: PremiumReactionsScreen?
private var presentationData: PresentationData
private let blurView: UIVisualEffectView
private let vibrancyView: UIVisualEffectView
private let dimNode: ASDisplayNode
private let darkDimNode: ASDisplayNode
private let containerNode: ASDisplayNode
private let textNode: ImmediateTextNode
private let overlayTextNode: ImmediateTextNode
private let proceedButton: SolidRoundedButtonNode
private let cancelButton: HighlightableButtonNode
private let carouselNode: ReactionCarouselNode
private var validLayout: ContainerViewLayout?
init(controller: PremiumReactionsScreen) {
self.controller = controller
self.presentationData = controller.presentationData
self.dimNode = ASDisplayNode()
let blurEffect = UIBlurEffect(style: self.presentationData.theme.overallDarkAppearance ? .dark : .light)
self.blurView = UIVisualEffectView(effect: blurEffect)
self.blurView.isUserInteractionEnabled = false
self.vibrancyView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: blurEffect))
self.darkDimNode = ASDisplayNode()
self.darkDimNode.alpha = 0.0
self.darkDimNode.backgroundColor = self.presentationData.theme.contextMenu.dimColor
self.darkDimNode.isUserInteractionEnabled = false
self.dimNode.backgroundColor = UIColor(white: self.presentationData.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.5)
self.containerNode = ASDisplayNode()
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.textAlignment = .center
self.textNode.maximumNumberOfLines = 0
self.textNode.lineSpacing = 0.1
self.overlayTextNode = ImmediateTextNode()
self.overlayTextNode.displaysAsynchronously = false
self.overlayTextNode.textAlignment = .center
self.overlayTextNode.maximumNumberOfLines = 0
self.overlayTextNode.lineSpacing = 0.1
self.proceedButton = SolidRoundedButtonNode(title: self.presentationData.strings.Premium_Reactions_Proceed, icon: UIImage(bundleImageName: "Premium/ButtonIcon"), theme: SolidRoundedButtonTheme(
backgroundColor: .white,
backgroundColors: [
UIColor(rgb: 0x0077ff),
UIColor(rgb: 0x6b93ff),
UIColor(rgb: 0x8878ff),
UIColor(rgb: 0xe46ace)
], foregroundColor: .white), height: 50.0, cornerRadius: 11.0, gloss: true)
self.cancelButton = HighlightableButtonNode()
self.cancelButton.setTitle(self.presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: self.presentationData.theme.list.itemAccentColor, for: .normal)
self.carouselNode = ReactionCarouselNode(context: controller.context, theme: controller.presentationData.theme, reactions: controller.reactions)
super.init()
self.addSubnode(self.dimNode)
self.addSubnode(self.darkDimNode)
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.proceedButton)
self.containerNode.addSubnode(self.cancelButton)
self.addSubnode(self.carouselNode)
let textColor: UIColor
if self.presentationData.theme.overallDarkAppearance {
textColor = UIColor(white: 1.0, alpha: 1.0)
self.overlayTextNode.alpha = 0.2
self.addSubnode(self.overlayTextNode)
} else {
textColor = self.presentationData.theme.contextMenu.secondaryColor
}
self.textNode.attributedText = NSAttributedString(string: self.presentationData.strings.Premium_Reactions_Description, font: Font.regular(17.0), textColor: textColor)
self.overlayTextNode.attributedText = NSAttributedString(string: self.presentationData.strings.Premium_Reactions_Description, font: Font.regular(17.0), textColor: textColor)
self.proceedButton.pressed = { [weak self] in
if let strongSelf = self, let controller = strongSelf.controller, let navigationController = controller.navigationController {
strongSelf.animateOut()
navigationController.pushViewController(PremiumIntroScreen(context: controller.context, source: .reactions), animated: true)
}
}
self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside)
}
override func didLoad() {
super.didLoad()
self.view.insertSubview(self.blurView, aboveSubview: self.dimNode.view)
self.blurView.contentView.addSubview(self.vibrancyView)
self.vibrancyView.contentView.addSubview(self.textNode.view)
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimNodeTap(_:))))
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
self.dimNode.backgroundColor = UIColor(white: self.presentationData.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.5)
self.darkDimNode.backgroundColor = self.presentationData.theme.contextMenu.dimColor
self.blurView.effect = UIBlurEffect(style: self.presentationData.theme.overallDarkAppearance ? .dark : .light)
}
func animateIn() {
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.blurView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.carouselNode.layer.animatePosition(from: CGPoint(x: 312.0, y: 252.0), to: self.carouselNode.position, duration: 0.45, timingFunction: kCAMediaTimingFunctionSpring)
self.carouselNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.45, timingFunction: kCAMediaTimingFunctionSpring)
self.carouselNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.carouselNode.animateIn()
self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.containerNode.layer.animateScale(from: 0.95, to: 1.0, duration: 0.3)
self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.vibrancyView.layer.animateScale(from: 0.95, to: 1.0, duration: 0.3)
}
func animateOut() {
self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
self.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
self.carouselNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
self.carouselNode.animateOut()
self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
self.blurView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak self] _ in
if let strongSelf = self {
strongSelf.controller?.dismiss(animated: false, completion: nil)
}
})
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
self.validLayout = layout
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
transition.updateFrame(node: self.darkDimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
transition.updateFrame(view: self.blurView, frame: CGRect(origin: CGPoint(), size: layout.size))
transition.updateFrame(view: self.vibrancyView, frame: CGRect(origin: CGPoint(), size: layout.size))
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: layout.size))
let carouselFrame = CGRect(origin: CGPoint(x: 0.0, y: 100.0), size: CGSize(width: layout.size.width, height: layout.size.width))
self.carouselNode.updateLayout(size: carouselFrame.size, transition: transition)
transition.updateFrame(node: self.carouselNode, frame: carouselFrame)
let sideInset: CGFloat = 16.0
let cancelSize = self.cancelButton.measure(layout.size)
self.cancelButton.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - cancelSize.width) / 2.0), y: layout.size.height - cancelSize.height - 49.0), size: cancelSize)
let buttonWidth = layout.size.width - sideInset * 2.0
let buttonHeight = self.proceedButton.updateLayout(width: buttonWidth, transition: transition)
self.proceedButton.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - buttonWidth) / 2.0), y: layout.size.height - cancelSize.height - 49.0 - buttonHeight - 23.0), size: CGSize(width: buttonWidth, height: buttonHeight))
let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - sideInset * 5.0, height: CGFloat.greatestFiniteMagnitude))
let _ = self.overlayTextNode.updateLayout(CGSize(width: layout.size.width - sideInset * 5.0, height: CGFloat.greatestFiniteMagnitude))
let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - textSize.width) / 2.0), y: layout.size.height - cancelSize.height - 48.0 - buttonHeight - 20.0 - textSize.height - 31.0), size: textSize)
self.textNode.frame = textFrame
self.overlayTextNode.frame = textFrame
}
@objc private func dimNodeTap(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.cancelPressed()
}
}
@objc private func cancelPressed() {
self.animateOut()
}
}
private var controllerNode: Node {
return self.displayNode as! Node
}
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, reactions: [AvailableReactions.Reaction]) {
self.context = context
self.reactions = reactions
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
self.presentationData = presentationData
self.updatedPresentationData = updatedPresentationData
super.init(navigationBarPresentationData: nil)
self.navigationPresentation = .flatModal
self.statusBar.statusBarStyle = .Ignore
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData)
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
let previousTheme = strongSelf.presentationData.theme
let previousStrings = strongSelf.presentationData.strings
strongSelf.presentationData = presentationData
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
strongSelf.controllerNode.updatePresentationData(strongSelf.presentationData)
}
}
})
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDataDisposable?.dispose()
}
override public func loadDisplayNode() {
self.displayNode = Node(controller: self)
super.displayNodeDidLoad()
}
private var didAppear = false
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !self.didAppear {
self.didAppear = true
self.controllerNode.animateIn()
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, transition: transition)
}
}

View File

@ -46,13 +46,15 @@ private func generateDiffuseTexture() -> UIImage {
class PremiumStarComponent: Component {
let isVisible: Bool
let hasIdleAnimations: Bool
init(isVisible: Bool) {
init(isVisible: Bool, hasIdleAnimations: Bool) {
self.isVisible = isVisible
self.hasIdleAnimations = hasIdleAnimations
}
static func ==(lhs: PremiumStarComponent, rhs: PremiumStarComponent) -> Bool {
return lhs.isVisible == rhs.isVisible
return lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations
}
final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView {
@ -75,6 +77,7 @@ class PremiumStarComponent: Component {
private var previousInteractionTimestamp: Double = 0.0
private var timer: SwiftSignalKit.Timer?
private var hasIdleAnimations = false
override init(frame: CGRect) {
self.sceneView = SCNView(frame: frame)
@ -249,7 +252,7 @@ class PremiumStarComponent: Component {
self.previousInteractionTimestamp = CACurrentMediaTime()
self.timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in
if let strongSelf = self {
if let strongSelf = self, strongSelf.hasIdleAnimations {
let currentTimestamp = CACurrentMediaTime()
if currentTimestamp > strongSelf.previousInteractionTimestamp + 5.0 {
strongSelf.playAppearanceAnimation()
@ -359,6 +362,8 @@ class PremiumStarComponent: Component {
self.sceneView.bounds = CGRect(origin: .zero, size: CGSize(width: availableSize.width * 2.0, height: availableSize.height * 2.0))
self.sceneView.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)
self.hasIdleAnimations = component.hasIdleAnimations
return availableSize
}
}

View File

@ -2,15 +2,86 @@ import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import TelegramCore
import AccountContext
import ReactionSelectionNode
import TelegramPresentationData
import AccountContext
final class ReactionsCarouselComponent: Component {
public typealias EnvironmentType = DemoPageEnvironment
let context: AccountContext
let theme: PresentationTheme
let reactions: [AvailableReactions.Reaction]
public init(
context: AccountContext,
theme: PresentationTheme,
reactions: [AvailableReactions.Reaction]
) {
self.context = context
self.theme = theme
self.reactions = reactions
}
public static func ==(lhs: ReactionsCarouselComponent, rhs: ReactionsCarouselComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.reactions != rhs.reactions {
return false
}
return true
}
public final class View: UIView {
private var component: ReactionsCarouselComponent?
private var node: ReactionCarouselNode?
public func update(component: ReactionsCarouselComponent, availableSize: CGSize, transition: Transition) -> CGSize {
if self.node == nil {
let node = ReactionCarouselNode(
context: component.context,
theme: component.theme,
reactions: component.reactions
)
self.node = node
self.addSubnode(node)
}
let isFirstTime = self.component == nil
self.component = component
if let node = self.node {
node.frame = CGRect(origin: CGPoint(x: 0.0, y: -20.0), size: availableSize)
node.updateLayout(size: availableSize, transition: .immediate)
}
if isFirstTime {
self.node?.animateIn()
}
return availableSize
}
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<DemoPageEnvironment>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
private let itemSize = CGSize(width: 110.0, height: 110.0)
final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
private let context: AccountContext
private let theme: PresentationTheme
private let reactions: [AvailableReactions.Reaction]
@ -167,7 +238,7 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
func playReaction() {
let delta = self.positionDelta
let index = max(0, min(self.itemNodes.count - 1, Int(round(self.currentPosition / delta))))
let index = max(0, Int(round(self.currentPosition / delta)) % self.itemNodes.count)
guard !self.playingIndices.contains(index) else {
return
@ -223,7 +294,8 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
self.scrollStartPosition = (scrollView.contentOffset.x, self.currentPosition)
}
}
private let hapticFeedback = HapticFeedback()
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard !self.ignoreContentOffsetChange, let (startContentOffset, startPosition) = self.scrollStartPosition else {
return
@ -241,10 +313,12 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
self.currentPosition = updatedPosition
let indexDelta = self.positionDelta
let index = max(0, min(self.itemNodes.count - 1, Int(round(self.currentPosition / indexDelta))))
let index = max(0, Int(round(self.currentPosition / indexDelta)) % self.itemNodes.count)
if index != self.currentIndex {
self.currentIndex = index
print(index)
if self.scrollNode.view.isTracking || self.scrollNode.view.isDecelerating {
self.hapticFeedback.tap()
}
}
if let size = self.validLayout {
@ -272,7 +346,7 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
self.resetScrollPosition()
let delta = self.positionDelta
let index = max(0, min(self.itemNodes.count - 1, Int(round(self.currentPosition / delta))))
let index = max(0, Int(round(self.currentPosition / delta)) % self.itemNodes.count)
self.scrollTo(index, playReaction: true, duration: 0.2)
}
}
@ -287,14 +361,14 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
self.scrollNode.frame = CGRect(origin: CGPoint(), size: size)
if self.scrollNode.view.contentSize.width.isZero {
self.scrollNode.view.contentSize = CGSize(width: 10000000, height: size.height)
self.scrollNode.view.contentSize = CGSize(width: 10000000.0, height: size.height)
self.tapNode.frame = CGRect(origin: CGPoint(), size: self.scrollNode.view.contentSize)
self.resetScrollPosition()
}
let delta = self.positionDelta
let areaSize = CGSize(width: floor(size.width * 0.7), height: size.height * 0.5)
let areaSize = CGSize(width: floor(size.width * 0.7), height: size.height * 0.45)
for i in 0 ..< self.itemNodes.count {
let itemNode = self.itemNodes[i]
@ -326,8 +400,8 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
let itemFrame = CGRect(origin: CGPoint(x: size.width * 0.5 + point.x * areaSize.width * 0.5 - itemSize.width * 0.5, y: size.height * 0.5 + point.y * areaSize.height * 0.5 - itemSize.height * 0.5), size: itemSize)
containerNode.bounds = CGRect(origin: CGPoint(), size: itemFrame.size)
containerNode.position = itemFrame.center
transition.updateTransformScale(node: containerNode, scale: 1.0 - distance * 0.45)
containerNode.position = CGPoint(x: itemFrame.midX, y: itemFrame.midY)
transition.updateTransformScale(node: containerNode, scale: 1.0 - distance * 0.55)
itemNode.frame = CGRect(origin: CGPoint(), size: itemFrame.size)
itemNode.updateLayout(size: itemFrame.size, isExpanded: false, largeExpanded: false, isPreviewing: false, transition: transition)

View File

@ -1,4 +1,5 @@
import UIKit
import Display
private extension UILabel {
func textWidth() -> CGFloat {
@ -32,22 +33,19 @@ open class RollingLabel: UILabel {
private let duration = 1.12
private let durationOffset = 0.2
private let textsNotAnimated = [","]
public func text(num: Int) {
self.configure(with: num)
self.text = " "
self.animate()
public func setSuffix(suffix: String) {
self.suffix = suffix
}
public func setPrefix(prefix: String) {
self.suffix = prefix
}
private func configure(with number: Int) {
fullText = String(number)
func configure(with string: String) {
fullText = string
clean()
setupSubviews()
self.text = " "
self.animate()
}
private func animate(ascending: Bool = true) {
@ -99,9 +97,10 @@ open class RollingLabel: UILabel {
}
stringArray.enumerated().forEach { index, text in
if textsNotAnimated.contains(text) {
let nonDigits = CharacterSet.decimalDigits.inverted
if text.rangeOfCharacter(from: nonDigits) != nil {
let label = UILabel()
label.frame.origin = CGPoint(x: x, y: y)
label.frame.origin = CGPoint(x: x, y: y - 1.0 - UIScreenPixel)
label.textColor = textColor
label.font = font
label.text = text
@ -118,28 +117,28 @@ open class RollingLabel: UILabel {
label.text = "0"
label.textAlignment = .center
label.sizeToFit()
createScrollLayer(to: label, text: text)
createScrollLayer(to: label, text: text, index: index)
x += label.bounds.width
}
}
}
private func createScrollLayer(to label: UILabel, text: String) {
private func createScrollLayer(to label: UILabel, text: String, index: Int) {
let scrollLayer = CAScrollLayer()
scrollLayer.frame = label.frame
scrollLayer.frame = CGRect(x: label.frame.minX, y: label.frame.minY - 10.0, width: label.frame.width, height: label.frame.height * 3.0)
scrollLayers.append(scrollLayer)
self.layer.addSublayer(scrollLayer)
createContentForLayer(scrollLayer: scrollLayer, text: text)
createContentForLayer(scrollLayer: scrollLayer, text: text, index: index)
}
private func createContentForLayer(scrollLayer: CAScrollLayer, text: String) {
private func createContentForLayer(scrollLayer: CAScrollLayer, text: String, index: Int) {
var textsForScroll: [String] = []
let max: Int
var found = false
if let val = Int(text) {
if let val = Int(text), index == 0 {
max = val
found = true
} else {
@ -150,11 +149,11 @@ open class RollingLabel: UILabel {
let str = String(i)
textsForScroll.append(str)
}
if !found {
if !found && text != "9" {
textsForScroll.append(text)
}
var height: CGFloat = 0
var height: CGFloat = 0.0
for text in textsForScroll {
let label = UILabel()
label.text = text
@ -179,17 +178,18 @@ open class RollingLabel: UILabel {
animation.duration = duration + offset
animation.timingFunction = CAMediaTimingFunction(name: .easeOut)
let verticalOffset = 20.0
if ascending {
animation.fromValue = maxY
animation.fromValue = maxY + verticalOffset
animation.toValue = 0
} else {
animation.fromValue = 0
animation.toValue = maxY
animation.toValue = maxY + verticalOffset
}
scrollLayer.scrollMode = .vertically
scrollLayer.add(animation, forKey: nil)
scrollLayer.scroll(to: CGPoint(x: 0, y: maxY))
scrollLayer.scroll(to: CGPoint(x: 0, y: maxY + verticalOffset))
offset += self.durationOffset
}

View File

@ -0,0 +1,498 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import ComponentFlow
import TelegramCore
import AccountContext
import ReactionSelectionNode
import TelegramPresentationData
import AccountContext
import AnimatedStickerNode
import TelegramAnimatedStickerNode
final class StickersCarouselComponent: Component {
public typealias EnvironmentType = DemoPageEnvironment
let context: AccountContext
let stickers: [TelegramMediaFile]
public init(
context: AccountContext,
stickers: [TelegramMediaFile]
) {
self.context = context
self.stickers = stickers
}
public static func ==(lhs: StickersCarouselComponent, rhs: StickersCarouselComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.stickers != rhs.stickers {
return false
}
return true
}
public final class View: UIView {
private var component: StickersCarouselComponent?
private var node: StickersCarouselNode?
public func update(component: StickersCarouselComponent, availableSize: CGSize, environment: Environment<DemoPageEnvironment>, transition: Transition) -> CGSize {
let isDisplaying = environment[DemoPageEnvironment.self].isDisplaying
if self.node == nil {
let node = StickersCarouselNode(
context: component.context,
stickers: component.stickers
)
self.node = node
self.addSubnode(node)
}
let isFirstTime = self.component == nil
self.component = component
if let node = self.node {
node.setVisible(isDisplaying)
node.frame = CGRect(origin: .zero, size: availableSize)
node.updateLayout(size: availableSize, transition: .immediate)
}
if isFirstTime {
self.node?.animateIn()
}
return availableSize
}
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<DemoPageEnvironment>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition)
}
}
private let itemSize = CGSize(width: 220.0, height: 220.0)
private class StickerNode: ASDisplayNode {
private let context: AccountContext
private let file: TelegramMediaFile
public var imageNode: TransformImageNode
public var animationNode: AnimatedStickerNode?
public var additionalAnimationNode: AnimatedStickerNode?
private let disposable = MetaDisposable()
private let effectDisposable = MetaDisposable()
init(context: AccountContext, file: TelegramMediaFile) {
self.context = context
self.file = file
self.imageNode = TransformImageNode()
if file.isPremiumSticker {
let animationNode = AnimatedStickerNode()
self.animationNode = animationNode
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 400.0, height: 400.0))
let pathPrefix = context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id)
animationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: file.resource, isVideo: file.isVideoSticker), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .direct(cachePathPrefix: pathPrefix))
self.disposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, fileReference: .standalone(media: file), resource: file.resource).start())
if let effect = file.videoThumbnails.first {
self.effectDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, fileReference: .standalone(media: file), resource: effect.resource).start())
let source = AnimatedStickerResourceSource(account: self.context.account, resource: effect.resource, fitzModifier: nil)
let additionalAnimationNode = AnimatedStickerNode()
let pathPrefix = context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(effect.resource.id)
additionalAnimationNode.setup(source: source, width: Int(fittedDimensions.width * 2.0), height: Int(fittedDimensions.height * 2.0), playbackMode: .loop, mode: .direct(cachePathPrefix: pathPrefix))
self.additionalAnimationNode = additionalAnimationNode
}
} else {
self.animationNode = nil
}
super.init()
self.isUserInteractionEnabled = false
if let animationNode = self.animationNode {
self.addSubnode(animationNode)
} else {
self.addSubnode(self.imageNode)
}
if let additionalAnimationNode = self.additionalAnimationNode {
self.addSubnode(additionalAnimationNode)
}
}
deinit {
self.disposable.dispose()
self.effectDisposable.dispose()
}
private var visibility: Bool = false
private var centrality: Bool = false
public func setCentral(_ central: Bool) {
self.centrality = central
self.updatePlayback()
}
public func setVisible(_ visible: Bool) {
self.visibility = visible
self.updatePlayback()
}
private func updatePlayback() {
self.animationNode?.visibility = self.visibility
if let additionalAnimationNode = self.additionalAnimationNode {
let wasVisible = additionalAnimationNode.visibility
let isVisible = self.visibility && self.centrality
if wasVisible && !isVisible {
additionalAnimationNode.alpha = 0.0
additionalAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak additionalAnimationNode] _ in
additionalAnimationNode?.visibility = isVisible
})
} else if isVisible {
additionalAnimationNode.visibility = isVisible
if !wasVisible {
additionalAnimationNode.play(fromIndex: 0)
Queue.mainQueue().after(0.05, {
additionalAnimationNode.alpha = 1.0
})
}
}
}
}
public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
let boundingSize = CGSize(width: 240.0, height: 240.0)
if let dimensitons = self.file.dimensions {
let imageSize = dimensitons.cgSize.aspectFitted(boundingSize)
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
let imageFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: 0.0), size: imageSize)
self.imageNode.frame = imageFrame
if let animationNode = self.animationNode {
animationNode.frame = imageFrame
animationNode.updateLayout(size: imageSize)
if let additionalAnimationNode = self.additionalAnimationNode {
additionalAnimationNode.frame = imageFrame.offsetBy(dx: -imageFrame.width * 0.245 + 21, dy: -1.0).insetBy(dx: -imageFrame.width * 0.245, dy: -imageFrame.height * 0.245)
additionalAnimationNode.updateLayout(size: additionalAnimationNode.frame.size)
}
}
}
}
}
private class StickersCarouselNode: ASDisplayNode, UIScrollViewDelegate {
private let context: AccountContext
private let stickers: [TelegramMediaFile]
private var itemContainerNodes: [ASDisplayNode] = []
private var itemNodes: [StickerNode] = []
private let scrollNode: ASScrollNode
private let tapNode: ASDisplayNode
private var animator: DisplayLinkAnimator?
private var currentPosition: CGFloat = 0.0
private var currentIndex: Int = 0
private var validLayout: CGSize?
private var playingIndices = Set<Int>()
private let positionDelta: Double
init(context: AccountContext, stickers: [TelegramMediaFile]) {
self.context = context
self.stickers = Array(stickers.shuffled().prefix(14))
self.scrollNode = ASScrollNode()
self.tapNode = ASDisplayNode()
self.positionDelta = 1.0 / CGFloat(self.stickers.count)
super.init()
self.clipsToBounds = true
self.addSubnode(self.scrollNode)
self.scrollNode.addSubnode(self.tapNode)
self.setup()
}
override func didLoad() {
super.didLoad()
self.scrollNode.view.delegate = self
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.showsVerticalScrollIndicator = false
self.scrollNode.view.canCancelContentTouches = true
self.tapNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.stickerTapped(_:))))
}
@objc private func stickerTapped(_ gestureRecognizer: UITapGestureRecognizer) {
guard self.animator == nil, self.scrollStartPosition == nil else {
return
}
let point = gestureRecognizer.location(in: self.view)
guard let index = self.itemContainerNodes.firstIndex(where: { $0.frame.contains(point) }) else {
return
}
self.scrollTo(index, playAnimation: true, duration: 0.4)
}
func animateIn() {
self.scrollTo(1, playAnimation: true, duration: 0.5, clockwise: true)
}
func scrollTo(_ index: Int, playAnimation: Bool, duration: Double, clockwise: Bool? = nil) {
guard index >= 0 && index < self.itemNodes.count else {
return
}
self.currentIndex = index
let delta = self.positionDelta
let startPosition = self.currentPosition
let newPosition = delta * CGFloat(index)
var change = newPosition - startPosition
if let clockwise = clockwise {
if clockwise {
if change > 0.0 {
change = change - 1.0
}
} else {
if change < 0.0 {
change = 1.0 + change
}
}
} else {
if change > 0.5 {
change = change - 1.0
} else if change < -0.5 {
change = 1.0 + change
}
}
self.animator = DisplayLinkAnimator(duration: duration * UIView.animationDurationFactor(), from: 0.0, to: 1.0, update: { [weak self] t in
let t = listViewAnimationCurveSystem(t)
var updatedPosition = startPosition + change * t
while updatedPosition >= 1.0 {
updatedPosition -= 1.0
}
while updatedPosition < 0.0 {
updatedPosition += 1.0
}
self?.currentPosition = updatedPosition
if let size = self?.validLayout {
self?.updateLayout(size: size, transition: .immediate)
}
}, completion: { [weak self] in
self?.animator = nil
if playAnimation {
self?.playSelectedSticker()
}
})
}
private var visibility = false
func setVisible(_ visible: Bool) {
guard self.visibility != visible else {
return
}
self.visibility = visible
if let size = self.validLayout {
self.updateLayout(size: size, transition: .immediate)
}
}
func setup() {
for sticker in self.stickers {
let containerNode = ASDisplayNode()
let itemNode = StickerNode(context: self.context, file: sticker)
containerNode.isUserInteractionEnabled = false
containerNode.addSubnode(itemNode)
self.addSubnode(containerNode)
self.itemContainerNodes.append(containerNode)
self.itemNodes.append(itemNode)
}
}
private var ignoreContentOffsetChange = false
private func resetScrollPosition() {
self.scrollStartPosition = nil
self.ignoreContentOffsetChange = true
self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: 5000.0 - self.scrollNode.frame.height * 0.5)
self.ignoreContentOffsetChange = false
}
func playSelectedSticker() {
let delta = self.positionDelta
let index = max(0, Int(round(self.currentPosition / delta)) % self.itemNodes.count)
guard !self.playingIndices.contains(index) else {
return
}
for i in 0 ..< self.itemNodes.count {
let itemNode = self.itemNodes[i]
let containerNode = self.itemContainerNodes[i]
let isCentral = i == index
itemNode.setCentral(isCentral)
if isCentral {
containerNode.view.superview?.bringSubviewToFront(containerNode.view)
}
}
}
private var scrollStartPosition: (contentOffset: CGFloat, position: CGFloat)?
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
if self.scrollStartPosition == nil {
self.scrollStartPosition = (scrollView.contentOffset.y, self.currentPosition)
}
}
private let hapticFeedback = HapticFeedback()
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard !self.ignoreContentOffsetChange, let (startContentOffset, startPosition) = self.scrollStartPosition else {
return
}
let delta = scrollView.contentOffset.y - startContentOffset
let positionDelta = delta * 0.0005
var updatedPosition = startPosition + positionDelta
while updatedPosition >= 1.0 {
updatedPosition -= 1.0
}
while updatedPosition < 0.0 {
updatedPosition += 1.0
}
self.currentPosition = updatedPosition
let indexDelta = self.positionDelta
let index = max(0, Int(round(self.currentPosition / indexDelta)) % self.itemNodes.count)
if index != self.currentIndex {
self.currentIndex = index
if self.scrollNode.view.isTracking || self.scrollNode.view.isDecelerating {
self.hapticFeedback.tap()
}
}
if let size = self.validLayout {
self.updateLayout(size: size, transition: .immediate)
}
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
guard let (startContentOffset, _) = self.scrollStartPosition, abs(velocity.y) > 0.0 else {
return
}
let delta = self.positionDelta
let scrollDelta = targetContentOffset.pointee.y - startContentOffset
let positionDelta = scrollDelta * 0.0005
let positionCounts = round(positionDelta / delta)
let adjustedPositionDelta = delta * positionCounts
let adjustedScrollDelta = adjustedPositionDelta * 2000.0
targetContentOffset.pointee = CGPoint(x: 0.0, y: startContentOffset + adjustedScrollDelta)
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
self.resetScrollPosition()
let delta = self.positionDelta
let index = max(0, Int(round(self.currentPosition / delta)) % self.itemNodes.count)
self.scrollTo(index, playAnimation: true, duration: 0.2)
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.resetScrollPosition()
self.playSelectedSticker()
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayout = size
self.scrollNode.frame = CGRect(origin: CGPoint(), size: size)
if self.scrollNode.view.contentSize.width.isZero {
self.scrollNode.view.contentSize = CGSize(width: size.width, height: 10000000.0)
self.tapNode.frame = CGRect(origin: CGPoint(), size: self.scrollNode.view.contentSize)
self.resetScrollPosition()
}
let delta = self.positionDelta
let bounds = CGRect(origin: .zero, size: size)
let areaSize = CGSize(width: floor(size.width * 4.0), height: size.height * 2.2)
var visibleCount = 0
for i in 0 ..< self.itemNodes.count {
let itemNode = self.itemNodes[i]
let containerNode = self.itemContainerNodes[i]
var angle = CGFloat.pi * 0.5 + CGFloat(i) * delta * CGFloat.pi * 2.0 - self.currentPosition * CGFloat.pi * 2.0 - CGFloat.pi * 0.5
if angle < 0.0 {
angle = CGFloat.pi * 2.0 + angle
}
if angle > CGFloat.pi * 2.0 {
angle = angle - CGFloat.pi * 2.0
}
func calculateRelativeAngle(_ angle: CGFloat) -> CGFloat {
var relativeAngle = angle
if relativeAngle > CGFloat.pi {
relativeAngle = (2.0 * CGFloat.pi - relativeAngle) * -1.0
}
return relativeAngle
}
let relativeAngle = calculateRelativeAngle(angle)
let distance = abs(relativeAngle)
let point = CGPoint(
x: cos(angle),
y: sin(angle)
)
let itemFrame = CGRect(origin: CGPoint(x: -size.width - 0.5 * itemSize.width - 30.0 + point.x * areaSize.width * 0.5 - itemSize.width * 0.5, y: size.height * 0.5 + point.y * areaSize.height * 0.5 - itemSize.height * 0.5), size: itemSize)
containerNode.bounds = CGRect(origin: CGPoint(), size: itemFrame.size)
containerNode.position = CGPoint(x: itemFrame.midX, y: itemFrame.midY)
transition.updateTransformScale(node: containerNode, scale: 1.0 - distance * 0.65)
transition.updateAlpha(node: containerNode, alpha: 1.0 - distance * 0.5)
let isVisible = self.visibility && itemFrame.intersects(bounds)
itemNode.setVisible(isVisible)
if isVisible {
visibleCount += 1
}
itemNode.frame = CGRect(origin: CGPoint(), size: itemFrame.size)
itemNode.updateLayout(size: itemFrame.size, transition: transition)
}
}
}

View File

@ -152,10 +152,10 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC
if isPremiumSticker {
animationNode.completed = { [weak self] _ in
if let strongSelf = self, let animationNode = strongSelf.animationNode, let additionalAnimationNode = strongSelf.additionalAnimationNode {
Queue.mainQueue().after(0.1, {
Queue.mainQueue().async {
animationNode.play()
additionalAnimationNode.play()
})
}
}
}
}

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "dots@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,11 @@
{
"images" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1031,9 +1031,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
actions.context = strongSelf.context
var premiumReactions: [AvailableReactions.Reaction] = []
if canAddMessageReactions(message: topMessage), let availableReactions = availableReactions, let allowedReactions = allowedReactions {
var hasPremiumPlaceholder = false
filterReactions: for reaction in availableReactions.reactions {
@ -1043,9 +1041,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let aroundAnimation = reaction.aroundAnimation else {
continue
}
if reaction.isPremium {
premiumReactions.append(reaction)
}
if !reaction.isEnabled {
continue
}
@ -1094,9 +1089,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
if case .premium = value {
controller?.dismiss()
controller?.dismissWithoutContent()
let controller = PremiumReactionsScreen(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, reactions: premiumReactions)
let context = strongSelf.context
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumDemoScreen(context: context, subject: .uniqueReactions, action: {
let controller = PremiumIntroScreen(context: context, source: .reactions)
replaceImpl?(controller)
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
strongSelf.push(controller)
return
}
@ -11530,7 +11533,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
for item in results {
if let item = item {
if item.fileSize > Int64(premiumLimits.maxUploadFileParts) * 512 * 1024 {
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.Conversation_PremiumUploadFileTooLarge, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.Premium_FileTooLarge, text: strongSelf.presentationData.strings.Conversation_PremiumUploadFileTooLarge, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return
} else if item.fileSize > Int64(limits.maxUploadFileParts) * 512 * 1024 && !isPremium {
let context = strongSelf.context

View File

@ -159,11 +159,14 @@ public struct WebAppParameters {
public func generateWebAppThemeParams(_ presentationTheme: PresentationTheme) -> [String: Any] {
var backgroundColor = presentationTheme.list.plainBackgroundColor.rgb
var secondaryBackgroundColor = presentationTheme.list.blocksBackgroundColor.rgb
if backgroundColor == 0x000000 {
backgroundColor = presentationTheme.list.itemBlocksBackgroundColor.rgb
secondaryBackgroundColor = presentationTheme.list.plainBackgroundColor.rgb
}
return [
"bg_color": Int32(bitPattern: backgroundColor),
"secondary_bg_color": Int32(bitPattern: secondaryBackgroundColor),
"text_color": Int32(bitPattern: presentationTheme.list.itemPrimaryTextColor.rgb),
"hint_color": Int32(bitPattern: presentationTheme.list.itemSecondaryTextColor.rgb),
"link_color": Int32(bitPattern: presentationTheme.list.itemAccentColor.rgb),