mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Various improvements
This commit is contained in:
parent
ce03f9dac3
commit
21f06b62fc
@ -7541,9 +7541,6 @@ Sorry for the inconvenience.";
|
||||
"Premium.Stickers.Description" = "Unlock this sticker and more by subscribing to Telegram Premium.";
|
||||
"Premium.Stickers.Proceed" = "Unlock Premium Stickers";
|
||||
|
||||
"Premium.Reactions.Description" = "Unlock additional reactions by subscribing to Telegram Premium.";
|
||||
"Premium.Reactions.Proceed" = "Unlock Additional Reactions";
|
||||
|
||||
"AccessDenied.LocationPreciseDenied" = "To share your specific location in this chat, please go to Settings > Privacy > Location Services > Telegram and set Precise Location to On.";
|
||||
|
||||
"Chat.MultipleTypingPair" = "%@ and %@";
|
||||
@ -7557,11 +7554,12 @@ Sorry for the inconvenience.";
|
||||
"OldChannels.LeaveCommunities_1" = "Leave %@ Community";
|
||||
"OldChannels.LeaveCommunities_any" = "Leave %@ Communities";
|
||||
|
||||
"Premium.FileTooLarge" = "File Too Large";
|
||||
"Premium.LimitReached" = "Limit Reached";
|
||||
"Premium.IncreaseLimit" = "Increase Limit";
|
||||
|
||||
"Premium.MaxFoldersCountText" = "You have reached the limit of **%@** folders. You can double the limit to **%@** folders by subscribing to **Telegram Premium**.";
|
||||
"Premium.MaxChatsInFolderCountText" = "Sorry, you can't add more than **%@** chats to a folder. You can increase this limit to **%@** by upgrading to **Telegram Premium**.";
|
||||
"Premium.MaxFoldersCountText" = "You have reached the limit of **%1$@** folders. You can double the limit to **%2$@** folders by subscribing to **Telegram Premium**.";
|
||||
"Premium.MaxChatsInFolderCountText" = "Sorry, you can't add more than **%1$@** chats to a folder. You can increase this limit to **%2$@** by upgrading to **Telegram Premium**.";
|
||||
"Premium.MaxFileSizeText" = "Double this limit to %@ per file by subscribing to **Telegram Premium**.";
|
||||
"Premium.MaxPinsText" = "Sorry, you can't pin more than **%1$@** chats to the top. Unpin some of the currently pinned ones or subscribe to **Telegram Premium** to double the limit to **%2$@** chats.";
|
||||
"Premium.MaxFavedStickersTitle" = "The Limit of %@ Stickers Reached";
|
||||
@ -7573,6 +7571,12 @@ Sorry for the inconvenience.";
|
||||
"Premium.Title" = "Telegram Premium";
|
||||
"Premium.Description" = "Go **beyond the limits**, get **exclusive features** and support us by subscribing to **Telegram Premium**.";
|
||||
|
||||
"Premium.PersonalTitle" = "%@ is a subscriber of Telegram Premium";
|
||||
"Premium.PersonalDescription" = "Owners of **Telegram Premium** accounts have exclusive access to multiple additional features.";
|
||||
|
||||
"Premium.SubscribedTitle" = "You are all set!";
|
||||
"Premium.SubscribedDescription" = "Thank you for subsribing to **Telegram Premium**. Here's what is now unlocked.";
|
||||
|
||||
"Premium.DoubledLimits" = "Doubled Limits";
|
||||
"Premium.DoubledLimitsInfo" = "Up to 1000 channels, 20 folders, 10 pins, 20 public links, 4 accounts and more.";
|
||||
|
||||
@ -7610,6 +7614,8 @@ Sorry for the inconvenience.";
|
||||
|
||||
"Premium.Terms" = "By purchasing a Premium subscription, you agree to our [Terms of Service](terms) and [Privacy Policy](privacy).";
|
||||
|
||||
"Premium.MoreAboutPremium" = "More About Premium";
|
||||
|
||||
"Conversation.CopyProtectionSavingDisabledSecret" = "Saving is restricted";
|
||||
"Conversation.CopyProtectionForwardingDisabledSecret" = "Forwards are restricted";
|
||||
|
||||
@ -7622,3 +7628,5 @@ Sorry for the inconvenience.";
|
||||
"Conversation.PremiumUploadFileTooLarge" = "File could not be sent, because it is larger than 4 GB.\n\nYou can send as many files as you like, but each must be smaller than 4 GB.";
|
||||
|
||||
"SponsoredMessageMenu.Hide" = "Hide";
|
||||
|
||||
"ChatListFolder.MaxChatsInFolder" = "Sorry, you can't add more than %d chats to a folder.";
|
||||
|
@ -244,6 +244,8 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
|
||||
private var emojiViewProvider: ((String) -> UIView)?
|
||||
|
||||
private var maxCaptionLength: Int32?
|
||||
|
||||
public init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, isCaption: Bool = false, isAttachment: Bool = false, presentController: @escaping (ViewController) -> Void) {
|
||||
self.context = context
|
||||
self.presentationInterfaceState = presentationInterfaceState
|
||||
@ -323,6 +325,23 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
}
|
||||
|
||||
self.updateSendButtonEnabled(isCaption || isAttachment, animated: false)
|
||||
|
||||
if self.isCaption || self.isAttachment {
|
||||
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|
||||
|> mapToSignal { peer -> Signal<Int32, NoError> in
|
||||
if let peer = peer {
|
||||
return self.context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.UserLimits.init(isPremium: peer.isPremium))
|
||||
|> map { limits in
|
||||
return limits.maxCaptionLengthCount
|
||||
}
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { [weak self] maxCaptionLength in
|
||||
self?.maxCaptionLength = maxCaptionLength
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public var sendPressed: ((NSAttributedString?) -> Void)?
|
||||
@ -931,8 +950,8 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
|
||||
private func updateCounterTextNode(transition: ContainedViewLayoutTransition) {
|
||||
let inputTextMaxLength: Int32?
|
||||
if self.isCaption || self.isAttachment {
|
||||
inputTextMaxLength = self.context.currentLimitsConfiguration.with { $0 }.maxMediaCaptionLength
|
||||
if let maxCaptionLength = self.maxCaptionLength {
|
||||
inputTextMaxLength = maxCaptionLength
|
||||
} else {
|
||||
inputTextMaxLength = nil
|
||||
}
|
||||
@ -1301,8 +1320,8 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
|
||||
@objc func sendButtonPressed() {
|
||||
let inputTextMaxLength: Int32?
|
||||
if self.isCaption || self.isAttachment {
|
||||
inputTextMaxLength = self.context.currentLimitsConfiguration.with { $0 }.maxMediaCaptionLength
|
||||
if let maxCaptionLength = self.maxCaptionLength {
|
||||
inputTextMaxLength = maxCaptionLength
|
||||
} else {
|
||||
inputTextMaxLength = nil
|
||||
}
|
||||
|
@ -1461,7 +1461,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
tabContextGesture(id, sourceNode, gesture, true)
|
||||
}
|
||||
|
||||
self.ready.set(self.chatListDisplayNode.containerNode.ready)
|
||||
if case .group = self.groupId {
|
||||
self.ready.set(self.chatListDisplayNode.containerNode.ready)
|
||||
} else {
|
||||
self.ready.set(.never())
|
||||
}
|
||||
|
||||
self.displayNodeDidLoad()
|
||||
}
|
||||
@ -2060,10 +2064,19 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
strongSelf.chatListDisplayNode.containerNode.updateAvailableFilters(availableFilters, limit: filtersLimit)
|
||||
|
||||
if !strongSelf.initializedFilters && selectedEntryId != strongSelf.chatListDisplayNode.containerNode.currentItemFilter {
|
||||
strongSelf.chatListDisplayNode.containerNode.switchToFilter(id: selectedEntryId, animated: false, completion: nil)
|
||||
if !strongSelf.initializedFilters {
|
||||
if selectedEntryId != strongSelf.chatListDisplayNode.containerNode.currentItemFilter {
|
||||
strongSelf.chatListDisplayNode.containerNode.switchToFilter(id: selectedEntryId, animated: false, completion: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.ready.set(strongSelf.chatListDisplayNode.containerNode.currentItemNode.ready)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
strongSelf.ready.set(strongSelf.chatListDisplayNode.containerNode.currentItemNode.ready)
|
||||
}
|
||||
strongSelf.initializedFilters = true
|
||||
}
|
||||
strongSelf.initializedFilters = true
|
||||
|
||||
|
||||
let isEmpty = resolvedItems.count <= 1 || displayTabsAtBottom
|
||||
|
||||
@ -3236,15 +3249,23 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
let _ = (combineLatest(queue: .mainQueue(),
|
||||
self.context.engine.peers.currentChatListFilters(),
|
||||
chatListFilterItems(context: self.context)
|
||||
|> take(1)
|
||||
|> take(1),
|
||||
context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId),
|
||||
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false),
|
||||
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true)
|
||||
)
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] presetList, filterItemsAndTotalCount in
|
||||
|> deliverOnMainQueue).start(next: { [weak self] presetList, filterItemsAndTotalCount, result in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = strongSelf.context.engine.peers.markChatListFeaturedFiltersAsSeen().start()
|
||||
let (accountPeer, limits, _) = result
|
||||
let isPremium = accountPeer?.isPremium ?? false
|
||||
|
||||
|
||||
let _ = strongSelf.context.engine.peers.markChatListFeaturedFiltersAsSeen().start()
|
||||
let (_, filterItems) = filterItemsAndTotalCount
|
||||
|
||||
var items: [ContextMenuItem] = []
|
||||
@ -3272,11 +3293,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
|
||||
if !presetList.isEmpty {
|
||||
items.append(.separator)
|
||||
|
||||
if presetList.count > 1 {
|
||||
items.append(.separator)
|
||||
}
|
||||
var filterCount = 0
|
||||
for case let .filter(id, title, _, data) in presetList {
|
||||
let filterType = chatListFilterType(data)
|
||||
var badge: ContextMenuActionBadge?
|
||||
var isDisabled = false
|
||||
if !isPremium && filterCount >= limits.maxFoldersCount {
|
||||
isDisabled = true
|
||||
}
|
||||
|
||||
for item in filterItems {
|
||||
if item.0.id == id && item.1 != 0 {
|
||||
badge = ContextMenuActionBadge(value: "\(item.1)", color: item.2 ? .accent : .inactive)
|
||||
@ -3284,23 +3312,27 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
items.append(.action(ContextMenuActionItem(text: title, badge: badge, icon: { theme in
|
||||
let imageName: String
|
||||
switch filterType {
|
||||
case .generic:
|
||||
imageName = "Chat/Context Menu/List"
|
||||
case .unmuted:
|
||||
imageName = "Chat/Context Menu/Unmute"
|
||||
case .unread:
|
||||
imageName = "Chat/Context Menu/MarkAsUnread"
|
||||
case .channels:
|
||||
imageName = "Chat/Context Menu/Channels"
|
||||
case .groups:
|
||||
imageName = "Chat/Context Menu/Groups"
|
||||
case .bots:
|
||||
imageName = "Chat/Context Menu/Bots"
|
||||
case .contacts:
|
||||
imageName = "Chat/Context Menu/User"
|
||||
case .nonContacts:
|
||||
imageName = "Chat/Context Menu/UnknownUser"
|
||||
if isDisabled {
|
||||
imageName = "Chat/Context Menu/Lock"
|
||||
} else {
|
||||
switch filterType {
|
||||
case .generic:
|
||||
imageName = "Chat/Context Menu/List"
|
||||
case .unmuted:
|
||||
imageName = "Chat/Context Menu/Unmute"
|
||||
case .unread:
|
||||
imageName = "Chat/Context Menu/MarkAsUnread"
|
||||
case .channels:
|
||||
imageName = "Chat/Context Menu/Channels"
|
||||
case .groups:
|
||||
imageName = "Chat/Context Menu/Groups"
|
||||
case .bots:
|
||||
imageName = "Chat/Context Menu/Bots"
|
||||
case .contacts:
|
||||
imageName = "Chat/Context Menu/User"
|
||||
case .nonContacts:
|
||||
imageName = "Chat/Context Menu/UnknownUser"
|
||||
}
|
||||
}
|
||||
return generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.contextMenu.primaryColor)
|
||||
}, action: { _, f in
|
||||
@ -3308,8 +3340,23 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.selectTab(id: .filter(id))
|
||||
if isDisabled {
|
||||
let context = strongSelf.context
|
||||
var replaceImpl: ((ViewController) -> Void)?
|
||||
let controller = PremiumLimitScreen(context: context, subject: .folders, count: strongSelf.tabContainerNode.filtersCount, action: {
|
||||
let controller = PremiumIntroScreen(context: context, source: .folders)
|
||||
replaceImpl?(controller)
|
||||
})
|
||||
replaceImpl = { [weak controller] c in
|
||||
controller?.replace(with: c)
|
||||
}
|
||||
strongSelf.push(controller)
|
||||
} else {
|
||||
strongSelf.selectTab(id: .filter(id))
|
||||
}
|
||||
})))
|
||||
|
||||
filterCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -878,20 +878,28 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
let disposable = MetaDisposable()
|
||||
self.pendingItemNode = (id, itemNode, disposable)
|
||||
|
||||
disposable.set((combineLatest(itemNode.listNode.ready, self.validLayoutReady)
|
||||
|> filter { $0 && $1 }
|
||||
disposable.set((itemNode.listNode.ready
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self, weak itemNode] _ in
|
||||
guard let strongSelf = self, let itemNode = itemNode, itemNode === strongSelf.pendingItemNode?.1 else {
|
||||
return
|
||||
}
|
||||
guard let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, isReorderingFilters, isEditing) = strongSelf.validLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.pendingItemNode = nil
|
||||
|
||||
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.35, curve: .spring) : .immediate
|
||||
guard let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, isReorderingFilters, isEditing) = strongSelf.validLayout else {
|
||||
strongSelf.itemNodes[id] = itemNode
|
||||
strongSelf.addSubnode(itemNode)
|
||||
|
||||
strongSelf.selectedId = id
|
||||
strongSelf.applyItemNodeAsCurrent(id: id, itemNode: itemNode)
|
||||
strongSelf.currentItemFilterUpdated?(strongSelf.currentItemFilter, strongSelf.transitionFraction, .immediate, false)
|
||||
|
||||
completion?()
|
||||
return
|
||||
}
|
||||
|
||||
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.35, curve: .spring) : .immediate
|
||||
if let previousIndex = strongSelf.availableFilters.firstIndex(where: { $0.id == strongSelf.selectedId }), let index = strongSelf.availableFilters.firstIndex(where: { $0.id == id }) {
|
||||
let previousId = strongSelf.selectedId
|
||||
let offsetDirection: CGFloat = index < previousIndex ? 1.0 : -1.0
|
||||
|
@ -601,14 +601,22 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
|
||||
|
||||
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .chatSelection(title: presentationData.strings.ChatListFolder_IncludeChatsTitle, selectedChats: Set(filterData.includePeers.peers), additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories), chatListFilters: allFilters), options: [], filters: [], alwaysEnabled: true, limit: 100))
|
||||
controller.navigationPresentation = .modal
|
||||
let _ = (controller.result
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak controller] result in
|
||||
let _ = combineLatest(
|
||||
queue: Queue.mainQueue(),
|
||||
controller.result |> take(1),
|
||||
context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false),
|
||||
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true)
|
||||
)
|
||||
)
|
||||
.start(next: { [weak controller] result, data in
|
||||
guard case let .result(peerIds, additionalCategoryIds) = result else {
|
||||
controller?.dismiss()
|
||||
return
|
||||
}
|
||||
|
||||
let (limits, premiumLimits) = data
|
||||
|
||||
var includePeers: [PeerId] = []
|
||||
for peerId in peerIds {
|
||||
switch peerId {
|
||||
@ -620,6 +628,26 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
|
||||
}
|
||||
includePeers.sort()
|
||||
|
||||
if includePeers.count > limits.maxFolderChatsCount {
|
||||
if includePeers.count > premiumLimits.maxFolderChatsCount {
|
||||
let alertController = textAlertController(context: context, title: nil, text: presentationData.strings.ChatListFolder_MaxChatsInFolder(Int(premiumLimits.maxFolderChatsCount)).string, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
|
||||
controller?.present(alertController, in: .window(.root))
|
||||
return
|
||||
}
|
||||
|
||||
var replaceImpl: ((ViewController) -> Void)?
|
||||
let limitController = PremiumLimitScreen(context: context, subject: .chatsInFolder, count: Int32(includePeers.count), action: {
|
||||
let introController = PremiumIntroScreen(context: context, source: .chatsPerFolder)
|
||||
replaceImpl?(introController)
|
||||
})
|
||||
replaceImpl = { [weak controller] c in
|
||||
controller?.replace(with: c)
|
||||
}
|
||||
controller?.push(limitController)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var categories: ChatListFilterPeerCategories = []
|
||||
for id in additionalCategoryIds {
|
||||
if let index = categoryMapping.firstIndex(where: { $0.1.rawValue == id }) {
|
||||
|
@ -307,7 +307,7 @@ private final class ItemNode: ASDisplayNode {
|
||||
self.badgeTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((badgeBackgroundFrame.width - badgeSize.width) / 2.0), y: floor((badgeBackgroundFrame.height - badgeSize.height) / 2.0)), size: badgeSize)
|
||||
|
||||
let width: CGFloat
|
||||
if self.unreadCount == 0 || self.isReordering || self.isEditing {
|
||||
if self.unreadCount == 0 || self.isReordering || self.isEditing || self.isDisabled {
|
||||
if !self.isReordering {
|
||||
self.badgeContainerNode.alpha = 0.0
|
||||
}
|
||||
|
@ -2,13 +2,20 @@ import Foundation
|
||||
import UIKit
|
||||
|
||||
public final class List<ChildEnvironment: Equatable>: CombinedComponent {
|
||||
public enum Direction {
|
||||
case horizontal
|
||||
case vertical
|
||||
}
|
||||
|
||||
public typealias EnvironmentType = ChildEnvironment
|
||||
|
||||
private let items: [AnyComponentWithIdentity<ChildEnvironment>]
|
||||
private let direction: Direction
|
||||
private let appear: Transition.Appear
|
||||
|
||||
public init(_ items: [AnyComponentWithIdentity<ChildEnvironment>], appear: Transition.Appear = .default()) {
|
||||
public init(_ items: [AnyComponentWithIdentity<ChildEnvironment>], direction: Direction = .vertical, appear: Transition.Appear = .default()) {
|
||||
self.items = items
|
||||
self.direction = direction
|
||||
self.appear = appear
|
||||
}
|
||||
|
||||
@ -16,6 +23,9 @@ public final class List<ChildEnvironment: Equatable>: CombinedComponent {
|
||||
if lhs.items != rhs.items {
|
||||
return false
|
||||
}
|
||||
if lhs.direction != rhs.direction {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -35,11 +45,19 @@ public final class List<ChildEnvironment: Equatable>: CombinedComponent {
|
||||
|
||||
var nextOrigin: CGFloat = 0.0
|
||||
for child in updatedChildren {
|
||||
let position: CGPoint
|
||||
switch context.component.direction {
|
||||
case .horizontal:
|
||||
position = CGPoint(x: nextOrigin + child.size.width / 2.0, y: child.size.height / 2.0)
|
||||
nextOrigin += child.size.width
|
||||
case .vertical:
|
||||
position = CGPoint(x: child.size.width / 2.0, y: nextOrigin + child.size.height / 2.0)
|
||||
nextOrigin += child.size.height
|
||||
}
|
||||
context.add(child
|
||||
.position(CGPoint(x: child.size.width / 2.0, y: nextOrigin + child.size.height / 2.0))
|
||||
.position(position)
|
||||
.appear(context.component.appear)
|
||||
)
|
||||
nextOrigin += child.size.height
|
||||
}
|
||||
|
||||
return context.availableSize
|
||||
|
@ -71,9 +71,7 @@ public final class ComponentHostView<EnvironmentType>: UIView {
|
||||
}
|
||||
|
||||
let isEnvironmentUpdated = context.erasedEnvironment.calculateIsUpdated()
|
||||
if isEnvironmentUpdated {
|
||||
context.erasedEnvironment._isUpdated = false
|
||||
}
|
||||
|
||||
|
||||
if !forceUpdate, !isEnvironmentUpdated, let currentComponent = self.currentComponent, let currentContainerSize = self.currentContainerSize, let currentSize = self.currentSize {
|
||||
if currentContainerSize == containerSize && currentComponent == component {
|
||||
@ -98,6 +96,10 @@ public final class ComponentHostView<EnvironmentType>: UIView {
|
||||
transition.setFrame(view: componentView, frame: CGRect(origin: CGPoint(), size: updatedSize))
|
||||
}
|
||||
|
||||
if isEnvironmentUpdated {
|
||||
context.erasedEnvironment._isUpdated = false
|
||||
}
|
||||
|
||||
self.isUpdating = false
|
||||
|
||||
return updatedSize
|
||||
|
@ -2437,6 +2437,10 @@ public final class ContextController: ViewController, StandalonePresentableContr
|
||||
self.dismiss(result: .default, completion: completion)
|
||||
}
|
||||
|
||||
public func dismissWithoutContent() {
|
||||
self.dismiss(result: .dismissWithoutContent, completion: nil)
|
||||
}
|
||||
|
||||
public func dismissNow() {
|
||||
self.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
self.dismissed?()
|
||||
|
44
submodules/PremiumUI/Sources/DemoComponent.swift
Normal file
44
submodules/PremiumUI/Sources/DemoComponent.swift
Normal 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)
|
||||
}
|
||||
}
|
892
submodules/PremiumUI/Sources/PremiumDemoScreen.swift
Normal file
892
submodules/PremiumUI/Sources/PremiumDemoScreen.swift
Normal 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
|
||||
}
|
||||
}
|
||||
|
@ -718,15 +718,26 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
typealias EnvironmentType = (ViewControllerComponentContainer.Environment, ScrollChildEnvironment)
|
||||
|
||||
let context: AccountContext
|
||||
let price: String?
|
||||
let present: (ViewController) -> Void
|
||||
let buy: () -> Void
|
||||
let updateIsFocused: (Bool) -> Void
|
||||
|
||||
init(context: AccountContext) {
|
||||
init(context: AccountContext, price: String?, present: @escaping (ViewController) -> Void, buy: @escaping () -> Void, updateIsFocused: @escaping (Bool) -> Void) {
|
||||
self.context = context
|
||||
self.price = price
|
||||
self.present = present
|
||||
self.buy = buy
|
||||
self.updateIsFocused = updateIsFocused
|
||||
}
|
||||
|
||||
static func ==(lhs: PremiumIntroScreenContentComponent, rhs: PremiumIntroScreenContentComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.price != rhs.price {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@ -862,6 +873,11 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
|
||||
var items: [SectionGroupComponent.Item] = []
|
||||
|
||||
let accountContext = context.component.context
|
||||
let present = context.component.present
|
||||
let buy = context.component.buy
|
||||
let updateIsFocused = context.component.updateIsFocused
|
||||
let price = context.component.price
|
||||
var i = 0
|
||||
for perk in state.configuration.perks {
|
||||
let iconBackgroundColors = gradientColors[i]
|
||||
@ -884,7 +900,48 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
)
|
||||
),
|
||||
action: {
|
||||
var demoSubject: PremiumDemoScreen.Subject
|
||||
switch perk {
|
||||
case .doubleLimits:
|
||||
return
|
||||
case .moreUpload:
|
||||
demoSubject = .moreUpload
|
||||
case .fasterDownload:
|
||||
demoSubject = .fasterDownload
|
||||
case .voiceToText:
|
||||
demoSubject = .voiceToText
|
||||
case .noAds:
|
||||
demoSubject = .noAds
|
||||
case .uniqueReactions:
|
||||
demoSubject = .uniqueReactions
|
||||
case .premiumStickers:
|
||||
demoSubject = .premiumStickers
|
||||
case .advancedChatManagement:
|
||||
demoSubject = .advancedChatManagement
|
||||
case .profileBadge:
|
||||
demoSubject = .profileBadge
|
||||
case .animatedUserpics:
|
||||
demoSubject = .animatedUserpics
|
||||
}
|
||||
|
||||
var dismissImpl: (() -> Void)?
|
||||
let controller = PremiumDemoScreen(
|
||||
context: accountContext,
|
||||
subject: demoSubject,
|
||||
source: .intro(price),
|
||||
action: {
|
||||
dismissImpl?()
|
||||
buy()
|
||||
}
|
||||
)
|
||||
controller.disposed = {
|
||||
updateIsFocused(false)
|
||||
}
|
||||
present(controller)
|
||||
dismissImpl = { [weak controller] in
|
||||
controller?.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
updateIsFocused(true)
|
||||
}
|
||||
))
|
||||
i += 1
|
||||
@ -901,241 +958,6 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude),
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
//
|
||||
|
||||
// let section = section.update(
|
||||
// component: SectionGroupComponent(
|
||||
// items: [
|
||||
// SectionGroupComponent.Item(
|
||||
// AnyComponentWithIdentity(
|
||||
// id: "limits",
|
||||
// component: AnyComponent(
|
||||
// PerkComponent(
|
||||
// iconName: "Premium/Perk/Limits",
|
||||
// iconBackgroundColors: [
|
||||
// UIColor(rgb: 0xF28528),
|
||||
// UIColor(rgb: 0xEF7633)
|
||||
// ],
|
||||
// title: strings.Premium_DoubledLimits,
|
||||
// titleColor: titleColor,
|
||||
// subtitle: strings.Premium_DoubledLimitsInfo,
|
||||
// subtitleColor: subtitleColor,
|
||||
// arrowColor: arrowColor
|
||||
// )
|
||||
// )
|
||||
// ),
|
||||
// action: {
|
||||
//
|
||||
// }
|
||||
// ),
|
||||
// SectionGroupComponent.Item(
|
||||
// AnyComponentWithIdentity(
|
||||
// id: "upload",
|
||||
// component: AnyComponent(
|
||||
// PerkComponent(
|
||||
// iconName: "Premium/Perk/Upload",
|
||||
// iconBackgroundColors: [
|
||||
// UIColor(rgb: 0xEA5F43),
|
||||
// UIColor(rgb: 0xE7504E)
|
||||
// ],
|
||||
// title: strings.Premium_UploadSize,
|
||||
// titleColor: titleColor,
|
||||
// subtitle: strings.Premium_UploadSizeInfo,
|
||||
// subtitleColor: subtitleColor,
|
||||
// arrowColor: arrowColor
|
||||
// )
|
||||
// )
|
||||
// ),
|
||||
// action: {
|
||||
//
|
||||
// }
|
||||
// ),
|
||||
// SectionGroupComponent.Item(
|
||||
// AnyComponentWithIdentity(
|
||||
// id: "speed",
|
||||
// component: AnyComponent(
|
||||
// PerkComponent(
|
||||
// iconName: "Premium/Perk/Speed",
|
||||
// iconBackgroundColors: [
|
||||
// UIColor(rgb: 0xDE4768),
|
||||
// UIColor(rgb: 0xD54D82)
|
||||
// ],
|
||||
// title: strings.Premium_FasterSpeed,
|
||||
// titleColor: titleColor,
|
||||
// subtitle: strings.Premium_FasterSpeedInfo,
|
||||
// subtitleColor: subtitleColor,
|
||||
// arrowColor: arrowColor
|
||||
// )
|
||||
// )
|
||||
// ),
|
||||
// action: {
|
||||
//
|
||||
// }
|
||||
// ),
|
||||
// SectionGroupComponent.Item(
|
||||
// AnyComponentWithIdentity(
|
||||
// id: "voice",
|
||||
// component: AnyComponent(
|
||||
// PerkComponent(
|
||||
// iconName: "Premium/Perk/Voice",
|
||||
// iconBackgroundColors: [
|
||||
// UIColor(rgb: 0xDE4768),
|
||||
// UIColor(rgb: 0xD54D82)
|
||||
// ],
|
||||
// title: strings.Premium_VoiceToText,
|
||||
// titleColor: titleColor,
|
||||
// subtitle: strings.Premium_VoiceToTextInfo,
|
||||
// subtitleColor: subtitleColor,
|
||||
// arrowColor: arrowColor
|
||||
// )
|
||||
// )
|
||||
// ),
|
||||
// action: {
|
||||
//
|
||||
// }
|
||||
// ),
|
||||
// SectionGroupComponent.Item(
|
||||
// AnyComponentWithIdentity(
|
||||
// id: "noAds",
|
||||
// component: AnyComponent(
|
||||
// PerkComponent(
|
||||
// iconName: "Premium/Perk/NoAds",
|
||||
// iconBackgroundColors: [
|
||||
// UIColor(rgb: 0xC654A8),
|
||||
// UIColor(rgb: 0xBE5AC2)
|
||||
// ],
|
||||
// title: strings.Premium_NoAds,
|
||||
// titleColor: titleColor,
|
||||
// subtitle: strings.Premium_NoAdsInfo,
|
||||
// subtitleColor: subtitleColor,
|
||||
// arrowColor: arrowColor
|
||||
// )
|
||||
// )
|
||||
// ),
|
||||
// action: {
|
||||
//
|
||||
// }
|
||||
// ),
|
||||
// SectionGroupComponent.Item(
|
||||
// AnyComponentWithIdentity(
|
||||
// id: "reactions",
|
||||
// component: AnyComponent(
|
||||
// PerkComponent(
|
||||
// iconName: "Premium/Perk/Reactions",
|
||||
// iconBackgroundColors: [
|
||||
// UIColor(rgb: 0xAF62E9),
|
||||
// UIColor(rgb: 0xA668FF)
|
||||
// ],
|
||||
// title: strings.Premium_Reactions,
|
||||
// titleColor: titleColor,
|
||||
// subtitle: strings.Premium_ReactionsInfo,
|
||||
// subtitleColor: subtitleColor,
|
||||
// arrowColor: arrowColor
|
||||
// )
|
||||
// )
|
||||
// ),
|
||||
// action: {
|
||||
//
|
||||
// }
|
||||
// ),
|
||||
// SectionGroupComponent.Item(
|
||||
// AnyComponentWithIdentity(
|
||||
// id: "stickers",
|
||||
// component: AnyComponent(
|
||||
// PerkComponent(
|
||||
// iconName: "Premium/Perk/Stickers",
|
||||
// iconBackgroundColors: [
|
||||
// UIColor(rgb: 0x9674FF),
|
||||
// UIColor(rgb: 0x8C7DFF)
|
||||
// ],
|
||||
// title: strings.Premium_Stickers,
|
||||
// titleColor: titleColor,
|
||||
// subtitle: strings.Premium_StickersInfo,
|
||||
// subtitleColor: subtitleColor,
|
||||
// arrowColor: arrowColor
|
||||
// )
|
||||
// )
|
||||
// ),
|
||||
// action: {
|
||||
//
|
||||
// }
|
||||
// ),
|
||||
// SectionGroupComponent.Item(
|
||||
// AnyComponentWithIdentity(
|
||||
// id: "chat",
|
||||
// component: AnyComponent(
|
||||
// PerkComponent(
|
||||
// iconName: "Premium/Perk/Chat",
|
||||
// iconBackgroundColors: [
|
||||
// UIColor(rgb: 0x9674FF),
|
||||
// UIColor(rgb: 0x8C7DFF)
|
||||
// ],
|
||||
// title: strings.Premium_ChatManagement,
|
||||
// titleColor: titleColor,
|
||||
// subtitle: strings.Premium_ChatManagementInfo,
|
||||
// subtitleColor: subtitleColor,
|
||||
// arrowColor: arrowColor
|
||||
// )
|
||||
// )
|
||||
// ),
|
||||
// action: {
|
||||
//
|
||||
// }
|
||||
// ),
|
||||
// SectionGroupComponent.Item(
|
||||
// AnyComponentWithIdentity(
|
||||
// id: "badge",
|
||||
// component: AnyComponent(
|
||||
// PerkComponent(
|
||||
// iconName: "Premium/Perk/Badge",
|
||||
// iconBackgroundColors: [
|
||||
// UIColor(rgb: 0x7B88FF),
|
||||
// UIColor(rgb: 0x7091FF)
|
||||
// ],
|
||||
// title: strings.Premium_Badge,
|
||||
// titleColor: titleColor,
|
||||
// subtitle: strings.Premium_BadgeInfo,
|
||||
// subtitleColor: subtitleColor,
|
||||
// arrowColor: arrowColor
|
||||
// )
|
||||
// )
|
||||
// ),
|
||||
// action: {
|
||||
//
|
||||
// }
|
||||
// ),
|
||||
// SectionGroupComponent.Item(
|
||||
// AnyComponentWithIdentity(
|
||||
// id: "avatar",
|
||||
// component: AnyComponent(
|
||||
// PerkComponent(
|
||||
// iconName: "Premium/Perk/Avatar",
|
||||
// iconBackgroundColors: [
|
||||
// UIColor(rgb: 0x609DFF),
|
||||
// UIColor(rgb: 0x56A5FF)
|
||||
// ],
|
||||
// title: strings.Premium_Avatar,
|
||||
// titleColor: titleColor,
|
||||
// subtitle: strings.Premium_AvatarInfo,
|
||||
// subtitleColor: subtitleColor,
|
||||
// arrowColor: arrowColor
|
||||
// )
|
||||
// )
|
||||
// ),
|
||||
// action: {
|
||||
//
|
||||
// }
|
||||
// ),
|
||||
// ],
|
||||
// backgroundColor: environment.theme.list.itemBlocksBackgroundColor,
|
||||
// selectionColor: environment.theme.list.itemHighlightedBackgroundColor,
|
||||
// separatorColor: environment.theme.list.itemBlocksSeparatorColor
|
||||
// ),
|
||||
// environment: {},
|
||||
// availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude),
|
||||
// transition: context.transition
|
||||
// )
|
||||
context.add(section
|
||||
.position(CGPoint(x: availableWidth / 2.0, y: size.height + section.size.height / 2.0))
|
||||
.clipsToBounds(true)
|
||||
@ -1315,11 +1137,13 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
|
||||
let context: AccountContext
|
||||
let updateInProgress: (Bool) -> Void
|
||||
let present: (ViewController) -> Void
|
||||
let completion: () -> Void
|
||||
|
||||
init(context: AccountContext, updateInProgress: @escaping (Bool) -> Void, completion: @escaping () -> Void) {
|
||||
init(context: AccountContext, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.updateInProgress = updateInProgress
|
||||
self.present = present
|
||||
self.completion = completion
|
||||
}
|
||||
|
||||
@ -1338,6 +1162,8 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
var topContentOffset: CGFloat?
|
||||
var bottomContentOffset: CGFloat?
|
||||
|
||||
var hasIdleAnimations = true
|
||||
|
||||
var inProgress = false
|
||||
var premiumProduct: InAppPurchaseManager.Product?
|
||||
private var disposable: Disposable?
|
||||
@ -1398,6 +1224,11 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func updateIsFocused(_ isFocused: Bool) {
|
||||
self.hasIdleAnimations = !isFocused
|
||||
self.updated(transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
func makeState() -> State {
|
||||
@ -1427,7 +1258,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
}
|
||||
|
||||
let star = star.update(
|
||||
component: PremiumStarComponent(isVisible: starIsVisible),
|
||||
component: PremiumStarComponent(isVisible: starIsVisible, hasIdleAnimations: state.hasIdleAnimations),
|
||||
availableSize: CGSize(width: min(390.0, context.availableSize.width), height: 220.0),
|
||||
transition: context.transition
|
||||
)
|
||||
@ -1504,7 +1335,14 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
let scrollContent = scrollContent.update(
|
||||
component: ScrollComponent<EnvironmentType>(
|
||||
content: AnyComponent(PremiumIntroScreenContentComponent(
|
||||
context: context.component.context
|
||||
context: context.component.context,
|
||||
price: state.premiumProduct?.price,
|
||||
present: context.component.present,
|
||||
buy: { [weak state] in
|
||||
state?.buy()
|
||||
}, updateIsFocused: { [weak state] isFocused in
|
||||
state?.updateIsFocused(isFocused)
|
||||
}
|
||||
)),
|
||||
contentInsets: UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: bottomPanel.size.height, right: 0.0),
|
||||
contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in
|
||||
@ -1610,12 +1448,16 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
|
||||
self.context = context
|
||||
|
||||
var updateInProgressImpl: ((Bool) -> Void)?
|
||||
var presentImpl: ((ViewController) -> Void)?
|
||||
var completionImpl: (() -> Void)?
|
||||
super.init(context: context, component: PremiumIntroScreenComponent(
|
||||
context: context,
|
||||
updateInProgress: { inProgress in
|
||||
updateInProgressImpl?(inProgress)
|
||||
},
|
||||
present: { c in
|
||||
presentImpl?(c)
|
||||
},
|
||||
completion: {
|
||||
completionImpl?()
|
||||
}
|
||||
@ -1639,6 +1481,10 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
|
||||
}
|
||||
}
|
||||
|
||||
presentImpl = { [weak self] c in
|
||||
self?.push(c)
|
||||
}
|
||||
|
||||
completionImpl = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.view.addSubview(ConfettiView(frame: strongSelf.view.bounds))
|
||||
|
@ -16,16 +16,16 @@ import BundleIconComponent
|
||||
import SolidRoundedButtonComponent
|
||||
import Markdown
|
||||
|
||||
private func generateCloseButtonImage(theme: PresentationTheme) -> UIImage? {
|
||||
func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? {
|
||||
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setFillColor(UIColor(rgb: 0x808084, alpha: 0.1).cgColor)
|
||||
context.setFillColor(backgroundColor.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setLineWidth(2.0)
|
||||
context.setLineCap(.round)
|
||||
context.setStrokeColor(theme.actionSheet.inputClearButtonColor.cgColor)
|
||||
context.setStrokeColor(foregroundColor.cgColor)
|
||||
|
||||
context.move(to: CGPoint(x: 10.0, y: 10.0))
|
||||
context.addLine(to: CGPoint(x: 20.0, y: 20.0))
|
||||
@ -143,7 +143,7 @@ private class PremiumLimitAnimationComponent: Component {
|
||||
self.badgeCountLabel = RollingLabel()
|
||||
self.badgeCountLabel.font = Font.with(size: 24.0, design: .round, weight: .semibold, traits: [])
|
||||
self.badgeCountLabel.textColor = .white
|
||||
self.badgeCountLabel.text(num: 0)
|
||||
self.badgeCountLabel.configure(with: "0")
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
@ -203,8 +203,8 @@ private class PremiumLimitAnimationComponent: Component {
|
||||
self.badgeView.layer.add(rotateAnimation, forKey: "appearance2")
|
||||
self.badgeView.layer.add(returnAnimation, forKey: "appearance3")
|
||||
|
||||
if let badgeText = component.badgeText, let num = Int(badgeText) {
|
||||
self.badgeCountLabel.text(num: num)
|
||||
if let badgeText = component.badgeText {
|
||||
self.badgeCountLabel.configure(with: badgeText)
|
||||
}
|
||||
}
|
||||
|
||||
@ -251,7 +251,18 @@ private class PremiumLimitAnimationComponent: Component {
|
||||
self.badgeMaskArrowView.frame = CGRect(origin: CGPoint(x: (badgeSize.width - 44.0) / 2.0, y: badgeSize.height - 12.0), size: CGSize(width: 44.0, height: 12.0))
|
||||
|
||||
self.badgeView.bounds = CGRect(origin: .zero, size: badgeSize)
|
||||
self.badgeView.center = CGPoint(x: 3.0 + (availableSize.width - 6.0) * component.badgePosition, y: 82.0)
|
||||
if component.badgePosition > 1.0 - .ulpOfOne {
|
||||
let offset = badgeWidth / 2.0 - 16.0
|
||||
self.badgeView.center = CGPoint(x: 3.0 + (availableSize.width - 6.0) * component.badgePosition - offset, y: 82.0)
|
||||
self.badgeMaskArrowView.frame = self.badgeMaskArrowView.frame.offsetBy(dx: offset - 18.0, dy: 0.0)
|
||||
} else {
|
||||
self.badgeView.center = CGPoint(x: 3.0 + (availableSize.width - 6.0) * component.badgePosition, y: 82.0)
|
||||
|
||||
if self.badgeView.frame.maxX > availableSize.width {
|
||||
let delta = self.badgeView.frame.maxX - availableSize.width - 6.0
|
||||
self.badgeView.center = self.badgeView.center.offsetBy(dx: -delta, dy: 0.0)
|
||||
}
|
||||
}
|
||||
self.badgeForeground.bounds = CGRect(origin: CGPoint(), size: CGSize(width: badgeSize.width * 3.0, height: badgeSize.height))
|
||||
if self.badgeForeground.animation(forKey: "movement") == nil {
|
||||
self.badgeForeground.position = CGPoint(x: badgeSize.width * 3.0 / 2.0 - self.badgeForeground.frame.width * 0.35, y: badgeSize.height / 2.0)
|
||||
@ -616,7 +627,7 @@ private final class LimitSheetContent: CombinedComponent {
|
||||
if let (image, theme) = state.cachedCloseImage, theme === environment.theme {
|
||||
closeImage = image
|
||||
} else {
|
||||
closeImage = generateCloseButtonImage(theme: theme)!
|
||||
closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: theme.actionSheet.inputClearButtonColor)!
|
||||
state.cachedCloseImage = (closeImage, theme)
|
||||
}
|
||||
|
||||
@ -634,6 +645,7 @@ private final class LimitSheetContent: CombinedComponent {
|
||||
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - closeButton.size.width, y: 28.0))
|
||||
)
|
||||
|
||||
var titleText = strings.Premium_LimitReached
|
||||
let iconName: String
|
||||
let badgeText: String
|
||||
let string: String
|
||||
@ -669,20 +681,21 @@ private final class LimitSheetContent: CombinedComponent {
|
||||
premiumValue = "\(premiumLimit)"
|
||||
badgePosition = CGFloat(component.count) / CGFloat(premiumLimit)
|
||||
case .files:
|
||||
let limit = Int64(state.limits.maxUploadFileParts) * 512 * 1024
|
||||
let premiumLimit = Int64(state.limits.maxUploadFileParts) * 512 * 1024
|
||||
let limit = Int64(state.limits.maxUploadFileParts) * 512 * 1024 + 1024 * 1024 * 100
|
||||
let premiumLimit = Int64(state.premiumLimits.maxUploadFileParts) * 512 * 1024 + 1024 * 1024 * 100
|
||||
iconName = "Premium/File"
|
||||
badgeText = dataSizeString(limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))
|
||||
string = strings.Premium_MaxFileSizeText(dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))).string
|
||||
defaultValue = ""
|
||||
premiumValue = dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))
|
||||
badgePosition = 0.5
|
||||
titleText = strings.Premium_FileTooLarge
|
||||
}
|
||||
|
||||
let title = title.update(
|
||||
component: MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: strings.Premium_LimitReached,
|
||||
string: titleText,
|
||||
font: Font.semibold(17.0),
|
||||
textColor: theme.actionSheet.primaryTextColor,
|
||||
paragraphAlignment: .center
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -46,13 +46,15 @@ private func generateDiffuseTexture() -> UIImage {
|
||||
|
||||
class PremiumStarComponent: Component {
|
||||
let isVisible: Bool
|
||||
let hasIdleAnimations: Bool
|
||||
|
||||
init(isVisible: Bool) {
|
||||
init(isVisible: Bool, hasIdleAnimations: Bool) {
|
||||
self.isVisible = isVisible
|
||||
self.hasIdleAnimations = hasIdleAnimations
|
||||
}
|
||||
|
||||
static func ==(lhs: PremiumStarComponent, rhs: PremiumStarComponent) -> Bool {
|
||||
return lhs.isVisible == rhs.isVisible
|
||||
return lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations
|
||||
}
|
||||
|
||||
final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView {
|
||||
@ -75,6 +77,7 @@ class PremiumStarComponent: Component {
|
||||
|
||||
private var previousInteractionTimestamp: Double = 0.0
|
||||
private var timer: SwiftSignalKit.Timer?
|
||||
private var hasIdleAnimations = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.sceneView = SCNView(frame: frame)
|
||||
@ -249,7 +252,7 @@ class PremiumStarComponent: Component {
|
||||
|
||||
self.previousInteractionTimestamp = CACurrentMediaTime()
|
||||
self.timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
if let strongSelf = self, strongSelf.hasIdleAnimations {
|
||||
let currentTimestamp = CACurrentMediaTime()
|
||||
if currentTimestamp > strongSelf.previousInteractionTimestamp + 5.0 {
|
||||
strongSelf.playAppearanceAnimation()
|
||||
@ -359,6 +362,8 @@ class PremiumStarComponent: Component {
|
||||
self.sceneView.bounds = CGRect(origin: .zero, size: CGSize(width: availableSize.width * 2.0, height: availableSize.height * 2.0))
|
||||
self.sceneView.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)
|
||||
|
||||
self.hasIdleAnimations = component.hasIdleAnimations
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
@ -2,15 +2,86 @@ import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import ComponentFlow
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
import ReactionSelectionNode
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
|
||||
final class ReactionsCarouselComponent: Component {
|
||||
public typealias EnvironmentType = DemoPageEnvironment
|
||||
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let reactions: [AvailableReactions.Reaction]
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
reactions: [AvailableReactions.Reaction]
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.reactions = reactions
|
||||
}
|
||||
|
||||
public static func ==(lhs: ReactionsCarouselComponent, rhs: ReactionsCarouselComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.reactions != rhs.reactions {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private var component: ReactionsCarouselComponent?
|
||||
private var node: ReactionCarouselNode?
|
||||
|
||||
public func update(component: ReactionsCarouselComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
if self.node == nil {
|
||||
let node = ReactionCarouselNode(
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
reactions: component.reactions
|
||||
)
|
||||
self.node = node
|
||||
self.addSubnode(node)
|
||||
}
|
||||
|
||||
let isFirstTime = self.component == nil
|
||||
self.component = component
|
||||
|
||||
if let node = self.node {
|
||||
node.frame = CGRect(origin: CGPoint(x: 0.0, y: -20.0), size: availableSize)
|
||||
node.updateLayout(size: availableSize, transition: .immediate)
|
||||
}
|
||||
|
||||
if isFirstTime {
|
||||
self.node?.animateIn()
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<DemoPageEnvironment>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private let itemSize = CGSize(width: 110.0, height: 110.0)
|
||||
|
||||
final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
private let context: AccountContext
|
||||
private let theme: PresentationTheme
|
||||
private let reactions: [AvailableReactions.Reaction]
|
||||
@ -167,7 +238,7 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
|
||||
func playReaction() {
|
||||
let delta = self.positionDelta
|
||||
let index = max(0, min(self.itemNodes.count - 1, Int(round(self.currentPosition / delta))))
|
||||
let index = max(0, Int(round(self.currentPosition / delta)) % self.itemNodes.count)
|
||||
|
||||
guard !self.playingIndices.contains(index) else {
|
||||
return
|
||||
@ -223,7 +294,8 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
self.scrollStartPosition = (scrollView.contentOffset.x, self.currentPosition)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
guard !self.ignoreContentOffsetChange, let (startContentOffset, startPosition) = self.scrollStartPosition else {
|
||||
return
|
||||
@ -241,10 +313,12 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
self.currentPosition = updatedPosition
|
||||
|
||||
let indexDelta = self.positionDelta
|
||||
let index = max(0, min(self.itemNodes.count - 1, Int(round(self.currentPosition / indexDelta))))
|
||||
let index = max(0, Int(round(self.currentPosition / indexDelta)) % self.itemNodes.count)
|
||||
if index != self.currentIndex {
|
||||
self.currentIndex = index
|
||||
print(index)
|
||||
if self.scrollNode.view.isTracking || self.scrollNode.view.isDecelerating {
|
||||
self.hapticFeedback.tap()
|
||||
}
|
||||
}
|
||||
|
||||
if let size = self.validLayout {
|
||||
@ -272,7 +346,7 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
self.resetScrollPosition()
|
||||
|
||||
let delta = self.positionDelta
|
||||
let index = max(0, min(self.itemNodes.count - 1, Int(round(self.currentPosition / delta))))
|
||||
let index = max(0, Int(round(self.currentPosition / delta)) % self.itemNodes.count)
|
||||
self.scrollTo(index, playReaction: true, duration: 0.2)
|
||||
}
|
||||
}
|
||||
@ -287,14 +361,14 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
|
||||
self.scrollNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
if self.scrollNode.view.contentSize.width.isZero {
|
||||
self.scrollNode.view.contentSize = CGSize(width: 10000000, height: size.height)
|
||||
self.scrollNode.view.contentSize = CGSize(width: 10000000.0, height: size.height)
|
||||
self.tapNode.frame = CGRect(origin: CGPoint(), size: self.scrollNode.view.contentSize)
|
||||
self.resetScrollPosition()
|
||||
}
|
||||
|
||||
let delta = self.positionDelta
|
||||
|
||||
let areaSize = CGSize(width: floor(size.width * 0.7), height: size.height * 0.5)
|
||||
let areaSize = CGSize(width: floor(size.width * 0.7), height: size.height * 0.45)
|
||||
|
||||
for i in 0 ..< self.itemNodes.count {
|
||||
let itemNode = self.itemNodes[i]
|
||||
@ -326,8 +400,8 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
|
||||
let itemFrame = CGRect(origin: CGPoint(x: size.width * 0.5 + point.x * areaSize.width * 0.5 - itemSize.width * 0.5, y: size.height * 0.5 + point.y * areaSize.height * 0.5 - itemSize.height * 0.5), size: itemSize)
|
||||
containerNode.bounds = CGRect(origin: CGPoint(), size: itemFrame.size)
|
||||
containerNode.position = itemFrame.center
|
||||
transition.updateTransformScale(node: containerNode, scale: 1.0 - distance * 0.45)
|
||||
containerNode.position = CGPoint(x: itemFrame.midX, y: itemFrame.midY)
|
||||
transition.updateTransformScale(node: containerNode, scale: 1.0 - distance * 0.55)
|
||||
|
||||
itemNode.frame = CGRect(origin: CGPoint(), size: itemFrame.size)
|
||||
itemNode.updateLayout(size: itemFrame.size, isExpanded: false, largeExpanded: false, isPreviewing: false, transition: transition)
|
@ -1,4 +1,5 @@
|
||||
import UIKit
|
||||
import Display
|
||||
|
||||
private extension UILabel {
|
||||
func textWidth() -> CGFloat {
|
||||
@ -32,22 +33,19 @@ open class RollingLabel: UILabel {
|
||||
private let duration = 1.12
|
||||
private let durationOffset = 0.2
|
||||
private let textsNotAnimated = [","]
|
||||
|
||||
public func text(num: Int) {
|
||||
self.configure(with: num)
|
||||
self.text = " "
|
||||
self.animate()
|
||||
|
||||
public func setSuffix(suffix: String) {
|
||||
self.suffix = suffix
|
||||
}
|
||||
|
||||
public func setPrefix(prefix: String) {
|
||||
self.suffix = prefix
|
||||
}
|
||||
|
||||
private func configure(with number: Int) {
|
||||
fullText = String(number)
|
||||
func configure(with string: String) {
|
||||
fullText = string
|
||||
|
||||
clean()
|
||||
setupSubviews()
|
||||
|
||||
self.text = " "
|
||||
self.animate()
|
||||
}
|
||||
|
||||
private func animate(ascending: Bool = true) {
|
||||
@ -99,9 +97,10 @@ open class RollingLabel: UILabel {
|
||||
}
|
||||
|
||||
stringArray.enumerated().forEach { index, text in
|
||||
if textsNotAnimated.contains(text) {
|
||||
let nonDigits = CharacterSet.decimalDigits.inverted
|
||||
if text.rangeOfCharacter(from: nonDigits) != nil {
|
||||
let label = UILabel()
|
||||
label.frame.origin = CGPoint(x: x, y: y)
|
||||
label.frame.origin = CGPoint(x: x, y: y - 1.0 - UIScreenPixel)
|
||||
label.textColor = textColor
|
||||
label.font = font
|
||||
label.text = text
|
||||
@ -118,28 +117,28 @@ open class RollingLabel: UILabel {
|
||||
label.text = "0"
|
||||
label.textAlignment = .center
|
||||
label.sizeToFit()
|
||||
createScrollLayer(to: label, text: text)
|
||||
createScrollLayer(to: label, text: text, index: index)
|
||||
|
||||
x += label.bounds.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func createScrollLayer(to label: UILabel, text: String) {
|
||||
private func createScrollLayer(to label: UILabel, text: String, index: Int) {
|
||||
let scrollLayer = CAScrollLayer()
|
||||
scrollLayer.frame = label.frame
|
||||
scrollLayer.frame = CGRect(x: label.frame.minX, y: label.frame.minY - 10.0, width: label.frame.width, height: label.frame.height * 3.0)
|
||||
scrollLayers.append(scrollLayer)
|
||||
self.layer.addSublayer(scrollLayer)
|
||||
|
||||
createContentForLayer(scrollLayer: scrollLayer, text: text)
|
||||
createContentForLayer(scrollLayer: scrollLayer, text: text, index: index)
|
||||
}
|
||||
|
||||
private func createContentForLayer(scrollLayer: CAScrollLayer, text: String) {
|
||||
private func createContentForLayer(scrollLayer: CAScrollLayer, text: String, index: Int) {
|
||||
var textsForScroll: [String] = []
|
||||
|
||||
let max: Int
|
||||
var found = false
|
||||
if let val = Int(text) {
|
||||
if let val = Int(text), index == 0 {
|
||||
max = val
|
||||
found = true
|
||||
} else {
|
||||
@ -150,11 +149,11 @@ open class RollingLabel: UILabel {
|
||||
let str = String(i)
|
||||
textsForScroll.append(str)
|
||||
}
|
||||
if !found {
|
||||
if !found && text != "9" {
|
||||
textsForScroll.append(text)
|
||||
}
|
||||
|
||||
var height: CGFloat = 0
|
||||
var height: CGFloat = 0.0
|
||||
for text in textsForScroll {
|
||||
let label = UILabel()
|
||||
label.text = text
|
||||
@ -179,17 +178,18 @@ open class RollingLabel: UILabel {
|
||||
animation.duration = duration + offset
|
||||
animation.timingFunction = CAMediaTimingFunction(name: .easeOut)
|
||||
|
||||
let verticalOffset = 20.0
|
||||
if ascending {
|
||||
animation.fromValue = maxY
|
||||
animation.fromValue = maxY + verticalOffset
|
||||
animation.toValue = 0
|
||||
} else {
|
||||
animation.fromValue = 0
|
||||
animation.toValue = maxY
|
||||
animation.toValue = maxY + verticalOffset
|
||||
}
|
||||
|
||||
scrollLayer.scrollMode = .vertically
|
||||
scrollLayer.add(animation, forKey: nil)
|
||||
scrollLayer.scroll(to: CGPoint(x: 0, y: maxY))
|
||||
scrollLayer.scroll(to: CGPoint(x: 0, y: maxY + verticalOffset))
|
||||
|
||||
offset += self.durationOffset
|
||||
}
|
||||
|
498
submodules/PremiumUI/Sources/StickersCarouselComponent.swift
Normal file
498
submodules/PremiumUI/Sources/StickersCarouselComponent.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -152,10 +152,10 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC
|
||||
if isPremiumSticker {
|
||||
animationNode.completed = { [weak self] _ in
|
||||
if let strongSelf = self, let animationNode = strongSelf.animationNode, let additionalAnimationNode = strongSelf.additionalAnimationNode {
|
||||
Queue.mainQueue().after(0.1, {
|
||||
Queue.mainQueue().async {
|
||||
animationNode.play()
|
||||
additionalAnimationNode.play()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
21
submodules/TelegramUI/Images.xcassets/Components/Dots.imageset/Contents.json
vendored
Normal file
21
submodules/TelegramUI/Images.xcassets/Components/Dots.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Components/Dots.imageset/dots@3x.png
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Components/Dots.imageset/dots@3x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
11
submodules/TelegramUI/Images.xcassets/Premium/File.imageset/Contents.json
vendored
Normal file
11
submodules/TelegramUI/Images.xcassets/Premium/File.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -1031,9 +1031,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
|
||||
actions.context = strongSelf.context
|
||||
|
||||
var premiumReactions: [AvailableReactions.Reaction] = []
|
||||
|
||||
|
||||
if canAddMessageReactions(message: topMessage), let availableReactions = availableReactions, let allowedReactions = allowedReactions {
|
||||
var hasPremiumPlaceholder = false
|
||||
filterReactions: for reaction in availableReactions.reactions {
|
||||
@ -1043,9 +1041,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
guard let aroundAnimation = reaction.aroundAnimation else {
|
||||
continue
|
||||
}
|
||||
if reaction.isPremium {
|
||||
premiumReactions.append(reaction)
|
||||
}
|
||||
if !reaction.isEnabled {
|
||||
continue
|
||||
}
|
||||
@ -1094,9 +1089,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
|
||||
if case .premium = value {
|
||||
controller?.dismiss()
|
||||
controller?.dismissWithoutContent()
|
||||
|
||||
let controller = PremiumReactionsScreen(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, reactions: premiumReactions)
|
||||
let context = strongSelf.context
|
||||
var replaceImpl: ((ViewController) -> Void)?
|
||||
let controller = PremiumDemoScreen(context: context, subject: .uniqueReactions, action: {
|
||||
let controller = PremiumIntroScreen(context: context, source: .reactions)
|
||||
replaceImpl?(controller)
|
||||
})
|
||||
replaceImpl = { [weak controller] c in
|
||||
controller?.replace(with: c)
|
||||
}
|
||||
strongSelf.push(controller)
|
||||
return
|
||||
}
|
||||
@ -11530,7 +11533,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
for item in results {
|
||||
if let item = item {
|
||||
if item.fileSize > Int64(premiumLimits.maxUploadFileParts) * 512 * 1024 {
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.Conversation_PremiumUploadFileTooLarge, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.Premium_FileTooLarge, text: strongSelf.presentationData.strings.Conversation_PremiumUploadFileTooLarge, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
return
|
||||
} else if item.fileSize > Int64(limits.maxUploadFileParts) * 512 * 1024 && !isPremium {
|
||||
let context = strongSelf.context
|
||||
|
@ -159,11 +159,14 @@ public struct WebAppParameters {
|
||||
|
||||
public func generateWebAppThemeParams(_ presentationTheme: PresentationTheme) -> [String: Any] {
|
||||
var backgroundColor = presentationTheme.list.plainBackgroundColor.rgb
|
||||
var secondaryBackgroundColor = presentationTheme.list.blocksBackgroundColor.rgb
|
||||
if backgroundColor == 0x000000 {
|
||||
backgroundColor = presentationTheme.list.itemBlocksBackgroundColor.rgb
|
||||
secondaryBackgroundColor = presentationTheme.list.plainBackgroundColor.rgb
|
||||
}
|
||||
return [
|
||||
"bg_color": Int32(bitPattern: backgroundColor),
|
||||
"secondary_bg_color": Int32(bitPattern: secondaryBackgroundColor),
|
||||
"text_color": Int32(bitPattern: presentationTheme.list.itemPrimaryTextColor.rgb),
|
||||
"hint_color": Int32(bitPattern: presentationTheme.list.itemSecondaryTextColor.rgb),
|
||||
"link_color": Int32(bitPattern: presentationTheme.list.itemAccentColor.rgb),
|
||||
|
Loading…
x
Reference in New Issue
Block a user