diff --git a/submodules/TelegramApi/Sources/Api32.swift b/submodules/TelegramApi/Sources/Api32.swift index 492d41accf..b1a7a8b75f 100644 --- a/submodules/TelegramApi/Sources/Api32.swift +++ b/submodules/TelegramApi/Sources/Api32.swift @@ -2472,6 +2472,21 @@ public extension Api.functions.channels { }) } } +public extension Api.functions.channels { + static func getChannelRecommendations(channelId: Api.InputChannel) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-873707987) + channelId.serialize(buffer, true) + return (FunctionDescription(name: "channels.getChannelRecommendations", parameters: [("channelId", String(describing: channelId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.Chats? in + 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) { let buffer = Buffer() diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index 3c308396b0..7e578ea714 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -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 { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index 23ed143d9e..22d09a7aa0 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -111,6 +111,7 @@ 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 public init(decoder: PostboxDecoder) { let rawValue: Int32 = decoder.decodeInt32ForKey("_rawValue", orElse: 0) @@ -209,6 +210,8 @@ 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 default: self = .unknown } @@ -401,6 +404,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { encoder.encodeInt32(months, forKey: "months") case .giveawayLaunched: encoder.encodeInt32(37, forKey: "_rawValue") + case .joinedChannel: + encoder.encodeInt32(38, forKey: "_rawValue") } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelRecommendation.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelRecommendation.swift new file mode 100644 index 0000000000..621585ebb7 --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelRecommendation.swift @@ -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 { + 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(channelId: inputChannel)) + |> retryRequest + |> mapToSignal { result -> Signal 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 { + public struct Channel { + 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 { + let key = PostboxViewKey.cachedItem(entryId(peerId: peerId)) + return account.postbox.combinedView(keys: [key]) + |> mapToSignal { views -> Signal 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 { + 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 +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift index 2e7e672a4a..7c3d6b1f6e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift @@ -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) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index a03756c415..081916728b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -1240,6 +1240,14 @@ public extension TelegramEngine { public func applyChannelBoost(peerId: EnginePeer.Id, slots: [Int32]) -> Signal { return _internal_applyChannelBoost(account: self.account, peerId: peerId, slots: slots) } + + public func recommendedChannels(peerId: EnginePeer.Id) -> Signal { + return _internal_recommendedChannels(account: self.account, peerId: peerId) + } + + public func toggleRecommendedChannelsHidden(peerId: EnginePeer.Id, hidden: Bool) -> Signal { + return _internal_toggleRecommendedChannelsHidden(account: self.account, peerId: peerId, hidden: hidden) + } } } diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 6e0823ad61..8188ceae29 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -903,6 +903,8 @@ 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 .unknown: attributedString = nil } diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift index 2078f00d80..270c37e7e6 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift @@ -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(), + associatedMessages: SimpleDictionary(), + associatedMessageIds: [], + associatedMedia: [:], + associatedThreadInfo: nil, + associatedStories: [:] + ) } var existingGroupStableIds: [UInt32] = []