From 7020c5ed0562076cb7b4be338c41c2e296d340d3 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Mon, 5 Sep 2022 03:57:37 +0400 Subject: [PATCH] Status and reaction improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 9 + .../Sources/ChatListController.swift | 10 +- .../ContextUI/Sources/ContextController.swift | 7 +- .../ContextControllerActionsStackNode.swift | 20 +- ...tControllerExtractedPresentationNode.swift | 1 + submodules/Display/Source/NavigationBar.swift | 10 +- submodules/Postbox/Sources/Peer.swift | 1 + submodules/Postbox/Sources/PeerTable.swift | 23 +- .../Sources/PeerTimeoutAttributesView.swift | 45 +++ .../Sources/PeerTimeoutPropertiesTable.swift | 102 ++++++ submodules/Postbox/Sources/Postbox.swift | 20 +- .../Postbox/Sources/PostboxTransaction.swift | 7 +- submodules/Postbox/Sources/Views.swift | 11 + .../Sources/ReactionContextNode.swift | 55 ++- .../Sources/ReactionSelectionNode.swift | 21 ++ .../Sources/Account/Account.swift | 1 + .../ManagedAutoremoveMessageOperations.swift | 1 - ...agedPeerTimestampAttributeOperations.swift | 110 ++++++ .../SyncCore/SyncCore_TelegramChannel.swift | 2 + .../SyncCore/SyncCore_TelegramGroup.swift | 2 + .../SyncCore_TelegramSecretChat.swift | 2 + .../SyncCore/SyncCore_TelegramUser.swift | 12 + .../TelegramEngineAccountData.swift | 2 +- .../Sources/PresenceStrings.swift | 24 ++ .../EmojiStatusSelectionComponent/BUILD | 3 + .../Sources/EmojiStatusPreviewScreen.swift | 321 +++++++++++++++++- .../EmojiStatusSelectionComponent.swift | 7 +- .../Sources/EmojiPagerContentComponent.swift | 34 +- .../TelegramUI/Sources/ChatController.swift | 18 +- .../Sources/ChatControllerNode.swift | 2 +- .../ChatInterfaceTitlePanelNodes.swift | 4 +- .../ChatReportPeerTitlePanelNode.swift | 78 ++++- .../Sources/PeerInfo/PeerInfoScreen.swift | 9 +- 33 files changed, 907 insertions(+), 67 deletions(-) create mode 100644 submodules/Postbox/Sources/PeerTimeoutAttributesView.swift create mode 100644 submodules/Postbox/Sources/PeerTimeoutPropertiesTable.swift create mode 100644 submodules/TelegramCore/Sources/State/ManagedPeerTimestampAttributeOperations.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 0f87ce23d3..120e3a23d0 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -8022,3 +8022,12 @@ Sorry for the inconvenience."; "SetTimeoutFor.Days_1" = "Set for 1 day"; "SetTimeoutFor.Days_any" = "Set for %@ days"; + +"PeerStatusExpiration.Minutes_1" = "Your status expires in one minute"; +"PeerStatusExpiration.Minutes_any" = "Your status expires in %@ minutes"; +"PeerStatusExpiration.Hours_1" = "Your status expires in one hour"; +"PeerStatusExpiration.Hours_any" = "Your status expires in %@ hours"; +"PeerStatusExpiration.TomorrowAt" = "Your status expires tomorrow at %@"; +"PeerStatusExpiration.AtDate" = "Your status expires on %@"; + +"Chat.PanelCustomStatusInfo" = "This account uses %@ as a custom status next to its name. Such emoji statuses are available to all subscribers of Telegram Premium."; diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 83246e2df4..81947572ac 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -35,6 +35,7 @@ import AnimationCache import MultiAnimationRenderer import EmojiStatusSelectionComponent import EntityKeyboard +import TelegramStringFormatting private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool { if listNode.scroller.isDragging { @@ -851,8 +852,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController private func openStatusSetup(sourceView: UIView) { self.emojiStatusSelectionController?.dismiss() var selectedItems = Set() + //TODO:localize + var topStatusTitle = "Long tap to set a timer" if let peerStatus = self.titleView.title.peerStatus, case let .emoji(emojiStatus) = peerStatus { selectedItems.insert(MediaId(namespace: Namespaces.Media.CloudFile, id: emojiStatus.fileId)) + + if let timestamp = emojiStatus.expirationDate { + topStatusTitle = peerStatusExpirationString(statusTimestamp: timestamp, relativeTo: Int32(Date().timeIntervalSince1970), strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat) + } } let controller = EmojiStatusSelectionController( context: self.context, @@ -869,7 +876,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController areUnicodeEmojiEnabled: false, areCustomEmojiEnabled: true, chatPeerId: self.context.account.peerId, - selectedItems: selectedItems + selectedItems: selectedItems, + topStatusTitle: topStatusTitle ), destinationItemView: { [weak sourceView] in return sourceView diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 96b2a2be29..00f73ae31f 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -1522,7 +1522,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } if !items.reactionItems.isEmpty, let context = items.context, let animationCache = items.animationCache { - let reactionContextNode = ReactionContextNode(context: context, animationCache: animationCache, presentationData: self.presentationData, items: items.reactionItems, getEmojiContent: items.getEmojiContent, isExpandedUpdated: { _ in }, requestLayout: { _ in }) + let reactionContextNode = ReactionContextNode(context: context, animationCache: animationCache, presentationData: self.presentationData, items: items.reactionItems, selectedItems: items.selectedReactionItems, getEmojiContent: items.getEmojiContent, isExpandedUpdated: { _ in }, requestLayout: { _ in }) self.reactionContextNode = reactionContextNode self.addSubnode(reactionContextNode) @@ -2399,17 +2399,19 @@ public final class ContextController: ViewController, StandalonePresentableContr public var content: Content public var context: AccountContext? public var reactionItems: [ReactionContextItem] + public var selectedReactionItems: Set public var animationCache: AnimationCache? public var getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)? public var disablePositionLock: Bool public var tip: Tip? public var tipSignal: Signal? - public init(content: Content, context: AccountContext? = nil, reactionItems: [ReactionContextItem] = [], animationCache: AnimationCache? = nil, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)? = nil, disablePositionLock: Bool = false, tip: Tip? = nil, tipSignal: Signal? = nil) { + public init(content: Content, context: AccountContext? = nil, reactionItems: [ReactionContextItem] = [], selectedReactionItems: Set = Set(), animationCache: AnimationCache? = nil, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)? = nil, disablePositionLock: Bool = false, tip: Tip? = nil, tipSignal: Signal? = nil) { self.content = content self.context = context self.animationCache = animationCache self.reactionItems = reactionItems + self.selectedReactionItems = selectedReactionItems self.getEmojiContent = getEmojiContent self.disablePositionLock = disablePositionLock self.tip = tip @@ -2420,6 +2422,7 @@ public final class ContextController: ViewController, StandalonePresentableContr self.content = .list([]) self.context = nil self.reactionItems = [] + self.selectedReactionItems = Set() self.getEmojiContent = nil self.disablePositionLock = false self.tip = nil diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index 93a70ca8d9..706342a77e 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -42,7 +42,7 @@ public protocol ContextControllerActionsStackItem: AnyObject { var tip: ContextController.Tip? { get } var tipSignal: Signal? { get } - var reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? { get } + var reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? { get } } protocol ContextControllerActionsListItemNode: ASDisplayNode { @@ -631,13 +631,13 @@ final class ContextControllerActionsListStackItem: ContextControllerActionsStack } private let items: [ContextMenuItem] - let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? + let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? let tip: ContextController.Tip? let tipSignal: Signal? init( items: [ContextMenuItem], - reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)?, + reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)?, tip: ContextController.Tip?, tipSignal: Signal? ) { @@ -723,13 +723,13 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta } private let content: ContextControllerItemsContent - let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? + let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? let tip: ContextController.Tip? let tipSignal: Signal? init( content: ContextControllerItemsContent, - reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)?, + reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)?, tip: ContextController.Tip?, tipSignal: Signal? ) { @@ -755,9 +755,9 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta } func makeContextControllerActionsStackItem(items: ContextController.Items) -> ContextControllerActionsStackItem { - var reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? + var reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? if let context = items.context, let animationCache = items.animationCache, !items.reactionItems.isEmpty { - reactionItems = (context, items.reactionItems, animationCache, items.getEmojiContent) + reactionItems = (context, items.reactionItems, items.selectedReactionItems, animationCache, items.getEmojiContent) } switch items.content { case let .list(listItems): @@ -874,7 +874,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode { var tip: ContextController.Tip? let tipSignal: Signal? var tipNode: InnerTextSelectionTipContainerNode? - let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? + let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? var storedScrollingState: CGFloat? let positionLock: CGFloat? @@ -888,7 +888,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode { item: ContextControllerActionsStackItem, tip: ContextController.Tip?, tipSignal: Signal?, - reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)?, + reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)?, positionLock: CGFloat? ) { self.getController = getController @@ -1032,7 +1032,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode { private var selectionPanGesture: UIPanGestureRecognizer? - var topReactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? { + var topReactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? { return self.itemContainers.last?.reactionItems } diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index a7ebbdaf55..7b3a024752 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -518,6 +518,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo animationCache: reactionItems.animationCache, presentationData: presentationData, items: reactionItems.reactionItems, + selectedItems: reactionItems.selectedReactionItems, getEmojiContent: reactionItems.getEmojiContent, isExpandedUpdated: { [weak self] transition in guard let strongSelf = self else { diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index fdbe56e465..2a7196c5f0 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -387,7 +387,7 @@ open class BlurredBackgroundView: UIView { self.updateBackgroundBlur(forceKeepBlur: forceKeepBlur) } - public func update(size: CGSize, cornerRadius: CGFloat = 0.0, transition: ContainedViewLayoutTransition) { + public func update(size: CGSize, cornerRadius: CGFloat = 0.0, maskedCorners: CACornerMask = [.layerMaxXMaxYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMinXMinYCorner], transition: ContainedViewLayoutTransition) { self.validLayout = (size, cornerRadius) let contentFrame = CGRect(origin: CGPoint(), size: size) @@ -400,11 +400,19 @@ open class BlurredBackgroundView: UIView { } } } + + if #available(iOS 11.0, *) { + self.backgroundView.layer.maskedCorners = maskedCorners + } transition.updateCornerRadius(layer: self.backgroundView.layer, cornerRadius: cornerRadius) if let effectView = self.effectView { transition.updateCornerRadius(layer: effectView.layer, cornerRadius: cornerRadius) effectView.clipsToBounds = !cornerRadius.isZero + + if #available(iOS 11.0, *) { + effectView.layer.maskedCorners = maskedCorners + } } } diff --git a/submodules/Postbox/Sources/Peer.swift b/submodules/Postbox/Sources/Peer.swift index a3675a7fcd..59bf6a6ee9 100644 --- a/submodules/Postbox/Sources/Peer.swift +++ b/submodules/Postbox/Sources/Peer.swift @@ -300,6 +300,7 @@ public protocol Peer: AnyObject, PostboxCoding { var associatedPeerId: PeerId? { get } var notificationSettingsPeerId: PeerId? { get } var associatedMediaIds: [MediaId]? { get } + var timeoutAttribute: UInt32? { get } func isEqual(_ other: Peer) -> Bool } diff --git a/submodules/Postbox/Sources/PeerTable.swift b/submodules/Postbox/Sources/PeerTable.swift index c515cd5a21..207fff8f63 100644 --- a/submodules/Postbox/Sources/PeerTable.swift +++ b/submodules/Postbox/Sources/PeerTable.swift @@ -6,7 +6,7 @@ final class PeerTable: Table { } private let reverseAssociatedTable: ReverseAssociatedPeerTable - //private let peerTimeoutPropertiesTable: PeerTimeoutPropertiesTable + private let peerTimeoutPropertiesTable: PeerTimeoutPropertiesTable private let sharedEncoder = PostboxEncoder() private let sharedKey = ValueBoxKey(length: 8) @@ -14,8 +14,9 @@ final class PeerTable: Table { private var cachedPeers: [PeerId: Peer] = [:] private var updatedInitialPeers: [PeerId: Peer?] = [:] - init(valueBox: ValueBox, table: ValueBoxTable, useCaches: Bool, reverseAssociatedTable: ReverseAssociatedPeerTable) { + init(valueBox: ValueBox, table: ValueBoxTable, useCaches: Bool, reverseAssociatedTable: ReverseAssociatedPeerTable, peerTimeoutPropertiesTable: PeerTimeoutPropertiesTable) { self.reverseAssociatedTable = reverseAssociatedTable + self.peerTimeoutPropertiesTable = peerTimeoutPropertiesTable super.init(valueBox: valueBox, table: table, useCaches: useCaches) } @@ -64,6 +65,24 @@ final class PeerTable: Table { return result } + func commitDependentTables() { + for (peerId, previousPeer) in self.updatedInitialPeers { + if let peer = self.cachedPeers[peerId] { + let previousTimeout = previousPeer?.timeoutAttribute + if previousTimeout != peer.timeoutAttribute { + if let previousTimeout = previousTimeout { + self.peerTimeoutPropertiesTable.remove(peerId: peerId, timestamp: previousTimeout) + } + if let updatedTimeout = peer.timeoutAttribute { + self.peerTimeoutPropertiesTable.add(peerId: peerId, timestamp: updatedTimeout) + } + } + } else { + assertionFailure() + } + } + } + override func beforeCommit() { if !self.updatedInitialPeers.isEmpty { for (peerId, previousPeer) in self.updatedInitialPeers { diff --git a/submodules/Postbox/Sources/PeerTimeoutAttributesView.swift b/submodules/Postbox/Sources/PeerTimeoutAttributesView.swift new file mode 100644 index 0000000000..eb676df4ba --- /dev/null +++ b/submodules/Postbox/Sources/PeerTimeoutAttributesView.swift @@ -0,0 +1,45 @@ +import Foundation + +final class MutablePeerTimeoutAttributesView: MutablePostboxView { + fileprivate var minValue: (peerId: PeerId, timestamp: UInt32)? + + init(postbox: PostboxImpl) { + self.minValue = postbox.peerTimeoutPropertiesTable.min() + } + + func replay(postbox: PostboxImpl, transaction: PostboxTransaction) -> Bool { + var updated = false + + if transaction.updatedPeerTimeoutAttributes { + let minValue = postbox.peerTimeoutPropertiesTable.min() + if self.minValue?.0 != minValue?.0 || self.minValue?.1 != minValue?.1 { + updated = true + self.minValue = minValue + } + } + + return updated + } + + func refreshDueToExternalTransaction(postbox: PostboxImpl) -> Bool { + let minValue = postbox.peerTimeoutPropertiesTable.min() + if self.minValue?.0 != minValue?.0 || self.minValue?.1 != minValue?.1 { + self.minValue = minValue + return true + } else { + return false + } + } + + func immutableView() -> PostboxView { + return PeerTimeoutAttributesView(self) + } +} + +public final class PeerTimeoutAttributesView: PostboxView { + public let minValue: (peerId: PeerId, timestamp: UInt32)? + + init(_ view: MutablePeerTimeoutAttributesView) { + self.minValue = view.minValue + } +} diff --git a/submodules/Postbox/Sources/PeerTimeoutPropertiesTable.swift b/submodules/Postbox/Sources/PeerTimeoutPropertiesTable.swift new file mode 100644 index 0000000000..d3e05895c3 --- /dev/null +++ b/submodules/Postbox/Sources/PeerTimeoutPropertiesTable.swift @@ -0,0 +1,102 @@ +import Foundation + +final class PeerTimeoutPropertiesTable: Table { + private struct Key: Hashable { + var peerId: PeerId + var timestamp: UInt32 + } + + static func tableSpec(_ id: Int32) -> ValueBoxTable { + return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: false) + } + + private let sharedKey = ValueBoxKey(length: 4 + 8) + + private var cache: [Key: Bool] = [:] + private var updated = Set() + + var hasUpdates: Bool { + return !self.updated.isEmpty + } + + private func key(_ key: Key) -> ValueBoxKey { + self.sharedKey.setInt32(0, value: Int32(bitPattern: key.timestamp)) + self.sharedKey.setInt64(4, value: key.peerId.toInt64()) + return self.sharedKey + } + + private func lowerBound() -> ValueBoxKey { + let key = ValueBoxKey(length: 4 + 8) + key.setInt32(0, value: 0) + return key + } + + private func upperBound() -> ValueBoxKey { + let key = ValueBoxKey(length: 4 + 8) + key.setInt32(0, value: Int32(bitPattern: UInt32.max)) + key.setInt64(4, value: Int64(bitPattern: UInt64.max)) + return key + } + + func min() -> (peerId: PeerId, timestamp: UInt32)? { + var result: Key? + self.valueBox.range(self.table, start: self.lowerBound(), end: self.upperBound(), keys: { key in + result = Key(peerId: PeerId(key.getInt64(4)), timestamp: UInt32(bitPattern: key.getInt32(0))) + return false + }, limit: 1) + return result.flatMap { result -> (peerId: PeerId, timestamp: UInt32) in + return (result.peerId, result.timestamp) + } + } + + private func get(peerId: PeerId, timestamp: UInt32) -> Bool { + let key = Key(peerId: peerId, timestamp: timestamp) + if let cachedValue = self.cache[key] { + return cachedValue + } else { + let value = self.valueBox.exists(self.table, key: self.key(Key(peerId: peerId, timestamp: timestamp))) + self.cache[key] = value + return value + } + } + + func remove(peerId: PeerId, timestamp: UInt32) { + let key = Key(peerId: peerId, timestamp: timestamp) + if self.get(peerId: peerId, timestamp: timestamp) { + self.cache[key] = false + self.updated.insert(key) + } + } + + func add(peerId: PeerId, timestamp: UInt32) { + let key = Key(peerId: peerId, timestamp: timestamp) + if !self.get(peerId: peerId, timestamp: timestamp) { + self.cache[key] = true + self.updated.insert(key) + } + } + + override func clearMemoryCache() { + self.cache.removeAll() + } + + override func beforeCommit() { + if !self.updated.isEmpty { + for key in self.updated { + if let value = self.cache[key] { + if value { + self.valueBox.set(self.table, key: self.key(key), value: MemoryBuffer()) + } else { + self.valueBox.remove(self.table, key: self.key(key), secure: false) + } + } + } + + self.updated.removeAll() + + if !self.useCaches { + self.cache.removeAll() + } + } + } +} diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index 759dac9868..d508b4ad20 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -1153,6 +1153,11 @@ public final class Transaction { assert(!self.disposed) self.postbox?.clearTimestampBasedAttribute(id: id, tag: tag) } + + public func removePeerTimeoutAttributeEntry(peerId: PeerId, timestamp: UInt32) { + assert(!self.disposed) + self.postbox?.removePeerTimeoutAttributeEntry(peerId: peerId, timestamp: timestamp) + } } public enum PostboxResult { @@ -1464,6 +1469,7 @@ final class PostboxImpl { let deviceContactImportInfoTable: DeviceContactImportInfoTable let messageHistoryHoleIndexTable: MessageHistoryHoleIndexTable let groupMessageStatsTable: GroupMessageStatsTable + let peerTimeoutPropertiesTable: PeerTimeoutPropertiesTable //temporary let peerRatingTable: RatingTable @@ -1487,7 +1493,8 @@ final class PostboxImpl { self.keychainTable = KeychainTable(valueBox: self.valueBox, table: KeychainTable.tableSpec(1), useCaches: useCaches) self.reverseAssociatedPeerTable = ReverseAssociatedPeerTable(valueBox: self.valueBox, table:ReverseAssociatedPeerTable.tableSpec(40), useCaches: useCaches) - self.peerTable = PeerTable(valueBox: self.valueBox, table: PeerTable.tableSpec(2), useCaches: useCaches, reverseAssociatedTable: self.reverseAssociatedPeerTable) + self.peerTimeoutPropertiesTable = PeerTimeoutPropertiesTable(valueBox: self.valueBox, table: PeerTimeoutPropertiesTable.tableSpec(64), useCaches: useCaches) + self.peerTable = PeerTable(valueBox: self.valueBox, table: PeerTable.tableSpec(2), useCaches: useCaches, reverseAssociatedTable: self.reverseAssociatedPeerTable, peerTimeoutPropertiesTable: self.peerTimeoutPropertiesTable) self.globalMessageIdsTable = GlobalMessageIdsTable(valueBox: self.valueBox, table: GlobalMessageIdsTable.tableSpec(3), useCaches: useCaches, seedConfiguration: seedConfiguration) self.globallyUniqueMessageIdsTable = MessageGloballyUniqueIdTable(valueBox: self.valueBox, table: MessageGloballyUniqueIdTable.tableSpec(32), useCaches: useCaches) self.messageHistoryMetadataTable = MessageHistoryMetadataTable(valueBox: self.valueBox, table: MessageHistoryMetadataTable.tableSpec(10), useCaches: useCaches) @@ -1604,6 +1611,7 @@ final class PostboxImpl { tables.append(self.deviceContactImportInfoTable) tables.append(self.messageHistoryHoleIndexTable) tables.append(self.groupMessageStatsTable) + tables.append(self.peerTimeoutPropertiesTable) self.tables = tables @@ -1992,11 +2000,15 @@ final class PostboxImpl { let transactionParticipationInTotalUnreadCountUpdates = self.peerNotificationSettingsTable.transactionParticipationInTotalUnreadCountUpdates(postbox: self, transaction: currentTransaction) self.chatListIndexTable.commitWithTransaction(postbox: self, currentTransaction: currentTransaction, alteredInitialPeerCombinedReadStates: alteredInitialPeerCombinedReadStates, updatedPeers: updatedPeers, transactionParticipationInTotalUnreadCountUpdates: transactionParticipationInTotalUnreadCountUpdates, updatedTotalUnreadStates: &self.currentUpdatedTotalUnreadStates, updatedGroupTotalUnreadSummaries: &self.currentUpdatedGroupTotalUnreadSummaries, currentUpdatedGroupSummarySynchronizeOperations: &self.currentUpdatedGroupSummarySynchronizeOperations) + self.peerTable.commitDependentTables() + if self.currentNeedsReindexUnreadCounters { self.reindexUnreadCounters(currentTransaction: currentTransaction) } - let transaction = PostboxTransaction(currentUpdatedState: self.currentUpdatedState, currentPeerHoleOperations: self.currentPeerHoleOperations, currentOperationsByPeerId: self.currentOperationsByPeerId, chatListOperations: self.currentChatListOperations, currentUpdatedChatListInclusions: self.currentUpdatedChatListInclusions, currentUpdatedPeers: self.currentUpdatedPeers, currentUpdatedPeerNotificationSettings: self.currentUpdatedPeerNotificationSettings, currentUpdatedPeerNotificationBehaviorTimestamps: self.currentUpdatedPeerNotificationBehaviorTimestamps, currentUpdatedCachedPeerData: self.currentUpdatedCachedPeerData, currentUpdatedPeerPresences: currentUpdatedPeerPresences, currentUpdatedPeerChatListEmbeddedStates: self.currentUpdatedPeerChatListEmbeddedStates, currentUpdatedTotalUnreadStates: self.currentUpdatedTotalUnreadStates, currentUpdatedTotalUnreadSummaries: self.currentUpdatedGroupTotalUnreadSummaries, alteredInitialPeerCombinedReadStates: alteredInitialPeerCombinedReadStates, currentPeerMergedOperationLogOperations: self.currentPeerMergedOperationLogOperations, currentTimestampBasedMessageAttributesOperations: self.currentTimestampBasedMessageAttributesOperations, unsentMessageOperations: self.currentUnsentOperations, updatedSynchronizePeerReadStateOperations: self.currentUpdatedSynchronizeReadStateOperations, currentUpdatedGroupSummarySynchronizeOperations: self.currentUpdatedGroupSummarySynchronizeOperations, currentPreferencesOperations: self.currentPreferencesOperations, currentOrderedItemListOperations: self.currentOrderedItemListOperations, currentItemCollectionItemsOperations: self.currentItemCollectionItemsOperations, currentItemCollectionInfosOperations: self.currentItemCollectionInfosOperations, currentUpdatedPeerChatStates: self.currentUpdatedPeerChatStates, currentGlobalTagsOperations: self.currentGlobalTagsOperations, currentLocalTagsOperations: self.currentLocalTagsOperations, updatedMedia: self.currentUpdatedMedia, replaceRemoteContactCount: self.currentReplaceRemoteContactCount, replaceContactPeerIds: self.currentReplacedContactPeerIds, currentPendingMessageActionsOperations: self.currentPendingMessageActionsOperations, currentUpdatedMessageActionsSummaries: self.currentUpdatedMessageActionsSummaries, currentUpdatedMessageTagSummaries: self.currentUpdatedMessageTagSummaries, currentInvalidateMessageTagSummaries: self.currentInvalidateMessageTagSummaries, currentUpdatedPendingPeerNotificationSettings: self.currentUpdatedPendingPeerNotificationSettings, replacedAdditionalChatListItems: self.currentReplacedAdditionalChatListItems, updatedNoticeEntryKeys: self.currentUpdatedNoticeEntryKeys, updatedCacheEntryKeys: self.currentUpdatedCacheEntryKeys, currentUpdatedMasterClientId: currentUpdatedMasterClientId, updatedFailedMessagePeerIds: self.messageHistoryFailedTable.updatedPeerIds, updatedFailedMessageIds: self.messageHistoryFailedTable.updatedMessageIds, updatedGlobalNotificationSettings: self.currentNeedsReindexUnreadCounters) + let updatedPeerTimeoutAttributes = self.peerTimeoutPropertiesTable.hasUpdates + + let transaction = PostboxTransaction(currentUpdatedState: self.currentUpdatedState, currentPeerHoleOperations: self.currentPeerHoleOperations, currentOperationsByPeerId: self.currentOperationsByPeerId, chatListOperations: self.currentChatListOperations, currentUpdatedChatListInclusions: self.currentUpdatedChatListInclusions, currentUpdatedPeers: self.currentUpdatedPeers, currentUpdatedPeerNotificationSettings: self.currentUpdatedPeerNotificationSettings, currentUpdatedPeerNotificationBehaviorTimestamps: self.currentUpdatedPeerNotificationBehaviorTimestamps, currentUpdatedCachedPeerData: self.currentUpdatedCachedPeerData, currentUpdatedPeerPresences: currentUpdatedPeerPresences, currentUpdatedPeerChatListEmbeddedStates: self.currentUpdatedPeerChatListEmbeddedStates, currentUpdatedTotalUnreadStates: self.currentUpdatedTotalUnreadStates, currentUpdatedTotalUnreadSummaries: self.currentUpdatedGroupTotalUnreadSummaries, alteredInitialPeerCombinedReadStates: alteredInitialPeerCombinedReadStates, currentPeerMergedOperationLogOperations: self.currentPeerMergedOperationLogOperations, currentTimestampBasedMessageAttributesOperations: self.currentTimestampBasedMessageAttributesOperations, unsentMessageOperations: self.currentUnsentOperations, updatedSynchronizePeerReadStateOperations: self.currentUpdatedSynchronizeReadStateOperations, currentUpdatedGroupSummarySynchronizeOperations: self.currentUpdatedGroupSummarySynchronizeOperations, currentPreferencesOperations: self.currentPreferencesOperations, currentOrderedItemListOperations: self.currentOrderedItemListOperations, currentItemCollectionItemsOperations: self.currentItemCollectionItemsOperations, currentItemCollectionInfosOperations: self.currentItemCollectionInfosOperations, currentUpdatedPeerChatStates: self.currentUpdatedPeerChatStates, currentGlobalTagsOperations: self.currentGlobalTagsOperations, currentLocalTagsOperations: self.currentLocalTagsOperations, updatedMedia: self.currentUpdatedMedia, replaceRemoteContactCount: self.currentReplaceRemoteContactCount, replaceContactPeerIds: self.currentReplacedContactPeerIds, currentPendingMessageActionsOperations: self.currentPendingMessageActionsOperations, currentUpdatedMessageActionsSummaries: self.currentUpdatedMessageActionsSummaries, currentUpdatedMessageTagSummaries: self.currentUpdatedMessageTagSummaries, currentInvalidateMessageTagSummaries: self.currentInvalidateMessageTagSummaries, currentUpdatedPendingPeerNotificationSettings: self.currentUpdatedPendingPeerNotificationSettings, replacedAdditionalChatListItems: self.currentReplacedAdditionalChatListItems, updatedNoticeEntryKeys: self.currentUpdatedNoticeEntryKeys, updatedCacheEntryKeys: self.currentUpdatedCacheEntryKeys, currentUpdatedMasterClientId: currentUpdatedMasterClientId, updatedFailedMessagePeerIds: self.messageHistoryFailedTable.updatedPeerIds, updatedFailedMessageIds: self.messageHistoryFailedTable.updatedMessageIds, updatedGlobalNotificationSettings: self.currentNeedsReindexUnreadCounters, updatedPeerTimeoutAttributes: updatedPeerTimeoutAttributes) var updatedTransactionState: Int64? var updatedMasterClientId: Int64? if !transaction.isEmpty { @@ -3645,6 +3657,10 @@ final class PostboxImpl { self.timestampBasedMessageAttributesTable.remove(tag: tag, id: id, operations: &self.currentTimestampBasedMessageAttributesOperations) } + fileprivate func removePeerTimeoutAttributeEntry(peerId: PeerId, timestamp: UInt32) { + self.peerTimeoutPropertiesTable.remove(peerId: peerId, timestamp: timestamp) + } + fileprivate func reindexUnreadCounters(currentTransaction: Transaction) { self.groupMessageStatsTable.removeAll() let _ = CFAbsoluteTimeGetCurrent() diff --git a/submodules/Postbox/Sources/PostboxTransaction.swift b/submodules/Postbox/Sources/PostboxTransaction.swift index 03113f3a8f..6e96ccf0d8 100644 --- a/submodules/Postbox/Sources/PostboxTransaction.swift +++ b/submodules/Postbox/Sources/PostboxTransaction.swift @@ -43,6 +43,7 @@ final class PostboxTransaction { let updatedFailedMessagePeerIds: Set let updatedFailedMessageIds: Set let updatedGlobalNotificationSettings: Bool + let updatedPeerTimeoutAttributes: Bool var isEmpty: Bool { if currentUpdatedState != nil { @@ -171,10 +172,13 @@ final class PostboxTransaction { if self.updatedGlobalNotificationSettings { return false } + if self.updatedPeerTimeoutAttributes { + return false + } return true } - 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: Set, currentUpdatedTotalUnreadStates: [PeerGroupId: 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, currentGlobalTagsOperations: [GlobalMessageHistoryTagsOperation], currentLocalTagsOperations: [IntermediateMessageHistoryLocalTagsOperation], updatedMedia: [MediaId: Media?], replaceRemoteContactCount: Int32?, replaceContactPeerIds: Set?, currentPendingMessageActionsOperations: [PendingMessageActionsOperation], currentUpdatedMessageActionsSummaries: [PendingMessageActionsSummaryKey: Int32], currentUpdatedMessageTagSummaries: [MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary], currentInvalidateMessageTagSummaries: [InvalidatedMessageHistoryTagsSummaryEntryOperation], currentUpdatedPendingPeerNotificationSettings: Set, replacedAdditionalChatListItems: [AdditionalChatListItem]?, updatedNoticeEntryKeys: Set, updatedCacheEntryKeys: Set, currentUpdatedMasterClientId: Int64?, updatedFailedMessagePeerIds: Set, updatedFailedMessageIds: Set, updatedGlobalNotificationSettings: Bool) { + 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: Set, currentUpdatedTotalUnreadStates: [PeerGroupId: 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, currentGlobalTagsOperations: [GlobalMessageHistoryTagsOperation], currentLocalTagsOperations: [IntermediateMessageHistoryLocalTagsOperation], updatedMedia: [MediaId: Media?], replaceRemoteContactCount: Int32?, replaceContactPeerIds: Set?, currentPendingMessageActionsOperations: [PendingMessageActionsOperation], currentUpdatedMessageActionsSummaries: [PendingMessageActionsSummaryKey: Int32], currentUpdatedMessageTagSummaries: [MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary], currentInvalidateMessageTagSummaries: [InvalidatedMessageHistoryTagsSummaryEntryOperation], currentUpdatedPendingPeerNotificationSettings: Set, replacedAdditionalChatListItems: [AdditionalChatListItem]?, updatedNoticeEntryKeys: Set, updatedCacheEntryKeys: Set, currentUpdatedMasterClientId: Int64?, updatedFailedMessagePeerIds: Set, updatedFailedMessageIds: Set, updatedGlobalNotificationSettings: Bool, updatedPeerTimeoutAttributes: Bool) { self.currentUpdatedState = currentUpdatedState self.currentPeerHoleOperations = currentPeerHoleOperations self.currentOperationsByPeerId = currentOperationsByPeerId @@ -216,5 +220,6 @@ final class PostboxTransaction { self.updatedFailedMessagePeerIds = updatedFailedMessagePeerIds self.updatedFailedMessageIds = updatedFailedMessageIds self.updatedGlobalNotificationSettings = updatedGlobalNotificationSettings + self.updatedPeerTimeoutAttributes = updatedPeerTimeoutAttributes } } diff --git a/submodules/Postbox/Sources/Views.swift b/submodules/Postbox/Sources/Views.swift index 615db1d7b6..c07da381f0 100644 --- a/submodules/Postbox/Sources/Views.swift +++ b/submodules/Postbox/Sources/Views.swift @@ -37,6 +37,7 @@ public enum PostboxViewKey: Hashable { case messageGroup(id: MessageId) case isContact(id: PeerId) case chatListIndex(id: PeerId) + case peerTimeoutAttributes public func hash(into hasher: inout Hasher) { switch self { @@ -121,6 +122,8 @@ public enum PostboxViewKey: Hashable { hasher.combine(id) case let .chatListIndex(id): hasher.combine(id) + case .peerTimeoutAttributes: + hasher.combine(17) } } @@ -342,6 +345,12 @@ public enum PostboxViewKey: Hashable { } else { return false } + case .peerTimeoutAttributes: + if case .peerTimeoutAttributes = rhs { + return true + } else { + return false + } } } } @@ -420,5 +429,7 @@ func postboxViewForKey(postbox: PostboxImpl, key: PostboxViewKey) -> MutablePost return MutableIsContactView(postbox: postbox, id: id) case let .chatListIndex(id): return MutableChatListIndexView(postbox: postbox, id: id) + case .peerTimeoutAttributes: + return MutablePeerTimeoutAttributesView(postbox: postbox) } } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 99ba86362b..1d4800cb96 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -126,6 +126,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { private let animationCache: AnimationCache private let animationRenderer: MultiAnimationRenderer private let items: [ReactionContextItem] + private let selectedItems: Set private let getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)? private let isExpandedUpdated: (ContainedViewLayoutTransition) -> Void private let requestLayout: (ContainedViewLayoutTransition) -> Void @@ -239,10 +240,11 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } } - public init(context: AccountContext, animationCache: AnimationCache, presentationData: PresentationData, items: [ReactionContextItem], getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?, isExpandedUpdated: @escaping (ContainedViewLayoutTransition) -> Void, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void) { + public init(context: AccountContext, animationCache: AnimationCache, presentationData: PresentationData, items: [ReactionContextItem], selectedItems: Set, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?, isExpandedUpdated: @escaping (ContainedViewLayoutTransition) -> Void, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void) { self.context = context self.presentationData = presentationData self.items = items + self.selectedItems = selectedItems self.getEmojiContent = getEmojiContent self.isExpandedUpdated = isExpandedUpdated self.requestLayout = requestLayout @@ -639,11 +641,16 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { validIndices.insert(i) var itemFrame = baseItemFrame + var selectionItemFrame = itemFrame + let normalItemScale: CGFloat = 1.0 var isPreviewing = false if let highlightedReaction = self.highlightedReaction, highlightedReaction == self.items[i].reaction { isPreviewing = true - } else if self.highlightedReaction != nil { + } + + if let reaction = self.items[i].reaction, self.selectedItems.contains(reaction.rawValue), !isPreviewing { + itemFrame = itemFrame.insetBy(dx: (itemFrame.width - 0.8 * itemFrame.width) * 0.5, dy: (itemFrame.height - 0.8 * itemFrame.height) * 0.5) } var animateIn = false @@ -668,6 +675,12 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { self.visibleItemNodes[i] = itemNode self.scrollNode.addSubnode(itemNode) + if let itemNode = itemNode as? ReactionNode { + if let reaction = self.items[i].reaction, self.selectedItems.contains(reaction.rawValue) { + self.contentTintContainer.view.addSubview(itemNode.selectionTintView) + self.scrollNode.view.addSubview(itemNode.selectionView) + } + } if let maskNode = maskNode { self.visibleItemMaskNodes[i] = maskNode @@ -690,7 +703,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } if self.getEmojiContent != nil && i == visibleItemCount - 1 { - itemFrame.origin.x -= (1.0 - compressionFactor) * itemFrame.width * 0.5 + itemFrame.origin.x -= (1.0 - compressionFactor) * selectionItemFrame.width * 0.5 + selectionItemFrame.origin.x -= (1.0 - compressionFactor) * selectionItemFrame.width * 0.5 itemNode.isUserInteractionEnabled = false } else { itemNode.isUserInteractionEnabled = true @@ -711,15 +725,29 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { }) itemNode.updateLayout(size: itemFrame.size, isExpanded: false, largeExpanded: false, isPreviewing: isPreviewing, transition: itemTransition) + if let itemNode = itemNode as? ReactionNode { + if let reaction = self.items[i].reaction, self.selectedItems.contains(reaction.rawValue) { + itemNode.selectionTintView.isHidden = false + itemNode.selectionView.isHidden = false + } + itemTransition.updateFrame(view: itemNode.selectionTintView, frame: selectionItemFrame.offsetBy(dx: 0.0, dy: self.extensionDistance * 0.5)) + itemTransition.updateCornerRadius(layer: itemNode.selectionTintView.layer, cornerRadius: min(selectionItemFrame.width, selectionItemFrame.height) / 2.0) + + itemTransition.updateFrame(view: itemNode.selectionView, frame: selectionItemFrame.offsetBy(dx: 0.0, dy: self.extensionDistance * 0.5)) + itemTransition.updateCornerRadius(layer: itemNode.selectionView.layer, cornerRadius: min(selectionItemFrame.width, selectionItemFrame.height) / 2.0) + } + if animateIn { itemNode.appear(animated: !self.context.sharedContext.currentPresentationData.with({ $0 }).reduceMotion) } if self.getEmojiContent != nil && i == visibleItemCount - 1 { - transition.updateSublayerTransformScale(node: itemNode, scale: 0.001 * (1.0 - compressionFactor) + 1.0 * compressionFactor) + transition.updateSublayerTransformScale(node: itemNode, scale: 0.001 * (1.0 - compressionFactor) + normalItemScale * compressionFactor) let alphaFraction = min(compressionFactor, 0.2) / 0.2 transition.updateAlpha(node: itemNode, alpha: alphaFraction) + } else { + transition.updateSublayerTransformScale(node: itemNode, scale: normalItemScale) } } } @@ -888,9 +916,14 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { self.contentContainer.view.mask = nil for (_, itemNode) in self.visibleItemNodes { itemNode.isHidden = true + + if let itemNode = itemNode as? ReactionNode { + itemNode.selectionView.isHidden = true + itemNode.selectionTintView.isHidden = true + } } if let emojiView = reactionSelectionComponentHost.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: AnyHashable("emoji"))) as? EmojiPagerContentComponent.View { - var initialPositionAndFrame: [MediaId: (position: CGPoint, frameIndex: Int, placeholder: UIImage)] = [:] + var initialPositionAndFrame: [MediaId: (frame: CGRect, frameIndex: Int, placeholder: UIImage)] = [:] for (_, itemNode) in self.visibleItemNodes { guard let itemNode = itemNode as? ReactionNode else { continue @@ -902,7 +935,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { continue } initialPositionAndFrame[itemNode.item.stillAnimation.fileId] = ( - position: itemNode.frame.center, + frame: itemNode.frame, frameIndex: itemNode.currentFrameIndex, placeholder: placeholder ) @@ -911,7 +944,10 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { emojiView.animateInReactionSelection(sourceItems: initialPositionAndFrame) if let mirrorContentClippingView = emojiView.mirrorContentClippingView { - Transition(transition).animateBoundsOrigin(view: mirrorContentClippingView, from: CGPoint(x: 0.0, y: 46.0), to: CGPoint(), additive: true) + mirrorContentClippingView.clipsToBounds = false + Transition(transition).animateBoundsOrigin(view: mirrorContentClippingView, from: CGPoint(x: 0.0, y: 46.0), to: CGPoint(), additive: true, completion: { [weak mirrorContentClippingView] _ in + mirrorContentClippingView?.clipsToBounds = true + }) } } if let topPanelView = reactionSelectionComponentHost.findTaggedView(tag: EntityKeyboardTopPanelComponent.Tag(id: AnyHashable("emoji"))) as? EntityKeyboardTopPanelComponent.View { @@ -1164,7 +1200,10 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + itemDelay * UIView.animationDurationFactor(), execute: { [weak itemNode] in - itemNode?.appear(animated: true) + guard let itemNode = itemNode else { + return + } + itemNode.appear(animated: true) }) } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift index 5d387a6d7e..0945f3ce3c 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift @@ -53,6 +53,9 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { private let hasAppearAnimation: Bool private let useDirectRendering: Bool + let selectionTintView: UIView + let selectionView: UIView + private var animateInAnimationNode: AnimatedStickerNode? private let staticAnimationNode: AnimatedStickerNode private var stillAnimationNode: AnimatedStickerNode? @@ -87,6 +90,14 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { self.hasAppearAnimation = hasAppearAnimation self.useDirectRendering = useDirectRendering + self.selectionTintView = UIView() + self.selectionTintView.backgroundColor = UIColor(white: 1.0, alpha: 0.2) + self.selectionTintView.isHidden = true + + self.selectionView = UIView() + self.selectionView.backgroundColor = theme.chat.inputMediaPanel.panelContentControlVibrantSelectionColor + self.selectionView.isHidden = true + self.staticAnimationNode = self.useDirectRendering ? DirectAnimatedStickerNode() : DefaultAnimatedStickerNodeImpl() if hasAppearAnimation { @@ -147,8 +158,18 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { } else { self.animateInAnimationNode?.visibility = true } + + self.selectionView.alpha = 1.0 + self.selectionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.selectionView.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4) + + self.selectionTintView.alpha = 1.0 + self.selectionTintView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.selectionTintView.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4) } else { self.animateInAnimationNode?.completed(true) + self.selectionView.alpha = 1.0 + self.selectionTintView.alpha = 1.0 } } diff --git a/submodules/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift index e36c06bd08..93f73fc34f 100644 --- a/submodules/TelegramCore/Sources/Account/Account.swift +++ b/submodules/TelegramCore/Sources/Account/Account.swift @@ -1074,6 +1074,7 @@ public class Account { self.managedOperationsDisposable.add(managedCloudChatRemoveMessagesOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) self.managedOperationsDisposable.add(managedAutoremoveMessageOperations(network: self.network, postbox: self.postbox, isRemove: true).start()) self.managedOperationsDisposable.add(managedAutoremoveMessageOperations(network: self.network, postbox: self.postbox, isRemove: false).start()) + self.managedOperationsDisposable.add(managedPeerTimestampAttributeOperations(network: self.network, postbox: self.postbox).start()) self.managedOperationsDisposable.add(managedGlobalNotificationSettings(postbox: self.postbox, network: self.network).start()) self.managedOperationsDisposable.add(managedSynchronizePinnedChatsOperations(postbox: self.postbox, network: self.network, accountPeerId: self.peerId, stateManager: self.stateManager).start()) diff --git a/submodules/TelegramCore/Sources/State/ManagedAutoremoveMessageOperations.swift b/submodules/TelegramCore/Sources/State/ManagedAutoremoveMessageOperations.swift index 8d18ba594f..2f79891113 100644 --- a/submodules/TelegramCore/Sources/State/ManagedAutoremoveMessageOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedAutoremoveMessageOperations.swift @@ -4,7 +4,6 @@ import SwiftSignalKit import TelegramApi import MtProtoKit - private typealias SignalKitTimer = SwiftSignalKit.Timer private final class ManagedAutoremoveMessageOperationsHelper { diff --git a/submodules/TelegramCore/Sources/State/ManagedPeerTimestampAttributeOperations.swift b/submodules/TelegramCore/Sources/State/ManagedPeerTimestampAttributeOperations.swift new file mode 100644 index 0000000000..472ceee5ce --- /dev/null +++ b/submodules/TelegramCore/Sources/State/ManagedPeerTimestampAttributeOperations.swift @@ -0,0 +1,110 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramApi +import MtProtoKit + +private typealias SignalKitTimer = SwiftSignalKit.Timer + +private final class ManagedPeerTimestampAttributeOperationsHelper { + struct Entry: Equatable { + var peerId: PeerId + var timestamp: UInt32 + } + + var entry: (Entry, MetaDisposable)? + + func update(_ head: Entry?) -> (disposeOperations: [Disposable], beginOperations: [(Entry, MetaDisposable)]) { + var disposeOperations: [Disposable] = [] + var beginOperations: [(Entry, MetaDisposable)] = [] + + if self.entry?.0 != head { + if let (_, disposable) = self.entry { + self.entry = nil + disposeOperations.append(disposable) + } + if let head = head { + let disposable = MetaDisposable() + self.entry = (head, disposable) + beginOperations.append((head, disposable)) + } + } + + return (disposeOperations, beginOperations) + } + + func reset() -> [Disposable] { + if let entry = entry { + return [entry.1] + } else { + return [] + } + } +} + +func managedPeerTimestampAttributeOperations(network: Network, postbox: Postbox) -> Signal { + return Signal { _ in + let helper = Atomic(value: ManagedPeerTimestampAttributeOperationsHelper()) + + let timeOffsetOnce = Signal { subscriber in + subscriber.putNext(network.globalTimeDifference) + return EmptyDisposable + } + + let timeOffset = ( + timeOffsetOnce + |> then( + Signal.complete() + |> delay(1.0, queue: .mainQueue()) + ) + ) + |> restart + |> map { value -> Double in + round(value) + } + |> distinctUntilChanged + + let disposable = combineLatest(timeOffset, postbox.combinedView(keys: [PostboxViewKey.peerTimeoutAttributes])).start(next: { timeOffset, views in + guard let view = views.views[PostboxViewKey.peerTimeoutAttributes] as? PeerTimeoutAttributesView else { + return + } + let topEntry = view.minValue.flatMap { value in + return ManagedPeerTimestampAttributeOperationsHelper.Entry(peerId: value.peerId, timestamp: value.timestamp) + } + let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(ManagedPeerTimestampAttributeOperationsHelper.Entry, MetaDisposable)]) in + return helper.update(topEntry) + } + + for disposable in disposeOperations { + disposable.dispose() + } + + for (entry, disposable) in beginOperations { + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + timeOffset + let delay = max(0.0, Double(entry.timestamp) - timestamp) + let signal = Signal.complete() + |> suspendAwareDelay(delay, queue: Queue.concurrentDefaultQueue()) + |> then(postbox.transaction { transaction -> Void in + if let peer = transaction.getPeer(entry.peerId) { + if let user = peer as? TelegramUser { + updatePeers(transaction: transaction, peers: [user.withUpdatedEmojiStatus(nil)], update: { _, updated in updated }) + } + } + //failsafe + transaction.removePeerTimeoutAttributeEntry(peerId: entry.peerId, timestamp: entry.timestamp) + }) + disposable.set(signal.start()) + } + }) + + return ActionDisposable { + disposable.dispose() + let disposables = helper.with { helper -> [Disposable] in + return helper.reset() + } + for disposable in disposables { + disposable.dispose() + } + } + } +} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift index 9ed693c616..31b084db2f 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift @@ -174,6 +174,8 @@ public final class TelegramChannel: Peer, Equatable { public let associatedPeerId: PeerId? = nil public let notificationSettingsPeerId: PeerId? = nil + public var timeoutAttribute: UInt32? { return nil } + public init(id: PeerId, accessHash: TelegramPeerAccessHash?, title: String, username: String?, photo: [TelegramMediaImageRepresentation], creationDate: Int32, version: Int32, participationStatus: TelegramChannelParticipationStatus, info: TelegramChannelInfo, flags: TelegramChannelFlags, restrictionInfo: PeerAccessRestrictionInfo?, adminRights: TelegramChatAdminRights?, bannedRights: TelegramChatBannedRights?, defaultBannedRights: TelegramChatBannedRights?) { self.id = id self.accessHash = accessHash diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramGroup.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramGroup.swift index 0231ebae13..dc5e790fc2 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramGroup.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramGroup.swift @@ -97,6 +97,8 @@ public final class TelegramGroup: Peer, Equatable { public let associatedPeerId: PeerId? = nil public let notificationSettingsPeerId: PeerId? = nil + public var timeoutAttribute: UInt32? { return nil } + public init(id: PeerId, title: String, photo: [TelegramMediaImageRepresentation], participantCount: Int, role: TelegramGroupRole, membership: TelegramGroupMembership, flags: TelegramGroupFlags, defaultBannedRights: TelegramChatBannedRights?, migrationReference: TelegramGroupToChannelMigrationReference?, creationDate: Int32, version: Int) { self.id = id self.title = title diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramSecretChat.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramSecretChat.swift index a0df67bac2..5ceb400fed 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramSecretChat.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramSecretChat.swift @@ -19,6 +19,8 @@ public final class TelegramSecretChat: Peer, Equatable { public let associatedPeerId: PeerId? public let notificationSettingsPeerId: PeerId? + public var timeoutAttribute: UInt32? { return nil } + public init(id: PeerId, creationDate: Int32, regularPeerId: PeerId, accessHash: Int64, role: SecretChatRole, embeddedState: SecretChatEmbeddedPeerState, messageAutoremoveTimeout: Int32?) { self.id = id self.regularPeerId = regularPeerId diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift index bee75b9f92..5d9a5b0ed8 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift @@ -115,6 +115,18 @@ public final class TelegramUser: Peer, Equatable { public let associatedPeerId: PeerId? = nil public let notificationSettingsPeerId: PeerId? = nil + public var timeoutAttribute: UInt32? { + if let emojiStatus = self.emojiStatus { + if let expirationDate = emojiStatus.expirationDate { + return UInt32(max(0, expirationDate)) + } else { + return nil + } + } else { + return nil + } + } + public init(id: PeerId, accessHash: TelegramPeerAccessHash?, firstName: String?, lastName: String?, username: String?, phone: String?, photo: [TelegramMediaImageRepresentation], botInfo: BotUserInfo?, restrictionInfo: PeerAccessRestrictionInfo?, flags: UserInfoFlags, emojiStatus: PeerEmojiStatus?) { self.id = id self.accessHash = accessHash diff --git a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift index 9b173039f4..f224727208 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift @@ -76,7 +76,7 @@ public extension TelegramEngine { if let entry = CodableEntry(RecentMediaItem(file)) { let itemEntry = OrderedItemListEntry(id: RecentMediaItemId(file.fileId).rawValue, contents: entry) - transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudRecentStatusEmoji, item: itemEntry, removeTailIfCountExceeds: 50) + transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudRecentStatusEmoji, item: itemEntry, removeTailIfCountExceeds: 32) } } diff --git a/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift b/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift index e51ef88254..5627ab6f09 100644 --- a/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift @@ -458,6 +458,30 @@ public func stringAndActivityForUserPresence(strings: PresentationStrings, dateT } } +public func peerStatusExpirationString(statusTimestamp: Int32, relativeTo timestamp: Int32, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat) -> String { + let difference = max(statusTimestamp - timestamp, 60) + if difference < 60 * 60 { + return strings.PeerStatusExpiration_Minutes(Int32(round(Double(difference) / Double(60)))) + } else if difference < 24 * 60 * 60 { + return strings.PeerStatusExpiration_Hours(Int32(round(Double(difference) / Double(60 * 60)))) + } else { + var t: time_t = time_t(statusTimestamp) + var timeinfo: tm = tm() + localtime_r(&t, &timeinfo) + + var now: time_t = time_t(timestamp) + var timeinfoNow: tm = tm() + localtime_r(&now, &timeinfoNow) + + let dayDifference = timeinfo.tm_yday - timeinfoNow.tm_yday + if dayDifference == 1 { + return strings.PeerStatusExpiration_TomorrowAt(stringForShortTimestamp(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, dateTimeFormat: dateTimeFormat)).string + } else { + return strings.PeerStatusExpiration_AtDate(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, dateTimeFormat: dateTimeFormat)).string + } + } +} + public func userPresenceStringRefreshTimeout(_ presence: TelegramUserPresence, relativeTo timestamp: Int32) -> Double { switch presence.status { case let .present(statusTimestamp): diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/BUILD b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/BUILD index 2bb7cef7a4..bb7ae0a1d5 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/BUILD +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/BUILD @@ -24,12 +24,15 @@ swift_library( "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", "//submodules/Components/PagerComponent:PagerComponent", "//submodules/Components/MultilineTextComponent:MultilineTextComponent", + "//submodules/Components/SolidRoundedButtonComponent:SolidRoundedButtonComponent", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/AccountContext:AccountContext", "//submodules/lottie-ios:Lottie", "//submodules/TextFormat:TextFormat", "//submodules/AppBundle:AppBundle", "//submodules/GZip:GZip", + "//submodules/TelegramStringFormatting:TelegramStringFormatting", + "//submodules/PresentationDataUtils:PresentationDataUtils", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusPreviewScreen.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusPreviewScreen.swift index 9d3138ecc0..e9723aa130 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusPreviewScreen.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusPreviewScreen.swift @@ -10,18 +10,26 @@ import AccountContext import ComponentDisplayAdapters import MultilineTextComponent import EmojiStatusComponent +import TelegramStringFormatting +import SolidRoundedButtonComponent +import PresentationDataUtils protocol ContextMenuItemWithAction: AnyObject { - func performAction() + func performAction() -> ContextMenuPerformActionResult +} + +enum ContextMenuPerformActionResult { + case none + case clearHighlight } private final class ContextMenuActionItem: Component, ContextMenuItemWithAction { typealias EnvironmentType = ContextMenuActionItemEnvironment let title: String - let action: () -> Void + let action: () -> ContextMenuPerformActionResult - init(title: String, action: @escaping () -> Void) { + init(title: String, action: @escaping () -> ContextMenuPerformActionResult) { self.title = title self.action = action } @@ -33,8 +41,8 @@ private final class ContextMenuActionItem: Component, ContextMenuItemWithAction return true } - func performAction() { - self.action() + func performAction() -> ContextMenuPerformActionResult { + return self.action() } final class View: UIView { @@ -169,7 +177,12 @@ private final class ContextMenuActionsComponent: Component { for item in component.items { if item.id == id { if let itemComponent = item.component.wrapped as? ContextMenuItemWithAction { - itemComponent.performAction() + switch itemComponent.performAction() { + case .none: + break + case .clearHighlight: + self.setHighlightedItem(id: nil) + } } break } @@ -332,9 +345,193 @@ private final class ContextMenuActionsComponent: Component { } } +private final class TimeSelectionControlComponent: Component { + let theme: PresentationTheme + let strings: PresentationStrings + let bottomInset: CGFloat + let apply: (Int32) -> Void + let cancel: () -> Void + + init( + theme: PresentationTheme, + strings: PresentationStrings, + bottomInset: CGFloat, + apply: @escaping (Int32) -> Void, + cancel: @escaping () -> Void + ) { + self.theme = theme + self.strings = strings + self.bottomInset = bottomInset + self.apply = apply + self.cancel = cancel + } + + static func ==(lhs: TimeSelectionControlComponent, rhs: TimeSelectionControlComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.bottomInset != rhs.bottomInset { + return false + } + return true + } + + final class View: UIView { + private let backgroundView: BlurredBackgroundView + private let pickerView: UIDatePicker + private let titleView: ComponentView + private let leftButtonView: ComponentView + private let actionButtonView: ComponentView + + private var component: TimeSelectionControlComponent? + + override init(frame: CGRect) { + self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) + self.pickerView = UIDatePicker() + + self.titleView = ComponentView() + self.leftButtonView = ComponentView() + self.actionButtonView = ComponentView() + + super.init(frame: frame) + + self.addSubview(self.backgroundView) + + self.pickerView.timeZone = TimeZone(secondsFromGMT: 0) + self.pickerView.datePickerMode = .countDownTimer + self.pickerView.datePickerMode = .dateAndTime + if #available(iOS 13.4, *) { + self.pickerView.preferredDatePickerStyle = .wheels + } + self.pickerView.minimumDate = Date(timeIntervalSince1970: Date().timeIntervalSince1970 + Double(TimeZone.current.secondsFromGMT())) + self.pickerView.maximumDate = Date(timeIntervalSince1970: Double(Int32.max - 1)) + + self.addSubview(self.pickerView) + self.pickerView.addTarget(self, action: #selector(self.datePickerUpdated), for: .valueChanged) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func datePickerUpdated() { + } + + func update(component: TimeSelectionControlComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + if self.component?.theme !== component.theme { + UILabel.setDateLabel(component.theme.list.itemPrimaryTextColor) + + self.pickerView.setValue(component.theme.list.itemPrimaryTextColor, forKey: "textColor") + + self.backgroundView.updateColor(color: component.theme.contextMenu.backgroundColor, transition: .immediate) + } + + self.component = component + + let topPanelHeight: CGFloat = 54.0 + let pickerSpacing: CGFloat = 10.0 + + let pickerSize = CGSize(width: availableSize.width, height: 216.0) + let pickerFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight + pickerSpacing), size: pickerSize) + + //TODO:localize + let titleSize = self.titleView.update( + transition: transition, + component: AnyComponent(Text(text: "Set Until", font: Font.semibold(17.0), color: component.theme.list.itemPrimaryTextColor)), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + if let titleComponentView = self.titleView.view { + if titleComponentView.superview == nil { + self.addSubview(titleComponentView) + } + transition.setFrame(view: titleComponentView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: floor((topPanelHeight - titleSize.height) / 2.0)), size: titleSize)) + } + + let leftButtonSize = self.leftButtonView.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(Text( + text: component.strings.Common_Cancel, + font: Font.regular(17.0), + color: component.theme.list.itemAccentColor + )), + action: { [weak self] in + self?.component?.cancel() + } + ).minSize(CGSize(width: 16.0, height: topPanelHeight))), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + if let leftButtonComponentView = self.leftButtonView.view { + if leftButtonComponentView.superview == nil { + self.addSubview(leftButtonComponentView) + } + transition.setFrame(view: leftButtonComponentView, frame: CGRect(origin: CGPoint(x: 16.0, y: floor((topPanelHeight - leftButtonSize.height) / 2.0)), size: leftButtonSize)) + } + + //TODO:localize + let actionButtonSize = self.actionButtonView.update( + transition: transition, + component: AnyComponent(SolidRoundedButtonComponent( + title: "Set Until", + icon: nil, + theme: SolidRoundedButtonComponent.Theme(theme: component.theme), + font: .bold, + fontSize: 17.0, + height: 50.0, + cornerRadius: 10.0, + gloss: false, + action: { [weak self] in + guard let strongSelf = self, let component = strongSelf.component else { + return + } + + let timestamp = Int32(strongSelf.pickerView.date.timeIntervalSince1970) + component.apply(timestamp) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 50.0) + ) + let actionButtonFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - actionButtonSize.width) / 2.0), y: pickerFrame.maxY + pickerSpacing), size: actionButtonSize) + if let actionButtonComponentView = self.actionButtonView.view { + if actionButtonComponentView.superview == nil { + self.addSubview(actionButtonComponentView) + } + transition.setFrame(view: actionButtonComponentView, frame: actionButtonFrame) + } + + self.pickerView.frame = pickerFrame + + var size = CGSize(width: availableSize.width, height: actionButtonFrame.maxY) + if component.bottomInset.isZero { + size.height += 10.0 + } else { + size.height += max(10.0, component.bottomInset) + } + + self.backgroundView.update(size: size, cornerRadius: 10.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition.containedViewLayoutTransition) + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + final class EmojiStatusPreviewScreenComponent: Component { struct StatusResult { - let timeout: Int + let timestamp: Int32 let sourceView: UIView } @@ -350,21 +547,29 @@ final class EmojiStatusPreviewScreenComponent: Component { } } + private enum CurrentState { + case menu + case timeSelection + } + typealias EnvironmentType = Empty let theme: PresentationTheme let strings: PresentationStrings + let bottomInset: CGFloat let item: EmojiStatusComponent let dismiss: (StatusResult?) -> Void init( theme: PresentationTheme, strings: PresentationStrings, + bottomInset: CGFloat, item: EmojiStatusComponent, dismiss: @escaping (StatusResult?) -> Void ) { self.theme = theme self.strings = strings + self.bottomInset = bottomInset self.item = item self.dismiss = dismiss } @@ -376,6 +581,9 @@ final class EmojiStatusPreviewScreenComponent: Component { if lhs.strings !== rhs.strings { return false } + if lhs.bottomInset != rhs.bottomInset { + return false + } if lhs.item != rhs.item { return false } @@ -386,13 +594,18 @@ final class EmojiStatusPreviewScreenComponent: Component { private let backgroundView: BlurredBackgroundView private let itemView: ComponentView private let actionsView: ComponentView + private let timeSelectionView: ComponentView + + private var currentState: CurrentState = .menu private var component: EmojiStatusPreviewScreenComponent? + private weak var state: EmptyComponentState? override init(frame: CGRect) { self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) self.itemView = ComponentView() self.actionsView = ComponentView() + self.timeSelectionView = ComponentView() super.init(frame: frame) @@ -406,12 +619,29 @@ final class EmojiStatusPreviewScreenComponent: Component { @objc private func backgroundTapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { - self.component?.dismiss(nil) + switch self.currentState { + case .menu: + self.component?.dismiss(nil) + case .timeSelection: + self.toggleState() + } + } + } + + private func toggleState() { + switch self.currentState { + case .menu: + self.currentState = .timeSelection + self.state?.updated(transition: Transition(animation: .curve(duration: 0.5, curve: .spring))) + case .timeSelection: + self.currentState = .menu + self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) } } func update(component: EmojiStatusPreviewScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.component = component + self.state = state let itemSpacing: CGFloat = 12.0 @@ -434,15 +664,25 @@ final class EmojiStatusPreviewScreenComponent: Component { title: setTimeoutForIntervalString(strings: component.strings, value: Int32(duration)), action: { [weak self] in guard let strongSelf = self, let component = strongSelf.component else { - return + return .none } guard let itemComponentView = strongSelf.itemView.view else { - return + return .none } - component.dismiss(StatusResult(timeout: duration, sourceView: itemComponentView)) + component.dismiss(StatusResult(timestamp: Int32(Date().timeIntervalSince1970) + Int32(duration), sourceView: itemComponentView)) + return .none } )))) } + //TODO:localize + menuItems.append(AnyComponentWithIdentity(id: "Other", component: AnyComponent(ContextMenuActionItem( + title: "Other", + action: { [weak self] in + self?.toggleState() + return .clearHighlight + } + )))) + let actionsSize = self.actionsView.update( transition: transition, component: AnyComponent(ContextMenuActionsComponent( @@ -453,13 +693,41 @@ final class EmojiStatusPreviewScreenComponent: Component { containerSize: availableSize ) - let totalContentHeight = itemSize.height + itemSpacing + actionsSize.height + let timeSelectionSize = self.timeSelectionView.update( + transition: transition, + component: AnyComponent(TimeSelectionControlComponent( + theme: component.theme, + strings: component.strings, + bottomInset: component.bottomInset, + apply: { [weak self] timestamp in + guard let strongSelf = self, let component = strongSelf.component else { + return + } + guard let itemComponentView = strongSelf.itemView.view else { + return + } + component.dismiss(StatusResult(timestamp: timestamp, sourceView: itemComponentView)) + }, + cancel: { [weak self] in + self?.toggleState() + } + )), + environment: {}, + containerSize: availableSize + ) + + let totalContentHeight = itemSize.height + itemSpacing + max(actionsSize.height, timeSelectionSize.height) let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: floor((availableSize.height - totalContentHeight) / 2.0)), size: CGSize(width: availableSize.width, height: totalContentHeight)) let itemFrame = CGRect(origin: CGPoint(x: contentFrame.minX + floor((contentFrame.width - itemSize.width) / 2.0), y: contentFrame.minY), size: itemSize) let actionsFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - actionsSize.width) / 2.0), y: itemFrame.maxY + itemSpacing), size: actionsSize) + var timeSelectionFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - timeSelectionSize.width) / 2.0), y: availableSize.height - timeSelectionSize.height), size: timeSelectionSize) + if case .menu = self.currentState { + timeSelectionFrame.origin.y = availableSize.height + } + if let itemComponentView = self.itemView.view { if itemComponentView.superview == nil { self.addSubview(itemComponentView) @@ -471,7 +739,25 @@ final class EmojiStatusPreviewScreenComponent: Component { if actionsComponentView.superview == nil { self.addSubview(actionsComponentView) } - transition.setFrame(view: actionsComponentView, frame: actionsFrame) + transition.setPosition(view: actionsComponentView, position: actionsFrame.center) + transition.setBounds(view: actionsComponentView, bounds: CGRect(origin: CGPoint(), size: actionsFrame.size)) + + if case .menu = self.currentState { + transition.setTransform(view: actionsComponentView, transform: CATransform3DIdentity) + transition.setAlpha(view: actionsComponentView, alpha: 1.0) + actionsComponentView.isUserInteractionEnabled = true + } else { + transition.setTransform(view: actionsComponentView, transform: CATransform3DMakeScale(0.001, 0.001, 1.0)) + transition.setAlpha(view: actionsComponentView, alpha: 0.0) + actionsComponentView.isUserInteractionEnabled = false + } + } + + if let timeSelectionComponentView = self.timeSelectionView.view { + if timeSelectionComponentView.superview == nil { + self.addSubview(timeSelectionComponentView) + } + transition.setFrame(view: timeSelectionComponentView, frame: timeSelectionFrame) } self.backgroundView.updateColor(color: component.theme.contextMenu.dimColor, transition: .immediate) @@ -507,7 +793,6 @@ final class EmojiStatusPreviewScreenComponent: Component { actionsComponentView.layer.animateSpring(from: (-actionsComponentView.bounds.height / 2.0) as NSNumber, to: 0.0 as NSNumber, keyPath: "transform.translation.y", duration: 0.6) let _ = additionalPositionDifference - //actionsComponentView.layer.animatePosition(from: CGPoint(x: -additionalPositionDifference.x, y: -additionalPositionDifference.y), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) } } } @@ -552,12 +837,20 @@ final class EmojiStatusPreviewScreenComponent: Component { actionsComponentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in }) } + + if let timeSelectionComponentView = self.timeSelectionView.view { + timeSelectionComponentView.layer.animatePosition(from: timeSelectionComponentView.layer.position, to: CGPoint(x: timeSelectionComponentView.layer.position.x, y: self.bounds.height + timeSelectionComponentView.bounds.height / 2.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + } } else { self.backgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) if let actionsComponentView = self.actionsView.view { actionsComponentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in completion() }) + + if let timeSelectionComponentView = self.timeSelectionView.view { + timeSelectionComponentView.layer.animatePosition(from: timeSelectionComponentView.layer.position, to: CGPoint(x: timeSelectionComponentView.layer.position.x, y: self.bounds.height + timeSelectionComponentView.bounds.height / 2.0), duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + } } else { completion() } diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift index 903282511d..a805f7a6e9 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -744,6 +744,7 @@ public final class EmojiStatusSelectionController: ViewController { component: AnyComponent(EmojiStatusPreviewScreenComponent( theme: self.presentationData.theme, strings: self.presentationData.strings, + bottomInset: layout.insets(options: []).bottom, item: EmojiStatusComponent( context: self.context, animationCache: self.context.animationCache, @@ -809,10 +810,8 @@ public final class EmojiStatusSelectionController: ViewController { return } - var expirationDate: Int32? - if result.timeout > 0 { - expirationDate = Int32(Date().timeIntervalSince1970) + Int32(result.timeout) - } + let expirationDate: Int32? = result.timestamp + let _ = (strongSelf.context.engine.accountData.setEmojiStatus(file: previewItem.item.itemFile, expirationDate: expirationDate) |> deliverOnMainQueue).start() diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 2e23e781b0..f47244062b 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -2779,7 +2779,7 @@ public final class EmojiPagerContentComponent: Component { } } - public func animateInReactionSelection(sourceItems: [MediaId: (position: CGPoint, frameIndex: Int, placeholder: UIImage)]) { + public func animateInReactionSelection(sourceItems: [MediaId: (frame: CGRect, frameIndex: Int, placeholder: UIImage)]) { guard let component = self.component, let itemLayout = self.itemLayout else { return } @@ -2792,10 +2792,12 @@ public final class EmojiPagerContentComponent: Component { continue } if let sourceItem = sourceItems[file.fileId] { - itemLayer.animatePosition(from: CGPoint(x: sourceItem.position.x - itemLayer.position.x, y: 0.0), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + itemLayer.animatePosition(from: CGPoint(x: sourceItem.frame.center.x - itemLayer.position.x, y: 0.0), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) if let itemSelectionLayer = self.visibleItemSelectionLayers[key] { - itemSelectionLayer.animatePosition(from: CGPoint(x: sourceItem.position.x - itemLayer.position.x, y: 0.0), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + itemSelectionLayer.animatePosition(from: CGPoint(x: sourceItem.frame.center.x - itemLayer.position.x, y: 0.0), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + + itemSelectionLayer.animate(from: (min(sourceItem.frame.width, sourceItem.frame.height) * 0.5) as NSNumber, to: 8.0 as NSNumber, keyPath: "cornerRadius", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.3) } component.animationRenderer.setFrameIndex(itemId: animationData.resource.resource.id.stringRepresentation, size: itemLayer.pixelSize, frameIndex: sourceItem.frameIndex, placeholder: sourceItem.placeholder) @@ -4130,8 +4132,10 @@ public final class EmojiPagerContentComponent: Component { itemLayer?.removeFromSuperlayer() }) if let itemSelectionLayer = itemSelectionLayer { - transition.setPosition(layer: itemSelectionLayer, position: position, completion: { [weak itemSelectionLayer] _ in + let itemSelectionTintContainerLayer = itemSelectionLayer.tintContainerLayer + transition.setPosition(layer: itemSelectionLayer, position: position, completion: { [weak itemSelectionLayer, weak itemSelectionTintContainerLayer] _ in itemSelectionLayer?.removeFromSuperlayer() + itemSelectionTintContainerLayer?.removeFromSuperlayer() }) } } else { @@ -4160,6 +4164,7 @@ public final class EmojiPagerContentComponent: Component { itemLayer.removeFromSuperlayer() if let itemSelectionLayer = itemSelectionLayer { itemSelectionLayer.removeFromSuperlayer() + itemSelectionLayer.tintContainerLayer.removeFromSuperlayer() } } } @@ -4191,7 +4196,6 @@ public final class EmojiPagerContentComponent: Component { } else { itemSelectionLayer.removeFromSuperlayer() removedItemSelectionLayerIds.append(id) - } } for id in removedItemSelectionLayerIds { @@ -4782,7 +4786,21 @@ public final class EmojiPagerContentComponent: Component { return hasPremium } - public static func emojiInputData(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, isStandalone: Bool, isStatusSelection: Bool, isReactionSelection: Bool, isQuickReactionSelection: Bool = false, topReactionItems: [EmojiComponentReactionItem], areUnicodeEmojiEnabled: Bool, areCustomEmojiEnabled: Bool, chatPeerId: EnginePeer.Id?, selectedItems: Set = Set()) -> Signal { + public static func emojiInputData( + context: AccountContext, + animationCache: AnimationCache, + animationRenderer: MultiAnimationRenderer, + isStandalone: Bool, + isStatusSelection: Bool, + isReactionSelection: Bool, + isQuickReactionSelection: Bool = false, + topReactionItems: [EmojiComponentReactionItem], + areUnicodeEmojiEnabled: Bool, + areCustomEmojiEnabled: Bool, + chatPeerId: EnginePeer.Id?, + selectedItems: Set = Set(), + topStatusTitle: String? = nil + ) -> Signal { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let isPremiumDisabled = premiumConfiguration.isPremiumDisabled @@ -4878,7 +4896,7 @@ public final class EmojiPagerContentComponent: Component { } else { itemGroupIndexById[groupId] = itemGroups.count //TODO:localize - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: "Long tap to set a timer".uppercased(), subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: 5, isClearable: false, headerItem: nil, items: [resultItem])) + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: topStatusTitle?.uppercased(), subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: 5, isClearable: false, headerItem: nil, items: [resultItem])) } var existingIds = Set() @@ -4964,7 +4982,7 @@ public final class EmojiPagerContentComponent: Component { } } if let featuredStatusEmoji = featuredStatusEmoji { - for item in featuredStatusEmoji.items { + for item in featuredStatusEmoji.items.prefix(7) { guard let item = item.contents.get(RecentMediaItem.self) else { continue } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index ff2f8b517c..2c7060d980 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -998,10 +998,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.context.account.peerId)), contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, context: strongSelf.context, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, selectAll: selectAll, interfaceInteraction: strongSelf.interfaceInteraction, messageNode: node as? ChatMessageItemView), peerMessageAllowedReactions(context: strongSelf.context, message: topMessage), - peerMessageSelectedReactionFiles(context: strongSelf.context, message: topMessage), + peerMessageSelectedReactions(context: strongSelf.context, message: topMessage), topMessageReactions(context: strongSelf.context, message: topMessage), ApplicationSpecificNotice.getChatTextSelectionTips(accountManager: strongSelf.context.sharedContext.accountManager) - ).start(next: { peer, actions, allowedReactions, selectedReactionFiles, topReactions, chatTextSelectionTips in + ).start(next: { peer, actions, allowedReactions, selectedReactions, topReactions, chatTextSelectionTips in guard let strongSelf = self else { return } @@ -1062,6 +1062,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if canAddMessageReactions(message: topMessage), let allowedReactions = allowedReactions, !topReactions.isEmpty { actions.reactionItems = topReactions.map(ReactionContextItem.reaction) + actions.selectedReactionItems = selectedReactions.reactions if !actions.reactionItems.isEmpty { let reactionItems: [EmojiComponentReactionItem] = actions.reactionItems.compactMap { item -> EmojiComponentReactionItem? in @@ -1102,7 +1103,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G areUnicodeEmojiEnabled: false, areCustomEmojiEnabled: true, chatPeerId: strongSelf.chatLocation.peerId, - selectedItems: selectedReactionFiles + selectedItems: selectedReactions.files ) } } @@ -17017,14 +17018,19 @@ func peerMessageAllowedReactions(context: AccountContext, message: Message) -> S } } -func peerMessageSelectedReactionFiles(context: AccountContext, message: Message) -> Signal, NoError> { +func peerMessageSelectedReactions(context: AccountContext, message: Message) -> Signal<(reactions: Set, files: Set), NoError> { return context.engine.stickers.availableReactions() |> take(1) - |> map { availableReactions -> Set in + |> map { availableReactions -> (reactions: Set, files: Set) in var result = Set() + var reactions = Set() if let effectiveReactions = message.effectiveReactions { for reaction in effectiveReactions { + if !reaction.isSelected { + continue + } + reactions.insert(reaction.value) switch reaction.value { case .builtin: if let availableReaction = availableReactions?.reactions.first(where: { $0.value == reaction.value }) { @@ -17036,7 +17042,7 @@ func peerMessageSelectedReactionFiles(context: AccountContext, message: Message) } } - return result + return (reactions, result) } } diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 78e643edc8..b0b2a01aa2 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -1341,7 +1341,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let _ = inputMediaNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, standardInputHeight: layout.standardInputHeight, inputHeight: layout.inputHeight ?? 0.0, maximumHeight: maximumInputNodeHeight, inputPanelHeight: inputPanelSize?.height ?? 0.0, transition: .immediate, interfaceState: self.chatPresentationInterfaceState, deviceMetrics: layout.deviceMetrics, isVisible: false, isExpanded: self.inputPanelContainerNode.stableIsExpanded) } - transition.updateFrame(node: self.titleAccessoryPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: 66.0))) + transition.updateFrame(node: self.titleAccessoryPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: 100.0))) transition.updateFrame(node: self.inputContextPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: layout.size.height))) transition.updateFrame(node: self.inputContextOverTextPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: layout.size.height))) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift index 94007aeba6..898f2f8521 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift @@ -77,8 +77,8 @@ func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceStat if displayActionsPanel && (selectedContext == nil || selectedContext! <= .pinnedMessage) { if let currentPanel = currentPanel as? ChatReportPeerTitlePanelNode { return currentPanel - } else { - let panel = ChatReportPeerTitlePanelNode() + } else if let controllerInteraction = controllerInteraction { + let panel = ChatReportPeerTitlePanelNode(context: context, animationCache: controllerInteraction.presentationContext.animationCache, animationRenderer: controllerInteraction.presentationContext.animationRenderer) panel.interfaceInteraction = interfaceInteraction return panel } diff --git a/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift index 428e779c1d..f70a795952 100644 --- a/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift @@ -10,6 +10,10 @@ import TelegramStringFormatting import TextFormat import Markdown import ChatPresentationInterfaceState +import TextNodeWithEntities +import AnimationCache +import MultiAnimationRenderer +import AccountContext private enum ChatReportPeerTitleButton: Equatable { case block @@ -301,11 +305,16 @@ private final class ChatInfoTitlePanelPeerNearbyInfoNode: ASDisplayNode { } final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { + private let context: AccountContext + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer + private let separatorNode: ASDisplayNode private let closeButton: HighlightableButtonNode private var buttons: [(ChatReportPeerTitleButton, UIButton)] = [] private let textNode: ImmediateTextNode + private var emojiStatusTextNode: TextNodeWithEntities? private var theme: PresentationTheme? @@ -314,7 +323,11 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { private var tapGestureRecognizer: UITapGestureRecognizer? - override init() { + init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer) { + self.context = context + self.animationCache = animationCache + self.animationRenderer = animationRenderer + self.separatorNode = ASDisplayNode() self.separatorNode.isLayerBacked = true @@ -481,9 +494,70 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { self.tapGestureRecognizer?.isEnabled = false } - let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: width - contentRightInset - closeButtonSize.width, y: floorToScreenPixels((panelHeight - closeButtonSize.height) / 2.0)), size: closeButtonSize)) + + var emojiStatus: PeerEmojiStatus? + if let user = interfaceState.renderedPeer?.peer as? TelegramUser, let emojiStatusValue = user.emojiStatus { + emojiStatus = emojiStatusValue + } + + /*#if DEBUG + emojiStatus = PeerEmojiStatus(fileId: 5062172592505356289, expirationDate: nil) + #endif*/ + + if let emojiStatus = emojiStatus { + let emojiStatusTextNode: TextNodeWithEntities + if let current = self.emojiStatusTextNode { + emojiStatusTextNode = current + } else { + emojiStatusTextNode = TextNodeWithEntities() + self.emojiStatusTextNode = emojiStatusTextNode + self.addSubnode(emojiStatusTextNode.textNode) + } + + let plainText = interfaceState.strings.Chat_PanelCustomStatusInfo(".") + let attributedText = NSMutableAttributedString(attributedString: NSAttributedString(string: plainText.string, font: Font.regular(13.0), textColor: interfaceState.theme.rootController.navigationBar.secondaryTextColor, paragraphAlignment: .center)) + for range in plainText.ranges { + attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: emojiStatus.fileId, file: nil), range: range.range) + } + + let makeEmojiStatusLayout = TextNodeWithEntities.asyncLayout(emojiStatusTextNode) + let (emojiStatusLayout, emojiStatusApply) = makeEmojiStatusLayout(TextNodeLayoutArguments( + attributedString: attributedText, + backgroundColor: nil, + minimumNumberOfLines: 0, + maximumNumberOfLines: 0, + truncationType: .end, + constrainedSize: CGSize(width: width - leftInset * 2.0 - 8.0 * 2.0, height: CGFloat.greatestFiniteMagnitude), + alignment: .center, + verticalAlignment: .top, + lineSpacing: 0.0, + cutout: nil, + insets: UIEdgeInsets(), + lineColor: nil, + textShadowColor: nil, + textStroke: nil, + displaySpoilers: false, + displayEmbeddedItemsUnderSpoilers: false + )) + let _ = emojiStatusApply(TextNodeWithEntities.Arguments( + context: self.context, + cache: self.animationCache, + renderer: self.animationRenderer, + placeholderColor: interfaceState.theme.list.mediaPlaceholderColor, + attemptSynchronous: false + )) + transition.updateFrame(node: emojiStatusTextNode.textNode, frame: CGRect(origin: CGPoint(x: floor((width - emojiStatusLayout.size.width) / 2.0), y: panelHeight), size: emojiStatusLayout.size)) + panelHeight += emojiStatusLayout.size.height + 8.0 + + emojiStatusTextNode.visibilityRect = .infinite + } else { + if let emojiStatusTextNode = self.emojiStatusTextNode { + self.emojiStatusTextNode = nil + emojiStatusTextNode.textNode.removeFromSupernode() + } + } let initialPanelHeight = panelHeight transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 36ac1507d8..11a6e4b183 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -3106,11 +3106,17 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate strongSelf.emojiStatusSelectionController?.dismiss() var selectedItems = Set() + var topStatusTitle = "Long tap to set a timer" if let peer = strongSelf.data?.peer { if let user = peer as? TelegramUser, let emojiStatus = user.emojiStatus { selectedItems.insert(MediaId(namespace: Namespaces.Media.CloudFile, id: emojiStatus.fileId)) + + if let timestamp = emojiStatus.expirationDate { + topStatusTitle = peerStatusExpirationString(statusTimestamp: timestamp, relativeTo: Int32(Date().timeIntervalSince1970), strings: strongSelf.presentationData.strings, dateTimeFormat: strongSelf.presentationData.dateTimeFormat) + } } } + //TODO:localize let emojiStatusSelectionController = EmojiStatusSelectionController( context: strongSelf.context, mode: .statusSelection, @@ -3126,7 +3132,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate areUnicodeEmojiEnabled: false, areCustomEmojiEnabled: true, chatPeerId: strongSelf.context.account.peerId, - selectedItems: selectedItems + selectedItems: selectedItems, + topStatusTitle: topStatusTitle ), destinationItemView: { [weak sourceView] in return sourceView