mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-17 11:50: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";
|
"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_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.";
|
"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 hasBots: Bool
|
||||||
public let translateToLanguage: String?
|
public let translateToLanguage: String?
|
||||||
public let maxReadStoryId: Int32?
|
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.automaticDownloadPeerType = automaticDownloadPeerType
|
||||||
self.automaticDownloadPeerId = automaticDownloadPeerId
|
self.automaticDownloadPeerId = automaticDownloadPeerId
|
||||||
self.automaticDownloadNetworkType = automaticDownloadNetworkType
|
self.automaticDownloadNetworkType = automaticDownloadNetworkType
|
||||||
@ -74,6 +99,7 @@ public final class ChatMessageItemAssociatedData: Equatable {
|
|||||||
self.hasBots = hasBots
|
self.hasBots = hasBots
|
||||||
self.translateToLanguage = translateToLanguage
|
self.translateToLanguage = translateToLanguage
|
||||||
self.maxReadStoryId = maxReadStoryId
|
self.maxReadStoryId = maxReadStoryId
|
||||||
|
self.recommendedChannels = recommendedChannels
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func == (lhs: ChatMessageItemAssociatedData, rhs: ChatMessageItemAssociatedData) -> Bool {
|
public static func == (lhs: ChatMessageItemAssociatedData, rhs: ChatMessageItemAssociatedData) -> Bool {
|
||||||
@ -140,6 +166,9 @@ public final class ChatMessageItemAssociatedData: Equatable {
|
|||||||
if lhs.maxReadStoryId != rhs.maxReadStoryId {
|
if lhs.maxReadStoryId != rhs.maxReadStoryId {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.recommendedChannels != rhs.recommendedChannels {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -472,7 +472,6 @@ private class MessageBackgroundNode: ASDisplayNode {
|
|||||||
private var absoluteRect: (CGRect, CGSize)?
|
private var absoluteRect: (CGRect, CGSize)?
|
||||||
|
|
||||||
func update(size: CGSize, theme: PresentationTheme, wallpaper: TelegramWallpaper, graphics: PrincipalThemeEssentialGraphics, wallpaperBackgroundNode: WallpaperBackgroundNode, transition: ContainedViewLayoutTransition) {
|
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.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.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)
|
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 {
|
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()
|
let buffer = Buffer()
|
||||||
buffer.appendInt32(-873707987)
|
buffer.appendInt32(-2085155433)
|
||||||
channelId.serialize(buffer, true)
|
channel.serialize(buffer, true)
|
||||||
return (FunctionDescription(name: "channels.getChannelRecommendations", parameters: [("channelId", String(describing: channelId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.Chats? in
|
return (FunctionDescription(name: "channels.getChannelRecommendations", parameters: [("channel", String(describing: channel))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.Chats? in
|
||||||
let reader = BufferReader(buffer)
|
let reader = BufferReader(buffer)
|
||||||
var result: Api.messages.Chats?
|
var result: Api.messages.Chats?
|
||||||
if let signature = reader.readInt32() {
|
if let signature = reader.readInt32() {
|
||||||
|
|||||||
@ -49,7 +49,7 @@ func _internal_requestRecommendedChannels(account: Account, peerId: EnginePeer.I
|
|||||||
guard let inputChannel = channel.flatMap(apiInputChannel) else {
|
guard let inputChannel = channel.flatMap(apiInputChannel) else {
|
||||||
return .complete()
|
return .complete()
|
||||||
}
|
}
|
||||||
return account.network.request(Api.functions.channels.getChannelRecommendations(channelId: inputChannel))
|
return account.network.request(Api.functions.channels.getChannelRecommendations(channel: inputChannel))
|
||||||
|> retryRequest
|
|> retryRequest
|
||||||
|> mapToSignal { result -> Signal<Never, NoError> in
|
|> mapToSignal { result -> Signal<Never, NoError> in
|
||||||
return account.postbox.transaction { transaction -> [EnginePeer] 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 RecommendedChannels: Equatable {
|
||||||
public struct Channel {
|
public struct Channel: Equatable {
|
||||||
public let peer: EnginePeer
|
public let peer: EnginePeer
|
||||||
public let subscribers: Int32
|
public let subscribers: Int32
|
||||||
}
|
}
|
||||||
|
|||||||
@ -79,6 +79,7 @@ swift_library(
|
|||||||
"//submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode",
|
"//submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode",
|
||||||
"//submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode",
|
"//submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode",
|
||||||
"//submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode",
|
"//submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode",
|
||||||
|
"//submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
|||||||
@ -69,6 +69,7 @@ import ChatMessageUnsupportedBubbleContentNode
|
|||||||
import ChatMessageWallpaperBubbleContentNode
|
import ChatMessageWallpaperBubbleContentNode
|
||||||
import ChatMessageGiftBubbleContentNode
|
import ChatMessageGiftBubbleContentNode
|
||||||
import ChatMessageGiveawayBubbleContentNode
|
import ChatMessageGiveawayBubbleContentNode
|
||||||
|
import ChatMessageJoinedChannelBubbleContentNode
|
||||||
|
|
||||||
private struct BubbleItemAttributes {
|
private struct BubbleItemAttributes {
|
||||||
var isAttachment: Bool
|
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)))
|
result.append((message, ChatMessageWallpaperBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
|
||||||
} else if case .giftCode = action.action {
|
} else if case .giftCode = action.action {
|
||||||
result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
|
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 {
|
} else {
|
||||||
result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
|
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
|
var hasInstantVideo = false
|
||||||
for contentNodeItemValue in contentNodeMessagesAndClasses {
|
for contentNodeItemValue in contentNodeMessagesAndClasses {
|
||||||
let contentNodeItem = contentNodeItemValue as (message: Message, type: AnyClass, attributes: ChatMessageEntryAttributes, bubbleAttributes: BubbleItemAttributes)
|
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 {
|
if contentNodeItem.type == ChatMessageGiveawayBubbleContentNode.self {
|
||||||
maximumContentWidth = min(305.0, maximumContentWidth)
|
maximumContentWidth = min(305.0, maximumContentWidth)
|
||||||
break
|
break
|
||||||
@ -3939,17 +3946,27 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||||||
strongSelf.mainContextSourceNode.layoutUpdated?(strongSelf.mainContextSourceNode.bounds.size, animation)
|
strongSelf.mainContextSourceNode.layoutUpdated?(strongSelf.mainContextSourceNode.bounds.size, animation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var hasMenuGesture = true
|
||||||
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject {
|
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject {
|
||||||
if case .link = info {
|
if case .link = info {
|
||||||
} else {
|
} else {
|
||||||
strongSelf.tapRecognizer?.isEnabled = false
|
strongSelf.tapRecognizer?.isEnabled = false
|
||||||
}
|
}
|
||||||
strongSelf.replyRecognizer?.isEnabled = false
|
strongSelf.replyRecognizer?.isEnabled = false
|
||||||
strongSelf.mainContainerNode.isGestureEnabled = false
|
hasMenuGesture = false
|
||||||
for contentContainer in strongSelf.contentContainers {
|
}
|
||||||
contentContainer.containerNode.isGestureEnabled = 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()
|
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 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: [:])
|
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):
|
case let .changeAbout(prev, new):
|
||||||
var peers = SimpleDictionary<PeerId, Peer>()
|
var peers = SimpleDictionary<PeerId, Peer>()
|
||||||
var author: 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
|
contextItems.append(.action(ContextMenuActionItem(text: globalTitle, textColor: .destructive, icon: { _ in nil }, action: { [weak self] _, f in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
var giveaway: TelegramMediaGiveaway?
|
||||||
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone()
|
for messageId in messageIds {
|
||||||
f(.dismissWithoutContent)
|
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
|
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 automaticDownloadPeerId: EnginePeer.Id?
|
||||||
var automaticMediaDownloadPeerType: MediaAutoDownloadPeerType = .channel
|
var automaticMediaDownloadPeerType: MediaAutoDownloadPeerType = .channel
|
||||||
var contactsPeerIds: Set<PeerId> = Set()
|
var contactsPeerIds: Set<PeerId> = Set()
|
||||||
@ -374,7 +393,7 @@ private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHist
|
|||||||
automaticDownloadPeerId = message.messageId.peerId
|
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 {
|
private extension ChatHistoryLocationInput {
|
||||||
@ -1290,7 +1309,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
self.pendingRemovedMessagesPromise.get(),
|
self.pendingRemovedMessagesPromise.get(),
|
||||||
self.currentlyPlayingMessageIdPromise.get(),
|
self.currentlyPlayingMessageIdPromise.get(),
|
||||||
self.scrollToMessageIdPromise.get(),
|
self.scrollToMessageIdPromise.get(),
|
||||||
self.chatHasBotsPromise.get()
|
self.chatHasBotsPromise.get(),
|
||||||
|
self.allAdMessagesPromise.get()
|
||||||
)
|
)
|
||||||
|
|
||||||
let maxReadStoryId: Signal<Int32?, NoError>
|
let maxReadStoryId: Signal<Int32?, NoError>
|
||||||
@ -1311,6 +1331,13 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
maxReadStoryId = .single(nil)
|
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 messageViewQueue = Queue.mainQueue()
|
||||||
let historyViewTransitionDisposable = combineLatest(queue: messageViewQueue,
|
let historyViewTransitionDisposable = combineLatest(queue: messageViewQueue,
|
||||||
historyViewUpdate,
|
historyViewUpdate,
|
||||||
@ -1328,11 +1355,11 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
audioTranscriptionSuggestion,
|
audioTranscriptionSuggestion,
|
||||||
promises,
|
promises,
|
||||||
topicAuthorId,
|
topicAuthorId,
|
||||||
self.allAdMessagesPromise.get(),
|
|
||||||
translationState,
|
translationState,
|
||||||
maxReadStoryId
|
maxReadStoryId,
|
||||||
).startStrict(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, availableReactions, defaultReaction, accountPeer, suggestAudioTranscription, promises, topicAuthorId, allAdMessages, translationState, maxReadStoryId in
|
recommendedChannels
|
||||||
let (historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, currentlyPlayingMessageIdAndType, scrollToMessageId, chatHasBots) = promises
|
).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() {
|
func applyHole() {
|
||||||
Queue.mainQueue().async {
|
Queue.mainQueue().async {
|
||||||
@ -1455,7 +1482,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
reverseGroups = reverseGroupsValue
|
reverseGroups = reverseGroupsValue
|
||||||
}
|
}
|
||||||
|
|
||||||
var isCopyProtectionEnabled: Bool = data.initialData?.peer?.isCopyProtectionEnabled ?? false
|
var isCopyProtectionEnabled: Bool = data.initialData?.peer?.isCopyProtectionEnabled ?? false
|
||||||
for entry in view.additionalData {
|
for entry in view.additionalData {
|
||||||
if case let .peer(_, maybePeer) = entry, let peer = maybePeer {
|
if case let .peer(_, maybePeer) = entry, let peer = maybePeer {
|
||||||
isCopyProtectionEnabled = peer.isCopyProtectionEnabled
|
isCopyProtectionEnabled = peer.isCopyProtectionEnabled
|
||||||
@ -1487,7 +1514,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
translateToLanguage = languageCode
|
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(
|
let filteredEntries = chatHistoryEntriesForView(
|
||||||
location: chatLocation,
|
location: chatLocation,
|
||||||
|
|||||||
@ -565,7 +565,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
|||||||
var loadStickerSaveStatus: MediaId?
|
var loadStickerSaveStatus: MediaId?
|
||||||
var loadCopyMediaResource: MediaResource?
|
var loadCopyMediaResource: MediaResource?
|
||||||
var isAction = false
|
var isAction = false
|
||||||
var isGiveawayLaunch = false
|
var isGiveawayServiceMessage = false
|
||||||
var diceEmoji: String?
|
var diceEmoji: String?
|
||||||
if messages.count == 1 {
|
if messages.count == 1 {
|
||||||
for media in messages[0].media {
|
for media in messages[0].media {
|
||||||
@ -580,8 +580,13 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
|||||||
}
|
}
|
||||||
} else if media is TelegramMediaAction || media is TelegramMediaExpiredContent {
|
} else if media is TelegramMediaAction || media is TelegramMediaExpiredContent {
|
||||||
isAction = true
|
isAction = true
|
||||||
if let action = media as? TelegramMediaAction, case .giveawayLaunched = action.action {
|
if let action = media as? TelegramMediaAction {
|
||||||
isGiveawayLaunch = true
|
switch action.action {
|
||||||
|
case .giveawayLaunched, .giveawayResults:
|
||||||
|
isGiveawayServiceMessage = true
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if let image = media as? TelegramMediaImage {
|
} else if let image = media as? TelegramMediaImage {
|
||||||
if !messages[0].containsSecretMedia {
|
if !messages[0].containsSecretMedia {
|
||||||
@ -643,7 +648,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
|||||||
canPin = false
|
canPin = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if isGiveawayLaunch {
|
if isGiveawayServiceMessage {
|
||||||
canReply = false
|
canReply = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user