Initial ad messages implementation

This commit is contained in:
Ali 2021-08-24 19:40:25 +02:00
parent 01d708ef4d
commit 44562a845a
26 changed files with 930 additions and 38 deletions

View File

@ -443,7 +443,7 @@ public protocol PresentationGroupCall: AnyObject {
func playTone(_ tone: PresentationGroupCallTone)
func updateMuteState(peerId: PeerId, isMuted: Bool) -> GroupCallParticipantsContext.Participant.MuteState?
func setShouldBeRecording(_ shouldBeRecording: Bool, title: String?)
func setShouldBeRecording(_ shouldBeRecording: Bool, title: String?, videoOrientation: Bool?)
func updateTitle(_ title: String)

View File

@ -71,6 +71,10 @@ final class ItemCacheTable: Table {
self.valueBox.set(self.table, key: self.itemKey(id: id), value: encoder.readBufferNoCopy())
})
}
func putData(id: ItemCacheEntryId, entry: Data, metaTable: ItemCacheMetaTable) {
self.valueBox.set(self.table, key: self.itemKey(id: id), value: ReadBuffer(data: entry))
}
func retrieve(id: ItemCacheEntryId, metaTable: ItemCacheMetaTable) -> PostboxCoding? {
if let value = self.valueBox.get(self.table, key: self.itemKey(id: id)), let entry = PostboxDecoder(buffer: value).decodeRootObject() {
@ -78,6 +82,13 @@ final class ItemCacheTable: Table {
}
return nil
}
func retrieveData(id: ItemCacheEntryId, metaTable: ItemCacheMetaTable) -> Data? {
if let value = self.valueBox.get(self.table, key: self.itemKey(id: id)) {
return value.makeData()
}
return nil
}
func remove(id: ItemCacheEntryId, metaTable: ItemCacheMetaTable) {
self.valueBox.remove(self.table, key: self.itemKey(id: id), secure: false)

View File

@ -712,6 +712,11 @@ public final class Transaction {
assert(!self.disposed)
self.postbox?.putItemCacheEntry(id: id, entry: entry, collectionSpec: collectionSpec)
}
public func putItemCacheEntryData(id: ItemCacheEntryId, entry: Data, collectionSpec: ItemCacheCollectionSpec) {
assert(!self.disposed)
self.postbox?.putItemCacheEntryData(id: id, entry: entry, collectionSpec: collectionSpec)
}
public func removeItemCacheEntry(id: ItemCacheEntryId) {
assert(!self.disposed)
@ -722,6 +727,11 @@ public final class Transaction {
assert(!self.disposed)
return self.postbox?.retrieveItemCacheEntry(id: id)
}
public func retrieveItemCacheEntryData(id: ItemCacheEntryId) -> Data? {
assert(!self.disposed)
return self.postbox?.retrieveItemCacheEntryData(id: id)
}
public func clearItemCacheCollection(collectionId: ItemCacheCollectionId) {
assert(!self.disposed)
@ -2332,10 +2342,19 @@ public final class Postbox {
self.itemCacheTable.put(id: id, entry: entry, metaTable: self.itemCacheMetaTable)
self.currentUpdatedCacheEntryKeys.insert(id)
}
fileprivate func putItemCacheEntryData(id: ItemCacheEntryId, entry: Data, collectionSpec: ItemCacheCollectionSpec) {
self.itemCacheTable.putData(id: id, entry: entry, metaTable: self.itemCacheMetaTable)
self.currentUpdatedCacheEntryKeys.insert(id)
}
func retrieveItemCacheEntry(id: ItemCacheEntryId) -> PostboxCoding? {
return self.itemCacheTable.retrieve(id: id, metaTable: self.itemCacheMetaTable)
}
func retrieveItemCacheEntryData(id: ItemCacheEntryId) -> Data? {
return self.itemCacheTable.retrieveData(id: id, metaTable: self.itemCacheMetaTable)
}
func clearItemCacheCollection(collectionId: ItemCacheCollectionId) {
return self.itemCacheTable.removeAll(collectionId: collectionId)

View File

@ -160,6 +160,12 @@ public func combineLatest<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, E>(
}, initialValues: [:], queue: queue)
}
public func combineLatest<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, E>(queue: Queue? = nil, _ s1: Signal<T1, E>, _ s2: Signal<T2, E>, _ s3: Signal<T3, E>, _ s4: Signal<T4, E>, _ s5: Signal<T5, E>, _ s6: Signal<T6, E>, _ s7: Signal<T7, E>, _ s8: Signal<T8, E>, _ s9: Signal<T9, E>, _ s10: Signal<T10, E>, _ s11: Signal<T11, E>, _ s12: Signal<T12, E>, _ s13: Signal<T13, E>) -> Signal<(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13), E> {
return combineLatestAny([signalOfAny(s1), signalOfAny(s2), signalOfAny(s3), signalOfAny(s4), signalOfAny(s5), signalOfAny(s6), signalOfAny(s7), signalOfAny(s8), signalOfAny(s9), signalOfAny(s10), signalOfAny(s11), signalOfAny(s12), signalOfAny(s13)], combine: { values in
return (values[0] as! T1, values[1] as! T2, values[2] as! T3, values[3] as! T4, values[4] as! T5, values[5] as! T6, values[6] as! T7, values[7] as! T8, values[8] as! T9, values[9] as! T10, values[10] as! T11, values[11] as! T12, values[12] as! T13)
}, initialValues: [:], queue: queue)
}
public func combineLatest<T, E>(queue: Queue? = nil, _ signals: [Signal<T, E>]) -> Signal<[T], E> {
if signals.count == 0 {
return single([T](), E.self)

View File

@ -477,6 +477,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-1626209256] = { return Api.ChatBannedRights.parse_chatBannedRights($0) }
dict[1968737087] = { return Api.InputClientProxy.parse_inputClientProxy($0) }
dict[649453030] = { return Api.messages.MessageEditData.parse_messageEditData($0) }
dict[1705297877] = { return Api.messages.SponsoredMessages.parse_sponsoredMessages($0) }
dict[-886477832] = { return Api.LabeledPrice.parse_labeledPrice($0) }
dict[-438840932] = { return Api.messages.ChatFull.parse_chatFull($0) }
dict[1578088377] = { return Api.messages.HistoryImportParsed.parse_historyImportParsed($0) }
@ -736,6 +737,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-1625153079] = { return Api.InputWebFileLocation.parse_inputWebFileGeoPointLocation($0) }
dict[-1275374751] = { return Api.EmojiLanguage.parse_emojiLanguage($0) }
dict[1601666510] = { return Api.MessageFwdHeader.parse_messageFwdHeader($0) }
dict[-160304943] = { return Api.SponsoredMessage.parse_sponsoredMessage($0) }
dict[-1012849566] = { return Api.BaseTheme.parse_baseThemeClassic($0) }
dict[-69724536] = { return Api.BaseTheme.parse_baseThemeDay($0) }
dict[-1212997976] = { return Api.BaseTheme.parse_baseThemeNight($0) }
@ -1236,6 +1238,8 @@ public struct Api {
_1.serialize(buffer, boxed)
case let _1 as Api.messages.MessageEditData:
_1.serialize(buffer, boxed)
case let _1 as Api.messages.SponsoredMessages:
_1.serialize(buffer, boxed)
case let _1 as Api.LabeledPrice:
_1.serialize(buffer, boxed)
case let _1 as Api.messages.ChatFull:
@ -1504,6 +1508,8 @@ public struct Api {
_1.serialize(buffer, boxed)
case let _1 as Api.MessageFwdHeader:
_1.serialize(buffer, boxed)
case let _1 as Api.SponsoredMessage:
_1.serialize(buffer, boxed)
case let _1 as Api.BaseTheme:
_1.serialize(buffer, boxed)
case let _1 as Api.help.Support:

View File

@ -915,6 +915,66 @@ public struct messages {
}
}
}
public enum SponsoredMessages: TypeConstructorDescription {
case sponsoredMessages(messages: [Api.SponsoredMessage], chats: [Api.Chat], users: [Api.User])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .sponsoredMessages(let messages, let chats, let users):
if boxed {
buffer.appendInt32(1705297877)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(messages.count))
for item in messages {
item.serialize(buffer, true)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(chats.count))
for item in chats {
item.serialize(buffer, true)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(users.count))
for item in users {
item.serialize(buffer, true)
}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .sponsoredMessages(let messages, let chats, let users):
return ("sponsoredMessages", [("messages", messages), ("chats", chats), ("users", users)])
}
}
public static func parse_sponsoredMessages(_ reader: BufferReader) -> SponsoredMessages? {
var _1: [Api.SponsoredMessage]?
if let _ = reader.readInt32() {
_1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.SponsoredMessage.self)
}
var _2: [Api.Chat]?
if let _ = reader.readInt32() {
_2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
}
var _3: [Api.User]?
if let _ = reader.readInt32() {
_3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
if _c1 && _c2 && _c3 {
return Api.messages.SponsoredMessages.sponsoredMessages(messages: _1!, chats: _2!, users: _3!)
}
else {
return nil
}
}
}
public enum ChatFull: TypeConstructorDescription {
case chatFull(fullChat: Api.ChatFull, chats: [Api.Chat], users: [Api.User])

View File

@ -19216,6 +19216,76 @@ public extension Api {
}
}
}
public enum SponsoredMessage: TypeConstructorDescription {
case sponsoredMessage(flags: Int32, randomId: Buffer, peerId: Api.Peer, fromId: Api.Peer, message: String, media: Api.MessageMedia?, entities: [Api.MessageEntity]?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .sponsoredMessage(let flags, let randomId, let peerId, let fromId, let message, let media, let entities):
if boxed {
buffer.appendInt32(-160304943)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeBytes(randomId, buffer: buffer, boxed: false)
peerId.serialize(buffer, true)
fromId.serialize(buffer, true)
serializeString(message, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {media!.serialize(buffer, true)}
if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261)
buffer.appendInt32(Int32(entities!.count))
for item in entities! {
item.serialize(buffer, true)
}}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .sponsoredMessage(let flags, let randomId, let peerId, let fromId, let message, let media, let entities):
return ("sponsoredMessage", [("flags", flags), ("randomId", randomId), ("peerId", peerId), ("fromId", fromId), ("message", message), ("media", media), ("entities", entities)])
}
}
public static func parse_sponsoredMessage(_ reader: BufferReader) -> SponsoredMessage? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Buffer?
_2 = parseBytes(reader)
var _3: Api.Peer?
if let signature = reader.readInt32() {
_3 = Api.parse(reader, signature: signature) as? Api.Peer
}
var _4: Api.Peer?
if let signature = reader.readInt32() {
_4 = Api.parse(reader, signature: signature) as? Api.Peer
}
var _5: String?
_5 = parseString(reader)
var _6: Api.MessageMedia?
if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() {
_6 = Api.parse(reader, signature: signature) as? Api.MessageMedia
} }
var _7: [Api.MessageEntity]?
if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() {
_7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self)
} }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = _4 != nil
let _c5 = _5 != nil
let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil
let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 {
return Api.SponsoredMessage.sponsoredMessage(flags: _1!, randomId: _2!, peerId: _3!, fromId: _4!, message: _5!, media: _6, entities: _7)
}
else {
return nil
}
}
}
public enum BaseTheme: TypeConstructorDescription {
case baseThemeClassic

View File

@ -5025,6 +5025,35 @@ public extension Api {
return result
})
}
public static func viewSponsoredMessage(channel: Api.InputChannel, randomId: Buffer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer()
buffer.appendInt32(-1095836780)
channel.serialize(buffer, true)
serializeBytes(randomId, buffer: buffer, boxed: false)
return (FunctionDescription(name: "channels.viewSponsoredMessage", parameters: [("channel", channel), ("randomId", randomId)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
let reader = BufferReader(buffer)
var result: Api.Bool?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.Bool
}
return result
})
}
public static func getSponsoredMessages(channel: Api.InputChannel) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.SponsoredMessages>) {
let buffer = Buffer()
buffer.appendInt32(-333377601)
channel.serialize(buffer, true)
return (FunctionDescription(name: "channels.getSponsoredMessages", parameters: [("channel", channel)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.SponsoredMessages? in
let reader = BufferReader(buffer)
var result: Api.messages.SponsoredMessages?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.messages.SponsoredMessages
}
return result
})
}
}
public struct payments {
public static func getPaymentForm(flags: Int32, peer: Api.InputPeer, msgId: Int32, themeParams: Api.DataJSON?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.payments.PaymentForm>) {
@ -8155,13 +8184,14 @@ public extension Api {
})
}
public static func toggleGroupCallRecord(flags: Int32, call: Api.InputGroupCall, title: String?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
public static func toggleGroupCallRecord(flags: Int32, call: Api.InputGroupCall, title: String?, videoPortrait: Api.Bool?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
let buffer = Buffer()
buffer.appendInt32(-1070962985)
buffer.appendInt32(-248985848)
serializeInt32(flags, buffer: buffer, boxed: false)
call.serialize(buffer, true)
if Int(flags) & Int(1 << 1) != 0 {serializeString(title!, buffer: buffer, boxed: false)}
return (FunctionDescription(name: "phone.toggleGroupCallRecord", parameters: [("flags", flags), ("call", call), ("title", title)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
if Int(flags) & Int(1 << 2) != 0 {videoPortrait!.serialize(buffer, true)}
return (FunctionDescription(name: "phone.toggleGroupCallRecord", parameters: [("flags", flags), ("call", call), ("title", title), ("videoPortrait", videoPortrait)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
let reader = BufferReader(buffer)
var result: Api.Updates?
if let signature = reader.readInt32() {

View File

@ -2859,14 +2859,14 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
}
public func setShouldBeRecording(_ shouldBeRecording: Bool, title: String?) {
public func setShouldBeRecording(_ shouldBeRecording: Bool, title: String?, videoOrientation: Bool?) {
if !self.stateValue.canManageCall {
return
}
if (self.stateValue.recordingStartTimestamp != nil) == shouldBeRecording {
return
}
self.participantsContext?.updateShouldBeRecording(shouldBeRecording, title: title)
self.participantsContext?.updateShouldBeRecording(shouldBeRecording, title: title, videoOrientation: videoOrientation)
}
private func requestCall(movingFromBroadcastToRtc: Bool) {

View File

@ -2551,7 +2551,7 @@ public final class VoiceChatController: ViewController {
let alertController = textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: strongSelf.presentationData.strings.VoiceChat_StopRecordingTitle, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.VoiceChat_StopRecordingStop, action: {
if let strongSelf = self {
strongSelf.call.setShouldBeRecording(false, title: nil)
strongSelf.call.setShouldBeRecording(false, title: nil, videoOrientation: nil)
strongSelf.presentUndoOverlay(content: .forward(savedMessages: true, text: strongSelf.presentationData.strings.VoiceChat_RecordingSaved), action: { [weak self] value in
if case .info = value, let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController {
@ -2581,9 +2581,9 @@ public final class VoiceChatController: ViewController {
return
}
let controller = VoiceChatRecordingSetupController(context: strongSelf.context, completion: { [weak self] in
let controller = VoiceChatRecordingSetupController(context: strongSelf.context, completion: { [weak self] videoOrientation in
if let strongSelf = self {
strongSelf.call.setShouldBeRecording(true, title: "")
strongSelf.call.setShouldBeRecording(true, title: "", videoOrientation: videoOrientation)
strongSelf.presentUndoOverlay(content: .voiceChatRecording(text: strongSelf.presentationData.strings.VoiceChat_RecordingStarted), action: { _ in return false })
strongSelf.call.playTone(.recordingStarted)

View File

@ -18,13 +18,13 @@ final class VoiceChatRecordingSetupController: ViewController {
}
private let context: AccountContext
private let completion: () -> Void
private let completion: (Bool?) -> Void
private var animatedIn = false
private var presentationDataDisposable: Disposable?
init(context: AccountContext, completion: @escaping () -> Void) {
init(context: AccountContext, completion: @escaping (Bool?) -> Void) {
self.context = context
self.completion = completion
@ -54,8 +54,8 @@ final class VoiceChatRecordingSetupController: ViewController {
override public func loadDisplayNode() {
self.displayNode = VoiceChatRecordingSetupControllerNode(controller: self, context: self.context)
self.controllerNode.completion = { [weak self] in
self?.completion()
self.controllerNode.completion = { [weak self] videoOrientation in
self?.completion(videoOrientation)
}
self.controllerNode.dismiss = { [weak self] in
self?.presentingViewController?.dismiss(animated: false, completion: nil)
@ -143,7 +143,7 @@ private class VoiceChatRecordingSetupControllerNode: ViewControllerTracingNode,
private var mediaMode: MediaMode = .videoAndAudio
private var videoMode: VideoMode = .portrait
var completion: (() -> Void)?
var completion: ((Bool?) -> Void)?
var dismiss: (() -> Void)?
var cancel: (() -> Void)?
@ -304,7 +304,19 @@ private class VoiceChatRecordingSetupControllerNode: ViewControllerTracingNode,
}
@objc private func donePressed() {
self.completion?()
let videoOrientation: Bool?
switch self.mediaMode {
case .audioOnly:
videoOrientation = nil
case .videoAndAudio:
switch self.videoMode {
case .portrait:
videoOrientation = true
case .landscape:
videoOrientation = false
}
}
self.completion?(videoOrientation)
self.dismiss?()
}

View File

@ -0,0 +1,18 @@
import Foundation
import Postbox
public final class AdMessageAttribute: MessageAttribute {
public let opaqueId: Data
public init(opaqueId: Data) {
self.opaqueId = opaqueId
}
public init(decoder: PostboxDecoder) {
preconditionFailure()
}
public func encode(_ encoder: PostboxEncoder) {
preconditionFailure()
}
}

View File

@ -76,6 +76,7 @@ public struct Namespaces {
public static let cachedPeerInvitationImporters: Int8 = 12
public static let cachedPeerExportedInvitations: Int8 = 13
public static let cachedGroupCallDisplayAsPeers: Int8 = 14
public static let cachedAdMessageStates: Int8 = 15
}
public struct UnorderedItemList {

View File

@ -1981,7 +1981,7 @@ public final class GroupCallParticipantsContext {
self.updateMuteState(peerId: self.myPeerId, muteState: nil, volume: nil, raiseHand: false)
}
public func updateShouldBeRecording(_ shouldBeRecording: Bool, title: String?) {
public func updateShouldBeRecording(_ shouldBeRecording: Bool, title: String?, videoOrientation: Bool?) {
var flags: Int32 = 0
if shouldBeRecording {
flags |= 1 << 0
@ -1989,7 +1989,13 @@ public final class GroupCallParticipantsContext {
if let title = title, !title.isEmpty {
flags |= (1 << 1)
}
self.updateShouldBeRecordingDisposable.set((self.account.network.request(Api.functions.phone.toggleGroupCallRecord(flags: flags, call: .inputGroupCall(id: self.id, accessHash: self.accessHash), title: title))
var videoPortrait: Api.Bool?
if let videoOrientation = videoOrientation {
flags |= (1 << 2)
videoPortrait = videoOrientation ? .boolTrue : .boolFalse
}
self.updateShouldBeRecordingDisposable.set((self.account.network.request(Api.functions.phone.toggleGroupCallRecord(flags: flags, call: .inputGroupCall(id: self.id, accessHash: self.accessHash), title: title, videoPortrait: videoPortrait))
|> deliverOnMainQueue).start(next: { [weak self] updates in
guard let strongSelf = self else {
return

View File

@ -0,0 +1,406 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
private class AdMessagesHistoryContextImpl {
final class CachedMessage: Equatable, Codable {
enum CodingKeys: String, CodingKey {
case opaqueId
case text
case textEntities
case media
case authorId
}
public let opaqueId: Data
public let text: String
public let textEntities: [MessageTextEntity]
public let media: [Media]
public let authorId: PeerId
public init(
opaqueId: Data,
text: String,
textEntities: [MessageTextEntity],
media: [Media],
authorId: PeerId
) {
self.opaqueId = opaqueId
self.text = text
self.textEntities = textEntities
self.media = media
self.authorId = authorId
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.opaqueId = try container.decode(Data.self, forKey: .opaqueId)
self.text = try container.decode(String.self, forKey: .text)
self.textEntities = try container.decode([MessageTextEntity].self, forKey: .textEntities)
let mediaData = try container.decode([Data].self, forKey: .media)
self.media = mediaData.compactMap { data -> Media? in
return PostboxDecoder(buffer: MemoryBuffer(data: data)).decodeRootObject() as? Media
}
self.authorId = try container.decode(PeerId.self, forKey: .authorId)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.opaqueId, forKey: .opaqueId)
try container.encode(self.text, forKey: .text)
try container.encode(self.textEntities, forKey: .textEntities)
let mediaData = self.media.map { media -> Data in
let encoder = PostboxEncoder()
encoder.encodeRootObject(media)
return encoder.makeData()
}
try container.encode(mediaData, forKey: .media)
try container.encode(self.authorId, forKey: .authorId)
}
public static func ==(lhs: CachedMessage, rhs: CachedMessage) -> Bool {
if lhs.opaqueId != rhs.opaqueId {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.textEntities != rhs.textEntities {
return false
}
if lhs.media.count != rhs.media.count {
return false
}
for i in 0 ..< lhs.media.count {
if !lhs.media[i].isEqual(to: rhs.media[i]) {
return false
}
}
if lhs.authorId != rhs.authorId {
return false
}
return true
}
func toMessage(peerId: PeerId, transaction: Transaction) -> Message {
var attributes: [MessageAttribute] = []
attributes.append(AdMessageAttribute(opaqueId: self.opaqueId))
if !self.textEntities.isEmpty {
let attribute = TextEntitiesMessageAttribute(entities: self.textEntities)
attributes.append(attribute)
}
var messagePeers = SimpleDictionary<PeerId, Peer>()
if let peer = transaction.getPeer(peerId) {
messagePeers[peer.id] = peer
}
if let peer = transaction.getPeer(self.authorId) {
messagePeers[peer.id] = peer
}
return Message(
stableId: 0,
stableVersion: 0,
id: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: 0),
globallyUniqueId: nil,
groupingKey: nil,
groupInfo: nil,
threadId: nil,
timestamp: Int32.max - 1,
flags: [.Incoming],
tags: [],
globalTags: [],
localTags: [],
forwardInfo: nil,
author: transaction.getPeer(self.authorId),
text: self.text,
attributes: attributes,
media: self.media,
peers: messagePeers,
associatedMessages: SimpleDictionary<MessageId, Message>(),
associatedMessageIds: []
)
}
}
private let queue: Queue
private let account: Account
private let peerId: PeerId
private let maskAsSeenDisposables = DisposableDict<Data>()
struct CachedState: Codable, PostboxCoding {
enum CodingKeys: String, CodingKey {
case timestamp
case messages
}
var timestamp: Int32
var messages: [CachedMessage]
init(timestamp: Int32, messages: [CachedMessage]) {
self.timestamp = timestamp
self.messages = messages
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.timestamp = try container.decode(Int32.self, forKey: .timestamp)
self.messages = try container.decode([CachedMessage].self, forKey: .messages)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.timestamp, forKey: .timestamp)
try container.encode(self.messages, forKey: .messages)
}
init(decoder: PostboxDecoder) {
self.timestamp = decoder.decodeInt32ForKey("timestamp", orElse: 0)
if let messagesData = decoder.decodeOptionalDataArrayForKey("messages") {
self.messages = messagesData.compactMap { data -> CachedMessage? in
return try? AdaptedPostboxDecoder().decode(CachedMessage.self, from: data)
}
} else {
self.messages = []
}
}
func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt32(self.timestamp, forKey: "timestamp")
encoder.encodeDataArray(self.messages.compactMap { message -> Data? in
return try? AdaptedPostboxEncoder().encode(message)
}, forKey: "messages")
}
private static let collectionSpec = ItemCacheCollectionSpec(lowWaterItemCount: 5, highWaterItemCount: 10)
public static func getCached(postbox: Postbox, peerId: PeerId) -> Signal<CachedState?, NoError> {
return postbox.transaction { transaction -> CachedState? in
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: peerId.toInt64())
if let entry = transaction.retrieveItemCacheEntryData(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedAdMessageStates, key: key)) {
return try? AdaptedPostboxDecoder().decode(CachedState.self, from: entry)
} else {
return nil
}
}
}
public static func setCached(transaction: Transaction, peerId: PeerId, state: CachedState?) {
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: peerId.toInt64())
let id = ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedAdMessageStates, key: key)
if let state = state, let stateData = try? AdaptedPostboxEncoder().encode(state) {
transaction.putItemCacheEntryData(id: id, entry: stateData, collectionSpec: collectionSpec)
} else {
transaction.removeItemCacheEntry(id: id)
}
}
}
struct State: Equatable {
var messages: [Message]
static func ==(lhs: State, rhs: State) -> Bool {
if lhs.messages.count != rhs.messages.count {
return false
}
for i in 0 ..< lhs.messages.count {
if lhs.messages[i].id != rhs.messages[i].id {
return false
}
if lhs.messages[i].stableId != rhs.messages[i].stableId {
return false
}
}
return true
}
}
let state = Promise<State>()
private var stateValue: State? {
didSet {
if let stateValue = self.stateValue, stateValue != oldValue {
self.state.set(.single(stateValue))
}
}
}
private let disposable = MetaDisposable()
init(queue: Queue, account: Account, peerId: PeerId) {
self.queue = queue
self.account = account
self.peerId = peerId
self.stateValue = State(messages: [])
self.state.set(CachedState.getCached(postbox: account.postbox, peerId: peerId)
|> mapToSignal { cachedState -> Signal<State, NoError> in
if let cachedState = cachedState, cachedState.timestamp >= Int32(Date().timeIntervalSince1970) - 5 * 60 {
return account.postbox.transaction { transaction -> State in
return State(messages: cachedState.messages.map { message in
return message.toMessage(peerId: peerId, transaction: transaction)
})
}
} else {
return .single(State(messages: []))
}
})
let signal: Signal<[Message], NoError> = account.postbox.transaction { transaction -> Api.InputChannel? in
return transaction.getPeer(peerId).flatMap(apiInputChannel)
}
|> mapToSignal { inputChannel -> Signal<[Message], NoError> in
guard let inputChannel = inputChannel else {
return .single([])
}
return account.network.request(Api.functions.channels.getSponsoredMessages(channel: inputChannel))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.SponsoredMessages?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<[Message], NoError> in
guard let result = result else {
return .single([])
}
return account.postbox.transaction { transaction -> [Message] in
switch result {
case let .sponsoredMessages(messages, chats, users):
var peers: [Peer] = []
var peerPresences: [PeerId: PeerPresence] = [:]
for chat in chats {
if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) {
peers.append(groupOrChannel)
}
}
for user in users {
let telegramUser = TelegramUser(user: user)
peers.append(telegramUser)
if let presence = TelegramUserPresence(apiUser: user) {
peerPresences[telegramUser.id] = presence
}
}
updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in
return updated
})
updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences)
var parsedMessages: [CachedMessage] = []
for message in messages {
switch message {
case let .sponsoredMessage(_, randomId, _, fromId, message, media, entities):
var parsedEntities: [MessageTextEntity] = []
if let entities = entities {
parsedEntities = messageTextEntitiesFromApiEntities(entities)
}
var parsedMedia: [Media] = []
if let media = media {
let (mediaValue, _) = textMediaAndExpirationTimerFromApiMedia(media, peerId)
if let mediaValue = mediaValue {
parsedMedia.append(mediaValue)
}
}
parsedMessages.append(CachedMessage(
opaqueId: randomId.makeData(),
text: message,
textEntities: parsedEntities,
media: parsedMedia, authorId: fromId.peerId
))
}
}
CachedState.setCached(transaction: transaction, peerId: peerId, state: CachedState(timestamp: Int32(Date().timeIntervalSince1970), messages: parsedMessages))
return parsedMessages.map { message in
return message.toMessage(peerId: peerId, transaction: transaction)
}
}
}
}
}
self.disposable.set((signal
|> deliverOn(self.queue)).start(next: { [weak self] messages in
guard let strongSelf = self else {
return
}
strongSelf.stateValue = State(messages: messages)
}))
}
deinit {
self.disposable.dispose()
self.maskAsSeenDisposables.dispose()
}
func markAsSeen(opaqueId: Data) {
let signal: Signal<Never, NoError> = account.postbox.transaction { transaction -> Api.InputChannel? in
return transaction.getPeer(self.peerId).flatMap(apiInputChannel)
}
|> mapToSignal { inputChannel -> Signal<Never, NoError> in
guard let inputChannel = inputChannel else {
return .complete()
}
return self.account.network.request(Api.functions.channels.viewSponsoredMessage(channel: inputChannel, randomId: Buffer(data: opaqueId)))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> ignoreValues
}
self.maskAsSeenDisposables.set(signal.start(), forKey: opaqueId)
}
}
public class AdMessagesHistoryContext {
private let queue = Queue()
private let impl: QueueLocalObject<AdMessagesHistoryContextImpl>
public var state: Signal<[Message], NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
let stateDisposable = impl.state.get().start(next: { state in
subscriber.putNext(state.messages)
})
disposable.set(stateDisposable)
}
return disposable
}
}
public init(account: Account, peerId: PeerId) {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return AdMessagesHistoryContextImpl(queue: queue, account: account, peerId: peerId)
})
}
public func markAsSeen(opaqueId: Data) {
self.impl.with { impl in
impl.markAsSeen(opaqueId: opaqueId)
}
}
}

View File

@ -199,5 +199,9 @@ public extension TelegramEngine {
)
}
}
public func adMessages(peerId: PeerId) -> AdMessagesHistoryContext {
return AdMessagesHistoryContext(account: self.account, peerId: peerId)
}
}
}

View File

@ -291,3 +291,14 @@ public extension Message {
}
}
public extension Message {
var adAttribute: AdMessageAttribute? {
for attribute in self.attributes {
if let attribute = attribute as? AdMessageAttribute {
return attribute
}
}
return nil
}
}

View File

@ -7,7 +7,26 @@ import AccountContext
import TelegramPresentationData
func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView, includeUnreadEntry: Bool, includeEmptyEntry: Bool, includeChatInfoEntry: Bool, includeSearchEntry: Bool, reverse: Bool, groupMessages: Bool, selectedMessages: Set<MessageId>?, presentationData: ChatPresentationData, historyAppearsCleared: Bool, pendingUnpinnedAllMessages: Bool, pendingRemovedMessages: Set<MessageId>, associatedData: ChatMessageItemAssociatedData, updatingMedia: [MessageId: ChatUpdatingMessageMedia], customChannelDiscussionReadState: MessageId?, customThreadOutgoingReadState: MessageId?) -> [ChatHistoryEntry] {
func chatHistoryEntriesForView(
location: ChatLocation,
view: MessageHistoryView,
includeUnreadEntry: Bool,
includeEmptyEntry: Bool,
includeChatInfoEntry: Bool,
includeSearchEntry: Bool,
reverse: Bool,
groupMessages: Bool,
selectedMessages: Set<MessageId>?,
presentationData: ChatPresentationData,
historyAppearsCleared: Bool,
pendingUnpinnedAllMessages: Bool,
pendingRemovedMessages: Set<MessageId>,
associatedData: ChatMessageItemAssociatedData,
updatingMedia: [MessageId: ChatUpdatingMessageMedia],
customChannelDiscussionReadState: MessageId?,
customThreadOutgoingReadState: MessageId?,
adMessages: [Message]
) -> [ChatHistoryEntry] {
if historyAppearsCleared {
return []
}
@ -238,6 +257,39 @@ func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView,
}
}
}
if view.laterId == nil && !view.isLoading {
if !entries.isEmpty, case let .MessageEntry(lastMessage, _, _, _, _, _) = entries[entries.count - 1], !adMessages.isEmpty {
var nextAdMessageId: Int32 = 1
for message in adMessages {
let updatedMessage = Message(
stableId: UInt32.max - 1 - UInt32(nextAdMessageId),
stableVersion: message.stableVersion,
id: MessageId(peerId: message.id.peerId, namespace: message.id.namespace, id: nextAdMessageId),
globallyUniqueId: nil,
groupingKey: nil,
groupInfo: nil,
threadId: nil,
timestamp: lastMessage.timestamp,
flags: message.flags,
tags: message.tags,
globalTags: message.globalTags,
localTags: message.localTags,
forwardInfo: message.forwardInfo,
author: message.author,
text: message.text,
attributes: message.attributes,
media: message.media,
peers: message.peers,
associatedMessages: message.associatedMessages,
associatedMessageIds: message.associatedMessageIds
)
nextAdMessageId += 1
entries.append(.MessageEntry(updatedMessage, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false)))
}
}
}
} else if includeSearchEntry {
if view.laterId == nil {
if !view.entries.isEmpty {

View File

@ -469,6 +469,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
private let galleryHiddenMesageAndMediaDisposable = MetaDisposable()
private let messageProcessingManager = ChatMessageThrottledProcessingManager()
private let adSeenProcessingManager = ChatMessageThrottledProcessingManager()
private let messageReactionsProcessingManager = ChatMessageThrottledProcessingManager()
private let seenLiveLocationProcessingManager = ChatMessageThrottledProcessingManager()
private let unsupportedMessageProcessingManager = ChatMessageThrottledProcessingManager()
@ -556,6 +557,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
private var freezeOverscrollControl: Bool = false
private var feedback: HapticFeedback?
var openNextChannelToRead: ((EnginePeer, TelegramEngine.NextUnreadChannelLocation) -> Void)?
private let adMessagesContext: AdMessagesHistoryContext?
private let clientId: Atomic<Int32>
@ -581,6 +584,16 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
self.chatPresentationDataPromise = Promise(self.currentPresentationData)
self.prefetchManager = InChatPrefetchManager(context: context)
let adMessages: Signal<[Message], NoError>
if case .bubbles = mode, case let .peer(peerId) = chatLocation, case .none = subject {
let adMessagesContext = context.engine.messages.adMessages(peerId: peerId)
self.adMessagesContext = adMessagesContext
adMessages = adMessagesContext.state
} else {
self.adMessagesContext = nil
adMessages = .single([])
}
let clientId = Atomic<Int32>(value: nextClientId)
self.clientId = clientId
@ -606,6 +619,16 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
self.messageProcessingManager.process = { [weak context] messageIds in
context?.account.viewTracker.updateViewCountForMessageIds(messageIds: messageIds, clientId: clientId.with { $0 })
}
self.adSeenProcessingManager.process = { [weak self] messageIds in
guard let strongSelf = self, let adMessagesContext = strongSelf.adMessagesContext else {
return
}
for id in messageIds {
if let message = strongSelf.messageInCurrentHistoryView(id), let adAttribute = message.adAttribute {
adMessagesContext.markAsSeen(opaqueId: adAttribute.opaqueId)
}
}
}
self.messageReactionsProcessingManager.process = { [weak context] messageIds in
context?.account.viewTracker.updateReactionsForMessageIds(messageIds: messageIds)
}
@ -846,8 +869,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
animatedEmojiStickers,
customChannelDiscussionReadState,
customThreadOutgoingReadState,
self.currentlyPlayingMessageIdPromise.get()
).start(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, animatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, currentlyPlayingMessageId in
self.currentlyPlayingMessageIdPromise.get(),
adMessages
).start(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, animatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, currentlyPlayingMessageId, adMessages in
func applyHole() {
Queue.mainQueue().async {
if let strongSelf = self {
@ -932,7 +956,26 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, animatedEmojiStickers: animatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageId)
let filteredEntries = chatHistoryEntriesForView(location: chatLocation, view: view, includeUnreadEntry: mode == .bubbles, includeEmptyEntry: mode == .bubbles && tagMask == nil, includeChatInfoEntry: mode == .bubbles, includeSearchEntry: includeSearchEntry && tagMask != nil, reverse: reverse, groupMessages: mode == .bubbles, selectedMessages: selectedMessages, presentationData: chatPresentationData, historyAppearsCleared: historyAppearsCleared, pendingUnpinnedAllMessages: pendingUnpinnedAllMessages, pendingRemovedMessages: pendingRemovedMessages, associatedData: associatedData, updatingMedia: updatingMedia, customChannelDiscussionReadState: customChannelDiscussionReadState, customThreadOutgoingReadState: customThreadOutgoingReadState)
let filteredEntries = chatHistoryEntriesForView(
location: chatLocation,
view: view,
includeUnreadEntry: mode == .bubbles,
includeEmptyEntry: mode == .bubbles && tagMask == nil,
includeChatInfoEntry: mode == .bubbles,
includeSearchEntry: includeSearchEntry && tagMask != nil,
reverse: reverse,
groupMessages: mode == .bubbles,
selectedMessages: selectedMessages,
presentationData: chatPresentationData,
historyAppearsCleared: historyAppearsCleared,
pendingUnpinnedAllMessages: pendingUnpinnedAllMessages,
pendingRemovedMessages: pendingRemovedMessages,
associatedData: associatedData,
updatingMedia: updatingMedia,
customChannelDiscussionReadState: customChannelDiscussionReadState,
customThreadOutgoingReadState: customThreadOutgoingReadState,
adMessages: adMessages
)
let lastHeaderId = filteredEntries.last.flatMap { listMessageDateHeaderId(timestamp: $0.index.timestamp) } ?? 0
let processedView = ChatHistoryView(originalView: view, filteredEntries: filteredEntries, associatedData: associatedData, lastHeaderId: lastHeaderId, id: id, locationInput: update.2)
let previousValueAndVersion = previousView.swap((processedView, update.1, selectedMessages))
@ -1334,6 +1377,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
let toLaterRange = (historyView.filteredEntries.count - 1 - (visible.firstIndex - 1), historyView.filteredEntries.count - 1)
var messageIdsWithViewCount: [MessageId] = []
var messageIdsWithAds: [MessageId] = []
var messageIdsWithUpdateableReactions: [MessageId] = []
var messageIdsWithLiveLocation: [MessageId] = []
var messageIdsWithUnsupportedMedia: [MessageId] = []
@ -1360,6 +1404,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
if message.id.namespace == Namespaces.Message.Cloud {
messageIdsWithViewCount.append(message.id)
}
} else if attribute is AdMessageAttribute {
messageIdsWithAds.append(message.id)
} else if attribute is ReplyThreadMessageAttribute {
if message.id.namespace == Namespaces.Message.Cloud {
messageIdsWithViewCount.append(message.id)
@ -1529,6 +1575,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
if !messageIdsWithViewCount.isEmpty {
self.messageProcessingManager.add(messageIdsWithViewCount)
}
if !messageIdsWithAds.isEmpty {
self.adSeenProcessingManager.add(messageIdsWithAds)
}
if !messageIdsWithUpdateableReactions.isEmpty {
self.messageReactionsProcessingManager.add(messageIdsWithUpdateableReactions)
}

View File

@ -288,6 +288,72 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
guard let interfaceInteraction = interfaceInteraction, let controllerInteraction = controllerInteraction else {
return .single([])
}
if messages.count == 1, let _ = messages[0].adAttribute {
let message = messages[0]
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var actions: [ContextMenuItem] = []
//TODO:localize
actions.append(.action(ContextMenuActionItem(text: "What are sponsored\nmessages?", textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0)), badge: nil, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor)
}, iconSource: nil, action: { _, f in
f(.default)
})))
actions.append(.separator)
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuCopy, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
let copyTextWithEntities = {
var messageEntities: [MessageTextEntity]?
var restrictedText: String?
for attribute in message.attributes {
if let attribute = attribute as? TextEntitiesMessageAttribute {
messageEntities = attribute.entities
}
if let attribute = attribute as? RestrictedContentMessageAttribute {
restrictedText = attribute.platformText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) ?? ""
}
}
if let restrictedText = restrictedText {
storeMessageTextInPasteboard(restrictedText, entities: nil)
} else {
storeMessageTextInPasteboard(message.text, entities: messageEntities)
}
Queue.mainQueue().after(0.2, {
let content: UndoOverlayContent = .copy(text: chatPresentationInterfaceState.strings.Conversation_MessageCopied)
controllerInteraction.displayUndo(content)
})
}
copyTextWithEntities()
f(.default)
})))
if let author = message.author, let addressName = author.addressName {
let link = "https://t.me/\(addressName)"
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuCopyLink, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
UIPasteboard.general.string = link
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
Queue.mainQueue().after(0.2, {
controllerInteraction.displayUndo(.linkCopied(text: presentationData.strings.Conversation_LinkCopied))
})
f(.default)
})))
}
return .single(actions)
}
var loadStickerSaveStatus: MediaId?
var loadCopyMediaResource: MediaResource?

View File

@ -297,9 +297,9 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
let incoming = message.effectivelyIncoming(context.account.peerId)
var horizontalInsets = UIEdgeInsets(top: 0.0, left: 12.0, bottom: 0.0, right: 12.0)
var horizontalInsets = UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 10.0)
if displayLine {
horizontalInsets.left += 10.0
horizontalInsets.left += 12.0
}
var preferMediaBeforeText = false
@ -612,9 +612,11 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
return (initialWidth, { constrainedSize, position in
var insets = UIEdgeInsets(top: 0.0, left: horizontalInsets.left, bottom: 5.0, right: horizontalInsets.right)
var lineInsets = insets
switch position {
case .linear(.None, _):
insets.top += 8.0
lineInsets.top += 8.0 + 8.0
default:
break
}
@ -705,7 +707,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
boundingSize.width = max(boundingSize.width, videoLayout.contentSize.width + videoLayout.overflowLeft + videoLayout.overflowRight)
}
lineHeight += insets.top + insets.bottom
lineHeight += lineInsets.top + lineInsets.bottom
var imageApply: (() -> Void)?
if let inlineImageSize = inlineImageSize, let inlineImageDimensions = inlineImageDimensions {
@ -806,7 +808,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
var actionButtonSizeAndApply: ((CGSize, () -> ChatMessageAttachedContentButtonNode))?
if let continueActionButtonLayout = continueActionButtonLayout {
let (size, apply) = continueActionButtonLayout(boundingWidth - 13.0 - insets.right)
let (size, apply) = continueActionButtonLayout(boundingWidth - 12.0 - insets.right)
actionButtonSizeAndApply = (size, apply)
adjustedBoundingSize.width = max(adjustedBoundingSize.width, insets.left + size.width + insets.right)
adjustedBoundingSize.height += 7.0 + size.height
@ -976,7 +978,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
}
}
}
buttonNode.frame = CGRect(origin: CGPoint(x: 13.0, y: adjustedLineHeight - insets.top - insets.bottom - 2.0 + 6.0), size: size)
buttonNode.frame = CGRect(origin: CGPoint(x: 12.0, y: adjustedLineHeight - insets.top - insets.bottom - 2.0 + 6.0), size: size)
} else if let buttonNode = strongSelf.buttonNode {
buttonNode.removeFromSupernode()
strongSelf.buttonNode = nil

View File

@ -147,6 +147,12 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
break inner
}
}
if message.adAttribute != nil {
result.removeAll()
result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
}
if isUnsupportedMedia {
result.append((message, ChatMessageUnsupportedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
@ -177,6 +183,10 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.effectiveTopId == firstMessage.id {
hasDiscussion = false
}
if firstMessage.adAttribute != nil {
hasDiscussion = false
}
if hasDiscussion {
var canComment = false
@ -1171,6 +1181,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
ignoreForward = true
effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt32Value(Int32(clamping: authorSignature.persistentHashValue))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: UserInfoFlags())
displayAuthorInfo = !mergedTop.merged && incoming
} else if let _ = item.content.firstMessage.adAttribute, let author = item.content.firstMessage.author {
ignoreForward = true
effectiveAuthor = author
displayAuthorInfo = !mergedTop.merged && incoming
} else {
effectiveAuthor = firstMessage.author
@ -1287,6 +1301,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
if isPreview {
needShareButton = false
}
if item.content.firstMessage.adAttribute != nil {
needShareButton = false
}
var tmpWidth: CGFloat
if allowFullWidth {
@ -1549,7 +1566,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
}
if initialDisplayHeader && displayAuthorInfo {
if let peer = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case .broadcast = peer.info {
if let peer = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case .broadcast = peer.info, item.content.firstMessage.adAttribute == nil {
authorNameString = peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
authorNameColor = chatMessagePeerIdColors[Int(peer.id.id._internalGetInt32Value() % 7)]
} else if let effectiveAuthor = effectiveAuthor {

View File

@ -16,6 +16,9 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou
private let selectAll: Bool
var shouldBeDismissed: Signal<Bool, NoError> {
if self.message.adAttribute != nil {
return .single(false)
}
let viewKey = PostboxViewKey.messages(Set([self.message.id]))
return self.postbox.combinedView(keys: [viewKey])
|> map { views -> Bool in

View File

@ -63,17 +63,21 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
}
self.contentNode.activateAction = { [weak self] in
if let strongSelf = self, let item = strongSelf.item {
var webPageContent: TelegramMediaWebpageLoadedContent?
for media in item.message.media {
if let media = media as? TelegramMediaWebpage {
if case let .Loaded(content) = media.content {
webPageContent = content
if let _ = item.message.adAttribute, let author = item.message.author {
item.controllerInteraction.openPeer(author.id, .chat(textInputState: nil, subject: nil, peekData: nil), nil)
} else {
var webPageContent: TelegramMediaWebpageLoadedContent?
for media in item.message.media {
if let media = media as? TelegramMediaWebpage {
if case let .Loaded(content) = media.content {
webPageContent = content
}
break
}
break
}
}
if let webpage = webPageContent {
item.controllerInteraction.openUrl(webpage.url, false, nil, nil)
if let webpage = webPageContent {
item.controllerInteraction.openUrl(webpage.url, false, nil, nil)
}
}
}
}
@ -108,6 +112,8 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
var actionIcon: ChatMessageAttachedContentActionIcon?
var actionTitle: String?
var displayLine: Bool = true
if let webpage = webPageContent {
let type = websiteType(of: webpage.websiteName)
@ -297,9 +303,33 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
break
}
}
} else if let _ = item.message.adAttribute {
title = nil
subtitle = nil
text = item.message.text
for attribute in item.message.attributes {
if let attribute = attribute as? TextEntitiesMessageAttribute {
entities = attribute.entities
}
}
for media in item.message.media {
switch media {
case _ as TelegramMediaImage, _ as TelegramMediaFile:
mediaAndFlags = (media, ChatMessageAttachedContentNodeMediaFlags())
default:
break
}
}
if let author = item.message.author as? TelegramChannel, case .group = author.info {
actionTitle = item.presentationData.strings.Conversation_ViewGroup
} else {
actionTitle = item.presentationData.strings.Conversation_ViewChannel
}
displayLine = false
}
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, item.chatLocation, title, subtitle, text, entities, mediaAndFlags, badge, actionIcon, actionTitle, true, layoutConstants, preparePosition, constrainedSize)
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, item.chatLocation, title, subtitle, text, entities, mediaAndFlags, badge, actionIcon, actionTitle, displayLine, layoutConstants, preparePosition, constrainedSize)
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
@ -345,9 +375,17 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
}
override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
guard let item = self.item else {
return .none
}
if self.bounds.contains(point) {
let contentNodeFrame = self.contentNode.frame
let result = self.contentNode.tapActionAtPoint(point.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY), gesture: gesture, isEstimating: isEstimating)
if item.message.adAttribute != nil {
return result
}
switch result {
case .none:
break

View File

@ -29,6 +29,11 @@ private func dateStringForDay(strings: PresentationStrings, dateTimeFormat: Pres
}
func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Message, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, strings: PresentationStrings, format: MessageTimestampStatusFormat = .regular, reactionCount: Int) -> String {
if message.adAttribute != nil {
//TODO:localize
return "sponsored"
}
let timestamp: Int32
if let scheduleTime = message.scheduleTime {
timestamp = scheduleTime

@ -1 +1 @@
Subproject commit 6b986a40a333502288db5bcb9e1af7dfa042dcc9
Subproject commit 0704bb6d9e32f5d74bb2994b1dcfe49698c6c443