Merge commit 'ac6dc52c2582862be1d3de9ff67d4fa39e13aef5'

This commit is contained in:
Ali 2023-11-09 21:22:31 +04:00
commit ee4606ccf3
26 changed files with 1265 additions and 44 deletions

View File

@ -10464,3 +10464,14 @@ Sorry for the inconvenience.";
"BoostGift.StartConfirmation.Start" = "Start";
"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";

View File

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

View File

@ -241,7 +241,7 @@
SAtomic *context = [[SAtomic alloc] initWithValue:[TGMediaVideoConversionContext contextWithQueue:queue subscriber:subscriber]];
NSURL *outputUrl = [NSURL fileURLWithPath:path];
NSString *path = TGComponentsPathForResource(@"blank", @"mp4");
NSString *path = TGComponentsPathForResource(@"BlankVideo", @"m4v");
AVAsset *avAsset = [[AVURLAsset alloc] initWithURL:[NSURL fileURLWithPath:path] options:nil];
NSArray *requiredKeys = @[ @"tracks", @"duration", @"playable" ];

View File

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

View File

@ -490,6 +490,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-758129906] = { return Api.MessageAction.parse_messageActionGiftCode($0) }
dict[-935499028] = { return Api.MessageAction.parse_messageActionGiftPremium($0) }
dict[858499565] = { return Api.MessageAction.parse_messageActionGiveawayLaunch($0) }
dict[1927497572] = { return Api.MessageAction.parse_messageActionGiveawayResults($0) }
dict[2047704898] = { return Api.MessageAction.parse_messageActionGroupCall($0) }
dict[-1281329567] = { return Api.MessageAction.parse_messageActionGroupCallScheduled($0) }
dict[-1615153660] = { return Api.MessageAction.parse_messageActionHistoryClear($0) }

View File

@ -612,6 +612,7 @@ public extension Api {
case messageActionGiftCode(flags: Int32, boostPeer: Api.Peer?, months: Int32, slug: String)
case messageActionGiftPremium(flags: Int32, currency: String, amount: Int64, months: Int32, cryptoCurrency: String?, cryptoAmount: Int64?)
case messageActionGiveawayLaunch
case messageActionGiveawayResults(winnersCount: Int32)
case messageActionGroupCall(flags: Int32, call: Api.InputGroupCall, duration: Int32?)
case messageActionGroupCallScheduled(call: Api.InputGroupCall, scheduleDate: Int32)
case messageActionHistoryClear
@ -778,6 +779,12 @@ public extension Api {
buffer.appendInt32(858499565)
}
break
case .messageActionGiveawayResults(let winnersCount):
if boxed {
buffer.appendInt32(1927497572)
}
serializeInt32(winnersCount, buffer: buffer, boxed: false)
break
case .messageActionGroupCall(let flags, let call, let duration):
if boxed {
@ -990,6 +997,8 @@ public extension Api {
return ("messageActionGiftPremium", [("flags", flags as Any), ("currency", currency as Any), ("amount", amount as Any), ("months", months as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any)])
case .messageActionGiveawayLaunch:
return ("messageActionGiveawayLaunch", [])
case .messageActionGiveawayResults(let winnersCount):
return ("messageActionGiveawayResults", [("winnersCount", winnersCount as Any)])
case .messageActionGroupCall(let flags, let call, let duration):
return ("messageActionGroupCall", [("flags", flags as Any), ("call", call as Any), ("duration", duration as Any)])
case .messageActionGroupCallScheduled(let call, let scheduleDate):
@ -1274,6 +1283,17 @@ public extension Api {
public static func parse_messageActionGiveawayLaunch(_ reader: BufferReader) -> MessageAction? {
return Api.MessageAction.messageActionGiveawayLaunch
}
public static func parse_messageActionGiveawayResults(_ reader: BufferReader) -> MessageAction? {
var _1: Int32?
_1 = reader.readInt32()
let _c1 = _1 != nil
if _c1 {
return Api.MessageAction.messageActionGiveawayResults(winnersCount: _1!)
}
else {
return nil
}
}
public static func parse_messageActionGroupCall(_ reader: BufferReader) -> MessageAction? {
var _1: Int32?
_1 = reader.readInt32()

View File

@ -2472,6 +2472,21 @@ public extension Api.functions.channels {
})
}
}
public extension Api.functions.channels {
static func getChannelRecommendations(channel: Api.InputChannel) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.Chats>) {
let buffer = Buffer()
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() {
result = Api.parse(reader, signature: signature) as? Api.messages.Chats
}
return result
})
}
}
public extension Api.functions.channels {
static func getChannels(id: [Api.InputChannel]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.Chats>) {
let buffer = Buffer()

View File

@ -217,7 +217,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] {
}
switch action {
case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionTopicCreate, .messageActionTopicEdit, .messageActionSuggestProfilePhoto, .messageActionSetChatWallPaper, .messageActionSetSameChatWallPaper, .messageActionGiveawayLaunch:
case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionTopicCreate, .messageActionTopicEdit, .messageActionSuggestProfilePhoto, .messageActionSetChatWallPaper, .messageActionSetSameChatWallPaper, .messageActionGiveawayLaunch, .messageActionGiveawayResults:
break
case let .messageActionChannelMigrateFrom(_, chatId):
result.append(PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId)))

