import Foundation import SwiftSignalKit import Postbox import TelegramApi public enum EngineOutgoingMessageContent { case text(String, [MessageTextEntity]) case file(FileMediaReference) case contextResult(ChatContextResultCollection, ChatContextResult) } public final class StoryPreloadInfo { public enum Priority: Comparable { case top(position: Int) case next(position: Int) } public let peer: PeerReference public let storyId: Int32 public let media: EngineMedia public let reactions: [MessageReaction.Reaction] public let priority: Priority public init( peer: PeerReference, storyId: Int32, media: EngineMedia, reactions: [MessageReaction.Reaction], priority: Priority ) { self.peer = peer self.storyId = storyId self.media = media self.reactions = reactions self.priority = priority } } public extension TelegramEngine { final class Messages { private let account: Account init(account: Account) { self.account = account } public func clearCloudDraftsInteractively() -> Signal { return _internal_clearCloudDraftsInteractively(postbox: self.account.postbox, network: self.account.network, accountPeerId: self.account.peerId) } public func applyMaxReadIndexInteractively(index: MessageIndex) -> Signal { return _internal_applyMaxReadIndexInteractively(postbox: self.account.postbox, stateManager: self.account.stateManager, index: index) } public func sendScheduledMessageNowInteractively(messageId: MessageId) -> Signal { return _internal_sendScheduledMessageNowInteractively(postbox: self.account.postbox, messageId: messageId) } public func requestMessageActionCallbackPasswordCheck(messageId: MessageId, isGame: Bool, data: MemoryBuffer?) -> Signal { return _internal_requestMessageActionCallbackPasswordCheck(account: self.account, messageId: messageId, isGame: isGame, data: data) } public func requestMessageActionCallback(messageId: MessageId, isGame: Bool, password: String?, data: MemoryBuffer?) -> Signal { return _internal_requestMessageActionCallback(account: self.account, messageId: messageId, isGame: isGame, password: password, data: data) } public func requestMessageActionUrlAuth(subject: MessageActionUrlSubject) -> Signal { _internal_requestMessageActionUrlAuth(account: self.account, subject: subject) } public func acceptMessageActionUrlAuth(subject: MessageActionUrlSubject, allowWriteAccess: Bool) -> Signal { return _internal_acceptMessageActionUrlAuth(account: self.account, subject: subject, allowWriteAccess: allowWriteAccess) } public func searchMessages(location: SearchMessagesLocation, query: String, state: SearchMessagesState?, centerId: MessageId? = nil, limit: Int32 = 100) -> Signal<(SearchMessagesResult, SearchMessagesState), NoError> { return _internal_searchMessages(account: self.account, location: location, query: query, state: state, centerId: centerId, limit: limit) } public func searchHashtagPosts(hashtag: String, state: SearchMessagesState?, limit: Int32 = 100) -> Signal<(SearchMessagesResult, SearchMessagesState), NoError> { return _internal_searchHashtagPosts(account: self.account, hashtag: hashtag, state: state, limit: limit) } public func downloadMessage(messageId: MessageId) -> Signal { return _internal_downloadMessage(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, messageId: messageId) } public func searchMessageIdByTimestamp(peerId: PeerId, threadId: Int64?, timestamp: Int32) -> Signal { return _internal_searchMessageIdByTimestamp(account: self.account, peerId: peerId, threadId: threadId, timestamp: timestamp) } public func deleteMessages(transaction: Transaction, ids: [MessageId]) { return _internal_deleteMessages(transaction: transaction, mediaBox: self.account.postbox.mediaBox, ids: ids, deleteMedia: true, manualAddMessageThreadStatsDifference: nil) } public func deleteAllMessagesWithAuthor(peerId: PeerId, authorId: PeerId, namespace: MessageId.Namespace) -> Signal { return self.account.postbox.transaction { transaction -> Void in _internal_deleteAllMessagesWithAuthor(transaction: transaction, mediaBox: self.account.postbox.mediaBox, peerId: peerId, authorId: authorId, namespace: namespace) } |> ignoreValues } public func deleteAllMessagesWithForwardAuthor(peerId: EnginePeer.Id, forwardAuthorId: EnginePeer.Id, namespace: MessageId.Namespace) -> Signal { return self.account.postbox.transaction { transaction -> Void in _internal_deleteAllMessagesWithForwardAuthor(transaction: transaction, mediaBox: self.account.postbox.mediaBox, peerId: peerId, forwardAuthorId: forwardAuthorId, namespace: namespace) } |> ignoreValues } public func clearCallHistory(forEveryone: Bool) -> Signal { return _internal_clearCallHistory(account: self.account, forEveryone: forEveryone) } public func deleteMessagesInteractively(messageIds: [MessageId], type: InteractiveMessagesDeletionType, deleteAllInGroup: Bool = false) -> Signal { self.account.stateManager.messagesRemovedContext.addIsMessagesDeletedInteractively(ids: messageIds.map { id -> DeletedMessageId in if id.namespace == Namespaces.Message.Cloud && (id.peerId.namespace == Namespaces.Peer.CloudUser || id.peerId.namespace == Namespaces.Peer.CloudGroup) { return .global(id.id) } else { return .messageId(id) } }) return _internal_deleteMessagesInteractively(account: self.account, messageIds: messageIds, type: type, deleteAllInGroup: deleteAllInGroup) } public func clearHistoryInteractively(peerId: PeerId, threadId: Int64?, type: InteractiveHistoryClearingType) -> Signal { return _internal_clearHistoryInteractively(postbox: self.account.postbox, peerId: peerId, threadId: threadId, type: type) } public func clearAuthorHistory(peerId: PeerId, memberId: PeerId) -> Signal { return _internal_clearAuthorHistory(account: self.account, peerId: peerId, memberId: memberId) } public func requestEditMessage(messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute? = nil, invertMediaAttribute: InvertMediaMessageAttribute? = nil, disableUrlPreview: Bool = false, scheduleTime: Int32? = nil) -> Signal { return _internal_requestEditMessage(account: self.account, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime, invertMediaAttribute: invertMediaAttribute) } public func requestEditLiveLocation(messageId: MessageId, stop: Bool, coordinate: (latitude: Double, longitude: Double, accuracyRadius: Int32?)?, heading: Int32?, proximityNotificationRadius: Int32?, extendPeriod: Int32?) -> Signal { return _internal_requestEditLiveLocation(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, messageId: messageId, stop: stop, coordinate: coordinate, heading: heading, proximityNotificationRadius: proximityNotificationRadius, extendPeriod: extendPeriod) } public func addSecretChatMessageScreenshot(peerId: PeerId) -> Signal { return _internal_addSecretChatMessageScreenshot(account: self.account, peerId: peerId) |> ignoreValues } public func forwardGameWithScore(messageId: MessageId, to peerId: PeerId, threadId: Int64?, as senderPeerId: PeerId?) -> Signal { return _internal_forwardGameWithScore(account: self.account, messageId: messageId, to: peerId, threadId: threadId, as: senderPeerId) } public func requestUpdatePinnedMessage(peerId: PeerId, update: PinnedMessageUpdate) -> Signal { return _internal_requestUpdatePinnedMessage(account: self.account, peerId: peerId, update: update) } public func requestUnpinAllMessages(peerId: PeerId, threadId: Int64?) -> Signal { return _internal_requestUnpinAllMessages(account: self.account, peerId: peerId, threadId: threadId) } public func fetchChannelReplyThreadMessage(messageId: MessageId, atMessageId: MessageId?) -> Signal { return _internal_fetchChannelReplyThreadMessage(account: self.account, messageId: messageId, atMessageId: atMessageId) } public func requestStartBot(botPeerId: PeerId, payload: String?) -> Signal { return _internal_requestStartBot(account: self.account, botPeerId: botPeerId, payload: payload) } public func requestStartBotInGroup(botPeerId: PeerId, groupPeerId: PeerId, payload: String?) -> Signal { return _internal_requestStartBotInGroup(account: self.account, botPeerId: botPeerId, groupPeerId: groupPeerId, payload: payload) } public func markAllChatsAsRead() -> Signal { return _internal_markAllChatsAsRead(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager) } public func getMessagesLoadIfNecessary(_ messageIds: [MessageId], strategy: GetMessagesStrategy = .cloud(skipLocal: false)) -> Signal { return _internal_getMessagesLoadIfNecessary(messageIds, postbox: self.account.postbox, network: self.account.network, accountPeerId: self.account.peerId, strategy: strategy) } public func markMessageContentAsConsumedInteractively(messageId: MessageId) -> Signal { return _internal_markMessageContentAsConsumedInteractively(postbox: self.account.postbox, messageId: messageId) } public func installInteractiveReadMessagesAction(peerId: PeerId) -> Disposable { return _internal_installInteractiveReadMessagesAction(postbox: self.account.postbox, stateManager: self.account.stateManager, peerId: peerId) } public func installInteractiveReadReactionsAction(peerId: PeerId, getVisibleRange: @escaping () -> VisibleMessageRange?, didReadReactionsInMessages: @escaping ([MessageId: [ReactionsMessageAttribute.RecentPeer]]) -> Void) -> Disposable { return _internal_installInteractiveReadReactionsAction(postbox: self.account.postbox, stateManager: self.account.stateManager, peerId: peerId, getVisibleRange: getVisibleRange, didReadReactionsInMessages: didReadReactionsInMessages) } public func requestMessageSelectPollOption(messageId: MessageId, opaqueIdentifiers: [Data]) -> Signal { return _internal_requestMessageSelectPollOption(account: self.account, messageId: messageId, opaqueIdentifiers: opaqueIdentifiers) } public func requestClosePoll(messageId: MessageId) -> Signal { return _internal_requestClosePoll(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, messageId: messageId) } public func pollResults(messageId: MessageId, poll: TelegramMediaPoll) -> PollResultsContext { return PollResultsContext(account: self.account, messageId: messageId, poll: poll) } public func earliestUnseenPersonalMentionMessage(peerId: PeerId, threadId: Int64?) -> Signal { let account = self.account return _internal_earliestUnseenPersonalMentionMessage(account: self.account, peerId: peerId, threadId: threadId) |> mapToSignal { result -> Signal in switch result { case .loading: return .single(result) case let .result(messageId): if messageId == nil { let _ = clearPeerUnseenPersonalMessagesInteractively(account: account, peerId: peerId, threadId: threadId).start() } return .single(result) } } } public func earliestUnseenPersonalReactionMessage(peerId: PeerId, threadId: Int64?) -> Signal { let account = self.account return _internal_earliestUnseenPersonalReactionMessage(account: self.account, peerId: peerId, threadId: threadId) |> mapToSignal { result -> Signal in switch result { case .loading: return .single(result) case let .result(messageId): if messageId == nil { let _ = clearPeerUnseenReactionsInteractively(account: account, peerId: peerId, threadId: threadId).start() } return .single(result) } } } public func exportMessageLink(peerId: PeerId, messageId: MessageId, isThread: Bool = false) -> Signal { return _internal_exportMessageLink(postbox: self.account.postbox, network: self.account.network, peerId: peerId, messageId: messageId, isThread: isThread) } public func enqueueOutgoingMessage( to peerId: EnginePeer.Id, replyTo replyToMessageId: EngineMessageReplySubject?, threadId: Int64? = nil, storyId: StoryId? = nil, content: EngineOutgoingMessageContent, silentPosting: Bool = false, scheduleTime: Int32? = nil ) -> Signal<[MessageId?], NoError> { var message: EnqueueMessage? if case let .contextResult(results, result) = content { message = self.outgoingMessageWithChatContextResult(to: peerId, threadId: nil, botId: results.botId, result: result, replyToMessageId: replyToMessageId, replyToStoryId: storyId, hideVia: true, silentPosting: silentPosting, scheduleTime: scheduleTime, correlationId: nil) } else { var attributes: [MessageAttribute] = [] if silentPosting { attributes.append(NotificationInfoMessageAttribute(flags: .muted)) } if let scheduleTime = scheduleTime { attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: scheduleTime)) } var text: String = "" var mediaReference: AnyMediaReference? switch content { case let .text(textValue, entities): if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } text = textValue case let .file(fileReference): mediaReference = fileReference.abstract default: fatalError() } message = .message( text: text, attributes: attributes, inlineStickers: [:], mediaReference: mediaReference, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: storyId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [] ) } guard let message = message else { return .complete() } return enqueueMessages( account: self.account, peerId: peerId, messages: [message] ) } public func enqueueOutgoingMessageWithChatContextResult(to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: EngineMessageReplySubject? = nil, replyToStoryId: StoryId? = nil, hideVia: Bool = false, silentPosting: Bool = false, scheduleTime: Int32? = nil, correlationId: Int64? = nil) -> Bool { return _internal_enqueueOutgoingMessageWithChatContextResult(account: self.account, to: peerId, threadId: threadId, botId: botId, result: result, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime, correlationId: correlationId) } public func outgoingMessageWithChatContextResult(to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: EngineMessageReplySubject?, replyToStoryId: StoryId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, correlationId: Int64?) -> EnqueueMessage? { return _internal_outgoingMessageWithChatContextResult(to: peerId, threadId: threadId, botId: botId, result: result, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime, correlationId: correlationId) } public func setMessageReactions( ids: [EngineMessage.Id], reactions: [UpdateMessageReaction] ) { let _ = updateMessageReactionsInteractively( account: self.account, messageIds: ids, reactions: reactions, isLarge: false, storeAsRecentlyUsed: false, add: false ).start() } public func addMessageReactions( ids: [EngineMessage.Id], reactions: [UpdateMessageReaction] ) { let _ = updateMessageReactionsInteractively( account: self.account, messageIds: ids, reactions: reactions, isLarge: false, storeAsRecentlyUsed: false, add: true ).start() } public func requestChatContextResults(botId: PeerId, peerId: PeerId, query: String, location: Signal<(Double, Double)?, NoError> = .single(nil), offset: String, incompleteResults: Bool = false, staleCachedResults: Bool = false) -> Signal { return _internal_requestChatContextResults(account: self.account, botId: botId, peerId: peerId, query: query, location: location, offset: offset, incompleteResults: incompleteResults, staleCachedResults: staleCachedResults) } public func removeRecentlyUsedHashtag(string: String) -> Signal { return _internal_removeRecentlyUsedHashtag(postbox: self.account.postbox, string: string) } public func recentlyUsedHashtags() -> Signal<[String], NoError> { return _internal_recentlyUsedHashtags(postbox: self.account.postbox) } public func topPeerActiveLiveLocationMessages(peerId: PeerId) -> Signal<(Peer?, [Message]), NoError> { return _internal_topPeerActiveLiveLocationMessages(viewTracker: self.account.viewTracker, accountPeerId: self.account.peerId, peerId: peerId) } public func chatList(group: EngineChatList.Group, count: Int) -> Signal { let accountPeerId = self.account.peerId return self.account.postbox.tailChatListView(groupId: group._asGroup(), count: count, summaryComponents: ChatListEntrySummaryComponents()) |> map { view -> EngineChatList in return EngineChatList(view.0, accountPeerId: accountPeerId) } } public func callList(scope: EngineCallList.Scope, index: EngineMessage.Index, itemCount: Int) -> Signal { return self.account.viewTracker.callListView( type: scope == .all ? .all : .missed, index: index, count: itemCount ) |> map { view -> EngineCallList in return EngineCallList( items: view.entries.map { entry -> EngineCallList.Item in switch entry { case let .message(message, group): return .message(message: EngineMessage(message), group: group.map(EngineMessage.init)) case let .hole(index): return .hole(index) } }, hasEarlier: view.earlier != nil, hasLater: view.later != nil ) } } public func adMessages(peerId: PeerId) -> AdMessagesHistoryContext { return AdMessagesHistoryContext(account: self.account, peerId: peerId) } public func messageReadStats(id: MessageId) -> Signal { return _internal_messageReadStats(account: self.account, id: id) } public func requestCancelLiveLocation(ids: [MessageId]) -> Signal { return self.account.postbox.transaction { transaction -> Void in for id in ids { transaction.updateMessage(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 updatedMedia = currentMessage.media let timestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) for i in 0 ..< updatedMedia.count { if let media = updatedMedia[i] as? TelegramMediaMap, let _ = media.liveBroadcastingTimeout { updatedMedia[i] = TelegramMediaMap(latitude: media.latitude, longitude: media.longitude, heading: media.heading, accuracyRadius: media.accuracyRadius, venue: media.venue, liveBroadcastingTimeout: max(0, timestamp - currentMessage.timestamp - 1), liveProximityNotificationRadius: nil) } } return .update(StoreMessage(id: currentMessage.id, 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: updatedMedia)) }) } } |> ignoreValues } public func activeLiveLocationMessages() -> Signal<[EngineMessage], NoError> { let viewKey: PostboxViewKey = .localMessageTag(.OutgoingLiveLocation) return self.account.postbox.combinedView(keys: [viewKey]) |> map { view in if let view = view.views[viewKey] as? LocalMessageTagsView { return view.messages.values.map(EngineMessage.init) } else { return [] } } } public func sparseMessageList(peerId: EnginePeer.Id, threadId: Int64?, tag: EngineMessage.Tags) -> SparseMessageList { return SparseMessageList(account: self.account, peerId: peerId, threadId: threadId, messageTag: tag) } public func sparseMessageCalendar(peerId: EnginePeer.Id, threadId: Int64?, tag: EngineMessage.Tags, displayMedia: Bool) -> SparseMessageCalendar { return SparseMessageCalendar(account: self.account, peerId: peerId, threadId: threadId, messageTag: tag, displayMedia: displayMedia) } public func refreshMessageTagStats(peerId: EnginePeer.Id, threadId: Int64?, tags: [EngineMessage.Tags]) -> Signal { let account = self.account return self.account.postbox.transaction { transaction -> (Api.InputPeer?, Api.InputPeer?) in var inputSavedPeer: Api.InputPeer? if let threadId = threadId { if peerId == account.peerId { inputSavedPeer = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer) } } return (transaction.getPeer(peerId).flatMap(apiInputPeer), inputSavedPeer) } |> mapToSignal { inputPeer, inputSavedPeer -> Signal in guard let inputPeer = inputPeer else { return .complete() } var signals: [Signal<(count: Int32?, topId: Int32?), NoError>] = [] for tag in tags { guard let filter = messageFilterForTagMask(tag) else { signals.append(.single((nil, nil))) continue } var flags: Int32 = 0 var topMsgId: Int32? if let threadId = threadId { if peerId == account.peerId { if inputSavedPeer != nil { flags |= (1 << 2) } } else { flags |= (1 << 1) topMsgId = Int32(clamping: threadId) } } signals.append(self.account.network.request(Api.functions.messages.search(flags: flags, peer: inputPeer, q: "", fromId: nil, savedPeerId: inputSavedPeer, savedReaction: nil, topMsgId: topMsgId, filter: filter, minDate: 0, maxDate: 0, offsetId: 0, addOffset: 0, limit: 1, maxId: 0, minId: 0, hash: 0)) |> map { result -> (count: Int32?, topId: Int32?) in switch result { case let .messagesSlice(_, count, _, _, messages, _, _): return (count, messages.first?.id(namespace: Namespaces.Message.Cloud)?.id) case let .channelMessages(_, _, count, _, messages, _, _, _): return (count, messages.first?.id(namespace: Namespaces.Message.Cloud)?.id) case let .messages(messages, _, _): return (Int32(messages.count), messages.first?.id(namespace: Namespaces.Message.Cloud)?.id) case .messagesNotModified: return (nil, nil) } } |> `catch` { _ -> Signal<(count: Int32?, topId: Int32?), NoError> in return .single((nil, nil)) }) } return combineLatest(signals) |> mapToSignal { counts -> Signal in return account.postbox.transaction { transaction in for i in 0 ..< tags.count { let (count, maxId) = counts[i] if let count = count { if count == 0, peerId == account.peerId, let threadId { var localCount = 0 var maxId: Int32 = 1 transaction.scanMessages(peerId: peerId, threadId: threadId, namespace: Namespaces.Message.Cloud, tag: tags[i], { message in localCount += 1 maxId = max(maxId, message.id.id) return true }) transaction.replaceMessageTagSummary(peerId: peerId, threadId: threadId, tagMask: tags[i], namespace: Namespaces.Message.Cloud, customTag: nil, count: Int32(localCount), maxId: maxId) } else { transaction.replaceMessageTagSummary(peerId: peerId, threadId: threadId, tagMask: tags[i], namespace: Namespaces.Message.Cloud, customTag: nil, count: count, maxId: maxId ?? 1) } } } } |> ignoreValues } } } public func messageReactionList(message: EngineMessage, readStats: MessageReadStats?, reaction: MessageReaction.Reaction?) -> EngineMessageReactionListContext { return EngineMessageReactionListContext(account: self.account, message: message, readStats: readStats, reaction: reaction) } public func translate(text: String, toLang: String, entities: [MessageTextEntity] = []) -> Signal<(String, [MessageTextEntity])?, TranslationError> { return _internal_translate(network: self.account.network, text: text, toLang: toLang, entities: entities) } public func translate(texts: [(String, [MessageTextEntity])], toLang: String) -> Signal<[(String, [MessageTextEntity])], TranslationError> { return _internal_translate_texts(network: self.account.network, texts: texts, toLang: toLang) } public func translateMessages(messageIds: [EngineMessage.Id], toLang: String) -> Signal { return _internal_translateMessages(account: self.account, messageIds: messageIds, toLang: toLang) } public func togglePeerMessagesTranslationHidden(peerId: EnginePeer.Id, hidden: Bool) -> Signal { return _internal_togglePeerMessagesTranslationHidden(account: self.account, peerId: peerId, hidden: hidden) } public func transcribeAudio(messageId: MessageId) -> Signal { return _internal_transcribeAudio(postbox: self.account.postbox, network: self.account.network, messageId: messageId) } public func storeLocallyTranscribedAudio(messageId: MessageId, text: String, isFinal: Bool, error: AudioTranscriptionMessageAttribute.TranscriptionError?) -> Signal { return self.account.postbox.transaction { transaction -> Void in transaction.updateMessage(messageId, update: { currentMessage in let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init) var attributes = currentMessage.attributes.filter { !($0 is AudioTranscriptionMessageAttribute) } attributes.append(AudioTranscriptionMessageAttribute(id: 0, text: text, isPending: !isFinal, didRate: false, error: error)) return .update(StoreMessage(id: currentMessage.id, 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)) }) } |> ignoreValues } public func storeLocallyDerivedData(messageId: MessageId, data: [String: CodableEntry]) -> Signal { return self.account.postbox.transaction { transaction -> Void in transaction.updateMessage(messageId, update: { currentMessage in let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init) var attributes = currentMessage.attributes.filter { !($0 is DerivedDataMessageAttribute) } if !data.isEmpty { attributes.append(DerivedDataMessageAttribute(data: data)) } return .update(StoreMessage(id: currentMessage.id, 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)) }) } |> ignoreValues } public func rateAudioTranscription(messageId: MessageId, id: Int64, isGood: Bool) -> Signal { return _internal_rateAudioTranscription(postbox: self.account.postbox, network: self.account.network, messageId: messageId, id: id, isGood: isGood) } public func requestWebView(peerId: PeerId, botId: PeerId, url: String?, payload: String?, themeParams: [String: Any]?, fromMenu: Bool, replyToMessageId: MessageId?, threadId: Int64?) -> Signal { return _internal_requestWebView(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, peerId: peerId, botId: botId, url: url, payload: payload, themeParams: themeParams, fromMenu: fromMenu, replyToMessageId: replyToMessageId, threadId: threadId) } public func requestSimpleWebView(botId: PeerId, url: String?, source: RequestSimpleWebViewSource, themeParams: [String: Any]?) -> Signal { return _internal_requestSimpleWebView(postbox: self.account.postbox, network: self.account.network, botId: botId, url: url, source: source, themeParams: themeParams) } public func requestAppWebView(peerId: PeerId, appReference: BotAppReference, payload: String?, themeParams: [String: Any]?, compact: Bool, allowWrite: Bool) -> Signal { return _internal_requestAppWebView(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, peerId: peerId, appReference: appReference, payload: payload, themeParams: themeParams, compact: compact, allowWrite: allowWrite) } public func sendWebViewData(botId: PeerId, buttonText: String, data: String) -> Signal { return _internal_sendWebViewData(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, botId: botId, buttonText: buttonText, data: data) } public func canBotSendMessages(botId: PeerId) -> Signal { return _internal_canBotSendMessages(postbox: self.account.postbox, network: self.account.network, botId: botId) } public func allowBotSendMessages(botId: PeerId) -> Signal { return _internal_allowBotSendMessages(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, botId: botId) } public func invokeBotCustomMethod(botId: PeerId, method: String, params: String) -> Signal { return _internal_invokeBotCustomMethod(postbox: self.account.postbox, network: self.account.network, botId: botId, method: method, params: params) } public func refreshAttachMenuBots() { let _ = managedSynchronizeAttachMenuBots(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, force: true).startStandalone() } public func addBotToAttachMenu(botId: PeerId, allowWrite: Bool) -> Signal { return _internal_addBotToAttachMenu(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, botId: botId, allowWrite: allowWrite) } public func removeBotFromAttachMenu(botId: PeerId) -> Signal { return _internal_removeBotFromAttachMenu(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, botId: botId) } public func acceptAttachMenuBotDisclaimer(botId: PeerId) -> Signal { return _internal_acceptAttachMenuBotDisclaimer(postbox: self.account.postbox, botId: botId) } public func getAttachMenuBot(botId: PeerId, cached: Bool = false) -> Signal { return _internal_getAttachMenuBot(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, botId: botId, cached: cached) } public func attachMenuBots() -> Signal<[AttachMenuBot], NoError> { return _internal_attachMenuBots(postbox: self.account.postbox) } public func getBotApp(botId: PeerId, shortName: String, cached: Bool = false) -> Signal { return _internal_getBotApp(account: self.account, reference: .shortName(peerId: botId, shortName: shortName)) } public func ensureMessagesAreLocallyAvailable(messages: [EngineMessage]) { let _ = self.account.postbox.transaction({ transaction in for message in messages { _internal_storeMessageFromSearch(transaction: transaction, message: message._asMessage()) } }).start() } public func findRandomMessage(peerId: EnginePeer.Id, namespace: EngineMessage.Id.Namespace, tag: EngineMessage.Tags, ignoreIds: ([EngineMessage.Id], Set)) -> Signal { return self.account.postbox.transaction { transaction -> EngineMessage.Index? in return transaction.findRandomMessage(peerId: peerId, namespace: namespace, tag: tag, ignoreIds: ignoreIds) } } public func failedMessageGroup(id: EngineMessage.Id) -> Signal<[EngineMessage], NoError> { return self.account.postbox.transaction { transaction -> [EngineMessage] in return transaction.getMessageFailedGroup(id)?.map(EngineMessage.init) ?? [] } } public func unreadChatListPeerIds(groupId: EngineChatList.Group, filterPredicate: ChatListFilterPredicate?) -> Signal<[EnginePeer.Id], NoError> { return self.account.postbox.transaction { transaction -> [EnginePeer.Id] in return transaction.getUnreadChatListPeerIds(groupId: groupId._asGroup(), filterPredicate: filterPredicate, additionalFilter: nil, stopOnFirstMatch: false) } } public func markAllChatsAsReadInteractively(items: [(groupId: EngineChatList.Group, filterPredicate: ChatListFilterPredicate?)]) -> Signal { let account = self.account return self.account.postbox.transaction { transaction -> Void in for (groupId, filterPredicate) in items { _internal_markAllChatsAsReadInteractively(transaction: transaction, network: self.account.network, viewTracker: account.viewTracker, groupId: groupId._asGroup(), filterPredicate: filterPredicate) } } |> ignoreValues } public func getRelativeUnreadChatListIndex(filtered: Bool, position: EngineChatList.RelativePosition, groupId: EngineChatList.Group) -> Signal { guard let position = position._asPosition() else { return .single(nil) } return self.account.postbox.transaction { transaction -> EngineChatList.Item.Index? in return transaction.getRelativeUnreadChatListIndex(filtered: filtered, position: position, groupId: groupId._asGroup()).flatMap(EngineChatList.Item.Index.chatList) } } public func togglePeersUnreadMarkInteractively(peerIds: [EnginePeer.Id], setToValue: Bool?) -> Signal { return self.account.postbox.transaction { transaction -> Void in for peerId in peerIds { _internal_togglePeerUnreadMarkInteractively(transaction: transaction, network: self.account.network, viewTracker: self.account.viewTracker, peerId: peerId, setToValue: setToValue) } } |> ignoreValues } public func markForumThreadAsRead(peerId: EnginePeer.Id, threadId: Int64) -> Signal { return self.account.postbox.transaction { transaction -> Void in _internal_markForumThreadAsReadInteractively(transaction: transaction, network: self.account.network, viewTracker: self.account.viewTracker, peerId: peerId, threadId: threadId) } |> ignoreValues } public func markForumThreadsAsRead(peerId: EnginePeer.Id, threadIds: [Int64]) -> Signal { return self.account.postbox.transaction { transaction -> Void in for threadId in threadIds { _internal_markForumThreadAsReadInteractively(transaction: transaction, network: self.account.network, viewTracker: self.account.viewTracker, peerId: peerId, threadId: threadId) } } |> ignoreValues } public func searchForumTopics(peerId: EnginePeer.Id, query: String) -> Signal<[EngineChatList.Item], NoError> { return _internal_searchForumTopics(account: self.account, peerId: peerId, query: query) } public func editMessageFactCheck(messageId: EngineMessage.Id, text: String, entities: [MessageTextEntity]) -> Signal { return _internal_editMessageFactCheck(account: self.account, messageId: messageId, text: text, entities: entities) } public func deleteMessageFactCheck(messageId: EngineMessage.Id) -> Signal { return _internal_deleteMessageFactCheck(account: self.account, messageId: messageId) } public func getMessagesFactCheck(messageIds: [EngineMessage.Id]) -> Signal { return _internal_getMessagesFactCheck(account: self.account, messageIds: messageIds) } public func debugAddHoles() -> Signal { return self.account.postbox.transaction { transaction -> Void in transaction.addHolesEverywhere(peerNamespaces: [Namespaces.Peer.CloudUser, Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel], holeNamespace: Namespaces.Message.Cloud) } |> ignoreValues } public func debugResetTagHoles() -> Signal { return self.account.postbox.transaction { transaction -> Void in transaction.resetCustomTagHoles() } |> ignoreValues } public func debugReindexUnreadCounters() -> Signal { return self.account.postbox.transaction { transaction -> Void in transaction.reindexUnreadCounters() } |> ignoreValues } public func keepMessageCountersSyncrhonized(peerId: EnginePeer.Id, threadId: Int64) -> Signal { return managedSynchronizeMessageHistoryTagSummaries(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, peerId: peerId, threadId: threadId) |> ignoreValues } public func keepMessageCountersSyncrhonized(peerId: EnginePeer.Id) -> Signal { return managedSynchronizeMessageHistoryTagSummaries(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, peerId: peerId, threadId: nil) |> ignoreValues } public func getSynchronizeAutosaveItemOperations() -> Signal<[(index: Int32, message: Message, mediaId: MediaId)], NoError> { return self.account.postbox.transaction { transaction -> [(index: Int32, message: Message, mediaId: MediaId)] in return _internal_getSynchronizeAutosaveItemOperations(transaction: transaction) } } func removeSyncrhonizeAutosaveItemOperations(indices: [Int32]) { let _ = (self.account.postbox.transaction { transaction -> Void in _internal_removeSyncrhonizeAutosaveItemOperations(transaction: transaction, indices: indices) }).start() } public func peerStoriesAreReady(id: EnginePeer.Id, minId: Int32) -> Signal { return self.account.postbox.combinedView(keys: [ PostboxViewKey.storyItems(peerId: id) ]) |> map { views -> Bool in guard let view = views.views[PostboxViewKey.storyItems(peerId: id)] as? StoryItemsView else { return false } return view.items.contains(where: { item in return item.id >= minId }) } } public func storySubscriptions(isHidden: Bool, tempKeepNewlyArchived: Bool = false) -> Signal { return `deferred` { () -> Signal in let debugTimerSignal: Signal #if DEBUG && false debugTimerSignal = Signal.single(true) |> then( Signal.single(true) |> delay(1.0, queue: .mainQueue()) |> then( Signal.single(false) |> delay(1.0, queue: .mainQueue()) ) |> restart ) #else debugTimerSignal = .single(true) #endif let previousIdList = Atomic>(value: Set()) let subscriptionsKey: PostboxStorySubscriptionsKey = isHidden ? .hidden : .filtered let basicPeerKey = PostboxViewKey.basicPeer(self.account.peerId) let storySubscriptionsKey = PostboxViewKey.storySubscriptions(key: subscriptionsKey) return combineLatest(debugTimerSignal |> distinctUntilChanged, self.account.postbox.combinedView(keys: [ basicPeerKey, storySubscriptionsKey, PostboxViewKey.storiesState( key: .subscriptions(subscriptionsKey) ), PostboxViewKey.storiesState( key: .local ) ])) |> mapToSignal { debugTimer, views -> Signal in guard let basicPeerView = views.views[basicPeerKey] as? BasicPeerView, let accountPeer = basicPeerView.peer else { return .single(EngineStorySubscriptions(accountItem: nil, items: [], hasMoreToken: nil)) } guard let storySubscriptionsView = views.views[storySubscriptionsKey] as? StorySubscriptionsView else { return .single(EngineStorySubscriptions(accountItem: nil, items: [], hasMoreToken: nil)) } guard let storiesStateView = views.views[PostboxViewKey.storiesState(key: .subscriptions(subscriptionsKey))] as? StoryStatesView else { return .single(EngineStorySubscriptions(accountItem: nil, items: [], hasMoreToken: nil)) } var additionalDataKeys: [PostboxViewKey] = [] additionalDataKeys.append(PostboxViewKey.storyItems(peerId: self.account.peerId)) additionalDataKeys.append(PostboxViewKey.storiesState(key: .peer(self.account.peerId))) additionalDataKeys.append(PostboxViewKey.storiesState(key: .local)) var subscriptionPeerIds = storySubscriptionsView.peerIds.filter { $0 != self.account.peerId } if !debugTimer { subscriptionPeerIds.removeAll() } if tempKeepNewlyArchived { let updatedList = previousIdList.modify { list in var list = list list.formUnion(subscriptionPeerIds) return list } for id in updatedList { if !subscriptionPeerIds.contains(id) { subscriptionPeerIds.append(id) } } } additionalDataKeys.append(contentsOf: subscriptionPeerIds.map { peerId -> PostboxViewKey in return PostboxViewKey.storyItems(peerId: peerId) }) additionalDataKeys.append(contentsOf: subscriptionPeerIds.map { peerId -> PostboxViewKey in return PostboxViewKey.storiesState(key: .peer(peerId)) }) var additionalPeerIds = subscriptionPeerIds if let view = views.views[PostboxViewKey.storiesState(key: .local)] as? StoryStatesView, let localState = view.value?.get(Stories.LocalState.self) { for item in localState.items { if case let .peer(id) = item.target { if !additionalPeerIds.contains(id) { additionalPeerIds.append(id) } } } } additionalDataKeys.append(contentsOf: additionalPeerIds.map { peerId -> PostboxViewKey in return PostboxViewKey.basicPeer(peerId) }) return self.account.postbox.combinedView(keys: additionalDataKeys) |> map { views -> EngineStorySubscriptions in let _ = accountPeer var hasMoreToken: String? if let subscriptionsState = storiesStateView.value?.get(Stories.SubscriptionsState.self) { if subscriptionsState.hasMore { hasMoreToken = subscriptionsState.opaqueState + "_\(subscriptionsState.refreshId)" } else { hasMoreToken = nil } } else { hasMoreToken = "" } var localState: Stories.LocalState? if let view = views.views[PostboxViewKey.storiesState(key: .local)] as? StoryStatesView { localState = view.value?.get(Stories.LocalState.self) } var accountPendingItemCount = 0 if let localState = localState { for item in localState.items { if case .myStories = item.target { accountPendingItemCount += 1 } } } var accountItem: EngineStorySubscriptions.Item = EngineStorySubscriptions.Item( peer: EnginePeer(accountPeer), hasUnseen: false, hasUnseenCloseFriends: false, hasPending: accountPendingItemCount != 0, storyCount: accountPendingItemCount, unseenCount: 0, lastTimestamp: 0 ) var items: [EngineStorySubscriptions.Item] = [] do { let peerId = self.account.peerId if let itemsView = views.views[PostboxViewKey.storyItems(peerId: peerId)] as? StoryItemsView, let stateView = views.views[PostboxViewKey.storiesState(key: .peer(peerId))] as? StoryStatesView { if let lastEntry = itemsView.items.last?.value.get(Stories.StoredItem.self) { let peerState: Stories.PeerState? = stateView.value?.get(Stories.PeerState.self) var hasUnseen = false var hasUnseenCloseFriends = false var unseenCount = 0 if let peerState = peerState { hasUnseen = peerState.maxReadId < lastEntry.id for item in itemsView.items { if item.id > peerState.maxReadId { unseenCount += 1 } if case let .item(item) = item.value.get(Stories.StoredItem.self) { if item.id > peerState.maxReadId { if item.isCloseFriends { hasUnseenCloseFriends = true } } } } } let item = EngineStorySubscriptions.Item( peer: EnginePeer(accountPeer), hasUnseen: hasUnseen, hasUnseenCloseFriends: hasUnseenCloseFriends, hasPending: accountPendingItemCount != 0, storyCount: itemsView.items.count + accountPendingItemCount, unseenCount: unseenCount, lastTimestamp: lastEntry.timestamp ) accountItem = item } } } for peerId in subscriptionPeerIds { guard let peerView = views.views[PostboxViewKey.basicPeer(peerId)] as? BasicPeerView else { continue } guard let peer = peerView.peer else { continue } var isPeerHidden = false if let user = peer as? TelegramUser { isPeerHidden = user.storiesHidden ?? false } else if let channel = peer as? TelegramChannel { isPeerHidden = channel.storiesHidden ?? false } if isPeerHidden != isHidden { continue } guard let itemsView = views.views[PostboxViewKey.storyItems(peerId: peerId)] as? StoryItemsView else { continue } guard let stateView = views.views[PostboxViewKey.storiesState(key: .peer(peerId))] as? StoryStatesView else { continue } guard let lastEntry = itemsView.items.last?.value.get(Stories.StoredItem.self) else { continue } let peerState: Stories.PeerState? = stateView.value?.get(Stories.PeerState.self) var hasUnseen = false var hasUnseenCloseFriends = false var unseenCount = 0 if let peerState = peerState { hasUnseen = peerState.maxReadId < lastEntry.id for item in itemsView.items { if item.id > peerState.maxReadId { unseenCount += 1 if case let .item(item) = item.value.get(Stories.StoredItem.self) { if item.isCloseFriends { hasUnseenCloseFriends = true } } } } } var maxPendingTimestamp: Int32? if let localState = localState { for item in localState.items { if case .peer(peerId) = item.target { if let maxPendingTimestampValue = maxPendingTimestamp { maxPendingTimestamp = max(maxPendingTimestampValue, item.timestamp) } else { maxPendingTimestamp = item.timestamp } } } } var lastItemTimestamp = lastEntry.timestamp if let maxPendingTimestamp = maxPendingTimestamp, maxPendingTimestamp > lastItemTimestamp { lastItemTimestamp = maxPendingTimestamp } let item = EngineStorySubscriptions.Item( peer: EnginePeer(peer), hasUnseen: hasUnseen, hasUnseenCloseFriends: hasUnseenCloseFriends, hasPending: maxPendingTimestamp != nil, storyCount: itemsView.items.count, unseenCount: unseenCount, lastTimestamp: lastItemTimestamp ) if peerId == accountPeer.id { accountItem = item } else { items.append(item) } } if let localState = localState { for item in localState.items { if case let .peer(peerId) = item.target, !items.contains(where: { $0.peer.id == peerId }) { guard let peerView = views.views[PostboxViewKey.basicPeer(peerId)] as? BasicPeerView else { continue } guard let peer = peerView.peer else { continue } var isPeerHidden = false if let user = peer as? TelegramUser { isPeerHidden = user.storiesHidden ?? false } else if let channel = peer as? TelegramChannel { isPeerHidden = channel.storiesHidden ?? false } if isPeerHidden != isHidden { continue } let item = EngineStorySubscriptions.Item( peer: EnginePeer(peer), hasUnseen: false, hasUnseenCloseFriends: false, hasPending: true, storyCount: 0, unseenCount: 0, lastTimestamp: 0 ) items.append(item) } } } items.sort(by: { lhs, rhs in let lhsUnseenOrPending = lhs.hasUnseen || lhs.hasPending let rhsUnseenOrPending = rhs.hasUnseen || rhs.hasPending if lhsUnseenOrPending != rhsUnseenOrPending { if lhsUnseenOrPending { return true } else { return false } } if lhs.peer.isService != rhs.peer.isService { if lhs.peer.isService { return true } else { return false } } if lhs.peer.isPremium != rhs.peer.isPremium { if lhs.peer.isPremium { return true } else { return false } } if lhs.lastTimestamp != rhs.lastTimestamp { return lhs.lastTimestamp > rhs.lastTimestamp } return lhs.peer.id < rhs.peer.id }) if !isHidden { assert(true) } return EngineStorySubscriptions(accountItem: accountItem, items: items, hasMoreToken: hasMoreToken) } } } } public func preloadStorySubscriptions(isHidden: Bool, preferHighQuality: Signal) -> Signal<[EngineMedia.Id: StoryPreloadInfo], NoError> { let basicPeerKey = PostboxViewKey.basicPeer(self.account.peerId) let subscriptionsKey: PostboxStorySubscriptionsKey = isHidden ? .hidden : .filtered let storySubscriptionsKey = PostboxViewKey.storySubscriptions(key: subscriptionsKey) return combineLatest( self.account.postbox.combinedView(keys: [ basicPeerKey, storySubscriptionsKey, PostboxViewKey.storiesState(key: .subscriptions(subscriptionsKey)) ]), preferHighQuality ) |> mapToSignal { views, preferHighQuality -> Signal<[EngineMedia.Id: StoryPreloadInfo], NoError> in guard let basicPeerView = views.views[basicPeerKey] as? BasicPeerView, let accountPeer = basicPeerView.peer else { return .single([:]) } guard let storySubscriptionsView = views.views[storySubscriptionsKey] as? StorySubscriptionsView else { return .single([:]) } guard let storiesStateView = views.views[PostboxViewKey.storiesState(key: .subscriptions(subscriptionsKey))] as? StoryStatesView else { return .single([:]) } var additionalDataKeys: [PostboxViewKey] = [] additionalDataKeys.append(contentsOf: storySubscriptionsView.peerIds.map { peerId -> PostboxViewKey in return PostboxViewKey.storyItems(peerId: peerId) }) additionalDataKeys.append(contentsOf: storySubscriptionsView.peerIds.map { peerId -> PostboxViewKey in return PostboxViewKey.storiesState(key: .peer(peerId)) }) additionalDataKeys.append(contentsOf: storySubscriptionsView.peerIds.map { peerId -> PostboxViewKey in return PostboxViewKey.basicPeer(peerId) }) return self.account.postbox.combinedView(keys: additionalDataKeys) |> map { views -> [EngineMedia.Id: StoryPreloadInfo] in let _ = accountPeer let _ = storiesStateView var sortedItems: [(peer: Peer, item: Stories.Item, hasUnseen: Bool, lastTimestamp: Int32)] = [] for peerId in storySubscriptionsView.peerIds { guard let peerView = views.views[PostboxViewKey.basicPeer(peerId)] as? BasicPeerView else { continue } guard let peer = peerView.peer else { continue } guard let itemsView = views.views[PostboxViewKey.storyItems(peerId: peerId)] as? StoryItemsView else { continue } guard let stateView = views.views[PostboxViewKey.storiesState(key: .peer(peerId))] as? StoryStatesView else { continue } var nextItem: Stories.StoredItem? = itemsView.items.first?.value.get(Stories.StoredItem.self) let lastTimestamp = itemsView.items.last?.value.get(Stories.StoredItem.self)?.timestamp let peerState: Stories.PeerState? = stateView.value?.get(Stories.PeerState.self) var hasUnseen = false if let peerState = peerState { if let item = itemsView.items.first(where: { $0.id > peerState.maxReadId }) { hasUnseen = true nextItem = item.value.get(Stories.StoredItem.self) } } if let nextItem = nextItem, case let .item(item) = nextItem, let lastTimestamp = lastTimestamp { sortedItems.append((peer, item, hasUnseen, lastTimestamp)) } } sortedItems.sort(by: { lhs, rhs in if lhs.hasUnseen != rhs.hasUnseen { if lhs.hasUnseen { return true } else { return false } } if EnginePeer(lhs.peer).isService != EnginePeer(rhs.peer).isService { if EnginePeer(lhs.peer).isService { return true } else { return false } } if lhs.peer.isPremium != rhs.peer.isPremium { if lhs.peer.isPremium { return true } else { return false } } if lhs.lastTimestamp != rhs.lastTimestamp { return lhs.lastTimestamp > rhs.lastTimestamp } return lhs.peer.id < rhs.peer.id }) var nextPriority: Int = 0 var resultResources: [EngineMedia.Id: StoryPreloadInfo] = [:] for itemAndPeer in sortedItems.prefix(10) { guard let peerReference = PeerReference(itemAndPeer.peer) else { continue } guard let media = itemAndPeer.item.media, let mediaId = media.id else { continue } var reactions: [MessageReaction.Reaction] = [] for mediaArea in itemAndPeer.item.mediaAreas { if case let .reaction(_, reaction, _) = mediaArea { if !reactions.contains(reaction) { reactions.append(reaction) } } } var selectedMedia: EngineMedia if let alternativeMedia = itemAndPeer.item.alternativeMedia.flatMap(EngineMedia.init), (!preferHighQuality && !itemAndPeer.item.isMy) { selectedMedia = alternativeMedia } else { selectedMedia = EngineMedia(media) } resultResources[mediaId] = StoryPreloadInfo( peer: peerReference, storyId: itemAndPeer.item.id, media: selectedMedia, reactions: reactions, priority: .top(position: nextPriority) ) nextPriority += 1 } return resultResources } } } public func refreshStories(peerId: EnginePeer.Id, ids: [Int32]) -> Signal { return _internal_refreshStories(account: self.account, peerId: peerId, ids: ids) } public func refreshStoryViews(peerId: EnginePeer.Id, ids: [Int32]) -> Signal { if peerId != self.account.peerId && peerId.namespace != Namespaces.Peer.CloudChannel { return .complete() } return _internal_getStoryViews(account: self.account, peerId: peerId, ids: ids) |> mapToSignal { views -> Signal in return self.account.postbox.transaction { transaction -> Void in var currentItems = transaction.getStoryItems(peerId: peerId) for i in 0 ..< currentItems.count { if ids.contains(currentItems[i].id) { if case let .item(item) = currentItems[i].value.get(Stories.StoredItem.self) { let updatedItem: Stories.StoredItem = .item(Stories.Item( id: item.id, timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, alternativeMedia: item.alternativeMedia, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, views: views[currentItems[i].id], 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 )) if let entry = CodableEntry(updatedItem) { currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends) } } } } transaction.setStoryItems(peerId: peerId, items: currentItems) } |> ignoreValues } } public func uploadStory(target: Stories.PendingTarget, media: EngineStoryInputMedia, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, period: Int, randomId: Int64, forwardInfo: Stories.PendingForwardInfo?) -> Signal { return _internal_uploadStory(account: self.account, target: target, media: media, mediaAreas: mediaAreas, text: text, entities: entities, pin: pin, privacy: privacy, isForwardingDisabled: isForwardingDisabled, period: period, randomId: randomId, forwardInfo: forwardInfo) } public func allStoriesUploadEvents() -> Signal<(Int32, Int32), NoError> { guard let pendingStoryManager = self.account.pendingStoryManager else { return .complete() } return pendingStoryManager.allStoriesUploadEvents() } public func lookUpPendingStoryIdMapping(peerId: EnginePeer.Id, stableId: Int32) -> Int32? { return self.account.pendingStoryManager?.lookUpPendingStoryIdMapping(peerId: peerId, stableId: stableId) } public func allStoriesUploadProgress() -> Signal<[EnginePeer.Id: Float], NoError> { guard let pendingStoryManager = self.account.pendingStoryManager else { return .single([:]) } return pendingStoryManager.allStoriesUploadProgress } public func storyUploadProgress(stableId: Int32) -> Signal { guard let pendingStoryManager = self.account.pendingStoryManager else { return .single(0.0) } return pendingStoryManager.storyUploadProgress(stableId: stableId) } public func cancelStoryUpload(stableId: Int32) { _internal_cancelStoryUpload(account: self.account, stableId: stableId) } public func editStory(peerId: EnginePeer.Id, id: Int32, media: EngineStoryInputMedia?, mediaAreas: [MediaArea]?, text: String?, entities: [MessageTextEntity]?, privacy: EngineStoryPrivacy?) -> Signal { return _internal_editStory(account: self.account, peerId: peerId, id: id, media: media, mediaAreas: mediaAreas, text: text, entities: entities, privacy: privacy) } public func editStoryPrivacy(id: Int32, privacy: EngineStoryPrivacy) -> Signal { return _internal_editStoryPrivacy(account: self.account, id: id, privacy: privacy) } public func checkStoriesUploadAvailability(target: Stories.PendingTarget) -> Signal { return _internal_checkStoriesUploadAvailability(account: self.account, target: target) } public func deleteStories(peerId: EnginePeer.Id, ids: [Int32]) -> Signal { return _internal_deleteStories(account: self.account, peerId: peerId, ids: ids) } public func markStoryAsSeen(peerId: EnginePeer.Id, id: Int32, asPinned: Bool) -> Signal { return _internal_markStoryAsSeen(account: self.account, peerId: peerId, id: id, asPinned: asPinned) } public func updateStoriesArePinned(peerId: EnginePeer.Id, ids: [Int32: EngineStoryItem], isPinned: Bool) -> Signal { return _internal_updateStoriesArePinned(account: self.account, peerId: peerId, ids: ids, isPinned: isPinned) } public func updatePinnedToTopStories(peerId: EnginePeer.Id, ids: [Int32]) -> Signal { return _internal_updatePinnedToTopStories(account: self.account, peerId: peerId, ids: ids) } public func storyViewList(peerId: EnginePeer.Id, id: Int32, views: EngineStoryItem.Views, listMode: EngineStoryViewListContext.ListMode, sortMode: EngineStoryViewListContext.SortMode, searchQuery: String? = nil, parentSource: EngineStoryViewListContext? = nil) -> EngineStoryViewListContext { return EngineStoryViewListContext(account: self.account, peerId: peerId, storyId: id, views: views, listMode: listMode, sortMode: sortMode, searchQuery: searchQuery, parentSource: parentSource) } public func exportStoryLink(peerId: EnginePeer.Id, id: Int32) -> Signal { return _internal_exportStoryLink(account: self.account, peerId: peerId, id: id) } public func enableStoryStealthMode() -> Signal { return _internal_enableStoryStealthMode(account: self.account) } public func setStoryReaction(peerId: EnginePeer.Id, id: Int32, reaction: MessageReaction.Reaction?) -> Signal { return _internal_setStoryReaction(account: self.account, peerId: peerId, id: id, reaction: reaction) } public func getStory(peerId: EnginePeer.Id, id: Int32) -> Signal { return _internal_getStoryById(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, peerId: peerId, id: id) } public func synchronouslyIsMessageDeletedInteractively(ids: [EngineMessage.Id]) -> [EngineMessage.Id] { return self.account.stateManager.synchronouslyIsMessageDeletedInteractively(ids: ids) } public func synchronouslyLookupCorrelationId(correlationId: Int64) -> EngineMessage.Id? { return self.account.pendingMessageManager.synchronouslyLookupCorrelationId(correlationId: correlationId) } public func savedMessagesPeerListHead() -> Signal { return self.account.postbox.combinedView(keys: [.savedMessagesIndex(peerId: self.account.peerId)]) |> map { views -> EnginePeer.Id? in //TODO:api optimize guard let view = views.views[.savedMessagesIndex(peerId: self.account.peerId)] as? MessageHistorySavedMessagesIndexView else { return nil } if view.isLoading { return nil } else { return view.items.first?.peer?.id } } } public func savedMessagesHasPeersOtherThanSaved() -> Signal { return self.account.postbox.combinedView(keys: [.savedMessagesIndex(peerId: self.account.peerId)]) |> map { views -> Bool in //TODO:api optimize guard let view = views.views[.savedMessagesIndex(peerId: self.account.peerId)] as? MessageHistorySavedMessagesIndexView else { return false } if view.isLoading { return false } else { return view.items.contains(where: { $0.peer?.id != self.account.peerId }) } } } public func savedMessagesPeersStats() -> Signal { return self.account.postbox.combinedView(keys: [.savedMessagesStats(peerId: self.account.peerId)]) |> map { views -> Int? in guard let view = views.views[.savedMessagesStats(peerId: self.account.peerId)] as? MessageHistorySavedMessagesStatsView else { return nil } if view.isLoading { return nil } else { return view.count } } } public func searchLocalSavedMessagesPeers(query: String, indexNameMapping: [EnginePeer.Id: [PeerIndexNameRepresentation]]) -> Signal<[EnginePeer], NoError> { return _internal_searchLocalSavedMessagesPeers(account: self.account, query: query, indexNameMapping: indexNameMapping) } public func internalReindexSavedMessagesCustomTagsIfNeeded(threadId: Int64?, tag: MemoryBuffer) { let _ = self.account.postbox.transaction({ transaction in transaction.reindexSavedMessagesCustomTagsWithTagsIfNeeded(peerId: self.account.peerId, threadId: threadId, tag: tag) }).startStandalone() } public func reportAdMessage(peerId: EnginePeer.Id, opaqueId: Data, option: Data?) -> Signal { return _internal_reportAdMessage(account: self.account, peerId: peerId, opaqueId: opaqueId, option: option) } public func updateExtendedMedia(messageIds: [EngineMessage.Id]) -> Signal { return _internal_updateExtendedMedia(account: self.account, messageIds: messageIds) } public func getAllLocalChannels(count: Int) -> Signal<[EnginePeer.Id], NoError> { return self.account.postbox.transaction { transaction -> [EnginePeer.Id] in var result: [EnginePeer.Id] = [] var peerIds = Set() for id in transaction.chatListGetAllPeerIds(groupId: .root) { if peerIds.contains(id) { continue } peerIds.insert(id) result.append(id) } for id in transaction.chatListGetAllPeerIds(groupId: Namespaces.PeerGroup.archive) { if peerIds.contains(id) { continue } peerIds.insert(id) result.append(id) } var filteredResult: [PeerId] = [] for id in result { if id.namespace != Namespaces.Peer.CloudChannel { continue } guard let peer = transaction.getPeer(id) else { continue } guard let channel = peer as? TelegramChannel, case .broadcast = channel.info else { continue } filteredResult.append(id) if filteredResult.count >= count { break } } return filteredResult } } } }