Status and reaction improvements

This commit is contained in:
Ali 2022-09-05 03:57:37 +04:00
parent e4f1c6aa72
commit 7020c5ed05
33 changed files with 907 additions and 67 deletions

View File

@ -8022,3 +8022,12 @@ Sorry for the inconvenience.";
"SetTimeoutFor.Days_1" = "Set for 1 day"; "SetTimeoutFor.Days_1" = "Set for 1 day";
"SetTimeoutFor.Days_any" = "Set for %@ days"; "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.";

View File

@ -35,6 +35,7 @@ import AnimationCache
import MultiAnimationRenderer import MultiAnimationRenderer
import EmojiStatusSelectionComponent import EmojiStatusSelectionComponent
import EntityKeyboard import EntityKeyboard
import TelegramStringFormatting
private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool { private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool {
if listNode.scroller.isDragging { if listNode.scroller.isDragging {
@ -851,8 +852,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
private func openStatusSetup(sourceView: UIView) { private func openStatusSetup(sourceView: UIView) {
self.emojiStatusSelectionController?.dismiss() self.emojiStatusSelectionController?.dismiss()
var selectedItems = Set<MediaId>() var selectedItems = Set<MediaId>()
//TODO:localize
var topStatusTitle = "Long tap to set a timer"
if let peerStatus = self.titleView.title.peerStatus, case let .emoji(emojiStatus) = peerStatus { if let peerStatus = self.titleView.title.peerStatus, case let .emoji(emojiStatus) = peerStatus {
selectedItems.insert(MediaId(namespace: Namespaces.Media.CloudFile, id: emojiStatus.fileId)) 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( let controller = EmojiStatusSelectionController(
context: self.context, context: self.context,
@ -869,7 +876,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
areUnicodeEmojiEnabled: false, areUnicodeEmojiEnabled: false,
areCustomEmojiEnabled: true, areCustomEmojiEnabled: true,
chatPeerId: self.context.account.peerId, chatPeerId: self.context.account.peerId,
selectedItems: selectedItems selectedItems: selectedItems,
topStatusTitle: topStatusTitle
), ),
destinationItemView: { [weak sourceView] in destinationItemView: { [weak sourceView] in
return sourceView return sourceView

View File

@ -1522,7 +1522,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
} }
if !items.reactionItems.isEmpty, let context = items.context, let animationCache = items.animationCache { 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.reactionContextNode = reactionContextNode
self.addSubnode(reactionContextNode) self.addSubnode(reactionContextNode)
@ -2399,17 +2399,19 @@ public final class ContextController: ViewController, StandalonePresentableContr
public var content: Content public var content: Content
public var context: AccountContext? public var context: AccountContext?
public var reactionItems: [ReactionContextItem] public var reactionItems: [ReactionContextItem]
public var selectedReactionItems: Set<MessageReaction.Reaction>
public var animationCache: AnimationCache? public var animationCache: AnimationCache?
public var getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)? public var getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?
public var disablePositionLock: Bool public var disablePositionLock: Bool
public var tip: Tip? public var tip: Tip?
public var tipSignal: Signal<Tip?, NoError>? public var tipSignal: Signal<Tip?, NoError>?
public init(content: Content, context: AccountContext? = nil, reactionItems: [ReactionContextItem] = [], animationCache: AnimationCache? = nil, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)? = nil, disablePositionLock: Bool = false, tip: Tip? = nil, tipSignal: Signal<Tip?, NoError>? = nil) { public init(content: Content, context: AccountContext? = nil, reactionItems: [ReactionContextItem] = [], selectedReactionItems: Set<MessageReaction.Reaction> = Set(), animationCache: AnimationCache? = nil, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)? = nil, disablePositionLock: Bool = false, tip: Tip? = nil, tipSignal: Signal<Tip?, NoError>? = nil) {
self.content = content self.content = content
self.context = context self.context = context
self.animationCache = animationCache self.animationCache = animationCache
self.reactionItems = reactionItems self.reactionItems = reactionItems
self.selectedReactionItems = selectedReactionItems
self.getEmojiContent = getEmojiContent self.getEmojiContent = getEmojiContent
self.disablePositionLock = disablePositionLock self.disablePositionLock = disablePositionLock
self.tip = tip self.tip = tip
@ -2420,6 +2422,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
self.content = .list([]) self.content = .list([])
self.context = nil self.context = nil
self.reactionItems = [] self.reactionItems = []
self.selectedReactionItems = Set()
self.getEmojiContent = nil self.getEmojiContent = nil
self.disablePositionLock = false self.disablePositionLock = false
self.tip = nil self.tip = nil

View File

@ -42,7 +42,7 @@ public protocol ContextControllerActionsStackItem: AnyObject {
var tip: ContextController.Tip? { get } var tip: ContextController.Tip? { get }
var tipSignal: Signal<ContextController.Tip?, NoError>? { get } var tipSignal: Signal<ContextController.Tip?, NoError>? { get }
var reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)? { get } var reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)? { get }
} }
protocol ContextControllerActionsListItemNode: ASDisplayNode { protocol ContextControllerActionsListItemNode: ASDisplayNode {
@ -631,13 +631,13 @@ final class ContextControllerActionsListStackItem: ContextControllerActionsStack
} }
private let items: [ContextMenuItem] private let items: [ContextMenuItem]
let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)? let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?
let tip: ContextController.Tip? let tip: ContextController.Tip?
let tipSignal: Signal<ContextController.Tip?, NoError>? let tipSignal: Signal<ContextController.Tip?, NoError>?
init( init(
items: [ContextMenuItem], items: [ContextMenuItem],
reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?, reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?,
tip: ContextController.Tip?, tip: ContextController.Tip?,
tipSignal: Signal<ContextController.Tip?, NoError>? tipSignal: Signal<ContextController.Tip?, NoError>?
) { ) {
@ -723,13 +723,13 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta
} }
private let content: ContextControllerItemsContent private let content: ContextControllerItemsContent
let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)? let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?
let tip: ContextController.Tip? let tip: ContextController.Tip?
let tipSignal: Signal<ContextController.Tip?, NoError>? let tipSignal: Signal<ContextController.Tip?, NoError>?
init( init(
content: ContextControllerItemsContent, content: ContextControllerItemsContent,
reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?, reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?,
tip: ContextController.Tip?, tip: ContextController.Tip?,
tipSignal: Signal<ContextController.Tip?, NoError>? tipSignal: Signal<ContextController.Tip?, NoError>?
) { ) {
@ -755,9 +755,9 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta
} }
func makeContextControllerActionsStackItem(items: ContextController.Items) -> ContextControllerActionsStackItem { func makeContextControllerActionsStackItem(items: ContextController.Items) -> ContextControllerActionsStackItem {
var reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)? var reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?
if let context = items.context, let animationCache = items.animationCache, !items.reactionItems.isEmpty { 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 { switch items.content {
case let .list(listItems): case let .list(listItems):
@ -874,7 +874,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
var tip: ContextController.Tip? var tip: ContextController.Tip?
let tipSignal: Signal<ContextController.Tip?, NoError>? let tipSignal: Signal<ContextController.Tip?, NoError>?
var tipNode: InnerTextSelectionTipContainerNode? var tipNode: InnerTextSelectionTipContainerNode?
let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)? let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?
var storedScrollingState: CGFloat? var storedScrollingState: CGFloat?
let positionLock: CGFloat? let positionLock: CGFloat?
@ -888,7 +888,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
item: ContextControllerActionsStackItem, item: ContextControllerActionsStackItem,
tip: ContextController.Tip?, tip: ContextController.Tip?,
tipSignal: Signal<ContextController.Tip?, NoError>?, tipSignal: Signal<ContextController.Tip?, NoError>?,
reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?, reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?,
positionLock: CGFloat? positionLock: CGFloat?
) { ) {
self.getController = getController self.getController = getController
@ -1032,7 +1032,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
private var selectionPanGesture: UIPanGestureRecognizer? private var selectionPanGesture: UIPanGestureRecognizer?
var topReactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)? { var topReactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)? {
return self.itemContainers.last?.reactionItems return self.itemContainers.last?.reactionItems
} }

View File

@ -518,6 +518,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
animationCache: reactionItems.animationCache, animationCache: reactionItems.animationCache,
presentationData: presentationData, presentationData: presentationData,
items: reactionItems.reactionItems, items: reactionItems.reactionItems,
selectedItems: reactionItems.selectedReactionItems,
getEmojiContent: reactionItems.getEmojiContent, getEmojiContent: reactionItems.getEmojiContent,
isExpandedUpdated: { [weak self] transition in isExpandedUpdated: { [weak self] transition in
guard let strongSelf = self else { guard let strongSelf = self else {

View File

@ -387,7 +387,7 @@ open class BlurredBackgroundView: UIView {
self.updateBackgroundBlur(forceKeepBlur: forceKeepBlur) 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) self.validLayout = (size, cornerRadius)
let contentFrame = CGRect(origin: CGPoint(), size: size) let contentFrame = CGRect(origin: CGPoint(), size: size)
@ -401,10 +401,18 @@ open class BlurredBackgroundView: UIView {
} }
} }
if #available(iOS 11.0, *) {
self.backgroundView.layer.maskedCorners = maskedCorners
}
transition.updateCornerRadius(layer: self.backgroundView.layer, cornerRadius: cornerRadius) transition.updateCornerRadius(layer: self.backgroundView.layer, cornerRadius: cornerRadius)
if let effectView = self.effectView { if let effectView = self.effectView {
transition.updateCornerRadius(layer: effectView.layer, cornerRadius: cornerRadius) transition.updateCornerRadius(layer: effectView.layer, cornerRadius: cornerRadius)
effectView.clipsToBounds = !cornerRadius.isZero effectView.clipsToBounds = !cornerRadius.isZero
if #available(iOS 11.0, *) {
effectView.layer.maskedCorners = maskedCorners
}
} }
} }

View File

@ -300,6 +300,7 @@ public protocol Peer: AnyObject, PostboxCoding {
var associatedPeerId: PeerId? { get } var associatedPeerId: PeerId? { get }
var notificationSettingsPeerId: PeerId? { get } var notificationSettingsPeerId: PeerId? { get }
var associatedMediaIds: [MediaId]? { get } var associatedMediaIds: [MediaId]? { get }
var timeoutAttribute: UInt32? { get }
func isEqual(_ other: Peer) -> Bool func isEqual(_ other: Peer) -> Bool
} }

