Experimental chat list filtering

This commit is contained in:
Ali 2020-01-24 13:01:49 +04:00
parent 13b0847b1e
commit d2e3c9d54a
18 changed files with 880 additions and 87 deletions

View File

@ -99,7 +99,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent
}
}
public class ChatListControllerImpl: TelegramBaseController, ChatListController, UIViewControllerPreviewingDelegate {
public class ChatListControllerImpl: TelegramBaseController, ChatListController, UIViewControllerPreviewingDelegate, TabBarContainedController {
private var validLayout: ContainerViewLayout?
public let context: AccountContext
@ -1790,4 +1790,20 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
return nil
}
}
public func presentTabBarPreviewingController(sourceNodes: [ASDisplayNode]) {
if self.isNodeLoaded {
let controller = TabBarChatListFilterController(context: self.context, sourceNodes: sourceNodes, currentFilter: self.chatListDisplayNode.chatListNode.chatListFilter, updateFilter: { [weak self] value in
guard let strongSelf = self else {
return
}
strongSelf.chatListDisplayNode.chatListNode.chatListFilter = value
})
self.context.sharedContext.mainWindow?.present(controller, on: .root)
}
}
public func updateTabBarPreviewingControllerPresentation(_ update: TabBarContainedControllerPresentationUpdate) {
}
}

View File

