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"; "BoostGift.StartConfirmation.Start" = "Start";
"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_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 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
} }
} }

View File

@ -241,7 +241,7 @@
SAtomic *context = [[SAtomic alloc] initWithValue:[TGMediaVideoConversionContext contextWithQueue:queue subscriber:subscriber]]; SAtomic *context = [[SAtomic alloc] initWithValue:[TGMediaVideoConversionContext contextWithQueue:queue subscriber:subscriber]];
NSURL *outputUrl = [NSURL fileURLWithPath:path]; 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]; AVAsset *avAsset = [[AVURLAsset alloc] initWithURL:[NSURL fileURLWithPath:path] options:nil];
NSArray *requiredKeys = @[ @"tracks", @"duration", @"playable" ]; NSArray *requiredKeys = @[ @"tracks", @"duration", @"playable" ];

View File

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

View File

@ -490,6 +490,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-758129906] = { return Api.MessageAction.parse_messageActionGiftCode($0) } dict[-758129906] = { return Api.MessageAction.parse_messageActionGiftCode($0) }
dict[-935499028] = { return Api.MessageAction.parse_messageActionGiftPremium($0) } dict[-935499028] = { return Api.MessageAction.parse_messageActionGiftPremium($0) }
dict[858499565] = { return Api.MessageAction.parse_messageActionGiveawayLaunch($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[2047704898] = { return Api.MessageAction.parse_messageActionGroupCall($0) }
dict[-1281329567] = { return Api.MessageAction.parse_messageActionGroupCallScheduled($0) } dict[-1281329567] = { return Api.MessageAction.parse_messageActionGroupCallScheduled($0) }
dict[-1615153660] = { return Api.MessageAction.parse_messageActionHistoryClear($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 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 messageActionGiftPremium(flags: Int32, currency: String, amount: Int64, months: Int32, cryptoCurrency: String?, cryptoAmount: Int64?)
case messageActionGiveawayLaunch case messageActionGiveawayLaunch
case messageActionGiveawayResults(winnersCount: Int32)
case messageActionGroupCall(flags: Int32, call: Api.InputGroupCall, duration: Int32?) case messageActionGroupCall(flags: Int32, call: Api.InputGroupCall, duration: Int32?)
case messageActionGroupCallScheduled(call: Api.InputGroupCall, scheduleDate: Int32) case messageActionGroupCallScheduled(call: Api.InputGroupCall, scheduleDate: Int32)
case messageActionHistoryClear case messageActionHistoryClear
@ -778,6 +779,12 @@ public extension Api {
buffer.appendInt32(858499565) buffer.appendInt32(858499565)
} }
break
case .messageActionGiveawayResults(let winnersCount):
if boxed {
buffer.appendInt32(1927497572)
}
serializeInt32(winnersCount, buffer: buffer, boxed: false)
break break
case .messageActionGroupCall(let flags, let call, let duration): case .messageActionGroupCall(let flags, let call, let duration):
if boxed { 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)]) 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: case .messageActionGiveawayLaunch:
return ("messageActionGiveawayLaunch", []) return ("messageActionGiveawayLaunch", [])
case .messageActionGiveawayResults(let winnersCount):
return ("messageActionGiveawayResults", [("winnersCount", winnersCount as Any)])
case .messageActionGroupCall(let flags, let call, let duration): case .messageActionGroupCall(let flags, let call, let duration):
return ("messageActionGroupCall", [("flags", flags as Any), ("call", call as Any), ("duration", duration as Any)]) return ("messageActionGroupCall", [("flags", flags as Any), ("call", call as Any), ("duration", duration as Any)])
case .messageActionGroupCallScheduled(let call, let scheduleDate): case .messageActionGroupCallScheduled(let call, let scheduleDate):
@ -1274,6 +1283,17 @@ public extension Api {
public static func parse_messageActionGiveawayLaunch(_ reader: BufferReader) -> MessageAction? { public static func parse_messageActionGiveawayLaunch(_ reader: BufferReader) -> MessageAction? {
return Api.MessageAction.messageActionGiveawayLaunch 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? { public static func parse_messageActionGroupCall(_ reader: BufferReader) -> MessageAction? {
var _1: Int32? var _1: Int32?
_1 = reader.readInt32() _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 { public extension Api.functions.channels {
static func getChannels(id: [Api.InputChannel]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.Chats>) { static func getChannels(id: [Api.InputChannel]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.Chats>) {
let buffer = Buffer() let buffer = Buffer()

View File

@ -217,7 +217,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] {
} }
switch action { 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 break
case let .messageActionChannelMigrateFrom(_, chatId): case let .messageActionChannelMigrateFrom(_, chatId):
result.append(PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(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)) return TelegramMediaAction(action: .giftCode(slug: slug, fromGiveaway: (flags & (1 << 0)) != 0, isUnclaimed: (flags & (1 << 2)) != 0, boostPeerId: boostPeer?.peerId, months: months))
case .messageActionGiveawayLaunch: case .messageActionGiveawayLaunch:
return TelegramMediaAction(action: .giveawayLaunched) 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 storySendAsPeerIds: Int8 = 29
public static let cachedChannelBoosts: Int8 = 31 public static let cachedChannelBoosts: Int8 = 31
public static let displayedMessageNotifications: Int8 = 32 public static let displayedMessageNotifications: Int8 = 32
public static let recommendedChannels: Int8 = 33
} }
public struct UnorderedItemList { public struct UnorderedItemList {

View File

@ -111,6 +111,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case setSameChatWallpaper(wallpaper: TelegramWallpaper) case setSameChatWallpaper(wallpaper: TelegramWallpaper)
case giftCode(slug: String, fromGiveaway: Bool, isUnclaimed: Bool, boostPeerId: PeerId?, months: Int32) case giftCode(slug: String, fromGiveaway: Bool, isUnclaimed: Bool, boostPeerId: PeerId?, months: Int32)
case giveawayLaunched case giveawayLaunched
case joinedChannel
case giveawayResults(winners: Int32)
public init(decoder: PostboxDecoder) { public init(decoder: PostboxDecoder) {
let rawValue: Int32 = decoder.decodeInt32ForKey("_rawValue", orElse: 0) 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)) 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: case 37:
self = .giveawayLaunched self = .giveawayLaunched
case 38:
self = .joinedChannel
case 39:
self = .giveawayResults(winners: decoder.decodeInt32ForKey("winners", orElse: 0))
default: default:
self = .unknown self = .unknown
} }
@ -401,6 +407,11 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
encoder.encodeInt32(months, forKey: "months") encoder.encodeInt32(months, forKey: "months")
case .giveawayLaunched: case .giveawayLaunched:
encoder.encodeInt32(37, forKey: "_rawValue") 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) |> castError(JoinChannelError.self)
} }
} }
|> afterCompleted {
if hash == nil {
let _ = _internal_requestRecommendedChannels(account: account, peerId: peerId).startStandalone()
}
}
} else { } else {
return .fail(.generic) return .fail(.generic)
} }

