Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,708 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
private class AdMessagesHistoryContextImpl {
final class CachedMessage: Equatable, Codable {
enum CodingKeys: String, CodingKey {
case opaqueId
case messageType
case title
case text
case textEntities
case media
case contentMedia
case color
case backgroundEmojiId
case url
case buttonText
case sponsorInfo
case additionalInfo
case canReport
case minDisplayDuration
case maxDisplayDuration
}
enum MessageType: Int32, Codable {
case sponsored = 0
case recommended = 1
}
public let opaqueId: Data
public let messageType: MessageType
public let title: String
public let text: String
public let textEntities: [MessageTextEntity]
public let media: [Media]
public let contentMedia: [Media]
public let color: PeerNameColor?
public let backgroundEmojiId: Int64?
public let url: String
public let buttonText: String
public let sponsorInfo: String?
public let additionalInfo: String?
public let canReport: Bool
public let minDisplayDuration: Int32?
public let maxDisplayDuration: Int32?
public init(
opaqueId: Data,
messageType: MessageType,
title: String,
text: String,
textEntities: [MessageTextEntity],
media: [Media],
contentMedia: [Media],
color: PeerNameColor?,
backgroundEmojiId: Int64?,
url: String,
buttonText: String,
sponsorInfo: String?,
additionalInfo: String?,
canReport: Bool,
minDisplayDuration: Int32?,
maxDisplayDuration: Int32?
) {
self.opaqueId = opaqueId
self.messageType = messageType
self.title = title
self.text = text
self.textEntities = textEntities
self.media = media
self.contentMedia = contentMedia
self.color = color
self.backgroundEmojiId = backgroundEmojiId
self.url = url
self.buttonText = buttonText
self.sponsorInfo = sponsorInfo
self.additionalInfo = additionalInfo
self.canReport = canReport
self.minDisplayDuration = minDisplayDuration
self.maxDisplayDuration = maxDisplayDuration
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.opaqueId = try container.decode(Data.self, forKey: .opaqueId)
if let messageType = try container.decodeIfPresent(Int32.self, forKey: .messageType) {
self.messageType = MessageType(rawValue: messageType) ?? .sponsored
} else {
self.messageType = .sponsored
}
self.title = try container.decode(String.self, forKey: .title)
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
}
let contentMediaData = try container.decode([Data].self, forKey: .contentMedia)
self.contentMedia = contentMediaData.compactMap { data -> Media? in
return PostboxDecoder(buffer: MemoryBuffer(data: data)).decodeRootObject() as? Media
}
self.color = try container.decodeIfPresent(Int32.self, forKey: .color).flatMap { PeerNameColor(rawValue: $0) }
self.backgroundEmojiId = try container.decodeIfPresent(Int64.self, forKey: .backgroundEmojiId)
self.url = try container.decode(String.self, forKey: .url)
self.buttonText = try container.decode(String.self, forKey: .buttonText)
self.sponsorInfo = try container.decodeIfPresent(String.self, forKey: .sponsorInfo)
self.additionalInfo = try container.decodeIfPresent(String.self, forKey: .additionalInfo)
self.canReport = try container.decodeIfPresent(Bool.self, forKey: .canReport) ?? false
self.minDisplayDuration = try container.decodeIfPresent(Int32.self, forKey: .minDisplayDuration)
self.maxDisplayDuration = try container.decodeIfPresent(Int32.self, forKey: .maxDisplayDuration)
}
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.messageType.rawValue, forKey: .messageType)
try container.encode(self.title, forKey: .title)
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)
let contentMediaData = self.contentMedia.map { media -> Data in
let encoder = PostboxEncoder()
encoder.encodeRootObject(media)
return encoder.makeData()
}
try container.encode(contentMediaData, forKey: .contentMedia)
try container.encodeIfPresent(self.color?.rawValue, forKey: .color)
try container.encodeIfPresent(self.backgroundEmojiId, forKey: .backgroundEmojiId)
try container.encode(self.url, forKey: .url)
try container.encode(self.buttonText, forKey: .buttonText)
try container.encodeIfPresent(self.sponsorInfo, forKey: .sponsorInfo)
try container.encodeIfPresent(self.additionalInfo, forKey: .additionalInfo)
try container.encode(self.canReport, forKey: .canReport)
try container.encodeIfPresent(self.minDisplayDuration, forKey: .minDisplayDuration)
try container.encodeIfPresent(self.maxDisplayDuration, forKey: .maxDisplayDuration)
}
public static func ==(lhs: CachedMessage, rhs: CachedMessage) -> Bool {
if lhs.opaqueId != rhs.opaqueId {
return false
}
if lhs.messageType != rhs.messageType {
return false
}
if lhs.title != rhs.title {
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.contentMedia.count != rhs.contentMedia.count {
return false
}
for i in 0 ..< lhs.contentMedia.count {
if !lhs.contentMedia[i].isEqual(to: rhs.contentMedia[i]) {
return false
}
}
if lhs.url != rhs.url {
return false
}
if lhs.buttonText != rhs.buttonText {
return false
}
if lhs.sponsorInfo != rhs.sponsorInfo {
return false
}
if lhs.additionalInfo != rhs.additionalInfo {
return false
}
if lhs.canReport != rhs.canReport {
return false
}
if lhs.minDisplayDuration != rhs.minDisplayDuration {
return false
}
if lhs.maxDisplayDuration != rhs.maxDisplayDuration {
return false
}
return true
}
func toMessage(peerId: PeerId, transaction: Transaction) -> Message? {
var attributes: [MessageAttribute] = []
let mappedMessageType: AdMessageAttribute.MessageType
switch self.messageType {
case .sponsored:
mappedMessageType = .sponsored
case .recommended:
mappedMessageType = .recommended
}
let adAttribute = AdMessageAttribute(
opaqueId: self.opaqueId,
messageType: mappedMessageType,
url: self.url,
buttonText: self.buttonText,
sponsorInfo: self.sponsorInfo,
additionalInfo: self.additionalInfo,
canReport: self.canReport,
hasContentMedia: !self.contentMedia.isEmpty,
minDisplayDuration: self.minDisplayDuration,
maxDisplayDuration: self.maxDisplayDuration
)
attributes.append(adAttribute)
if !self.textEntities.isEmpty {
let entitiesAttribute = TextEntitiesMessageAttribute(entities: self.textEntities)
attributes.append(entitiesAttribute)
}
var messagePeers = SimpleDictionary<PeerId, Peer>()
if let peer = transaction.getPeer(peerId) {
messagePeers[peer.id] = peer
}
let author: Peer = TelegramChannel(
id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(1)),
accessHash: nil,
title: self.title,
username: nil,
photo: [],
creationDate: 0,
version: 0,
participationStatus: .left,
info: .broadcast(TelegramChannelBroadcastInfo(flags: [])),
flags: [],
restrictionInfo: nil,
adminRights: nil,
bannedRights: nil,
defaultBannedRights: nil,
usernames: [],
storiesHidden: nil,
nameColor: self.color ?? .blue,
backgroundEmojiId: self.backgroundEmojiId,
profileColor: nil,
profileBackgroundEmojiId: nil,
emojiStatus: nil,
approximateBoostLevel: nil,
subscriptionUntilDate: nil,
verificationIconFileId: nil,
sendPaidMessageStars: nil,
linkedMonoforumId: nil
)
messagePeers[author.id] = author
let messageHash = (self.text.hashValue &+ 31 &* peerId.hashValue) &* 31 &+ author.id.hashValue
let messageStableVersion = UInt32(bitPattern: Int32(truncatingIfNeeded: messageHash))
return Message(
stableId: 0,
stableVersion: messageStableVersion,
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: [],
customTags: [],
forwardInfo: nil,
author: author,
text: self.text,
attributes: attributes,
media: !self.contentMedia.isEmpty ? self.contentMedia : self.media,
peers: messagePeers,
associatedMessages: SimpleDictionary<MessageId, Message>(),
associatedMessageIds: [],
associatedMedia: [:],
associatedThreadInfo: nil,
associatedStories: [:]
)
}
}
private let queue: Queue
private let account: Account
private let peerId: EnginePeer.Id
private let messageId: EngineMessage.Id?
private let maskAsSeenDisposables = DisposableDict<Data>()
struct CachedState: Codable, PostboxCoding {
enum CodingKeys: String, CodingKey {
case timestamp
case interPostInterval
case messages
}
var timestamp: Int32
var interPostInterval: Int32?
var messages: [CachedMessage]
init(timestamp: Int32, interPostInterval: Int32?, messages: [CachedMessage]) {
self.timestamp = timestamp
self.interPostInterval = interPostInterval
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.interPostInterval = try container.decodeIfPresent(Int32.self, forKey: .interPostInterval)
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.encodeIfPresent(self.interPostInterval, forKey: .interPostInterval)
try container.encode(self.messages, forKey: .messages)
}
init(decoder: PostboxDecoder) {
self.timestamp = decoder.decodeInt32ForKey("timestamp", orElse: 0)
self.interPostInterval = decoder.decodeOptionalInt32ForKey("interPostInterval")
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")
if let interPostInterval = self.interPostInterval {
encoder.encodeInt32(interPostInterval, forKey: "interPostInterval")
} else {
encoder.encodeNil(forKey: "interPostInterval")
}
encoder.encodeDataArray(self.messages.compactMap { message -> Data? in
return try? AdaptedPostboxEncoder().encode(message)
}, forKey: "messages")
}
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.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedAdMessageStates, key: key))?.get(CachedState.self) {
return 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 entry = CodableEntry(state) {
transaction.putItemCacheEntry(id: id, entry: entry)
} else {
transaction.removeItemCacheEntry(id: id)
}
}
}
struct State: Equatable {
var interPostInterval: Int32?
var startDelay: Int32?
var betweenDelay: Int32?
var messages: [Message]
static func ==(lhs: State, rhs: State) -> Bool {
if lhs.interPostInterval != rhs.interPostInterval {
return false
}
if lhs.startDelay != rhs.startDelay {
return false
}
if lhs.betweenDelay != rhs.betweenDelay {
return false
}
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 var isActivated: Bool = false
private let disposable = MetaDisposable()
init(queue: Queue, account: Account, peerId: EnginePeer.Id, messageId: EngineMessage.Id?, activateManually: Bool) {
self.queue = queue
self.account = account
self.peerId = peerId
self.messageId = messageId
self.stateValue = State(interPostInterval: nil, messages: [])
if messageId == nil {
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(interPostInterval: cachedState.interPostInterval, messages: cachedState.messages.compactMap { message -> Message? in
return message.toMessage(peerId: peerId, transaction: transaction)
})
}
} else {
return .single(State(interPostInterval: nil, messages: []))
}
})
}
if !activateManually {
self.activate()
}
}
deinit {
self.disposable.dispose()
self.maskAsSeenDisposables.dispose()
}
func activate() {
if self.isActivated {
return
}
self.isActivated = true
let peerId = self.peerId
let accountPeerId = self.account.peerId
let account = self.account
let messageId = self.messageId
let signal: Signal<(interPostInterval: Int32?, startDelay: Int32?, betweenDelay: Int32?, messages: [Message]), NoError> = account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<(interPostInterval: Int32?, startDelay: Int32?, betweenDelay: Int32?, messages: [Message]), NoError> in
guard let inputPeer else {
return .single((nil, nil, nil, []))
}
var flags: Int32 = 0
if let _ = messageId {
flags |= (1 << 0)
}
return account.network.request(Api.functions.messages.getSponsoredMessages(flags: flags, peer: inputPeer, msgId: messageId?.id))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.SponsoredMessages?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<(interPostInterval: Int32?, startDelay: Int32?, betweenDelay: Int32?, messages: [Message]), NoError> in
guard let result = result else {
return .single((nil, nil, nil, []))
}
return account.postbox.transaction { transaction -> (interPostInterval: Int32?, startDelay: Int32?, betweenDelay: Int32?, messages: [Message]) in
switch result {
case let .sponsoredMessages(_, postsBetween, startDelay, betweenDelay, messages, chats, users):
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
var parsedMessages: [CachedMessage] = []
for message in messages {
switch message {
case let .sponsoredMessage(flags, randomId, url, title, message, entities, photo, media, color, buttonText, sponsorInfo, additionalInfo, minDisplayDuration, maxDisplayDuration):
var parsedEntities: [MessageTextEntity] = []
if let entities = entities {
parsedEntities = messageTextEntitiesFromApiEntities(entities)
}
let isRecommended = (flags & (1 << 5)) != 0
let canReport = (flags & (1 << 12)) != 0
var nameColorIndex: Int32?
var backgroundEmojiId: Int64?
if let color {
switch color {
case let .peerColor(_, color, backgroundEmojiIdValue):
nameColorIndex = color
backgroundEmojiId = backgroundEmojiIdValue
default:
break
}
}
let photo = photo.flatMap { telegramMediaImageFromApiPhoto($0) }
let contentMedia = textMediaAndExpirationTimerFromApiMedia(media, peerId).media
parsedMessages.append(CachedMessage(
opaqueId: randomId.makeData(),
messageType: isRecommended ? .recommended : .sponsored,
title: title,
text: message,
textEntities: parsedEntities,
media: photo.flatMap { [$0] } ?? [],
contentMedia: contentMedia.flatMap { [$0] } ?? [],
color: nameColorIndex.flatMap { PeerNameColor(rawValue: $0) },
backgroundEmojiId: backgroundEmojiId,
url: url,
buttonText: buttonText,
sponsorInfo: sponsorInfo,
additionalInfo: additionalInfo,
canReport: canReport,
minDisplayDuration: minDisplayDuration,
maxDisplayDuration: maxDisplayDuration
))
}
}
if messageId == nil {
CachedState.setCached(transaction: transaction, peerId: peerId, state: CachedState(timestamp: Int32(Date().timeIntervalSince1970), interPostInterval: postsBetween, messages: parsedMessages))
}
return (postsBetween, startDelay, betweenDelay, parsedMessages.compactMap { message -> Message? in
return message.toMessage(peerId: peerId, transaction: transaction)
})
case .sponsoredMessagesEmpty:
return (nil, nil, nil, [])
}
}
}
}
self.disposable.set((signal
|> deliverOn(self.queue)).start(next: { [weak self] interPostInterval, startDelay, betweenDelay, messages in
guard let strongSelf = self else {
return
}
strongSelf.stateValue = State(interPostInterval: interPostInterval, startDelay: startDelay, betweenDelay: betweenDelay, messages: messages)
}))
}
func markAsSeen(opaqueId: Data) {
let signal: Signal<Never, NoError> = self.account.network.request(Api.functions.messages.viewSponsoredMessage(randomId: Buffer(data: opaqueId)))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> ignoreValues
self.maskAsSeenDisposables.set(signal.start(), forKey: opaqueId)
}
func markAction(opaqueId: Data, media: Bool, fullscreen: Bool) {
_internal_markAdAction(account: self.account, opaqueId: opaqueId, media: media, fullscreen: fullscreen)
}
func remove(opaqueId: Data) {
if var stateValue = self.stateValue {
if let index = stateValue.messages.firstIndex(where: { $0.adAttribute?.opaqueId == opaqueId }) {
stateValue.messages.remove(at: index)
self.stateValue = stateValue
}
}
let peerId = self.peerId
let _ = (self.account.postbox.transaction { transaction -> Void in
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: peerId.toInt64())
let id = ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedAdMessageStates, key: key)
guard var cachedState = transaction.retrieveItemCacheEntry(id: id)?.get(CachedState.self) else {
return
}
if let index = cachedState.messages.firstIndex(where: { $0.opaqueId == opaqueId }) {
cachedState.messages.remove(at: index)
if let entry = CodableEntry(cachedState) {
transaction.putItemCacheEntry(id: id, entry: entry)
}
}
}).start()
}
}
public class AdMessagesHistoryContext {
private let queue = Queue()
private let impl: QueueLocalObject<AdMessagesHistoryContextImpl>
public let peerId: EnginePeer.Id
public let messageId: EngineMessage.Id?
public var state: Signal<(interPostInterval: Int32?, messages: [Message], startDelay: Int32?, betweenDelay: Int32?), 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.interPostInterval, state.messages, state.startDelay, state.betweenDelay))
})
disposable.set(stateDisposable)
}
return disposable
}
}
public init(account: Account, peerId: EnginePeer.Id, messageId: EngineMessage.Id? = nil, activateManually: Bool = false) {
self.peerId = peerId
self.messageId = messageId
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return AdMessagesHistoryContextImpl(queue: queue, account: account, peerId: peerId, messageId: messageId, activateManually: activateManually)
})
}
public func markAsSeen(opaqueId: Data) {
self.impl.with { impl in
impl.markAsSeen(opaqueId: opaqueId)
}
}
public func markAction(opaqueId: Data, media: Bool, fullscreen: Bool) {
self.impl.with { impl in
impl.markAction(opaqueId: opaqueId, media: media, fullscreen: fullscreen)
}
}
public func remove(opaqueId: Data) {
self.impl.with { impl in
impl.remove(opaqueId: opaqueId)
}
}
public func activate() {
self.impl.with { impl in
impl.activate()
}
}
}
func _internal_markAdAction(account: Account, opaqueId: Data, media: Bool, fullscreen: Bool) {
var flags: Int32 = 0
if media {
flags |= (1 << 0)
}
if fullscreen {
flags |= (1 << 1)
}
let signal = account.network.request(Api.functions.messages.clickSponsoredMessage(flags: flags, randomId: Buffer(data: opaqueId)))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> ignoreValues
let _ = signal.start()
}
func _internal_markAdAsSeen(account: Account, opaqueId: Data) {
let signal = account.network.request(Api.functions.messages.viewSponsoredMessage(randomId: Buffer(data: opaqueId)))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> ignoreValues
let _ = signal.start()
}
@@ -0,0 +1,336 @@
import Foundation
import Postbox
import TelegramApi
import SwiftSignalKit
func _internal_applyMaxReadIndexInteractively(postbox: Postbox, stateManager: AccountStateManager, index: MessageIndex) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
_internal_applyMaxReadIndexInteractively(transaction: transaction, stateManager: stateManager, index: index)
}
}
func _internal_applyMaxReadIndexInteractively(transaction: Transaction, stateManager: AccountStateManager, index: MessageIndex) {
let messageIds = transaction.applyInteractiveReadMaxIndex(index)
if let channel = transaction.getPeer(index.id.peerId) as? TelegramChannel, channel.isForumOrMonoForum {
if let combinedPeerReadState = transaction.getCombinedPeerReadState(channel.id), combinedPeerReadState.count == 0 {
for item in transaction.getMessageHistoryThreadIndex(peerId: channel.id, limit: 100) {
guard var data = transaction.getMessageHistoryThreadInfo(peerId: index.id.peerId, threadId: item.threadId)?.data.get(MessageHistoryThreadData.self) else {
continue
}
guard let messageIndex = transaction.getMessageHistoryThreadTopMessage(peerId: index.id.peerId, threadId: item.threadId, namespaces: Set([Namespaces.Message.Cloud])) else {
continue
}
if data.incomingUnreadCount != 0 {
data.incomingUnreadCount = 0
data.isMarkedUnread = false
data.maxIncomingReadId = max(messageIndex.id.id, data.maxIncomingReadId)
data.maxKnownMessageId = max(data.maxKnownMessageId, messageIndex.id.id)
if let entry = StoredMessageHistoryThreadInfo(data) {
transaction.setMessageHistoryThreadInfo(peerId: index.id.peerId, threadId: item.threadId, info: entry)
}
}
}
}
}
if index.id.peerId.namespace == Namespaces.Peer.SecretChat {
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
for id in messageIds {
if let message = transaction.getMessage(id) {
for attribute in message.attributes {
if let attribute = attribute as? AutoremoveTimeoutMessageAttribute {
if (attribute.countdownBeginTime == nil || attribute.countdownBeginTime == 0) && !message.containsSecretMedia {
transaction.updateMessage(message.id, update: { currentMessage in
var storeForwardInfo: StoreMessageForwardInfo?
if let forwardInfo = currentMessage.forwardInfo {
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
}
let updatedAttributes = currentMessage.attributes.map({ currentAttribute -> MessageAttribute in
if let currentAttribute = currentAttribute as? AutoremoveTimeoutMessageAttribute {
return AutoremoveTimeoutMessageAttribute(timeout: currentAttribute.timeout, countdownBeginTime: timestamp)
} else {
return currentAttribute
}
})
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: updatedAttributes, media: currentMessage.media))
})
}
break
}
}
}
}
} else if index.id.peerId.namespace == Namespaces.Peer.CloudUser || index.id.peerId.namespace == Namespaces.Peer.CloudGroup || index.id.peerId.namespace == Namespaces.Peer.CloudChannel {
stateManager.notifyAppliedIncomingReadMessages([index.id])
}
}
func applyOutgoingReadMaxIndex(transaction: Transaction, index: MessageIndex, beginCountdownAt timestamp: Int32) {
let messageIds = transaction.applyOutgoingReadMaxIndex(index)
if index.id.peerId.namespace == Namespaces.Peer.SecretChat {
for id in messageIds {
applySecretOutgoingMessageReadActions(transaction: transaction, id: id, beginCountdownAt: timestamp)
}
}
}
func maybeReadSecretOutgoingMessage(transaction: Transaction, index: MessageIndex) {
guard index.id.peerId.namespace == Namespaces.Peer.SecretChat else {
assertionFailure()
return
}
guard index.id.namespace == Namespaces.Message.Local else {
assertionFailure()
return
}
guard let combinedState = transaction.getCombinedPeerReadState(index.id.peerId) else {
return
}
if combinedState.isOutgoingMessageIndexRead(index) {
applySecretOutgoingMessageReadActions(transaction: transaction, id: index.id, beginCountdownAt: index.timestamp)
}
}
func applySecretOutgoingMessageReadActions(transaction: Transaction, id: MessageId, beginCountdownAt timestamp: Int32) {
guard id.peerId.namespace == Namespaces.Peer.SecretChat else {
assertionFailure()
return
}
guard id.namespace == Namespaces.Message.Local else {
assertionFailure()
return
}
if let message = transaction.getMessage(id), message.flags.intersection(.IsIncomingMask).isEmpty {
if message.flags.intersection([.Unsent, .Sending, .Failed]).isEmpty {
for attribute in message.attributes {
if let attribute = attribute as? AutoremoveTimeoutMessageAttribute {
if (attribute.countdownBeginTime == nil || attribute.countdownBeginTime == 0) && !message.containsSecretMedia {
transaction.updateMessage(message.id, update: { currentMessage in
var storeForwardInfo: StoreMessageForwardInfo?
if let forwardInfo = currentMessage.forwardInfo {
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
}
let updatedAttributes = currentMessage.attributes.map({ currentAttribute -> MessageAttribute in
if let currentAttribute = currentAttribute as? AutoremoveTimeoutMessageAttribute {
return AutoremoveTimeoutMessageAttribute(timeout: currentAttribute.timeout, countdownBeginTime: timestamp)
} else {
return currentAttribute
}
})
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: updatedAttributes, media: currentMessage.media))
})
}
break
}
}
}
}
}
func _internal_togglePeerUnreadMarkInteractively(postbox: Postbox, network: Network, viewTracker: AccountViewTracker, peerId: PeerId, setToValue: Bool? = nil) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
_internal_togglePeerUnreadMarkInteractively(transaction: transaction, network: network, viewTracker: viewTracker, peerId: peerId, setToValue: setToValue)
}
}
func _internal_toggleForumThreadUnreadMarkInteractively(transaction: Transaction, network: Network, viewTracker: AccountViewTracker, peerId: PeerId, threadId: Int64, setToValue: Bool?) {
guard let peer = transaction.getPeer(peerId) else {
return
}
guard peer.isForumOrMonoForum else {
return
}
guard var data = transaction.getMessageHistoryThreadInfo(peerId: peerId, threadId: threadId)?.data.get(MessageHistoryThreadData.self) else {
return
}
guard let messageIndex = transaction.getMessageHistoryThreadTopMessage(peerId: peerId, threadId: threadId, namespaces: Set([Namespaces.Message.Cloud])) else {
return
}
let setToValue = setToValue ?? !(data.incomingUnreadCount != 0 || data.isMarkedUnread)
if setToValue {
data.isMarkedUnread = true
if let entry = StoredMessageHistoryThreadInfo(data) {
transaction.setMessageHistoryThreadInfo(peerId: peerId, threadId: threadId, info: entry)
}
if peer.isForum {
} else if peer.isMonoForum {
if let inputPeer = apiInputPeer(peer), let subPeer = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer) {
let _ = network.request(Api.functions.messages.markDialogUnread(flags: 1 << 0, parentPeer: inputPeer, peer: .inputDialogPeer(peer: subPeer))).start()
}
}
} else {
if data.incomingUnreadCount != 0 || data.isMarkedUnread {
data.incomingUnreadCount = 0
data.isMarkedUnread = false
data.maxIncomingReadId = max(messageIndex.id.id, data.maxIncomingReadId)
data.maxKnownMessageId = max(data.maxKnownMessageId, messageIndex.id.id)
if let entry = StoredMessageHistoryThreadInfo(data) {
transaction.setMessageHistoryThreadInfo(peerId: peerId, threadId: threadId, info: entry)
}
if peer.isForum {
if let inputPeer = apiInputPeer(peer) {
let _ = network.request(Api.functions.messages.readDiscussion(peer: inputPeer, msgId: Int32(clamping: threadId), readMaxId: messageIndex.id.id)).start()
}
} else if peer.isMonoForum {
if let inputPeer = apiInputPeer(peer), let subPeer = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer) {
let _ = network.request(Api.functions.messages.readSavedHistory(parentPeer: inputPeer, peer: subPeer, maxId: messageIndex.id.id)).start()
}
}
}
}
}
func _internal_markForumThreadAsReadInteractively(transaction: Transaction, network: Network, viewTracker: AccountViewTracker, peerId: PeerId, threadId: Int64) {
guard let peer = transaction.getPeer(peerId) else {
return
}
guard peer.isForumOrMonoForum else {
return
}
guard var data = transaction.getMessageHistoryThreadInfo(peerId: peerId, threadId: threadId)?.data.get(MessageHistoryThreadData.self) else {
return
}
guard let messageIndex = transaction.getMessageHistoryThreadTopMessage(peerId: peerId, threadId: threadId, namespaces: Set([Namespaces.Message.Cloud])) else {
return
}
if data.incomingUnreadCount != 0 {
data.incomingUnreadCount = 0
data.isMarkedUnread = false
data.maxIncomingReadId = max(messageIndex.id.id, data.maxIncomingReadId)
data.maxKnownMessageId = max(data.maxKnownMessageId, messageIndex.id.id)
if let entry = StoredMessageHistoryThreadInfo(data) {
transaction.setMessageHistoryThreadInfo(peerId: peerId, threadId: threadId, info: entry)
}
if peer.isForum {
if let inputPeer = apiInputPeer(peer) {
let _ = network.request(Api.functions.messages.readDiscussion(peer: inputPeer, msgId: Int32(clamping: threadId), readMaxId: messageIndex.id.id)).start()
}
} else if peer.isMonoForum {
if let inputPeer = apiInputPeer(peer), let subPeer = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer) {
let _ = network.request(Api.functions.messages.readSavedHistory(parentPeer: inputPeer, peer: subPeer, maxId: messageIndex.id.id)).start()
}
}
}
}
func _internal_togglePeerUnreadMarkInteractively(transaction: Transaction, network: Network, viewTracker: AccountViewTracker, peerId: PeerId, setToValue: Bool? = nil) {
guard let peer = transaction.getPeer(peerId) else {
return
}
var displayAsRegularChat: Bool = false
if let channel = peer as? TelegramChannel, channel.flags.contains(.displayForumAsTabs) {
displayAsRegularChat = true
} else if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData {
displayAsRegularChat = cachedData.viewForumAsMessages.knownValue ?? false
}
if peer.isForumOrMonoForum, !displayAsRegularChat {
for item in transaction.getMessageHistoryThreadIndex(peerId: peerId, limit: 20) {
guard var data = transaction.getMessageHistoryThreadInfo(peerId: peerId, threadId: item.threadId)?.data.get(MessageHistoryThreadData.self) else {
continue
}
guard let messageIndex = transaction.getMessageHistoryThreadTopMessage(peerId: peerId, threadId: item.threadId, namespaces: Set([Namespaces.Message.Cloud])) else {
continue
}
if data.incomingUnreadCount != 0 {
data.incomingUnreadCount = 0
data.isMarkedUnread = false
data.maxIncomingReadId = max(messageIndex.id.id, data.maxIncomingReadId)
data.maxKnownMessageId = max(data.maxKnownMessageId, messageIndex.id.id)
if let entry = StoredMessageHistoryThreadInfo(data) {
transaction.setMessageHistoryThreadInfo(peerId: peerId, threadId: item.threadId, info: entry)
}
if peer.isForum {
if let inputPeer = apiInputPeer(peer) {
let _ = network.request(Api.functions.messages.readDiscussion(peer: inputPeer, msgId: Int32(clamping: item.threadId), readMaxId: messageIndex.id.id)).start()
}
} else if peer.isMonoForum {
if let inputPeer = apiInputPeer(peer), let subPeer = transaction.getPeer(PeerId(item.threadId)).flatMap(apiInputPeer) {
let _ = network.request(Api.functions.messages.readSavedHistory(parentPeer: inputPeer, peer: subPeer, maxId: messageIndex.id.id)).start()
}
}
}
}
} else {
let principalNamespace: MessageId.Namespace
if peerId.namespace == Namespaces.Peer.SecretChat {
principalNamespace = Namespaces.Message.SecretIncoming
} else {
principalNamespace = Namespaces.Message.Cloud
}
var hasUnread = false
if let states = transaction.getPeerReadStates(peerId) {
for state in states {
if state.1.isUnread {
hasUnread = true
break
}
}
}
if !hasUnread && peerId.namespace == Namespaces.Peer.SecretChat {
let unseenSummary = transaction.getMessageTagSummary(peerId: peerId, threadId: nil, tagMask: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud, customTag: nil)
let actionSummary = transaction.getPendingMessageActionsSummary(peerId: peerId, type: PendingMessageActionType.consumeUnseenPersonalMessage, namespace: Namespaces.Message.Cloud)
if (unseenSummary?.count ?? 0) - (actionSummary ?? 0) > 0 {
hasUnread = true
}
}
if hasUnread {
if setToValue == nil || !(setToValue!) {
if let index = transaction.getTopPeerMessageIndex(peerId: peerId) {
let _ = transaction.applyInteractiveReadMaxIndex(index)
} else {
transaction.applyMarkUnread(peerId: peerId, namespace: principalNamespace, value: false, interactive: true)
}
viewTracker.updateMarkAllMentionsSeen(peerId: peerId, threadId: nil)
}
} else {
if setToValue == nil || setToValue! {
transaction.applyMarkUnread(peerId: peerId, namespace: principalNamespace, value: true, interactive: true)
}
}
}
}
public func clearPeerUnseenPersonalMessagesInteractively(account: Account, peerId: PeerId, threadId: Int64?) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Void in
if peerId.namespace == Namespaces.Peer.SecretChat {
return
}
account.viewTracker.updateMarkAllMentionsSeen(peerId: peerId, threadId: threadId)
}
|> ignoreValues
}
public func clearPeerUnseenReactionsInteractively(account: Account, peerId: PeerId, threadId: Int64?) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Void in
if peerId.namespace == Namespaces.Peer.SecretChat {
return
}
account.viewTracker.updateMarkAllReactionsSeen(peerId: peerId, threadId: threadId)
}
|> ignoreValues
}
func _internal_markAllChatsAsReadInteractively(transaction: Transaction, network: Network, viewTracker: AccountViewTracker, groupId: PeerGroupId, filterPredicate: ChatListFilterPredicate?) {
for peerId in transaction.getUnreadChatListPeerIds(groupId: groupId, filterPredicate: filterPredicate, additionalFilter: nil, stopOnFirstMatch: false) {
_internal_togglePeerUnreadMarkInteractively(transaction: transaction, network: network, viewTracker: viewTracker, peerId: peerId, setToValue: false)
}
}
@@ -0,0 +1,778 @@
import Foundation
import TelegramApi
import Postbox
import SwiftSignalKit
public final class AttachMenuBots: Equatable, Codable {
public final class Bot: Equatable, Codable {
private enum CodingKeys: String, CodingKey {
case peerId
case name
case botIcons
case peerTypes
case hasSettings
case flags
}
public enum IconName: Int32, Codable {
case `default` = 0
case iOSStatic
case iOSAnimated
case iOSSettingsStatic
case macOSAnimated
case macOSSettingsStatic
case placeholder
init?(string: String) {
switch string {
case "default_static":
self = .default
case "ios_static":
self = .iOSStatic
case "ios_animated":
self = .iOSAnimated
case "ios_side_menu_static":
self = .iOSSettingsStatic
case "macos_side_menu_static":
self = .macOSSettingsStatic
case "macos_animated":
self = .macOSAnimated
case "placeholder_static":
self = .placeholder
default:
return nil
}
}
}
public struct Flags: OptionSet {
public var rawValue: Int32
public init(rawValue: Int32) {
self.rawValue = rawValue
}
public init() {
self.rawValue = 0
}
public static let hasSettings = Flags(rawValue: 1 << 0)
public static let requiresWriteAccess = Flags(rawValue: 1 << 1)
public static let showInAttachMenu = Flags(rawValue: 1 << 2)
public static let showInSettings = Flags(rawValue: 1 << 3)
public static let showInSettingsDisclaimer = Flags(rawValue: 1 << 4)
public static let notActivated = Flags(rawValue: 1 << 5)
}
public struct PeerFlags: OptionSet, Codable {
public var rawValue: UInt32
public init(rawValue: UInt32) {
self.rawValue = rawValue
}
public init() {
self.rawValue = 0
}
public static let sameBot = PeerFlags(rawValue: 1 << 0)
public static let bot = PeerFlags(rawValue: 1 << 1)
public static let user = PeerFlags(rawValue: 1 << 2)
public static let group = PeerFlags(rawValue: 1 << 3)
public static let channel = PeerFlags(rawValue: 1 << 4)
public static var all: PeerFlags {
return [.sameBot, .bot, .user, .group, .channel]
}
public static var `default`: PeerFlags {
return [.sameBot, .bot, .user]
}
}
private struct IconPair: Codable {
var name: IconName
var value: TelegramMediaFile
init(_ name: IconName, value: TelegramMediaFile) {
self.name = name
self.value = value
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
self.name = IconName(rawValue: try container.decode(Int32.self, forKey: "k")) ?? .default
let data = try container.decode(AdaptedPostboxDecoder.RawObjectData.self, forKey: "v")
self.value = TelegramMediaFile(decoder: PostboxDecoder(buffer: MemoryBuffer(data: data.data)))
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self)
try container.encode(self.name.rawValue, forKey: "k")
try container.encode(PostboxEncoder().encodeObjectToRawData(self.value), forKey: "v")
}
}
public let peerId: PeerId
public let name: String
public let icons: [IconName: TelegramMediaFile]
public let peerTypes: PeerFlags
public let flags: Flags
public init(
peerId: PeerId,
name: String,
icons: [IconName: TelegramMediaFile],
peerTypes: PeerFlags,
flags: Flags
) {
self.peerId = peerId
self.name = name
self.icons = icons
self.peerTypes = peerTypes
self.flags = flags
}
public static func ==(lhs: Bot, rhs: Bot) -> Bool {
if lhs.peerId != rhs.peerId {
return false
}
if lhs.name != rhs.name {
return false
}
if lhs.icons != rhs.icons {
return false
}
if lhs.peerTypes != rhs.peerTypes {
return false
}
if lhs.flags != rhs.flags {
return false
}
return true
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let peerIdValue = try container.decode(Int64.self, forKey: .peerId)
self.peerId = PeerId(peerIdValue)
self.name = try container.decodeIfPresent(String.self, forKey: .name) ?? ""
let iconPairs = try container.decodeIfPresent([IconPair].self, forKey: .botIcons) ?? []
var icons: [IconName: TelegramMediaFile] = [:]
for iconPair in iconPairs {
icons[iconPair.name] = iconPair.value
}
self.icons = icons
let value = try container.decodeIfPresent(Int32.self, forKey: .peerTypes) ?? Int32(PeerFlags.default.rawValue)
self.peerTypes = PeerFlags(rawValue: UInt32(value))
if let flags = try container.decodeIfPresent(Int32.self, forKey: .flags) {
self.flags = Flags(rawValue: flags)
} else {
let hasSettings = try container.decodeIfPresent(Bool.self, forKey: .hasSettings) ?? false
self.flags = hasSettings ? [.hasSettings] : []
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.peerId.toInt64(), forKey: .peerId)
try container.encode(self.name, forKey: .name)
var iconPairs: [IconPair] = []
for (key, value) in self.icons {
iconPairs.append(IconPair(key, value: value))
}
try container.encode(iconPairs, forKey: .botIcons)
try container.encode(Int32(self.peerTypes.rawValue), forKey: .peerTypes)
try container.encode(Int32(self.flags.rawValue), forKey: .flags)
}
func withUpdatedFlags(_ flags: Flags) -> Bot {
return Bot(peerId: self.peerId, name: self.name, icons: self.icons, peerTypes: self.peerTypes, flags: flags)
}
}
private enum CodingKeys: String, CodingKey {
case hash
case bots
}
public let hash: Int64
public let bots: [Bot]
public init(
hash: Int64,
bots: [Bot]
) {
self.hash = hash
self.bots = bots
}
public static func ==(lhs: AttachMenuBots, rhs: AttachMenuBots) -> Bool {
if lhs.hash != rhs.hash {
return false
}
if lhs.bots != rhs.bots {
return false
}
return true
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.hash = try container.decode(Int64.self, forKey: .hash)
self.bots = try container.decode([Bot].self, forKey: .bots)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.hash, forKey: .hash)
try container.encode(self.bots, forKey: .bots)
}
}
private func cachedAttachMenuBots(postbox: Postbox) -> Signal<AttachMenuBots?, NoError> {
return postbox.transaction { transaction -> AttachMenuBots? in
return cachedAttachMenuBots(transaction: transaction)
}
}
private func cachedAttachMenuBots(transaction: Transaction) -> AttachMenuBots? {
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: 0)
let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.attachMenuBots, key: key))?.get(AttachMenuBots.self)
if let cached = cached {
return cached
} else {
return nil
}
}
private func setCachedAttachMenuBots(transaction: Transaction, attachMenuBots: AttachMenuBots) {
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: 0)
let entryId = ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.attachMenuBots, key: key)
if let entry = CodableEntry(attachMenuBots) {
transaction.putItemCacheEntry(id: entryId, entry: entry)
} else {
transaction.removeItemCacheEntry(id: entryId)
}
}
private func removeCachedAttachMenuBot(postbox: Postbox, botId: PeerId) -> Signal<Void, NoError> {
return postbox.transaction { transaction in
if let bots = cachedAttachMenuBots(transaction: transaction) {
let updatedBots = bots.bots.filter { $0.peerId != botId }
setCachedAttachMenuBots(transaction: transaction, attachMenuBots: AttachMenuBots(hash: bots.hash, bots: updatedBots))
}
}
}
func managedSynchronizeAttachMenuBots(accountPeerId: PeerId, postbox: Postbox, network: Network, force: Bool = false) -> Signal<Void, NoError> {
let poll = Signal<Void, NoError> { subscriber in
let signal: Signal<Void, NoError> = cachedAttachMenuBots(postbox: postbox)
|> mapToSignal { current in
return (network.request(Api.functions.messages.getAttachMenuBots(hash: force ? 0 : (current?.hash ?? 0)))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.AttachMenuBots?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Void, NoError> in
guard let result = result else {
return .complete()
}
return postbox.transaction { transaction -> Void in
switch result {
case let .attachMenuBots(hash, bots, users):
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users))
var resultBots: [AttachMenuBots.Bot] = []
for bot in bots {
switch bot {
case let .attachMenuBot(apiFlags, botId, name, apiPeerTypes, botIcons):
var icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile] = [:]
for icon in botIcons {
switch icon {
case let .attachMenuBotIcon(_, name, icon, _):
if let iconName = AttachMenuBots.Bot.IconName(string: name), let icon = telegramMediaFileFromApiDocument(icon, altDocuments: []) {
icons[iconName] = icon
}
}
}
if !icons.isEmpty {
var peerTypes: AttachMenuBots.Bot.PeerFlags = []
for apiType in apiPeerTypes ?? [] {
switch apiType {
case .attachMenuPeerTypeSameBotPM:
peerTypes.insert(.sameBot)
case .attachMenuPeerTypeBotPM:
peerTypes.insert(.bot)
case .attachMenuPeerTypePM:
peerTypes.insert(.user)
case .attachMenuPeerTypeChat:
peerTypes.insert(.group)
case .attachMenuPeerTypeBroadcast:
peerTypes.insert(.channel)
}
}
var flags: AttachMenuBots.Bot.Flags = []
if (apiFlags & (1 << 0)) != 0 {
flags.insert(.notActivated)
}
if (apiFlags & (1 << 1)) != 0 {
flags.insert(.hasSettings)
}
if (apiFlags & (1 << 2)) != 0 {
flags.insert(.requiresWriteAccess)
}
if (apiFlags & (1 << 3)) != 0 {
flags.insert(.showInAttachMenu)
}
if (apiFlags & (1 << 4)) != 0 {
flags.insert(.showInSettings)
}
if (apiFlags & (1 << 5)) != 0 {
flags.insert(.showInSettingsDisclaimer)
}
resultBots.append(AttachMenuBots.Bot(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId)), name: name, icons: icons, peerTypes: peerTypes, flags: flags))
}
}
}
let attachMenuBots = AttachMenuBots(hash: hash, bots: resultBots)
setCachedAttachMenuBots(transaction: transaction, attachMenuBots: attachMenuBots)
case .attachMenuBotsNotModified:
break
}
return Void()
}
})
}
return signal.start(next: { value in
subscriber.putNext(value)
}, completed: {
subscriber.putCompletion()
})
}
return (
poll
|> then(
.complete()
|> suspendAwareDelay(2.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue())
)
)
|> restart
}
public enum AddBotToAttachMenuError {
case generic
}
func _internal_addBotToAttachMenu(accountPeerId: PeerId, postbox: Postbox, network: Network, botId: PeerId, allowWrite: Bool) -> Signal<Bool, AddBotToAttachMenuError> {
return postbox.transaction { transaction -> Signal<Bool, AddBotToAttachMenuError> in
guard let peer = transaction.getPeer(botId), let inputUser = apiInputUser(peer) else {
return .complete()
}
var flags: Int32 = 0
if allowWrite {
flags |= (1 << 0)
}
return network.request(Api.functions.messages.toggleBotInAttachMenu(flags: flags, bot: inputUser, enabled: .boolTrue))
|> map { value -> Bool in
switch value {
case .boolTrue:
return true
default:
return false
}
}
|> mapError { _ -> AddBotToAttachMenuError in
return .generic
}
|> mapToSignal { value -> Signal<Bool, AddBotToAttachMenuError> in
if value {
return managedSynchronizeAttachMenuBots(accountPeerId: accountPeerId, postbox: postbox, network: network, force: true)
|> castError(AddBotToAttachMenuError.self)
|> take(1)
|> map { _ -> Bool in
return true
}
} else {
return .fail(.generic)
}
}
}
|> castError(AddBotToAttachMenuError.self)
|> switchToLatest
}
func _internal_removeBotFromAttachMenu(accountPeerId: PeerId, postbox: Postbox, network: Network, botId: PeerId) -> Signal<Bool, NoError> {
let _ = removeCachedAttachMenuBot(postbox: postbox, botId: botId).start()
return postbox.transaction { transaction -> Signal<Bool, NoError> in
guard let peer = transaction.getPeer(botId), let inputUser = apiInputUser(peer) else {
return .complete()
}
return network.request(Api.functions.messages.toggleBotInAttachMenu(flags: 0, bot: inputUser, enabled: .boolFalse))
|> map { value -> Bool in
switch value {
case .boolTrue:
return true
default:
return false
}
}
|> `catch` { error -> Signal<Bool, NoError> in
return .single(false)
}
|> afterCompleted {
let _ = (managedSynchronizeAttachMenuBots(accountPeerId: accountPeerId, postbox: postbox, network: network, force: true)
|> take(1)).start(completed: {
let _ = removeCachedAttachMenuBot(postbox: postbox, botId: botId).start()
})
}
}
|> switchToLatest
}
func _internal_acceptAttachMenuBotDisclaimer(postbox: Postbox, botId: PeerId) -> Signal<Never, NoError> {
return postbox.transaction { transaction in
if let attachMenuBots = cachedAttachMenuBots(transaction: transaction) {
var updatedAttachMenuBots = attachMenuBots
if let index = attachMenuBots.bots.firstIndex(where: { $0.peerId == botId }) {
var updatedFlags = attachMenuBots.bots[index].flags
updatedFlags.remove(.showInSettingsDisclaimer)
let updatedBot = attachMenuBots.bots[index].withUpdatedFlags(updatedFlags)
var updatedBots = attachMenuBots.bots
updatedBots[index] = updatedBot
updatedAttachMenuBots = AttachMenuBots(hash: attachMenuBots.hash, bots: updatedBots)
}
setCachedAttachMenuBots(transaction: transaction, attachMenuBots: updatedAttachMenuBots)
}
} |> ignoreValues
}
public struct AttachMenuBot: Equatable {
public let peer: EnginePeer
public let shortName: String
public let icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile]
public let peerTypes: AttachMenuBots.Bot.PeerFlags
public let flags: AttachMenuBots.Bot.Flags
public init(peer: EnginePeer, shortName: String, icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile], peerTypes: AttachMenuBots.Bot.PeerFlags, flags: AttachMenuBots.Bot.Flags) {
self.peer = peer
self.shortName = shortName
self.icons = icons
self.peerTypes = peerTypes
self.flags = flags
}
}
func _internal_attachMenuBots(postbox: Postbox) -> Signal<[AttachMenuBot], NoError> {
return postbox.transaction { transaction -> [AttachMenuBot] in
guard let cachedBots = cachedAttachMenuBots(transaction: transaction)?.bots else {
return []
}
var resultBots: [AttachMenuBot] = []
for bot in cachedBots {
if let peer = transaction.getPeer(bot.peerId) {
resultBots.append(AttachMenuBot(peer: EnginePeer(peer), shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, flags: bot.flags))
}
}
return resultBots
}
}
public enum GetAttachMenuBotError {
case generic
}
func _internal_getAttachMenuBot(accountPeerId: PeerId, postbox: Postbox, network: Network, botId: PeerId, cached: Bool) -> Signal<AttachMenuBot, GetAttachMenuBotError> {
return postbox.transaction { transaction -> Signal<AttachMenuBot, GetAttachMenuBotError> in
if cached, let cachedBots = cachedAttachMenuBots(transaction: transaction)?.bots {
if let bot = cachedBots.first(where: { $0.peerId == botId }), let peer = transaction.getPeer(bot.peerId) {
return .single(AttachMenuBot(peer: EnginePeer(peer), shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, flags: bot.flags))
}
}
guard let peer = transaction.getPeer(botId), let inputUser = apiInputUser(peer) else {
return .complete()
}
return network.request(Api.functions.messages.getAttachMenuBot(bot: inputUser))
|> mapError { _ -> GetAttachMenuBotError in
return .generic
}
|> mapToSignal { result -> Signal<AttachMenuBot, GetAttachMenuBotError> in
return postbox.transaction { transaction -> Signal<AttachMenuBot, GetAttachMenuBotError> in
switch result {
case let .attachMenuBotsBot(bot, users):
var peer: Peer?
for user in users {
let telegramUser = TelegramUser(user: user)
if telegramUser.id == botId {
peer = telegramUser
}
}
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users))
guard let peer = peer else {
return .fail(.generic)
}
switch bot {
case let .attachMenuBot(apiFlags, _, name, apiPeerTypes, botIcons):
var icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile] = [:]
for icon in botIcons {
switch icon {
case let .attachMenuBotIcon(_, name, icon, _):
if let iconName = AttachMenuBots.Bot.IconName(string: name), let icon = telegramMediaFileFromApiDocument(icon, altDocuments: []) {
icons[iconName] = icon
}
}
}
var peerTypes: AttachMenuBots.Bot.PeerFlags = []
for apiType in apiPeerTypes ?? [] {
switch apiType {
case .attachMenuPeerTypeSameBotPM:
peerTypes.insert(.sameBot)
case .attachMenuPeerTypeBotPM:
peerTypes.insert(.bot)
case .attachMenuPeerTypePM:
peerTypes.insert(.user)
case .attachMenuPeerTypeChat:
peerTypes.insert(.group)
case .attachMenuPeerTypeBroadcast:
peerTypes.insert(.channel)
}
}
var flags: AttachMenuBots.Bot.Flags = []
if (apiFlags & (1 << 1)) != 0 {
flags.insert(.hasSettings)
}
if (apiFlags & (1 << 2)) != 0 {
flags.insert(.requiresWriteAccess)
}
if (apiFlags & (1 << 3)) != 0 {
flags.insert(.showInAttachMenu)
}
if (apiFlags & (1 << 4)) != 0 {
flags.insert(.showInSettings)
}
if (apiFlags & (1 << 5)) != 0 {
flags.insert(.showInSettingsDisclaimer)
}
return .single(AttachMenuBot(peer: EnginePeer(peer), shortName: name, icons: icons, peerTypes: peerTypes, flags: flags))
}
}
}
|> castError(GetAttachMenuBotError.self)
|> switchToLatest
}
}
|> castError(GetAttachMenuBotError.self)
|> switchToLatest
}
public enum BotAppReference : Equatable {
case id(id: Int64, accessHash: Int64)
case shortName(peerId: PeerId, shortName: String)
}
public final class BotApp: Equatable, Codable {
private enum CodingKeys: String, CodingKey {
case id
case accessHash
case shortName
case title
case description
case photo
case document
case hash
case flags
}
public struct Flags: OptionSet {
public var rawValue: Int32
public init(rawValue: Int32) {
self.rawValue = rawValue
}
public init() {
self.rawValue = 0
}
public static let notActivated = Flags(rawValue: 1 << 0)
public static let requiresWriteAccess = Flags(rawValue: 1 << 1)
public static let hasSettings = Flags(rawValue: 1 << 2)
}
public let id: Int64
public let accessHash: Int64
public let shortName: String
public let title: String
public let description: String
public let photo: TelegramMediaImage?
public let document: TelegramMediaFile?
public let hash: Int64
public let flags: Flags
public init(
id: Int64,
accessHash: Int64,
shortName: String,
title: String,
description: String,
photo: TelegramMediaImage?,
document: TelegramMediaFile?,
hash: Int64,
flags: Flags
) {
self.id = id
self.accessHash = accessHash
self.shortName = shortName
self.title = title
self.description = description
self.photo = photo
self.document = document
self.hash = hash
self.flags = flags
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int64.self, forKey: .id)
self.accessHash = try container.decode(Int64.self, forKey: .accessHash)
self.shortName = try container.decode(String.self, forKey: .shortName)
self.title = try container.decode(String.self, forKey: .title)
self.description = try container.decode(String.self, forKey: .description)
self.photo = try container.decodeIfPresent(TelegramMediaImage.self, forKey: .photo)
self.document = try container.decodeIfPresent(TelegramMediaFile.self, forKey: .document)
self.hash = try container.decode(Int64.self, forKey: .hash)
self.flags = Flags(rawValue: try container.decode(Int32.self, forKey: .flags))
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.id, forKey: .id)
try container.encode(self.accessHash, forKey: .accessHash)
try container.encode(self.shortName, forKey: .shortName)
try container.encode(self.title, forKey: .title)
try container.encode(self.description, forKey: .description)
try container.encodeIfPresent(self.photo, forKey: .photo)
try container.encodeIfPresent(self.document, forKey: .document)
try container.encode(self.hash, forKey: .hash)
try container.encode(self.flags.rawValue, forKey: .flags)
}
public static func ==(lhs: BotApp, rhs: BotApp) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.accessHash != rhs.accessHash {
return false
}
if lhs.shortName != rhs.shortName {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.description != rhs.description {
return false
}
if lhs.photo != rhs.photo {
return false
}
if lhs.document != rhs.document {
return false
}
if lhs.hash != rhs.hash {
return false
}
if lhs.flags != rhs.flags {
return false
}
return true
}
}
public enum GetBotAppError {
case generic
}
func _internal_getBotApp(account: Account, reference: BotAppReference) -> Signal<BotApp, GetBotAppError> {
return account.postbox.transaction { transaction -> Signal<BotApp, GetBotAppError> in
let app: Api.InputBotApp
switch reference {
case let .id(id, accessHash):
app = .inputBotAppID(id: id, accessHash: accessHash)
case let .shortName(peerId, shortName):
guard let bot = transaction.getPeer(peerId), let inputBot = apiInputUser(bot) else {
return .fail(.generic)
}
app = .inputBotAppShortName(botId: inputBot, shortName: shortName)
}
return account.network.request(Api.functions.messages.getBotApp(app: app, hash: 0))
|> mapError { _ -> GetBotAppError in
return .generic
}
|> mapToSignal { result -> Signal<BotApp, GetBotAppError> in
switch result {
case let .botApp(botAppFlags, app):
switch app {
case let .botApp(flags, id, accessHash, shortName, title, description, photo, document, hash):
let _ = flags
var appFlags = BotApp.Flags()
if (botAppFlags & (1 << 0)) != 0 {
appFlags.insert(.notActivated)
}
if (botAppFlags & (1 << 1)) != 0 {
appFlags.insert(.requiresWriteAccess)
}
if (botAppFlags & (1 << 2)) != 0 {
appFlags.insert(.hasSettings)
}
return .single(BotApp(id: id, accessHash: accessHash, shortName: shortName, title: title, description: description, photo: telegramMediaImageFromApiPhoto(photo), document: document.flatMap { telegramMediaFileFromApiDocument($0, altDocuments: []) }, hash: hash, flags: appFlags))
case .botAppNotModified:
return .complete()
}
}
}
}
|> castError(GetBotAppError.self)
|> switchToLatest
}
extension BotApp {
convenience init?(apiBotApp: Api.BotApp) {
switch apiBotApp {
case let .botApp(_, id, accessHash, shortName, title, description, photo, document, hash):
self.init(id: id, accessHash: accessHash, shortName: shortName, title: title, description: description, photo: telegramMediaImageFromApiPhoto(photo), document: document.flatMap { telegramMediaFileFromApiDocument($0, altDocuments: []) }, hash: hash, flags: [])
case .botAppNotModified:
return nil
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,27 @@
import Postbox
public final class EngineCallList {
public enum Scope {
case all
case missed
}
public enum Item {
case message(message: EngineMessage, group: [EngineMessage])
case hole(EngineMessage.Index)
}
public let items: [Item]
public let hasEarlier: Bool
public let hasLater: Bool
init(
items: [Item],
hasEarlier: Bool,
hasLater: Bool
) {
self.items = items
self.hasEarlier = hasEarlier
self.hasLater = hasLater
}
}
@@ -0,0 +1,663 @@
import Postbox
public final class EngineChatList: Equatable {
public enum Group {
case root
case archive
}
public typealias MessageTagSummaryInfo = ChatListMessageTagSummaryInfo
public typealias StoryStats = PeerStoryStats
public enum PinnedItem {
public typealias Id = PinnedItemId
}
public enum RelativePosition {
case later(than: EngineChatList.Item.Index?)
case earlier(than: EngineChatList.Item.Index?)
}
public struct Draft: Equatable {
public var text: String
public var entities: [MessageTextEntity]
public init(text: String, entities: [MessageTextEntity]) {
self.text = text
self.entities = entities
}
}
public final class ForumTopicData: Equatable {
public let id: Int64
public let title: String
public let iconFileId: Int64?
public let iconColor: Int32
public let maxOutgoingReadMessageId: EngineMessage.Id
public let isUnread: Bool
public let threadPeer: EnginePeer?
public init(id: Int64, title: String, iconFileId: Int64?, iconColor: Int32, maxOutgoingReadMessageId: EngineMessage.Id, isUnread: Bool, threadPeer: EnginePeer?) {
self.id = id
self.title = title
self.iconFileId = iconFileId
self.iconColor = iconColor
self.maxOutgoingReadMessageId = maxOutgoingReadMessageId
self.isUnread = isUnread
self.threadPeer = threadPeer
}
public static func ==(lhs: ForumTopicData, rhs: ForumTopicData) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.iconFileId != rhs.iconFileId {
return false
}
if lhs.iconColor != rhs.iconColor {
return false
}
if lhs.maxOutgoingReadMessageId != rhs.maxOutgoingReadMessageId {
return false
}
if lhs.isUnread != rhs.isUnread {
return false
}
if lhs.threadPeer != rhs.threadPeer {
return false
}
return true
}
}
public enum MediaDraftContentType: Int32 {
case audio
case video
}
public final class Item: Equatable {
public enum Id: Hashable {
case chatList(EnginePeer.Id)
case forum(Int64)
}
public enum PinnedIndex: Equatable, Comparable {
case none
case index(Int)
public static func <(lhs: PinnedIndex, rhs: PinnedIndex) -> Bool {
switch lhs {
case .none:
switch rhs {
case .none:
return false
case .index:
return false
}
case let .index(lhsValue):
switch rhs {
case .none:
return true
case let .index(rhsValue):
return lhsValue >= rhsValue
}
}
}
}
public enum Index: Equatable, Comparable {
public typealias ChatList = ChatListIndex
case chatList(ChatListIndex)
case forum(pinnedIndex: PinnedIndex, timestamp: Int32, threadId: Int64, namespace: EngineMessage.Id.Namespace, id: EngineMessage.Id.Id)
public static func <(lhs: Index, rhs: Index) -> Bool {
switch lhs {
case let .chatList(lhsIndex):
if case let .chatList(rhsIndex) = rhs {
return lhsIndex < rhsIndex
} else {
return true
}
case let .forum(lhsPinnedIndex, lhsTimestamp, lhsThreadId, lhsNamespace, lhsId):
if case let .forum(rhsPinnedIndex, rhsTimestamp, rhsThreadId, rhsNamespace, rhsId) = rhs {
if lhsPinnedIndex != rhsPinnedIndex {
return lhsPinnedIndex < rhsPinnedIndex
}
if lhsTimestamp != rhsTimestamp {
return lhsTimestamp < rhsTimestamp
}
if lhsThreadId != rhsThreadId {
return lhsThreadId < rhsThreadId
}
if lhsNamespace != rhsNamespace {
return lhsNamespace < rhsNamespace
}
return lhsId < rhsId
} else {
return false
}
}
}
}
public let id: Id
public let index: Index
public let messages: [EngineMessage]
public let readCounters: EnginePeerReadCounters?
public let isMuted: Bool
public let draft: Draft?
public let threadData: MessageHistoryThreadData?
public let renderedPeer: EngineRenderedPeer
public let presence: EnginePeer.Presence?
public let hasUnseenMentions: Bool
public let hasUnseenReactions: Bool
public let forumTopicData: ForumTopicData?
public let topForumTopicItems: [EngineChatList.ForumTopicData]
public let hasFailed: Bool
public let isContact: Bool
public let autoremoveTimeout: Int32?
public let storyStats: StoryStats?
public let displayAsTopicList: Bool
public let isPremiumRequiredToMessage: Bool
public let mediaDraftContentType: EngineChatList.MediaDraftContentType?
public init(
id: Id,
index: Index,
messages: [EngineMessage],
readCounters: EnginePeerReadCounters?,
isMuted: Bool,
draft: Draft?,
threadData: MessageHistoryThreadData?,
renderedPeer: EngineRenderedPeer,
presence: EnginePeer.Presence?,
hasUnseenMentions: Bool,
hasUnseenReactions: Bool,
forumTopicData: ForumTopicData?,
topForumTopicItems: [EngineChatList.ForumTopicData],
hasFailed: Bool,
isContact: Bool,
autoremoveTimeout: Int32?,
storyStats: StoryStats?,
displayAsTopicList: Bool,
isPremiumRequiredToMessage: Bool,
mediaDraftContentType: EngineChatList.MediaDraftContentType?
) {
self.id = id
self.index = index
self.messages = messages
self.readCounters = readCounters
self.isMuted = isMuted
self.draft = draft
self.threadData = threadData
self.renderedPeer = renderedPeer
self.presence = presence
self.hasUnseenMentions = hasUnseenMentions
self.hasUnseenReactions = hasUnseenReactions
self.forumTopicData = forumTopicData
self.topForumTopicItems = topForumTopicItems
self.hasFailed = hasFailed
self.isContact = isContact
self.autoremoveTimeout = autoremoveTimeout
self.storyStats = storyStats
self.displayAsTopicList = displayAsTopicList
self.isPremiumRequiredToMessage = isPremiumRequiredToMessage
self.mediaDraftContentType = mediaDraftContentType
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.index != rhs.index {
return false
}
if lhs.messages != rhs.messages {
return false
}
if lhs.readCounters != rhs.readCounters {
return false
}
if lhs.isMuted != rhs.isMuted {
return false
}
if lhs.draft != rhs.draft {
return false
}
if lhs.threadData != rhs.threadData {
return false
}
if lhs.renderedPeer != rhs.renderedPeer {
return false
}
if lhs.presence != rhs.presence {
return false
}
if lhs.hasUnseenMentions != rhs.hasUnseenMentions {
return false
}
if lhs.hasUnseenReactions != rhs.hasUnseenReactions {
return false
}
if lhs.forumTopicData != rhs.forumTopicData {
return false
}
if lhs.topForumTopicItems != rhs.topForumTopicItems {
return false
}
if lhs.hasFailed != rhs.hasFailed {
return false
}
if lhs.isContact != rhs.isContact {
return false
}
if lhs.autoremoveTimeout != rhs.autoremoveTimeout {
return false
}
if lhs.storyStats != rhs.storyStats {
return false
}
if lhs.displayAsTopicList != rhs.displayAsTopicList {
return false
}
if lhs.isPremiumRequiredToMessage != rhs.isPremiumRequiredToMessage {
return false
}
if lhs.mediaDraftContentType != rhs.mediaDraftContentType {
return false
}
return true
}
}
public final class GroupItem: Equatable {
public final class Item: Equatable {
public let peer: EngineRenderedPeer
public let isUnread: Bool
public init(peer: EngineRenderedPeer, isUnread: Bool) {
self.peer = peer
self.isUnread = isUnread
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs.peer != rhs.peer {
return false
}
if lhs.isUnread != rhs.isUnread {
return false
}
return true
}
}
public let id: Group
public let topMessage: EngineMessage?
public let items: [Item]
public let unreadCount: Int
public init(
id: Group,
topMessage: EngineMessage?,
items: [Item],
unreadCount: Int
) {
self.id = id
self.topMessage = topMessage
self.items = items
self.unreadCount = unreadCount
}
public static func ==(lhs: GroupItem, rhs: GroupItem) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.topMessage?.index != rhs.topMessage?.index {
return false
}
if lhs.topMessage?.stableVersion != rhs.topMessage?.stableVersion {
return false
}
if lhs.items != rhs.items {
return false
}
if lhs.unreadCount != rhs.unreadCount {
return false
}
return true
}
}
public final class AdditionalItem: Equatable {
public final class PromoInfo: Equatable {
public enum Content: Equatable {
case proxy
case psa(type: String, message: String?)
}
public let content: Content
public init(content: Content) {
self.content = content
}
public static func ==(lhs: PromoInfo, rhs: PromoInfo) -> Bool {
if lhs.content != rhs.content {
return false
}
return true
}
}
public let item: Item
public let promoInfo: PromoInfo
public init(item: Item, promoInfo: PromoInfo) {
self.item = item
self.promoInfo = promoInfo
}
public static func ==(lhs: AdditionalItem, rhs: AdditionalItem) -> Bool {
if lhs.item != rhs.item {
return false
}
if lhs.promoInfo != rhs.promoInfo {
return false
}
return true
}
}
public let items: [Item]
public let groupItems: [GroupItem]
public let additionalItems: [AdditionalItem]
public let hasEarlier: Bool
public let hasLater: Bool
public let isLoading: Bool
public init(
items: [Item],
groupItems: [GroupItem],
additionalItems: [AdditionalItem],
hasEarlier: Bool,
hasLater: Bool,
isLoading: Bool
) {
self.items = items
self.groupItems = groupItems
self.additionalItems = additionalItems
self.hasEarlier = hasEarlier
self.hasLater = hasLater
self.isLoading = isLoading
}
public static func ==(lhs: EngineChatList, rhs: EngineChatList) -> Bool {
if lhs.items != rhs.items {
return false
}
if lhs.groupItems != rhs.groupItems {
return false
}
if lhs.additionalItems != rhs.additionalItems {
return false
}
if lhs.hasEarlier != rhs.hasEarlier {
return false
}
if lhs.hasLater != rhs.hasLater {
return false
}
if lhs.isLoading != rhs.isLoading {
return false
}
return true
}
}
public extension EngineChatList.Group {
init(_ group: PeerGroupId) {
switch group {
case .root:
self = .root
case let .group(value):
assert(value == Namespaces.PeerGroup.archive.rawValue)
self = .archive
}
}
func _asGroup() -> PeerGroupId {
switch self {
case .root:
return .root
case .archive:
return Namespaces.PeerGroup.archive
}
}
}
public extension EngineChatList.RelativePosition {
init(_ position: ChatListRelativePosition) {
switch position {
case let .earlier(than):
self = .earlier(than: than.flatMap(EngineChatList.Item.Index.chatList))
case let .later(than):
self = .later(than: than.flatMap(EngineChatList.Item.Index.chatList))
}
}
func _asPosition() -> ChatListRelativePosition? {
switch self {
case let .earlier(than):
guard case let .chatList(than) = than else {
return nil
}
return .earlier(than: than)
case let .later(than):
guard case let .chatList(than) = than else {
return nil
}
return .later(than: than)
}
}
}
private func calculateIsPremiumRequiredToMessage(isPremium: Bool, targetPeer: Peer, cachedIsPremiumRequired: Bool) -> Bool {
if isPremium {
return false
}
guard let targetPeer = targetPeer as? TelegramUser else {
return false
}
if !targetPeer.flags.contains(.requirePremium) {
return false
}
return cachedIsPremiumRequired
}
extension EngineChatList.Item {
convenience init?(_ entry: ChatListEntry, isPremium: Bool, displayAsTopicList: Bool) {
switch entry {
case let .MessageEntry(entryData):
let index = entryData.index
let messages = entryData.messages
let readState = entryData.readState
let isRemovedFromTotalUnreadCount = entryData.isRemovedFromTotalUnreadCount
let embeddedState = entryData.embeddedInterfaceState
let renderedPeer = entryData.renderedPeer
let presence = entryData.presence
let tagSummaryInfo = entryData.summaryInfo
let forumTopicData = entryData.forumTopicData
let topForumTopics = entryData.topForumTopics
let hasFailed = entryData.hasFailed
let isContact = entryData.isContact
let autoremoveTimeout = entryData.autoremoveTimeout
var isPremiumRequiredToMessage = false
if let targetPeer = renderedPeer.chatMainPeer, let extractedData = entryData.extractedCachedData?.base as? ExtractedChatListItemCachedData {
isPremiumRequiredToMessage = calculateIsPremiumRequiredToMessage(isPremium: isPremium, targetPeer: targetPeer, cachedIsPremiumRequired: extractedData.isPremiumRequiredToMessage)
}
var draft: EngineChatList.Draft?
var mediaDraftContentType: EngineChatList.MediaDraftContentType?
if let embeddedState = embeddedState, let _ = embeddedState.overrideChatTimestamp {
if let opaqueState = _internal_decodeStoredChatInterfaceState(state: embeddedState) {
if let text = opaqueState.synchronizeableInputState?.text {
draft = EngineChatList.Draft(text: text, entities: opaqueState.synchronizeableInputState?.entities ?? [])
}
mediaDraftContentType = opaqueState.mediaDraftState?.contentType
}
}
var hasUnseenMentions = false
if let info = tagSummaryInfo[ChatListEntryMessageTagSummaryKey(
tag: .unseenPersonalMessage,
actionType: PendingMessageActionType.consumeUnseenPersonalMessage
)] {
hasUnseenMentions = (info.tagSummaryCount ?? 0) > (info.actionsSummaryCount ?? 0)
}
var hasUnseenReactions = false
if let info = tagSummaryInfo[ChatListEntryMessageTagSummaryKey(
tag: .unseenReaction,
actionType: PendingMessageActionType.readReaction
)] {
hasUnseenReactions = (info.tagSummaryCount ?? 0) != 0// > (info.actionsSummaryCount ?? 0)
}
var forumTopicDataValue: EngineChatList.ForumTopicData?
if let forumTopicData {
let id = forumTopicData.id
if let forumTopicInfo = forumTopicData.info.data.get(MessageHistoryThreadData.self) {
forumTopicDataValue = EngineChatList.ForumTopicData(id: id, title: forumTopicInfo.info.title, iconFileId: forumTopicInfo.info.icon, iconColor: forumTopicInfo.info.iconColor, maxOutgoingReadMessageId: MessageId(peerId: index.messageIndex.id.peerId, namespace: Namespaces.Message.Cloud, id: forumTopicInfo.maxOutgoingReadId), isUnread: forumTopicInfo.incomingUnreadCount > 0, threadPeer: forumTopicData.threadPeer.flatMap(EnginePeer.init))
}
}
var topForumTopicItems: [EngineChatList.ForumTopicData] = []
for item in topForumTopics {
if let forumTopicInfo = item.info.data.get(MessageHistoryThreadData.self) {
topForumTopicItems.append(EngineChatList.ForumTopicData(id: item.id, title: forumTopicInfo.info.title, iconFileId: forumTopicInfo.info.icon, iconColor: forumTopicInfo.info.iconColor, maxOutgoingReadMessageId: MessageId(peerId: index.messageIndex.id.peerId, namespace: Namespaces.Message.Cloud, id: forumTopicInfo.maxOutgoingReadId), isUnread: forumTopicInfo.incomingUnreadCount > 0, threadPeer: item.threadPeer.flatMap(EnginePeer.init)))
}
}
let readCounters = readState.flatMap(EnginePeerReadCounters.init)
if let channel = renderedPeer.peer as? TelegramChannel {
if channel.isForumOrMonoForum {
draft = nil
} else {
forumTopicDataValue = nil
topForumTopicItems = []
}
}
self.init(
id: .chatList(index.messageIndex.id.peerId),
index: .chatList(index),
messages: messages.map(EngineMessage.init),
readCounters: readCounters,
isMuted: isRemovedFromTotalUnreadCount,
draft: draft,
threadData: nil,
renderedPeer: EngineRenderedPeer(renderedPeer),
presence: presence.flatMap(EnginePeer.Presence.init),
hasUnseenMentions: hasUnseenMentions,
hasUnseenReactions: hasUnseenReactions,
forumTopicData: forumTopicDataValue,
topForumTopicItems: topForumTopicItems,
hasFailed: hasFailed,
isContact: isContact,
autoremoveTimeout: autoremoveTimeout,
storyStats: entryData.storyStats,
displayAsTopicList: displayAsTopicList,
isPremiumRequiredToMessage: isPremiumRequiredToMessage,
mediaDraftContentType: mediaDraftContentType
)
case .HoleEntry:
return nil
}
}
}
extension EngineChatList.GroupItem {
convenience init(_ entry: ChatListGroupReferenceEntry) {
self.init(
id: EngineChatList.Group(entry.groupId),
topMessage: entry.message.flatMap(EngineMessage.init),
items: entry.renderedPeers.map { peer in
return EngineChatList.GroupItem.Item(
peer: EngineRenderedPeer(peer.peer),
isUnread: peer.isUnread
)
},
unreadCount: Int(entry.unreadState.count(countingCategory: .chats, mutedCategory: .all))
)
}
}
extension EngineChatList.AdditionalItem.PromoInfo {
convenience init(_ item: PromoChatListItem) {
let content: EngineChatList.AdditionalItem.PromoInfo.Content
switch item.kind {
case .proxy:
content = .proxy
case let .psa(type, message):
content = .psa(type: type, message: message)
}
self.init(content: content)
}
}
extension EngineChatList.AdditionalItem {
convenience init?(_ entry: ChatListAdditionalItemEntry) {
guard let item = EngineChatList.Item(entry.entry, isPremium: false, displayAsTopicList: false) else {
return nil
}
guard let promoInfo = (entry.info as? PromoChatListItem).flatMap(EngineChatList.AdditionalItem.PromoInfo.init) else {
return nil
}
self.init(item: item, promoInfo: promoInfo)
}
}
public extension EngineChatList {
convenience init(_ view: ChatListView, accountPeerId: PeerId) {
var isLoading = false
var displaySavedMessagesAsTopicList = false
if let value = view.displaySavedMessagesAsTopicList?.get(EngineDisplaySavedChatsAsTopics.self) {
displaySavedMessagesAsTopicList = value.value
}
let isPremium = view.accountPeer?.isPremium ?? false
var items: [EngineChatList.Item] = []
loop: for entry in view.entries {
switch entry {
case .MessageEntry:
if let item = EngineChatList.Item(entry, isPremium: isPremium, displayAsTopicList: entry.index.messageIndex.id.peerId == accountPeerId ? displaySavedMessagesAsTopicList : false) {
items.append(item)
}
case .HoleEntry:
isLoading = true
break loop
}
}
self.init(
items: items,
groupItems: view.groupEntries.map(EngineChatList.GroupItem.init),
additionalItems: view.additionalItemEntries.compactMap(EngineChatList.AdditionalItem.init),
hasEarlier: view.earlierIndex != nil,
hasLater: view.laterIndex != nil,
isLoading: isLoading
)
}
}
@@ -0,0 +1,83 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
func _internal_clearCloudDraftsInteractively(postbox: Postbox, network: Network, accountPeerId: PeerId) -> Signal<Void, NoError> {
return network.request(Api.functions.messages.getAllDrafts())
|> retryRequest
|> mapToSignal { updates -> Signal<Void, NoError> in
return postbox.transaction { transaction -> Signal<Void, NoError> in
struct Key: Hashable {
var peerId: PeerId
var threadId: Int64?
}
var keys = Set<Key>()
switch updates {
case let .updates(updates, users, chats, _, _):
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
for update in updates {
switch update {
case let .updateDraftMessage(_, peer, topMsgId, savedPeerId, _):
var threadId: Int64?
if let savedPeerId {
threadId = savedPeerId.peerId.toInt64()
} else if let topMsgId {
threadId = Int64(topMsgId)
}
keys.insert(Key(peerId: peer.peerId, threadId: threadId))
default:
break
}
}
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
var signals: [Signal<Void, NoError>] = []
for key in keys {
_internal_updateChatInputState(transaction: transaction, peerId: key.peerId, threadId: key.threadId, inputState: nil)
if let peer = transaction.getPeer(key.peerId), let inputPeer = apiInputPeer(peer) {
var topMsgId: Int32?
var monoforumPeerId: Api.InputPeer?
if let threadId = key.threadId {
if let channel = peer as? TelegramChannel, channel.flags.contains(.isMonoforum) {
monoforumPeerId = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer)
} else {
topMsgId = Int32(clamping: threadId)
}
}
var flags: Int32 = 0
var replyTo: Api.InputReplyTo?
if let topMsgId {
flags |= (1 << 0)
var innerFlags: Int32 = 0
innerFlags |= 1 << 0
replyTo = .inputReplyToMessage(flags: innerFlags, replyToMsgId: 0, topMsgId: topMsgId, replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil, monoforumPeerId: nil, todoItemId: nil)
} else if let monoforumPeerId {
flags |= (1 << 0)
replyTo = .inputReplyToMonoForum(monoforumPeerId: monoforumPeerId)
}
signals.append(network.request(Api.functions.messages.saveDraft(flags: flags, replyTo: replyTo, peer: inputPeer, message: "", entities: nil, media: nil, effect: nil, suggestedPost: nil))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
})
}
}
return combineLatest(signals)
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
default:
break
}
return .complete()
} |> switchToLatest
}
}
@@ -0,0 +1,217 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
func addMessageMediaResourceIdsToRemove(media: Media, resourceIds: inout [MediaResourceId]) {
if let image = media as? TelegramMediaImage {
for representation in image.representations {
resourceIds.append(representation.resource.id)
}
} else if let file = media as? TelegramMediaFile {
for representation in file.previewRepresentations {
resourceIds.append(representation.resource.id)
}
resourceIds.append(file.resource.id)
}
}
func addMessageMediaResourceIdsToRemove(message: Message, resourceIds: inout [MediaResourceId]) {
for media in message.media {
addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds)
}
}
public func _internal_deleteMessages(transaction: Transaction, mediaBox: MediaBox, ids: [MessageId], deleteMedia: Bool = true, manualAddMessageThreadStatsDifference: ((MessageThreadKey, Int, Int) -> Void)? = nil) {
var resourceIds: [MediaResourceId] = []
if deleteMedia {
for id in ids {
if id.peerId.namespace == Namespaces.Peer.SecretChat {
if let message = transaction.getMessage(id) {
addMessageMediaResourceIdsToRemove(message: message, resourceIds: &resourceIds)
}
}
}
}
if !resourceIds.isEmpty {
let _ = mediaBox.removeCachedResources(Array(Set(resourceIds)), force: true).start()
}
for id in ids {
if id.peerId.namespace == Namespaces.Peer.CloudChannel && id.namespace == Namespaces.Message.Cloud {
if let message = transaction.getMessage(id) {
if let threadId = message.threadId {
let messageThreadKey = MessageThreadKey(peerId: message.id.peerId, threadId: threadId)
if id.peerId.namespace == Namespaces.Peer.CloudChannel {
if let manualAddMessageThreadStatsDifference = manualAddMessageThreadStatsDifference {
manualAddMessageThreadStatsDifference(messageThreadKey, 0, 1)
} else {
updateMessageThreadStats(transaction: transaction, threadKey: messageThreadKey, removedCount: 1, addedMessagePeers: [])
}
}
}
}
}
}
transaction.deleteMessages(ids, forEachMedia: { _ in
})
}
func _internal_deleteAllMessagesWithAuthor(transaction: Transaction, mediaBox: MediaBox, peerId: PeerId, authorId: PeerId, namespace: MessageId.Namespace) {
var resourceIds: [MediaResourceId] = []
transaction.removeAllMessagesWithAuthor(peerId, authorId: authorId, namespace: namespace, forEachMedia: { media in
addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds)
})
if !resourceIds.isEmpty {
let _ = mediaBox.removeCachedResources(Array(Set(resourceIds))).start()
}
}
func _internal_deleteAllMessagesWithForwardAuthor(transaction: Transaction, mediaBox: MediaBox, peerId: PeerId, forwardAuthorId: PeerId, namespace: MessageId.Namespace) {
var resourceIds: [MediaResourceId] = []
transaction.removeAllMessagesWithForwardAuthor(peerId, forwardAuthorId: forwardAuthorId, namespace: namespace, forEachMedia: { media in
addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds)
})
if !resourceIds.isEmpty {
let _ = mediaBox.removeCachedResources(Array(Set(resourceIds)), force: true).start()
}
}
func _internal_clearHistory(transaction: Transaction, mediaBox: MediaBox, peerId: PeerId, threadId: Int64?, namespaces: MessageIdNamespaces) {
if peerId.namespace == Namespaces.Peer.SecretChat {
var resourceIds: [MediaResourceId] = []
transaction.withAllMessages(peerId: peerId, { message in
addMessageMediaResourceIdsToRemove(message: message, resourceIds: &resourceIds)
return true
})
if !resourceIds.isEmpty {
let _ = mediaBox.removeCachedResources(Array(Set(resourceIds)), force: true).start()
}
}
transaction.clearHistory(peerId, threadId: threadId, minTimestamp: nil, maxTimestamp: nil, namespaces: namespaces, forEachMedia: { _ in
})
}
func _internal_clearHistoryInRange(transaction: Transaction, mediaBox: MediaBox, peerId: PeerId, threadId: Int64?, minTimestamp: Int32, maxTimestamp: Int32, namespaces: MessageIdNamespaces) {
if peerId.namespace == Namespaces.Peer.SecretChat {
var resourceIds: [MediaResourceId] = []
transaction.withAllMessages(peerId: peerId, { message in
if message.timestamp >= minTimestamp && message.timestamp <= maxTimestamp {
addMessageMediaResourceIdsToRemove(message: message, resourceIds: &resourceIds)
}
return true
})
if !resourceIds.isEmpty {
let _ = mediaBox.removeCachedResources(Array(Set(resourceIds)), force: true).start()
}
}
transaction.clearHistory(peerId, threadId: threadId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, namespaces: namespaces, forEachMedia: { _ in
})
}
public enum ClearCallHistoryError {
case generic
}
func _internal_clearCallHistory(account: Account, forEveryone: Bool) -> Signal<Never, ClearCallHistoryError> {
return account.postbox.transaction { transaction -> Signal<Void, NoError> in
var flags: Int32 = 0
if forEveryone {
flags |= 1 << 0
}
let signal = account.network.request(Api.functions.messages.deletePhoneCallHistory(flags: flags))
|> map { result -> Api.messages.AffectedFoundMessages? in
return result
}
|> `catch` { _ -> Signal<Api.messages.AffectedFoundMessages?, Bool> in
return .fail(false)
}
|> mapToSignal { result -> Signal<Void, Bool> in
if let result = result {
switch result {
case let .affectedFoundMessages(pts, ptsCount, offset, _):
account.stateManager.addUpdateGroups([.updatePts(pts: pts, ptsCount: ptsCount)])
if offset == 0 {
return .fail(true)
} else {
return .complete()
}
}
} else {
return .fail(true)
}
}
return (signal
|> restart)
|> `catch` { success -> Signal<Void, NoError> in
if success {
return account.postbox.transaction { transaction -> Void in
transaction.removeAllMessagesWithGlobalTag(tag: GlobalMessageTags.Calls)
}
} else {
return .complete()
}
}
}
|> switchToLatest
|> ignoreValues
|> castError(ClearCallHistoryError.self)
}
public enum SetChatMessageAutoremoveTimeoutError {
case generic
}
func _internal_setChatMessageAutoremoveTimeoutInteractively(account: Account, peerId: PeerId, timeout: Int32?) -> Signal<Never, SetChatMessageAutoremoveTimeoutError> {
return account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}
|> castError(SetChatMessageAutoremoveTimeoutError.self)
|> mapToSignal { inputPeer -> Signal<Never, SetChatMessageAutoremoveTimeoutError> in
guard let inputPeer = inputPeer else {
return .fail(.generic)
}
return account.network.request(Api.functions.messages.setHistoryTTL(peer: inputPeer, period: timeout ?? 0))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> castError(SetChatMessageAutoremoveTimeoutError.self)
|> mapToSignal { result -> Signal<Never, SetChatMessageAutoremoveTimeoutError> in
if let result = result {
account.stateManager.addUpdates(result)
return account.postbox.transaction { transaction -> Void in
transaction.updatePeerCachedData(peerIds: [peerId], update: { _, current in
let updatedTimeout: CachedPeerAutoremoveTimeout
if let timeout = timeout {
updatedTimeout = .known(CachedPeerAutoremoveTimeout.Value(peerValue: timeout))
} else {
updatedTimeout = .known(nil)
}
if peerId.namespace == Namespaces.Peer.CloudUser {
let current = (current as? CachedUserData) ?? CachedUserData()
return current.withUpdatedAutoremoveTimeout(updatedTimeout)
} else if peerId.namespace == Namespaces.Peer.CloudChannel {
let current = (current as? CachedChannelData) ?? CachedChannelData()
return current.withUpdatedAutoremoveTimeout(updatedTimeout)
} else if peerId.namespace == Namespaces.Peer.CloudGroup {
let current = (current as? CachedGroupData) ?? CachedGroupData()
return current.withUpdatedAutoremoveTimeout(updatedTimeout)
} else {
return current
}
})
}
|> castError(SetChatMessageAutoremoveTimeoutError.self)
|> ignoreValues
} else {
return .fail(.generic)
}
}
|> `catch` { _ -> Signal<Never, SetChatMessageAutoremoveTimeoutError> in
return .complete()
}
}
}
@@ -0,0 +1,250 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
func _internal_deleteMessagesInteractively(account: Account, messageIds: [MessageId], type: InteractiveMessagesDeletionType, deleteAllInGroup: Bool = false) -> Signal<Void, NoError> {
return account.postbox.transaction { transaction -> Void in
deleteMessagesInteractively(transaction: transaction, stateManager: account.stateManager, postbox: account.postbox, messageIds: messageIds, type: type, removeIfPossiblyDelivered: true)
}
}
func deleteMessagesInteractively(transaction: Transaction, stateManager: AccountStateManager?, postbox: Postbox, messageIds initialMessageIds: [MessageId], type: InteractiveMessagesDeletionType, deleteAllInGroup: Bool = false, removeIfPossiblyDelivered: Bool) {
var messageIds: [MessageAndThreadId] = []
if deleteAllInGroup {
var tempIds: [MessageId] = initialMessageIds
for id in initialMessageIds {
if let group = transaction.getMessageGroup(id) ?? transaction.getMessageForwardedGroup(id) {
for message in group {
if !tempIds.contains(message.id) {
tempIds.append(message.id)
}
}
} else {
tempIds.append(id)
}
}
messageIds = tempIds.map { id in
if id.namespace == Namespaces.Message.QuickReplyCloud {
if let message = transaction.getMessage(id) {
return MessageAndThreadId(messageId: id, threadId: message.threadId)
} else {
return MessageAndThreadId(messageId: id, threadId: nil)
}
} else {
return MessageAndThreadId(messageId: id, threadId: nil)
}
}
} else {
messageIds = initialMessageIds.map { id in
if id.namespace == Namespaces.Message.QuickReplyCloud {
if let message = transaction.getMessage(id) {
return MessageAndThreadId(messageId: id, threadId: message.threadId)
} else {
return MessageAndThreadId(messageId: id, threadId: nil)
}
} else {
return MessageAndThreadId(messageId: id, threadId: nil)
}
}
}
var uniqueIds: [Int64: PeerId] = [:]
for (peerAndThreadId, peerMessageIds) in messagesIdsGroupedByPeerId(messageIds) {
let peerId = peerAndThreadId.peerId
let threadId = peerAndThreadId.threadId
for id in peerMessageIds {
if let message = transaction.getMessage(id) {
for attribute in message.attributes {
if let attribute = attribute as? OutgoingMessageInfoAttribute {
uniqueIds[attribute.uniqueId] = peerId
}
}
}
}
if peerId.namespace == Namespaces.Peer.CloudChannel || peerId.namespace == Namespaces.Peer.CloudGroup || peerId.namespace == Namespaces.Peer.CloudUser {
let remoteMessageIds = peerMessageIds.filter { id in
if id.namespace == Namespaces.Message.Local || id.namespace == Namespaces.Message.ScheduledLocal || id.namespace == Namespaces.Message.QuickReplyLocal {
return false
}
return true
}
if !remoteMessageIds.isEmpty {
cloudChatAddRemoveMessagesOperation(transaction: transaction, peerId: peerId, threadId: threadId, messageIds: remoteMessageIds, type: CloudChatRemoveMessagesType(type))
}
} else if peerId.namespace == Namespaces.Peer.SecretChat {
if let state = transaction.getPeerChatState(peerId) as? SecretChatState {
var layer: SecretChatLayer?
switch state.embeddedState {
case .terminated, .handshake:
break
case .basicLayer:
layer = .layer8
case let .sequenceBasedLayer(sequenceState):
layer = sequenceState.layerNegotiationState.activeLayer.secretChatLayer
}
if let layer = layer {
var globallyUniqueIds: [Int64] = []
for messageId in peerMessageIds {
if let message = transaction.getMessage(messageId), let globallyUniqueId = message.globallyUniqueId {
globallyUniqueIds.append(globallyUniqueId)
}
}
let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: SecretChatOutgoingOperationContents.deleteMessages(layer: layer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), globallyUniqueIds: globallyUniqueIds), state: state)
if updatedState != state {
transaction.setPeerChatState(peerId, state: updatedState)
}
}
}
}
}
_internal_deleteMessages(transaction: transaction, mediaBox: postbox.mediaBox, ids: messageIds.map(\.messageId))
stateManager?.notifyDeletedMessages(messageIds: messageIds.map(\.messageId))
if !uniqueIds.isEmpty && removeIfPossiblyDelivered {
stateManager?.removePossiblyDeliveredMessages(uniqueIds: uniqueIds)
}
}
func _internal_clearHistoryInRangeInteractively(postbox: Postbox, peerId: PeerId, threadId: Int64?, minTimestamp: Int32, maxTimestamp: Int32, type: InteractiveHistoryClearingType) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.CloudGroup || peerId.namespace == Namespaces.Peer.CloudChannel {
cloudChatAddClearHistoryOperation(transaction: transaction, peerId: peerId, threadId: threadId, explicitTopMessageId: nil, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, type: CloudChatClearHistoryType(type))
if type == .scheduledMessages {
} else {
_internal_clearHistoryInRange(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, threadId: threadId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, namespaces: .not(Namespaces.Message.allNonRegular))
}
} else if peerId.namespace == Namespaces.Peer.SecretChat {
/*_internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, namespaces: .all)
if let state = transaction.getPeerChatState(peerId) as? SecretChatState {
var layer: SecretChatLayer?
switch state.embeddedState {
case .terminated, .handshake:
break
case .basicLayer:
layer = .layer8
case let .sequenceBasedLayer(sequenceState):
layer = sequenceState.layerNegotiationState.activeLayer.secretChatLayer
}
if let layer = layer {
let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: SecretChatOutgoingOperationContents.clearHistory(layer: layer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max)), state: state)
if updatedState != state {
transaction.setPeerChatState(peerId, state: updatedState)
}
}
}*/
}
}
}
func _internal_clearHistoryInteractively(postbox: Postbox, peerId: PeerId, threadId: Int64?, type: InteractiveHistoryClearingType) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.CloudGroup || peerId.namespace == Namespaces.Peer.CloudChannel {
cloudChatAddClearHistoryOperation(transaction: transaction, peerId: peerId, threadId: threadId, explicitTopMessageId: nil, minTimestamp: nil, maxTimestamp: nil, type: CloudChatClearHistoryType(type))
if type == .scheduledMessages {
_internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, threadId: threadId, namespaces: .just(Namespaces.Message.allScheduled))
} else {
var topIndex: MessageIndex?
if let topMessageId = transaction.getTopPeerMessageId(peerId: peerId, namespace: Namespaces.Message.Cloud), let topMessage = transaction.getMessage(topMessageId) {
topIndex = topMessage.index
}
_internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, threadId: threadId, namespaces: .not(Namespaces.Message.allNonRegular))
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData, let migrationReference = cachedData.migrationReference {
cloudChatAddClearHistoryOperation(transaction: transaction, peerId: migrationReference.maxMessageId.peerId, threadId: threadId, explicitTopMessageId: MessageId(peerId: migrationReference.maxMessageId.peerId, namespace: migrationReference.maxMessageId.namespace, id: migrationReference.maxMessageId.id + 1), minTimestamp: nil, maxTimestamp: nil, type: CloudChatClearHistoryType(type))
_internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: migrationReference.maxMessageId.peerId, threadId: threadId, namespaces: .all)
}
if let topIndex = topIndex {
if peerId.namespace == Namespaces.Peer.CloudUser {
var addEmptyMessage = false
if threadId == nil {
addEmptyMessage = true
} else {
if transaction.getTopPeerMessageId(peerId: peerId, namespace: Namespaces.Message.Cloud) == nil {
addEmptyMessage = true
}
}
if addEmptyMessage {
let _ = transaction.addMessages([StoreMessage(id: topIndex.id, customStableId: nil, globallyUniqueId: nil, groupingKey: nil, threadId: nil, timestamp: topIndex.timestamp, flags: StoreMessageFlags(), tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: nil, text: "", attributes: [], media: [TelegramMediaAction(action: .historyCleared)])], location: .Random)
}
} else {
updatePeerChatInclusionWithMinTimestamp(transaction: transaction, id: peerId, minTimestamp: topIndex.timestamp, forceRootGroupIfNotExists: false)
}
}
}
} else if peerId.namespace == Namespaces.Peer.SecretChat {
_internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, threadId: nil, namespaces: .all)
if let state = transaction.getPeerChatState(peerId) as? SecretChatState {
var layer: SecretChatLayer?
switch state.embeddedState {
case .terminated, .handshake:
break
case .basicLayer:
layer = .layer8
case let .sequenceBasedLayer(sequenceState):
layer = sequenceState.layerNegotiationState.activeLayer.secretChatLayer
}
if let layer = layer {
let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: SecretChatOutgoingOperationContents.clearHistory(layer: layer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max)), state: state)
if updatedState != state {
transaction.setPeerChatState(peerId, state: updatedState)
}
}
}
}
}
}
func _internal_clearAuthorHistory(account: Account, peerId: PeerId, memberId: PeerId) -> Signal<Void, NoError> {
return account.postbox.transaction { transaction -> Signal<Void, NoError> in
if let peer = transaction.getPeer(peerId), let memberPeer = transaction.getPeer(memberId), let inputChannel = apiInputChannel(peer), let inputUser = apiInputPeer(memberPeer) {
let signal = peer.isMonoForum ? .fail(true) : account.network.request(Api.functions.channels.deleteParticipantHistory(channel: inputChannel, participant: inputUser))
|> map { result -> Api.messages.AffectedHistory? in
return result
}
|> `catch` { _ -> Signal<Api.messages.AffectedHistory?, Bool> in
return .fail(false)
}
|> mapToSignal { result -> Signal<Void, Bool> in
if let result = result {
switch result {
case let .affectedHistory(pts, ptsCount, offset):
account.stateManager.addUpdateGroups([.updatePts(pts: pts, ptsCount: ptsCount)])
if offset == 0 {
return .fail(true)
} else {
return .complete()
}
}
} else {
return .fail(true)
}
}
return (signal
|> restart)
|> `catch` { success -> Signal<Void, NoError> in
if success {
return account.postbox.transaction { transaction -> Void in
_internal_deleteAllMessagesWithAuthor(transaction: transaction, mediaBox: account.postbox.mediaBox, peerId: peerId, authorId: memberId, namespace: Namespaces.Message.Cloud)
}
} else {
return .complete()
}
}
} else {
return .complete()
}
} |> switchToLatest
}
@@ -0,0 +1,144 @@
import Foundation
import Postbox
import SwiftSignalKit
import MtProtoKit
public enum EarliestUnseenPersonalMentionMessageResult: Equatable {
case loading
case result(MessageId?)
}
func _internal_earliestUnseenPersonalMentionMessage(account: Account, peerId: PeerId, threadId: Int64?) -> Signal<EarliestUnseenPersonalMentionMessageResult, NoError> {
return account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId: peerId, threadId: threadId), index: .lowerBound, anchorIndex: .lowerBound, count: 4, fixedCombinedReadStates: nil, tag: .tag(.unseenPersonalMessage), additionalData: [.peerChatState(peerId)])
|> mapToSignal { view -> Signal<EarliestUnseenPersonalMentionMessageResult, NoError> in
if view.0.isLoading {
return .single(.loading)
}
if case .FillHole = view.1 {
return _internal_earliestUnseenPersonalMentionMessage(account: account, peerId: peerId, threadId: threadId)
}
if let message = view.0.entries.first?.message {
if peerId.namespace == Namespaces.Peer.CloudChannel {
var invalidatedPts: Int32?
for data in view.0.additionalData {
switch data {
case let .peerChatState(_, state):
if let state = state as? ChannelState {
invalidatedPts = state.invalidatedPts
}
default:
break
}
}
if let invalidatedPts = invalidatedPts {
var messagePts: Int32?
for attribute in message.attributes {
if let attribute = attribute as? ChannelMessageStateVersionAttribute {
messagePts = attribute.pts
break
}
}
if let messagePts = messagePts {
if messagePts < invalidatedPts {
return .single(.loading)
}
}
}
return .single(.result(message.id))
} else {
return .single(.result(message.id))
}
} else {
return account.postbox.transaction { transaction -> EarliestUnseenPersonalMentionMessageResult in
if let topId = transaction.getTopPeerMessageId(peerId: peerId, namespace: Namespaces.Message.Cloud) {
transaction.replaceMessageTagSummary(peerId: peerId, threadId: threadId, tagMask: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud, customTag: nil, count: 0, maxId: topId.id)
transaction.removeHole(peerId: peerId, threadId: threadId, namespace: Namespaces.Message.Cloud, space: .tag(.unseenPersonalMessage), range: 1 ... (Int32.max - 1))
let ids = transaction.getMessageIndicesWithTag(peerId: peerId, threadId: threadId, namespace: Namespaces.Message.Cloud, tag: .unseenPersonalMessage).map({ $0.id })
for id in ids {
markUnseenPersonalMessage(transaction: transaction, id: id, addSynchronizeAction: false)
}
}
return .result(nil)
}
}
}
|> distinctUntilChanged
|> take(until: { value in
if case .result = value {
return SignalTakeAction(passthrough: true, complete: true)
} else {
return SignalTakeAction(passthrough: true, complete: false)
}
})
}
func _internal_earliestUnseenPersonalReactionMessage(account: Account, peerId: PeerId, threadId: Int64?) -> Signal<EarliestUnseenPersonalMentionMessageResult, NoError> {
return account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId: peerId, threadId: threadId), index: .lowerBound, anchorIndex: .lowerBound, count: 4, fixedCombinedReadStates: nil, tag: .tag(.unseenReaction), additionalData: [.peerChatState(peerId)])
|> mapToSignal { view -> Signal<EarliestUnseenPersonalMentionMessageResult, NoError> in
if view.0.isLoading {
return .single(.loading)
}
if case .FillHole = view.1 {
return _internal_earliestUnseenPersonalReactionMessage(account: account, peerId: peerId, threadId: threadId)
}
if let message = view.0.entries.first?.message {
if peerId.namespace == Namespaces.Peer.CloudChannel {
var invalidatedPts: Int32?
for data in view.0.additionalData {
switch data {
case let .peerChatState(_, state):
if let state = state as? ChannelState {
invalidatedPts = state.invalidatedPts
}
default:
break
}
}
if let invalidatedPts = invalidatedPts {
var messagePts: Int32?
for attribute in message.attributes {
if let attribute = attribute as? ChannelMessageStateVersionAttribute {
messagePts = attribute.pts
break
}
}
if let messagePts = messagePts {
if messagePts < invalidatedPts {
return .single(.loading)
}
}
}
return .single(.result(message.id))
} else {
return .single(.result(message.id))
}
} else {
return account.postbox.transaction { transaction -> EarliestUnseenPersonalMentionMessageResult in
if let topId = transaction.getTopPeerMessageId(peerId: peerId, namespace: Namespaces.Message.Cloud) {
transaction.replaceMessageTagSummary(peerId: peerId, threadId: threadId, tagMask: .unseenReaction, namespace: Namespaces.Message.Cloud, customTag: nil, count: 0, maxId: topId.id)
transaction.removeHole(peerId: peerId, threadId: threadId, namespace: Namespaces.Message.Cloud, space: .tag(.unseenReaction), range: 1 ... (Int32.max - 1))
let ids = transaction.getMessageIndicesWithTag(peerId: peerId, threadId: threadId, namespace: Namespaces.Message.Cloud, tag: .unseenReaction).map({ $0.id })
for id in ids {
markUnseenReactionMessage(transaction: transaction, id: id, addSynchronizeAction: false)
}
}
return .result(nil)
}
}
}
|> distinctUntilChanged
|> take(until: { value in
if case .result = value {
return SignalTakeAction(passthrough: true, complete: true)
} else {
return SignalTakeAction(passthrough: true, complete: false)
}
})
}
@@ -0,0 +1,39 @@
import Postbox
public final class EngineGroupCallDescription {
public let id: Int64
public let accessHash: Int64
public let title: String?
public let scheduleTimestamp: Int32?
public let subscribedToScheduled: Bool
public let isStream: Bool?
public init(
id: Int64,
accessHash: Int64,
title: String?,
scheduleTimestamp: Int32?,
subscribedToScheduled: Bool,
isStream: Bool?
) {
self.id = id
self.accessHash = accessHash
self.title = title
self.scheduleTimestamp = scheduleTimestamp
self.subscribedToScheduled = subscribedToScheduled
self.isStream = isStream
}
}
public extension EngineGroupCallDescription {
convenience init(_ activeCall: CachedChannelData.ActiveCall) {
self.init(
id: activeCall.id,
accessHash: activeCall.accessHash,
title: activeCall.title,
scheduleTimestamp: activeCall.scheduleTimestamp,
subscribedToScheduled: activeCall.subscribedToScheduled,
isStream: activeCall.isStream
)
}
}
@@ -0,0 +1,904 @@
import Foundation
import SwiftSignalKit
import Postbox
import TelegramApi
public final class EngineStoryViewListContext {
public struct LoadMoreToken: Equatable {
var value: String
}
public enum ListMode {
case everyone
case contacts
}
public enum SortMode {
case repostsFirst
case reactionsFirst
case recentFirst
}
public enum Item: Equatable {
public final class View: Equatable {
public let peer: EnginePeer
public let timestamp: Int32
public let storyStats: PeerStoryStats?
public let reaction: MessageReaction.Reaction?
public let reactionFile: TelegramMediaFile?
public init(
peer: EnginePeer,
timestamp: Int32,
storyStats: PeerStoryStats?,
reaction: MessageReaction.Reaction?,
reactionFile: TelegramMediaFile?
) {
self.peer = peer
self.timestamp = timestamp
self.storyStats = storyStats
self.reaction = reaction
self.reactionFile = reactionFile
}
public static func ==(lhs: View, rhs: View) -> Bool {
if lhs.peer != rhs.peer {
return false
}
if lhs.timestamp != rhs.timestamp {
return false
}
if lhs.storyStats != rhs.storyStats {
return false
}
if lhs.reaction != rhs.reaction {
return false
}
if lhs.reactionFile?.fileId != rhs.reactionFile?.fileId {
return false
}
return true
}
}
public final class Repost: Equatable {
public let peer: EnginePeer
public let story: EngineStoryItem
public let storyStats: PeerStoryStats?
init(peer: EnginePeer, story: EngineStoryItem, storyStats: PeerStoryStats?) {
self.peer = peer
self.story = story
self.storyStats = storyStats
}
public static func ==(lhs: Repost, rhs: Repost) -> Bool {
if lhs.peer != rhs.peer {
return false
}
if lhs.story != rhs.story {
return false
}
if lhs.storyStats != rhs.storyStats {
return false
}
return true
}
}
public final class Forward: Equatable {
public let message: EngineMessage
public let storyStats: PeerStoryStats?
init(message: EngineMessage, storyStats: PeerStoryStats?) {
self.message = message
self.storyStats = storyStats
}
public static func ==(lhs: Forward, rhs: Forward) -> Bool {
if lhs.message != rhs.message {
return false
}
if lhs.storyStats != rhs.storyStats {
return false
}
return true
}
}
case view(View)
case repost(Repost)
case forward(Forward)
public var peer: EnginePeer {
switch self {
case let .view(view):
return view.peer
case let .repost(repost):
return repost.peer
case let .forward(forward):
return EnginePeer(forward.message.peers[forward.message.id.peerId]!)
}
}
public var timestamp: Int32 {
switch self {
case let .view(view):
return view.timestamp
case let .repost(repost):
return repost.story.timestamp
case let .forward(forward):
return forward.message.timestamp
}
}
public var reaction: MessageReaction.Reaction? {
switch self {
case let .view(view):
return view.reaction
case .repost:
return nil
case .forward:
return nil
}
}
public var story: EngineStoryItem? {
switch self {
case .view:
return nil
case let .repost(repost):
return repost.story
case .forward:
return nil
}
}
public var message: EngineMessage? {
switch self {
case .view:
return nil
case .repost:
return nil
case let .forward(forward):
return forward.message
}
}
public var storyStats: PeerStoryStats? {
switch self {
case let .view(view):
return view.storyStats
case let .repost(repost):
return repost.storyStats
case let .forward(forward):
return forward.storyStats
}
}
public struct ItemHash: Hashable {
public var peerId: EnginePeer.Id
public var storyId: Int32?
public var messageId: EngineMessage.Id?
}
public var uniqueId: ItemHash {
switch self {
case let .view(view):
return ItemHash(peerId: view.peer.id, storyId: nil, messageId: nil)
case let .repost(repost):
return ItemHash(peerId: repost.peer.id, storyId: repost.story.id, messageId: nil)
case let .forward(forward):
return ItemHash(peerId: forward.message.id.peerId, storyId: nil, messageId: forward.message.id)
}
}
}
public struct State: Equatable {
public var totalCount: Int
public var totalReactedCount: Int
public var items: [Item]
public var loadMoreToken: LoadMoreToken?
public init(
totalCount: Int,
totalReactedCount: Int,
items: [Item],
loadMoreToken: LoadMoreToken?
) {
self.totalCount = totalCount
self.totalReactedCount = totalReactedCount
self.items = items
self.loadMoreToken = loadMoreToken
}
}
private final class Impl {
struct NextOffset: Equatable {
var value: String
}
struct InternalState: Equatable {
var totalCount: Int
var totalViewsCount: Int
var totalForwardsCount: Int
var totalReactedCount: Int
var items: [Item]
var canLoadMore: Bool
var nextOffset: NextOffset?
}
let queue: Queue
let account: Account
let peerId: EnginePeer.Id
let storyId: Int32
let listMode: ListMode
let sortMode: SortMode
let searchQuery: String?
let disposable = MetaDisposable()
let storyStatsDisposable = MetaDisposable()
var state: InternalState?
let statePromise = Promise<InternalState>()
private var parentSource: Impl?
var isLoadingMore: Bool = false
init(queue: Queue, account: Account, peerId: EnginePeer.Id, storyId: Int32, views: EngineStoryItem.Views, listMode: ListMode, sortMode: SortMode, searchQuery: String?, parentSource: Impl?) {
self.queue = queue
self.account = account
self.peerId = peerId
self.storyId = storyId
self.listMode = listMode
self.sortMode = sortMode
self.searchQuery = searchQuery
if let parentSource = parentSource, (parentSource.listMode == .everyone || parentSource.listMode == listMode), let parentState = parentSource.state, parentState.totalCount <= 100 {
self.parentSource = parentSource
let matchesMode = parentSource.listMode == listMode
if parentState.items.count < 100 && !matchesMode {
parentSource.loadMore()
}
self.disposable.set((parentSource.statePromise.get()
|> mapToSignal { state -> Signal<InternalState, NoError> in
let needUpdate: Signal<Void, NoError>
if listMode == .contacts {
var keys: [PostboxViewKey] = []
for item in state.items {
keys.append(.isContact(id: item.peer.id))
}
needUpdate = account.postbox.combinedView(keys: keys)
|> map { views -> [Bool] in
var result: [Bool] = []
for item in state.items {
if let view = views.views[.isContact(id: item.peer.id)] as? IsContactView {
result.append(view.isContact)
}
}
return result
}
|> distinctUntilChanged
|> map { _ -> Void in
return Void()
}
} else {
needUpdate = .single(Void())
}
return needUpdate
|> mapToSignal { _ -> Signal<InternalState, NoError> in
return account.postbox.transaction { transaction -> InternalState in
/*if state.canLoadMore && !matchesMode {
return InternalState(
totalCount: listMode == .everyone ? state.totalCount : 100, totalReactedCount: state.totalReactedCount, items: [], canLoadMore: true, nextOffset: state.nextOffset)
}*/
var items: [Item] = []
switch listMode {
case .everyone:
items = state.items
case .contacts:
items = state.items.filter { item in
return transaction.isPeerContact(peerId: item.peer.id)
}
}
if let searchQuery = searchQuery, !searchQuery.isEmpty {
let normalizedQuery = searchQuery.lowercased()
items = state.items.filter { item in
return item.peer.indexName.matchesByTokens(normalizedQuery)
}
}
switch sortMode {
case .repostsFirst:
items.sort(by: { lhs, rhs in
if (lhs.story == nil) != (rhs.story == nil) {
return lhs.story != nil
}
if (lhs.message == nil) != (rhs.message == nil) {
return lhs.message != nil
}
if lhs.timestamp != rhs.timestamp {
return lhs.timestamp > rhs.timestamp
}
return lhs.peer.id < rhs.peer.id
})
case .reactionsFirst:
items.sort(by: { lhs, rhs in
if (lhs.story == nil) != (rhs.story == nil) {
return lhs.story == nil
}
if (lhs.message == nil) != (rhs.message == nil) {
return lhs.message == nil
}
if (lhs.reaction == nil) != (rhs.reaction == nil) {
return lhs.reaction != nil
}
if lhs.timestamp != rhs.timestamp {
return lhs.timestamp > rhs.timestamp
}
return lhs.peer.id < rhs.peer.id
})
case .recentFirst:
items.sort(by: { lhs, rhs in
if lhs.timestamp != rhs.timestamp {
return lhs.timestamp > rhs.timestamp
}
return lhs.peer.id < rhs.peer.id
})
}
var totalCount = items.count
var totalReactedCount = 0
for item in items {
if item.reaction != nil {
totalReactedCount += 1
}
}
if state.canLoadMore {
totalCount = state.totalCount
totalReactedCount = state.totalReactedCount
}
return InternalState(
totalCount: totalCount,
totalViewsCount: 0,
totalForwardsCount: 0,
totalReactedCount: totalReactedCount,
items: items,
canLoadMore: state.canLoadMore
)
}
}
}
|> deliverOn(self.queue)).start(next: { [weak self] state in
guard let `self` = self else {
return
}
self.updateInternalState(state: state)
}))
} else {
let initialState = State(totalCount: listMode == .everyone ? views.seenCount : 100, totalReactedCount: views.reactedCount, items: [], loadMoreToken: LoadMoreToken(value: ""))
let state = InternalState(totalCount: initialState.totalCount, totalViewsCount: initialState.totalCount, totalForwardsCount: initialState.totalCount, totalReactedCount: initialState.totalReactedCount, items: initialState.items, canLoadMore: initialState.loadMoreToken != nil, nextOffset: nil)
self.state = state
self.statePromise.set(.single(state))
if initialState.loadMoreToken != nil {
self.loadMore()
}
}
}
deinit {
assert(self.queue.isCurrent())
self.disposable.dispose()
self.storyStatsDisposable.dispose()
}
func loadMore() {
if let parentSource = self.parentSource {
parentSource.loadMore()
return
}
guard let state = self.state else {
return
}
if !state.canLoadMore {
return
}
if self.isLoadingMore {
return
}
self.isLoadingMore = true
let account = self.account
let accountPeerId = account.peerId
let peerId = self.peerId
let storyId = self.storyId
let listMode = self.listMode
let sortMode = self.sortMode
let searchQuery = self.searchQuery
let currentOffset = state.nextOffset
let limit = 50
let signal: Signal<InternalState, NoError>
if peerId.namespace == Namespaces.Peer.CloudUser {
signal = self.account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<InternalState, NoError> in
guard let inputPeer = inputPeer else {
return .complete()
}
var flags: Int32 = 0
switch listMode {
case .everyone:
break
case .contacts:
flags |= (1 << 0)
}
switch sortMode {
case .reactionsFirst:
flags |= (1 << 2)
case .recentFirst, .repostsFirst:
break
}
if searchQuery != nil {
flags |= (1 << 1)
}
return account.network.request(Api.functions.stories.getStoryViewsList(flags: flags, peer: inputPeer, q: searchQuery, id: storyId, offset: currentOffset?.value ?? "", limit: Int32(limit)))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.stories.StoryViewsList?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<InternalState, NoError> in
return account.postbox.transaction { transaction -> InternalState in
switch result {
case let .storyViewsList(_, count, viewsCount, forwardsCount, reactionsCount, views, chats, users, nextOffset):
let peers = AccumulatedPeers(chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: peers)
var items: [Item] = []
for view in views {
switch view {
case let .storyView(flags, userId, date, reaction):
let isBlocked = (flags & (1 << 0)) != 0
let isBlockedFromStories = (flags & (1 << 1)) != 0
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData in
let previousData: CachedUserData
if let current = cachedData as? CachedUserData {
previousData = current
} else {
previousData = CachedUserData()
}
var updatedFlags = previousData.flags
if isBlockedFromStories {
updatedFlags.insert(.isBlockedFromStories)
} else {
updatedFlags.remove(.isBlockedFromStories)
}
return previousData.withUpdatedIsBlocked(isBlocked).withUpdatedFlags(updatedFlags)
})
if let peer = transaction.getPeer(peerId) {
let parsedReaction = reaction.flatMap(MessageReaction.Reaction.init(apiReaction:))
items.append(.view(Item.View(
peer: EnginePeer(peer),
timestamp: date,
storyStats: transaction.getPeerStoryStats(peerId: peerId),
reaction: parsedReaction,
reactionFile: parsedReaction.flatMap { reaction -> TelegramMediaFile? in
switch reaction {
case .builtin:
return nil
case let .custom(fileId):
return transaction.getMedia(MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)) as? TelegramMediaFile
case .stars:
return nil
}
}
)))
}
case let .storyViewPublicForward(flags, message):
let _ = flags
if let storeMessage = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: false), let message = locallyRenderedMessage(message: storeMessage, peers: peers.peers) {
items.append(.forward(Item.Forward(
message: EngineMessage(message),
storyStats: transaction.getPeerStoryStats(peerId: message.id.peerId)
)))
}
case let .storyViewPublicRepost(flags, peerId, story):
let _ = flags
if let peer = transaction.getPeer(peerId.peerId) {
if let storedItem = Stories.StoredItem(apiStoryItem: story, peerId: peer.id, transaction: transaction), case let .item(item) = storedItem, let media = item.media {
items.append(.repost(Item.Repost(
peer: EnginePeer(peer),
story: EngineStoryItem(
id: item.id,
timestamp: item.timestamp,
expirationTimestamp: item.expirationTimestamp,
media: EngineMedia(media),
alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init),
mediaAreas: item.mediaAreas,
text: item.text,
entities: item.entities,
views: item.views.flatMap { views in
return EngineStoryItem.Views(
seenCount: views.seenCount,
reactedCount: views.reactedCount,
forwardCount: views.forwardCount,
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
return transaction.getPeer(id).flatMap(EnginePeer.init)
},
reactions: views.reactions,
hasList: views.hasList
)
},
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
isPinned: item.isPinned,
isExpired: item.isExpired,
isPublic: item.isPublic,
isPending: false,
isCloseFriends: item.isCloseFriends,
isContacts: item.isContacts,
isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited,
isMy: item.isMy,
myReaction: item.myReaction,
forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) },
author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) },
folderIds: item.folderIds
),
storyStats: transaction.getPeerStoryStats(peerId: peer.id)
)))
}
}
}
}
if listMode == .everyone, searchQuery == nil {
if let storedItem = transaction.getStory(id: StoryId(peerId: account.peerId, id: storyId))?.get(Stories.StoredItem.self), case let .item(item) = storedItem, let currentViews = item.views {
let updatedItem: Stories.StoredItem = .item(Stories.Item(
id: item.id,
timestamp: item.timestamp,
expirationTimestamp: item.expirationTimestamp,
media: item.media,
alternativeMediaList: item.alternativeMediaList,
mediaAreas: item.mediaAreas,
text: item.text,
entities: item.entities,
views: Stories.Item.Views(
seenCount: Int(count),
reactedCount: Int(reactionsCount),
forwardCount: Int(forwardsCount),
seenPeerIds: currentViews.seenPeerIds,
reactions: currentViews.reactions,
hasList: currentViews.hasList
),
privacy: item.privacy,
isPinned: item.isPinned,
isExpired: item.isExpired,
isPublic: item.isPublic,
isCloseFriends: item.isCloseFriends,
isContacts: item.isContacts,
isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited,
isMy: item.isMy,
myReaction: item.myReaction,
forwardInfo: item.forwardInfo,
authorId: item.authorId,
folderIds: item.folderIds
))
if let entry = CodableEntry(updatedItem) {
transaction.setStory(id: StoryId(peerId: account.peerId, id: storyId), value: entry)
}
}
var currentItems = transaction.getStoryItems(peerId: account.peerId)
for i in 0 ..< currentItems.count {
if currentItems[i].id == storyId {
if case let .item(item) = currentItems[i].value.get(Stories.StoredItem.self), let currentViews = item.views {
let updatedItem: Stories.StoredItem = .item(Stories.Item(
id: item.id,
timestamp: item.timestamp,
expirationTimestamp: item.expirationTimestamp,
media: item.media,
alternativeMediaList: item.alternativeMediaList,
mediaAreas: item.mediaAreas,
text: item.text,
entities: item.entities,
views: Stories.Item.Views(
seenCount: Int(count),
reactedCount: Int(reactionsCount),
forwardCount: Int(forwardsCount),
seenPeerIds: currentViews.seenPeerIds,
reactions: currentViews.reactions,
hasList: currentViews.hasList
),
privacy: item.privacy,
isPinned: item.isPinned,
isExpired: item.isExpired,
isPublic: item.isPublic,
isCloseFriends: item.isCloseFriends,
isContacts: item.isContacts,
isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited,
isMy: item.isMy,
myReaction: item.myReaction,
forwardInfo: item.forwardInfo,
authorId: item.authorId,
folderIds: item.folderIds
))
if let entry = CodableEntry(updatedItem) {
currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends, isLiveStream: updatedItem.isLiveStream)
}
}
}
}
transaction.setStoryItems(peerId: account.peerId, items: currentItems)
}
return InternalState(totalCount: Int(count), totalViewsCount: Int(viewsCount), totalForwardsCount: Int(forwardsCount), totalReactedCount: Int(reactionsCount), items: items, canLoadMore: nextOffset != nil, nextOffset: nextOffset.flatMap { NextOffset(value: $0) })
case .none:
return InternalState(totalCount: 0, totalViewsCount: 0, totalForwardsCount: 0, totalReactedCount: 0, items: [], canLoadMore: false, nextOffset: nil)
}
}
}
}
} else {
signal = self.account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<InternalState, NoError> in
guard let inputPeer = inputPeer else {
return .complete()
}
var flags: Int32 = 0
if let _ = currentOffset {
flags |= (1 << 1)
}
if case .repostsFirst = sortMode {
flags |= (1 << 2)
}
return account.network.request(Api.functions.stories.getStoryReactionsList(flags: flags, peer: inputPeer, id: storyId, reaction: nil, offset: currentOffset?.value, limit: Int32(limit)))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.stories.StoryReactionsList?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<InternalState, NoError> in
return account.postbox.transaction { transaction -> InternalState in
switch result {
case let .storyReactionsList(_, count, reactions, chats, users, nextOffset):
let peers = AccumulatedPeers(chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: peers)
var items: [Item] = []
for reaction in reactions {
switch reaction {
case let .storyReaction(peerId, date, reaction):
if let peer = transaction.getPeer(peerId.peerId) {
if let parsedReaction = MessageReaction.Reaction(apiReaction: reaction) {
let reactionFile: TelegramMediaFile?
switch parsedReaction {
case .builtin:
reactionFile = nil
case let .custom(fileId):
reactionFile = transaction.getMedia(MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)) as? TelegramMediaFile
case .stars:
reactionFile = nil
}
items.append(.view(Item.View(
peer: EnginePeer(peer),
timestamp: date,
storyStats: transaction.getPeerStoryStats(peerId: peer.id),
reaction: parsedReaction,
reactionFile: reactionFile
)))
}
}
case let .storyReactionPublicForward(message):
if let storeMessage = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: false), let message = locallyRenderedMessage(message: storeMessage, peers: peers.peers) {
items.append(.forward(Item.Forward(
message: EngineMessage(message),
storyStats: transaction.getPeerStoryStats(peerId: message.id.peerId)
)))
}
case let .storyReactionPublicRepost(peerId, story):
if let peer = transaction.getPeer(peerId.peerId) {
if let storedItem = Stories.StoredItem(apiStoryItem: story, peerId: peer.id, transaction: transaction), case let .item(item) = storedItem, let media = item.media {
items.append(.repost(Item.Repost(
peer: EnginePeer(peer),
story: EngineStoryItem(
id: item.id,
timestamp: item.timestamp,
expirationTimestamp: item.expirationTimestamp,
media: EngineMedia(media),
alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init),
mediaAreas: item.mediaAreas,
text: item.text,
entities: item.entities,
views: item.views.flatMap { views in
return EngineStoryItem.Views(
seenCount: views.seenCount,
reactedCount: views.reactedCount,
forwardCount: views.forwardCount,
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
return transaction.getPeer(id).flatMap(EnginePeer.init)
},
reactions: views.reactions,
hasList: views.hasList
)
},
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
isPinned: item.isPinned,
isExpired: item.isExpired,
isPublic: item.isPublic,
isPending: false,
isCloseFriends: item.isCloseFriends,
isContacts: item.isContacts,
isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited,
isMy: item.isMy,
myReaction: item.myReaction,
forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) },
author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) },
folderIds: item.folderIds
),
storyStats: transaction.getPeerStoryStats(peerId: peer.id)
)))
}
}
}
}
return InternalState(totalCount: Int(count), totalViewsCount: 0, totalForwardsCount: 0, totalReactedCount: Int(count), items: items, canLoadMore: nextOffset != nil, nextOffset: nextOffset.flatMap { NextOffset(value: $0) })
case .none:
return InternalState(totalCount: 0, totalViewsCount: 0, totalForwardsCount: 0, totalReactedCount: 0, items: [], canLoadMore: false, nextOffset: nil)
}
}
}
}
}
self.disposable.set((signal
|> deliverOn(self.queue)).start(next: { [weak self] state in
guard let `self` = self else {
return
}
self.updateInternalState(state: state)
}))
}
private func updateInternalState(state: InternalState) {
var currentState = self.state ?? InternalState(
totalCount: 0, totalViewsCount: 0, totalForwardsCount: 0, totalReactedCount: 0, items: [], canLoadMore: false, nextOffset: nil)
if self.parentSource != nil {
currentState.items.removeAll()
}
var existingItems = Set<Item.ItemHash>()
for item in currentState.items {
existingItems.insert(item.uniqueId)
}
for item in state.items {
let itemHash = item.uniqueId
if existingItems.contains(itemHash) {
continue
}
existingItems.insert(itemHash)
currentState.items.append(item)
}
var allReactedCount = 0
for item in currentState.items {
if case let .view(view) = item, view.reaction != nil {
allReactedCount += 1
} else {
break
}
}
if state.canLoadMore {
currentState.totalCount = max(state.totalCount, currentState.items.count)
currentState.totalReactedCount = max(state.totalReactedCount, allReactedCount)
} else {
currentState.totalCount = currentState.items.count
currentState.totalReactedCount = allReactedCount
}
currentState.canLoadMore = state.canLoadMore
currentState.nextOffset = state.nextOffset
self.isLoadingMore = false
self.state = currentState
self.statePromise.set(.single(currentState))
let statsKey: PostboxViewKey = .peerStoryStats(peerIds: Set(currentState.items.map(\.peer.id)))
self.storyStatsDisposable.set((self.account.postbox.combinedView(keys: [statsKey])
|> deliverOn(self.queue)).start(next: { [weak self] views in
guard let `self` = self, var state = self.state else {
return
}
guard let view = views.views[statsKey] as? PeerStoryStatsView else {
return
}
var updated = false
var items = state.items
for i in 0 ..< state.items.count {
let item = items[i]
let value = view.storyStats[item.peer.id]
if case let .view(view) = item, view.storyStats != value {
updated = true
items[i] = .view(Item.View(
peer: view.peer,
timestamp: view.timestamp,
storyStats: value,
reaction: view.reaction,
reactionFile: view.reactionFile
))
}
}
if updated {
state.items = items
self.state = state
self.statePromise.set(.single(state))
}
}))
}
}
private let queue: Queue
private let impl: QueueLocalObject<Impl>
public var state: Signal<State, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.statePromise.get().start(next: { state in
var loadMoreToken: LoadMoreToken?
if let nextOffset = state.nextOffset {
loadMoreToken = LoadMoreToken(value: nextOffset.value)
}
subscriber.putNext(State(
totalCount: state.totalCount,
totalReactedCount: state.totalReactedCount,
items: state.items,
loadMoreToken: loadMoreToken
))
}))
}
return disposable
}
}
init(account: Account, peerId: EnginePeer.Id, storyId: Int32, views: EngineStoryItem.Views, listMode: ListMode, sortMode: SortMode, searchQuery: String?, parentSource: EngineStoryViewListContext?) {
let queue = Queue.mainQueue()
self.queue = queue
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, account: account, peerId: peerId, storyId: storyId, views: views, listMode: listMode, sortMode: sortMode, searchQuery: searchQuery, parentSource: parentSource?.impl.syncWith { $0 })
})
}
public func loadMore() {
self.impl.with { impl in
impl.loadMore()
}
}
}
@@ -0,0 +1,38 @@
import Postbox
import TelegramApi
import SwiftSignalKit
public func _internal_exportMessageLink(postbox: Postbox, network: Network, peerId: PeerId, messageId: MessageId, isThread: Bool = false) -> Signal<String?, NoError> {
return postbox.transaction { transaction -> (Peer, MessageId)? in
let peer: Peer? = transaction.getPeer(messageId.peerId)
if let peer = peer {
return (peer, messageId)
} else {
return nil
}
}
|> mapToSignal { data -> Signal<String?, NoError> in
guard let (peer, sourceMessageId) = data else {
return .single(nil)
}
if let input = apiInputChannel(peer) {
var flags: Int32 = 0
flags |= 1 << 0
if isThread {
flags |= 1 << 1
}
return network.request(Api.functions.channels.exportMessageLink(flags: flags, channel: input, id: sourceMessageId.id)) |> mapError { _ in return }
|> map { res in
switch res {
case let .exportedMessageLink(link, _):
return link
}
} |> `catch` { _ -> Signal<String?, NoError> in
return .single(nil)
}
} else {
return .single(nil)
}
}
}
@@ -0,0 +1,40 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
private func _internal_updateExtendedMediaById(account: Account, peerId: EnginePeer.Id, messageIds: [EngineMessage.Id]) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Peer? in
if let peer = transaction.getPeer(peerId) {
return peer
} else {
return nil
}
}
|> mapToSignal { peer -> Signal<Never, NoError> in
guard let peer = peer, let inputPeer = apiInputPeer(peer) else {
return .complete()
}
return account.network.request(Api.functions.messages.getExtendedMedia(peer: inputPeer, id: messageIds.map { $0.id }))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Never, NoError> in
if let updates = updates {
account.stateManager.addUpdates(updates)
}
return .complete()
}
}
}
func _internal_updateExtendedMedia(account: Account, messageIds: [EngineMessage.Id]) -> Signal<Never, NoError> {
var signals: [Signal<Never, NoError>] = []
for (peerId, messageIds) in messagesIdsGroupedByPeerId(messageIds) {
signals.append(_internal_updateExtendedMediaById(account: account, peerId: peerId, messageIds: messageIds))
}
return combineLatest(signals)
|> ignoreValues
}
@@ -0,0 +1,123 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
func _internal_editMessageFactCheck(account: Account, messageId: EngineMessage.Id, text: String, entities: [MessageTextEntity]) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<Never, NoError> in
guard let inputPeer else {
return .complete()
}
return account.network.request(Api.functions.messages.editFactCheck(
peer: inputPeer,
msgId: messageId.id,
text: .textWithEntities(
text: text,
entities: apiEntitiesFromMessageTextEntities(entities, associatedPeers: SimpleDictionary())
)
))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Never, NoError> in
if let updates = updates {
account.stateManager.addUpdates(updates)
}
return .complete()
}
}
}
func _internal_deleteMessageFactCheck(account: Account, messageId: EngineMessage.Id) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<Never, NoError> in
guard let inputPeer else {
return .complete()
}
return account.network.request(Api.functions.messages.deleteFactCheck(peer: inputPeer, msgId: messageId.id))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Never, NoError> in
if let updates = updates {
account.stateManager.addUpdates(updates)
}
return .complete()
}
}
}
func _internal_getMessagesFactCheck(account: Account, messageIds: [EngineMessage.Id]) -> Signal<Never, NoError> {
var signals: [Signal<Never, NoError>] = []
for (peerId, messageIds) in messagesIdsGroupedByPeerId(messageIds) {
signals.append(_internal_getMessagesFactCheckByPeerId(account: account, peerId: peerId, messageIds: messageIds))
}
return combineLatest(signals)
|> ignoreValues
}
func _internal_getMessagesFactCheckByPeerId(account: Account, peerId: EnginePeer.Id, messageIds: [EngineMessage.Id]) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> (Api.InputPeer?, [Message]) in
return (transaction.getPeer(peerId).flatMap(apiInputPeer), messageIds.compactMap({ transaction.getMessage($0) }))
}
|> mapToSignal { (inputPeer, messages) -> Signal<Never, NoError> in
guard let inputPeer = inputPeer else {
return .never()
}
let ids: [Int32] = messageIds.map { $0.id }
let results: Signal<[Api.FactCheck]?, NoError>
if ids.isEmpty {
results = .single(nil)
} else {
results = account.network.request(Api.functions.messages.getFactCheck(peer: inputPeer, msgId: ids))
|> map(Optional.init)
|> `catch` { _ in
return .single(nil)
}
}
return results
|> mapToSignal { results -> Signal<Never, NoError> in
guard let results else {
return .complete()
}
return account.postbox.transaction { transaction in
var index = 0
for result in results {
let messageId = messageIds[index]
switch result {
case let .factCheck(_, country, text, hash):
let content: FactCheckMessageAttribute.Content
if let text, let country {
switch text {
case let .textWithEntities(text, entities):
content = .Loaded(text: text, entities: messageTextEntitiesFromApiEntities(entities), country: country)
}
} else {
content = .Pending
}
let attribute = FactCheckMessageAttribute(content: content, hash: hash)
transaction.updateMessage(messageId, update: { currentMessage in
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
var attributes = currentMessage.attributes.filter { !($0 is FactCheckMessageAttribute) }
attributes.append(attribute)
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
}
index += 1
}
}
|> ignoreValues
}
}
}
@@ -0,0 +1,31 @@
import Foundation
import Postbox
import TelegramApi
import SwiftSignalKit
func _internal_forwardGameWithScore(account: Account, messageId: MessageId, to peerId: PeerId, threadId: Int64?, as sendAsPeerId: PeerId?) -> Signal<Void, NoError> {
return account.postbox.transaction { transaction -> Signal<Void, NoError> in
if let _ = transaction.getMessage(messageId), let fromPeer = transaction.getPeer(messageId.peerId), let fromInputPeer = apiInputPeer(fromPeer), let toPeer = transaction.getPeer(peerId), let toInputPeer = apiInputPeer(toPeer) {
var flags: Int32 = 1 << 8
var sendAsInputPeer: Api.InputPeer?
if let sendAsPeerId = sendAsPeerId, let sendAsPeer = transaction.getPeer(sendAsPeerId), let inputPeer = apiInputPeerOrSelf(sendAsPeer, accountPeerId: account.peerId) {
sendAsInputPeer = inputPeer
flags |= (1 << 13)
}
return account.network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: fromInputPeer, id: [messageId.id], randomId: [Int64.random(in: Int64.min ... Int64.max)], toPeer: toInputPeer, topMsgId: threadId.flatMap { Int32(clamping: $0) }, replyTo: nil, scheduleDate: nil, scheduleRepeatPeriod: nil, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil, videoTimestamp: nil, allowPaidStars: nil, suggestedPost: nil))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Void, NoError> in
if let updates = updates {
account.stateManager.addUpdates(updates)
}
return .complete()
}
}
return .complete()
} |> switchToLatest
}
@@ -0,0 +1,197 @@
import Foundation
import Postbox
import TelegramApi
import SwiftSignalKit
func _internal_installInteractiveReadMessagesAction(postbox: Postbox, stateManager: AccountStateManager, peerId: PeerId, threadId: Int64?) -> Disposable {
return postbox.installStoreMessageAction(peerId: peerId, { messages, transaction in
var consumeMessageIds: [MessageId] = []
var readReactionIds: [MessageId] = []
readReactionIds.removeAll()
var readMessageIndexByNamespace: [MessageId.Namespace: MessageIndex] = [:]
for message in messages {
if case let .Id(id) = message.id {
if threadId == nil || message.threadId == threadId {
} else {
continue
}
var hasUnconsumedMention = false
var hasUnconsumedContent = false
var hasUnseenReactions = false
if message.tags.contains(.unseenPersonalMessage) || message.tags.contains(.unseenReaction) {
inner: for attribute in message.attributes {
if let attribute = attribute as? ConsumablePersonalMentionMessageAttribute, !attribute.consumed, !attribute.pending {
hasUnconsumedMention = true
} else if let attribute = attribute as? ConsumableContentMessageAttribute, !attribute.consumed {
hasUnconsumedContent = true
} else if let attribute = attribute as? ReactionsMessageAttribute, attribute.hasUnseen {
hasUnseenReactions = true
}
}
}
if hasUnconsumedMention && !hasUnconsumedContent {
consumeMessageIds.append(id)
}
if hasUnseenReactions {
//readReactionIds.append(id)
}
if !message.flags.intersection(.IsIncomingMask).isEmpty {
let index = MessageIndex(id: id, timestamp: message.timestamp)
let current = readMessageIndexByNamespace[id.namespace]
if current == nil || current! < index {
readMessageIndexByNamespace[id.namespace] = index
}
}
}
}
for id in Set(consumeMessageIds + readReactionIds) {
transaction.updateMessage(id, update: { currentMessage in
var attributes = currentMessage.attributes
if consumeMessageIds.contains(id) {
mentionsLoop: for j in 0 ..< attributes.count {
if let attribute = attributes[j] as? ConsumablePersonalMentionMessageAttribute {
attributes[j] = ConsumablePersonalMentionMessageAttribute(consumed: attribute.consumed, pending: true)
break mentionsLoop
}
}
}
var tags = currentMessage.tags
if readReactionIds.contains(id) {
reactionsLoop: for j in 0 ..< attributes.count {
if let attribute = attributes[j] as? ReactionsMessageAttribute {
attributes[j] = attribute.withAllSeen()
break reactionsLoop
}
}
tags.remove(.unseenReaction)
}
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init), authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
if consumeMessageIds.contains(id) {
transaction.setPendingMessageAction(type: .consumeUnseenPersonalMessage, id: id, action: ConsumePersonalMessageAction())
}
if readReactionIds.contains(id) {
transaction.setPendingMessageAction(type: .readReaction, id: id, action: ReadReactionAction())
}
}
for (_, index) in readMessageIndexByNamespace {
if let threadId {
if var data = transaction.getMessageHistoryThreadInfo(peerId: peerId, threadId: threadId)?.data.get(MessageHistoryThreadData.self) {
if index.id.id >= data.maxIncomingReadId {
if let count = transaction.getThreadMessageCount(peerId: peerId, threadId: threadId, namespace: Namespaces.Message.Cloud, fromIdExclusive: data.maxIncomingReadId, toIndex: index) {
data.incomingUnreadCount = max(0, data.incomingUnreadCount - Int32(count))
data.maxIncomingReadId = index.id.id
}
if let topMessageIndex = transaction.getMessageHistoryThreadTopMessage(peerId: peerId, threadId: threadId, namespaces: Set([Namespaces.Message.Cloud])) {
if index.id.id >= topMessageIndex.id.id {
let containingHole = transaction.getThreadIndexHole(peerId: peerId, threadId: threadId, namespace: topMessageIndex.id.namespace, containing: topMessageIndex.id.id)
if let _ = containingHole[.everywhere] {
} else {
data.incomingUnreadCount = 0
}
}
}
data.maxKnownMessageId = max(data.maxKnownMessageId, index.id.id)
if let entry = StoredMessageHistoryThreadInfo(data) {
transaction.setMessageHistoryThreadInfo(peerId: peerId, threadId: threadId, info: entry)
}
}
}
} else {
_internal_applyMaxReadIndexInteractively(transaction: transaction, stateManager: stateManager, index: index)
}
}
})
}
public struct VisibleMessageRange {
public var lowerBound: MessageIndex
public var upperBound: MessageIndex?
public init(lowerBound: MessageIndex, upperBound: MessageIndex?) {
self.lowerBound = lowerBound
self.upperBound = upperBound
}
fileprivate func contains(index: MessageIndex) -> Bool {
if index < lowerBound {
return false
}
if let upperBound = self.upperBound {
if index > upperBound {
return false
}
}
return true
}
}
private final class StoreOrUpdateMessageActionImpl: StoreOrUpdateMessageAction {
private let getVisibleRange: () -> VisibleMessageRange?
private let didReadReactionsInMessages: ([MessageId: [ReactionsMessageAttribute.RecentPeer]]) -> Void
init(getVisibleRange: @escaping () -> VisibleMessageRange?, didReadReactionsInMessages: @escaping ([MessageId: [ReactionsMessageAttribute.RecentPeer]]) -> Void) {
self.getVisibleRange = getVisibleRange
self.didReadReactionsInMessages = didReadReactionsInMessages
}
func addOrUpdate(messages: [StoreMessage], transaction: Transaction) {
var readReactionIds: [MessageId: [ReactionsMessageAttribute.RecentPeer]] = [:]
guard let visibleRange = self.getVisibleRange() else {
return
}
for message in messages {
guard let index = message.index else {
continue
}
if !visibleRange.contains(index: index) {
continue
}
if message.tags.contains(.unseenReaction) {
inner: for attribute in message.attributes {
if let attribute = attribute as? ReactionsMessageAttribute, attribute.hasUnseen {
readReactionIds[index.id] = attribute.recentPeers
break inner
}
}
}
}
for id in readReactionIds.keys {
transaction.updateMessage(id, update: { currentMessage in
var attributes = currentMessage.attributes
reactionsLoop: for j in 0 ..< attributes.count {
if let attribute = attributes[j] as? ReactionsMessageAttribute {
attributes[j] = attribute.withAllSeen()
break reactionsLoop
}
}
var tags = currentMessage.tags
tags.remove(.unseenReaction)
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init), authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
transaction.setPendingMessageAction(type: .readReaction, id: id, action: ReadReactionAction())
}
self.didReadReactionsInMessages(readReactionIds)
}
}
func _internal_installInteractiveReadReactionsAction(postbox: Postbox, stateManager: AccountStateManager, peerId: PeerId, getVisibleRange: @escaping () -> VisibleMessageRange?, didReadReactionsInMessages: @escaping ([MessageId: [ReactionsMessageAttribute.RecentPeer]]) -> Void) -> Disposable {
return postbox.installStoreOrUpdateMessageAction(peerId: peerId, action: StoreOrUpdateMessageActionImpl(getVisibleRange: getVisibleRange, didReadReactionsInMessages: didReadReactionsInMessages))
}
@@ -0,0 +1,132 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public enum GetMessagesResult {
case progress
case result([Message])
}
public enum GetMessagesStrategy {
case local
case cloud(skipLocal: Bool)
}
public enum GetMessagesError {
case privateChannel
}
func _internal_getMessagesLoadIfNecessary(_ messageIds: [MessageId], postbox: Postbox, network: Network, accountPeerId: PeerId, strategy: GetMessagesStrategy = .cloud(skipLocal: false)) -> Signal<GetMessagesResult, GetMessagesError> {
let postboxSignal = postbox.transaction { transaction -> ([Message], Set<MessageId>, SimpleDictionary<PeerId, Peer>) in
var ids = messageIds
if let cachedData = transaction.getPeerCachedData(peerId: messageIds[0].peerId) as? CachedChannelData {
if let minAvailableMessageId = cachedData.minAvailableMessageId {
ids = ids.filter({$0 < minAvailableMessageId})
}
}
var messages:[Message] = []
var missingMessageIds:Set<MessageId> = Set()
var supportPeers: SimpleDictionary<PeerId, Peer> = SimpleDictionary()
for messageId in ids {
if case let .cloud(skipLocal) = strategy, skipLocal {
missingMessageIds.insert(messageId)
if let peer = transaction.getPeer(messageId.peerId) {
supportPeers[messageId.peerId] = peer
}
} else {
if let message = transaction.getMessage(messageId) {
messages.append(message)
} else {
missingMessageIds.insert(messageId)
if let peer = transaction.getPeer(messageId.peerId) {
supportPeers[messageId.peerId] = peer
}
}
}
}
return (messages, missingMessageIds, supportPeers)
}
if case .cloud = strategy {
return postboxSignal
|> castError(GetMessagesError.self)
|> mapToSignal { (existMessages, missingMessageIds, supportPeers) in
var signals: [Signal<(Peer, [Api.Message], [Api.Chat], [Api.User]), GetMessagesError>] = []
for (peerId, messageIds) in messagesIdsGroupedByPeerId(missingMessageIds) {
if let peer = supportPeers[peerId] {
var signal: Signal<Api.messages.Messages, MTRpcError>?
if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.CloudGroup {
signal = network.request(Api.functions.messages.getMessages(id: messageIds.map({ Api.InputMessage.inputMessageID(id: $0.id) })))
} else if peerId.namespace == Namespaces.Peer.CloudChannel {
if let inputChannel = apiInputChannel(peer) {
signal = network.request(Api.functions.channels.getMessages(channel: inputChannel, id: messageIds.map({ Api.InputMessage.inputMessageID(id: $0.id) })))
}
}
if let signal = signal {
signals.append(signal |> map { result in
switch result {
case let .messages(messages, _, chats, users):
return (peer, messages, chats, users)
case let .messagesSlice(_, _, _, _, _, messages, _, chats, users):
return (peer, messages, chats, users)
case let .channelMessages(_, _, _, _, messages, apiTopics, chats, users):
let _ = apiTopics
return (peer, messages, chats, users)
case .messagesNotModified:
return (peer, [], [], [])
}
} |> `catch` { error in
if error.errorDescription == "CHANNEL_PRIVATE" {
return .fail(.privateChannel)
} else {
return Signal<(Peer, [Api.Message], [Api.Chat], [Api.User]), GetMessagesError>.single((peer, [], [], []))
}
})
}
}
}
return .single(.progress)
|> castError(GetMessagesError.self)
|> then(combineLatest(signals) |> mapToSignal { results -> Signal<GetMessagesResult, GetMessagesError> in
return postbox.transaction { transaction -> GetMessagesResult in
for (peer, messages, chats, users) in results {
if !messages.isEmpty {
var storeMessages: [StoreMessage] = []
for message in messages {
if let message = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peer.isForumOrMonoForum) {
storeMessages.append(message)
}
}
_ = transaction.addMessages(storeMessages, location: .Random)
}
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
}
var loadedMessages:[Message] = []
for messageId in missingMessageIds {
if let message = transaction.getMessage(messageId) {
loadedMessages.append(message)
}
}
return .result(existMessages + loadedMessages)
}
|> castError(GetMessagesError.self)
})
}
} else {
return postboxSignal
|> castError(GetMessagesError.self)
|> map {
return .result($0.0)
}
}
}
@@ -0,0 +1,71 @@
import Foundation
import TelegramApi
import Postbox
import SwiftSignalKit
import MtProtoKit
func _internal_markAllChatsAsRead(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal<Void, NoError> {
return network.request(Api.functions.messages.getDialogUnreadMarks(flags: 0, parentPeer: nil))
|> map(Optional.init)
|> `catch` { _ -> Signal<[Api.DialogPeer]?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Void, NoError> in
guard let result = result else {
return .complete()
}
return postbox.transaction { transaction -> Signal<Void, NoError> in
var signals: [Signal<Void, NoError>] = []
for peer in result {
switch peer {
case let .dialogPeer(peer):
let peerId = peer.peerId
if peerId.namespace == Namespaces.Peer.CloudChannel {
if let inputChannel = transaction.getPeer(peerId).flatMap(apiInputChannel) {
signals.append(network.request(Api.functions.channels.readHistory(channel: inputChannel, maxId: Int32.max - 1))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
})
}
} else if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.CloudGroup {
if let inputPeer = transaction.getPeer(peerId).flatMap(apiInputPeer) {
signals.append(network.request(Api.functions.messages.readHistory(peer: inputPeer, maxId: Int32.max - 1))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.AffectedMessages?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Void, NoError> in
if let result = result {
switch result {
case let .affectedMessages(pts, ptsCount):
stateManager.addUpdateGroups([.updatePts(pts: pts, ptsCount: ptsCount)])
}
}
return .complete()
})
}
} else {
assertionFailure()
}
case .dialogPeerFolder:
assertionFailure()
}
}
let applyLocally = postbox.transaction { transaction -> Void in
}
return combineLatest(signals)
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
|> then(applyLocally)
} |> switchToLatest
}
}
@@ -0,0 +1,252 @@
import Foundation
import Postbox
import TelegramApi
import SwiftSignalKit
func _internal_markMessageContentAsConsumedInteractively(postbox: Postbox, messageId: MessageId) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
if let message = transaction.getMessage(messageId), message.flags.contains(.Incoming) {
var updateMessage = false
var updatedAttributes = message.attributes
for i in 0 ..< updatedAttributes.count {
if let attribute = updatedAttributes[i] as? ConsumableContentMessageAttribute {
if !attribute.consumed {
updatedAttributes[i] = ConsumableContentMessageAttribute(consumed: true)
updateMessage = true
if message.id.peerId.namespace == Namespaces.Peer.SecretChat {
if let state = transaction.getPeerChatState(message.id.peerId) as? SecretChatState {
var layer: SecretChatLayer?
switch state.embeddedState {
case .terminated, .handshake:
break
case .basicLayer:
layer = .layer8
case let .sequenceBasedLayer(sequenceState):
layer = sequenceState.layerNegotiationState.activeLayer.secretChatLayer
}
if let layer = layer {
var globallyUniqueIds: [Int64] = []
if let globallyUniqueId = message.globallyUniqueId {
globallyUniqueIds.append(globallyUniqueId)
let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: message.id.peerId, operation: SecretChatOutgoingOperationContents.readMessagesContent(layer: layer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), globallyUniqueIds: globallyUniqueIds), state: state)
if updatedState != state {
transaction.setPeerChatState(message.id.peerId, state: updatedState)
}
}
}
}
} else {
addSynchronizeConsumeMessageContentsOperation(transaction: transaction, messageIds: [message.id])
}
}
} else if let attribute = updatedAttributes[i] as? ConsumablePersonalMentionMessageAttribute, !attribute.consumed {
transaction.setPendingMessageAction(type: .consumeUnseenPersonalMessage, id: messageId, action: ConsumePersonalMessageAction())
updatedAttributes[i] = ConsumablePersonalMentionMessageAttribute(consumed: attribute.consumed, pending: true)
}
}
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
for i in 0 ..< updatedAttributes.count {
if let attribute = updatedAttributes[i] as? AutoremoveTimeoutMessageAttribute {
if attribute.countdownBeginTime == nil || attribute.countdownBeginTime == 0 {
var timeout = attribute.timeout
if let duration = message.secretMediaDuration {
timeout = max(timeout, Int32(duration))
}
updatedAttributes[i] = AutoremoveTimeoutMessageAttribute(timeout: timeout, countdownBeginTime: timestamp)
updateMessage = true
if messageId.peerId.namespace == Namespaces.Peer.SecretChat {
var layer: SecretChatLayer?
let state = transaction.getPeerChatState(message.id.peerId) as? SecretChatState
if let state = state {
switch state.embeddedState {
case .terminated, .handshake:
break
case .basicLayer:
layer = .layer8
case let .sequenceBasedLayer(sequenceState):
layer = sequenceState.layerNegotiationState.activeLayer.secretChatLayer
}
}
if let state = state, let layer = layer, let globallyUniqueId = message.globallyUniqueId {
let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: messageId.peerId, operation: .readMessagesContent(layer: layer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), globallyUniqueIds: [globallyUniqueId]), state: state)
if updatedState != state {
transaction.setPeerChatState(messageId.peerId, state: updatedState)
}
}
}
}
} else if let attribute = updatedAttributes[i] as? AutoclearTimeoutMessageAttribute {
if attribute.countdownBeginTime == nil || attribute.countdownBeginTime == 0 {
var timeout = attribute.timeout
if let duration = message.secretMediaDuration, timeout != viewOnceTimeout {
timeout = max(timeout, Int32(duration))
}
updatedAttributes[i] = AutoclearTimeoutMessageAttribute(timeout: timeout, countdownBeginTime: timestamp)
updateMessage = true
if messageId.peerId.namespace == Namespaces.Peer.SecretChat {
var layer: SecretChatLayer?
let state = transaction.getPeerChatState(message.id.peerId) as? SecretChatState
if let state = state {
switch state.embeddedState {
case .terminated, .handshake:
break
case .basicLayer:
layer = .layer8
case let .sequenceBasedLayer(sequenceState):
layer = sequenceState.layerNegotiationState.activeLayer.secretChatLayer
}
}
if let state = state, let layer = layer, let globallyUniqueId = message.globallyUniqueId {
let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: messageId.peerId, operation: .readMessagesContent(layer: layer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), globallyUniqueIds: [globallyUniqueId]), state: state)
if updatedState != state {
transaction.setPeerChatState(messageId.peerId, state: updatedState)
}
}
}
}
}
}
if updateMessage {
transaction.updateMessage(message.id, update: { currentMessage in
var storeForwardInfo: StoreMessageForwardInfo?
if let forwardInfo = currentMessage.forwardInfo {
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
}
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: updatedAttributes, media: currentMessage.media))
})
}
}
}
}
func _internal_markReactionsAsSeenInteractively(postbox: Postbox, messageId: MessageId) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
if let message = transaction.getMessage(messageId), message.tags.contains(.unseenReaction) {
var updateMessage = false
var updatedAttributes = message.attributes
for i in 0 ..< updatedAttributes.count {
if let attribute = updatedAttributes[i] as? ReactionsMessageAttribute, attribute.hasUnseen {
updatedAttributes[i] = attribute.withAllSeen()
updateMessage = true
if message.id.peerId.namespace == Namespaces.Peer.SecretChat {
} else {
transaction.setPendingMessageAction(type: .readReaction, id: messageId, action: ReadReactionAction())
}
}
}
if updateMessage {
transaction.updateMessage(message.id, update: { currentMessage in
var storeForwardInfo: StoreMessageForwardInfo?
if let forwardInfo = currentMessage.forwardInfo {
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
}
var tags = currentMessage.tags
tags.remove(.unseenReaction)
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: updatedAttributes, media: currentMessage.media))
})
}
}
}
}
func markMessageContentAsConsumedRemotely(transaction: Transaction, messageId: MessageId, consumeDate: Int32?) {
if let message = transaction.getMessage(messageId) {
var updateMessage = false
var updatedAttributes = message.attributes
var updatedMedia = message.media
var updatedTags = message.tags
for i in 0 ..< updatedAttributes.count {
if let attribute = updatedAttributes[i] as? ConsumableContentMessageAttribute {
if !attribute.consumed {
updatedAttributes[i] = ConsumableContentMessageAttribute(consumed: true)
updateMessage = true
}
} else if let attribute = updatedAttributes[i] as? ConsumablePersonalMentionMessageAttribute, !attribute.consumed {
if attribute.pending {
transaction.setPendingMessageAction(type: .consumeUnseenPersonalMessage, id: messageId, action: nil)
}
updatedAttributes[i] = ConsumablePersonalMentionMessageAttribute(consumed: true, pending: false)
updatedTags.remove(.unseenPersonalMessage)
updateMessage = true
}
}
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
let countdownBeginTime = consumeDate ?? timestamp
for i in 0 ..< updatedAttributes.count {
if let attribute = updatedAttributes[i] as? AutoremoveTimeoutMessageAttribute {
if (attribute.countdownBeginTime == nil || attribute.countdownBeginTime == 0) && message.containsSecretMedia {
updatedAttributes[i] = AutoremoveTimeoutMessageAttribute(timeout: attribute.timeout, countdownBeginTime: countdownBeginTime)
updateMessage = true
if message.id.peerId.namespace == Namespaces.Peer.SecretChat {
} else {
if attribute.timeout == viewOnceTimeout || timestamp >= countdownBeginTime + attribute.timeout {
for i in 0 ..< updatedMedia.count {
if let _ = updatedMedia[i] as? TelegramMediaImage {
updatedMedia[i] = TelegramMediaExpiredContent(data: .image)
} else if let file = updatedMedia[i] as? TelegramMediaFile {
if file.isInstantVideo {
updatedMedia[i] = TelegramMediaExpiredContent(data: .videoMessage)
} else if file.isVoice {
updatedMedia[i] = TelegramMediaExpiredContent(data: .voiceMessage)
} else {
updatedMedia[i] = TelegramMediaExpiredContent(data: .file)
}
}
}
}
}
}
} else if let attribute = updatedAttributes[i] as? AutoclearTimeoutMessageAttribute {
if (attribute.countdownBeginTime == nil || attribute.countdownBeginTime == 0) && message.containsSecretMedia {
updatedAttributes[i] = AutoclearTimeoutMessageAttribute(timeout: attribute.timeout, countdownBeginTime: countdownBeginTime)
updateMessage = true
if message.id.peerId.namespace == Namespaces.Peer.SecretChat {
} else {
for i in 0 ..< updatedMedia.count {
if attribute.timeout == viewOnceTimeout || timestamp >= countdownBeginTime + attribute.timeout {
if let _ = updatedMedia[i] as? TelegramMediaImage {
updatedMedia[i] = TelegramMediaExpiredContent(data: .image)
} else if let file = updatedMedia[i] as? TelegramMediaFile {
if file.isInstantVideo {
updatedMedia[i] = TelegramMediaExpiredContent(data: .videoMessage)
} else if file.isVoice {
updatedMedia[i] = TelegramMediaExpiredContent(data: .voiceMessage)
} else {
updatedMedia[i] = TelegramMediaExpiredContent(data: .file)
}
}
}
}
}
}
}
}
if updateMessage {
transaction.updateMessage(message.id, update: { currentMessage in
var storeForwardInfo: StoreMessageForwardInfo?
if let forwardInfo = currentMessage.forwardInfo {
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
}
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: updatedTags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: updatedAttributes, media: updatedMedia))
})
}
}
}
@@ -0,0 +1,160 @@
import Postbox
public enum EngineMedia: Equatable {
public typealias Id = MediaId
case image(TelegramMediaImage)
case file(TelegramMediaFile)
case geo(TelegramMediaMap)
case contact(TelegramMediaContact)
case action(TelegramMediaAction)
case dice(TelegramMediaDice)
case expiredContent(TelegramMediaExpiredContent)
case game(TelegramMediaGame)
case invoice(TelegramMediaInvoice)
case poll(TelegramMediaPoll)
case unsupported(TelegramMediaUnsupported)
case webFile(TelegramMediaWebFile)
case webpage(TelegramMediaWebpage)
case story(TelegramMediaStory)
case giveaway(TelegramMediaGiveaway)
case giveawayResults(TelegramMediaGiveawayResults)
case paidContent(TelegramMediaPaidContent)
case todo(TelegramMediaTodo)
case liveStream(TelegramMediaLiveStream)
}
public extension EngineMedia {
var id: Id? {
switch self {
case let .image(image):
return image.id
case let .file(file):
return file.id
case let .geo(geo):
return geo.id
case let .contact(contact):
return contact.id
case let .action(action):
return action.id
case let .dice(dice):
return dice.id
case let .expiredContent(expiredContent):
return expiredContent.id
case let .game(game):
return game.id
case let .invoice(invoice):
return invoice.id
case let .poll(poll):
return poll.id
case let .unsupported(unsupported):
return unsupported.id
case let .webFile(webFile):
return webFile.id
case let .webpage(webpage):
return webpage.id
case let .story(story):
return story.id
case let .giveaway(giveaway):
return giveaway.id
case let .giveawayResults(giveawayResults):
return giveawayResults.id
case let .paidContent(paidContent):
return paidContent.id
case .todo:
return nil
case .liveStream:
return nil
}
}
}
public extension EngineMedia {
init(_ media: Media) {
switch media {
case let image as TelegramMediaImage:
self = .image(image)
case let file as TelegramMediaFile:
self = .file(file)
case let geo as TelegramMediaMap:
self = .geo(geo)
case let contact as TelegramMediaContact:
self = .contact(contact)
case let action as TelegramMediaAction:
self = .action(action)
case let dice as TelegramMediaDice:
self = .dice(dice)
case let expiredContent as TelegramMediaExpiredContent:
self = .expiredContent(expiredContent)
case let game as TelegramMediaGame:
self = .game(game)
case let invoice as TelegramMediaInvoice:
self = .invoice(invoice)
case let poll as TelegramMediaPoll:
self = .poll(poll)
case let unsupported as TelegramMediaUnsupported:
self = .unsupported(unsupported)
case let webFile as TelegramMediaWebFile:
self = .webFile(webFile)
case let webpage as TelegramMediaWebpage:
self = .webpage(webpage)
case let story as TelegramMediaStory:
self = .story(story)
case let giveaway as TelegramMediaGiveaway:
self = .giveaway(giveaway)
case let giveawayResults as TelegramMediaGiveawayResults:
self = .giveawayResults(giveawayResults)
case let paidContent as TelegramMediaPaidContent:
self = .paidContent(paidContent)
case let todo as TelegramMediaTodo:
self = .todo(todo)
case let liveStream as TelegramMediaLiveStream:
self = .liveStream(liveStream)
default:
preconditionFailure()
}
}
func _asMedia() -> Media {
switch self {
case let .image(image):
return image
case let .file(file):
return file
case let .geo(geo):
return geo
case let .contact(contact):
return contact
case let .action(action):
return action
case let .dice(dice):
return dice
case let .expiredContent(expiredContent):
return expiredContent
case let .game(game):
return game
case let .invoice(invoice):
return invoice
case let .poll(poll):
return poll
case let .unsupported(unsupported):
return unsupported
case let .webFile(webFile):
return webFile
case let .webpage(webpage):
return webpage
case let .story(story):
return story
case let .giveaway(giveaway):
return giveaway
case let .giveawayResults(giveawayResults):
return giveawayResults
case let .paidContent(paidContent):
return paidContent
case let .todo(todo):
return todo
case let .liveStream(liveStream):
return liveStream
}
}
}
@@ -0,0 +1,274 @@
import Foundation
import Postbox
public enum MediaArea: Codable, Equatable {
private enum CodingKeys: CodingKey {
case type
case coordinates
case value
case flags
case temperature
case color
}
public struct Coordinates: Codable, Equatable {
private enum CodingKeys: CodingKey {
case x
case y
case width
case height
case rotation
case cornerRadius
}
public var x: Double
public var y: Double
public var width: Double
public var height: Double
public var rotation: Double
public var cornerRadius: Double?
public init(
x: Double,
y: Double,
width: Double,
height: Double,
rotation: Double,
cornerRadius: Double?
) {
self.x = x
self.y = y
self.width = width
self.height = height
self.rotation = rotation
self.cornerRadius = cornerRadius
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.x = try container.decode(Double.self, forKey: .x)
self.y = try container.decode(Double.self, forKey: .y)
self.width = try container.decode(Double.self, forKey: .width)
self.height = try container.decode(Double.self, forKey: .height)
self.rotation = try container.decode(Double.self, forKey: .rotation)
self.cornerRadius = try container.decodeIfPresent(Double.self, forKey: .cornerRadius)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.x, forKey: .x)
try container.encode(self.y, forKey: .y)
try container.encode(self.width, forKey: .width)
try container.encode(self.height, forKey: .height)
try container.encode(self.rotation, forKey: .rotation)
try container.encodeIfPresent(self.cornerRadius, forKey: .cornerRadius)
}
}
public struct Venue: Codable, Equatable {
private enum CodingKeys: CodingKey {
case latitude
case longitude
case venue
case address
case queryId
case resultId
}
public let latitude: Double
public let longitude: Double
public let venue: MapVenue?
public let address: MapGeoAddress?
public let queryId: Int64?
public let resultId: String?
public init(
latitude: Double,
longitude: Double,
venue: MapVenue?,
address: MapGeoAddress?,
queryId: Int64?,
resultId: String?
) {
self.latitude = latitude
self.longitude = longitude
self.venue = venue
self.address = address
self.queryId = queryId
self.resultId = resultId
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.latitude = try container.decode(Double.self, forKey: .latitude)
self.longitude = try container.decode(Double.self, forKey: .longitude)
if let venueData = try container.decodeIfPresent(Data.self, forKey: .venue) {
self.venue = PostboxDecoder(buffer: MemoryBuffer(data: venueData)).decodeRootObject() as? MapVenue
} else {
self.venue = nil
}
if let addressData = try container.decodeIfPresent(Data.self, forKey: .address) {
self.address = PostboxDecoder(buffer: MemoryBuffer(data: addressData)).decodeRootObject() as? MapGeoAddress
} else {
self.address = nil
}
self.queryId = try container.decodeIfPresent(Int64.self, forKey: .queryId)
self.resultId = try container.decodeIfPresent(String.self, forKey: .resultId)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.latitude, forKey: .latitude)
try container.encode(self.longitude, forKey: .longitude)
if let venue = self.venue {
let encoder = PostboxEncoder()
encoder.encodeRootObject(venue)
let venueData = encoder.makeData()
try container.encode(venueData, forKey: .venue)
}
if let address = self.address {
let encoder = PostboxEncoder()
encoder.encodeRootObject(address)
let addressData = encoder.makeData()
try container.encode(addressData, forKey: .address)
}
try container.encodeIfPresent(self.queryId, forKey: .queryId)
try container.encodeIfPresent(self.resultId, forKey: .resultId)
}
}
case venue(coordinates: Coordinates, venue: Venue)
case reaction(coordinates: Coordinates, reaction: MessageReaction.Reaction, flags: ReactionFlags)
case channelMessage(coordinates: Coordinates, messageId: EngineMessage.Id)
case link(coordinates: Coordinates, url: String)
case weather(coordinates: Coordinates, emoji: String, temperature: Double, color: Int32)
case starGift(coordinates: Coordinates, slug: String)
public struct ReactionFlags: OptionSet {
public var rawValue: Int32
public init(rawValue: Int32) {
self.rawValue = rawValue
}
public init() {
self.rawValue = 0
}
public static let isDark = ReactionFlags(rawValue: 1 << 0)
public static let isFlipped = ReactionFlags(rawValue: 1 << 1)
}
private enum MediaAreaType: Int32 {
case venue
case reaction
case channelMessage
case link
case weather
case starGift
}
public enum DecodingError: Error {
case generic
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
guard let type = MediaAreaType(rawValue: try container.decode(Int32.self, forKey: .type)) else {
throw DecodingError.generic
}
switch type {
case .venue:
let coordinates = try container.decode(MediaArea.Coordinates.self, forKey: .coordinates)
let venue = try container.decode(MediaArea.Venue.self, forKey: .value)
self = .venue(coordinates: coordinates, venue: venue)
case .reaction:
let coordinates = try container.decode(MediaArea.Coordinates.self, forKey: .coordinates)
let reaction = try container.decode(MessageReaction.Reaction.self, forKey: .value)
let flags = ReactionFlags(rawValue: try container.decodeIfPresent(Int32.self, forKey: .flags) ?? 0)
self = .reaction(coordinates: coordinates, reaction: reaction, flags: flags)
case .channelMessage:
let coordinates = try container.decode(MediaArea.Coordinates.self, forKey: .coordinates)
let messageId = try container.decode(MessageId.self, forKey: .value)
self = .channelMessage(coordinates: coordinates, messageId: messageId)
case .link:
let coordinates = try container.decode(MediaArea.Coordinates.self, forKey: .coordinates)
let url = try container.decode(String.self, forKey: .value)
self = .link(coordinates: coordinates, url: url)
case .weather:
let coordinates = try container.decode(MediaArea.Coordinates.self, forKey: .coordinates)
let emoji = try container.decode(String.self, forKey: .value)
let temperature = try container.decode(Double.self, forKey: .temperature)
let color = try container.decodeIfPresent(Int32.self, forKey: .color) ?? 0
self = .weather(coordinates: coordinates, emoji: emoji, temperature: temperature, color: color)
case .starGift:
let coordinates = try container.decode(MediaArea.Coordinates.self, forKey: .coordinates)
let slug = try container.decode(String.self, forKey: .value)
self = .starGift(coordinates: coordinates, slug: slug)
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .venue(coordinates, venue):
try container.encode(MediaAreaType.venue.rawValue, forKey: .type)
try container.encode(coordinates, forKey: .coordinates)
try container.encode(venue, forKey: .value)
case let .reaction(coordinates, reaction, flags):
try container.encode(MediaAreaType.reaction.rawValue, forKey: .type)
try container.encode(coordinates, forKey: .coordinates)
try container.encode(reaction, forKey: .value)
try container.encode(flags.rawValue, forKey: .flags)
case let .channelMessage(coordinates, messageId):
try container.encode(MediaAreaType.channelMessage.rawValue, forKey: .type)
try container.encode(coordinates, forKey: .coordinates)
try container.encode(messageId, forKey: .value)
case let .link(coordinates, url):
try container.encode(MediaAreaType.link.rawValue, forKey: .type)
try container.encode(coordinates, forKey: .coordinates)
try container.encode(url, forKey: .value)
case let .weather(coordinates, emoji, temperature, color):
try container.encode(MediaAreaType.weather.rawValue, forKey: .type)
try container.encode(coordinates, forKey: .coordinates)
try container.encode(emoji, forKey: .value)
try container.encode(temperature, forKey: .temperature)
try container.encode(color, forKey: .color)
case let .starGift(coordinates, slug):
try container.encode(MediaAreaType.starGift.rawValue, forKey: .type)
try container.encode(coordinates, forKey: .coordinates)
try container.encode(slug, forKey: .value)
}
}
}
public extension MediaArea {
var coordinates: Coordinates {
switch self {
case let .venue(coordinates, _):
return coordinates
case let .reaction(coordinates, _, _):
return coordinates
case let .channelMessage(coordinates, _):
return coordinates
case let .link(coordinates, _):
return coordinates
case let .weather(coordinates, _, _, _):
return coordinates
case let .starGift(coordinates, _):
return coordinates
}
}
}
@@ -0,0 +1,230 @@
import Postbox
public final class EngineMessage: Equatable {
public static let newTopicThreadId: Int64 = Message.newTopicThreadId
public typealias Id = MessageId
public typealias StableId = UInt32
public typealias Index = MessageIndex
public typealias Tags = MessageTags
public typealias Attribute = MessageAttribute
public typealias GroupInfo = MessageGroupInfo
public typealias Flags = MessageFlags
public typealias GlobalTags = GlobalMessageTags
public typealias LocalTags = LocalMessageTags
public typealias ForwardInfo = MessageForwardInfo
public typealias CustomTag = MemoryBuffer
public enum InputTag: Hashable {
case tag(Tags)
case custom(CustomTag)
}
private let impl: Message
public var stableId: UInt32 {
return self.impl.stableId
}
public var stableVersion: UInt32 {
return self.impl.stableVersion
}
public var id: Id {
return self.impl.id
}
public var globallyUniqueId: Int64? {
return self.impl.globallyUniqueId
}
public var groupingKey: Int64? {
return self.impl.groupingKey
}
public var groupInfo: GroupInfo? {
return self.impl.groupInfo
}
public var threadId: Int64? {
return self.impl.threadId
}
public var timestamp: Int32 {
return self.impl.timestamp
}
public var flags: Flags {
return self.impl.flags
}
public var tags: Tags {
return self.impl.tags
}
public var globalTags: GlobalTags {
return self.impl.globalTags
}
public var localTags: LocalTags {
return self.impl.localTags
}
public var customTags: [CustomTag] {
return self.impl.customTags
}
public var forwardInfo: ForwardInfo? {
return self.impl.forwardInfo
}
public var author: EnginePeer? {
return self.impl.author.flatMap(EnginePeer.init)
}
public var text: String {
return self.impl.text
}
public var attributes: [Attribute] {
return self.impl.attributes
}
public var media: [Media] {
return self.impl.media
}
public var peers: SimpleDictionary<EnginePeer.Id, Peer> {
return self.impl.peers
}
public var associatedMessages: SimpleDictionary<EngineMessage.Id, Message> {
return self.impl.associatedMessages
}
public var associatedMessageIds: [EngineMessage.Id] {
return self.impl.associatedMessageIds
}
public var associatedMedia: [MediaId: Media] {
return self.impl.associatedMedia
}
public var associatedThreadInfo: Message.AssociatedThreadInfo? {
return self.impl.associatedThreadInfo
}
public var associatedStories: [StoryId: CodableEntry] {
return self.impl.associatedStories
}
public var index: MessageIndex {
return self.impl.index
}
public init(
stableId: UInt32,
stableVersion: UInt32,
id: EngineMessage.Id,
globallyUniqueId: Int64?,
groupingKey: Int64?,
groupInfo: EngineMessage.GroupInfo?,
threadId: Int64?,
timestamp: Int32,
flags: EngineMessage.Flags,
tags: EngineMessage.Tags,
globalTags: EngineMessage.GlobalTags,
localTags: EngineMessage.LocalTags,
customTags: [EngineMessage.CustomTag],
forwardInfo: EngineMessage.ForwardInfo?,
author: EnginePeer?,
text: String,
attributes: [Attribute],
media: [EngineMedia],
peers: [EnginePeer.Id: EnginePeer],
associatedMessages: [EngineMessage.Id: EngineMessage],
associatedMessageIds: [EngineMessage.Id],
associatedMedia: [MediaId: Media],
associatedThreadInfo: Message.AssociatedThreadInfo?,
associatedStories: [StoryId: CodableEntry]
) {
var mappedPeers: [PeerId: Peer] = [:]
for (id, peer) in peers {
mappedPeers[id] = peer._asPeer()
}
var mappedAssociatedMessages: [MessageId: Message] = [:]
for (id, message) in associatedMessages {
mappedAssociatedMessages[id] = message._asMessage()
}
self.impl = Message(
stableId: stableId,
stableVersion: stableVersion,
id: id,
globallyUniqueId: globallyUniqueId,
groupingKey: groupingKey,
groupInfo: groupInfo,
threadId: threadId,
timestamp: timestamp,
flags: flags,
tags: tags,
globalTags: globalTags,
localTags: localTags,
customTags: customTags,
forwardInfo: forwardInfo,
author: author?._asPeer(),
text: text,
attributes: attributes,
media: media.map { $0._asMedia() },
peers: SimpleDictionary(mappedPeers),
associatedMessages: SimpleDictionary(mappedAssociatedMessages),
associatedMessageIds: associatedMessageIds,
associatedMedia: associatedMedia,
associatedThreadInfo: associatedThreadInfo,
associatedStories: associatedStories
)
}
public init(_ impl: Message) {
self.impl = impl
}
public func _asMessage() -> Message {
return self.impl
}
public static func ==(lhs: EngineMessage, rhs: EngineMessage) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.globallyUniqueId != rhs.globallyUniqueId {
return false
}
if lhs.groupingKey != rhs.groupingKey {
return false
}
if lhs.groupInfo != rhs.groupInfo {
return false
}
if lhs.threadId != rhs.threadId {
return false
}
if lhs.timestamp != rhs.timestamp {
return false
}
if lhs.flags != rhs.flags {
return false
}
if lhs.tags != rhs.tags {
return false
}
if lhs.globalTags != rhs.globalTags {
return false
}
if lhs.localTags != rhs.localTags {
return false
}
if lhs.forwardInfo != rhs.forwardInfo {
return false
}
if lhs.author != rhs.author {
return false
}
if lhs.text != rhs.text {
return false
}
if !areMediaArraysEqual(lhs.media, rhs.media) {
return false
}
if lhs.associatedThreadInfo != rhs.associatedThreadInfo {
return false
}
if lhs.attributes.count != rhs.attributes.count {
return false
}
if lhs.stableVersion != rhs.stableVersion {
return false
}
return true
}
}
@@ -0,0 +1,120 @@
import Postbox
import SwiftSignalKit
import TelegramApi
public final class MessageReadStats {
public let reactionCount: Int
public let peers: [EnginePeer]
public let readTimestamps: [EnginePeer.Id: Int32]
public init(reactionCount: Int, peers: [EnginePeer], readTimestamps: [EnginePeer.Id: Int32]) {
self.reactionCount = reactionCount
self.peers = peers
self.readTimestamps = readTimestamps
}
}
func _internal_messageReadStats(account: Account, id: MessageId) -> Signal<MessageReadStats?, NoError> {
return account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(id.peerId)
}
|> mapToSignal { peer -> Signal<MessageReadStats?, NoError> in
guard let peer, let inputPeer = apiInputPeer(peer) else {
return .single(nil)
}
if id.namespace != Namespaces.Message.Cloud {
return .single(nil)
}
if id.peerId.namespace == Namespaces.Peer.CloudUser {
return account.network.request(Api.functions.messages.getOutboxReadDate(peer: inputPeer, msgId: id.id))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.OutboxReadDate?, NoError> in
return .single(nil)
}
|> map { result -> MessageReadStats? in
guard let result else {
return MessageReadStats(reactionCount: 0, peers: [], readTimestamps: [:])
}
switch result {
case let .outboxReadDate(date):
return MessageReadStats(reactionCount: 0, peers: [EnginePeer(peer)], readTimestamps: [peer.id: date])
}
}
} else {
let readPeers: Signal<[(Int64, Int32)]?, NoError> = account.network.request(Api.functions.messages.getMessageReadParticipants(peer: inputPeer, msgId: id.id))
|> map { result -> [(Int64, Int32)]? in
var items: [(Int64, Int32)] = []
for item in result {
switch item {
case let .readParticipantDate(userId, date):
items.append((userId, date))
}
}
return items
}
|> `catch` { _ -> Signal<[(Int64, Int32)]?, NoError> in
return .single(nil)
}
let reactionCount: Signal<Int, NoError> = account.network.request(Api.functions.messages.getMessageReactionsList(flags: 0, peer: inputPeer, id: id.id, reaction: nil, offset: nil, limit: 1))
|> map { result -> Int in
switch result {
case let .messageReactionsList(_, count, _, _, _, _):
return Int(count)
}
}
|> `catch` { _ -> Signal<Int, NoError> in
return .single(0)
}
return combineLatest(readPeers, reactionCount)
|> mapToSignal { result, reactionCount -> Signal<MessageReadStats?, NoError> in
return account.postbox.transaction { transaction -> (peerIds: [PeerId], readTimestamps: [PeerId: Int32], missingPeerIds: [PeerId]) in
var peerIds: [PeerId] = []
var readTimestamps: [PeerId: Int32] = [:]
var missingPeerIds: [PeerId] = []
let authorId = transaction.getMessage(id)?.author?.id
if let result = result {
for (id, timestamp) in result {
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id))
readTimestamps[peerId] = timestamp
if peerId == account.peerId {
continue
}
if peerId == authorId {
continue
}
peerIds.append(peerId)
if transaction.getPeer(peerId) == nil {
missingPeerIds.append(peerId)
}
}
}
return (peerIds: peerIds, readTimestamps: readTimestamps, missingPeerIds: missingPeerIds)
}
|> mapToSignal { peerIds, readTimestamps, missingPeerIds -> Signal<MessageReadStats?, NoError> in
if missingPeerIds.isEmpty || id.peerId.namespace != Namespaces.Peer.CloudChannel {
return account.postbox.transaction { transaction -> MessageReadStats? in
return MessageReadStats(reactionCount: reactionCount, peers: peerIds.compactMap { peerId -> EnginePeer? in
return transaction.getPeer(peerId).flatMap(EnginePeer.init)
}, readTimestamps: readTimestamps)
}
} else {
return _internal_channelMembers(postbox: account.postbox, network: account.network, accountPeerId: account.peerId, peerId: id.peerId, category: .recent(.all), offset: 0, limit: 50, hash: 0)
|> mapToSignal { _ -> Signal<MessageReadStats?, NoError> in
return account.postbox.transaction { transaction -> MessageReadStats? in
return MessageReadStats(reactionCount: reactionCount, peers: peerIds.compactMap { peerId -> EnginePeer? in
return transaction.getPeer(peerId).flatMap(EnginePeer.init)
}, readTimestamps: readTimestamps)
}
}
}
}
}
}
}
}
@@ -0,0 +1,189 @@
import Foundation
import Postbox
import SwiftSignalKit
func _internal_enqueueOutgoingMessageWithChatContextResult(account: Account, to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: EngineMessageReplySubject?, replyToStoryId: StoryId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, sendPaidMessageStars: StarsAmount?, postpone: Bool, correlationId: Int64?) -> Bool {
guard let message = _internal_outgoingMessageWithChatContextResult(to: peerId, threadId: threadId, botId: botId, result: result, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime, sendPaidMessageStars: sendPaidMessageStars, postpone: postpone, correlationId: correlationId) else {
return false
}
let _ = enqueueMessages(account: account, peerId: peerId, messages: [message]).start()
return true
}
func _internal_outgoingMessageWithChatContextResult(to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: EngineMessageReplySubject?, replyToStoryId: StoryId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, sendPaidMessageStars: StarsAmount?, postpone: Bool, correlationId: Int64?) -> EnqueueMessage? {
var replyToMessageId = replyToMessageId
if replyToMessageId == nil, let threadId = threadId {
replyToMessageId = EngineMessageReplySubject(messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: MessageId.Id(clamping: threadId)), quote: nil, todoItemId: nil)
}
var webpageUrl: String?
if case let .webpage(_, _, url, _, _) = result.message {
webpageUrl = url
}
var attributes: [MessageAttribute] = []
attributes.append(OutgoingChatContextResultMessageAttribute(queryId: result.queryId, id: result.id, hideVia: hideVia, webpageUrl: webpageUrl))
if !hideVia {
attributes.append(InlineBotMessageAttribute(peerId: botId, title: nil))
}
if let scheduleTime = scheduleTime {
attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: scheduleTime, repeatPeriod: nil))
}
if silentPosting {
attributes.append(NotificationInfoMessageAttribute(flags: .muted))
}
if let sendPaidMessageStars {
attributes.append(PaidStarsMessageAttribute(stars: sendPaidMessageStars, postponeSending: postpone))
}
switch result.message {
case let .auto(caption, entities, replyMarkup):
if let entities = entities {
attributes.append(entities)
}
if let replyMarkup = replyMarkup {
attributes.append(replyMarkup)
}
switch result {
case let .internalReference(internalReference):
if internalReference.type == "game" {
if peerId.namespace == Namespaces.Peer.SecretChat {
let filteredAttributes = attributes.filter { attribute in
if let _ = attribute as? ReplyMarkupMessageAttribute {
return false
}
return true
}
if let media: Media = internalReference.file ?? internalReference.image {
return .message(text: caption, attributes: filteredAttributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])
} else {
return .message(text: caption, attributes: filteredAttributes, inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])
}
} else {
return .message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaGame(gameId: 0, accessHash: 0, name: "", title: internalReference.title ?? "", description: internalReference.description ?? "", image: internalReference.image, file: internalReference.file)), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])
}
} else if let file = internalReference.file, internalReference.type == "gif" {
return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: file), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])
} else if let image = internalReference.image {
return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: image), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])
} else if let file = internalReference.file {
return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: file), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])
} else {
return nil
}
case let .externalReference(externalReference):
if externalReference.type == "photo" {
if let thumbnail = externalReference.thumbnail {
var randomId: Int64 = 0
arc4random_buf(&randomId, 8)
let thumbnailResource = thumbnail.resource
let imageDimensions = thumbnail.dimensions ?? PixelDimensions(width: 128, height: 128)
let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: imageDimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])
return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: tmpImage), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])
} else {
return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])
}
} else if externalReference.type == "document" || externalReference.type == "gif" || externalReference.type == "audio" || externalReference.type == "voice" {
var videoThumbnails: [TelegramMediaFile.VideoThumbnail] = []
var previewRepresentations: [TelegramMediaImageRepresentation] = []
if let thumbnail = externalReference.thumbnail {
var randomId: Int64 = 0
arc4random_buf(&randomId, 8)
let thumbnailResource = thumbnail.resource
if thumbnail.mimeType.hasPrefix("video/") {
videoThumbnails.append(TelegramMediaFile.VideoThumbnail(dimensions: thumbnail.dimensions ?? PixelDimensions(width: 128, height: 128), resource: thumbnailResource))
} else {
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: thumbnail.dimensions ?? PixelDimensions(width: 128, height: 128), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false))
}
}
var fileName = "file"
if let content = externalReference.content {
var contentUrl: String?
if let resource = content.resource as? HttpReferenceMediaResource {
contentUrl = resource.url
} else if let resource = content.resource as? WebFileReferenceMediaResource {
contentUrl = resource.url
}
if let contentUrl = contentUrl, let url = URL(string: contentUrl) {
if !url.lastPathComponent.isEmpty {
fileName = url.lastPathComponent
}
}
}
var fileAttributes: [TelegramMediaFileAttribute] = []
fileAttributes.append(.FileName(fileName: fileName))
if externalReference.type == "gif" {
fileAttributes.append(.Animated)
}
if let dimensions = externalReference.content?.dimensions {
fileAttributes.append(.ImageSize(size: dimensions))
if externalReference.type == "gif" {
fileAttributes.append(.Video(duration: externalReference.content?.duration ?? 0.0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil))
}
}
if externalReference.type == "audio" || externalReference.type == "voice" {
fileAttributes.append(.Audio(isVoice: externalReference.type == "voice", duration: Int(Int32(externalReference.content?.duration ?? 0)), title: externalReference.title, performer: externalReference.description, waveform: nil))
}
var randomId: Int64 = 0
arc4random_buf(&randomId, 8)
let resource: TelegramMediaResource
if peerId.namespace == Namespaces.Peer.SecretChat, let webResource = externalReference.content?.resource as? WebFileReferenceMediaResource {
resource = webResource
} else {
resource = EmptyMediaResource()
}
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: videoThumbnails, immediateThumbnailData: nil, mimeType: externalReference.content?.mimeType ?? "application/binary", size: nil, attributes: fileAttributes, alternativeRepresentations: [])
return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: file), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])
} else {
return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])
}
}
case let .text(text, entities, disableUrlPreview, previewParameters, replyMarkup):
if let entities = entities {
attributes.append(entities)
}
if let replyMarkup = replyMarkup {
attributes.append(replyMarkup)
}
if let previewParameters = previewParameters {
attributes.append(previewParameters)
}
if disableUrlPreview {
attributes.append(OutgoingContentInfoMessageAttribute(flags: [.disableLinkPreviews]))
}
return .message(text: text, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])
case let .mapLocation(media, replyMarkup):
if let replyMarkup = replyMarkup {
attributes.append(replyMarkup)
}
return .message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])
case let .contact(media, replyMarkup):
if let replyMarkup = replyMarkup {
attributes.append(replyMarkup)
}
return .message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])
case let .invoice(media, replyMarkup):
if let replyMarkup = replyMarkup {
attributes.append(replyMarkup)
}
return .message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])
case let .webpage(text, entities, _, previewParameters, replyMarkup):
if let entities = entities {
attributes.append(entities)
}
if let replyMarkup = replyMarkup {
attributes.append(replyMarkup)
}
if let previewParameters = previewParameters {
attributes.append(previewParameters)
}
return .message(text: text, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])
}
}
@@ -0,0 +1,32 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
func _internal_topPeerActiveLiveLocationMessages(viewTracker: AccountViewTracker, accountPeerId: PeerId, peerId: PeerId) -> Signal<(Peer?, [Message]), NoError> {
return viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId: peerId, threadId: nil), index: .upperBound, anchorIndex: .upperBound, count: 50, fixedCombinedReadStates: nil, tag: .tag(.liveLocation), orderStatistics: [], additionalData: [.peer(accountPeerId)])
|> map { (view, _, _) -> (Peer?, [Message]) in
var accountPeer: Peer?
for entry in view.additionalData {
if case let .peer(_, peer) = entry {
accountPeer = peer
break
}
}
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
var result: [Message] = []
for entry in view.entries {
for media in entry.message.media {
if let location = media as? TelegramMediaMap, let liveBroadcastingTimeout = location.liveBroadcastingTimeout {
if liveBroadcastingTimeout == liveLocationIndefinitePeriod || entry.message.timestamp + liveBroadcastingTimeout > timestamp {
result.append(entry.message)
}
} else {
assertionFailure()
}
}
}
return (accountPeer, result)
}
}
@@ -0,0 +1,598 @@
import Foundation
import SwiftSignalKit
import Postbox
import TelegramApi
public extension Stories {
enum PendingTarget: Codable {
private enum CodingKeys: String, CodingKey {
case discriminator = "tt"
case peerId = "peerId"
case language = "language"
}
case myStories
case peer(PeerId)
case botPreview(id: PeerId, language: String?)
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
switch try container.decode(Int32.self, forKey: .discriminator) {
case 0:
self = .myStories
case 1:
self = .peer(try container.decode(PeerId.self, forKey: .peerId))
case 2:
self = .botPreview(id: try container.decode(PeerId.self, forKey: .peerId), language: try container.decodeIfPresent(String.self, forKey: .language))
default:
self = .myStories
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .myStories:
try container.encode(0 as Int32, forKey: .discriminator)
case let .peer(peerId):
try container.encode(1 as Int32, forKey: .discriminator)
try container.encode(peerId, forKey: .peerId)
case let .botPreview(peerId, language):
try container.encode(2 as Int32, forKey: .discriminator)
try container.encode(peerId, forKey: .peerId)
try container.encodeIfPresent(language, forKey: .language)
}
}
}
struct PendingForwardInfo: Codable, Equatable {
private enum CodingKeys: String, CodingKey {
case peerId = "peerId"
case storyId = "storyId"
case isModified = "isModified"
}
public let peerId: EnginePeer.Id
public let storyId: Int32
public let isModified: Bool
public init(peerId: EnginePeer.Id, storyId: Int32, isModified: Bool) {
self.peerId = peerId
self.storyId = storyId
self.isModified = isModified
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.peerId = EnginePeer.Id(try container.decode(Int64.self, forKey: .peerId))
self.storyId = try container.decode(Int32.self, forKey: .storyId)
self.isModified = try container.decodeIfPresent(Bool.self, forKey: .isModified) ?? false
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.peerId.toInt64(), forKey: .peerId)
try container.encode(self.storyId, forKey: .storyId)
try container.encode(self.isModified, forKey: .isModified)
}
}
final class PendingItem: Equatable, Codable {
private enum CodingKeys: CodingKey {
case target
case stableId
case timestamp
case media
case mediaAreas
case text
case entities
case embeddedStickers
case pin
case privacy
case isForwardingDisabled
case period
case randomId
case forwardInfo
case uploadInfo
case folders
}
public let target: PendingTarget
public let stableId: Int32
public let timestamp: Int32
public let media: Media
public let mediaAreas: [MediaArea]
public let text: String
public let entities: [MessageTextEntity]
public let embeddedStickers: [TelegramMediaFile]
public let pin: Bool
public let privacy: EngineStoryPrivacy
public let isForwardingDisabled: Bool
public let period: Int32
public let randomId: Int64
public let forwardInfo: PendingForwardInfo?
public let folders: [Int64]
public let uploadInfo: StoryUploadInfo?
public init(
target: PendingTarget,
stableId: Int32,
timestamp: Int32,
media: Media,
mediaAreas: [MediaArea],
text: String,
entities: [MessageTextEntity],
embeddedStickers: [TelegramMediaFile],
pin: Bool,
privacy: EngineStoryPrivacy,
isForwardingDisabled: Bool,
period: Int32,
randomId: Int64,
forwardInfo: PendingForwardInfo?,
folders: [Int64],
uploadInfo: StoryUploadInfo?
) {
self.target = target
self.stableId = stableId
self.timestamp = timestamp
self.media = media
self.mediaAreas = mediaAreas
self.text = text
self.entities = entities
self.embeddedStickers = embeddedStickers
self.pin = pin
self.privacy = privacy
self.isForwardingDisabled = isForwardingDisabled
self.period = period
self.randomId = randomId
self.forwardInfo = forwardInfo
self.folders = folders
self.uploadInfo = uploadInfo
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.target = try container.decodeIfPresent(PendingTarget.self, forKey: .target) ?? .myStories
self.stableId = try container.decode(Int32.self, forKey: .stableId)
self.timestamp = try container.decode(Int32.self, forKey: .timestamp)
let mediaData = try container.decode(Data.self, forKey: .media)
self.media = PostboxDecoder(buffer: MemoryBuffer(data: mediaData)).decodeRootObject() as! Media
self.mediaAreas = try container.decodeIfPresent([MediaArea].self, forKey: .mediaAreas) ?? []
self.text = try container.decode(String.self, forKey: .text)
self.entities = try container.decode([MessageTextEntity].self, forKey: .entities)
let stickersData = try container.decode(Data.self, forKey: .embeddedStickers)
let stickersDecoder = PostboxDecoder(buffer: MemoryBuffer(data: stickersData))
self.embeddedStickers = (try? stickersDecoder.decodeObjectArrayWithCustomDecoderForKey("stickers", decoder: { TelegramMediaFile(decoder: $0) })) ?? []
self.pin = try container.decode(Bool.self, forKey: .pin)
self.privacy = try container.decode(EngineStoryPrivacy.self, forKey: .privacy)
self.isForwardingDisabled = try container.decodeIfPresent(Bool.self, forKey: .isForwardingDisabled) ?? false
self.period = try container.decode(Int32.self, forKey: .period)
self.randomId = try container.decode(Int64.self, forKey: .randomId)
self.forwardInfo = try container.decodeIfPresent(PendingForwardInfo.self, forKey: .forwardInfo)
self.folders = try container.decodeIfPresent([Int64].self, forKey: .folders) ?? []
self.uploadInfo = try container.decodeIfPresent(StoryUploadInfo.self, forKey: .uploadInfo)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.target, forKey: .target)
try container.encode(self.stableId, forKey: .stableId)
try container.encode(self.timestamp, forKey: .timestamp)
let mediaEncoder = PostboxEncoder()
mediaEncoder.encodeRootObject(self.media)
try container.encode(mediaEncoder.makeData(), forKey: .media)
try container.encode(self.mediaAreas, forKey: .mediaAreas)
try container.encode(self.text, forKey: .text)
try container.encode(self.entities, forKey: .entities)
let stickersEncoder = PostboxEncoder()
stickersEncoder.encodeObjectArray(self.embeddedStickers, forKey: "stickers")
try container.encode(stickersEncoder.makeData(), forKey: .embeddedStickers)
try container.encode(self.pin, forKey: .pin)
try container.encode(self.privacy, forKey: .privacy)
try container.encode(self.isForwardingDisabled, forKey: .isForwardingDisabled)
try container.encode(self.period, forKey: .period)
try container.encode(self.randomId, forKey: .randomId)
try container.encodeIfPresent(self.forwardInfo, forKey: .forwardInfo)
try container.encode(self.folders, forKey: .folders)
try container.encodeIfPresent(self.uploadInfo, forKey: .uploadInfo)
}
public static func ==(lhs: PendingItem, rhs: PendingItem) -> Bool {
if lhs.timestamp != rhs.timestamp {
return false
}
if lhs.stableId != rhs.stableId {
return false
}
if !lhs.media.isEqual(to: rhs.media) {
return false
}
if lhs.mediaAreas != rhs.mediaAreas {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.entities != rhs.entities {
return false
}
if lhs.pin != rhs.pin {
return false
}
if lhs.privacy != rhs.privacy {
return false
}
if lhs.isForwardingDisabled != rhs.isForwardingDisabled {
return false
}
if lhs.period != rhs.period {
return false
}
if lhs.randomId != rhs.randomId {
return false
}
if lhs.forwardInfo != rhs.forwardInfo {
return false
}
if lhs.folders != rhs.folders {
return false
}
if lhs.uploadInfo != rhs.uploadInfo {
return false
}
return true
}
}
struct LocalState: Equatable, Codable {
public var items: [PendingItem]
public init(
items: [PendingItem]
) {
self.items = items
}
}
}
final class PendingStoryManager {
private final class PendingItemContext {
let queue: Queue
let item: Stories.PendingItem
let updated: () -> Void
var progress: Float = 0.0
var disposable: Disposable?
init(queue: Queue, item: Stories.PendingItem, updated: @escaping () -> Void) {
self.queue = queue
self.item = item
self.updated = updated
}
deinit {
self.disposable?.dispose()
}
}
private final class Impl {
let queue: Queue
let postbox: Postbox
let network: Network
let accountPeerId: PeerId
let stateManager: AccountStateManager
let messageMediaPreuploadManager: MessageMediaPreuploadManager
let revalidationContext: MediaReferenceRevalidationContext
let auxiliaryMethods: AccountAuxiliaryMethods
var itemsDisposable: Disposable?
var currentPendingItemContext: PendingItemContext?
var queuedPendingItems = Set<PeerId>()
var storyObserverContexts: [Int32: Bag<(Float) -> Void>] = [:]
private let allStoriesEventsPipe = ValuePipe<(Int32, Int32)>()
var allStoriesUploadEvents: Signal<(Int32, Int32), NoError> {
return self.allStoriesEventsPipe.signal()
}
private let allStoriesUploadProgressPromise = Promise<[PeerId: Float]>([:])
private var allStoriesUploadProgressValue: [PeerId: Float] = [:]
var allStoriesUploadProgress: Signal<[PeerId: Float], NoError> {
return self.allStoriesUploadProgressPromise.get()
}
private let hasPendingPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
var hasPending: Signal<Bool, NoError> {
return self.hasPendingPromise.get()
}
func storyUploadProgress(stableId: Int32, next: @escaping (Float) -> Void) -> Disposable {
let bag: Bag<(Float) -> Void>
if let current = self.storyObserverContexts[stableId] {
bag = current
} else {
bag = Bag()
self.storyObserverContexts[stableId] = bag
}
let index = bag.add(next)
if let currentPendingItemContext = self.currentPendingItemContext, currentPendingItemContext.item.stableId == stableId {
next(currentPendingItemContext.progress)
} else {
next(0.0)
}
let queue = self.queue
return ActionDisposable { [weak self, weak bag] in
queue.async {
guard let `self` = self else {
return
}
if let bag = bag, let listBag = self.storyObserverContexts[stableId], listBag === bag {
bag.remove(index)
if bag.isEmpty {
self.storyObserverContexts.removeValue(forKey: stableId)
}
}
}
}
}
init(queue: Queue, postbox: Postbox, network: Network, accountPeerId: PeerId, stateManager: AccountStateManager, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, auxiliaryMethods: AccountAuxiliaryMethods) {
self.queue = queue
self.postbox = postbox
self.network = network
self.accountPeerId = accountPeerId
self.stateManager = stateManager
self.messageMediaPreuploadManager = messageMediaPreuploadManager
self.revalidationContext = revalidationContext
self.auxiliaryMethods = auxiliaryMethods
self.itemsDisposable = (postbox.combinedView(keys: [PostboxViewKey.storiesState(key: .local)])
|> deliverOn(self.queue)).start(next: { [weak self] views in
guard let `self` = self else {
return
}
guard let view = views.views[PostboxViewKey.storiesState(key: .local)] as? StoryStatesView else {
return
}
let localState: Stories.LocalState
if let value = view.value?.get(Stories.LocalState.self) {
localState = value
} else {
localState = Stories.LocalState(items: [])
}
self.update(localState: localState)
})
}
deinit {
self.itemsDisposable?.dispose()
}
private func update(localState: Stories.LocalState) {
if let currentPendingItemContext = self.currentPendingItemContext, !localState.items.contains(where: { $0.randomId == currentPendingItemContext.item.randomId }) {
self.currentPendingItemContext = nil
self.queue.after(0.1, {
let _ = currentPendingItemContext
print(currentPendingItemContext)
})
}
self.queuedPendingItems = Set(localState.items.compactMap { item -> PeerId? in
switch item.target {
case .myStories:
return self.accountPeerId
case let .peer(id):
return id
case .botPreview:
return nil
}
})
if self.currentPendingItemContext == nil, let firstItem = localState.items.first {
let queue = self.queue
let itemStableId = firstItem.stableId
let pendingItemContext = PendingItemContext(queue: queue, item: firstItem, updated: { [weak self] in
queue.async {
guard let `self` = self else {
return
}
self.processContextsUpdated()
if let pendingItemContext = self.currentPendingItemContext, pendingItemContext.item.stableId == itemStableId, let bag = self.storyObserverContexts[itemStableId] {
for f in bag.copyItems() {
f(pendingItemContext.progress)
}
}
}
})
self.currentPendingItemContext = pendingItemContext
let toPeerId: PeerId
var isBotPreview = false
var botPreviewLanguage: String?
switch firstItem.target {
case .myStories:
toPeerId = self.accountPeerId
case let .peer(peerId):
toPeerId = peerId
case let .botPreview(peerId, language):
toPeerId = peerId
botPreviewLanguage = language
isBotPreview = true
}
let stableId = firstItem.stableId
if isBotPreview {
pendingItemContext.disposable = (_internal_uploadBotPreviewImpl(
postbox: self.postbox,
network: self.network,
accountPeerId: self.accountPeerId,
stateManager: self.stateManager,
messageMediaPreuploadManager: self.messageMediaPreuploadManager,
revalidationContext: self.revalidationContext,
auxiliaryMethods: self.auxiliaryMethods,
toPeerId: toPeerId,
language: botPreviewLanguage,
stableId: stableId,
media: firstItem.media,
mediaAreas: firstItem.mediaAreas,
text: firstItem.text,
entities: firstItem.entities,
embeddedStickers: firstItem.embeddedStickers,
randomId: firstItem.randomId
)
|> deliverOn(self.queue)).start(next: { [weak self] event in
guard let self else {
return
}
switch event {
case let .progress(progress):
if let currentPendingItemContext = self.currentPendingItemContext, currentPendingItemContext.item.stableId == stableId {
currentPendingItemContext.progress = progress
currentPendingItemContext.updated()
}
case let .completed(id):
if let id = id {
self.allStoriesEventsPipe.putNext((stableId, id))
}
// wait for the local state to change via Postbox
break
}
})
} else {
if let uploadInfo = pendingItemContext.item.uploadInfo {
let partTotalProgress = 1.0 / Float(uploadInfo.total)
pendingItemContext.progress = Float(uploadInfo.index) * partTotalProgress
}
pendingItemContext.disposable = (_internal_uploadStoryImpl(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, stateManager: self.stateManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, revalidationContext: self.revalidationContext, auxiliaryMethods: self.auxiliaryMethods, toPeerId: toPeerId, stableId: stableId, media: firstItem.media, mediaAreas: firstItem.mediaAreas, text: firstItem.text, entities: firstItem.entities, embeddedStickers: firstItem.embeddedStickers, pin: firstItem.pin, privacy: firstItem.privacy, isForwardingDisabled: firstItem.isForwardingDisabled, period: Int(firstItem.period), folders: firstItem.folders, randomId: firstItem.randomId, forwardInfo: firstItem.forwardInfo)
|> deliverOn(self.queue)).start(next: { [weak self] event in
guard let `self` = self else {
return
}
switch event {
case let .progress(progress):
if let currentPendingItemContext = self.currentPendingItemContext, currentPendingItemContext.item.stableId == stableId {
if let uploadInfo = currentPendingItemContext.item.uploadInfo {
let partTotalProgress = 1.0 / Float(uploadInfo.total)
currentPendingItemContext.progress = Float(uploadInfo.index) * partTotalProgress + progress * partTotalProgress
} else {
currentPendingItemContext.progress = progress
}
currentPendingItemContext.updated()
}
case let .completed(id):
if let id = id {
self.allStoriesEventsPipe.putNext((stableId, id))
}
// wait for the local state to change via Postbox
break
}
})
}
}
self.processContextsUpdated()
}
private func processContextsUpdated() {
var currentProgress: [PeerId: Float] = [:]
for peerId in self.queuedPendingItems {
currentProgress[peerId] = 0.0
}
if let currentPendingItemContext = self.currentPendingItemContext {
switch currentPendingItemContext.item.target {
case .myStories:
currentProgress[self.accountPeerId] = currentPendingItemContext.progress
case let .peer(id):
currentProgress[id] = currentPendingItemContext.progress
case .botPreview:
break
}
}
if self.allStoriesUploadProgressValue != currentProgress {
let previousProgress = self.allStoriesUploadProgressValue
self.allStoriesUploadProgressValue = currentProgress
if !previousProgress.isEmpty && currentProgress.isEmpty {
// Hack: the UI is updated after 2 Postbox queries
let signal: Signal<[PeerId: Float], NoError> = Signal { subscriber in
Postbox.sharedQueue.justDispatch {
Postbox.sharedQueue.justDispatch {
subscriber.putNext([:])
}
}
return EmptyDisposable
}
|> deliverOnMainQueue
self.allStoriesUploadProgressPromise.set(signal)
} else {
self.allStoriesUploadProgressPromise.set(.single(currentProgress))
}
}
self.hasPendingPromise.set(self.currentPendingItemContext != nil)
}
}
private let queue: Queue
private let impl: QueueLocalObject<Impl>
private let accountPeerId: PeerId
public var allStoriesUploadProgress: Signal<[PeerId: Float], NoError> {
return self.impl.signalWith { impl, subscriber in
return impl.allStoriesUploadProgress.start(next: subscriber.putNext)
}
}
public var hasPending: Signal<Bool, NoError> {
return self.impl.signalWith { impl, subscriber in
return impl.hasPending.start(next: subscriber.putNext)
}
}
public func storyUploadProgress(stableId: Int32) -> Signal<Float, NoError> {
return self.impl.signalWith { impl, subscriber in
return impl.storyUploadProgress(stableId: stableId, next: subscriber.putNext)
}
}
public func allStoriesUploadEvents() -> Signal<(Int32, Int32), NoError> {
return self.impl.signalWith { impl, subscriber in
return impl.allStoriesUploadEvents.start(next: subscriber.putNext)
}
}
init(postbox: Postbox, network: Network, accountPeerId: PeerId, stateManager: AccountStateManager, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, auxiliaryMethods: AccountAuxiliaryMethods) {
let queue = Queue.mainQueue()
self.queue = queue
self.accountPeerId = accountPeerId
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, postbox: postbox, network: network, accountPeerId: accountPeerId, stateManager: stateManager, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, auxiliaryMethods: auxiliaryMethods)
})
}
func lookUpPendingStoryIdMapping(peerId: PeerId, stableId: Int32) -> Int32? {
return _internal_lookUpPendingStoryIdMapping(peerId: peerId, stableId: stableId)
}
}
@@ -0,0 +1,436 @@
import Foundation
import TelegramApi
import Postbox
import SwiftSignalKit
import MtProtoKit
public enum RequestMessageSelectPollOptionError {
case generic
}
func _internal_requestMessageSelectPollOption(account: Account, messageId: MessageId, opaqueIdentifiers: [Data]) -> Signal<TelegramMediaPoll?, RequestMessageSelectPollOptionError> {
return account.postbox.loadedPeerWithId(messageId.peerId)
|> take(1)
|> castError(RequestMessageSelectPollOptionError.self)
|> mapToSignal { peer in
if let inputPeer = apiInputPeer(peer) {
return account.network.request(Api.functions.messages.sendVote(peer: inputPeer, msgId: messageId.id, options: opaqueIdentifiers.map { Buffer(data: $0) }))
|> mapError { _ -> RequestMessageSelectPollOptionError in
return .generic
}
|> mapToSignal { result -> Signal<TelegramMediaPoll?, RequestMessageSelectPollOptionError> in
return account.postbox.transaction { transaction -> TelegramMediaPoll? in
var resultPoll: TelegramMediaPoll?
switch result {
case let .updates(updates, _, _, _, _):
for update in updates {
switch update {
case let .updateMessagePoll(_, id, poll, results):
let pollId = MediaId(namespace: Namespaces.Media.CloudPoll, id: id)
resultPoll = transaction.getMedia(pollId) as? TelegramMediaPoll
if let poll = poll {
switch poll {
case let .poll(_, flags, question, answers, closePeriod, _):
let publicity: TelegramMediaPollPublicity
if (flags & (1 << 1)) != 0 {
publicity = .public
} else {
publicity = .anonymous
}
let kind: TelegramMediaPollKind
if (flags & (1 << 3)) != 0 {
kind = .quiz
} else {
kind = .poll(multipleAnswers: (flags & (1 << 2)) != 0)
}
let questionText: String
let questionEntities: [MessageTextEntity]
switch question {
case let .textWithEntities(text, entities):
questionText = text
questionEntities = messageTextEntitiesFromApiEntities(entities)
}
resultPoll = TelegramMediaPoll(pollId: pollId, publicity: publicity, kind: kind, text: questionText, textEntities: questionEntities, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod)
}
}
let resultsMin: Bool
switch results {
case let .pollResults(flags, _, _, _, _, _):
resultsMin = (flags & (1 << 0)) != 0
}
resultPoll = resultPoll?.withUpdatedResults(TelegramMediaPollResults(apiResults: results), min: resultsMin)
if let resultPoll = resultPoll {
updateMessageMedia(transaction: transaction, id: pollId, media: resultPoll)
}
default:
break
}
}
break
default:
break
}
account.stateManager.addUpdates(result)
return resultPoll
}
|> castError(RequestMessageSelectPollOptionError.self)
}
} else {
return .single(nil)
}
}
}
func _internal_requestClosePoll(postbox: Postbox, network: Network, stateManager: AccountStateManager, messageId: MessageId) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> (TelegramMediaPoll, Api.InputPeer)? in
guard let inputPeer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) else {
return nil
}
guard let message = transaction.getMessage(messageId) else {
return nil
}
for media in message.media {
if let poll = media as? TelegramMediaPoll {
return (poll, inputPeer)
}
}
return nil
}
|> mapToSignal { pollAndInputPeer -> Signal<Void, NoError> in
guard let (poll, inputPeer) = pollAndInputPeer, poll.pollId.namespace == Namespaces.Media.CloudPoll else {
return .complete()
}
var flags: Int32 = 0
flags |= 1 << 14
var pollFlags: Int32 = 0
switch poll.kind {
case let .poll(multipleAnswers):
if multipleAnswers {
pollFlags |= 1 << 2
}
case .quiz:
pollFlags |= 1 << 3
}
switch poll.publicity {
case .anonymous:
break
case .public:
pollFlags |= 1 << 1
}
var pollMediaFlags: Int32 = 0
var correctAnswers: [Buffer]?
if let correctAnswersValue = poll.correctAnswers {
pollMediaFlags |= 1 << 0
correctAnswers = correctAnswersValue.map { Buffer(data: $0) }
}
pollFlags |= 1 << 0
if poll.deadlineTimeout != nil {
pollFlags |= 1 << 4
}
var mappedSolution: String?
var mappedSolutionEntities: [Api.MessageEntity]?
if let solution = poll.results.solution {
mappedSolution = solution.text
mappedSolutionEntities = apiTextAttributeEntities(TextEntitiesMessageAttribute(entities: solution.entities), associatedPeers: SimpleDictionary())
pollMediaFlags |= 1 << 1
}
return network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: nil, media: .inputMediaPoll(flags: pollMediaFlags, poll: .poll(id: poll.pollId.id, flags: pollFlags, question: .textWithEntities(text: poll.text, entities: apiEntitiesFromMessageTextEntities(poll.textEntities, associatedPeers: SimpleDictionary())), answers: poll.options.map({ $0.apiOption }), closePeriod: poll.deadlineTimeout, closeDate: nil), correctAnswers: correctAnswers, solution: mappedSolution, solutionEntities: mappedSolutionEntities), replyMarkup: nil, entities: nil, scheduleDate: nil, scheduleRepeatPeriod: nil, quickReplyShortcutId: nil))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Void, NoError> in
if let updates = updates {
stateManager.addUpdates(updates)
}
return .complete()
}
}
}
final class CachedPollOptionResult: Codable {
let peerIds: [PeerId]
let count: Int32
public static func key(pollId: MediaId, optionOpaqueIdentifier: Data) -> ValueBoxKey {
let key = ValueBoxKey(length: 4 + 8 + optionOpaqueIdentifier.count)
key.setInt32(0, value: pollId.namespace)
key.setInt64(4, value: pollId.id)
key.setData(4 + 8, value: optionOpaqueIdentifier)
return key
}
public init(peerIds: [PeerId], count: Int32) {
self.peerIds = peerIds
self.count = count
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
self.peerIds = (try container.decode([Int64].self, forKey: "peerIds")).map(PeerId.init)
self.count = try container.decode(Int32.self, forKey: "count")
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self)
try container.encode(self.peerIds.map { $0.toInt64() }, forKey: "peerIds")
try container.encode(self.count, forKey: "count")
}
}
private final class PollResultsOptionContext {
private let queue: Queue
private let account: Account
private let pollId: MediaId
private let messageId: MessageId
private let opaqueIdentifier: Data
private let disposable = MetaDisposable()
private var isLoadingMore: Bool = false
private var hasLoadedOnce: Bool = false
private var canLoadMore: Bool = true
private var nextOffset: String?
private var results: [RenderedPeer] = []
private var count: Int
private var populateCache: Bool = true
let state = Promise<PollResultsOptionState>()
init(queue: Queue, account: Account, pollId: MediaId, messageId: MessageId, opaqueIdentifier: Data, count: Int) {
self.queue = queue
self.account = account
self.pollId = pollId
self.messageId = messageId
self.opaqueIdentifier = opaqueIdentifier
self.count = count
self.isLoadingMore = true
self.disposable.set((account.postbox.transaction { transaction -> (peers: [RenderedPeer], canLoadMore: Bool)? in
let cachedResult = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPollResults, key: CachedPollOptionResult.key(pollId: pollId, optionOpaqueIdentifier: opaqueIdentifier)))?.get(CachedPollOptionResult.self)
if let cachedResult = cachedResult, Int(cachedResult.count) == count {
var result: [RenderedPeer] = []
for peerId in cachedResult.peerIds {
if let peer = transaction.getPeer(peerId) {
result.append(RenderedPeer(peer: peer))
} else {
return nil
}
}
return (result, Int(cachedResult.count) > result.count)
} else {
return nil
}
}
|> deliverOn(self.queue)).start(next: { [weak self] cachedPeersAndCanLoadMore in
guard let strongSelf = self else {
return
}
strongSelf.isLoadingMore = false
if let (cachedPeers, canLoadMore) = cachedPeersAndCanLoadMore {
strongSelf.results = cachedPeers
strongSelf.hasLoadedOnce = true
strongSelf.canLoadMore = canLoadMore
}
strongSelf.loadMore()
}))
}
deinit {
self.disposable.dispose()
}
func loadMore() {
if self.isLoadingMore {
return
}
self.isLoadingMore = true
let pollId = self.pollId
let messageId = self.messageId
let opaqueIdentifier = self.opaqueIdentifier
let account = self.account
let accountPeerId = account.peerId
let nextOffset = self.nextOffset
let populateCache = self.populateCache
self.disposable.set((self.account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<([RenderedPeer], Int, String?), NoError> in
if let inputPeer = inputPeer {
var flags: Int32 = 1 << 0
if let _ = nextOffset {
flags |= (1 << 1)
}
let signal = account.network.request(Api.functions.messages.getPollVotes(flags: flags, peer: inputPeer, id: messageId.id, option: Buffer(data: opaqueIdentifier), offset: nextOffset, limit: nextOffset == nil ? 10 : 50))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.VotesList?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<([RenderedPeer], Int, String?), NoError> in
return account.postbox.transaction { transaction -> ([RenderedPeer], Int, String?) in
guard let result = result else {
return ([], 0, nil)
}
switch result {
case let .votesList(_, count, votes, chats, users, nextOffset):
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
var resultPeers: [RenderedPeer] = []
for vote in votes {
let peerId: PeerId
switch vote {
case let .messagePeerVote(peerIdValue, _, _):
peerId = peerIdValue.peerId
case let .messagePeerVoteInputOption(peerIdValue, _):
peerId = peerIdValue.peerId
case let .messagePeerVoteMultiple(peerIdValue, _, _):
peerId = peerIdValue.peerId
}
if let peer = transaction.getPeer(peerId) {
resultPeers.append(RenderedPeer(peer: peer))
}
}
if populateCache {
if let entry = CodableEntry(CachedPollOptionResult(peerIds: resultPeers.map { $0.peerId }, count: count)) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPollResults, key: CachedPollOptionResult.key(pollId: pollId, optionOpaqueIdentifier: opaqueIdentifier)), entry: entry)
}
}
return (resultPeers, Int(count), nextOffset)
}
}
}
return signal
} else {
return .single(([], 0, nil))
}
}
|> deliverOn(self.queue)).start(next: { [weak self] peers, updatedCount, nextOffset in
guard let strongSelf = self else {
return
}
if strongSelf.populateCache {
strongSelf.populateCache = false
strongSelf.results.removeAll()
}
var existingIds = Set(strongSelf.results.map { $0.peerId })
for peer in peers {
if !existingIds.contains(peer.peerId) {
strongSelf.results.append(peer)
existingIds.insert(peer.peerId)
}
}
strongSelf.isLoadingMore = false
strongSelf.hasLoadedOnce = true
strongSelf.canLoadMore = nextOffset != nil
strongSelf.nextOffset = nextOffset
if strongSelf.canLoadMore {
strongSelf.count = max(updatedCount, strongSelf.results.count)
} else {
strongSelf.count = strongSelf.results.count
}
strongSelf.updateState()
}))
self.updateState()
}
func updateState() {
self.state.set(.single(PollResultsOptionState(peers: self.results, isLoadingMore: self.isLoadingMore, hasLoadedOnce: self.hasLoadedOnce, canLoadMore: self.canLoadMore, count: self.count)))
}
}
public struct PollResultsOptionState: Equatable {
public var peers: [RenderedPeer]
public var isLoadingMore: Bool
public var hasLoadedOnce: Bool
public var canLoadMore: Bool
public var count: Int
}
public struct PollResultsState: Equatable {
public var options: [Data: PollResultsOptionState]
}
private final class PollResultsContextImpl {
private let queue: Queue
private var optionContexts: [Data: PollResultsOptionContext] = [:]
let state = Promise<PollResultsState>()
init(queue: Queue, account: Account, messageId: MessageId, poll: TelegramMediaPoll) {
self.queue = queue
for option in poll.options {
var count = 0
if let voters = poll.results.voters {
for voter in voters {
if voter.opaqueIdentifier == option.opaqueIdentifier {
count = Int(voter.count)
}
}
}
self.optionContexts[option.opaqueIdentifier] = PollResultsOptionContext(queue: self.queue, account: account, pollId: poll.pollId, messageId: messageId, opaqueIdentifier: option.opaqueIdentifier, count: count)
}
self.state.set(combineLatest(queue: self.queue, self.optionContexts.map { (opaqueIdentifier, context) -> Signal<(Data, PollResultsOptionState), NoError> in
return context.state.get()
|> map { state -> (Data, PollResultsOptionState) in
return (opaqueIdentifier, state)
}
})
|> map { states -> PollResultsState in
var options: [Data: PollResultsOptionState] = [:]
for (opaqueIdentifier, state) in states {
options[opaqueIdentifier] = state
}
return PollResultsState(options: options)
})
for (_, context) in self.optionContexts {
context.loadMore()
}
}
func loadMore(optionOpaqueIdentifier: Data) {
self.optionContexts[optionOpaqueIdentifier]?.loadMore()
}
}
public final class PollResultsContext {
private let queue: Queue = Queue()
private let impl: QueueLocalObject<PollResultsContextImpl>
public var state: Signal<PollResultsState, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.state.get().start(next: { value in
subscriber.putNext(value)
}))
}
return disposable
}
}
init(account: Account, messageId: MessageId, poll: TelegramMediaPoll) {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return PollResultsContextImpl(queue: queue, account: account, messageId: messageId, poll: poll)
})
}
public func loadMore(optionOpaqueIdentifier: Data) {
self.impl.with { impl in
impl.loadMore(optionOpaqueIdentifier: optionOpaqueIdentifier)
}
}
}
@@ -0,0 +1,70 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public struct PreparedInlineMessage: Equatable {
public let botId: EnginePeer.Id
public let queryId: Int64
public let result: ChatContextResult
public let peerTypes: ReplyMarkupButtonAction.PeerTypes
}
func _internal_getPreparedInlineMessage(account: Account, botId: EnginePeer.Id, id: String) -> Signal<PreparedInlineMessage?, NoError> {
return account.postbox.transaction { transaction -> Api.InputUser? in
return transaction.getPeer(botId).flatMap(apiInputUser)
}
|> mapToSignal { inputBot -> Signal<PreparedInlineMessage?, NoError> in
guard let inputBot else {
return .single(nil)
}
return account.network.request(Api.functions.messages.getPreparedInlineMessage(bot: inputBot, id: id))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.PreparedInlineMessage?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<PreparedInlineMessage?, NoError> in
guard let result else {
return .single(nil)
}
return account.postbox.transaction { transaction -> PreparedInlineMessage? in
switch result {
case let .preparedInlineMessage(queryId, result, apiPeerTypes, cacheTime, users):
updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: AccumulatedPeers(users: users))
let _ = cacheTime
return PreparedInlineMessage(
botId: botId,
queryId: queryId,
result: ChatContextResult(apiResult: result, queryId: queryId),
peerTypes: ReplyMarkupButtonAction.PeerTypes(apiType: apiPeerTypes)
)
}
}
}
}
}
func _internal_checkBotDownload(account: Account, botId: EnginePeer.Id, fileName: String, url: String) -> Signal<Bool, NoError> {
return account.postbox.transaction { transaction -> Api.InputUser? in
return transaction.getPeer(botId).flatMap(apiInputUser)
}
|> mapToSignal { inputBot -> Signal<Bool, NoError> in
guard let inputBot else {
return .single(false)
}
return account.network.request(Api.functions.bots.checkDownloadFileParams(bot: inputBot, fileName: fileName, url: url))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> map { value in
switch value {
case .boolTrue:
return true
case .boolFalse:
return false
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,57 @@
import Foundation
import Postbox
import SwiftSignalKit
private struct RecentHashtagItemId {
public let rawValue: MemoryBuffer
var value: String {
return String(data: self.rawValue.makeData(), encoding: .utf8) ?? ""
}
init(_ rawValue: MemoryBuffer) {
self.rawValue = rawValue
}
init?(_ value: String) {
if let data = value.data(using: .utf8) {
self.rawValue = MemoryBuffer(data: data)
} else {
return nil
}
}
}
func addRecentlyUsedHashtag(transaction: Transaction, string: String) {
if let itemId = RecentHashtagItemId(string) {
if let entry = CodableEntry(RecentHashtagItem()) {
transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.RecentlyUsedHashtags, item: OrderedItemListEntry(id: itemId.rawValue, contents: entry), removeTailIfCountExceeds: 100)
}
}
}
func _internal_removeRecentlyUsedHashtag(postbox: Postbox, string: String) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
if let itemId = RecentHashtagItemId(string) {
transaction.removeOrderedItemListItem(collectionId: Namespaces.OrderedItemList.RecentlyUsedHashtags, itemId: itemId.rawValue)
}
}
}
func _internal_recentlyUsedHashtags(postbox: Postbox) -> Signal<[String], NoError> {
return postbox.combinedView(keys: [.orderedItemList(id: Namespaces.OrderedItemList.RecentlyUsedHashtags)])
|> mapToSignal { view -> Signal<[String], NoError> in
return postbox.transaction { transaction -> [String] in
var result: [String] = []
if let view = view.views[.orderedItemList(id: Namespaces.OrderedItemList.RecentlyUsedHashtags)] as? OrderedItemListView {
for item in view.items {
let value = RecentHashtagItemId(item.id).value
result.append(value)
}
}
return result
}
}
}
@@ -0,0 +1,988 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
private struct DiscussionMessage {
var messageId: MessageId
var channelMessageId: MessageId?
var isChannelPost: Bool
var isForumPost: Bool
var isMonoforumPost: Bool
var maxMessage: MessageId?
var maxReadIncomingMessageId: MessageId?
var maxReadOutgoingMessageId: MessageId?
var unreadCount: Int
}
private class ReplyThreadHistoryContextImpl {
private let queue: Queue
private let account: Account
private let peerId: PeerId
private let threadId: Int64
private var currentHole: (MessageHistoryHolesViewEntry, Disposable)?
struct State: Equatable {
var messageId: MessageId
var holeIndices: [MessageId.Namespace: IndexSet]
var maxReadIncomingMessageId: MessageId?
var maxReadOutgoingMessageId: MessageId?
}
let state = Promise<State>()
private var stateValue: State? {
didSet {
if let stateValue = self.stateValue {
if stateValue != oldValue {
self.state.set(.single(stateValue))
}
}
}
}
let maxReadOutgoingMessageId = Promise<MessageId?>()
private var maxReadOutgoingMessageIdValue: MessageId? {
didSet {
if self.maxReadOutgoingMessageIdValue != oldValue {
self.maxReadOutgoingMessageId.set(.single(self.maxReadOutgoingMessageIdValue))
}
}
}
private var maxReadIncomingMessageIdValue: MessageId?
let unreadCount = Promise<Int>()
private var unreadCountValue: Int = 0 {
didSet {
if self.unreadCountValue != oldValue {
self.unreadCount.set(.single(self.unreadCountValue))
}
}
}
private var initialStateDisposable: Disposable?
private var holesDisposable: Disposable?
private var readStateDisposable: Disposable?
private var updateInitialStateDisposable: Disposable?
private let readDisposable = MetaDisposable()
init(queue: Queue, account: Account, data: ChatReplyThreadMessage) {
self.queue = queue
self.account = account
self.peerId = data.peerId
self.threadId = data.threadId
let referencedMessageId = MessageId(peerId: data.peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: data.threadId))
self.maxReadOutgoingMessageIdValue = data.maxReadOutgoingMessageId
self.maxReadOutgoingMessageId.set(.single(self.maxReadOutgoingMessageIdValue))
self.maxReadIncomingMessageIdValue = data.maxReadIncomingMessageId
self.unreadCountValue = data.unreadCount
self.unreadCount.set(.single(self.unreadCountValue))
self.initialStateDisposable = (account.postbox.transaction { transaction -> State in
var indices = transaction.getThreadIndexHoles(peerId: data.peerId, threadId: data.threadId, namespace: Namespaces.Message.Cloud)
indices.subtract(data.initialFilledHoles)
if let maxMessageId = data.maxMessage {
indices.remove(integersIn: Int(maxMessageId.id + 1) ..< Int(Int32.max))
} else {
indices.removeAll()
}
return State(messageId: referencedMessageId, holeIndices: [Namespaces.Message.Cloud: indices], maxReadIncomingMessageId: data.maxReadIncomingMessageId, maxReadOutgoingMessageId: data.maxReadOutgoingMessageId)
}
|> deliverOn(self.queue)).start(next: { [weak self] state in
guard let strongSelf = self else {
return
}
strongSelf.stateValue = state
strongSelf.state.set(.single(state))
})
let threadId = self.threadId
self.holesDisposable = (account.postbox.messageHistoryHolesView()
|> map { view -> MessageHistoryHolesViewEntry? in
for entry in view.entries {
switch entry.hole {
case let .peer(hole):
if hole.threadId == threadId {
return entry
}
}
}
return nil
}
|> distinctUntilChanged
|> deliverOn(self.queue)).start(next: { [weak self] entry in
guard let strongSelf = self else {
return
}
strongSelf.setCurrentHole(entry: entry)
})
self.readStateDisposable = (account.stateManager.threadReadStateUpdates
|> deliverOn(self.queue)).start(next: { [weak self] (_, outgoing) in
guard let strongSelf = self else {
return
}
if let value = outgoing[PeerAndBoundThreadId(peerId: referencedMessageId.peerId, threadId: Int64(referencedMessageId.id))] {
strongSelf.maxReadOutgoingMessageIdValue = MessageId(peerId: data.peerId, namespace: Namespaces.Message.Cloud, id: value)
}
})
let accountPeerId = account.peerId
let updateInitialState: Signal<DiscussionMessage, FetchChannelReplyThreadMessageError> = account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(data.peerId)
}
|> castError(FetchChannelReplyThreadMessageError.self)
|> mapToSignal { peer -> Signal<DiscussionMessage, FetchChannelReplyThreadMessageError> in
guard let peer = peer else {
return .fail(.generic)
}
guard let inputPeer = apiInputPeer(peer) else {
return .fail(.generic)
}
return account.network.request(Api.functions.messages.getDiscussionMessage(peer: inputPeer, msgId: Int32(clamping: data.threadId)))
|> mapError { _ -> FetchChannelReplyThreadMessageError in
return .generic
}
|> mapToSignal { discussionMessage -> Signal<DiscussionMessage, FetchChannelReplyThreadMessageError> in
return account.postbox.transaction { transaction -> Signal<DiscussionMessage, FetchChannelReplyThreadMessageError> in
switch discussionMessage {
case let .discussionMessage(_, messages, maxId, readInboxMaxId, readOutboxMaxId, unreadCount, chats, users):
let parsedMessages = messages.compactMap { message -> StoreMessage? in
StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peer.isForumOrMonoForum)
}
guard let topMessage = parsedMessages.last, let parsedIndex = topMessage.index else {
return .fail(.generic)
}
var channelMessageId: MessageId?
var replyThreadAttribute: ReplyThreadMessageAttribute?
for attribute in topMessage.attributes {
if let attribute = attribute as? SourceReferenceMessageAttribute {
channelMessageId = attribute.messageId
} else if let attribute = attribute as? ReplyThreadMessageAttribute {
replyThreadAttribute = attribute
}
}
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
let _ = transaction.addMessages(parsedMessages, location: .Random)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
let resolvedMaxMessage: MessageId?
if let maxId = maxId {
resolvedMaxMessage = MessageId(
peerId: parsedIndex.id.peerId,
namespace: Namespaces.Message.Cloud,
id: maxId
)
} else {
resolvedMaxMessage = nil
}
var isChannelPost = false
for attribute in topMessage.attributes {
if let _ = attribute as? SourceReferenceMessageAttribute {
isChannelPost = true
break
}
}
let maxReadIncomingMessageId = readInboxMaxId.flatMap { readMaxId in
MessageId(peerId: parsedIndex.id.peerId, namespace: Namespaces.Message.Cloud, id: readMaxId)
}
if let channelMessageId = channelMessageId, let replyThreadAttribute = replyThreadAttribute {
account.viewTracker.updateReplyInfoForMessageId(channelMessageId, info: AccountViewTracker.UpdatedMessageReplyInfo(
timestamp: Int32(CFAbsoluteTimeGetCurrent()),
commentsPeerId: parsedIndex.id.peerId,
maxReadIncomingMessageId: maxReadIncomingMessageId,
maxMessageId: resolvedMaxMessage
))
transaction.updateMessage(channelMessageId, update: { currentMessage in
var attributes = currentMessage.attributes
loop: for j in 0 ..< attributes.count {
if let attribute = attributes[j] as? ReplyThreadMessageAttribute {
attributes[j] = ReplyThreadMessageAttribute(
count: replyThreadAttribute.count,
latestUsers: attribute.latestUsers,
commentsPeerId: attribute.commentsPeerId,
maxMessageId: replyThreadAttribute.maxMessageId,
maxReadMessageId: replyThreadAttribute.maxReadMessageId
)
}
}
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init), authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
}
var isForumPost = false
var isMonoforumPost = false
if let channel = transaction.getPeer(parsedIndex.id.peerId) as? TelegramChannel {
if channel.isForumOrMonoForum {
isForumPost = true
}
if channel.isMonoForum {
isMonoforumPost = true
}
} else if let user = transaction.getPeer(parsedIndex.id.peerId) as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.hasForum) {
isForumPost = true
}
return .single(DiscussionMessage(
messageId: parsedIndex.id,
channelMessageId: channelMessageId,
isChannelPost: isChannelPost,
isForumPost: isForumPost,
isMonoforumPost: isMonoforumPost,
maxMessage: resolvedMaxMessage,
maxReadIncomingMessageId: maxReadIncomingMessageId,
maxReadOutgoingMessageId: readOutboxMaxId.flatMap { readMaxId in
MessageId(peerId: parsedIndex.id.peerId, namespace: Namespaces.Message.Cloud, id: readMaxId)
},
unreadCount: Int(unreadCount)
))
}
}
|> castError(FetchChannelReplyThreadMessageError.self)
|> switchToLatest
}
}
self.updateInitialStateDisposable = (updateInitialState
|> deliverOnMainQueue).start(next: { [weak self] updatedData in
guard let strongSelf = self else {
return
}
if let maxReadOutgoingMessageId = updatedData.maxReadOutgoingMessageId {
if let current = strongSelf.maxReadOutgoingMessageIdValue {
if maxReadOutgoingMessageId > current {
strongSelf.maxReadOutgoingMessageIdValue = maxReadOutgoingMessageId
}
} else {
strongSelf.maxReadOutgoingMessageIdValue = maxReadOutgoingMessageId
}
}
})
}
deinit {
self.initialStateDisposable?.dispose()
self.holesDisposable?.dispose()
self.readDisposable.dispose()
self.updateInitialStateDisposable?.dispose()
}
func setCurrentHole(entry: MessageHistoryHolesViewEntry?) {
if self.currentHole?.0 != entry {
self.currentHole?.1.dispose()
if let entry = entry {
self.currentHole = (entry, self.fetchHole(entry: entry).start(next: { [weak self] removedHoleIndices in
guard let strongSelf = self, let removedHoleIndices = removedHoleIndices else {
return
}
if var currentHoles = strongSelf.stateValue?.holeIndices[Namespaces.Message.Cloud] {
currentHoles.subtract(removedHoleIndices.removedIndices)
strongSelf.stateValue?.holeIndices[Namespaces.Message.Cloud] = currentHoles
}
}))
} else {
self.currentHole = nil
}
}
}
private func fetchHole(entry: MessageHistoryHolesViewEntry) -> Signal<FetchMessageHistoryHoleResult?, NoError> {
switch entry.hole {
case let .peer(hole):
let fetchCount = min(entry.count, 100)
return fetchMessageHistoryHole(accountPeerId: self.account.peerId, source: .network(self.account.network), postbox: self.account.postbox, peerInput: .direct(peerId: hole.peerId, threadId: hole.threadId), namespace: hole.namespace, direction: entry.direction, space: entry.space, count: fetchCount)
}
}
func applyMaxReadIndex(messageIndex: MessageIndex) {
let peerId = self.peerId
let threadId = self.threadId
if messageIndex.id.namespace != Namespaces.Message.Cloud {
return
}
let fromIdExclusive: Int32?
let toIndex = messageIndex
if let maxReadIncomingMessageId = self.maxReadIncomingMessageIdValue {
fromIdExclusive = maxReadIncomingMessageId.id
} else {
fromIdExclusive = nil
}
self.maxReadIncomingMessageIdValue = messageIndex.id
let account = self.account
let _ = (self.account.postbox.transaction { transaction -> (Api.InputPeer?, Api.InputPeer?, MessageId?, Int?) in
guard let peer = transaction.getPeer(peerId) else {
return (nil, nil, nil, nil)
}
var markMainAsRead = false
markMainAsRead = !"".isEmpty
if var data = transaction.getMessageHistoryThreadInfo(peerId: peerId, threadId: threadId)?.data.get(MessageHistoryThreadData.self) {
if messageIndex.id.id >= data.maxIncomingReadId {
if let count = transaction.getThreadMessageCount(peerId: peerId, threadId: threadId, namespace: Namespaces.Message.Cloud, fromIdExclusive: data.maxIncomingReadId, toIndex: messageIndex) {
data.incomingUnreadCount = max(0, data.incomingUnreadCount - Int32(count))
data.maxIncomingReadId = messageIndex.id.id
}
if let topMessageIndex = transaction.getMessageHistoryThreadTopMessage(peerId: peerId, threadId: threadId, namespaces: Set([Namespaces.Message.Cloud])) {
if messageIndex.id.id >= topMessageIndex.id.id {
let containingHole = transaction.getThreadIndexHole(peerId: peerId, threadId: threadId, namespace: topMessageIndex.id.namespace, containing: topMessageIndex.id.id)
if let _ = containingHole[.everywhere] {
} else {
data.incomingUnreadCount = 0
}
}
}
data.maxKnownMessageId = max(data.maxKnownMessageId, messageIndex.id.id)
if let entry = StoredMessageHistoryThreadInfo(data) {
transaction.setMessageHistoryThreadInfo(peerId: peerId, threadId: threadId, info: entry)
}
}
}
if markMainAsRead {
_internal_applyMaxReadIndexInteractively(transaction: transaction, stateManager: account.stateManager, index: messageIndex)
}
var subPeerId: Api.InputPeer?
if let channel = peer as? TelegramChannel, channel.flags.contains(.isMonoforum) {
subPeerId = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer)
} else {
let referencedMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId))
if let message = transaction.getMessage(referencedMessageId) {
for attribute in message.attributes {
if let attribute = attribute as? SourceReferenceMessageAttribute {
if let sourceMessage = transaction.getMessage(attribute.messageId) {
account.viewTracker.applyMaxReadIncomingMessageIdForReplyInfo(id: attribute.messageId, maxReadIncomingMessageId: messageIndex.id)
var updatedAttribute: ReplyThreadMessageAttribute?
for i in 0 ..< sourceMessage.attributes.count {
if let attribute = sourceMessage.attributes[i] as? ReplyThreadMessageAttribute {
if let maxReadMessageId = attribute.maxReadMessageId {
if maxReadMessageId < messageIndex.id.id {
updatedAttribute = ReplyThreadMessageAttribute(count: attribute.count, latestUsers: attribute.latestUsers, commentsPeerId: attribute.commentsPeerId, maxMessageId: attribute.maxMessageId, maxReadMessageId: messageIndex.id.id)
}
} else {
updatedAttribute = ReplyThreadMessageAttribute(count: attribute.count, latestUsers: attribute.latestUsers, commentsPeerId: attribute.commentsPeerId, maxMessageId: attribute.maxMessageId, maxReadMessageId: messageIndex.id.id)
}
break
}
}
if let updatedAttribute = updatedAttribute {
transaction.updateMessage(sourceMessage.id, update: { currentMessage in
var attributes = currentMessage.attributes
loop: for j in 0 ..< attributes.count {
if let _ = attributes[j] as? ReplyThreadMessageAttribute {
attributes[j] = updatedAttribute
}
}
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init), authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
}
}
break
}
}
}
}
let inputPeer = transaction.getPeer(messageIndex.id.peerId).flatMap(apiInputPeer)
let readCount = transaction.getThreadMessageCount(peerId: peerId, threadId: threadId, namespace: Namespaces.Message.Cloud, fromIdExclusive: fromIdExclusive, toIndex: toIndex)
let topMessageId = transaction.getMessagesWithThreadId(peerId: peerId, namespace: Namespaces.Message.Cloud, threadId: threadId, from: MessageIndex.upperBound(peerId: peerId, namespace: Namespaces.Message.Cloud), includeFrom: false, to: MessageIndex.lowerBound(peerId: peerId, namespace: Namespaces.Message.Cloud), limit: 1).first?.id
return (inputPeer, subPeerId, topMessageId, readCount)
}
|> deliverOnMainQueue).start(next: { [weak self] inputPeer, subPeerId, topMessageId, readCount in
guard let strongSelf = self else {
return
}
guard let inputPeer else {
return
}
var revalidate = false
var unreadCountValue = strongSelf.unreadCountValue
if let readCount = readCount {
unreadCountValue = max(0, unreadCountValue - Int(readCount))
} else {
revalidate = true
}
if let topMessageId = topMessageId {
if topMessageId.id <= messageIndex.id.id {
unreadCountValue = 0
}
}
strongSelf.unreadCountValue = unreadCountValue
if let state = strongSelf.stateValue {
if let indices = state.holeIndices[messageIndex.id.namespace] {
let fromIdInt: Int
if let fromIdExclusive = fromIdExclusive {
fromIdInt = Int(fromIdExclusive + 1)
} else {
fromIdInt = 1
}
let toIdInt = Int(toIndex.id.id)
if fromIdInt <= toIdInt, indices.intersects(integersIn: fromIdInt ..< toIdInt) {
revalidate = true
}
}
}
if let subPeerId {
let signal = strongSelf.account.network.request(Api.functions.messages.readSavedHistory(parentPeer: inputPeer, peer: subPeerId, maxId: messageIndex.id.id))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> ignoreValues
if revalidate {
}
strongSelf.readDisposable.set(signal.start())
} else {
var signal = strongSelf.account.network.request(Api.functions.messages.readDiscussion(peer: inputPeer, msgId: Int32(clamping: threadId), readMaxId: messageIndex.id.id))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> ignoreValues
if revalidate {
let validateSignal = strongSelf.account.network.request(Api.functions.messages.getDiscussionMessage(peer: inputPeer, msgId: Int32(clamping: threadId)))
|> map { result -> (MessageId?, Int) in
switch result {
case let .discussionMessage(_, _, _, readInboxMaxId, _, unreadCount, _, _):
return (readInboxMaxId.flatMap({ MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) }), Int(unreadCount))
}
}
|> `catch` { _ -> Signal<(MessageId?, Int)?, NoError> in
return .single(nil)
}
|> afterNext { result in
guard let (incomingMessageId, count) = result else {
return
}
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
strongSelf.maxReadIncomingMessageIdValue = incomingMessageId
strongSelf.unreadCountValue = count
}
}
|> ignoreValues
signal = signal
|> then(validateSignal)
}
strongSelf.readDisposable.set(signal.start())
}
})
}
}
public class ReplyThreadHistoryContext {
fileprivate final class GuardReference {
private let deallocated: () -> Void
init(deallocated: @escaping () -> Void) {
self.deallocated = deallocated
}
deinit {
self.deallocated()
}
}
private let queue = Queue()
private let impl: QueueLocalObject<ReplyThreadHistoryContextImpl>
public var state: Signal<MessageHistoryViewExternalInput, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
let stateDisposable = impl.state.get().start(next: { state in
subscriber.putNext(MessageHistoryViewExternalInput(
content: .thread(peerId: state.messageId.peerId, id: Int64(state.messageId.id), holes: state.holeIndices),
maxReadIncomingMessageId: state.maxReadIncomingMessageId,
maxReadOutgoingMessageId: state.maxReadOutgoingMessageId
))
})
disposable.set(stateDisposable)
}
return disposable
}
}
public var maxReadOutgoingMessageId: Signal<MessageId?, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.maxReadOutgoingMessageId.get().start(next: { value in
subscriber.putNext(value)
}))
}
return disposable
}
}
public var unreadCount: Signal<Int, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.unreadCount.get().start(next: { value in
subscriber.putNext(value)
}))
}
return disposable
}
}
public init(account: Account, peerId: PeerId, data: ChatReplyThreadMessage) {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return ReplyThreadHistoryContextImpl(queue: queue, account: account, data: data)
})
}
public func applyMaxReadIndex(messageIndex: MessageIndex) {
self.impl.with { impl in
impl.applyMaxReadIndex(messageIndex: messageIndex)
}
}
}
public struct ChatReplyThreadMessage: Equatable {
public enum Anchor: Equatable {
case automatic
case lowerBoundMessage(MessageIndex)
}
public var peerId: PeerId
public var threadId: Int64
public var channelMessageId: MessageId?
public var isChannelPost: Bool
public var isForumPost: Bool
public var isMonoforumPost: Bool
public var maxMessage: MessageId?
public var maxReadIncomingMessageId: MessageId?
public var maxReadOutgoingMessageId: MessageId?
public var unreadCount: Int
public var initialFilledHoles: IndexSet
public var initialAnchor: Anchor
public var isNotAvailable: Bool
public var effectiveMessageId: MessageId? {
if self.peerId.namespace == Namespaces.Peer.CloudChannel || self.isForumPost {
return MessageId(peerId: self.peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: self.threadId))
} else {
return nil
}
}
public init(peerId: PeerId, threadId: Int64, channelMessageId: MessageId?, isChannelPost: Bool, isForumPost: Bool, isMonoforumPost: Bool, maxMessage: MessageId?, maxReadIncomingMessageId: MessageId?, maxReadOutgoingMessageId: MessageId?, unreadCount: Int, initialFilledHoles: IndexSet, initialAnchor: Anchor, isNotAvailable: Bool) {
self.peerId = peerId
self.threadId = threadId
self.channelMessageId = channelMessageId
self.isChannelPost = isChannelPost
self.isForumPost = isForumPost
self.isMonoforumPost = isMonoforumPost
self.maxMessage = maxMessage
self.maxReadIncomingMessageId = maxReadIncomingMessageId
self.maxReadOutgoingMessageId = maxReadOutgoingMessageId
self.unreadCount = unreadCount
self.initialFilledHoles = initialFilledHoles
self.initialAnchor = initialAnchor
self.isNotAvailable = isNotAvailable
}
public var normalized: ChatReplyThreadMessage {
if self.isForumPost {
return ChatReplyThreadMessage(peerId: self.peerId, threadId: self.threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, isMonoforumPost: self.isMonoforumPost, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)
} else {
return self
}
}
}
public enum FetchChannelReplyThreadMessageError {
case generic
}
func _internal_fetchChannelReplyThreadMessage(account: Account, messageId: MessageId, atMessageId: MessageId?) -> Signal<ChatReplyThreadMessage, FetchChannelReplyThreadMessageError> {
let accountPeerId = account.peerId
return account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(messageId.peerId)
}
|> castError(FetchChannelReplyThreadMessageError.self)
|> mapToSignal { peer -> Signal<ChatReplyThreadMessage, FetchChannelReplyThreadMessageError> in
guard let peer = peer else {
return .fail(.generic)
}
guard let inputPeer = apiInputPeer(peer) else {
return .fail(.generic)
}
let replyInfo = Promise<MessageHistoryThreadData?>()
replyInfo.set(account.postbox.transaction { transaction -> MessageHistoryThreadData? in
return transaction.getMessageHistoryThreadInfo(peerId: messageId.peerId, threadId: Int64(messageId.id))?.data.get(MessageHistoryThreadData.self)
})
let remoteDiscussionMessageSignal: Signal<DiscussionMessage?, NoError> = account.network.request(Api.functions.messages.getDiscussionMessage(peer: inputPeer, msgId: messageId.id))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.DiscussionMessage?, NoError> in
return .single(nil)
}
|> mapToSignal { discussionMessage -> Signal<DiscussionMessage?, NoError> in
guard let discussionMessage = discussionMessage else {
return .single(nil)
}
return account.postbox.transaction { transaction -> DiscussionMessage? in
switch discussionMessage {
case let .discussionMessage(_, messages, maxId, readInboxMaxId, readOutboxMaxId, unreadCount, chats, users):
let parsedMessages = messages.compactMap { message -> StoreMessage? in
StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peer.isForumOrMonoForum)
}
guard let topMessage = parsedMessages.last, let parsedIndex = topMessage.index else {
return nil
}
var channelMessageId: MessageId?
for attribute in topMessage.attributes {
if let attribute = attribute as? SourceReferenceMessageAttribute {
channelMessageId = attribute.messageId
break
}
}
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
let _ = transaction.addMessages(parsedMessages, location: .Random)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
let resolvedMaxMessage: MessageId?
if let maxId = maxId {
resolvedMaxMessage = MessageId(
peerId: parsedIndex.id.peerId,
namespace: Namespaces.Message.Cloud,
id: maxId
)
} else {
resolvedMaxMessage = nil
}
var isChannelPost = false
for attribute in topMessage.attributes {
if let _ = attribute as? SourceReferenceMessageAttribute {
isChannelPost = true
break
}
}
var isForumPost = false
var isMonoforumPost = false
if let channel = transaction.getPeer(parsedIndex.id.peerId) as? TelegramChannel {
if channel.isForumOrMonoForum {
isForumPost = true
}
if channel.isMonoForum {
isMonoforumPost = true
}
} else if let user = transaction.getPeer(parsedIndex.id.peerId) as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.hasForum) {
isForumPost = true
}
return DiscussionMessage(
messageId: parsedIndex.id,
channelMessageId: channelMessageId,
isChannelPost: isChannelPost,
isForumPost: isForumPost,
isMonoforumPost: isMonoforumPost,
maxMessage: resolvedMaxMessage,
maxReadIncomingMessageId: readInboxMaxId.flatMap { readMaxId in
MessageId(peerId: parsedIndex.id.peerId, namespace: Namespaces.Message.Cloud, id: readMaxId)
},
maxReadOutgoingMessageId: readOutboxMaxId.flatMap { readMaxId in
MessageId(peerId: parsedIndex.id.peerId, namespace: Namespaces.Message.Cloud, id: readMaxId)
},
unreadCount: Int(unreadCount)
)
}
}
}
let discussionMessageSignal = (replyInfo.get()
|> take(1)
|> mapToSignal { threadData -> Signal<DiscussionMessage?, NoError> in
guard let threadData = threadData else {
return .single(nil)
}
return account.postbox.transaction { transaction -> DiscussionMessage? in
return DiscussionMessage(
messageId: messageId,
channelMessageId: nil,
isChannelPost: false,
isForumPost: true,
isMonoforumPost: peer.isMonoForum,
maxMessage: MessageId(peerId: messageId.peerId, namespace: messageId.namespace, id: threadData.maxKnownMessageId),
maxReadIncomingMessageId: MessageId(peerId: messageId.peerId, namespace: messageId.namespace, id: threadData.maxIncomingReadId),
maxReadOutgoingMessageId: MessageId(peerId: messageId.peerId, namespace: messageId.namespace, id: threadData.maxOutgoingReadId),
unreadCount: Int(threadData.incomingUnreadCount)
)
/*var foundDiscussionMessageId: MessageId?
transaction.scanMessageAttributes(peerId: replyInfo.commentsPeerId, namespace: Namespaces.Message.Cloud, limit: 1000, { id, attributes in
for attribute in attributes {
if let attribute = attribute as? SourceReferenceMessageAttribute {
if attribute.messageId == messageId {
foundDiscussionMessageId = id
return true
}
}
}
if foundDiscussionMessageId != nil {
return false
}
return true
})
guard let discussionMessageId = foundDiscussionMessageId else {
return nil
}
return DiscussionMessage(
messageId: discussionMessageId,
channelMessageId: messageId,
isChannelPost: true,
maxMessage: replyInfo.maxMessageId,
maxReadIncomingMessageId: replyInfo.maxReadIncomingMessageId,
maxReadOutgoingMessageId: nil,
unreadCount: 0
)*/
}
})
|> mapToSignal { result -> Signal<DiscussionMessage?, NoError> in
if let result = result {
return .single(result)
} else {
return remoteDiscussionMessageSignal
}
}
let discussionMessage = Promise<DiscussionMessage?>()
discussionMessage.set(discussionMessageSignal)
enum Anchor {
case message(MessageId)
case lowerBound
case upperBound
}
let preloadedHistoryPosition: Signal<(FetchMessageHistoryHoleThreadInput, PeerId, MessageId?, Anchor, MessageId?), FetchChannelReplyThreadMessageError> = replyInfo.get()
|> take(1)
|> castError(FetchChannelReplyThreadMessageError.self)
|> mapToSignal { threadData -> Signal<(FetchMessageHistoryHoleThreadInput, PeerId, MessageId?, Anchor, MessageId?), FetchChannelReplyThreadMessageError> in
if let _ = threadData, !"".isEmpty {
return .fail(.generic)
} else {
return discussionMessage.get()
|> take(1)
|> castError(FetchChannelReplyThreadMessageError.self)
|> mapToSignal { discussionMessage -> Signal<(FetchMessageHistoryHoleThreadInput, PeerId, MessageId?, Anchor, MessageId?), FetchChannelReplyThreadMessageError> in
guard let discussionMessage = discussionMessage else {
return .fail(.generic)
}
let topMessageId = discussionMessage.messageId
let commentsPeerId = topMessageId.peerId
let anchor: Anchor
if let atMessageId = atMessageId {
anchor = .message(atMessageId)
} else if let maxReadIncomingMessageId = discussionMessage.maxReadIncomingMessageId {
anchor = .message(maxReadIncomingMessageId)
} else {
anchor = .lowerBound
}
return .single((.direct(peerId: commentsPeerId, threadId: Int64(topMessageId.id)), commentsPeerId, discussionMessage.messageId, anchor, discussionMessage.maxMessage))
}
}
}
let preloadedHistory: Signal<(FetchMessageHistoryHoleResult?, ChatReplyThreadMessage.Anchor), FetchChannelReplyThreadMessageError>
preloadedHistory = preloadedHistoryPosition
|> mapToSignal { peerInput, commentsPeerId, threadMessageId, anchor, maxMessageId -> Signal<(FetchMessageHistoryHoleResult?, ChatReplyThreadMessage.Anchor), FetchChannelReplyThreadMessageError> in
guard let maxMessageId = maxMessageId else {
return .single((FetchMessageHistoryHoleResult(removedIndices: IndexSet(integersIn: 1 ..< Int(Int32.max - 1)), strictRemovedIndices: IndexSet(), actualPeerId: nil, actualThreadId: nil, ids: []), .automatic))
}
return account.postbox.transaction { transaction -> Signal<(FetchMessageHistoryHoleResult?, ChatReplyThreadMessage.Anchor), FetchChannelReplyThreadMessageError> in
if let threadMessageId = threadMessageId {
var holes = transaction.getThreadIndexHoles(peerId: threadMessageId.peerId, threadId: Int64(threadMessageId.id), namespace: Namespaces.Message.Cloud)
holes.remove(integersIn: Int(maxMessageId.id + 1) ..< Int(Int32.max))
let isParticipant = transaction.getPeerChatListIndex(commentsPeerId) != nil
if isParticipant {
let historyHoles = transaction.getHoles(peerId: commentsPeerId, namespace: Namespaces.Message.Cloud)
holes.formIntersection(historyHoles)
}
let inputAnchor: HistoryViewInputAnchor
switch anchor {
case .lowerBound:
inputAnchor = .lowerBound
case .upperBound:
inputAnchor = .upperBound
case let .message(id):
inputAnchor = .message(id)
}
let testView = transaction.getMessagesHistoryViewState(
input: .external(MessageHistoryViewExternalInput(
content: .thread(
peerId: commentsPeerId,
id: Int64(threadMessageId.id),
holes: [
Namespaces.Message.Cloud: holes
]
),
maxReadIncomingMessageId: nil,
maxReadOutgoingMessageId: nil
)),
ignoreMessagesInTimestampRange: nil,
ignoreMessageIds: Set(),
count: 40,
clipHoles: true,
anchor: inputAnchor,
namespaces: .not(Namespaces.Message.allNonRegular)
)
if !testView.isLoading || transaction.getMessageHistoryThreadInfo(peerId: threadMessageId.peerId, threadId: Int64(threadMessageId.id)) != nil {
let initialAnchor: ChatReplyThreadMessage.Anchor
switch anchor {
case .lowerBound:
if let entry = testView.entries.first {
initialAnchor = .lowerBoundMessage(entry.index)
} else {
initialAnchor = .automatic
}
case .upperBound:
initialAnchor = .automatic
case .message:
initialAnchor = .automatic
}
return .single((FetchMessageHistoryHoleResult(removedIndices: IndexSet(), strictRemovedIndices: IndexSet(), actualPeerId: nil, actualThreadId: nil, ids: []), initialAnchor))
}
}
let direction: MessageHistoryViewRelativeHoleDirection
switch anchor {
case .lowerBound:
direction = .range(start: MessageId(peerId: commentsPeerId, namespace: Namespaces.Message.Cloud, id: 1), end: MessageId(peerId: commentsPeerId, namespace: Namespaces.Message.Cloud, id: Int32.max - 1))
case .upperBound:
direction = .range(start: MessageId(peerId: commentsPeerId, namespace: Namespaces.Message.Cloud, id: Int32.max - 1), end: MessageId(peerId: commentsPeerId, namespace: Namespaces.Message.Cloud, id: 1))
case let .message(id):
direction = .aroundId(id)
}
return fetchMessageHistoryHole(accountPeerId: account.peerId, source: .network(account.network), postbox: account.postbox, peerInput: peerInput, namespace: Namespaces.Message.Cloud, direction: direction, space: .everywhere, count: 40)
|> castError(FetchChannelReplyThreadMessageError.self)
|> mapToSignal { result -> Signal<(FetchMessageHistoryHoleResult?, ChatReplyThreadMessage.Anchor), FetchChannelReplyThreadMessageError> in
return account.postbox.transaction { transaction -> (FetchMessageHistoryHoleResult?, ChatReplyThreadMessage.Anchor) in
guard let result = result else {
return (nil, .automatic)
}
let initialAnchor: ChatReplyThreadMessage.Anchor
switch anchor {
case .lowerBound:
if let actualPeerId = result.actualPeerId, let actualThreadId = result.actualThreadId {
if let firstMessage = transaction.getMessagesWithThreadId(peerId: actualPeerId, namespace: Namespaces.Message.Cloud, threadId: actualThreadId, from: MessageIndex.lowerBound(peerId: actualPeerId, namespace: Namespaces.Message.Cloud), includeFrom: false, to: MessageIndex.upperBound(peerId: actualPeerId, namespace: Namespaces.Message.Cloud), limit: 1).first {
initialAnchor = .lowerBoundMessage(firstMessage.index)
} else {
initialAnchor = .automatic
}
} else {
initialAnchor = .automatic
}
case .upperBound:
initialAnchor = .automatic
case .message:
initialAnchor = .automatic
}
return (result, initialAnchor)
}
|> castError(FetchChannelReplyThreadMessageError.self)
}
}
|> castError(FetchChannelReplyThreadMessageError.self)
|> switchToLatest
}
return combineLatest(
discussionMessage.get()
|> take(1)
|> castError(FetchChannelReplyThreadMessageError.self),
preloadedHistory
)
|> mapToSignal { discussionMessage, initialFilledHolesAndInitialAnchor -> Signal<ChatReplyThreadMessage, FetchChannelReplyThreadMessageError> in
guard let discussionMessage = discussionMessage else {
return .fail(.generic)
}
let (initialFilledHoles, initialAnchor) = initialFilledHolesAndInitialAnchor
return account.postbox.transaction { transaction -> Signal<ChatReplyThreadMessage, FetchChannelReplyThreadMessageError> in
if let initialFilledHoles = initialFilledHoles {
for range in initialFilledHoles.strictRemovedIndices.rangeView {
transaction.removeHole(peerId: discussionMessage.messageId.peerId, threadId: Int64(discussionMessage.messageId.id), namespace: Namespaces.Message.Cloud, space: .everywhere, range: Int32(range.lowerBound) ... Int32(range.upperBound))
}
}
return .single(ChatReplyThreadMessage(
peerId: discussionMessage.messageId.peerId,
threadId: Int64(discussionMessage.messageId.id),
channelMessageId: discussionMessage.channelMessageId,
isChannelPost: discussionMessage.isChannelPost,
isForumPost: discussionMessage.isForumPost,
isMonoforumPost: discussionMessage.isMonoforumPost,
maxMessage: discussionMessage.maxMessage,
maxReadIncomingMessageId: discussionMessage.maxReadIncomingMessageId,
maxReadOutgoingMessageId: discussionMessage.maxReadOutgoingMessageId,
unreadCount: discussionMessage.unreadCount,
initialFilledHoles: initialFilledHoles?.removedIndices ?? IndexSet(),
initialAnchor: initialAnchor,
isNotAvailable: initialFilledHoles == nil
))
}
|> castError(FetchChannelReplyThreadMessageError.self)
|> switchToLatest
}
}
}
@@ -0,0 +1,45 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
public enum ReportAdMessageResult {
public struct Option: Equatable {
public let text: String
public let option: Data
}
case options(title: String, options: [Option])
case adsHidden
case reported
}
public enum ReportAdMessageError {
case generic
case premiumRequired
}
func _internal_reportAdMessage(account: Account, opaqueId: Data, option: Data?) -> Signal<ReportAdMessageResult, ReportAdMessageError> {
return account.network.request(Api.functions.messages.reportSponsoredMessage(randomId: Buffer(data: opaqueId), option: Buffer(data: option)))
|> mapError { error -> ReportAdMessageError in
if error.errorDescription == "PREMIUM_ACCOUNT_REQUIRED" {
return .premiumRequired
}
return .generic
}
|> map { result -> ReportAdMessageResult in
switch result {
case let .sponsoredMessageReportResultChooseOption(title, options):
return .options(title: title, options: options.map {
switch $0 {
case let .sponsoredMessageReportOption(text, option):
return ReportAdMessageResult.Option(text: text, option: option.makeData())
}
})
case .sponsoredMessageReportResultAdsHidden:
return .adsHidden
case .sponsoredMessageReportResultReported:
return .reported
}
}
}
@@ -0,0 +1,82 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public enum ReportContentResult {
public struct Option: Equatable {
public let text: String
public let option: Data
}
case options(title: String, options: [Option])
case addComment(optional: Bool, option: Data)
case reported
}
public enum ReportContentError {
case generic
case messageIdRequired
}
public enum ReportContentSubject: Equatable {
case peer(EnginePeer.Id)
case messages([EngineMessage.Id])
case stories(EnginePeer.Id, [Int32])
public var peerId: EnginePeer.Id {
switch self {
case let .peer(peerId):
return peerId
case let .messages(messageIds):
return messageIds.first!.peerId
case let .stories(peerId, _):
return peerId
}
}
}
func _internal_reportContent(account: Account, subject: ReportContentSubject, option: Data?, message: String?) -> Signal<ReportContentResult, ReportContentError> {
return account.postbox.transaction { transaction -> Signal<ReportContentResult, ReportContentError> in
guard let peer = transaction.getPeer(subject.peerId), let inputPeer = apiInputPeer(peer) else {
return .fail(.generic)
}
let request: Signal<Api.ReportResult, MTRpcError>
if case let .stories(_, ids) = subject {
request = account.network.request(Api.functions.stories.report(peer: inputPeer, id: ids, option: Buffer(data: option), message: message ?? ""))
} else {
var ids: [Int32] = []
if case let .messages(messageIds) = subject {
ids = messageIds.map { $0.id }
}
request = account.network.request(Api.functions.messages.report(peer: inputPeer, id: ids, option: Buffer(data: option), message: message ?? ""))
}
return request
|> mapError { error -> ReportContentError in
if error.errorDescription == "MESSAGE_ID_REQUIRED" {
return .messageIdRequired
}
return .generic
}
|> map { result -> ReportContentResult in
switch result {
case let .reportResultChooseOption(title, options):
return .options(title: title, options: options.map {
switch $0 {
case let .messageReportOption(text, option):
return ReportContentResult.Option(text: text, option: option.makeData())
}
})
case let .reportResultAddComment(flags, option):
return .addComment(optional: (flags & (1 << 0)) != 0, option: option.makeData())
case .reportResultReported:
return .reported
}
}
}
|> castError(ReportContentError.self)
|> switchToLatest
}
@@ -0,0 +1,35 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
public func _internal_reportMessageDelivery(postbox: Postbox, network: Network, messageIds: [EngineMessage.Id], fromPushNotification: Bool) -> Signal<Bool, NoError> {
var signals: [Signal<Void, NoError>] = []
for (peerId, messageIds) in messagesIdsGroupedByPeerId(messageIds) {
signals.append(_internal_reportMessageDeliveryByPeerId(postbox: postbox, network: network, peerId: peerId, messageIds: messageIds, fromPushNotification: fromPushNotification))
}
return combineLatest(signals)
|> mapToSignal { _ in
return .single(true)
}
}
private func _internal_reportMessageDeliveryByPeerId(postbox: Postbox, network: Network, peerId: EnginePeer.Id, messageIds: [EngineMessage.Id], fromPushNotification: Bool) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
guard let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) else {
return .complete()
}
var flags: Int32 = 0
if fromPushNotification {
flags |= (1 << 0)
}
return network.request(Api.functions.messages.reportMessagesDelivery(flags: flags, peer: inputPeer, id: messageIds.map { $0.id }))
|> `catch` { error -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { _ in
return .complete()
}
}
|> switchToLatest
}
@@ -0,0 +1,167 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public enum RequestChatContextResultsError {
case generic
case locationRequired
}
public final class CachedChatContextResult: Codable {
public let data: Data
public let timestamp: Int32
public init(data: Data, timestamp: Int32) {
self.data = data
self.timestamp = timestamp
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
self.data = try container.decode(Data.self, forKey: "data")
self.timestamp = try container.decode(Int32.self, forKey: "timestamp")
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self)
try container.encode(self.data, forKey: "data")
try container.encode(self.timestamp, forKey: "timestamp")
}
}
private struct RequestData: Codable {
let version: String
let botId: PeerId
let peerId: PeerId
let query: String
}
private let requestVersion = "3"
public struct RequestChatContextResultsResult {
public let results: ChatContextResultCollection
public let isStale: Bool
public init(results: ChatContextResultCollection, isStale: Bool) {
self.results = results
self.isStale = isStale
}
}
func _internal_requestChatContextResults(account: Account, botId: PeerId, peerId: PeerId, query: String, location: Signal<(Double, Double)?, NoError> = .single(nil), offset: String, incompleteResults: Bool = false, staleCachedResults: Bool = false) -> Signal<RequestChatContextResultsResult?, RequestChatContextResultsError> {
return account.postbox.transaction { transaction -> (bot: Peer, peer: Peer)? in
if let bot = transaction.getPeer(botId), let peer = transaction.getPeer(peerId) {
return (bot, peer)
} else {
return nil
}
}
|> mapToSignal { botAndPeer -> Signal<((bot: Peer, peer: Peer)?, (Double, Double)?), NoError> in
if let (bot, _) = botAndPeer, let botUser = bot as? TelegramUser, let botInfo = botUser.botInfo, botInfo.flags.contains(.requiresGeolocationForInlineRequests) {
return location
|> take(1)
|> map { location -> ((bot: Peer, peer: Peer)?, (Double, Double)?) in
return (botAndPeer, location)
}
} else {
return .single((botAndPeer, nil))
}
}
|> castError(RequestChatContextResultsError.self)
|> mapToSignal { botAndPeer, location -> Signal<RequestChatContextResultsResult?, RequestChatContextResultsError> in
guard let (bot, peer) = botAndPeer, let inputBot = apiInputUser(bot) else {
return .single(nil)
}
return account.postbox.transaction { transaction -> Signal<RequestChatContextResultsResult?, RequestChatContextResultsError> in
var staleResult: RequestChatContextResultsResult?
if offset.isEmpty && location == nil {
let requestData = RequestData(version: requestVersion, botId: botId, peerId: peerId, query: query)
if let keyData = try? JSONEncoder().encode(requestData) {
let key = ValueBoxKey(MemoryBuffer(data: keyData))
if let cachedEntry = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedContextResults, key: key))?.get(CachedChatContextResult.self) {
if let cachedResult = try? JSONDecoder().decode(ChatContextResultCollection.self, from: cachedEntry.data) {
let timestamp = Int32(Date().timeIntervalSince1970)
if cachedEntry.timestamp + cachedResult.cacheTimeout > timestamp {
return .single(RequestChatContextResultsResult(results: cachedResult, isStale: false))
} else if staleCachedResults {
let staleCollection = ChatContextResultCollection(
botId: cachedResult.botId,
peerId: cachedResult.peerId,
query: cachedResult.query,
geoPoint: cachedResult.geoPoint,
queryId: cachedResult.queryId,
nextOffset: nil,
presentation: cachedResult.presentation,
switchPeer: cachedResult.switchPeer,
webView: cachedResult.webView,
results: cachedResult.results,
cacheTimeout: 0
)
staleResult = RequestChatContextResultsResult(results: staleCollection, isStale: true)
}
}
}
}
}
var flags: Int32 = 0
var inputPeer: Api.InputPeer = .inputPeerEmpty
var geoPoint: Api.InputGeoPoint?
if let actualInputPeer = apiInputPeer(peer) {
inputPeer = actualInputPeer
}
if let (latitude, longitude) = location {
flags |= (1 << 0)
let geoPointFlags: Int32 = 0
geoPoint = Api.InputGeoPoint.inputGeoPoint(flags: geoPointFlags, lat: latitude, long: longitude, accuracyRadius: nil)
}
var signal: Signal<RequestChatContextResultsResult?, RequestChatContextResultsError> = account.network.request(Api.functions.messages.getInlineBotResults(flags: flags, bot: inputBot, peer: inputPeer, geoPoint: geoPoint, query: query, offset: offset))
|> map { result -> ChatContextResultCollection? in
return ChatContextResultCollection(apiResults: result, botId: bot.id, peerId: peerId, query: query, geoPoint: location)
}
|> mapError { error -> RequestChatContextResultsError in
if error.errorDescription == "BOT_INLINE_GEO_REQUIRED" {
return .locationRequired
} else {
return .generic
}
}
|> mapToSignal { result -> Signal<RequestChatContextResultsResult?, RequestChatContextResultsError> in
guard let result = result else {
return .single(nil)
}
return account.postbox.transaction { transaction -> RequestChatContextResultsResult? in
if result.cacheTimeout > 10, offset.isEmpty && location == nil {
if let resultData = try? JSONEncoder().encode(result) {
let requestData = RequestData(version: requestVersion, botId: botId, peerId: peerId, query: query)
if let keyData = try? JSONEncoder().encode(requestData) {
let key = ValueBoxKey(MemoryBuffer(data: keyData))
if let entry = CodableEntry(CachedChatContextResult(data: resultData, timestamp: Int32(Date().timeIntervalSince1970))) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedContextResults, key: key), entry: entry)
}
}
}
}
return RequestChatContextResultsResult(results: result, isStale: false)
}
|> castError(RequestChatContextResultsError.self)
}
if incompleteResults {
signal = .single(staleResult) |> then(signal)
}
return signal
}
|> castError(RequestChatContextResultsError.self)
|> switchToLatest
}
}
@@ -0,0 +1,267 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public enum MessageActionCallbackResult {
case none
case alert(String)
case toast(String)
case url(String)
}
public enum MessageActionCallbackError {
case generic
case twoStepAuthMissing
case twoStepAuthTooFresh(Int32)
case authSessionTooFresh(Int32)
case limitExceeded
case requestPassword
case invalidPassword
case restricted
case userBlocked
}
func _internal_requestMessageActionCallbackPasswordCheck(account: Account, messageId: MessageId, isGame: Bool, data: MemoryBuffer?) -> Signal<Never, MessageActionCallbackError> {
return account.postbox.loadedPeerWithId(messageId.peerId)
|> castError(MessageActionCallbackError.self)
|> take(1)
|> mapToSignal { peer in
if let inputPeer = apiInputPeer(peer) {
var flags: Int32 = 0
var dataBuffer: Buffer?
if let data = data {
flags |= Int32(1 << 0)
dataBuffer = Buffer(data: data.makeData())
}
if isGame {
flags |= Int32(1 << 1)
}
return account.network.request(Api.functions.messages.getBotCallbackAnswer(flags: flags, peer: inputPeer, msgId: messageId.id, data: dataBuffer, password: .inputCheckPasswordEmpty))
|> mapError { error -> MessageActionCallbackError in
if error.errorDescription == "PASSWORD_HASH_INVALID" {
return .requestPassword
} else if error.errorDescription == "PASSWORD_MISSING" {
return .twoStepAuthMissing
} else if error.errorDescription.hasPrefix("PASSWORD_TOO_FRESH_") {
let timeout = String(error.errorDescription[error.errorDescription.index(error.errorDescription.startIndex, offsetBy: "PASSWORD_TOO_FRESH_".count)...])
if let value = Int32(timeout) {
return .twoStepAuthTooFresh(value)
}
} else if error.errorDescription.hasPrefix("SESSION_TOO_FRESH_") {
let timeout = String(error.errorDescription[error.errorDescription.index(error.errorDescription.startIndex, offsetBy: "SESSION_TOO_FRESH_".count)...])
if let value = Int32(timeout) {
return .authSessionTooFresh(value)
}
} else if error.errorDescription == "USER_PRIVACY_RESTRICTED" {
return .restricted
} else if error.errorDescription == "USER_BLOCKED" {
return .userBlocked
}
return .generic
}
|> mapToSignal { _ -> Signal<Never, MessageActionCallbackError> in
return .complete()
}
} else {
return .fail(.generic)
}
}
}
func _internal_requestMessageActionCallback(account: Account, messageId: MessageId, isGame :Bool, password: String?, data: MemoryBuffer?) -> Signal<MessageActionCallbackResult, MessageActionCallbackError> {
return account.postbox.loadedPeerWithId(messageId.peerId)
|> castError(MessageActionCallbackError.self)
|> take(1)
|> mapToSignal { peer in
if let inputPeer = apiInputPeer(peer) {
var flags: Int32 = 0
var dataBuffer: Buffer?
if let data = data {
flags |= Int32(1 << 0)
dataBuffer = Buffer(data: data.makeData())
}
if isGame {
flags |= Int32(1 << 1)
}
let checkPassword: Signal<Api.InputCheckPasswordSRP?, MessageActionCallbackError>
if let password = password, !password.isEmpty {
flags |= Int32(1 << 2)
checkPassword = _internal_twoStepAuthData(account.network)
|> mapError { error -> MessageActionCallbackError in
if error.errorDescription.hasPrefix("FLOOD_WAIT") {
return .limitExceeded
} else {
return .generic
}
}
|> mapToSignal { authData -> Signal<Api.InputCheckPasswordSRP?, MessageActionCallbackError> in
if let currentPasswordDerivation = authData.currentPasswordDerivation, let srpSessionData = authData.srpSessionData {
guard let kdfResult = passwordKDF(encryptionProvider: account.network.encryptionProvider, password: password, derivation: currentPasswordDerivation, srpSessionData: srpSessionData) else {
return .fail(.generic)
}
return .single(.inputCheckPasswordSRP(srpId: kdfResult.id, A: Buffer(data: kdfResult.A), M1: Buffer(data: kdfResult.M1)))
} else {
return .fail(.twoStepAuthMissing)
}
}
} else {
checkPassword = .single(nil)
}
return checkPassword
|> mapToSignal { password -> Signal<MessageActionCallbackResult, MessageActionCallbackError> in
return account.network.request(Api.functions.messages.getBotCallbackAnswer(flags: flags, peer: inputPeer, msgId: messageId.id, data: dataBuffer, password: password))
|> map(Optional.init)
|> mapError { error -> MessageActionCallbackError in
if error.errorDescription.hasPrefix("FLOOD_WAIT") {
return .limitExceeded
} else if error.errorDescription == "PASSWORD_HASH_INVALID" {
return .invalidPassword
} else if error.errorDescription == "PASSWORD_MISSING" {
return .twoStepAuthMissing
} else if error.errorDescription.hasPrefix("PASSWORD_TOO_FRESH_") {
let timeout = String(error.errorDescription[error.errorDescription.index(error.errorDescription.startIndex, offsetBy: "PASSWORD_TOO_FRESH_".count)...])
if let value = Int32(timeout) {
return .twoStepAuthTooFresh(value)
}
} else if error.errorDescription.hasPrefix("SESSION_TOO_FRESH_") {
let timeout = String(error.errorDescription[error.errorDescription.index(error.errorDescription.startIndex, offsetBy: "SESSION_TOO_FRESH_".count)...])
if let value = Int32(timeout) {
return .authSessionTooFresh(value)
}
} else if error.errorDescription == "USER_PRIVACY_RESTRICTED" {
return .restricted
} else if error.errorDescription == "USER_BLOCKED" {
return .userBlocked
}
return .generic
}
|> map { result -> MessageActionCallbackResult in
guard let result = result else {
return .none
}
switch result {
case let .botCallbackAnswer(flags, message, url, _):
if let message = message {
if (flags & (1 << 1)) != 0 {
return .alert(message)
} else {
return .toast(message)
}
} else if let url = url {
return .url(url)
} else {
return .none
}
}
}
}
} else {
return .single(.none)
}
}
}
public enum MessageActionUrlAuthResult {
case `default`
case accepted(String)
case request(String, Peer, Bool)
}
public enum MessageActionUrlSubject {
case message(id: MessageId, buttonId: Int32)
case url(String)
}
func _internal_requestMessageActionUrlAuth(account: Account, subject: MessageActionUrlSubject) -> Signal<MessageActionUrlAuthResult, NoError> {
let request: Signal<Api.UrlAuthResult?, MTRpcError>
var flags: Int32 = 0
switch subject {
case let .message(messageId, buttonId):
flags |= (1 << 1)
request = account.postbox.loadedPeerWithId(messageId.peerId)
|> take(1)
|> castError(MTRpcError.self)
|> mapToSignal { peer -> Signal<Api.UrlAuthResult?, MTRpcError> in
if let inputPeer = apiInputPeer(peer) {
return account.network.request(Api.functions.messages.requestUrlAuth(flags: flags, peer: inputPeer, msgId: messageId.id, buttonId: buttonId, url: nil))
|> map(Optional.init)
} else {
return .single(nil)
}
}
case let .url(url):
flags |= (1 << 2)
request = account.network.request(Api.functions.messages.requestUrlAuth(flags: flags, peer: nil, msgId: nil, buttonId: nil, url: url))
|> map(Optional.init)
}
return request
|> `catch` { _ -> Signal<Api.UrlAuthResult?, NoError> in
return .single(nil)
}
|> map { result -> MessageActionUrlAuthResult in
guard let result = result else {
return .default
}
switch result {
case .urlAuthResultDefault:
return .default
case let .urlAuthResultAccepted(url):
return .accepted(url)
case let .urlAuthResultRequest(flags, bot, domain):
return .request(domain, TelegramUser(user: bot), (flags & (1 << 0)) != 0)
}
}
}
func _internal_acceptMessageActionUrlAuth(account: Account, subject: MessageActionUrlSubject, allowWriteAccess: Bool) -> Signal<MessageActionUrlAuthResult, NoError> {
var flags: Int32 = 0
if allowWriteAccess {
flags |= Int32(1 << 0)
}
let request: Signal<Api.UrlAuthResult?, MTRpcError>
switch subject {
case let .message(messageId, buttonId):
flags |= (1 << 1)
request = account.postbox.loadedPeerWithId(messageId.peerId)
|> take(1)
|> castError(MTRpcError.self)
|> mapToSignal { peer -> Signal<Api.UrlAuthResult?, MTRpcError> in
if let inputPeer = apiInputPeer(peer) {
let flags: Int32 = 1 << 1
return account.network.request(Api.functions.messages.acceptUrlAuth(flags: flags, peer: inputPeer, msgId: messageId.id, buttonId: buttonId, url: nil))
|> map(Optional.init)
} else {
return .single(nil)
}
}
case let .url(url):
flags |= (1 << 2)
request = account.network.request(Api.functions.messages.acceptUrlAuth(flags: flags, peer: nil, msgId: nil, buttonId: nil, url: url))
|> map(Optional.init)
}
return request
|> `catch` { _ -> Signal<Api.UrlAuthResult?, NoError> in
return .single(nil)
}
|> map { result -> MessageActionUrlAuthResult in
guard let result = result else {
return .default
}
switch result {
case let .urlAuthResultAccepted(url):
return .accepted(url)
default:
return .default
}
}
}
@@ -0,0 +1,82 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
func _internal_requestStartBot(account: Account, botPeerId: PeerId, payload: String?) -> Signal<Void, NoError> {
if let payload = payload, !payload.isEmpty {
return account.postbox.loadedPeerWithId(botPeerId)
|> mapToSignal { botPeer -> Signal<Void, NoError> in
if let inputUser = apiInputUser(botPeer) {
return account.network.request(Api.functions.messages.startBot(bot: inputUser, peer: .inputPeerEmpty, randomId: Int64.random(in: Int64.min ... Int64.max), startParam: payload))
|> mapToSignal { result -> Signal<Void, MTRpcError> in
account.stateManager.addUpdates(result)
return .complete()
}
|> `catch` { _ -> Signal<Void, MTRpcError> in
return .complete()
}
|> retryRequest
} else {
return .complete()
}
}
} else {
return enqueueMessages(account: account, peerId: botPeerId, messages: [.message(text: "/start", attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) |> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
}
}
public enum RequestStartBotInGroupError {
case generic
}
public enum StartBotInGroupResult {
case none
case channelParticipant(RenderedChannelParticipant)
}
func _internal_requestStartBotInGroup(account: Account, botPeerId: PeerId, groupPeerId: PeerId, payload: String?) -> Signal<StartBotInGroupResult, RequestStartBotInGroupError> {
return account.postbox.transaction { transaction -> (Peer?, Peer?) in
return (transaction.getPeer(botPeerId), transaction.getPeer(groupPeerId))
}
|> mapError { _ -> RequestStartBotInGroupError in }
|> mapToSignal { botPeer, groupPeer -> Signal<StartBotInGroupResult, RequestStartBotInGroupError> in
if let botPeer = botPeer, let inputUser = apiInputUser(botPeer), let groupPeer = groupPeer, let inputGroup = apiInputPeer(groupPeer) {
let request = account.network.request(Api.functions.messages.startBot(bot: inputUser, peer: inputGroup, randomId: Int64.random(in: Int64.min ... Int64.max), startParam: payload ?? ""))
|> mapError { _ -> RequestStartBotInGroupError in
return .generic
}
|> mapToSignal { result -> Signal<StartBotInGroupResult, RequestStartBotInGroupError> in
account.stateManager.addUpdates(result)
if groupPeerId.namespace == Namespaces.Peer.CloudChannel {
return _internal_fetchChannelParticipant(account: account, peerId: groupPeerId, participantId: botPeerId)
|> mapError { _ -> RequestStartBotInGroupError in }
|> mapToSignal { participant -> Signal<StartBotInGroupResult, RequestStartBotInGroupError> in
return account.postbox.transaction { transaction -> StartBotInGroupResult in
if let participant = participant, let peer = transaction.getPeer(participant.peerId) {
var peers: [PeerId: Peer] = [:]
let presences: [PeerId: PeerPresence] = [:]
peers[peer.id] = peer
return .channelParticipant(RenderedChannelParticipant(participant: participant, peer: peer, peers: peers, presences: presences))
} else {
return .none
}
}
|> mapError { _ -> RequestStartBotInGroupError in }
}
} else {
return .single(.none)
}
}
return request
} else {
return .complete()
}
}
}
@@ -0,0 +1,161 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
func _internal_sendScheduledMessageNowInteractively(postbox: Postbox, messageId: MessageId) -> Signal<Never, NoError> {
return postbox.transaction { transaction -> Void in
transaction.setPendingMessageAction(type: .sendScheduledMessageImmediately, id: messageId, action: SendScheduledMessageImmediatelyAction())
}
|> ignoreValues
}
private final class ManagedApplyPendingScheduledMessagesActionsHelper {
var operationDisposables: [MessageId: Disposable] = [:]
func update(entries: [PendingMessageActionsEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PendingMessageActionsEntry, MetaDisposable)]) {
var disposeOperations: [Disposable] = []
var beginOperations: [(PendingMessageActionsEntry, MetaDisposable)] = []
var hasRunningOperationForPeerId = Set<PeerId>()
var validIds = Set<MessageId>()
for entry in entries {
if !hasRunningOperationForPeerId.contains(entry.id.peerId) {
hasRunningOperationForPeerId.insert(entry.id.peerId)
validIds.insert(entry.id)
if self.operationDisposables[entry.id] == nil {
let disposable = MetaDisposable()
beginOperations.append((entry, disposable))
self.operationDisposables[entry.id] = disposable
}
}
}
var removeMergedIds: [MessageId] = []
for (id, disposable) in self.operationDisposables {
if !validIds.contains(id) {
removeMergedIds.append(id)
disposeOperations.append(disposable)
}
}
for id in removeMergedIds {
self.operationDisposables.removeValue(forKey: id)
}
return (disposeOperations, beginOperations)
}
func reset() -> [Disposable] {
let disposables = Array(self.operationDisposables.values)
self.operationDisposables.removeAll()
return disposables
}
}
private func withTakenAction(postbox: Postbox, type: PendingMessageActionType, id: MessageId, _ f: @escaping (Transaction, PendingMessageActionsEntry?) -> Signal<Never, NoError>) -> Signal<Never, NoError> {
return postbox.transaction { transaction -> Signal<Never, NoError> in
var result: PendingMessageActionsEntry?
if let action = transaction.getPendingMessageAction(type: type, id: id) as? SendScheduledMessageImmediatelyAction {
result = PendingMessageActionsEntry(id: id, action: action)
}
return f(transaction, result)
}
|> switchToLatest
}
func managedApplyPendingScheduledMessagesActions(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal<Void, NoError> {
return Signal { _ in
let helper = Atomic<ManagedApplyPendingScheduledMessagesActionsHelper>(value: ManagedApplyPendingScheduledMessagesActionsHelper())
let actionsKey = PostboxViewKey.pendingMessageActions(type: .sendScheduledMessageImmediately)
let disposable = postbox.combinedView(keys: [actionsKey]).start(next: { view in
var entries: [PendingMessageActionsEntry] = []
if let v = view.views[actionsKey] as? PendingMessageActionsView {
entries = v.entries
}
let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PendingMessageActionsEntry, MetaDisposable)]) in
return helper.update(entries: entries)
}
for disposable in disposeOperations {
disposable.dispose()
}
for (entry, disposable) in beginOperations {
let signal = withTakenAction(postbox: postbox, type: .sendScheduledMessageImmediately, id: entry.id, { transaction, entry -> Signal<Never, NoError> in
if let entry = entry {
if let _ = entry.action as? SendScheduledMessageImmediatelyAction {
return sendScheduledMessageNow(postbox: postbox, network: network, stateManager: stateManager, messageId: entry.id)
|> `catch` { _ -> Signal<Never, NoError> in
return .complete()
}
} else {
assertionFailure()
}
}
return .complete()
})
|> then(
postbox.transaction { transaction -> Void in
var resourceIds: [MediaResourceId] = []
transaction.deleteMessages([entry.id], forEachMedia: { media in
addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds)
})
if !resourceIds.isEmpty {
let _ = postbox.mediaBox.removeCachedResources(Array(Set(resourceIds))).start()
}
}
|> ignoreValues
)
disposable.set(signal.start())
}
})
return ActionDisposable {
let disposables = helper.with { helper -> [Disposable] in
return helper.reset()
}
for disposable in disposables {
disposable.dispose()
}
disposable.dispose()
}
}
}
private enum SendScheduledMessageNowError {
case generic
}
private func sendScheduledMessageNow(postbox: Postbox, network: Network, stateManager: AccountStateManager, messageId: MessageId) -> Signal<Never, SendScheduledMessageNowError> {
return postbox.transaction { transaction -> Peer? in
guard let peer = transaction.getPeer(messageId.peerId) else {
return nil
}
return peer
}
|> castError(SendScheduledMessageNowError.self)
|> mapToSignal { peer -> Signal<Never, SendScheduledMessageNowError> in
guard let peer = peer else {
return .fail(.generic)
}
guard let inputPeer = apiInputPeer(peer) else {
return .fail(.generic)
}
return network.request(Api.functions.messages.sendScheduledMessages(peer: inputPeer, id: [messageId.id]))
|> mapError { _ -> SendScheduledMessageNowError in
return .generic
}
|> mapToSignal { updates -> Signal<Never, SendScheduledMessageNowError> in
stateManager.addUpdates(updates)
return .complete()
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,327 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public struct SendAsPeer: Equatable {
public let peer: Peer
public let subscribers: Int32?
public let isPremiumRequired: Bool
public init(peer: Peer, subscribers: Int32?, isPremiumRequired: Bool) {
self.peer = peer
self.subscribers = subscribers
self.isPremiumRequired = isPremiumRequired
}
public static func ==(lhs: SendAsPeer, rhs: SendAsPeer) -> Bool {
return lhs.peer.isEqual(rhs.peer) && lhs.subscribers == rhs.subscribers && lhs.isPremiumRequired == rhs.isPremiumRequired
}
}
public final class CachedSendAsPeers: Codable {
public let peerIds: [PeerId]
public let premiumRequiredPeerIds: Set<PeerId>
public let timestamp: Int32
public init(peerIds: [PeerId], premiumRequiredPeerIds: Set<PeerId>, timestamp: Int32) {
self.peerIds = peerIds
self.premiumRequiredPeerIds = premiumRequiredPeerIds
self.timestamp = timestamp
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
self.peerIds = (try container.decode([Int64].self, forKey: "peerIds")).map(PeerId.init)
self.premiumRequiredPeerIds = Set((try container.decodeIfPresent([Int64].self, forKey: "premiumRequiredPeerIds") ?? []).map(PeerId.init))
self.timestamp = try container.decode(Int32.self, forKey: "timestamp")
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self)
try container.encode(self.peerIds.map { $0.toInt64() }, forKey: "peerIds")
try container.encode(Array(self.premiumRequiredPeerIds).map { $0.toInt64() }, forKey: "premiumRequiredPeerIds")
try container.encode(self.timestamp, forKey: "timestamp")
}
}
func _internal_cachedPeerSendAsAvailablePeers(account: Account, peerId: PeerId) -> Signal<[SendAsPeer], NoError> {
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: peerId.toInt64())
return account.postbox.transaction { transaction -> ([SendAsPeer], Int32)? in
let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedSendAsPeers, key: key))?.get(CachedSendAsPeers.self)
if let cached = cached {
var peers: [SendAsPeer] = []
for peerId in cached.peerIds {
if let peer = transaction.getPeer(peerId) {
var subscribers: Int32?
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData {
subscribers = cachedData.participantsSummary.memberCount
}
peers.append(SendAsPeer(peer: peer, subscribers: subscribers, isPremiumRequired: cached.premiumRequiredPeerIds.contains(peer.id)))
}
}
return (peers, cached.timestamp)
} else {
return nil
}
}
|> mapToSignal { cachedPeersAndTimestamp -> Signal<[SendAsPeer], NoError> in
let initialSignal: Signal<[SendAsPeer], NoError>
if let (cachedPeers, _) = cachedPeersAndTimestamp {
initialSignal = .single(cachedPeers)
} else {
initialSignal = .complete()
}
return initialSignal
|> then(_internal_peerSendAsAvailablePeers(accountPeerId: account.peerId, network: account.network, postbox: account.postbox, peerId: peerId)
|> mapToSignal { peers -> Signal<[SendAsPeer], NoError> in
return account.postbox.transaction { transaction -> [SendAsPeer] in
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
var premiumRequiredPeerIds = Set<PeerId>()
for peer in peers {
if peer.isPremiumRequired {
premiumRequiredPeerIds.insert(peer.peer.id)
}
}
if let entry = CodableEntry(CachedSendAsPeers(peerIds: peers.map { $0.peer.id }, premiumRequiredPeerIds: premiumRequiredPeerIds, timestamp: currentTimestamp)) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedSendAsPeers, key: key), entry: entry)
}
return peers
}
})
}
}
func _internal_peerSendAsAvailablePeers(accountPeerId: PeerId, network: Network, postbox: Postbox, peerId: PeerId) -> Signal<[SendAsPeer], NoError> {
return postbox.transaction { transaction -> Peer? in
return transaction.getPeer(peerId)
}
|> mapToSignal { peer -> Signal<[SendAsPeer], NoError> in
guard let peer = peer else {
return .single([])
}
guard let inputPeer = apiInputPeer(peer) else {
return .single([])
}
if let channel = peer as? TelegramChannel {
if case .group = channel.info {
} else if channel.adminRights != nil || channel.flags.contains(.isCreator) {
} else {
return .single([])
}
} else {
return .single([])
}
return network.request(Api.functions.channels.getSendAs(flags: 0, peer: inputPeer))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.channels.SendAsPeers?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<[SendAsPeer], NoError> in
guard let result = result else {
return .single([])
}
switch result {
case let .sendAsPeers(sendAsPeers, chats, _):
return postbox.transaction { transaction -> [SendAsPeer] in
var subscribers: [PeerId: Int32] = [:]
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: [])
var premiumRequiredPeerIds = Set<PeerId>()
for sendAsPeer in sendAsPeers {
if case let .sendAsPeer(flags, peer) = sendAsPeer, (flags & (1 << 0)) != 0 {
premiumRequiredPeerIds.insert(peer.peerId)
}
}
for chat in chats {
if let groupOrChannel = parsedPeers.get(chat.peerId) {
switch chat {
case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _, _, _, _, _):
if let participantsCount = participantsCount {
subscribers[groupOrChannel.id] = participantsCount
}
case let .chat(_, _, _, _, participantsCount, _, _, _, _, _):
subscribers[groupOrChannel.id] = participantsCount
default:
break
}
}
}
updatePeers(transaction: transaction, accountPeerId: accountPeerId,peers: parsedPeers)
var peers: [Peer] = []
for chat in chats {
if let peer = transaction.getPeer(chat.peerId) {
peers.append(peer)
}
}
return peers.map { SendAsPeer(peer: $0, subscribers: subscribers[$0.id], isPremiumRequired: premiumRequiredPeerIds.contains($0.id)) }
}
}
}
}
}
public enum UpdatePeerSendAsPeerError {
case generic
}
func _internal_updatePeerSendAsPeer(account: Account, peerId: PeerId, sendAs: PeerId) -> Signal<Never, UpdatePeerSendAsPeerError> {
return account.postbox.transaction { transaction -> (Api.InputPeer, Api.InputPeer)? in
if let peer = transaction.getPeer(peerId), let sendAsPeer = transaction.getPeer(sendAs), let inputPeer = apiInputPeer(peer), let sendAsInputPeer = apiInputPeerOrSelf(sendAsPeer, accountPeerId: account.peerId) {
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in
if let cachedData = cachedData as? CachedChannelData {
return cachedData.withUpdatedSendAsPeerId(sendAs)
} else {
return cachedData
}
})
return (inputPeer, sendAsInputPeer)
} else {
return nil
}
}
|> castError(UpdatePeerSendAsPeerError.self)
|> mapToSignal { result in
guard let (inputPeer, sendAsInputPeer) = result else {
return .fail(.generic)
}
return account.network.request(Api.functions.messages.saveDefaultSendAs(peer: inputPeer, sendAs: sendAsInputPeer))
|> mapError { _ -> UpdatePeerSendAsPeerError in
return .generic
}
|> mapToSignal { result -> Signal<Never, UpdatePeerSendAsPeerError> in
return account.postbox.transaction { transaction in
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in
if let cachedData = cachedData as? CachedChannelData {
return cachedData.withUpdatedSendAsPeerId(sendAs)
} else {
return cachedData
}
})
}
|> castError(UpdatePeerSendAsPeerError.self)
|> ignoreValues
}
}
}
func _internal_cachedLiveStorySendAsAvailablePeers(account: Account, peerId: PeerId) -> Signal<[SendAsPeer], NoError> {
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: peerId.toInt64())
return account.postbox.transaction { transaction -> ([SendAsPeer], Int32)? in
let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedLiveStorySendAsPeers, key: key))?.get(CachedSendAsPeers.self)
if let cached = cached {
var peers: [SendAsPeer] = []
for peerId in cached.peerIds {
if let peer = transaction.getPeer(peerId) {
var subscribers: Int32?
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData {
subscribers = cachedData.participantsSummary.memberCount
}
peers.append(SendAsPeer(peer: peer, subscribers: subscribers, isPremiumRequired: cached.premiumRequiredPeerIds.contains(peer.id)))
}
}
return (peers, cached.timestamp)
} else {
return nil
}
}
|> mapToSignal { cachedPeersAndTimestamp -> Signal<[SendAsPeer], NoError> in
let initialSignal: Signal<[SendAsPeer], NoError>
if let (cachedPeers, _) = cachedPeersAndTimestamp {
initialSignal = .single(cachedPeers)
} else {
initialSignal = .complete()
}
return initialSignal
|> then(_internal_liveStorySendAsAvailablePeers(account: account, peerId: peerId)
|> mapToSignal { peers -> Signal<[SendAsPeer], NoError> in
return account.postbox.transaction { transaction -> [SendAsPeer] in
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
var premiumRequiredPeerIds = Set<PeerId>()
for peer in peers {
if peer.isPremiumRequired {
premiumRequiredPeerIds.insert(peer.peer.id)
}
}
if let entry = CodableEntry(CachedSendAsPeers(peerIds: peers.map { $0.peer.id }, premiumRequiredPeerIds: premiumRequiredPeerIds, timestamp: currentTimestamp)) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedLiveStorySendAsPeers, key: key), entry: entry)
}
return peers
}
})
}
}
func _internal_liveStorySendAsAvailablePeers(account: Account, peerId: PeerId) -> Signal<[SendAsPeer], NoError> {
return account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(peerId)
}
|> mapToSignal { peer -> Signal<[SendAsPeer], NoError> in
guard let peer else {
return .single([])
}
guard let inputPeer = apiInputPeer(peer) else {
return .single([])
}
return account.network.request(Api.functions.channels.getSendAs(flags: 1 << 1, peer: inputPeer))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.channels.SendAsPeers?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<[SendAsPeer], NoError> in
guard let result = result else {
return .single([])
}
switch result {
case let .sendAsPeers(sendAsPeers, chats, _):
return account.postbox.transaction { transaction -> [SendAsPeer] in
var subscribers: [PeerId: Int32] = [:]
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: [])
var premiumRequiredPeerIds = Set<PeerId>()
for sendAsPeer in sendAsPeers {
if case let .sendAsPeer(flags, peer) = sendAsPeer, (flags & (1 << 0)) != 0 {
premiumRequiredPeerIds.insert(peer.peerId)
}
}
for chat in chats {
if let groupOrChannel = parsedPeers.get(chat.peerId) {
switch chat {
case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _, _, _, _, _):
if let participantsCount = participantsCount {
subscribers[groupOrChannel.id] = participantsCount
}
case let .chat(_, _, _, _, participantsCount, _, _, _, _, _):
subscribers[groupOrChannel.id] = participantsCount
default:
break
}
}
}
updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: parsedPeers)
var peers: [Peer] = []
for chat in chats {
if let peer = transaction.getPeer(chat.peerId) {
peers.append(peer)
}
}
return peers.map { SendAsPeer(peer: $0, subscribers: subscribers[$0.id], isPremiumRequired: premiumRequiredPeerIds.contains($0.id)) }
}
}
}
}
}
@@ -0,0 +1,974 @@
import SwiftSignalKit
import Postbox
import TelegramApi
public final class SparseMessageList {
private final class Impl {
private let queue: Queue
private let account: Account
private let peerId: PeerId
private let threadId: Int64?
private let messageTag: MessageTags
private struct TopSection: Equatable {
var messages: [Message]
static func ==(lhs: TopSection, rhs: TopSection) -> 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].stableVersion != rhs.messages[i].stableVersion {
return false
}
}
return true
}
}
private struct SparseItems: Equatable {
enum Item: Equatable {
case range(count: Int)
case anchor(id: MessageId, timestamp: Int32, message: Message?)
static func ==(lhs: Item, rhs: Item) -> Bool {
switch lhs {
case let .range(count):
if case .range(count) = rhs {
return true
} else {
return false
}
case let .anchor(lhsId, lhsTimestamp, lhsMessage):
if case let .anchor(rhsId, rhsTimestamp, rhsMessage) = rhs {
if lhsId != rhsId {
return false
}
if lhsTimestamp != rhsTimestamp {
return false
}
if let lhsMessage = lhsMessage, let rhsMessage = rhsMessage {
if lhsMessage.id != rhsMessage.id {
return false
}
if lhsMessage.stableVersion != rhsMessage.stableVersion {
return false
}
} else if (lhsMessage != nil) != (rhsMessage != nil) {
return false
}
return true
} else {
return false
}
}
}
}
var items: [Item]
}
private var topSectionItemRequestCount: Int = 100
private var topSection: TopSection?
private var topItemsDisposable = MetaDisposable()
private var deletedMessagesDisposable: Disposable?
private var sparseItems: SparseItems?
private var sparseItemsDisposable: Disposable?
private struct LoadingHole: Equatable {
var anchor: MessageId
var direction: LoadHoleDirection
}
private let loadHoleDisposable = MetaDisposable()
private var loadingHole: LoadingHole?
private var loadingPlaceholders: [MessageId: Disposable] = [:]
private var loadedPlaceholders: [MessageId: Message] = [:]
let statePromise = Promise<SparseMessageList.State>()
init(queue: Queue, account: Account, peerId: PeerId, threadId: Int64?, messageTag: MessageTags) {
self.queue = queue
self.account = account
self.peerId = peerId
self.threadId = threadId
self.messageTag = messageTag
self.resetTopSection()
if self.threadId == nil {
self.sparseItemsDisposable = (self.account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<SparseItems, NoError> in
guard let inputPeer = inputPeer else {
return .single(SparseItems(items: []))
}
guard let messageFilter = messageFilterForTagMask(messageTag) else {
return .single(SparseItems(items: []))
}
//TODO:api
return account.network.request(Api.functions.messages.getSearchResultsPositions(flags: 0, peer: inputPeer, savedPeerId: nil, filter: messageFilter, offsetId: 0, limit: 1000))
|> map { result -> SparseItems in
switch result {
case let .searchResultsPositions(totalCount, positions):
struct Position: Equatable {
var id: Int32
var date: Int32
var offset: Int
}
var positions: [Position] = positions.map { position -> Position in
switch position {
case let .searchResultPosition(id, date, offset):
return Position(id: id, date: date, offset: Int(offset))
}
}
positions.sort(by: { lhs, rhs in
return lhs.id > rhs.id
})
var result = SparseItems(items: [])
for i in 0 ..< positions.count {
if i != 0 {
let deltaCount = positions[i].offset - 1 - positions[i - 1].offset
if deltaCount > 0 {
result.items.append(.range(count: deltaCount))
}
}
result.items.append(.anchor(id: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: positions[i].id), timestamp: positions[i].date, message: nil))
if i == positions.count - 1 {
let remainingCount = Int(totalCount) - 1 - positions[i].offset
if remainingCount > 0 {
result.items.append(.range(count: remainingCount))
}
}
}
return result
}
}
|> `catch` { _ -> Signal<SparseItems, NoError> in
return .single(SparseItems(items: []))
}
}
|> deliverOn(self.queue)).start(next: { [weak self] sparseItems in
guard let strongSelf = self else {
return
}
strongSelf.sparseItems = sparseItems
if strongSelf.topSection != nil {
strongSelf.updateState()
}
})
}
self.deletedMessagesDisposable = (account.postbox.combinedView(keys: [.deletedMessages(peerId: peerId)])
|> deliverOn(self.queue)).start(next: { [weak self] views in
guard let strongSelf = self else {
return
}
guard let view = views.views[.deletedMessages(peerId: peerId)] as? DeletedMessagesView else {
return
}
strongSelf.processDeletedMessages(ids: view.currentDeletedMessages)
})
}
deinit {
self.topItemsDisposable.dispose()
self.sparseItemsDisposable?.dispose()
self.loadHoleDisposable.dispose()
self.deletedMessagesDisposable?.dispose()
}
private func resetTopSection() {
let count: Int
count = 200
let location: ChatLocationInput = .peer(peerId: self.peerId, threadId: self.threadId)
self.topItemsDisposable.set((self.account.postbox.aroundMessageHistoryViewForLocation(location, anchor: .upperBound, ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set(), count: count, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: .tag(self.messageTag), appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: [])
|> deliverOn(self.queue)).start(next: { [weak self] view, updateType, _ in
guard let strongSelf = self else {
return
}
switch updateType {
case .FillHole:
strongSelf.resetTopSection()
default:
strongSelf.updateTopSection(view: view)
}
}))
}
private func processDeletedMessages(ids: [MessageId]) {
if let sparseItems = self.sparseItems {
let idsSet = Set(ids)
var removeIndices: [Int] = []
for i in 0 ..< sparseItems.items.count {
switch sparseItems.items[i] {
case let .anchor(id, _, _):
if idsSet.contains(id) {
removeIndices.append(i)
}
default:
break
}
}
if !removeIndices.isEmpty {
for index in removeIndices.reversed() {
self.sparseItems?.items.remove(at: index)
}
self.updateState()
}
}
}
func loadMoreFromTopSection() {
self.topSectionItemRequestCount += 100
self.resetTopSection()
}
func loadHole(anchor: MessageId, direction: LoadHoleDirection, completion: @escaping () -> Void) {
guard let sparseItems = self.sparseItems else {
completion()
return
}
var loadRange: ClosedRange<Int32>?
var loadCount: Int?
centralItemSearch: for i in 0 ..< sparseItems.items.count {
switch sparseItems.items[i] {
case let .anchor(id, _, _):
if id == anchor {
func lowerStep(index: Int, holeRange: inout ClosedRange<Int32>, holeCount: inout Int) -> Bool {
switch sparseItems.items[index] {
case let .anchor(id, _, message):
holeRange = id.id ... holeRange.upperBound
holeCount += 1
if message != nil {
return false
} else {
if holeCount > 90 {
return false
}
}
case let .range(count):
if holeCount + count > 90 {
return false
}
holeCount += count
}
return true
}
func upperStep(index: Int, holeRange: inout ClosedRange<Int32>, holeCount: inout Int) -> Bool {
switch sparseItems.items[index] {
case let .anchor(id, _, message):
holeRange = holeRange.lowerBound ... id.id
holeCount += 1
if message != nil {
return false
} else {
if holeCount > 90 {
return false
}
}
case let .range(count):
if holeCount + count > 90 {
return false
}
holeCount += count
}
return true
}
var holeCount = 1
var holeRange: ClosedRange<Int32> = id.id ... id.id
var lowerIndex = i - 1
var upperIndex = i + 1
while true {
if holeCount > 90 {
break
}
if lowerIndex == -1 && upperIndex == sparseItems.items.count {
break
}
if lowerIndex >= 0 {
if !upperStep(index: lowerIndex, holeRange: &holeRange, holeCount: &holeCount) {
lowerIndex = -1
} else {
lowerIndex -= 1
if lowerIndex == -1 {
holeRange = holeRange.lowerBound ... (Int32.max - 1)
}
}
}
if upperIndex < sparseItems.items.count {
if !lowerStep(index: upperIndex, holeRange: &holeRange, holeCount: &holeCount) {
upperIndex = sparseItems.items.count
} else {
upperIndex += 1
if upperIndex == sparseItems.items.count {
holeRange = 1 ... holeRange.upperBound
}
}
}
}
loadRange = holeRange
loadCount = holeCount
break centralItemSearch
}
default:
break
}
}
guard let range = loadRange, let expectedCount = loadCount else {
completion()
return
}
let loadingHole = LoadingHole(anchor: anchor, direction: direction)
if self.loadingHole != nil {
completion()
return
}
self.loadingHole = loadingHole
let mappedDirection: MessageHistoryViewRelativeHoleDirection = .range(start: MessageId(peerId: anchor.peerId, namespace: anchor.namespace, id: range.upperBound), end: MessageId(peerId: anchor.peerId, namespace: anchor.namespace, id: range.lowerBound - 1))
let account = self.account
self.loadHoleDisposable.set((fetchMessageHistoryHole(
accountPeerId: self.account.peerId,
source: .network(self.account.network),
postbox: self.account.postbox,
peerInput: .direct(peerId: self.peerId, threadId: nil),
namespace: Namespaces.Message.Cloud,
direction: mappedDirection,
space: .tag(self.messageTag),
count: 100
)
|> mapToSignal { result -> Signal<[Message], NoError> in
guard let result = result else {
return .single([])
}
return account.postbox.transaction { transaction -> [Message] in
return result.ids.sorted(by: { $0 > $1 }).compactMap(transaction.getMessage)
}
}
|> deliverOn(self.queue)).start(next: { [weak self] messages in
guard let strongSelf = self else {
completion()
return
}
if messages.count != expectedCount {
Logger.shared.log("SparseMessageList", "unexpected message count")
}
var lowerIndex: Int?
var upperIndex: Int?
for i in 0 ..< strongSelf.sparseItems!.items.count {
switch strongSelf.sparseItems!.items[i] {
case let .anchor(id, _, _):
if id.id == range.lowerBound {
lowerIndex = i
}
if id.id == range.upperBound {
upperIndex = i
}
default:
break
}
}
if range.lowerBound <= 1 {
lowerIndex = strongSelf.sparseItems!.items.count - 1
}
if range.upperBound >= Int32.max - 1 {
upperIndex = 0
}
if let lowerIndex = lowerIndex, let upperIndex = upperIndex {
strongSelf.sparseItems!.items.removeSubrange(upperIndex ... lowerIndex)
var insertIndex = upperIndex
for message in messages.sorted(by: { $0.id > $1.id }) {
strongSelf.sparseItems!.items.insert(.anchor(id: message.id, timestamp: message.timestamp, message: message), at: insertIndex)
insertIndex += 1
}
}
let anchors = strongSelf.sparseItems!.items.compactMap { item -> MessageId? in
if case let .anchor(id, _, _) = item {
return id
} else {
return nil
}
}
assert(anchors.sorted(by: >) == anchors)
strongSelf.updateState()
if strongSelf.loadingHole == loadingHole {
strongSelf.loadingHole = nil
}
completion()
}))
/*let mappedDirection: MessageHistoryViewRelativeHoleDirection
switch direction {
case .around:
mappedDirection = .aroundId(anchor)
case .earlier:
mappedDirection = .range(start: anchor, end: MessageId(peerId: anchor.peerId, namespace: anchor.namespace, id: 1))
case .later:
mappedDirection = .range(start: anchor, end: MessageId(peerId: anchor.peerId, namespace: anchor.namespace, id: Int32.max - 1))
}
let account = self.account
self.loadHoleDisposable.set((fetchMessageHistoryHole(accountPeerId: self.account.peerId, source: .network(self.account.network), postbox: self.account.postbox, peerInput: .direct(peerId: self.peerId, threadId: nil), namespace: Namespaces.Message.Cloud, direction: mappedDirection, space: .tag(self.messageTag), count: 100)
|> mapToSignal { result -> Signal<[Message], NoError> in
guard let result = result else {
return .single([])
}
return account.postbox.transaction { transaction -> [Message] in
return result.ids.sorted(by: { $0 > $1 }).compactMap(transaction.getMessage)
}
}
|> deliverOn(self.queue)).start(next: { [weak self] messages in
guard let strongSelf = self else {
completion()
return
}
if strongSelf.sparseItems != nil {
var sparseHoles: [(itemIndex: Int, leftId: MessageId, rightId: MessageId)] = []
for i in 0 ..< strongSelf.sparseItems!.items.count {
switch strongSelf.sparseItems!.items[i] {
case let .anchor(id, timestamp, _):
for messageIndex in 0 ..< messages.count {
if messages[messageIndex].id == id {
strongSelf.sparseItems!.items[i] = .anchor(id: id, timestamp: timestamp, message: messages[messageIndex])
}
}
case .range:
if i == 0 {
assertionFailure()
} else {
var leftId: MessageId?
switch strongSelf.sparseItems!.items[i - 1] {
case .range:
assertionFailure()
case let .anchor(id, _, _):
leftId = id
}
var rightId: MessageId?
if i != strongSelf.sparseItems!.items.count - 1 {
switch strongSelf.sparseItems!.items[i + 1] {
case .range:
assertionFailure()
case let .anchor(id, _, _):
rightId = id
}
}
if let leftId = leftId, let rightId = rightId {
sparseHoles.append((itemIndex: i, leftId: leftId, rightId: rightId))
} else if let leftId = leftId, i == strongSelf.sparseItems!.items.count - 1 {
sparseHoles.append((itemIndex: i, leftId: leftId, rightId: MessageId(peerId: leftId.peerId, namespace: leftId.namespace, id: 1)))
} else {
assertionFailure()
}
}
}
}
for (itemIndex, initialLeftId, initialRightId) in sparseHoles.reversed() {
var leftCovered = false
var rightCovered = false
for message in messages {
if message.id == initialLeftId {
leftCovered = true
}
if message.id == initialRightId {
rightCovered = true
}
}
if leftCovered && rightCovered {
strongSelf.sparseItems!.items.remove(at: itemIndex)
var insertIndex = itemIndex
for message in messages {
if message.id < initialLeftId && message.id > initialRightId {
strongSelf.sparseItems!.items.insert(.anchor(id: message.id, timestamp: message.timestamp, message: message), at: insertIndex)
insertIndex += 1
}
}
} else if leftCovered {
for i in 0 ..< messages.count {
if messages[i].id == initialLeftId {
var spaceItemIndex = itemIndex
for j in i + 1 ..< messages.count {
switch strongSelf.sparseItems!.items[spaceItemIndex] {
case let .range(count):
strongSelf.sparseItems!.items[spaceItemIndex] = .range(count: count - 1)
case .anchor:
assertionFailure()
}
strongSelf.sparseItems!.items.insert(.anchor(id: messages[j].id, timestamp: messages[j].timestamp, message: messages[j]), at: spaceItemIndex)
spaceItemIndex += 1
}
switch strongSelf.sparseItems!.items[spaceItemIndex] {
case let .range(count):
if count <= 0 {
strongSelf.sparseItems!.items.remove(at: spaceItemIndex)
}
case .anchor:
assertionFailure()
}
break
}
}
} else if rightCovered {
for i in (0 ..< messages.count).reversed() {
if messages[i].id == initialRightId {
for j in (0 ..< i).reversed() {
switch strongSelf.sparseItems!.items[itemIndex] {
case let .range(count):
strongSelf.sparseItems!.items[itemIndex] = .range(count: count - 1)
case .anchor:
assertionFailure()
}
strongSelf.sparseItems!.items.insert(.anchor(id: messages[j].id, timestamp: messages[j].timestamp, message: messages[j]), at: itemIndex + 1)
}
switch strongSelf.sparseItems!.items[itemIndex] {
case let .range(count):
if count <= 0 {
strongSelf.sparseItems!.items.remove(at: itemIndex)
}
case .anchor:
assertionFailure()
}
break
}
}
}
}
strongSelf.updateState()
}
if strongSelf.loadingHole == loadingHole {
strongSelf.loadingHole = nil
}
completion()
}))*/
}
private func updateTopSection(view: MessageHistoryView) {
var topSection: TopSection?
if view.isLoading {
topSection = nil
} else {
topSection = TopSection(messages: view.entries.lazy.reversed().map { entry in
return entry.message
})
}
if self.topSection != topSection {
self.topSection = topSection
}
self.updateState()
}
private func updateState() {
var items: [SparseMessageList.State.Item] = []
var minMessageId: MessageId?
if let topSection = self.topSection {
for i in 0 ..< topSection.messages.count {
let message = topSection.messages[i]
items.append(SparseMessageList.State.Item(index: items.count, content: .message(message: message, isLocal: true)))
if let minMessageIdValue = minMessageId {
if message.id < minMessageIdValue {
minMessageId = message.id
}
} else {
minMessageId = message.id
}
}
}
let topItemCount = items.count
var totalCount = items.count
if let sparseItems = self.sparseItems {
var sparseIndex = 0
for i in 0 ..< sparseItems.items.count {
switch sparseItems.items[i] {
case let .anchor(id, timestamp, message):
if sparseIndex >= topItemCount {
if let message = message {
items.append(SparseMessageList.State.Item(index: totalCount, content: .message(message: message, isLocal: false)))
} else {
items.append(SparseMessageList.State.Item(index: totalCount, content: .placeholder(id: id, timestamp: timestamp)))
}
totalCount += 1
}
sparseIndex += 1
case let .range(count):
if sparseIndex >= topItemCount {
totalCount += count
} else {
let overflowCount = sparseIndex + count - topItemCount
if overflowCount > 0 {
totalCount += count
}
}
sparseIndex += count
}
}
}
self.statePromise.set(.single(SparseMessageList.State(
items: items,
totalCount: totalCount,
isLoading: self.topSection == nil
)))
}
}
private let queue: Queue
private let impl: QueueLocalObject<Impl>
public struct State {
public final class Item {
public enum Content {
case message(message: Message, isLocal: Bool)
case placeholder(id: MessageId, timestamp: Int32)
}
public let index: Int
public let content: Content
init(index: Int, content: Content) {
self.index = index
self.content = content
}
}
public var items: [Item]
public var totalCount: Int
public var isLoading: Bool
}
public enum LoadHoleDirection {
case around
case earlier
case later
}
public var state: Signal<State, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.statePromise.get().start(next: subscriber.putNext))
}
return disposable
}
}
init(account: Account, peerId: PeerId, threadId: Int64?, messageTag: MessageTags) {
self.queue = Queue()
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, account: account, peerId: peerId, threadId: threadId, messageTag: messageTag)
})
}
public func loadMoreFromTopSection() {
self.impl.with { impl in
impl.loadMoreFromTopSection()
}
}
public func loadHole(anchor: MessageId, direction: LoadHoleDirection, completion: @escaping () -> Void) {
self.impl.with { impl in
impl.loadHole(anchor: anchor, direction: direction, completion: completion)
}
}
}
public final class SparseMessageCalendar {
private final class Impl {
struct InternalState {
var nextRequestOffset: Int32?
var minTimestamp: Int32?
var messagesByDay: [Int32: SparseMessageCalendar.Entry]
}
private let queue: Queue
private let account: Account
private let peerId: PeerId
private let threadId: Int64?
private let messageTag: MessageTags
private let displayMedia: Bool
private var state: InternalState
let statePromise = Promise<InternalState>()
private let disposable = MetaDisposable()
private var isLoadingMore: Bool = false {
didSet {
self.isLoadingMorePromise.set(.single(self.isLoadingMore))
}
}
private let isLoadingMorePromise = Promise<Bool>(false)
var isLoadingMoreSignal: Signal<Bool, NoError> {
return self.isLoadingMorePromise.get()
}
init(queue: Queue, account: Account, peerId: PeerId, threadId: Int64?, messageTag: MessageTags, displayMedia: Bool) {
self.queue = queue
self.account = account
self.peerId = peerId
self.threadId = threadId
self.messageTag = messageTag
self.displayMedia = displayMedia
self.state = InternalState(nextRequestOffset: 0, minTimestamp: nil, messagesByDay: [:])
self.statePromise.set(.single(self.state))
self.maybeLoadMore()
}
deinit {
self.disposable.dispose()
}
func maybeLoadMore() {
if self.isLoadingMore {
return
}
self.loadMore()
}
func removeMessagesInRange(minTimestamp: Int32, maxTimestamp: Int32, type: InteractiveHistoryClearingType, completion: @escaping () -> Void) -> Disposable {
var removeKeys: [Int32] = []
for (id, entry) in self.state.messagesByDay {
if entry.message.timestamp >= minTimestamp && entry.message.timestamp <= maxTimestamp {
removeKeys.append(id)
}
}
for id in removeKeys {
self.state.messagesByDay.removeValue(forKey: id)
}
self.statePromise.set(.single(self.state))
return _internal_clearHistoryInRangeInteractively(postbox: self.account.postbox, peerId: self.peerId, threadId: self.threadId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, type: type).start(completed: {
completion()
})
}
private func loadMore() {
guard let nextRequestOffset = self.state.nextRequestOffset else {
return
}
if self.threadId != nil {
return
}
if !self.displayMedia {
return
}
self.isLoadingMore = true
struct LoadResult {
var messagesByDay: [Int32: SparseMessageCalendar.Entry]
var nextOffset: Int32?
var minMessageId: MessageId?
var minTimestamp: Int32?
}
let account = self.account
let accountPeerId = account.peerId
let peerId = self.peerId
let messageTag = self.messageTag
self.disposable.set((self.account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(peerId)
}
|> mapToSignal { peer -> Signal<LoadResult, NoError> in
guard let peer = peer else {
return .single(LoadResult(messagesByDay: [:], nextOffset: nil, minMessageId: nil, minTimestamp: nil))
}
guard let inputPeer = apiInputPeer(peer) else {
return .single(LoadResult(messagesByDay: [:], nextOffset: nil, minMessageId: nil, minTimestamp: nil))
}
guard let messageFilter = messageFilterForTagMask(messageTag) else {
return .single(LoadResult(messagesByDay: [:], nextOffset: nil, minMessageId: nil, minTimestamp: nil))
}
//TODO:api
return self.account.network.request(Api.functions.messages.getSearchResultsCalendar(flags: 0, peer: inputPeer, savedPeerId: nil, filter: messageFilter, offsetId: nextRequestOffset, offsetDate: 0))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.SearchResultsCalendar?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<LoadResult, NoError> in
return account.postbox.transaction { transaction -> LoadResult in
guard let result = result else {
return LoadResult(messagesByDay: [:], nextOffset: nil, minMessageId: nil, minTimestamp: nil)
}
switch result {
case let .searchResultsCalendar(_, _, minDate, minMsgId, _, periods, messages, chats, users):
var parsedMessages: [StoreMessage] = []
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
for message in messages {
if let parsedMessage = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peer.isForumOrMonoForum) {
parsedMessages.append(parsedMessage)
}
}
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
let _ = transaction.addMessages(parsedMessages, location: .Random)
var minMessageId: Int32?
var messagesByDay: [Int32: SparseMessageCalendar.Entry] = [:]
for period in periods {
switch period {
case let .searchResultsCalendarPeriod(date, minMsgId, _, count):
if let message = transaction.getMessage(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: minMsgId)) {
messagesByDay[date] = SparseMessageCalendar.Entry(message: message, count: Int(count))
}
if let minMessageIdValue = minMessageId {
if minMsgId < minMessageIdValue {
minMessageId = minMsgId
}
} else {
minMessageId = minMsgId
}
}
}
return LoadResult(messagesByDay: messagesByDay, nextOffset: minMessageId, minMessageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: minMsgId), minTimestamp: minDate)
}
}
}
}
|> deliverOn(self.queue)).start(next: { [weak self] result in
guard let strongSelf = self else {
return
}
if let minTimestamp = result.minTimestamp {
strongSelf.state.minTimestamp = minTimestamp
}
strongSelf.state.nextRequestOffset = result.nextOffset
for (timestamp, entry) in result.messagesByDay {
strongSelf.state.messagesByDay[timestamp] = entry
}
strongSelf.statePromise.set(.single(strongSelf.state))
strongSelf.isLoadingMore = false
}))
}
}
public struct Entry {
public var message: Message
public var count: Int
}
public struct State {
public var messagesByDay: [Int32: Entry]
public var minTimestamp: Int32?
public var hasMore: Bool
}
private let queue: Queue
private let impl: QueueLocalObject<Impl>
public var minTimestamp: Int32?
private var disposable: Disposable?
init(account: Account, peerId: PeerId, threadId: Int64?, messageTag: MessageTags, displayMedia: Bool) {
let queue = Queue()
self.queue = queue
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, account: account, peerId: peerId, threadId: threadId, messageTag: messageTag, displayMedia: displayMedia)
})
self.disposable = self.state.start(next: { [weak self] state in
self?.minTimestamp = state.minTimestamp
})
}
deinit {
self.disposable?.dispose()
}
public var state: Signal<State, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.statePromise.get().start(next: { state in
subscriber.putNext(State(
messagesByDay: state.messagesByDay,
minTimestamp: state.minTimestamp,
hasMore: state.nextRequestOffset != nil
))
}))
}
return disposable
}
}
public var isLoadingMore: Signal<Bool, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.isLoadingMoreSignal.start(next: subscriber.putNext))
}
return disposable
}
}
public func loadMore() {
self.impl.with { impl in
impl.maybeLoadMore()
}
}
public func removeMessagesInRange(minTimestamp: Int32, maxTimestamp: Int32, type: InteractiveHistoryClearingType, completion: @escaping () -> Void) -> Disposable {
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.removeMessagesInRange(minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, type: type, completion: completion))
}
return disposable
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,106 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public final class TimeZoneList: Codable, Equatable {
public final class Item: Codable, Equatable {
public let id: String
public let title: String
public let utcOffset: Int32
public init(id: String, title: String, utcOffset: Int32) {
self.id = id
self.title = title
self.utcOffset = utcOffset
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs === rhs {
return true
}
if lhs.id != rhs.id {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.utcOffset != rhs.utcOffset {
return false
}
return true
}
}
public let items: [Item]
public let hashValue: Int32
public init(items: [Item], hashValue: Int32) {
self.items = items
self.hashValue = hashValue
}
public static func ==(lhs: TimeZoneList, rhs: TimeZoneList) -> Bool {
if lhs === rhs {
return true
}
if lhs.items != rhs.items {
return false
}
if lhs.hashValue != rhs.hashValue {
return false
}
return true
}
}
func _internal_cachedTimeZoneList(account: Account) -> Signal<TimeZoneList?, NoError> {
let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.timezoneList()]))
return account.postbox.combinedView(keys: [viewKey])
|> map { views -> TimeZoneList? in
guard let view = views.views[viewKey] as? PreferencesView else {
return nil
}
guard let value = view.values[PreferencesKeys.timezoneList()]?.get(TimeZoneList.self) else {
return nil
}
return value
}
}
func _internal_keepCachedTimeZoneListUpdated(account: Account) -> Signal<Never, NoError> {
let updateSignal = _internal_cachedTimeZoneList(account: account)
|> take(1)
|> mapToSignal { list -> Signal<Never, NoError> in
return account.network.request(Api.functions.help.getTimezonesList(hash: list?.hashValue ?? 0))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.help.TimezonesList?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Never, NoError> in
guard let result else {
return .complete()
}
return account.postbox.transaction { transaction in
switch result {
case let .timezonesList(timezones, hash):
var items: [TimeZoneList.Item] = []
for item in timezones {
switch item {
case let .timezone(id, name, utcOffset):
items.append(TimeZoneList.Item(id: id, title: name, utcOffset: utcOffset))
}
}
transaction.setPreferencesEntry(key: PreferencesKeys.timezoneList(), value: PreferencesEntry(TimeZoneList(items: items, hashValue: hash)))
case .timezonesListNotModified:
break
}
}
|> ignoreValues
}
}
return updateSignal
}
@@ -0,0 +1,80 @@
import Foundation
import TelegramApi
import Postbox
import SwiftSignalKit
import MtProtoKit
public enum RequestUpdateTodoMessageError {
case generic
}
func _internal_requestUpdateTodoMessageItems(account: Account, messageId: MessageId, completedIds: [Int32], incompletedIds: [Int32]) -> Signal<Never, RequestUpdateTodoMessageError> {
return account.postbox.transaction { transaction -> Signal<Never, RequestUpdateTodoMessageError> in
guard let peer = transaction.getPeer(messageId.peerId), let inputPeer = apiInputPeer(peer) else {
return .complete()
}
var completedBy = account.peerId
if messageId.peerId.namespace == Namespaces.Peer.CloudChannel, let cachedChannelData = transaction.getPeerCachedData(peerId: messageId.peerId) as? CachedChannelData, let sendAsPeerId = cachedChannelData.sendAsPeerId {
completedBy = sendAsPeerId
}
transaction.updateMessage(messageId, update: { currentMessage in
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
var media: [Media] = []
if let todo = currentMessage.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo {
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
var updatedCompletions = todo.completions
for id in completedIds {
updatedCompletions.append(TelegramMediaTodo.Completion(id: id, date: timestamp, completedBy: completedBy))
}
updatedCompletions.removeAll(where: { incompletedIds.contains($0.id) })
media = [todo.withUpdated(completions: updatedCompletions)]
}
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: media))
})
return account.network.request(Api.functions.messages.toggleTodoCompleted(peer: inputPeer, msgId: messageId.id, completed: completedIds, incompleted: incompletedIds))
|> mapError { _ -> RequestUpdateTodoMessageError in
return .generic
}
|> map { result in
account.stateManager.addUpdates(result)
}
|> ignoreValues
}
|> castError(RequestUpdateTodoMessageError.self)
|> switchToLatest
|> ignoreValues
}
public enum AppendTodoMessageError {
case generic
}
func _internal_appendTodoMessageItems(account: Account, messageId: MessageId, items: [TelegramMediaTodo.Item]) -> Signal<Never, AppendTodoMessageError> {
return account.postbox.transaction { transaction -> Signal<Never, AppendTodoMessageError> in
guard let peer = transaction.getPeer(messageId.peerId), let inputPeer = apiInputPeer(peer) else {
return .complete()
}
transaction.updateMessage(messageId, update: { currentMessage in
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
var media: [Media] = []
if let todo = currentMessage.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo {
var updatedItems = todo.items
updatedItems.append(contentsOf: items)
media = [todo.withUpdated(items: updatedItems)]
}
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: media))
})
return account.network.request(Api.functions.messages.appendTodoList(peer: inputPeer, msgId: messageId.id, list: items.map { $0.apiItem }))
|> mapError { _ -> AppendTodoMessageError in
return .generic
}
|> map { result in
account.stateManager.addUpdates(result)
}
|> ignoreValues
}
|> castError(AppendTodoMessageError.self)
|> switchToLatest
|> ignoreValues
}
@@ -0,0 +1,175 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public enum EngineAudioTranscriptionResult {
case success
case error
}
private enum InternalAudioTranscriptionResult {
case success(Api.messages.TranscribedAudio)
case error(AudioTranscriptionMessageAttribute.TranscriptionError)
case limitExceeded(Int32)
}
func _internal_transcribeAudio(postbox: Postbox, network: Network, messageId: MessageId) -> Signal<EngineAudioTranscriptionResult, NoError> {
return postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<EngineAudioTranscriptionResult, NoError> in
guard let inputPeer = inputPeer else {
return .single(.error)
}
return network.request(Api.functions.messages.transcribeAudio(peer: inputPeer, msgId: messageId.id))
|> map { result -> InternalAudioTranscriptionResult in
return .success(result)
}
|> `catch` { error -> Signal<InternalAudioTranscriptionResult, NoError> in
let mappedError: AudioTranscriptionMessageAttribute.TranscriptionError
if error.errorDescription.hasPrefix("FLOOD_WAIT_") {
if let range = error.errorDescription.range(of: "_", options: .backwards) {
if let value = Int32(error.errorDescription[range.upperBound...]) {
return .single(.limitExceeded(value))
}
}
mappedError = .generic
} else if error.errorDescription == "MSG_VOICE_TOO_LONG" {
mappedError = .tooLong
} else {
mappedError = .generic
}
return .single(.error(mappedError))
}
|> mapToSignal { result -> Signal<EngineAudioTranscriptionResult, NoError> in
return postbox.transaction { transaction -> EngineAudioTranscriptionResult in
let updatedAttribute: AudioTranscriptionMessageAttribute
switch result {
case let .success(transcribedAudio):
switch transcribedAudio {
case let .transcribedAudio(flags, transcriptionId, text, trialRemainingCount, trialUntilDate):
let isPending = (flags & (1 << 0)) != 0
updatedAttribute = AudioTranscriptionMessageAttribute(id: transcriptionId, text: text, isPending: isPending, didRate: false, error: nil)
_internal_updateAudioTranscriptionTrialState(transaction: transaction) { current in
var updated = current
if let trialRemainingCount = trialRemainingCount, trialRemainingCount > 0 {
updated = updated.withUpdatedRemainingCount(trialRemainingCount)
} else if let trialUntilDate = trialUntilDate {
updated = updated.withUpdatedCooldownUntilTime(trialUntilDate)
} else {
updated = updated.withUpdatedCooldownUntilTime(nil)
}
return updated
}
}
case let .error(error):
updatedAttribute = AudioTranscriptionMessageAttribute(id: 0, text: "", isPending: false, didRate: false, error: error)
case let .limitExceeded(timeout):
let cooldownTime = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + timeout
_internal_updateAudioTranscriptionTrialState(transaction: transaction) { current in
var updated = current
updated = updated.withUpdatedCooldownUntilTime(cooldownTime)
return updated
}
return .error
}
transaction.updateMessage(messageId, update: { currentMessage in
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
var attributes = currentMessage.attributes.filter { !($0 is AudioTranscriptionMessageAttribute) }
attributes.append(updatedAttribute)
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
if updatedAttribute.error == nil {
return .success
} else {
return .error
}
}
}
}
}
func _internal_rateAudioTranscription(postbox: Postbox, network: Network, messageId: MessageId, id: Int64, isGood: Bool) -> Signal<Never, NoError> {
return postbox.transaction { transaction -> Api.InputPeer? in
transaction.updateMessage(messageId, update: { currentMessage in
var storeForwardInfo: StoreMessageForwardInfo?
if let forwardInfo = currentMessage.forwardInfo {
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
}
var attributes = currentMessage.attributes
for i in 0 ..< attributes.count {
if let attribute = attributes[i] as? AudioTranscriptionMessageAttribute {
attributes[i] = attribute.withDidRate()
}
}
return .update(StoreMessage(
id: currentMessage.id,
customStableId: nil,
globallyUniqueId: currentMessage.globallyUniqueId,
groupingKey: currentMessage.groupingKey,
threadId: currentMessage.threadId,
timestamp: currentMessage.timestamp,
flags: StoreMessageFlags(currentMessage.flags),
tags: currentMessage.tags,
globalTags: currentMessage.globalTags,
localTags: currentMessage.localTags,
forwardInfo: storeForwardInfo,
authorId: currentMessage.author?.id,
text: currentMessage.text,
attributes: attributes,
media: currentMessage.media
))
})
return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<Never, NoError> in
guard let inputPeer = inputPeer else {
return .complete()
}
return network.request(Api.functions.messages.rateTranscribedAudio(peer: inputPeer, msgId: messageId.id, transcriptionId: id, good: isGood ? .boolTrue : .boolFalse))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> ignoreValues
}
}
public enum AudioTranscription {
public struct TrialState: Equatable, Codable {
public let cooldownUntilTime: Int32?
public let remainingCount: Int32
func withUpdatedCooldownUntilTime(_ time: Int32?) -> AudioTranscription.TrialState {
return AudioTranscription.TrialState(cooldownUntilTime: time, remainingCount: time != nil ? 0 : max(1, self.remainingCount))
}
func withUpdatedRemainingCount(_ remainingCount: Int32) -> AudioTranscription.TrialState {
return AudioTranscription.TrialState(remainingCount: remainingCount)
}
public init(cooldownUntilTime: Int32? = nil, remainingCount: Int32) {
self.cooldownUntilTime = cooldownUntilTime
self.remainingCount = remainingCount
}
public static var defaultValue: AudioTranscription.TrialState {
return AudioTranscription.TrialState(
cooldownUntilTime: nil,
remainingCount: 1
)
}
}
}
func _internal_updateAudioTranscriptionTrialState(transaction: Transaction, _ f: (AudioTranscription.TrialState) -> AudioTranscription.TrialState) {
let current = transaction.getPreferencesEntry(key: PreferencesKeys.audioTranscriptionTrialState)?.get(AudioTranscription.TrialState.self) ?? .defaultValue
transaction.setPreferencesEntry(key: PreferencesKeys.audioTranscriptionTrialState, value: PreferencesEntry(f(current)))
}
@@ -0,0 +1,336 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public enum TranslationError {
case generic
case invalidMessageId
case textIsEmpty
case textTooLong
case invalidLanguage
case limitExceeded
case tryAlternative
}
func _internal_translate(network: Network, text: String, toLang: String, entities: [MessageTextEntity] = []) -> Signal<(String, [MessageTextEntity])?, TranslationError> {
var flags: Int32 = 0
flags |= (1 << 1)
return network.request(Api.functions.messages.translateText(flags: flags, peer: nil, id: nil, text: [.textWithEntities(text: text, entities: apiEntitiesFromMessageTextEntities(entities, associatedPeers: SimpleDictionary()))], toLang: toLang))
|> mapError { error -> TranslationError in
if error.errorDescription.hasPrefix("FLOOD_WAIT") {
return .limitExceeded
} else if error.errorDescription == "MSG_ID_INVALID" {
return .invalidMessageId
} else if error.errorDescription == "INPUT_TEXT_EMPTY" {
return .textIsEmpty
} else if error.errorDescription == "INPUT_TEXT_TOO_LONG" {
return .textTooLong
} else if error.errorDescription == "TO_LANG_INVALID" {
return .invalidLanguage
} else if error.errorDescription == "TRANSLATIONS_DISABLED_ALT" {
return .tryAlternative
} else {
return .generic
}
}
|> mapToSignal { result -> Signal<(String, [MessageTextEntity])?, TranslationError> in
switch result {
case let .translateResult(results):
if case let .textWithEntities(text, entities) = results.first {
return .single((text, messageTextEntitiesFromApiEntities(entities)))
} else {
return .single(nil)
}
}
}
}
func _internal_translateTexts(network: Network, texts: [(String, [MessageTextEntity])], toLang: String) -> Signal<[(String, [MessageTextEntity])], TranslationError> {
var flags: Int32 = 0
flags |= (1 << 1)
var apiTexts: [Api.TextWithEntities] = []
for text in texts {
apiTexts.append(.textWithEntities(text: text.0, entities: apiEntitiesFromMessageTextEntities(text.1, associatedPeers: SimpleDictionary())))
}
return network.request(Api.functions.messages.translateText(flags: flags, peer: nil, id: nil, text: apiTexts, toLang: toLang))
|> mapError { error -> TranslationError in
if error.errorDescription.hasPrefix("FLOOD_WAIT") {
return .limitExceeded
} else if error.errorDescription == "MSG_ID_INVALID" {
return .invalidMessageId
} else if error.errorDescription == "INPUT_TEXT_EMPTY" {
return .textIsEmpty
} else if error.errorDescription == "INPUT_TEXT_TOO_LONG" {
return .textTooLong
} else if error.errorDescription == "TO_LANG_INVALID" {
return .invalidLanguage
} else {
return .generic
}
}
|> mapToSignal { result -> Signal<[(String, [MessageTextEntity])], TranslationError> in
var texts: [(String, [MessageTextEntity])] = []
switch result {
case let .translateResult(results):
for result in results {
if case let .textWithEntities(text, entities) = result {
texts.append((text, messageTextEntitiesFromApiEntities(entities)))
}
}
}
return .single(texts)
}
}
func _internal_translateMessages(account: Account, messageIds: [EngineMessage.Id], fromLang: String?, toLang: String, enableLocalIfPossible: Bool) -> Signal<Never, TranslationError> {
var signals: [Signal<Void, TranslationError>] = []
for (peerId, messageIds) in messagesIdsGroupedByPeerId(messageIds) {
signals.append(_internal_translateMessagesByPeerId(account: account, peerId: peerId, messageIds: messageIds, fromLang: fromLang, toLang: toLang, enableLocalIfPossible: enableLocalIfPossible))
}
return combineLatest(signals)
|> ignoreValues
}
public protocol ExperimentalInternalTranslationService: AnyObject {
func translate(texts: [AnyHashable: String], fromLang: String, toLang: String) -> Signal<[AnyHashable: String]?, NoError>
}
public var engineExperimentalInternalTranslationService: ExperimentalInternalTranslationService?
private func _internal_translateMessagesByPeerId(account: Account, peerId: EnginePeer.Id, messageIds: [EngineMessage.Id], fromLang: String?, toLang: String, enableLocalIfPossible: Bool) -> Signal<Void, TranslationError> {
return account.postbox.transaction { transaction -> (Api.InputPeer?, [Message]) in
return (transaction.getPeer(peerId).flatMap(apiInputPeer), messageIds.compactMap({ transaction.getMessage($0) }))
}
|> castError(TranslationError.self)
|> mapToSignal { (inputPeer, messages) -> Signal<Void, TranslationError> in
guard let inputPeer = inputPeer else {
return .never()
}
let polls = messages.compactMap { message in
if let poll = message.media.first as? TelegramMediaPoll {
return (poll, message.id)
} else {
return nil
}
}
let pollSignals = polls.map { (poll, id) in
var texts: [(String, [MessageTextEntity])] = []
texts.append((poll.text, poll.textEntities))
for option in poll.options {
texts.append((option.text, option.entities))
}
if let solution = poll.results.solution {
texts.append((solution.text, solution.entities))
}
return _internal_translateTexts(network: account.network, texts: texts, toLang: toLang)
}
let audioTranscriptions = messages.compactMap { message in
if let audioTranscription = message.attributes.first(where: { $0 is AudioTranscriptionMessageAttribute }) as? AudioTranscriptionMessageAttribute, !audioTranscription.text.isEmpty && !audioTranscription.isPending {
return (audioTranscription.text, message.id)
} else {
return nil
}
}
let audioTranscriptionsSignals = audioTranscriptions.map { (text, id) in
return _internal_translate(network: account.network, text: text, toLang: toLang)
}
var flags: Int32 = 0
flags |= (1 << 0)
let id: [Int32] = messageIds.map { $0.id }
let msgs: Signal<Api.messages.TranslatedText?, TranslationError>
if id.isEmpty {
msgs = .single(nil)
} else {
if enableLocalIfPossible, let engineExperimentalInternalTranslationService, let fromLang {
msgs = account.postbox.transaction { transaction -> [MessageId: String] in
var texts: [MessageId: String] = [:]
for messageId in messageIds {
if let message = transaction.getMessage(messageId) {
texts[message.id] = message.text
}
}
return texts
}
|> castError(TranslationError.self)
|> mapToSignal { messageTexts -> Signal<Api.messages.TranslatedText?, TranslationError> in
var mappedTexts: [AnyHashable: String] = [:]
for (id, text) in messageTexts {
mappedTexts[AnyHashable(id)] = text
}
return engineExperimentalInternalTranslationService.translate(texts: mappedTexts, fromLang: fromLang, toLang: toLang)
|> castError(TranslationError.self)
|> mapToSignal { resultTexts -> Signal<Api.messages.TranslatedText?, TranslationError> in
guard let resultTexts else {
return .fail(.generic)
}
var result: [Api.TextWithEntities] = []
for messageId in messageIds {
if let text = resultTexts[AnyHashable(messageId)] {
result.append(.textWithEntities(text: text, entities: []))
} else if let text = messageTexts[messageId] {
result.append(.textWithEntities(text: text, entities: []))
} else {
result.append(.textWithEntities(text: "", entities: []))
}
}
return .single(.translateResult(result: result))
}
}
} else {
msgs = account.network.request(Api.functions.messages.translateText(flags: flags, peer: inputPeer, id: id, text: nil, toLang: toLang))
|> map(Optional.init)
|> mapError { error -> TranslationError in
if error.errorDescription.hasPrefix("FLOOD_WAIT") {
return .limitExceeded
} else if error.errorDescription == "MSG_ID_INVALID" {
return .invalidMessageId
} else if error.errorDescription == "INPUT_TEXT_EMPTY" {
return .textIsEmpty
} else if error.errorDescription == "INPUT_TEXT_TOO_LONG" {
return .textTooLong
} else if error.errorDescription == "TO_LANG_INVALID" {
return .invalidLanguage
} else {
return .generic
}
}
}
}
return combineLatest(msgs, combineLatest(pollSignals), combineLatest(audioTranscriptionsSignals))
|> mapToSignal { (result, pollResults, audioTranscriptionsResults) -> Signal<Void, TranslationError> in
return account.postbox.transaction { transaction in
if case let .translateResult(results) = result {
var index = 0
for result in results {
let messageId = messageIds[index]
if case let .textWithEntities(text, entities) = result {
let updatedAttribute: TranslationMessageAttribute = TranslationMessageAttribute(text: text, entities: messageTextEntitiesFromApiEntities(entities), toLang: toLang)
transaction.updateMessage(messageId, update: { currentMessage in
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
var attributes = currentMessage.attributes.filter { !($0 is TranslationMessageAttribute) }
attributes.append(updatedAttribute)
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
}
index += 1
}
}
if !pollResults.isEmpty {
for (i, poll) in polls.enumerated() {
let result = pollResults[i]
if !result.isEmpty {
transaction.updateMessage(poll.1, update: { currentMessage in
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
var attributes = currentMessage.attributes.filter { !($0 is TranslationMessageAttribute) }
var attrOptions: [TranslationMessageAttribute.Additional] = []
for (i, _) in poll.0.options.enumerated() {
var translated = result.count > i + 1 ? result[i + 1] : (poll.0.options[i].text, poll.0.options[i].entities)
if translated.0.isEmpty {
translated = (poll.0.options[i].text, poll.0.options[i].entities)
}
attrOptions.append(TranslationMessageAttribute.Additional(text: translated.0, entities: translated.1))
}
let solution: TranslationMessageAttribute.Additional?
if result.count > 1 + poll.0.options.count, !result[result.count - 1].0.isEmpty {
solution = TranslationMessageAttribute.Additional(text: result[result.count - 1].0, entities: result[result.count - 1].1)
} else {
solution = nil
}
let title = result[0].0.isEmpty ? (poll.0.text, poll.0.textEntities) : result[0]
let updatedAttribute: TranslationMessageAttribute = TranslationMessageAttribute(text: title.0, entities: title.1, additional: attrOptions, pollSolution: solution, toLang: toLang)
attributes.append(updatedAttribute)
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
}
}
}
if !audioTranscriptionsResults.isEmpty {
for (i, audioTranscription) in audioTranscriptions.enumerated() {
if let result = audioTranscriptionsResults[i] {
transaction.updateMessage(audioTranscription.1, update: { currentMessage in
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
var attributes = currentMessage.attributes.filter { !($0 is TranslationMessageAttribute) }
let updatedAttribute: TranslationMessageAttribute = TranslationMessageAttribute(text: result.0, entities: result.1, additional: [], pollSolution: nil, toLang: toLang)
attributes.append(updatedAttribute)
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
}
}
}
}
|> castError(TranslationError.self)
}
}
}
func _internal_togglePeerMessagesTranslationHidden(account: Account, peerId: EnginePeer.Id, hidden: Bool) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Api.InputPeer? in
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in
if let cachedData = cachedData as? CachedUserData {
var updatedFlags = cachedData.flags
if hidden {
updatedFlags.insert(.translationHidden)
} else {
updatedFlags.remove(.translationHidden)
}
return cachedData.withUpdatedFlags(updatedFlags)
} else if let cachedData = cachedData as? CachedGroupData {
var updatedFlags = cachedData.flags
if hidden {
updatedFlags.insert(.translationHidden)
} else {
updatedFlags.remove(.translationHidden)
}
return cachedData.withUpdatedFlags(updatedFlags)
} else if let cachedData = cachedData as? CachedChannelData {
var updatedFlags = cachedData.flags
if hidden {
updatedFlags.insert(.translationHidden)
} else {
updatedFlags.remove(.translationHidden)
}
return cachedData.withUpdatedFlags(updatedFlags)
} else {
return cachedData
}
})
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<Never, NoError> in
guard let inputPeer = inputPeer else {
return .never()
}
var flags: Int32 = 0
if hidden {
flags |= (1 << 0)
}
return account.network.request(Api.functions.messages.togglePeerTranslations(flags: flags, peer: inputPeer))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Bool?, NoError> in
return .single(nil)
}
|> ignoreValues
}
}
@@ -0,0 +1,223 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public enum UpdatePinnedMessageError {
case generic
}
public enum PinnedMessageUpdate {
case pin(id: MessageId, silent: Bool, forThisPeerOnlyIfPossible: Bool)
case clear(id: MessageId)
}
func _internal_requestUpdatePinnedMessage(account: Account, peerId: PeerId, update: PinnedMessageUpdate) -> Signal<Void, UpdatePinnedMessageError> {
return account.postbox.transaction { transaction -> (Peer?, CachedPeerData?) in
return (transaction.getPeer(peerId), transaction.getPeerCachedData(peerId: peerId))
}
|> mapError { _ -> UpdatePinnedMessageError in
}
|> mapToSignal { peer, cachedPeerData -> Signal<Void, UpdatePinnedMessageError> in
guard let peer = peer, let inputPeer = apiInputPeer(peer) else {
return .fail(.generic)
}
if let channel = peer as? TelegramChannel {
let canManagePin = channel.hasPermission(.pinMessages)
if !canManagePin {
return .fail(.generic)
}
} else if let group = peer as? TelegramGroup {
switch group.role {
case .creator, .admin:
break
default:
if let defaultBannedRights = group.defaultBannedRights {
if defaultBannedRights.flags.contains(.banPinMessages) {
return .fail(.generic)
}
}
}
} else if let _ = peer as? TelegramUser, let cachedPeerData = cachedPeerData as? CachedUserData {
if !cachedPeerData.canPinMessages {
return .fail(.generic)
}
}
var flags: Int32 = 0
let messageId: Int32
switch update {
case let .pin(id, silent, forThisPeerOnlyIfPossible):
messageId = id.id
if silent {
flags |= (1 << 0)
}
if forThisPeerOnlyIfPossible {
flags |= (1 << 2)
}
case let .clear(id):
messageId = id.id
flags |= 1 << 1
}
let request = Api.functions.messages.updatePinnedMessage(flags: flags, peer: inputPeer, id: messageId)
return account.network.request(request)
|> mapError { _ -> UpdatePinnedMessageError in
return .generic
}
|> mapToSignal { updates -> Signal<Void, UpdatePinnedMessageError> in
account.stateManager.addUpdates(updates)
return account.postbox.transaction { transaction in
switch updates {
case let .updates(updates, _, _, _, _):
if updates.isEmpty {
if peerId.namespace == Namespaces.Peer.CloudChannel {
let messageId: MessageId
switch update {
case let .pin(id, _, _):
messageId = id
case let .clear(id):
messageId = id
}
transaction.updateMessage(messageId, update: { currentMessage in
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
var updatedTags = currentMessage.tags
switch update {
case .pin:
updatedTags.insert(.pinned)
case .clear:
updatedTags.remove(.pinned)
}
if updatedTags == currentMessage.tags {
return .skip
}
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: updatedTags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: currentMessage.media))
})
}
}
default:
break
}
}
|> mapError { _ -> UpdatePinnedMessageError in
}
}
}
}
func _internal_requestUnpinAllMessages(account: Account, peerId: PeerId, threadId: Int64?) -> Signal<Never, UpdatePinnedMessageError> {
return account.postbox.transaction { transaction -> (Peer?, Peer?, CachedPeerData?) in
let peer = transaction.getPeer(peerId)
var subPeer: Peer?
if let channel = peer as? TelegramChannel, channel.isMonoForum, let threadId {
subPeer = transaction.getPeer(PeerId(threadId))
}
return (peer, subPeer, transaction.getPeerCachedData(peerId: peerId))
}
|> mapError { _ -> UpdatePinnedMessageError in
}
|> mapToSignal { peer, subPeer, cachedPeerData -> Signal<Never, UpdatePinnedMessageError> in
guard let peer = peer, let inputPeer = apiInputPeer(peer) else {
return .fail(.generic)
}
if let channel = peer as? TelegramChannel {
let canManagePin = channel.hasPermission(.pinMessages)
if !canManagePin {
return .fail(.generic)
}
} else if let group = peer as? TelegramGroup {
switch group.role {
case .creator, .admin:
break
default:
if let defaultBannedRights = group.defaultBannedRights {
if defaultBannedRights.flags.contains(.banPinMessages) {
return .fail(.generic)
}
}
}
} else if let _ = peer as? TelegramUser, let cachedPeerData = cachedPeerData as? CachedUserData {
if !cachedPeerData.canPinMessages {
return .fail(.generic)
}
}
enum InternalError {
case error(String)
case restart
}
var flags: Int32 = 0
var topMsgId: Int32?
var savedPeerId: Api.InputPeer?
if let threadId {
if let channel = peer as? TelegramChannel, channel.isMonoForum {
if let inputSubPeer = subPeer.flatMap(apiInputPeer) {
flags |= (1 << 1)
savedPeerId = inputSubPeer
}
} else {
flags |= (1 << 0)
topMsgId = Int32(clamping: threadId)
}
}
let request: Signal<Never, InternalError> = account.network.request(Api.functions.messages.unpinAllMessages(flags: flags, peer: inputPeer, topMsgId: topMsgId, savedPeerId: savedPeerId))
|> mapError { error -> InternalError in
return .error(error.errorDescription)
}
|> mapToSignal { result -> Signal<Bool, InternalError> in
switch result {
case let .affectedHistory(_, _, count):
if count != 0 {
return .fail(.restart)
}
}
return .single(true)
}
|> retry(retryOnError: { error -> Bool in
switch error {
case .restart:
return true
default:
return false
}
}, delayIncrement: 0.0, maxDelay: 0.0, maxRetries: 100, onQueue: .concurrentDefaultQueue())
|> mapToSignal { _ -> Signal<Never, InternalError> in
let signal: Signal<Never, InternalError> = account.postbox.transaction { transaction -> Void in
for index in transaction.getMessageIndicesWithTag(peerId: peerId, threadId: nil, namespace: Namespaces.Message.Cloud, tag: .pinned) {
transaction.updateMessage(index.id, update: { currentMessage in
var storeForwardInfo: StoreMessageForwardInfo?
if let forwardInfo = currentMessage.forwardInfo {
storeForwardInfo = StoreMessageForwardInfo(forwardInfo)
}
var tags = currentMessage.tags
tags.remove(.pinned)
if tags == currentMessage.tags {
return .skip
}
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: currentMessage.media))
})
}
}
|> castError(InternalError.self)
|> ignoreValues
return signal
}
return request
|> mapError { _ -> UpdatePinnedMessageError in
return .generic
}
}
}