Context UI animations

This commit is contained in:
Ali 2021-09-10 20:24:31 +04:00
parent 6967ccb589
commit e3d5a26d67
10 changed files with 174 additions and 80 deletions

View File

@ -6814,3 +6814,5 @@ Ads should no longer be synonymous with abuse of user privacy. Let us redefine h
"VideoChat.RecordingSaved" = "Video chat recording saved to **Saved Messages**."; "VideoChat.RecordingSaved" = "Video chat recording saved to **Saved Messages**.";
"LiveStream.RecordingSaved" = "Live stream recording saved to **Saved Messages**."; "LiveStream.RecordingSaved" = "Live stream recording saved to **Saved Messages**.";
"ChatContextMenu.MessageViewsPrivacyTip" = "To protect privacy, views are only stored for 7 days.";

View File

@ -324,20 +324,26 @@ private final class InnerTextSelectionTipContainerNode: ASDisplayNode {
private let iconNode: ASImageNode private let iconNode: ASImageNode
private let text: String private let text: String
private let targetSelectionIndex: Int private let targetSelectionIndex: Int?
init(presentationData: PresentationData) { init(presentationData: PresentationData, tip: ContextController.Tip) {
self.presentationData = presentationData self.presentationData = presentationData
self.textNode = TextNode() self.textNode = TextNode()
var rawText = self.presentationData.strings.ChatContextMenu_TextSelectionTip switch tip {
if let range = rawText.range(of: "|") { case .textSelection:
rawText.removeSubrange(range) var rawText = self.presentationData.strings.ChatContextMenu_TextSelectionTip
self.text = rawText if let range = rawText.range(of: "|") {
self.targetSelectionIndex = NSRange(range, in: rawText).lowerBound rawText.removeSubrange(range)
} else { self.text = rawText
self.text = rawText self.targetSelectionIndex = NSRange(range, in: rawText).lowerBound
self.targetSelectionIndex = 1 } else {
self.text = rawText
self.targetSelectionIndex = 1
}
case .messageViewsPrivacy:
self.text = self.presentationData.strings.ChatContextMenu_MessageViewsPrivacyTip
self.targetSelectionIndex = nil
} }
self.iconNode = ASImageNode() self.iconNode = ASImageNode()
@ -430,13 +436,13 @@ private final class InnerTextSelectionTipContainerNode: ASDisplayNode {
} }
func animateIn() { func animateIn() {
if let textSelectionNode = self.textSelectionNode { if let textSelectionNode = self.textSelectionNode, let targetSelectionIndex = self.targetSelectionIndex {
textSelectionNode.pretendInitiateSelection() textSelectionNode.pretendInitiateSelection()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5, execute: { [weak self] in DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5, execute: { [weak self] in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
strongSelf.textSelectionNode?.pretendExtendSelection(to: strongSelf.targetSelectionIndex) strongSelf.textSelectionNode?.pretendExtendSelection(to: targetSelectionIndex)
}) })
} }
} }
@ -463,7 +469,7 @@ final class ContextActionsContainerNode: ASDisplayNode {
return self.additionalActionsNode != nil return self.additionalActionsNode != nil
} }
init(presentationData: PresentationData, items: [ContextMenuItem], getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void, feedbackTap: @escaping () -> Void, displayTextSelectionTip: Bool, blurBackground: Bool) { init(presentationData: PresentationData, items: [ContextMenuItem], getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void, feedbackTap: @escaping () -> Void, tip: ContextController.Tip?, blurBackground: Bool) {
self.blurBackground = blurBackground self.blurBackground = blurBackground
self.shadowNode = ASImageNode() self.shadowNode = ASImageNode()
self.shadowNode.displaysAsynchronously = false self.shadowNode.displaysAsynchronously = false
@ -490,8 +496,8 @@ final class ContextActionsContainerNode: ASDisplayNode {
} }
self.actionsNode = InnerActionsContainerNode(presentationData: presentationData, items: items, getController: getController, actionSelected: actionSelected, feedbackTap: feedbackTap, blurBackground: blurBackground) self.actionsNode = InnerActionsContainerNode(presentationData: presentationData, items: items, getController: getController, actionSelected: actionSelected, feedbackTap: feedbackTap, blurBackground: blurBackground)
if displayTextSelectionTip { if let tip = tip {
let textSelectionTipNode = InnerTextSelectionTipContainerNode(presentationData: presentationData) let textSelectionTipNode = InnerTextSelectionTipContainerNode(presentationData: presentationData, tip: tip)
textSelectionTipNode.isUserInteractionEnabled = false textSelectionTipNode.isUserInteractionEnabled = false
self.textSelectionTipNode = textSelectionTipNode self.textSelectionTipNode = textSelectionTipNode
} else { } else {

View File

@ -125,7 +125,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
fileprivate var dismissedForCancel: (() -> Void)? fileprivate var dismissedForCancel: (() -> Void)?
private let getController: () -> ContextControllerProtocol? private let getController: () -> ContextControllerProtocol?
private weak var gesture: ContextGesture? private weak var gesture: ContextGesture?
private var displayTextSelectionTip: Bool private var tip: ContextController.Tip?
private var didSetItemsReady = false private var didSetItemsReady = false
let itemsReady = Promise<Bool>() let itemsReady = Promise<Bool>()
@ -169,7 +169,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
private let blurBackground: Bool private let blurBackground: Bool
init(account: Account, controller: ContextController, presentationData: PresentationData, source: ContextContentSource, items: Signal<[ContextMenuItem], NoError>, reactionItems: [ReactionContextItem], beginDismiss: @escaping (ContextMenuActionResult) -> Void, recognizer: TapLongTapOrDoubleTapGestureRecognizer?, gesture: ContextGesture?, reactionSelected: @escaping (ReactionContextItem.Reaction) -> Void, beganAnimatingOut: @escaping () -> Void, attemptTransitionControllerIntoNavigation: @escaping () -> Void, displayTextSelectionTip: Bool) { init(account: Account, controller: ContextController, presentationData: PresentationData, source: ContextContentSource, items: Signal<[ContextMenuItem], NoError>, reactionItems: [ReactionContextItem], beginDismiss: @escaping (ContextMenuActionResult) -> Void, recognizer: TapLongTapOrDoubleTapGestureRecognizer?, gesture: ContextGesture?, reactionSelected: @escaping (ReactionContextItem.Reaction) -> Void, beganAnimatingOut: @escaping () -> Void, attemptTransitionControllerIntoNavigation: @escaping () -> Void, tip: ContextController.Tip?) {
self.presentationData = presentationData self.presentationData = presentationData
self.source = source self.source = source
self.items = items self.items = items
@ -178,7 +178,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
self.beganAnimatingOut = beganAnimatingOut self.beganAnimatingOut = beganAnimatingOut
self.attemptTransitionControllerIntoNavigation = attemptTransitionControllerIntoNavigation self.attemptTransitionControllerIntoNavigation = attemptTransitionControllerIntoNavigation
self.gesture = gesture self.gesture = gesture
self.displayTextSelectionTip = displayTextSelectionTip self.tip = tip
self.getController = { [weak controller] in self.getController = { [weak controller] in
return controller return controller
@ -237,7 +237,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
beginDismiss(result) beginDismiss(result)
}, feedbackTap: { }, feedbackTap: {
feedbackTap?() feedbackTap?()
}, displayTextSelectionTip: self.displayTextSelectionTip, blurBackground: blurBackground) }, tip: self.tip, blurBackground: blurBackground)
if !reactionItems.isEmpty { if !reactionItems.isEmpty {
let reactionContextNode = ReactionContextNode(account: account, theme: presentationData.theme, items: reactionItems) let reactionContextNode = ReactionContextNode(account: account, theme: presentationData.theme, items: reactionItems)
@ -1199,24 +1199,25 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
self.currentActionsMinHeight = minHeight self.currentActionsMinHeight = minHeight
let previousActionsContainerNode = self.actionsContainerNode let previousActionsContainerNode = self.actionsContainerNode
let previousActionsContainerFrame = previousActionsContainerNode.view.convert(previousActionsContainerNode.bounds, to: self.view)
self.actionsContainerNode = ContextActionsContainerNode(presentationData: self.presentationData, items: items, getController: { [weak self] in self.actionsContainerNode = ContextActionsContainerNode(presentationData: self.presentationData, items: items, getController: { [weak self] in
return self?.getController() return self?.getController()
}, actionSelected: { [weak self] result in }, actionSelected: { [weak self] result in
self?.beginDismiss(result) self?.beginDismiss(result)
}, feedbackTap: { [weak self] in }, feedbackTap: { [weak self] in
self?.hapticFeedback.tap() self?.hapticFeedback.tap()
}, displayTextSelectionTip: self.displayTextSelectionTip, blurBackground: self.blurBackground) }, tip: self.tip, blurBackground: self.blurBackground)
self.scrollNode.insertSubnode(self.actionsContainerNode, aboveSubnode: previousActionsContainerNode) self.scrollNode.insertSubnode(self.actionsContainerNode, aboveSubnode: previousActionsContainerNode)
if let layout = self.validLayout { if let layout = self.validLayout {
self.updateLayout(layout: layout, transition: .animated(duration: 0.3, curve: .spring), previousActionsContainerNode: previousActionsContainerNode, previousActionsTransition: previousActionsTransition) self.updateLayout(layout: layout, transition: .animated(duration: 0.3, curve: .spring), previousActionsContainerNode: previousActionsContainerNode, previousActionsContainerFrame: previousActionsContainerFrame, previousActionsTransition: previousActionsTransition)
} else { } else {
previousActionsContainerNode.removeFromSupernode() previousActionsContainerNode.removeFromSupernode()
} }
if !self.didSetItemsReady { if !self.didSetItemsReady {
self.didSetItemsReady = true self.didSetItemsReady = true
self.displayTextSelectionTip = false self.tip = nil
self.itemsReady.set(.single(true)) self.itemsReady.set(.single(true))
} }
} }
@ -1228,11 +1229,11 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
self.actionsContainerNode.updateTheme(presentationData: presentationData) self.actionsContainerNode.updateTheme(presentationData: presentationData)
if let validLayout = self.validLayout { if let validLayout = self.validLayout {
self.updateLayout(layout: validLayout, transition: .immediate, previousActionsContainerNode: nil) self.updateLayout(layout: validLayout, transition: .immediate, previousActionsContainerNode: nil, previousActionsContainerFrame: nil)
} }
} }
func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition, previousActionsContainerNode: ContextActionsContainerNode?, previousActionsTransition: ContextController.PreviousActionsTransition = .scale) { func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition, previousActionsContainerNode: ContextActionsContainerNode?, previousActionsContainerFrame: CGRect? = nil, previousActionsTransition: ContextController.PreviousActionsTransition = .scale) {
if self.isAnimatingOut { if self.isAnimatingOut {
return return
} }
@ -1475,12 +1476,18 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
actionsContainerTransition.updateFrame(node: self.actionsContainerNode, frame: originalActionsFrame.offsetBy(dx: 0.0, dy: -overflowOffset)) actionsContainerTransition.updateFrame(node: self.actionsContainerNode, frame: originalActionsFrame.offsetBy(dx: 0.0, dy: -overflowOffset))
if isInitialLayout { if isInitialLayout {
let previousContentOffset = self.scrollNode.view.contentOffset.y
if !keepInPlace { if !keepInPlace {
self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: -overflowOffset) self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: -overflowOffset)
} }
let currentContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) let currentContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view)
var offset: CGFloat = 0.0
offset -= previousContentOffset - self.scrollNode.view.contentOffset.y
//offset += previousContainerFrame.minY - currentContainerFrame.minY
transition.animatePositionAdditive(node: self.contentContainerNode, offset: CGPoint(x: 0.0, y: offset))
if overflowOffset < 0.0 { if overflowOffset < 0.0 {
transition.animateOffsetAdditive(node: self.scrollNode, offset: currentContainerFrame.minY - previousContainerFrame.minY) let _ = currentContainerFrame
let _ = previousContainerFrame
} }
} }
@ -1650,6 +1657,10 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
}) })
self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
} else { } else {
if let previousActionsContainerFrame = previousActionsContainerFrame {
previousActionsContainerNode.frame = self.view.convert(previousActionsContainerFrame, to: self.actionsContainerNode.view.superview!)
}
switch previousActionsTransition { switch previousActionsTransition {
case .scale: case .scale:
transition.updateTransformScale(node: previousActionsContainerNode, scale: 0.1) transition.updateTransformScale(node: previousActionsContainerNode, scale: 0.1)
@ -1660,33 +1671,26 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
transition.animateTransformScale(node: self.actionsContainerNode, from: 0.1) transition.animateTransformScale(node: self.actionsContainerNode, from: 0.1)
self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
case let .slide(forward): case let .slide(forward):
if case .compact = layout.metrics.widthClass { let deltaY = self.actionsContainerNode.frame.minY - previousActionsContainerNode.frame.minY
if forward { var previousNodePosition = previousActionsContainerNode.position.offsetBy(dx: 0.0, dy: deltaY)
transition.updatePosition(node: previousActionsContainerNode, position: CGPoint(x: -previousActionsContainerNode.bounds.width / 2.0, y: previousActionsContainerNode.position.y), completion: { [weak previousActionsContainerNode] _ in let additionalHorizontalOffset: CGFloat = 20.0
previousActionsContainerNode?.removeFromSupernode() let currentNodeOffset: CGFloat
}) if forward {
transition.animatePositionAdditive(node: self.actionsContainerNode, offset: CGPoint(x: layout.size.width + self.actionsContainerNode.bounds.width / 2.0 - self.actionsContainerNode.position.x, y: 0.0)) previousNodePosition = previousNodePosition.offsetBy(dx: -previousActionsContainerNode.frame.width / 2.0 - additionalHorizontalOffset, dy: -previousActionsContainerNode.frame.height / 2.0)
} else { currentNodeOffset = self.actionsContainerNode.bounds.width / 2.0 + additionalHorizontalOffset
transition.updatePosition(node: previousActionsContainerNode, position: CGPoint(x: layout.size.width + previousActionsContainerNode.bounds.width / 2.0, y: previousActionsContainerNode.position.y), completion: { [weak previousActionsContainerNode] _ in
previousActionsContainerNode?.removeFromSupernode()
})
transition.animatePositionAdditive(node: self.actionsContainerNode, offset: CGPoint(x: -self.actionsContainerNode.bounds.width / 2.0 - self.actionsContainerNode.position.x, y: 0.0))
}
} else { } else {
let offset: CGFloat previousNodePosition = previousNodePosition.offsetBy(dx: previousActionsContainerNode.frame.width / 2.0 + additionalHorizontalOffset, dy: -previousActionsContainerNode.frame.height / 2.0)
if forward { currentNodeOffset = -self.actionsContainerNode.bounds.width / 2.0 - additionalHorizontalOffset
offset = previousActionsContainerNode.bounds.width
} else {
offset = -previousActionsContainerNode.bounds.width
}
transition.updatePosition(node: previousActionsContainerNode, position: previousActionsContainerNode.position.offsetBy(dx: -offset, dy: 0.0))
previousActionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousActionsContainerNode] _ in
previousActionsContainerNode?.removeFromSupernode()
})
transition.animatePositionAdditive(node: self.actionsContainerNode, offset: CGPoint(x: offset, y: 0.0))
self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
} }
transition.updatePosition(node: previousActionsContainerNode, position: previousNodePosition)
transition.updateTransformScale(node: previousActionsContainerNode, scale: 0.01)
previousActionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousActionsContainerNode] _ in
previousActionsContainerNode?.removeFromSupernode()
})
transition.animatePositionAdditive(node: self.actionsContainerNode, offset: CGPoint(x: currentNodeOffset, y: -deltaY - self.actionsContainerNode.bounds.height / 2.0))
transition.animateTransformScale(node: self.actionsContainerNode, from: 0.01)
self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
} }
} }
} else { } else {
@ -1868,6 +1872,11 @@ public final class ContextController: ViewController, StandalonePresentableContr
case slide(forward: Bool) case slide(forward: Bool)
} }
public enum Tip {
case textSelection
case messageViewsPrivacy
}
private let account: Account private let account: Account
private var presentationData: PresentationData private var presentationData: PresentationData
private let source: ContextContentSource private let source: ContextContentSource
@ -1881,7 +1890,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
private weak var recognizer: TapLongTapOrDoubleTapGestureRecognizer? private weak var recognizer: TapLongTapOrDoubleTapGestureRecognizer?
private weak var gesture: ContextGesture? private weak var gesture: ContextGesture?
private let displayTextSelectionTip: Bool private let tip: Tip?
private var animatedDidAppear = false private var animatedDidAppear = false
private var wasDismissed = false private var wasDismissed = false
@ -1903,7 +1912,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
private var shouldBeDismissedDisposable: Disposable? private var shouldBeDismissedDisposable: Disposable?
public init(account: Account, presentationData: PresentationData, source: ContextContentSource, items: Signal<[ContextMenuItem], NoError>, reactionItems: [ReactionContextItem], recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil, displayTextSelectionTip: Bool = false) { public init(account: Account, presentationData: PresentationData, source: ContextContentSource, items: Signal<[ContextMenuItem], NoError>, reactionItems: [ReactionContextItem], recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil, tip: Tip? = nil) {
self.account = account self.account = account
self.presentationData = presentationData self.presentationData = presentationData
self.source = source self.source = source
@ -1911,7 +1920,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
self.reactionItems = reactionItems self.reactionItems = reactionItems
self.recognizer = recognizer self.recognizer = recognizer
self.gesture = gesture self.gesture = gesture
self.displayTextSelectionTip = displayTextSelectionTip self.tip = tip
super.init(navigationBarPresentationData: nil) super.init(navigationBarPresentationData: nil)
@ -1982,7 +1991,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
default: default:
break break
} }
}, displayTextSelectionTip: self.displayTextSelectionTip) }, tip: self.tip)
self.controllerNode.dismissedForCancel = self.dismissedForCancel self.controllerNode.dismissedForCancel = self.dismissedForCancel
self.displayNodeDidLoad() self.displayNodeDidLoad()

