mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-25 17:43:18 +00:00
Support view list
This commit is contained in:
parent
8a6f5dd66e
commit
6404f54b80
@ -6793,3 +6793,6 @@ Sorry for the inconvenience.";
|
|||||||
"Gif.Emotion.Party" = "Party";
|
"Gif.Emotion.Party" = "Party";
|
||||||
|
|
||||||
"Conversation.ForwardFrom" = "From: %@";
|
"Conversation.ForwardFrom" = "From: %@";
|
||||||
|
|
||||||
|
"Conversation.ContextMenuSeen_1" = "1 Seen";
|
||||||
|
"Conversation.ContextMenuSeen_any" = "%@ Seen";
|
||||||
|
|||||||
@ -11,14 +11,17 @@ import AudioBlob
|
|||||||
public final class AnimatedAvatarSetContext {
|
public final class AnimatedAvatarSetContext {
|
||||||
public final class Content {
|
public final class Content {
|
||||||
fileprivate final class Item {
|
fileprivate final class Item {
|
||||||
fileprivate struct Key: Hashable {
|
fileprivate enum Key: Hashable {
|
||||||
var peerId: EnginePeer.Id
|
case peer(EnginePeer.Id)
|
||||||
|
case placeholder(Int)
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate let peer: EnginePeer
|
fileprivate let peer: EnginePeer?
|
||||||
|
fileprivate let placeholderColor: UIColor
|
||||||
|
|
||||||
fileprivate init(peer: EnginePeer) {
|
fileprivate init(peer: EnginePeer?, placeholderColor: UIColor) {
|
||||||
self.peer = peer
|
self.peer = peer
|
||||||
|
self.placeholderColor = placeholderColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,7 +49,15 @@ public final class AnimatedAvatarSetContext {
|
|||||||
public func update(peers: [EnginePeer], animated: Bool) -> Content {
|
public func update(peers: [EnginePeer], animated: Bool) -> Content {
|
||||||
var items: [(Content.Item.Key, Content.Item)] = []
|
var items: [(Content.Item.Key, Content.Item)] = []
|
||||||
for peer in peers {
|
for peer in peers {
|
||||||
items.append((Content.Item.Key(peerId: peer.id), Content.Item(peer: peer)))
|
items.append((.peer(peer.id), Content.Item(peer: peer, placeholderColor: .white)))
|
||||||
|
}
|
||||||
|
return Content(items: items)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func updatePlaceholder(color: UIColor, count: Int, animated: Bool) -> Content {
|
||||||
|
var items: [(Content.Item.Key, Content.Item)] = []
|
||||||
|
for i in 0 ..< count {
|
||||||
|
items.append((.placeholder(i), Content.Item(peer: nil, placeholderColor: color)))
|
||||||
}
|
}
|
||||||
return Content(items: items)
|
return Content(items: items)
|
||||||
}
|
}
|
||||||
@ -60,9 +71,15 @@ private final class ContentNode: ASDisplayNode {
|
|||||||
private let unclippedNode: ASImageNode
|
private let unclippedNode: ASImageNode
|
||||||
private let clippedNode: ASImageNode
|
private let clippedNode: ASImageNode
|
||||||
|
|
||||||
|
private var size: CGSize
|
||||||
|
private var spacing: CGFloat
|
||||||
|
|
||||||
private var disposable: Disposable?
|
private var disposable: Disposable?
|
||||||
|
|
||||||
init(context: AccountContext, peer: EnginePeer, synchronousLoad: Bool) {
|
init(context: AccountContext, peer: EnginePeer?, placeholderColor: UIColor, synchronousLoad: Bool, size: CGSize, spacing: CGFloat) {
|
||||||
|
self.size = size
|
||||||
|
self.spacing = spacing
|
||||||
|
|
||||||
self.unclippedNode = ASImageNode()
|
self.unclippedNode = ASImageNode()
|
||||||
self.clippedNode = ASImageNode()
|
self.clippedNode = ASImageNode()
|
||||||
|
|
||||||
@ -71,37 +88,46 @@ private final class ContentNode: ASDisplayNode {
|
|||||||
self.addSubnode(self.unclippedNode)
|
self.addSubnode(self.unclippedNode)
|
||||||
self.addSubnode(self.clippedNode)
|
self.addSubnode(self.clippedNode)
|
||||||
|
|
||||||
if let representation = peer.smallProfileImage, let signal = peerAvatarImage(account: context.account, peerReference: PeerReference(peer._asPeer()), authorOfMessage: nil, representation: representation, displayDimensions: CGSize(width: 30.0, height: 30.0), synchronousLoad: synchronousLoad) {
|
if let peer = peer {
|
||||||
let image = generateImage(CGSize(width: 30.0, height: 30.0), rotatedContext: { size, context in
|
if let representation = peer.smallProfileImage, let signal = peerAvatarImage(account: context.account, peerReference: PeerReference(peer._asPeer()), authorOfMessage: nil, representation: representation, displayDimensions: size, synchronousLoad: synchronousLoad) {
|
||||||
|
let image = generateImage(size, rotatedContext: { size, context in
|
||||||
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
|
context.setFillColor(UIColor.lightGray.cgColor)
|
||||||
|
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||||
|
})!
|
||||||
|
self.updateImage(image: image, size: size, spacing: spacing)
|
||||||
|
|
||||||
|
let disposable = (signal
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] imageVersions in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let image = imageVersions?.0
|
||||||
|
if let image = image {
|
||||||
|
strongSelf.updateImage(image: image, size: size, spacing: spacing)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
self.disposable = disposable
|
||||||
|
} else {
|
||||||
|
let image = generateImage(size, rotatedContext: { size, context in
|
||||||
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
|
drawPeerAvatarLetters(context: context, size: size, font: avatarFont, letters: peer.displayLetters, peerId: peer.id)
|
||||||
|
})!
|
||||||
|
self.updateImage(image: image, size: size, spacing: spacing)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let image = generateImage(size, rotatedContext: { size, context in
|
||||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
context.setFillColor(UIColor.lightGray.cgColor)
|
context.setFillColor(placeholderColor.cgColor)
|
||||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||||
})!
|
})!
|
||||||
self.updateImage(image: image)
|
self.updateImage(image: image, size: size, spacing: spacing)
|
||||||
|
|
||||||
let disposable = (signal
|
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] imageVersions in
|
|
||||||
guard let strongSelf = self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let image = imageVersions?.0
|
|
||||||
if let image = image {
|
|
||||||
strongSelf.updateImage(image: image)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
self.disposable = disposable
|
|
||||||
} else {
|
|
||||||
let image = generateImage(CGSize(width: 30.0, height: 30.0), rotatedContext: { size, context in
|
|
||||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
|
||||||
drawPeerAvatarLetters(context: context, size: size, font: avatarFont, letters: peer.displayLetters, peerId: peer.id)
|
|
||||||
})!
|
|
||||||
self.updateImage(image: image)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateImage(image: UIImage) {
|
private func updateImage(image: UIImage, size: CGSize, spacing: CGFloat) {
|
||||||
self.unclippedNode.image = image
|
self.unclippedNode.image = image
|
||||||
self.clippedNode.image = generateImage(CGSize(width: 30.0, height: 30.0), rotatedContext: { size, context in
|
self.clippedNode.image = generateImage(size, rotatedContext: { size, context in
|
||||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
||||||
context.scaleBy(x: 1.0, y: -1.0)
|
context.scaleBy(x: 1.0, y: -1.0)
|
||||||
@ -113,7 +139,7 @@ private final class ContentNode: ASDisplayNode {
|
|||||||
|
|
||||||
context.setBlendMode(.copy)
|
context.setBlendMode(.copy)
|
||||||
context.setFillColor(UIColor.clear.cgColor)
|
context.setFillColor(UIColor.clear.cgColor)
|
||||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: -1.5, dy: -1.5).offsetBy(dx: -20.0, dy: 0.0))
|
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: -1.5, dy: -1.5).offsetBy(dx: spacing - size.width, dy: 0.0))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,10 +218,17 @@ public final class AnimatedAvatarSetNode: ASDisplayNode {
|
|||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func update(context: AccountContext, content: AnimatedAvatarSetContext.Content, itemSize: CGSize = CGSize(width: 30.0, height: 30.0), animated: Bool, synchronousLoad: Bool) -> CGSize {
|
public func update(context: AccountContext, content: AnimatedAvatarSetContext.Content, itemSize: CGSize = CGSize(width: 30.0, height: 30.0), customSpacing: CGFloat? = nil, animated: Bool, synchronousLoad: Bool) -> CGSize {
|
||||||
var contentWidth: CGFloat = 0.0
|
var contentWidth: CGFloat = 0.0
|
||||||
let contentHeight: CGFloat = itemSize.height
|
let contentHeight: CGFloat = itemSize.height
|
||||||
|
|
||||||
|
let spacing: CGFloat
|
||||||
|
if let customSpacing = customSpacing {
|
||||||
|
spacing = customSpacing
|
||||||
|
} else {
|
||||||
|
spacing = 10.0
|
||||||
|
}
|
||||||
|
|
||||||
let transition: ContainedViewLayoutTransition
|
let transition: ContainedViewLayoutTransition
|
||||||
if animated {
|
if animated {
|
||||||
transition = .animated(duration: 0.2, curve: .easeInOut)
|
transition = .animated(duration: 0.2, curve: .easeInOut)
|
||||||
@ -218,7 +251,7 @@ public final class AnimatedAvatarSetNode: ASDisplayNode {
|
|||||||
itemNode.updateLayout(size: itemSize, isClipped: index != 0, animated: animated)
|
itemNode.updateLayout(size: itemSize, isClipped: index != 0, animated: animated)
|
||||||
transition.updateFrame(node: itemNode, frame: itemFrame)
|
transition.updateFrame(node: itemNode, frame: itemFrame)
|
||||||
} else {
|
} else {
|
||||||
itemNode = ContentNode(context: context, peer: item.peer, synchronousLoad: synchronousLoad)
|
itemNode = ContentNode(context: context, peer: item.peer, placeholderColor: item.placeholderColor, synchronousLoad: synchronousLoad, size: itemSize, spacing: spacing)
|
||||||
self.addSubnode(itemNode)
|
self.addSubnode(itemNode)
|
||||||
self.contentNodes[key] = itemNode
|
self.contentNodes[key] = itemNode
|
||||||
itemNode.updateLayout(size: itemSize, isClipped: index != 0, animated: false)
|
itemNode.updateLayout(size: itemSize, isClipped: index != 0, animated: false)
|
||||||
@ -229,7 +262,7 @@ public final class AnimatedAvatarSetNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
itemNode.zPosition = CGFloat(100 - i)
|
itemNode.zPosition = CGFloat(100 - i)
|
||||||
contentWidth += itemSize.width - 10.0
|
contentWidth += itemSize.width - spacing
|
||||||
index += 1
|
index += 1
|
||||||
}
|
}
|
||||||
var removeKeys: [AnimatedAvatarSetContext.Content.Item.Key] = []
|
var removeKeys: [AnimatedAvatarSetContext.Content.Item.Key] = []
|
||||||
@ -253,7 +286,7 @@ public final class AnimatedAvatarSetNode: ASDisplayNode {
|
|||||||
|
|
||||||
public func updateAudioLevels(color: UIColor, backgroundColor: UIColor, levels: [EnginePeer.Id: Float]) {
|
public func updateAudioLevels(color: UIColor, backgroundColor: UIColor, levels: [EnginePeer.Id: Float]) {
|
||||||
for (key, itemNode) in self.contentNodes {
|
for (key, itemNode) in self.contentNodes {
|
||||||
if let value = levels[key.peerId] {
|
if case let .peer(peerId) = key, let value = levels[peerId] {
|
||||||
itemNode.updateAudioLevel(color: color, backgroundColor: backgroundColor, value: value)
|
itemNode.updateAudioLevel(color: color, backgroundColor: backgroundColor, value: value)
|
||||||
} else {
|
} else {
|
||||||
itemNode.updateAudioLevel(color: color, backgroundColor: backgroundColor, value: 0.0)
|
itemNode.updateAudioLevel(color: color, backgroundColor: backgroundColor, value: 0.0)
|
||||||
|
|||||||
@ -13,6 +13,7 @@ enum ContextActionSibling {
|
|||||||
public protocol ContextActionNodeProtocol: ASDisplayNode {
|
public protocol ContextActionNodeProtocol: ASDisplayNode {
|
||||||
func setIsHighlighted(_ value: Bool)
|
func setIsHighlighted(_ value: Bool)
|
||||||
func performAction()
|
func performAction()
|
||||||
|
var isActionEnabled: Bool { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol {
|
final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol {
|
||||||
@ -33,6 +34,10 @@ final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol {
|
|||||||
|
|
||||||
private var pointerInteraction: PointerInteraction?
|
private var pointerInteraction: PointerInteraction?
|
||||||
|
|
||||||
|
var isActionEnabled: Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
init(presentationData: PresentationData, action: ContextMenuActionItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) {
|
init(presentationData: PresentationData, action: ContextMenuActionItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) {
|
||||||
self.action = action
|
self.action = action
|
||||||
self.getController = getController
|
self.getController = getController
|
||||||
|
|||||||
@ -139,7 +139,10 @@ private final class InnerActionsContainerNode: ASDisplayNode {
|
|||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let actionNode = strongSelf.actionNode(at: point)
|
var actionNode = strongSelf.actionNode(at: point)
|
||||||
|
if let actionNodeValue = actionNode, !actionNodeValue.isActionEnabled {
|
||||||
|
actionNode = nil
|
||||||
|
}
|
||||||
if actionNode !== strongSelf.currentHighlightedActionNode {
|
if actionNode !== strongSelf.currentHighlightedActionNode {
|
||||||
if actionNode != nil, moved {
|
if actionNode != nil, moved {
|
||||||
strongSelf.feedbackTap()
|
strongSelf.feedbackTap()
|
||||||
|
|||||||
@ -368,7 +368,11 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
|||||||
}
|
}
|
||||||
if strongSelf.didMoveFromInitialGesturePoint {
|
if strongSelf.didMoveFromInitialGesturePoint {
|
||||||
let actionPoint = strongSelf.view.convert(localPoint, to: strongSelf.actionsContainerNode.view)
|
let actionPoint = strongSelf.view.convert(localPoint, to: strongSelf.actionsContainerNode.view)
|
||||||
let actionNode = strongSelf.actionsContainerNode.actionNode(at: actionPoint)
|
var actionNode = strongSelf.actionsContainerNode.actionNode(at: actionPoint)
|
||||||
|
if let actionNodeValue = actionNode, !actionNodeValue.isActionEnabled {
|
||||||
|
actionNode = nil
|
||||||
|
}
|
||||||
|
|
||||||
if strongSelf.highlightedActionNode !== actionNode {
|
if strongSelf.highlightedActionNode !== actionNode {
|
||||||
strongSelf.highlightedActionNode?.setIsHighlighted(false)
|
strongSelf.highlightedActionNode?.setIsHighlighted(false)
|
||||||
strongSelf.highlightedActionNode = actionNode
|
strongSelf.highlightedActionNode = actionNode
|
||||||
|
|||||||
@ -0,0 +1,71 @@
|
|||||||
|
import Postbox
|
||||||
|
import SwiftSignalKit
|
||||||
|
import TelegramApi
|
||||||
|
|
||||||
|
public final class MessageReadStats {
|
||||||
|
public let peers: [EnginePeer]
|
||||||
|
|
||||||
|
public init(peers: [EnginePeer]) {
|
||||||
|
self.peers = peers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func _internal_messageReadStats(account: Account, id: MessageId) -> Signal<MessageReadStats?, NoError> {
|
||||||
|
return account.postbox.transaction { transaction -> Api.InputPeer? in
|
||||||
|
return transaction.getPeer(id.peerId).flatMap(apiInputPeer)
|
||||||
|
}
|
||||||
|
|> mapToSignal { inputPeer -> Signal<MessageReadStats?, NoError> in
|
||||||
|
guard let inputPeer = inputPeer else {
|
||||||
|
return .single(nil)
|
||||||
|
}
|
||||||
|
if id.namespace != Namespaces.Message.Cloud {
|
||||||
|
return .single(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return account.network.request(Api.functions.messages.getMessageReadParticipants(peer: inputPeer, msgId: id.id))
|
||||||
|
|> map(Optional.init)
|
||||||
|
|> `catch` { _ -> Signal<[Int64]?, NoError> in
|
||||||
|
return .single(nil)
|
||||||
|
}
|
||||||
|
|> mapToSignal { result -> Signal<MessageReadStats?, NoError> in
|
||||||
|
guard let result = result else {
|
||||||
|
return .single(nil)
|
||||||
|
}
|
||||||
|
return account.postbox.transaction { transaction -> (peerIds: [PeerId], missingPeerIds: [PeerId]) in
|
||||||
|
var peerIds: [PeerId] = []
|
||||||
|
var missingPeerIds: [PeerId] = []
|
||||||
|
|
||||||
|
for id in result {
|
||||||
|
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id))
|
||||||
|
if peerId == account.peerId {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
peerIds.append(peerId)
|
||||||
|
if transaction.getPeer(peerId) == nil {
|
||||||
|
missingPeerIds.append(peerId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (peerIds: peerIds, missingPeerIds: missingPeerIds)
|
||||||
|
}
|
||||||
|
|> mapToSignal { peerIds, missingPeerIds -> Signal<MessageReadStats?, NoError> in
|
||||||
|
if missingPeerIds.isEmpty || id.peerId.namespace != Namespaces.Peer.CloudChannel {
|
||||||
|
return account.postbox.transaction { transaction -> MessageReadStats? in
|
||||||
|
return MessageReadStats(peers: peerIds.compactMap { peerId -> EnginePeer? in
|
||||||
|
return transaction.getPeer(peerId).flatMap(EnginePeer.init)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return _internal_channelMembers(postbox: account.postbox, network: account.network, accountPeerId: account.peerId, peerId: id.peerId, category: .recent(.all), offset: 0, limit: 50, hash: 0)
|
||||||
|
|> mapToSignal { _ -> Signal<MessageReadStats?, NoError> in
|
||||||
|
return account.postbox.transaction { transaction -> MessageReadStats? in
|
||||||
|
return MessageReadStats(peers: peerIds.compactMap { peerId -> EnginePeer? in
|
||||||
|
return transaction.getPeer(peerId).flatMap(EnginePeer.init)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -203,5 +203,9 @@ public extension TelegramEngine {
|
|||||||
public func adMessages(peerId: PeerId) -> AdMessagesHistoryContext {
|
public func adMessages(peerId: PeerId) -> AdMessagesHistoryContext {
|
||||||
return AdMessagesHistoryContext(account: self.account, peerId: peerId)
|
return AdMessagesHistoryContext(account: self.account, peerId: peerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func messageReadStats(id: MessageId) -> Signal<MessageReadStats?, NoError> {
|
||||||
|
return _internal_messageReadStats(account: self.account, id: id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,9 @@ import PresentationDataUtils
|
|||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import TelegramStringFormatting
|
import TelegramStringFormatting
|
||||||
import UndoUI
|
import UndoUI
|
||||||
|
import ShimmerEffect
|
||||||
|
import AnimatedAvatarSetNode
|
||||||
|
import AvatarNode
|
||||||
|
|
||||||
private struct MessageContextMenuData {
|
private struct MessageContextMenuData {
|
||||||
let starStatus: Bool?
|
let starStatus: Bool?
|
||||||
@ -136,6 +139,51 @@ private func canEditMessage(accountPeerId: PeerId, limitsConfiguration: LimitsCo
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func canViewReadStats(message: Message, appConfig: AppConfiguration) -> Bool {
|
||||||
|
if message.flags.contains(.Incoming) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
guard let peer = message.peers[message.id.peerId] else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for media in message.media {
|
||||||
|
if let _ = media as? TelegramMediaAction {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var maxParticipantCount = 50
|
||||||
|
var maxTimeout = 7 * 86400
|
||||||
|
if let data = appConfig.data {
|
||||||
|
if let value = data["chat_read_mark_size_threshold"] as? Double {
|
||||||
|
maxParticipantCount = Int(value)
|
||||||
|
}
|
||||||
|
if let value = data["chat_read_mark_expire_period"] as? Double {
|
||||||
|
maxTimeout = Int(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch peer {
|
||||||
|
case let channel as TelegramChannel:
|
||||||
|
if case .broadcast = channel.info {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case let group as TelegramGroup:
|
||||||
|
if group.participantCount > maxParticipantCount {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
|
||||||
|
if Int64(message.timestamp) + Int64(maxTimeout) < Int64(timestamp) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceState) -> Bool {
|
func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceState) -> Bool {
|
||||||
guard let peer = chatPresentationInterfaceState.renderedPeer?.peer else {
|
guard let peer = chatPresentationInterfaceState.renderedPeer?.peer else {
|
||||||
return false
|
return false
|
||||||
@ -284,7 +332,7 @@ func updatedChatEditInterfaceMessageState(state: ChatPresentationInterfaceState,
|
|||||||
return updated
|
return updated
|
||||||
}
|
}
|
||||||
|
|
||||||
func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, messages: [Message], controllerInteraction: ChatControllerInteraction?, selectAll: Bool, interfaceInteraction: ChatPanelInterfaceInteraction?) -> Signal<[ContextMenuItem], NoError> {
|
func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, messages: [Message], controllerInteraction: ChatControllerInteraction?, selectAll: Bool, interfaceInteraction: ChatPanelInterfaceInteraction?, readStats: MessageReadStats? = nil) -> Signal<[ContextMenuItem], NoError> {
|
||||||
guard let interfaceInteraction = interfaceInteraction, let controllerInteraction = controllerInteraction else {
|
guard let interfaceInteraction = interfaceInteraction, let controllerInteraction = controllerInteraction else {
|
||||||
return .single([])
|
return .single([])
|
||||||
}
|
}
|
||||||
@ -422,8 +470,6 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
|||||||
canPin = true
|
canPin = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/*case .group:
|
|
||||||
break*/
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
canReply = false
|
canReply = false
|
||||||
@ -463,15 +509,17 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
|||||||
|> map(Optional.init)
|
|> map(Optional.init)
|
||||||
}
|
}
|
||||||
|
|
||||||
let loadLimits = context.account.postbox.transaction { transaction -> LimitsConfiguration in
|
let loadLimits = context.account.postbox.transaction { transaction -> (LimitsConfiguration, AppConfiguration) in
|
||||||
return transaction.getPreferencesEntry(key: PreferencesKeys.limitsConfiguration) as? LimitsConfiguration ?? LimitsConfiguration.defaultValue
|
let limitsConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.limitsConfiguration) as? LimitsConfiguration ?? LimitsConfiguration.defaultValue
|
||||||
|
let appConfig = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration) as? AppConfiguration ?? AppConfiguration.defaultValue
|
||||||
|
return (limitsConfiguration, appConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
let cachedData = context.account.postbox.transaction { transaction -> CachedPeerData? in
|
let cachedData = context.account.postbox.transaction { transaction -> CachedPeerData? in
|
||||||
return transaction.getPeerCachedData(peerId: messages[0].id.peerId)
|
return transaction.getPeerCachedData(peerId: messages[0].id.peerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
let dataSignal: Signal<(MessageContextMenuData, [MessageId: ChatUpdatingMessageMedia], CachedPeerData?), NoError> = combineLatest(
|
let dataSignal: Signal<(MessageContextMenuData, [MessageId: ChatUpdatingMessageMedia], CachedPeerData?, AppConfiguration), NoError> = combineLatest(
|
||||||
loadLimits,
|
loadLimits,
|
||||||
loadStickerSaveStatusSignal,
|
loadStickerSaveStatusSignal,
|
||||||
loadResourceStatusSignal,
|
loadResourceStatusSignal,
|
||||||
@ -480,19 +528,20 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
|||||||
|> take(1),
|
|> take(1),
|
||||||
cachedData
|
cachedData
|
||||||
)
|
)
|
||||||
|> map { limitsConfiguration, stickerSaveStatus, resourceStatus, messageActions, updatingMessageMedia, cachedData -> (MessageContextMenuData, [MessageId: ChatUpdatingMessageMedia], CachedPeerData?) in
|
|> map { limitsAndAppConfig, stickerSaveStatus, resourceStatus, messageActions, updatingMessageMedia, cachedData -> (MessageContextMenuData, [MessageId: ChatUpdatingMessageMedia], CachedPeerData?, AppConfiguration) in
|
||||||
|
let (limitsConfiguration, appConfig) = limitsAndAppConfig
|
||||||
var canEdit = false
|
var canEdit = false
|
||||||
if !isAction {
|
if !isAction {
|
||||||
let message = messages[0]
|
let message = messages[0]
|
||||||
canEdit = canEditMessage(context: context, limitsConfiguration: limitsConfiguration, message: message)
|
canEdit = canEditMessage(context: context, limitsConfiguration: limitsConfiguration, message: message)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (MessageContextMenuData(starStatus: stickerSaveStatus, canReply: canReply, canPin: canPin, canEdit: canEdit, canSelect: canSelect, resourceStatus: resourceStatus, messageActions: messageActions), updatingMessageMedia, cachedData)
|
return (MessageContextMenuData(starStatus: stickerSaveStatus, canReply: canReply, canPin: canPin, canEdit: canEdit, canSelect: canSelect, resourceStatus: resourceStatus, messageActions: messageActions), updatingMessageMedia, cachedData, appConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
return dataSignal
|
return dataSignal
|
||||||
|> deliverOnMainQueue
|
|> deliverOnMainQueue
|
||||||
|> map { data, updatingMessageMedia, cachedData -> [ContextMenuItem] in
|
|> map { data, updatingMessageMedia, cachedData, appConfig -> [ContextMenuItem] in
|
||||||
var actions: [ContextMenuItem] = []
|
var actions: [ContextMenuItem] = []
|
||||||
|
|
||||||
var isPinnedMessages = false
|
var isPinnedMessages = false
|
||||||
@ -950,13 +999,6 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/*if !isReplyThreadHead, !data.messageActions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty && isAction {
|
|
||||||
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in
|
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
|
|
||||||
}, action: { controller, f in
|
|
||||||
interfaceInteraction.deleteMessages(messages, controller, f)
|
|
||||||
})))
|
|
||||||
}*/
|
|
||||||
|
|
||||||
if data.messageActions.options.contains(.viewStickerPack) {
|
if data.messageActions.options.contains(.viewStickerPack) {
|
||||||
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.StickerPack_ViewPack, icon: { theme in
|
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.StickerPack_ViewPack, icon: { theme in
|
||||||
@ -1012,6 +1054,28 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
|||||||
clearCacheAsDelete = true
|
clearCacheAsDelete = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !isPinnedMessages, !isReplyThreadHead, data.canSelect {
|
||||||
|
if !selectAll || messages.count == 1 {
|
||||||
|
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuSelect, icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.actionSheet.primaryTextColor)
|
||||||
|
}, action: { _, f in
|
||||||
|
interfaceInteraction.beginMessageSelection(selectAll ? messages.map { $0.id } : [message.id], { transition in
|
||||||
|
f(.custom(transition))
|
||||||
|
})
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
if messages.count > 1 {
|
||||||
|
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuSelectAll(Int32(messages.count)), icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/SelectAll"), color: theme.actionSheet.primaryTextColor)
|
||||||
|
}, action: { _, f in
|
||||||
|
interfaceInteraction.beginMessageSelection(messages.map { $0.id }, { transition in
|
||||||
|
f(.custom(transition))
|
||||||
|
})
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !isReplyThreadHead, (!data.messageActions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty || clearCacheAsDelete) {
|
if !isReplyThreadHead, (!data.messageActions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty || clearCacheAsDelete) {
|
||||||
var autoremoveDeadline: Int32?
|
var autoremoveDeadline: Int32?
|
||||||
for attribute in message.attributes {
|
for attribute in message.attributes {
|
||||||
@ -1036,7 +1100,12 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
|||||||
} else {
|
} else {
|
||||||
title = chatPresentationInterfaceState.strings.Conversation_ContextMenuDelete
|
title = chatPresentationInterfaceState.strings.Conversation_ContextMenuDelete
|
||||||
}
|
}
|
||||||
|
|
||||||
if let autoremoveDeadline = autoremoveDeadline, !isEditing, !isSending {
|
if let autoremoveDeadline = autoremoveDeadline, !isEditing, !isSending {
|
||||||
|
if !actions.isEmpty {
|
||||||
|
actions.append(.separator)
|
||||||
|
}
|
||||||
|
|
||||||
actions.append(.custom(ChatDeleteMessageContextItem(timestamp: Double(autoremoveDeadline), action: { controller, f in
|
actions.append(.custom(ChatDeleteMessageContextItem(timestamp: Double(autoremoveDeadline), action: { controller, f in
|
||||||
if isEditing {
|
if isEditing {
|
||||||
context.account.pendingUpdateMessageManager.cancel(messageId: message.id)
|
context.account.pendingUpdateMessageManager.cancel(messageId: message.id)
|
||||||
@ -1046,6 +1115,10 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
|||||||
}
|
}
|
||||||
}), false))
|
}), false))
|
||||||
} else if !isUnremovableAction {
|
} else if !isUnremovableAction {
|
||||||
|
if !actions.isEmpty {
|
||||||
|
actions.append(.separator)
|
||||||
|
}
|
||||||
|
|
||||||
actions.append(.action(ContextMenuActionItem(text: title, textColor: .destructive, icon: { theme in
|
actions.append(.action(ContextMenuActionItem(text: title, textColor: .destructive, icon: { theme in
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: isSending ? "Chat/Context Menu/Clear" : "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
|
return generateTintedImage(image: UIImage(bundleImageName: isSending ? "Chat/Context Menu/Clear" : "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
|
||||||
}, action: { controller, f in
|
}, action: { controller, f in
|
||||||
@ -1059,28 +1132,65 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isPinnedMessages, !isReplyThreadHead, data.canSelect {
|
if let peer = message.peers[message.id.peerId], canViewReadStats(message: message, appConfig: appConfig) {
|
||||||
if !actions.isEmpty {
|
var hasReadReports = false
|
||||||
actions.append(.separator)
|
if let channel = peer as? TelegramChannel {
|
||||||
}
|
if case .group = channel.info {
|
||||||
if !selectAll || messages.count == 1 {
|
if let cachedData = cachedData as? CachedChannelData, let memberCount = cachedData.participantsSummary.memberCount, memberCount <= 50 {
|
||||||
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuSelect, icon: { theme in
|
hasReadReports = true
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.actionSheet.primaryTextColor)
|
}
|
||||||
}, action: { _, f in
|
}
|
||||||
interfaceInteraction.beginMessageSelection(selectAll ? messages.map { $0.id } : [message.id], { transition in
|
} else if let group = peer as? TelegramGroup {
|
||||||
f(.custom(transition))
|
if group.participantCount <= 50 {
|
||||||
})
|
hasReadReports = true
|
||||||
})))
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if messages.count > 1 {
|
if hasReadReports {
|
||||||
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuSelectAll(Int32(messages.count)), icon: { theme in
|
if !actions.isEmpty {
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/SelectAll"), color: theme.actionSheet.primaryTextColor)
|
actions.insert(.separator, at: 0)
|
||||||
}, action: { _, f in
|
}
|
||||||
interfaceInteraction.beginMessageSelection(messages.map { $0.id }, { transition in
|
|
||||||
f(.custom(transition))
|
actions.insert(.custom(ChatReadReportContextItem(context: context, message: message, stats: readStats, action: { c, f, stats in
|
||||||
})
|
if stats.peers.count == 1 {
|
||||||
})))
|
c.dismiss(completion: {
|
||||||
|
controllerInteraction.openPeer(stats.peers[0].id, .default, nil)
|
||||||
|
})
|
||||||
|
} else if !stats.peers.isEmpty {
|
||||||
|
var subActions: [ContextMenuItem] = []
|
||||||
|
|
||||||
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
|
||||||
|
subActions.append(.action(ContextMenuActionItem(text: presentationData.strings.Common_Back, textColor: .primary, icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor)
|
||||||
|
}, action: { controller, _ in
|
||||||
|
controller.setItems(contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: chatPresentationInterfaceState, context: context, messages: messages, controllerInteraction: controllerInteraction, selectAll: selectAll, interfaceInteraction: interfaceInteraction, readStats: stats))
|
||||||
|
})))
|
||||||
|
|
||||||
|
let debugRepeatCount: Int
|
||||||
|
#if DEBUG
|
||||||
|
debugRepeatCount = stats.peers.count == 1 ? 1 : 50
|
||||||
|
#else
|
||||||
|
debugRepeatCount = 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
for _ in 0 ..< debugRepeatCount {
|
||||||
|
for peer in stats.peers {
|
||||||
|
let avatarSignal = peerAvatarCompleteImage(account: context.account, peer: peer._asPeer(), size: CGSize(width: 30.0, height: 30.0))
|
||||||
|
|
||||||
|
subActions.append(.action(ContextMenuActionItem(text: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), textLayout: .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: CGSize(width: 30.0, height: 30.0), signal: avatarSignal), action: { _, f in
|
||||||
|
c.dismiss(completion: {
|
||||||
|
controllerInteraction.openPeer(peer.id, .default, nil)
|
||||||
|
})
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.setItems(.single(subActions))
|
||||||
|
} else {
|
||||||
|
f(.default)
|
||||||
|
}
|
||||||
|
}), false), at: 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1394,6 +1504,10 @@ private final class ChatDeleteMessageContextItemNode: ASDisplayNode, ContextMenu
|
|||||||
|
|
||||||
private var pointerInteraction: PointerInteraction?
|
private var pointerInteraction: PointerInteraction?
|
||||||
|
|
||||||
|
var isActionEnabled: Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
init(presentationData: PresentationData, item: ChatDeleteMessageContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) {
|
init(presentationData: PresentationData, item: ChatDeleteMessageContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) {
|
||||||
self.item = item
|
self.item = item
|
||||||
self.presentationData = presentationData
|
self.presentationData = presentationData
|
||||||
@ -1569,6 +1683,279 @@ private final class ChatDeleteMessageContextItemNode: ASDisplayNode, ContextMenu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class ChatReadReportContextItem: ContextMenuCustomItem {
|
||||||
|
fileprivate let context: AccountContext
|
||||||
|
fileprivate let message: Message
|
||||||
|
fileprivate let stats: MessageReadStats?
|
||||||
|
fileprivate let action: (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void, MessageReadStats) -> Void
|
||||||
|
|
||||||
|
init(context: AccountContext, message: Message, stats: MessageReadStats?, action: @escaping (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void, MessageReadStats) -> Void) {
|
||||||
|
self.context = context
|
||||||
|
self.message = message
|
||||||
|
self.stats = stats
|
||||||
|
self.action = action
|
||||||
|
}
|
||||||
|
|
||||||
|
func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode {
|
||||||
|
return ChatReadReportContextItemNode(presentationData: presentationData, item: self, getController: getController, actionSelected: actionSelected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCustomNode, ContextActionNodeProtocol {
|
||||||
|
private let item: ChatReadReportContextItem
|
||||||
|
private var presentationData: PresentationData
|
||||||
|
private let getController: () -> ContextControllerProtocol?
|
||||||
|
private let actionSelected: (ContextMenuActionResult) -> Void
|
||||||
|
|
||||||
|
private let backgroundNode: ASDisplayNode
|
||||||
|
private let highlightedBackgroundNode: ASDisplayNode
|
||||||
|
private let textNode: ImmediateTextNode
|
||||||
|
private let shimmerNode: ShimmerEffectNode
|
||||||
|
private let iconNode: ASImageNode
|
||||||
|
|
||||||
|
private let avatarsNode: AnimatedAvatarSetNode
|
||||||
|
private let avatarsContext: AnimatedAvatarSetContext
|
||||||
|
|
||||||
|
private let placeholderAvatarsNode: AnimatedAvatarSetNode
|
||||||
|
private let placeholderAvatarsContext: AnimatedAvatarSetContext
|
||||||
|
|
||||||
|
private let buttonNode: HighlightTrackingButtonNode
|
||||||
|
|
||||||
|
private var pointerInteraction: PointerInteraction?
|
||||||
|
|
||||||
|
private var disposable: Disposable?
|
||||||
|
private var currentStats: MessageReadStats?
|
||||||
|
|
||||||
|
init(presentationData: PresentationData, item: ChatReadReportContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) {
|
||||||
|
self.item = item
|
||||||
|
self.presentationData = presentationData
|
||||||
|
self.getController = getController
|
||||||
|
self.actionSelected = actionSelected
|
||||||
|
self.currentStats = item.stats
|
||||||
|
|
||||||
|
let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize)
|
||||||
|
|
||||||
|
self.backgroundNode = ASDisplayNode()
|
||||||
|
self.backgroundNode.isAccessibilityElement = false
|
||||||
|
self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor
|
||||||
|
self.highlightedBackgroundNode = ASDisplayNode()
|
||||||
|
self.highlightedBackgroundNode.isAccessibilityElement = false
|
||||||
|
self.highlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor
|
||||||
|
self.highlightedBackgroundNode.alpha = 0.0
|
||||||
|
|
||||||
|
self.textNode = ImmediateTextNode()
|
||||||
|
self.textNode.isAccessibilityElement = false
|
||||||
|
self.textNode.isUserInteractionEnabled = false
|
||||||
|
self.textNode.displaysAsynchronously = false
|
||||||
|
self.textNode.attributedText = NSAttributedString(string: " ", font: textFont, textColor: presentationData.theme.contextMenu.destructiveColor)
|
||||||
|
self.textNode.maximumNumberOfLines = 1
|
||||||
|
self.textNode.alpha = 0.0
|
||||||
|
|
||||||
|
self.shimmerNode = ShimmerEffectNode()
|
||||||
|
self.shimmerNode.clipsToBounds = true
|
||||||
|
|
||||||
|
self.buttonNode = HighlightTrackingButtonNode()
|
||||||
|
self.buttonNode.isAccessibilityElement = true
|
||||||
|
self.buttonNode.accessibilityLabel = presentationData.strings.VoiceChat_StopRecording
|
||||||
|
|
||||||
|
self.iconNode = ASImageNode()
|
||||||
|
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: presentationData.theme.actionSheet.primaryTextColor)
|
||||||
|
|
||||||
|
self.avatarsNode = AnimatedAvatarSetNode()
|
||||||
|
self.avatarsContext = AnimatedAvatarSetContext()
|
||||||
|
|
||||||
|
self.placeholderAvatarsNode = AnimatedAvatarSetNode()
|
||||||
|
self.placeholderAvatarsContext = AnimatedAvatarSetContext()
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.addSubnode(self.backgroundNode)
|
||||||
|
self.addSubnode(self.highlightedBackgroundNode)
|
||||||
|
self.addSubnode(self.shimmerNode)
|
||||||
|
self.addSubnode(self.textNode)
|
||||||
|
self.addSubnode(self.iconNode)
|
||||||
|
self.addSubnode(self.avatarsNode)
|
||||||
|
self.addSubnode(self.placeholderAvatarsNode)
|
||||||
|
self.addSubnode(self.buttonNode)
|
||||||
|
|
||||||
|
self.buttonNode.highligthedChanged = { [weak self] highligted in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if highligted {
|
||||||
|
strongSelf.highlightedBackgroundNode.alpha = 1.0
|
||||||
|
} else {
|
||||||
|
strongSelf.highlightedBackgroundNode.alpha = 0.0
|
||||||
|
strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
||||||
|
|
||||||
|
if let currentStats = self.currentStats {
|
||||||
|
self.buttonNode.isUserInteractionEnabled = !currentStats.peers.isEmpty
|
||||||
|
} else {
|
||||||
|
self.buttonNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
self.disposable = (item.context.engine.messages.messageReadStats(id: item.message.id)
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let value = value {
|
||||||
|
strongSelf.updateStats(stats: value, transition: .animated(duration: 0.2, curve: .easeInOut))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.disposable?.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didLoad() {
|
||||||
|
super.didLoad()
|
||||||
|
|
||||||
|
self.pointerInteraction = PointerInteraction(node: self.buttonNode, style: .hover, willEnter: { [weak self] in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.highlightedBackgroundNode.alpha = 0.75
|
||||||
|
}
|
||||||
|
}, willExit: { [weak self] in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.highlightedBackgroundNode.alpha = 0.0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private var validLayout: (calculatedWidth: CGFloat, size: CGSize)?
|
||||||
|
|
||||||
|
func updateStats(stats: MessageReadStats, transition: ContainedViewLayoutTransition) {
|
||||||
|
self.buttonNode.isUserInteractionEnabled = !stats.peers.isEmpty
|
||||||
|
|
||||||
|
guard let (calculatedWidth, size) = self.validLayout else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.currentStats = stats
|
||||||
|
|
||||||
|
let (_, apply) = self.updateLayout(constrainedWidth: calculatedWidth)
|
||||||
|
apply(size, transition)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateLayout(constrainedWidth: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) {
|
||||||
|
let sideInset: CGFloat = 14.0
|
||||||
|
let verticalInset: CGFloat = 12.0
|
||||||
|
|
||||||
|
let iconSize: CGSize = self.iconNode.image?.size ?? CGSize(width: 10.0, height: 10.0)
|
||||||
|
|
||||||
|
let rightTextInset: CGFloat = sideInset + 36.0
|
||||||
|
|
||||||
|
let calculatedWidth = min(constrainedWidth, 260.0)
|
||||||
|
|
||||||
|
let textFont = Font.regular(self.presentationData.listsFontSize.baseDisplaySize)
|
||||||
|
|
||||||
|
if let currentStats = self.currentStats {
|
||||||
|
if currentStats.peers.isEmpty {
|
||||||
|
//TODO:localize
|
||||||
|
self.textNode.attributedText = NSAttributedString(string: "No Views", font: textFont, textColor: self.presentationData.theme.contextMenu.secondaryColor)
|
||||||
|
} else if currentStats.peers.count == 1 {
|
||||||
|
self.textNode.attributedText = NSAttributedString(string: currentStats.peers[0].displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), font: textFont, textColor: self.presentationData.theme.contextMenu.primaryColor)
|
||||||
|
} else {
|
||||||
|
self.textNode.attributedText = NSAttributedString(string: self.presentationData.strings.Conversation_ContextMenuSeen(Int32(currentStats.peers.count)), font: textFont, textColor: self.presentationData.theme.contextMenu.primaryColor)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.textNode.attributedText = NSAttributedString(string: " ", font: textFont, textColor: self.presentationData.theme.contextMenu.primaryColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
let textSize = self.textNode.updateLayout(CGSize(width: calculatedWidth - sideInset - rightTextInset - iconSize.width - 4.0, height: .greatestFiniteMagnitude))
|
||||||
|
|
||||||
|
let combinedTextHeight = textSize.height
|
||||||
|
return (CGSize(width: calculatedWidth, height: verticalInset * 2.0 + combinedTextHeight), { size, transition in
|
||||||
|
self.validLayout = (calculatedWidth: calculatedWidth, size: size)
|
||||||
|
let verticalOrigin = floor((size.height - combinedTextHeight) / 2.0)
|
||||||
|
let textFrame = CGRect(origin: CGPoint(x: sideInset + iconSize.width + 4.0, y: verticalOrigin), size: textSize)
|
||||||
|
transition.updateFrameAdditive(node: self.textNode, frame: textFrame)
|
||||||
|
transition.updateAlpha(node: self.textNode, alpha: self.currentStats == nil ? 0.0 : 1.0)
|
||||||
|
|
||||||
|
let shimmerHeight: CGFloat = 8.0
|
||||||
|
|
||||||
|
self.shimmerNode.frame = CGRect(origin: CGPoint(x: textFrame.minX, y: floor((size.height - shimmerHeight) / 2.0)), size: CGSize(width: min(100.0, size.width - 40.0), height: shimmerHeight))
|
||||||
|
self.shimmerNode.cornerRadius = shimmerHeight / 2.0
|
||||||
|
let shimmeringForegroundColor = self.presentationData.theme.contextMenu.itemSeparatorColor.blitOver(self.presentationData.theme.list.plainBackgroundColor, alpha: 0.9)
|
||||||
|
let shimmeringColor = self.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.2)
|
||||||
|
self.shimmerNode.update(backgroundColor: self.presentationData.theme.list.plainBackgroundColor, foregroundColor: shimmeringForegroundColor, shimmeringColor: shimmeringColor, shapes: [.rect(rect: self.shimmerNode.bounds)], horizontal: true, size: self.shimmerNode.bounds.size)
|
||||||
|
self.shimmerNode.updateAbsoluteRect(self.shimmerNode.frame, within: size)
|
||||||
|
transition.updateAlpha(node: self.shimmerNode, alpha: self.currentStats == nil ? 1.0 : 0.0)
|
||||||
|
|
||||||
|
if !iconSize.width.isZero {
|
||||||
|
transition.updateFrameAdditive(node: self.iconNode, frame: CGRect(origin: CGPoint(x: sideInset - 2.0, y: floor((size.height - iconSize.height) / 2.0)), size: iconSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
let avatarsContent: AnimatedAvatarSetContext.Content
|
||||||
|
let placeholderAvatarsContent: AnimatedAvatarSetContext.Content
|
||||||
|
|
||||||
|
var avatarsPeers: [EnginePeer] = []
|
||||||
|
if let peers = self.currentStats?.peers {
|
||||||
|
for i in 0 ..< min(3, peers.count) {
|
||||||
|
avatarsPeers.append(peers[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
avatarsContent = self.avatarsContext.update(peers: avatarsPeers, animated: false)
|
||||||
|
placeholderAvatarsContent = self.avatarsContext.updatePlaceholder(color: shimmeringForegroundColor, count: 3, animated: false)
|
||||||
|
|
||||||
|
let avatarsSize = self.avatarsNode.update(context: self.item.context, content: avatarsContent, itemSize: CGSize(width: 24.0, height: 24.0), customSpacing: 10.0, animated: false, synchronousLoad: true)
|
||||||
|
self.avatarsNode.frame = CGRect(origin: CGPoint(x: size.width - sideInset - 8.0 - avatarsSize.width, y: floor((size.height - avatarsSize.height) / 2.0)), size: avatarsSize)
|
||||||
|
transition.updateAlpha(node: self.avatarsNode, alpha: self.currentStats == nil ? 0.0 : 1.0)
|
||||||
|
|
||||||
|
let placeholderAvatarsSize = self.placeholderAvatarsNode.update(context: self.item.context, content: placeholderAvatarsContent, itemSize: CGSize(width: 24.0, height: 24.0), customSpacing: 10.0, animated: false, synchronousLoad: true)
|
||||||
|
self.placeholderAvatarsNode.frame = CGRect(origin: CGPoint(x: size.width - sideInset - 8.0 - placeholderAvatarsSize.width, y: floor((size.height - placeholderAvatarsSize.height) / 2.0)), size: placeholderAvatarsSize)
|
||||||
|
transition.updateAlpha(node: self.placeholderAvatarsNode, alpha: self.currentStats == nil ? 1.0 : 0.0)
|
||||||
|
|
||||||
|
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
|
||||||
|
transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
|
||||||
|
transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateTheme(presentationData: PresentationData) {
|
||||||
|
self.presentationData = presentationData
|
||||||
|
|
||||||
|
self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor
|
||||||
|
self.highlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor
|
||||||
|
|
||||||
|
let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize)
|
||||||
|
|
||||||
|
self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: textFont, textColor: presentationData.theme.contextMenu.primaryColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func buttonPressed() {
|
||||||
|
self.performAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
func performAction() {
|
||||||
|
guard let controller = self.getController(), let currentStats = self.currentStats else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.item.action(controller, { [weak self] result in
|
||||||
|
self?.actionSelected(result)
|
||||||
|
}, currentStats)
|
||||||
|
}
|
||||||
|
|
||||||
|
var isActionEnabled: Bool {
|
||||||
|
guard let currentStats = self.currentStats else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !currentStats.peers.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
func setIsHighlighted(_ value: Bool) {
|
||||||
|
if value {
|
||||||
|
self.highlightedBackgroundNode.alpha = 1.0
|
||||||
|
} else {
|
||||||
|
self.highlightedBackgroundNode.alpha = 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func stringForRemainingTime(_ duration: Int32, strings: PresentationStrings) -> String {
|
private func stringForRemainingTime(_ duration: Int32, strings: PresentationStrings) -> String {
|
||||||
let days = duration / (3600 * 24)
|
let days = duration / (3600 * 24)
|
||||||
let hours = duration / 3600
|
let hours = duration / 3600
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user