Status and reaction improvements

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

View File

@ -8022,3 +8022,12 @@ Sorry for the inconvenience.";
"SetTimeoutFor.Days_1" = "Set for 1 day";
"SetTimeoutFor.Days_any" = "Set for %@ days";
"PeerStatusExpiration.Minutes_1" = "Your status expires in one minute";
"PeerStatusExpiration.Minutes_any" = "Your status expires in %@ minutes";
"PeerStatusExpiration.Hours_1" = "Your status expires in one hour";
"PeerStatusExpiration.Hours_any" = "Your status expires in %@ hours";
"PeerStatusExpiration.TomorrowAt" = "Your status expires tomorrow at %@";
"PeerStatusExpiration.AtDate" = "Your status expires on %@";
"Chat.PanelCustomStatusInfo" = "This account uses %@ as a custom status next to its name. Such emoji statuses are available to all subscribers of Telegram Premium.";

View File

@ -35,6 +35,7 @@ import AnimationCache
import MultiAnimationRenderer
import 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,45 @@
import Foundation
final class MutablePeerTimeoutAttributesView: MutablePostboxView {
fileprivate var minValue: (peerId: PeerId, timestamp: UInt32)?
init(postbox: PostboxImpl) {
self.minValue = postbox.peerTimeoutPropertiesTable.min()
}
func replay(postbox: PostboxImpl, transaction: PostboxTransaction) -> Bool {
var updated = false
if transaction.updatedPeerTimeoutAttributes {
let minValue = postbox.peerTimeoutPropertiesTable.min()
if self.minValue?.0 != minValue?.0 || self.minValue?.1 != minValue?.1 {
updated = true
self.minValue = minValue
}
}
return updated
}
func refreshDueToExternalTransaction(postbox: PostboxImpl) -> Bool {
let minValue = postbox.peerTimeoutPropertiesTable.min()
if self.minValue?.0 != minValue?.0 || self.minValue?.1 != minValue?.1 {
self.minValue = minValue
return true
} else {
return false
}
}
func immutableView() -> PostboxView {
return PeerTimeoutAttributesView(self)
}
}
public final class PeerTimeoutAttributesView: PostboxView {
public let minValue: (peerId: PeerId, timestamp: UInt32)?
init(_ view: MutablePeerTimeoutAttributesView) {
self.minValue = view.minValue
}
}

View File

@ -0,0 +1,102 @@
import Foundation
final class PeerTimeoutPropertiesTable: Table {
private struct Key: Hashable {
var peerId: PeerId
var timestamp: UInt32
}
static func tableSpec(_ id: Int32) -> ValueBoxTable {
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: false)
}
private let sharedKey = ValueBoxKey(length: 4 + 8)
private var cache: [Key: Bool] = [:]
private var updated = Set<Key>()
var hasUpdates: Bool {
return !self.updated.isEmpty
}
private func key(_ key: Key) -> ValueBoxKey {
self.sharedKey.setInt32(0, value: Int32(bitPattern: key.timestamp))
self.sharedKey.setInt64(4, value: key.peerId.toInt64())
return self.sharedKey
}
private func lowerBound() -> ValueBoxKey {
let key = ValueBoxKey(length: 4 + 8)
key.setInt32(0, value: 0)
return key
}
private func upperBound() -> ValueBoxKey {
let key = ValueBoxKey(length: 4 + 8)
key.setInt32(0, value: Int32(bitPattern: UInt32.max))
key.setInt64(4, value: Int64(bitPattern: UInt64.max))
return key
}
func min() -> (peerId: PeerId, timestamp: UInt32)? {
var result: Key?
self.valueBox.range(self.table, start: self.lowerBound(), end: self.upperBound(), keys: { key in
result = Key(peerId: PeerId(key.getInt64(4)), timestamp: UInt32(bitPattern: key.getInt32(0)))
return false
}, limit: 1)
return result.flatMap { result -> (peerId: PeerId, timestamp: UInt32) in
return (result.peerId, result.timestamp)
}
}
private func get(peerId: PeerId, timestamp: UInt32) -> Bool {
let key = Key(peerId: peerId, timestamp: timestamp)
if let cachedValue = self.cache[key] {
return cachedValue
} else {
let value = self.valueBox.exists(self.table, key: self.key(Key(peerId: peerId, timestamp: timestamp)))
self.cache[key] = value
return value
}
}
func remove(peerId: PeerId, timestamp: UInt32) {
let key = Key(peerId: peerId, timestamp: timestamp)
if self.get(peerId: peerId, timestamp: timestamp) {
self.cache[key] = false
self.updated.insert(key)
}
}
func add(peerId: PeerId, timestamp: UInt32) {
let key = Key(peerId: peerId, timestamp: timestamp)
if !self.get(peerId: peerId, timestamp: timestamp) {
self.cache[key] = true
self.updated.insert(key)
}
}
override func clearMemoryCache() {
self.cache.removeAll()
}
override func beforeCommit() {
if !self.updated.isEmpty {
for key in self.updated {
if let value = self.cache[key] {
if value {
self.valueBox.set(self.table, key: self.key(key), value: MemoryBuffer())
} else {
self.valueBox.remove(self.table, key: self.key(key), secure: false)
}
}
}
self.updated.removeAll()
if !self.useCaches {
self.cache.removeAll()
}
}
}
}

View File