View File

@ -78,7 +78,7 @@ final class PeekControllerNode: ViewControllerTracingNode {
activatedActionImpl?() activatedActionImpl?()
}, feedbackTap: { }, feedbackTap: {
feedbackTapImpl?() feedbackTapImpl?()
}, displayTextSelectionTip: false, blurBackground: true) }, tip: nil, blurBackground: true)
self.actionsContainerNode.alpha = 0.0 self.actionsContainerNode.alpha = 0.0
super.init() super.init()
@ -334,7 +334,7 @@ final class PeekControllerNode: ViewControllerTracingNode {
self?.requestDismiss() self?.requestDismiss()
}, feedbackTap: { [weak self] in }, feedbackTap: { [weak self] in
self?.hapticFeedback.tap() self?.hapticFeedback.tap()
}, displayTextSelectionTip: false, blurBackground: true) }, tip: nil, blurBackground: true)
self.actionsContainerNode.alpha = 0.0 self.actionsContainerNode.alpha = 0.0
self.insertSubnode(self.actionsContainerNode, aboveSubnode: previousActionsContainerNode) self.insertSubnode(self.actionsContainerNode, aboveSubnode: previousActionsContainerNode)
previousActionsContainerNode.removeFromSupernode() previousActionsContainerNode.removeFromSupernode()

