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