View File

@ -131,6 +131,8 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
return TelegramMediaAction(action: .giftCode(slug: slug, fromGiveaway: (flags & (1 << 0)) != 0, isUnclaimed: (flags & (1 << 2)) != 0, boostPeerId: boostPeer?.peerId, months: months))
case .messageActionGiveawayLaunch:
return TelegramMediaAction(action: .giveawayLaunched)
case let .messageActionGiveawayResults(winners):
return TelegramMediaAction(action: .giveawayResults(winners: winners))
}
}

View File

@ -113,6 +113,7 @@ public struct Namespaces {
public static let storySendAsPeerIds: Int8 = 29
public static let cachedChannelBoosts: Int8 = 31
public static let displayedMessageNotifications: Int8 = 32
public static let recommendedChannels: Int8 = 33
}
public struct UnorderedItemList {

View File

@ -111,6 +111,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case setSameChatWallpaper(wallpaper: TelegramWallpaper)
case giftCode(slug: String, fromGiveaway: Bool, isUnclaimed: Bool, boostPeerId: PeerId?, months: Int32)
case giveawayLaunched
case joinedChannel
case giveawayResults(winners: Int32)
public init(decoder: PostboxDecoder) {
let rawValue: Int32 = decoder.decodeInt32ForKey("_rawValue", orElse: 0)
@ -209,6 +211,10 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
self = .giftCode(slug: decoder.decodeStringForKey("slug", orElse: ""), fromGiveaway: decoder.decodeBoolForKey("give", orElse: false), isUnclaimed: decoder.decodeBoolForKey("unclaimed", orElse: false), boostPeerId: PeerId(decoder.decodeInt64ForKey("pi", orElse: 0)), months: decoder.decodeInt32ForKey("months", orElse: 0))
case 37:
self = .giveawayLaunched
case 38:
self = .joinedChannel
case 39:
self = .giveawayResults(winners: decoder.decodeInt32ForKey("winners", orElse: 0))
default:
self = .unknown
}
@ -401,6 +407,11 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
encoder.encodeInt32(months, forKey: "months")
case .giveawayLaunched:
encoder.encodeInt32(37, forKey: "_rawValue")
case .joinedChannel:
encoder.encodeInt32(38, forKey: "_rawValue")
case let .giveawayResults(winners):
encoder.encodeInt32(39, forKey: "_rawValue")
encoder.encodeInt32(winners, forKey: "winners")
}
}

View File

