mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 14:20:20 +00:00
[WIP] Stories
This commit is contained in:
@@ -912,6 +912,7 @@ public class Account {
|
||||
private var resetPeerHoleManagement: ((PeerId) -> Void)?
|
||||
|
||||
public private(set) var pendingMessageManager: PendingMessageManager!
|
||||
private(set) var pendingStoryManager: PendingStoryManager?
|
||||
public private(set) var pendingUpdateMessageManager: PendingUpdateMessageManager!
|
||||
private(set) var messageMediaPreuploadManager: MessageMediaPreuploadManager!
|
||||
private(set) var mediaReferenceRevalidationContext: MediaReferenceRevalidationContext!
|
||||
@@ -1041,6 +1042,11 @@ public class Account {
|
||||
|
||||
self.messageMediaPreuploadManager = MessageMediaPreuploadManager()
|
||||
self.pendingMessageManager = PendingMessageManager(network: network, postbox: postbox, accountPeerId: peerId, auxiliaryMethods: auxiliaryMethods, stateManager: self.stateManager, localInputActivityManager: self.localInputActivityManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, revalidationContext: self.mediaReferenceRevalidationContext)
|
||||
if !supplementary {
|
||||
self.pendingStoryManager = PendingStoryManager(postbox: postbox, network: network, accountPeerId: peerId, stateManager: self.stateManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, revalidationContext: self.mediaReferenceRevalidationContext, auxiliaryMethods: self.auxiliaryMethods)
|
||||
} else {
|
||||
self.pendingStoryManager = nil
|
||||
}
|
||||
self.pendingUpdateMessageManager = PendingUpdateMessageManager(postbox: postbox, network: network, stateManager: self.stateManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, mediaReferenceRevalidationContext: self.mediaReferenceRevalidationContext)
|
||||
self.pendingPeerMediaUploadManager = PendingPeerMediaUploadManager(postbox: postbox, network: network, stateManager: self.stateManager, accountPeerId: self.peerId)
|
||||
|
||||
@@ -1155,6 +1161,7 @@ public class Account {
|
||||
let extractedExpr: [Signal<AccountRunningImportantTasks, NoError>] = [
|
||||
managedSynchronizeChatInputStateOperations(postbox: self.postbox, network: self.network) |> map { $0 ? AccountRunningImportantTasks.other : [] },
|
||||
self.pendingMessageManager.hasPendingMessages |> map { !$0.isEmpty ? AccountRunningImportantTasks.pendingMessages : [] },
|
||||
(self.pendingStoryManager?.hasPending ?? .single(false)) |> map { hasPending in hasPending ? AccountRunningImportantTasks.pendingMessages : [] },
|
||||
self.pendingUpdateMessageManager.updatingMessageMedia |> map { !$0.isEmpty ? AccountRunningImportantTasks.pendingMessages : [] },
|
||||
self.pendingPeerMediaUploadManager.uploadingPeerMedia |> map { !$0.isEmpty ? AccountRunningImportantTasks.pendingMessages : [] },
|
||||
self.accountPresenceManager.isPerformingUpdate() |> map { $0 ? AccountRunningImportantTasks.other : [] },
|
||||
|
||||
@@ -38,6 +38,12 @@ func applyMediaResourceChanges(from: Media, to: Media, postbox: Postbox, force:
|
||||
if let fromVideoThumbnail = fromFile.videoThumbnails.first, let toVideoThumbnail = toFile.videoThumbnails.first, fromVideoThumbnail.resource.id != toVideoThumbnail.resource.id {
|
||||
copyOrMoveResourceData(from: fromVideoThumbnail.resource, to: toVideoThumbnail.resource, mediaBox: postbox.mediaBox)
|
||||
}
|
||||
let videoFirstFrameFromPath = postbox.mediaBox.cachedRepresentationCompletePath(fromFile.resource.id, keepDuration: .general, representationId: "first-frame")
|
||||
let videoFirstFrameToPath = postbox.mediaBox.cachedRepresentationCompletePath(toFile.resource.id, keepDuration: .general, representationId: "first-frame")
|
||||
if FileManager.default.fileExists(atPath: videoFirstFrameFromPath) {
|
||||
let _ = try? FileManager.default.copyItem(atPath: videoFirstFrameFromPath, toPath: videoFirstFrameToPath)
|
||||
}
|
||||
|
||||
if (force || fromFile.size == toFile.size || fromFile.resource.size == toFile.resource.size) && fromFile.mimeType == toFile.mimeType {
|
||||
copyOrMoveResourceData(from: fromFile.resource, to: toFile.resource, mediaBox: postbox.mediaBox)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
import Foundation
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramApi
|
||||
|
||||
public extension Stories {
|
||||
final class PendingItem: Equatable, Codable {
|
||||
private enum CodingKeys: CodingKey {
|
||||
case stableId
|
||||
case timestamp
|
||||
case media
|
||||
case text
|
||||
case entities
|
||||
case pin
|
||||
case privacy
|
||||
case period
|
||||
case randomId
|
||||
}
|
||||
|
||||
public let stableId: Int32
|
||||
public let timestamp: Int32
|
||||
public let media: Media
|
||||
public let text: String
|
||||
public let entities: [MessageTextEntity]
|
||||
public let pin: Bool
|
||||
public let privacy: EngineStoryPrivacy
|
||||
public let period: Int32
|
||||
public let randomId: Int64
|
||||
|
||||
public init(
|
||||
stableId: Int32,
|
||||
timestamp: Int32,
|
||||
media: Media,
|
||||
text: String,
|
||||
entities: [MessageTextEntity],
|
||||
pin: Bool,
|
||||
privacy: EngineStoryPrivacy,
|
||||
period: Int32,
|
||||
randomId: Int64
|
||||
) {
|
||||
self.stableId = stableId
|
||||
self.timestamp = timestamp
|
||||
self.media = media
|
||||
self.text = text
|
||||
self.entities = entities
|
||||
self.pin = pin
|
||||
self.privacy = privacy
|
||||
self.period = period
|
||||
self.randomId = randomId
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
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.text = try container.decode(String.self, forKey: .text)
|
||||
self.entities = try container.decode([MessageTextEntity].self, forKey: .entities)
|
||||
self.pin = try container.decode(Bool.self, forKey: .pin)
|
||||
self.privacy = try container.decode(EngineStoryPrivacy.self, forKey: .privacy)
|
||||
self.period = try container.decode(Int32.self, forKey: .period)
|
||||
self.randomId = try container.decode(Int64.self, forKey: .randomId)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
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.text, forKey: .text)
|
||||
try container.encode(self.entities, forKey: .entities)
|
||||
try container.encode(self.pin, forKey: .pin)
|
||||
try container.encode(self.privacy, forKey: .privacy)
|
||||
try container.encode(self.period, forKey: .period)
|
||||
try container.encode(self.randomId, forKey: .randomId)
|
||||
}
|
||||
|
||||
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.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.period != rhs.period {
|
||||
return false
|
||||
}
|
||||
if lhs.randomId != rhs.randomId {
|
||||
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 storyObserverContexts: [Int32: Bag<(Float) -> Void>] = [:]
|
||||
|
||||
private let allStoriesUploadProgressPromise = ValuePromise<Float?>(nil, ignoreRepeated: true)
|
||||
var allStoriesUploadProgress: Signal<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(1.0)
|
||||
}
|
||||
|
||||
let queue = self.queue
|
||||
return ActionDisposable { [weak self, weak bag] in
|
||||
queue.async {
|
||||
guard let `self` = self else {
|
||||
return
|
||||
}
|
||||
if let 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
|
||||
}
|
||||
|
||||
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 stableId = firstItem.stableId
|
||||
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, stableId: stableId, media: firstItem.media, text: firstItem.text, entities: firstItem.entities, pin: firstItem.pin, privacy: firstItem.privacy, period: Int(firstItem.period), randomId: firstItem.randomId)
|
||||
|> 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 {
|
||||
currentPendingItemContext.progress = progress
|
||||
currentPendingItemContext.updated()
|
||||
}
|
||||
case .completed:
|
||||
// wait for the local state to change via Postbox
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
self.processContextsUpdated()
|
||||
}
|
||||
|
||||
private func processContextsUpdated() {
|
||||
self.allStoriesUploadProgressPromise.set(self.currentPendingItemContext?.progress)
|
||||
self.hasPendingPromise.set(self.currentPendingItemContext != nil)
|
||||
}
|
||||
}
|
||||
|
||||
private let queue: Queue
|
||||
private let impl: QueueLocalObject<Impl>
|
||||
private let accountPeerId: PeerId
|
||||
|
||||
public var allStoriesUploadProgress: Signal<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)
|
||||
}
|
||||
}
|
||||
|
||||
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(stableId: Int32) -> Int32? {
|
||||
return _internal_lookUpPendingStoryIdMapping(accountPeerId: self.accountPeerId, stableId: stableId)
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,10 @@ import TelegramApi
|
||||
|
||||
public enum EngineStoryInputMedia {
|
||||
case image(dimensions: PixelDimensions, data: Data)
|
||||
case video(dimensions: PixelDimensions, duration: Double, resource: TelegramMediaResource)
|
||||
case video(dimensions: PixelDimensions, duration: Double, resource: TelegramMediaResource, firstFrameImageData: Data?)
|
||||
}
|
||||
|
||||
public struct EngineStoryPrivacy: Equatable {
|
||||
public struct EngineStoryPrivacy: Codable, Equatable {
|
||||
public typealias Base = Stories.Item.Privacy.Base
|
||||
|
||||
public var base: Base
|
||||
@@ -66,11 +66,27 @@ public enum Stories {
|
||||
case additionallyIncludePeers = "addPeers"
|
||||
}
|
||||
|
||||
public enum Base: Int32 {
|
||||
public enum Base: Int32, Codable {
|
||||
private enum CodingKeys: CodingKey {
|
||||
case value
|
||||
}
|
||||
|
||||
case everyone = 0
|
||||
case contacts = 1
|
||||
case closeFriends = 2
|
||||
case nobody = 3
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self.init(rawValue: try container.decode(Int32.self, forKey: .value))!
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(self.rawValue, forKey: .value)
|
||||
}
|
||||
}
|
||||
|
||||
public var base: Base
|
||||
@@ -501,10 +517,7 @@ public enum StoryUploadResult {
|
||||
case completed(Int32?)
|
||||
}
|
||||
|
||||
private func uploadedStoryContent(account: Account, media: EngineStoryInputMedia) -> (signal: Signal<PendingMessageUploadedContentResult?, NoError>, media: Media) {
|
||||
let originalMedia: Media
|
||||
let contentToUpload: MessageContentToUpload
|
||||
|
||||
private func prepareUploadStoryContent(account: Account, media: EngineStoryInputMedia) -> Media {
|
||||
switch media {
|
||||
case let .image(dimensions, data):
|
||||
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
|
||||
@@ -518,31 +531,21 @@ private func uploadedStoryContent(account: Account, media: EngineStoryInputMedia
|
||||
partialReference: nil,
|
||||
flags: []
|
||||
)
|
||||
originalMedia = imageMedia
|
||||
return imageMedia
|
||||
case let .video(dimensions, duration, resource, firstFrameImageData):
|
||||
var previewRepresentations: [TelegramMediaImageRepresentation] = []
|
||||
if let firstFrameImageData = firstFrameImageData {
|
||||
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
|
||||
account.postbox.mediaBox.storeResourceData(resource.id, data: firstFrameImageData)
|
||||
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false))
|
||||
}
|
||||
|
||||
contentToUpload = messageContentToUpload(
|
||||
accountPeerId: account.peerId,
|
||||
network: account.network,
|
||||
postbox: account.postbox,
|
||||
auxiliaryMethods: account.auxiliaryMethods,
|
||||
transformOutgoingMessageMedia: nil,
|
||||
messageMediaPreuploadManager: account.messageMediaPreuploadManager,
|
||||
revalidationContext: account.mediaReferenceRevalidationContext,
|
||||
forceReupload: true,
|
||||
isGrouped: false,
|
||||
passFetchProgress: false,
|
||||
peerId: account.peerId,
|
||||
messageId: nil,
|
||||
attributes: [],
|
||||
text: "",
|
||||
media: [imageMedia]
|
||||
)
|
||||
case let .video(dimensions, duration, resource):
|
||||
let fileMedia = TelegramMediaFile(
|
||||
fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: MediaId.Id.random(in: MediaId.Id.min ... MediaId.Id.max)),
|
||||
partialReference: nil,
|
||||
resource: resource,
|
||||
previewRepresentations: [],
|
||||
previewRepresentations: previewRepresentations,
|
||||
videoThumbnails: [],
|
||||
immediateThumbnailData: nil,
|
||||
mimeType: "video/mp4",
|
||||
@@ -551,26 +554,32 @@ private func uploadedStoryContent(account: Account, media: EngineStoryInputMedia
|
||||
TelegramMediaFileAttribute.Video(duration: duration, size: dimensions, flags: .supportsStreaming, preloadSize: nil)
|
||||
]
|
||||
)
|
||||
originalMedia = fileMedia
|
||||
|
||||
contentToUpload = messageContentToUpload(
|
||||
accountPeerId: account.peerId,
|
||||
network: account.network,
|
||||
postbox: account.postbox,
|
||||
auxiliaryMethods: account.auxiliaryMethods,
|
||||
transformOutgoingMessageMedia: nil,
|
||||
messageMediaPreuploadManager: account.messageMediaPreuploadManager,
|
||||
revalidationContext: account.mediaReferenceRevalidationContext,
|
||||
forceReupload: true,
|
||||
isGrouped: false,
|
||||
passFetchProgress: true,
|
||||
peerId: account.peerId,
|
||||
messageId: nil,
|
||||
attributes: [],
|
||||
text: "",
|
||||
media: [fileMedia]
|
||||
)
|
||||
return fileMedia
|
||||
}
|
||||
}
|
||||
|
||||
private func uploadedStoryContent(postbox: Postbox, network: Network, media: Media, accountPeerId: PeerId, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, auxiliaryMethods: AccountAuxiliaryMethods) -> (signal: Signal<PendingMessageUploadedContentResult?, NoError>, media: Media) {
|
||||
let originalMedia: Media = media
|
||||
let contentToUpload: MessageContentToUpload
|
||||
|
||||
contentToUpload = messageContentToUpload(
|
||||
accountPeerId: accountPeerId,
|
||||
network: network,
|
||||
postbox: postbox,
|
||||
auxiliaryMethods: auxiliaryMethods,
|
||||
transformOutgoingMessageMedia: nil,
|
||||
messageMediaPreuploadManager: messageMediaPreuploadManager,
|
||||
revalidationContext: revalidationContext,
|
||||
forceReupload: true,
|
||||
isGrouped: false,
|
||||
passFetchProgress: false,
|
||||
peerId: accountPeerId,
|
||||
messageId: nil,
|
||||
attributes: [],
|
||||
text: "",
|
||||
media: [media]
|
||||
)
|
||||
|
||||
let contentSignal: Signal<PendingMessageUploadedContentResult, PendingMessageUploadError>
|
||||
switch contentToUpload {
|
||||
@@ -624,15 +633,82 @@ private func apiInputPrivacyRules(privacy: EngineStoryPrivacy, transaction: Tran
|
||||
return privacyRules
|
||||
}
|
||||
|
||||
func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, period: Int, randomId: Int64) -> Signal<StoryUploadResult, NoError> {
|
||||
let (contentSignal, originalMedia) = uploadedStoryContent(account: account, media: media)
|
||||
func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, period: Int, randomId: Int64) {
|
||||
let inputMedia = prepareUploadStoryContent(account: account, media: media)
|
||||
|
||||
let _ = (account.postbox.transaction { transaction in
|
||||
var currentState: Stories.LocalState
|
||||
if let value = transaction.getLocalStoryState()?.get(Stories.LocalState.self) {
|
||||
currentState = value
|
||||
} else {
|
||||
currentState = Stories.LocalState(items: [])
|
||||
}
|
||||
var stableId: Int32 = Int32.random(in: 2000000 ..< Int32.max)
|
||||
while currentState.items.contains(where: { $0.stableId == stableId }) {
|
||||
stableId = Int32.random(in: 2000000 ..< Int32.max)
|
||||
}
|
||||
currentState.items.append(Stories.PendingItem(
|
||||
stableId: stableId,
|
||||
timestamp: Int32(Date().timeIntervalSince1970),
|
||||
media: inputMedia,
|
||||
text: text,
|
||||
entities: entities,
|
||||
pin: pin,
|
||||
privacy: privacy,
|
||||
period: Int32(period),
|
||||
randomId: randomId
|
||||
))
|
||||
transaction.setLocalStoryState(state: CodableEntry(currentState))
|
||||
}).start()
|
||||
}
|
||||
|
||||
func _internal_cancelStoryUpload(account: Account, stableId: Int32) {
|
||||
let _ = (account.postbox.transaction { transaction in
|
||||
var currentState: Stories.LocalState
|
||||
if let value = transaction.getLocalStoryState()?.get(Stories.LocalState.self) {
|
||||
currentState = value
|
||||
} else {
|
||||
currentState = Stories.LocalState(items: [])
|
||||
}
|
||||
if let index = currentState.items.firstIndex(where: { $0.stableId == stableId }) {
|
||||
currentState.items.remove(at: index)
|
||||
transaction.setLocalStoryState(state: CodableEntry(currentState))
|
||||
}
|
||||
}).start()
|
||||
}
|
||||
|
||||
private struct PendingStoryIdMappingKey: Hashable {
|
||||
var accountPeerId: PeerId
|
||||
var stableId: Int32
|
||||
}
|
||||
|
||||
private let pendingStoryIdMapping = Atomic<[PendingStoryIdMappingKey: Int32]>(value: [:])
|
||||
|
||||
func _internal_lookUpPendingStoryIdMapping(accountPeerId: PeerId, stableId: Int32) -> Int32? {
|
||||
return pendingStoryIdMapping.with { dict in
|
||||
return dict[PendingStoryIdMappingKey(accountPeerId: accountPeerId, stableId: stableId)]
|
||||
}
|
||||
}
|
||||
|
||||
private func _internal_putPendingStoryIdMapping(accountPeerId: PeerId, stableId: Int32, id: Int32) {
|
||||
let _ = pendingStoryIdMapping.modify { dict in
|
||||
var dict = dict
|
||||
|
||||
dict[PendingStoryIdMappingKey(accountPeerId: accountPeerId, stableId: stableId)] = id
|
||||
|
||||
return dict
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId: PeerId, stateManager: AccountStateManager, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, auxiliaryMethods: AccountAuxiliaryMethods, stableId: Int32, media: Media, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, period: Int, randomId: Int64) -> Signal<StoryUploadResult, NoError> {
|
||||
let (contentSignal, originalMedia) = uploadedStoryContent(postbox: postbox, network: network, media: media, accountPeerId: accountPeerId, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, auxiliaryMethods: auxiliaryMethods)
|
||||
return contentSignal
|
||||
|> mapToSignal { result -> Signal<StoryUploadResult, NoError> in
|
||||
switch result {
|
||||
case let .progress(progress):
|
||||
return .single(.progress(progress))
|
||||
case let .content(content):
|
||||
return account.postbox.transaction { transaction -> Signal<StoryUploadResult, NoError> in
|
||||
return postbox.transaction { transaction -> Signal<StoryUploadResult, NoError> in
|
||||
let privacyRules = apiInputPrivacyRules(privacy: privacy, transaction: transaction)
|
||||
switch content.content {
|
||||
case let .media(inputMedia, _):
|
||||
@@ -664,7 +740,7 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text:
|
||||
|
||||
flags |= 1 << 3
|
||||
|
||||
return account.network.request(Api.functions.stories.sendStory(
|
||||
return network.request(Api.functions.stories.sendStory(
|
||||
flags: flags,
|
||||
media: inputMedia,
|
||||
caption: apiCaption,
|
||||
@@ -678,27 +754,69 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text:
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { updates -> Signal<StoryUploadResult, NoError> in
|
||||
var id: Int32?
|
||||
if let updates = updates {
|
||||
for update in updates.allUpdates {
|
||||
if case let .updateStory(_, story) = update {
|
||||
switch story {
|
||||
case let .storyItem(_, idValue, _, _, _, _, media, _, _):
|
||||
id = idValue
|
||||
let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, account.peerId)
|
||||
if let parsedMedia = parsedMedia {
|
||||
applyMediaResourceChanges(from: originalMedia, to: parsedMedia, postbox: account.postbox, force: false)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return postbox.transaction { transaction -> StoryUploadResult in
|
||||
var currentState: Stories.LocalState
|
||||
if let value = transaction.getLocalStoryState()?.get(Stories.LocalState.self) {
|
||||
currentState = value
|
||||
} else {
|
||||
currentState = Stories.LocalState(items: [])
|
||||
}
|
||||
if let index = currentState.items.firstIndex(where: { $0.stableId == stableId }) {
|
||||
currentState.items.remove(at: index)
|
||||
transaction.setLocalStoryState(state: CodableEntry(currentState))
|
||||
}
|
||||
|
||||
account.stateManager.addUpdates(updates)
|
||||
var id: Int32?
|
||||
if let updates = updates {
|
||||
for update in updates.allUpdates {
|
||||
if case let .updateStory(_, story) = update {
|
||||
switch story {
|
||||
case let .storyItem(_, idValue, _, _, _, _, media, _, _):
|
||||
if let parsedStory = Stories.StoredItem(apiStoryItem: story, peerId: accountPeerId, transaction: transaction) {
|
||||
var items = transaction.getStoryItems(peerId: accountPeerId)
|
||||
var updatedItems: [Stories.Item] = []
|
||||
if items.firstIndex(where: { $0.id == id }) == nil, case let .item(item) = parsedStory {
|
||||
let updatedItem = Stories.Item(
|
||||
id: item.id,
|
||||
timestamp: item.timestamp,
|
||||
expirationTimestamp: item.expirationTimestamp,
|
||||
media: item.media,
|
||||
text: item.text,
|
||||
entities: item.entities,
|
||||
views: item.views,
|
||||
privacy: Stories.Item.Privacy(base: privacy.base, additionallyIncludePeers: privacy.additionallyIncludePeers),
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic
|
||||
)
|
||||
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
|
||||
items.append(StoryItemsTableEntry(value: entry, id: item.id))
|
||||
}
|
||||
updatedItems.append(updatedItem)
|
||||
}
|
||||
transaction.setStoryItems(peerId: accountPeerId, items: items)
|
||||
}
|
||||
|
||||
id = idValue
|
||||
let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, accountPeerId)
|
||||
if let parsedMedia = parsedMedia {
|
||||
applyMediaResourceChanges(from: originalMedia, to: parsedMedia, postbox: postbox, force: originalMedia is TelegramMediaFile && parsedMedia is TelegramMediaFile)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let id = id {
|
||||
_internal_putPendingStoryIdMapping(accountPeerId: accountPeerId, stableId: stableId, id: id)
|
||||
}
|
||||
|
||||
stateManager.addUpdates(updates)
|
||||
}
|
||||
|
||||
return .completed(id)
|
||||
}
|
||||
|
||||
return .single(.completed(id))
|
||||
}
|
||||
default:
|
||||
return .complete()
|
||||
@@ -715,7 +833,7 @@ func _internal_editStory(account: Account, media: EngineStoryInputMedia?, id: In
|
||||
let contentSignal: Signal<PendingMessageUploadedContentResult?, NoError>
|
||||
let originalMedia: Media?
|
||||
if let media = media {
|
||||
(contentSignal, originalMedia) = uploadedStoryContent(account: account, media: media)
|
||||
(contentSignal, originalMedia) = uploadedStoryContent(postbox: account.postbox, network: account.network, media: prepareUploadStoryContent(account: account, media: media), accountPeerId: account.peerId, messageMediaPreuploadManager: account.messageMediaPreuploadManager, revalidationContext: account.mediaReferenceRevalidationContext, auxiliaryMethods: account.auxiliaryMethods)
|
||||
} else {
|
||||
contentSignal = .single(nil)
|
||||
originalMedia = nil
|
||||
@@ -803,6 +921,73 @@ func _internal_editStory(account: Account, media: EngineStoryInputMedia?, id: In
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStoryPrivacy) -> Signal<Never, NoError> {
|
||||
return account.postbox.transaction { transaction -> [Api.InputPrivacyRule] in
|
||||
let storyId = StoryId(peerId: account.peerId, id: id)
|
||||
if let storyItem = transaction.getStory(id: storyId)?.get(Stories.StoredItem.self), case let .item(item) = storyItem {
|
||||
let updatedItem = Stories.Item(
|
||||
id: item.id,
|
||||
timestamp: item.timestamp,
|
||||
expirationTimestamp: item.expirationTimestamp,
|
||||
media: item.media,
|
||||
text: item.text,
|
||||
entities: item.entities,
|
||||
views: item.views,
|
||||
privacy: Stories.Item.Privacy(base: privacy.base, additionallyIncludePeers: privacy.additionallyIncludePeers),
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic
|
||||
)
|
||||
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
|
||||
transaction.setStory(id: storyId, value: entry)
|
||||
}
|
||||
}
|
||||
|
||||
var items = transaction.getStoryItems(peerId: account.peerId)
|
||||
var updatedItems: [Stories.Item] = []
|
||||
if let index = items.firstIndex(where: { $0.id == id }), case let .item(item) = items[index].value.get(Stories.StoredItem.self) {
|
||||
let updatedItem = Stories.Item(
|
||||
id: item.id,
|
||||
timestamp: item.timestamp,
|
||||
expirationTimestamp: item.expirationTimestamp,
|
||||
media: item.media,
|
||||
text: item.text,
|
||||
entities: item.entities,
|
||||
views: item.views,
|
||||
privacy: Stories.Item.Privacy(base: privacy.base, additionallyIncludePeers: privacy.additionallyIncludePeers),
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic
|
||||
)
|
||||
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
|
||||
items[index] = StoryItemsTableEntry(value: entry, id: item.id)
|
||||
}
|
||||
|
||||
updatedItems.append(updatedItem)
|
||||
}
|
||||
transaction.setStoryItems(peerId: account.peerId, items: items)
|
||||
|
||||
return apiInputPrivacyRules(privacy: privacy, transaction: transaction)
|
||||
}
|
||||
|> mapToSignal { inputRules -> Signal<Never, NoError> in
|
||||
var flags: Int32 = 0
|
||||
flags |= 1 << 2
|
||||
|
||||
return account.network.request(Api.functions.stories.editStory(flags: flags, id: id, media: nil, caption: nil, entities: nil, privacyRules: inputRules))
|
||||
|> 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_deleteStories(account: Account, ids: [Int32]) -> Signal<Never, NoError> {
|
||||
return account.postbox.transaction { transaction -> Void in
|
||||
var items = transaction.getStoryItems(peerId: account.peerId)
|
||||
|
||||
@@ -42,8 +42,9 @@ public final class EngineStoryItem: Equatable {
|
||||
public let isPinned: Bool
|
||||
public let isExpired: Bool
|
||||
public let isPublic: Bool
|
||||
public let isPending: Bool
|
||||
|
||||
public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool) {
|
||||
public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool, isPending: Bool) {
|
||||
self.id = id
|
||||
self.timestamp = timestamp
|
||||
self.expirationTimestamp = expirationTimestamp
|
||||
@@ -55,6 +56,7 @@ public final class EngineStoryItem: Equatable {
|
||||
self.isPinned = isPinned
|
||||
self.isExpired = isExpired
|
||||
self.isPublic = isPublic
|
||||
self.isPending = isPending
|
||||
}
|
||||
|
||||
public static func ==(lhs: EngineStoryItem, rhs: EngineStoryItem) -> Bool {
|
||||
@@ -91,6 +93,9 @@ public final class EngineStoryItem: Equatable {
|
||||
if lhs.isPublic != rhs.isPublic {
|
||||
return false
|
||||
}
|
||||
if lhs.isPending != rhs.isPending {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -474,7 +479,8 @@ public final class PeerStoryListContext {
|
||||
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic
|
||||
isPublic: item.isPublic,
|
||||
isPending: false
|
||||
)
|
||||
items.append(mappedItem)
|
||||
}
|
||||
@@ -578,7 +584,8 @@ public final class PeerStoryListContext {
|
||||
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic
|
||||
isPublic: item.isPublic,
|
||||
isPending: false
|
||||
)
|
||||
storyItems.append(mappedItem)
|
||||
}
|
||||
@@ -709,7 +716,8 @@ public final class PeerStoryListContext {
|
||||
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic
|
||||
isPublic: item.isPublic,
|
||||
isPending: false
|
||||
)
|
||||
finalUpdatedState = updatedState
|
||||
}
|
||||
@@ -745,7 +753,8 @@ public final class PeerStoryListContext {
|
||||
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic
|
||||
isPublic: item.isPublic,
|
||||
isPending: false
|
||||
))
|
||||
updatedState.items.sort(by: { lhs, rhs in
|
||||
return lhs.timestamp > rhs.timestamp
|
||||
@@ -890,7 +899,8 @@ public final class PeerExpiringStoryListContext {
|
||||
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic
|
||||
isPublic: item.isPublic,
|
||||
isPending: false
|
||||
)
|
||||
items.append(.item(mappedItem))
|
||||
}
|
||||
|
||||
@@ -902,14 +902,40 @@ public extension TelegramEngine {
|
||||
}
|
||||
}
|
||||
|
||||
public func uploadStory(media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, period: Int, randomId: Int64) -> Signal<StoryUploadResult, NoError> {
|
||||
return _internal_uploadStory(account: self.account, media: media, text: text, entities: entities, pin: pin, privacy: privacy, period: period, randomId: randomId)
|
||||
public func uploadStory(media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, period: Int, randomId: Int64) {
|
||||
_internal_uploadStory(account: self.account, media: media, text: text, entities: entities, pin: pin, privacy: privacy, period: period, randomId: randomId)
|
||||
}
|
||||
|
||||
public func lookUpPendingStoryIdMapping(stableId: Int32) -> Int32? {
|
||||
return self.account.pendingStoryManager?.lookUpPendingStoryIdMapping(stableId: stableId)
|
||||
}
|
||||
|
||||
public func allStoriesUploadProgress() -> Signal<Float?, NoError> {
|
||||
guard let pendingStoryManager = self.account.pendingStoryManager else {
|
||||
return .single(nil)
|
||||
}
|
||||
return pendingStoryManager.allStoriesUploadProgress
|
||||
}
|
||||
|
||||
public func storyUploadProgress(stableId: Int32) -> Signal<Float, NoError> {
|
||||
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(media: EngineStoryInputMedia?, id: Int32, text: String, entities: [MessageTextEntity], privacy: EngineStoryPrivacy?) -> Signal<StoryUploadResult, NoError> {
|
||||
return _internal_editStory(account: self.account, media: media, id: id, text: text, entities: entities, privacy: privacy)
|
||||
}
|
||||
|
||||
public func editStoryPrivacy(id: Int32, privacy: EngineStoryPrivacy) -> Signal<Never, NoError> {
|
||||
return _internal_editStoryPrivacy(account: self.account, id: id, privacy: privacy)
|
||||
}
|
||||
|
||||
public func deleteStories(ids: [Int32]) -> Signal<Never, NoError> {
|
||||
return _internal_deleteStories(account: self.account, ids: ids)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user