diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index a6aa64fcb1..0eed0bf19b 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -198,7 +198,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } } - public final var didScrollWithOffset: ((CGFloat, ContainedViewLayoutTransition) -> Void)? + public final var didScrollWithOffset: ((CGFloat, ContainedViewLayoutTransition, ListViewItemNode?) -> Void)? private var topItemOverscrollBackground: ListViewOverscrollBackgroundNode? private var bottomItemOverscrollBackground: ASDisplayNode? @@ -789,7 +789,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture anchor = 0.0 } - self.didScrollWithOffset?(deltaY, .immediate) + self.didScrollWithOffset?(deltaY, .immediate, nil) for itemNode in self.itemNodes { itemNode.updateFrame(itemNode.frame.offsetBy(dx: 0.0, dy: -deltaY), within: self.visibleSize) @@ -1070,7 +1070,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } if abs(offset) > CGFloat.ulpOfOne { - self.didScrollWithOffset?(-offset, .immediate) + self.didScrollWithOffset?(-offset, .immediate, nil) for itemNode in self.itemNodes { var frame = itemNode.frame @@ -2634,7 +2634,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } } - self.didScrollWithOffset?(-offset, scrollToItemTransition) + self.didScrollWithOffset?(-offset, scrollToItemTransition, nil) } for itemNode in self.itemNodes { @@ -2720,7 +2720,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } } - self.didScrollWithOffset?(-offsetFix, updateSizeAndInsetsTransition) + self.didScrollWithOffset?(-offsetFix, updateSizeAndInsetsTransition, nil) for itemNode in self.itemNodes { itemNode.updateFrame(itemNode.frame.offsetBy(dx: 0.0, dy: offsetFix), within: self.visibleSize) @@ -2731,7 +2731,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture if !snappedTopInset.isZero && (previousVisibleSize.height.isZero || previousApparentFrames.isEmpty) { offsetFix += snappedTopInset - self.didScrollWithOffset?(-offsetFix, .immediate) + self.didScrollWithOffset?(-offsetFix, .immediate, nil) for itemNode in self.itemNodes { itemNode.updateFrame(itemNode.frame.offsetBy(dx: 0.0, dy: snappedTopInset), within: self.visibleSize) @@ -2855,7 +2855,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture let (snappedTopInset, snapToBoundsOffset) = self.snapToBounds(snapTopItem: scrollToItem != nil && scrollToItem?.directionHint != .Down, stackFromBottom: self.stackFromBottom, updateSizeAndInsets: updateSizeAndInsets, scrollToItem: scrollToItem, insetDeltaOffsetFix: 0.0) if !snappedTopInset.isZero && previousApparentFrames.isEmpty { - self.didScrollWithOffset?(-snappedTopInset, .immediate) + self.didScrollWithOffset?(-snappedTopInset, .immediate, nil) for itemNode in self.itemNodes { itemNode.updateFrame(itemNode.frame.offsetBy(dx: 0.0, dy: snappedTopInset), within: self.visibleSize) @@ -2967,7 +2967,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture self.updateItemHeaders(leftInset: listInsets.left, rightInset: listInsets.right, transition: headerNodesTransition, animateInsertion: animated || !requestItemInsertionAnimationsIndices.isEmpty) if let offset = offset, !offset.isZero { - self.didScrollWithOffset?(-offset, headerNodesTransition.0) + self.didScrollWithOffset?(-offset, headerNodesTransition.0, nil) let lowestNodeToInsertBelow = self.lowestNodeToInsertBelow() for itemNode in temporaryPreviousNodes { itemNode.updateFrame(itemNode.frame.offsetBy(dx: 0.0, dy: offset), within: self.visibleSize) @@ -3796,6 +3796,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture let offset = offsetRanges.offsetForIndex(index) if offset != 0.0 { itemNode.updateFrame(itemNode.frame.offsetBy(dx: 0.0, dy: offset), within: self.visibleSize) + self.didScrollWithOffset?(-offset, .immediate, itemNode) } index += 1 diff --git a/submodules/SyncCore/Sources/ChannelState.swift b/submodules/SyncCore/Sources/ChannelState.swift index a4dd98fc50..1de495adb7 100644 --- a/submodules/SyncCore/Sources/ChannelState.swift +++ b/submodules/SyncCore/Sources/ChannelState.swift @@ -3,15 +3,18 @@ import Postbox public final class ChannelState: PeerChatState, Equatable, CustomStringConvertible { public let pts: Int32 public let invalidatedPts: Int32? + public let synchronizedUntilMessageId: Int32? - public init(pts: Int32, invalidatedPts: Int32?) { + public init(pts: Int32, invalidatedPts: Int32?, synchronizedUntilMessageId: Int32?) { self.pts = pts self.invalidatedPts = invalidatedPts + self.synchronizedUntilMessageId = synchronizedUntilMessageId } public init(decoder: PostboxDecoder) { self.pts = decoder.decodeInt32ForKey("pts", orElse: 0) self.invalidatedPts = decoder.decodeOptionalInt32ForKey("ipts") + self.synchronizedUntilMessageId = decoder.decodeOptionalInt32ForKey("sumi") } public func encode(_ encoder: PostboxEncoder) { @@ -21,14 +24,23 @@ public final class ChannelState: PeerChatState, Equatable, CustomStringConvertib } else { encoder.encodeNil(forKey: "ipts") } + if let synchronizedUntilMessageId = self.synchronizedUntilMessageId { + encoder.encodeInt32(synchronizedUntilMessageId, forKey: "sumi") + } else { + encoder.encodeNil(forKey: "sumi") + } } public func withUpdatedPts(_ pts: Int32) -> ChannelState { - return ChannelState(pts: pts, invalidatedPts: self.invalidatedPts) + return ChannelState(pts: pts, invalidatedPts: self.invalidatedPts, synchronizedUntilMessageId: self.synchronizedUntilMessageId) } public func withUpdatedInvalidatedPts(_ invalidatedPts: Int32?) -> ChannelState { - return ChannelState(pts: self.pts, invalidatedPts: invalidatedPts) + return ChannelState(pts: self.pts, invalidatedPts: invalidatedPts, synchronizedUntilMessageId: self.synchronizedUntilMessageId) + } + + public func withUpdatedSynchronizedUntilMessageId(_ synchronizedUntilMessageId: Int32?) -> ChannelState { + return ChannelState(pts: self.pts, invalidatedPts: self.invalidatedPts, synchronizedUntilMessageId: synchronizedUntilMessageId) } public func equals(_ other: PeerChatState) -> Bool { @@ -43,6 +55,6 @@ public final class ChannelState: PeerChatState, Equatable, CustomStringConvertib } public static func ==(lhs: ChannelState, rhs: ChannelState) -> Bool { - return lhs.pts == rhs.pts && lhs.invalidatedPts == rhs.invalidatedPts + return lhs.pts == rhs.pts && lhs.invalidatedPts == rhs.invalidatedPts && lhs.synchronizedUntilMessageId == rhs.synchronizedUntilMessageId } } diff --git a/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift index 8190fedbf4..3ca40ebb64 100644 --- a/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift @@ -1544,7 +1544,7 @@ private func resolveMissingPeerChatInfos(network: Network, state: AccountMutable updatedState.resetMessageTagSummary(peer.peerId, namespace: Namespaces.Message.Cloud, count: unreadMentionsCount, range: MessageHistoryTagNamespaceCountValidityRange(maxId: topMessage)) updatedState.peerChatInfos[peer.peerId] = PeerChatInfo(notificationSettings: notificationSettings) if let pts = pts { - channelStates[peer.peerId] = ChannelState(pts: pts, invalidatedPts: pts) + channelStates[peer.peerId] = ChannelState(pts: pts, invalidatedPts: pts, synchronizedUntilMessageId: nil) } case .dialogFolder: assertionFailure() @@ -1708,7 +1708,7 @@ private func resetChannels(network: Network, peers: [Peer], state: AccountMutabl } if let apiChannelPts = apiChannelPts { - channelStates[peerId] = ChannelState(pts: apiChannelPts, invalidatedPts: apiChannelPts) + channelStates[peerId] = ChannelState(pts: apiChannelPts, invalidatedPts: apiChannelPts, synchronizedUntilMessageId: nil) } notificationSettings[peerId] = TelegramPeerNotificationSettings(apiSettings: apiNotificationSettings) @@ -1812,7 +1812,7 @@ private func pollChannel(network: Network, peer: Peer, state: AccountMutableStat if let previousState = updatedState.chatStates[peer.id] as? ChannelState { channelState = previousState.withUpdatedPts(pts) } else { - channelState = ChannelState(pts: pts, invalidatedPts: nil) + channelState = ChannelState(pts: pts, invalidatedPts: nil, synchronizedUntilMessageId: nil) } updatedState.updateChannelState(peer.id, state: channelState) @@ -1893,7 +1893,7 @@ private func pollChannel(network: Network, peer: Peer, state: AccountMutableStat if let previousState = updatedState.chatStates[peer.id] as? ChannelState { channelState = previousState.withUpdatedPts(pts) } else { - channelState = ChannelState(pts: pts, invalidatedPts: nil) + channelState = ChannelState(pts: pts, invalidatedPts: nil, synchronizedUntilMessageId: nil) } updatedState.updateChannelState(peer.id, state: channelState) case let .channelDifferenceTooLong(_, timeout, dialog, messages, chats, users): @@ -1911,7 +1911,7 @@ private func pollChannel(network: Network, peer: Peer, state: AccountMutableStat } if let (peer, pts, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, unreadMentionsCount) = parameters { - let channelState = ChannelState(pts: pts, invalidatedPts: pts) + let channelState = ChannelState(pts: pts, invalidatedPts: pts, synchronizedUntilMessageId: nil) updatedState.updateChannelState(peer.peerId, state: channelState) updatedState.mergeChats(chats) diff --git a/submodules/TelegramCore/Sources/ChatListFiltering.swift b/submodules/TelegramCore/Sources/ChatListFiltering.swift index 22652ae25c..0cc46fd73e 100644 --- a/submodules/TelegramCore/Sources/ChatListFiltering.swift +++ b/submodules/TelegramCore/Sources/ChatListFiltering.swift @@ -644,7 +644,7 @@ private func loadAndStorePeerChatInfos(accountPeerId: PeerId, postbox: Postbox, transaction.replaceMessageTagSummary(peerId: peerId, tagMask: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud, count: unreadMentionsCount, maxId: topMessage) if let pts = pts { - let channelState = ChannelState(pts: pts, invalidatedPts: pts) + let channelState = ChannelState(pts: pts, invalidatedPts: pts, synchronizedUntilMessageId: nil) transaction.setPeerChatState(peerId, state: channelState) channelStates[peer.peerId] = channelState } diff --git a/submodules/TelegramCore/Sources/FetchChatList.swift b/submodules/TelegramCore/Sources/FetchChatList.swift index 5f957e8447..3d7f923cd7 100644 --- a/submodules/TelegramCore/Sources/FetchChatList.swift +++ b/submodules/TelegramCore/Sources/FetchChatList.swift @@ -131,7 +131,7 @@ private func parseDialogs(apiDialogs: [Api.Dialog], apiMessages: [Api.Message], } if let apiChannelPts = apiChannelPts { - chatStates[peerId] = ChannelState(pts: apiChannelPts, invalidatedPts: nil) + chatStates[peerId] = ChannelState(pts: apiChannelPts, invalidatedPts: nil, synchronizedUntilMessageId: nil) } notificationSettings[peerId] = TelegramPeerNotificationSettings(apiSettings: apiNotificationSettings) diff --git a/submodules/TelegramCore/Sources/ManagedSynchronizePinnedChatsOperations.swift b/submodules/TelegramCore/Sources/ManagedSynchronizePinnedChatsOperations.swift index 8de969c9f1..d726df5b03 100644 --- a/submodules/TelegramCore/Sources/ManagedSynchronizePinnedChatsOperations.swift +++ b/submodules/TelegramCore/Sources/ManagedSynchronizePinnedChatsOperations.swift @@ -200,7 +200,7 @@ private func synchronizePinnedChats(transaction: Transaction, postbox: Postbox, readStates[peerId]![Namespaces.Message.Cloud] = .idBased(maxIncomingReadId: apiReadInboxMaxId, maxOutgoingReadId: apiReadOutboxMaxId, maxKnownId: apiTopMessage, count: apiUnreadCount, markedUnread: apiMarkedUnread) if let apiChannelPts = apiChannelPts { - chatStates[peerId] = ChannelState(pts: apiChannelPts, invalidatedPts: nil) + chatStates[peerId] = ChannelState(pts: apiChannelPts, invalidatedPts: nil, synchronizedUntilMessageId: nil) } notificationSettings[peerId] = TelegramPeerNotificationSettings(apiSettings: apiNotificationSettings) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 86860c76f7..c0060e8b55 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -286,7 +286,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private weak var mediaRestrictedTooltipController: TooltipController? private var mediaRestrictedTooltipControllerMode = true - private var currentMessageTooltipScreens: [TooltipScreen] = [] + private var currentMessageTooltipScreens: [(TooltipScreen, ListViewItemNode)] = [] private weak var slowmodeTooltipController: ChatSlowmodeHintController? @@ -1697,20 +1697,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let solution = resultPoll.results.solution { for contentNode in itemNode.contentNodes { if let contentNode = contentNode as? ChatMessagePollBubbleContentNode, let sourceNode = contentNode.solutionTipSourceNode { - let absoluteFrame = sourceNode.view.convert(sourceNode.bounds, to: strongSelf.view).insetBy(dx: 0.0, dy: -4.0).offsetBy(dx: 0.0, dy: 0.0) - let tooltipScreen = TooltipScreen(text: solution.text, textEntities: solution.entities, icon: nil, location: absoluteFrame, shouldDismissOnTouch: { point in - return .dismiss(consume: absoluteFrame.contains(point)) - }, openUrl: { url in - self?.openUrl(url, concealed: false) - }) - tooltipScreen.becameDismissed = { tooltipScreen in - guard let strongSelf = self else { - return - } - strongSelf.currentMessageTooltipScreens.removeAll(where: { $0 === tooltipScreen }) - } - strongSelf.currentMessageTooltipScreens.append(tooltipScreen) - strongSelf.present(tooltipScreen, in: .current) + strongSelf.controllerInteraction?.displayPollSolution(solution, sourceNode) } } } @@ -1962,20 +1949,30 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self else { return } - let absoluteFrame = sourceNode.view.convert(sourceNode.bounds, to: strongSelf.view).insetBy(dx: 0.0, dy: -4.0).offsetBy(dx: 0.0, dy: 0.0) - let tooltipScreen = TooltipScreen(text: solution.text, textEntities: solution.entities, icon: nil, location: absoluteFrame, shouldDismissOnTouch: { point in - return .dismiss(consume: absoluteFrame.contains(point)) - }, openUrl: { url in - self?.openUrl(url, concealed: false) - }) - tooltipScreen.becameDismissed = { tooltipScreen in - guard let strongSelf = self else { - return + var foundItemNode: ListViewItemNode? + strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + if sourceNode.view.isDescendant(of: itemNode.view) { + foundItemNode = itemNode + } } - strongSelf.currentMessageTooltipScreens.removeAll(where: { $0 === tooltipScreen }) } - strongSelf.currentMessageTooltipScreens.append(tooltipScreen) - strongSelf.present(tooltipScreen, in: .current) + if let foundItemNode = foundItemNode { + let absoluteFrame = sourceNode.view.convert(sourceNode.bounds, to: strongSelf.view).insetBy(dx: 0.0, dy: -4.0).offsetBy(dx: 0.0, dy: 0.0) + let tooltipScreen = TooltipScreen(text: solution.text, textEntities: solution.entities, icon: nil, location: absoluteFrame, shouldDismissOnTouch: { point in + return .dismiss(consume: absoluteFrame.contains(point)) + }, openUrl: { url in + self?.openUrl(url, concealed: false) + }) + tooltipScreen.becameDismissed = { tooltipScreen in + guard let strongSelf = self else { + return + } + strongSelf.currentMessageTooltipScreens.removeAll(where: { $0.0 === tooltipScreen }) + } + strongSelf.currentMessageTooltipScreens.append((tooltipScreen, foundItemNode)) + strongSelf.present(tooltipScreen, in: .current) + } }, requestMessageUpdate: { [weak self] id in if let strongSelf = self { strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id) @@ -2689,12 +2686,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G override public func loadDisplayNode() { self.displayNode = ChatControllerNode(context: self.context, chatLocation: self.chatLocation, subject: self.subject, controllerInteraction: self.controllerInteraction!, chatPresentationInterfaceState: self.presentationInterfaceState, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, navigationBar: self.navigationBar, controller: self) - self.chatDisplayNode.historyNode.didScrollWithOffset = { [weak self] offset, transition in + self.chatDisplayNode.historyNode.didScrollWithOffset = { [weak self] offset, transition, itemNode in guard let strongSelf = self else { return } - for tooltipScreen in strongSelf.currentMessageTooltipScreens { - tooltipScreen.addRelativeScrollingOffset(-offset, transition: transition) + for (tooltipScreen, tooltipItemNode) in strongSelf.currentMessageTooltipScreens { + if let itemNode = itemNode { + if itemNode === tooltipItemNode { + tooltipScreen.addRelativeScrollingOffset(-offset, transition: transition) + } + } else { + tooltipScreen.addRelativeScrollingOffset(-offset, transition: transition) + } } } diff --git a/submodules/TooltipUI/Sources/TooltipScreen.swift b/submodules/TooltipUI/Sources/TooltipScreen.swift index f148bbbed4..159b59f9dc 100644 --- a/submodules/TooltipUI/Sources/TooltipScreen.swift +++ b/submodules/TooltipUI/Sources/TooltipScreen.swift @@ -25,6 +25,8 @@ private final class TooltipScreenNode: ViewControllerTracingNode { private var isArrowInverted: Bool = false + private var validLayout: ContainerViewLayout? + init(text: String, textEntities: [MessageTextEntity], icon: TooltipScreen.Icon?, location: CGRect, shouldDismissOnTouch: @escaping (CGPoint) -> TooltipScreen.DismissOnTouch, requestDismiss: @escaping () -> Void, openUrl: @escaping (String) -> Void) { self.icon = icon self.location = location @@ -113,6 +115,8 @@ private final class TooltipScreenNode: ViewControllerTracingNode { } func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.validLayout = layout + self.scrollingContainer.frame = CGRect(origin: CGPoint(), size: layout.size) let sideInset: CGFloat = 13.0 @@ -140,7 +144,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - contentInset * 2.0 - sideInset * 2.0 - animationSize.width - animationSpacing, height: .greatestFiniteMagnitude)) - let backgroundWidth = textSize.width + contentInset * 2.0 + sideInset * 2.0 + animationSize.width + animationSpacing + let backgroundWidth = textSize.width + contentInset * 2.0 + animationSize.width + animationSpacing let backgroundHeight = max(animationSize.height, textSize.height) + contentVerticalInset * 2.0 var backgroundFrame = CGRect(origin: CGPoint(x: self.location.midX - backgroundWidth / 2.0, y: self.location.minY - bottomInset - backgroundHeight), size: CGSize(width: backgroundWidth, height: backgroundHeight)) if backgroundFrame.minX < sideInset { @@ -210,9 +214,9 @@ private final class TooltipScreenNode: ViewControllerTracingNode { } func animateIn() { - self.containerNode.layer.animateSpring(from: NSNumber(value: Float(0.01)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6) + self.containerNode.layer.animateSpring(from: NSNumber(value: Float(0.01)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.4, damping: 105.0) let arrowY: CGFloat = self.isArrowInverted ? self.arrowContainer.frame.minY : self.arrowContainer.frame.maxY - self.containerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: self.arrowContainer.frame.midX - self.containerNode.bounds.width / 2.0, y: arrowY - self.containerNode.bounds.height / 2.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.6, additive: true) + self.containerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: self.arrowContainer.frame.midX - self.containerNode.bounds.width / 2.0, y: arrowY - self.containerNode.bounds.height / 2.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.4, damping: 105.0, additive: true) self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) let animationDelay: Double @@ -243,6 +247,13 @@ private final class TooltipScreenNode: ViewControllerTracingNode { func addRelativeScrollingOffset(_ value: CGFloat, transition: ContainedViewLayoutTransition) { self.scrollingContainer.bounds = self.scrollingContainer.bounds.offsetBy(dx: 0.0, dy: value) transition.animateOffsetAdditive(node: self.scrollingContainer, offset: -value) + + if let layout = self.validLayout { + let projectedContainerFrame = self.containerNode.frame.offsetBy(dx: 0.0, dy: -self.scrollingContainer.bounds.origin.y) + if projectedContainerFrame.minY - 30.0 < layout.insets(options: .statusBar).top { + self.requestDismiss() + } + } } }