View File

@ -1240,6 +1240,14 @@ public extension TelegramEngine {
public func applyChannelBoost(peerId: EnginePeer.Id, slots: [Int32]) -> Signal<MyBoostStatus?, NoError> { public func applyChannelBoost(peerId: EnginePeer.Id, slots: [Int32]) -> Signal<MyBoostStatus?, NoError> {
return _internal_applyChannelBoost(account: self.account, peerId: peerId, slots: slots) 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: case .giveawayLaunched:
let resultTitleString = strings.Notification_GiveawayStarted(compactAuthorName) let resultTitleString = strings.Notification_GiveawayStarted(compactAuthorName)
attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) 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: case .unknown:
attributedString = nil 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) var backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: labelLayout.size.height + 4.0)
if let _ = image { if let _ = image {
backgroundSize.height += imageSize.height + 10 backgroundSize.height += imageSize.height + 10
} }
return (backgroundSize.width, { boundingWidth in return (backgroundSize.width, { boundingWidth in
return (backgroundSize, { [weak self] animation, synchronousLoads, _ in return (backgroundSize, { [weak self] animation, synchronousLoads, _ in
if let strongSelf = self { if let strongSelf = self {

View File

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

View File

@ -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,16 +3946,26 @@ 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()

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

@ -791,7 +791,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return false return false
} }
switch action.action { switch action.action {
case .pinnedMessageUpdated: case .pinnedMessageUpdated, .gameScore, .setSameChatWallpaper, .giveawayResults:
for attribute in message.attributes { for attribute in message.attributes {
if let attribute = attribute as? ReplyMessageAttribute { 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))) 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): case let .photoUpdated(image):
openMessageByAction = image != nil 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: case .groupPhoneCall, .inviteToGroupPhoneCall:
if let activeCall = strongSelf.presentationInterfaceState.activeGroupCallInfo?.activeCall { 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)) 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) strongSelf.push(wallpaperPreviewController)
return true 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, _, _): case let .giftPremium(_, _, duration, _, _):
strongSelf.chatDisplayNode.dismissInput() strongSelf.chatDisplayNode.dismissInput()
let fromPeerId: PeerId = message.author?.id == strongSelf.context.account.peerId ? strongSelf.context.account.peerId : message.id.peerId let fromPeerId: PeerId = message.author?.id == strongSelf.context.account.peerId ? strongSelf.context.account.peerId : message.id.peerId
@ -17819,10 +17805,33 @@ 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 {
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() } }) strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() 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) 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
actionSheet?.dismissAnimated() actionSheet?.dismissAnimated()

View File

@ -80,6 +80,32 @@ func chatHistoryEntriesForView(
associatedThreadInfo: nil, associatedThreadInfo: nil,
associatedStories: [:] 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] = [] 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 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 {
@ -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,

View File

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

View File

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