mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-03 05:03:45 +00:00
Merge commit '3fb304bd6ffd03c9bc1ae63db6a33c0f9afe229c'
This commit is contained in:
commit
352916f866
@ -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";
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
})
|
||||
|
||||
@ -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))
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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))
|
||||
|
||||
|
||||
@ -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)))
|
||||
|
||||
@ -390,7 +390,7 @@ final class ChatListFilterTabInlineContainerNode: ASDisplayNode {
|
||||
$0.compactMap {
|
||||
switch $0 {
|
||||
case .all:
|
||||
return nil
|
||||
return 0
|
||||
case let .filter(id):
|
||||
return id
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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, _):
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
|
||||
@ -30,5 +30,7 @@ public protocol PeekControllerContentNode {
|
||||
}
|
||||
|
||||
public protocol PeekControllerAccessoryNode {
|
||||
var dismiss: () -> Void { get set }
|
||||
|
||||
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)],
|
||||
|
||||
@ -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",
|
||||
|
||||
BIN
submodules/PremiumUI/Resources/star
Normal file
BIN
submodules/PremiumUI/Resources/star
Normal file
Binary file not shown.
Binary file not shown.
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
373
submodules/PremiumUI/Sources/PremiumStarComponent.swift
Normal file
373
submodules/PremiumUI/Sources/PremiumStarComponent.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)))
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "premiumbadge_16.pdf",
|
||||
"filename" : "premiumbadge_16 (1).pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
|
||||
97
submodules/TelegramUI/Images.xcassets/Chat List/PeerPremiumIcon.imageset/premiumbadge_16 (1).pdf
vendored
Normal file
97
submodules/TelegramUI/Images.xcassets/Chat List/PeerPremiumIcon.imageset/premiumbadge_16 (1).pdf
vendored
Normal 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
|
||||
@ -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
|
||||
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "verifybadge1_16.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "verifybadge2_16.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
76
submodules/TelegramUI/Images.xcassets/Premium/Perk/Chat.imageset/Chat.pdf
vendored
Normal file
76
submodules/TelegramUI/Images.xcassets/Premium/Perk/Chat.imageset/Chat.pdf
vendored
Normal 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
|
||||
12
submodules/TelegramUI/Images.xcassets/Premium/Perk/Chat.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Premium/Perk/Chat.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Chat.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
12
submodules/TelegramUI/Images.xcassets/Premium/Perk/Voice.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Premium/Perk/Voice.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Voice.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
91
submodules/TelegramUI/Images.xcassets/Premium/Perk/Voice.imageset/Voice.pdf
vendored
Normal file
91
submodules/TelegramUI/Images.xcassets/Premium/Perk/Voice.imageset/Voice.pdf
vendored
Normal 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
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
27
submodules/Utils/RangeSet/Package.swift
Normal file
27
submodules/Utils/RangeSet/Package.swift
Normal 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")
|
||||
]
|
||||
)
|
||||
@ -25,6 +25,7 @@ swift_library(
|
||||
"//submodules/LegacyComponents:LegacyComponents",
|
||||
"//submodules/UrlHandling:UrlHandling",
|
||||
"//submodules/MoreButtonNode:MoreButtonNode",
|
||||
"//submodules/BotPaymentsUI:BotPaymentsUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user