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