@ -1153,6 +1153,11 @@ public final class Transaction {
assert(!self.disposed)
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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,110 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
private typealias SignalKitTimer = SwiftSignalKit.Timer
private final class ManagedPeerTimestampAttributeOperationsHelper {
struct Entry: Equatable {
var peerId: PeerId
var timestamp: UInt32
}
var entry: (Entry, MetaDisposable)?
func update(_ head: Entry?) -> (disposeOperations: [Disposable], beginOperations: [(Entry, MetaDisposable)]) {
var disposeOperations: [Disposable] = []
var beginOperations: [(Entry, MetaDisposable)] = []
if self.entry?.0 != head {
if let (_, disposable) = self.entry {
self.entry = nil
disposeOperations.append(disposable)
}
if let head = head {
let disposable = MetaDisposable()
self.entry = (head, disposable)
beginOperations.append((head, disposable))
}
}
return (disposeOperations, beginOperations)
}
func reset() -> [Disposable] {
if let entry = entry {
return [entry.1]
} else {
return []
}
}
}
func managedPeerTimestampAttributeOperations(network: Network, postbox: Postbox) -> Signal<Void, NoError> {
return Signal { _ in
let helper = Atomic(value: ManagedPeerTimestampAttributeOperationsHelper())
let timeOffsetOnce = Signal<Double, NoError> { subscriber in
subscriber.putNext(network.globalTimeDifference)
return EmptyDisposable
}
let timeOffset = (
timeOffsetOnce
|> then(
Signal<Double, NoError>.complete()
|> delay(1.0, queue: .mainQueue())
)
)
|> restart
|> map { value -> Double in
round(value)
}
|> distinctUntilChanged
let disposable = combineLatest(timeOffset, postbox.combinedView(keys: [PostboxViewKey.peerTimeoutAttributes])).start(next: { timeOffset, views in
guard let view = views.views[PostboxViewKey.peerTimeoutAttributes] as? PeerTimeoutAttributesView else {
return
}
let topEntry = view.minValue.flatMap { value in
return ManagedPeerTimestampAttributeOperationsHelper.Entry(peerId: value.peerId, timestamp: value.timestamp)
}
let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(ManagedPeerTimestampAttributeOperationsHelper.Entry, MetaDisposable)]) in
return helper.update(topEntry)
}
for disposable in disposeOperations {
disposable.dispose()
}
for (entry, disposable) in beginOperations {
let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + timeOffset
let delay = max(0.0, Double(entry.timestamp) - timestamp)
let signal = Signal<Void, NoError>.complete()
|> suspendAwareDelay(delay, queue: Queue.concurrentDefaultQueue())
|> then(postbox.transaction { transaction -> Void in
if let peer = transaction.getPeer(entry.peerId) {
if let user = peer as? TelegramUser {
updatePeers(transaction: transaction, peers: [user.withUpdatedEmojiStatus(nil)], update: { _, updated in updated })
}
}
//failsafe
transaction.removePeerTimeoutAttributeEntry(peerId: entry.peerId, timestamp: entry.timestamp)
})
disposable.set(signal.start())
}
})
return ActionDisposable {
disposable.dispose()
let disposables = helper.with { helper -> [Disposable] in
return helper.reset()
}
for disposable in disposables {
disposable.dispose()
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -458,6 +458,30 @@ public func stringAndActivityForUserPresence(strings: PresentationStrings, dateT
}
}
public func peerStatusExpirationString(statusTimestamp: Int32, relativeTo timestamp: Int32, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat) -> String {
let difference = max(statusTimestamp - timestamp, 60)
if difference < 60 * 60 {
return strings.PeerStatusExpiration_Minutes(Int32(round(Double(difference) / Double(60))))
} else if difference < 24 * 60 * 60 {
return strings.PeerStatusExpiration_Hours(Int32(round(Double(difference) / Double(60 * 60))))
} else {
var t: time_t = time_t(statusTimestamp)
var timeinfo: tm = tm()
localtime_r(&t, &timeinfo)
var now: time_t = time_t(timestamp)
var timeinfoNow: tm = tm()
localtime_r(&now, &timeinfoNow)
let dayDifference = timeinfo.tm_yday - timeinfoNow.tm_yday
if dayDifference == 1 {
return strings.PeerStatusExpiration_TomorrowAt(stringForShortTimestamp(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, dateTimeFormat: dateTimeFormat)).string
} else {
return strings.PeerStatusExpiration_AtDate(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, dateTimeFormat: dateTimeFormat)).string
}
}
}
public func userPresenceStringRefreshTimeout(_ presence: TelegramUserPresence, relativeTo timestamp: Int32) -> Double {
switch presence.status {
case let .present(statusTimestamp):

View File

@ -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",

View File

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

View File

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

View File

@ -2779,7 +2779,7 @@ public final class EmojiPagerContentComponent: Component {
}
}
public func animateInReactionSelection(sourceItems: [MediaId: (position: CGPoint, frameIndex: Int, placeholder: UIImage)]) {
public func animateInReactionSelection(sourceItems: [MediaId: (frame: CGRect, frameIndex: Int, placeholder: UIImage)]) {
guard let component = self.component, let itemLayout = self.itemLayout else {
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
}

View File

@ -998,10 +998,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.context.account.peerId)),
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)
}
}

View File

@ -1341,7 +1341,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
let _ = inputMediaNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, standardInputHeight: layout.standardInputHeight, inputHeight: layout.inputHeight ?? 0.0, maximumHeight: maximumInputNodeHeight, inputPanelHeight: inputPanelSize?.height ?? 0.0, transition: .immediate, interfaceState: self.chatPresentationInterfaceState, deviceMetrics: layout.deviceMetrics, isVisible: false, isExpanded: self.inputPanelContainerNode.stableIsExpanded)
}
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)))

View File

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

View File

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

View File

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