mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-24 07:05:35 +00:00
[WIP] Stories
This commit is contained in:
@@ -9,22 +9,26 @@ public enum EngineStoryInputMedia {
|
||||
}
|
||||
|
||||
public struct EngineStoryPrivacy: Equatable {
|
||||
public enum Base {
|
||||
case everyone
|
||||
case contacts
|
||||
case closeFriends
|
||||
case nobody
|
||||
}
|
||||
public typealias Base = Stories.Item.Privacy.Base
|
||||
|
||||
public var base: Base
|
||||
public var additionallyIncludePeers: [EnginePeer.Id]
|
||||
|
||||
public init(base: Base, additionallyIncludePeers: [EnginePeer.Id]) {
|
||||
public init(base: Stories.Item.Privacy.Base, additionallyIncludePeers: [EnginePeer.Id]) {
|
||||
self.base = base
|
||||
self.additionallyIncludePeers = additionallyIncludePeers
|
||||
}
|
||||
}
|
||||
|
||||
public extension EngineStoryPrivacy {
|
||||
init(_ privacy: Stories.Item.Privacy) {
|
||||
self.init(
|
||||
base: privacy.base,
|
||||
additionallyIncludePeers: privacy.additionallyIncludePeers
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public enum Stories {
|
||||
public final class Item: Codable, Equatable {
|
||||
public struct Views: Codable, Equatable {
|
||||
@@ -100,6 +104,9 @@ public enum Stories {
|
||||
case entities
|
||||
case views
|
||||
case privacy
|
||||
case isPinned
|
||||
case isExpired
|
||||
case isPublic
|
||||
}
|
||||
|
||||
public let id: Int32
|
||||
@@ -109,6 +116,9 @@ public enum Stories {
|
||||
public let entities: [MessageTextEntity]
|
||||
public let views: Views?
|
||||
public let privacy: Privacy?
|
||||
public let isPinned: Bool
|
||||
public let isExpired: Bool
|
||||
public let isPublic: Bool
|
||||
|
||||
public init(
|
||||
id: Int32,
|
||||
@@ -117,7 +127,10 @@ public enum Stories {
|
||||
text: String,
|
||||
entities: [MessageTextEntity],
|
||||
views: Views?,
|
||||
privacy: Privacy?
|
||||
privacy: Privacy?,
|
||||
isPinned: Bool,
|
||||
isExpired: Bool,
|
||||
isPublic: Bool
|
||||
) {
|
||||
self.id = id
|
||||
self.timestamp = timestamp
|
||||
@@ -126,6 +139,9 @@ public enum Stories {
|
||||
self.entities = entities
|
||||
self.views = views
|
||||
self.privacy = privacy
|
||||
self.isPinned = isPinned
|
||||
self.isExpired = isExpired
|
||||
self.isPublic = isPublic
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
@@ -144,6 +160,9 @@ public enum Stories {
|
||||
self.entities = try container.decode([MessageTextEntity].self, forKey: .entities)
|
||||
self.views = try container.decodeIfPresent(Views.self, forKey: .views)
|
||||
self.privacy = try container.decodeIfPresent(Privacy.self, forKey: .privacy)
|
||||
self.isPinned = try container.decodeIfPresent(Bool.self, forKey: .isPinned) ?? false
|
||||
self.isExpired = try container.decodeIfPresent(Bool.self, forKey: .isExpired) ?? false
|
||||
self.isPublic = try container.decodeIfPresent(Bool.self, forKey: .isPublic) ?? false
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
@@ -163,6 +182,9 @@ public enum Stories {
|
||||
try container.encode(self.entities, forKey: .entities)
|
||||
try container.encodeIfPresent(self.views, forKey: .views)
|
||||
try container.encodeIfPresent(self.privacy, forKey: .privacy)
|
||||
try container.encode(self.isPinned, forKey: .isPinned)
|
||||
try container.encode(self.isExpired, forKey: .isExpired)
|
||||
try container.encode(self.isPublic, forKey: .isPublic)
|
||||
}
|
||||
|
||||
public static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||
@@ -195,6 +217,15 @@ public enum Stories {
|
||||
if lhs.privacy != rhs.privacy {
|
||||
return false
|
||||
}
|
||||
if lhs.isPinned != rhs.isPinned {
|
||||
return false
|
||||
}
|
||||
if lhs.isExpired != rhs.isExpired {
|
||||
return false
|
||||
}
|
||||
if lhs.isPublic != rhs.isPublic {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -698,6 +729,41 @@ func _internal_markStoryAsSeen(account: Account, peerId: PeerId, id: Int32) -> S
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_updateStoryIsPinned(account: Account, id: Int32, isPinned: Bool) -> Signal<Never, NoError> {
|
||||
return account.postbox.transaction { transaction -> Void in
|
||||
var items = transaction.getStoryItems(peerId: account.peerId)
|
||||
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,
|
||||
media: item.media,
|
||||
text: item.text,
|
||||
entities: item.entities,
|
||||
views: item.views,
|
||||
privacy: item.privacy,
|
||||
isPinned: isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic
|
||||
)
|
||||
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
|
||||
items[index] = StoryItemsTableEntry(value: entry, id: item.id)
|
||||
transaction.setStoryItems(peerId: account.peerId, items: items)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
account.stateManager.injectStoryUpdates(updates: [.added(peerId: account.peerId, item: Stories.StoredItem.item(updatedItem))])
|
||||
}
|
||||
}
|
||||
}
|
||||
|> mapToSignal { _ -> Signal<Never, NoError> in
|
||||
return account.network.request(Api.functions.stories.togglePinned(id: [id], pinned: isPinned ? .boolTrue : .boolFalse))
|
||||
|> `catch` { _ -> Signal<[Int32], NoError> in
|
||||
return .single([])
|
||||
}
|
||||
|> ignoreValues
|
||||
}
|
||||
}
|
||||
|
||||
extension Api.StoryItem {
|
||||
var id: Int32 {
|
||||
switch self {
|
||||
@@ -762,6 +828,10 @@ extension Stories.StoredItem {
|
||||
parsedPrivacy = Stories.Item.Privacy(base: base, additionallyIncludePeers: additionalPeerIds)
|
||||
}
|
||||
|
||||
let isPinned = (flags & (1 << 5)) != 0
|
||||
let isExpired = (flags & (1 << 6)) != 0
|
||||
let isPublic = (flags & (1 << 7)) != 0
|
||||
|
||||
let item = Stories.Item(
|
||||
id: id,
|
||||
timestamp: date,
|
||||
@@ -769,7 +839,10 @@ extension Stories.StoredItem {
|
||||
text: caption ?? "",
|
||||
entities: entities.flatMap { entities in return messageTextEntitiesFromApiEntities(entities) } ?? [],
|
||||
views: views.flatMap(Stories.Item.Views.init(apiViews:)),
|
||||
privacy: parsedPrivacy
|
||||
privacy: parsedPrivacy,
|
||||
isPinned: isPinned,
|
||||
isExpired: isExpired,
|
||||
isPublic: isPublic
|
||||
)
|
||||
self = .item(item)
|
||||
} else {
|
||||
|
||||
@@ -2,6 +2,7 @@ import Foundation
|
||||
import Postbox
|
||||
import TelegramApi
|
||||
import SwiftSignalKit
|
||||
import MtProtoKit
|
||||
|
||||
enum InternalStoryUpdate {
|
||||
case deleted(peerId: PeerId, id: Int32)
|
||||
@@ -37,8 +38,11 @@ public final class EngineStoryItem: Equatable {
|
||||
public let entities: [MessageTextEntity]
|
||||
public let views: Views?
|
||||
public let privacy: EngineStoryPrivacy?
|
||||
public let isPinned: Bool
|
||||
public let isExpired: Bool
|
||||
public let isPublic: Bool
|
||||
|
||||
public init(id: Int32, timestamp: Int32, media: EngineMedia, text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?) {
|
||||
public init(id: Int32, timestamp: Int32, media: EngineMedia, text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool) {
|
||||
self.id = id
|
||||
self.timestamp = timestamp
|
||||
self.media = media
|
||||
@@ -46,6 +50,9 @@ public final class EngineStoryItem: Equatable {
|
||||
self.entities = entities
|
||||
self.views = views
|
||||
self.privacy = privacy
|
||||
self.isPinned = isPinned
|
||||
self.isExpired = isExpired
|
||||
self.isPublic = isPublic
|
||||
}
|
||||
|
||||
public static func ==(lhs: EngineStoryItem, rhs: EngineStoryItem) -> Bool {
|
||||
@@ -70,6 +77,15 @@ public final class EngineStoryItem: Equatable {
|
||||
if lhs.privacy != rhs.privacy {
|
||||
return false
|
||||
}
|
||||
if lhs.isPinned != rhs.isPinned {
|
||||
return false
|
||||
}
|
||||
if lhs.isExpired != rhs.isExpired {
|
||||
return false
|
||||
}
|
||||
if lhs.isPublic != rhs.isPublic {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -255,6 +271,15 @@ public final class StorySubscriptionsContext {
|
||||
var updatedPeerEntries: [StoryItemsTableEntry] = []
|
||||
for story in stories {
|
||||
if let storedItem = Stories.StoredItem(apiStoryItem: story, peerId: peerId, transaction: transaction) {
|
||||
/*#if DEBUG
|
||||
if "".isEmpty {
|
||||
if let codedEntry = CodableEntry(Stories.StoredItem.placeholder(Stories.Placeholder(id: storedItem.id, timestamp: storedItem.timestamp))) {
|
||||
updatedPeerEntries.append(StoryItemsTableEntry(value: codedEntry, id: storedItem.id))
|
||||
}
|
||||
continue
|
||||
}
|
||||
#endif*/
|
||||
|
||||
if case .placeholder = storedItem, let previousEntry = previousPeerEntries.first(where: { $0.id == storedItem.id }) {
|
||||
updatedPeerEntries.append(previousEntry)
|
||||
} else {
|
||||
@@ -331,3 +356,275 @@ public final class StorySubscriptionsContext {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class PeerStoryListContext {
|
||||
public struct State: Equatable {
|
||||
public var peerReference: PeerReference?
|
||||
public var items: [EngineStoryItem]
|
||||
public var totalCount: Int
|
||||
public var loadMoreToken: Int?
|
||||
|
||||
init(
|
||||
peerReference: PeerReference?,
|
||||
items: [EngineStoryItem],
|
||||
totalCount: Int,
|
||||
loadMoreToken: Int?
|
||||
) {
|
||||
self.peerReference = peerReference
|
||||
self.items = items
|
||||
self.totalCount = totalCount
|
||||
self.loadMoreToken = loadMoreToken
|
||||
}
|
||||
}
|
||||
|
||||
private let account: Account
|
||||
private let peerId: EnginePeer.Id
|
||||
private let isArchived: Bool
|
||||
|
||||
private let statePromise = Promise<State>()
|
||||
private var stateValue: State {
|
||||
didSet {
|
||||
self.statePromise.set(.single(self.stateValue))
|
||||
}
|
||||
}
|
||||
public var state: Signal<State, NoError> {
|
||||
return self.statePromise.get()
|
||||
}
|
||||
|
||||
private var isLoadingMore: Bool = false
|
||||
private var requestDisposable: Disposable?
|
||||
|
||||
private var updatesDisposable: Disposable?
|
||||
|
||||
public init(account: Account, peerId: EnginePeer.Id, isArchived: Bool) {
|
||||
self.account = account
|
||||
self.peerId = peerId
|
||||
self.isArchived = isArchived
|
||||
|
||||
self.stateValue = State(peerReference: nil, items: [], totalCount: 0, loadMoreToken: 0)
|
||||
self.statePromise.set(.single(self.stateValue))
|
||||
|
||||
self.loadMore()
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.requestDisposable?.dispose()
|
||||
}
|
||||
|
||||
public func loadMore() {
|
||||
if self.isLoadingMore {
|
||||
return
|
||||
}
|
||||
guard let loadMoreToken = self.stateValue.loadMoreToken else {
|
||||
return
|
||||
}
|
||||
|
||||
self.isLoadingMore = true
|
||||
|
||||
let peerId = self.peerId
|
||||
let account = self.account
|
||||
let isArchived = self.isArchived
|
||||
self.requestDisposable = (self.account.postbox.transaction { transaction -> Api.InputUser? in
|
||||
return transaction.getPeer(peerId).flatMap(apiInputUser)
|
||||
}
|
||||
|> mapToSignal { inputUser -> Signal<([EngineStoryItem], Int, PeerReference?), NoError> in
|
||||
guard let inputUser = inputUser else {
|
||||
return .single(([], 0, nil))
|
||||
}
|
||||
|
||||
let signal: Signal<Api.stories.Stories, MTRpcError>
|
||||
if isArchived {
|
||||
signal = account.network.request(Api.functions.stories.getExpiredStories(offsetId: Int32(loadMoreToken), limit: 100))
|
||||
} else {
|
||||
signal = account.network.request(Api.functions.stories.getPinnedStories(userId: inputUser, offsetId: Int32(loadMoreToken), limit: 100))
|
||||
}
|
||||
return signal
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.stories.Stories?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { result -> Signal<([EngineStoryItem], Int, PeerReference?), NoError> in
|
||||
guard let result = result else {
|
||||
return .single(([], 0, nil))
|
||||
}
|
||||
|
||||
return account.postbox.transaction { transaction -> ([EngineStoryItem], Int, PeerReference?) in
|
||||
var storyItems: [EngineStoryItem] = []
|
||||
var totalCount: Int = 0
|
||||
|
||||
switch result {
|
||||
case let .stories(count, stories, users):
|
||||
totalCount = Int(count)
|
||||
|
||||
var peers: [Peer] = []
|
||||
var peerPresences: [PeerId: Api.User] = [:]
|
||||
|
||||
for user in users {
|
||||
let telegramUser = TelegramUser(user: user)
|
||||
peers.append(telegramUser)
|
||||
peerPresences[telegramUser.id] = user
|
||||
}
|
||||
|
||||
updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in
|
||||
return updated
|
||||
})
|
||||
updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences)
|
||||
|
||||
for story in stories {
|
||||
if let storedItem = Stories.StoredItem(apiStoryItem: story, peerId: peerId, transaction: transaction) {
|
||||
if case let .item(item) = storedItem, let media = item.media {
|
||||
let mappedItem = EngineStoryItem(
|
||||
id: item.id,
|
||||
timestamp: item.timestamp,
|
||||
media: EngineMedia(media),
|
||||
text: item.text,
|
||||
entities: item.entities,
|
||||
views: item.views.flatMap { views in
|
||||
return EngineStoryItem.Views(
|
||||
seenCount: views.seenCount,
|
||||
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
|
||||
return transaction.getPeer(id).flatMap(EnginePeer.init)
|
||||
}
|
||||
)
|
||||
},
|
||||
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic
|
||||
)
|
||||
storyItems.append(mappedItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (storyItems, totalCount, transaction.getPeer(peerId).flatMap(PeerReference.init))
|
||||
}
|
||||
}
|
||||
}).start(next: { [weak self] storyItems, totalCount, peerReference in
|
||||
guard let `self` = self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.isLoadingMore = false
|
||||
|
||||
var updatedState = self.stateValue
|
||||
|
||||
var existingIds = Set(updatedState.items.map { $0.id })
|
||||
for item in storyItems {
|
||||
if existingIds.contains(item.id) {
|
||||
continue
|
||||
}
|
||||
existingIds.insert(item.id)
|
||||
|
||||
updatedState.items.append(item)
|
||||
}
|
||||
|
||||
if updatedState.peerReference == nil {
|
||||
updatedState.peerReference = peerReference
|
||||
}
|
||||
|
||||
updatedState.loadMoreToken = (storyItems.last?.id).flatMap(Int.init)
|
||||
if updatedState.loadMoreToken != nil {
|
||||
updatedState.totalCount = max(totalCount, updatedState.items.count)
|
||||
} else {
|
||||
updatedState.totalCount = updatedState.items.count
|
||||
}
|
||||
self.stateValue = updatedState
|
||||
|
||||
if self.updatesDisposable == nil {
|
||||
self.updatesDisposable = (self.account.stateManager.storyUpdates
|
||||
|> deliverOnMainQueue).start(next: { [weak self] updates in
|
||||
guard let `self` = self else {
|
||||
return
|
||||
}
|
||||
let selfPeerId = self.peerId
|
||||
let _ = (self.account.postbox.transaction { transaction -> [PeerId: Peer] in
|
||||
var peers: [PeerId: Peer] = [:]
|
||||
|
||||
for update in updates {
|
||||
switch update {
|
||||
case let .added(peerId, item):
|
||||
if selfPeerId == peerId {
|
||||
if case let .item(item) = item {
|
||||
if let views = item.views {
|
||||
for id in views.seenPeerIds {
|
||||
if let peer = transaction.getPeer(id) {
|
||||
peers[peer.id] = peer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return peers
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peers in
|
||||
guard let `self` = self else {
|
||||
return
|
||||
}
|
||||
|
||||
for update in updates {
|
||||
switch update {
|
||||
case let .deleted(peerId, id):
|
||||
if self.peerId == peerId {
|
||||
if let index = self.stateValue.items.firstIndex(where: { $0.id == id }) {
|
||||
var updatedState = self.stateValue
|
||||
updatedState.items.remove(at: index)
|
||||
updatedState.totalCount = max(0, updatedState.totalCount - 1)
|
||||
self.stateValue = updatedState
|
||||
}
|
||||
}
|
||||
case let .added(peerId, item):
|
||||
if self.peerId == peerId {
|
||||
if let index = self.stateValue.items.firstIndex(where: { $0.id == item.id }) {
|
||||
if !self.isArchived {
|
||||
if case let .item(item) = item {
|
||||
if item.isPinned {
|
||||
if let media = item.media {
|
||||
var updatedState = self.stateValue
|
||||
updatedState.items[index] = EngineStoryItem(
|
||||
id: item.id,
|
||||
timestamp: item.timestamp,
|
||||
media: EngineMedia(media),
|
||||
text: item.text,
|
||||
entities: item.entities,
|
||||
views: item.views.flatMap { views in
|
||||
return EngineStoryItem.Views(
|
||||
seenCount: views.seenCount,
|
||||
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
|
||||
return peers[id].flatMap(EnginePeer.init)
|
||||
}
|
||||
)
|
||||
},
|
||||
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic
|
||||
)
|
||||
self.stateValue = updatedState
|
||||
}
|
||||
} else {
|
||||
var updatedState = self.stateValue
|
||||
updatedState.items.remove(at: index)
|
||||
updatedState.totalCount = max(0, updatedState.totalCount - 1)
|
||||
self.stateValue = updatedState
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case .read:
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -871,6 +871,10 @@ public extension TelegramEngine {
|
||||
return _internal_markStoryAsSeen(account: self.account, peerId: peerId, id: id)
|
||||
}
|
||||
|
||||
public func updateStoryIsPinned(id: Int32, isPinned: Bool) -> Signal<Never, NoError> {
|
||||
return _internal_updateStoryIsPinned(account: self.account, id: id, isPinned: isPinned)
|
||||
}
|
||||
|
||||
public func getStoryViewList(account: Account, id: Int32, offsetTimestamp: Int32?, offsetPeerId: PeerId?, limit: Int) -> Signal<StoryViewList?, NoError> {
|
||||
return _internal_getStoryViewList(account: account, id: id, offsetTimestamp: offsetTimestamp, offsetPeerId: offsetPeerId, limit: limit)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user