Merge commit '3fb304bd6ffd03c9bc1ae63db6a33c0f9afe229c'

This commit is contained in:
Ali 2022-05-20 06:55:28 +03:00
commit 352916f866
71 changed files with 2190 additions and 1193 deletions

View File

@ -7582,6 +7582,9 @@ Sorry for the inconvenience.";
"Premium.FasterSpeed" = "Faster Download Speed";
"Premium.FasterSpeedInfo" = "No more limits on the speed with which media and documents are downloaded.";
"Premium.VoiceToText" = "Voice-to-Text Conversion";
"Premium.VoiceToTextInfo" = "Ability to read the transcript of any incoming voice message.";
"Premium.NoAds" = "No Ads";
"Premium.NoAdsInfo" = "No more ads in public channels where Telegram sometimes shows ads.";
@ -7591,16 +7594,24 @@ Sorry for the inconvenience.";
"Premium.Stickers" = "Premium Stickers";
"Premium.StickersInfo" = "Exclusive enlarged stickers featuring additional effects, updated monthly.";
"Premium.ChatManagement" = "Advanced Chat Management";
"Premium.ChatManagementInfo" = "Tools to set default folder, auto-archive and hide new chats.";
"Premium.Badge" = "Profile Badge";
"Premium.BadgeInfo" = "A badge next to your name showing that you are helping support Telegram.";
"Premium.Avatar" = "Animated Profile Pictures";
"Premium.AvatarInfo" = "Video avatars animated in chat lists and chats to allow for additional self-expression.";
"Premium.SubscribeFor" = "Subscribe for %@ per month";
"Premium.SubscribeFor" = "Subscribe for %@ / month";
"Premium.AboutTitle" = "ABOUT TELEGRAM PREMIUM";
"Premium.AboutText" = "While the free version of Telegram already gives its users more than any other messaging application, **Telegram Premium** pushes its capabilities even further.\n\n**Telegram Premium** is a paid option, because most Premium Features require additional expenses from Telegram to third parties such as data center providers and server manufacturers. Contributions from **Telegram Premium** users allow us to cover such costs and also help Telegram stay free for everyone.";
"Premium.Terms" = "By purchasing a Premium subscription, you agree to our [Terms of Service](terms) and [Privacy Policy](privacy).";
"Conversation.CopyProtectionSavingDisabledSecret" = "Saving is restricted";
"Conversation.CopyProtectionForwardingDisabledSecret" = "Forwards are restricted";
"Settings.Terms_URL" = "https://telegram.org/tos";
"Settings.PrivacyPolicy_URL" = "https://telegram.org/privacy";

View File