@ -699,7 +699,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
isPeerGroup = false
isAd = isAdValue
displayAsMessage = displayAsMessageValue
hasFailedMessages = hasFailedMessagesValue
hasFailedMessages = messageValue?.flags.contains(.Failed) ?? false // hasFailedMessagesValue
case let .groupReference(_, peers, messageValue, unreadState, hiddenByDefault):
if let _ = messageValue, !peers.isEmpty {
contentPeer = .chat(peers[0].peer)

View File

@ -366,6 +366,14 @@ public final class ChatListNode: ListView {
}
private var currentLocation: ChatListNodeLocation?
var chatListFilter: ChatListNodeFilter = .all {
didSet {
if self.chatListFilter != oldValue {
self.chatListFilterValue.set(self.chatListFilter)
}
}
}
private let chatListFilterValue = ValuePromise<ChatListNodeFilter>(.all)
private let chatListLocation = ValuePromise<ChatListNodeLocation>()
private let chatListDisposable = MetaDisposable()
private var activityStatusesDisposable: Disposable?
@ -522,10 +530,21 @@ public final class ChatListNode: ListView {
let viewProcessingQueue = self.viewProcessingQueue
let chatListViewUpdate = self.chatListLocation.get()
|> distinctUntilChanged
|> mapToSignal { location in
return chatListViewForLocation(groupId: groupId, location: location, account: context.account)
let chatListViewUpdate = combineLatest(self.chatListLocation.get(), self.chatListFilterValue.get())
|> distinctUntilChanged(isEqual: { lhs, rhs in
if lhs.0 != rhs.0 {
return false
}
if lhs.1 != rhs.1 {
return false
}
return true
})
|> mapToSignal { location, filter -> Signal<(ChatListNodeViewUpdate, ChatListNodeFilter), NoError> in
return chatListViewForLocation(groupId: groupId, filter: filter, location: location, account: context.account)
|> map { update in
return (update, filter)
}
}
let previousState = Atomic<ChatListNodeState>(value: self.currentState)
@ -575,7 +594,8 @@ public final class ChatListNode: ListView {
let currentPeerId: PeerId = context.account.peerId
let chatListNodeViewTransition = combineLatest(queue: viewProcessingQueue, hideArchivedFolderByDefault, displayArchiveIntro, savedMessagesPeer, chatListViewUpdate, self.statePromise.get())
|> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, savedMessagesPeer, update, state) -> Signal<ChatListNodeListViewTransition, NoError> in
|> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, savedMessagesPeer, updateAndFilter, state) -> Signal<ChatListNodeListViewTransition, NoError> in
let (update, filter) = updateAndFilter
let previousHideArchivedFolderByDefaultValue = previousHideArchivedFolderByDefault.swap(hideArchivedFolderByDefault)
@ -633,7 +653,7 @@ public final class ChatListNode: ListView {
}
}
let processedView = ChatListNodeView(originalView: update.view, filteredEntries: entries, isLoading: isLoading)
let processedView = ChatListNodeView(originalView: update.view, filteredEntries: entries, isLoading: isLoading, filter: filter)
let previousView = previousView.swap(processedView)
let previousState = previousState.swap(state)
@ -746,6 +766,10 @@ public final class ChatListNode: ListView {
searchMode = true
}
if filter != previousView?.filter {
disableAnimations = true
}
return preparedChatListNodeViewTransition(from: previousView, to: processedView, reason: reason, previewing: previewing, disableAnimations: disableAnimations, account: context.account, scrollPosition: updatedScrollPosition, searchMode: searchMode)
|> map({ mappedChatListNodeViewListTransition(context: context, nodeInteraction: nodeInteraction, peerGroupId: groupId, mode: mode, transition: $0) })
|> runOn(prepareOnMainQueue ? Queue.mainQueue() : viewProcessingQueue)
@ -1439,17 +1463,23 @@ public final class ChatListNode: ListView {
guard index < 10 else {
return
}
let _ = (chatListViewForLocation(groupId: self.groupId, location: .initial(count: 10), account: self.context.account)
let _ = (self.chatListFilterValue.get()
|> take(1)
|> deliverOnMainQueue).start(next: { update in
let entries = update.view.entries
if entries.count > index, case let .MessageEntry(index, _, _, _, _, renderedPeer, _, _, _) = entries[10 - index - 1] {
let location: ChatListNodeLocation = .scroll(index: index, sourceIndex: .absoluteLowerBound, scrollPosition: .center(.top), animated: true)
self.setChatListLocation(location)
self.peerSelected?(renderedPeer.peerId, false, false)
|> deliverOnMainQueue).start(next: { [weak self] filter in
guard let self = self else {
return
}
let _ = (chatListViewForLocation(groupId: self.groupId, filter: filter, location: .initial(count: 10), account: self.context.account)
|> take(1)
|> deliverOnMainQueue).start(next: { update in
let entries = update.view.entries
if entries.count > index, case let .MessageEntry(index, _, _, _, _, renderedPeer, _, _, _) = entries[10 - index - 1] {
let location: ChatListNodeLocation = .scroll(index: index, sourceIndex: .absoluteLowerBound, scrollPosition: .center(.top), animated: true)
self.setChatListLocation(location)
self.peerSelected?(renderedPeer.peerId, false, false)
}
})
})
break
}
}

View File

@ -31,18 +31,90 @@ struct ChatListNodeViewUpdate {
let scrollPosition: ChatListNodeViewScrollPosition?
}
func chatListViewForLocation(groupId: PeerGroupId, location: ChatListNodeLocation, account: Account) -> Signal<ChatListNodeViewUpdate, NoError> {
struct ChatListNodeFilter: OptionSet {
var rawValue: Int32
init(rawValue: Int32) {
self.rawValue = rawValue
}
static let muted = ChatListNodeFilter(rawValue: 1 << 1)
static let privateChats = ChatListNodeFilter(rawValue: 1 << 2)
static let groups = ChatListNodeFilter(rawValue: 1 << 3)
static let bots = ChatListNodeFilter(rawValue: 1 << 4)
static let channels = ChatListNodeFilter(rawValue: 1 << 5)
static let all: ChatListNodeFilter = [
.muted,
.privateChats,
.groups,
.bots,
.channels
]
}
func chatListViewForLocation(groupId: PeerGroupId, filter: ChatListNodeFilter, location: ChatListNodeLocation, account: Account) -> Signal<ChatListNodeViewUpdate, NoError> {
let filterPredicate: ((Peer, PeerNotificationSettings?) -> Bool)?
if filter == .all {
filterPredicate = nil
} else {
filterPredicate = { peer, notificationSettings in
if !filter.contains(.muted) {
if let notificationSettings = notificationSettings as? TelegramPeerNotificationSettings {
if case .muted = notificationSettings.muteState {
return false
}
} else {
return false
}
}
if !filter.contains(.privateChats) {
if let user = peer as? TelegramUser {
if user.botInfo == nil {
return false
}
} else if let _ = peer as? TelegramSecretChat {
return false
}
}
if !filter.contains(.bots) {
if let user = peer as? TelegramUser {
if user.botInfo != nil {
return false
}
}
}
if !filter.contains(.groups) {
if let _ = peer as? TelegramGroup {
return false
} else if let channel = peer as? TelegramChannel {
if case .group = channel.info {
return false
}
}
}
if !filter.contains(.channels) {
if let channel = peer as? TelegramChannel {
if case .broadcast = channel.info {
return false
}
}
}
return true
}
}
switch location {
case let .initial(count):
let signal: Signal<(ChatListView, ViewUpdateType), NoError>
signal = account.viewTracker.tailChatListView(groupId: groupId, count: count)
signal = account.viewTracker.tailChatListView(groupId: groupId, filterPredicate: filterPredicate, count: count)
return signal
|> map { view, updateType -> ChatListNodeViewUpdate in
return ChatListNodeViewUpdate(view: view, type: updateType, scrollPosition: nil)
}
case let .navigation(index):
var first = true
return account.viewTracker.aroundChatListView(groupId: groupId, index: index, count: 80)
return account.viewTracker.aroundChatListView(groupId: groupId, filterPredicate: filterPredicate, index: index, count: 80)
|> map { view, updateType -> ChatListNodeViewUpdate in
let genericType: ViewUpdateType
if first {
@ -57,7 +129,7 @@ func chatListViewForLocation(groupId: PeerGroupId, location: ChatListNodeLocatio
let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up
let chatScrollPosition: ChatListNodeViewScrollPosition = .index(index: index, position: scrollPosition, directionHint: directionHint, animated: animated)
var first = true
return account.viewTracker.aroundChatListView(groupId: groupId, index: index, count: 80)
return account.viewTracker.aroundChatListView(groupId: groupId, filterPredicate: filterPredicate, index: index, count: 80)
|> map { view, updateType -> ChatListNodeViewUpdate in
let genericType: ViewUpdateType
let scrollPosition: ChatListNodeViewScrollPosition? = first ? chatScrollPosition : nil

View File

@ -11,6 +11,7 @@ struct ChatListNodeView {
let originalView: ChatListView
let filteredEntries: [ChatListNodeEntry]
let isLoading: Bool
let filter: ChatListNodeFilter
}
enum ChatListNodeViewTransitionReason {

View File

@ -0,0 +1,477 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import AsyncDisplayKit
import TelegramPresentationData
import AccountContext
final class TabBarChatListFilterController: ViewController {
private var controllerNode: TabBarChatListFilterControllerNode {
return self.displayNode as! TabBarChatListFilterControllerNode
}
private let _ready = Promise<Bool>(true)
override public var ready: Promise<Bool> {
return self._ready
}
private let context: AccountContext
private let sourceNodes: [ASDisplayNode]
private let currentFilter: ChatListNodeFilter
private let updateFilter: (ChatListNodeFilter) -> Void
private var presentationData: PresentationData
private var didPlayPresentationAnimation = false
private let hapticFeedback = HapticFeedback()
public init(context: AccountContext, sourceNodes: [ASDisplayNode], currentFilter: ChatListNodeFilter, updateFilter: @escaping (ChatListNodeFilter) -> Void) {
self.context = context
self.sourceNodes = sourceNodes
self.currentFilter = currentFilter
self.updateFilter = updateFilter
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
super.init(navigationBarPresentationData: nil)
self.statusBar.statusBarStyle = .Ignore
self.statusBar.ignoreInCall = true
self.lockOrientation = true
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
override public func loadDisplayNode() {
self.displayNode = TabBarChatListFilterControllerNode(context: self.context, presentationData: self.presentationData, cancel: { [weak self] in
self?.dismiss()
}, sourceNodes: self.sourceNodes, currentFilter: self.currentFilter, updateFilter: self.updateFilter)
self.displayNodeDidLoad()
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !self.didPlayPresentationAnimation {
self.didPlayPresentationAnimation = true
self.hapticFeedback.impact()
self.controllerNode.animateIn()
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, transition: transition)
}
override public func dismiss(completion: (() -> Void)? = nil) {
self.dismiss(sourceNodes: [])
}
public func dismiss(sourceNodes: [ASDisplayNode]) {
self.controllerNode.animateOut(sourceNodes: sourceNodes, completion: { [weak self] in
self?.didPlayPresentationAnimation = false
self?.presentingViewController?.dismiss(animated: false, completion: nil)
})
}
}
private let animationDurationFactor: Double = 1.0
private protocol AbstractTabBarChatListFilterItemNode {
func updateLayout(maxWidth: CGFloat) -> (CGFloat, CGFloat, (CGFloat) -> Void)
}
private final class FilterItemNode: ASDisplayNode, AbstractTabBarChatListFilterItemNode {
private let context: AccountContext
private let title: String
private let isCurrent: Bool
private let presentationData: PresentationData
private let action: () -> Bool
private let separatorNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let buttonNode: HighlightTrackingButtonNode
private let titleNode: ImmediateTextNode
private let checkNode: ASImageNode
private let badgeBackgroundNode: ASImageNode
private let badgeTitleNode: ImmediateTextNode
init(context: AccountContext, title: String, isCurrent: Bool, displaySeparator: Bool, presentationData: PresentationData, action: @escaping () -> Bool) {
self.context = context
self.title = title
self.isCurrent = isCurrent
self.presentationData = presentationData
self.action = action
self.separatorNode = ASDisplayNode()
self.separatorNode.backgroundColor = presentationData.theme.actionSheet.opaqueItemSeparatorColor
self.separatorNode.isHidden = !displaySeparator
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.backgroundColor = presentationData.theme.actionSheet.opaqueItemHighlightedBackgroundColor
self.highlightedBackgroundNode.alpha = 0.0
self.buttonNode = HighlightTrackingButtonNode()
self.titleNode = ImmediateTextNode()
self.titleNode.maximumNumberOfLines = 1
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.regular(17.0), textColor: presentationData.theme.actionSheet.primaryTextColor)
self.checkNode = ASImageNode()
self.checkNode.image = generateItemListCheckIcon(color: presentationData.theme.actionSheet.primaryTextColor)
self.checkNode.isHidden = !isCurrent
self.badgeBackgroundNode = ASImageNode()
self.badgeBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 20.0, color: presentationData.theme.list.itemCheckColors.fillColor)
self.badgeTitleNode = ImmediateTextNode()
self.badgeBackgroundNode.isHidden = true
self.badgeTitleNode.isHidden = true
super.init()
self.addSubnode(self.separatorNode)
self.addSubnode(self.highlightedBackgroundNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.checkNode)
self.addSubnode(self.badgeBackgroundNode)
self.addSubnode(self.badgeTitleNode)
self.addSubnode(self.buttonNode)
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.buttonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.highlightedBackgroundNode.layer.removeAnimation(forKey: "opacity")
strongSelf.highlightedBackgroundNode.alpha = 1.0
} else {
strongSelf.highlightedBackgroundNode.alpha = 0.0
strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
}
}
}
}
func updateLayout(maxWidth: CGFloat) -> (CGFloat, CGFloat, (CGFloat) -> Void) {
let leftInset: CGFloat = 16.0
let badgeTitleSize = self.badgeTitleNode.updateLayout(CGSize(width: 100.0, height: .greatestFiniteMagnitude))
let badgeMinSize = self.badgeBackgroundNode.image?.size.width ?? 20.0
let badgeSize = CGSize(width: max(badgeMinSize, badgeTitleSize.width + 12.0), height: badgeMinSize)
let rightInset: CGFloat = max(60.0, badgeSize.width + 40.0)
let titleSize = self.titleNode.updateLayout(CGSize(width: maxWidth - leftInset - rightInset, height: .greatestFiniteMagnitude))
let height: CGFloat = 61.0
return (titleSize.width + leftInset + rightInset, height, { width in
self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize)
if let image = self.checkNode.image {
self.checkNode.frame = CGRect(origin: CGPoint(x: width - rightInset + floor((rightInset - image.size.width) / 2.0), y: floor((height - image.size.height) / 2.0)), size: image.size)
}
let badgeBackgroundFrame = CGRect(origin: CGPoint(x: width - rightInset + floor((rightInset - badgeSize.width) / 2.0), y: floor((height - badgeSize.height) / 2.0)), size: badgeSize)
self.badgeBackgroundNode.frame = badgeBackgroundFrame
self.badgeTitleNode.frame = CGRect(origin: CGPoint(x: badgeBackgroundFrame.minX + floor((badgeBackgroundFrame.width - badgeTitleSize.width) / 2.0), y: badgeBackgroundFrame.minY + floor((badgeBackgroundFrame.height - badgeTitleSize.height) / 2.0)), size: badgeTitleSize)
self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: height - UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel))
self.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: height))
self.buttonNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: height))
})
}
@objc private func buttonPressed() {
let isCurrent = self.action()
self.checkNode.isHidden = !isCurrent
}
}
private final class TabBarChatListFilterControllerNode: ViewControllerTracingNode {
private let presentationData: PresentationData
private let cancel: () -> Void
private let effectView: UIVisualEffectView
private var propertyAnimator: AnyObject?
private var displayLinkAnimator: DisplayLinkAnimator?
private let dimNode: ASDisplayNode
private let contentContainerNode: ASDisplayNode
private let contentNodes: [ASDisplayNode & AbstractTabBarChatListFilterItemNode]
private var sourceNodes: [ASDisplayNode]
private var snapshotViews: [UIView] = []
private var validLayout: ContainerViewLayout?
init(context: AccountContext, presentationData: PresentationData, cancel: @escaping () -> Void, sourceNodes: [ASDisplayNode], currentFilter: ChatListNodeFilter, updateFilter: @escaping (ChatListNodeFilter) -> Void) {
self.presentationData = presentationData
self.cancel = cancel
self.sourceNodes = sourceNodes
self.effectView = UIVisualEffectView()
if #available(iOS 9.0, *) {
} else {
if presentationData.theme.rootController.keyboardColor == .dark {
self.effectView.effect = UIBlurEffect(style: .dark)
} else {
self.effectView.effect = UIBlurEffect(style: .light)
}
self.effectView.alpha = 0.0
}
self.dimNode = ASDisplayNode()
self.dimNode.alpha = 1.0
if presentationData.theme.rootController.keyboardColor == .light {
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.04)
} else {
self.dimNode.backgroundColor = presentationData.theme.chatList.backgroundColor.withAlphaComponent(0.2)
}
self.contentContainerNode = ASDisplayNode()
self.contentContainerNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemBackgroundColor
self.contentContainerNode.cornerRadius = 20.0
self.contentContainerNode.clipsToBounds = true
var contentNodes: [ASDisplayNode & AbstractTabBarChatListFilterItemNode] = []
let labels: [(String, ChatListNodeFilter)] = [
("Private Chats", .privateChats),
("Groups", .groups),
("Bots", .bots),
("Channels", .channels),
("Muted", .muted)
]
var updatedFilter = currentFilter
let toggleFilter: (ChatListNodeFilter) -> Void = { filter in
if updatedFilter.contains(filter) {
updatedFilter.remove(filter)
} else {
updatedFilter.insert(filter)
}
updateFilter(updatedFilter)
}
for i in 0 ..< labels.count {
let filter = labels[i].1
contentNodes.append(FilterItemNode(context: context, title: labels[i].0, isCurrent: updatedFilter.contains(filter), displaySeparator: i != labels.count - 1, presentationData: presentationData, action: {
toggleFilter(filter)
return updatedFilter.contains(filter)
}))
}
self.contentNodes = contentNodes
super.init()
self.view.addSubview(self.effectView)
self.addSubnode(self.dimNode)
self.addSubnode(self.contentContainerNode)
self.contentNodes.forEach(self.contentContainerNode.addSubnode)
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
}
deinit {
if let propertyAnimator = self.propertyAnimator {
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
let propertyAnimator = propertyAnimator as? UIViewPropertyAnimator
propertyAnimator?.stopAnimation(true)
}
}
}
func animateIn() {
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
if #available(iOS 10.0, *) {
if let propertyAnimator = self.propertyAnimator {
let propertyAnimator = propertyAnimator as? UIViewPropertyAnimator
propertyAnimator?.stopAnimation(true)
}
self.propertyAnimator = UIViewPropertyAnimator(duration: 0.2 * animationDurationFactor, curve: .easeInOut, animations: { [weak self] in
self?.effectView.effect = makeCustomZoomBlurEffect()
})
}
if let _ = self.propertyAnimator {
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
self.displayLinkAnimator = DisplayLinkAnimator(duration: 0.2 * animationDurationFactor, from: 0.0, to: 1.0, update: { [weak self] value in
(self?.propertyAnimator as? UIViewPropertyAnimator)?.fractionComplete = value
}, completion: {
})
}
} else {
UIView.animate(withDuration: 0.2 * animationDurationFactor, animations: {
self.effectView.effect = makeCustomZoomBlurEffect()
}, completion: { _ in
})
}
self.contentContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
if let _ = self.validLayout, let sourceNode = self.sourceNodes.first {
let sourceFrame = sourceNode.view.convert(sourceNode.bounds, to: self.view)
self.contentContainerNode.layer.animateFrame(from: sourceFrame, to: self.contentContainerNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
}
for sourceNode in self.sourceNodes {
if let imageNode = sourceNode as? ASImageNode {
let snapshot = UIImageView()
snapshot.image = imageNode.image
snapshot.frame = sourceNode.view.convert(sourceNode.bounds, to: self.view)
snapshot.isUserInteractionEnabled = false
self.view.addSubview(snapshot)
self.snapshotViews.append(snapshot)
} else if let snapshot = sourceNode.view.snapshotContentTree() {
snapshot.frame = sourceNode.view.convert(sourceNode.bounds, to: self.view)
snapshot.isUserInteractionEnabled = false
self.view.addSubview(snapshot)
self.snapshotViews.append(snapshot)
}
sourceNode.alpha = 0.0
}
}
func animateOut(sourceNodes: [ASDisplayNode], completion: @escaping () -> Void) {
self.isUserInteractionEnabled = false
var completedEffect = false
var completedSourceNodes = false
let intermediateCompletion: () -> Void = {
if completedEffect && completedSourceNodes {
completion()
}
}
if #available(iOS 10.0, *) {
if let propertyAnimator = self.propertyAnimator {
let propertyAnimator = propertyAnimator as? UIViewPropertyAnimator
propertyAnimator?.stopAnimation(true)
}
self.propertyAnimator = UIViewPropertyAnimator(duration: 0.2, curve: .easeInOut, animations: { [weak self] in
self?.effectView.effect = nil
})
}
if let _ = self.propertyAnimator {
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
self.displayLinkAnimator = DisplayLinkAnimator(duration: 0.2 * animationDurationFactor, from: 0.0, to: 0.999, update: { [weak self] value in
(self?.propertyAnimator as? UIViewPropertyAnimator)?.fractionComplete = value
}, completion: { [weak self] in
if let strongSelf = self {
for sourceNode in strongSelf.sourceNodes {
sourceNode.alpha = 1.0
}
}
completedEffect = true
intermediateCompletion()
})
}
self.effectView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.05 * animationDurationFactor, delay: 0.15, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false)
} else {
UIView.animate(withDuration: 0.21 * animationDurationFactor, animations: {
if #available(iOS 9.0, *) {
self.effectView.effect = nil
} else {
self.effectView.alpha = 0.0
}
}, completion: { [weak self] _ in
if let strongSelf = self {
for sourceNode in strongSelf.sourceNodes {
sourceNode.alpha = 1.0
}
}
completedEffect = true
intermediateCompletion()
})
}
self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
self.contentContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { _ in
})
if let _ = self.validLayout, let sourceNode = self.sourceNodes.first {
let sourceFrame = sourceNode.view.convert(sourceNode.bounds, to: self.view)
self.contentContainerNode.layer.animateFrame(from: self.contentContainerNode.frame, to: sourceFrame, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue, removeOnCompletion: false)
}
completedSourceNodes = true
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
self.validLayout = layout
transition.updateFrame(view: self.effectView, frame: CGRect(origin: CGPoint(), size: layout.size))
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
let sideInset: CGFloat = 18.0
var contentSize = CGSize()
contentSize.width = min(layout.size.width - 40.0, 250.0)
var applyNodes: [(ASDisplayNode, CGFloat, (CGFloat) -> Void)] = []
for itemNode in self.contentNodes {
let (width, height, apply) = itemNode.updateLayout(maxWidth: layout.size.width - sideInset * 2.0)
applyNodes.append((itemNode, height, apply))
contentSize.width = max(contentSize.width, width)
contentSize.height += height
}
let insets = layout.insets(options: .input)
let contentOrigin: CGPoint
if let sourceNode = self.sourceNodes.first, let screenFrame = sourceNode.supernode?.convert(sourceNode.frame, to: nil) {
contentOrigin = CGPoint(x: screenFrame.maxX - contentSize.width + 8.0, y: layout.size.height - 66.0 - insets.bottom - contentSize.height)
} else {
contentOrigin = CGPoint(x: layout.size.width - sideInset - contentSize.width, y: layout.size.height - 66.0 - layout.intrinsicInsets.bottom - contentSize.height)
}
transition.updateFrame(node: self.contentContainerNode, frame: CGRect(origin: contentOrigin, size: contentSize))
var nextY: CGFloat = 0.0
for (itemNode, height, apply) in applyNodes {
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: nextY), size: CGSize(width: contentSize.width, height: height)))
apply(contentSize.width)
nextY += height
}
}
@objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.cancel()
}
}
}
private func setAnchorPoint(anchorPoint: CGPoint, forView view: UIView) {
var newPoint = CGPoint(x: view.bounds.size.width * anchorPoint.x,
y: view.bounds.size.height * anchorPoint.y)
var oldPoint = CGPoint(x: view.bounds.size.width * view.layer.anchorPoint.x,
y: view.bounds.size.height * view.layer.anchorPoint.y)
newPoint = newPoint.applying(view.transform)
oldPoint = oldPoint.applying(view.transform)
var position = view.layer.position
position.x -= oldPoint.x
position.x += newPoint.x
position.y -= oldPoint.y
position.y += newPoint.y
view.layer.position = position
view.layer.anchorPoint = anchorPoint
}