View File

@ -6,7 +6,7 @@ final class PeerTable: Table {
} }
private let reverseAssociatedTable: ReverseAssociatedPeerTable private let reverseAssociatedTable: ReverseAssociatedPeerTable
//private let peerTimeoutPropertiesTable: PeerTimeoutPropertiesTable private let peerTimeoutPropertiesTable: PeerTimeoutPropertiesTable
private let sharedEncoder = PostboxEncoder() private let sharedEncoder = PostboxEncoder()
private let sharedKey = ValueBoxKey(length: 8) private let sharedKey = ValueBoxKey(length: 8)
@ -14,8 +14,9 @@ final class PeerTable: Table {
private var cachedPeers: [PeerId: Peer] = [:] private var cachedPeers: [PeerId: Peer] = [:]
private var updatedInitialPeers: [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.reverseAssociatedTable = reverseAssociatedTable
self.peerTimeoutPropertiesTable = peerTimeoutPropertiesTable
super.init(valueBox: valueBox, table: table, useCaches: useCaches) super.init(valueBox: valueBox, table: table, useCaches: useCaches)
} }
@ -64,6 +65,24 @@ final class PeerTable: Table {
return result 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() { override func beforeCommit() {
if !self.updatedInitialPeers.isEmpty { if !self.updatedInitialPeers.isEmpty {
for (peerId, previousPeer) in self.updatedInitialPeers { for (peerId, previousPeer) in self.updatedInitialPeers {

View File

@ -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
}
}

View File

@ -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<Key>()
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()
}
}
}
}

View File

@ -1153,6 +1153,11 @@ public final class Transaction {
assert(!self.disposed) assert(!self.disposed)
self.postbox?.clearTimestampBasedAttribute(id: id, tag: tag) 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 { public enum PostboxResult {
@ -1464,6 +1469,7 @@ final class PostboxImpl {
let deviceContactImportInfoTable: DeviceContactImportInfoTable let deviceContactImportInfoTable: DeviceContactImportInfoTable
let messageHistoryHoleIndexTable: MessageHistoryHoleIndexTable let messageHistoryHoleIndexTable: MessageHistoryHoleIndexTable
let groupMessageStatsTable: GroupMessageStatsTable let groupMessageStatsTable: GroupMessageStatsTable
let peerTimeoutPropertiesTable: PeerTimeoutPropertiesTable
//temporary //temporary
let peerRatingTable: RatingTable<PeerId> let peerRatingTable: RatingTable<PeerId>
@ -1487,7 +1493,8 @@ final class PostboxImpl {
self.keychainTable = KeychainTable(valueBox: self.valueBox, table: KeychainTable.tableSpec(1), useCaches: useCaches) 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.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.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.globallyUniqueMessageIdsTable = MessageGloballyUniqueIdTable(valueBox: self.valueBox, table: MessageGloballyUniqueIdTable.tableSpec(32), useCaches: useCaches)
self.messageHistoryMetadataTable = MessageHistoryMetadataTable(valueBox: self.valueBox, table: MessageHistoryMetadataTable.tableSpec(10), 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.deviceContactImportInfoTable)
tables.append(self.messageHistoryHoleIndexTable) tables.append(self.messageHistoryHoleIndexTable)
tables.append(self.groupMessageStatsTable) tables.append(self.groupMessageStatsTable)
tables.append(self.peerTimeoutPropertiesTable)
self.tables = tables self.tables = tables
@ -1992,11 +2000,15 @@ final class PostboxImpl {
let transactionParticipationInTotalUnreadCountUpdates = self.peerNotificationSettingsTable.transactionParticipationInTotalUnreadCountUpdates(postbox: self, transaction: currentTransaction) 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.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 { if self.currentNeedsReindexUnreadCounters {
self.reindexUnreadCounters(currentTransaction: currentTransaction) 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 updatedTransactionState: Int64?
var updatedMasterClientId: Int64? var updatedMasterClientId: Int64?
if !transaction.isEmpty { if !transaction.isEmpty {
@ -3645,6 +3657,10 @@ final class PostboxImpl {
self.timestampBasedMessageAttributesTable.remove(tag: tag, id: id, operations: &self.currentTimestampBasedMessageAttributesOperations) 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) { fileprivate func reindexUnreadCounters(currentTransaction: Transaction) {
self.groupMessageStatsTable.removeAll() self.groupMessageStatsTable.removeAll()
let _ = CFAbsoluteTimeGetCurrent() let _ = CFAbsoluteTimeGetCurrent()

View File

@ -43,6 +43,7 @@ final class PostboxTransaction {
let updatedFailedMessagePeerIds: Set<PeerId> let updatedFailedMessagePeerIds: Set<PeerId>
let updatedFailedMessageIds: Set<MessageId> let updatedFailedMessageIds: Set<MessageId>
let updatedGlobalNotificationSettings: Bool let updatedGlobalNotificationSettings: Bool
let updatedPeerTimeoutAttributes: Bool
var isEmpty: Bool { var isEmpty: Bool {
if currentUpdatedState != nil { if currentUpdatedState != nil {
@ -171,10 +172,13 @@ final class PostboxTransaction {
if self.updatedGlobalNotificationSettings { if self.updatedGlobalNotificationSettings {
return false return false
} }
if self.updatedPeerTimeoutAttributes {
return false
}
return true 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<PeerId>, 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<PeerId>, currentGlobalTagsOperations: [GlobalMessageHistoryTagsOperation], currentLocalTagsOperations: [IntermediateMessageHistoryLocalTagsOperation], updatedMedia: [MediaId: Media?], replaceRemoteContactCount: Int32?, replaceContactPeerIds: Set<PeerId>?, currentPendingMessageActionsOperations: [PendingMessageActionsOperation], currentUpdatedMessageActionsSummaries: [PendingMessageActionsSummaryKey: Int32], currentUpdatedMessageTagSummaries: [MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary], currentInvalidateMessageTagSummaries: [InvalidatedMessageHistoryTagsSummaryEntryOperation], currentUpdatedPendingPeerNotificationSettings: Set<PeerId>, replacedAdditionalChatListItems: [AdditionalChatListItem]?, updatedNoticeEntryKeys: Set<NoticeEntryKey>, updatedCacheEntryKeys: Set<ItemCacheEntryId>, currentUpdatedMasterClientId: Int64?, updatedFailedMessagePeerIds: Set<PeerId>, updatedFailedMessageIds: Set<MessageId>, 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<PeerId>, 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<PeerId>, currentGlobalTagsOperations: [GlobalMessageHistoryTagsOperation], currentLocalTagsOperations: [IntermediateMessageHistoryLocalTagsOperation], updatedMedia: [MediaId: Media?], replaceRemoteContactCount: Int32?, replaceContactPeerIds: Set<PeerId>?, currentPendingMessageActionsOperations: [PendingMessageActionsOperation], currentUpdatedMessageActionsSummaries: [PendingMessageActionsSummaryKey: Int32], currentUpdatedMessageTagSummaries: [MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary], currentInvalidateMessageTagSummaries: [InvalidatedMessageHistoryTagsSummaryEntryOperation], currentUpdatedPendingPeerNotificationSettings: Set<PeerId>, replacedAdditionalChatListItems: [AdditionalChatListItem]?, updatedNoticeEntryKeys: Set<NoticeEntryKey>, updatedCacheEntryKeys: Set<ItemCacheEntryId>, currentUpdatedMasterClientId: Int64?, updatedFailedMessagePeerIds: Set<PeerId>, updatedFailedMessageIds: Set<MessageId>, updatedGlobalNotificationSettings: Bool, updatedPeerTimeoutAttributes: Bool) {
self.currentUpdatedState = currentUpdatedState self.currentUpdatedState = currentUpdatedState
self.currentPeerHoleOperations = currentPeerHoleOperations self.currentPeerHoleOperations = currentPeerHoleOperations
self.currentOperationsByPeerId = currentOperationsByPeerId self.currentOperationsByPeerId = currentOperationsByPeerId
@ -216,5 +220,6 @@ final class PostboxTransaction {
self.updatedFailedMessagePeerIds = updatedFailedMessagePeerIds self.updatedFailedMessagePeerIds = updatedFailedMessagePeerIds
self.updatedFailedMessageIds = updatedFailedMessageIds self.updatedFailedMessageIds = updatedFailedMessageIds
self.updatedGlobalNotificationSettings = updatedGlobalNotificationSettings self.updatedGlobalNotificationSettings = updatedGlobalNotificationSettings
self.updatedPeerTimeoutAttributes = updatedPeerTimeoutAttributes
} }
} }

View File

@ -37,6 +37,7 @@ public enum PostboxViewKey: Hashable {
case messageGroup(id: MessageId) case messageGroup(id: MessageId)
case isContact(id: PeerId) case isContact(id: PeerId)
case chatListIndex(id: PeerId) case chatListIndex(id: PeerId)
case peerTimeoutAttributes
public func hash(into hasher: inout Hasher) { public func hash(into hasher: inout Hasher) {
switch self { switch self {
@ -121,6 +122,8 @@ public enum PostboxViewKey: Hashable {
hasher.combine(id) hasher.combine(id)
case let .chatListIndex(id): case let .chatListIndex(id):
hasher.combine(id) hasher.combine(id)
case .peerTimeoutAttributes:
hasher.combine(17)
} }
} }
@ -342,6 +345,12 @@ public enum PostboxViewKey: Hashable {
} else { } else {
return false 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) return MutableIsContactView(postbox: postbox, id: id)
case let .chatListIndex(id): case let .chatListIndex(id):
return MutableChatListIndexView(postbox: postbox, id: id) return MutableChatListIndexView(postbox: postbox, id: id)
case .peerTimeoutAttributes:
return MutablePeerTimeoutAttributesView(postbox: postbox)
} }
} }

View File

@ -126,6 +126,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
private let animationCache: AnimationCache private let animationCache: AnimationCache
private let animationRenderer: MultiAnimationRenderer private let animationRenderer: MultiAnimationRenderer
private let items: [ReactionContextItem] private let items: [ReactionContextItem]
private let selectedItems: Set<MessageReaction.Reaction>
private let getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)? private let getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?
private let isExpandedUpdated: (ContainedViewLayoutTransition) -> Void private let isExpandedUpdated: (ContainedViewLayoutTransition) -> Void
private let requestLayout: (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<EmojiPagerContentComponent, NoError>)?, isExpandedUpdated: @escaping (ContainedViewLayoutTransition) -> Void, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void) { public init(context: AccountContext, animationCache: AnimationCache, presentationData: PresentationData, items: [ReactionContextItem], selectedItems: Set<MessageReaction.Reaction>, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?, isExpandedUpdated: @escaping (ContainedViewLayoutTransition) -> Void, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void) {
self.context = context self.context = context
self.presentationData = presentationData self.presentationData = presentationData
self.items = items self.items = items
self.selectedItems = selectedItems
self.getEmojiContent = getEmojiContent self.getEmojiContent = getEmojiContent
self.isExpandedUpdated = isExpandedUpdated self.isExpandedUpdated = isExpandedUpdated
self.requestLayout = requestLayout self.requestLayout = requestLayout
@ -639,11 +641,16 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
validIndices.insert(i) validIndices.insert(i)
var itemFrame = baseItemFrame var itemFrame = baseItemFrame
var selectionItemFrame = itemFrame
let normalItemScale: CGFloat = 1.0
var isPreviewing = false var isPreviewing = false
if let highlightedReaction = self.highlightedReaction, highlightedReaction == self.items[i].reaction { if let highlightedReaction = self.highlightedReaction, highlightedReaction == self.items[i].reaction {
isPreviewing = true 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 var animateIn = false
@ -668,6 +675,12 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
self.visibleItemNodes[i] = itemNode self.visibleItemNodes[i] = itemNode
self.scrollNode.addSubnode(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 { if let maskNode = maskNode {
self.visibleItemMaskNodes[i] = maskNode self.visibleItemMaskNodes[i] = maskNode
@ -690,7 +703,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
} }
if self.getEmojiContent != nil && i == visibleItemCount - 1 { 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 itemNode.isUserInteractionEnabled = false
} else { } else {
itemNode.isUserInteractionEnabled = true 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) 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 { if animateIn {
itemNode.appear(animated: !self.context.sharedContext.currentPresentationData.with({ $0 }).reduceMotion) itemNode.appear(animated: !self.context.sharedContext.currentPresentationData.with({ $0 }).reduceMotion)
} }
if self.getEmojiContent != nil && i == visibleItemCount - 1 { 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 let alphaFraction = min(compressionFactor, 0.2) / 0.2
transition.updateAlpha(node: itemNode, alpha: alphaFraction) 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 self.contentContainer.view.mask = nil
for (_, itemNode) in self.visibleItemNodes { for (_, itemNode) in self.visibleItemNodes {
itemNode.isHidden = true 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 { 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 { for (_, itemNode) in self.visibleItemNodes {
guard let itemNode = itemNode as? ReactionNode else { guard let itemNode = itemNode as? ReactionNode else {
continue continue
@ -902,7 +935,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
continue continue
} }
initialPositionAndFrame[itemNode.item.stillAnimation.fileId] = ( initialPositionAndFrame[itemNode.item.stillAnimation.fileId] = (
position: itemNode.frame.center, frame: itemNode.frame,
frameIndex: itemNode.currentFrameIndex, frameIndex: itemNode.currentFrameIndex,
placeholder: placeholder placeholder: placeholder
) )
@ -911,7 +944,10 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
emojiView.animateInReactionSelection(sourceItems: initialPositionAndFrame) emojiView.animateInReactionSelection(sourceItems: initialPositionAndFrame)
if let mirrorContentClippingView = emojiView.mirrorContentClippingView { 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 { 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 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)
}) })
} }

View File

@ -53,6 +53,9 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
private let hasAppearAnimation: Bool private let hasAppearAnimation: Bool
private let useDirectRendering: Bool private let useDirectRendering: Bool
let selectionTintView: UIView
let selectionView: UIView
private var animateInAnimationNode: AnimatedStickerNode? private var animateInAnimationNode: AnimatedStickerNode?
private let staticAnimationNode: AnimatedStickerNode private let staticAnimationNode: AnimatedStickerNode
private var stillAnimationNode: AnimatedStickerNode? private var stillAnimationNode: AnimatedStickerNode?
@ -87,6 +90,14 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
self.hasAppearAnimation = hasAppearAnimation self.hasAppearAnimation = hasAppearAnimation
self.useDirectRendering = useDirectRendering 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() self.staticAnimationNode = self.useDirectRendering ? DirectAnimatedStickerNode() : DefaultAnimatedStickerNodeImpl()
if hasAppearAnimation { if hasAppearAnimation {
@ -147,8 +158,18 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
} else { } else {
self.animateInAnimationNode?.visibility = true 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 { } else {
self.animateInAnimationNode?.completed(true) self.animateInAnimationNode?.completed(true)
self.selectionView.alpha = 1.0
self.selectionTintView.alpha = 1.0
} }
} }

View File

@ -1074,6 +1074,7 @@ public class Account {
self.managedOperationsDisposable.add(managedCloudChatRemoveMessagesOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) 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: true).start())
self.managedOperationsDisposable.add(managedAutoremoveMessageOperations(network: self.network, postbox: self.postbox, isRemove: false).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(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()) self.managedOperationsDisposable.add(managedSynchronizePinnedChatsOperations(postbox: self.postbox, network: self.network, accountPeerId: self.peerId, stateManager: self.stateManager).start())

View File

@ -4,7 +4,6 @@ import SwiftSignalKit
import TelegramApi import TelegramApi
import MtProtoKit import MtProtoKit
private typealias SignalKitTimer = SwiftSignalKit.Timer private typealias SignalKitTimer = SwiftSignalKit.Timer
private final class ManagedAutoremoveMessageOperationsHelper { private final class ManagedAutoremoveMessageOperationsHelper {

View File

@ -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<Void, NoError> {
return Signal { _ in
let helper = Atomic(value: ManagedPeerTimestampAttributeOperationsHelper())
let timeOffsetOnce = Signal<Double, NoError> { subscriber in
subscriber.putNext(network.globalTimeDifference)
return EmptyDisposable
}
let timeOffset = (
timeOffsetOnce
|> then(
Signal<Double, NoError>.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<Void, NoError>.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()
}
}
}
}

View File

@ -174,6 +174,8 @@ public final class TelegramChannel: Peer, Equatable {
public let associatedPeerId: PeerId? = nil public let associatedPeerId: PeerId? = nil
public let notificationSettingsPeerId: 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?) { 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.id = id
self.accessHash = accessHash self.accessHash = accessHash

View File

@ -97,6 +97,8 @@ public final class TelegramGroup: Peer, Equatable {
public let associatedPeerId: PeerId? = nil public let associatedPeerId: PeerId? = nil
public let notificationSettingsPeerId: 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) { 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.id = id
self.title = title self.title = title

View File

@ -19,6 +19,8 @@ public final class TelegramSecretChat: Peer, Equatable {
public let associatedPeerId: PeerId? public let associatedPeerId: PeerId?
public let notificationSettingsPeerId: 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?) { public init(id: PeerId, creationDate: Int32, regularPeerId: PeerId, accessHash: Int64, role: SecretChatRole, embeddedState: SecretChatEmbeddedPeerState, messageAutoremoveTimeout: Int32?) {
self.id = id self.id = id
self.regularPeerId = regularPeerId self.regularPeerId = regularPeerId

View File

@ -115,6 +115,18 @@ public final class TelegramUser: Peer, Equatable {
public let associatedPeerId: PeerId? = nil public let associatedPeerId: PeerId? = nil
public let notificationSettingsPeerId: 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?) { 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.id = id
self.accessHash = accessHash self.accessHash = accessHash

View File

@ -76,7 +76,7 @@ public extension TelegramEngine {
if let entry = CodableEntry(RecentMediaItem(file)) { if let entry = CodableEntry(RecentMediaItem(file)) {
let itemEntry = OrderedItemListEntry(id: RecentMediaItemId(file.fileId).rawValue, contents: entry) 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)
} }
} }

View File

@ -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 { public func userPresenceStringRefreshTimeout(_ presence: TelegramUserPresence, relativeTo timestamp: Int32) -> Double {
switch presence.status { switch presence.status {
case let .present(statusTimestamp): case let .present(statusTimestamp):

View File

@ -24,12 +24,15 @@ swift_library(
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
"//submodules/Components/PagerComponent:PagerComponent", "//submodules/Components/PagerComponent:PagerComponent",
"//submodules/Components/MultilineTextComponent:MultilineTextComponent", "//submodules/Components/MultilineTextComponent:MultilineTextComponent",
"//submodules/Components/SolidRoundedButtonComponent:SolidRoundedButtonComponent",
"//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/AccountContext:AccountContext", "//submodules/AccountContext:AccountContext",
"//submodules/lottie-ios:Lottie", "//submodules/lottie-ios:Lottie",
"//submodules/TextFormat:TextFormat", "//submodules/TextFormat:TextFormat",
"//submodules/AppBundle:AppBundle", "//submodules/AppBundle:AppBundle",
"//submodules/GZip:GZip", "//submodules/GZip:GZip",
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
"//submodules/PresentationDataUtils:PresentationDataUtils",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -10,18 +10,26 @@ import AccountContext
import ComponentDisplayAdapters import ComponentDisplayAdapters
import MultilineTextComponent import MultilineTextComponent
import EmojiStatusComponent import EmojiStatusComponent
import TelegramStringFormatting
import SolidRoundedButtonComponent
import PresentationDataUtils
protocol ContextMenuItemWithAction: AnyObject { protocol ContextMenuItemWithAction: AnyObject {
func performAction() func performAction() -> ContextMenuPerformActionResult
}
enum ContextMenuPerformActionResult {
case none
case clearHighlight
} }
private final class ContextMenuActionItem: Component, ContextMenuItemWithAction { private final class ContextMenuActionItem: Component, ContextMenuItemWithAction {
typealias EnvironmentType = ContextMenuActionItemEnvironment typealias EnvironmentType = ContextMenuActionItemEnvironment
let title: String 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.title = title
self.action = action self.action = action
} }
@ -33,8 +41,8 @@ private final class ContextMenuActionItem: Component, ContextMenuItemWithAction
return true return true
} }
func performAction() { func performAction() -> ContextMenuPerformActionResult {
self.action() return self.action()
} }
final class View: UIView { final class View: UIView {
@ -169,7 +177,12 @@ private final class ContextMenuActionsComponent: Component {
for item in component.items { for item in component.items {
if item.id == id { if item.id == id {
if let itemComponent = item.component.wrapped as? ContextMenuItemWithAction { if let itemComponent = item.component.wrapped as? ContextMenuItemWithAction {
itemComponent.performAction() switch itemComponent.performAction() {
case .none:
break
case .clearHighlight:
self.setHighlightedItem(id: nil)
}
} }
break 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<Empty>
private let leftButtonView: ComponentView<Empty>
private let actionButtonView: ComponentView<Empty>
private var component: TimeSelectionControlComponent?
override init(frame: CGRect) {
self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
self.pickerView = UIDatePicker()
self.titleView = ComponentView<Empty>()
self.leftButtonView = ComponentView<Empty>()
self.actionButtonView = ComponentView<Empty>()
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<Empty>, 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<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class EmojiStatusPreviewScreenComponent: Component { final class EmojiStatusPreviewScreenComponent: Component {
struct StatusResult { struct StatusResult {
let timeout: Int let timestamp: Int32
let sourceView: UIView let sourceView: UIView
} }
@ -350,21 +547,29 @@ final class EmojiStatusPreviewScreenComponent: Component {
} }
} }
private enum CurrentState {
case menu
case timeSelection
}
typealias EnvironmentType = Empty typealias EnvironmentType = Empty
let theme: PresentationTheme let theme: PresentationTheme
let strings: PresentationStrings let strings: PresentationStrings
let bottomInset: CGFloat
let item: EmojiStatusComponent let item: EmojiStatusComponent
let dismiss: (StatusResult?) -> Void let dismiss: (StatusResult?) -> Void
init( init(
theme: PresentationTheme, theme: PresentationTheme,
strings: PresentationStrings, strings: PresentationStrings,
bottomInset: CGFloat,
item: EmojiStatusComponent, item: EmojiStatusComponent,
dismiss: @escaping (StatusResult?) -> Void dismiss: @escaping (StatusResult?) -> Void
) { ) {
self.theme = theme self.theme = theme
self.strings = strings self.strings = strings
self.bottomInset = bottomInset
self.item = item self.item = item
self.dismiss = dismiss self.dismiss = dismiss
} }
@ -376,6 +581,9 @@ final class EmojiStatusPreviewScreenComponent: Component {
if lhs.strings !== rhs.strings { if lhs.strings !== rhs.strings {
return false return false
} }
if lhs.bottomInset != rhs.bottomInset {
return false
}
if lhs.item != rhs.item { if lhs.item != rhs.item {
return false return false
} }
@ -386,13 +594,18 @@ final class EmojiStatusPreviewScreenComponent: Component {
private let backgroundView: BlurredBackgroundView private let backgroundView: BlurredBackgroundView
private let itemView: ComponentView<Empty> private let itemView: ComponentView<Empty>
private let actionsView: ComponentView<Empty> private let actionsView: ComponentView<Empty>
private let timeSelectionView: ComponentView<Empty>
private var currentState: CurrentState = .menu
private var component: EmojiStatusPreviewScreenComponent? private var component: EmojiStatusPreviewScreenComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) { override init(frame: CGRect) {
self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
self.itemView = ComponentView<Empty>() self.itemView = ComponentView<Empty>()
self.actionsView = ComponentView<Empty>() self.actionsView = ComponentView<Empty>()
self.timeSelectionView = ComponentView<Empty>()
super.init(frame: frame) super.init(frame: frame)
@ -406,12 +619,29 @@ final class EmojiStatusPreviewScreenComponent: Component {
@objc private func backgroundTapGesture(_ recognizer: UITapGestureRecognizer) { @objc private func backgroundTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state { if case .ended = recognizer.state {
switch self.currentState {
case .menu:
self.component?.dismiss(nil) 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<EnvironmentType>, transition: Transition) -> CGSize { func update(component: EmojiStatusPreviewScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.component = component self.component = component
self.state = state
let itemSpacing: CGFloat = 12.0 let itemSpacing: CGFloat = 12.0
@ -434,15 +664,25 @@ final class EmojiStatusPreviewScreenComponent: Component {
title: setTimeoutForIntervalString(strings: component.strings, value: Int32(duration)), title: setTimeoutForIntervalString(strings: component.strings, value: Int32(duration)),
action: { [weak self] in action: { [weak self] in
guard let strongSelf = self, let component = strongSelf.component else { guard let strongSelf = self, let component = strongSelf.component else {
return return .none
} }
guard let itemComponentView = strongSelf.itemView.view else { 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( let actionsSize = self.actionsView.update(
transition: transition, transition: transition,
component: AnyComponent(ContextMenuActionsComponent( component: AnyComponent(ContextMenuActionsComponent(
@ -453,13 +693,41 @@ final class EmojiStatusPreviewScreenComponent: Component {
containerSize: availableSize 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 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 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) 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 let itemComponentView = self.itemView.view {
if itemComponentView.superview == nil { if itemComponentView.superview == nil {
self.addSubview(itemComponentView) self.addSubview(itemComponentView)
@ -471,7 +739,25 @@ final class EmojiStatusPreviewScreenComponent: Component {
if actionsComponentView.superview == nil { if actionsComponentView.superview == nil {
self.addSubview(actionsComponentView) 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) 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) 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 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 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 { } else {
self.backgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) self.backgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
if let actionsComponentView = self.actionsView.view { if let actionsComponentView = self.actionsView.view {
actionsComponentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in actionsComponentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
completion() 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 { } else {
completion() completion()
} }

View File

@ -744,6 +744,7 @@ public final class EmojiStatusSelectionController: ViewController {
component: AnyComponent(EmojiStatusPreviewScreenComponent( component: AnyComponent(EmojiStatusPreviewScreenComponent(
theme: self.presentationData.theme, theme: self.presentationData.theme,
strings: self.presentationData.strings, strings: self.presentationData.strings,
bottomInset: layout.insets(options: []).bottom,
item: EmojiStatusComponent( item: EmojiStatusComponent(
context: self.context, context: self.context,
animationCache: self.context.animationCache, animationCache: self.context.animationCache,
@ -809,10 +810,8 @@ public final class EmojiStatusSelectionController: ViewController {
return return
} }
var expirationDate: Int32? let expirationDate: Int32? = result.timestamp
if result.timeout > 0 {
expirationDate = Int32(Date().timeIntervalSince1970) + Int32(result.timeout)
}
let _ = (strongSelf.context.engine.accountData.setEmojiStatus(file: previewItem.item.itemFile, expirationDate: expirationDate) let _ = (strongSelf.context.engine.accountData.setEmojiStatus(file: previewItem.item.itemFile, expirationDate: expirationDate)
|> deliverOnMainQueue).start() |> deliverOnMainQueue).start()

View File

@ -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 { guard let component = self.component, let itemLayout = self.itemLayout else {
return return
} }
@ -2792,10 +2792,12 @@ public final class EmojiPagerContentComponent: Component {
continue continue
} }
if let sourceItem = sourceItems[file.fileId] { 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] { 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) 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() itemLayer?.removeFromSuperlayer()
}) })
if let itemSelectionLayer = itemSelectionLayer { 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() itemSelectionLayer?.removeFromSuperlayer()
itemSelectionTintContainerLayer?.removeFromSuperlayer()
}) })
} }
} else { } else {
@ -4160,6 +4164,7 @@ public final class EmojiPagerContentComponent: Component {
itemLayer.removeFromSuperlayer() itemLayer.removeFromSuperlayer()
if let itemSelectionLayer = itemSelectionLayer { if let itemSelectionLayer = itemSelectionLayer {
itemSelectionLayer.removeFromSuperlayer() itemSelectionLayer.removeFromSuperlayer()
itemSelectionLayer.tintContainerLayer.removeFromSuperlayer()
} }
} }
} }
@ -4191,7 +4196,6 @@ public final class EmojiPagerContentComponent: Component {
} else { } else {
itemSelectionLayer.removeFromSuperlayer() itemSelectionLayer.removeFromSuperlayer()
removedItemSelectionLayerIds.append(id) removedItemSelectionLayerIds.append(id)
} }
} }
for id in removedItemSelectionLayerIds { for id in removedItemSelectionLayerIds {
@ -4782,7 +4786,21 @@ public final class EmojiPagerContentComponent: Component {
return hasPremium 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<MediaId> = Set()) -> Signal<EmojiPagerContentComponent, NoError> { 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<MediaId> = Set(),
topStatusTitle: String? = nil
) -> Signal<EmojiPagerContentComponent, NoError> {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let isPremiumDisabled = premiumConfiguration.isPremiumDisabled let isPremiumDisabled = premiumConfiguration.isPremiumDisabled
@ -4878,7 +4896,7 @@ public final class EmojiPagerContentComponent: Component {
} else { } else {
itemGroupIndexById[groupId] = itemGroups.count itemGroupIndexById[groupId] = itemGroups.count
//TODO:localize //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<MediaId>() var existingIds = Set<MediaId>()
@ -4964,7 +4982,7 @@ public final class EmojiPagerContentComponent: Component {
} }
} }
if let featuredStatusEmoji = featuredStatusEmoji { 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 { guard let item = item.contents.get(RecentMediaItem.self) else {
continue continue
} }

View File

@ -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)), 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), 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), peerMessageAllowedReactions(context: strongSelf.context, message: topMessage),
peerMessageSelectedReactionFiles(context: strongSelf.context, message: topMessage), peerMessageSelectedReactions(context: strongSelf.context, message: topMessage),
topMessageReactions(context: strongSelf.context, message: topMessage), topMessageReactions(context: strongSelf.context, message: topMessage),
ApplicationSpecificNotice.getChatTextSelectionTips(accountManager: strongSelf.context.sharedContext.accountManager) 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 { guard let strongSelf = self else {
return return
} }
@ -1062,6 +1062,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if canAddMessageReactions(message: topMessage), let allowedReactions = allowedReactions, !topReactions.isEmpty { if canAddMessageReactions(message: topMessage), let allowedReactions = allowedReactions, !topReactions.isEmpty {
actions.reactionItems = topReactions.map(ReactionContextItem.reaction) actions.reactionItems = topReactions.map(ReactionContextItem.reaction)
actions.selectedReactionItems = selectedReactions.reactions
if !actions.reactionItems.isEmpty { if !actions.reactionItems.isEmpty {
let reactionItems: [EmojiComponentReactionItem] = actions.reactionItems.compactMap { item -> EmojiComponentReactionItem? in let reactionItems: [EmojiComponentReactionItem] = actions.reactionItems.compactMap { item -> EmojiComponentReactionItem? in
@ -1102,7 +1103,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
areUnicodeEmojiEnabled: false, areUnicodeEmojiEnabled: false,
areCustomEmojiEnabled: true, areCustomEmojiEnabled: true,
chatPeerId: strongSelf.chatLocation.peerId, 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<Set<MediaId>, NoError> { func peerMessageSelectedReactions(context: AccountContext, message: Message) -> Signal<(reactions: Set<MessageReaction.Reaction>, files: Set<MediaId>), NoError> {
return context.engine.stickers.availableReactions() return context.engine.stickers.availableReactions()
|> take(1) |> take(1)
|> map { availableReactions -> Set<MediaId> in |> map { availableReactions -> (reactions: Set<MessageReaction.Reaction>, files: Set<MediaId>) in
var result = Set<MediaId>() var result = Set<MediaId>()
var reactions = Set<MessageReaction.Reaction>()
if let effectiveReactions = message.effectiveReactions { if let effectiveReactions = message.effectiveReactions {
for reaction in effectiveReactions { for reaction in effectiveReactions {
if !reaction.isSelected {
continue
}
reactions.insert(reaction.value)
switch reaction.value { switch reaction.value {
case .builtin: case .builtin:
if let availableReaction = availableReactions?.reactions.first(where: { $0.value == reaction.value }) { 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)
} }
} }

View File

@ -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) 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.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))) 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)))

View File

@ -77,8 +77,8 @@ func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceStat
if displayActionsPanel && (selectedContext == nil || selectedContext! <= .pinnedMessage) { if displayActionsPanel && (selectedContext == nil || selectedContext! <= .pinnedMessage) {
if let currentPanel = currentPanel as? ChatReportPeerTitlePanelNode { if let currentPanel = currentPanel as? ChatReportPeerTitlePanelNode {
return currentPanel return currentPanel
} else { } else if let controllerInteraction = controllerInteraction {
let panel = ChatReportPeerTitlePanelNode() let panel = ChatReportPeerTitlePanelNode(context: context, animationCache: controllerInteraction.presentationContext.animationCache, animationRenderer: controllerInteraction.presentationContext.animationRenderer)
panel.interfaceInteraction = interfaceInteraction panel.interfaceInteraction = interfaceInteraction
return panel return panel
} }

View File

@ -10,6 +10,10 @@ import TelegramStringFormatting
import TextFormat import TextFormat
import Markdown import Markdown
import ChatPresentationInterfaceState import ChatPresentationInterfaceState
import TextNodeWithEntities
import AnimationCache
import MultiAnimationRenderer
import AccountContext
private enum ChatReportPeerTitleButton: Equatable { private enum ChatReportPeerTitleButton: Equatable {
case block case block
@ -301,11 +305,16 @@ private final class ChatInfoTitlePanelPeerNearbyInfoNode: ASDisplayNode {
} }
final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode {
private let context: AccountContext
private let animationCache: AnimationCache
private let animationRenderer: MultiAnimationRenderer
private let separatorNode: ASDisplayNode private let separatorNode: ASDisplayNode
private let closeButton: HighlightableButtonNode private let closeButton: HighlightableButtonNode
private var buttons: [(ChatReportPeerTitleButton, UIButton)] = [] private var buttons: [(ChatReportPeerTitleButton, UIButton)] = []
private let textNode: ImmediateTextNode private let textNode: ImmediateTextNode
private var emojiStatusTextNode: TextNodeWithEntities?
private var theme: PresentationTheme? private var theme: PresentationTheme?
@ -314,7 +323,11 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode {
private var tapGestureRecognizer: UITapGestureRecognizer? 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 = ASDisplayNode()
self.separatorNode.isLayerBacked = true self.separatorNode.isLayerBacked = true
@ -481,10 +494,71 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode {
self.tapGestureRecognizer?.isEnabled = false self.tapGestureRecognizer?.isEnabled = false
} }
let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) 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)) 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 let initialPanelHeight = panelHeight
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel)))

View File

@ -3106,11 +3106,17 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
strongSelf.emojiStatusSelectionController?.dismiss() strongSelf.emojiStatusSelectionController?.dismiss()
var selectedItems = Set<MediaId>() var selectedItems = Set<MediaId>()
var topStatusTitle = "Long tap to set a timer"
if let peer = strongSelf.data?.peer { if let peer = strongSelf.data?.peer {
if let user = peer as? TelegramUser, let emojiStatus = user.emojiStatus { if let user = peer as? TelegramUser, let emojiStatus = user.emojiStatus {
selectedItems.insert(MediaId(namespace: Namespaces.Media.CloudFile, id: emojiStatus.fileId)) 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( let emojiStatusSelectionController = EmojiStatusSelectionController(
context: strongSelf.context, context: strongSelf.context,
mode: .statusSelection, mode: .statusSelection,
@ -3126,7 +3132,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
areUnicodeEmojiEnabled: false, areUnicodeEmojiEnabled: false,
areCustomEmojiEnabled: true, areCustomEmojiEnabled: true,
chatPeerId: strongSelf.context.account.peerId, chatPeerId: strongSelf.context.account.peerId,
selectedItems: selectedItems selectedItems: selectedItems,
topStatusTitle: topStatusTitle
), ),
destinationItemView: { [weak sourceView] in destinationItemView: { [weak sourceView] in
return sourceView return sourceView