[WIP] Stories

This commit is contained in:
Ali
2023-06-16 22:42:48 +03:00
parent 0882817bed
commit b64aa1445c
59 changed files with 1601 additions and 365 deletions

View File

@@ -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 : [] },

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -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))
}

View File

@@ -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)
}