View File

@ -88,11 +88,15 @@ public final class Transaction {
return self.postbox!.messageHistoryThreadHoleIndexTable.closest(peerId: peerId, threadId: threadId, namespace: namespace, space: .everywhere, range: 1 ... (Int32.max - 1)) return self.postbox!.messageHistoryThreadHoleIndexTable.closest(peerId: peerId, threadId: threadId, namespace: namespace, space: .everywhere, range: 1 ... (Int32.max - 1))
} }
public func getThreadMessageCount(peerId: PeerId, threadId: Int64, namespace: MessageId.Namespace, fromId: Int32?, toIndex: MessageIndex) -> Int? { public func getThreadMessageCount(peerId: PeerId, threadId: Int64, namespace: MessageId.Namespace, fromIdExclusive: Int32?, toIndex: MessageIndex) -> Int? {
assert(!self.disposed) assert(!self.disposed)
let fromIndex: MessageIndex? let fromIndex: MessageIndex?
if let fromId = fromId { if let fromIdExclusive = fromIdExclusive {
fromIndex = self.postbox!.messageHistoryIndexTable.closestIndex(id: MessageId(peerId: peerId, namespace: namespace, id: fromId)) if let message = self.postbox?.getMessage(MessageId(peerId: peerId, namespace: namespace, id: fromIdExclusive)) {
fromIndex = message.index.peerLocalSuccessor()
} else {
fromIndex = self.postbox!.messageHistoryIndexTable.closestIndex(id: MessageId(peerId: peerId, namespace: namespace, id: fromIdExclusive))?.peerLocalSuccessor()
}
} else { } else {
fromIndex = nil fromIndex = nil
} }

View File

@ -35,11 +35,16 @@ func _internal_messageReadStats(account: Account, id: MessageId) -> Signal<Messa
var peerIds: [PeerId] = [] var peerIds: [PeerId] = []
var missingPeerIds: [PeerId] = [] var missingPeerIds: [PeerId] = []
let authorId = transaction.getMessage(id)?.author?.id
for id in result { for id in result {
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id)) let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id))
if peerId == account.peerId { if peerId == account.peerId {
continue continue
} }
if peerId == authorId {
continue
}
peerIds.append(peerId) peerIds.append(peerId)
if transaction.getPeer(peerId) == nil { if transaction.getPeer(peerId) == nil {
missingPeerIds.append(peerId) missingPeerIds.append(peerId)

View File

@ -322,13 +322,14 @@ private class ReplyThreadHistoryContextImpl {
return return
} }
let fromId: Int32? let fromIdExclusive: Int32?
let toIndex = messageIndex let toIndex = messageIndex
if let maxReadIncomingMessageId = self.maxReadIncomingMessageIdValue { if let maxReadIncomingMessageId = self.maxReadIncomingMessageIdValue {
fromId = maxReadIncomingMessageId.id + 1 fromIdExclusive = maxReadIncomingMessageId.id
} else { } else {
fromId = nil fromIdExclusive = nil
} }
self.maxReadIncomingMessageIdValue = messageIndex.id
let account = self.account let account = self.account
@ -370,7 +371,7 @@ private class ReplyThreadHistoryContextImpl {
} }
let inputPeer = transaction.getPeer(messageIndex.id.peerId).flatMap(apiInputPeer) let inputPeer = transaction.getPeer(messageIndex.id.peerId).flatMap(apiInputPeer)
let readCount = transaction.getThreadMessageCount(peerId: messageId.peerId, threadId: makeMessageThreadId(messageId), namespace: messageId.namespace, fromId: fromId, toIndex: toIndex) let readCount = transaction.getThreadMessageCount(peerId: messageId.peerId, threadId: makeMessageThreadId(messageId), namespace: messageId.namespace, fromIdExclusive: fromIdExclusive, toIndex: toIndex)
let topMessageId = transaction.getMessagesWithThreadId(peerId: messageId.peerId, namespace: messageId.namespace, threadId: makeMessageThreadId(messageId), from: MessageIndex.upperBound(peerId: messageId.peerId, namespace: messageId.namespace), includeFrom: false, to: MessageIndex.lowerBound(peerId: messageId.peerId, namespace: messageId.namespace), limit: 1).first?.id let topMessageId = transaction.getMessagesWithThreadId(peerId: messageId.peerId, namespace: messageId.namespace, threadId: makeMessageThreadId(messageId), from: MessageIndex.upperBound(peerId: messageId.peerId, namespace: messageId.namespace), includeFrom: false, to: MessageIndex.lowerBound(peerId: messageId.peerId, namespace: messageId.namespace), limit: 1).first?.id
return (inputPeer, topMessageId, readCount) return (inputPeer, topMessageId, readCount)
@ -386,7 +387,6 @@ private class ReplyThreadHistoryContextImpl {
var revalidate = false var revalidate = false
strongSelf.maxReadIncomingMessageIdValue = messageIndex.id
var unreadCountValue = strongSelf.unreadCountValue var unreadCountValue = strongSelf.unreadCountValue
if let readCount = readCount { if let readCount = readCount {
unreadCountValue = max(0, unreadCountValue - Int(readCount)) unreadCountValue = max(0, unreadCountValue - Int(readCount))
@ -404,7 +404,12 @@ private class ReplyThreadHistoryContextImpl {
if let state = strongSelf.stateValue { if let state = strongSelf.stateValue {
if let indices = state.holeIndices[messageIndex.id.namespace] { if let indices = state.holeIndices[messageIndex.id.namespace] {
let fromIdInt = Int(fromId ?? 1) let fromIdInt: Int
if let fromIdExclusive = fromIdExclusive {
fromIdInt = Int(fromIdExclusive + 1)
} else {
fromIdInt = 1
}
let toIdInt = Int(toIndex.id.id) let toIdInt = Int(toIndex.id.id)
if fromIdInt <= toIdInt, indices.intersects(integersIn: fromIdInt ..< toIdInt) { if fromIdInt <= toIdInt, indices.intersects(integersIn: fromIdInt ..< toIdInt) {
revalidate = true revalidate = true

View File

@ -169,6 +169,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 {
case dismissedTrendingStickerPacks = 22 case dismissedTrendingStickerPacks = 22
case chatSpecificThemesDarkPreviewTip = 23 case chatSpecificThemesDarkPreviewTip = 23
case chatForwardOptionsTip = 24 case chatForwardOptionsTip = 24
case messageViewsPrivacyTips = 25
var key: ValueBoxKey { var key: ValueBoxKey {
let v = ValueBoxKey(length: 4) let v = ValueBoxKey(length: 4)
@ -292,6 +293,10 @@ private struct ApplicationSpecificNoticeKeys {
static func chatTextSelectionTip() -> NoticeEntryKey { static func chatTextSelectionTip() -> NoticeEntryKey {
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.chatTextSelectionTip.key) return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.chatTextSelectionTip.key)
} }
static func messageViewsPrivacyTips() -> NoticeEntryKey {
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.messageViewsPrivacyTips.key)
}
static func themeChangeTip() -> NoticeEntryKey { static func themeChangeTip() -> NoticeEntryKey {
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.themeChangeTip.key) return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.themeChangeTip.key)
@ -745,6 +750,28 @@ public struct ApplicationSpecificNotice {
transaction.setNotice(ApplicationSpecificNoticeKeys.chatTextSelectionTip(), ApplicationSpecificCounterNotice(value: currentValue)) transaction.setNotice(ApplicationSpecificNoticeKeys.chatTextSelectionTip(), ApplicationSpecificCounterNotice(value: currentValue))
} }
} }
public static func getMessageViewsPrivacyTips(accountManager: AccountManager<TelegramAccountManagerTypes>) -> Signal<Int32, NoError> {
return accountManager.transaction { transaction -> Int32 in
if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.messageViewsPrivacyTips()) as? ApplicationSpecificCounterNotice {
return value.value
} else {
return 0
}
}
}
public static func incrementMessageViewsPrivacyTips(accountManager: AccountManager<TelegramAccountManagerTypes>, count: Int32 = 1) -> Signal<Void, NoError> {
return accountManager.transaction { transaction -> Void in
var currentValue: Int32 = 0
if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.messageViewsPrivacyTips()) as? ApplicationSpecificCounterNotice {
currentValue = value.value
}
currentValue += count
transaction.setNotice(ApplicationSpecificNoticeKeys.messageViewsPrivacyTips(), ApplicationSpecificCounterNotice(value: currentValue))
}
}
public static func getThemeChangeTip(accountManager: AccountManager<TelegramAccountManagerTypes>) -> Signal<Bool, NoError> { public static func getThemeChangeTip(accountManager: AccountManager<TelegramAccountManagerTypes>) -> Signal<Bool, NoError> {
return accountManager.transaction { transaction -> Bool in return accountManager.transaction { transaction -> Bool in

View File

@ -924,8 +924,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
break break
} }
} }
let _ = combineLatest(queue: .mainQueue(), contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, context: strongSelf.context, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, selectAll: selectAll, interfaceInteraction: strongSelf.interfaceInteraction), strongSelf.context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false), ApplicationSpecificNotice.getChatTextSelectionTips(accountManager: strongSelf.context.sharedContext.accountManager) let _ = combineLatest(queue: .mainQueue(), contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, context: strongSelf.context, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, selectAll: selectAll, interfaceInteraction: strongSelf.interfaceInteraction), strongSelf.context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false), ApplicationSpecificNotice.getChatTextSelectionTips(accountManager: strongSelf.context.sharedContext.accountManager),
).start(next: { actions, animatedEmojiStickers, chatTextSelectionTips in ApplicationSpecificNotice.getMessageViewsPrivacyTips(accountManager: strongSelf.context.sharedContext.accountManager)
).start(next: { actions, animatedEmojiStickers, chatTextSelectionTips, messageViewsPrivacyTips in
guard let strongSelf = self, !actions.isEmpty else { guard let strongSelf = self, !actions.isEmpty else {
return return
} }
@ -958,14 +959,32 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if Namespaces.Message.allScheduled.contains(message.id.namespace) || message.id.peerId.namespace == Namespaces.Peer.SecretChat { if Namespaces.Message.allScheduled.contains(message.id.namespace) || message.id.peerId.namespace == Namespaces.Peer.SecretChat {
reactionItems = [] reactionItems = []
} }
let numberOfComponents = message.text.components(separatedBy: CharacterSet.whitespacesAndNewlines).count var tip: ContextController.Tip?
let displayTextSelectionTip = numberOfComponents >= 3 && !message.text.isEmpty && chatTextSelectionTips < 3 for action in actions {
if displayTextSelectionTip { switch action {
let _ = ApplicationSpecificNotice.incrementChatTextSelectionTips(accountManager: strongSelf.context.sharedContext.accountManager).start() case let .custom(item, _):
if item is ChatReadReportContextItem {
if messageViewsPrivacyTips < 3 {
tip = .messageViewsPrivacy
let _ = ApplicationSpecificNotice.incrementMessageViewsPrivacyTips(accountManager: strongSelf.context.sharedContext.accountManager).start()
}
}
default:
break
}
}
if tip == nil {
let numberOfComponents = message.text.components(separatedBy: CharacterSet.whitespacesAndNewlines).count
let displayTextSelectionTip = numberOfComponents >= 3 && !message.text.isEmpty && chatTextSelectionTips < 3
if displayTextSelectionTip {
let _ = ApplicationSpecificNotice.incrementChatTextSelectionTips(accountManager: strongSelf.context.sharedContext.accountManager).start()
tip = .textSelection
}
} }
let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, postbox: strongSelf.context.account.postbox, message: message, selectAll: selectAll)), items: .single(actions), reactionItems: reactionItems, recognizer: recognizer, gesture: gesture, displayTextSelectionTip: displayTextSelectionTip) let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, postbox: strongSelf.context.account.postbox, message: message, selectAll: selectAll)), items: .single(actions), reactionItems: reactionItems, recognizer: recognizer, gesture: gesture, tip: tip)
strongSelf.currentContextController = controller strongSelf.currentContextController = controller
controller.reactionSelected = { [weak controller] value in controller.reactionSelected = { [weak controller] value in
guard let strongSelf = self, let message = updatedMessages.first else { guard let strongSelf = self, let message = updatedMessages.first else {

View File

@ -141,15 +141,32 @@ private func canEditMessage(accountPeerId: PeerId, limitsConfiguration: LimitsCo
} }
private func canViewReadStats(message: Message, isMessageRead: Bool, appConfig: AppConfiguration) -> Bool { private func canViewReadStats(message: Message, isMessageRead: Bool, appConfig: AppConfiguration) -> Bool {
if !isMessageRead {
return false
}
if message.flags.contains(.Incoming) {
return false
}
guard let peer = message.peers[message.id.peerId] else { guard let peer = message.peers[message.id.peerId] else {
return false return false
} }
if message.flags.contains(.Incoming) {
switch peer {
case let channel as TelegramChannel:
if channel.adminRights == nil {
return false
}
case let group as TelegramGroup:
switch group.role {
case .creator, .admin:
break
case .member:
return false
}
default:
return false
}
} else {
if !isMessageRead {
return false
}
}
for media in message.media { for media in message.media {
if let _ = media as? TelegramMediaAction { if let _ = media as? TelegramMediaAction {
return false return false