mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-16 03:09:56 +00:00
Channel recommendation
This commit is contained in:
parent
65ffd81904
commit
a5ab3697ae
@ -10465,5 +10465,13 @@ Sorry for the inconvenience.";
|
||||
|
||||
"Channel.Info.Stats" = "Statistics and Boosts";
|
||||
|
||||
"Conversation.FreeTranscriptionLimitTooltip_1" = "You have **%@** free voice transcription left this month.";
|
||||
"Conversation.FreeTranscriptionLimitTooltip_any" = "You have **%@** free voice transcriptions left this month.";
|
||||
|
||||
"Notification.GiveawayResults_1" = "%@ winner of the giveaway was randomly selected by Telegram and received private message with giftcode.";
|
||||
"Notification.GiveawayResults_any" = "%@ winners of the giveaway were randomly selected by Telegram and received private messages with giftcodes.";
|
||||
|
||||
"Chat.Giveaway.DeleteConfirmation.Title" = "Do you want to delete the Giveaway Announcement?";
|
||||
"Chat.Giveaway.DeleteConfirmation.Text" = "Deleting this message won't cancel the giveaway - the winners will still be selected on **%@**.\n\nOnce deleted, the Giveaway Announcement cannot be recovered.";
|
||||
|
||||
"Chat.SimilarChannels" = "Similar Channels";
|
||||
|
||||
@ -50,8 +50,33 @@ public final class ChatMessageItemAssociatedData: Equatable {
|
||||
public let hasBots: Bool
|
||||
public let translateToLanguage: String?
|
||||
public let maxReadStoryId: Int32?
|
||||
public let recommendedChannels: RecommendedChannels?
|
||||
|
||||
public init(automaticDownloadPeerType: MediaAutoDownloadPeerType, automaticDownloadPeerId: EnginePeer.Id?, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, isRecentActions: Bool = false, subject: ChatControllerSubject? = nil, contactsPeerIds: Set<EnginePeer.Id> = Set(), channelDiscussionGroup: ChannelDiscussionGroupStatus = .unknown, animatedEmojiStickers: [String: [StickerPackItem]] = [:], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]] = [:], forcedResourceStatus: FileMediaResourceStatus? = nil, currentlyPlayingMessageId: EngineMessage.Index? = nil, isCopyProtectionEnabled: Bool = false, availableReactions: AvailableReactions?, defaultReaction: MessageReaction.Reaction?, isPremium: Bool, accountPeer: EnginePeer?, forceInlineReactions: Bool = false, alwaysDisplayTranscribeButton: DisplayTranscribeButton = DisplayTranscribeButton(canBeDisplayed: false, displayForNotConsumed: false), topicAuthorId: EnginePeer.Id? = nil, hasBots: Bool = false, translateToLanguage: String? = nil, maxReadStoryId: Int32? = nil) {
|
||||
public init(
|
||||
automaticDownloadPeerType: MediaAutoDownloadPeerType,
|
||||
automaticDownloadPeerId: EnginePeer.Id?,
|
||||
automaticDownloadNetworkType: MediaAutoDownloadNetworkType,
|
||||
isRecentActions: Bool = false,
|
||||
subject: ChatControllerSubject? = nil,
|
||||
contactsPeerIds: Set<EnginePeer.Id> = Set(),
|
||||
channelDiscussionGroup: ChannelDiscussionGroupStatus = .unknown,
|
||||
animatedEmojiStickers: [String: [StickerPackItem]] = [:],
|
||||
additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]] = [:],
|
||||
forcedResourceStatus: FileMediaResourceStatus? = nil,
|
||||
currentlyPlayingMessageId: EngineMessage.Index? = nil,
|
||||
isCopyProtectionEnabled: Bool = false,
|
||||
availableReactions: AvailableReactions?,
|
||||
defaultReaction: MessageReaction.Reaction?,
|
||||
isPremium: Bool,
|
||||
accountPeer: EnginePeer?,
|
||||
forceInlineReactions: Bool = false,
|
||||
alwaysDisplayTranscribeButton: DisplayTranscribeButton = DisplayTranscribeButton(canBeDisplayed: false, displayForNotConsumed: false),
|
||||
topicAuthorId: EnginePeer.Id? = nil,
|
||||
hasBots: Bool = false,
|
||||
translateToLanguage: String? = nil,
|
||||
maxReadStoryId: Int32? = nil,
|
||||
recommendedChannels: RecommendedChannels? = nil
|
||||
) {
|
||||
self.automaticDownloadPeerType = automaticDownloadPeerType
|
||||
self.automaticDownloadPeerId = automaticDownloadPeerId
|
||||
self.automaticDownloadNetworkType = automaticDownloadNetworkType
|
||||
@ -74,6 +99,7 @@ public final class ChatMessageItemAssociatedData: Equatable {
|
||||
self.hasBots = hasBots
|
||||
self.translateToLanguage = translateToLanguage
|
||||
self.maxReadStoryId = maxReadStoryId
|
||||
self.recommendedChannels = recommendedChannels
|
||||
}
|
||||
|
||||
public static func == (lhs: ChatMessageItemAssociatedData, rhs: ChatMessageItemAssociatedData) -> Bool {
|
||||
@ -140,6 +166,9 @@ public final class ChatMessageItemAssociatedData: Equatable {
|
||||
if lhs.maxReadStoryId != rhs.maxReadStoryId {
|
||||
return false
|
||||
}
|
||||
if lhs.recommendedChannels != rhs.recommendedChannels {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@ -472,7 +472,6 @@ private class MessageBackgroundNode: ASDisplayNode {
|
||||
private var absoluteRect: (CGRect, CGSize)?
|
||||
|
||||
func update(size: CGSize, theme: PresentationTheme, wallpaper: TelegramWallpaper, graphics: PrincipalThemeEssentialGraphics, wallpaperBackgroundNode: WallpaperBackgroundNode, transition: ContainedViewLayoutTransition) {
|
||||
|
||||
self.backgroundNode.setType(type: .outgoing(.Extracted), highlighted: false, graphics: graphics, maskMode: false, hasWallpaper: wallpaper.hasWallpaper, transition: transition, backgroundNode: wallpaperBackgroundNode)
|
||||
self.backgroundWallpaperNode.setType(type: .outgoing(.Extracted), theme: ChatPresentationThemeData(theme: theme, wallpaper: wallpaper), essentialGraphics: graphics, maskMode: true, backgroundNode: wallpaperBackgroundNode)
|
||||
self.shadowNode.setType(type: .outgoing(.Extracted), hasWallpaper: wallpaper.hasWallpaper, graphics: graphics)
|
||||
|
||||
@ -2473,11 +2473,11 @@ public extension Api.functions.channels {
|
||||
}
|
||||
}
|
||||
public extension Api.functions.channels {
|
||||
static func getChannelRecommendations(channelId: Api.InputChannel) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.Chats>) {
|
||||
static func getChannelRecommendations(channel: Api.InputChannel) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.Chats>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(-873707987)
|
||||
channelId.serialize(buffer, true)
|
||||
return (FunctionDescription(name: "channels.getChannelRecommendations", parameters: [("channelId", String(describing: channelId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.Chats? in
|
||||
buffer.appendInt32(-2085155433)
|
||||
channel.serialize(buffer, true)
|
||||
return (FunctionDescription(name: "channels.getChannelRecommendations", parameters: [("channel", String(describing: channel))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.Chats? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.messages.Chats?
|
||||
if let signature = reader.readInt32() {
|
||||
|
||||
@ -49,7 +49,7 @@ func _internal_requestRecommendedChannels(account: Account, peerId: EnginePeer.I
|
||||
guard let inputChannel = channel.flatMap(apiInputChannel) else {
|
||||
return .complete()
|
||||
}
|
||||
return account.network.request(Api.functions.channels.getChannelRecommendations(channelId: inputChannel))
|
||||
return account.network.request(Api.functions.channels.getChannelRecommendations(channel: inputChannel))
|
||||
|> retryRequest
|
||||
|> mapToSignal { result -> Signal<Never, NoError> in
|
||||
return account.postbox.transaction { transaction -> [EnginePeer] in
|
||||
@ -90,8 +90,8 @@ func _internal_requestRecommendedChannels(account: Account, peerId: EnginePeer.I
|
||||
}
|
||||
}
|
||||
|
||||
public struct RecommendedChannels {
|
||||
public struct Channel {
|
||||
public struct RecommendedChannels: Equatable {
|
||||
public struct Channel: Equatable {
|
||||
public let peer: EnginePeer
|
||||
public let subscribers: Int32
|
||||
}
|
||||
|
||||
@ -79,6 +79,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -69,6 +69,7 @@ import ChatMessageUnsupportedBubbleContentNode
|
||||
import ChatMessageWallpaperBubbleContentNode
|
||||
import ChatMessageGiftBubbleContentNode
|
||||
import ChatMessageGiveawayBubbleContentNode
|
||||
import ChatMessageJoinedChannelBubbleContentNode
|
||||
|
||||
private struct BubbleItemAttributes {
|
||||
var isAttachment: Bool
|
||||
@ -183,6 +184,8 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
|
||||
result.append((message, ChatMessageWallpaperBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
|
||||
} else if case .giftCode = action.action {
|
||||
result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
|
||||
} else if case .joinedChannel = action.action {
|
||||
result.append((message, ChatMessageJoinedChannelBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
|
||||
} else {
|
||||
result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
|
||||
}
|
||||
@ -1555,6 +1558,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
var hasInstantVideo = false
|
||||
for contentNodeItemValue in contentNodeMessagesAndClasses {
|
||||
let contentNodeItem = contentNodeItemValue as (message: Message, type: AnyClass, attributes: ChatMessageEntryAttributes, bubbleAttributes: BubbleItemAttributes)
|
||||
if contentNodeItem.type == ChatMessageJoinedChannelBubbleContentNode.self {
|
||||
maximumContentWidth = baseWidth
|
||||
break
|
||||
}
|
||||
if contentNodeItem.type == ChatMessageGiveawayBubbleContentNode.self {
|
||||
maximumContentWidth = min(305.0, maximumContentWidth)
|
||||
break
|
||||
@ -3939,17 +3946,27 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
strongSelf.mainContextSourceNode.layoutUpdated?(strongSelf.mainContextSourceNode.bounds.size, animation)
|
||||
}
|
||||
|
||||
var hasMenuGesture = true
|
||||
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject {
|
||||
if case .link = info {
|
||||
} else {
|
||||
strongSelf.tapRecognizer?.isEnabled = false
|
||||
}
|
||||
strongSelf.replyRecognizer?.isEnabled = false
|
||||
strongSelf.mainContainerNode.isGestureEnabled = false
|
||||
for contentContainer in strongSelf.contentContainers {
|
||||
contentContainer.containerNode.isGestureEnabled = false
|
||||
hasMenuGesture = false
|
||||
}
|
||||
for media in item.message.media {
|
||||
if let action = media as? TelegramMediaAction {
|
||||
if case .joinedChannel = action.action {
|
||||
hasMenuGesture = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
strongSelf.mainContainerNode.isGestureEnabled = hasMenuGesture
|
||||
for contentContainer in strongSelf.contentContainers {
|
||||
contentContainer.containerNode.isGestureEnabled = hasMenuGesture
|
||||
}
|
||||
|
||||
strongSelf.updateSearchTextHighlightState()
|
||||
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ChatMessageJoinedChannelBubbleContentNode",
|
||||
module_name = "ChatMessageJoinedChannelBubbleContentNode",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/TelegramUIPreferences",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/LocalizedPeerData",
|
||||
"//submodules/UrlEscaping",
|
||||
"//submodules/TelegramStringFormatting",
|
||||
"//submodules/WallpaperBackgroundNode",
|
||||
"//submodules/TelegramUI/Components/ChatControllerInteraction",
|
||||
"//submodules/ShimmerEffect",
|
||||
"//submodules/Markdown",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
|
||||
"//submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/ChatMessageBackground",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
@ -0,0 +1,858 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import TextFormat
|
||||
import LocalizedPeerData
|
||||
import UrlEscaping
|
||||
import TelegramStringFormatting
|
||||
import WallpaperBackgroundNode
|
||||
import ReactionSelectionNode
|
||||
import ChatControllerInteraction
|
||||
import ShimmerEffect
|
||||
import Markdown
|
||||
import ChatMessageBubbleContentNode
|
||||
import ChatMessageItemCommon
|
||||
import RoundedRectWithTailPath
|
||||
import AvatarNode
|
||||
import MultilineTextComponent
|
||||
import ChatMessageBackground
|
||||
|
||||
private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id) -> NSAttributedString? {
|
||||
return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: false, forForumOverview: false)
|
||||
}
|
||||
|
||||
private func generateCloseButtonImage(color: UIColor) -> UIImage? {
|
||||
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setAlpha(color.alpha)
|
||||
context.setBlendMode(.copy)
|
||||
|
||||
context.setLineWidth(2.0)
|
||||
context.setLineCap(.round)
|
||||
context.setStrokeColor(color.withAlphaComponent(1.0).cgColor)
|
||||
|
||||
context.move(to: CGPoint(x: 10.0, y: 10.0))
|
||||
context.addLine(to: CGPoint(x: 20.0, y: 20.0))
|
||||
context.strokePath()
|
||||
|
||||
context.move(to: CGPoint(x: 20.0, y: 10.0))
|
||||
context.addLine(to: CGPoint(x: 10.0, y: 20.0))
|
||||
context.strokePath()
|
||||
})
|
||||
}
|
||||
|
||||
public class ChatMessageJoinedChannelBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
private let labelNode: TextNode
|
||||
private var backgroundNode: WallpaperBubbleBackgroundNode?
|
||||
private let backgroundMaskNode: ASImageNode
|
||||
private var linkHighlightingNode: LinkHighlightingNode?
|
||||
|
||||
private let panelNode: ASDisplayNode
|
||||
private let panelBackgroundNode: MessageBackgroundNode
|
||||
private let titleNode: TextNode
|
||||
private let closeButtonNode: HighlightTrackingButtonNode
|
||||
private let closeIconNode: ASImageNode
|
||||
private let panelListView = ComponentView<Empty>()
|
||||
|
||||
private var cachedMaskBackgroundImage: (CGPoint, UIImage, [CGRect])?
|
||||
private var absoluteRect: (CGRect, CGSize)?
|
||||
|
||||
private var currentMaskSize: CGSize?
|
||||
private var panelMaskLayer: CAShapeLayer?
|
||||
|
||||
private var isExpanded: Bool?
|
||||
|
||||
required public init() {
|
||||
self.labelNode = TextNode()
|
||||
self.labelNode.isUserInteractionEnabled = false
|
||||
self.labelNode.displaysAsynchronously = false
|
||||
|
||||
self.backgroundMaskNode = ASImageNode()
|
||||
|
||||
self.panelNode = ASDisplayNode()
|
||||
self.panelBackgroundNode = MessageBackgroundNode()
|
||||
|
||||
self.titleNode = TextNode()
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
self.titleNode.displaysAsynchronously = false
|
||||
|
||||
self.closeButtonNode = HighlightTrackingButtonNode()
|
||||
|
||||
self.closeIconNode = ASImageNode()
|
||||
self.closeIconNode.displaysAsynchronously = false
|
||||
self.closeIconNode.isUserInteractionEnabled = false
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.labelNode)
|
||||
|
||||
self.panelNode.anchorPoint = CGPoint(x: 0.5, y: -0.1)
|
||||
|
||||
self.addSubnode(self.panelNode)
|
||||
self.panelNode.addSubnode(self.panelBackgroundNode)
|
||||
self.panelNode.addSubnode(self.titleNode)
|
||||
|
||||
self.panelNode.addSubnode(self.closeIconNode)
|
||||
self.panelNode.addSubnode(self.closeButtonNode)
|
||||
|
||||
self.closeButtonNode.highligthedChanged = { [weak self] highlighted in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if highlighted {
|
||||
self.closeIconNode.layer.removeAnimation(forKey: "opacity")
|
||||
self.closeIconNode.alpha = 0.4
|
||||
} else {
|
||||
self.closeIconNode.alpha = 1.0
|
||||
self.closeIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
self.closeButtonNode.addTarget(self, action: #selector(self.closeButtonPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.panelMaskLayer = CAShapeLayer()
|
||||
}
|
||||
|
||||
@objc private func pressed() {
|
||||
guard let item = self.item, let recommendedChannels = item.associatedData.recommendedChannels else {
|
||||
return
|
||||
}
|
||||
let _ = item.context.engine.peers.toggleRecommendedChannelsHidden(peerId: item.message.id.peerId, hidden: !recommendedChannels.isHidden).startStandalone()
|
||||
}
|
||||
|
||||
@objc private func closeButtonPressed() {
|
||||
guard let item = self.item else {
|
||||
return
|
||||
}
|
||||
let _ = item.context.engine.peers.toggleRecommendedChannelsHidden(peerId: item.message.id.peerId, hidden: true).startStandalone()
|
||||
}
|
||||
|
||||
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
|
||||
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
|
||||
let cachedMaskBackgroundImage = self.cachedMaskBackgroundImage
|
||||
|
||||
return { item, layoutConstants, _, _, constrainedSize, _ in
|
||||
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center)
|
||||
|
||||
let unboundWidth: CGFloat = constrainedSize.width - 10.0 * 2.0
|
||||
return (contentProperties, nil, unboundWidth, { constrainedSize, position in
|
||||
let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, message: EngineMessage(item.message), accountPeerId: item.context.account.peerId)
|
||||
|
||||
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Chat_SimilarChannels, font: Font.semibold(15.0), textColor: item.presentationData.theme.theme.chat.message.incoming.primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
var labelRects = labelLayout.linesRects()
|
||||
if labelRects.count > 1 {
|
||||
let sortedIndices = (0 ..< labelRects.count).sorted(by: { labelRects[$0].width > labelRects[$1].width })
|
||||
for i in 0 ..< sortedIndices.count {
|
||||
let index = sortedIndices[i]
|
||||
for j in -1 ... 1 {
|
||||
if j != 0 && index + j >= 0 && index + j < sortedIndices.count {
|
||||
if abs(labelRects[index + j].width - labelRects[index].width) < 40.0 {
|
||||
labelRects[index + j].size.width = max(labelRects[index + j].width, labelRects[index].width)
|
||||
labelRects[index].size.width = labelRects[index + j].size.width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for i in 0 ..< labelRects.count {
|
||||
labelRects[i] = labelRects[i].insetBy(dx: -6.0, dy: floor((labelRects[i].height - 20.0) / 2.0))
|
||||
labelRects[i].size.height = 20.0
|
||||
labelRects[i].origin.x = floor((labelLayout.size.width - labelRects[i].width) / 2.0)
|
||||
}
|
||||
|
||||
let backgroundMaskImage: (CGPoint, UIImage)?
|
||||
var backgroundMaskUpdated = false
|
||||
if let (currentOffset, currentImage, currentRects) = cachedMaskBackgroundImage, currentRects == labelRects {
|
||||
backgroundMaskImage = (currentOffset, currentImage)
|
||||
} else {
|
||||
backgroundMaskImage = LinkHighlightingNode.generateImage(color: .black, inset: 0.0, innerRadius: 10.0, outerRadius: 10.0, rects: labelRects, useModernPathCalculation: false)
|
||||
backgroundMaskUpdated = true
|
||||
}
|
||||
|
||||
let isExpanded: Bool
|
||||
if let recommendedChannels = item.associatedData.recommendedChannels, !recommendedChannels.isHidden {
|
||||
isExpanded = true
|
||||
} else {
|
||||
isExpanded = false
|
||||
}
|
||||
|
||||
let spacing: CGFloat = 17.0
|
||||
let margin: CGFloat = 4.0
|
||||
var contentSize = CGSize(width: constrainedSize.width, height: labelLayout.size.height)
|
||||
if isExpanded {
|
||||
contentSize.height += spacing + 140.0 + margin
|
||||
} else {
|
||||
contentSize.height += margin
|
||||
}
|
||||
|
||||
return (contentSize.width, { boundingWidth in
|
||||
return (contentSize, { [weak self] animation, synchronousLoads, info in
|
||||
if let strongSelf = self {
|
||||
let themeUpdated = strongSelf.item?.presentationData.theme !== item.presentationData.theme
|
||||
strongSelf.item = item
|
||||
strongSelf.isExpanded = isExpanded
|
||||
|
||||
info?.setInvertOffsetDirection()
|
||||
|
||||
let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: labelLayout.size.height + spacing - 14.0), size: CGSize(width: constrainedSize.width, height: 140.0))
|
||||
|
||||
strongSelf.panelNode.position = CGPoint(x: panelFrame.midX, y: panelFrame.minY)
|
||||
strongSelf.panelNode.bounds = CGRect(origin: .zero, size: panelFrame.size)
|
||||
|
||||
let panelInnerSize = CGSize(width: panelFrame.width + 8.0, height: panelFrame.height + 10.0)
|
||||
if let backgroundNode = item.controllerInteraction.presentationContext.backgroundNode {
|
||||
let graphics = PresentationResourcesChat.principalGraphics(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper, bubbleCorners: item.presentationData.chatBubbleCorners)
|
||||
strongSelf.panelBackgroundNode.update(size: panelInnerSize, theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper, graphics: graphics, wallpaperBackgroundNode: backgroundNode, transition: .immediate)
|
||||
}
|
||||
strongSelf.panelBackgroundNode.frame = CGRect(origin: CGPoint(x: -7.0, y: -8.0), size: panelInnerSize)
|
||||
|
||||
if strongSelf.panelBackgroundNode.layer.mask == nil {
|
||||
strongSelf.panelBackgroundNode.layer.mask = strongSelf.panelMaskLayer
|
||||
}
|
||||
strongSelf.panelMaskLayer?.frame = CGRect(origin: .zero, size: panelInnerSize)
|
||||
if strongSelf.panelMaskLayer?.path == nil {
|
||||
let path = generateRoundedRectWithTailPath(rectSize: CGSize(width: panelFrame.width, height: panelFrame.height), cornerRadius: 16.0, tailSize: CGSize(width: 16.0, height: 6.0), tailRadius: 2.0, tailPosition: 0.5, transformTail: false)
|
||||
path.apply(CGAffineTransform(translationX: 7.0, y: 2.0))
|
||||
strongSelf.panelMaskLayer?.path = path.cgPath
|
||||
}
|
||||
|
||||
if themeUpdated {
|
||||
strongSelf.closeIconNode.image = generateCloseButtonImage(color: item.presentationData.theme.theme.chat.message.incoming.secondaryTextColor)
|
||||
}
|
||||
|
||||
let _ = labelApply()
|
||||
let _ = titleApply()
|
||||
|
||||
let labelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((contentSize.width - labelLayout.size.width) / 2.0), y: 2.0), size: labelLayout.size)
|
||||
strongSelf.labelNode.frame = labelFrame
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: 16.0, y: 11.0), size: titleLayout.size)
|
||||
strongSelf.titleNode.frame = titleFrame
|
||||
|
||||
if let icon = strongSelf.closeIconNode.image {
|
||||
let closeFrame = CGRect(origin: CGPoint(x: panelFrame.width - 5.0 - icon.size.width, y: 5.0), size: icon.size)
|
||||
strongSelf.closeIconNode.frame = closeFrame
|
||||
strongSelf.closeButtonNode.frame = closeFrame.insetBy(dx: -4.0, dy: -4.0)
|
||||
}
|
||||
|
||||
if isExpanded {
|
||||
animation.animator.updateAlpha(layer: strongSelf.panelNode.layer, alpha: 1.0, completion: nil)
|
||||
animation.animator.updateScale(layer: strongSelf.panelNode.layer, scale: 1.0, completion: nil)
|
||||
} else {
|
||||
animation.animator.updateAlpha(layer: strongSelf.panelNode.layer, alpha: 0.0, completion: nil)
|
||||
animation.animator.updateScale(layer: strongSelf.panelNode.layer, scale: 0.1, completion: nil)
|
||||
}
|
||||
|
||||
let baseBackgroundFrame = labelFrame.offsetBy(dx: 0.0, dy: -11.0)
|
||||
if let (offset, image) = backgroundMaskImage {
|
||||
if strongSelf.backgroundNode == nil {
|
||||
if let backgroundNode = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) {
|
||||
strongSelf.backgroundNode = backgroundNode
|
||||
strongSelf.insertSubnode(backgroundNode, at: 0)
|
||||
|
||||
backgroundNode.view.addGestureRecognizer(UITapGestureRecognizer(target: strongSelf, action: #selector(strongSelf.pressed)))
|
||||
}
|
||||
}
|
||||
|
||||
if backgroundMaskUpdated, let backgroundNode = strongSelf.backgroundNode {
|
||||
if labelRects.count == 1 {
|
||||
backgroundNode.clipsToBounds = true
|
||||
backgroundNode.cornerRadius = labelRects[0].height / 2.0
|
||||
backgroundNode.view.mask = nil
|
||||
} else {
|
||||
backgroundNode.clipsToBounds = false
|
||||
backgroundNode.cornerRadius = 0.0
|
||||
backgroundNode.view.mask = strongSelf.backgroundMaskNode.view
|
||||
}
|
||||
}
|
||||
|
||||
if let backgroundNode = strongSelf.backgroundNode {
|
||||
backgroundNode.frame = CGRect(origin: CGPoint(x: baseBackgroundFrame.minX + offset.x, y: baseBackgroundFrame.minY + offset.y), size: image.size)
|
||||
}
|
||||
strongSelf.backgroundMaskNode.image = image
|
||||
strongSelf.backgroundMaskNode.frame = CGRect(origin: CGPoint(), size: image.size)
|
||||
|
||||
strongSelf.cachedMaskBackgroundImage = (offset, image, labelRects)
|
||||
}
|
||||
if let (rect, size) = strongSelf.absoluteRect {
|
||||
strongSelf.updateAbsoluteRect(rect, within: size)
|
||||
}
|
||||
|
||||
strongSelf.updateList()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func updateList() {
|
||||
guard let item = self.item, let recommendedChannels = item.associatedData.recommendedChannels else {
|
||||
return
|
||||
}
|
||||
let listSize = self.panelListView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
ChannelListPanelComponent(
|
||||
context: item.context,
|
||||
theme: item.presentationData.theme.theme,
|
||||
peers: recommendedChannels,
|
||||
action: { peer in
|
||||
item.controllerInteraction.openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default)
|
||||
}
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: self.panelNode.frame.width, height: 100.0)
|
||||
)
|
||||
if let view = self.panelListView.view {
|
||||
if view.superview == nil {
|
||||
self.panelNode.view.addSubview(view)
|
||||
}
|
||||
view.frame = CGRect(origin: CGPoint(x: 0.0, y: 42.0), size: listSize)
|
||||
}
|
||||
}
|
||||
|
||||
override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
|
||||
self.absoluteRect = (rect, containerSize)
|
||||
|
||||
if let backgroundNode = self.backgroundNode {
|
||||
var backgroundFrame = backgroundNode.frame
|
||||
backgroundFrame.origin.x += rect.minX
|
||||
backgroundFrame.origin.y += rect.minY
|
||||
backgroundNode.update(rect: backgroundFrame, within: containerSize, transition: .immediate)
|
||||
}
|
||||
|
||||
var panelBackgroundFrame = panelBackgroundNode.frame
|
||||
panelBackgroundFrame.origin.x += self.panelNode.frame.minX + rect.minX
|
||||
panelBackgroundFrame.origin.y += self.panelNode.frame.minY + rect.minY
|
||||
self.panelBackgroundNode.updateAbsoluteRect(panelBackgroundFrame, within: containerSize)
|
||||
}
|
||||
|
||||
override public func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) {
|
||||
if let backgroundNode = self.backgroundNode {
|
||||
backgroundNode.offset(value: value, animationCurve: animationCurve, duration: duration)
|
||||
}
|
||||
}
|
||||
|
||||
override public func applyAbsoluteOffsetSpring(value: CGFloat, duration: Double, damping: CGFloat) {
|
||||
if let backgroundNode = self.backgroundNode {
|
||||
backgroundNode.offsetSpring(value: value, duration: duration, damping: damping)
|
||||
}
|
||||
}
|
||||
|
||||
override public func updateTouchesAtPoint(_ point: CGPoint?) {
|
||||
if let item = self.item {
|
||||
var rects: [(CGRect, CGRect)]?
|
||||
let textNodeFrame = self.labelNode.frame
|
||||
if let point = point {
|
||||
if let (index, attributes) = self.labelNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY - 10.0)) {
|
||||
let possibleNames: [String] = [
|
||||
TelegramTextAttributes.URL,
|
||||
TelegramTextAttributes.PeerMention,
|
||||
TelegramTextAttributes.PeerTextMention,
|
||||
TelegramTextAttributes.BotCommand,
|
||||
TelegramTextAttributes.Hashtag
|
||||
]
|
||||
for name in possibleNames {
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: name)] {
|
||||
rects = self.labelNode.lineAndAttributeRects(name: name, at: index)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let rects = rects {
|
||||
var mappedRects: [CGRect] = []
|
||||
for i in 0 ..< rects.count {
|
||||
let lineRect = rects[i].0
|
||||
var itemRect = rects[i].1
|
||||
itemRect.origin.x = floor((textNodeFrame.size.width - lineRect.width) / 2.0) + itemRect.origin.x
|
||||
mappedRects.append(itemRect)
|
||||
}
|
||||
|
||||
let linkHighlightingNode: LinkHighlightingNode
|
||||
if let current = self.linkHighlightingNode {
|
||||
linkHighlightingNode = current
|
||||
} else {
|
||||
let serviceColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)
|
||||
linkHighlightingNode = LinkHighlightingNode(color: serviceColor.linkHighlight)
|
||||
linkHighlightingNode.inset = 2.5
|
||||
self.linkHighlightingNode = linkHighlightingNode
|
||||
self.insertSubnode(linkHighlightingNode, belowSubnode: self.labelNode)
|
||||
}
|
||||
linkHighlightingNode.frame = self.labelNode.frame.offsetBy(dx: 0.0, dy: 1.5)
|
||||
linkHighlightingNode.updateRects(mappedRects)
|
||||
} else if let linkHighlightingNode = self.linkHighlightingNode {
|
||||
self.linkHighlightingNode = nil
|
||||
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
|
||||
linkHighlightingNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
|
||||
let textNodeFrame = self.labelNode.frame
|
||||
if let (index, attributes) = self.labelNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY - 10.0)), gesture == .tap {
|
||||
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
|
||||
var concealed = true
|
||||
if let (attributeText, fullText) = self.labelNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
|
||||
concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText)
|
||||
}
|
||||
return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: url, concealed: concealed)))
|
||||
} else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
|
||||
return ChatMessageBubbleContentTapAction(content: .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false))
|
||||
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
|
||||
return ChatMessageBubbleContentTapAction(content: .textMention(peerName))
|
||||
} else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String {
|
||||
return ChatMessageBubbleContentTapAction(content: .botCommand(botCommand))
|
||||
} else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
|
||||
return ChatMessageBubbleContentTapAction(content: .hashtag(hashtag.peerName, hashtag.hashtag))
|
||||
}
|
||||
}
|
||||
|
||||
if let backgroundNode = self.backgroundNode, backgroundNode.frame.contains(point) {
|
||||
return ChatMessageBubbleContentTapAction(content: .ignore)
|
||||
}
|
||||
|
||||
if self.panelNode.frame.contains(point) {
|
||||
let panelPoint = self.view.convert(point, to: self.panelNode.view)
|
||||
if self.closeButtonNode.frame.contains(panelPoint) {
|
||||
return ChatMessageBubbleContentTapAction(content: .ignore)
|
||||
}
|
||||
}
|
||||
|
||||
return ChatMessageBubbleContentTapAction(content: .none)
|
||||
}
|
||||
}
|
||||
|
||||
private class MessageBackgroundNode: ASDisplayNode {
|
||||
private let backgroundWallpaperNode: ChatMessageBubbleBackdrop
|
||||
private let backgroundNode: ChatMessageBackground
|
||||
|
||||
override init() {
|
||||
self.backgroundWallpaperNode = ChatMessageBubbleBackdrop()
|
||||
self.backgroundNode = ChatMessageBackground()
|
||||
self.backgroundNode.backdropNode = self.backgroundWallpaperNode
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.backgroundWallpaperNode)
|
||||
}
|
||||
|
||||
private var absoluteRect: (CGRect, CGSize)?
|
||||
|
||||
func update(size: CGSize, theme: PresentationTheme, wallpaper: TelegramWallpaper, graphics: PrincipalThemeEssentialGraphics, wallpaperBackgroundNode: WallpaperBackgroundNode, transition: ContainedViewLayoutTransition) {
|
||||
self.backgroundNode.setType(type: .incoming(.Extracted), highlighted: false, graphics: graphics, maskMode: false, hasWallpaper: wallpaper.hasWallpaper, transition: transition, backgroundNode: wallpaperBackgroundNode)
|
||||
self.backgroundWallpaperNode.setType(type: .incoming(.Extracted), theme: ChatPresentationThemeData(theme: theme, wallpaper: wallpaper), essentialGraphics: graphics, maskMode: false, backgroundNode: wallpaperBackgroundNode)
|
||||
|
||||
let backgroundFrame = CGRect(origin: CGPoint(), size: size)
|
||||
self.backgroundNode.updateLayout(size: backgroundFrame.size, transition: transition)
|
||||
self.backgroundWallpaperNode.updateFrame(backgroundFrame, transition: transition)
|
||||
|
||||
if let (rect, size) = self.absoluteRect {
|
||||
self.updateAbsoluteRect(rect, within: size)
|
||||
}
|
||||
}
|
||||
|
||||
func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
|
||||
self.absoluteRect = (rect, containerSize)
|
||||
|
||||
var backgroundWallpaperFrame = self.backgroundWallpaperNode.frame
|
||||
backgroundWallpaperFrame.origin.x += rect.minX
|
||||
backgroundWallpaperFrame.origin.y += rect.minY
|
||||
self.backgroundWallpaperNode.update(rect: backgroundWallpaperFrame, within: containerSize)
|
||||
}
|
||||
}
|
||||
|
||||
private let itemSize = CGSize(width: 94.0, height: 90.0)
|
||||
|
||||
private final class ChannelItemComponent: Component {
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let peer: EnginePeer
|
||||
let subtitle: String
|
||||
let action: (EnginePeer) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
peer: EnginePeer,
|
||||
subtitle: String,
|
||||
action: @escaping (EnginePeer) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.peer = peer
|
||||
self.subtitle = subtitle
|
||||
self.action = action
|
||||
}
|
||||
|
||||
static func ==(lhs: ChannelItemComponent, rhs: ChannelItemComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
if lhs.subtitle != rhs.subtitle {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let containerButton: HighlightTrackingButton
|
||||
|
||||
private let title = ComponentView<Empty>()
|
||||
private let subtitle = ComponentView<Empty>()
|
||||
private let avatarNode: AvatarNode
|
||||
|
||||
private var component: ChannelItemComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
|
||||
self.avatarNode.isUserInteractionEnabled = false
|
||||
|
||||
self.containerButton = HighlightTrackingButton()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.containerButton)
|
||||
self.addSubnode(self.avatarNode)
|
||||
|
||||
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func pressed() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.action(component.peer)
|
||||
}
|
||||
|
||||
func update(component: ChannelItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: component.peer.compactDisplayTitle, font: Font.regular(11.0), textColor: component.theme.chat.message.incoming.primaryTextColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: itemSize.width - 20.0, height: 100.0)
|
||||
)
|
||||
|
||||
let subtitleSize = self.subtitle.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: component.subtitle, font: Font.regular(10.0), textColor: component.theme.chat.message.incoming.secondaryTextColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: itemSize.width - 12.0, height: 100.0)
|
||||
)
|
||||
|
||||
let avatarSize = CGSize(width: 60.0, height: 60.0)
|
||||
let avatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((itemSize.width - avatarSize.width) / 2.0), y: 0.0), size: avatarSize)
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((itemSize.width - titleSize.width) / 2.0), y: avatarFrame.maxY + 4.0), size: titleSize)
|
||||
let subtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((itemSize.width - subtitleSize.width) / 2.0), y: titleFrame.maxY + 1.0), size: subtitleSize)
|
||||
|
||||
self.avatarNode.frame = avatarFrame
|
||||
self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer)
|
||||
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
titleView.isUserInteractionEnabled = false
|
||||
self.containerButton.addSubview(titleView)
|
||||
}
|
||||
titleView.frame = titleFrame
|
||||
}
|
||||
if let subtitleView = self.subtitle.view {
|
||||
if subtitleView.superview == nil {
|
||||
subtitleView.isUserInteractionEnabled = false
|
||||
self.containerButton.addSubview(subtitleView)
|
||||
}
|
||||
subtitleView.frame = subtitleFrame
|
||||
}
|
||||
|
||||
self.containerButton.frame = CGRect(origin: .zero, size: itemSize)
|
||||
|
||||
return itemSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
final class ChannelListPanelComponent: Component {
|
||||
typealias EnvironmentType = Empty
|
||||
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let peers: RecommendedChannels
|
||||
let action: (EnginePeer) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
peers: RecommendedChannels,
|
||||
action: @escaping (EnginePeer) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.peers = peers
|
||||
self.action = action
|
||||
}
|
||||
|
||||
static func ==(lhs: ChannelListPanelComponent, rhs: ChannelListPanelComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.peers != rhs.peers {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private struct ItemLayout: Equatable {
|
||||
let containerInsets: UIEdgeInsets
|
||||
let containerHeight: CGFloat
|
||||
let itemWidth: CGFloat
|
||||
let itemCount: Int
|
||||
|
||||
let contentWidth: CGFloat
|
||||
|
||||
init(
|
||||
containerInsets: UIEdgeInsets,
|
||||
containerHeight: CGFloat,
|
||||
itemWidth: CGFloat,
|
||||
itemCount: Int
|
||||
) {
|
||||
self.containerInsets = containerInsets
|
||||
self.containerHeight = containerHeight
|
||||
self.itemWidth = itemWidth
|
||||
self.itemCount = itemCount
|
||||
|
||||
self.contentWidth = containerInsets.left + containerInsets.right + CGFloat(itemCount) * itemWidth
|
||||
}
|
||||
|
||||
func visibleItems(for rect: CGRect) -> Range<Int>? {
|
||||
let offsetRect = rect.offsetBy(dx: -self.containerInsets.left, dy: -self.containerInsets.top)
|
||||
var minVisibleRow = Int(floor((offsetRect.minX) / (self.itemWidth)))
|
||||
minVisibleRow = max(0, minVisibleRow)
|
||||
let maxVisibleRow = Int(ceil((offsetRect.maxX) / (self.itemWidth)))
|
||||
|
||||
let minVisibleIndex = minVisibleRow
|
||||
let maxVisibleIndex = maxVisibleRow
|
||||
|
||||
if maxVisibleIndex >= minVisibleIndex {
|
||||
return minVisibleIndex ..< (maxVisibleIndex + 1)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func itemFrame(for index: Int) -> CGRect {
|
||||
return CGRect(origin: CGPoint(x: self.containerInsets.top + CGFloat(index) * self.itemWidth, y: 0.0), size: CGSize(width: self.itemWidth, height: self.containerHeight))
|
||||
}
|
||||
}
|
||||
|
||||
private final class ScrollViewImpl: UIScrollView {
|
||||
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
class View: UIView, UIScrollViewDelegate {
|
||||
private let scrollView: ScrollViewImpl
|
||||
|
||||
private let measureItem = ComponentView<Empty>()
|
||||
private var visibleItems: [EnginePeer.Id: ComponentView<Empty>] = [:]
|
||||
|
||||
private var ignoreScrolling: Bool = false
|
||||
|
||||
private var component: ChannelListPanelComponent?
|
||||
private var itemLayout: ItemLayout?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.scrollView = ScrollViewImpl()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.scrollView.delaysContentTouches = true
|
||||
self.scrollView.canCancelContentTouches = true
|
||||
self.scrollView.clipsToBounds = false
|
||||
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
if #available(iOS 13.0, *) {
|
||||
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
|
||||
}
|
||||
self.scrollView.showsVerticalScrollIndicator = false
|
||||
self.scrollView.showsHorizontalScrollIndicator = false
|
||||
self.scrollView.alwaysBounceHorizontal = true
|
||||
self.scrollView.scrollsToTop = false
|
||||
self.scrollView.delegate = self
|
||||
self.scrollView.clipsToBounds = true
|
||||
self.addSubview(self.scrollView)
|
||||
|
||||
self.disablesInteractiveTransitionGestureRecognizer = true
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if !self.ignoreScrolling {
|
||||
self.updateScrolling(transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateScrolling(transition: Transition) {
|
||||
guard let component = self.component, let itemLayout = self.itemLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
let visibleBounds = self.scrollView.bounds.insetBy(dx: -100.0, dy: 0.0)
|
||||
|
||||
var validIds = Set<EnginePeer.Id>()
|
||||
if let visibleItems = itemLayout.visibleItems(for: visibleBounds) {
|
||||
for index in visibleItems.lowerBound ..< visibleItems.upperBound {
|
||||
if index >= component.peers.channels.count {
|
||||
continue
|
||||
}
|
||||
let item = component.peers.channels[index]
|
||||
let id = item.peer.id
|
||||
validIds.insert(id)
|
||||
|
||||
var itemTransition = transition
|
||||
let itemView: ComponentView<Empty>
|
||||
if let current = self.visibleItems[id] {
|
||||
itemView = current
|
||||
} else {
|
||||
itemTransition = .immediate
|
||||
itemView = ComponentView()
|
||||
self.visibleItems[id] = itemView
|
||||
}
|
||||
|
||||
let subtitle = countString(Int64(item.subscribers))
|
||||
let _ = itemView.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(ChannelItemComponent(
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
peer: item.peer,
|
||||
subtitle: subtitle,
|
||||
action: component.action
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: itemLayout.itemWidth, height: itemLayout.containerHeight)
|
||||
)
|
||||
let itemFrame = itemLayout.itemFrame(for: index)
|
||||
if let itemComponentView = itemView.view {
|
||||
if itemComponentView.superview == nil {
|
||||
self.scrollView.addSubview(itemComponentView)
|
||||
}
|
||||
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var removeIds: [EnginePeer.Id] = []
|
||||
for (id, itemView) in self.visibleItems {
|
||||
if !validIds.contains(id) {
|
||||
removeIds.append(id)
|
||||
if let itemComponentView = itemView.view {
|
||||
transition.setAlpha(view: itemComponentView, alpha: 0.0, completion: { [weak itemComponentView] _ in
|
||||
itemComponentView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
for id in removeIds {
|
||||
self.visibleItems.removeValue(forKey: id)
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: ChannelListPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
|
||||
let itemLayout = ItemLayout(
|
||||
containerInsets: .zero,
|
||||
containerHeight: availableSize.height,
|
||||
itemWidth: itemSize.width,
|
||||
itemCount: component.peers.channels.count
|
||||
)
|
||||
self.itemLayout = itemLayout
|
||||
|
||||
self.ignoreScrolling = true
|
||||
let contentOffset = self.scrollView.bounds.minY
|
||||
transition.setPosition(view: self.scrollView, position: CGRect(origin: CGPoint(), size: availableSize).center)
|
||||
var scrollBounds = self.scrollView.bounds
|
||||
scrollBounds.size = availableSize
|
||||
transition.setBounds(view: self.scrollView, bounds: scrollBounds)
|
||||
let contentSize = CGSize(width: itemLayout.contentWidth, height: availableSize.height)
|
||||
if self.scrollView.contentSize != contentSize {
|
||||
self.scrollView.contentSize = contentSize
|
||||
}
|
||||
if !transition.animation.isImmediate && self.scrollView.bounds.minY != contentOffset {
|
||||
let deltaOffset = self.scrollView.bounds.minY - contentOffset
|
||||
transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: -deltaOffset), to: CGPoint(), additive: true)
|
||||
}
|
||||
self.ignoreScrolling = false
|
||||
self.updateScrolling(transition: transition)
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
@ -117,7 +117,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
|
||||
|
||||
let action = TelegramMediaActionType.titleUpdated(title: new)
|
||||
let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
|
||||
return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil))
|
||||
return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil))
|
||||
case let .changeAbout(prev, new):
|
||||
var peers = SimpleDictionary<PeerId, Peer>()
|
||||
var author: Peer?
|
||||
|
||||
@ -17805,9 +17805,32 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
contextItems.append(.action(ContextMenuActionItem(text: globalTitle, textColor: .destructive, icon: { _ in nil }, action: { [weak self] _, f in
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
||||
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone()
|
||||
f(.dismissWithoutContent)
|
||||
var giveaway: TelegramMediaGiveaway?
|
||||
for messageId in messageIds {
|
||||
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) {
|
||||
if let media = message.media.first(where: { $0 is TelegramMediaGiveaway }) as? TelegramMediaGiveaway {
|
||||
giveaway = media
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
let commit = {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
||||
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone()
|
||||
}
|
||||
if let giveaway {
|
||||
Queue.mainQueue().after(0.2) {
|
||||
let dateString = stringForDate(timestamp: giveaway.untilDate, timeZone: .current, strings: strongSelf.presentationData.strings)
|
||||
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: strongSelf.presentationData.strings.Chat_Giveaway_DeleteConfirmation_Title, text: strongSelf.presentationData.strings.Chat_Giveaway_DeleteConfirmation_Text(dateString).string, actions: [TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Common_Delete, action: {
|
||||
commit()
|
||||
}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
|
||||
})], parseMarkdown: true), in: .window(.root))
|
||||
}
|
||||
f(.default)
|
||||
} else {
|
||||
commit()
|
||||
f(.dismissWithoutContent)
|
||||
}
|
||||
}
|
||||
})))
|
||||
items.append(ActionSheetButtonItem(title: globalTitle, color: .destructive, action: { [weak self, weak actionSheet] in
|
||||
|
||||
@ -320,7 +320,26 @@ private final class ChatHistoryTransactionOpaqueState {
|
||||
}
|
||||
}
|
||||
|
||||
private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHistoryView, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, animatedEmojiStickers: [String: [StickerPackItem]], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]], subject: ChatControllerSubject?, currentlyPlayingMessageId: MessageIndex?, isCopyProtectionEnabled: Bool, availableReactions: AvailableReactions?, defaultReaction: MessageReaction.Reaction?, isPremium: Bool, alwaysDisplayTranscribeButton: ChatMessageItemAssociatedData.DisplayTranscribeButton, accountPeer: EnginePeer?, topicAuthorId: EnginePeer.Id?, hasBots: Bool, translateToLanguage: String?, maxReadStoryId: Int32?) -> ChatMessageItemAssociatedData {
|
||||
private func extractAssociatedData(
|
||||
chatLocation: ChatLocation,
|
||||
view: MessageHistoryView,
|
||||
automaticDownloadNetworkType: MediaAutoDownloadNetworkType,
|
||||
animatedEmojiStickers: [String: [StickerPackItem]],
|
||||
additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]],
|
||||
subject: ChatControllerSubject?,
|
||||
currentlyPlayingMessageId: MessageIndex?,
|
||||
isCopyProtectionEnabled: Bool,
|
||||
availableReactions: AvailableReactions?,
|
||||
defaultReaction: MessageReaction.Reaction?,
|
||||
isPremium: Bool,
|
||||
alwaysDisplayTranscribeButton: ChatMessageItemAssociatedData.DisplayTranscribeButton,
|
||||
accountPeer: EnginePeer?,
|
||||
topicAuthorId: EnginePeer.Id?,
|
||||
hasBots: Bool,
|
||||
translateToLanguage: String?,
|
||||
maxReadStoryId: Int32?,
|
||||
recommendedChannels: RecommendedChannels?
|
||||
) -> ChatMessageItemAssociatedData {
|
||||
var automaticDownloadPeerId: EnginePeer.Id?
|
||||
var automaticMediaDownloadPeerType: MediaAutoDownloadPeerType = .channel
|
||||
var contactsPeerIds: Set<PeerId> = Set()
|
||||
@ -374,7 +393,7 @@ private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHist
|
||||
automaticDownloadPeerId = message.messageId.peerId
|
||||
}
|
||||
|
||||
return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId)
|
||||
return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels)
|
||||
}
|
||||
|
||||
private extension ChatHistoryLocationInput {
|
||||
@ -1290,7 +1309,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
self.pendingRemovedMessagesPromise.get(),
|
||||
self.currentlyPlayingMessageIdPromise.get(),
|
||||
self.scrollToMessageIdPromise.get(),
|
||||
self.chatHasBotsPromise.get()
|
||||
self.chatHasBotsPromise.get(),
|
||||
self.allAdMessagesPromise.get()
|
||||
)
|
||||
|
||||
let maxReadStoryId: Signal<Int32?, NoError>
|
||||
@ -1311,6 +1331,13 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
maxReadStoryId = .single(nil)
|
||||
}
|
||||
|
||||
let recommendedChannels: Signal<RecommendedChannels?, NoError>
|
||||
if let peerId = self.chatLocation.peerId, peerId.namespace == Namespaces.Peer.CloudChannel {
|
||||
recommendedChannels = self.context.engine.peers.recommendedChannels(peerId: peerId)
|
||||
} else {
|
||||
recommendedChannels = .single(nil)
|
||||
}
|
||||
|
||||
let messageViewQueue = Queue.mainQueue()
|
||||
let historyViewTransitionDisposable = combineLatest(queue: messageViewQueue,
|
||||
historyViewUpdate,
|
||||
@ -1328,11 +1355,11 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
audioTranscriptionSuggestion,
|
||||
promises,
|
||||
topicAuthorId,
|
||||
self.allAdMessagesPromise.get(),
|
||||
translationState,
|
||||
maxReadStoryId
|
||||
).startStrict(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, availableReactions, defaultReaction, accountPeer, suggestAudioTranscription, promises, topicAuthorId, allAdMessages, translationState, maxReadStoryId in
|
||||
let (historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, currentlyPlayingMessageIdAndType, scrollToMessageId, chatHasBots) = promises
|
||||
maxReadStoryId,
|
||||
recommendedChannels
|
||||
).startStrict(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, availableReactions, defaultReaction, accountPeer, suggestAudioTranscription, promises, topicAuthorId, translationState, maxReadStoryId, recommendedChannels in
|
||||
let (historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, currentlyPlayingMessageIdAndType, scrollToMessageId, chatHasBots, allAdMessages) = promises
|
||||
|
||||
func applyHole() {
|
||||
Queue.mainQueue().async {
|
||||
@ -1455,7 +1482,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
reverseGroups = reverseGroupsValue
|
||||
}
|
||||
|
||||
var isCopyProtectionEnabled: Bool = data.initialData?.peer?.isCopyProtectionEnabled ?? false
|
||||
var isCopyProtectionEnabled: Bool = data.initialData?.peer?.isCopyProtectionEnabled ?? false
|
||||
for entry in view.additionalData {
|
||||
if case let .peer(_, maybePeer) = entry, let peer = maybePeer {
|
||||
isCopyProtectionEnabled = peer.isCopyProtectionEnabled
|
||||
@ -1487,7 +1514,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
translateToLanguage = languageCode
|
||||
}
|
||||
|
||||
let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, defaultReaction: defaultReaction, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId)
|
||||
let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, defaultReaction: defaultReaction, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels)
|
||||
|
||||
let filteredEntries = chatHistoryEntriesForView(
|
||||
location: chatLocation,
|
||||
|
||||
@ -565,7 +565,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
var loadStickerSaveStatus: MediaId?
|
||||
var loadCopyMediaResource: MediaResource?
|
||||
var isAction = false
|
||||
var isGiveawayLaunch = false
|
||||
var isGiveawayServiceMessage = false
|
||||
var diceEmoji: String?
|
||||
if messages.count == 1 {
|
||||
for media in messages[0].media {
|
||||
@ -580,8 +580,13 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
}
|
||||
} else if media is TelegramMediaAction || media is TelegramMediaExpiredContent {
|
||||
isAction = true
|
||||
if let action = media as? TelegramMediaAction, case .giveawayLaunched = action.action {
|
||||
isGiveawayLaunch = true
|
||||
if let action = media as? TelegramMediaAction {
|
||||
switch action.action {
|
||||
case .giveawayLaunched, .giveawayResults:
|
||||
isGiveawayServiceMessage = true
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if let image = media as? TelegramMediaImage {
|
||||
if !messages[0].containsSecretMedia {
|
||||
@ -643,7 +648,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
canPin = false
|
||||
}
|
||||
|
||||
if isGiveawayLaunch {
|
||||
if isGiveawayServiceMessage {
|
||||
canReply = false
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user