@ -380,6 +380,8 @@ public class AttachmentController: ViewController {
override func didLoad() {
super.didLoad()
self.view.disablesInteractiveModalDismiss = true
self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
if let controller = self.controller {

View File

@ -165,13 +165,15 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
let isContact = transaction.isPeerContact(peerId: peerId)
if case let .chatList(currentFilter) = source {
if let currentFilter = currentFilter {
if let currentFilter = currentFilter, case let .filter(id, title, emoticon, data) = currentFilter {
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_RemoveFromFolder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/RemoveFromFolder"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in
var filters = filters
for i in 0 ..< filters.count {
if filters[i].id == currentFilter.id {
let _ = filters[i].data.addExcludePeer(peerId: peer.id)
var updatedData = data
let _ = updatedData.addExcludePeer(peerId: peer.id)
filters[i] = .filter(id: id, title: title, emoticon: emoticon, data: updatedData)
break
}
}
@ -179,7 +181,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
}
|> deliverOnMainQueue).start(completed: {
c.dismiss(completion: {
chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatRemovedFromFolder(chatTitle: EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: currentFilter.title), elevatedLayout: false, animateInAsReplacement: true, action: { _ in
chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatRemovedFromFolder(chatTitle: EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title), elevatedLayout: false, animateInAsReplacement: true, action: { _ in
return false
}), in: .current)
})
@ -188,13 +190,13 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
} else {
var hasFolders = false
for filter in filters {
let predicate = chatListFilterPredicate(filter: filter.data)
for case let .filter(_, _, _, data) in filters {
let predicate = chatListFilterPredicate(filter: data)
if predicate.includes(peer: peer, groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: false) {
continue
}
var data = filter.data
var data = data
if data.addIncludePeer(peerId: peer.id) {
hasFolders = true
break
@ -206,56 +208,62 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
var updatedItems: [ContextMenuItem] = []
for filter in filters {
let predicate = chatListFilterPredicate(filter: filter.data)
if predicate.includes(peer: peer, groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: false) {
continue
}
var data = filter.data
if !data.addIncludePeer(peerId: peer.id) {
continue
}
let filterType = chatListFilterType(filter)
updatedItems.append(.action(ContextMenuActionItem(text: filter.title, icon: { theme in
let imageName: String
switch filterType {
case .generic:
imageName = "Chat/Context Menu/List"
case .unmuted:
imageName = "Chat/Context Menu/Unmute"
case .unread:
imageName = "Chat/Context Menu/MarkAsUnread"
case .channels:
imageName = "Chat/Context Menu/Channels"
case .groups:
imageName = "Chat/Context Menu/Groups"
case .bots:
imageName = "Chat/Context Menu/Bots"
case .contacts:
imageName = "Chat/Context Menu/User"
case .nonContacts:
imageName = "Chat/Context Menu/UnknownUser"
if case let .filter(_, title, _, data) = filter {
let predicate = chatListFilterPredicate(filter: data)
if predicate.includes(peer: peer, groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: false) {
continue
}
return generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.contextMenu.primaryColor)
}, action: { c, f in
c.dismiss(completion: {
let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in
var filters = filters
for i in 0 ..< filters.count {
if filters[i].id == filter.id {
let _ = filters[i].data.addIncludePeer(peerId: peer.id)
break
}
}
return filters
}).start()
chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatAddedToFolder(chatTitle: EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: filter.title), elevatedLayout: false, animateInAsReplacement: true, action: { _ in
return false
}), in: .current)
})
})))
var data = data
if !data.addIncludePeer(peerId: peer.id) {
continue
}
let filterType = chatListFilterType(data)
updatedItems.append(.action(ContextMenuActionItem(text: title, icon: { theme in
let imageName: String
switch filterType {
case .generic:
imageName = "Chat/Context Menu/List"
case .unmuted:
imageName = "Chat/Context Menu/Unmute"
case .unread:
imageName = "Chat/Context Menu/MarkAsUnread"
case .channels:
imageName = "Chat/Context Menu/Channels"
case .groups:
imageName = "Chat/Context Menu/Groups"
case .bots:
imageName = "Chat/Context Menu/Bots"
case .contacts:
imageName = "Chat/Context Menu/User"
case .nonContacts:
imageName = "Chat/Context Menu/UnknownUser"
}
return generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.contextMenu.primaryColor)
}, action: { c, f in
c.dismiss(completion: {
let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in
var filters = filters
for i in 0 ..< filters.count {
if filters[i].id == filter.id {
if case let .filter(id, title, emoticon, data) = filter {
var updatedData = data
let _ = updatedData.addIncludePeer(peerId: peer.id)
filters[i] = .filter(id: id, title: title, emoticon: emoticon, data: updatedData)
}
break
}
}
return filters
}).start()
chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatAddedToFolder(chatTitle: EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title), elevatedLayout: false, animateInAsReplacement: true, action: { _ in
return false
}), in: .current)
})
})))
}
}
updatedItems.append(.separator)
@ -318,11 +326,11 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
switch result {
case .done:
f(.default)
case .limitExceeded:
case let .limitExceeded(count, _):
f(.default)
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumLimitScreen(context: context, subject: .pins, action: {
let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: {
let premiumScreen = PremiumIntroScreen(context: context)
replaceImpl?(premiumScreen)
})

View File

@ -160,6 +160,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
private var activeDownloadsDisposable: Disposable?
private var clearUnseenDownloadsTimer: SwiftSignalKit.Timer?
private var isPremium: Bool = false
private var didSetupTabs = false
public override func updateNavigationCustomData(_ data: Any?, progress: CGFloat, transition: ContainedViewLayoutTransition) {
@ -191,8 +193,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
let title: String
if let filter = self.filter {
title = filter.title
if let filter = self.filter, case let .filter(_, filterTitle, _, _) = filter {
title = filterTitle
} else if self.groupId == .root {
title = self.presentationData.strings.DialogList_Title
} else {
@ -762,7 +764,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
strongSelf.tabContainerNode.cancelAnimations()
strongSelf.chatListDisplayNode.inlineTabContainerNode.cancelAnimations()
}
strongSelf.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: tabContainerData.0, selectedFilter: filter, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || (strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing, transitionFraction: fraction, presentationData: strongSelf.presentationData, transition: transition)
strongSelf.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: tabContainerData.0, selectedFilter: filter, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || (strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing, canReorderAllChats: strongSelf.isPremium, transitionFraction: fraction, presentationData: strongSelf.presentationData, transition: transition)
strongSelf.chatListDisplayNode.inlineTabContainerNode.update(size: CGSize(width: layout.size.width, height: 40.0), sideInset: layout.safeInsets.left, filters: tabContainerData.0, selectedFilter: filter, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || (strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: false, transitionFraction: fraction, presentationData: strongSelf.presentationData, transition: transition)
}
self.reloadFilters()
@ -833,7 +835,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData))
if let layout = self.validLayout {
self.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData?.0 ?? [], selectedFilter: self.chatListDisplayNode.containerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || (self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !self.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing, transitionFraction: self.chatListDisplayNode.containerNode.transitionFraction, presentationData: self.presentationData, transition: .immediate)
self.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData?.0 ?? [], selectedFilter: self.chatListDisplayNode.containerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || (self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !self.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing, canReorderAllChats: self.isPremium, transitionFraction: self.chatListDisplayNode.containerNode.transitionFraction, presentationData: self.presentationData, transition: .immediate)
self.chatListDisplayNode.inlineTabContainerNode.update(size: CGSize(width: layout.size.width, height: 40.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData?.0 ?? [], selectedFilter: self.chatListDisplayNode.containerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || (self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !self.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: false, transitionFraction: self.chatListDisplayNode.containerNode.transitionFraction, presentationData: self.presentationData, transition: .immediate)
}
@ -1204,8 +1206,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
var archiveEnabled = options.delete
var displayArchive = true
if let filter = strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter {
if !filter.data.excludeArchived {
if let filter = strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter, case let .filter(_, _, _, data) = filter {
if !data.excludeArchived {
displayArchive = false
}
}
@ -1265,8 +1267,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let strongSelf = self else {
return
}
let _ = (strongSelf.context.engine.peers.currentChatListFilters()
|> deliverOnMainQueue).start(next: { [weak self] filters in
let _ = combineLatest(
queue: Queue.mainQueue(),
strongSelf.context.engine.peers.currentChatListFilters(),
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)
)
).start(next: { [weak self] filters, result in
guard let strongSelf = self else {
return
}
@ -1299,7 +1308,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
})
})
})))
if let filter = filters.first(where: { $0.id == id }), filter.data.includePeers.peers.count < 100 {
let (_, _, premiumLimits) = result
let premiumLimit = premiumLimits.maxFolderChatsCount
if let filter = filters.first(where: { $0.id == id }), case let .filter(_, _, _, data) = filter, data.includePeers.peers.count < premiumLimit {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.ChatList_AddChatsToFolder, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor)
}, action: { c, f in
@ -1322,20 +1335,20 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
var found = false
for filter in presetList {
if filter.id == id {
if filter.id == id, case let .filter(_, _, _, data) = filter {
let (accountPeer, limits, premiumLimits) = result
let limit = limits.maxFolderChatsCount
let premiumLimit = premiumLimits.maxFolderChatsCount
if let accountPeer = accountPeer, accountPeer.isPremium {
if filter.data.includePeers.peers.count >= premiumLimit {
if data.includePeers.peers.count >= premiumLimit {
//printPremiumError
return
}
} else {
if filter.data.includePeers.peers.count >= limit {
if data.includePeers.peers.count >= limit {
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumLimitScreen(context: context, subject: .chatsInFolder, action: {
let controller = PremiumLimitScreen(context: context, subject: .chatsInFolder, count: Int32(data.includePeers.peers.count), action: {
let controller = PremiumIntroScreen(context: context)
replaceImpl?(controller)
})
@ -1792,7 +1805,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let navigationBarHeight = self.navigationBar?.frame.maxY ?? 0.0
transition.updateFrame(node: self.tabContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight - self.additionalNavigationBarHeight - 46.0 + tabContainerOffset), size: CGSize(width: layout.size.width, height: 46.0)))
self.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData?.0 ?? [], selectedFilter: self.chatListDisplayNode.containerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || (self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !self.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing, transitionFraction: self.chatListDisplayNode.containerNode.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring))
self.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData?.0 ?? [], selectedFilter: self.chatListDisplayNode.containerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || (self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !self.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing, canReorderAllChats: self.isPremium, transitionFraction: self.chatListDisplayNode.containerNode.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring))
if let tabContainerData = self.tabContainerData {
self.chatListDisplayNode.inlineTabContainerNode.isHidden = !tabContainerData.1 || tabContainerData.0.count <= 1
} else {
@ -1858,7 +1871,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let defaultFilterIds = defaultFilters.0.compactMap { entry -> Int32? in
switch entry {
case .all:
return nil
return 0
case let .filter(id, _, _):
return id
}
@ -1908,6 +1921,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
private var initializedFilters = false
private func reloadFilters(firstUpdate: (() -> Void)? = nil) {
let preferencesKey: PostboxViewKey = .preferences(keys: Set([
ApplicationSpecificPreferencesKeys.chatListFilterSettings
@ -1923,21 +1937,31 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let filterItems = chatListFilterItems(context: self.context)
var notifiedFirstUpdate = false
self.filterDisposable.set((combineLatest(queue: .mainQueue(),
context.account.postbox.combinedView(keys: [
self.context.account.postbox.combinedView(keys: [
preferencesKey
]),
filterItems,
displayTabsAtBottom
displayTabsAtBottom,
self.context.account.postbox.peerView(id: self.context.account.peerId)
)
|> deliverOnMainQueue).start(next: { [weak self] _, countAndFilterItems, displayTabsAtBottom in
|> deliverOnMainQueue).start(next: { [weak self] _, countAndFilterItems, displayTabsAtBottom, peerView in
guard let strongSelf = self else {
return
}
let isPremium = peerView.peers[peerView.peerId]?.isPremium ?? false
strongSelf.isPremium = isPremium
let (_, items) = countAndFilterItems
var filterItems: [ChatListFilterTabEntry] = []
filterItems.append(.all(unreadCount: 0))
for (filter, unreadCount, hasUnmutedUnread) in items {
filterItems.append(.filter(id: filter.id, text: filter.title, unread: ChatListFilterTabEntryUnreadCount(value: unreadCount, hasUnmuted: hasUnmutedUnread)))
switch filter {
case .allChats:
filterItems.append(.all(unreadCount: 0))
case let .filter(id, title, _, _):
filterItems.append(.filter(id: id, text: title, unread: ChatListFilterTabEntryUnreadCount(value: unreadCount, hasUnmuted: hasUnmutedUnread)))
}
}
var resolvedItems = filterItems
@ -1951,7 +1975,17 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
} else {
wasEmpty = true
}
var selectedEntryId = strongSelf.chatListDisplayNode.containerNode.currentItemFilter
let firstItem = countAndFilterItems.1.first?.0 ?? .allChats
let firstItemEntryId: ChatListFilterTabEntryId
switch firstItem {
case .allChats:
firstItemEntryId = .all
case let .filter(id, _, _, _):
firstItemEntryId = .filter(id)
}
var selectedEntryId = !strongSelf.initializedFilters ? firstItemEntryId : strongSelf.chatListDisplayNode.containerNode.currentItemFilter
var resetCurrentEntry = false
if !resolvedItems.contains(where: { $0.id == selectedEntryId }) {
resetCurrentEntry = true
@ -1975,12 +2009,26 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
strongSelf.tabContainerData = (resolvedItems, displayTabsAtBottom)
var availableFilters: [ChatListContainerNodeFilter] = []
availableFilters.append(.all)
var hasAllChats = false
for item in items {
availableFilters.append(.filter(item.0))
switch item.0 {
case .allChats:
hasAllChats = true
availableFilters.append(.all)
case .filter:
availableFilters.append(.filter(item.0))
}
}
if !hasAllChats {
availableFilters.insert(.all, at: 0)
}
strongSelf.chatListDisplayNode.containerNode.updateAvailableFilters(availableFilters)
if !strongSelf.initializedFilters && selectedEntryId != strongSelf.chatListDisplayNode.containerNode.currentItemFilter {
strongSelf.chatListDisplayNode.containerNode.switchToFilter(id: selectedEntryId, animated: false, completion: nil)
}
strongSelf.initializedFilters = true
let isEmpty = resolvedItems.count <= 1 || displayTabsAtBottom
let animated = strongSelf.didSetupTabs
@ -1999,7 +2047,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
strongSelf.containerLayoutUpdated(layout, transition: transition)
(strongSelf.parent as? TabBarController)?.updateLayout(transition: transition)
} else {
strongSelf.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: resolvedItems, selectedFilter: selectedEntryId, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || (strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing, transitionFraction: strongSelf.chatListDisplayNode.containerNode.transitionFraction, presentationData: strongSelf.presentationData, transition: .animated(duration: 0.4, curve: .spring))
strongSelf.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: resolvedItems, selectedFilter: selectedEntryId, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || (strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing, canReorderAllChats: isPremium, transitionFraction: strongSelf.chatListDisplayNode.containerNode.transitionFraction, presentationData: strongSelf.presentationData, transition: .animated(duration: 0.4, curve: .spring))
strongSelf.chatListDisplayNode.inlineTabContainerNode.update(size: CGSize(width: layout.size.width, height: 40.0), sideInset: layout.safeInsets.left, filters: resolvedItems, selectedFilter: selectedEntryId, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || (strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: false, transitionFraction: strongSelf.chatListDisplayNode.containerNode.transitionFraction, presentationData: strongSelf.presentationData, transition: .animated(duration: 0.4, curve: .spring))
}
}
@ -2335,7 +2383,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
} else {
let groupId = self.groupId
let filterPredicate = (self.chatListDisplayNode.containerNode.currentItemNode.chatListFilter?.data).flatMap(chatListFilterPredicate)
let filterPredicate: ChatListFilterPredicate?
if let filter = self.chatListDisplayNode.containerNode.currentItemNode.chatListFilter, case let .filter(_, _, _, data) = filter {
filterPredicate = chatListFilterPredicate(filter: data)
} else {
filterPredicate = nil
}
signal = self.context.account.postbox.transaction { transaction -> Void in
markAllChatsAsReadInteractively(transaction: transaction, viewTracker: context.account.viewTracker, groupId: groupId, filterPredicate: filterPredicate)
if let filterPredicate = filterPredicate {
@ -3185,15 +3238,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if !presetList.isEmpty {
items.append(.separator)
for preset in presetList {
let filterType = chatListFilterType(preset)
for case let .filter(id, title, _, data) in presetList {
let filterType = chatListFilterType(data)
var badge: ContextMenuActionBadge?
for item in filterItems {
if item.0.id == preset.id && item.1 != 0 {
if item.0.id == id && item.1 != 0 {
badge = ContextMenuActionBadge(value: "\(item.1)", color: item.2 ? .accent : .inactive)
}
}
items.append(.action(ContextMenuActionItem(text: preset.title, badge: badge, icon: { theme in
items.append(.action(ContextMenuActionItem(text: title, badge: badge, icon: { theme in
let imageName: String
switch filterType {
case .generic:
@ -3219,7 +3272,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let strongSelf = self else {
return
}
strongSelf.selectTab(id: .filter(preset.id))
strongSelf.selectTab(id: .filter(id))
})))
}
}

View File

@ -448,6 +448,11 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
return _ready.get()
}
private let _validLayoutReady = Promise<Bool>()
var validLayoutReady: Signal<Bool, NoError> {
return _validLayoutReady.get()
}
private var currentItemNodeValue: ChatListContainerItemNode?
var currentItemNode: ChatListNode {
return self.currentItemNodeValue!.listNode
@ -833,13 +838,13 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
}
}
func switchToFilter(id: ChatListFilterTabEntryId, completion: (() -> Void)? = nil) {
guard let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, isReorderingFilters, isEditing) = self.validLayout else {
return
}
func switchToFilter(id: ChatListFilterTabEntryId, animated: Bool = true, completion: (() -> Void)? = nil) {
self.onFilterSwitch?()
if id != self.selectedId, let index = self.availableFilters.firstIndex(where: { $0.id == id }) {
if let itemNode = self.itemNodes[id] {
guard let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, isReorderingFilters, isEditing) = self.validLayout else {
return
}
self.selectedId = id
if let currentItemNode = self.currentItemNodeValue {
itemNode.listNode.adjustScrollOffsetForNavigation(isNavigationHidden: currentItemNode.listNode.isNavigationHidden)
@ -859,8 +864,8 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
let disposable = MetaDisposable()
self.pendingItemNode = (id, itemNode, disposable)
disposable.set((itemNode.listNode.ready
|> filter { $0 }
disposable.set((combineLatest(itemNode.listNode.ready, self.validLayoutReady)
|> filter { $0 && $1 }
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self, weak itemNode] _ in
guard let strongSelf = self, let itemNode = itemNode, itemNode === strongSelf.pendingItemNode?.1 else {
@ -871,7 +876,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
}
strongSelf.pendingItemNode = nil
let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring)
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.35, curve: .spring) : .immediate
if let previousIndex = strongSelf.availableFilters.firstIndex(where: { $0.id == strongSelf.selectedId }), let index = strongSelf.availableFilters.firstIndex(where: { $0.id == id }) {
let previousId = strongSelf.selectedId
@ -937,6 +942,8 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
func update(layout: ContainerViewLayout, navigationBarHeight: CGFloat, visualNavigationHeight: CGFloat, cleanNavigationBarHeight: CGFloat, isReorderingFilters: Bool, isEditing: Bool, transition: ContainedViewLayoutTransition) {
self.validLayout = (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, isReorderingFilters, isEditing)
self._validLayoutReady.set(.single(true))
var insets = layout.insets(options: [.input])
insets.top += navigationBarHeight

View File

@ -550,6 +550,10 @@ func chatListFilterAddChatsController(context: AccountContext, filter: ChatListF
}
private func internalChatListFilterAddChatsController(context: AccountContext, filter: ChatListFilter, allFilters: [ChatListFilter], applyAutomatically: Bool, updated: @escaping (ChatListFilter) -> Void) -> ViewController {
guard case let .filter(_, _, _, filterData) = filter else {
return ViewController(navigationBarPresentationData: nil)
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let additionalCategories: [ChatListNodeAdditionalCategory] = [
ChatListNodeAdditionalCategory(
@ -587,12 +591,12 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
.bots: .bots
]
for (category, id) in categoryMapping {
if filter.data.categories.contains(category) {
if filterData.categories.contains(category) {
selectedCategories.insert(id.rawValue)
}
}
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .chatSelection(title: presentationData.strings.ChatListFolder_IncludeChatsTitle, selectedChats: Set(filter.data.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
let _ = (controller.result
|> take(1)
@ -627,9 +631,13 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
var filters = filters
for i in 0 ..< filters.count {
if filters[i].id == filter.id {
filters[i].data.categories = categories
filters[i].data.includePeers.setPeers(includePeers)
filters[i].data.excludePeers = filters[i].data.excludePeers.filter { !filters[i].data.includePeers.peers.contains($0) }
if case let .filter(id, title, emoticon, data) = filter {
var updatedData = data
updatedData.categories = categories
updatedData.includePeers.setPeers(includePeers)
updatedData.excludePeers = updatedData.excludePeers.filter { !updatedData.includePeers.peers.contains($0) }
filters[i] = .filter(id: id, title: title, emoticon: emoticon, data: updatedData)
}
}
}
return filters
@ -639,9 +647,13 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
})
} else {
var filter = filter
filter.data.categories = categories
filter.data.includePeers.setPeers(includePeers)
filter.data.excludePeers = filter.data.excludePeers.filter { !filter.data.includePeers.peers.contains($0) }
if case let .filter(id, title, emoticon, data) = filter {
var updatedData = data
updatedData.categories = categories
updatedData.includePeers.setPeers(includePeers)
updatedData.excludePeers = updatedData.excludePeers.filter { !updatedData.includePeers.peers.contains($0) }
filter = .filter(id: id, title: title, emoticon: emoticon, data: updatedData)
}
updated(filter)
controller?.dismiss()
}
@ -650,6 +662,9 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
}
private func internalChatListFilterExcludeChatsController(context: AccountContext, filter: ChatListFilter, allFilters: [ChatListFilter], applyAutomatically: Bool, updated: @escaping (ChatListFilter) -> Void) -> ViewController {
guard case let .filter(_, _, _, filterData) = filter else {
return ViewController(navigationBarPresentationData: nil)
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let additionalCategories: [ChatListNodeAdditionalCategory] = [
ChatListNodeAdditionalCategory(
@ -669,17 +684,17 @@ private func internalChatListFilterExcludeChatsController(context: AccountContex
),
]
var selectedCategories = Set<Int>()
if filter.data.excludeMuted {
if filterData.excludeMuted {
selectedCategories.insert(AdditionalExcludeCategoryId.muted.rawValue)
}
if filter.data.excludeRead {
if filterData.excludeRead {
selectedCategories.insert(AdditionalExcludeCategoryId.read.rawValue)
}
if filter.data.excludeArchived {
if filterData.excludeArchived {
selectedCategories.insert(AdditionalExcludeCategoryId.archived.rawValue)
}
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .chatSelection(title: presentationData.strings.ChatListFolder_ExcludeChatsTitle, selectedChats: Set(filter.data.excludePeers), 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_ExcludeChatsTitle, selectedChats: Set(filterData.excludePeers), additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories), chatListFilters: allFilters), options: [], filters: [], alwaysEnabled: true, limit: 100))
controller.navigationPresentation = .modal
let _ = (controller.result
|> take(1)
@ -705,11 +720,15 @@ private func internalChatListFilterExcludeChatsController(context: AccountContex
var filters = filters
for i in 0 ..< filters.count {
if filters[i].id == filter.id {
filters[i].data.excludeMuted = additionalCategoryIds.contains(AdditionalExcludeCategoryId.muted.rawValue)
filters[i].data.excludeRead = additionalCategoryIds.contains(AdditionalExcludeCategoryId.read.rawValue)
filters[i].data.excludeArchived = additionalCategoryIds.contains(AdditionalExcludeCategoryId.archived.rawValue)
filters[i].data.excludePeers = excludePeers
filters[i].data.includePeers.setPeers(filters[i].data.includePeers.peers.filter { !filters[i].data.excludePeers.contains($0) })
if case let .filter(id, title, emoticon, data) = filter {
var updatedData = data
updatedData.excludeMuted = additionalCategoryIds.contains(AdditionalExcludeCategoryId.muted.rawValue)
updatedData.excludeRead = additionalCategoryIds.contains(AdditionalExcludeCategoryId.read.rawValue)
updatedData.excludeArchived = additionalCategoryIds.contains(AdditionalExcludeCategoryId.archived.rawValue)
updatedData.excludePeers = excludePeers
updatedData.includePeers.setPeers(updatedData.includePeers.peers.filter { !updatedData.excludePeers.contains($0) })
filters[i] = .filter(id: id, title: title, emoticon: emoticon, data: updatedData)
}
}
}
return filters
@ -719,11 +738,15 @@ private func internalChatListFilterExcludeChatsController(context: AccountContex
})
} else {
var filter = filter
filter.data.excludeMuted = additionalCategoryIds.contains(AdditionalExcludeCategoryId.muted.rawValue)
filter.data.excludeRead = additionalCategoryIds.contains(AdditionalExcludeCategoryId.read.rawValue)
filter.data.excludeArchived = additionalCategoryIds.contains(AdditionalExcludeCategoryId.archived.rawValue)
filter.data.excludePeers = excludePeers
filter.data.includePeers.setPeers(filter.data.includePeers.peers.filter { !filter.data.excludePeers.contains($0) })
if case let .filter(id, title, emoticon, data) = filter {
var updatedData = data
updatedData.excludeMuted = additionalCategoryIds.contains(AdditionalExcludeCategoryId.muted.rawValue)
updatedData.excludeRead = additionalCategoryIds.contains(AdditionalExcludeCategoryId.read.rawValue)
updatedData.excludeArchived = additionalCategoryIds.contains(AdditionalExcludeCategoryId.archived.rawValue)
updatedData.excludePeers = excludePeers
updatedData.includePeers.setPeers(updatedData.includePeers.peers.filter { !updatedData.excludePeers.contains($0) })
filter = .filter(id: id, title: title, emoticon: emoticon, data: updatedData)
}
updated(filter)
controller?.dismiss()
}
@ -742,27 +765,27 @@ enum ChatListFilterType {
case nonContacts
}
func chatListFilterType(_ filter: ChatListFilter) -> ChatListFilterType {
func chatListFilterType(_ data: ChatListFilterData) -> ChatListFilterType {
let filterType: ChatListFilterType
if filter.data.categories == .all {
if filter.data.excludeRead {
if data.categories == .all {
if data.excludeRead {
filterType = .unread
} else if filter.data.excludeMuted {
} else if data.excludeMuted {
filterType = .unmuted
} else {
filterType = .generic
}
} else {
if filter.data.categories == .channels {
if data.categories == .channels {
filterType = .channels
} else if filter.data.categories == .groups {
} else if data.categories == .groups {
filterType = .groups
} else if filter.data.categories == .bots {
} else if data.categories == .bots {
filterType = .bots
} else if filter.data.categories == .contacts {
} else if data.categories == .contacts {
filterType = .contacts
} else if filter.data.categories == .nonContacts {
} else if data.categories == .nonContacts {
filterType = .nonContacts
} else {
filterType = .generic
@ -772,6 +795,32 @@ func chatListFilterType(_ filter: ChatListFilter) -> ChatListFilterType {
return filterType
}
private extension ChatListFilter {
var title: String {
if case let .filter(_, title, _, _) = self {
return title
} else {
return ""
}
}
var emoticon: String? {
if case let .filter(_, _, emoticon, _) = self {
return emoticon
} else {
return nil
}
}
var data: ChatListFilterData? {
if case let .filter(_, _, _, data) = self {
return data
} else {
return nil
}
}
}
func chatListFilterPresetController(context: AccountContext, currentPreset: ChatListFilter?, updated: @escaping ([ChatListFilter]) -> Void) -> ViewController {
let initialName: String
if let currentPreset = currentPreset {
@ -779,7 +828,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
} else {
initialName = ""
}
let initialState = ChatListFilterPresetControllerState(name: initialName, changedName: currentPreset != nil, includeCategories: currentPreset?.data.categories ?? [], excludeMuted: currentPreset?.data.excludeMuted ?? false, excludeRead: currentPreset?.data.excludeRead ?? false, excludeArchived: currentPreset?.data.excludeArchived ?? false, additionallyIncludePeers: currentPreset?.data.includePeers.peers ?? [], additionallyExcludePeers: currentPreset?.data.excludePeers ?? [], expandedSections: [])
let initialState = ChatListFilterPresetControllerState(name: initialName, changedName: currentPreset != nil, includeCategories: currentPreset?.data?.categories ?? [], excludeMuted: currentPreset?.data?.excludeMuted ?? false, excludeRead: currentPreset?.data?.excludeRead ?? false, excludeArchived: currentPreset?.data?.excludeArchived ?? false, additionallyIncludePeers: currentPreset?.data?.includePeers.peers ?? [], additionallyExcludePeers: currentPreset?.data?.excludePeers ?? [], expandedSections: [])
let stateValue = Atomic(value: initialState)
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let updateState: ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void = { f in
@ -789,24 +838,26 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var includePeers = ChatListFilterIncludePeers()
includePeers.setPeers(state.additionallyIncludePeers)
let filter = ChatListFilter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers))
switch chatListFilterType(filter) {
case .generic:
state.name = initialName
case .unmuted:
state.name = presentationData.strings.ChatListFolder_NameNonMuted
case .unread:
state.name = presentationData.strings.ChatListFolder_NameUnread
case .channels:
state.name = presentationData.strings.ChatListFolder_NameChannels
case .groups:
state.name = presentationData.strings.ChatListFolder_NameGroups
case .bots:
state.name = presentationData.strings.ChatListFolder_NameBots
case .contacts:
state.name = presentationData.strings.ChatListFolder_NameContacts
case .nonContacts:
state.name = presentationData.strings.ChatListFolder_NameNonContacts
let filter: ChatListFilter = .filter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers))
if let data = filter.data {
switch chatListFilterType(data) {
case .generic:
state.name = initialName
case .unmuted:
state.name = presentationData.strings.ChatListFolder_NameNonMuted
case .unread:
state.name = presentationData.strings.ChatListFolder_NameUnread
case .channels:
state.name = presentationData.strings.ChatListFolder_NameChannels
case .groups:
state.name = presentationData.strings.ChatListFolder_NameGroups
case .bots:
state.name = presentationData.strings.ChatListFolder_NameBots
case .contacts:
state.name = presentationData.strings.ChatListFolder_NameContacts
case .nonContacts:
state.name = presentationData.strings.ChatListFolder_NameNonContacts
}
}
}
return state
@ -832,7 +883,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
let state = stateValue.with { $0 }
var includePeers = ChatListFilterIncludePeers()
includePeers.setPeers(state.additionallyIncludePeers)
let filter = ChatListFilter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers))
let filter: ChatListFilter = .filter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers))
let _ = (context.engine.peers.currentChatListFilters()
|> deliverOnMainQueue).start(next: { filters in
@ -840,9 +891,9 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
skipStateAnimation = true
updateState { state in
var state = state
state.additionallyIncludePeers = filter.data.includePeers.peers
state.additionallyExcludePeers = filter.data.excludePeers
state.includeCategories = filter.data.categories
state.additionallyIncludePeers = filter.data?.includePeers.peers ?? []
state.additionallyExcludePeers = filter.data?.excludePeers ?? []
state.includeCategories = filter.data?.categories ?? []
return state
}
})
@ -853,7 +904,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
let state = stateValue.with { $0 }
var includePeers = ChatListFilterIncludePeers()
includePeers.setPeers(state.additionallyIncludePeers)
let filter = ChatListFilter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers))
let filter: ChatListFilter = .filter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers))
let _ = (context.engine.peers.currentChatListFilters()
|> deliverOnMainQueue).start(next: { filters in
@ -861,12 +912,12 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
skipStateAnimation = true
updateState { state in
var state = state
state.additionallyIncludePeers = filter.data.includePeers.peers
state.additionallyExcludePeers = filter.data.excludePeers
state.includeCategories = filter.data.categories
state.excludeRead = filter.data.excludeRead
state.excludeMuted = filter.data.excludeMuted
state.excludeArchived = filter.data.excludeArchived
state.additionallyIncludePeers = filter.data?.includePeers.peers ?? []
state.additionallyExcludePeers = filter.data?.excludePeers ?? []
state.includeCategories = filter.data?.categories ?? []
state.excludeRead = filter.data?.excludeRead ?? false
state.excludeMuted = filter.data?.excludeMuted ?? false
state.excludeArchived = filter.data?.excludeArchived ?? false
return state
}
})
@ -997,18 +1048,23 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in
var includePeers = ChatListFilterIncludePeers()
includePeers.setPeers(state.additionallyIncludePeers)
var updatedFilter = ChatListFilter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers))
var filterId = currentPreset?.id ?? -1
if currentPreset == nil {
updatedFilter.id = context.engine.peers.generateNewChatListFilterId(filters: filters)
filterId = context.engine.peers.generateNewChatListFilterId(filters: filters)
}
var updatedFilter: ChatListFilter = .filter(id: filterId, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers))
var filters = filters
if let _ = currentPreset {
var found = false
for i in 0 ..< filters.count {
if filters[i].id == updatedFilter.id {
var includePeers = filters[i].data.includePeers
if filters[i].id == updatedFilter.id, case let .filter(_, _, _, data) = filters[i] {
var updatedData = data
var includePeers = updatedData.includePeers
includePeers.setPeers(state.additionallyIncludePeers)
updatedFilter.data.includePeers = includePeers
updatedData.includePeers = includePeers
updatedFilter = .filter(id: filterId, title: state.name, emoticon: currentPreset?.emoticon, data: updatedData)
filters[i] = updatedFilter
found = true
}
@ -1100,16 +1156,19 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
}
attemptNavigationImpl = {
let state = stateValue.with { $0 }
if let currentPreset = currentPreset {
var currentPresetWithoutPinnerPeers = currentPreset
if let currentPreset = currentPreset, case let .filter(currentId, currentTitle, currentEmoticon, currentData) = currentPreset {
var currentPresetWithoutPinnedPeers = currentPreset
var currentIncludePeers = ChatListFilterIncludePeers()
currentIncludePeers.setPeers(currentPresetWithoutPinnerPeers.data.includePeers.peers)
currentPresetWithoutPinnerPeers.data.includePeers = currentIncludePeers
currentIncludePeers.setPeers(currentData.includePeers.peers)
var currentPresetWithoutPinnedPeersData = currentData
currentPresetWithoutPinnedPeersData.includePeers = currentIncludePeers
currentPresetWithoutPinnedPeers = .filter(id: currentId, title: currentTitle, emoticon: currentEmoticon, data: currentPresetWithoutPinnedPeersData)
var includePeers = ChatListFilterIncludePeers()
includePeers.setPeers(state.additionallyIncludePeers)
let filter = ChatListFilter(id: currentPreset.id, title: state.name, emoticon: currentPreset.emoticon, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers))
if currentPresetWithoutPinnerPeers != filter {
let filter: ChatListFilter = .filter(id: currentPreset.id, title: state.name, emoticon: currentPreset.emoticon, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers))
if currentPresetWithoutPinnedPeers != filter {
displaySaveAlert()
return false
}

View File

@ -74,7 +74,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry {
case suggestedPreset(index: PresetIndex, title: String, label: String, preset: ChatListFilterData)
case suggestedAddCustom(String)
case listHeader(String)
case preset(index: PresetIndex, title: String, label: String, preset: ChatListFilter, canBeReordered: Bool, canBeDeleted: Bool, isEditing: Bool)
case preset(index: PresetIndex, title: String, label: String, preset: ChatListFilter, canBeReordered: Bool, canBeDeleted: Bool, isEditing: Bool, isAllChats: Bool)
case addItem(text: String, isEditing: Bool)
case listFooter(String)
@ -95,7 +95,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry {
return 0
case .listHeader:
return 100
case let .preset(index, _, _, _, _, _, _):
case let .preset(index, _, _, _, _, _, _, _):
return 101 + index.value
case .addItem:
return 1000
@ -122,7 +122,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry {
return .suggestedAddCustom
case .listHeader:
return .listHeader
case let .preset(_, _, _, preset, _, _, _):
case let .preset(_, _, _, preset, _, _, _, _):
return .preset(preset.id)
case .addItem:
return .addItem
@ -152,8 +152,8 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry {
})
case let .listHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, multiline: true, sectionId: self.section)
case let .preset(_, title, label, preset, canBeReordered, canBeDeleted, isEditing):
return ChatListFilterPresetListItem(presentationData: presentationData, preset: preset, title: title, label: label, editing: ChatListFilterPresetListItemEditing(editable: true, editing: isEditing, revealed: false), canBeReordered: canBeReordered, canBeDeleted: canBeDeleted, sectionId: self.section, action: {
case let .preset(_, title, label, preset, canBeReordered, canBeDeleted, isEditing, isAllChats):
return ChatListFilterPresetListItem(presentationData: presentationData, preset: preset, title: title, label: label, editing: ChatListFilterPresetListItemEditing(editable: true, editing: isEditing, revealed: false), canBeReordered: canBeReordered, canBeDeleted: canBeDeleted, isAllChats: isAllChats, sectionId: self.section, action: {
arguments.openPreset(preset)
}, setItemWithRevealedOptions: { lhs, rhs in
arguments.setItemWithRevealedOptions(lhs, rhs)
@ -191,15 +191,17 @@ private func filtersWithAppliedOrder(filters: [(ChatListFilter, Int)], order: [I
return sortedFilters
}
private func chatListFilterPresetListControllerEntries(presentationData: PresentationData, state: ChatListFilterPresetListControllerState, filters: [(ChatListFilter, Int)], updatedFilterOrder: [Int32]?, suggestedFilters: [ChatListFeaturedFilter], settings: ChatListFilterSettings) -> [ChatListFilterPresetListEntry] {
private func chatListFilterPresetListControllerEntries(presentationData: PresentationData, state: ChatListFilterPresetListControllerState, filters: [(ChatListFilter, Int)], updatedFilterOrder: [Int32]?, suggestedFilters: [ChatListFeaturedFilter], settings: ChatListFilterSettings, isPremium: Bool) -> [ChatListFilterPresetListEntry] {
var entries: [ChatListFilterPresetListEntry] = []
entries.append(.screenHeader(presentationData.strings.ChatListFolderSettings_Info))
let filteredSuggestedFilters = suggestedFilters.filter { suggestedFilter in
for (filter, _) in filters {
if filter.data == suggestedFilter.data {
return false
if case let .filter(_, _, _, data) = filter {
if data == suggestedFilter.data {
return false
}
}
}
return true
@ -209,7 +211,12 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present
entries.append(.listHeader(presentationData.strings.ChatListFolderSettings_FoldersSection))
for (filter, chatCount) in filtersWithAppliedOrder(filters: filters, order: updatedFilterOrder) {
entries.append(.preset(index: PresetIndex(value: entries.count), title: filter.title, label: chatCount == 0 ? "" : "\(chatCount)", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: true, isEditing: state.isEditing))
if isPremium, case .allChats = filter {
entries.append(.preset(index: PresetIndex(value: entries.count), title: "", label: "", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: false, isEditing: state.isEditing, isAllChats: true))
}
if case let .filter(_, title, _, _) = filter {
entries.append(.preset(index: PresetIndex(value: entries.count), title: title, label: chatCount == 0 ? "" : "\(chatCount)", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: true, isEditing: state.isEditing, isAllChats: false))
}
}
if filters.count < 10 {
entries.append(.addItem(text: presentationData.strings.ChatListFolderSettings_NewFolder, isEditing: state.isEditing))
@ -263,7 +270,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in
var filters = filters
let id = context.engine.peers.generateNewChatListFilterId(filters: filters)
filters.insert(ChatListFilter(id: id, title: title, emoticon: nil, data: data), at: 0)
filters.insert(.filter(id: id, title: title, emoticon: nil, data: data), at: 0)
return filters
}
|> deliverOnMainQueue).start(next: { _ in
@ -291,7 +298,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
} else {
if filters.count >= limit {
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumLimitScreen(context: context, subject: .folders, action: {
let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(filters.count), action: {
let controller = PremiumIntroScreen(context: context)
replaceImpl?(controller)
})
@ -360,9 +367,12 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
filtersWithCounts.get(),
preferences,
updatedFilterOrder.get(),
featuredFilters
featuredFilters,
context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
)
|> map { presentationData, state, filtersWithCountsValue, preferences, updatedFilterOrderValue, suggestedFilters -> (ItemListControllerState, (ItemListNodeState, Any)) in
|> map { presentationData, state, filtersWithCountsValue, preferences, updatedFilterOrderValue, suggestedFilters, result -> (ItemListControllerState, (ItemListNodeState, Any)) in
let isPremium = result?.isPremium ?? false
let filterSettings = preferences.values[ApplicationSpecificPreferencesKeys.chatListFilterSettings]?.get(ChatListFilterSettings.self) ?? ChatListFilterSettings.default
let leftNavigationButton: ItemListNavigationButton?
switch mode {
@ -431,7 +441,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChatListFolderSettings_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCountsValue, updatedFilterOrder: updatedFilterOrderValue, suggestedFilters: suggestedFilters, settings: filterSettings), style: .blocks, animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCountsValue, updatedFilterOrder: updatedFilterOrderValue, suggestedFilters: suggestedFilters, settings: filterSettings, isPremium: isPremium), style: .blocks, animateChanges: true)
return (controllerState, (listState, arguments))
}
@ -461,7 +471,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
}
controller.setReorderEntry({ (fromIndex: Int, toIndex: Int, entries: [ChatListFilterPresetListEntry]) -> Signal<Bool, NoError> in
let fromEntry = entries[fromIndex]
guard case let .preset(_, _, _, fromPreset, _, _, _) = fromEntry else {
guard case let .preset(_, _, _, fromPreset, _, _, _, _) = fromEntry else {
return .single(false)
}
var referenceFilter: ChatListFilter?
@ -469,7 +479,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
var afterAll = false
if toIndex < entries.count {
switch entries[toIndex] {
case let .preset(_, _, _, preset, _, _, _):
case let .preset(_, _, _, preset, _, _, _, _):
referenceFilter = preset
default:
if entries[toIndex] < fromEntry {

View File

@ -23,6 +23,7 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem {
let editing: ChatListFilterPresetListItemEditing
let canBeReordered: Bool
let canBeDeleted: Bool
let isAllChats: Bool
let sectionId: ItemListSectionId
let action: () -> Void
let setItemWithRevealedOptions: (Int32?, Int32?) -> Void
@ -36,6 +37,7 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem {
editing: ChatListFilterPresetListItemEditing,
canBeReordered: Bool,
canBeDeleted: Bool,
isAllChats: Bool,
sectionId: ItemListSectionId,
action: @escaping () -> Void,
setItemWithRevealedOptions: @escaping (Int32?, Int32?) -> Void,
@ -48,6 +50,7 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem {
self.editing = editing
self.canBeReordered = canBeReordered
self.canBeDeleted = canBeDeleted
self.isAllChats = isAllChats
self.sectionId = sectionId
self.action = action
self.setItemWithRevealedOptions = setItemWithRevealedOptions
@ -92,7 +95,9 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem {
}
}
var selectable: Bool = true
var selectable: Bool {
return !self.isAllChats
}
func selected(listView: ListView){
listView.clearHighlightAnimated(true)
@ -205,7 +210,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
}
let titleAttributedString = NSMutableAttributedString()
titleAttributedString.append(NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor))
titleAttributedString.append(NSAttributedString(string: item.isAllChats ? item.presentationData.strings.ChatList_FolderAllChats : item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor))
var editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)?
var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode)?
@ -293,6 +298,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
editableControlNode?.removeFromSupernode()
})
}
strongSelf.editableControlNode?.isHidden = !item.canBeDeleted
if let reorderControlSizeAndApply = reorderControlSizeAndApply {
if strongSelf.reorderControlNode == nil {
@ -374,6 +380,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
if let arrowImage = strongSelf.arrowNode.image {
strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 7.0 - arrowImage.size.width + revealOffset, y: floorToScreenPixels((layout.contentSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size)
}
strongSelf.arrowNode.isHidden = item.isAllChats
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 0.0), size: CGSize(width: params.width - params.rightInset - 56.0 - (leftInset + revealOffset + editingOffset), height: layout.contentSize.height))

View File

@ -192,7 +192,7 @@ private final class ItemNode: ASDisplayNode {
self.pressed()
}
func updateText(strings: PresentationStrings, title: String, shortTitle: String, unreadCount: Int, unreadHasUnmuted: Bool, isNoFilter: Bool, selectionFraction: CGFloat, isEditing: Bool, isAllChats: Bool, isReordering: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) {
func updateText(strings: PresentationStrings, title: String, shortTitle: String, unreadCount: Int, unreadHasUnmuted: Bool, isNoFilter: Bool, selectionFraction: CGFloat, isEditing: Bool, isAllChats: Bool, isReordering: Bool, canReorderAllChats: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) {
self.isEditing = isEditing
if self.theme !== presentationData.theme {
@ -215,7 +215,7 @@ private final class ItemNode: ASDisplayNode {
self.selectionFraction = selectionFraction
self.unreadCount = unreadCount
transition.updateAlpha(node: self.containerNode, alpha: isEditing || (isReordering && isAllChats) ? 0.5 : 1.0)
transition.updateAlpha(node: self.containerNode, alpha: isEditing || (isReordering && isAllChats && !canReorderAllChats) ? 0.5 : 1.0)
if isReordering && !isAllChats {
if self.deleteButtonNode == nil {
@ -265,7 +265,7 @@ private final class ItemNode: ASDisplayNode {
if self.isReordering != isReordering {
self.isReordering = isReordering
if self.isReordering && !isAllChats {
if self.isReordering && (!isAllChats || canReorderAllChats) {
self.startShaking()
} else {
self.layer.removeAnimation(forKey: "shaking_position")
@ -474,14 +474,14 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
private var reorderedItemIds: [ChatListFilterTabEntryId]?
private lazy var hapticFeedback = { HapticFeedback() }()
private var currentParams: (size: CGSize, sideInset: CGFloat, filters: [ChatListFilterTabEntry], selectedFilter: ChatListFilterTabEntryId?, isReordering: Bool, isEditing: Bool, transitionFraction: CGFloat, presentationData: PresentationData)?
private var currentParams: (size: CGSize, sideInset: CGFloat, filters: [ChatListFilterTabEntry], selectedFilter: ChatListFilterTabEntryId?, isReordering: Bool, isEditing: Bool, canReorderAllChats: Bool, transitionFraction: CGFloat, presentationData: PresentationData)?
var reorderedFilterIds: [Int32]? {
return self.reorderedItemIds.flatMap {
$0.compactMap {
switch $0 {
case .all:
return nil
return 0
case let .filter(id):
return id
}
@ -516,7 +516,7 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
}
for (id, itemNode) in strongSelf.itemNodes {
if itemNode.view.convert(itemNode.bounds, to: strongSelf.view).contains(point) {
if case .all = id {
if case .all = id, !(strongSelf.currentParams?.canReorderAllChats ?? false) {
return false
}
return true
@ -553,8 +553,8 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
strongSelf.addSubnode(itemNode)
strongSelf.reorderingItemPosition = (itemNode.frame.minX, 0.0)
if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, transitionFraction, presentationData) = strongSelf.currentParams {
strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, transitionFraction: transitionFraction, presentationData: presentationData, transition: .animated(duration: 0.25, curve: .easeInOut))
if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, canReorderAllChats, transitionFraction, presentationData) = strongSelf.currentParams {
strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, canReorderAllChats: canReorderAllChats, transitionFraction: transitionFraction, presentationData: presentationData, transition: .animated(duration: 0.25, curve: .easeInOut))
}
return
}
@ -573,13 +573,15 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
strongSelf.reorderingItemPosition = nil
strongSelf.reorderingAutoScrollAnimator?.invalidate()
strongSelf.reorderingAutoScrollAnimator = nil
if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, transitionFraction, presentationData) = strongSelf.currentParams {
strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, transitionFraction: transitionFraction, presentationData: presentationData, transition: .animated(duration: 0.25, curve: .easeInOut))
if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, canReorderAllChats, transitionFraction, presentationData) = strongSelf.currentParams {
strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, canReorderAllChats: canReorderAllChats, transitionFraction: transitionFraction, presentationData: presentationData, transition: .animated(duration: 0.25, curve: .easeInOut))
}
}, moved: { [weak self] offset in
guard let strongSelf = self, let reorderingItem = strongSelf.reorderingItem else {
return
}
let minIndex = (strongSelf.currentParams?.canReorderAllChats ?? false) ? 0 : 1
if let reorderingItemNode = strongSelf.itemNodes[reorderingItem], let (initial, _) = strongSelf.reorderingItemPosition, let reorderedItemIds = strongSelf.reorderedItemIds, let currentItemIndex = reorderedItemIds.firstIndex(of: reorderingItem) {
for (id, itemNode) in strongSelf.itemNodes {
@ -591,9 +593,9 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
if reorderingItemNode.frame.intersects(itemFrame) {
let targetIndex: Int
if reorderingItemNode.frame.midX < itemFrame.midX {
targetIndex = max(1, itemIndex - 1)
targetIndex = max(minIndex, itemIndex - 1)
} else {
targetIndex = max(1, min(reorderedItemIds.count - 1, itemIndex))
targetIndex = max(minIndex, min(reorderedItemIds.count - 1, itemIndex))
}
if targetIndex != currentItemIndex {
strongSelf.hapticFeedback.tap()
@ -607,8 +609,8 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
updatedReorderedItemIds.insert(reorderingItem, at: targetIndex)
}
strongSelf.reorderedItemIds = updatedReorderedItemIds
if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, transitionFraction, presentationData) = strongSelf.currentParams {
strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, transitionFraction: transitionFraction, presentationData: presentationData, transition: .animated(duration: 0.25, curve: .easeInOut))
if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, canReorderAllChats, transitionFraction, presentationData) = strongSelf.currentParams {
strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, canReorderAllChats: canReorderAllChats, transitionFraction: transitionFraction, presentationData: presentationData, transition: .animated(duration: 0.25, curve: .easeInOut))
}
}
break
@ -618,8 +620,8 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
strongSelf.reorderingItemPosition = (initial, offset)
}
if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, transitionFraction, presentationData) = strongSelf.currentParams {
strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, transitionFraction: transitionFraction, presentationData: presentationData, transition: .immediate)
if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, canReorderAllChats, transitionFraction, presentationData) = strongSelf.currentParams {
strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, canReorderAllChats: canReorderAllChats, transitionFraction: transitionFraction, presentationData: presentationData, transition: .immediate)
}
})
self.reorderingGesture = reorderingGesture
@ -635,7 +637,7 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
self.scrollNode.layer.removeAllAnimations()
}
func update(size: CGSize, sideInset: CGFloat, filters: [ChatListFilterTabEntry], selectedFilter: ChatListFilterTabEntryId?, isReordering: Bool, isEditing: Bool, transitionFraction: CGFloat, presentationData: PresentationData, transition proposedTransition: ContainedViewLayoutTransition) {
func update(size: CGSize, sideInset: CGFloat, filters: [ChatListFilterTabEntry], selectedFilter: ChatListFilterTabEntryId?, isReordering: Bool, isEditing: Bool, canReorderAllChats: Bool, transitionFraction: CGFloat, presentationData: PresentationData, transition proposedTransition: ContainedViewLayoutTransition) {
let isFirstTime = self.currentParams == nil
let transition: ContainedViewLayoutTransition = isFirstTime ? .immediate : proposedTransition
@ -680,7 +682,7 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
self.reorderedItemIds = nil
}
self.currentParams = (size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering, isEditing, transitionFraction, presentationData: presentationData)
self.currentParams = (size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering, isEditing, canReorderAllChats, transitionFraction, presentationData: presentationData)
self.reorderingGesture?.isEnabled = isReordering
@ -762,7 +764,7 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
selectionFraction = 0.0
}
itemNode.updateText(strings: presentationData.strings, title: filter.title(strings: presentationData.strings), shortTitle: filter.shortTitle(strings: presentationData.strings), unreadCount: unreadCount, unreadHasUnmuted: unreadHasUnmuted, isNoFilter: isNoFilter, selectionFraction: selectionFraction, isEditing: isEditing, isAllChats: isNoFilter, isReordering: isReordering, presentationData: presentationData, transition: itemNodeTransition)
itemNode.updateText(strings: presentationData.strings, title: filter.title(strings: presentationData.strings), shortTitle: filter.shortTitle(strings: presentationData.strings), unreadCount: unreadCount, unreadHasUnmuted: unreadHasUnmuted, isNoFilter: isNoFilter, selectionFraction: selectionFraction, isEditing: isEditing, isAllChats: isNoFilter, isReordering: isReordering, canReorderAllChats: canReorderAllChats, presentationData: presentationData, transition: itemNodeTransition)
}
var removeKeys: [ChatListFilterTabEntryId] = []
for (id, _) in self.itemNodes {
@ -893,7 +895,13 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
} else {
transition.updateFrame(node: self.selectedLineNode, frame: lineFrame)
}
transition.updateAlpha(node: self.selectedLineNode, alpha: isReordering && selectedFilter == .all ? 0.5 : 1.0)
let lineAlpha: CGFloat
if isReordering && canReorderAllChats {
lineAlpha = 0.0
} else {
lineAlpha = isReordering && selectedFilter == .all ? 0.5 : 1.0
}
transition.updateAlpha(node: self.selectedLineNode, alpha: lineAlpha)
if let previousSelectedFrame = self.previousSelectedFrame {
let previousContentOffsetX = max(0.0, min(previousContentWidth - previousScrollBounds.width, floor(previousSelectedFrame.midX - previousScrollBounds.width / 2.0)))

View File

@ -390,7 +390,7 @@ final class ChatListFilterTabInlineContainerNode: ASDisplayNode {
$0.compactMap {
switch $0 {
case .all:
return nil
return 0
case let .filter(id):
return id
}

View File

@ -842,9 +842,9 @@ public final class ChatListNode: ListView {
switch result {
case .done:
break
case .limitExceeded:
case let .limitExceeded(count, _):
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumLimitScreen(context: context, subject: .pins, action: {
let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: {
let premiumScreen = PremiumIntroScreen(context: context)
replaceImpl?(premiumScreen)
})
@ -1175,8 +1175,12 @@ public final class ChatListNode: ListView {
updatedScrollPosition = nil
}
let filterData = filter.flatMap { filter -> ChatListItemFilterData in
return ChatListItemFilterData(excludesArchived: filter.data.excludeArchived)
let filterData = filter.flatMap { filter -> ChatListItemFilterData? in
if case let .filter(_, _, _, data) = filter {
return ChatListItemFilterData(excludesArchived: data.excludeArchived)
} else {
return nil
}
}
return preparedChatListNodeViewTransition(from: previousView, to: processedView, reason: reason, previewing: previewing, disableAnimations: disableAnimations, account: context.account, scrollPosition: updatedScrollPosition, searchMode: searchMode)
@ -2265,13 +2269,13 @@ private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: Pres
if let chatListFilters = chatListFilters {
var result = ""
for filter in chatListFilters {
let predicate = chatListFilterPredicate(filter: filter.data)
for case let .filter(_, title, _, data) in chatListFilters {
let predicate = chatListFilterPredicate(filter: data)
if predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: hasUnseenMentions) {
if !result.isEmpty {
result.append(", ")
}
result.append(filter.title)
result.append(title)
}
}

View File

@ -110,7 +110,12 @@ public func chatListFilterPredicate(filter: ChatListFilterData) -> ChatListFilte
}
func chatListViewForLocation(groupId: PeerGroupId, location: ChatListNodeLocation, account: Account) -> Signal<ChatListNodeViewUpdate, NoError> {
let filterPredicate: ChatListFilterPredicate? = (location.filter?.data).flatMap(chatListFilterPredicate)
let filterPredicate: ChatListFilterPredicate?
if let filter = location.filter, case let .filter(_, _, _, data) = filter {
filterPredicate = chatListFilterPredicate(filter: data)
} else {
filterPredicate = nil
}
switch location {
case let .initial(count, _):

View File

@ -188,9 +188,7 @@ func preparedChatListNodeViewTransition(from fromView: ChatListNodeView?, to toV
}
} else if fromView.filteredEntries.isEmpty || fromView.filter != toView.filter {
var updateEmpty = true
if !fromView.filteredEntries.isEmpty, let fromFilter = fromView.filter, let toFilter = toView.filter, fromFilter.data.includePeers.pinnedPeers != toFilter.data.includePeers.pinnedPeers {
var fromData = fromFilter.data
let toData = toFilter.data
if !fromView.filteredEntries.isEmpty, let fromFilter = fromView.filter, let toFilter = toView.filter, case var .filter(_, _, _, fromData) = fromFilter, case let .filter(_, _, _, toData) = toFilter, fromData.includePeers.pinnedPeers != toData.includePeers.pinnedPeers {
fromData.includePeers = toData.includePeers
if fromData == toData {
options.insert(.AnimateInsertion)

View File

@ -17,10 +17,10 @@ func chatListFilterItems(context: AccountContext) -> Signal<(Int, [(ChatListFilt
unreadCountItems.append(.totalInGroup(.root))
var additionalPeerIds = Set<PeerId>()
var additionalGroupIds = Set<PeerGroupId>()
for filter in filters {
additionalPeerIds.formUnion(filter.data.includePeers.peers)
additionalPeerIds.formUnion(filter.data.excludePeers)
if !filter.data.excludeArchived {
for case let .filter(_, _, _, data) in filters {
additionalPeerIds.formUnion(data.includePeers.peers)
additionalPeerIds.formUnion(data.excludePeers)
if !data.excludeArchived {
additionalGroupIds.insert(Namespaces.PeerGroup.archive)
}
}
@ -79,50 +79,29 @@ func chatListFilterItems(context: AccountContext) -> Signal<(Int, [(ChatListFilt
let totalBadge = 0
for filter in filters {
var tags: [PeerSummaryCounterTags] = []
if filter.data.categories.contains(.contacts) {
tags.append(.contact)
}
if filter.data.categories.contains(.nonContacts) {
tags.append(.nonContact)
}
if filter.data.categories.contains(.groups) {
tags.append(.group)
}
if filter.data.categories.contains(.bots) {
tags.append(.bot)
}
if filter.data.categories.contains(.channels) {
tags.append(.channel)
}
var count = 0
var unmutedUnreadCount = 0
if let totalState = totalStates[.root] {
for tag in tags {
if filter.data.excludeMuted {
if let value = totalState.filteredCounters[tag] {
if value.chatCount != 0 {
count += Int(value.chatCount)
unmutedUnreadCount += Int(value.chatCount)
}
}
} else {
if let value = totalState.absoluteCounters[tag] {
count += Int(value.chatCount)
}
if let value = totalState.filteredCounters[tag] {
if value.chatCount != 0 {
unmutedUnreadCount += Int(value.chatCount)
}
}
}
if case let .filter(_, _, _, data) = filter {
var tags: [PeerSummaryCounterTags] = []
if data.categories.contains(.contacts) {
tags.append(.contact)
}
}
if !filter.data.excludeArchived {
if let totalState = totalStates[Namespaces.PeerGroup.archive] {
if data.categories.contains(.nonContacts) {
tags.append(.nonContact)
}
if data.categories.contains(.groups) {
tags.append(.group)
}
if data.categories.contains(.bots) {
tags.append(.bot)
}
if data.categories.contains(.channels) {
tags.append(.channel)
}
if let totalState = totalStates[.root] {
for tag in tags {
if filter.data.excludeMuted {
if data.excludeMuted {
if let value = totalState.filteredCounters[tag] {
if value.chatCount != 0 {
count += Int(value.chatCount)
@ -141,62 +120,85 @@ func chatListFilterItems(context: AccountContext) -> Signal<(Int, [(ChatListFilt
}
}
}
}
for peerId in filter.data.includePeers.peers {
if let (tag, peerCount, hasUnmuted, groupIdValue, isMuted) = peerTagAndCount[peerId], peerCount != 0, let groupId = groupIdValue {
var matches = true
if tags.contains(tag) {
if isMuted && filter.data.excludeMuted {
} else {
matches = false
}
}
if matches {
let matchesGroup: Bool
switch groupId {
case .root:
matchesGroup = true
case .group:
if groupId == Namespaces.PeerGroup.archive {
matchesGroup = !filter.data.excludeArchived
if !data.excludeArchived {
if let totalState = totalStates[Namespaces.PeerGroup.archive] {
for tag in tags {
if data.excludeMuted {
if let value = totalState.filteredCounters[tag] {
if value.chatCount != 0 {
count += Int(value.chatCount)
unmutedUnreadCount += Int(value.chatCount)
}
}
} else {
matchesGroup = false
}
}
if matchesGroup && peerCount != 0 {
count += 1
if hasUnmuted {
unmutedUnreadCount += 1
if let value = totalState.absoluteCounters[tag] {
count += Int(value.chatCount)
}
if let value = totalState.filteredCounters[tag] {
if value.chatCount != 0 {
unmutedUnreadCount += Int(value.chatCount)
}
}
}
}
}
}
}
for peerId in filter.data.excludePeers {
if let (tag, peerCount, _, groupIdValue, isMuted) = peerTagAndCount[peerId], peerCount != 0, let groupId = groupIdValue {
var matches = true
if tags.contains(tag) {
if isMuted && filter.data.excludeMuted {
matches = false
}
}
if matches {
let matchesGroup: Bool
switch groupId {
case .root:
matchesGroup = true
case .group:
if groupId == Namespaces.PeerGroup.archive {
matchesGroup = !filter.data.excludeArchived
for peerId in data.includePeers.peers {
if let (tag, peerCount, hasUnmuted, groupIdValue, isMuted) = peerTagAndCount[peerId], peerCount != 0, let groupId = groupIdValue {
var matches = true
if tags.contains(tag) {
if isMuted && data.excludeMuted {
} else {
matchesGroup = false
matches = false
}
}
if matchesGroup && peerCount != 0 {
count -= 1
if !isMuted {
unmutedUnreadCount -= 1
if matches {
let matchesGroup: Bool
switch groupId {
case .root:
matchesGroup = true
case .group:
if groupId == Namespaces.PeerGroup.archive {
matchesGroup = !data.excludeArchived
} else {
matchesGroup = false
}
}
if matchesGroup && peerCount != 0 {
count += 1
if hasUnmuted {
unmutedUnreadCount += 1
}
}
}
}
}
for peerId in data.excludePeers {
if let (tag, peerCount, _, groupIdValue, isMuted) = peerTagAndCount[peerId], peerCount != 0, let groupId = groupIdValue {
var matches = true
if tags.contains(tag) {
if isMuted && data.excludeMuted {
matches = false
}
}
if matches {
let matchesGroup: Bool
switch groupId {
case .root:
matchesGroup = true
case .group:
if groupId == Namespaces.PeerGroup.archive {
matchesGroup = !data.excludeArchived
} else {
matchesGroup = false
}
}
if matchesGroup && peerCount != 0 {
count -= 1
if !isMuted {
unmutedUnreadCount -= 1
}
}
}
}

View File

@ -20,6 +20,10 @@ public final class MultilineTextComponent: Component {
public let insets: UIEdgeInsets
public let textShadowColor: UIColor?
public let textStroke: (UIColor, CGFloat)?
public let highlightColor: UIColor?
public let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)?
public let tapAction: (([NSAttributedString.Key: Any], Int) -> Void)?
public let longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)?
public init(
text: TextContent,
@ -31,7 +35,11 @@ public final class MultilineTextComponent: Component {
cutout: TextNodeCutout? = nil,
insets: UIEdgeInsets = UIEdgeInsets(),
textShadowColor: UIColor? = nil,
textStroke: (UIColor, CGFloat)? = nil
textStroke: (UIColor, CGFloat)? = nil,
highlightColor: UIColor? = nil,
highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = nil,
tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil,
longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil
) {
self.text = text
self.horizontalAlignment = horizontalAlignment
@ -43,6 +51,10 @@ public final class MultilineTextComponent: Component {
self.insets = insets
self.textShadowColor = textShadowColor
self.textStroke = textStroke
self.highlightColor = highlightColor
self.highlightAction = highlightAction
self.tapAction = tapAction
self.longTapAction = longTapAction
}
public static func ==(lhs: MultilineTextComponent, rhs: MultilineTextComponent) -> Bool {
@ -90,10 +102,18 @@ public final class MultilineTextComponent: Component {
return false
}
if let lhsHighlightColor = lhs.highlightColor, let rhsHighlightColor = rhs.highlightColor {
if !lhsHighlightColor.isEqual(rhsHighlightColor) {
return false
}
} else if (lhs.highlightColor != nil) != (rhs.highlightColor != nil) {
return false
}
return true
}
public final class View: TextView {
public final class View: ImmediateTextView {
public func update(component: MultilineTextComponent, availableSize: CGSize) -> CGSize {
let attributedString: NSAttributedString
switch component.text {
@ -102,26 +122,25 @@ public final class MultilineTextComponent: Component {
case let .markdown(text, attributes):
attributedString = parseMarkdownIntoAttributedString(text, attributes: attributes)
}
self.attributedText = attributedString
self.maximumNumberOfLines = component.maximumNumberOfLines
self.truncationType = component.truncationType
self.textAlignment = component.horizontalAlignment
self.verticalAlignment = component.verticalAlignment
self.lineSpacing = component.lineSpacing
self.cutout = component.cutout
self.insets = component.insets
self.textShadowColor = component.textShadowColor
self.textStroke = component.textStroke
self.linkHighlightColor = component.highlightColor
self.highlightAttributeAction = component.highlightAction
self.tapAttributeAction = component.tapAction
self.longTapAttributeAction = component.longTapAction
let makeLayout = TextView.asyncLayout(self)
let (layout, apply) = makeLayout(TextNodeLayoutArguments(
attributedString: attributedString,
backgroundColor: nil,
maximumNumberOfLines: component.maximumNumberOfLines,
truncationType: component.truncationType,
constrainedSize: availableSize,
alignment: component.horizontalAlignment,
verticalAlignment: component.verticalAlignment,
lineSpacing: component.lineSpacing,
cutout: component.cutout,
insets: component.insets,
textShadowColor: component.textShadowColor,
textStroke: component.textStroke,
displaySpoilers: false
))
let _ = apply()
return layout.size
let size = self.updateLayout(availableSize)
return size
}
}

View File

@ -59,6 +59,12 @@ open class ViewControllerComponentContainer: ViewController {
case `default`
}
public enum StatusBarStyle {
case none
case ignore
case `default`
}
public final class Environment: Equatable {
public let statusBarHeight: CGFloat
public let navigationHeight: CGFloat
@ -127,11 +133,11 @@ open class ViewControllerComponentContainer: ViewController {
}
public final class Node: ViewControllerTracingNode {
private var presentationData: PresentationData
fileprivate var presentationData: PresentationData
private weak var controller: ViewControllerComponentContainer?
private var component: AnyComponent<ViewControllerComponentContainer.Environment>
private let theme: PresentationTheme?
var theme: PresentationTheme?
public let hostView: ComponentHostView<ViewControllerComponentContainer.Environment>
private var currentIsVisible: Bool = false
@ -204,30 +210,68 @@ open class ViewControllerComponentContainer: ViewController {
}
private let context: AccountContext
private let theme: PresentationTheme?
private var theme: PresentationTheme?
private let component: AnyComponent<ViewControllerComponentContainer.Environment>
public init<C: Component>(context: AccountContext, component: C, navigationBarAppearance: NavigationBarAppearance, theme: PresentationTheme? = nil) where C.EnvironmentType == ViewControllerComponentContainer.Environment {
private var presentationDataDisposable: Disposable?
private var validLayout: ContainerViewLayout?
public init<C: Component>(context: AccountContext, component: C, navigationBarAppearance: NavigationBarAppearance, statusBarStyle: StatusBarStyle = .default, theme: PresentationTheme? = nil) where C.EnvironmentType == ViewControllerComponentContainer.Environment {
self.context = context
self.component = AnyComponent(component)
self.theme = theme
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let navigationBarPresentationData: NavigationBarPresentationData?
switch navigationBarAppearance {
case .none:
navigationBarPresentationData = nil
case .transparent:
navigationBarPresentationData = NavigationBarPresentationData(presentationData: context.sharedContext.currentPresentationData.with { $0 }, hideBackground: true, hideBadge: false, hideSeparator: true)
navigationBarPresentationData = NavigationBarPresentationData(presentationData: presentationData, hideBackground: true, hideBadge: false, hideSeparator: true)
case .default:
navigationBarPresentationData = NavigationBarPresentationData(presentationData: context.sharedContext.currentPresentationData.with { $0 })
navigationBarPresentationData = NavigationBarPresentationData(presentationData: presentationData)
}
super.init(navigationBarPresentationData: navigationBarPresentationData)
self.presentationDataDisposable = (self.context.sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
strongSelf.node.presentationData = presentationData
switch statusBarStyle {
case .none:
strongSelf.statusBar.statusBarStyle = .Hide
case .ignore:
strongSelf.statusBar.statusBarStyle = .Ignore
case .default:
strongSelf.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style
}
if let layout = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, transition: .immediate)
}
}
})
switch statusBarStyle {
case .none:
self.statusBar.statusBarStyle = .Hide
case .ignore:
self.statusBar.statusBarStyle = .Ignore
case .default:
self.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDataDisposable?.dispose()
}
override open func loadDisplayNode() {
self.displayNode = Node(context: self.context, controller: self, component: self.component, theme: self.theme)
@ -255,6 +299,7 @@ open class ViewControllerComponentContainer: ViewController {
let navigationHeight = self.navigationLayout(layout: layout).navigationFrame.maxY
self.validLayout = layout
self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition))
}

View File

@ -30,5 +30,7 @@ public protocol PeekControllerContentNode {
}
public protocol PeekControllerAccessoryNode {
var dismiss: () -> Void { get set }
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition)
}

View File

@ -115,6 +115,9 @@ final class PeekControllerNode: ViewControllerTracingNode {
self.addSubnode(self.actionsContainerNode)
if let fullScreenAccessoryNode = self.fullScreenAccessoryNode {
self.fullScreenAccessoryNode?.dismiss = { [weak self] in
self?.requestDismiss()
}
self.addSubnode(fullScreenAccessoryNode)
}
@ -194,6 +197,7 @@ final class PeekControllerNode: ViewControllerTracingNode {
if let fullScreenAccessoryNode = self.fullScreenAccessoryNode {
fullScreenAccessoryNode.updateLayout(size: layout.size, transition: transition)
transition.updateFrame(node: fullScreenAccessoryNode, frame: CGRect(origin: .zero, size: layout.size))
}
self.contentNodeHasValidLayout = true

View File

@ -226,7 +226,7 @@ public class ASTextNode: ImmediateTextNode {
}
}
public class ImmediateTextView: TextView {
open class ImmediateTextView: TextView {
public var attributedText: NSAttributedString?
public var textAlignment: NSTextAlignment = .natural
public var verticalAlignment: TextVerticalAlignment = .top

View File

@ -4,7 +4,7 @@ import SwiftSignalKit
private var backArrowImageCache: [Int32: UIImage] = [:]
public final class SparseNode: ASDisplayNode {
open class SparseNode: ASDisplayNode {
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.alpha.isZero {
return nil

View File

@ -1621,7 +1621,6 @@ open class TextView: UIView {
private class func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textStroke: (UIColor, CGFloat)?, displaySpoilers: Bool) -> TextNodeLayout {
if let attributedString = attributedString {
let stringLength = attributedString.length
let font: CTFont

View File

@ -15,6 +15,8 @@ swift_library(
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/PersistentStringHash:PersistentStringHash",
"//submodules/AccountContext:AccountContext",
"//submodules/UrlHandling:UrlHandling",
],
visibility = [
"//visibility:public",

View File

@ -0,0 +1,88 @@
import Foundation
import SwiftSignalKit
import Postbox
import TelegramCore
import AccountContext
//import InstantPageUI
import UrlHandling
public func extractAnchor(string: String) -> (String, String?) {
var anchorValue: String?
if let anchorRange = string.range(of: "#") {
let anchor = string[anchorRange.upperBound...]
if !anchor.isEmpty {
anchorValue = String(anchor)
}
}
var trimmedUrl = string
if let anchor = anchorValue, let anchorRange = string.range(of: "#\(anchor)") {
let url = string[..<anchorRange.lowerBound]
if !url.isEmpty {
trimmedUrl = String(url)
}
}
return (trimmedUrl, anchorValue)
}
private let refreshTimeout: Int32 = 60 * 60 * 12
public func cachedFaqInstantPage(context: AccountContext) -> Signal<ResolvedUrl, NoError> {
var faqUrl = context.sharedContext.currentPresentationData.with { $0 }.strings.Settings_FAQ_URL
if faqUrl == "Settings.FAQ_URL" || faqUrl.isEmpty {
faqUrl = "https://telegram.org/faq#general-questions"
}
return cachedInternalInstantPage(context: context, url: faqUrl)
}
public func cachedTermsPage(context: AccountContext) -> Signal<ResolvedUrl, NoError> {
var termsUrl = context.sharedContext.currentPresentationData.with { $0 }.strings.Settings_Terms_URL
if termsUrl == "Settings.Terms_URL" || termsUrl.isEmpty {
termsUrl = "https://telegram.org/tos"
}
return cachedInternalInstantPage(context: context, url: termsUrl)
}
public func cachedPrivacyPage(context: AccountContext) -> Signal<ResolvedUrl, NoError> {
var privacyUrl = context.sharedContext.currentPresentationData.with { $0 }.strings.Settings_PrivacyPolicy_URL
if privacyUrl == "Settings.PrivacyPolicy_URL" || privacyUrl.isEmpty {
privacyUrl = "https://telegram.org/privacy"
}
return cachedInternalInstantPage(context: context, url: privacyUrl)
}
private func cachedInternalInstantPage(context: AccountContext, url: String) -> Signal<ResolvedUrl, NoError> {
let (cachedUrl, anchor) = extractAnchor(string: url)
return cachedInstantPage(postbox: context.account.postbox, url: cachedUrl)
|> mapToSignal { cachedInstantPage -> Signal<ResolvedUrl, NoError> in
let updated = resolveInstantViewUrl(account: context.account, url: url)
|> afterNext { result in
if case let .instantView(webPage, _) = result, case let .Loaded(content) = webPage.content, let instantPage = content.instantPage {
if instantPage.isComplete {
let _ = updateCachedInstantPage(postbox: context.account.postbox, url: cachedUrl, webPage: webPage).start()
} else {
let _ = (actualizedWebpage(postbox: context.account.postbox, network: context.account.network, webpage: webPage)
|> mapToSignal { webPage -> Signal<Void, NoError> in
if case let .Loaded(content) = webPage.content, let instantPage = content.instantPage, instantPage.isComplete {
return updateCachedInstantPage(postbox: context.account.postbox, url: cachedUrl, webPage: webPage)
} else {
return .complete()
}
}).start()
}
}
}
let now = Int32(CFAbsoluteTimeGetCurrent())
if let cachedInstantPage = cachedInstantPage, case let .Loaded(content) = cachedInstantPage.webPage.content, let instantPage = content.instantPage, instantPage.isComplete {
let current: Signal<ResolvedUrl, NoError> = .single(.instantView(cachedInstantPage.webPage, anchor))
if now > cachedInstantPage.timestamp + refreshTimeout {
return current
|> then(updated)
} else {
return current
}
} else {
return updated
}
}
}

View File

@ -603,14 +603,16 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
var updatedLabelBadgeImage: UIImage?
var currentCredibilityIconImage: UIImage?
if item.peer.isScam {
currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular)
} else if item.peer.isFake {
currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular)
} else if item.peer.isVerified {
currentCredibilityIconImage = PresentationResourcesChatList.verifiedIcon(item.presentationData.theme)
} else if item.peer.isPremium {
currentCredibilityIconImage = PresentationResourcesChatList.premiumIcon(item.presentationData.theme)
if item.peer.id != item.context.account.peerId {
if item.peer.isScam {
currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular)
} else if item.peer.isFake {
currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular)
} else if item.peer.isVerified {
currentCredibilityIconImage = PresentationResourcesChatList.verifiedIcon(item.presentationData.theme)
} else if item.peer.isPremium {
currentCredibilityIconImage = PresentationResourcesChatList.premiumIcon(item.presentationData.theme)
}
}
var titleIconsWidth: CGFloat = 0.0

View File

@ -241,6 +241,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
self.installationActionBackgroundNode.displayWithoutProcessing = true
self.installationActionBackgroundNode.isLayerBacked = true
self.installationActionNode = HighlightableButtonNode()
self.installationActionNode.hitTestSlop = UIEdgeInsets(top: -16.0, left: -16.0, bottom: -16.0, right: -16.0)
self.installTextNode = TextNode()
self.installTextNode.isUserInteractionEnabled = false

View File

@ -153,12 +153,14 @@ class IncreaseLimitHeaderItemNode: ListViewItemNode {
UIColor(rgb: 0xe46ace)
],
inactiveTitle: item.strings.Premium_Free,
inactiveValue: "",
inactiveTitleColor: .black,
activeTitle: item.strings.Premium_Premium,
activeValue: "\(item.premiumCount)",
activeTitleColor: .white,
badgeIconName: badgeIconName,
badgeText: "\(item.count)"
badgeText: "\(item.count)",
badgePosition: CGFloat(item.count) / CGFloat(item.premiumCount)
)),
environment: {},
containerSize: CGSize(width: layout.size.width - params.leftInset - params.rightInset, height: 200.0)

View File

@ -20,6 +20,7 @@ let package = Package(
.package(name: "sqlcipher", path: "../sqlcipher"),
.package(name: "StringTransliteration", path: "../StringTransliteration"),
.package(name: "ManagedFile", path: "../ManagedFile"),
.package(name: "RangeSet", path: "../Utils/RangeSet"),
.package(name: "SSignalKit", path: "../SSignalKit"),
],
targets: [
@ -30,6 +31,7 @@ let package = Package(
dependencies: [.product(name: "MurMurHash32", package: "MurMurHash32", condition: nil),
.product(name: "SwiftSignalKit", package: "SSignalKit", condition: nil),
.product(name: "ManagedFile", package: "ManagedFile", condition: nil),
.product(name: "RangeSet", package: "RangeSet", condition: nil),
.product(name: "sqlcipher", package: "sqlcipher", condition: nil),
.product(name: "StringTransliteration", package: "StringTransliteration", condition: nil),
.product(name: "Crc32", package: "Crc32", condition: nil)],

View File

@ -42,6 +42,9 @@ swift_library(
"//submodules/Components/Forms/PrefixSectionGroupComponent:PrefixSectionGroupComponent",
"//submodules/InAppPurchaseManager:InAppPurchaseManager",
"//submodules/ConfettiEffect:ConfettiEffect",
"//submodules/TextFormat:TextFormat",
"//submodules/GZip:GZip",
"//submodules/InstantPageCache:InstantPageCache",
],
visibility = [
"//visibility:public",

Binary file not shown.

View File

@ -12,320 +12,10 @@ import PrefixSectionGroupComponent
import BundleIconComponent
import SolidRoundedButtonComponent
import Markdown
import SceneKit
import InAppPurchaseManager
import ConfettiEffect
private func deg2rad(_ number: Float) -> Float {
return number * .pi / 180
}
private func rad2deg(_ number: Float) -> Float {
return number * 180.0 / .pi
}
private class StarComponent: Component {
let isVisible: Bool
init(isVisible: Bool) {
self.isVisible = isVisible
}
static func ==(lhs: StarComponent, rhs: StarComponent) -> Bool {
return lhs.isVisible == rhs.isVisible
}
final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView {
final class Tag {
}
func matches(tag: Any) -> Bool {
if let _ = tag as? Tag {
return true
}
return false
}
private var _ready = Promise<Bool>()
var ready: Signal<Bool, NoError> {
return self._ready.get()
}
private let sceneView: SCNView
private var previousInteractionTimestamp: Double = 0.0
private var timer: SwiftSignalKit.Timer?
override init(frame: CGRect) {
self.sceneView = SCNView(frame: frame)
self.sceneView.backgroundColor = .clear
self.sceneView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
self.sceneView.isUserInteractionEnabled = false
super.init(frame: frame)
self.addSubview(self.sceneView)
self.setup()
let panGestureRecoginzer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
self.addGestureRecognizer(panGestureRecoginzer)
let tapGestureRecoginzer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
self.addGestureRecognizer(tapGestureRecoginzer)
self.disablesInteractiveModalDismiss = true
self.disablesInteractiveTransitionGestureRecognizer = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.timer?.invalidate()
}
@objc private func handleTap(_ gesture: UITapGestureRecognizer) {
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
return
}
self.previousInteractionTimestamp = CACurrentMediaTime()
var left = true
if let view = gesture.view {
let point = gesture.location(in: view)
let distanceFromCenter = abs(point.x - view.frame.size.width / 2.0)
if distanceFromCenter > 60.0 {
return
}
if point.x > view.frame.width / 2.0 {
left = false
}
}
if node.animationKeys.contains("tapRotate") {
self.playAppearanceAnimation(velocity: nil, mirror: left, explode: true)
return
}
let initial = node.rotation
let target = SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: left ? -0.6 : 0.6)
let animation = CABasicAnimation(keyPath: "rotation")
animation.fromValue = NSValue(scnVector4: initial)
animation.toValue = NSValue(scnVector4: target)
animation.duration = 0.25
animation.timingFunction = CAMediaTimingFunction(name: .easeOut)
animation.fillMode = .forwards
node.addAnimation(animation, forKey: "tapRotate")
node.rotation = target
Queue.mainQueue().after(0.25) {
node.rotation = initial
let springAnimation = CASpringAnimation(keyPath: "rotation")
springAnimation.fromValue = NSValue(scnVector4: target)
springAnimation.toValue = NSValue(scnVector4: SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: 0.0))
springAnimation.mass = 1.0
springAnimation.stiffness = 21.0
springAnimation.damping = 5.8
springAnimation.duration = springAnimation.settlingDuration * 0.8
node.addAnimation(springAnimation, forKey: "tapRotate")
}
}
private var previousAngle: Float = 0.0
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
return
}
self.previousInteractionTimestamp = CACurrentMediaTime()
if #available(iOS 11.0, *) {
node.removeAnimation(forKey: "rotate", blendOutDuration: 0.1)
node.removeAnimation(forKey: "tapRotate", blendOutDuration: 0.1)
} else {
node.removeAllAnimations()
}
switch gesture.state {
case .began:
self.previousAngle = 0.0
case .changed:
let translation = gesture.translation(in: gesture.view)
let anglePan = deg2rad(Float(translation.x))
self.previousAngle = anglePan
node.rotation = SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: self.previousAngle)
case .ended:
let velocity = gesture.velocity(in: gesture.view)
var smallAngle = false
if (self.previousAngle < .pi / 2 && self.previousAngle > -.pi / 2) && abs(velocity.x) < 200 {
smallAngle = true
}
self.playAppearanceAnimation(velocity: velocity.x, smallAngle: smallAngle, explode: !smallAngle && abs(velocity.x) > 600)
node.rotation = SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: 0.0)
default:
break
}
}
private func setup() {
guard let scene = SCNScene(named: "star.scn") else {
return
}
self.sceneView.scene = scene
self.sceneView.delegate = self
let _ = self.sceneView.snapshot()
}
private var didSetReady = false
func renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval) {
if !self.didSetReady {
self.didSetReady = true
self._ready.set(.single(true))
self.onReady()
}
}
private func onReady() {
self.setupGradientAnimation()
self.setupShineAnimation()
self.playAppearanceAnimation(explode: true)
self.previousInteractionTimestamp = CACurrentMediaTime()
self.timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in
if let strongSelf = self {
let currentTimestamp = CACurrentMediaTime()
if currentTimestamp > strongSelf.previousInteractionTimestamp + 5.0 {
strongSelf.playAppearanceAnimation()
}
}
}, queue: Queue.mainQueue())
self.timer?.start()
}
private func setupGradientAnimation() {
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
return
}
guard let initial = node.geometry?.materials.first?.diffuse.contentsTransform else {
return
}
let animation = CABasicAnimation(keyPath: "contentsTransform")
animation.duration = 4.5
animation.fromValue = NSValue(scnMatrix4: initial)
animation.toValue = NSValue(scnMatrix4: SCNMatrix4Translate(initial, -0.35, 0.35, 0))
animation.timingFunction = CAMediaTimingFunction(name: .linear)
animation.autoreverses = true
animation.repeatCount = .infinity
node.geometry?.materials.first?.diffuse.addAnimation(animation, forKey: "gradient")
}
private func setupShineAnimation() {
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
return
}
guard let initial = node.geometry?.materials.first?.emission.contentsTransform else {
return
}
let animation = CABasicAnimation(keyPath: "contentsTransform")
animation.fillMode = .forwards
animation.fromValue = NSValue(scnMatrix4: initial)
animation.toValue = NSValue(scnMatrix4: SCNMatrix4Translate(initial, -1.6, 0.0, 0.0))
animation.timingFunction = CAMediaTimingFunction(name: .easeOut)
animation.beginTime = 0.6
animation.duration = 0.9
let group = CAAnimationGroup()
group.animations = [animation]
group.beginTime = 1.0
group.duration = 3.0
group.repeatCount = .infinity
node.geometry?.materials.first?.emission.addAnimation(group, forKey: "shimmer")
}
private func playAppearanceAnimation(velocity: CGFloat? = nil, smallAngle: Bool = false, mirror: Bool = false, explode: Bool = false) {
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
return
}
self.previousInteractionTimestamp = CACurrentMediaTime()
if explode, let node = scene.rootNode.childNode(withName: "swirl", recursively: false), let particles = scene.rootNode.childNode(withName: "particles", recursively: false) {
let particleSystem = particles.particleSystems?.first
particleSystem?.particleColorVariation = SCNVector4(0.15, 0.2, 0.35, 0.3)
particleSystem?.particleVelocity = 2.2
particleSystem?.birthRate = 4.5
particleSystem?.particleLifeSpan = 2.0
node.physicsField?.isActive = true
Queue.mainQueue().after(1.0) {
node.physicsField?.isActive = false
particles.particleSystems?.first?.birthRate = 1.2
particleSystem?.particleVelocity = 1.0
particleSystem?.particleLifeSpan = 4.0
}
}
let from = node.presentation.rotation
node.removeAnimation(forKey: "tapRotate")
var toValue: Float = smallAngle ? 0.0 : .pi * 2.0
if let velocity = velocity, !smallAngle && abs(velocity) > 200 && velocity < 0.0 {
toValue *= -1
}
if mirror {
toValue *= -1
}
let to = SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: toValue)
let distance = rad2deg(to.w - from.w)
guard !distance.isZero else {
return
}
let springAnimation = CASpringAnimation(keyPath: "rotation")
springAnimation.fromValue = NSValue(scnVector4: from)
springAnimation.toValue = NSValue(scnVector4: to)
springAnimation.mass = 1.0
springAnimation.stiffness = 21.0
springAnimation.damping = 5.8
springAnimation.duration = springAnimation.settlingDuration * 0.75
springAnimation.initialVelocity = velocity.flatMap { abs($0 / CGFloat(distance)) } ?? 1.7
node.addAnimation(springAnimation, forKey: "rotate")
}
func update(component: StarComponent, availableSize: CGSize, transition: Transition) -> CGSize {
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)
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
import TextFormat
import InstantPageCache
private final class SectionGroupComponent: Component {
public final class Item: Equatable {
@ -813,6 +503,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
let infoBackground = Child(RoundedRectangle.self)
let infoTitle = Child(MultilineTextComponent.self)
let infoText = Child(MultilineTextComponent.self)
let termsText = Child(MultilineTextComponent.self)
return { context in
let sideInset: CGFloat = 16.0
@ -954,6 +645,28 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
}
),
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",
@ -1020,6 +733,28 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
}
),
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",
@ -1134,7 +869,65 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
.position(CGPoint(x: sideInset + environment.safeInsets.left + textSideInset + infoText.size.width / 2.0, y: size.height + textPadding + infoText.size.height / 2.0))
)
size.height += infoBackground.size.height
size.height += 3.0
size.height += 6.0
let termsFont = Font.regular(13.0)
let termsTextColor = environment.theme.list.freeTextColor
let termsMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), bold: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), link: MarkdownAttributeSet(font: termsFont, textColor: environment.theme.list.itemAccentColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})
let termsText = termsText.update(
component: MultilineTextComponent(
text: .markdown(
text: strings.Premium_Terms,
attributes: termsMarkdownAttributes
),
horizontalAlignment: .natural,
maximumNumberOfLines: 0,
lineSpacing: 0.0,
highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.3),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { attributes, _ in
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String,
let controller = environment.controller() as? PremiumIntroScreen, let navigationController = controller.navigationController as? NavigationController {
let context = controller.context
let signal: Signal<ResolvedUrl, NoError>?
switch url {
case "terms":
signal = cachedTermsPage(context: context)
case "privacy":
signal = cachedPrivacyPage(context: context)
default:
signal = nil
}
if let signal = signal {
let _ = (signal
|> deliverOnMainQueue).start(next: { resolvedUrl in
context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in
}, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { [weak controller] c, arguments in
controller?.push(c)
}, dismissInput: {}, contentContext: nil)
})
}
}
}
),
environment: {},
availableSize: CGSize(width: availableWidth - sideInsets - textSideInset * 2.0, height: .greatestFiniteMagnitude),
transition: context.transition
)
context.add(termsText
.position(CGPoint(x: sideInset + environment.safeInsets.left + textSideInset + termsText.size.width / 2.0, y: size.height + termsText.size.height / 2.0))
)
size.height += termsText.size.height
size.height += 10.0
size.height += scrollEnvironment.insets.bottom
return size
@ -1286,7 +1079,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
static var body: Body {
let background = Child(Rectangle.self)
let scrollContent = Child(ScrollComponent<EnvironmentType>.self)
let star = Child(StarComponent.self)
let star = Child(PremiumStarComponent.self)
let topPanel = Child(BlurredRectangle.self)
let topSeparator = Child(Rectangle.self)
let title = Child(Text.self)
@ -1306,7 +1099,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
}
let star = star.update(
component: StarComponent(isVisible: starIsVisible),
component: PremiumStarComponent(isVisible: starIsVisible),
availableSize: CGSize(width: min(390.0, context.availableSize.width), height: 220.0),
transition: context.transition
)
@ -1462,7 +1255,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
.opacity(bottomPanelAlpha)
)
context.add(bottomSeparator
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height - bottomSeparator.size.height))
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height))
.opacity(bottomPanelAlpha)
)
context.add(button
@ -1475,7 +1268,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
}
public final class PremiumIntroScreen: ViewControllerComponentContainer {
private let context: AccountContext
fileprivate let context: AccountContext
private var didSetReady = false
private let _ready = Promise<Bool>()
@ -1538,7 +1331,7 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
super.containerLayoutUpdated(layout, transition: transition)
if !self.didSetReady {
if let view = self.node.hostView.findTaggedView(tag: StarComponent.View.Tag()) as? StarComponent.View {
if let view = self.node.hostView.findTaggedView(tag: PremiumStarComponent.View.Tag()) as? PremiumStarComponent.View {
self.didSetReady = true
self._ready.set(view.ready)
}

View File

@ -22,19 +22,22 @@ private class PremiumLimitAnimationComponent: Component {
private let activeColors: [UIColor]
private let textColor: UIColor
private let badgeText: String?
private let badgePosition: CGFloat
init(
iconName: String,
inactiveColor: UIColor,
activeColors: [UIColor],
textColor: UIColor,
badgeText: String?
badgeText: String?,
badgePosition: CGFloat
) {
self.iconName = iconName
self.inactiveColor = inactiveColor
self.activeColors = activeColors
self.textColor = textColor
self.badgeText = badgeText
self.badgePosition = badgePosition
}
static func ==(lhs: PremiumLimitAnimationComponent, rhs: PremiumLimitAnimationComponent) -> Bool {
@ -53,6 +56,9 @@ private class PremiumLimitAnimationComponent: Component {
if lhs.badgeText != rhs.badgeText {
return false
}
if lhs.badgePosition != rhs.badgePosition {
return false
}
return true
}
@ -144,9 +150,8 @@ private class PremiumLimitAnimationComponent: Component {
let now = self.badgeView.layer.convertTime(CACurrentMediaTime(), from: nil)
let positionAnimation = CABasicAnimation(keyPath: "position.x")
positionAnimation.fromValue = NSValue(cgPoint: CGPoint(x: -availableSize.width / 2.0, y: 0.0))
positionAnimation.toValue = NSValue(cgPoint: CGPoint())
positionAnimation.isAdditive = true
positionAnimation.fromValue = NSValue(cgPoint: CGPoint(x: 0.0, y: 0.0))
positionAnimation.toValue = NSValue(cgPoint: self.badgeView.center)
positionAnimation.duration = 0.5
positionAnimation.fillMode = .forwards
positionAnimation.beginTime = now
@ -225,7 +230,7 @@ private class PremiumLimitAnimationComponent: Component {
self.badgeMaskArrowView.frame = CGRect(origin: CGPoint(x: (badgeSize.width - 44.0) / 2.0, y: badgeSize.height - 12.0), size: CGSize(width: 44.0, height: 12.0))
self.badgeView.bounds = CGRect(origin: .zero, size: badgeSize)
self.badgeView.center = CGPoint(x: availableSize.width / 2.0, y: 82.0)
self.badgeView.center = CGPoint(x: availableSize.width * component.badgePosition, y: 82.0)
self.badgeForeground.bounds = CGRect(origin: CGPoint(), size: CGSize(width: badgeSize.width * 3.0, height: badgeSize.height))
if self.badgeForeground.animation(forKey: "movement") == nil {
self.badgeForeground.position = CGPoint(x: badgeSize.width * 3.0 / 2.0 - self.badgeForeground.frame.width * 0.35, y: badgeSize.height / 2.0)
@ -318,33 +323,39 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
public let inactiveColor: UIColor
public let activeColors: [UIColor]
public let inactiveTitle: String
public let inactiveValue: String
public let inactiveTitleColor: UIColor
public let activeTitle: String
public let activeValue: String
public let activeTitleColor: UIColor
public let badgeIconName: String
public let badgeText: String?
public let badgePosition: CGFloat
public init(
inactiveColor: UIColor,
activeColors: [UIColor],
inactiveTitle: String,
inactiveValue: String,
inactiveTitleColor: UIColor,
activeTitle: String,
activeValue: String,
activeTitleColor: UIColor,
badgeIconName: String,
badgeText: String?
badgeText: String?,
badgePosition: CGFloat
) {
self.inactiveColor = inactiveColor
self.activeColors = activeColors
self.inactiveTitle = inactiveTitle
self.inactiveValue = inactiveValue
self.inactiveTitleColor = inactiveTitleColor
self.activeTitle = activeTitle
self.activeValue = activeValue
self.activeTitleColor = activeTitleColor
self.badgeIconName = badgeIconName
self.badgeText = badgeText
self.badgePosition = badgePosition
}
public static func ==(lhs: PremiumLimitDisplayComponent, rhs: PremiumLimitDisplayComponent) -> Bool {
@ -357,6 +368,9 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
if lhs.inactiveTitle != rhs.inactiveTitle {
return false
}
if lhs.inactiveValue != rhs.inactiveValue {
return false
}
if lhs.inactiveTitleColor != rhs.inactiveTitleColor {
return false
}
@ -375,11 +389,15 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
if lhs.badgeText != rhs.badgeText {
return false
}
if lhs.badgePosition != rhs.badgePosition {
return false
}
return true
}
public static var body: Body {
let inactiveTitle = Child(MultilineTextComponent.self)
let inactiveValue = Child(MultilineTextComponent.self)
let activeTitle = Child(MultilineTextComponent.self)
let activeValue = Child(MultilineTextComponent.self)
let animation = Child(PremiumLimitAnimationComponent.self)
@ -404,6 +422,20 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
transition: context.transition
)
let inactiveValue = inactiveValue.update(
component: MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.inactiveValue,
font: Font.semibold(15.0),
textColor: component.inactiveTitleColor
)
)
),
availableSize: context.availableSize,
transition: context.transition
)
let activeTitle = activeTitle.update(
component: MultilineTextComponent(
text: .plain(
@ -438,7 +470,8 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
inactiveColor: component.inactiveColor,
activeColors: component.activeColors,
textColor: component.activeTitleColor,
badgeText: component.badgeText
badgeText: component.badgeText,
badgePosition: component.badgePosition
),
availableSize: CGSize(width: context.availableSize.width, height: height),
transition: context.transition
@ -452,8 +485,12 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
.position(CGPoint(x: inactiveTitle.size.width / 2.0 + 12.0, y: height - lineHeight / 2.0))
)
context.add(inactiveValue
.position(CGPoint(x: context.availableSize.width / 2.0 - activeValue.size.width / 2.0 - 12.0, y: height - lineHeight / 2.0))
)
context.add(activeTitle
.position(CGPoint(x: context.availableSize.width / 2.0 + 1.0 + activeTitle.size.width / 2.0 + 12.0, y: height - lineHeight / 2.0))
.position(CGPoint(x: context.availableSize.width / 2.0 + activeTitle.size.width / 2.0 + 12.0, y: height - lineHeight / 2.0))
)
context.add(activeValue
@ -470,12 +507,14 @@ private final class LimitSheetContent: CombinedComponent {
let context: AccountContext
let subject: PremiumLimitScreen.Subject
let count: Int32
let action: () -> Void
let dismiss: () -> Void
init(context: AccountContext, subject: PremiumLimitScreen.Subject, action: @escaping () -> Void, dismiss: @escaping () -> Void) {
init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, action: @escaping () -> Void, dismiss: @escaping () -> Void) {
self.context = context
self.subject = subject
self.count = count
self.action = action
self.dismiss = dismiss
}
@ -487,6 +526,9 @@ private final class LimitSheetContent: CombinedComponent {
if lhs.subject != rhs.subject {
return false
}
if lhs.count != rhs.count {
return false
}
return true
}
@ -549,36 +591,46 @@ private final class LimitSheetContent: CombinedComponent {
let iconName: String
let badgeText: String
let string: String
let defaultValue: String
let premiumValue: String
let badgePosition: CGFloat
switch subject {
case .folders:
let limit = state.limits.maxFoldersCount
let premiumLimit = state.premiumLimits.maxFoldersCount
iconName = "Premium/Folder"
badgeText = "\(limit)"
badgeText = "\(component.count)"
string = strings.Premium_MaxFoldersCountText("\(limit)", "\(premiumLimit)").string
defaultValue = component.count > limit ? "\(limit)" : ""
premiumValue = "\(premiumLimit)"
badgePosition = CGFloat(component.count) / CGFloat(premiumLimit)
case .chatsInFolder:
let limit = state.limits.maxFolderChatsCount
let premiumLimit = state.premiumLimits.maxFolderChatsCount
iconName = "Premium/Chat"
badgeText = "\(limit)"
badgeText = "\(component.count)"
string = strings.Premium_MaxChatsInFolderCountText("\(limit)", "\(premiumLimit)").string
defaultValue = component.count > limit ? "\(limit)" : ""
premiumValue = "\(premiumLimit)"
badgePosition = CGFloat(component.count) / CGFloat(premiumLimit)
case .pins:
let limit = state.limits.maxPinnedChatCount
let premiumLimit = state.premiumLimits.maxPinnedChatCount
iconName = "Premium/Pin"
badgeText = "\(limit)"
badgeText = "\(component.count)"
string = strings.Premium_MaxPinsText("\(limit)", "\(premiumLimit)").string
defaultValue = component.count > limit ? "\(limit)" : ""
premiumValue = "\(premiumLimit)"
badgePosition = CGFloat(component.count) / CGFloat(premiumLimit)
case .files:
let limit: Int64 = 2048 * 1024 * 1024 * Int64(state.limits.maxUploadFileParts)
let premiumLimit: Int64 = 4096 * 1024 * 1024 * Int64(state.limits.maxUploadFileParts)
iconName = "Premium/File"
badgeText = dataSizeString(limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))
string = strings.Premium_MaxFileSizeText(dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))).string
defaultValue = ""
premiumValue = dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))
badgePosition = CGFloat(component.count) / CGFloat(premiumLimit)
}
let title = title.update(
@ -625,12 +677,14 @@ private final class LimitSheetContent: CombinedComponent {
UIColor(rgb: 0xe46ace)
],
inactiveTitle: strings.Premium_Free,
inactiveValue: defaultValue,
inactiveTitleColor: .black,
activeTitle: strings.Premium_Premium,
activeValue: premiumValue,
activeTitleColor: .white,
badgeIconName: iconName,
badgeText: badgeText
badgeText: badgeText,
badgePosition: badgePosition
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
transition: .immediate
@ -696,11 +750,13 @@ private final class LimitSheetComponent: CombinedComponent {
let context: AccountContext
let subject: PremiumLimitScreen.Subject
let count: Int32
let action: () -> Void
init(context: AccountContext, subject: PremiumLimitScreen.Subject, action: @escaping () -> Void) {
init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, action: @escaping () -> Void) {
self.context = context
self.subject = subject
self.count = count
self.action = action
}
@ -729,6 +785,7 @@ private final class LimitSheetComponent: CombinedComponent {
content: AnyComponent<EnvironmentType>(LimitSheetContent(
context: context.component.context,
subject: context.component.subject,
count: context.component.count,
action: context.component.action,
dismiss: {
animateOut.invoke(Action { _ in
@ -775,8 +832,8 @@ public class PremiumLimitScreen: ViewControllerComponentContainer {
case files
}
public init(context: AccountContext, subject: PremiumLimitScreen.Subject, action: @escaping () -> Void) {
super.init(context: context, component: LimitSheetComponent(context: context, subject: subject, action: action), navigationBarAppearance: .none)
public init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, action: @escaping () -> Void) {
super.init(context: context, component: LimitSheetComponent(context: context, subject: subject, count: count, action: action), navigationBarAppearance: .none)
self.navigationPresentation = .flatModal
}

View File

@ -11,7 +11,7 @@ import PresentationDataUtils
import SolidRoundedButtonNode
import AppBundle
public final class PremiumStickersScreen: ViewController {
public final class PremiumReactionsScreen: ViewController {
private let context: AccountContext
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
@ -21,7 +21,7 @@ public final class PremiumStickersScreen: ViewController {
public var proceed: (() -> Void)?
private class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate {
private weak var controller: PremiumStickersScreen?
private weak var controller: PremiumReactionsScreen?
private var presentationData: PresentationData
private let blurView: UIVisualEffectView
@ -38,7 +38,7 @@ public final class PremiumStickersScreen: ViewController {
private var validLayout: ContainerViewLayout?
init(controller: PremiumStickersScreen) {
init(controller: PremiumReactionsScreen) {
self.controller = controller
self.presentationData = controller.presentationData
@ -70,7 +70,14 @@ public final class PremiumStickersScreen: ViewController {
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(theme: self.presentationData.theme), height: 50.0, cornerRadius: 11.0, gloss: true)
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)
@ -78,7 +85,7 @@ public final class PremiumStickersScreen: ViewController {
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)
@ -99,7 +106,10 @@ public final class PremiumStickersScreen: ViewController {
self.overlayTextNode.attributedText = NSAttributedString(string: self.presentationData.strings.Premium_Reactions_Description, font: Font.regular(17.0), textColor: textColor)
self.proceedButton.pressed = { [weak self] in
self?.animateOut()
if let strongSelf = self, let controller = strongSelf.controller, let navigationController = controller.navigationController {
strongSelf.animateOut()
navigationController.pushViewController(PremiumIntroScreen(context: controller.context), animated: true)
}
}
self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside)
@ -209,6 +219,8 @@ public final class PremiumStickersScreen: ViewController {
super.init(navigationBarPresentationData: nil)
self.navigationPresentation = .flatModal
self.statusBar.statusBarStyle = .Ignore
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)

View File

@ -0,0 +1,373 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import SceneKit
import GZip
import AppBundle
private let sceneVersion: Int = 1
private func deg2rad(_ number: Float) -> Float {
return number * .pi / 180
}
private func rad2deg(_ number: Float) -> Float {
return number * 180.0 / .pi
}
private func generateParticlesTexture() -> UIImage {
return UIImage()
}
private func generateFlecksTexture() -> UIImage {
return UIImage()
}
private func generateShineTexture() -> UIImage {
return UIImage()
}
private func generateDiffuseTexture() -> UIImage {
return generateImage(CGSize(width: 256, height: 256), rotatedContext: { size, context in
let colorsArray: [CGColor] = [
UIColor(rgb: 0x0079ff).cgColor,
UIColor(rgb: 0x6a93ff).cgColor,
UIColor(rgb: 0x9172fe).cgColor,
UIColor(rgb: 0xe46acd).cgColor,
]
var locations: [CGFloat] = [0.0, 0.25, 0.5, 0.75, 1.0]
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: size.height), options: CGGradientDrawingOptions())
})!
}
class PremiumStarComponent: Component {
let isVisible: Bool
init(isVisible: Bool) {
self.isVisible = isVisible
}
static func ==(lhs: PremiumStarComponent, rhs: PremiumStarComponent) -> Bool {
return lhs.isVisible == rhs.isVisible
}
final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView {
final class Tag {
}
func matches(tag: Any) -> Bool {
if let _ = tag as? Tag {
return true
}
return false
}
private var _ready = Promise<Bool>()
var ready: Signal<Bool, NoError> {
return self._ready.get()
}
private let sceneView: SCNView
private var previousInteractionTimestamp: Double = 0.0
private var timer: SwiftSignalKit.Timer?
override init(frame: CGRect) {
self.sceneView = SCNView(frame: frame)
self.sceneView.backgroundColor = .clear
self.sceneView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
self.sceneView.isUserInteractionEnabled = false
super.init(frame: frame)
self.addSubview(self.sceneView)
self.setup()
let panGestureRecoginzer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
self.addGestureRecognizer(panGestureRecoginzer)
let tapGestureRecoginzer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
self.addGestureRecognizer(tapGestureRecoginzer)
self.disablesInteractiveModalDismiss = true
self.disablesInteractiveTransitionGestureRecognizer = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.timer?.invalidate()
}
@objc private func handleTap(_ gesture: UITapGestureRecognizer) {
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
return
}
self.previousInteractionTimestamp = CACurrentMediaTime()
var left: Bool?
var top: Bool?
if let view = gesture.view {
let point = gesture.location(in: view)
let horizontalDistanceFromCenter = abs(point.x - view.frame.size.width / 2.0)
if horizontalDistanceFromCenter > 60.0 {
return
}
let verticalDistanceFromCenter = abs(point.y - view.frame.size.height / 2.0)
if horizontalDistanceFromCenter > 20.0 {
left = point.x < view.frame.width / 2.0
}
if verticalDistanceFromCenter > 20.0 {
top = point.y < view.frame.height / 2.0
}
}
if node.animationKeys.contains("tapRotate"), let left = left {
self.playAppearanceAnimation(velocity: nil, mirror: left, explode: true)
return
}
let initial = node.eulerAngles
var yaw: CGFloat = 0.0
var pitch: CGFloat = 0.0
if let left = left {
yaw = left ? -0.6 : 0.6
}
if let top = top {
pitch = top ? -0.3 : 0.3
}
let target = SCNVector3(pitch, yaw, 0.0)
let animation = CABasicAnimation(keyPath: "eulerAngles")
animation.fromValue = NSValue(scnVector3: initial)
animation.toValue = NSValue(scnVector3: target)
animation.duration = 0.25
animation.timingFunction = CAMediaTimingFunction(name: .easeOut)
animation.fillMode = .forwards
node.addAnimation(animation, forKey: "tapRotate")
node.eulerAngles = target
Queue.mainQueue().after(0.25) {
node.eulerAngles = initial
let springAnimation = CASpringAnimation(keyPath: "eulerAngles")
springAnimation.fromValue = NSValue(scnVector3: target)
springAnimation.toValue = NSValue(scnVector3: SCNVector3(x: 0.0, y: 0.0, z: 0.0))
springAnimation.mass = 1.0
springAnimation.stiffness = 21.0
springAnimation.damping = 5.8
springAnimation.duration = springAnimation.settlingDuration * 0.8
node.addAnimation(springAnimation, forKey: "tapRotate")
}
}
private var previousYaw: Float = 0.0
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
return
}
self.previousInteractionTimestamp = CACurrentMediaTime()
if #available(iOS 11.0, *) {
node.removeAnimation(forKey: "rotate", blendOutDuration: 0.1)
node.removeAnimation(forKey: "tapRotate", blendOutDuration: 0.1)
} else {
node.removeAllAnimations()
}
switch gesture.state {
case .began:
self.previousYaw = 0.0
case .changed:
let translation = gesture.translation(in: gesture.view)
let yawPan = deg2rad(Float(translation.x))
let pitchPan = deg2rad(Float(translation.y))
self.previousYaw = yawPan
node.eulerAngles = SCNVector3(pitchPan, yawPan, 0.0)
case .ended:
let velocity = gesture.velocity(in: gesture.view)
var smallAngle = false
if (self.previousYaw < .pi / 2 && self.previousYaw > -.pi / 2) && abs(velocity.x) < 200 {
smallAngle = true
}
self.playAppearanceAnimation(velocity: velocity.x, smallAngle: smallAngle, explode: !smallAngle && abs(velocity.x) > 600)
node.eulerAngles = SCNVector3(0.0, 0.0, 0.0)
default:
break
}
}
private func setup() {
guard let url = getAppBundle().url(forResource: "star", withExtension: ""),
let compressedData = try? Data(contentsOf: url),
let decompressedData = TGGUnzipData(compressedData, 8 * 1024 * 1024) else {
return
}
let fileName = "star_\(sceneVersion).scn"
let tmpURL = URL(fileURLWithPath: NSTemporaryDirectory() + fileName)
if !FileManager.default.fileExists(atPath: tmpURL.path) {
try? decompressedData.write(to: tmpURL)
}
guard let scene = try? SCNScene(url: tmpURL, options: nil) else {
return
}
self.sceneView.scene = scene
self.sceneView.delegate = self
let _ = self.sceneView.snapshot()
}
private var didSetReady = false
func renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval) {
if !self.didSetReady {
self.didSetReady = true
self._ready.set(.single(true))
self.onReady()
}
}
private func onReady() {
self.setupGradientAnimation()
self.setupShineAnimation()
self.playAppearanceAnimation(explode: true)
self.previousInteractionTimestamp = CACurrentMediaTime()
self.timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in
if let strongSelf = self {
let currentTimestamp = CACurrentMediaTime()
if currentTimestamp > strongSelf.previousInteractionTimestamp + 5.0 {
strongSelf.playAppearanceAnimation()
}
}
}, queue: Queue.mainQueue())
self.timer?.start()
}
private func setupGradientAnimation() {
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
return
}
guard let initial = node.geometry?.materials.first?.diffuse.contentsTransform else {
return
}
let animation = CABasicAnimation(keyPath: "contentsTransform")
animation.duration = 4.5
animation.fromValue = NSValue(scnMatrix4: initial)
animation.toValue = NSValue(scnMatrix4: SCNMatrix4Translate(initial, -0.35, 0.35, 0))
animation.timingFunction = CAMediaTimingFunction(name: .linear)
animation.autoreverses = true
animation.repeatCount = .infinity
node.geometry?.materials.first?.diffuse.addAnimation(animation, forKey: "gradient")
}
private func setupShineAnimation() {
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
return
}
guard let initial = node.geometry?.materials.first?.emission.contentsTransform else {
return
}
let animation = CABasicAnimation(keyPath: "contentsTransform")
animation.fillMode = .forwards
animation.fromValue = NSValue(scnMatrix4: initial)
animation.toValue = NSValue(scnMatrix4: SCNMatrix4Translate(initial, -1.6, 0.0, 0.0))
animation.timingFunction = CAMediaTimingFunction(name: .easeOut)
animation.beginTime = 0.6
animation.duration = 0.9
let group = CAAnimationGroup()
group.animations = [animation]
group.beginTime = 1.0
group.duration = 3.0
group.repeatCount = .infinity
node.geometry?.materials.first?.emission.addAnimation(group, forKey: "shimmer")
}
private func playAppearanceAnimation(velocity: CGFloat? = nil, smallAngle: Bool = false, mirror: Bool = false, explode: Bool = false) {
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
return
}
self.previousInteractionTimestamp = CACurrentMediaTime()
if explode, let node = scene.rootNode.childNode(withName: "swirl", recursively: false), let particles = scene.rootNode.childNode(withName: "particles", recursively: false) {
let particleSystem = particles.particleSystems?.first
particleSystem?.particleColorVariation = SCNVector4(0.15, 0.2, 0.35, 0.3)
particleSystem?.particleVelocity = 2.2
particleSystem?.birthRate = 4.5
particleSystem?.particleLifeSpan = 2.0
node.physicsField?.isActive = true
Queue.mainQueue().after(1.0) {
node.physicsField?.isActive = false
particles.particleSystems?.first?.birthRate = 1.2
particleSystem?.particleVelocity = 1.0
particleSystem?.particleLifeSpan = 4.0
}
}
let from = node.presentation.eulerAngles
node.removeAnimation(forKey: "tapRotate")
var toValue: Float = smallAngle ? 0.0 : .pi * 2.0
if let velocity = velocity, !smallAngle && abs(velocity) > 200 && velocity < 0.0 {
toValue *= -1
}
if mirror {
toValue *= -1
}
let to = SCNVector3(x: 0.0, y: toValue, z: 0.0)
let distance = rad2deg(to.y - from.y)
guard !distance.isZero else {
return
}
let springAnimation = CASpringAnimation(keyPath: "eulerAngles")
springAnimation.fromValue = NSValue(scnVector3: from)
springAnimation.toValue = NSValue(scnVector3: to)
springAnimation.mass = 1.0
springAnimation.stiffness = 21.0
springAnimation.damping = 5.8
springAnimation.duration = springAnimation.settlingDuration * 0.75
springAnimation.initialVelocity = velocity.flatMap { abs($0 / CGFloat(distance)) } ?? 1.7
node.addAnimation(springAnimation, forKey: "rotate")
}
func update(component: PremiumStarComponent, availableSize: CGSize, transition: Transition) -> CGSize {
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)
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}

View File

@ -7,69 +7,6 @@ import InstantPageUI
import InstantPageCache
import UrlHandling
private func extractAnchor(string: String) -> (String, String?) {
var anchorValue: String?
if let anchorRange = string.range(of: "#") {
let anchor = string[anchorRange.upperBound...]
if !anchor.isEmpty {
anchorValue = String(anchor)
}
}
var trimmedUrl = string
if let anchor = anchorValue, let anchorRange = string.range(of: "#\(anchor)") {
let url = string[..<anchorRange.lowerBound]
if !url.isEmpty {
trimmedUrl = String(url)
}
}
return (trimmedUrl, anchorValue)
}
private let refreshTimeout: Int32 = 60 * 60 * 12
public func cachedFaqInstantPage(context: AccountContext) -> Signal<ResolvedUrl, NoError> {
var faqUrl = context.sharedContext.currentPresentationData.with { $0 }.strings.Settings_FAQ_URL
if faqUrl == "Settings.FAQ_URL" || faqUrl.isEmpty {
faqUrl = "https://telegram.org/faq#general-questions"
}
let (cachedUrl, anchor) = extractAnchor(string: faqUrl)
return cachedInstantPage(postbox: context.account.postbox, url: cachedUrl)
|> mapToSignal { cachedInstantPage -> Signal<ResolvedUrl, NoError> in
let updated = resolveInstantViewUrl(account: context.account, url: faqUrl)
|> afterNext { result in
if case let .instantView(webPage, _) = result, case let .Loaded(content) = webPage.content, let instantPage = content.instantPage {
if instantPage.isComplete {
let _ = updateCachedInstantPage(postbox: context.account.postbox, url: cachedUrl, webPage: webPage).start()
} else {
let _ = (actualizedWebpage(postbox: context.account.postbox, network: context.account.network, webpage: webPage)
|> mapToSignal { webPage -> Signal<Void, NoError> in
if case let .Loaded(content) = webPage.content, let instantPage = content.instantPage, instantPage.isComplete {
return updateCachedInstantPage(postbox: context.account.postbox, url: cachedUrl, webPage: webPage)
} else {
return .complete()
}
}).start()
}
}
}
let now = Int32(CFAbsoluteTimeGetCurrent())
if let cachedInstantPage = cachedInstantPage, case let .Loaded(content) = cachedInstantPage.webPage.content, let instantPage = content.instantPage, instantPage.isComplete {
let current: Signal<ResolvedUrl, NoError> = .single(.instantView(cachedInstantPage.webPage, anchor))
if now > cachedInstantPage.timestamp + refreshTimeout {
return current
|> then(updated)
} else {
return current
}
} else {
return updated
}
}
}
func faqSearchableItems(context: AccountContext, resolvedUrl: Signal<ResolvedUrl?, NoError>, suggestAccountDeletion: Bool) -> Signal<[SettingsSearchableItem], NoError> {
let strings = context.sharedContext.currentPresentationData.with { $0 }.strings
return resolvedUrl

View File

@ -236,7 +236,9 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol
}
})))
}
return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, item: .pack(item), isLocked: item.file.isPremiumSticker && !hasPremium, menu: menuItems))
return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, item: .pack(item), isLocked: item.file.isPremiumSticker && !hasPremium, menu: menuItems, openPremiumIntro: {
}))
} else {
return nil
}

View File

@ -369,7 +369,9 @@ private final class StickerPackContainer: ASDisplayNode {
}
})))
}
return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, item: .pack(item), isLocked: item.file.isPremiumSticker && !hasPremium, menu: menuItems))
return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, item: .pack(item), isLocked: item.file.isPremiumSticker && !hasPremium, menu: menuItems, openPremiumIntro: {
}))
} else {
return nil
}
@ -1319,6 +1321,7 @@ public enum StickerPackScreenPerformedAction {
}
public func StickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, mode: StickerPackPreviewControllerMode = .default, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], parentNavigationController: NavigationController? = nil, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? = nil, actionPerformed: ((StickerPackCollectionInfo, [StickerPackItem], StickerPackScreenPerformedAction) -> Void)? = nil, dismissed: (() -> Void)? = nil) -> ViewController {
let stickerPacks = [mainStickerPack]
let controller = StickerPackScreenImpl(context: context, stickerPacks: stickerPacks, selectedStickerPackIndex: stickerPacks.firstIndex(of: mainStickerPack) ?? 0, parentNavigationController: parentNavigationController, sendSticker: sendSticker, actionPerformed: actionPerformed)
controller.dismissed = dismissed
return controller

View File

@ -35,8 +35,9 @@ public final class StickerPreviewPeekContent: PeekControllerContent {
public let item: StickerPreviewPeekItem
let isLocked: Bool
let menu: [ContextMenuItem]
let openPremiumIntro: () -> Void
public init(account: Account, theme: PresentationTheme, strings: PresentationStrings, item: StickerPreviewPeekItem, isLocked: Bool = false, menu: [ContextMenuItem]) {
public init(account: Account, theme: PresentationTheme, strings: PresentationStrings, item: StickerPreviewPeekItem, isLocked: Bool = false, menu: [ContextMenuItem], openPremiumIntro: @escaping () -> Void) {
self.account = account
self.theme = theme
self.strings = strings
@ -47,6 +48,7 @@ public final class StickerPreviewPeekContent: PeekControllerContent {
} else {
self.menu = menu
}
self.openPremiumIntro = openPremiumIntro
}
public func presentation() -> PeekControllerContentPresentation {
@ -71,7 +73,7 @@ public final class StickerPreviewPeekContent: PeekControllerContent {
public func fullScreenAccessoryNode(blurView: UIVisualEffectView) -> (PeekControllerAccessoryNode & ASDisplayNode)? {
if self.isLocked {
return PremiumStickerPackAccessoryNode(theme: self.theme, strings: self.strings)
return PremiumStickerPackAccessoryNode(theme: self.theme, strings: self.strings, proceed: self.openPremiumIntro)
} else {
return nil
}
@ -211,12 +213,17 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC
}
}
final class PremiumStickerPackAccessoryNode: ASDisplayNode, PeekControllerAccessoryNode {
final class PremiumStickerPackAccessoryNode: SparseNode, PeekControllerAccessoryNode {
var dismiss: () -> Void = {}
let proceed: () -> Void
let textNode: ImmediateTextNode
let proceedButton: SolidRoundedButtonNode
let cancelButton: HighlightableButtonNode
init(theme: PresentationTheme, strings: PresentationStrings) {
init(theme: PresentationTheme, strings: PresentationStrings, proceed: @escaping () -> Void) {
self.proceed = proceed
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.textAlignment = .center
@ -224,7 +231,14 @@ final class PremiumStickerPackAccessoryNode: ASDisplayNode, PeekControllerAccess
self.textNode.attributedText = NSAttributedString(string: strings.Premium_Stickers_Description, font: Font.regular(17.0), textColor: theme.actionSheet.secondaryTextColor)
self.textNode.lineSpacing = 0.1
self.proceedButton = SolidRoundedButtonNode(title: strings.Premium_Stickers_Proceed, icon: UIImage(bundleImageName: "Premium/ButtonIcon"), theme: SolidRoundedButtonTheme(theme: theme), height: 50.0, cornerRadius: 11.0, gloss: true)
self.proceedButton = SolidRoundedButtonNode(title: strings.Premium_Stickers_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(strings.Common_Cancel, with: Font.regular(17.0), with: theme.list.itemAccentColor, for: .normal)
@ -235,14 +249,14 @@ final class PremiumStickerPackAccessoryNode: ASDisplayNode, PeekControllerAccess
self.addSubnode(self.proceedButton)
self.addSubnode(self.cancelButton)
self.proceedButton.pressed = {
self.proceedButton.pressed = { [weak self] in
self?.proceed()
}
self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside)
}
@objc func cancelPressed() {
self.dismiss()
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {

View File

@ -126,7 +126,7 @@ public func downloadAppUpdate(account: Account, source: String, messageId: Int32
case let .Fetching(_, progress):
if let size = media.size {
if progress == 0 {
subscriber.putNext(.started(size))
subscriber.putNext(.started(Int(size)))
} else {
subscriber.putNext(.progress(Int(progress * Float(size)), Int(size)))
}

View File

@ -493,6 +493,19 @@ public final class TelegramMediaFile: Media, Equatable, Codable {
return false
}
public var premiumEffect: TelegramMediaFile.VideoThumbnail? {
if let effect = self.videoThumbnails.first(where: { thumbnail in
if let resource = thumbnail.resource as? CloudDocumentSizeMediaResource, resource.sizeSpec == "f" {
return true
} else {
return false
}
}) {
return effect
}
return nil
}
public var isVideoSticker: Bool {
if self.mimeType == "video/webm" {
var hasSticker = false

View File

@ -231,66 +231,78 @@ public struct ChatListFilterData: Equatable, Hashable {
}
}
public struct ChatListFilter: Codable, Equatable {
public var id: Int32
public var title: String
public var emoticon: String?
public var data: ChatListFilterData
public enum ChatListFilter: Codable, Equatable {
case allChats
case filter(id: Int32, title: String, emoticon: String?, data: ChatListFilterData)
public init(
id: Int32,
title: String,
emoticon: String?,
data: ChatListFilterData
) {
self.id = id
self.title = title
self.emoticon = emoticon
self.data = data
public var id: Int32 {
switch self {
case .allChats:
return 0
case let .filter(id, _, _, _):
return id
}
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
self.id = try container.decode(Int32.self, forKey: "id")
self.title = try container.decode(String.self, forKey: "title")
self.emoticon = try container.decodeIfPresent(String.self, forKey: "emoticon")
self.data = ChatListFilterData(
categories: ChatListFilterPeerCategories(rawValue: try container.decode(Int32.self, forKey: "categories")),
excludeMuted: (try container.decode(Int32.self, forKey: "excludeMuted")) != 0,
excludeRead: (try container.decode(Int32.self, forKey: "excludeRead")) != 0,
excludeArchived: (try container.decode(Int32.self, forKey: "excludeArchived")) != 0,
includePeers: ChatListFilterIncludePeers(
peers: (try container.decode([Int64].self, forKey: "includePeers")).map(PeerId.init),
pinnedPeers: (try container.decode([Int64].self, forKey: "pinnedPeers")).map(PeerId.init)
),
excludePeers: (try container.decode([Int64].self, forKey: "excludePeers")).map(PeerId.init)
)
let type = try container.decodeIfPresent(Int32.self, forKey: "t") ?? 1
if type == 0 {
self = .allChats
} else {
let id = try container.decode(Int32.self, forKey: "id")
let title = try container.decode(String.self, forKey: "title")
let emoticon = try container.decodeIfPresent(String.self, forKey: "emoticon")
let data = ChatListFilterData(
categories: ChatListFilterPeerCategories(rawValue: try container.decode(Int32.self, forKey: "categories")),
excludeMuted: (try container.decode(Int32.self, forKey: "excludeMuted")) != 0,
excludeRead: (try container.decode(Int32.self, forKey: "excludeRead")) != 0,
excludeArchived: (try container.decode(Int32.self, forKey: "excludeArchived")) != 0,
includePeers: ChatListFilterIncludePeers(
peers: (try container.decode([Int64].self, forKey: "includePeers")).map(PeerId.init),
pinnedPeers: (try container.decode([Int64].self, forKey: "pinnedPeers")).map(PeerId.init)
),
excludePeers: (try container.decode([Int64].self, forKey: "excludePeers")).map(PeerId.init)
)
self = .filter(id: id, title: title, emoticon: emoticon, data: data)
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self)
try container.encode(self.id, forKey: "id")
try container.encode(self.title, forKey: "title")
try container.encodeIfPresent(self.emoticon, forKey: "emoticon")
try container.encode(self.data.categories.rawValue, forKey: "categories")
try container.encode((self.data.excludeMuted ? 1 : 0) as Int32, forKey: "excludeMuted")
try container.encode((self.data.excludeRead ? 1 : 0) as Int32, forKey: "excludeRead")
try container.encode((self.data.excludeArchived ? 1 : 0) as Int32, forKey: "excludeArchived")
try container.encode(self.data.includePeers.peers.map { $0.toInt64() }, forKey: "includePeers")
try container.encode(self.data.includePeers.pinnedPeers.map { $0.toInt64() }, forKey: "pinnedPeers")
try container.encode(self.data.excludePeers.map { $0.toInt64() }, forKey: "excludePeers")
switch self {
case .allChats:
let type: Int32 = 0
try container.encode(type, forKey: "t")
case let .filter(id, title, emoticon, data):
let type: Int32 = 1
try container.encode(type, forKey: "t")
try container.encode(id, forKey: "id")
try container.encode(title, forKey: "title")
try container.encodeIfPresent(emoticon, forKey: "emoticon")
try container.encode(data.categories.rawValue, forKey: "categories")
try container.encode((data.excludeMuted ? 1 : 0) as Int32, forKey: "excludeMuted")
try container.encode((data.excludeRead ? 1 : 0) as Int32, forKey: "excludeRead")
try container.encode((data.excludeArchived ? 1 : 0) as Int32, forKey: "excludeArchived")
try container.encode(data.includePeers.peers.map { $0.toInt64() }, forKey: "includePeers")
try container.encode(data.includePeers.pinnedPeers.map { $0.toInt64() }, forKey: "pinnedPeers")
try container.encode(data.excludePeers.map { $0.toInt64() }, forKey: "excludePeers")
}
}
}
extension ChatListFilter {
init(apiFilter: Api.DialogFilter) {
switch apiFilter {
case .dialogFilterDefault:
self = .allChats
case let .dialogFilter(flags, id, title, emoticon, pinnedPeers, includePeers, excludePeers):
self.init(
self = .filter(
id: id,
title: title,
emoticon: emoticon,
@ -341,31 +353,36 @@ extension ChatListFilter {
}
}
func apiFilter(transaction: Transaction) -> Api.DialogFilter {
var flags: Int32 = 0
if self.data.excludeMuted {
flags |= 1 << 11
}
if self.data.excludeRead {
flags |= 1 << 12
}
if self.data.excludeArchived {
flags |= 1 << 13
}
flags |= self.data.categories.apiFlags
if self.emoticon != nil {
flags |= 1 << 25
}
return .dialogFilter(flags: flags, id: self.id, title: self.title, emoticon: self.emoticon, pinnedPeers: self.data.includePeers.pinnedPeers.compactMap { peerId -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}, includePeers: self.data.includePeers.peers.compactMap { peerId -> Api.InputPeer? in
if self.data.includePeers.pinnedPeers.contains(peerId) {
func apiFilter(transaction: Transaction) -> Api.DialogFilter? {
switch self {
case .allChats:
return nil
}
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}, excludePeers: self.data.excludePeers.compactMap { peerId -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
})
case let .filter(id, title, emoticon, data):
var flags: Int32 = 0
if data.excludeMuted {
flags |= 1 << 11
}
if data.excludeRead {
flags |= 1 << 12
}
if data.excludeArchived {
flags |= 1 << 13
}
flags |= data.categories.apiFlags
if emoticon != nil {
flags |= 1 << 25
}
return .dialogFilter(flags: flags, id: id, title: title, emoticon: emoticon, pinnedPeers: data.includePeers.pinnedPeers.compactMap { peerId -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}, includePeers: data.includePeers.peers.compactMap { peerId -> Api.InputPeer? in
if data.includePeers.pinnedPeers.contains(peerId) {
return nil
}
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}, excludePeers: data.excludePeers.compactMap { peerId -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
})
}
}
}
@ -427,6 +444,8 @@ private func requestChatListFilters(accountPeerId: PeerId, postbox: Postbox, net
let filter = ChatListFilter(apiFilter: apiFilter)
filters.append(filter)
switch apiFilter {
case .dialogFilterDefault:
break
case let .dialogFilter(_, _, _, _, pinnedPeers, includePeers, excludePeers):
for peer in pinnedPeers + includePeers + excludePeers {
var peerId: PeerId?
@ -982,11 +1001,15 @@ func _internal_updateChatListFeaturedFilters(postbox: Postbox, network: Network)
return postbox.transaction { transaction -> Void in
transaction.updatePreferencesEntry(key: PreferencesKeys.chatListFiltersFeaturedState, { entry in
var state = entry?.get(ChatListFiltersFeaturedState.self) ?? ChatListFiltersFeaturedState(filters: [], isSeen: false)
state.filters = result.map { item -> ChatListFeaturedFilter in
state.filters = result.compactMap { item -> ChatListFeaturedFilter? in
switch item {
case let .dialogFilterSuggested(filter, description):
let parsedFilter = ChatListFilter(apiFilter: filter)
return ChatListFeaturedFilter(title: parsedFilter.title, description: description, data: parsedFilter.data)
if case let .filter(_, title, _, data) = parsedFilter {
return ChatListFeaturedFilter(title: title, description: description, data: data)
} else {
return nil
}
}
}
return PreferencesEntry(state)

View File

@ -28,16 +28,23 @@ func _internal_removePeerChat(account: Account, transaction: Transaction, mediaB
transaction.setPeerChatInterfaceState(peerId, state: nil)
}
_internal_updateChatListFiltersInteractively(transaction: transaction, { filters in
var filters = filters
var updatedFilters: [ChatListFilter] = []
for i in 0 ..< filters.count {
if filters[i].data.includePeers.peers.contains(peerId) {
filters[i].data.includePeers.setPeers(filters[i].data.includePeers.peers.filter { $0 != peerId })
}
if filters[i].data.excludePeers.contains(peerId) {
filters[i].data.excludePeers = filters[i].data.excludePeers.filter { $0 != peerId }
let filter = filters[i]
if case let .filter(id, title, emoticon, data) = filter {
var updatedData = data
if updatedData.includePeers.peers.contains(peerId) {
updatedData.includePeers.setPeers(data.includePeers.peers.filter { $0 != peerId })
}
if updatedData.excludePeers.contains(peerId) {
updatedData.excludePeers = data.excludePeers.filter { $0 != peerId }
}
updatedFilters.append(.filter(id: id, title: title, emoticon: emoticon, data: updatedData))
} else {
updatedFilters.append(filter)
}
}
return filters
return updatedFilters
})
if peerId.namespace == Namespaces.Peer.SecretChat {
if let state = transaction.getPeerChatState(peerId) as? SecretChatState, state.embeddedState != .terminated {

View File

@ -585,8 +585,10 @@ public extension TelegramEngine {
sortedFilters.append(contentsOf: filters[index...])
sortedFilters.append(contentsOf: filters[0 ..< index])
for i in 0 ..< sortedFilters.count {
if let value = getForFilter(predicate: getFilterPredicate(sortedFilters[i].data), isArchived: false) {
return (peer: value.peer, unreadCount: value.unreadCount, location: i == 0 ? .same : .folder(id: sortedFilters[i].id, title: sortedFilters[i].title))
if case let .filter(id, title, _, data) = sortedFilters[i] {
if let value = getForFilter(predicate: getFilterPredicate(data), isArchived: false) {
return (peer: value.peer, unreadCount: value.unreadCount, location: i == 0 ? .same : .folder(id: id, title: title))
}
}
}
return nil

View File

@ -10,7 +10,7 @@ public enum TogglePeerChatPinnedLocation {
public enum TogglePeerChatPinnedResult {
case done
case limitExceeded(Int)
case limitExceeded(count: Int, limit: Int)
}
func _internal_toggleItemPinned(postbox: Postbox, accountPeerId: PeerId, location: TogglePeerChatPinnedLocation, itemId: PinnedItemId) -> Signal<TogglePeerChatPinnedResult, NoError> {
@ -50,8 +50,9 @@ func _internal_toggleItemPinned(postbox: Postbox, accountPeerId: PeerId, locatio
limitCount = Int(limitsConfiguration.maxArchivedPinnedChatCount)
}
if sameKind.count + additionalCount > limitCount {
return .limitExceeded(limitCount)
let count = sameKind.count + additionalCount
if count > limitCount, itemIds.firstIndex(of: itemId) == nil {
return .limitExceeded(count: sameKind.count, limit: limitCount)
} else {
if let index = itemIds.firstIndex(of: itemId) {
itemIds.remove(at: index)
@ -66,16 +67,18 @@ func _internal_toggleItemPinned(postbox: Postbox, accountPeerId: PeerId, locatio
var result: TogglePeerChatPinnedResult = .done
_internal_updateChatListFiltersInteractively(transaction: transaction, { filters in
var filters = filters
if let index = filters.firstIndex(where: { $0.id == filterId }) {
if let index = filters.firstIndex(where: { $0.id == filterId }), case let .filter(id, title, emoticon, data) = filters[index] {
switch itemId {
case let .peer(peerId):
if filters[index].data.includePeers.pinnedPeers.contains(peerId) {
filters[index].data.includePeers.removePinnedPeer(peerId)
var updatedData = data
if updatedData.includePeers.pinnedPeers.contains(peerId) {
updatedData.includePeers.removePinnedPeer(peerId)
} else {
if !filters[index].data.includePeers.addPinnedPeer(peerId) {
result = .limitExceeded(100)
if !updatedData.includePeers.addPinnedPeer(peerId) {
result = .limitExceeded(count: updatedData.includePeers.pinnedPeers.count, limit: 100)
}
}
filters[index] = .filter(id: id, title: title, emoticon: emoticon, data: updatedData)
}
}
return filters
@ -92,8 +95,8 @@ func _internal_getPinnedItemIds(transaction: Transaction, location: TogglePeerCh
case let .filter(filterId):
var itemIds: [PinnedItemId] = []
let _ = _internal_updateChatListFiltersInteractively(transaction: transaction, { filters in
if let index = filters.firstIndex(where: { $0.id == filterId }) {
itemIds = filters[index].data.includePeers.pinnedPeers.map { peerId in
if let index = filters.firstIndex(where: { $0.id == filterId }), case let .filter(_, _, _, data) = filters[index] {
itemIds = data.includePeers.pinnedPeers.map { peerId in
return .peer(peerId)
}
}
@ -117,7 +120,7 @@ func _internal_reorderPinnedItemIds(transaction: Transaction, location: TogglePe
var result: Bool = false
_internal_updateChatListFiltersInteractively(transaction: transaction, { filters in
var filters = filters
if let index = filters.firstIndex(where: { $0.id == filterId }) {
if let index = filters.firstIndex(where: { $0.id == filterId }), case let .filter(id, title, emoticon, data) = filters[index] {
let peerIds: [PeerId] = itemIds.map { itemId -> PeerId in
switch itemId {
case let .peer(peerId):
@ -125,8 +128,10 @@ func _internal_reorderPinnedItemIds(transaction: Transaction, location: TogglePe
}
}
if filters[index].data.includePeers.pinnedPeers != peerIds {
filters[index].data.includePeers.reorderPinnedPeers(peerIds)
var updatedData = data
if updatedData.includePeers.pinnedPeers != peerIds {
updatedData.includePeers.reorderPinnedPeers(peerIds)
filters[index] = .filter(id: id, title: title, emoticon: emoticon, data: updatedData)
result = true
}
}

View File

@ -225,7 +225,25 @@ public struct PresentationResourcesChatList {
public static func verifiedIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatListVerifiedIcon.rawValue, { theme in
return UIImage(bundleImageName: "Chat List/PeerVerifiedIcon")?.precomposed()
if let backgroundImage = UIImage(bundleImageName: "Chat List/PeerVerifiedIconBackground"), let foregroundImage = UIImage(bundleImageName: "Chat List/PeerVerifiedIconForeground") {
return generateImage(backgroundImage.size, contextGenerator: { size, context in
if let backgroundCgImage = backgroundImage.cgImage, let foregroundCgImage = foregroundImage.cgImage {
context.clear(CGRect(origin: CGPoint(), size: size))
context.saveGState()
context.clip(to: CGRect(origin: .zero, size: size), mask: backgroundCgImage)
context.setFillColor(theme.chatList.unreadBadgeActiveBackgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
context.restoreGState()
context.clip(to: CGRect(origin: .zero, size: size), mask: foregroundCgImage)
context.setFillColor(theme.chatList.unreadBadgeActiveTextColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
}
}, opaque: false)
} else {
return nil
}
})
}
@ -236,18 +254,9 @@ public struct PresentationResourcesChatList {
if let cgImage = image.cgImage {
context.clear(CGRect(origin: CGPoint(), size: size))
context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage)
let colorsArray: [CGColor] = [
UIColor(rgb: 0x1d95fa).cgColor,
UIColor(rgb: 0x1d95fa).cgColor,
UIColor(rgb: 0x7c8cfe).cgColor,
UIColor(rgb: 0xcb87f7).cgColor,
UIColor(rgb: 0xcb87f7).cgColor
]
var locations: [CGFloat] = [0.0, 0.35, 0.5, 0.65, 1.0]
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: size.height), options: CGGradientDrawingOptions())
context.setFillColor(theme.chatList.unreadBadgeActiveBackgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
}
}, opaque: false)
} else {

View File

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "premiumbadge_16.pdf",
"filename" : "premiumbadge_16 (1).pdf",
"idiom" : "universal"
}
],

View File

@ -0,0 +1,97 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 1.050293 1.510986 cm
0.000000 0.000000 0.000000 scn
6.588397 2.211904 m
3.455913 0.292931 l
3.130194 0.093393 2.704389 0.195683 2.504852 0.521402 c
2.407398 0.680485 2.378342 0.872190 2.424281 1.053005 c
2.909188 2.961615 l
3.084232 3.650591 3.555697 4.226520 4.196528 4.534193 c
7.613911 6.174939 l
7.773231 6.251431 7.840376 6.442595 7.763884 6.601914 c
7.701937 6.730938 7.561847 6.803138 7.420821 6.778723 c
3.616836 6.120156 l
2.843574 5.986285 2.050602 6.199815 1.449121 6.703874 c
0.247410 7.710940 l
-0.045357 7.956288 -0.083799 8.392517 0.161549 8.685284 c
0.280877 8.827677 0.452473 8.916075 0.637689 8.930570 c
4.309271 9.217907 l
4.568659 9.238207 4.794709 9.402359 4.894286 9.642731 c
6.310713 13.061901 l
6.456904 13.414798 6.861495 13.582366 7.214393 13.436174 c
7.383842 13.365978 7.518470 13.231350 7.588666 13.061901 c
9.005094 9.642731 l
9.104671 9.402359 9.330721 9.238207 9.590108 9.217907 c
13.281865 8.928991 l
13.662680 8.899189 13.947231 8.566318 13.917429 8.185503 c
13.903092 8.002308 13.816444 7.832350 13.676609 7.713137 c
10.861062 5.312818 l
10.662857 5.143844 10.576386 4.877848 10.637341 4.624624 c
11.502927 1.028795 l
11.592322 0.657424 11.363736 0.283898 10.992365 0.194502 c
10.813918 0.151546 10.625716 0.181284 10.469205 0.277163 c
7.310984 2.211904 l
7.089269 2.347727 6.810111 2.347727 6.588397 2.211904 c
h
f*
n
Q
endstream
endobj
3 0 obj
1423
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 16.000000 16.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001513 00000 n
0000001536 00000 n
0000001709 00000 n
0000001783 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1842
%%EOF

View File

@ -1,240 +0,0 @@
%PDF-1.7
1 0 obj
<< /Type /XObject
/Length 2 0 R
/Group << /Type /Group
/S /Transparency
>>
/Subtype /Form
/Resources << >>
/BBox [ 0.000000 0.000000 16.000000 16.000000 ]
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 -0.972168 3.548462 cm
0.976471 0.631373 0.101961 scn
8.051580 -0.065265 m
8.386255 0.136172 8.553593 0.236891 8.732291 0.276237 c
8.890351 0.311038 9.054091 0.311038 9.212152 0.276237 c
9.390850 0.236891 9.558187 0.136172 9.892862 -0.065265 c
10.895140 -0.668524 l
12.034245 -1.354137 12.603798 -1.696944 13.007534 -1.631411 c
13.357951 -1.574532 13.660391 -1.354298 13.822091 -1.038259 c
14.008394 -0.674131 13.856972 -0.026846 13.554128 1.267724 c
13.291615 2.389894 l
13.202210 2.772070 13.157508 2.963159 13.175235 3.146049 c
13.190914 3.307812 13.241741 3.464196 13.324162 3.604267 c
13.417347 3.762632 13.565852 3.890926 13.862864 4.147514 c
14.735314 4.901226 l
15.744158 5.772767 16.248579 6.208537 16.311743 6.613088 c
16.366564 6.964196 16.250860 7.320419 16.000189 7.572302 c
15.711361 7.862525 15.047148 7.918721 13.718721 8.031113 c
12.565221 8.128704 l
12.175616 8.161667 11.980813 8.178148 11.812840 8.251106 c
11.664268 8.315638 11.531527 8.411745 11.423840 8.532748 c
11.302092 8.669552 11.225623 8.849475 11.072685 9.209321 c
10.615263 10.285586 l
10.098071 11.502484 9.839476 12.110933 9.475130 12.294894 c
9.158871 12.454576 8.785572 12.454576 8.469313 12.294894 c
8.104968 12.110933 7.846372 11.502483 7.329179 10.285583 c
6.871758 9.209320 l
6.718821 8.849475 6.642353 8.669552 6.520605 8.532748 c
6.412918 8.411745 6.280177 8.315639 6.131604 8.251106 c
5.963631 8.178148 5.768828 8.161667 5.379222 8.128704 c
3.676359 7.984634 l
2.812796 7.911572 2.381014 7.875041 2.157691 7.739972 c
1.683759 7.453331 1.491831 6.862423 1.706876 6.352001 c
1.808207 6.111484 2.136114 5.828204 2.791929 5.261645 c
2.791929 5.261645 l
3.063776 5.026796 3.199700 4.909371 3.347136 4.820806 c
3.653106 4.637008 4.002688 4.538440 4.359606 4.535333 c
4.531591 4.533835 4.708836 4.562960 5.063324 4.621209 c
8.032967 5.109177 l
8.951306 5.260077 9.410476 5.335527 9.519723 5.245213 c
9.613404 5.167767 9.653514 5.042924 9.622472 4.925406 c
9.586272 4.788361 9.169083 4.582251 8.334703 4.170029 c
5.901720 2.968024 l
5.497570 2.768355 5.295495 2.668521 5.126820 2.533812 c
4.890324 2.344938 4.699779 2.104854 4.569541 1.831648 c
4.476653 1.636790 4.425312 1.417324 4.322631 0.978392 c
4.322631 0.978392 l
4.077288 -0.070379 3.954616 -0.594764 4.064208 -0.903394 c
4.218257 -1.337227 4.622936 -1.631910 5.083112 -1.645350 c
5.410482 -1.654910 5.871894 -1.377192 6.794718 -0.821755 c
8.051580 -0.065265 l
h
f*
n
Q
q
1.000000 0.000000 -0.000000 1.000000 -0.972168 3.548462 cm
0.000000 0.000000 0.000000 scn
8.051580 -0.065265 m
8.386255 0.136172 8.553593 0.236891 8.732291 0.276237 c
8.890351 0.311038 9.054091 0.311038 9.212152 0.276237 c
9.390850 0.236891 9.558187 0.136172 9.892862 -0.065265 c
10.895140 -0.668524 l
12.034245 -1.354137 12.603798 -1.696944 13.007534 -1.631411 c
13.357951 -1.574532 13.660391 -1.354298 13.822091 -1.038259 c
14.008394 -0.674131 13.856972 -0.026846 13.554128 1.267724 c
13.291615 2.389894 l
13.202210 2.772070 13.157508 2.963159 13.175235 3.146049 c
13.190914 3.307812 13.241741 3.464196 13.324162 3.604267 c
13.417347 3.762632 13.565852 3.890926 13.862864 4.147514 c
14.735314 4.901226 l
15.744158 5.772767 16.248579 6.208537 16.311743 6.613088 c
16.366564 6.964196 16.250860 7.320419 16.000189 7.572302 c
15.711361 7.862525 15.047148 7.918721 13.718721 8.031113 c
12.565221 8.128704 l
12.175616 8.161667 11.980813 8.178148 11.812840 8.251106 c
11.664268 8.315638 11.531527 8.411745 11.423840 8.532748 c
11.302092 8.669552 11.225623 8.849475 11.072685 9.209321 c
10.615263 10.285586 l
10.098071 11.502484 9.839476 12.110933 9.475130 12.294894 c
9.158871 12.454576 8.785572 12.454576 8.469313 12.294894 c
8.104968 12.110933 7.846372 11.502483 7.329179 10.285583 c
6.871758 9.209320 l
6.718821 8.849475 6.642353 8.669552 6.520605 8.532748 c
6.412918 8.411745 6.280177 8.315639 6.131604 8.251106 c
5.963631 8.178148 5.768828 8.161667 5.379222 8.128704 c
3.676359 7.984634 l
2.812796 7.911572 2.381014 7.875041 2.157691 7.739972 c
1.683759 7.453331 1.491831 6.862423 1.706876 6.352001 c
1.808207 6.111484 2.136114 5.828204 2.791929 5.261645 c
2.791929 5.261645 l
3.063776 5.026796 3.199700 4.909371 3.347136 4.820806 c
3.653106 4.637008 4.002688 4.538440 4.359606 4.535333 c
4.531591 4.533835 4.708836 4.562960 5.063324 4.621209 c
8.032967 5.109177 l
8.951306 5.260077 9.410476 5.335527 9.519723 5.245213 c
9.613404 5.167767 9.653514 5.042924 9.622472 4.925406 c
9.586272 4.788361 9.169083 4.582251 8.334703 4.170029 c
5.901720 2.968024 l
5.497570 2.768355 5.295495 2.668521 5.126820 2.533812 c
4.890324 2.344938 4.699779 2.104854 4.569541 1.831648 c
4.476653 1.636790 4.425312 1.417324 4.322631 0.978392 c
4.322631 0.978392 l
4.077288 -0.070379 3.954616 -0.594764 4.064208 -0.903394 c
4.218257 -1.337227 4.622936 -1.631910 5.083112 -1.645350 c
5.410482 -1.654910 5.871894 -1.377192 6.794718 -0.821755 c
8.051580 -0.065265 l
h
f*
n
Q
endstream
endobj
2 0 obj
4928
endobj
3 0 obj
<< /Type /XObject
/Length 4 0 R
/Group << /Type /Group
/S /Transparency
>>
/Subtype /Form
/Resources << >>
/BBox [ 0.000000 0.000000 16.000000 16.000000 ]
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
0.000000 0.000000 0.000000 scn
0.000000 16.000000 m
16.000000 16.000000 l
16.000000 0.000000 l
0.000000 0.000000 l
0.000000 16.000000 l
h
f
n
Q
endstream
endobj
4 0 obj
232
endobj
5 0 obj
<< /XObject << /X1 1 0 R >>
/ExtGState << /E1 << /SMask << /Type /Mask
/G 3 0 R
/S /Alpha
>>
/Type /ExtGState
>> >>
>>
endobj
6 0 obj
<< /Length 7 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
/E1 gs
/X1 Do
Q
endstream
endobj
7 0 obj
46
endobj
8 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 16.000000 16.000000 ]
/Resources 5 0 R
/Contents 6 0 R
/Parent 9 0 R
>>
endobj
9 0 obj
<< /Kids [ 8 0 R ]
/Count 1
/Type /Pages
>>
endobj
10 0 obj
<< /Pages 9 0 R
/Type /Catalog
>>
endobj
xref
0 11
0000000000 65535 f
0000000010 00000 n
0000005186 00000 n
0000005209 00000 n
0000005689 00000 n
0000005711 00000 n
0000006009 00000 n
0000006111 00000 n
0000006132 00000 n
0000006305 00000 n
0000006379 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 10 0 R
/Size 11
>>
startxref
6439
%%EOF

View File

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

View File

@ -0,0 +1,111 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 1.300293 1.044312 cm
0.000000 0.000000 0.000000 scn
-0.030118 6.492163 m
0.081165 6.149670 0.378177 5.852658 0.972203 5.258633 c
1.449829 4.781006 l
1.449829 4.105689 l
1.449829 3.265610 1.449829 2.845571 1.613319 2.524703 c
1.757129 2.242459 1.986600 2.012989 2.268843 1.869179 c
2.589711 1.705688 3.009750 1.705688 3.849829 1.705688 c
4.525146 1.705688 l
5.002711 1.228124 l
5.596736 0.634098 5.893749 0.337086 6.236242 0.225803 c
6.537507 0.127916 6.862028 0.127916 7.163293 0.225803 c
7.505786 0.337086 7.802798 0.634098 8.396824 1.228124 c
8.874389 1.705688 l
9.549829 1.705688 l
10.389908 1.705688 10.809947 1.705688 11.130815 1.869179 c
11.413058 2.012989 11.642529 2.242459 11.786339 2.524703 c
11.949829 2.845571 11.949829 3.265610 11.949829 4.105688 c
11.949829 4.781129 l
12.427332 5.258632 l
12.427341 5.258640 l
13.021361 5.852661 13.318372 6.149672 13.429653 6.492163 c
13.527540 6.793428 13.527540 7.117949 13.429653 7.419214 c
13.318372 7.761705 13.021361 8.058716 12.427344 8.652733 c
12.427333 8.652744 l
11.949829 9.130249 l
11.949829 9.805689 l
11.949829 10.645767 11.949829 11.065806 11.786339 11.386674 c
11.642529 11.668918 11.413058 11.898388 11.130815 12.042198 c
10.809947 12.205688 10.389908 12.205688 9.549829 12.205688 c
8.874389 12.205688 l
8.396824 12.683253 l
7.802798 13.277279 7.505786 13.574291 7.163293 13.685574 c
6.862028 13.783461 6.537507 13.783461 6.236242 13.685574 c
5.893750 13.574291 5.596738 13.277280 5.002716 12.683257 c
5.002711 12.683253 l
4.525146 12.205688 l
3.849829 12.205688 l
3.009750 12.205688 2.589711 12.205688 2.268843 12.042198 c
1.986600 11.898388 1.757129 11.668918 1.613319 11.386674 c
1.449829 11.065806 1.449829 10.645767 1.449829 9.805689 c
1.449829 9.130371 l
0.972203 8.652744 l
0.972199 8.652741 l
0.378176 8.058718 0.081164 7.761706 -0.030118 7.419214 c
-0.128005 7.117949 -0.128005 6.793428 -0.030118 6.492163 c
h
f*
n
Q
endstream
endobj
3 0 obj
1960
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 16.000000 16.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000002050 00000 n
0000002073 00000 n
0000002246 00000 n
0000002320 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
2379
%%EOF

View File

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

View File

@ -0,0 +1,92 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 5.375000 4.311890 cm
0.000000 0.000000 0.000000 scn
0.530330 3.926815 m
0.237437 4.219707 -0.237437 4.219707 -0.530330 3.926815 c
-0.823223 3.633921 -0.823223 3.159048 -0.530330 2.866154 c
0.530330 3.926815 l
h
1.750000 1.646484 m
1.219670 1.116154 l
1.512563 0.823261 1.987437 0.823261 2.280330 1.116154 c
1.750000 1.646484 l
h
5.780330 4.616154 m
6.073223 4.909048 6.073223 5.383921 5.780330 5.676815 c
5.487437 5.969707 5.012563 5.969707 4.719670 5.676815 c
5.780330 4.616154 l
h
-0.530330 2.866154 m
1.219670 1.116154 l
2.280330 2.176815 l
0.530330 3.926815 l
-0.530330 2.866154 l
h
2.280330 1.116154 m
5.780330 4.616154 l
4.719670 5.676815 l
1.219670 2.176815 l
2.280330 1.116154 l
h
f
n
Q
endstream
endobj
3 0 obj
762
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 16.000000 16.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000000852 00000 n
0000000874 00000 n
0000001047 00000 n
0000001121 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1180
%%EOF

View File

@ -0,0 +1,76 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 5.000000 4.484375 cm
1.000000 1.000000 1.000000 scn
20.000000 11.044711 m
20.000000 16.065481 15.522848 20.135620 10.000000 20.135620 c
4.477152 20.135620 0.000000 16.065481 0.000000 11.044711 c
0.000000 8.181209 1.337573 5.834022 3.613619 4.167677 c
3.904685 3.954580 4.172771 2.770550 3.523984 1.775995 c
2.875197 0.781441 2.066323 0.326941 2.471971 0.156790 c
2.722059 0.051889 4.199766 -0.000002 5.266314 0.598131 c
6.791368 1.453400 7.217727 2.304844 7.545889 2.229574 c
8.331102 2.049473 9.153261 1.953802 10.000000 1.953802 c
15.522848 1.953802 20.000000 6.023941 20.000000 11.044711 c
h
f
n
Q
endstream
endobj
3 0 obj
668
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000000758 00000 n
0000000780 00000 n
0000000953 00000 n
0000001027 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1086
%%EOF

View File

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

View File

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

View File

@ -0,0 +1,91 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 7.000000 4.400036 cm
1.000000 1.000000 1.000000 scn
4.000000 16.599964 m
4.000000 18.809103 5.790861 20.599964 8.000000 20.599964 c
10.209139 20.599964 12.000000 18.809103 12.000000 16.599964 c
12.000000 11.599964 l
12.000000 9.390825 10.209139 7.599964 8.000000 7.599964 c
5.790861 7.599964 4.000000 9.390825 4.000000 11.599964 c
4.000000 16.599964 l
h
1.000000 12.699940 m
1.552285 12.699940 2.000000 12.252225 2.000000 11.699940 c
2.000000 11.599937 l
2.000000 8.286230 4.686291 5.599939 8.000000 5.599939 c
11.313708 5.599939 14.000000 8.286230 14.000000 11.599938 c
14.000000 11.699940 l
14.000000 12.252225 14.447715 12.699940 15.000000 12.699940 c
15.552285 12.699940 16.000000 12.252225 16.000000 11.699940 c
16.000000 11.599938 l
16.000000 7.520320 12.946311 4.153931 9.000000 3.661833 c
9.000000 1.000000 l
9.000000 0.447716 8.552285 0.000000 8.000000 0.000000 c
7.447715 0.000000 7.000000 0.447716 7.000000 1.000000 c
7.000000 3.661833 l
3.053689 4.153931 0.000000 7.520320 0.000000 11.599937 c
0.000000 11.699940 l
0.000000 12.252225 0.447715 12.699940 1.000000 12.699940 c
h
f*
n
Q
endstream
endobj
3 0 obj
1162
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001252 00000 n
0000001275 00000 n
0000001448 00000 n
0000001522 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1581
%%EOF

View File

@ -1095,8 +1095,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if case .premium = value {
controller?.dismiss()
let controller = PremiumStickersScreen(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, reactions: premiumReactions)
strongSelf.present(controller, in: .window(.root))
let controller = PremiumReactionsScreen(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, reactions: premiumReactions)
strongSelf.push(controller)
return
}
@ -3481,8 +3481,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.chatDisplayNode.textInputPanelNode?.ensureFocused()
}
}
}, getNavigationController: { [weak self] in
return self?.effectiveNavigationController
})
strongSelf.present(controller, in: .window(.root))
controller.navigationPresentation = .flatModal
strongSelf.push(controller)
// strongSelf.present(controller, in: .window(.root))
strongSelf.currentMenuWebAppController = controller
} else if simple {
strongSelf.messageActionCallbackDisposable.set(((strongSelf.context.engine.messages.requestSimpleWebView(botId: peerId, url: url, themeParams: generateWebAppThemeParams(strongSelf.presentationData.theme))
@ -3496,6 +3500,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let params = WebAppParameters(peerId: peerId, botId: peerId, botName: botName, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, fromMenu: false, isSimple: true)
let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, openUrl: { [weak self] url in
self?.openUrl(url, concealed: true, forceExternal: true)
}, getNavigationController: { [weak self] in
return self?.effectiveNavigationController
})
strongSelf.currentWebAppController = controller
strongSelf.present(controller, in: .window(.root))
@ -3519,6 +3525,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self?.openUrl(url, concealed: true, forceExternal: true)
}, completion: { [weak self] in
self?.chatDisplayNode.historyNode.scrollToEndOfHistory()
}, getNavigationController: { [weak self] in
return self?.effectiveNavigationController
})
strongSelf.currentWebAppController = controller
strongSelf.present(controller, in: .window(.root))
@ -10745,6 +10753,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
public func presentAttachmentBot(botId: PeerId, payload: String?) {
self.attachmentController?.dismiss(animated: true, completion: nil)
self.presentAttachmentMenu(editMediaOptions: nil, editMediaReference: nil, botId: botId, botPayload: payload)
}

View File

@ -1617,7 +1617,9 @@ final class ChatMediaInputNode: ChatInputNode {
}
}
})))
return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.theme, strings: strongSelf.strings, item: item, menu: menuItems))
return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.theme, strings: strongSelf.strings, item: item, menu: menuItems, openPremiumIntro: {
}))
} else {
return nil
}
@ -1743,7 +1745,9 @@ final class ChatMediaInputNode: ChatInputNode {
}
}))
)
return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.theme, strings: strongSelf.strings, item: .pack(item), isLocked: item.file.isPremiumSticker && !hasPremium, menu: menuItems))
return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.theme, strings: strongSelf.strings, item: .pack(item), isLocked: item.file.isPremiumSticker && !hasPremium, menu: menuItems, openPremiumIntro: {
}))
} else {
return nil
}

View File

@ -748,7 +748,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
var imageHorizontalOffset: CGFloat = 0.0
if !(telegramFile?.videoThumbnails.isEmpty ?? true) {
displaySize = CGSize(width: 240.0, height: 240.0)
imageVerticalInset = -30.0
imageVerticalInset = -20.0
imageHorizontalOffset = 12.0
}
@ -1638,7 +1638,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
} else {
let pathPrefix = item.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(resource.id)
let additionalAnimationNode = AnimatedStickerNode()
additionalAnimationNode.setup(source: source, width: Int(animationSize.width * 2), height: Int(animationSize.height * 2), playbackMode: .once, mode: .direct(cachePathPrefix: pathPrefix))
additionalAnimationNode.setup(source: source, width: Int(animationSize.width * 2), height: Int(animationSize.height * 2), playbackMode: .once, mode: isStickerEffect ? .cached : .direct(cachePathPrefix: pathPrefix))
var animationFrame: CGRect
if isStickerEffect {
let scale: CGFloat = 0.245

View File

@ -139,14 +139,16 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
segments = [.text(0, NSAttributedString(string: EnginePeer(peer).displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder), font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))]
}
}
if peer.isFake {
titleCredibilityIcon = .fake
} else if peer.isScam {
titleCredibilityIcon = .scam
} else if peer.isVerified {
titleCredibilityIcon = .verified
} else if peer.isPremium {
titleCredibilityIcon = .premium
if peer.id != self.account.peerId {
if peer.isFake {
titleCredibilityIcon = .fake
} else if peer.isScam {
titleCredibilityIcon = .scam
} else if peer.isVerified {
titleCredibilityIcon = .verified
} else if peer.isPremium {
titleCredibilityIcon = .premium
}
}
}
if peerView.peerId.namespace == Namespaces.Peer.SecretChat {
@ -619,6 +621,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
self.strings = strings
let titleContent = self.titleContent
self.titleCredibilityIcon = .none
self.titleContent = titleContent
let _ = self.updateStatus()

View File

@ -531,7 +531,9 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode {
}
}))
]
return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, item: item, menu: menuItems))
return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, item: item, menu: menuItems, openPremiumIntro: {
}))
} else {
return nil
}
@ -595,7 +597,9 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode {
}
}))
]
return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, item: .pack(item), menu: menuItems))
return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, item: .pack(item), menu: menuItems, openPremiumIntro: {
}))
} else {
return nil
}

View File

@ -174,7 +174,9 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont
}
})))
}
selectedItemNodeAndContent = (itemNode, StickerPreviewPeekContent(account: item.account, theme: strongSelf.theme, strings: strongSelf.strings, item: .found(FoundStickerItem(file: file, stringRepresentations: [])), menu: menuItems))
selectedItemNodeAndContent = (itemNode, StickerPreviewPeekContent(account: item.account, theme: strongSelf.theme, strings: strongSelf.strings, item: .found(FoundStickerItem(file: file, stringRepresentations: [])), menu: menuItems, openPremiumIntro: {
}))
} else {
var menuItems: [ContextMenuItem] = []
if case let .internalReference(internalReference) = item.result, let file = internalReference.file, file.isAnimated {

View File

@ -220,7 +220,9 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode {
}
}))
]
return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.theme, strings: strongSelf.strings, item: .pack(item), menu: menuItems))
return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.theme, strings: strongSelf.strings, item: .pack(item), menu: menuItems, openPremiumIntro: {
}))
} else {
return nil
}

View File

@ -175,7 +175,9 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie
}
}))
)
return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.theme, strings: strongSelf.strings, item: .pack(item), menu: menuItems))
return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.theme, strings: strongSelf.strings, item: .pack(item), menu: menuItems, openPremiumIntro: {
}))
} else {
return nil
}

View File

@ -584,7 +584,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
|> `catch` { _ -> Signal<BotCheckoutController.InputData?, NoError> in
return .single(nil)
})
navigationController.pushViewController(BotCheckoutController(context: context, invoice: invoice, source: .slug(slug), inputData: inputData, completed: { currencyValue, receiptMessageId in
let checkoutController = BotCheckoutController(context: context, invoice: invoice, source: .slug(slug), inputData: inputData, completed: { currencyValue, receiptMessageId in
/*strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .paymentSent(currencyValue: currencyValue, itemTitle: invoice.title), elevatedLayout: false, action: { action in
guard let strongSelf = self, let receiptMessageId = receiptMessageId else {
return false
@ -596,7 +596,9 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
}
return false
}), in: .current)*/
}))
})
checkoutController.navigationPresentation = .modal
navigationController.pushViewController(checkoutController)
}
}
}

View File

@ -2250,7 +2250,16 @@ final class PeerInfoHeaderNode: ASDisplayNode {
self.avatarListNode.listContainerNode.updateEntryIsHidden(entry: entry)
}
private var initializedCredibilityIcon = false
private enum CredibilityIcon {
case none
case premium
case verified
case fake
case scam
}
private var currentCredibilityIcon: CredibilityIcon?
private var currentPanelStatusData: PeerInfoStatusData?
func update(width: CGFloat, containerHeight: CGFloat, containerInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, isModalOverlay: Bool, isMediaOnly: Bool, contentOffset: CGFloat, paneContainerY: CGFloat, presentationData: PresentationData, peer: Peer?, cachedData: CachedPeerData?, notificationSettings: TelegramPeerNotificationSettings?, statusData: PeerInfoStatusData?, panelStatusData: (PeerInfoStatusData?, PeerInfoStatusData?, CGFloat?), isSecretChat: Bool, isContact: Bool, isSettings: Bool, state: PeerInfoState, metrics: LayoutMetrics, transition: ContainedViewLayoutTransition, additive: Bool) -> CGFloat {
self.state = state
@ -2276,65 +2285,80 @@ final class PeerInfoHeaderNode: ASDisplayNode {
let themeUpdated = self.presentationData?.theme !== presentationData.theme
self.presentationData = presentationData
if themeUpdated || !initializedCredibilityIcon {
let credibilityIcon: CredibilityIcon
if let peer = peer {
if peer.isFake {
credibilityIcon = .fake
} else if peer.isScam {
credibilityIcon = .scam
} else if peer.isVerified {
credibilityIcon = .verified
} else if peer.isPremium {
credibilityIcon = .premium
} else {
credibilityIcon = .none
}
} else {
credibilityIcon = .none
}
if themeUpdated || self.currentCredibilityIcon != credibilityIcon {
self.currentCredibilityIcon = credibilityIcon
let image: UIImage?
var expandedImage: UIImage?
if let peer = peer {
self.initializedCredibilityIcon = true
if peer.isFake {
image = PresentationResourcesChatList.fakeIcon(presentationData.theme, strings: presentationData.strings, type: .regular)
} else if peer.isScam {
image = PresentationResourcesChatList.scamIcon(presentationData.theme, strings: presentationData.strings, type: .regular)
} else if peer.isVerified {
if let sourceImage = UIImage(bundleImageName: "Peer Info/VerifiedIcon") {
image = generateImage(sourceImage.size, contextGenerator: { size, context in
if case .fake = credibilityIcon {
image = PresentationResourcesChatList.fakeIcon(presentationData.theme, strings: presentationData.strings, type: .regular)
} else if case .scam = credibilityIcon {
image = PresentationResourcesChatList.scamIcon(presentationData.theme, strings: presentationData.strings, type: .regular)
} else if case .verified = credibilityIcon {
if let sourceImage = UIImage(bundleImageName: "Peer Info/VerifiedIcon") {
image = generateImage(sourceImage.size, contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(presentationData.theme.list.itemCheckColors.foregroundColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: 7.0, dy: 7.0))
context.setFillColor(presentationData.theme.list.itemCheckColors.fillColor.cgColor)
context.clip(to: CGRect(origin: CGPoint(), size: size), mask: sourceImage.cgImage!)
context.fill(CGRect(origin: CGPoint(), size: size))
})
} else {
image = nil
}
} else if case .premium = credibilityIcon {
if let sourceImage = UIImage(bundleImageName: "Peer Info/PremiumIcon") {
image = generateImage(sourceImage.size, contextGenerator: { size, context in
if let cgImage = sourceImage.cgImage {
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(presentationData.theme.list.itemCheckColors.foregroundColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: 7.0, dy: 7.0))
context.setFillColor(presentationData.theme.list.itemCheckColors.fillColor.cgColor)
context.clip(to: CGRect(origin: CGPoint(), size: size), mask: sourceImage.cgImage!)
context.fill(CGRect(origin: CGPoint(), size: size))
})
} else {
image = nil
}
} else if peer.isPremium {
if let sourceImage = UIImage(bundleImageName: "Peer Info/PremiumIcon") {
image = generateImage(sourceImage.size, contextGenerator: { size, context in
if let cgImage = sourceImage.cgImage {
context.clear(CGRect(origin: CGPoint(), size: size))
context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage)
let colorsArray: [CGColor] = [
UIColor(rgb: 0x6B93FF).cgColor,
UIColor(rgb: 0x6B93FF).cgColor,
UIColor(rgb: 0x976FFF).cgColor,
UIColor(rgb: 0xE46ACE).cgColor,
UIColor(rgb: 0xE46ACE).cgColor
]
var locations: [CGFloat] = [0.0, 0.35, 0.5, 0.65, 1.0]
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)!
context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage)
let colorsArray: [CGColor] = [
UIColor(rgb: 0x6B93FF).cgColor,
UIColor(rgb: 0x6B93FF).cgColor,
UIColor(rgb: 0x976FFF).cgColor,
UIColor(rgb: 0xE46ACE).cgColor,
UIColor(rgb: 0xE46ACE).cgColor
]
var locations: [CGFloat] = [0.0, 0.35, 0.5, 0.65, 1.0]
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: size.height), options: CGGradientDrawingOptions())
}
}, opaque: false)
expandedImage = generateImage(sourceImage.size, contextGenerator: { size, context in
if let cgImage = sourceImage.cgImage {
context.clear(CGRect(origin: CGPoint(), size: size))
context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage)
context.setFillColor(UIColor(rgb: 0xffffff, alpha: 0.75).cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
}
}, opaque: false)
} else {
image = nil
}
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: size.height), options: CGGradientDrawingOptions())
}
}, opaque: false)
expandedImage = generateImage(sourceImage.size, contextGenerator: { size, context in
if let cgImage = sourceImage.cgImage {
context.clear(CGRect(origin: CGPoint(), size: size))
context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage)
context.setFillColor(UIColor(rgb: 0xffffff, alpha: 0.75).cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
}
}, opaque: false)
} else {
image = nil
}
} else {
image = nil
}
self.titleCredibilityIconNode.image = image
self.titleExpandedCredibilityIconNode.image = expandedImage ?? image
}
@ -2409,6 +2433,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
let buttonKeys: [PeerInfoHeaderButtonKey] = self.isSettings ? [] : peerInfoHeaderButtons(peer: peer, cachedData: cachedData, isOpenedFromChat: self.isOpenedFromChat, isExpanded: true, videoCallsEnabled: width > 320.0 && self.videoCallsEnabled, isSecretChat: isSecretChat, isContact: isContact)
var isPremium = false
var isVerified = false
var isFake = false
let smallTitleString: NSAttributedString
@ -2419,6 +2444,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
var nextPanelSubtitleString: NSAttributedString?
let usernameString: NSAttributedString
if let peer = peer {
isPremium = peer.isPremium
isVerified = peer.isVerified
isFake = peer.isFake || peer.isScam
}
@ -2494,7 +2520,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
let textSideInset: CGFloat = 36.0
let expandedAvatarHeight: CGFloat = expandedAvatarListSize.height
let titleConstrainedSize = CGSize(width: width - textSideInset * 2.0 - (isVerified || isFake ? 20.0 : 0.0), height: .greatestFiniteMagnitude)
let titleConstrainedSize = CGSize(width: width - textSideInset * 2.0 - (isPremium || isVerified || isFake ? 20.0 : 0.0), height: .greatestFiniteMagnitude)
let titleNodeLayout = self.titleNode.updateLayout(states: [
TitleNodeStateRegular: MultiScaleTextState(attributedText: titleString, constrainedSize: titleConstrainedSize),

View File

@ -176,7 +176,9 @@ final class StickersChatInputContextPanelNode: ChatInputContextPanelNode {
}
}))
]
return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.theme, strings: strongSelf.strings, item: .pack(item), menu: menuItems))
return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.theme, strings: strongSelf.strings, item: .pack(item), menu: menuItems, openPremiumIntro: {
}))
} else {
return nil
}

View File

@ -242,6 +242,9 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? {
} else {
return .join(String(component.dropFirst()))
}
} else if pathComponents[0].hasPrefix("$") || pathComponents[0].hasPrefix("%24") {
let component = pathComponents[0].replacingOccurrences(of: "%24", with: "$")
return .invoice(component)
}
return .peerName(peerName, nil)
} else if pathComponents.count == 2 || pathComponents.count == 3 {

View File

@ -0,0 +1,27 @@
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "RangeSet",
platforms: [.macOS(.v10_11)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "RangeSet",
targets: ["RangeSet"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "RangeSet",
dependencies: [],
path: "Sources")
]
)

View File

@ -25,6 +25,7 @@ swift_library(
"//submodules/LegacyComponents:LegacyComponents",
"//submodules/UrlHandling:UrlHandling",
"//submodules/MoreButtonNode:MoreButtonNode",
"//submodules/BotPaymentsUI:BotPaymentsUI",
],
visibility = [
"//visibility:public",

View File

@ -18,6 +18,7 @@ import PhotoResources
import LegacyComponents
import UrlHandling
import MoreButtonNode
import BotPaymentsUI
private let durgerKingBotIds: [Int64] = [5104055776, 2200339955]
@ -80,7 +81,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
public var cancelPanGesture: () -> Void = { }
public var isContainerPanning: () -> Bool = { return false }
public var isContainerExpanded: () -> Bool = { return false }
fileprivate class Node: ViewControllerTracingNode, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate {
private weak var controller: WebAppController?
@ -93,23 +94,23 @@ public final class WebAppController: ViewController, AttachmentContainable {
private let context: AccountContext
var presentationData: PresentationData
private let present: (ViewController, Any?) -> Void
private var queryId: Int64?
private var placeholderDisposable: Disposable?
private var iconDisposable: Disposable?
private var keepAliveDisposable: Disposable?
private var paymentDisposable: Disposable?
private var didTransitionIn = false
private var dismissed = false
private var validLayout: (ContainerViewLayout, CGFloat)?
init(context: AccountContext, controller: WebAppController, present: @escaping (ViewController, Any?) -> Void) {
init(context: AccountContext, controller: WebAppController) {
self.context = context
self.controller = controller
self.presentationData = controller.presentationData
self.present = present
super.init()
@ -264,6 +265,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
self.placeholderDisposable?.dispose()
self.iconDisposable?.dispose()
self.keepAliveDisposable?.dispose()
self.paymentDisposable?.dispose()
self.webView?.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress))
}
@ -475,16 +477,38 @@ public final class WebAppController: ViewController, AttachmentContainable {
case "web_app_request_theme":
self.sendThemeChangedEvent()
case "web_app_expand":
self.controller?.requestAttachmentMenuExpansion()
controller.requestAttachmentMenuExpansion()
case "web_app_close":
self.controller?.dismiss()
controller.dismiss()
case "web_app_open_tg_link":
if let json = json, let path = json["path_full"] as? String {
print(path)
controller.openUrl("https://t.me\(path)")
controller.dismiss()
}
case "web_app_open_invoice":
if let json = json, let slug = json["slug"] as? String {
print(slug)
self.paymentDisposable = (context.engine.payments.fetchBotPaymentInvoice(source: .slug(slug))
|> map(Optional.init)
|> `catch` { _ -> Signal<TelegramMediaInvoice?, NoError> in
return .single(nil)
}
|> deliverOnMainQueue).start(next: { [weak self] invoice in
if let strongSelf = self, let invoice = invoice {
let inputData = Promise<BotCheckoutController.InputData?>()
inputData.set(BotCheckoutController.InputData.fetch(context: strongSelf.context, source: .slug(slug))
|> map(Optional.init)
|> `catch` { _ -> Signal<BotCheckoutController.InputData?, NoError> in
return .single(nil)
})
if let navigationController = strongSelf.controller?.getNavigationController() {
let checkoutController = BotCheckoutController(context: strongSelf.context, invoice: invoice, source: .slug(slug), inputData: inputData, completed: { currencyValue, receiptMessageId in
})
checkoutController.navigationPresentation = .modal
navigationController.pushViewController(checkoutController)
}
}
})
}
default:
break
@ -539,8 +563,27 @@ public final class WebAppController: ViewController, AttachmentContainable {
self.webView?.sendEvent(name: "theme_changed", data: themeParamsString)
}
private func sendInvoiceClosedEvent(slug: String, status: String) {
let paramsString = "{slug: \"\(slug)\", status: \"\(status)\"}"
enum InvoiceCloseResult {
case paid
case pending
case cancelled
case failed
var string: String {
switch self {
case .paid:
return "paid"
case .pending:
return "pending"
case .cancelled:
return "cancelled"
case .failed:
return "failed"
}
}
}
private func sendInvoiceClosedEvent(slug: String, result: InvoiceCloseResult) {
let paramsString = "{slug: \"\(slug)\", status: \"\(result.string)\"}"
self.webView?.sendEvent(name: "invoice_closed", data: paramsString)
}
}
@ -704,9 +747,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
override public func loadDisplayNode() {
self.displayNode = Node(context: self.context, controller: self, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
})
self.displayNode = Node(context: self.context, controller: self)
self.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate)
self.updateTabBarAlpha(1.0, .immediate)
@ -790,13 +831,14 @@ private final class WebAppContextReferenceContentSource: ContextReferenceContent
}
}
public func standaloneWebAppController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, params: WebAppParameters, openUrl: @escaping (String) -> Void, getInputContainerNode: @escaping () -> (CGFloat, ASDisplayNode, () -> AttachmentController.InputPanelTransition?)? = { return nil }, completion: @escaping () -> Void = {}, willDismiss: @escaping () -> Void = {}, didDismiss: @escaping () -> Void = {}) -> ViewController {
public func standaloneWebAppController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, params: WebAppParameters, openUrl: @escaping (String) -> Void, getInputContainerNode: @escaping () -> (CGFloat, ASDisplayNode, () -> AttachmentController.InputPanelTransition?)? = { return nil }, completion: @escaping () -> Void = {}, willDismiss: @escaping () -> Void = {}, didDismiss: @escaping () -> Void = {}, getNavigationController: @escaping () -> NavigationController? = { return nil }) -> ViewController {
let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: .peer(id: params.peerId), buttons: [.standalone], initialButton: .standalone, fromMenu: params.fromMenu)
controller.getInputContainerNode = getInputContainerNode
controller.requestController = { _, present in
let webAppController = WebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, replyToMessageId: nil)
webAppController.openUrl = openUrl
webAppController.completion = completion
webAppController.getNavigationController = getNavigationController
present(webAppController, webAppController.mediaPickerContext)
}
controller.willDismiss = willDismiss