@ -0,0 +1,131 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
final class CachedRecommendedChannels: Codable {
public let peerIds: [EnginePeer.Id]
public let isHidden: Bool
public init(peerIds: [EnginePeer.Id], isHidden: Bool) {
self.peerIds = peerIds
self.isHidden = isHidden
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
self.peerIds = try container.decode([Int64].self, forKey: "l").map(EnginePeer.Id.init)
self.isHidden = try container.decode(Bool.self, forKey: "h")
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self)
try container.encode(self.peerIds.map { $0.toInt64() }, forKey: "l")
try container.encode(self.isHidden, forKey: "h")
}
}
private func entryId(peerId: EnginePeer.Id) -> ItemCacheEntryId {
let cacheKey = ValueBoxKey(length: 8)
cacheKey.setInt64(0, value: peerId.toInt64())
return ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.recommendedChannels, key: cacheKey)
}
func _internal_requestRecommendedChannels(account: Account, peerId: EnginePeer.Id) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Peer? in
guard let channel = transaction.getPeer(peerId) else {
return nil
}
if let entry = transaction.retrieveItemCacheEntry(id: entryId(peerId: peerId))?.get(CachedRecommendedChannels.self), !entry.peerIds.isEmpty {
return nil
} else {
return channel
}
}
|> mapToSignal { channel in
guard let inputChannel = channel.flatMap(apiInputChannel) else {
return .complete()
}
return account.network.request(Api.functions.channels.getChannelRecommendations(channel: inputChannel))
|> retryRequest
|> mapToSignal { result -> Signal<Never, NoError> in
return account.postbox.transaction { transaction -> [EnginePeer] in
let chats: [Api.Chat]
let parsedPeers: AccumulatedPeers
switch result {
case let .chats(apiChats):
chats = apiChats
case let .chatsSlice(_, apiChats):
chats = apiChats
}
parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: [])
updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: parsedPeers)
var peers: [EnginePeer] = []
for chat in chats {
if let peer = transaction.getPeer(chat.peerId) {
peers.append(EnginePeer(peer))
if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _) = chat, let participantsCount = participantsCount {
transaction.updatePeerCachedData(peerIds: Set([peer.id]), update: { _, current in
var current = current as? CachedChannelData ?? CachedChannelData()
var participantsSummary = current.participantsSummary
participantsSummary.memberCount = participantsCount
current = current.withUpdatedParticipantsSummary(participantsSummary)
return current
})
}
}
}
if let entry = CodableEntry(CachedRecommendedChannels(peerIds: peers.map(\.id), isHidden: false)) {
transaction.putItemCacheEntry(id: entryId(peerId: peerId), entry: entry)
}
return peers
}
|> ignoreValues
}
}
}
public struct RecommendedChannels: Equatable {
public struct Channel: Equatable {
public let peer: EnginePeer
public let subscribers: Int32
}
public let channels: [Channel]
public let isHidden: Bool
}
func _internal_recommendedChannels(account: Account, peerId: EnginePeer.Id) -> Signal<RecommendedChannels?, NoError> {
let key = PostboxViewKey.cachedItem(entryId(peerId: peerId))
return account.postbox.combinedView(keys: [key])
|> mapToSignal { views -> Signal<RecommendedChannels?, NoError> in
guard let cachedChannels = (views.views[key] as? CachedItemView)?.value?.get(CachedRecommendedChannels.self) else {
return .single(nil)
}
return account.postbox.transaction { transaction -> RecommendedChannels? in
var channels: [RecommendedChannels.Channel] = []
for peerId in cachedChannels.peerIds {
if let peer = transaction.getPeer(peerId), let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData {
channels.append(RecommendedChannels.Channel(peer: EnginePeer(peer), subscribers: cachedData.participantsSummary.memberCount ?? 0))
}
}
return RecommendedChannels(channels: channels, isHidden: cachedChannels.isHidden)
}
}
}
func _internal_toggleRecommendedChannelsHidden(account: Account, peerId: EnginePeer.Id, hidden: Bool) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction in
if let cachedChannels = transaction.retrieveItemCacheEntry(id: entryId(peerId: peerId))?.get(CachedRecommendedChannels.self) {
if let entry = CodableEntry(CachedRecommendedChannels(peerIds: cachedChannels.peerIds, isHidden: hidden)) {
transaction.putItemCacheEntry(id: entryId(peerId: peerId), entry: entry)
}
}
}
|> ignoreValues
}

View File

@ -77,6 +77,11 @@ func _internal_joinChannel(account: Account, peerId: PeerId, hash: String?) -> S
|> castError(JoinChannelError.self)
}
}
|> afterCompleted {
if hash == nil {
let _ = _internal_requestRecommendedChannels(account: account, peerId: peerId).startStandalone()
}
}
} else {
return .fail(.generic)
}

View File

@ -1240,6 +1240,14 @@ public extension TelegramEngine {
public func applyChannelBoost(peerId: EnginePeer.Id, slots: [Int32]) -> Signal<MyBoostStatus?, NoError> {
return _internal_applyChannelBoost(account: self.account, peerId: peerId, slots: slots)
}
public func recommendedChannels(peerId: EnginePeer.Id) -> Signal<RecommendedChannels?, NoError> {
return _internal_recommendedChannels(account: self.account, peerId: peerId)
}
public func toggleRecommendedChannelsHidden(peerId: EnginePeer.Id, hidden: Bool) -> Signal<Never, NoError> {
return _internal_toggleRecommendedChannelsHidden(account: self.account, peerId: peerId, hidden: hidden)
}
}
}

View File

@ -903,6 +903,10 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
case .giveawayLaunched:
let resultTitleString = strings.Notification_GiveawayStarted(compactAuthorName)
attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
case .joinedChannel:
attributedString = NSAttributedString(string: strings.Notification_ChannelJoinedByYou, font: titleBoldFont, textColor: primaryTextColor)
case let .giveawayResults(winners):
attributedString = NSAttributedString(string: strings.Notification_GiveawayResults(winners), font: titleFont, textColor: primaryTextColor)
case .unknown:
attributedString = nil
}

