mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-08 19:10:53 +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";
|
||||
|
||||
"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 Content {
|
||||
fileprivate final class Item {
|
||||
fileprivate struct Key: Hashable {
|
||||
var peerId: EnginePeer.Id
|
||||
fileprivate enum Key: Hashable {
|
||||
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.placeholderColor = placeholderColor
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,7 +49,15 @@ public final class AnimatedAvatarSetContext {
|
||||
public func update(peers: [EnginePeer], animated: Bool) -> Content {
|
||||
var items: [(Content.Item.Key, Content.Item)] = []
|
||||
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)
|
||||
}
|
||||
@ -59,10 +70,16 @@ private final class ContentNode: ASDisplayNode {
|
||||
private var audioLevelBlobOverlay: UIImageView?
|
||||
private let unclippedNode: ASImageNode
|
||||
private let clippedNode: ASImageNode
|
||||
|
||||
private var size: CGSize
|
||||
private var spacing: CGFloat
|
||||
|
||||
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.clippedNode = ASImageNode()
|
||||
|
||||
@ -70,38 +87,47 @@ private final class ContentNode: ASDisplayNode {
|
||||
|
||||
self.addSubnode(self.unclippedNode)
|
||||
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) {
|
||||
let image = generateImage(CGSize(width: 30.0, height: 30.0), rotatedContext: { size, context in
|
||||
|
||||
if let peer = peer {
|
||||
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.setFillColor(UIColor.lightGray.cgColor)
|
||||
context.setFillColor(placeholderColor.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
})!
|
||||
self.updateImage(image: image)
|
||||
|
||||
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)
|
||||
self.updateImage(image: image, size: size, spacing: spacing)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateImage(image: UIImage) {
|
||||
private func updateImage(image: UIImage, size: CGSize, spacing: CGFloat) {
|
||||
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.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
||||
context.scaleBy(x: 1.0, y: -1.0)
|
||||
@ -113,7 +139,7 @@ private final class ContentNode: ASDisplayNode {
|
||||
|
||||
context.setBlendMode(.copy)
|
||||
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,9 +218,16 @@ public final class AnimatedAvatarSetNode: ASDisplayNode {
|
||||
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
|
||||
let contentHeight: CGFloat = itemSize.height
|
||||
|
||||
let spacing: CGFloat
|
||||
if let customSpacing = customSpacing {
|
||||
spacing = customSpacing
|
||||
} else {
|
||||
spacing = 10.0
|
||||
}
|
||||
|
||||
let transition: ContainedViewLayoutTransition
|
||||
if animated {
|
||||
@ -218,7 +251,7 @@ public final class AnimatedAvatarSetNode: ASDisplayNode {
|
||||
itemNode.updateLayout(size: itemSize, isClipped: index != 0, animated: animated)
|
||||
transition.updateFrame(node: itemNode, frame: itemFrame)
|
||||
} 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.contentNodes[key] = itemNode
|
||||
itemNode.updateLayout(size: itemSize, isClipped: index != 0, animated: false)
|
||||
@ -229,7 +262,7 @@ public final class AnimatedAvatarSetNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
itemNode.zPosition = CGFloat(100 - i)
|
||||
contentWidth += itemSize.width - 10.0
|
||||
contentWidth += itemSize.width - spacing
|
||||
index += 1
|
||||
}
|
||||
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]) {
|
||||
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)
|
||||
} else {
|
||||
itemNode.updateAudioLevel(color: color, backgroundColor: backgroundColor, value: 0.0)
|
||||
|
@ -13,6 +13,7 @@ enum ContextActionSibling {
|
||||
public protocol ContextActionNodeProtocol: ASDisplayNode {
|
||||
func setIsHighlighted(_ value: Bool)
|
||||
func performAction()
|
||||
var isActionEnabled: Bool { get }
|
||||
}
|
||||
|
||||
final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol {
|
||||
@ -32,6 +33,10 @@ final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol {
|
||||
private var iconDisposable: Disposable?
|
||||
|
||||
private var pointerInteraction: PointerInteraction?
|
||||
|
||||
var isActionEnabled: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
init(presentationData: PresentationData, action: ContextMenuActionItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) {
|
||||
self.action = action
|
||||
|
@ -139,7 +139,10 @@ private final class InnerActionsContainerNode: ASDisplayNode {
|
||||
guard let strongSelf = self else {
|
||||
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 != nil, moved {
|
||||
strongSelf.feedbackTap()
|
||||
|
@ -368,7 +368,11 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
}
|
||||
if strongSelf.didMoveFromInitialGesturePoint {
|
||||
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 {
|
||||
strongSelf.highlightedActionNode?.setIsHighlighted(false)
|
||||
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 {
|
||||
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 TelegramStringFormatting
|
||||
import UndoUI
|
||||
import ShimmerEffect
|
||||
import AnimatedAvatarSetNode
|
||||
import AvatarNode
|
||||
|
||||
private struct MessageContextMenuData {
|
||||
let starStatus: Bool?
|
||||
@ -136,6 +139,51 @@ private func canEditMessage(accountPeerId: PeerId, limitsConfiguration: LimitsCo
|
||||
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 {
|
||||
guard let peer = chatPresentationInterfaceState.renderedPeer?.peer else {
|
||||
return false
|
||||
@ -284,7 +332,7 @@ func updatedChatEditInterfaceMessageState(state: ChatPresentationInterfaceState,
|
||||
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 {
|
||||
return .single([])
|
||||
}
|
||||
@ -422,8 +470,6 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
canPin = true
|
||||
}
|
||||
}
|
||||
/*case .group:
|
||||
break*/
|
||||
}
|
||||
} else {
|
||||
canReply = false
|
||||
@ -463,15 +509,17 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
|> map(Optional.init)
|
||||
}
|
||||
|
||||
let loadLimits = context.account.postbox.transaction { transaction -> LimitsConfiguration in
|
||||
return transaction.getPreferencesEntry(key: PreferencesKeys.limitsConfiguration) as? LimitsConfiguration ?? LimitsConfiguration.defaultValue
|
||||
let loadLimits = context.account.postbox.transaction { transaction -> (LimitsConfiguration, AppConfiguration) in
|
||||
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
|
||||
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,
|
||||
loadStickerSaveStatusSignal,
|
||||
loadResourceStatusSignal,
|
||||
@ -480,19 +528,20 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
|> take(1),
|
||||
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
|
||||
if !isAction {
|
||||
let message = messages[0]
|
||||
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
|
||||
|> deliverOnMainQueue
|
||||
|> map { data, updatingMessageMedia, cachedData -> [ContextMenuItem] in
|
||||
|> map { data, updatingMessageMedia, cachedData, appConfig -> [ContextMenuItem] in
|
||||
var actions: [ContextMenuItem] = []
|
||||
|
||||
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) {
|
||||
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.StickerPack_ViewPack, icon: { theme in
|
||||
@ -1012,6 +1054,28 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
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) {
|
||||
var autoremoveDeadline: Int32?
|
||||
for attribute in message.attributes {
|
||||
@ -1036,7 +1100,12 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
} else {
|
||||
title = chatPresentationInterfaceState.strings.Conversation_ContextMenuDelete
|
||||
}
|
||||
|
||||
if let autoremoveDeadline = autoremoveDeadline, !isEditing, !isSending {
|
||||
if !actions.isEmpty {
|
||||
actions.append(.separator)
|
||||
}
|
||||
|
||||
actions.append(.custom(ChatDeleteMessageContextItem(timestamp: Double(autoremoveDeadline), action: { controller, f in
|
||||
if isEditing {
|
||||
context.account.pendingUpdateMessageManager.cancel(messageId: message.id)
|
||||
@ -1046,6 +1115,10 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
}
|
||||
}), false))
|
||||
} else if !isUnremovableAction {
|
||||
if !actions.isEmpty {
|
||||
actions.append(.separator)
|
||||
}
|
||||
|
||||
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)
|
||||
}, action: { controller, f in
|
||||
@ -1058,29 +1131,66 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
if !isPinnedMessages, !isReplyThreadHead, data.canSelect {
|
||||
if !actions.isEmpty {
|
||||
actions.append(.separator)
|
||||
|
||||
if let peer = message.peers[message.id.peerId], canViewReadStats(message: message, appConfig: appConfig) {
|
||||
var hasReadReports = false
|
||||
if let channel = peer as? TelegramChannel {
|
||||
if case .group = channel.info {
|
||||
if let cachedData = cachedData as? CachedChannelData, let memberCount = cachedData.participantsSummary.memberCount, memberCount <= 50 {
|
||||
hasReadReports = true
|
||||
}
|
||||
}
|
||||
} else if let group = peer as? TelegramGroup {
|
||||
if group.participantCount <= 50 {
|
||||
hasReadReports = true
|
||||
}
|
||||
}
|
||||
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 hasReadReports {
|
||||
if !actions.isEmpty {
|
||||
actions.insert(.separator, at: 0)
|
||||
}
|
||||
|
||||
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?
|
||||
|
||||
var isActionEnabled: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
init(presentationData: PresentationData, item: ChatDeleteMessageContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) {
|
||||
self.item = item
|
||||
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 {
|
||||
let days = duration / (3600 * 24)
|
||||
let hours = duration / 3600
|
||||
|
Loading…
x
Reference in New Issue
Block a user