View File

@ -381,7 +381,7 @@ final class ChatListTable: Table {
self.valueBox.remove(self.table, key: self.key(groupId: groupId, index: ChatListIndex(pinningIndex: nil, messageIndex: index), type: .hole), secure: false)
}
func entriesAround(groupId: PeerGroupId, index: ChatListIndex, messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable, count: Int) -> (entries: [ChatListIntermediateEntry], lower: ChatListIntermediateEntry?, upper: ChatListIntermediateEntry?) {
func entriesAround(groupId: PeerGroupId, index: ChatListIndex, messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable, count: Int, predicate: ((ChatListIntermediateEntry) -> Bool)?) -> (entries: [ChatListIntermediateEntry], lower: ChatListIntermediateEntry?, upper: ChatListIntermediateEntry?) {
self.ensureInitialized(groupId: groupId)
var lowerEntries: [ChatListIntermediateEntry] = []
@ -389,18 +389,38 @@ final class ChatListTable: Table {
var lower: ChatListIntermediateEntry?
var upper: ChatListIntermediateEntry?
self.valueBox.range(self.table, start: self.key(groupId: groupId, index: index, type: .message), end: self.lowerBound(groupId: groupId), values: { key, value in
lowerEntries.append(readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value))
return true
self.valueBox.filteredRange(self.table, start: self.key(groupId: groupId, index: index, type: .message), end: self.lowerBound(groupId: groupId), values: { key, value in
let entry = readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value)
if let predicate = predicate {
if predicate(entry) {
lowerEntries.append(entry)
return .accept
} else {
return .skip
}
} else {
lowerEntries.append(entry)
return .accept
}
}, limit: count / 2 + 1)
if lowerEntries.count >= count / 2 + 1 {
lower = lowerEntries.last
lowerEntries.removeLast()
}
self.valueBox.range(self.table, start: self.key(groupId: groupId, index: index, type: .message).predecessor, end: self.upperBound(groupId: groupId), values: { key, value in
upperEntries.append(readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value))
return true
self.valueBox.filteredRange(self.table, start: self.key(groupId: groupId, index: index, type: .message).predecessor, end: self.upperBound(groupId: groupId), values: { key, value in
let entry = readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value)
if let predicate = predicate {
if predicate(entry) {
upperEntries.append(entry)
return .accept
} else {
return .skip
}
} else {
upperEntries.append(entry)
return .accept
}
}, limit: count - lowerEntries.count + 1)
if upperEntries.count >= count - lowerEntries.count + 1 {
upper = upperEntries.last
@ -415,12 +435,20 @@ final class ChatListTable: Table {
startEntryType = .message
case .hole:
startEntryType = .hole
/*case .groupReference:
startEntryType = .groupReference*/
}
self.valueBox.range(self.table, start: self.key(groupId: groupId, index: lowerEntries.last!.index, type: startEntryType), end: self.lowerBound(groupId: groupId), values: { key, value in
additionalLowerEntries.append(readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value))
return true
self.valueBox.filteredRange(self.table, start: self.key(groupId: groupId, index: lowerEntries.last!.index, type: startEntryType), end: self.lowerBound(groupId: groupId), values: { key, value in
let entry = readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value)
if let predicate = predicate {
if predicate(entry) {
additionalLowerEntries.append(entry)
return .accept
} else {
return .skip
}
} else {
additionalLowerEntries.append(entry)
return .accept
}
}, limit: count - lowerEntries.count - upperEntries.count + 1)
if additionalLowerEntries.count >= count - lowerEntries.count + upperEntries.count + 1 {
lower = additionalLowerEntries.last
@ -502,7 +530,7 @@ final class ChatListTable: Table {
return result
}
func earlierEntries(groupId: PeerGroupId, index: (ChatListIndex, Bool)?, messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable, count: Int) -> [ChatListIntermediateEntry] {
func earlierEntries(groupId: PeerGroupId, index: (ChatListIndex, Bool)?, messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable, count: Int, predicate: ((ChatListIntermediateEntry) -> Bool)?) -> [ChatListIntermediateEntry] {
self.ensureInitialized(groupId: groupId)
var entries: [ChatListIntermediateEntry] = []
@ -513,9 +541,19 @@ final class ChatListTable: Table {
key = self.upperBound(groupId: groupId)
}
self.valueBox.range(self.table, start: key, end: self.lowerBound(groupId: groupId), values: { key, value in
entries.append(readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value))
return true
self.valueBox.filteredRange(self.table, start: key, end: self.lowerBound(groupId: groupId), values: { key, value in
let entry = readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value)
if let predicate = predicate {
if predicate(entry) {
entries.append(entry)
return .accept
} else {
return .skip
}
} else {
entries.append(entry)
return .accept
}
}, limit: count)
return entries
}
@ -556,7 +594,7 @@ final class ChatListTable: Table {
return entries
}
func laterEntries(groupId: PeerGroupId, index: (ChatListIndex, Bool)?, messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable, count: Int) -> [ChatListIntermediateEntry] {
func laterEntries(groupId: PeerGroupId, index: (ChatListIndex, Bool)?, messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable, count: Int, predicate: ((ChatListIntermediateEntry) -> Bool)?) -> [ChatListIntermediateEntry] {
self.ensureInitialized(groupId: groupId)
var entries: [ChatListIntermediateEntry] = []
@ -567,9 +605,19 @@ final class ChatListTable: Table {
key = self.lowerBound(groupId: groupId)
}
self.valueBox.range(self.table, start: key, end: self.upperBound(groupId: groupId), values: { key, value in
entries.append(readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value))
return true
self.valueBox.filteredRange(self.table, start: key, end: self.upperBound(groupId: groupId), values: { key, value in
let entry = readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value)
if let predicate = predicate {
if predicate(entry) {
entries.append(entry)
return .accept
} else {
return .skip
}
} else {
entries.append(entry)
return .accept
}
}, limit: count)
return entries
}
@ -590,6 +638,19 @@ final class ChatListTable: Table {
return nil
}
func getEntry(groupId: PeerGroupId, peerId: PeerId, messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable) -> ChatListIntermediateEntry? {
if let (peerGroupId, index) = self.getPeerChatListIndex(peerId: peerId), peerGroupId == groupId {
let key = self.key(groupId: groupId, index: index, type: .message)
if let value = self.valueBox.get(self.table, key: key) {
return readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value)
} else {
return nil
}
} else {
return nil
}
}
func allEntries(groupId: PeerGroupId) -> [ChatListEntryInfo] {
var entries: [ChatListEntryInfo] = []
self.valueBox.range(self.table, start: self.upperBound(groupId: groupId), end: self.lowerBound(groupId: groupId), values: { key, value in
@ -710,7 +771,7 @@ final class ChatListTable: Table {
}
func debugList(groupId: PeerGroupId, messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable) -> [ChatListIntermediateEntry] {
return self.laterEntries(groupId: groupId, index: (ChatListIndex.absoluteLowerBound, true), messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, count: 1000)
return self.laterEntries(groupId: groupId, index: (ChatListIndex.absoluteLowerBound, true), messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, count: 1000, predicate: nil)
}
func getNamespaceEntries(groupId: PeerGroupId, namespace: MessageId.Namespace, summaryTag: MessageTags?, messageIndexTable: MessageHistoryIndexTable, messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable, readStateTable: MessageHistoryReadStateTable, summaryTable: MessageHistoryTagsSummaryTable) -> [ChatListNamespaceEntry] {

View File

@ -297,6 +297,7 @@ private func updatedRenderedPeer(_ renderedPeer: RenderedPeer, updatedPeers: [Pe
final class MutableChatListView {
let groupId: PeerGroupId
let filterPredicate: ((Peer, PeerNotificationSettings?) -> Bool)?
private let summaryComponents: ChatListEntrySummaryComponents
fileprivate var additionalItemIds: Set<PeerId>
fileprivate var additionalItemEntries: [MutableChatListEntry]
@ -306,8 +307,11 @@ final class MutableChatListView {
fileprivate var groupEntries: [ChatListGroupReferenceEntry]
private var count: Int
init(postbox: Postbox, groupId: PeerGroupId, earlier: MutableChatListEntry?, entries: [MutableChatListEntry], later: MutableChatListEntry?, count: Int, summaryComponents: ChatListEntrySummaryComponents) {
init(postbox: Postbox, groupId: PeerGroupId, filterPredicate: ((Peer, PeerNotificationSettings?) -> Bool)?, aroundIndex: ChatListIndex, count: Int, summaryComponents: ChatListEntrySummaryComponents) {
let (entries, earlier, later) = postbox.fetchAroundChatEntries(groupId: groupId, index: aroundIndex, count: count, filterPredicate: filterPredicate)
self.groupId = groupId
self.filterPredicate = filterPredicate
self.earlier = earlier
self.entries = entries
self.later = later
@ -405,7 +409,7 @@ final class MutableChatListView {
index = self.entries[self.entries.count / 2].index
}
let (entries, earlier, later) = postbox.fetchAroundChatEntries(groupId: self.groupId, index: index, count: self.count)
let (entries, earlier, later) = postbox.fetchAroundChatEntries(groupId: self.groupId, index: index, count: self.count, filterPredicate: self.filterPredicate)
let currentGroupEntries = self.groupEntries
self.reloadGroups(postbox: postbox)
@ -426,7 +430,7 @@ final class MutableChatListView {
return updated
}
func replay(postbox: Postbox, operations: [PeerGroupId: [ChatListOperation]], updatedPeerNotificationSettings: [PeerId: PeerNotificationSettings], updatedPeers: [PeerId: Peer], updatedPeerPresences: [PeerId: PeerPresence], transaction: PostboxTransaction, context: MutableChatListViewReplayContext) -> Bool {
func replay(postbox: Postbox, operations: [PeerGroupId: [ChatListOperation]], updatedPeerNotificationSettings: [PeerId: (PeerNotificationSettings?, PeerNotificationSettings)], updatedPeers: [PeerId: Peer], updatedPeerPresences: [PeerId: PeerPresence], transaction: PostboxTransaction, context: MutableChatListViewReplayContext) -> Bool {
var hasChanges = false
if let groupOperations = operations[self.groupId] {
@ -489,6 +493,43 @@ final class MutableChatListView {
}
if !updatedPeerNotificationSettings.isEmpty {
if let filterPredicate = self.filterPredicate {
for (peerId, settingsChange) in updatedPeerNotificationSettings {
if let peer = postbox.peerTable.get(peerId) {
let wasIncluded = filterPredicate(peer, settingsChange.0)
let isIncluded = filterPredicate(peer, settingsChange.1)
if wasIncluded != isIncluded {
if isIncluded {
if let entry = postbox.chatListTable.getEntry(groupId: self.groupId, peerId: peerId, messageHistoryTable: postbox.messageHistoryTable, peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable) {
switch entry {
case let .message(index, message, embeddedState):
let combinedReadState = postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId)
if self.add(.IntermediateMessageEntry(entry.index, message, combinedReadState, embeddedState), postbox: postbox) {
hasChanges = true
}
default:
break
}
}
} else {
loop: for i in 0 ..< self.entries.count {
switch self.entries[i] {
case .MessageEntry(let index, _, _, _, _, _, _, _, _), .IntermediateMessageEntry(let index, _, _, _):
if index.messageIndex.id.peerId == peerId {
self.entries.remove(at: i)
hasChanges = true
break loop
}
default:
break
}
}
}
}
}
}
}
for i in 0 ..< self.entries.count {
switch self.entries[i] {
case let .MessageEntry(index, message, readState, _, embeddedState, peer, peerPresence, summaryInfo, hasFailed):
@ -496,7 +537,7 @@ final class MutableChatListView {
if let peer = peer.peers[peer.peerId], let associatedPeerId = peer.associatedPeerId {
notificationSettingsPeerId = associatedPeerId
}
if let settings = updatedPeerNotificationSettings[notificationSettingsPeerId] {
if let (_, settings) = updatedPeerNotificationSettings[notificationSettingsPeerId] {
self.entries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, hasFailed)
hasChanges = true
}
@ -619,7 +660,24 @@ final class MutableChatListView {
}
func add(_ initialEntry: MutableChatListEntry, postbox: Postbox) -> Bool {
if let filterPredicate = self.filterPredicate {
switch initialEntry {
case .IntermediateMessageEntry(let index, _, _, _), .MessageEntry(let index, _, _, _, _, _, _, _, _):
if let peer = postbox.peerTable.get(index.messageIndex.id.peerId) {
if !filterPredicate(peer, postbox.peerNotificationSettingsTable.getEffective(index.messageIndex.id.peerId)) {
return false
}
} else {
return false
}
break
default:
break
}
}
let entry = processedChatListEntry(initialEntry, cachedDataTable: postbox.cachedPeerDataTable, readStateTable: postbox.readStateTable, messageHistoryTable: postbox.messageHistoryTable)
if self.entries.count == 0 {
self.entries.append(entry)
return true
@ -756,10 +814,10 @@ final class MutableChatListView {
}
if let later = self.later {
addedEntries += postbox.fetchLaterChatEntries(groupId: self.groupId, index: later.index.predecessor, count: self.count)
addedEntries += postbox.fetchLaterChatEntries(groupId: self.groupId, index: later.index.predecessor, count: self.count, filterPredicate: filterPredicate)
}
if let earlier = self.earlier {
addedEntries += postbox.fetchEarlierChatEntries(groupId: self.groupId, index: earlier.index.successor, count: self.count)
addedEntries += postbox.fetchEarlierChatEntries(groupId: self.groupId, index: earlier.index.successor, count: self.count, filterPredicate: filterPredicate)
}
addedEntries += self.entries
@ -808,7 +866,7 @@ final class MutableChatListView {
earlyId = self.entries[i].index
}
let earlierEntries = postbox.fetchEarlierChatEntries(groupId: self.groupId, index: earlyId, count: 1)
let earlierEntries = postbox.fetchEarlierChatEntries(groupId: self.groupId, index: earlyId, count: 1, filterPredicate: self.filterPredicate)
self.earlier = earlierEntries.first
}
@ -819,7 +877,7 @@ final class MutableChatListView {
laterId = self.entries[i].index
}
let laterEntries = postbox.fetchLaterChatEntries(groupId: self.groupId, index: laterId, count: 1)
let laterEntries = postbox.fetchLaterChatEntries(groupId: self.groupId, index: laterId, count: 1, filterPredicate: self.filterPredicate)
self.later = laterEntries.first
}
}

View File

@ -225,7 +225,7 @@ final class PeerNotificationSettingsTable: Table {
return (added, removed)
}
func resetAll(to settings: PeerNotificationSettings, updatedSettings: inout Set<PeerId>, updatedTimestamps: inout [PeerId: PeerNotificationSettingsBehaviorTimestamp]) -> [PeerId] {
func resetAll(to settings: PeerNotificationSettings, updatedSettings: inout Set<PeerId>, updatedTimestamps: inout [PeerId: PeerNotificationSettingsBehaviorTimestamp]) -> [PeerId: PeerNotificationSettings?] {
let lowerBound = ValueBoxKey(length: 8)
lowerBound.setInt64(0, value: 0)
let upperBound = ValueBoxKey(length: 8)
@ -236,17 +236,17 @@ final class PeerNotificationSettingsTable: Table {
return true
}, limit: 0)
var updatedPeerIds: [PeerId] = []
var updatedPeers: [PeerId: PeerNotificationSettings?] = [:]
for peerId in peerIds {
let entry = self.getEntry(peerId)
if let current = entry.current, !current.isEqual(to: settings) || entry.pending != nil {
let _ = self.setCurrent(id: peerId, settings: settings, updatedTimestamps: &updatedTimestamps)
let _ = self.setPending(id: peerId, settings: nil, updatedSettings: &updatedSettings)
updatedPeerIds.append(peerId)
updatedPeers[peerId] = entry.effective
}
}
return updatedPeerIds
return updatedPeers
}
override func beforeCommit() {

View File

@ -26,7 +26,7 @@ final class MutablePeerNotificationSettingsView: MutablePostboxView {
if let peer = postbox.peerTable.get(peerId), let associatedPeerId = peer.associatedPeerId {
notificationPeerId = associatedPeerId
}
if let settings = transaction.currentUpdatedPeerNotificationSettings[notificationPeerId] {
if let (_, settings) = transaction.currentUpdatedPeerNotificationSettings[notificationPeerId] {
self.notificationSettings[peerId] = settings
updated = true
}

View File

@ -214,12 +214,12 @@ final class MutablePeerView: MutablePostboxView {
if let peer = self.peers[self.peerId] {
if let associatedPeerId = peer.associatedPeerId {
if let notificationSettings = updatedNotificationSettings[associatedPeerId] {
if let (_, notificationSettings) = updatedNotificationSettings[associatedPeerId] {
self.notificationSettings = notificationSettings
updated = true
}
} else {
if let notificationSettings = updatedNotificationSettings[peer.id] {
if let (_, notificationSettings) = updatedNotificationSettings[peer.id] {
self.notificationSettings = notificationSettings
updated = true
}

View File

@ -1077,7 +1077,7 @@ public final class Postbox {
private var currentPeerHoleOperations: [MessageHistoryIndexHoleOperationKey: [MessageHistoryIndexHoleOperation]] = [:]
private var currentUpdatedPeers: [PeerId: Peer] = [:]
private var currentUpdatedPeerNotificationSettings: [PeerId: PeerNotificationSettings] = [:]
private var currentUpdatedPeerNotificationSettings: [PeerId: (PeerNotificationSettings?, PeerNotificationSettings)] = [:]
private var currentUpdatedPeerNotificationBehaviorTimestamps: [PeerId: PeerNotificationSettingsBehaviorTimestamp] = [:]
private var currentUpdatedCachedPeerData: [PeerId: CachedPeerData] = [:]
private var currentUpdatedPeerPresences: [PeerId: PeerPresence] = [:]
@ -1650,8 +1650,28 @@ public final class Postbox {
self.synchronizeGroupMessageStatsTable.set(groupId: groupId, namespace: namespace, needsValidation: false, operations: &self.currentUpdatedGroupSummarySynchronizeOperations)
}
func fetchAroundChatEntries(groupId: PeerGroupId, index: ChatListIndex, count: Int) -> (entries: [MutableChatListEntry], earlier: MutableChatListEntry?, later: MutableChatListEntry?) {
let (intermediateEntries, intermediateLower, intermediateUpper) = self.chatListTable.entriesAround(groupId: groupId, index: index, messageHistoryTable: self.messageHistoryTable, peerChatInterfaceStateTable: self.peerChatInterfaceStateTable, count: count)
private func mappedChatListFilterPredicate(_ predicate: @escaping (Peer, PeerNotificationSettings?) -> Bool) -> (ChatListIntermediateEntry) -> Bool {
return { entry in
switch entry {
case let .message(index, _, _):
if let peer = self.peerTable.get(index.messageIndex.id.peerId) {
if predicate(peer, self.peerNotificationSettingsTable.getEffective(index.messageIndex.id.peerId)) {
return true
} else {
return false
}
} else {
return false
}
case .hole:
return true
}
}
}
func fetchAroundChatEntries(groupId: PeerGroupId, index: ChatListIndex, count: Int, filterPredicate: ((Peer, PeerNotificationSettings?) -> Bool)?) -> (entries: [MutableChatListEntry], earlier: MutableChatListEntry?, later: MutableChatListEntry?) {
let mappedPredicate = filterPredicate.flatMap(self.mappedChatListFilterPredicate)
let (intermediateEntries, intermediateLower, intermediateUpper) = self.chatListTable.entriesAround(groupId: groupId, index: index, messageHistoryTable: self.messageHistoryTable, peerChatInterfaceStateTable: self.peerChatInterfaceStateTable, count: count, predicate: mappedPredicate)
let entries: [MutableChatListEntry] = intermediateEntries.map { entry in
return MutableChatListEntry(entry, cachedDataTable: self.cachedPeerDataTable, readStateTable: self.readStateTable, messageHistoryTable: self.messageHistoryTable)
}
@ -1665,16 +1685,18 @@ public final class Postbox {
return (entries, lower, upper)
}
func fetchEarlierChatEntries(groupId: PeerGroupId, index: ChatListIndex?, count: Int) -> [MutableChatListEntry] {
let intermediateEntries = self.chatListTable.earlierEntries(groupId: groupId, index: index.flatMap({ ($0, true) }), messageHistoryTable: self.messageHistoryTable, peerChatInterfaceStateTable: self.peerChatInterfaceStateTable, count: count)
func fetchEarlierChatEntries(groupId: PeerGroupId, index: ChatListIndex?, count: Int, filterPredicate: ((Peer, PeerNotificationSettings?) -> Bool)?) -> [MutableChatListEntry] {
let mappedPredicate = filterPredicate.flatMap(self.mappedChatListFilterPredicate)
let intermediateEntries = self.chatListTable.earlierEntries(groupId: groupId, index: index.flatMap({ ($0, true) }), messageHistoryTable: self.messageHistoryTable, peerChatInterfaceStateTable: self.peerChatInterfaceStateTable, count: count, predicate: mappedPredicate)
let entries: [MutableChatListEntry] = intermediateEntries.map { entry in
return MutableChatListEntry(entry, cachedDataTable: self.cachedPeerDataTable, readStateTable: self.readStateTable, messageHistoryTable: self.messageHistoryTable)
}
return entries
}
func fetchLaterChatEntries(groupId: PeerGroupId, index: ChatListIndex?, count: Int) -> [MutableChatListEntry] {
let intermediateEntries = self.chatListTable.laterEntries(groupId: groupId, index: index.flatMap({ ($0, true) }), messageHistoryTable: self.messageHistoryTable, peerChatInterfaceStateTable: self.peerChatInterfaceStateTable, count: count)
func fetchLaterChatEntries(groupId: PeerGroupId, index: ChatListIndex?, count: Int, filterPredicate: ((Peer, PeerNotificationSettings?) -> Bool)?) -> [MutableChatListEntry] {
let mappedPredicate = filterPredicate.flatMap(self.mappedChatListFilterPredicate)
let intermediateEntries = self.chatListTable.laterEntries(groupId: groupId, index: index.flatMap({ ($0, true) }), messageHistoryTable: self.messageHistoryTable, peerChatInterfaceStateTable: self.peerChatInterfaceStateTable, count: count, predicate: mappedPredicate)
let entries: [MutableChatListEntry] = intermediateEntries.map { entry in
return MutableChatListEntry(entry, cachedDataTable: self.cachedPeerDataTable, readStateTable: self.readStateTable, messageHistoryTable: self.messageHistoryTable)
}
@ -1891,21 +1913,33 @@ public final class Postbox {
fileprivate func updateCurrentPeerNotificationSettings(_ notificationSettings: [PeerId: PeerNotificationSettings]) {
for (peerId, settings) in notificationSettings {
let previous: PeerNotificationSettings?
if let (value, _) = self.currentUpdatedPeerNotificationSettings[peerId] {
previous = value
} else {
previous = self.peerNotificationSettingsTable.getEffective(peerId)
}
if let updated = self.peerNotificationSettingsTable.setCurrent(id: peerId, settings: settings, updatedTimestamps: &self.currentUpdatedPeerNotificationBehaviorTimestamps) {
self.currentUpdatedPeerNotificationSettings[peerId] = updated
self.currentUpdatedPeerNotificationSettings[peerId] = (previous, updated)
}
}
}
fileprivate func updatePendingPeerNotificationSettings(peerId: PeerId, settings: PeerNotificationSettings?) {
let previous: PeerNotificationSettings?
if let (value, _) = self.currentUpdatedPeerNotificationSettings[peerId] {
previous = value
} else {
previous = self.peerNotificationSettingsTable.getEffective(peerId)
}
if let updated = self.peerNotificationSettingsTable.setPending(id: peerId, settings: settings, updatedSettings: &self.currentUpdatedPendingPeerNotificationSettings) {
self.currentUpdatedPeerNotificationSettings[peerId] = updated
self.currentUpdatedPeerNotificationSettings[peerId] = (previous, updated)
}
}
fileprivate func resetAllPeerNotificationSettings(_ notificationSettings: PeerNotificationSettings) {
for peerId in self.peerNotificationSettingsTable.resetAll(to: notificationSettings, updatedSettings: &self.currentUpdatedPendingPeerNotificationSettings, updatedTimestamps: &self.currentUpdatedPeerNotificationBehaviorTimestamps) {
self.currentUpdatedPeerNotificationSettings[peerId] = notificationSettings
for (peerId, previous) in self.peerNotificationSettingsTable.resetAll(to: notificationSettings, updatedSettings: &self.currentUpdatedPendingPeerNotificationSettings, updatedTimestamps: &self.currentUpdatedPeerNotificationBehaviorTimestamps) {
self.currentUpdatedPeerNotificationSettings[peerId] = (previous, notificationSettings)
}
}
@ -2508,15 +2542,13 @@ public final class Postbox {
|> switchToLatest
}
public func tailChatListView(groupId: PeerGroupId, count: Int, summaryComponents: ChatListEntrySummaryComponents) -> Signal<(ChatListView, ViewUpdateType), NoError> {
return self.aroundChatListView(groupId: groupId, index: ChatListIndex.absoluteUpperBound, count: count, summaryComponents: summaryComponents, userInteractive: true)
public func tailChatListView(groupId: PeerGroupId, filterPredicate: ((Peer, PeerNotificationSettings?) -> Bool)? = nil, count: Int, summaryComponents: ChatListEntrySummaryComponents) -> Signal<(ChatListView, ViewUpdateType), NoError> {
return self.aroundChatListView(groupId: groupId, filterPredicate: filterPredicate, index: ChatListIndex.absoluteUpperBound, count: count, summaryComponents: summaryComponents, userInteractive: true)
}
public func aroundChatListView(groupId: PeerGroupId, index: ChatListIndex, count: Int, summaryComponents: ChatListEntrySummaryComponents, userInteractive: Bool = false) -> Signal<(ChatListView, ViewUpdateType), NoError> {
public func aroundChatListView(groupId: PeerGroupId, filterPredicate: ((Peer, PeerNotificationSettings?) -> Bool)? = nil, index: ChatListIndex, count: Int, summaryComponents: ChatListEntrySummaryComponents, userInteractive: Bool = false) -> Signal<(ChatListView, ViewUpdateType), NoError> {
return self.transactionSignal(userInteractive: userInteractive, { subscriber, transaction in
let (entries, earlier, later) = self.fetchAroundChatEntries(groupId: groupId, index: index, count: count)
let mutableView = MutableChatListView(postbox: self, groupId: groupId, earlier: earlier, entries: entries, later: later, count: count, summaryComponents: summaryComponents)
let mutableView = MutableChatListView(postbox: self, groupId: groupId, filterPredicate: filterPredicate, aroundIndex: index, count: count, summaryComponents: summaryComponents)
mutableView.render(postbox: self, renderMessage: self.renderIntermediateMessage, getPeer: { id in
return self.peerTable.get(id)
}, getPeerNotificationSettings: { self.peerNotificationSettingsTable.getEffective($0) }, getPeerPresence: { self.peerPresenceTable.get($0) })

View File

@ -7,7 +7,7 @@ final class PostboxTransaction {
let chatListOperations: [PeerGroupId: [ChatListOperation]]
let currentUpdatedChatListInclusions: [PeerId: PeerChatListInclusion]
let currentUpdatedPeers: [PeerId: Peer]
let currentUpdatedPeerNotificationSettings: [PeerId: PeerNotificationSettings]
let currentUpdatedPeerNotificationSettings: [PeerId: (PeerNotificationSettings?, PeerNotificationSettings)]
let currentUpdatedPeerNotificationBehaviorTimestamps: [PeerId: PeerNotificationSettingsBehaviorTimestamp]
let currentUpdatedCachedPeerData: [PeerId: CachedPeerData]
let currentUpdatedPeerPresences: [PeerId: PeerPresence]
@ -172,7 +172,7 @@ final class PostboxTransaction {
return true
}
init(currentUpdatedState: PostboxCoding?, currentPeerHoleOperations: [MessageHistoryIndexHoleOperationKey: [MessageHistoryIndexHoleOperation]] = [:], currentOperationsByPeerId: [PeerId: [MessageHistoryOperation]], chatListOperations: [PeerGroupId: [ChatListOperation]], currentUpdatedChatListInclusions: [PeerId: PeerChatListInclusion], currentUpdatedPeers: [PeerId: Peer], currentUpdatedPeerNotificationSettings: [PeerId: PeerNotificationSettings], currentUpdatedPeerNotificationBehaviorTimestamps: [PeerId: PeerNotificationSettingsBehaviorTimestamp], currentUpdatedCachedPeerData: [PeerId: CachedPeerData], currentUpdatedPeerPresences: [PeerId: PeerPresence], currentUpdatedPeerChatListEmbeddedStates: [PeerId: PeerChatListEmbeddedInterfaceState?], currentUpdatedTotalUnreadState: ChatListTotalUnreadState?, currentUpdatedTotalUnreadSummaries: [PeerGroupId: PeerGroupUnreadCountersCombinedSummary], alteredInitialPeerCombinedReadStates: [PeerId: CombinedPeerReadState], currentPeerMergedOperationLogOperations: [PeerMergedOperationLogOperation], currentTimestampBasedMessageAttributesOperations: [TimestampBasedMessageAttributesOperation], unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], updatedSynchronizePeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?], currentUpdatedGroupSummarySynchronizeOperations: [PeerGroupAndNamespace: Bool], currentPreferencesOperations: [PreferencesOperation], currentOrderedItemListOperations: [Int32: [OrderedItemListOperation]], currentItemCollectionItemsOperations: [ItemCollectionId: [ItemCollectionItemsOperation]], currentItemCollectionInfosOperations: [ItemCollectionInfosOperation], currentUpdatedPeerChatStates: Set<PeerId>, currentGlobalTagsOperations: [GlobalMessageHistoryTagsOperation], currentLocalTagsOperations: [IntermediateMessageHistoryLocalTagsOperation], updatedMedia: [MediaId: Media?], replaceRemoteContactCount: Int32?, replaceContactPeerIds: Set<PeerId>?, currentPendingMessageActionsOperations: [PendingMessageActionsOperation], currentUpdatedMessageActionsSummaries: [PendingMessageActionsSummaryKey: Int32], currentUpdatedMessageTagSummaries: [MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary], currentInvalidateMessageTagSummaries: [InvalidatedMessageHistoryTagsSummaryEntryOperation], currentUpdatedPendingPeerNotificationSettings: Set<PeerId>, replacedAdditionalChatListItems: [PeerId]?, updatedNoticeEntryKeys: Set<NoticeEntryKey>, updatedCacheEntryKeys: Set<ItemCacheEntryId>, currentUpdatedMasterClientId: Int64?, updatedFailedMessagePeerIds: Set<PeerId>, updatedFailedMessageIds: Set<MessageId>) {
init(currentUpdatedState: PostboxCoding?, currentPeerHoleOperations: [MessageHistoryIndexHoleOperationKey: [MessageHistoryIndexHoleOperation]] = [:], currentOperationsByPeerId: [PeerId: [MessageHistoryOperation]], chatListOperations: [PeerGroupId: [ChatListOperation]], currentUpdatedChatListInclusions: [PeerId: PeerChatListInclusion], currentUpdatedPeers: [PeerId: Peer], currentUpdatedPeerNotificationSettings: [PeerId: (PeerNotificationSettings?, PeerNotificationSettings)], currentUpdatedPeerNotificationBehaviorTimestamps: [PeerId: PeerNotificationSettingsBehaviorTimestamp], currentUpdatedCachedPeerData: [PeerId: CachedPeerData], currentUpdatedPeerPresences: [PeerId: PeerPresence], currentUpdatedPeerChatListEmbeddedStates: [PeerId: PeerChatListEmbeddedInterfaceState?], currentUpdatedTotalUnreadState: ChatListTotalUnreadState?, currentUpdatedTotalUnreadSummaries: [PeerGroupId: PeerGroupUnreadCountersCombinedSummary], alteredInitialPeerCombinedReadStates: [PeerId: CombinedPeerReadState], currentPeerMergedOperationLogOperations: [PeerMergedOperationLogOperation], currentTimestampBasedMessageAttributesOperations: [TimestampBasedMessageAttributesOperation], unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], updatedSynchronizePeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?], currentUpdatedGroupSummarySynchronizeOperations: [PeerGroupAndNamespace: Bool], currentPreferencesOperations: [PreferencesOperation], currentOrderedItemListOperations: [Int32: [OrderedItemListOperation]], currentItemCollectionItemsOperations: [ItemCollectionId: [ItemCollectionItemsOperation]], currentItemCollectionInfosOperations: [ItemCollectionInfosOperation], currentUpdatedPeerChatStates: Set<PeerId>, currentGlobalTagsOperations: [GlobalMessageHistoryTagsOperation], currentLocalTagsOperations: [IntermediateMessageHistoryLocalTagsOperation], updatedMedia: [MediaId: Media?], replaceRemoteContactCount: Int32?, replaceContactPeerIds: Set<PeerId>?, currentPendingMessageActionsOperations: [PendingMessageActionsOperation], currentUpdatedMessageActionsSummaries: [PendingMessageActionsSummaryKey: Int32], currentUpdatedMessageTagSummaries: [MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary], currentInvalidateMessageTagSummaries: [InvalidatedMessageHistoryTagsSummaryEntryOperation], currentUpdatedPendingPeerNotificationSettings: Set<PeerId>, replacedAdditionalChatListItems: [PeerId]?, updatedNoticeEntryKeys: Set<NoticeEntryKey>, updatedCacheEntryKeys: Set<ItemCacheEntryId>, currentUpdatedMasterClientId: Int64?, updatedFailedMessagePeerIds: Set<PeerId>, updatedFailedMessageIds: Set<MessageId>) {
self.currentUpdatedState = currentUpdatedState
self.currentPeerHoleOperations = currentPeerHoleOperations
self.currentOperationsByPeerId = currentOperationsByPeerId

View File

@ -1498,6 +1498,38 @@ public final class SqliteValueBox: ValueBox {
withExtendedLifetime(end, {})
}
public func filteredRange(_ table: ValueBoxTable, start: ValueBoxKey, end: ValueBoxKey, values: (ValueBoxKey, ReadBuffer) -> ValueBoxFilterResult, limit: Int) {
var currentStart = start
var acceptedCount = 0
while acceptedCount < limit {
var hadStop = false
var lastKey: ValueBoxKey?
self.range(table, start: currentStart, end: end, values: { key, value in
lastKey = key
let result = values(key, value)
switch result {
case .accept:
acceptedCount += 1
return true
case .skip:
return true
case .stop:
hadStop = true
return false
}
return true
}, limit: limit)
if let lastKey = lastKey {
currentStart = lastKey
} else {
break
}
if hadStop {
break
}
}
}
public func range(_ table: ValueBoxTable, start: ValueBoxKey, end: ValueBoxKey, keys: (ValueBoxKey) -> Bool, limit: Int) {
precondition(self.queue.isCurrent())
if let _ = self.tables[table.id] {

View File

@ -57,6 +57,12 @@ public struct ValueBoxEncryptionParameters {
}
}
public enum ValueBoxFilterResult {
case accept
case skip
case stop
}
public protocol ValueBox {
func begin()
func commit()
@ -66,6 +72,7 @@ public protocol ValueBox {
func endStats()
func range(_ table: ValueBoxTable, start: ValueBoxKey, end: ValueBoxKey, values: (ValueBoxKey, ReadBuffer) -> Bool, limit: Int)
func filteredRange(_ table: ValueBoxTable, start: ValueBoxKey, end: ValueBoxKey, values: (ValueBoxKey, ReadBuffer) -> ValueBoxFilterResult, limit: Int)
func range(_ table: ValueBoxTable, start: ValueBoxKey, end: ValueBoxKey, keys: (ValueBoxKey) -> Bool, limit: Int)
func scan(_ table: ValueBoxTable, values: (ValueBoxKey, ReadBuffer) -> Bool)
func scan(_ table: ValueBoxTable, keys: (ValueBoxKey) -> Bool)

View File

@ -1330,17 +1330,17 @@ public final class AccountViewTracker {
})
}
public func tailChatListView(groupId: PeerGroupId, count: Int) -> Signal<(ChatListView, ViewUpdateType), NoError> {
public func tailChatListView(groupId: PeerGroupId, filterPredicate: ((Peer, PeerNotificationSettings?) -> Bool)? = nil, count: Int) -> Signal<(ChatListView, ViewUpdateType), NoError> {
if let account = self.account {
return self.wrappedChatListView(signal: account.postbox.tailChatListView(groupId: groupId, count: count, summaryComponents: ChatListEntrySummaryComponents(tagSummary: ChatListEntryMessageTagSummaryComponent(tag: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud), actionsSummary: ChatListEntryPendingMessageActionsSummaryComponent(type: PendingMessageActionType.consumeUnseenPersonalMessage, namespace: Namespaces.Message.Cloud))))
return self.wrappedChatListView(signal: account.postbox.tailChatListView(groupId: groupId, filterPredicate: filterPredicate, count: count, summaryComponents: ChatListEntrySummaryComponents(tagSummary: ChatListEntryMessageTagSummaryComponent(tag: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud), actionsSummary: ChatListEntryPendingMessageActionsSummaryComponent(type: PendingMessageActionType.consumeUnseenPersonalMessage, namespace: Namespaces.Message.Cloud))))
} else {
return .never()
}
}
public func aroundChatListView(groupId: PeerGroupId, index: ChatListIndex, count: Int) -> Signal<(ChatListView, ViewUpdateType), NoError> {
public func aroundChatListView(groupId: PeerGroupId, filterPredicate: ((Peer, PeerNotificationSettings?) -> Bool)? = nil, index: ChatListIndex, count: Int) -> Signal<(ChatListView, ViewUpdateType), NoError> {
if let account = self.account {
return self.wrappedChatListView(signal: account.postbox.aroundChatListView(groupId: groupId, index: index, count: count, summaryComponents: ChatListEntrySummaryComponents(tagSummary: ChatListEntryMessageTagSummaryComponent(tag: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud), actionsSummary: ChatListEntryPendingMessageActionsSummaryComponent(type: PendingMessageActionType.consumeUnseenPersonalMessage, namespace: Namespaces.Message.Cloud))))
return self.wrappedChatListView(signal: account.postbox.aroundChatListView(groupId: groupId, filterPredicate: filterPredicate, index: index, count: count, summaryComponents: ChatListEntrySummaryComponents(tagSummary: ChatListEntryMessageTagSummaryComponent(tag: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud), actionsSummary: ChatListEntryPendingMessageActionsSummaryComponent(type: PendingMessageActionType.consumeUnseenPersonalMessage, namespace: Namespaces.Message.Cloud))))
} else {
return .never()
}

View File

@ -177,15 +177,24 @@ public func messageBubbleImage(maxCornerRadius: CGFloat, minCornerRadius: CGFloa
context.setStrokeColor(UIColor.black.cgColor)
let borderWidth: CGFloat
if abs(UIScreenPixel - 0.5) < CGFloat.ulpOfOne {
borderWidth = UIScreenPixel
let borderOffset: CGFloat
let innerExtension: CGFloat
if knockout && !mask {
innerExtension = 0.25
} else {
borderWidth = UIScreenPixel * 2.0
innerExtension = 0.25
}
if abs(UIScreenPixel - 0.5) < CGFloat.ulpOfOne {
borderWidth = UIScreenPixel + innerExtension
borderOffset = -innerExtension / 2.0 + UIScreenPixel / 2.0
} else {
borderWidth = UIScreenPixel * 2.0 + innerExtension
borderOffset = -innerExtension / 2.0 + UIScreenPixel * 2.0 / 2.0
}
context.setLineWidth(borderWidth)
let borderOffset: CGFloat = borderWidth / 2.0
context.move(to: CGPoint(x: -borderOffset, y: topLeftRadius + borderOffset))
context.addArc(tangent1End: CGPoint(x: -borderOffset, y: -borderOffset), tangent2End: CGPoint(x: topLeftRadius + borderOffset, y: -borderOffset), radius: topLeftRadius + borderOffset * 2.0)
context.addLine(to: CGPoint(x: fixedMainDiameter - topRightRadius - borderOffset, y: -borderOffset))
@ -278,8 +287,6 @@ public func messageBubbleImage(maxCornerRadius: CGFloat, minCornerRadius: CGFloa
context.setBlendMode(.copy)
let image = outlineContext.generateImage()!
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.normal)
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size))
})!
let drawingContext = DrawingContext(size: imageSize)

View File

@ -82,8 +82,8 @@ class ChatMessageBackground: ASDisplayNode {
super.init()
self.isUserInteractionEnabled = false
self.addSubnode(self.imageNode)
self.addSubnode(self.outlineImageNode)
self.addSubnode(self.imageNode)
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {