mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Status and reaction improvements
This commit is contained in:
parent
e4f1c6aa72
commit
7020c5ed05
@ -8022,3 +8022,12 @@ Sorry for the inconvenience.";
|
||||
|
||||
"SetTimeoutFor.Days_1" = "Set for 1 day";
|
||||
"SetTimeoutFor.Days_any" = "Set for %@ days";
|
||||
|
||||
"PeerStatusExpiration.Minutes_1" = "Your status expires in one minute";
|
||||
"PeerStatusExpiration.Minutes_any" = "Your status expires in %@ minutes";
|
||||
"PeerStatusExpiration.Hours_1" = "Your status expires in one hour";
|
||||
"PeerStatusExpiration.Hours_any" = "Your status expires in %@ hours";
|
||||
"PeerStatusExpiration.TomorrowAt" = "Your status expires tomorrow at %@";
|
||||
"PeerStatusExpiration.AtDate" = "Your status expires on %@";
|
||||
|
||||
"Chat.PanelCustomStatusInfo" = "This account uses %@ as a custom status next to its name. Such emoji statuses are available to all subscribers of Telegram Premium.";
|
||||
|
@ -35,6 +35,7 @@ import AnimationCache
|
||||
import MultiAnimationRenderer
|
||||
import EmojiStatusSelectionComponent
|
||||
import EntityKeyboard
|
||||
import TelegramStringFormatting
|
||||
|
||||
private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool {
|
||||
if listNode.scroller.isDragging {
|
||||
@ -851,8 +852,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
private func openStatusSetup(sourceView: UIView) {
|
||||
self.emojiStatusSelectionController?.dismiss()
|
||||
var selectedItems = Set<MediaId>()
|
||||
//TODO:localize
|
||||
var topStatusTitle = "Long tap to set a timer"
|
||||
if let peerStatus = self.titleView.title.peerStatus, case let .emoji(emojiStatus) = peerStatus {
|
||||
selectedItems.insert(MediaId(namespace: Namespaces.Media.CloudFile, id: emojiStatus.fileId))
|
||||
|
||||
if let timestamp = emojiStatus.expirationDate {
|
||||
topStatusTitle = peerStatusExpirationString(statusTimestamp: timestamp, relativeTo: Int32(Date().timeIntervalSince1970), strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat)
|
||||
}
|
||||
}
|
||||
let controller = EmojiStatusSelectionController(
|
||||
context: self.context,
|
||||
@ -869,7 +876,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
areUnicodeEmojiEnabled: false,
|
||||
areCustomEmojiEnabled: true,
|
||||
chatPeerId: self.context.account.peerId,
|
||||
selectedItems: selectedItems
|
||||
selectedItems: selectedItems,
|
||||
topStatusTitle: topStatusTitle
|
||||
),
|
||||
destinationItemView: { [weak sourceView] in
|
||||
return sourceView
|
||||
|
@ -1522,7 +1522,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
}
|
||||
|
||||
if !items.reactionItems.isEmpty, let context = items.context, let animationCache = items.animationCache {
|
||||
let reactionContextNode = ReactionContextNode(context: context, animationCache: animationCache, presentationData: self.presentationData, items: items.reactionItems, getEmojiContent: items.getEmojiContent, isExpandedUpdated: { _ in }, requestLayout: { _ in })
|
||||
let reactionContextNode = ReactionContextNode(context: context, animationCache: animationCache, presentationData: self.presentationData, items: items.reactionItems, selectedItems: items.selectedReactionItems, getEmojiContent: items.getEmojiContent, isExpandedUpdated: { _ in }, requestLayout: { _ in })
|
||||
self.reactionContextNode = reactionContextNode
|
||||
self.addSubnode(reactionContextNode)
|
||||
|
||||
@ -2399,17 +2399,19 @@ public final class ContextController: ViewController, StandalonePresentableContr
|
||||
public var content: Content
|
||||
public var context: AccountContext?
|
||||
public var reactionItems: [ReactionContextItem]
|
||||
public var selectedReactionItems: Set<MessageReaction.Reaction>
|
||||
public var animationCache: AnimationCache?
|
||||
public var getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?
|
||||
public var disablePositionLock: Bool
|
||||
public var tip: Tip?
|
||||
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.context = context
|
||||
self.animationCache = animationCache
|
||||
self.reactionItems = reactionItems
|
||||
self.selectedReactionItems = selectedReactionItems
|
||||
self.getEmojiContent = getEmojiContent
|
||||
self.disablePositionLock = disablePositionLock
|
||||
self.tip = tip
|
||||
@ -2420,6 +2422,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
|
||||
self.content = .list([])
|
||||
self.context = nil
|
||||
self.reactionItems = []
|
||||
self.selectedReactionItems = Set()
|
||||
self.getEmojiContent = nil
|
||||
self.disablePositionLock = false
|
||||
self.tip = nil
|
||||
|
@ -42,7 +42,7 @@ public protocol ContextControllerActionsStackItem: AnyObject {
|
||||
|
||||
var tip: ContextController.Tip? { 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 {
|
||||
@ -631,13 +631,13 @@ final class ContextControllerActionsListStackItem: ContextControllerActionsStack
|
||||
}
|
||||
|
||||
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 tipSignal: Signal<ContextController.Tip?, NoError>?
|
||||
|
||||
init(
|
||||
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?,
|
||||
tipSignal: Signal<ContextController.Tip?, NoError>?
|
||||
) {
|
||||
@ -723,13 +723,13 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta
|
||||
}
|
||||
|
||||
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 tipSignal: Signal<ContextController.Tip?, NoError>?
|
||||
|
||||
init(
|
||||
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?,
|
||||
tipSignal: Signal<ContextController.Tip?, NoError>?
|
||||
) {
|
||||
@ -755,9 +755,9 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta
|
||||
}
|
||||
|
||||
func makeContextControllerActionsStackItem(items: ContextController.Items) -> ContextControllerActionsStackItem {
|
||||
var reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<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 {
|
||||
reactionItems = (context, items.reactionItems, animationCache, items.getEmojiContent)
|
||||
reactionItems = (context, items.reactionItems, items.selectedReactionItems, animationCache, items.getEmojiContent)
|
||||
}
|
||||
switch items.content {
|
||||
case let .list(listItems):
|
||||
@ -874,7 +874,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
|
||||
var tip: ContextController.Tip?
|
||||
let tipSignal: Signal<ContextController.Tip?, NoError>?
|
||||
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?
|
||||
let positionLock: CGFloat?
|
||||
|
||||
@ -888,7 +888,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
|
||||
item: ContextControllerActionsStackItem,
|
||||
tip: ContextController.Tip?,
|
||||
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?
|
||||
) {
|
||||
self.getController = getController
|
||||
@ -1032,7 +1032,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
|
||||
|
||||
private var selectionPanGesture: UIPanGestureRecognizer?
|
||||
|
||||
var topReactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<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
|
||||
}
|
||||
|
||||
|
@ -518,6 +518,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
|
||||
animationCache: reactionItems.animationCache,
|
||||
presentationData: presentationData,
|
||||
items: reactionItems.reactionItems,
|
||||
selectedItems: reactionItems.selectedReactionItems,
|
||||
getEmojiContent: reactionItems.getEmojiContent,
|
||||
isExpandedUpdated: { [weak self] transition in
|
||||
guard let strongSelf = self else {
|
||||
|
@ -387,7 +387,7 @@ open class BlurredBackgroundView: UIView {
|
||||
self.updateBackgroundBlur(forceKeepBlur: forceKeepBlur)
|
||||
}
|
||||
|
||||
public func update(size: CGSize, cornerRadius: CGFloat = 0.0, transition: ContainedViewLayoutTransition) {
|
||||
public func update(size: CGSize, cornerRadius: CGFloat = 0.0, maskedCorners: CACornerMask = [.layerMaxXMaxYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMinXMinYCorner], transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = (size, cornerRadius)
|
||||
|
||||
let contentFrame = CGRect(origin: CGPoint(), size: size)
|
||||
@ -400,11 +400,19 @@ open class BlurredBackgroundView: UIView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
self.backgroundView.layer.maskedCorners = maskedCorners
|
||||
}
|
||||
|
||||
transition.updateCornerRadius(layer: self.backgroundView.layer, cornerRadius: cornerRadius)
|
||||
if let effectView = self.effectView {
|
||||
transition.updateCornerRadius(layer: effectView.layer, cornerRadius: cornerRadius)
|
||||
effectView.clipsToBounds = !cornerRadius.isZero
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
effectView.layer.maskedCorners = maskedCorners
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -300,6 +300,7 @@ public protocol Peer: AnyObject, PostboxCoding {
|
||||
var associatedPeerId: PeerId? { get }
|
||||
var notificationSettingsPeerId: PeerId? { get }
|
||||
var associatedMediaIds: [MediaId]? { get }
|
||||
var timeoutAttribute: UInt32? { get }
|
||||
|
||||
func isEqual(_ other: Peer) -> Bool
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ final class PeerTable: Table {
|
||||
}
|
||||
|
||||
private let reverseAssociatedTable: ReverseAssociatedPeerTable
|
||||
//private let peerTimeoutPropertiesTable: PeerTimeoutPropertiesTable
|
||||
private let peerTimeoutPropertiesTable: PeerTimeoutPropertiesTable
|
||||
|
||||
private let sharedEncoder = PostboxEncoder()
|
||||
private let sharedKey = ValueBoxKey(length: 8)
|
||||
@ -14,8 +14,9 @@ final class PeerTable: Table {
|
||||
private var cachedPeers: [PeerId: Peer] = [:]
|
||||
private var updatedInitialPeers: [PeerId: Peer?] = [:]
|
||||
|
||||
init(valueBox: ValueBox, table: ValueBoxTable, useCaches: Bool, reverseAssociatedTable: ReverseAssociatedPeerTable) {
|
||||
init(valueBox: ValueBox, table: ValueBoxTable, useCaches: Bool, reverseAssociatedTable: ReverseAssociatedPeerTable, peerTimeoutPropertiesTable: PeerTimeoutPropertiesTable) {
|
||||
self.reverseAssociatedTable = reverseAssociatedTable
|
||||
self.peerTimeoutPropertiesTable = peerTimeoutPropertiesTable
|
||||
|
||||
super.init(valueBox: valueBox, table: table, useCaches: useCaches)
|
||||
}
|
||||
@ -64,6 +65,24 @@ final class PeerTable: Table {
|
||||
return result
|
||||
}
|
||||
|
||||
func commitDependentTables() {
|
||||
for (peerId, previousPeer) in self.updatedInitialPeers {
|
||||
if let peer = self.cachedPeers[peerId] {
|
||||
let previousTimeout = previousPeer?.timeoutAttribute
|
||||
if previousTimeout != peer.timeoutAttribute {
|
||||
if let previousTimeout = previousTimeout {
|
||||
self.peerTimeoutPropertiesTable.remove(peerId: peerId, timestamp: previousTimeout)
|
||||
}
|
||||
if let updatedTimeout = peer.timeoutAttribute {
|
||||
self.peerTimeoutPropertiesTable.add(peerId: peerId, timestamp: updatedTimeout)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func beforeCommit() {
|
||||
if !self.updatedInitialPeers.isEmpty {
|
||||
for (peerId, previousPeer) in self.updatedInitialPeers {
|
||||
|
45
submodules/Postbox/Sources/PeerTimeoutAttributesView.swift
Normal file
45
submodules/Postbox/Sources/PeerTimeoutAttributesView.swift
Normal 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
|
||||
}
|
||||
}
|
102
submodules/Postbox/Sources/PeerTimeoutPropertiesTable.swift
Normal file
102
submodules/Postbox/Sources/PeerTimeoutPropertiesTable.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1153,6 +1153,11 @@ public final class Transaction {
|
||||
assert(!self.disposed)
|
||||
self.postbox?.clearTimestampBasedAttribute(id: id, tag: tag)
|
||||
}
|
||||
|
||||
public func removePeerTimeoutAttributeEntry(peerId: PeerId, timestamp: UInt32) {
|
||||
assert(!self.disposed)
|
||||
self.postbox?.removePeerTimeoutAttributeEntry(peerId: peerId, timestamp: timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
public enum PostboxResult {
|
||||
@ -1464,6 +1469,7 @@ final class PostboxImpl {
|
||||
let deviceContactImportInfoTable: DeviceContactImportInfoTable
|
||||
let messageHistoryHoleIndexTable: MessageHistoryHoleIndexTable
|
||||
let groupMessageStatsTable: GroupMessageStatsTable
|
||||
let peerTimeoutPropertiesTable: PeerTimeoutPropertiesTable
|
||||
|
||||
//temporary
|
||||
let peerRatingTable: RatingTable<PeerId>
|
||||
@ -1487,7 +1493,8 @@ final class PostboxImpl {
|
||||
|
||||
self.keychainTable = KeychainTable(valueBox: self.valueBox, table: KeychainTable.tableSpec(1), useCaches: useCaches)
|
||||
self.reverseAssociatedPeerTable = ReverseAssociatedPeerTable(valueBox: self.valueBox, table:ReverseAssociatedPeerTable.tableSpec(40), useCaches: useCaches)
|
||||
self.peerTable = PeerTable(valueBox: self.valueBox, table: PeerTable.tableSpec(2), useCaches: useCaches, reverseAssociatedTable: self.reverseAssociatedPeerTable)
|
||||
self.peerTimeoutPropertiesTable = PeerTimeoutPropertiesTable(valueBox: self.valueBox, table: PeerTimeoutPropertiesTable.tableSpec(64), useCaches: useCaches)
|
||||
self.peerTable = PeerTable(valueBox: self.valueBox, table: PeerTable.tableSpec(2), useCaches: useCaches, reverseAssociatedTable: self.reverseAssociatedPeerTable, peerTimeoutPropertiesTable: self.peerTimeoutPropertiesTable)
|
||||
self.globalMessageIdsTable = GlobalMessageIdsTable(valueBox: self.valueBox, table: GlobalMessageIdsTable.tableSpec(3), useCaches: useCaches, seedConfiguration: seedConfiguration)
|
||||
self.globallyUniqueMessageIdsTable = MessageGloballyUniqueIdTable(valueBox: self.valueBox, table: MessageGloballyUniqueIdTable.tableSpec(32), useCaches: useCaches)
|
||||
self.messageHistoryMetadataTable = MessageHistoryMetadataTable(valueBox: self.valueBox, table: MessageHistoryMetadataTable.tableSpec(10), useCaches: useCaches)
|
||||
@ -1604,6 +1611,7 @@ final class PostboxImpl {
|
||||
tables.append(self.deviceContactImportInfoTable)
|
||||
tables.append(self.messageHistoryHoleIndexTable)
|
||||
tables.append(self.groupMessageStatsTable)
|
||||
tables.append(self.peerTimeoutPropertiesTable)
|
||||
|
||||
self.tables = tables
|
||||
|
||||
@ -1992,11 +2000,15 @@ final class PostboxImpl {
|
||||
let transactionParticipationInTotalUnreadCountUpdates = self.peerNotificationSettingsTable.transactionParticipationInTotalUnreadCountUpdates(postbox: self, transaction: currentTransaction)
|
||||
self.chatListIndexTable.commitWithTransaction(postbox: self, currentTransaction: currentTransaction, alteredInitialPeerCombinedReadStates: alteredInitialPeerCombinedReadStates, updatedPeers: updatedPeers, transactionParticipationInTotalUnreadCountUpdates: transactionParticipationInTotalUnreadCountUpdates, updatedTotalUnreadStates: &self.currentUpdatedTotalUnreadStates, updatedGroupTotalUnreadSummaries: &self.currentUpdatedGroupTotalUnreadSummaries, currentUpdatedGroupSummarySynchronizeOperations: &self.currentUpdatedGroupSummarySynchronizeOperations)
|
||||
|
||||
self.peerTable.commitDependentTables()
|
||||
|
||||
if self.currentNeedsReindexUnreadCounters {
|
||||
self.reindexUnreadCounters(currentTransaction: currentTransaction)
|
||||
}
|
||||
|
||||
let transaction = PostboxTransaction(currentUpdatedState: self.currentUpdatedState, currentPeerHoleOperations: self.currentPeerHoleOperations, currentOperationsByPeerId: self.currentOperationsByPeerId, chatListOperations: self.currentChatListOperations, currentUpdatedChatListInclusions: self.currentUpdatedChatListInclusions, currentUpdatedPeers: self.currentUpdatedPeers, currentUpdatedPeerNotificationSettings: self.currentUpdatedPeerNotificationSettings, currentUpdatedPeerNotificationBehaviorTimestamps: self.currentUpdatedPeerNotificationBehaviorTimestamps, currentUpdatedCachedPeerData: self.currentUpdatedCachedPeerData, currentUpdatedPeerPresences: currentUpdatedPeerPresences, currentUpdatedPeerChatListEmbeddedStates: self.currentUpdatedPeerChatListEmbeddedStates, currentUpdatedTotalUnreadStates: self.currentUpdatedTotalUnreadStates, currentUpdatedTotalUnreadSummaries: self.currentUpdatedGroupTotalUnreadSummaries, alteredInitialPeerCombinedReadStates: alteredInitialPeerCombinedReadStates, currentPeerMergedOperationLogOperations: self.currentPeerMergedOperationLogOperations, currentTimestampBasedMessageAttributesOperations: self.currentTimestampBasedMessageAttributesOperations, unsentMessageOperations: self.currentUnsentOperations, updatedSynchronizePeerReadStateOperations: self.currentUpdatedSynchronizeReadStateOperations, currentUpdatedGroupSummarySynchronizeOperations: self.currentUpdatedGroupSummarySynchronizeOperations, currentPreferencesOperations: self.currentPreferencesOperations, currentOrderedItemListOperations: self.currentOrderedItemListOperations, currentItemCollectionItemsOperations: self.currentItemCollectionItemsOperations, currentItemCollectionInfosOperations: self.currentItemCollectionInfosOperations, currentUpdatedPeerChatStates: self.currentUpdatedPeerChatStates, currentGlobalTagsOperations: self.currentGlobalTagsOperations, currentLocalTagsOperations: self.currentLocalTagsOperations, updatedMedia: self.currentUpdatedMedia, replaceRemoteContactCount: self.currentReplaceRemoteContactCount, replaceContactPeerIds: self.currentReplacedContactPeerIds, currentPendingMessageActionsOperations: self.currentPendingMessageActionsOperations, currentUpdatedMessageActionsSummaries: self.currentUpdatedMessageActionsSummaries, currentUpdatedMessageTagSummaries: self.currentUpdatedMessageTagSummaries, currentInvalidateMessageTagSummaries: self.currentInvalidateMessageTagSummaries, currentUpdatedPendingPeerNotificationSettings: self.currentUpdatedPendingPeerNotificationSettings, replacedAdditionalChatListItems: self.currentReplacedAdditionalChatListItems, updatedNoticeEntryKeys: self.currentUpdatedNoticeEntryKeys, updatedCacheEntryKeys: self.currentUpdatedCacheEntryKeys, currentUpdatedMasterClientId: currentUpdatedMasterClientId, updatedFailedMessagePeerIds: self.messageHistoryFailedTable.updatedPeerIds, updatedFailedMessageIds: self.messageHistoryFailedTable.updatedMessageIds, updatedGlobalNotificationSettings: self.currentNeedsReindexUnreadCounters)
|
||||
let updatedPeerTimeoutAttributes = self.peerTimeoutPropertiesTable.hasUpdates
|
||||
|
||||
let transaction = PostboxTransaction(currentUpdatedState: self.currentUpdatedState, currentPeerHoleOperations: self.currentPeerHoleOperations, currentOperationsByPeerId: self.currentOperationsByPeerId, chatListOperations: self.currentChatListOperations, currentUpdatedChatListInclusions: self.currentUpdatedChatListInclusions, currentUpdatedPeers: self.currentUpdatedPeers, currentUpdatedPeerNotificationSettings: self.currentUpdatedPeerNotificationSettings, currentUpdatedPeerNotificationBehaviorTimestamps: self.currentUpdatedPeerNotificationBehaviorTimestamps, currentUpdatedCachedPeerData: self.currentUpdatedCachedPeerData, currentUpdatedPeerPresences: currentUpdatedPeerPresences, currentUpdatedPeerChatListEmbeddedStates: self.currentUpdatedPeerChatListEmbeddedStates, currentUpdatedTotalUnreadStates: self.currentUpdatedTotalUnreadStates, currentUpdatedTotalUnreadSummaries: self.currentUpdatedGroupTotalUnreadSummaries, alteredInitialPeerCombinedReadStates: alteredInitialPeerCombinedReadStates, currentPeerMergedOperationLogOperations: self.currentPeerMergedOperationLogOperations, currentTimestampBasedMessageAttributesOperations: self.currentTimestampBasedMessageAttributesOperations, unsentMessageOperations: self.currentUnsentOperations, updatedSynchronizePeerReadStateOperations: self.currentUpdatedSynchronizeReadStateOperations, currentUpdatedGroupSummarySynchronizeOperations: self.currentUpdatedGroupSummarySynchronizeOperations, currentPreferencesOperations: self.currentPreferencesOperations, currentOrderedItemListOperations: self.currentOrderedItemListOperations, currentItemCollectionItemsOperations: self.currentItemCollectionItemsOperations, currentItemCollectionInfosOperations: self.currentItemCollectionInfosOperations, currentUpdatedPeerChatStates: self.currentUpdatedPeerChatStates, currentGlobalTagsOperations: self.currentGlobalTagsOperations, currentLocalTagsOperations: self.currentLocalTagsOperations, updatedMedia: self.currentUpdatedMedia, replaceRemoteContactCount: self.currentReplaceRemoteContactCount, replaceContactPeerIds: self.currentReplacedContactPeerIds, currentPendingMessageActionsOperations: self.currentPendingMessageActionsOperations, currentUpdatedMessageActionsSummaries: self.currentUpdatedMessageActionsSummaries, currentUpdatedMessageTagSummaries: self.currentUpdatedMessageTagSummaries, currentInvalidateMessageTagSummaries: self.currentInvalidateMessageTagSummaries, currentUpdatedPendingPeerNotificationSettings: self.currentUpdatedPendingPeerNotificationSettings, replacedAdditionalChatListItems: self.currentReplacedAdditionalChatListItems, updatedNoticeEntryKeys: self.currentUpdatedNoticeEntryKeys, updatedCacheEntryKeys: self.currentUpdatedCacheEntryKeys, currentUpdatedMasterClientId: currentUpdatedMasterClientId, updatedFailedMessagePeerIds: self.messageHistoryFailedTable.updatedPeerIds, updatedFailedMessageIds: self.messageHistoryFailedTable.updatedMessageIds, updatedGlobalNotificationSettings: self.currentNeedsReindexUnreadCounters, updatedPeerTimeoutAttributes: updatedPeerTimeoutAttributes)
|
||||
var updatedTransactionState: Int64?
|
||||
var updatedMasterClientId: Int64?
|
||||
if !transaction.isEmpty {
|
||||
@ -3645,6 +3657,10 @@ final class PostboxImpl {
|
||||
self.timestampBasedMessageAttributesTable.remove(tag: tag, id: id, operations: &self.currentTimestampBasedMessageAttributesOperations)
|
||||
}
|
||||
|
||||
fileprivate func removePeerTimeoutAttributeEntry(peerId: PeerId, timestamp: UInt32) {
|
||||
self.peerTimeoutPropertiesTable.remove(peerId: peerId, timestamp: timestamp)
|
||||
}
|
||||
|
||||
fileprivate func reindexUnreadCounters(currentTransaction: Transaction) {
|
||||
self.groupMessageStatsTable.removeAll()
|
||||
let _ = CFAbsoluteTimeGetCurrent()
|
||||
|
@ -43,6 +43,7 @@ final class PostboxTransaction {
|
||||
let updatedFailedMessagePeerIds: Set<PeerId>
|
||||
let updatedFailedMessageIds: Set<MessageId>
|
||||
let updatedGlobalNotificationSettings: Bool
|
||||
let updatedPeerTimeoutAttributes: Bool
|
||||
|
||||
var isEmpty: Bool {
|
||||
if currentUpdatedState != nil {
|
||||
@ -171,10 +172,13 @@ final class PostboxTransaction {
|
||||
if self.updatedGlobalNotificationSettings {
|
||||
return false
|
||||
}
|
||||
if self.updatedPeerTimeoutAttributes {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
init(currentUpdatedState: PostboxCoding?, currentPeerHoleOperations: [MessageHistoryIndexHoleOperationKey: [MessageHistoryIndexHoleOperation]] = [:], currentOperationsByPeerId: [PeerId: [MessageHistoryOperation]], chatListOperations: [PeerGroupId: [ChatListOperation]], currentUpdatedChatListInclusions: [PeerId: PeerChatListInclusion], currentUpdatedPeers: [PeerId: Peer], currentUpdatedPeerNotificationSettings: [PeerId: (PeerNotificationSettings?, PeerNotificationSettings)], currentUpdatedPeerNotificationBehaviorTimestamps: [PeerId: PeerNotificationSettingsBehaviorTimestamp], currentUpdatedCachedPeerData: [PeerId: CachedPeerData], currentUpdatedPeerPresences: [PeerId: PeerPresence], currentUpdatedPeerChatListEmbeddedStates: Set<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.currentPeerHoleOperations = currentPeerHoleOperations
|
||||
self.currentOperationsByPeerId = currentOperationsByPeerId
|
||||
@ -216,5 +220,6 @@ final class PostboxTransaction {
|
||||
self.updatedFailedMessagePeerIds = updatedFailedMessagePeerIds
|
||||
self.updatedFailedMessageIds = updatedFailedMessageIds
|
||||
self.updatedGlobalNotificationSettings = updatedGlobalNotificationSettings
|
||||
self.updatedPeerTimeoutAttributes = updatedPeerTimeoutAttributes
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ public enum PostboxViewKey: Hashable {
|
||||
case messageGroup(id: MessageId)
|
||||
case isContact(id: PeerId)
|
||||
case chatListIndex(id: PeerId)
|
||||
case peerTimeoutAttributes
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
@ -121,6 +122,8 @@ public enum PostboxViewKey: Hashable {
|
||||
hasher.combine(id)
|
||||
case let .chatListIndex(id):
|
||||
hasher.combine(id)
|
||||
case .peerTimeoutAttributes:
|
||||
hasher.combine(17)
|
||||
}
|
||||
}
|
||||
|
||||
@ -342,6 +345,12 @@ public enum PostboxViewKey: Hashable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .peerTimeoutAttributes:
|
||||
if case .peerTimeoutAttributes = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -420,5 +429,7 @@ func postboxViewForKey(postbox: PostboxImpl, key: PostboxViewKey) -> MutablePost
|
||||
return MutableIsContactView(postbox: postbox, id: id)
|
||||
case let .chatListIndex(id):
|
||||
return MutableChatListIndexView(postbox: postbox, id: id)
|
||||
case .peerTimeoutAttributes:
|
||||
return MutablePeerTimeoutAttributesView(postbox: postbox)
|
||||
}
|
||||
}
|
||||
|
@ -126,6 +126,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
private let animationCache: AnimationCache
|
||||
private let animationRenderer: MultiAnimationRenderer
|
||||
private let items: [ReactionContextItem]
|
||||
private let selectedItems: Set<MessageReaction.Reaction>
|
||||
private let getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?
|
||||
private let isExpandedUpdated: (ContainedViewLayoutTransition) -> Void
|
||||
private let requestLayout: (ContainedViewLayoutTransition) -> Void
|
||||
@ -239,10 +240,11 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
public init(context: AccountContext, animationCache: AnimationCache, presentationData: PresentationData, items: [ReactionContextItem], getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<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.presentationData = presentationData
|
||||
self.items = items
|
||||
self.selectedItems = selectedItems
|
||||
self.getEmojiContent = getEmojiContent
|
||||
self.isExpandedUpdated = isExpandedUpdated
|
||||
self.requestLayout = requestLayout
|
||||
@ -639,11 +641,16 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
validIndices.insert(i)
|
||||
|
||||
var itemFrame = baseItemFrame
|
||||
var selectionItemFrame = itemFrame
|
||||
let normalItemScale: CGFloat = 1.0
|
||||
|
||||
var isPreviewing = false
|
||||
if let highlightedReaction = self.highlightedReaction, highlightedReaction == self.items[i].reaction {
|
||||
isPreviewing = true
|
||||
} else if self.highlightedReaction != nil {
|
||||
}
|
||||
|
||||
if let reaction = self.items[i].reaction, self.selectedItems.contains(reaction.rawValue), !isPreviewing {
|
||||
itemFrame = itemFrame.insetBy(dx: (itemFrame.width - 0.8 * itemFrame.width) * 0.5, dy: (itemFrame.height - 0.8 * itemFrame.height) * 0.5)
|
||||
}
|
||||
|
||||
var animateIn = false
|
||||
@ -668,6 +675,12 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
self.visibleItemNodes[i] = itemNode
|
||||
|
||||
self.scrollNode.addSubnode(itemNode)
|
||||
if let itemNode = itemNode as? ReactionNode {
|
||||
if let reaction = self.items[i].reaction, self.selectedItems.contains(reaction.rawValue) {
|
||||
self.contentTintContainer.view.addSubview(itemNode.selectionTintView)
|
||||
self.scrollNode.view.addSubview(itemNode.selectionView)
|
||||
}
|
||||
}
|
||||
|
||||
if let maskNode = maskNode {
|
||||
self.visibleItemMaskNodes[i] = maskNode
|
||||
@ -690,7 +703,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
|
||||
if self.getEmojiContent != nil && i == visibleItemCount - 1 {
|
||||
itemFrame.origin.x -= (1.0 - compressionFactor) * itemFrame.width * 0.5
|
||||
itemFrame.origin.x -= (1.0 - compressionFactor) * selectionItemFrame.width * 0.5
|
||||
selectionItemFrame.origin.x -= (1.0 - compressionFactor) * selectionItemFrame.width * 0.5
|
||||
itemNode.isUserInteractionEnabled = false
|
||||
} else {
|
||||
itemNode.isUserInteractionEnabled = true
|
||||
@ -711,15 +725,29 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
})
|
||||
itemNode.updateLayout(size: itemFrame.size, isExpanded: false, largeExpanded: false, isPreviewing: isPreviewing, transition: itemTransition)
|
||||
|
||||
if let itemNode = itemNode as? ReactionNode {
|
||||
if let reaction = self.items[i].reaction, self.selectedItems.contains(reaction.rawValue) {
|
||||
itemNode.selectionTintView.isHidden = false
|
||||
itemNode.selectionView.isHidden = false
|
||||
}
|
||||
itemTransition.updateFrame(view: itemNode.selectionTintView, frame: selectionItemFrame.offsetBy(dx: 0.0, dy: self.extensionDistance * 0.5))
|
||||
itemTransition.updateCornerRadius(layer: itemNode.selectionTintView.layer, cornerRadius: min(selectionItemFrame.width, selectionItemFrame.height) / 2.0)
|
||||
|
||||
itemTransition.updateFrame(view: itemNode.selectionView, frame: selectionItemFrame.offsetBy(dx: 0.0, dy: self.extensionDistance * 0.5))
|
||||
itemTransition.updateCornerRadius(layer: itemNode.selectionView.layer, cornerRadius: min(selectionItemFrame.width, selectionItemFrame.height) / 2.0)
|
||||
}
|
||||
|
||||
if animateIn {
|
||||
itemNode.appear(animated: !self.context.sharedContext.currentPresentationData.with({ $0 }).reduceMotion)
|
||||
}
|
||||
|
||||
if self.getEmojiContent != nil && i == visibleItemCount - 1 {
|
||||
transition.updateSublayerTransformScale(node: itemNode, scale: 0.001 * (1.0 - compressionFactor) + 1.0 * compressionFactor)
|
||||
transition.updateSublayerTransformScale(node: itemNode, scale: 0.001 * (1.0 - compressionFactor) + normalItemScale * compressionFactor)
|
||||
|
||||
let alphaFraction = min(compressionFactor, 0.2) / 0.2
|
||||
transition.updateAlpha(node: itemNode, alpha: alphaFraction)
|
||||
} else {
|
||||
transition.updateSublayerTransformScale(node: itemNode, scale: normalItemScale)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -888,9 +916,14 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
self.contentContainer.view.mask = nil
|
||||
for (_, itemNode) in self.visibleItemNodes {
|
||||
itemNode.isHidden = true
|
||||
|
||||
if let itemNode = itemNode as? ReactionNode {
|
||||
itemNode.selectionView.isHidden = true
|
||||
itemNode.selectionTintView.isHidden = true
|
||||
}
|
||||
}
|
||||
if let emojiView = reactionSelectionComponentHost.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: AnyHashable("emoji"))) as? EmojiPagerContentComponent.View {
|
||||
var initialPositionAndFrame: [MediaId: (position: CGPoint, frameIndex: Int, placeholder: UIImage)] = [:]
|
||||
var initialPositionAndFrame: [MediaId: (frame: CGRect, frameIndex: Int, placeholder: UIImage)] = [:]
|
||||
for (_, itemNode) in self.visibleItemNodes {
|
||||
guard let itemNode = itemNode as? ReactionNode else {
|
||||
continue
|
||||
@ -902,7 +935,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
continue
|
||||
}
|
||||
initialPositionAndFrame[itemNode.item.stillAnimation.fileId] = (
|
||||
position: itemNode.frame.center,
|
||||
frame: itemNode.frame,
|
||||
frameIndex: itemNode.currentFrameIndex,
|
||||
placeholder: placeholder
|
||||
)
|
||||
@ -911,7 +944,10 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
emojiView.animateInReactionSelection(sourceItems: initialPositionAndFrame)
|
||||
|
||||
if let mirrorContentClippingView = emojiView.mirrorContentClippingView {
|
||||
Transition(transition).animateBoundsOrigin(view: mirrorContentClippingView, from: CGPoint(x: 0.0, y: 46.0), to: CGPoint(), additive: true)
|
||||
mirrorContentClippingView.clipsToBounds = false
|
||||
Transition(transition).animateBoundsOrigin(view: mirrorContentClippingView, from: CGPoint(x: 0.0, y: 46.0), to: CGPoint(), additive: true, completion: { [weak mirrorContentClippingView] _ in
|
||||
mirrorContentClippingView?.clipsToBounds = true
|
||||
})
|
||||
}
|
||||
}
|
||||
if let topPanelView = reactionSelectionComponentHost.findTaggedView(tag: EntityKeyboardTopPanelComponent.Tag(id: AnyHashable("emoji"))) as? EntityKeyboardTopPanelComponent.View {
|
||||
@ -1164,7 +1200,10 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + itemDelay * UIView.animationDurationFactor(), execute: { [weak itemNode] in
|
||||
itemNode?.appear(animated: true)
|
||||
guard let itemNode = itemNode else {
|
||||
return
|
||||
}
|
||||
itemNode.appear(animated: true)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -53,6 +53,9 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
|
||||
private let hasAppearAnimation: Bool
|
||||
private let useDirectRendering: Bool
|
||||
|
||||
let selectionTintView: UIView
|
||||
let selectionView: UIView
|
||||
|
||||
private var animateInAnimationNode: AnimatedStickerNode?
|
||||
private let staticAnimationNode: AnimatedStickerNode
|
||||
private var stillAnimationNode: AnimatedStickerNode?
|
||||
@ -87,6 +90,14 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
|
||||
self.hasAppearAnimation = hasAppearAnimation
|
||||
self.useDirectRendering = useDirectRendering
|
||||
|
||||
self.selectionTintView = UIView()
|
||||
self.selectionTintView.backgroundColor = UIColor(white: 1.0, alpha: 0.2)
|
||||
self.selectionTintView.isHidden = true
|
||||
|
||||
self.selectionView = UIView()
|
||||
self.selectionView.backgroundColor = theme.chat.inputMediaPanel.panelContentControlVibrantSelectionColor
|
||||
self.selectionView.isHidden = true
|
||||
|
||||
self.staticAnimationNode = self.useDirectRendering ? DirectAnimatedStickerNode() : DefaultAnimatedStickerNodeImpl()
|
||||
|
||||
if hasAppearAnimation {
|
||||
@ -147,8 +158,18 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
|
||||
} else {
|
||||
self.animateInAnimationNode?.visibility = true
|
||||
}
|
||||
|
||||
self.selectionView.alpha = 1.0
|
||||
self.selectionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.selectionView.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4)
|
||||
|
||||
self.selectionTintView.alpha = 1.0
|
||||
self.selectionTintView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.selectionTintView.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4)
|
||||
} else {
|
||||
self.animateInAnimationNode?.completed(true)
|
||||
self.selectionView.alpha = 1.0
|
||||
self.selectionTintView.alpha = 1.0
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1074,6 +1074,7 @@ public class Account {
|
||||
self.managedOperationsDisposable.add(managedCloudChatRemoveMessagesOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start())
|
||||
self.managedOperationsDisposable.add(managedAutoremoveMessageOperations(network: self.network, postbox: self.postbox, isRemove: true).start())
|
||||
self.managedOperationsDisposable.add(managedAutoremoveMessageOperations(network: self.network, postbox: self.postbox, isRemove: false).start())
|
||||
self.managedOperationsDisposable.add(managedPeerTimestampAttributeOperations(network: self.network, postbox: self.postbox).start())
|
||||
self.managedOperationsDisposable.add(managedGlobalNotificationSettings(postbox: self.postbox, network: self.network).start())
|
||||
self.managedOperationsDisposable.add(managedSynchronizePinnedChatsOperations(postbox: self.postbox, network: self.network, accountPeerId: self.peerId, stateManager: self.stateManager).start())
|
||||
|
||||
|
@ -4,7 +4,6 @@ import SwiftSignalKit
|
||||
import TelegramApi
|
||||
import MtProtoKit
|
||||
|
||||
|
||||
private typealias SignalKitTimer = SwiftSignalKit.Timer
|
||||
|
||||
private final class ManagedAutoremoveMessageOperationsHelper {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -174,6 +174,8 @@ public final class TelegramChannel: Peer, Equatable {
|
||||
public let associatedPeerId: PeerId? = nil
|
||||
public let notificationSettingsPeerId: PeerId? = nil
|
||||
|
||||
public var timeoutAttribute: UInt32? { return nil }
|
||||
|
||||
public init(id: PeerId, accessHash: TelegramPeerAccessHash?, title: String, username: String?, photo: [TelegramMediaImageRepresentation], creationDate: Int32, version: Int32, participationStatus: TelegramChannelParticipationStatus, info: TelegramChannelInfo, flags: TelegramChannelFlags, restrictionInfo: PeerAccessRestrictionInfo?, adminRights: TelegramChatAdminRights?, bannedRights: TelegramChatBannedRights?, defaultBannedRights: TelegramChatBannedRights?) {
|
||||
self.id = id
|
||||
self.accessHash = accessHash
|
||||
|
@ -97,6 +97,8 @@ public final class TelegramGroup: Peer, Equatable {
|
||||
public let associatedPeerId: PeerId? = nil
|
||||
public let notificationSettingsPeerId: PeerId? = nil
|
||||
|
||||
public var timeoutAttribute: UInt32? { return nil }
|
||||
|
||||
public init(id: PeerId, title: String, photo: [TelegramMediaImageRepresentation], participantCount: Int, role: TelegramGroupRole, membership: TelegramGroupMembership, flags: TelegramGroupFlags, defaultBannedRights: TelegramChatBannedRights?, migrationReference: TelegramGroupToChannelMigrationReference?, creationDate: Int32, version: Int) {
|
||||
self.id = id
|
||||
self.title = title
|
||||
|
@ -19,6 +19,8 @@ public final class TelegramSecretChat: Peer, Equatable {
|
||||
public let associatedPeerId: PeerId?
|
||||
public let notificationSettingsPeerId: PeerId?
|
||||
|
||||
public var timeoutAttribute: UInt32? { return nil }
|
||||
|
||||
public init(id: PeerId, creationDate: Int32, regularPeerId: PeerId, accessHash: Int64, role: SecretChatRole, embeddedState: SecretChatEmbeddedPeerState, messageAutoremoveTimeout: Int32?) {
|
||||
self.id = id
|
||||
self.regularPeerId = regularPeerId
|
||||
|
@ -115,6 +115,18 @@ public final class TelegramUser: Peer, Equatable {
|
||||
public let associatedPeerId: PeerId? = nil
|
||||
public let notificationSettingsPeerId: PeerId? = nil
|
||||
|
||||
public var timeoutAttribute: UInt32? {
|
||||
if let emojiStatus = self.emojiStatus {
|
||||
if let expirationDate = emojiStatus.expirationDate {
|
||||
return UInt32(max(0, expirationDate))
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public init(id: PeerId, accessHash: TelegramPeerAccessHash?, firstName: String?, lastName: String?, username: String?, phone: String?, photo: [TelegramMediaImageRepresentation], botInfo: BotUserInfo?, restrictionInfo: PeerAccessRestrictionInfo?, flags: UserInfoFlags, emojiStatus: PeerEmojiStatus?) {
|
||||
self.id = id
|
||||
self.accessHash = accessHash
|
||||
|
@ -76,7 +76,7 @@ public extension TelegramEngine {
|
||||
|
||||
if let entry = CodableEntry(RecentMediaItem(file)) {
|
||||
let itemEntry = OrderedItemListEntry(id: RecentMediaItemId(file.fileId).rawValue, contents: entry)
|
||||
transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudRecentStatusEmoji, item: itemEntry, removeTailIfCountExceeds: 50)
|
||||
transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudRecentStatusEmoji, item: itemEntry, removeTailIfCountExceeds: 32)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -458,6 +458,30 @@ public func stringAndActivityForUserPresence(strings: PresentationStrings, dateT
|
||||
}
|
||||
}
|
||||
|
||||
public func peerStatusExpirationString(statusTimestamp: Int32, relativeTo timestamp: Int32, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat) -> String {
|
||||
let difference = max(statusTimestamp - timestamp, 60)
|
||||
if difference < 60 * 60 {
|
||||
return strings.PeerStatusExpiration_Minutes(Int32(round(Double(difference) / Double(60))))
|
||||
} else if difference < 24 * 60 * 60 {
|
||||
return strings.PeerStatusExpiration_Hours(Int32(round(Double(difference) / Double(60 * 60))))
|
||||
} else {
|
||||
var t: time_t = time_t(statusTimestamp)
|
||||
var timeinfo: tm = tm()
|
||||
localtime_r(&t, &timeinfo)
|
||||
|
||||
var now: time_t = time_t(timestamp)
|
||||
var timeinfoNow: tm = tm()
|
||||
localtime_r(&now, &timeinfoNow)
|
||||
|
||||
let dayDifference = timeinfo.tm_yday - timeinfoNow.tm_yday
|
||||
if dayDifference == 1 {
|
||||
return strings.PeerStatusExpiration_TomorrowAt(stringForShortTimestamp(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, dateTimeFormat: dateTimeFormat)).string
|
||||
} else {
|
||||
return strings.PeerStatusExpiration_AtDate(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, dateTimeFormat: dateTimeFormat)).string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func userPresenceStringRefreshTimeout(_ presence: TelegramUserPresence, relativeTo timestamp: Int32) -> Double {
|
||||
switch presence.status {
|
||||
case let .present(statusTimestamp):
|
||||
|
@ -24,12 +24,15 @@ swift_library(
|
||||
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
|
||||
"//submodules/Components/PagerComponent:PagerComponent",
|
||||
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
|
||||
"//submodules/Components/SolidRoundedButtonComponent:SolidRoundedButtonComponent",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/lottie-ios:Lottie",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
"//submodules/GZip:GZip",
|
||||
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
|
||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -10,18 +10,26 @@ import AccountContext
|
||||
import ComponentDisplayAdapters
|
||||
import MultilineTextComponent
|
||||
import EmojiStatusComponent
|
||||
import TelegramStringFormatting
|
||||
import SolidRoundedButtonComponent
|
||||
import PresentationDataUtils
|
||||
|
||||
protocol ContextMenuItemWithAction: AnyObject {
|
||||
func performAction()
|
||||
func performAction() -> ContextMenuPerformActionResult
|
||||
}
|
||||
|
||||
enum ContextMenuPerformActionResult {
|
||||
case none
|
||||
case clearHighlight
|
||||
}
|
||||
|
||||
private final class ContextMenuActionItem: Component, ContextMenuItemWithAction {
|
||||
typealias EnvironmentType = ContextMenuActionItemEnvironment
|
||||
|
||||
let title: String
|
||||
let action: () -> Void
|
||||
let action: () -> ContextMenuPerformActionResult
|
||||
|
||||
init(title: String, action: @escaping () -> Void) {
|
||||
init(title: String, action: @escaping () -> ContextMenuPerformActionResult) {
|
||||
self.title = title
|
||||
self.action = action
|
||||
}
|
||||
@ -33,8 +41,8 @@ private final class ContextMenuActionItem: Component, ContextMenuItemWithAction
|
||||
return true
|
||||
}
|
||||
|
||||
func performAction() {
|
||||
self.action()
|
||||
func performAction() -> ContextMenuPerformActionResult {
|
||||
return self.action()
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
@ -169,7 +177,12 @@ private final class ContextMenuActionsComponent: Component {
|
||||
for item in component.items {
|
||||
if item.id == id {
|
||||
if let itemComponent = item.component.wrapped as? ContextMenuItemWithAction {
|
||||
itemComponent.performAction()
|
||||
switch itemComponent.performAction() {
|
||||
case .none:
|
||||
break
|
||||
case .clearHighlight:
|
||||
self.setHighlightedItem(id: nil)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
@ -332,9 +345,193 @@ private final class ContextMenuActionsComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
private final class TimeSelectionControlComponent: Component {
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let bottomInset: CGFloat
|
||||
let apply: (Int32) -> Void
|
||||
let cancel: () -> Void
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
bottomInset: CGFloat,
|
||||
apply: @escaping (Int32) -> Void,
|
||||
cancel: @escaping () -> Void
|
||||
) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.bottomInset = bottomInset
|
||||
self.apply = apply
|
||||
self.cancel = cancel
|
||||
}
|
||||
|
||||
static func ==(lhs: TimeSelectionControlComponent, rhs: TimeSelectionControlComponent) -> Bool {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.bottomInset != rhs.bottomInset {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let backgroundView: BlurredBackgroundView
|
||||
private let pickerView: UIDatePicker
|
||||
private let titleView: ComponentView<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 {
|
||||
struct StatusResult {
|
||||
let timeout: Int
|
||||
let timestamp: Int32
|
||||
let sourceView: UIView
|
||||
}
|
||||
|
||||
@ -350,21 +547,29 @@ final class EmojiStatusPreviewScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
private enum CurrentState {
|
||||
case menu
|
||||
case timeSelection
|
||||
}
|
||||
|
||||
typealias EnvironmentType = Empty
|
||||
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let bottomInset: CGFloat
|
||||
let item: EmojiStatusComponent
|
||||
let dismiss: (StatusResult?) -> Void
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
bottomInset: CGFloat,
|
||||
item: EmojiStatusComponent,
|
||||
dismiss: @escaping (StatusResult?) -> Void
|
||||
) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.bottomInset = bottomInset
|
||||
self.item = item
|
||||
self.dismiss = dismiss
|
||||
}
|
||||
@ -376,6 +581,9 @@ final class EmojiStatusPreviewScreenComponent: Component {
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.bottomInset != rhs.bottomInset {
|
||||
return false
|
||||
}
|
||||
if lhs.item != rhs.item {
|
||||
return false
|
||||
}
|
||||
@ -386,13 +594,18 @@ final class EmojiStatusPreviewScreenComponent: Component {
|
||||
private let backgroundView: BlurredBackgroundView
|
||||
private let itemView: ComponentView<Empty>
|
||||
private let actionsView: ComponentView<Empty>
|
||||
private let timeSelectionView: ComponentView<Empty>
|
||||
|
||||
private var currentState: CurrentState = .menu
|
||||
|
||||
private var component: EmojiStatusPreviewScreenComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
|
||||
self.itemView = ComponentView<Empty>()
|
||||
self.actionsView = ComponentView<Empty>()
|
||||
self.timeSelectionView = ComponentView<Empty>()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
@ -406,12 +619,29 @@ final class EmojiStatusPreviewScreenComponent: Component {
|
||||
|
||||
@objc private func backgroundTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.component?.dismiss(nil)
|
||||
switch self.currentState {
|
||||
case .menu:
|
||||
self.component?.dismiss(nil)
|
||||
case .timeSelection:
|
||||
self.toggleState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func toggleState() {
|
||||
switch self.currentState {
|
||||
case .menu:
|
||||
self.currentState = .timeSelection
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.5, curve: .spring)))
|
||||
case .timeSelection:
|
||||
self.currentState = .menu
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: EmojiStatusPreviewScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let itemSpacing: CGFloat = 12.0
|
||||
|
||||
@ -434,15 +664,25 @@ final class EmojiStatusPreviewScreenComponent: Component {
|
||||
title: setTimeoutForIntervalString(strings: component.strings, value: Int32(duration)),
|
||||
action: { [weak self] in
|
||||
guard let strongSelf = self, let component = strongSelf.component else {
|
||||
return
|
||||
return .none
|
||||
}
|
||||
guard let itemComponentView = strongSelf.itemView.view else {
|
||||
return
|
||||
return .none
|
||||
}
|
||||
component.dismiss(StatusResult(timeout: duration, sourceView: itemComponentView))
|
||||
component.dismiss(StatusResult(timestamp: Int32(Date().timeIntervalSince1970) + Int32(duration), sourceView: itemComponentView))
|
||||
return .none
|
||||
}
|
||||
))))
|
||||
}
|
||||
//TODO:localize
|
||||
menuItems.append(AnyComponentWithIdentity(id: "Other", component: AnyComponent(ContextMenuActionItem(
|
||||
title: "Other",
|
||||
action: { [weak self] in
|
||||
self?.toggleState()
|
||||
return .clearHighlight
|
||||
}
|
||||
))))
|
||||
|
||||
let actionsSize = self.actionsView.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ContextMenuActionsComponent(
|
||||
@ -453,13 +693,41 @@ final class EmojiStatusPreviewScreenComponent: Component {
|
||||
containerSize: availableSize
|
||||
)
|
||||
|
||||
let totalContentHeight = itemSize.height + itemSpacing + actionsSize.height
|
||||
let timeSelectionSize = self.timeSelectionView.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(TimeSelectionControlComponent(
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
bottomInset: component.bottomInset,
|
||||
apply: { [weak self] timestamp in
|
||||
guard let strongSelf = self, let component = strongSelf.component else {
|
||||
return
|
||||
}
|
||||
guard let itemComponentView = strongSelf.itemView.view else {
|
||||
return
|
||||
}
|
||||
component.dismiss(StatusResult(timestamp: timestamp, sourceView: itemComponentView))
|
||||
},
|
||||
cancel: { [weak self] in
|
||||
self?.toggleState()
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
|
||||
let totalContentHeight = itemSize.height + itemSpacing + max(actionsSize.height, timeSelectionSize.height)
|
||||
|
||||
let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: floor((availableSize.height - totalContentHeight) / 2.0)), size: CGSize(width: availableSize.width, height: totalContentHeight))
|
||||
|
||||
let itemFrame = CGRect(origin: CGPoint(x: contentFrame.minX + floor((contentFrame.width - itemSize.width) / 2.0), y: contentFrame.minY), size: itemSize)
|
||||
let actionsFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - actionsSize.width) / 2.0), y: itemFrame.maxY + itemSpacing), size: actionsSize)
|
||||
|
||||
var timeSelectionFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - timeSelectionSize.width) / 2.0), y: availableSize.height - timeSelectionSize.height), size: timeSelectionSize)
|
||||
if case .menu = self.currentState {
|
||||
timeSelectionFrame.origin.y = availableSize.height
|
||||
}
|
||||
|
||||
if let itemComponentView = self.itemView.view {
|
||||
if itemComponentView.superview == nil {
|
||||
self.addSubview(itemComponentView)
|
||||
@ -471,7 +739,25 @@ final class EmojiStatusPreviewScreenComponent: Component {
|
||||
if actionsComponentView.superview == nil {
|
||||
self.addSubview(actionsComponentView)
|
||||
}
|
||||
transition.setFrame(view: actionsComponentView, frame: actionsFrame)
|
||||
transition.setPosition(view: actionsComponentView, position: actionsFrame.center)
|
||||
transition.setBounds(view: actionsComponentView, bounds: CGRect(origin: CGPoint(), size: actionsFrame.size))
|
||||
|
||||
if case .menu = self.currentState {
|
||||
transition.setTransform(view: actionsComponentView, transform: CATransform3DIdentity)
|
||||
transition.setAlpha(view: actionsComponentView, alpha: 1.0)
|
||||
actionsComponentView.isUserInteractionEnabled = true
|
||||
} else {
|
||||
transition.setTransform(view: actionsComponentView, transform: CATransform3DMakeScale(0.001, 0.001, 1.0))
|
||||
transition.setAlpha(view: actionsComponentView, alpha: 0.0)
|
||||
actionsComponentView.isUserInteractionEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
if let timeSelectionComponentView = self.timeSelectionView.view {
|
||||
if timeSelectionComponentView.superview == nil {
|
||||
self.addSubview(timeSelectionComponentView)
|
||||
}
|
||||
transition.setFrame(view: timeSelectionComponentView, frame: timeSelectionFrame)
|
||||
}
|
||||
|
||||
self.backgroundView.updateColor(color: component.theme.contextMenu.dimColor, transition: .immediate)
|
||||
@ -507,7 +793,6 @@ final class EmojiStatusPreviewScreenComponent: Component {
|
||||
actionsComponentView.layer.animateSpring(from: (-actionsComponentView.bounds.height / 2.0) as NSNumber, to: 0.0 as NSNumber, keyPath: "transform.translation.y", duration: 0.6)
|
||||
|
||||
let _ = additionalPositionDifference
|
||||
//actionsComponentView.layer.animatePosition(from: CGPoint(x: -additionalPositionDifference.x, y: -additionalPositionDifference.y), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -552,12 +837,20 @@ final class EmojiStatusPreviewScreenComponent: Component {
|
||||
actionsComponentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||
})
|
||||
}
|
||||
|
||||
if let timeSelectionComponentView = self.timeSelectionView.view {
|
||||
timeSelectionComponentView.layer.animatePosition(from: timeSelectionComponentView.layer.position, to: CGPoint(x: timeSelectionComponentView.layer.position.x, y: self.bounds.height + timeSelectionComponentView.bounds.height / 2.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
}
|
||||
} else {
|
||||
self.backgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
if let actionsComponentView = self.actionsView.view {
|
||||
actionsComponentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
|
||||
if let timeSelectionComponentView = self.timeSelectionView.view {
|
||||
timeSelectionComponentView.layer.animatePosition(from: timeSelectionComponentView.layer.position, to: CGPoint(x: timeSelectionComponentView.layer.position.x, y: self.bounds.height + timeSelectionComponentView.bounds.height / 2.0), duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
}
|
||||
} else {
|
||||
completion()
|
||||
}
|
||||
|
@ -744,6 +744,7 @@ public final class EmojiStatusSelectionController: ViewController {
|
||||
component: AnyComponent(EmojiStatusPreviewScreenComponent(
|
||||
theme: self.presentationData.theme,
|
||||
strings: self.presentationData.strings,
|
||||
bottomInset: layout.insets(options: []).bottom,
|
||||
item: EmojiStatusComponent(
|
||||
context: self.context,
|
||||
animationCache: self.context.animationCache,
|
||||
@ -809,10 +810,8 @@ public final class EmojiStatusSelectionController: ViewController {
|
||||
return
|
||||
}
|
||||
|
||||
var expirationDate: Int32?
|
||||
if result.timeout > 0 {
|
||||
expirationDate = Int32(Date().timeIntervalSince1970) + Int32(result.timeout)
|
||||
}
|
||||
let expirationDate: Int32? = result.timestamp
|
||||
|
||||
let _ = (strongSelf.context.engine.accountData.setEmojiStatus(file: previewItem.item.itemFile, expirationDate: expirationDate)
|
||||
|> deliverOnMainQueue).start()
|
||||
|
||||
|
@ -2779,7 +2779,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
public func animateInReactionSelection(sourceItems: [MediaId: (position: CGPoint, frameIndex: Int, placeholder: UIImage)]) {
|
||||
public func animateInReactionSelection(sourceItems: [MediaId: (frame: CGRect, frameIndex: Int, placeholder: UIImage)]) {
|
||||
guard let component = self.component, let itemLayout = self.itemLayout else {
|
||||
return
|
||||
}
|
||||
@ -2792,10 +2792,12 @@ public final class EmojiPagerContentComponent: Component {
|
||||
continue
|
||||
}
|
||||
if let sourceItem = sourceItems[file.fileId] {
|
||||
itemLayer.animatePosition(from: CGPoint(x: sourceItem.position.x - itemLayer.position.x, y: 0.0), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
itemLayer.animatePosition(from: CGPoint(x: sourceItem.frame.center.x - itemLayer.position.x, y: 0.0), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
|
||||
if let itemSelectionLayer = self.visibleItemSelectionLayers[key] {
|
||||
itemSelectionLayer.animatePosition(from: CGPoint(x: sourceItem.position.x - itemLayer.position.x, y: 0.0), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
itemSelectionLayer.animatePosition(from: CGPoint(x: sourceItem.frame.center.x - itemLayer.position.x, y: 0.0), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
|
||||
itemSelectionLayer.animate(from: (min(sourceItem.frame.width, sourceItem.frame.height) * 0.5) as NSNumber, to: 8.0 as NSNumber, keyPath: "cornerRadius", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.3)
|
||||
}
|
||||
|
||||
component.animationRenderer.setFrameIndex(itemId: animationData.resource.resource.id.stringRepresentation, size: itemLayer.pixelSize, frameIndex: sourceItem.frameIndex, placeholder: sourceItem.placeholder)
|
||||
@ -4130,8 +4132,10 @@ public final class EmojiPagerContentComponent: Component {
|
||||
itemLayer?.removeFromSuperlayer()
|
||||
})
|
||||
if let itemSelectionLayer = itemSelectionLayer {
|
||||
transition.setPosition(layer: itemSelectionLayer, position: position, completion: { [weak itemSelectionLayer] _ in
|
||||
let itemSelectionTintContainerLayer = itemSelectionLayer.tintContainerLayer
|
||||
transition.setPosition(layer: itemSelectionLayer, position: position, completion: { [weak itemSelectionLayer, weak itemSelectionTintContainerLayer] _ in
|
||||
itemSelectionLayer?.removeFromSuperlayer()
|
||||
itemSelectionTintContainerLayer?.removeFromSuperlayer()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
@ -4160,6 +4164,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
itemLayer.removeFromSuperlayer()
|
||||
if let itemSelectionLayer = itemSelectionLayer {
|
||||
itemSelectionLayer.removeFromSuperlayer()
|
||||
itemSelectionLayer.tintContainerLayer.removeFromSuperlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4191,7 +4196,6 @@ public final class EmojiPagerContentComponent: Component {
|
||||
} else {
|
||||
itemSelectionLayer.removeFromSuperlayer()
|
||||
removedItemSelectionLayerIds.append(id)
|
||||
|
||||
}
|
||||
}
|
||||
for id in removedItemSelectionLayerIds {
|
||||
@ -4782,7 +4786,21 @@ public final class EmojiPagerContentComponent: Component {
|
||||
return hasPremium
|
||||
}
|
||||
|
||||
public static func emojiInputData(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, isStandalone: Bool, isStatusSelection: Bool, isReactionSelection: Bool, isQuickReactionSelection: Bool = false, topReactionItems: [EmojiComponentReactionItem], areUnicodeEmojiEnabled: Bool, areCustomEmojiEnabled: Bool, chatPeerId: EnginePeer.Id?, selectedItems: Set<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 isPremiumDisabled = premiumConfiguration.isPremiumDisabled
|
||||
|
||||
@ -4878,7 +4896,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
} else {
|
||||
itemGroupIndexById[groupId] = itemGroups.count
|
||||
//TODO:localize
|
||||
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: "Long tap to set a timer".uppercased(), subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: 5, isClearable: false, headerItem: nil, items: [resultItem]))
|
||||
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: topStatusTitle?.uppercased(), subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: 5, isClearable: false, headerItem: nil, items: [resultItem]))
|
||||
}
|
||||
|
||||
var existingIds = Set<MediaId>()
|
||||
@ -4964,7 +4982,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
}
|
||||
}
|
||||
if let featuredStatusEmoji = featuredStatusEmoji {
|
||||
for item in featuredStatusEmoji.items {
|
||||
for item in featuredStatusEmoji.items.prefix(7) {
|
||||
guard let item = item.contents.get(RecentMediaItem.self) else {
|
||||
continue
|
||||
}
|
||||
|
@ -998,10 +998,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.context.account.peerId)),
|
||||
contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, context: strongSelf.context, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, selectAll: selectAll, interfaceInteraction: strongSelf.interfaceInteraction, messageNode: node as? ChatMessageItemView),
|
||||
peerMessageAllowedReactions(context: strongSelf.context, message: topMessage),
|
||||
peerMessageSelectedReactionFiles(context: strongSelf.context, message: topMessage),
|
||||
peerMessageSelectedReactions(context: strongSelf.context, message: topMessage),
|
||||
topMessageReactions(context: strongSelf.context, message: topMessage),
|
||||
ApplicationSpecificNotice.getChatTextSelectionTips(accountManager: strongSelf.context.sharedContext.accountManager)
|
||||
).start(next: { peer, actions, allowedReactions, selectedReactionFiles, topReactions, chatTextSelectionTips in
|
||||
).start(next: { peer, actions, allowedReactions, selectedReactions, topReactions, chatTextSelectionTips in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
@ -1062,6 +1062,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
|
||||
if canAddMessageReactions(message: topMessage), let allowedReactions = allowedReactions, !topReactions.isEmpty {
|
||||
actions.reactionItems = topReactions.map(ReactionContextItem.reaction)
|
||||
actions.selectedReactionItems = selectedReactions.reactions
|
||||
|
||||
if !actions.reactionItems.isEmpty {
|
||||
let reactionItems: [EmojiComponentReactionItem] = actions.reactionItems.compactMap { item -> EmojiComponentReactionItem? in
|
||||
@ -1102,7 +1103,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
areUnicodeEmojiEnabled: false,
|
||||
areCustomEmojiEnabled: true,
|
||||
chatPeerId: strongSelf.chatLocation.peerId,
|
||||
selectedItems: selectedReactionFiles
|
||||
selectedItems: selectedReactions.files
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -17017,14 +17018,19 @@ func peerMessageAllowedReactions(context: AccountContext, message: Message) -> S
|
||||
}
|
||||
}
|
||||
|
||||
func peerMessageSelectedReactionFiles(context: AccountContext, message: Message) -> Signal<Set<MediaId>, NoError> {
|
||||
func peerMessageSelectedReactions(context: AccountContext, message: Message) -> Signal<(reactions: Set<MessageReaction.Reaction>, files: Set<MediaId>), NoError> {
|
||||
return context.engine.stickers.availableReactions()
|
||||
|> take(1)
|
||||
|> map { availableReactions -> Set<MediaId> in
|
||||
|> map { availableReactions -> (reactions: Set<MessageReaction.Reaction>, files: Set<MediaId>) in
|
||||
var result = Set<MediaId>()
|
||||
var reactions = Set<MessageReaction.Reaction>()
|
||||
|
||||
if let effectiveReactions = message.effectiveReactions {
|
||||
for reaction in effectiveReactions {
|
||||
if !reaction.isSelected {
|
||||
continue
|
||||
}
|
||||
reactions.insert(reaction.value)
|
||||
switch reaction.value {
|
||||
case .builtin:
|
||||
if let availableReaction = availableReactions?.reactions.first(where: { $0.value == reaction.value }) {
|
||||
@ -17036,7 +17042,7 @@ func peerMessageSelectedReactionFiles(context: AccountContext, message: Message)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
return (reactions, result)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1341,7 +1341,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
let _ = inputMediaNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, standardInputHeight: layout.standardInputHeight, inputHeight: layout.inputHeight ?? 0.0, maximumHeight: maximumInputNodeHeight, inputPanelHeight: inputPanelSize?.height ?? 0.0, transition: .immediate, interfaceState: self.chatPresentationInterfaceState, deviceMetrics: layout.deviceMetrics, isVisible: false, isExpanded: self.inputPanelContainerNode.stableIsExpanded)
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.titleAccessoryPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: 66.0)))
|
||||
transition.updateFrame(node: self.titleAccessoryPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: 100.0)))
|
||||
|
||||
transition.updateFrame(node: self.inputContextPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: layout.size.height)))
|
||||
transition.updateFrame(node: self.inputContextOverTextPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: layout.size.height)))
|
||||
|
@ -77,8 +77,8 @@ func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceStat
|
||||
if displayActionsPanel && (selectedContext == nil || selectedContext! <= .pinnedMessage) {
|
||||
if let currentPanel = currentPanel as? ChatReportPeerTitlePanelNode {
|
||||
return currentPanel
|
||||
} else {
|
||||
let panel = ChatReportPeerTitlePanelNode()
|
||||
} else if let controllerInteraction = controllerInteraction {
|
||||
let panel = ChatReportPeerTitlePanelNode(context: context, animationCache: controllerInteraction.presentationContext.animationCache, animationRenderer: controllerInteraction.presentationContext.animationRenderer)
|
||||
panel.interfaceInteraction = interfaceInteraction
|
||||
return panel
|
||||
}
|
||||
|
@ -10,6 +10,10 @@ import TelegramStringFormatting
|
||||
import TextFormat
|
||||
import Markdown
|
||||
import ChatPresentationInterfaceState
|
||||
import TextNodeWithEntities
|
||||
import AnimationCache
|
||||
import MultiAnimationRenderer
|
||||
import AccountContext
|
||||
|
||||
private enum ChatReportPeerTitleButton: Equatable {
|
||||
case block
|
||||
@ -301,11 +305,16 @@ private final class ChatInfoTitlePanelPeerNearbyInfoNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode {
|
||||
private let context: AccountContext
|
||||
private let animationCache: AnimationCache
|
||||
private let animationRenderer: MultiAnimationRenderer
|
||||
|
||||
private let separatorNode: ASDisplayNode
|
||||
|
||||
private let closeButton: HighlightableButtonNode
|
||||
private var buttons: [(ChatReportPeerTitleButton, UIButton)] = []
|
||||
private let textNode: ImmediateTextNode
|
||||
private var emojiStatusTextNode: TextNodeWithEntities?
|
||||
|
||||
private var theme: PresentationTheme?
|
||||
|
||||
@ -314,7 +323,11 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode {
|
||||
|
||||
private var tapGestureRecognizer: UITapGestureRecognizer?
|
||||
|
||||
override init() {
|
||||
init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer) {
|
||||
self.context = context
|
||||
self.animationCache = animationCache
|
||||
self.animationRenderer = animationRenderer
|
||||
|
||||
self.separatorNode = ASDisplayNode()
|
||||
self.separatorNode.isLayerBacked = true
|
||||
|
||||
@ -481,9 +494,70 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode {
|
||||
self.tapGestureRecognizer?.isEnabled = false
|
||||
}
|
||||
|
||||
|
||||
let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0))
|
||||
transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: width - contentRightInset - closeButtonSize.width, y: floorToScreenPixels((panelHeight - closeButtonSize.height) / 2.0)), size: closeButtonSize))
|
||||
|
||||
var emojiStatus: PeerEmojiStatus?
|
||||
if let user = interfaceState.renderedPeer?.peer as? TelegramUser, let emojiStatusValue = user.emojiStatus {
|
||||
emojiStatus = emojiStatusValue
|
||||
}
|
||||
|
||||
/*#if DEBUG
|
||||
emojiStatus = PeerEmojiStatus(fileId: 5062172592505356289, expirationDate: nil)
|
||||
#endif*/
|
||||
|
||||
if let emojiStatus = emojiStatus {
|
||||
let emojiStatusTextNode: TextNodeWithEntities
|
||||
if let current = self.emojiStatusTextNode {
|
||||
emojiStatusTextNode = current
|
||||
} else {
|
||||
emojiStatusTextNode = TextNodeWithEntities()
|
||||
self.emojiStatusTextNode = emojiStatusTextNode
|
||||
self.addSubnode(emojiStatusTextNode.textNode)
|
||||
}
|
||||
|
||||
let plainText = interfaceState.strings.Chat_PanelCustomStatusInfo(".")
|
||||
let attributedText = NSMutableAttributedString(attributedString: NSAttributedString(string: plainText.string, font: Font.regular(13.0), textColor: interfaceState.theme.rootController.navigationBar.secondaryTextColor, paragraphAlignment: .center))
|
||||
for range in plainText.ranges {
|
||||
attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: emojiStatus.fileId, file: nil), range: range.range)
|
||||
}
|
||||
|
||||
let makeEmojiStatusLayout = TextNodeWithEntities.asyncLayout(emojiStatusTextNode)
|
||||
let (emojiStatusLayout, emojiStatusApply) = makeEmojiStatusLayout(TextNodeLayoutArguments(
|
||||
attributedString: attributedText,
|
||||
backgroundColor: nil,
|
||||
minimumNumberOfLines: 0,
|
||||
maximumNumberOfLines: 0,
|
||||
truncationType: .end,
|
||||
constrainedSize: CGSize(width: width - leftInset * 2.0 - 8.0 * 2.0, height: CGFloat.greatestFiniteMagnitude),
|
||||
alignment: .center,
|
||||
verticalAlignment: .top,
|
||||
lineSpacing: 0.0,
|
||||
cutout: nil,
|
||||
insets: UIEdgeInsets(),
|
||||
lineColor: nil,
|
||||
textShadowColor: nil,
|
||||
textStroke: nil,
|
||||
displaySpoilers: false,
|
||||
displayEmbeddedItemsUnderSpoilers: false
|
||||
))
|
||||
let _ = emojiStatusApply(TextNodeWithEntities.Arguments(
|
||||
context: self.context,
|
||||
cache: self.animationCache,
|
||||
renderer: self.animationRenderer,
|
||||
placeholderColor: interfaceState.theme.list.mediaPlaceholderColor,
|
||||
attemptSynchronous: false
|
||||
))
|
||||
transition.updateFrame(node: emojiStatusTextNode.textNode, frame: CGRect(origin: CGPoint(x: floor((width - emojiStatusLayout.size.width) / 2.0), y: panelHeight), size: emojiStatusLayout.size))
|
||||
panelHeight += emojiStatusLayout.size.height + 8.0
|
||||
|
||||
emojiStatusTextNode.visibilityRect = .infinite
|
||||
} else {
|
||||
if let emojiStatusTextNode = self.emojiStatusTextNode {
|
||||
self.emojiStatusTextNode = nil
|
||||
emojiStatusTextNode.textNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
|
||||
let initialPanelHeight = panelHeight
|
||||
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel)))
|
||||
|
@ -3106,11 +3106,17 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
|
||||
strongSelf.emojiStatusSelectionController?.dismiss()
|
||||
var selectedItems = Set<MediaId>()
|
||||
var topStatusTitle = "Long tap to set a timer"
|
||||
if let peer = strongSelf.data?.peer {
|
||||
if let user = peer as? TelegramUser, let emojiStatus = user.emojiStatus {
|
||||
selectedItems.insert(MediaId(namespace: Namespaces.Media.CloudFile, id: emojiStatus.fileId))
|
||||
|
||||
if let timestamp = emojiStatus.expirationDate {
|
||||
topStatusTitle = peerStatusExpirationString(statusTimestamp: timestamp, relativeTo: Int32(Date().timeIntervalSince1970), strings: strongSelf.presentationData.strings, dateTimeFormat: strongSelf.presentationData.dateTimeFormat)
|
||||
}
|
||||
}
|
||||
}
|
||||
//TODO:localize
|
||||
let emojiStatusSelectionController = EmojiStatusSelectionController(
|
||||
context: strongSelf.context,
|
||||
mode: .statusSelection,
|
||||
@ -3126,7 +3132,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
areUnicodeEmojiEnabled: false,
|
||||
areCustomEmojiEnabled: true,
|
||||
chatPeerId: strongSelf.context.account.peerId,
|
||||
selectedItems: selectedItems
|
||||
selectedItems: selectedItems,
|
||||
topStatusTitle: topStatusTitle
|
||||
),
|
||||
destinationItemView: { [weak sourceView] in
|
||||
return sourceView
|
||||
|
Loading…
x
Reference in New Issue
Block a user