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.Description" = "Unlock this sticker and more by subscribing to Telegram Premium.";
"Premium.Stickers.Proceed" = "Unlock Premium Stickers"; "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."; "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 %@"; "Chat.MultipleTypingPair" = "%@ and %@";
@ -7557,11 +7554,12 @@ Sorry for the inconvenience.";
"OldChannels.LeaveCommunities_1" = "Leave %@ Community"; "OldChannels.LeaveCommunities_1" = "Leave %@ Community";
"OldChannels.LeaveCommunities_any" = "Leave %@ Communities"; "OldChannels.LeaveCommunities_any" = "Leave %@ Communities";
"Premium.FileTooLarge" = "File Too Large";
"Premium.LimitReached" = "Limit Reached"; "Premium.LimitReached" = "Limit Reached";
"Premium.IncreaseLimit" = "Increase Limit"; "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.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 **%@** chats to a folder. You can increase this limit to **%@** by upgrading 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.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.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"; "Premium.MaxFavedStickersTitle" = "The Limit of %@ Stickers Reached";
@ -7573,6 +7571,12 @@ Sorry for the inconvenience.";
"Premium.Title" = "Telegram Premium"; "Premium.Title" = "Telegram Premium";
"Premium.Description" = "Go **beyond the limits**, get **exclusive features** and support us by subscribing to **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.DoubledLimits" = "Doubled Limits";
"Premium.DoubledLimitsInfo" = "Up to 1000 channels, 20 folders, 10 pins, 20 public links, 4 accounts and more."; "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.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.CopyProtectionSavingDisabledSecret" = "Saving is restricted";
"Conversation.CopyProtectionForwardingDisabledSecret" = "Forwards are 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."; "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"; "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 emojiViewProvider: ((String) -> UIView)?
private var maxCaptionLength: Int32?
public init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, isCaption: Bool = false, isAttachment: Bool = false, presentController: @escaping (ViewController) -> Void) { public init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, isCaption: Bool = false, isAttachment: Bool = false, presentController: @escaping (ViewController) -> Void) {
self.context = context self.context = context
self.presentationInterfaceState = presentationInterfaceState self.presentationInterfaceState = presentationInterfaceState
@ -323,6 +325,23 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
} }
self.updateSendButtonEnabled(isCaption || isAttachment, animated: false) 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)? public var sendPressed: ((NSAttributedString?) -> Void)?
@ -931,8 +950,8 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
private func updateCounterTextNode(transition: ContainedViewLayoutTransition) { private func updateCounterTextNode(transition: ContainedViewLayoutTransition) {
let inputTextMaxLength: Int32? let inputTextMaxLength: Int32?
if self.isCaption || self.isAttachment { if let maxCaptionLength = self.maxCaptionLength {
inputTextMaxLength = self.context.currentLimitsConfiguration.with { $0 }.maxMediaCaptionLength inputTextMaxLength = maxCaptionLength
} else { } else {
inputTextMaxLength = nil inputTextMaxLength = nil
} }
@ -1301,8 +1320,8 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
@objc func sendButtonPressed() { @objc func sendButtonPressed() {
let inputTextMaxLength: Int32? let inputTextMaxLength: Int32?
if self.isCaption || self.isAttachment { if let maxCaptionLength = self.maxCaptionLength {
inputTextMaxLength = self.context.currentLimitsConfiguration.with { $0 }.maxMediaCaptionLength inputTextMaxLength = maxCaptionLength
} else { } else {
inputTextMaxLength = nil inputTextMaxLength = nil
} }

View File

@ -1461,7 +1461,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
tabContextGesture(id, sourceNode, gesture, true) 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() self.displayNodeDidLoad()
} }
@ -2060,10 +2064,19 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
} }
strongSelf.chatListDisplayNode.containerNode.updateAvailableFilters(availableFilters, limit: filtersLimit) strongSelf.chatListDisplayNode.containerNode.updateAvailableFilters(availableFilters, limit: filtersLimit)
if !strongSelf.initializedFilters && selectedEntryId != strongSelf.chatListDisplayNode.containerNode.currentItemFilter { if !strongSelf.initializedFilters {
strongSelf.chatListDisplayNode.containerNode.switchToFilter(id: selectedEntryId, animated: false, completion: nil) 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 let isEmpty = resolvedItems.count <= 1 || displayTabsAtBottom
@ -3236,15 +3249,23 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let _ = (combineLatest(queue: .mainQueue(), let _ = (combineLatest(queue: .mainQueue(),
self.context.engine.peers.currentChatListFilters(), self.context.engine.peers.currentChatListFilters(),
chatListFilterItems(context: self.context) 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 { guard let strongSelf = self else {
return 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 let (_, filterItems) = filterItemsAndTotalCount
var items: [ContextMenuItem] = [] var items: [ContextMenuItem] = []
@ -3272,11 +3293,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
} }
if !presetList.isEmpty { 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 { for case let .filter(id, title, _, data) in presetList {
let filterType = chatListFilterType(data) let filterType = chatListFilterType(data)
var badge: ContextMenuActionBadge? var badge: ContextMenuActionBadge?
var isDisabled = false
if !isPremium && filterCount >= limits.maxFoldersCount {
isDisabled = true
}
for item in filterItems { for item in filterItems {
if item.0.id == id && item.1 != 0 { if item.0.id == id && item.1 != 0 {
badge = ContextMenuActionBadge(value: "\(item.1)", color: item.2 ? .accent : .inactive) 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 items.append(.action(ContextMenuActionItem(text: title, badge: badge, icon: { theme in
let imageName: String let imageName: String
switch filterType { if isDisabled {
case .generic: imageName = "Chat/Context Menu/Lock"
imageName = "Chat/Context Menu/List" } else {
case .unmuted: switch filterType {
imageName = "Chat/Context Menu/Unmute" case .generic:
case .unread: imageName = "Chat/Context Menu/List"
imageName = "Chat/Context Menu/MarkAsUnread" case .unmuted:
case .channels: imageName = "Chat/Context Menu/Unmute"
imageName = "Chat/Context Menu/Channels" case .unread:
case .groups: imageName = "Chat/Context Menu/MarkAsUnread"
imageName = "Chat/Context Menu/Groups" case .channels:
case .bots: imageName = "Chat/Context Menu/Channels"
imageName = "Chat/Context Menu/Bots" case .groups:
case .contacts: imageName = "Chat/Context Menu/Groups"
imageName = "Chat/Context Menu/User" case .bots:
case .nonContacts: imageName = "Chat/Context Menu/Bots"
imageName = "Chat/Context Menu/UnknownUser" case .contacts:
imageName = "Chat/Context Menu/User"
case .nonContacts:
imageName = "Chat/Context Menu/UnknownUser"
}
} }
return generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.contextMenu.primaryColor) return generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.contextMenu.primaryColor)
}, action: { _, f in }, action: { _, f in
@ -3308,8 +3340,23 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let strongSelf = self else { guard let strongSelf = self else {
return 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() let disposable = MetaDisposable()
self.pendingItemNode = (id, itemNode, disposable) self.pendingItemNode = (id, itemNode, disposable)
disposable.set((combineLatest(itemNode.listNode.ready, self.validLayoutReady) disposable.set((itemNode.listNode.ready
|> filter { $0 && $1 }
|> take(1) |> take(1)
|> deliverOnMainQueue).start(next: { [weak self, weak itemNode] _ in |> deliverOnMainQueue).start(next: { [weak self, weak itemNode] _ in
guard let strongSelf = self, let itemNode = itemNode, itemNode === strongSelf.pendingItemNode?.1 else { guard let strongSelf = self, let itemNode = itemNode, itemNode === strongSelf.pendingItemNode?.1 else {
return return
} }
guard let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, isReorderingFilters, isEditing) = strongSelf.validLayout else {
return
}
strongSelf.pendingItemNode = nil 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 }) { 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 previousId = strongSelf.selectedId
let offsetDirection: CGFloat = index < previousIndex ? 1.0 : -1.0 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)) 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 controller.navigationPresentation = .modal
let _ = (controller.result let _ = combineLatest(
|> take(1) queue: Queue.mainQueue(),
|> deliverOnMainQueue).start(next: { [weak controller] result in 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 { guard case let .result(peerIds, additionalCategoryIds) = result else {
controller?.dismiss() controller?.dismiss()
return return
} }
let (limits, premiumLimits) = data
var includePeers: [PeerId] = [] var includePeers: [PeerId] = []
for peerId in peerIds { for peerId in peerIds {
switch peerId { switch peerId {
@ -620,6 +628,26 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
} }
includePeers.sort() 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 = [] var categories: ChatListFilterPeerCategories = []
for id in additionalCategoryIds { for id in additionalCategoryIds {
if let index = categoryMapping.firstIndex(where: { $0.1.rawValue == id }) { 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) 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 let width: CGFloat
if self.unreadCount == 0 || self.isReordering || self.isEditing { if self.unreadCount == 0 || self.isReordering || self.isEditing || self.isDisabled {
if !self.isReordering { if !self.isReordering {
self.badgeContainerNode.alpha = 0.0 self.badgeContainerNode.alpha = 0.0
} }

View File

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

View File

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

View File

@ -2437,6 +2437,10 @@ public final class ContextController: ViewController, StandalonePresentableContr
self.dismiss(result: .default, completion: completion) self.dismiss(result: .default, completion: completion)
} }
public func dismissWithoutContent() {
self.dismiss(result: .dismissWithoutContent, completion: nil)
}
public func dismissNow() { public func dismissNow() {
self.presentingViewController?.dismiss(animated: false, completion: nil) self.presentingViewController?.dismiss(animated: false, completion: nil)
self.dismissed?() 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) typealias EnvironmentType = (ViewControllerComponentContainer.Environment, ScrollChildEnvironment)
let context: AccountContext 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.context = context
self.price = price
self.present = present
self.buy = buy
self.updateIsFocused = updateIsFocused
} }
static func ==(lhs: PremiumIntroScreenContentComponent, rhs: PremiumIntroScreenContentComponent) -> Bool { static func ==(lhs: PremiumIntroScreenContentComponent, rhs: PremiumIntroScreenContentComponent) -> Bool {
if lhs.context !== rhs.context { if lhs.context !== rhs.context {
return false return false
} }
if lhs.price != rhs.price {
return false
}
return true return true
} }
@ -862,6 +873,11 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
var items: [SectionGroupComponent.Item] = [] 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 var i = 0
for perk in state.configuration.perks { for perk in state.configuration.perks {
let iconBackgroundColors = gradientColors[i] let iconBackgroundColors = gradientColors[i]
@ -884,7 +900,48 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
) )
), ),
action: { 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 i += 1
@ -901,241 +958,6 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude), availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude),
transition: context.transition 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 context.add(section
.position(CGPoint(x: availableWidth / 2.0, y: size.height + section.size.height / 2.0)) .position(CGPoint(x: availableWidth / 2.0, y: size.height + section.size.height / 2.0))
.clipsToBounds(true) .clipsToBounds(true)
@ -1315,11 +1137,13 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
let context: AccountContext let context: AccountContext
let updateInProgress: (Bool) -> Void let updateInProgress: (Bool) -> Void
let present: (ViewController) -> Void
let completion: () -> 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.context = context
self.updateInProgress = updateInProgress self.updateInProgress = updateInProgress
self.present = present
self.completion = completion self.completion = completion
} }
@ -1338,6 +1162,8 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
var topContentOffset: CGFloat? var topContentOffset: CGFloat?
var bottomContentOffset: CGFloat? var bottomContentOffset: CGFloat?
var hasIdleAnimations = true
var inProgress = false var inProgress = false
var premiumProduct: InAppPurchaseManager.Product? var premiumProduct: InAppPurchaseManager.Product?
private var disposable: Disposable? 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 { func makeState() -> State {
@ -1427,7 +1258,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
} }
let star = star.update( 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), availableSize: CGSize(width: min(390.0, context.availableSize.width), height: 220.0),
transition: context.transition transition: context.transition
) )
@ -1504,7 +1335,14 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
let scrollContent = scrollContent.update( let scrollContent = scrollContent.update(
component: ScrollComponent<EnvironmentType>( component: ScrollComponent<EnvironmentType>(
content: AnyComponent(PremiumIntroScreenContentComponent( 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), contentInsets: UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: bottomPanel.size.height, right: 0.0),
contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in
@ -1610,12 +1448,16 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
self.context = context self.context = context
var updateInProgressImpl: ((Bool) -> Void)? var updateInProgressImpl: ((Bool) -> Void)?
var presentImpl: ((ViewController) -> Void)?
var completionImpl: (() -> Void)? var completionImpl: (() -> Void)?
super.init(context: context, component: PremiumIntroScreenComponent( super.init(context: context, component: PremiumIntroScreenComponent(
context: context, context: context,
updateInProgress: { inProgress in updateInProgress: { inProgress in
updateInProgressImpl?(inProgress) updateInProgressImpl?(inProgress)
}, },
present: { c in
presentImpl?(c)
},
completion: { completion: {
completionImpl?() completionImpl?()
} }
@ -1639,6 +1481,10 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
} }
} }
presentImpl = { [weak self] c in
self?.push(c)
}
completionImpl = { [weak self] in completionImpl = { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.view.addSubview(ConfettiView(frame: strongSelf.view.bounds)) strongSelf.view.addSubview(ConfettiView(frame: strongSelf.view.bounds))

View File

@ -16,16 +16,16 @@ import BundleIconComponent
import SolidRoundedButtonComponent import SolidRoundedButtonComponent
import Markdown 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 return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size)) 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.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.setLineWidth(2.0) context.setLineWidth(2.0)
context.setLineCap(.round) context.setLineCap(.round)
context.setStrokeColor(theme.actionSheet.inputClearButtonColor.cgColor) context.setStrokeColor(foregroundColor.cgColor)
context.move(to: CGPoint(x: 10.0, y: 10.0)) context.move(to: CGPoint(x: 10.0, y: 10.0))
context.addLine(to: CGPoint(x: 20.0, y: 20.0)) context.addLine(to: CGPoint(x: 20.0, y: 20.0))
@ -143,7 +143,7 @@ private class PremiumLimitAnimationComponent: Component {
self.badgeCountLabel = RollingLabel() self.badgeCountLabel = RollingLabel()
self.badgeCountLabel.font = Font.with(size: 24.0, design: .round, weight: .semibold, traits: []) self.badgeCountLabel.font = Font.with(size: 24.0, design: .round, weight: .semibold, traits: [])
self.badgeCountLabel.textColor = .white self.badgeCountLabel.textColor = .white
self.badgeCountLabel.text(num: 0) self.badgeCountLabel.configure(with: "0")
super.init(frame: frame) super.init(frame: frame)
@ -203,8 +203,8 @@ private class PremiumLimitAnimationComponent: Component {
self.badgeView.layer.add(rotateAnimation, forKey: "appearance2") self.badgeView.layer.add(rotateAnimation, forKey: "appearance2")
self.badgeView.layer.add(returnAnimation, forKey: "appearance3") self.badgeView.layer.add(returnAnimation, forKey: "appearance3")
if let badgeText = component.badgeText, let num = Int(badgeText) { if let badgeText = component.badgeText {
self.badgeCountLabel.text(num: num) 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.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.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)) self.badgeForeground.bounds = CGRect(origin: CGPoint(), size: CGSize(width: badgeSize.width * 3.0, height: badgeSize.height))
if self.badgeForeground.animation(forKey: "movement") == nil { 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) 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 { if let (image, theme) = state.cachedCloseImage, theme === environment.theme {
closeImage = image closeImage = image
} else { } else {
closeImage = generateCloseButtonImage(theme: theme)! closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: theme.actionSheet.inputClearButtonColor)!
state.cachedCloseImage = (closeImage, theme) 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)) .position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - closeButton.size.width, y: 28.0))
) )
var titleText = strings.Premium_LimitReached
let iconName: String let iconName: String
let badgeText: String let badgeText: String
let string: String let string: String
@ -669,20 +681,21 @@ private final class LimitSheetContent: CombinedComponent {
premiumValue = "\(premiumLimit)" premiumValue = "\(premiumLimit)"
badgePosition = CGFloat(component.count) / CGFloat(premiumLimit) badgePosition = CGFloat(component.count) / CGFloat(premiumLimit)
case .files: case .files:
let limit = Int64(state.limits.maxUploadFileParts) * 512 * 1024 let limit = Int64(state.limits.maxUploadFileParts) * 512 * 1024 + 1024 * 1024 * 100
let premiumLimit = Int64(state.limits.maxUploadFileParts) * 512 * 1024 let premiumLimit = Int64(state.premiumLimits.maxUploadFileParts) * 512 * 1024 + 1024 * 1024 * 100
iconName = "Premium/File" iconName = "Premium/File"
badgeText = dataSizeString(limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) 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 string = strings.Premium_MaxFileSizeText(dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))).string
defaultValue = "" defaultValue = ""
premiumValue = dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) premiumValue = dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))
badgePosition = 0.5 badgePosition = 0.5
titleText = strings.Premium_FileTooLarge
} }
let title = title.update( let title = title.update(
component: MultilineTextComponent( component: MultilineTextComponent(
text: .plain(NSAttributedString( text: .plain(NSAttributedString(
string: strings.Premium_LimitReached, string: titleText,
font: Font.semibold(17.0), font: Font.semibold(17.0),
textColor: theme.actionSheet.primaryTextColor, textColor: theme.actionSheet.primaryTextColor,
paragraphAlignment: .center 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 { class PremiumStarComponent: Component {
let isVisible: Bool let isVisible: Bool
let hasIdleAnimations: Bool
init(isVisible: Bool) { init(isVisible: Bool, hasIdleAnimations: Bool) {
self.isVisible = isVisible self.isVisible = isVisible
self.hasIdleAnimations = hasIdleAnimations
} }
static func ==(lhs: PremiumStarComponent, rhs: PremiumStarComponent) -> Bool { 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 { final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView {
@ -75,6 +77,7 @@ class PremiumStarComponent: Component {
private var previousInteractionTimestamp: Double = 0.0 private var previousInteractionTimestamp: Double = 0.0
private var timer: SwiftSignalKit.Timer? private var timer: SwiftSignalKit.Timer?
private var hasIdleAnimations = false
override init(frame: CGRect) { override init(frame: CGRect) {
self.sceneView = SCNView(frame: frame) self.sceneView = SCNView(frame: frame)
@ -249,7 +252,7 @@ class PremiumStarComponent: Component {
self.previousInteractionTimestamp = CACurrentMediaTime() self.previousInteractionTimestamp = CACurrentMediaTime()
self.timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in 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() let currentTimestamp = CACurrentMediaTime()
if currentTimestamp > strongSelf.previousInteractionTimestamp + 5.0 { if currentTimestamp > strongSelf.previousInteractionTimestamp + 5.0 {
strongSelf.playAppearanceAnimation() 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.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.sceneView.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)
self.hasIdleAnimations = component.hasIdleAnimations
return availableSize return availableSize
} }
} }

View File

@ -2,15 +2,86 @@ import Foundation
import UIKit import UIKit
import Display import Display
import AsyncDisplayKit import AsyncDisplayKit
import ComponentFlow
import TelegramCore import TelegramCore
import AccountContext import AccountContext
import ReactionSelectionNode import ReactionSelectionNode
import TelegramPresentationData import TelegramPresentationData
import AccountContext 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) 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 context: AccountContext
private let theme: PresentationTheme private let theme: PresentationTheme
private let reactions: [AvailableReactions.Reaction] private let reactions: [AvailableReactions.Reaction]
@ -167,7 +238,7 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
func playReaction() { func playReaction() {
let delta = self.positionDelta 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 { guard !self.playingIndices.contains(index) else {
return return
@ -223,7 +294,8 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
self.scrollStartPosition = (scrollView.contentOffset.x, self.currentPosition) self.scrollStartPosition = (scrollView.contentOffset.x, self.currentPosition)
} }
} }
private let hapticFeedback = HapticFeedback()
func scrollViewDidScroll(_ scrollView: UIScrollView) { func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard !self.ignoreContentOffsetChange, let (startContentOffset, startPosition) = self.scrollStartPosition else { guard !self.ignoreContentOffsetChange, let (startContentOffset, startPosition) = self.scrollStartPosition else {
return return
@ -241,10 +313,12 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
self.currentPosition = updatedPosition self.currentPosition = updatedPosition
let indexDelta = self.positionDelta 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 { if index != self.currentIndex {
self.currentIndex = index self.currentIndex = index
print(index) if self.scrollNode.view.isTracking || self.scrollNode.view.isDecelerating {
self.hapticFeedback.tap()
}
} }
if let size = self.validLayout { if let size = self.validLayout {
@ -272,7 +346,7 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
self.resetScrollPosition() self.resetScrollPosition()
let delta = self.positionDelta 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) 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) self.scrollNode.frame = CGRect(origin: CGPoint(), size: size)
if self.scrollNode.view.contentSize.width.isZero { 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.tapNode.frame = CGRect(origin: CGPoint(), size: self.scrollNode.view.contentSize)
self.resetScrollPosition() self.resetScrollPosition()
} }
let delta = self.positionDelta 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 { for i in 0 ..< self.itemNodes.count {
let itemNode = self.itemNodes[i] 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) 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.bounds = CGRect(origin: CGPoint(), size: itemFrame.size)
containerNode.position = itemFrame.center containerNode.position = CGPoint(x: itemFrame.midX, y: itemFrame.midY)
transition.updateTransformScale(node: containerNode, scale: 1.0 - distance * 0.45) transition.updateTransformScale(node: containerNode, scale: 1.0 - distance * 0.55)
itemNode.frame = CGRect(origin: CGPoint(), size: itemFrame.size) itemNode.frame = CGRect(origin: CGPoint(), size: itemFrame.size)
itemNode.updateLayout(size: itemFrame.size, isExpanded: false, largeExpanded: false, isPreviewing: false, transition: transition) itemNode.updateLayout(size: itemFrame.size, isExpanded: false, largeExpanded: false, isPreviewing: false, transition: transition)

View File

@ -1,4 +1,5 @@
import UIKit import UIKit
import Display
private extension UILabel { private extension UILabel {
func textWidth() -> CGFloat { func textWidth() -> CGFloat {
@ -32,22 +33,19 @@ open class RollingLabel: UILabel {
private let duration = 1.12 private let duration = 1.12
private let durationOffset = 0.2 private let durationOffset = 0.2
private let textsNotAnimated = [","] private let textsNotAnimated = [","]
public func text(num: Int) { public func setSuffix(suffix: String) {
self.configure(with: num) self.suffix = suffix
self.text = " "
self.animate()
} }
public func setPrefix(prefix: String) { func configure(with string: String) {
self.suffix = prefix fullText = string
}
private func configure(with number: Int) {
fullText = String(number)
clean() clean()
setupSubviews() setupSubviews()
self.text = " "
self.animate()
} }
private func animate(ascending: Bool = true) { private func animate(ascending: Bool = true) {
@ -99,9 +97,10 @@ open class RollingLabel: UILabel {
} }
stringArray.enumerated().forEach { index, text in stringArray.enumerated().forEach { index, text in
if textsNotAnimated.contains(text) { let nonDigits = CharacterSet.decimalDigits.inverted
if text.rangeOfCharacter(from: nonDigits) != nil {
let label = UILabel() 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.textColor = textColor
label.font = font label.font = font
label.text = text label.text = text
@ -118,28 +117,28 @@ open class RollingLabel: UILabel {
label.text = "0" label.text = "0"
label.textAlignment = .center label.textAlignment = .center
label.sizeToFit() label.sizeToFit()
createScrollLayer(to: label, text: text) createScrollLayer(to: label, text: text, index: index)
x += label.bounds.width 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() 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) scrollLayers.append(scrollLayer)
self.layer.addSublayer(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] = [] var textsForScroll: [String] = []
let max: Int let max: Int
var found = false var found = false
if let val = Int(text) { if let val = Int(text), index == 0 {
max = val max = val
found = true found = true
} else { } else {
@ -150,11 +149,11 @@ open class RollingLabel: UILabel {
let str = String(i) let str = String(i)
textsForScroll.append(str) textsForScroll.append(str)
} }
if !found { if !found && text != "9" {
textsForScroll.append(text) textsForScroll.append(text)
} }
var height: CGFloat = 0 var height: CGFloat = 0.0
for text in textsForScroll { for text in textsForScroll {
let label = UILabel() let label = UILabel()
label.text = text label.text = text
@ -179,17 +178,18 @@ open class RollingLabel: UILabel {
animation.duration = duration + offset animation.duration = duration + offset
animation.timingFunction = CAMediaTimingFunction(name: .easeOut) animation.timingFunction = CAMediaTimingFunction(name: .easeOut)
let verticalOffset = 20.0
if ascending { if ascending {
animation.fromValue = maxY animation.fromValue = maxY + verticalOffset
animation.toValue = 0 animation.toValue = 0
} else { } else {
animation.fromValue = 0 animation.fromValue = 0
animation.toValue = maxY animation.toValue = maxY + verticalOffset
} }
scrollLayer.scrollMode = .vertically scrollLayer.scrollMode = .vertically
scrollLayer.add(animation, forKey: nil) 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 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 { if isPremiumSticker {
animationNode.completed = { [weak self] _ in animationNode.completed = { [weak self] _ in
if let strongSelf = self, let animationNode = strongSelf.animationNode, let additionalAnimationNode = strongSelf.additionalAnimationNode { if let strongSelf = self, let animationNode = strongSelf.animationNode, let additionalAnimationNode = strongSelf.additionalAnimationNode {
Queue.mainQueue().after(0.1, { Queue.mainQueue().async {
animationNode.play() animationNode.play()
additionalAnimationNode.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 actions.context = strongSelf.context
var premiumReactions: [AvailableReactions.Reaction] = []
if canAddMessageReactions(message: topMessage), let availableReactions = availableReactions, let allowedReactions = allowedReactions { if canAddMessageReactions(message: topMessage), let availableReactions = availableReactions, let allowedReactions = allowedReactions {
var hasPremiumPlaceholder = false var hasPremiumPlaceholder = false
filterReactions: for reaction in availableReactions.reactions { filterReactions: for reaction in availableReactions.reactions {
@ -1043,9 +1041,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let aroundAnimation = reaction.aroundAnimation else { guard let aroundAnimation = reaction.aroundAnimation else {
continue continue
} }
if reaction.isPremium {
premiumReactions.append(reaction)
}
if !reaction.isEnabled { if !reaction.isEnabled {
continue continue
} }
@ -1094,9 +1089,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
if case .premium = value { 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) strongSelf.push(controller)
return return
} }
@ -11530,7 +11533,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
for item in results { for item in results {
if let item = item { if let item = item {
if item.fileSize > Int64(premiumLimits.maxUploadFileParts) * 512 * 1024 { 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 return
} else if item.fileSize > Int64(limits.maxUploadFileParts) * 512 * 1024 && !isPremium { } else if item.fileSize > Int64(limits.maxUploadFileParts) * 512 * 1024 && !isPremium {
let context = strongSelf.context let context = strongSelf.context

View File

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