View File

@ -227,11 +227,9 @@ public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
}
var backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: labelLayout.size.height + 4.0)
if let _ = image {
backgroundSize.height += imageSize.height + 10
}
return (backgroundSize.width, { boundingWidth in
return (backgroundSize, { [weak self] animation, synchronousLoads, _ in
if let strongSelf = self {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -791,7 +791,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return false
}
switch action.action {
case .pinnedMessageUpdated:
case .pinnedMessageUpdated, .gameScore, .setSameChatWallpaper, .giveawayResults:
for attribute in message.attributes {
if let attribute = attribute as? ReplyMessageAttribute {
strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote.flatMap { quote in NavigateToMessageParams.Quote(string: quote.text, offset: quote.offset) } : nil)))
@ -800,13 +800,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
case let .photoUpdated(image):
openMessageByAction = image != nil
case .gameScore:
for attribute in message.attributes {
if let attribute = attribute as? ReplyMessageAttribute {
strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote.flatMap { quote in NavigateToMessageParams.Quote(string: quote.text, offset: quote.offset) } : nil)))
break
}
}
case .groupPhoneCall, .inviteToGroupPhoneCall:
if let activeCall = strongSelf.presentationInterfaceState.activeGroupCallInfo?.activeCall {
strongSelf.joinGroupCall(peerId: message.id.peerId, invite: nil, activeCall: EngineGroupCallDescription(id: activeCall.id, accessHash: activeCall.accessHash, title: activeCall.title, scheduleTimestamp: activeCall.scheduleTimestamp, subscribedToScheduled: activeCall.subscribedToScheduled, isStream: activeCall.isStream))
@ -955,13 +948,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
strongSelf.push(wallpaperPreviewController)
return true
case .setSameChatWallpaper:
for attribute in message.attributes {
if let attribute = attribute as? ReplyMessageAttribute {
strongSelf.controllerInteraction?.navigateToMessage(message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: nil))
return true
}
}
case let .giftPremium(_, _, duration, _, _):
strongSelf.chatDisplayNode.dismissInput()
let fromPeerId: PeerId = message.author?.id == strongSelf.context.account.peerId ? strongSelf.context.account.peerId : message.id.peerId
@ -17819,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

View File

@ -54,7 +54,7 @@ func chatHistoryEntriesForView(
}
var joinMessage: Message?
if case let .peer(peerId) = location, case let cachedData = cachedData as? CachedChannelData, let invitedOn = cachedData?.invitedOn {
if case let .peer(peerId) = location, case let cachedData = cachedData as? CachedChannelData, let invitedOn = cachedData?.invitedOn {
joinMessage = Message(
stableId: UInt32.max - 1000,
stableVersion: 0,
@ -80,6 +80,32 @@ func chatHistoryEntriesForView(
associatedThreadInfo: nil,
associatedStories: [:]
)
} else if let peer = channelPeer as? TelegramChannel, case .broadcast = peer.info, case .member = peer.participationStatus {
joinMessage = Message(
stableId: UInt32.max - 1000,
stableVersion: 0,
id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Local, id: 0),
globallyUniqueId: nil,
groupingKey: nil,
groupInfo: nil,
threadId: nil,
timestamp: peer.creationDate,
flags: [.Incoming],
tags: [],
globalTags: [],
localTags: [],
forwardInfo: nil,
author: channelPeer,
text: "",
attributes: [],
media: [TelegramMediaAction(action: .joinedChannel)],
peers: SimpleDictionary<PeerId, Peer>(),
associatedMessages: SimpleDictionary<MessageId, Message>(),
associatedMessageIds: [],
associatedMedia: [:],
associatedThreadInfo: nil,
associatedStories: [:]
)
}
var existingGroupStableIds: [UInt32] = []

View File

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

View File

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

View File

@ -1097,8 +1097,8 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
self.panelNode.backgroundColor = .clear
}
self.panelNode.clipsToBounds = true
self.panelNode.cornerRadius = 9.0
self.panelNode.cornerRadius = 14.0
self.panelWrapperNode = ASDisplayNode()
self.effectView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
@ -1184,6 +1184,10 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
override func didLoad() {
super.didLoad()
if #available(iOS 13.0, *) {
self.panelNode.layer.cornerCurve = .continuous
}
self.panelNode.view.addSubview(self.effectView)
}