[WIP] Stories

This commit is contained in:
Ali 2023-05-23 23:53:43 +04:00
parent d086a8f674
commit 969724de40
58 changed files with 2653 additions and 186 deletions

View File

@ -18,7 +18,7 @@ public func isMediaStreamable(message: Message, media: TelegramMediaFile) -> Boo
return false
}
for attribute in media.attributes {
if case let .Video(_, _, flags) = attribute {
if case let .Video(_, _, flags, _) = attribute {
if flags.contains(.supportsStreaming) {
return true
}
@ -41,7 +41,7 @@ public func isMediaStreamable(media: TelegramMediaFile) -> Bool {
return false
}
for attribute in media.attributes {
if case let .Video(_, _, flags) = attribute {
if case let .Video(_, _, flags, _) = attribute {
if flags.contains(.supportsStreaming) {
return true
}

View File

@ -206,7 +206,7 @@ public final class AvatarVideoNode: ASDisplayNode {
self.backgroundNode.image = nil
let videoId = photo.id?.id ?? peer.id.id._internalGetInt64Value()
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [])]))
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [], preloadSize: nil)]))
let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: false, storeAfterDownload: nil)
if videoContent.id != self.videoContent?.id {
self.videoNode?.removeFromSupernode()

View File

@ -459,7 +459,7 @@ public final class ChatImportActivityScreen: ViewController {
if let path = getAppBundle().path(forResource: "BlankVideo", ofType: "m4v"), let size = fileSize(path) {
let decoration = ChatBubbleVideoDecoration(corners: ImageCorners(), nativeSize: CGSize(width: 100.0, height: 100.0), contentMode: .aspectFit, backgroundColor: .black)
let dummyFile = TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 1), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: 12345), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: size, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [])])
let dummyFile = TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 1), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: 12345), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: size, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [], preloadSize: nil)])
let videoContent = NativeVideoContent(id: .message(1, EngineMedia.Id(namespace: 0, id: 1)), userLocation: .other, fileReference: .standalone(media: dummyFile), streamVideo: .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .black, storeAfterDownload: nil)

View File

@ -245,8 +245,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
private var powerSavingMonitoringDisposable: Disposable?
public private(set) var storyListContext: StoryListContext?
private var storyListState: StoryListContext.State?
private var storyListStateDisposable: Disposable?
private var storySubscriptions: EngineStorySubscriptions?
private var storySubscriptionsDisposable: Disposable?
private var preloadStorySubscriptionsDisposable: Disposable?
private var preloadStoryResourceDisposables: [MediaResourceId: Disposable] = [:]
private var storyListHeight: CGFloat
@ -802,7 +805,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.joinForumDisposable.dispose()
self.actionDisposables.dispose()
self.powerSavingMonitoringDisposable?.dispose()
self.storyListStateDisposable?.dispose()
self.storySubscriptionsDisposable?.dispose()
self.preloadStorySubscriptionsDisposable?.dispose()
}
private func updateNavigationMetadata() {
@ -2123,30 +2127,59 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
})
let storyListContext = self.context.engine.messages.allStories()
self.storyListContext = storyListContext
self.storyListStateDisposable = (storyListContext.state
|> deliverOnMainQueue).start(next: { [weak self] state in
self.preloadStorySubscriptionsDisposable = (self.context.engine.messages.preloadStorySubscriptions()
|> deliverOnMainQueue).start(next: { [weak self] resources in
guard let self else {
return
}
var validIds: [MediaResourceId] = []
for (_, info) in resources.sorted(by: { $0.value.priority < $1.value.priority }) {
let resource = info.resource
validIds.append(resource.resource.id)
if preloadStoryResourceDisposables[resource.resource.id] == nil {
var fetchRange: (Range<Int64>, MediaBoxFetchPriority)?
if let size = info.size {
fetchRange = (0 ..< Int64(size), .default)
}
preloadStoryResourceDisposables[resource.resource.id] = fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: resource, range: fetchRange).start()
}
}
var removeIds: [MediaResourceId] = []
for (id, disposable) in self.preloadStoryResourceDisposables {
if !validIds.contains(id) {
removeIds.append(id)
disposable.dispose()
}
}
for id in removeIds {
self.preloadStoryResourceDisposables.removeValue(forKey: id)
}
})
self.storySubscriptionsDisposable = (self.context.engine.messages.storySubscriptions()
|> deliverOnMainQueue).start(next: { [weak self] storySubscriptions in
guard let self else {
return
}
var wasEmpty = true
if let storyListState = self.storyListState, !storyListState.itemSets.isEmpty {
if let storySubscriptions = self.storySubscriptions, !storySubscriptions.items.isEmpty {
wasEmpty = false
}
self.storyListState = state
let isEmpty = state.itemSets.isEmpty
self.storySubscriptions = storySubscriptions
let isEmpty = storySubscriptions.items.isEmpty
self.chatListDisplayNode.mainContainerNode.currentItemNode.updateState { chatListState in
var chatListState = chatListState
var peersWithNewStories = Set<EnginePeer.Id>()
for itemSet in state.itemSets {
if itemSet.peerId == self.context.account.peerId {
for item in storySubscriptions.items {
if item.peer.id == self.context.account.peerId {
continue
}
if itemSet.items.contains(where: { $0.id > itemSet.maxReadId }) {
peersWithNewStories.insert(itemSet.peerId)
if item.hasUnseen {
peersWithNewStories.insert(item.peer.id)
}
}
chatListState.peersWithNewStories = peersWithNewStories
@ -2156,14 +2189,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.storyListHeight = isEmpty ? 0.0 : 94.0
let tabsIsEmpty: Bool
if let (resolvedItems, displayTabsAtBottom, _) = self.tabContainerData {
tabsIsEmpty = resolvedItems.count <= 1 || displayTabsAtBottom
} else {
tabsIsEmpty = true
}
self.navigationBar?.secondaryContentHeight = (!tabsIsEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0)
if case .chatList(.root) = self.location {
self.searchContentNode?.additionalHeight = 94.0
}
@ -2413,11 +2438,121 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if let searchContentNode = self.searchContentNode, case .chatList(.root) = self.location {
if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
componentView.storyPeerAction = { [weak self] peer in
guard let self, let storyListContext = self.storyListContext else {
guard let self, let peer else {
return
}
let _ = (StoryChatContent.stories(
let storyFocusContext = self.context.engine.messages.peerStoryFocusContext(id: peer.id, focusItemId: nil)
let _ = (storyFocusContext.state
|> filter { state -> Bool in
if state.isLoading {
return false
} else {
return true
}
}
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] state in
guard let self else {
return
}
let _ = storyFocusContext
let _ = self
if let item = state.item {
let _ = item
let _ = (StoryChatContent.subscriptionsStories(
context: self.context,
peerId: peer.id
)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] initialContent in
guard let self else {
return
}
var transitionIn: StoryContainerScreen.TransitionIn?
if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peer.id) {
transitionIn = StoryContainerScreen.TransitionIn(
sourceView: transitionView,
sourceRect: transitionView.bounds,
sourceCornerRadius: transitionView.bounds.height * 0.5
)
}
}
var cameraTransitionIn: StoryCameraTransitionIn?
if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
cameraTransitionIn = StoryCameraTransitionIn(
sourceView: transitionView,
sourceRect: transitionView.bounds,
sourceCornerRadius: transitionView.bounds.height * 0.5
)
}
}
var initialFocusedId: AnyHashable?
//if let peer {
initialFocusedId = AnyHashable(peer.id)
//}
if initialFocusedId == AnyHashable(self.context.account.peerId), let firstItem = initialContent.first, firstItem.id == initialFocusedId && firstItem.items.isEmpty {
if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
rootController.openStoryCamera(transitionIn: cameraTransitionIn, transitionOut: { [weak self] _ in
guard let self else {
return nil
}
if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
return StoryCameraTransitionOut(
destinationView: transitionView,
destinationRect: transitionView.bounds,
destinationCornerRadius: transitionView.bounds.height * 0.5
)
}
}
return nil
})
}
return
}
let storyContainerScreen = StoryContainerScreen(
context: self.context,
initialFocusedId: initialFocusedId,
initialContent: initialContent,
transitionIn: transitionIn,
transitionOut: { [weak self] peerId, _ in
guard let self else {
return nil
}
if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) {
return StoryContainerScreen.TransitionOut(
destinationView: transitionView,
destinationRect: transitionView.bounds,
destinationCornerRadius: transitionView.bounds.height * 0.5,
destinationIsAvatar: true,
completed: {}
)
}
}
return nil
}
)
self.push(storyContainerScreen)
})
}
})
/*let _ = (StoryChatContent.stories(
context: self.context,
storyList: storyListContext,
focusItem: nil
@ -2503,7 +2638,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
)
self.push(storyContainerScreen)
})
})*/
}
let fraction = 94.0 / (navigationBarSearchContentHeight + 94.0)
@ -2512,7 +2647,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
visibleProgress = (1.0 / fraction) * visibleProgress
visibleProgress = max(0.0, min(1.0, visibleProgress))
componentView.updateStories(offset: visibleProgress, context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, storyListState: self.storyListState, transition: Transition(transition))
componentView.updateStories(offset: visibleProgress, context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, storySubscriptions: self.storySubscriptions, transition: Transition(transition))
}
}
}

View File

@ -177,7 +177,7 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
processed = true
break inner
}
case let .Video(_, _, flags):
case let .Video(_, _, flags, _):
if flags.contains(.instantRoundVideo) {
messageText = strings.Message_VideoMessage
processed = true

View File

@ -646,7 +646,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
} else if let media = media as? TelegramMediaFile, !media.isAnimated {
for attribute in media.attributes {
switch attribute {
case let .Video(_, dimensions, _):
case let .Video(_, dimensions, _, _):
isVideo = true
if dimensions.height > 0 {
if CGFloat(dimensions.width) / CGFloat(dimensions.height) > 1.33 {

View File

@ -1222,7 +1222,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}
if let file = file {
for attribute in file.attributes {
if case let .Video(duration, _, _) = attribute, duration >= 30 {
if case let .Video(duration, _, _, _) = attribute, duration >= 30 {
hintSeekable = true
break
}

View File

@ -53,7 +53,7 @@ final class InstantPageMediaPlaylistItem: SharedMediaPlaylistItem {
} else {
return SharedMediaPlaybackData(type: .music, source: .telegramFile(reference: .webPage(webPage: WebpageReference(self.webPage), media: file), isCopyProtected: false))
}
case let .Video(_, _, flags):
case let .Video(_, _, flags, _):
if flags.contains(.instantRoundVideo) {
return SharedMediaPlaybackData(type: .instantVideo, source: .telegramFile(reference: .webPage(webPage: WebpageReference(self.webPage), media: file), isCopyProtected: false))
} else {
@ -99,7 +99,7 @@ final class InstantPageMediaPlaylistItem: SharedMediaPlaylistItem {
return SharedMediaPlaybackDisplayData.music(title: updatedTitle, performer: updatedPerformer, albumArt: albumArt, long: false, caption: nil)
}
case let .Video(_, _, flags):
case let .Video(_, _, flags, _):
if flags.contains(.instantRoundVideo) {
return SharedMediaPlaybackDisplayData.instantVideo(author: nil, peer: nil, timestamp: 0)
} else {

View File

@ -348,7 +348,7 @@ public func legacyEnqueueGifMessage(account: Account, data: Data, correlationId:
let finalDimensions = TGMediaVideoConverter.dimensions(for: dimensions, adjustments: nil, preset: TGMediaVideoConversionPresetAnimation)
var fileAttributes: [TelegramMediaFileAttribute] = []
fileAttributes.append(.Video(duration: Int(0), size: PixelDimensions(finalDimensions), flags: [.supportsStreaming]))
fileAttributes.append(.Video(duration: Int(0), size: PixelDimensions(finalDimensions), flags: [.supportsStreaming], preloadSize: nil))
fileAttributes.append(.FileName(fileName: fileName))
fileAttributes.append(.Animated)
@ -390,7 +390,7 @@ public func legacyEnqueueVideoMessage(account: Account, data: Data, correlationI
let finalDimensions = TGMediaVideoConverter.dimensions(for: dimensions, adjustments: nil, preset: TGMediaVideoConversionPresetAnimation)
var fileAttributes: [TelegramMediaFileAttribute] = []
fileAttributes.append(.Video(duration: Int(0), size: PixelDimensions(finalDimensions), flags: [.supportsStreaming]))
fileAttributes.append(.Video(duration: Int(0), size: PixelDimensions(finalDimensions), flags: [.supportsStreaming], preloadSize: nil))
fileAttributes.append(.FileName(fileName: fileName))
fileAttributes.append(.Animated)
@ -723,7 +723,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A
fileAttributes.append(.Animated)
}
if !asFile {
fileAttributes.append(.Video(duration: Int(finalDuration), size: PixelDimensions(finalDimensions), flags: [.supportsStreaming]))
fileAttributes.append(.Video(duration: Int(finalDuration), size: PixelDimensions(finalDimensions), flags: [.supportsStreaming], preloadSize: nil))
if let adjustments = adjustments {
if adjustments.sendAsGif {
fileAttributes.append(.Animated)

View File

@ -187,7 +187,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode {
let subject: ShareControllerSubject
var actionCompletionText: String?
if let video = entry.videoRepresentations.last, let peerReference = PeerReference(peer._asPeer()) {
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [])]))
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil)]))
subject = .media(videoFileReference.abstract)
actionCompletionText = strongSelf.presentationData.strings.Gallery_VideoSaved
} else {
@ -279,7 +279,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode {
if let video = entry.videoRepresentations.last, let peerReference = PeerReference(self.peer._asPeer()) {
if video != previousVideoRepresentations?.last {
let mediaManager = self.context.sharedContext.mediaManager
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: entry.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [])]))
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: entry.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil)]))
let videoContent = NativeVideoContent(id: .profileVideo(id, category), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: true, useLargeThumbnail: true, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, storeAfterDownload: nil)
let videoNode = UniversalVideoNode(postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .overlay)
videoNode.isUserInteractionEnabled = false

View File

@ -511,7 +511,7 @@ public final class PeerInfoAvatarListItemNode: ASDisplayNode {
self.isReady.set(.single(true))
}
} else if let video = videoRepresentations.last, let peerReference = PeerReference(self.peer._asPeer()) {
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [])]))
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil)]))
let videoContent = NativeVideoContent(id: .profileVideo(id, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: fullSizeOnly, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, storeAfterDownload: nil)
if videoContent.id != self.videoContent?.id {

View File

@ -1255,6 +1255,50 @@ public final class Transaction {
assert(!self.disposed)
return self.postbox!.removeChatHidden(peerId: peerId, index: index)
}
public func getAllStorySubscriptions() -> (state: CodableEntry?, peerIds: [PeerId]) {
assert(!self.disposed)
return self.postbox!.getAllStorySubscriptions()
}
public func replaceAllStorySubscriptions(state: CodableEntry?, peerIds: [PeerId]) {
assert(!self.disposed)
self.postbox!.replaceAllStorySubscriptions(state: state, peerIds: peerIds)
}
public func setSubscriptionsStoriesState(state: CodableEntry?) {
assert(!self.disposed)
self.postbox!.setSubscriptionsStoriesState(state: state)
}
public func getLocalStoryState() -> CodableEntry? {
assert(!self.disposed)
return self.postbox!.getLocalStoryState()
}
public func setLocalStoryState(state: CodableEntry?) {
assert(!self.disposed)
self.postbox!.setLocalStoryState(state: state)
}
public func getPeerStoryState(peerId: PeerId) -> CodableEntry? {
assert(!self.disposed)
return self.postbox!.getPeerStoryState(peerId: peerId)
}
public func setPeerStoryState(peerId: PeerId, state: CodableEntry?) {
assert(!self.disposed)
self.postbox!.setPeerStoryState(peerId: peerId, state: state)
}
public func setStoryItems(peerId: PeerId, items: [StoryItemsTableEntry]) {
assert(!self.disposed)
self.postbox!.setStoryItems(peerId: peerId, items: items)
}
public func getStoryItems(peerId: PeerId) -> [StoryItemsTableEntry] {
return self.postbox!.getStoryItems(peerId: peerId)
}
}
public enum PostboxResult {
@ -1492,6 +1536,10 @@ final class PostboxImpl {
private var currentHiddenChatIds: [PeerId: Bag<Void>] = [:]
private var currentUpdatedHiddenPeerIds: Bool = false
private var currentStoryStatesEvents: [StoryStatesTable.Event] = []
private var currentStorySubscriptionsEvents: [StorySubscriptionsTable.Event] = []
private var currentStoryItemsEvents: [StoryItemsTable.Event] = []
var hiddenChatIds: Set<PeerId> {
if self.currentHiddenChatIds.isEmpty {
return Set()
@ -1610,6 +1658,9 @@ final class PostboxImpl {
let messageHistoryHoleIndexTable: MessageHistoryHoleIndexTable
let groupMessageStatsTable: GroupMessageStatsTable
let peerTimeoutPropertiesTable: PeerTimeoutPropertiesTable
let storyStatesTable: StoryStatesTable
let storySubscriptionsTable: StorySubscriptionsTable
let storyItemsTable: StoryItemsTable
//temporary
let peerRatingTable: RatingTable<PeerId>
@ -1699,6 +1750,9 @@ final class PostboxImpl {
self.noticeTable = NoticeTable(valueBox: self.valueBox, table: NoticeTable.tableSpec(43), useCaches: useCaches)
self.deviceContactImportInfoTable = DeviceContactImportInfoTable(valueBox: self.valueBox, table: DeviceContactImportInfoTable.tableSpec(54), useCaches: useCaches)
self.groupMessageStatsTable = GroupMessageStatsTable(valueBox: self.valueBox, table: GroupMessageStatsTable.tableSpec(58), useCaches: useCaches)
self.storyStatesTable = StoryStatesTable(valueBox: self.valueBox, table: StoryStatesTable.tableSpec(65), useCaches: useCaches)
self.storySubscriptionsTable = StorySubscriptionsTable(valueBox: self.valueBox, table: StorySubscriptionsTable.tableSpec(66), useCaches: useCaches)
self.storyItemsTable = StoryItemsTable(valueBox: self.valueBox, table: StoryItemsTable.tableSpec(69), useCaches: useCaches)
var tables: [Table] = []
tables.append(self.metadataTable)
@ -1766,6 +1820,9 @@ final class PostboxImpl {
tables.append(self.messageHistoryHoleIndexTable)
tables.append(self.groupMessageStatsTable)
tables.append(self.peerTimeoutPropertiesTable)
tables.append(self.storyStatesTable)
tables.append(self.storySubscriptionsTable)
tables.append(self.storyItemsTable)
self.tables = tables
@ -2104,6 +2161,46 @@ final class PostboxImpl {
self.synchronizeGroupMessageStatsTable.set(groupId: groupId, namespace: namespace, needsValidation: false, operations: &self.currentUpdatedGroupSummarySynchronizeOperations)
}
fileprivate func getAllStorySubscriptions() -> (state: CodableEntry?, peerIds: [PeerId]) {
return (
self.storyStatesTable.get(key: .subscriptions),
self.storySubscriptionsTable.getAll()
)
}
fileprivate func replaceAllStorySubscriptions(state: CodableEntry?, peerIds: [PeerId]) {
self.storyStatesTable.set(key: .subscriptions, value: state, events: &self.currentStoryStatesEvents)
self.storySubscriptionsTable.replaceAll(peerIds: peerIds, events: &self.currentStorySubscriptionsEvents)
}
fileprivate func getLocalStoryState() -> CodableEntry? {
return self.storyStatesTable.get(key: .local)
}
fileprivate func setSubscriptionsStoriesState(state: CodableEntry?) {
self.storyStatesTable.set(key: .subscriptions, value: state, events: &self.currentStoryStatesEvents)
}
fileprivate func setLocalStoryState(state: CodableEntry?) {
self.storyStatesTable.set(key: .local, value: state, events: &self.currentStoryStatesEvents)
}
fileprivate func getPeerStoryState(peerId: PeerId) -> CodableEntry? {
return self.storyStatesTable.get(key: .peer(peerId))
}
fileprivate func setPeerStoryState(peerId: PeerId, state: CodableEntry?) {
self.storyStatesTable.set(key: .peer(peerId), value: state, events: &self.currentStoryStatesEvents)
}
fileprivate func setStoryItems(peerId: PeerId, items: [StoryItemsTableEntry]) {
self.storyItemsTable.replace(peerId: peerId, entries: items, events: &self.currentStoryItemsEvents)
}
fileprivate func getStoryItems(peerId: PeerId) -> [StoryItemsTableEntry] {
return self.storyItemsTable.get(peerId: peerId)
}
func renderIntermediateMessage(_ message: IntermediateMessage) -> Message {
let renderedMessage = self.messageHistoryTable.renderMessage(message, peerTable: self.peerTable, threadIndexTable: self.messageHistoryThreadIndexTable)
@ -2170,7 +2267,7 @@ final class PostboxImpl {
let updatedPeerTimeoutAttributes = self.peerTimeoutPropertiesTable.hasUpdates
let transaction = PostboxTransaction(currentUpdatedState: self.currentUpdatedState, currentPeerHoleOperations: self.currentPeerHoleOperations, currentOperationsByPeerId: self.currentOperationsByPeerId, chatListOperations: self.currentChatListOperations, currentUpdatedChatListInclusions: self.currentUpdatedChatListInclusions, currentUpdatedPeers: self.currentUpdatedPeers, currentUpdatedPeerNotificationSettings: self.currentUpdatedPeerNotificationSettings, currentUpdatedPeerNotificationBehaviorTimestamps: self.currentUpdatedPeerNotificationBehaviorTimestamps, currentUpdatedCachedPeerData: self.currentUpdatedCachedPeerData, currentUpdatedPeerPresences: currentUpdatedPeerPresences, currentUpdatedPeerChatListEmbeddedStates: self.currentUpdatedPeerChatListEmbeddedStates, currentUpdatedTotalUnreadStates: self.currentUpdatedTotalUnreadStates, currentUpdatedTotalUnreadSummaries: self.currentUpdatedGroupTotalUnreadSummaries, alteredInitialPeerCombinedReadStates: alteredInitialPeerCombinedReadStates, currentPeerMergedOperationLogOperations: self.currentPeerMergedOperationLogOperations, currentTimestampBasedMessageAttributesOperations: self.currentTimestampBasedMessageAttributesOperations, unsentMessageOperations: self.currentUnsentOperations, updatedSynchronizePeerReadStateOperations: self.currentUpdatedSynchronizeReadStateOperations, currentUpdatedGroupSummarySynchronizeOperations: self.currentUpdatedGroupSummarySynchronizeOperations, currentPreferencesOperations: self.currentPreferencesOperations, currentOrderedItemListOperations: self.currentOrderedItemListOperations, currentItemCollectionItemsOperations: self.currentItemCollectionItemsOperations, currentItemCollectionInfosOperations: self.currentItemCollectionInfosOperations, currentUpdatedPeerChatStates: self.currentUpdatedPeerChatStates, currentGlobalTagsOperations: self.currentGlobalTagsOperations, currentLocalTagsOperations: self.currentLocalTagsOperations, updatedMedia: self.currentUpdatedMedia, replaceRemoteContactCount: self.currentReplaceRemoteContactCount, replaceContactPeerIds: self.currentReplacedContactPeerIds, currentPendingMessageActionsOperations: self.currentPendingMessageActionsOperations, currentUpdatedMessageActionsSummaries: self.currentUpdatedMessageActionsSummaries, currentUpdatedMessageTagSummaries: self.currentUpdatedMessageTagSummaries, currentInvalidateMessageTagSummaries: self.currentInvalidateMessageTagSummaries, currentUpdatedPendingPeerNotificationSettings: self.currentUpdatedPendingPeerNotificationSettings, replacedAdditionalChatListItems: self.currentReplacedAdditionalChatListItems, updatedNoticeEntryKeys: self.currentUpdatedNoticeEntryKeys, updatedCacheEntryKeys: self.currentUpdatedCacheEntryKeys, currentUpdatedMasterClientId: currentUpdatedMasterClientId, updatedFailedMessagePeerIds: self.messageHistoryFailedTable.updatedPeerIds, updatedFailedMessageIds: self.messageHistoryFailedTable.updatedMessageIds, updatedGlobalNotificationSettings: self.currentNeedsReindexUnreadCounters, updatedPeerTimeoutAttributes: updatedPeerTimeoutAttributes, updatedMessageThreadPeerIds: updatedMessageThreadPeerIds, updatedPeerThreadCombinedStates: self.currentUpdatedPeerThreadCombinedStates, updatedPeerThreadsSummaries: Set(alteredInitialPeerThreadsSummaries.keys), updatedPinnedThreads: self.currentUpdatedPinnedThreads, updatedHiddenPeerIds: self.currentUpdatedHiddenPeerIds)
let transaction = PostboxTransaction(currentUpdatedState: self.currentUpdatedState, currentPeerHoleOperations: self.currentPeerHoleOperations, currentOperationsByPeerId: self.currentOperationsByPeerId, chatListOperations: self.currentChatListOperations, currentUpdatedChatListInclusions: self.currentUpdatedChatListInclusions, currentUpdatedPeers: self.currentUpdatedPeers, currentUpdatedPeerNotificationSettings: self.currentUpdatedPeerNotificationSettings, currentUpdatedPeerNotificationBehaviorTimestamps: self.currentUpdatedPeerNotificationBehaviorTimestamps, currentUpdatedCachedPeerData: self.currentUpdatedCachedPeerData, currentUpdatedPeerPresences: currentUpdatedPeerPresences, currentUpdatedPeerChatListEmbeddedStates: self.currentUpdatedPeerChatListEmbeddedStates, currentUpdatedTotalUnreadStates: self.currentUpdatedTotalUnreadStates, currentUpdatedTotalUnreadSummaries: self.currentUpdatedGroupTotalUnreadSummaries, alteredInitialPeerCombinedReadStates: alteredInitialPeerCombinedReadStates, currentPeerMergedOperationLogOperations: self.currentPeerMergedOperationLogOperations, currentTimestampBasedMessageAttributesOperations: self.currentTimestampBasedMessageAttributesOperations, unsentMessageOperations: self.currentUnsentOperations, updatedSynchronizePeerReadStateOperations: self.currentUpdatedSynchronizeReadStateOperations, currentUpdatedGroupSummarySynchronizeOperations: self.currentUpdatedGroupSummarySynchronizeOperations, currentPreferencesOperations: self.currentPreferencesOperations, currentOrderedItemListOperations: self.currentOrderedItemListOperations, currentItemCollectionItemsOperations: self.currentItemCollectionItemsOperations, currentItemCollectionInfosOperations: self.currentItemCollectionInfosOperations, currentUpdatedPeerChatStates: self.currentUpdatedPeerChatStates, currentGlobalTagsOperations: self.currentGlobalTagsOperations, currentLocalTagsOperations: self.currentLocalTagsOperations, updatedMedia: self.currentUpdatedMedia, replaceRemoteContactCount: self.currentReplaceRemoteContactCount, replaceContactPeerIds: self.currentReplacedContactPeerIds, currentPendingMessageActionsOperations: self.currentPendingMessageActionsOperations, currentUpdatedMessageActionsSummaries: self.currentUpdatedMessageActionsSummaries, currentUpdatedMessageTagSummaries: self.currentUpdatedMessageTagSummaries, currentInvalidateMessageTagSummaries: self.currentInvalidateMessageTagSummaries, currentUpdatedPendingPeerNotificationSettings: self.currentUpdatedPendingPeerNotificationSettings, replacedAdditionalChatListItems: self.currentReplacedAdditionalChatListItems, updatedNoticeEntryKeys: self.currentUpdatedNoticeEntryKeys, updatedCacheEntryKeys: self.currentUpdatedCacheEntryKeys, currentUpdatedMasterClientId: currentUpdatedMasterClientId, updatedFailedMessagePeerIds: self.messageHistoryFailedTable.updatedPeerIds, updatedFailedMessageIds: self.messageHistoryFailedTable.updatedMessageIds, updatedGlobalNotificationSettings: self.currentNeedsReindexUnreadCounters, updatedPeerTimeoutAttributes: updatedPeerTimeoutAttributes, updatedMessageThreadPeerIds: updatedMessageThreadPeerIds, updatedPeerThreadCombinedStates: self.currentUpdatedPeerThreadCombinedStates, updatedPeerThreadsSummaries: Set(alteredInitialPeerThreadsSummaries.keys), updatedPinnedThreads: self.currentUpdatedPinnedThreads, updatedHiddenPeerIds: self.currentUpdatedHiddenPeerIds, storyStatesEvents: self.currentStoryStatesEvents, storySubscriptionsEvents: self.currentStorySubscriptionsEvents, storyItemsEvents: self.currentStoryItemsEvents)
var updatedTransactionState: Int64?
var updatedMasterClientId: Int64?
if !transaction.isEmpty {
@ -2226,6 +2323,9 @@ final class PostboxImpl {
self.currentUpdatedPinnedThreads.removeAll()
self.currentUpdatedHiddenPeerIds = false
self.currentNeedsReindexUnreadCounters = false
self.currentStoryStatesEvents.removeAll()
self.currentStorySubscriptionsEvents.removeAll()
self.currentStoryItemsEvents.removeAll()
for table in self.tables {
table.beforeCommit()

View File

@ -49,6 +49,9 @@ final class PostboxTransaction {
let updatedPeerThreadsSummaries: Set<PeerId>
let updatedPinnedThreads: Set<PeerId>
let updatedHiddenPeerIds: Bool
let storyStatesEvents: [StoryStatesTable.Event]
let storySubscriptionsEvents: [StorySubscriptionsTable.Event]
let storyItemsEvents: [StoryItemsTable.Event]
var isEmpty: Bool {
if currentUpdatedState != nil {
@ -195,10 +198,19 @@ final class PostboxTransaction {
if self.updatedHiddenPeerIds {
return false
}
if !self.storyStatesEvents.isEmpty {
return false
}
if !self.storySubscriptionsEvents.isEmpty {
return false
}
if !self.storyItemsEvents.isEmpty {
return false
}
return true
}
init(currentUpdatedState: PostboxCoding?, currentPeerHoleOperations: [MessageHistoryIndexHoleOperationKey: [MessageHistoryIndexHoleOperation]] = [:], currentOperationsByPeerId: [PeerId: [MessageHistoryOperation]], chatListOperations: [PeerGroupId: [ChatListOperation]], currentUpdatedChatListInclusions: [PeerId: PeerChatListInclusion], currentUpdatedPeers: [PeerId: Peer], currentUpdatedPeerNotificationSettings: [PeerId: (PeerNotificationSettings?, PeerNotificationSettings)], currentUpdatedPeerNotificationBehaviorTimestamps: [PeerId: PeerNotificationSettingsBehaviorTimestamp], currentUpdatedCachedPeerData: [PeerId: CachedPeerData], currentUpdatedPeerPresences: [PeerId: PeerPresence], currentUpdatedPeerChatListEmbeddedStates: Set<PeerId>, currentUpdatedTotalUnreadStates: [PeerGroupId: ChatListTotalUnreadState], currentUpdatedTotalUnreadSummaries: [PeerGroupId: PeerGroupUnreadCountersCombinedSummary], alteredInitialPeerCombinedReadStates: [PeerId: CombinedPeerReadState], currentPeerMergedOperationLogOperations: [PeerMergedOperationLogOperation], currentTimestampBasedMessageAttributesOperations: [TimestampBasedMessageAttributesOperation], unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], updatedSynchronizePeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?], currentUpdatedGroupSummarySynchronizeOperations: [PeerGroupAndNamespace: Bool], currentPreferencesOperations: [PreferencesOperation], currentOrderedItemListOperations: [Int32: [OrderedItemListOperation]], currentItemCollectionItemsOperations: [ItemCollectionId: [ItemCollectionItemsOperation]], currentItemCollectionInfosOperations: [ItemCollectionInfosOperation], currentUpdatedPeerChatStates: Set<PeerId>, currentGlobalTagsOperations: [GlobalMessageHistoryTagsOperation], currentLocalTagsOperations: [IntermediateMessageHistoryLocalTagsOperation], updatedMedia: [MediaId: Media?], replaceRemoteContactCount: Int32?, replaceContactPeerIds: Set<PeerId>?, currentPendingMessageActionsOperations: [PendingMessageActionsOperation], currentUpdatedMessageActionsSummaries: [PendingMessageActionsSummaryKey: Int32], currentUpdatedMessageTagSummaries: [MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary], currentInvalidateMessageTagSummaries: [InvalidatedMessageHistoryTagsSummaryEntryOperation], currentUpdatedPendingPeerNotificationSettings: Set<PeerId>, replacedAdditionalChatListItems: [AdditionalChatListItem]?, updatedNoticeEntryKeys: Set<NoticeEntryKey>, updatedCacheEntryKeys: Set<ItemCacheEntryId>, currentUpdatedMasterClientId: Int64?, updatedFailedMessagePeerIds: Set<PeerId>, updatedFailedMessageIds: Set<MessageId>, updatedGlobalNotificationSettings: Bool, updatedPeerTimeoutAttributes: Bool, updatedMessageThreadPeerIds: Set<PeerId>, updatedPeerThreadCombinedStates: Set<PeerId>, updatedPeerThreadsSummaries: Set<PeerId>, updatedPinnedThreads: Set<PeerId>, updatedHiddenPeerIds: Bool) {
init(currentUpdatedState: PostboxCoding?, currentPeerHoleOperations: [MessageHistoryIndexHoleOperationKey: [MessageHistoryIndexHoleOperation]] = [:], currentOperationsByPeerId: [PeerId: [MessageHistoryOperation]], chatListOperations: [PeerGroupId: [ChatListOperation]], currentUpdatedChatListInclusions: [PeerId: PeerChatListInclusion], currentUpdatedPeers: [PeerId: Peer], currentUpdatedPeerNotificationSettings: [PeerId: (PeerNotificationSettings?, PeerNotificationSettings)], currentUpdatedPeerNotificationBehaviorTimestamps: [PeerId: PeerNotificationSettingsBehaviorTimestamp], currentUpdatedCachedPeerData: [PeerId: CachedPeerData], currentUpdatedPeerPresences: [PeerId: PeerPresence], currentUpdatedPeerChatListEmbeddedStates: Set<PeerId>, currentUpdatedTotalUnreadStates: [PeerGroupId: ChatListTotalUnreadState], currentUpdatedTotalUnreadSummaries: [PeerGroupId: PeerGroupUnreadCountersCombinedSummary], alteredInitialPeerCombinedReadStates: [PeerId: CombinedPeerReadState], currentPeerMergedOperationLogOperations: [PeerMergedOperationLogOperation], currentTimestampBasedMessageAttributesOperations: [TimestampBasedMessageAttributesOperation], unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], updatedSynchronizePeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?], currentUpdatedGroupSummarySynchronizeOperations: [PeerGroupAndNamespace: Bool], currentPreferencesOperations: [PreferencesOperation], currentOrderedItemListOperations: [Int32: [OrderedItemListOperation]], currentItemCollectionItemsOperations: [ItemCollectionId: [ItemCollectionItemsOperation]], currentItemCollectionInfosOperations: [ItemCollectionInfosOperation], currentUpdatedPeerChatStates: Set<PeerId>, currentGlobalTagsOperations: [GlobalMessageHistoryTagsOperation], currentLocalTagsOperations: [IntermediateMessageHistoryLocalTagsOperation], updatedMedia: [MediaId: Media?], replaceRemoteContactCount: Int32?, replaceContactPeerIds: Set<PeerId>?, currentPendingMessageActionsOperations: [PendingMessageActionsOperation], currentUpdatedMessageActionsSummaries: [PendingMessageActionsSummaryKey: Int32], currentUpdatedMessageTagSummaries: [MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary], currentInvalidateMessageTagSummaries: [InvalidatedMessageHistoryTagsSummaryEntryOperation], currentUpdatedPendingPeerNotificationSettings: Set<PeerId>, replacedAdditionalChatListItems: [AdditionalChatListItem]?, updatedNoticeEntryKeys: Set<NoticeEntryKey>, updatedCacheEntryKeys: Set<ItemCacheEntryId>, currentUpdatedMasterClientId: Int64?, updatedFailedMessagePeerIds: Set<PeerId>, updatedFailedMessageIds: Set<MessageId>, updatedGlobalNotificationSettings: Bool, updatedPeerTimeoutAttributes: Bool, updatedMessageThreadPeerIds: Set<PeerId>, updatedPeerThreadCombinedStates: Set<PeerId>, updatedPeerThreadsSummaries: Set<PeerId>, updatedPinnedThreads: Set<PeerId>, updatedHiddenPeerIds: Bool, storyStatesEvents: [StoryStatesTable.Event], storySubscriptionsEvents: [StorySubscriptionsTable.Event], storyItemsEvents: [StoryItemsTable.Event]) {
self.currentUpdatedState = currentUpdatedState
self.currentPeerHoleOperations = currentPeerHoleOperations
self.currentOperationsByPeerId = currentOperationsByPeerId
@ -246,5 +258,8 @@ final class PostboxTransaction {
self.updatedPeerThreadsSummaries = updatedPeerThreadsSummaries
self.updatedPinnedThreads = updatedPinnedThreads
self.updatedHiddenPeerIds = updatedHiddenPeerIds
self.storyStatesEvents = storyStatesEvents
self.storySubscriptionsEvents = storySubscriptionsEvents
self.storyItemsEvents = storyItemsEvents
}
}

View File

@ -0,0 +1,101 @@
import Foundation
public final class StoryItemsTableEntry: Equatable {
public let value: CodableEntry
public let id: Int32
public init(
value: CodableEntry,
id: Int32
) {
self.value = value
self.id = id
}
public static func ==(lhs: StoryItemsTableEntry, rhs: StoryItemsTableEntry) -> Bool {
if lhs === rhs {
return true
}
if lhs.id != rhs.id {
return false
}
if lhs.value != rhs.value {
return false
}
return true
}
}
final class StoryItemsTable: Table {
enum Event {
case replace(peerId: PeerId)
}
private struct Key: Hashable {
var peerId: PeerId
var id: Int32
}
static func tableSpec(_ id: Int32) -> ValueBoxTable {
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: false)
}
private let sharedKey = ValueBoxKey(length: 8 + 4)
private func key(_ key: Key) -> ValueBoxKey {
self.sharedKey.setInt64(0, value: key.peerId.toInt64())
self.sharedKey.setInt32(8, value: key.id)
return self.sharedKey
}
private func lowerBound(peerId: PeerId) -> ValueBoxKey {
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: peerId.toInt64())
return key
}
private func upperBound(peerId: PeerId) -> ValueBoxKey {
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: peerId.toInt64())
return key.successor
}
public func get(peerId: PeerId) -> [StoryItemsTableEntry] {
var result: [StoryItemsTableEntry] = []
self.valueBox.range(self.table, start: self.lowerBound(peerId: peerId), end: self.upperBound(peerId: peerId), values: { key, value in
let id = key.getInt32(8)
let entry = CodableEntry(data: value.makeData())
result.append(StoryItemsTableEntry(value: entry, id: id))
return true
}, limit: 10000)
return result
}
public func replace(peerId: PeerId, entries: [StoryItemsTableEntry], events: inout [Event]) {
var previousKeys: [ValueBoxKey] = []
self.valueBox.range(self.table, start: self.lowerBound(peerId: peerId), end: self.upperBound(peerId: peerId), keys: { key in
previousKeys.append(key)
return true
}, limit: 10000)
for key in previousKeys {
self.valueBox.remove(self.table, key: key, secure: true)
}
for entry in entries {
self.valueBox.set(self.table, key: self.key(Key(peerId: peerId, id: entry.id)), value: MemoryBuffer(data: entry.value.data))
}
events.append(.replace(peerId: peerId))
}
override func clearMemoryCache() {
}
override func beforeCommit() {
}
}

View File

@ -0,0 +1,53 @@
import Foundation
final class MutableStoryItemsView: MutablePostboxView {
let peerId: PeerId
var items: [StoryItemsTableEntry]
init(postbox: PostboxImpl, peerId: PeerId) {
self.peerId = peerId
self.items = postbox.storyItemsTable.get(peerId: peerId)
}
func replay(postbox: PostboxImpl, transaction: PostboxTransaction) -> Bool {
var updated = false
if !transaction.storyStatesEvents.isEmpty {
loop: for event in transaction.storyItemsEvents {
switch event {
case .replace(peerId):
let items = postbox.storyItemsTable.get(peerId: self.peerId)
if self.items != items {
self.items = items
updated = true
}
break loop
default:
break
}
}
}
return updated
}
func refreshDueToExternalTransaction(postbox: PostboxImpl) -> Bool {
let items = postbox.storyItemsTable.get(peerId: self.peerId)
if self.items != items {
self.items = items
return true
}
return false
}
func immutableView() -> PostboxView {
return StoryItemsView(self)
}
}
public final class StoryItemsView: PostboxView {
public let items: [StoryItemsTableEntry]
init(_ view: MutableStoryItemsView) {
self.items = view.items
}
}

View File

@ -0,0 +1,70 @@
import Foundation
final class StoryStatesTable: Table {
enum Event {
case set(Key)
}
enum Key: Hashable {
case local
case subscriptions
case peer(PeerId)
init(key: ValueBoxKey) {
switch key.getUInt8(0) {
case 0:
self = .local
case 1:
self = .subscriptions
case 2:
self = .peer(PeerId(key.getInt64(1)))
default:
assertionFailure()
self = .peer(PeerId(namespace: PeerId.Namespace._internalFromInt32Value(0), id: ._internalFromInt64Value(0)))
}
}
func asKey() -> ValueBoxKey {
switch self {
case .local:
let key = ValueBoxKey(length: 1)
key.setUInt8(0, value: 0)
return key
case .subscriptions:
let key = ValueBoxKey(length: 1)
key.setUInt8(0, value: 1)
return key
case let .peer(peerId):
let key = ValueBoxKey(length: 1 + 8)
key.setUInt8(0, value: 2)
key.setInt64(1, value: peerId.toInt64())
return key
}
}
}
static func tableSpec(_ id: Int32) -> ValueBoxTable {
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: false)
}
private let sharedKey = ValueBoxKey(length: 8 + 4)
func get(key: Key) -> CodableEntry? {
return self.valueBox.get(self.table, key: key.asKey()).flatMap { CodableEntry(data: $0.makeData()) }
}
func set(key: Key, value: CodableEntry?, events: inout [Event]) {
if let value = value {
self.valueBox.set(self.table, key: key.asKey(), value: MemoryBuffer(data: value.data))
} else {
self.valueBox.remove(self.table, key: key.asKey(), secure: true)
}
events.append(.set(key))
}
override func clearMemoryCache() {
}
override func beforeCommit() {
}
}

View File

@ -0,0 +1,84 @@
import Foundation
public enum PostboxStoryStatesKey: Hashable {
case local
case subscriptions
case peer(PeerId)
}
private extension PostboxStoryStatesKey {
init(tableKey: StoryStatesTable.Key) {
switch tableKey {
case .local:
self = .local
case .subscriptions:
self = .subscriptions
case let .peer(peerId):
self = .peer(peerId)
}
}
var tableKey: StoryStatesTable.Key {
switch self {
case .local:
return .local
case .subscriptions:
return .subscriptions
case let .peer(peerId):
return .peer(peerId)
}
}
}
final class MutableStoryStatesView: MutablePostboxView {
let key: PostboxStoryStatesKey
var value: CodableEntry?
init(postbox: PostboxImpl, key: PostboxStoryStatesKey) {
self.key = key
self.value = postbox.storyStatesTable.get(key: key.tableKey)
}
func replay(postbox: PostboxImpl, transaction: PostboxTransaction) -> Bool {
var updated = false
if !transaction.storyStatesEvents.isEmpty {
let tableKey = self.key.tableKey
loop: for event in transaction.storyStatesEvents {
switch event {
case .set(tableKey):
let value = postbox.storyStatesTable.get(key: self.key.tableKey)
if value != self.value {
self.value = value
updated = true
}
break loop
default:
break
}
}
}
return updated
}
func refreshDueToExternalTransaction(postbox: PostboxImpl) -> Bool {
let value = postbox.storyStatesTable.get(key: self.key.tableKey)
if value != self.value {
self.value = value
return true
}
return false
}
func immutableView() -> PostboxView {
return StoryStatesView(self)
}
}
public final class StoryStatesView: PostboxView {
public let value: CodableEntry?
init(_ view: MutableStoryStatesView) {
self.value = view.value
}
}

View File

@ -0,0 +1,67 @@
import Foundation
final class StorySubscriptionsTable: Table {
enum Event {
case replaceAll
}
private struct Key: Hashable {
var peerId: PeerId
}
static func tableSpec(_ id: Int32) -> ValueBoxTable {
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: false)
}
private let sharedKey = ValueBoxKey(length: 8)
private func key(_ key: Key) -> ValueBoxKey {
self.sharedKey.setInt64(0, value: key.peerId.toInt64())
return self.sharedKey
}
private func getAllKeys() -> [Key] {
var result: [Key] = []
self.valueBox.scan(self.table, keys: { key in
let peerId = PeerId(key.getInt64(0))
result.append(Key(peerId: peerId))
return true
})
return result
}
public func getAll() -> [PeerId] {
var result: [PeerId] = []
self.valueBox.scan(self.table, keys: { key in
let peerId = PeerId(key.getInt64(0))
result.append(peerId)
return true
})
return result
}
public func replaceAll(peerIds: [PeerId], events: inout [Event]) {
for key in self.getAllKeys() {
self.valueBox.remove(self.table, key: self.key(key), secure: true)
}
for peerId in peerIds {
self.valueBox.set(self.table, key: self.key(Key(peerId: peerId)), value: MemoryBuffer())
}
events.append(.replaceAll)
}
override func clearMemoryCache() {
}
override func beforeCommit() {
}
}

View File

@ -0,0 +1,50 @@
import Foundation
final class MutableStorySubscriptionsView: MutablePostboxView {
var peerIds: [PeerId]
init(postbox: PostboxImpl) {
self.peerIds = postbox.storySubscriptionsTable.getAll()
}
func replay(postbox: PostboxImpl, transaction: PostboxTransaction) -> Bool {
var updated = false
if !transaction.storySubscriptionsEvents.isEmpty {
loop: for event in transaction.storySubscriptionsEvents {
switch event {
case .replaceAll:
let peerIds = postbox.storySubscriptionsTable.getAll()
if self.peerIds != peerIds {
updated = true
self.peerIds = peerIds
}
break loop
}
}
}
return updated
}
func refreshDueToExternalTransaction(postbox: PostboxImpl) -> Bool {
let peerIds = postbox.storySubscriptionsTable.getAll()
if self.peerIds != peerIds {
self.peerIds = peerIds
return true
}
return false
}
func immutableView() -> PostboxView {
return StorySubscriptionsView(self)
}
}
public final class StorySubscriptionsView: PostboxView {
public let peerIds: [PeerId]
init(_ view: MutableStorySubscriptionsView) {
self.peerIds = view.peerIds
}
}

View File

@ -40,6 +40,9 @@ public enum PostboxViewKey: Hashable {
case peerTimeoutAttributes
case messageHistoryThreadIndex(id: PeerId, summaryComponents: ChatListEntrySummaryComponents)
case messageHistoryThreadInfo(peerId: PeerId, threadId: Int64)
case storySubscriptions
case storiesState(key: PostboxStoryStatesKey)
case storyItems(peerId: PeerId)
public func hash(into hasher: inout Hasher) {
switch self {
@ -134,6 +137,12 @@ public enum PostboxViewKey: Hashable {
case let .messageHistoryThreadInfo(peerId, threadId):
hasher.combine(peerId)
hasher.combine(threadId)
case .storySubscriptions:
hasher.combine(18)
case let .storiesState(key):
hasher.combine(key)
case let .storyItems(peerId):
hasher.combine(peerId)
}
}
@ -373,6 +382,24 @@ public enum PostboxViewKey: Hashable {
} else {
return false
}
case .storySubscriptions:
if case .storySubscriptions = rhs {
return true
} else {
return false
}
case let .storiesState(key):
if case .storiesState(key) = rhs {
return true
} else {
return false
}
case let .storyItems(peerId):
if case .storyItems(peerId) = rhs {
return true
} else {
return false
}
}
}
}
@ -457,5 +484,11 @@ func postboxViewForKey(postbox: PostboxImpl, key: PostboxViewKey) -> MutablePost
return MutableMessageHistoryThreadIndexView(postbox: postbox, peerId: id, summaryComponents: summaryComponents)
case let .messageHistoryThreadInfo(peerId, threadId):
return MutableMessageHistoryThreadInfoView(postbox: postbox, peerId: peerId, threadId: threadId)
case .storySubscriptions:
return MutableStorySubscriptionsView(postbox: postbox)
case let .storiesState(key):
return MutableStoryStatesView(postbox: postbox, key: key)
case let .storyItems(peerId):
return MutableStoryItemsView(postbox: postbox, peerId: peerId)
}
}

View File

@ -275,7 +275,7 @@ public final class ShareProlongedLoadingContainerNode: ASDisplayNode, ShareConte
if let account = account, let path = getAppBundle().path(forResource: "BlankVideo", ofType: "m4v"), let size = fileSize(path) {
let decoration = ChatBubbleVideoDecoration(corners: ImageCorners(), nativeSize: CGSize(width: 100.0, height: 100.0), contentMode: .aspectFit, backgroundColor: .black)
let dummyFile = TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 1), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: 12345), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: size, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [])])
let dummyFile = TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 1), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: 12345), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: size, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [], preloadSize: nil)])
let videoContent = NativeVideoContent(id: .message(1, EngineMedia.Id(namespace: 0, id: 1)), userLocation: .other, fileReference: .standalone(media: dummyFile), streamVideo: .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .black, storeAfterDownload: nil)

View File

@ -146,7 +146,7 @@ private func preparedShareItem(account: Account, to peerId: PeerId, value: [Stri
let estimatedSize = TGMediaVideoConverter.estimatedSize(for: preset, duration: finalDuration, hasAudio: true)
let resource = LocalFileVideoMediaResource(randomId: Int64.random(in: Int64.min ... Int64.max), path: asset.url.path, adjustments: resourceAdjustments)
return standaloneUploadedFile(account: account, peerId: peerId, text: "", source: .resource(.standalone(resource: resource)), mimeType: "video/mp4", attributes: [.Video(duration: Int(finalDuration), size: PixelDimensions(width: Int32(finalDimensions.width), height: Int32(finalDimensions.height)), flags: flags)], hintFileIsLarge: estimatedSize > 10 * 1024 * 1024)
return standaloneUploadedFile(account: account, peerId: peerId, text: "", source: .resource(.standalone(resource: resource)), mimeType: "video/mp4", attributes: [.Video(duration: Int(finalDuration), size: PixelDimensions(width: Int32(finalDimensions.width), height: Int32(finalDimensions.height)), flags: flags, preloadSize: nil)], hintFileIsLarge: estimatedSize > 10 * 1024 * 1024)
|> mapError { _ -> PreparedShareItemError in
return .generic
}
@ -212,7 +212,7 @@ private func preparedShareItem(account: Account, to peerId: PeerId, value: [Stri
let mimeType: String
if converted {
mimeType = "video/mp4"
attributes = [.Video(duration: Int(duration), size: PixelDimensions(width: Int32(dimensions.width), height: Int32(dimensions.height)), flags: [.supportsStreaming]), .Animated, .FileName(fileName: "animation.mp4")]
attributes = [.Video(duration: Int(duration), size: PixelDimensions(width: Int32(dimensions.width), height: Int32(dimensions.height)), flags: [.supportsStreaming], preloadSize: nil), .Animated, .FileName(fileName: "animation.mp4")]
} else {
mimeType = "animation/gif"
attributes = [.ImageSize(size: PixelDimensions(width: Int32(dimensions.width), height: Int32(dimensions.height))), .Animated, .FileName(fileName: fileName ?? "animation.gif")]

View File

@ -973,6 +973,8 @@ public class Account {
let networkStatsContext: NetworkStatsContext
public let storySubscriptionsContext: StorySubscriptionsContext?
public init(accountManager: AccountManager<TelegramAccountManagerTypes>, id: AccountRecordId, basePath: String, testingEnvironment: Bool, postbox: Postbox, network: Network, networkArguments: NetworkInitializationArguments, peerId: PeerId, auxiliaryMethods: AccountAuxiliaryMethods, supplementary: Bool) {
self.accountManager = accountManager
self.id = id
@ -989,6 +991,13 @@ public class Account {
self.networkStatsContext = NetworkStatsContext(postbox: postbox)
self.peerInputActivityManager = PeerInputActivityManager()
if !supplementary {
self.storySubscriptionsContext = StorySubscriptionsContext(accountPeerId: peerId, postbox: postbox, network: network)
} else {
self.storySubscriptionsContext = nil
}
self.callSessionManager = CallSessionManager(postbox: postbox, network: network, maxLayer: networkArguments.voipMaxLayer, versions: networkArguments.voipVersions, addUpdates: { [weak self] updates in
self?.stateManager?.addUpdates(updates)
})

View File

@ -50,7 +50,7 @@ public func tagsForStoreMessage(incoming: Bool, attributes: [MessageAttribute],
var isAnimated = false
inner: for attribute in file.attributes {
switch attribute {
case let .Video(_, _, flags):
case let .Video(_, _, flags, _):
if flags.contains(.instantRoundVideo) {
refinedTag = .voiceOrInstantVideo
} else {

View File

@ -6,7 +6,7 @@ import TelegramApi
func dimensionsForFileAttributes(_ attributes: [TelegramMediaFileAttribute]) -> PixelDimensions? {
for attribute in attributes {
switch attribute {
case let .Video(_, size, _):
case let .Video(_, size, _, _):
return size
case let .ImageSize(size):
return size
@ -20,7 +20,7 @@ func dimensionsForFileAttributes(_ attributes: [TelegramMediaFileAttribute]) ->
func durationForFileAttributes(_ attributes: [TelegramMediaFileAttribute]) -> Int32? {
for attribute in attributes {
switch attribute {
case let .Video(duration, _, _):
case let .Video(duration, _, _, _):
return Int32(duration)
case let .Audio(_, duration, _, _, _):
return Int32(duration)
@ -97,7 +97,7 @@ func telegramMediaFileAttributesFromApiAttributes(_ attributes: [Api.DocumentAtt
result.append(.ImageSize(size: PixelDimensions(width: w, height: h)))
case .documentAttributeAnimated:
result.append(.Animated)
case let .documentAttributeVideo(flags, duration, w, h, _):
case let .documentAttributeVideo(flags, duration, w, h, preloadSize):
var videoFlags = TelegramMediaVideoFlags()
if (flags & (1 << 0)) != 0 {
videoFlags.insert(.instantRoundVideo)
@ -105,7 +105,7 @@ func telegramMediaFileAttributesFromApiAttributes(_ attributes: [Api.DocumentAtt
if (flags & (1 << 1)) != 0 {
videoFlags.insert(.supportsStreaming)
}
result.append(.Video(duration: Int(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags))
result.append(.Video(duration: Int(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags, preloadSize: preloadSize))
case let .documentAttributeAudio(flags, duration, title, performer, waveform):
let isVoice = (flags & (1 << 10)) != 0
let waveformBuffer: Data? = waveform?.makeData()

View File

@ -549,7 +549,7 @@ func inputDocumentAttributesFromFileAttributes(_ fileAttributes: [TelegramMediaF
attributes.append(.documentAttributeSticker(flags: flags, alt: displayText, stickerset: stickerSet, maskCoords: inputMaskCoords))
case .HasLinkedStickers:
attributes.append(.documentAttributeHasStickers)
case let .Video(duration, size, videoFlags):
case let .Video(duration, size, videoFlags, preloadSize):
var flags: Int32 = 0
if videoFlags.contains(.instantRoundVideo) {
flags |= (1 << 0)
@ -557,8 +557,11 @@ func inputDocumentAttributesFromFileAttributes(_ fileAttributes: [TelegramMediaF
if videoFlags.contains(.supportsStreaming) {
flags |= (1 << 1)
}
if preloadSize != nil {
flags |= (1 << 2)
}
attributes.append(.documentAttributeVideo(flags: flags, duration: Int32(duration), w: Int32(size.width), h: Int32(size.height), preloadPrefixSize: nil))
attributes.append(.documentAttributeVideo(flags: flags, duration: Int32(duration), w: Int32(size.width), h: Int32(size.height), preloadPrefixSize: preloadSize))
case let .Audio(isVoice, duration, title, performer, waveform):
var flags: Int32 = 0
if isVoice {
@ -628,7 +631,7 @@ public func statsCategoryForFileWithAttributes(_ attributes: [TelegramMediaFileA
} else {
return .audio
}
case let .Video(_, _, flags):
case let .Video(_, _, flags, _):
if flags.contains(TelegramMediaVideoFlags.instantRoundVideo) {
return .voiceMessages
} else {

View File

@ -538,7 +538,7 @@ private func decryptedAttributes46(_ attributes: [TelegramMediaFileAttribute], t
result.append(.documentAttributeSticker(alt: displayText, stickerset: stickerSet))
case let .ImageSize(size):
result.append(.documentAttributeImageSize(w: Int32(size.width), h: Int32(size.height)))
case let .Video(duration, size, _):
case let .Video(duration, size, _, _):
result.append(.documentAttributeVideo(duration: Int32(duration), w: Int32(size.width), h: Int32(size.height)))
case let .Audio(isVoice, duration, title, performer, waveform):
var flags: Int32 = 0
@ -597,7 +597,7 @@ private func decryptedAttributes73(_ attributes: [TelegramMediaFileAttribute], t
result.append(.documentAttributeSticker(alt: displayText, stickerset: stickerSet))
case let .ImageSize(size):
result.append(.documentAttributeImageSize(w: Int32(size.width), h: Int32(size.height)))
case let .Video(duration, size, videoFlags):
case let .Video(duration, size, videoFlags, _):
var flags: Int32 = 0
if videoFlags.contains(.instantRoundVideo) {
flags |= 1 << 0
@ -660,7 +660,7 @@ private func decryptedAttributes101(_ attributes: [TelegramMediaFileAttribute],
result.append(.documentAttributeSticker(alt: displayText, stickerset: stickerSet))
case let .ImageSize(size):
result.append(.documentAttributeImageSize(w: Int32(size.width), h: Int32(size.height)))
case let .Video(duration, size, videoFlags):
case let .Video(duration, size, videoFlags, _):
var flags: Int32 = 0
if videoFlags.contains(.instantRoundVideo) {
flags |= 1 << 0
@ -723,7 +723,7 @@ private func decryptedAttributes144(_ attributes: [TelegramMediaFileAttribute],
result.append(.documentAttributeSticker(alt: displayText, stickerset: stickerSet))
case let .ImageSize(size):
result.append(.documentAttributeImageSize(w: Int32(size.width), h: Int32(size.height)))
case let .Video(duration, size, videoFlags):
case let .Video(duration, size, videoFlags, _):
var flags: Int32 = 0
if videoFlags.contains(.instantRoundVideo) {
flags |= 1 << 0

View File

@ -610,7 +610,7 @@ extension TelegramMediaFileAttribute {
}
self = .Sticker(displayText: alt, packReference: packReference, maskData: nil)
case let .documentAttributeVideo(duration, w, h):
self = .Video(duration: Int(duration), size: PixelDimensions(width: w, height: h), flags: [])
self = .Video(duration: Int(duration), size: PixelDimensions(width: w, height: h), flags: [], preloadSize: nil)
}
}
}
@ -642,7 +642,7 @@ extension TelegramMediaFileAttribute {
if (flags & (1 << 0)) != 0 {
videoFlags.insert(.instantRoundVideo)
}
self = .Video(duration: Int(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags)
self = .Video(duration: Int(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags, preloadSize: nil)
}
}
}
@ -674,7 +674,7 @@ extension TelegramMediaFileAttribute {
if (flags & (1 << 0)) != 0 {
videoFlags.insert(.instantRoundVideo)
}
self = .Video(duration: Int(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags)
self = .Video(duration: Int(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags, preloadSize: nil)
}
}
}
@ -706,7 +706,7 @@ extension TelegramMediaFileAttribute {
if (flags & (1 << 0)) != 0 {
videoFlags.insert(.instantRoundVideo)
}
self = .Video(duration: Int(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags)
self = .Video(duration: Int(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags, preloadSize: nil)
}
}
}
@ -821,7 +821,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32
text = caption
}
if let file = file {
let parsedAttributes: [TelegramMediaFileAttribute] = [.Video(duration: Int(duration), size: PixelDimensions(width: w, height: h), flags: []), .FileName(fileName: "video.mov")]
let parsedAttributes: [TelegramMediaFileAttribute] = [.Video(duration: Int(duration), size: PixelDimensions(width: w, height: h), flags: [], preloadSize: nil), .FileName(fileName: "video.mov")]
var previewRepresentations: [TelegramMediaImageRepresentation] = []
if thumb.size != 0 {
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
@ -1021,7 +1021,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32
loop: for attr in parsedAttributes {
switch attr {
case let .Video(_, _, flags):
case let .Video(_, _, flags, _):
if flags.contains(.instantRoundVideo) {
attributes.append(ConsumableContentMessageAttribute(consumed: false))
}
@ -1040,7 +1040,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32
text = caption
}
if let file = file {
let parsedAttributes: [TelegramMediaFileAttribute] = [.Video(duration: Int(duration), size: PixelDimensions(width: w, height: h), flags: []), .FileName(fileName: "video.mov")]
let parsedAttributes: [TelegramMediaFileAttribute] = [.Video(duration: Int(duration), size: PixelDimensions(width: w, height: h), flags: [], preloadSize: nil), .FileName(fileName: "video.mov")]
var previewRepresentations: [TelegramMediaImageRepresentation] = []
if thumb.size != 0 {
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
@ -1300,7 +1300,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32
loop: for attr in parsedAttributes {
switch attr {
case let .Video(_, _, flags):
case let .Video(_, _, flags, _):
if flags.contains(.instantRoundVideo) {
attributes.append(ConsumableContentMessageAttribute(consumed: false))
}
@ -1319,7 +1319,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32
text = caption
}
if let file = file {
let parsedAttributes: [TelegramMediaFileAttribute] = [.Video(duration: Int(duration), size: PixelDimensions(width: w, height: h), flags: []), .FileName(fileName: "video.mov")]
let parsedAttributes: [TelegramMediaFileAttribute] = [.Video(duration: Int(duration), size: PixelDimensions(width: w, height: h), flags: [], preloadSize: nil), .FileName(fileName: "video.mov")]
var previewRepresentations: [TelegramMediaImageRepresentation] = []
if thumb.size != 0 {
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
@ -1501,7 +1501,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32
loop: for attr in parsedAttributes {
switch attr {
case let .Video(_, _, flags):
case let .Video(_, _, flags, _):
if flags.contains(.instantRoundVideo) {
attributes.append(ConsumableContentMessageAttribute(consumed: false))
}
@ -1520,7 +1520,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32
text = caption
}
if let file = file {
let parsedAttributes: [TelegramMediaFileAttribute] = [.Video(duration: Int(duration), size: PixelDimensions(width: w, height: h), flags: []), .FileName(fileName: "video.mov")]
let parsedAttributes: [TelegramMediaFileAttribute] = [.Video(duration: Int(duration), size: PixelDimensions(width: w, height: h), flags: [], preloadSize: nil), .FileName(fileName: "video.mov")]
var previewRepresentations: [TelegramMediaImageRepresentation] = []
if thumb.size != 0 {
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))

View File

@ -223,7 +223,7 @@ public enum TelegramMediaFileAttribute: PostboxCoding, Equatable {
case Sticker(displayText: String, packReference: StickerPackReference?, maskData: StickerMaskCoords?)
case ImageSize(size: PixelDimensions)
case Animated
case Video(duration: Int, size: PixelDimensions, flags: TelegramMediaVideoFlags)
case Video(duration: Int, size: PixelDimensions, flags: TelegramMediaVideoFlags, preloadSize: Int32?)
case Audio(isVoice: Bool, duration: Int, title: String?, performer: String?, waveform: Data?)
case HasLinkedStickers
case hintFileIsLarge
@ -243,7 +243,7 @@ public enum TelegramMediaFileAttribute: PostboxCoding, Equatable {
case typeAnimated:
self = .Animated
case typeVideo:
self = .Video(duration: Int(decoder.decodeInt32ForKey("du", orElse: 0)), size: PixelDimensions(width: decoder.decodeInt32ForKey("w", orElse: 0), height: decoder.decodeInt32ForKey("h", orElse: 0)), flags: TelegramMediaVideoFlags(rawValue: decoder.decodeInt32ForKey("f", orElse: 0)))
self = .Video(duration: Int(decoder.decodeInt32ForKey("du", orElse: 0)), size: PixelDimensions(width: decoder.decodeInt32ForKey("w", orElse: 0), height: decoder.decodeInt32ForKey("h", orElse: 0)), flags: TelegramMediaVideoFlags(rawValue: decoder.decodeInt32ForKey("f", orElse: 0)), preloadSize: decoder.decodeOptionalInt32ForKey("prs"))
case typeAudio:
let waveformBuffer = decoder.decodeBytesForKeyNoCopy("wf")
var waveform: Data?
@ -290,12 +290,17 @@ public enum TelegramMediaFileAttribute: PostboxCoding, Equatable {
encoder.encodeInt32(Int32(size.height), forKey: "h")
case .Animated:
encoder.encodeInt32(typeAnimated, forKey: "t")
case let .Video(duration, size, flags):
case let .Video(duration, size, flags, preloadSize):
encoder.encodeInt32(typeVideo, forKey: "t")
encoder.encodeInt32(Int32(duration), forKey: "du")
encoder.encodeInt32(Int32(size.width), forKey: "w")
encoder.encodeInt32(Int32(size.height), forKey: "h")
encoder.encodeInt32(flags.rawValue, forKey: "f")
if let preloadSize = preloadSize {
encoder.encodeInt32(preloadSize, forKey: "prs")
} else {
encoder.encodeNil(forKey: "prs")
}
case let .Audio(isVoice, duration, title, performer, waveform):
encoder.encodeInt32(typeAudio, forKey: "t")
encoder.encodeInt32(isVoice ? 1 : 0, forKey: "iv")
@ -568,13 +573,22 @@ public final class TelegramMediaFile: Media, Equatable, Codable {
public var isInstantVideo: Bool {
for attribute in self.attributes {
if case .Video(_, _, let flags) = attribute {
if case .Video(_, _, let flags, _) = attribute {
return flags.contains(.instantRoundVideo)
}
}
return false
}
public var preloadSize: Int32? {
for attribute in self.attributes {
if case .Video(_, _, _, let preloadSize) = attribute {
return preloadSize
}
}
return nil
}
public var isAnimated: Bool {
for attribute in self.attributes {
if case .Animated = attribute {

View File

@ -113,7 +113,7 @@ func _internal_outgoingMessageWithChatContextResult(to peerId: PeerId, threadId:
if let dimensions = externalReference.content?.dimensions {
fileAttributes.append(.ImageSize(size: dimensions))
if externalReference.type == "gif" {
fileAttributes.append(.Video(duration: Int(Int32(externalReference.content?.duration ?? 0)), size: dimensions, flags: []))
fileAttributes.append(.Video(duration: Int(Int32(externalReference.content?.duration ?? 0)), size: dimensions, flags: [], preloadSize: nil))
}
}

View File

@ -25,6 +25,423 @@ public struct EngineStoryPrivacy: Equatable {
}
}
public enum Stories {
public final class Item: Codable, Equatable {
public struct Views: Codable, Equatable {
private enum CodingKeys: String, CodingKey {
case seenCount = "seenCount"
case seenPeerIds = "seenPeerIds"
}
public var seenCount: Int
public var seenPeerIds: [PeerId]
public init(seenCount: Int, seenPeerIds: [PeerId]) {
self.seenCount = seenCount
self.seenPeerIds = seenPeerIds
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.seenCount = Int(try container.decode(Int32.self, forKey: .seenCount))
self.seenPeerIds = try container.decode([Int64].self, forKey: .seenPeerIds).map(PeerId.init)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(Int32(clamping: self.seenCount), forKey: .seenCount)
try container.encode(self.seenPeerIds.map { $0.toInt64() }, forKey: .seenPeerIds)
}
}
public struct Privacy: Codable, Equatable {
private enum CodingKeys: String, CodingKey {
case base = "base"
case additionallyIncludePeers = "addPeers"
}
public enum Base: Int32 {
case everyone = 0
case contacts = 1
case closeFriends = 2
case nobody = 3
}
public var base: Base
public var additionallyIncludePeers: [PeerId]
public init(base: Base, additionallyIncludePeers: [PeerId]) {
self.base = base
self.additionallyIncludePeers = additionallyIncludePeers
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.base = Base(rawValue: try container.decode(Int32.self, forKey: .base)) ?? .nobody
self.additionallyIncludePeers = try container.decode([Int64].self, forKey: .additionallyIncludePeers).map(PeerId.init)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.base.rawValue, forKey: .base)
try container.encode(self.additionallyIncludePeers.map { $0.toInt64() }, forKey: .additionallyIncludePeers)
}
}
private enum CodingKeys: String, CodingKey {
case id
case timestamp
case media
case text
case entities
case views
case privacy
}
public let id: Int32
public let timestamp: Int32
public let media: Media?
public let text: String
public let entities: [MessageTextEntity]
public let views: Views?
public let privacy: Privacy?
public init(
id: Int32,
timestamp: Int32,
media: Media?,
text: String,
entities: [MessageTextEntity],
views: Views?,
privacy: Privacy?
) {
self.id = id
self.timestamp = timestamp
self.media = media
self.text = text
self.entities = entities
self.views = views
self.privacy = privacy
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int32.self, forKey: .id)
self.timestamp = try container.decode(Int32.self, forKey: .timestamp)
if let mediaData = try container.decodeIfPresent(Data.self, forKey: .media) {
self.media = PostboxDecoder(buffer: MemoryBuffer(data: mediaData)).decodeRootObject() as? Media
} else {
self.media = nil
}
self.text = try container.decode(String.self, forKey: .text)
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)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.id, forKey: .id)
try container.encode(self.timestamp, forKey: .timestamp)
if let media = self.media {
let encoder = PostboxEncoder()
encoder.encodeRootObject(media)
let mediaData = encoder.makeData()
try container.encode(mediaData, forKey: .media)
}
try container.encode(self.text, forKey: .text)
try container.encode(self.entities, forKey: .entities)
try container.encodeIfPresent(self.views, forKey: .views)
try container.encodeIfPresent(self.privacy, forKey: .privacy)
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.timestamp != rhs.timestamp {
return false
}
if let lhsMedia = lhs.media, let rhsMedia = rhs.media {
if !lhsMedia.isEqual(to: rhsMedia) {
return false
}
} else {
if (lhs.media == nil) != (rhs.media == nil) {
return false
}
}
if lhs.text != rhs.text {
return false
}
if lhs.entities != rhs.entities {
return false
}
if lhs.views != rhs.views {
return false
}
if lhs.privacy != rhs.privacy {
return false
}
return true
}
}
public final class Placeholder: Codable, Equatable {
private enum CodingKeys: String, CodingKey {
case id
case timestamp
}
public let id: Int32
public let timestamp: Int32
public init(
id: Int32,
timestamp: Int32
) {
self.id = id
self.timestamp = timestamp
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int32.self, forKey: .id)
self.timestamp = try container.decode(Int32.self, forKey: .timestamp)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.id, forKey: .id)
try container.encode(self.timestamp, forKey: .timestamp)
}
public static func ==(lhs: Placeholder, rhs: Placeholder) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.timestamp != rhs.timestamp {
return false
}
return true
}
}
public enum StoredItem: Codable, Equatable {
public enum DecodingError: Error {
case generic
}
private enum CodingKeys: String, CodingKey {
case discriminator = "d"
case item = "i"
case placeholder = "p"
}
case item(Item)
case placeholder(Placeholder)
public var id: Int32 {
switch self {
case let .item(item):
return item.id
case let .placeholder(placeholder):
return placeholder.id
}
}
public var timestamp: Int32 {
switch self {
case let .item(item):
return item.timestamp
case let .placeholder(placeholder):
return placeholder.timestamp
}
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
switch try container.decode(Int32.self, forKey: .discriminator) {
case 0:
self = .item(try container.decode(Item.self, forKey: .item))
case 1:
self = .placeholder(try container.decode(Placeholder.self, forKey: .placeholder))
default:
throw DecodingError.generic
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .item(item):
try container.encode(0 as Int32, forKey: .discriminator)
try container.encode(item, forKey: .item)
case let .placeholder(placeholder):
try container.encode(1 as Int32, forKey: .discriminator)
try container.encode(placeholder, forKey: .placeholder)
}
}
}
public final class PeerState: Equatable, Codable {
private enum CodingKeys: CodingKey {
case subscriptionsOpaqueState
case maxReadId
}
public let subscriptionsOpaqueState: String?
public let maxReadId: Int32
public init(
subscriptionsOpaqueState: String?,
maxReadId: Int32
){
self.subscriptionsOpaqueState = subscriptionsOpaqueState
self.maxReadId = maxReadId
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.subscriptionsOpaqueState = try container.decodeIfPresent(String.self, forKey: .subscriptionsOpaqueState)
self.maxReadId = try container.decode(Int32.self, forKey: .maxReadId)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(self.subscriptionsOpaqueState, forKey: .subscriptionsOpaqueState)
try container.encode(self.maxReadId, forKey: .maxReadId)
}
public static func ==(lhs: PeerState, rhs: PeerState) -> Bool {
if lhs.subscriptionsOpaqueState != rhs.subscriptionsOpaqueState {
return false
}
if lhs.maxReadId != rhs.maxReadId {
return false
}
return true
}
}
public final class SubscriptionsState: Equatable, Codable {
private enum CodingKeys: CodingKey {
case opaqueState
case hasMore
}
public let opaqueState: String
public let hasMore: Bool
public init(
opaqueState: String,
hasMore: Bool
) {
self.opaqueState = opaqueState
self.hasMore = hasMore
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.opaqueState = try container.decode(String.self, forKey: .opaqueState)
self.hasMore = try container.decode(Bool.self, forKey: .hasMore)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.opaqueState, forKey: .opaqueState)
try container.encode(self.hasMore, forKey: .hasMore)
}
public static func ==(lhs: SubscriptionsState, rhs: SubscriptionsState) -> Bool {
if lhs.opaqueState != rhs.opaqueState {
return false
}
if lhs.hasMore != rhs.hasMore {
return false
}
return true
}
}
}
public final class EngineStorySubscriptions: Equatable {
public final class Item: Equatable {
public let peer: EnginePeer
public let hasUnseen: Bool
public let storyCount: Int
public let lastTimestamp: Int32
public init(
peer: EnginePeer,
hasUnseen: Bool,
storyCount: Int,
lastTimestamp: Int32
) {
self.peer = peer
self.hasUnseen = hasUnseen
self.storyCount = storyCount
self.lastTimestamp = lastTimestamp
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs.peer != rhs.peer {
return false
}
if lhs.hasUnseen != rhs.hasUnseen {
return false
}
if lhs.storyCount != rhs.storyCount {
return false
}
if lhs.lastTimestamp != rhs.lastTimestamp {
return false
}
return true
}
}
public let items: [Item]
public let hasMoreToken: String?
public init(items: [Item], hasMoreToken: String?) {
self.items = items
self.hasMoreToken = hasMoreToken
}
public static func ==(lhs: EngineStorySubscriptions, rhs: EngineStorySubscriptions) -> Bool {
if lhs.items != rhs.items {
return false
}
if lhs.hasMoreToken != rhs.hasMoreToken {
return false
}
return true
}
}
func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], privacy: EngineStoryPrivacy) -> Signal<Never, NoError> {
let originalMedia: Media
let contentToUpload: MessageContentToUpload
@ -71,7 +488,7 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text:
mimeType: "video/mp4",
size: nil,
attributes: [
TelegramMediaFileAttribute.Video(duration: duration, size: dimensions, flags: .supportsStreaming)
TelegramMediaFileAttribute.Video(duration: duration, size: dimensions, flags: .supportsStreaming, preloadSize: nil)
]
)
originalMedia = fileMedia
@ -227,6 +644,13 @@ func _internal_deleteStory(account: Account, id: Int32) -> Signal<Never, NoError
func _internal_markStoryAsSeen(account: Account, peerId: PeerId, id: Int32) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Api.InputUser? in
if let peerStoryState = transaction.getPeerStoryState(peerId: peerId)?.get(Stories.PeerState.self) {
transaction.setPeerStoryState(peerId: peerId, state: CodableEntry(Stories.PeerState(
subscriptionsOpaqueState: peerStoryState.subscriptionsOpaqueState,
maxReadId: max(peerStoryState.maxReadId, id)
)))
}
return transaction.getPeer(peerId).flatMap(apiInputUser)
}
|> mapToSignal { inputUser -> Signal<Never, NoError> in
@ -236,6 +660,12 @@ func _internal_markStoryAsSeen(account: Account, peerId: PeerId, id: Int32) -> S
account.stateManager.injectStoryUpdates(updates: [.read(peerId: peerId, maxId: id)])
#if DEBUG
if "".isEmpty {
return .complete()
}
#endif
return account.network.request(Api.functions.stories.readStories(userId: inputUser, maxId: id))
|> `catch` { _ -> Signal<[Int32], NoError> in
return .single([])
@ -257,6 +687,74 @@ extension Api.StoryItem {
}
}
extension Stories.Item.Views {
init(apiViews: Api.StoryViews) {
switch apiViews {
case let .storyViews(recentViewers, viewsCount):
self.init(seenCount: Int(viewsCount), seenPeerIds: recentViewers.map { PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) })
}
}
}
extension Stories.StoredItem {
init?(apiStoryItem: Api.StoryItem, peerId: PeerId, transaction: Transaction) {
switch apiStoryItem {
case let .storyItem(flags, id, date, caption, entities, media, privacy, views):
let _ = flags
let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, peerId)
if let parsedMedia = parsedMedia {
var parsedPrivacy: Stories.Item.Privacy?
if let privacy = privacy {
var base: Stories.Item.Privacy.Base = .everyone
var additionalPeerIds: [PeerId] = []
for rule in privacy {
switch rule {
case .privacyValueAllowAll:
base = .everyone
case .privacyValueAllowContacts:
base = .contacts
case .privacyValueAllowCloseFriends:
base = .closeFriends
case let .privacyValueAllowUsers(users):
for id in users {
additionalPeerIds.append(EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(id)))
}
case let .privacyValueAllowChatParticipants(chats):
for id in chats {
if let peer = transaction.getPeer(EnginePeer.Id(namespace: Namespaces.Peer.CloudGroup, id: EnginePeer.Id.Id._internalFromInt64Value(id))) {
additionalPeerIds.append(peer.id)
} else if let peer = transaction.getPeer(EnginePeer.Id(namespace: Namespaces.Peer.CloudChannel, id: EnginePeer.Id.Id._internalFromInt64Value(id))) {
additionalPeerIds.append(peer.id)
}
}
default:
break
}
}
parsedPrivacy = Stories.Item.Privacy(base: base, additionallyIncludePeers: additionalPeerIds)
}
let item = Stories.Item(
id: id,
timestamp: date,
media: parsedMedia,
text: caption ?? "",
entities: entities.flatMap { entities in return messageTextEntitiesFromApiEntities(entities) } ?? [],
views: views.flatMap(Stories.Item.Views.init(apiViews:)),
privacy: parsedPrivacy
)
self = .item(item)
} else {
return nil
}
case let .storyItemSkipped(id, date):
self = .placeholder(Stories.Placeholder(id: id, timestamp: date))
case .storyItemDeleted:
return nil
}
}
}
func _internal_parseApiStoryItem(transaction: Transaction, peerId: PeerId, apiStory: Api.StoryItem) -> StoryListContext.Item? {
switch apiStory {
case let .storyItem(flags, id, date, caption, entities, media, privacy, views):
@ -323,6 +821,50 @@ func _internal_parseApiStoryViews(transaction: Transaction, views: Api.StoryView
}
}
func _internal_getStoriesById(accountPeerId: PeerId, postbox: Postbox, network: Network, peerId: PeerId, ids: [Int32]) -> Signal<[Stories.StoredItem], NoError> {
return postbox.transaction { transaction -> Api.InputUser? in
return transaction.getPeer(peerId).flatMap(apiInputUser)
}
|> mapToSignal { inputUser -> Signal<[Stories.StoredItem], NoError> in
guard let inputUser = inputUser else {
return .single([])
}
return network.request(Api.functions.stories.getStoriesByID(userId: inputUser, id: ids))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.stories.Stories?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<[Stories.StoredItem], NoError> in
guard let result = result else {
return .single([])
}
return postbox.transaction { transaction -> [Stories.StoredItem] in
switch result {
case let .stories(_, stories, users):
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: accountPeerId, peerPresences: peerPresences)
return stories.compactMap { apiStoryItem -> Stories.StoredItem? in
return Stories.StoredItem(apiStoryItem: apiStoryItem, peerId: peerId, transaction: transaction)
}
}
}
}
}
}
func _internal_getStoryById(accountPeerId: PeerId, postbox: Postbox, network: Network, peer: PeerReference, id: Int32) -> Signal<StoryListContext.Item?, NoError> {
guard let inputUser = peer.inputUser else {
return .single(nil)

View File

@ -9,6 +9,439 @@ enum InternalStoryUpdate {
case read(peerId: PeerId, maxId: Int32)
}
public final class StorySubscriptionsContext {
private enum OpaqueStateMark: Equatable {
case empty
case value(String)
}
private struct TaskState {
var isRefreshScheduled: Bool = false
var isLoadMoreScheduled: Bool = false
}
private final class Impl {
private let accountPeerId: PeerId
private let queue: Queue
private let postbox: Postbox
private let network: Network
private var taskState = TaskState()
private var isLoading: Bool = false
private var loadedStateMark: OpaqueStateMark?
private var stateDisposable: Disposable?
private let loadMoreDisposable = MetaDisposable()
init(queue: Queue, accountPeerId: PeerId, postbox: Postbox, network: Network) {
self.accountPeerId = accountPeerId
self.queue = queue
self.postbox = postbox
self.network = network
self.taskState.isRefreshScheduled = true
self.updateTasks()
}
deinit {
self.stateDisposable?.dispose()
self.loadMoreDisposable.dispose()
}
func loadMore() {
self.taskState.isLoadMoreScheduled = true
self.updateTasks()
}
private func updateTasks() {
if self.isLoading {
return
}
if self.taskState.isRefreshScheduled {
self.isLoading = true
self.stateDisposable = (postbox.combinedView(keys: [PostboxViewKey.storiesState(key: .subscriptions)])
|> take(1)
|> deliverOn(self.queue)).start(next: { [weak self] views in
guard let `self` = self else {
return
}
guard let storiesStateView = views.views[PostboxViewKey.storiesState(key: .subscriptions)] as? StoryStatesView else {
return
}
let stateMark: OpaqueStateMark
if let subscriptionsState = storiesStateView.value?.get(Stories.SubscriptionsState.self) {
stateMark = .value(subscriptionsState.opaqueState)
} else {
stateMark = .empty
}
self.loadImpl(isRefresh: true, stateMark: stateMark)
})
} else if self.taskState.isLoadMoreScheduled {
self.isLoading = true
self.stateDisposable = (postbox.combinedView(keys: [PostboxViewKey.storiesState(key: .subscriptions)])
|> take(1)
|> deliverOn(self.queue)).start(next: { [weak self] views in
guard let `self` = self else {
return
}
guard let storiesStateView = views.views[PostboxViewKey.storiesState(key: .subscriptions)] as? StoryStatesView else {
return
}
let hasMore: Bool
let stateMark: OpaqueStateMark
if let subscriptionsState = storiesStateView.value?.get(Stories.SubscriptionsState.self) {
hasMore = subscriptionsState.hasMore
stateMark = .value(subscriptionsState.opaqueState)
} else {
stateMark = .empty
hasMore = true
}
if hasMore && self.loadedStateMark != stateMark {
self.loadImpl(isRefresh: false, stateMark: stateMark)
} else {
self.isLoading = false
self.taskState.isLoadMoreScheduled = false
self.updateTasks()
}
})
}
}
private func loadImpl(isRefresh: Bool, stateMark: OpaqueStateMark) {
var flags: Int32 = 0
var state: String?
switch stateMark {
case .empty:
break
case let .value(value):
state = value
flags |= 1 << 0
if !isRefresh {
flags |= 1 << 1
}
}
let accountPeerId = self.accountPeerId
self.loadMoreDisposable.set((self.network.request(Api.functions.stories.getAllStories(flags: flags, state: state))
|> deliverOn(self.queue)).start(next: { [weak self] result in
guard let self else {
return
}
let _ = (self.postbox.transaction { transaction -> Void in
switch result {
case let .allStoriesNotModified(state):
self.loadedStateMark = .value(state)
let (currentStateValue, _) = transaction.getAllStorySubscriptions()
let currentState = currentStateValue.flatMap { $0.get(Stories.SubscriptionsState.self) }
var hasMore = false
if let currentState = currentState {
hasMore = currentState.hasMore
}
transaction.setSubscriptionsStoriesState(state: CodableEntry(Stories.SubscriptionsState(opaqueState: state, hasMore: hasMore)))
case let .allStories(flags, state, userStories, users):
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: accountPeerId, peerPresences: peerPresences)
let hasMore: Bool = (flags & (1 << 0)) != 0
let (_, currentPeerItems) = transaction.getAllStorySubscriptions()
var peerEntries: [PeerId] = []
for userStorySet in userStories {
switch userStorySet {
case let .userStories(_, userId, maxReadId, stories):
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))
let previousPeerEntries: [StoryItemsTableEntry] = transaction.getStoryItems(peerId: peerId)
var updatedPeerEntries: [StoryItemsTableEntry] = []
for story in stories {
if let storedItem = Stories.StoredItem(apiStoryItem: story, peerId: peerId, transaction: transaction) {
if case .placeholder = storedItem, let previousEntry = previousPeerEntries.first(where: { $0.id == storedItem.id }) {
updatedPeerEntries.append(previousEntry)
} else {
if let codedEntry = CodableEntry(storedItem) {
updatedPeerEntries.append(StoryItemsTableEntry(value: codedEntry, id: storedItem.id))
}
}
}
}
peerEntries.append(peerId)
transaction.setStoryItems(peerId: peerId, items: updatedPeerEntries)
transaction.setPeerStoryState(peerId: peerId, state: CodableEntry(Stories.PeerState(
subscriptionsOpaqueState: state,
maxReadId: maxReadId ?? 0
)))
}
}
if !isRefresh {
let leftPeerIds = currentPeerItems.filter({ !peerEntries.contains($0) })
if !leftPeerIds.isEmpty {
peerEntries = leftPeerIds + peerEntries
}
}
transaction.replaceAllStorySubscriptions(state: CodableEntry(Stories.SubscriptionsState(opaqueState: state, hasMore: hasMore)), peerIds: peerEntries)
}
}
|> deliverOn(self.queue)).start(completed: { [weak self] in
guard let `self` = self else {
return
}
self.isLoading = false
if isRefresh {
self.taskState.isRefreshScheduled = false
} else {
self.taskState.isLoadMoreScheduled = false
}
self.updateTasks()
})
}))
}
}
private let queue = Queue(name: "StorySubscriptionsContext")
private let impl: QueueLocalObject<Impl>
init(accountPeerId: PeerId, postbox: Postbox, network: Network) {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
Impl(queue: queue, accountPeerId: accountPeerId, postbox: postbox, network: network)
})
}
public func loadMore() {
self.impl.with { impl in
impl.loadMore()
}
}
}
public final class PeerStoryFocusContext {
private final class Impl {
private let queue: Queue
private let account: Account
private let peerId: PeerId
private let focusItemId: Int32?
let state = Promise<State>()
private var disposable: Disposable?
private var isLoadingPlaceholder: Bool = false
private let loadDisposable = MetaDisposable()
private var placeholderLoadWasUnsuccessful: Bool = false
init(
queue: Queue,
account: Account,
peerId: PeerId,
focusItemId: Int32?
) {
self.queue = queue
self.account = account
self.peerId = peerId
self.focusItemId = focusItemId
self.disposable = (account.postbox.combinedView(keys: [
PostboxViewKey.storiesState(key: .peer(peerId)),
PostboxViewKey.storyItems(peerId: peerId)
])
|> deliverOn(self.queue)).start(next: { [weak self] views in
guard let `self` = self else {
return
}
guard let peerStateView = views.views[PostboxViewKey.storiesState(key: .peer(peerId))] as? StoryStatesView else {
return
}
guard let itemsView = views.views[PostboxViewKey.storyItems(peerId: peerId)] as? StoryItemsView else {
return
}
let peerState = peerStateView.value?.get(Stories.PeerState.self)
let items = itemsView.items.compactMap { $0.value.get(Stories.StoredItem.self) }
var item: (value: Stories.StoredItem, position: Int, previousId: Int32?, nextId: Int32?)?
var focusItemId = self.focusItemId
if focusItemId == nil {
if let peerState = peerState {
focusItemId = peerState.maxReadId + 1
}
}
if let focusItemId = focusItemId {
if let index = items.firstIndex(where: { $0.id >= focusItemId }) {
var previousId: Int32?
var nextId: Int32?
if index != 0 {
previousId = items[index - 1].id
}
if index != items.count - 1 {
nextId = items[index + 1].id
}
item = (items[index], index, previousId, nextId)
} else {
if !items.isEmpty {
var nextId: Int32?
if 0 != items.count - 1 {
nextId = items[0 + 1].id
}
item = (items[0], 0, nil, nextId)
}
}
} else {
if !items.isEmpty {
var nextId: Int32?
if 0 != items.count - 1 {
nextId = items[0 + 1].id
}
item = (items[0], 0, nil, nextId)
}
}
if let (item, position, previousId, nextId) = item {
switch item {
case let .item(item):
self.state.set(.single(State(item: item, previousId: previousId, nextId: nextId, isLoading: false, count: items.count, position: position)))
case .placeholder:
let count = items.count
if !self.isLoadingPlaceholder {
self.isLoadingPlaceholder = true
self.loadDisposable.set((_internal_getStoriesById(
accountPeerId: self.account.peerId,
postbox: self.account.postbox,
network: self.account.network,
peerId: self.peerId,
ids: [item.id]
)
|> deliverOn(self.queue)).start(next: { [weak self] result in
guard let self else {
return
}
if let loadedItem = result.first, case let .item(item) = loadedItem {
self.state.set(.single(State(item: item, previousId: previousId, nextId: nextId, isLoading: false, count: count, position: position)))
} else {
self.placeholderLoadWasUnsuccessful = false
self.state.set(.single(State(item: nil, previousId: nil, nextId: nil, isLoading: !self.placeholderLoadWasUnsuccessful, count: count, position: position)))
}
}))
}
self.state.set(.single(State(item: nil, previousId: nil, nextId: nil, isLoading: !self.placeholderLoadWasUnsuccessful, count: count, position: position)))
}
} else {
self.state.set(.single(State(item: nil, previousId: nil, nextId: nil, isLoading: false, count: 0, position: 0)))
}
})
}
deinit {
self.disposable?.dispose()
self.loadDisposable.dispose()
}
}
public final class State: Equatable {
public let item: Stories.Item?
public let previousId: Int32?
public let nextId: Int32?
public let isLoading: Bool
public let count: Int
public let position: Int
public init(
item: Stories.Item?,
previousId: Int32?,
nextId: Int32?,
isLoading: Bool,
count: Int,
position: Int
) {
self.item = item
self.previousId = previousId
self.nextId = nextId
self.isLoading = isLoading
self.count = count
self.position = position
}
public static func ==(lhs: State, rhs: State) -> Bool {
if lhs.item != rhs.item {
return false
}
if lhs.previousId != rhs.previousId {
return false
}
if lhs.nextId != rhs.nextId {
return false
}
if lhs.isLoading != rhs.isLoading {
return false
}
if lhs.count != rhs.count {
return false
}
if lhs.position != rhs.position {
return false
}
return true
}
}
private static let sharedQueue = Queue(name: "PeerStoryFocusContext")
private let queue: Queue
private let impl: QueueLocalObject<Impl>
public var state: Signal<State, NoError> {
return self.impl.signalWith { impl, subscriber in
return impl.state.get().start(next: subscriber.putNext)
}
}
init(
account: Account,
peerId: PeerId,
focusItemId: Int32?
) {
let queue = PeerStoryFocusContext.sharedQueue
self.queue = queue
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, account: account, peerId: peerId, focusItemId: focusItemId)
})
}
}
public final class StoryListContext {
public enum Scope {
case all

View File

@ -7,6 +7,27 @@ public enum EngineOutgoingMessageContent {
case text(String)
}
public final class StoryPreloadInfo {
public enum Priority: Comparable {
case top(position: Int)
case next(position: Int)
}
public let resource: MediaResourceReference
public let size: Int32?
public let priority: Priority
public init(
resource: MediaResourceReference,
size: Int32?,
priority: Priority
) {
self.resource = resource
self.size = size
self.priority = priority
}
}
public extension TelegramEngine {
final class Messages {
private let account: Account
@ -569,8 +590,215 @@ public extension TelegramEngine {
}).start()
}
public func allStories() -> StoryListContext {
return StoryListContext(account: self.account, scope: .all)
public func storySubscriptions() -> Signal<EngineStorySubscriptions, NoError> {
let basicPeerKey = PostboxViewKey.basicPeer(self.account.peerId)
return self.account.postbox.combinedView(keys: [
basicPeerKey,
PostboxViewKey.storySubscriptions,
PostboxViewKey.storiesState(key: .subscriptions)
])
|> mapToSignal { views -> Signal<EngineStorySubscriptions, NoError> in
guard let basicPeerView = views.views[basicPeerKey] as? BasicPeerView, let accountPeer = basicPeerView.peer else {
return .single(EngineStorySubscriptions(items: [], hasMoreToken: nil))
}
guard let storySubscriptionsView = views.views[PostboxViewKey.storySubscriptions] as? StorySubscriptionsView else {
return .single(EngineStorySubscriptions(items: [], hasMoreToken: nil))
}
guard let storiesStateView = views.views[PostboxViewKey.storiesState(key: .subscriptions)] as? StoryStatesView else {
return .single(EngineStorySubscriptions(items: [], hasMoreToken: nil))
}
var additionalDataKeys: [PostboxViewKey] = []
additionalDataKeys.append(contentsOf: storySubscriptionsView.peerIds.map { peerId -> PostboxViewKey in
return PostboxViewKey.storyItems(peerId: peerId)
})
additionalDataKeys.append(contentsOf: storySubscriptionsView.peerIds.map { peerId -> PostboxViewKey in
return PostboxViewKey.storiesState(key: .peer(peerId))
})
additionalDataKeys.append(contentsOf: storySubscriptionsView.peerIds.map { peerId -> PostboxViewKey in
return PostboxViewKey.basicPeer(peerId)
})
return self.account.postbox.combinedView(keys: additionalDataKeys)
|> map { views -> EngineStorySubscriptions in
let _ = accountPeer
let _ = storySubscriptionsView
let _ = storiesStateView
var hasMoreToken: String?
if let subscriptionsState = storiesStateView.value?.get(Stories.SubscriptionsState.self) {
if subscriptionsState.hasMore {
hasMoreToken = subscriptionsState.opaqueState
} else {
hasMoreToken = nil
}
} else {
hasMoreToken = ""
}
var items: [EngineStorySubscriptions.Item] = []
for peerId in storySubscriptionsView.peerIds {
guard let peerView = views.views[PostboxViewKey.basicPeer(peerId)] as? BasicPeerView else {
continue
}
guard let peer = peerView.peer else {
continue
}
guard let itemsView = views.views[PostboxViewKey.storyItems(peerId: peerId)] as? StoryItemsView else {
continue
}
guard let stateView = views.views[PostboxViewKey.storiesState(key: .peer(peerId))] as? StoryStatesView else {
continue
}
guard let lastEntry = itemsView.items.last?.value.get(Stories.StoredItem.self) else {
continue
}
let peerState: Stories.PeerState? = stateView.value?.get(Stories.PeerState.self)
var hasUnseen = false
if let peerState = peerState {
hasUnseen = peerState.maxReadId < lastEntry.id
}
items.append(EngineStorySubscriptions.Item(
peer: EnginePeer(peer),
hasUnseen: hasUnseen,
storyCount: itemsView.items.count,
lastTimestamp: lastEntry.timestamp
))
}
items.sort(by: { lhs, rhs in
return lhs.lastTimestamp > rhs.lastTimestamp
})
return EngineStorySubscriptions(items: items, hasMoreToken: hasMoreToken)
}
}
}
public func preloadStorySubscriptions() -> Signal<[EngineMediaResource.Id: StoryPreloadInfo], NoError> {
let basicPeerKey = PostboxViewKey.basicPeer(self.account.peerId)
return self.account.postbox.combinedView(keys: [
basicPeerKey,
PostboxViewKey.storySubscriptions,
PostboxViewKey.storiesState(key: .subscriptions)
])
|> mapToSignal { views -> Signal<[EngineMediaResource.Id: StoryPreloadInfo], NoError> in
guard let basicPeerView = views.views[basicPeerKey] as? BasicPeerView, let accountPeer = basicPeerView.peer else {
return .single([:])
}
guard let storySubscriptionsView = views.views[PostboxViewKey.storySubscriptions] as? StorySubscriptionsView else {
return .single([:])
}
guard let storiesStateView = views.views[PostboxViewKey.storiesState(key: .subscriptions)] as? StoryStatesView else {
return .single([:])
}
var additionalDataKeys: [PostboxViewKey] = []
additionalDataKeys.append(contentsOf: storySubscriptionsView.peerIds.map { peerId -> PostboxViewKey in
return PostboxViewKey.storyItems(peerId: peerId)
})
additionalDataKeys.append(contentsOf: storySubscriptionsView.peerIds.map { peerId -> PostboxViewKey in
return PostboxViewKey.storiesState(key: .peer(peerId))
})
additionalDataKeys.append(contentsOf: storySubscriptionsView.peerIds.map { peerId -> PostboxViewKey in
return PostboxViewKey.basicPeer(peerId)
})
return self.account.postbox.combinedView(keys: additionalDataKeys)
|> map { views -> [EngineMediaResource.Id: StoryPreloadInfo] in
let _ = accountPeer
let _ = storySubscriptionsView
let _ = storiesStateView
var nextPriority: Int = 0
var resultResources: [EngineMediaResource.Id: StoryPreloadInfo] = [:]
for peerId in storySubscriptionsView.peerIds {
guard let peerView = views.views[PostboxViewKey.basicPeer(peerId)] as? BasicPeerView else {
continue
}
guard let peer = peerView.peer else {
continue
}
guard let itemsView = views.views[PostboxViewKey.storyItems(peerId: peerId)] as? StoryItemsView else {
continue
}
guard let stateView = views.views[PostboxViewKey.storiesState(key: .peer(peerId))] as? StoryStatesView else {
continue
}
var nextItem: Stories.StoredItem? = itemsView.items.first?.value.get(Stories.StoredItem.self)
let peerState: Stories.PeerState? = stateView.value?.get(Stories.PeerState.self)
if let peerState = peerState {
if let item = itemsView.items.first(where: { $0.id >= peerState.maxReadId }) {
nextItem = item.value.get(Stories.StoredItem.self)
}
}
if let peerReference = PeerReference(peer) {
if let nextItem = nextItem, case let .item(item) = nextItem, let media = item.media {
if let image = media as? TelegramMediaImage, let resource = image.representations.last?.resource {
let resource = MediaResourceReference.media(media: .story(peer: peerReference, id: item.id, media: media), resource: resource)
resultResources[EngineMediaResource.Id(resource.resource.id)] = StoryPreloadInfo(
resource: resource,
size: nil,
priority: .top(position: nextPriority)
)
nextPriority += 1
} else if let file = media as? TelegramMediaFile {
if let preview = file.previewRepresentations.last {
let resource = MediaResourceReference.media(media: .story(peer: peerReference, id: item.id, media: file), resource: preview.resource)
resultResources[EngineMediaResource.Id(resource.resource.id)] = StoryPreloadInfo(
resource: resource,
size: nil,
priority: .top(position: nextPriority)
)
nextPriority += 1
}
let resource = MediaResourceReference.media(media: .story(peer: peerReference, id: item.id, media: file), resource: file.resource)
resultResources[EngineMediaResource.Id(resource.resource.id)] = StoryPreloadInfo(
resource: resource,
size: nil,
priority: .top(position: nextPriority)
)
nextPriority += 1
}
}
}
}
return resultResources
}
}
}
public func peerStoryFocusContext(id: EnginePeer.Id, focusItemId: Int32?) -> PeerStoryFocusContext {
return PeerStoryFocusContext(account: self.account, peerId: id, focusItemId: focusItemId)
}
public func refreshStories(peerId: EnginePeer.Id, ids: [Int32]) -> Signal<Never, NoError> {
return _internal_getStoriesById(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, peerId: peerId, ids: ids)
|> mapToSignal { result -> Signal<Never, NoError> in
return self.account.postbox.transaction { transaction -> Void in
var currentItems = transaction.getStoryItems(peerId: peerId)
for i in 0 ..< currentItems.count {
if let updatedItem = result.first(where: { $0.id == currentItems[i].id }) {
if case .item = updatedItem {
if let entry = CodableEntry(updatedItem) {
currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id)
}
}
}
}
transaction.setStoryItems(peerId: peerId, items: currentItems)
}
|> ignoreValues
}
}
public func peerStories(id: EnginePeer.Id) -> StoryListContext {

View File

@ -285,7 +285,7 @@ public func mediaContentKind(_ media: EngineMedia, message: EngineMessage? = nil
return .file(performer)
}
}
case let .Video(_, _, flags):
case let .Video(_, _, flags, _):
if file.isAnimated {
result = .animation
} else {

View File

@ -235,7 +235,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
} else {
for attribute in file.attributes {
switch attribute {
case let .Video(_, _, flags):
case let .Video(_, _, flags, _):
if flags.contains(.instantRoundVideo) {
type = .round
} else {

View File

@ -2946,7 +2946,7 @@ public func paneGifSearchForQuery(context: AccountContext, query: String, offset
))
}
}
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: uniqueId ?? 0), partialReference: nil, resource: resource, previewRepresentations: previews, videoThumbnails: videoThumbnails, immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])])
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: uniqueId ?? 0), partialReference: nil, resource: resource, previewRepresentations: previews, videoThumbnails: videoThumbnails, immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [], preloadSize: nil)])
references.append(MultiplexedVideoNodeFile(file: FileMediaReference.standalone(media: file), contextResult: (collection, result)))
}
case let .internalReference(internalReference):

View File

@ -745,9 +745,9 @@ public final class ChatListHeaderComponent: Component {
return defaultResult
}
public func updateStories(offset: CGFloat, context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, storyListState: StoryListContext.State?, transition: Transition) {
public func updateStories(offset: CGFloat, context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, storySubscriptions: EngineStorySubscriptions?, transition: Transition) {
var storyOffsetFraction: CGFloat = 1.0
if let storyListState, storyListState.itemSets.count > 1 {
if let storySubscriptions, !storySubscriptions.items.isEmpty {
storyOffsetFraction = offset
}
@ -770,7 +770,7 @@ public final class ChatListHeaderComponent: Component {
context: context,
theme: theme,
strings: strings,
state: storyListState,
storySubscriptions: storySubscriptions,
collapseFraction: 1.0 - offset,
peerAction: { [weak self] peer in
guard let self else {
@ -795,7 +795,7 @@ public final class ChatListHeaderComponent: Component {
storyListTransition.setFrame(view: storyPeerListComponentView, frame: CGRect(origin: CGPoint(x: 0.0, y: storyPeerListPosition), size: CGSize(width: self.bounds.width, height: 94.0)))
var storyListAlpha: CGFloat = 1.0
if let storyListState, storyListState.itemSets.count > 1 {
if let storySubscriptions, !storySubscriptions.items.isEmpty {
} else {
storyListAlpha = offset
}

View File

@ -212,7 +212,7 @@ public func legacyInstantVideoController(theme: PresentationTheme, panelFrame: C
}
}
let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.FileName(fileName: "video.mp4"), .Video(duration: Int(finalDuration), size: PixelDimensions(finalDimensions), flags: [.instantRoundVideo])])
let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.FileName(fileName: "video.mp4"), .Video(duration: Int(finalDuration), size: PixelDimensions(finalDimensions), flags: [.instantRoundVideo], preloadSize: nil)])
var message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])
let scheduleTime: Int32? = scheduleTimestamp > 0 ? scheduleTimestamp : nil

View File

@ -659,8 +659,8 @@ public final class MessageInputPanelComponent: Component {
transition.setPosition(view: reactionButtonView, position: reactionIconFrame.center)
transition.setBounds(view: reactionButtonView, bounds: CGRect(origin: CGPoint(), size: reactionIconFrame.size))
transition.setAlpha(view: reactionButtonView, alpha: (self.textFieldExternalState.hasText || hasMediaRecording || hasMediaEditing) ? 0.0 : 1.0)
transition.setScale(view: reactionButtonView, scale: (self.textFieldExternalState.hasText || hasMediaRecording || hasMediaEditing) ? 0.1 : 1.0)
transition.setAlpha(view: reactionButtonView, alpha: (self.textFieldExternalState.hasText || hasMediaRecording || hasMediaEditing || self.textFieldExternalState.isEditing) ? 0.0 : 1.0)
transition.setScale(view: reactionButtonView, scale: (self.textFieldExternalState.hasText || hasMediaRecording || hasMediaEditing || self.textFieldExternalState.isEditing) ? 0.1 : 1.0)
fieldIconNextX -= reactionButtonSize.width + 2.0
}

View File

@ -83,6 +83,8 @@ public final class StoryContentItemSlice {
public let focusedItemId: AnyHashable?
public let items: [StoryContentItem]
public let totalCount: Int
public let previousItemId: AnyHashable?
public let nextItemId: AnyHashable?
public let update: (StoryContentItemSlice, AnyHashable) -> Signal<StoryContentItemSlice, NoError>
public init(
@ -90,12 +92,16 @@ public final class StoryContentItemSlice {
focusedItemId: AnyHashable?,
items: [StoryContentItem],
totalCount: Int,
previousItemId: AnyHashable?,
nextItemId: AnyHashable?,
update: @escaping (StoryContentItemSlice, AnyHashable) -> Signal<StoryContentItemSlice, NoError>
) {
self.id = id
self.focusedItemId = focusedItemId
self.items = items
self.totalCount = totalCount
self.previousItemId = previousItemId
self.nextItemId = nextItemId
self.update = update
}
}

View File

@ -374,32 +374,63 @@ public final class StoryItemSetContainerComponent: Component {
} else {
nextIndex = currentIndex + 1
}
nextIndex = max(0, min(nextIndex, currentSlice.items.count - 1))
if nextIndex != currentIndex {
let focusedItemId = currentSlice.items[nextIndex].id
self.focusedItemId = focusedItemId
currentSlice.items[nextIndex].markAsSeen?()
self.state?.updated(transition: .immediate)
if nextIndex < 0, let previousId = currentSlice.previousItemId {
self.currentSliceDisposable?.dispose()
self.currentSliceDisposable = (currentSlice.update(
currentSlice,
focusedItemId
previousId
)
|> deliverOnMainQueue).start(next: { [weak self] contentSlice in
guard let self else {
return
}
self.currentSlice = contentSlice
self.focusedItemId = previousId
self.state?.updated(transition: .immediate)
})
} else if nextIndex >= currentSlice.items.count - 1, let nextId = currentSlice.nextItemId {
self.currentSliceDisposable?.dispose()
self.currentSliceDisposable = (currentSlice.update(
currentSlice,
nextId
)
|> deliverOnMainQueue).start(next: { [weak self] contentSlice in
guard let self else {
return
}
self.currentSlice = contentSlice
self.focusedItemId = nextId
self.state?.updated(transition: .immediate)
})
} else {
if point.x < itemLayout.size.width * 0.25 {
self.component?.navigateToItemSet(.previous)
nextIndex = max(0, min(nextIndex, currentSlice.items.count - 1))
if nextIndex != currentIndex {
let focusedItemId = currentSlice.items[nextIndex].id
self.focusedItemId = focusedItemId
currentSlice.items[nextIndex].markAsSeen?()
self.state?.updated(transition: .immediate)
self.currentSliceDisposable?.dispose()
self.currentSliceDisposable = (currentSlice.update(
currentSlice,
focusedItemId
)
|> deliverOnMainQueue).start(next: { [weak self] contentSlice in
guard let self else {
return
}
self.currentSlice = contentSlice
self.state?.updated(transition: .immediate)
})
} else {
self.component?.navigateToItemSet(.next)
if point.x < itemLayout.size.width * 0.25 {
self.component?.navigateToItemSet(.previous)
} else {
self.component?.navigateToItemSet(.next)
}
}
}
}
@ -854,7 +885,7 @@ public final class StoryItemSetContainerComponent: Component {
return true
}
self.displayReactions = true
self.displayReactions = !self.displayReactions
self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut)))
})
},
@ -914,31 +945,6 @@ public final class StoryItemSetContainerComponent: Component {
currentSlice.items[nextIndex].markAsSeen?()
/*var updatedItems: [StoryContentItem] = []
for item in currentSlice.items {
if item.id != focusedItemId {
updatedItems.append(StoryContentItem(
id: item.id,
position: updatedItems.count,
component: item.component,
centerInfoComponent: item.centerInfoComponent,
rightInfoComponent: item.rightInfoComponent,
targetMessageId: item.targetMessageId,
preload: item.preload,
delete: item.delete,
hasLike: item.hasLike,
isMy: item.isMy
))
}
}*/
/*self.currentSlice = StoryContentItemSlice(
id: currentSlice.id,
focusedItemId: nil,
items: updatedItems,
totalCount: currentSlice.totalCount - 1,
update: currentSlice.update
)*/
self.state?.updated(transition: .immediate)
}
@ -1455,7 +1461,7 @@ public final class StoryItemSetContainerComponent: Component {
if self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil {
inlineActionsAlpha = 0.0
}
if self.reactionItems != nil {
if self.displayReactions {
inlineActionsAlpha = 0.0
}
if component.hideUI {

View File

@ -17,6 +17,7 @@ swift_library(
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/AccountContext",
"//submodules/TelegramCore",
"//submodules/Postbox",
"//submodules/PhotoResources",
"//submodules/MediaPlayer:UniversalMediaPlayer",
"//submodules/TelegramUniversalVideoContent",

View File

@ -5,9 +5,518 @@ import ComponentFlow
import SwiftSignalKit
import AccountContext
import TelegramCore
import Postbox
import StoryContainerScreen
public final class StoryContentContextState {
public final class FocusedSlice {
public let peer: EnginePeer
public let item: StoryContentItem
public let totalCount: Int
public let previousItemId: Int32?
public let nextItemId: Int32?
public init(
peer: EnginePeer,
item: StoryContentItem,
totalCount: Int,
previousItemId: Int32?,
nextItemId: Int32?
) {
self.peer = peer
self.item = item
self.totalCount = totalCount
self.previousItemId = previousItemId
self.nextItemId = nextItemId
}
}
public let slice: FocusedSlice?
public let previousSlice: FocusedSlice?
public let nextSlice: FocusedSlice?
public init(
slice: FocusedSlice?,
previousSlice: FocusedSlice?,
nextSlice: FocusedSlice?
) {
self.slice = slice
self.previousSlice = previousSlice
self.nextSlice = nextSlice
}
}
public enum StoryContentContextNavigation {
public enum Direction {
case previous
case next
}
case item(Direction)
case peer(Direction)
}
public protocol StoryContentContext {
var stateValue: StoryContentContextState? { get }
var state: Signal<StoryContentContextState, NoError> { get }
func navigate(navigation: StoryContentContextNavigation)
}
public final class StoryContentContextImpl: StoryContentContext {
private struct StoryKey: Hashable {
var peerId: EnginePeer.Id
var id: Int32
}
private final class PeerContext {
private let context: AccountContext
private let peerId: EnginePeer.Id
private(set) var sliceValue: StoryContentContextState.FocusedSlice?
let updated = Promise<Void>()
var isReady: Bool {
return false
}
private var disposable: Disposable?
private var loadDisposable: Disposable?
init(context: AccountContext, peerId: EnginePeer.Id, focusedId: Int32?, loadIds: @escaping ([StoryKey]) -> Void) {
self.context = context
self.peerId = peerId
self.disposable = (context.account.postbox.combinedView(
keys: [
PostboxViewKey.basicPeer(peerId),
PostboxViewKey.storiesState(key: .peer(peerId)),
PostboxViewKey.storyItems(peerId: peerId)
]
)
|> deliverOnMainQueue).start(next: { [weak self] views in
guard let self else {
return
}
guard let peerView = views.views[PostboxViewKey.basicPeer(peerId)] as? BasicPeerView else {
return
}
guard let stateView = views.views[PostboxViewKey.storiesState(key: .peer(peerId))] as? StoryStatesView else {
return
}
guard let itemsView = views.views[PostboxViewKey.storyItems(peerId: peerId)] as? StoryItemsView else {
return
}
guard let peer = peerView.peer.flatMap(EnginePeer.init) else {
return
}
let state = stateView.value?.get(Stories.PeerState.self)
var focusedIndex: Int?
if let focusedId {
focusedIndex = itemsView.items.firstIndex(where: { $0.id == focusedId })
}
if focusedIndex == nil, let state {
focusedIndex = itemsView.items.firstIndex(where: { $0.id >= state.maxReadId })
}
if focusedIndex == nil {
if !itemsView.items.isEmpty {
focusedIndex = 0
}
}
if let focusedIndex {
var previousItemId: Int32?
var nextItemId: Int32?
if focusedIndex != 0 {
previousItemId = itemsView.items[focusedIndex - 1].id
}
if focusedIndex != itemsView.items.count - 1 {
nextItemId = itemsView.items[focusedIndex + 1].id
}
var loadKeys: [StoryKey] = []
for index in (focusedIndex - 2) ... (focusedIndex + 2) {
if index >= 0 && index < itemsView.items.count {
if let item = itemsView.items[focusedIndex].value.get(Stories.StoredItem.self), case .placeholder = item {
loadKeys.append(StoryKey(peerId: peerId, id: item.id))
}
}
}
if let item = itemsView.items[focusedIndex].value.get(Stories.StoredItem.self), case let .item(item) = item, let media = item.media {
let mappedItem = StoryListContext.Item(
id: item.id,
timestamp: item.timestamp,
media: EngineMedia(media),
text: item.text,
entities: item.entities,
views: nil,
privacy: nil
)
self.sliceValue = StoryContentContextState.FocusedSlice(
peer: peer,
item: StoryContentItem(
id: AnyHashable(item.id),
position: focusedIndex,
component: AnyComponent(StoryItemContentComponent(
context: context,
peer: peer,
item: mappedItem
)),
centerInfoComponent: AnyComponent(StoryAuthorInfoComponent(
context: context,
peer: peer,
timestamp: item.timestamp
)),
rightInfoComponent: AnyComponent(StoryAvatarInfoComponent(
context: context,
peer: peer
)),
peerId: peer.id,
storyItem: mappedItem,
preload: nil,
delete: { [weak context] in
guard let context else {
return
}
let _ = context
},
markAsSeen: { [weak context] in
guard let context else {
return
}
let _ = context.engine.messages.markStoryAsSeen(peerId: peerId, id: item.id).start()
},
hasLike: false,
isMy: peerId == context.account.peerId
),
totalCount: itemsView.items.count,
previousItemId: previousItemId,
nextItemId: nextItemId
)
self.updated.set(.single(Void()))
}
}
})
}
deinit {
self.disposable?.dispose()
self.loadDisposable?.dispose()
}
}
private final class StateContext {
let centralPeerContext: PeerContext
let previousPeerContext: PeerContext?
let nextPeerContext: PeerContext?
let updated = Promise<Void>()
var isReady: Bool {
if !self.centralPeerContext.isReady {
return false
}
if let previousPeerContext = self.previousPeerContext, !previousPeerContext.isReady {
return false
}
if let nextPeerContext = self.nextPeerContext, !nextPeerContext.isReady {
return false
}
return true
}
private var centralDisposable: Disposable?
private var previousDisposable: Disposable?
private var nextDisposable: Disposable?
init(
centralPeerContext: PeerContext,
previousPeerContext: PeerContext?,
nextPeerContext: PeerContext?
) {
self.centralPeerContext = centralPeerContext
self.previousPeerContext = previousPeerContext
self.nextPeerContext = nextPeerContext
self.centralDisposable = (centralPeerContext.updated.get()
|> deliverOnMainQueue).start(next: { [weak self] _ in
guard let self else {
return
}
self.updated.set(.single(Void()))
})
if let previousPeerContext {
self.previousDisposable = (previousPeerContext.updated.get()
|> deliverOnMainQueue).start(next: { [weak self] _ in
guard let self else {
return
}
self.updated.set(.single(Void()))
})
}
if let nextPeerContext {
self.nextDisposable = (nextPeerContext.updated.get()
|> deliverOnMainQueue).start(next: { [weak self] _ in
guard let self else {
return
}
self.updated.set(.single(Void()))
})
}
}
deinit {
self.centralDisposable?.dispose()
self.previousDisposable?.dispose()
self.nextDisposable?.dispose()
}
}
private let context: AccountContext
public private(set) var stateValue: StoryContentContextState?
public var state: Signal<StoryContentContextState, NoError> {
return self.statePromise.get()
}
private let statePromise = Promise<StoryContentContextState>()
private var focusedItem: (peerId: EnginePeer.Id, storyId: Int32?)?
private var currentState: StateContext?
private var currentStateUpdatedDisposable: Disposable?
private var pendingState: StateContext?
private var pendingStateReadyDisposable: Disposable?
private var storySubscriptions: EngineStorySubscriptions?
private var storySubscriptionsDisposable: Disposable?
private var requestedStoryKeys = Set<StoryKey>()
private var requestStoryDisposables = DisposableSet()
public init(
context: AccountContext,
focusedPeerId: EnginePeer.Id?
) {
self.context = context
if let focusedPeerId {
self.focusedItem = (focusedPeerId, nil)
}
self.storySubscriptionsDisposable = (context.engine.messages.storySubscriptions()
|> deliverOnMainQueue).start(next: { [weak self] storySubscriptions in
guard let self else {
return
}
self.storySubscriptions = storySubscriptions
self.updatePeerContexts()
})
}
deinit {
self.storySubscriptionsDisposable?.dispose()
self.requestStoryDisposables.dispose()
}
private func updatePeerContexts() {
if let currentState = self.currentState {
let _ = currentState
} else {
self.switchToFocusedPeerId()
}
}
private func switchToFocusedPeerId() {
if let storySubscriptions = self.storySubscriptions {
if self.pendingState == nil {
var centralIndex: Int?
if let (focusedPeerId, _) = self.focusedItem {
if let index = storySubscriptions.items.firstIndex(where: { $0.peer.id == focusedPeerId }) {
centralIndex = index
}
}
if centralIndex == nil {
if !storySubscriptions.items.isEmpty {
centralIndex = 0
}
}
if let centralIndex {
let loadIds: ([StoryKey]) -> Void = { [weak self] keys in
guard let self else {
return
}
let missingKeys = Set(keys).subtracting(self.requestedStoryKeys)
if !missingKeys.isEmpty {
var idsByPeerId: [EnginePeer.Id: [Int32]] = [:]
for key in missingKeys {
if idsByPeerId[key.peerId] == nil {
idsByPeerId[key.peerId] = [key.id]
} else {
idsByPeerId[key.peerId]?.append(key.id)
}
}
for (peerId, ids) in idsByPeerId {
self.requestStoryDisposables.add(self.context.engine.messages.refreshStories(peerId: peerId, ids: ids).start())
}
}
}
let pendingState = StateContext(
centralPeerContext: PeerContext(context: self.context, peerId: storySubscriptions.items[centralIndex].peer.id, focusedId: nil, loadIds: loadIds),
previousPeerContext: centralIndex == 0 ? nil : PeerContext(context: self.context, peerId: storySubscriptions.items[centralIndex - 1].peer.id, focusedId: nil, loadIds: loadIds),
nextPeerContext: (centralIndex == storySubscriptions.items.count - 1) ? nil : PeerContext(context: self.context, peerId: storySubscriptions.items[centralIndex + 1].peer.id, focusedId: nil, loadIds: loadIds)
)
self.pendingState = pendingState
self.pendingStateReadyDisposable = (pendingState.updated.get()
|> deliverOnMainQueue).start(next: { [weak self, weak pendingState] _ in
guard let self, let pendingState, self.pendingState === pendingState, pendingState.isReady else {
return
}
self.pendingState = nil
self.pendingStateReadyDisposable?.dispose()
self.pendingStateReadyDisposable = nil
self.currentState = pendingState
self.updateState()
self.currentStateUpdatedDisposable?.dispose()
self.currentStateUpdatedDisposable = (pendingState.updated.get()
|> deliverOnMainQueue).start(next: { [weak self, weak pendingState] _ in
guard let self, let pendingState, self.currentState === pendingState else {
return
}
self.updateState()
})
})
}
}
}
}
private func updateState() {
preconditionFailure()
}
public func navigate(navigation: StoryContentContextNavigation) {
}
}
public enum StoryChatContent {
public static func peerStories(context: AccountContext, peerId: EnginePeer.Id, focusItem: Int32?) -> Signal<[StoryContentItemSlice], NoError> {
let focusContext = context.engine.messages.peerStoryFocusContext(id: peerId, focusItemId: focusItem)
return combineLatest(
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)),
focusContext.state
)
|> mapToSignal { peer, state -> Signal<[StoryContentItemSlice], NoError> in
let _ = focusContext
if let peer, let item = state.item, let media = item.media {
let mappedItem = StoryListContext.Item(
id: item.id,
timestamp: item.timestamp,
media: EngineMedia(media),
text: item.text,
entities: item.entities,
views: nil,
privacy: nil
)
let slice = StoryContentItemSlice(
id: AnyHashable(peerId),
focusedItemId: AnyHashable(item.id),
items: [
StoryContentItem(
id: AnyHashable(item.id),
position: state.position,
component: AnyComponent(StoryItemContentComponent(
context: context,
peer: peer,
item: mappedItem
)),
centerInfoComponent: AnyComponent(StoryAuthorInfoComponent(
context: context,
peer: peer,
timestamp: item.timestamp
)),
rightInfoComponent: AnyComponent(StoryAvatarInfoComponent(
context: context,
peer: peer
)),
peerId: peer.id,
storyItem: mappedItem,
preload: nil,
delete: { [weak context] in
guard let context else {
return
}
let _ = context
},
markAsSeen: { [weak context] in
guard let context else {
return
}
let _ = context.engine.messages.markStoryAsSeen(peerId: peerId, id: item.id).start()
},
hasLike: false,
isMy: peerId == context.account.peerId
)
],
totalCount: state.count,
previousItemId: state.previousId,
nextItemId: state.nextId,
update: { requestedItemSet, itemId in
var focusItem: Int32?
if let id = itemId.base as? Int32 {
focusItem = id
}
return StoryChatContent.peerStories(context: context, peerId: peerId, focusItem: focusItem)
|> mapToSignal { result in
if let first = result.first {
return .single(first)
} else {
return .never()
}
}
}
)
return .single([slice])
} else {
return .single([])
}
}
}
public static func subscriptionsStories(context: AccountContext, peerId: EnginePeer.Id?) -> Signal<[StoryContentItemSlice], NoError> {
return context.engine.messages.storySubscriptions()
|> mapToSignal { subscriptions -> Signal<[StoryContentItemSlice], NoError> in
var signals: [Signal<[StoryContentItemSlice], NoError>] = []
for item in subscriptions.items {
signals.append(peerStories(context: context, peerId: item.peer.id, focusItem: nil))
}
return combineLatest(queue: .mainQueue(), signals)
|> map { peerItems -> [StoryContentItemSlice] in
var result: [StoryContentItemSlice] = []
for item in peerItems {
result.append(contentsOf: item)
}
return result
}
}
}
public static func stories(context: AccountContext, storyList: StoryListContext, focusItem: Int32?) -> Signal<[StoryContentItemSlice], NoError> {
return storyList.state
|> map { state -> [StoryContentItemSlice] in
@ -72,6 +581,8 @@ public enum StoryChatContent {
focusedItemId: sliceFocusedItemId,
items: items,
totalCount: items.count,
previousItemId: nil,
nextItemId: nil,
update: { requestedItemSet, itemId in
var focusItem: Int32?
if let id = itemId.base as? Int32 {

View File

@ -13,7 +13,7 @@ public final class StoryPeerListComponent: Component {
public let context: AccountContext
public let theme: PresentationTheme
public let strings: PresentationStrings
public let state: StoryListContext.State?
public let storySubscriptions: EngineStorySubscriptions?
public let collapseFraction: CGFloat
public let peerAction: (EnginePeer?) -> Void
@ -21,14 +21,14 @@ public final class StoryPeerListComponent: Component {
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
state: StoryListContext.State?,
storySubscriptions: EngineStorySubscriptions?,
collapseFraction: CGFloat,
peerAction: @escaping (EnginePeer?) -> Void
) {
self.context = context
self.theme = theme
self.strings = strings
self.state = state
self.storySubscriptions = storySubscriptions
self.collapseFraction = collapseFraction
self.peerAction = peerAction
}
@ -43,7 +43,7 @@ public final class StoryPeerListComponent: Component {
if lhs.strings !== rhs.strings {
return false
}
if lhs.state != rhs.state {
if lhs.storySubscriptions != rhs.storySubscriptions {
return false
}
if lhs.collapseFraction != rhs.collapseFraction {
@ -102,7 +102,7 @@ public final class StoryPeerListComponent: Component {
private var ignoreScrolling: Bool = false
private var itemLayout: ItemLayout?
private var sortedItemSets: [StoryListContext.PeerItemSet] = []
private var sortedItems: [EngineStorySubscriptions.Item] = []
private var visibleItems: [EnginePeer.Id: VisibleItem] = [:]
private let title = ComponentView<Empty>()
@ -110,6 +110,9 @@ public final class StoryPeerListComponent: Component {
private var component: StoryPeerListComponent?
private weak var state: EmptyComponentState?
private var requestedLoadMoreToken: String?
private let loadMoreDisposable = MetaDisposable()
public override init(frame: CGRect) {
self.collapsedButton = HighlightableButton()
@ -153,6 +156,10 @@ public final class StoryPeerListComponent: Component {
preconditionFailure()
}
deinit {
self.loadMoreDisposable.dispose()
}
@objc private func collapsedButtonPressed() {
guard let component = self.component else {
return
@ -182,14 +189,14 @@ public final class StoryPeerListComponent: Component {
}
var hasStories: Bool = false
if let state = component.state, state.itemSets.count > 1 {
if let storySubscriptions = component.storySubscriptions, !storySubscriptions.items.isEmpty {
hasStories = true
}
let titleSpacing: CGFloat = 8.0
let titleText: String
let storyCount = self.sortedItemSets.count - 1
let storyCount = self.sortedItems.count
if storyCount <= 0 {
titleText = "No Stories"
} else {
@ -211,7 +218,7 @@ public final class StoryPeerListComponent: Component {
let collapsedItemWidth: CGFloat = 24.0
let collapsedItemDistance: CGFloat = 14.0
let collapsedItemCount: CGFloat = CGFloat(min(self.sortedItemSets.count - collapseStartIndex, 3))
let collapsedItemCount: CGFloat = CGFloat(min(self.sortedItems.count - collapseStartIndex, 3))
var collapsedContentWidth: CGFloat = 0.0
if collapsedItemCount > 0 {
collapsedContentWidth = 1.0 * collapsedItemWidth + (collapsedItemDistance) * max(0.0, collapsedItemCount - 1.0)
@ -257,11 +264,9 @@ public final class StoryPeerListComponent: Component {
let visibleBounds = self.scrollView.bounds
var validIds: [EnginePeer.Id] = []
for i in 0 ..< self.sortedItemSets.count {
let itemSet = self.sortedItemSets[i]
guard let peer = itemSet.peer else {
continue
}
for i in 0 ..< self.sortedItems.count {
let itemSet = self.sortedItems[i]
let peer = itemSet.peer
let regularItemFrame = itemLayout.frame(at: i)
if !visibleBounds.intersects(regularItemFrame) {
@ -271,32 +276,29 @@ public final class StoryPeerListComponent: Component {
//}
}
validIds.append(itemSet.peerId)
validIds.append(itemSet.peer.id)
let visibleItem: VisibleItem
var itemTransition = transition
if let current = self.visibleItems[itemSet.peerId] {
if let current = self.visibleItems[itemSet.peer.id] {
visibleItem = current
} else {
itemTransition = .immediate
visibleItem = VisibleItem()
self.visibleItems[itemSet.peerId] = visibleItem
self.visibleItems[itemSet.peer.id] = visibleItem
}
var hasUnseen = false
let hasItems = !itemSet.items.isEmpty
hasUnseen = itemSet.hasUnseen
let hasItems = true
var itemProgress: CGFloat?
if peer.id == component.context.account.peerId {
itemProgress = component.state?.uploadProgress
itemProgress = nil
//itemProgress = component.state?.uploadProgress
//itemProgress = 0.0
}
for item in itemSet.items {
if item.id > itemSet.maxReadId {
hasUnseen = true
}
}
let collapsedItemFrame = CGRect(origin: CGPoint(x: collapsedContentOrigin + CGFloat(i - collapseStartIndex) * collapsedItemDistance, y: regularItemFrame.minY + collapsedItemOffsetY), size: CGSize(width: collapsedItemWidth, height: regularItemFrame.height))
let itemFrame = regularItemFrame.interpolate(to: collapsedItemFrame, amount: component.collapseFraction)
@ -406,24 +408,27 @@ public final class StoryPeerListComponent: Component {
self.component = component
self.state = state
if let storySubscriptions = component.storySubscriptions, let hasMoreToken = storySubscriptions.hasMoreToken {
if self.requestedLoadMoreToken != hasMoreToken {
self.requestedLoadMoreToken = hasMoreToken
if let storySubscriptionsContext = component.context.account.storySubscriptionsContext {
storySubscriptionsContext.loadMore()
}
}
}
self.collapsedButton.isUserInteractionEnabled = component.collapseFraction >= 1.0 - .ulpOfOne
self.sortedItemSets.removeAll(keepingCapacity: true)
if let state = component.state {
if let myIndex = state.itemSets.firstIndex(where: { $0.peerId == component.context.account.peerId }) {
self.sortedItemSets.append(state.itemSets[myIndex])
self.sortedItems.removeAll(keepingCapacity: true)
if let storySubscriptions = component.storySubscriptions {
if let myIndex = storySubscriptions.items.firstIndex(where: { $0.peer.id == component.context.account.peerId }) {
self.sortedItems.append(storySubscriptions.items[myIndex])
}
for i in 0 ..< 1 {
for itemSet in state.itemSets {
if itemSet.peerId == component.context.account.peerId {
continue
}
if i == 0 {
self.sortedItemSets.append(itemSet)
} else {
self.sortedItemSets.append(StoryListContext.PeerItemSet(peerId: EnginePeer.Id(namespace: itemSet.peerId.namespace, id: EnginePeer.Id.Id._internalFromInt64Value(itemSet.peerId.id._internalGetInt64Value() + Int64(i))), peer: itemSet.peer, maxReadId: itemSet.maxReadId, items: itemSet.items, totalCount: itemSet.totalCount))
}
for itemSet in storySubscriptions.items {
if itemSet.peer.id == component.context.account.peerId {
continue
}
self.sortedItems.append(itemSet)
}
}
@ -432,7 +437,7 @@ public final class StoryPeerListComponent: Component {
containerInsets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 10.0),
itemSize: CGSize(width: 60.0, height: 77.0),
itemSpacing: 24.0,
itemCount: self.sortedItemSets.count
itemCount: self.sortedItems.count
)
self.itemLayout = itemLayout

View File

@ -172,7 +172,7 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont
imageDimensions = externalReference.content?.dimensions?.cgSize
if let content = externalReference.content, externalReference.type == "gif", let thumbnailResource = imageResource
, let dimensions = content.dimensions {
videoFileReference = .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])]))
videoFileReference = .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [], preloadSize: nil)]))
imageResource = nil
}
case let .internalReference(internalReference):

View File

@ -338,7 +338,7 @@ func messageMediaEditingOptions(message: Message) -> MessageMediaEditingOptions
return []
case .Animated:
return []
case let .Video(_, _, flags):
case let .Video(_, _, flags, _):
if flags.contains(.instantRoundVideo) {
return []
} else {

View File

@ -258,7 +258,7 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.mediaBackgroundNode.image = backgroundImage
if let image = image, let video = image.videoRepresentations.last, let id = image.id?.id {
let videoFileReference = FileMediaReference.message(message: MessageReference(item.message), media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: image.representations, videoThumbnails: [], immediateThumbnailData: image.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [])]))
let videoFileReference = FileMediaReference.message(message: MessageReference(item.message), media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: image.representations, videoThumbnails: [], immediateThumbnailData: image.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [], preloadSize: nil)]))
let videoContent = NativeVideoContent(id: .profileVideo(id, "action"), userLocation: .peer(item.message.id.peerId), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, storeAfterDownload: nil)
if videoContent.id != strongSelf.videoContent?.id {
let mediaManager = item.context.sharedContext.mediaManager

View File

@ -585,7 +585,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
let messageTheme = arguments.incoming ? arguments.presentationData.theme.theme.chat.message.incoming : arguments.presentationData.theme.theme.chat.message.outgoing
let isInstantVideo = arguments.file.isInstantVideo
for attribute in arguments.file.attributes {
if case let .Video(videoDuration, _, flags) = attribute, flags.contains(.instantRoundVideo) {
if case let .Video(videoDuration, _, flags, _) = attribute, flags.contains(.instantRoundVideo) {
isAudio = true
isVoice = true
@ -1439,7 +1439,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
var isVoice = false
var audioDuration: Int32?
for attribute in file.attributes {
if case let .Video(duration, _, flags) = attribute, flags.contains(.instantRoundVideo) {
if case let .Video(duration, _, flags, _) = attribute, flags.contains(.instantRoundVideo) {
isAudio = true
isVoice = true
audioDuration = Int32(duration)

View File

@ -86,7 +86,7 @@ private func mediaMergeableStyle(_ media: Media) -> ChatMessageMerge {
switch attribute {
case .Sticker:
return .semanticallyMerged
case let .Video(_, _, flags):
case let .Video(_, _, flags, _):
if flags.contains(.instantRoundVideo) {
return .none
}
@ -462,7 +462,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible {
viewClassName = ChatMessageStickerItemNode.self
}
break loop
case let .Video(_, _, flags):
case let .Video(_, _, flags, _):
if flags.contains(.instantRoundVideo) {
// viewClassName = ChatMessageInstantVideoItemNode.self
viewClassName = ChatMessageBubbleItemNode.self

View File

@ -299,7 +299,7 @@ final class ChatMessageAccessibilityData {
text = item.presentationData.strings.VoiceOver_Chat_MusicTitle(title, performer).string
text.append(item.presentationData.strings.VoiceOver_Chat_Duration(durationString).string)
}
case let .Video(duration, _, flags):
case let .Video(duration, _, flags, _):
isSpecialFile = true
if isSelected == nil {
hint = item.presentationData.strings.VoiceOver_Chat_PlayHint

View File

@ -215,7 +215,7 @@ class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleContentNode
}
if let photo = photo, let video = photo.videoRepresentations.last, let id = photo.id?.id {
let videoFileReference = FileMediaReference.message(message: MessageReference(item.message), media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [])]))
let videoFileReference = FileMediaReference.message(message: MessageReference(item.message), media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [], preloadSize: nil)]))
let videoContent = NativeVideoContent(id: .profileVideo(id, "action"), userLocation: .peer(item.message.id.peerId), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, storeAfterDownload: nil)
if videoContent.id != strongSelf.videoContent?.id {
let mediaManager = item.context.sharedContext.mediaManager

View File

@ -260,7 +260,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
}
imageDimensions = externalReference.content?.dimensions?.cgSize
if externalReference.type == "gif", let thumbnailResource = externalReference.thumbnail?.resource, let content = externalReference.content, let dimensions = content.dimensions {
videoFile = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: thumbnailResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])])
videoFile = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: thumbnailResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [], preloadSize: nil)])
imageResource = nil
}

View File

@ -552,7 +552,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
markupNode.update(markup: markup, size: CGSize(width: 320.0, height: 320.0))
markupNode.updateVisibility(true)
} else if threadInfo == nil, let video = videoRepresentations.last, let peerReference = PeerReference(peer) {
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [])]))
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil)]))
let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: peer.isCopyProtectionEnabled, storeAfterDownload: nil)
if videoContent.id != self.videoContent?.id {
self.videoNode?.removeFromSupernode()
@ -949,7 +949,7 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode {
markupNode.removeFromSupernode()
}
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [])]))
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil)]))
let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: peer.isCopyProtectionEnabled, storeAfterDownload: nil)
if videoContent.id != self.videoContent?.id {
self.videoNode?.removeFromSupernode()

View File

@ -72,7 +72,7 @@ final class MessageMediaPlaylistItem: SharedMediaPlaylistItem {
} else {
return SharedMediaPlaybackData(type: .music, source: source)
}
case let .Video(_, _, flags):
case let .Video(_, _, flags, _):
if flags.contains(.instantRoundVideo) {
return SharedMediaPlaybackData(type: .instantVideo, source: source)
} else {
@ -129,7 +129,7 @@ final class MessageMediaPlaylistItem: SharedMediaPlaylistItem {
displayData = SharedMediaPlaybackDisplayData.music(title: updatedTitle, performer: updatedPerformer, albumArt: albumArt, long: CGFloat(duration) > 10.0 * 60.0, caption: caption)
}
return displayData
case let .Video(_, _, flags):
case let .Video(_, _, flags, _):
if flags.contains(.instantRoundVideo) {
return SharedMediaPlaybackDisplayData.instantVideo(author: self.message.effectiveAuthor.flatMap(EnginePeer.init), peer: self.message.peers[self.message.id.peerId].flatMap(EnginePeer.init), timestamp: self.message.timestamp)
} else {

View File

@ -363,11 +363,12 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
return
}
if let chatListController = self.chatListController as? ChatListControllerImpl, let storyListContext = chatListController.storyListContext {
if let chatListController = self.chatListController as? ChatListControllerImpl {
switch mediaResult {
case let .image(image, dimensions, caption):
if let imageData = compressImageToJPEG(image, quality: 0.6) {
storyListContext.upload(media: .image(dimensions: dimensions, data: imageData), text: caption?.string ?? "", entities: [], privacy: privacy)
//storyListContext.upload(media: .image(dimensions: dimensions, data: imageData), text: caption?.string ?? "", entities: [], privacy: privacy)
let _ = self.context.engine.messages.uploadStory(media: .image(dimensions: dimensions, data: imageData), text: caption?.string ?? "", entities: [], privacy: privacy).start()
Queue.mainQueue().after(0.2, { [weak chatListController] in
chatListController?.animateStoryUploadRipple()
})
@ -388,7 +389,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
case let .asset(localIdentifier):
resource = VideoLibraryMediaResource(localIdentifier: localIdentifier, conversion: .compress(adjustments))
}
storyListContext.upload(media: .video(dimensions: dimensions, duration: Int(duration), resource: resource), text: caption?.string ?? "", entities: [], privacy: privacy)
let _ = self.context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: Int(duration), resource: resource), text: caption?.string ?? "", entities: [], privacy: privacy).start()
Queue.mainQueue().after(0.2, { [weak chatListController] in
chatListController?.animateStoryUploadRipple()
})

View File

@ -172,7 +172,7 @@ func makeBridgeMedia(message: Message, strings: PresentationStrings, chatPeer: P
for attribute in file.attributes {
switch attribute {
case let .Video(duration, size, flags):
case let .Video(duration, size, flags, _):
bridgeVideo.duration = Int32(clamping: duration)
bridgeVideo.dimensions = size.cgSize
bridgeVideo.round = flags.contains(.instantRoundVideo)

View File

@ -37,7 +37,7 @@ struct WebSearchGalleryEntry: Equatable {
switch self.result {
case let .externalReference(externalReference):
if let content = externalReference.content, externalReference.type == "gif", let thumbnailResource = externalReference.thumbnail?.resource, let dimensions = content.dimensions {
let fileReference = FileMediaReference.standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])]))
let fileReference = FileMediaReference.standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [], preloadSize: nil)]))
return WebSearchVideoGalleryItem(context: context, presentationData: presentationData, index: self.index, result: self.result, content: NativeVideoContent(id: .contextResult(self.result.queryId, self.result.id), userLocation: .other, fileReference: fileReference, loopVideo: true, enableSound: false, fetchAutomatically: true, storeAfterDownload: nil), controllerInteraction: controllerInteraction)
}
case let .internalReference(internalReference):

View File

@ -22,7 +22,7 @@ public extension WidgetDataPeer.Message {
switch attribute {
case let .Sticker(altText, _, _):
content = .sticker(WidgetDataPeer.Message.Content.Sticker(altText: altText))
case let .Video(duration, _, flags):
case let .Video(duration, _, flags, _):
if flags.contains(.instantRoundVideo) {
content = .videoMessage(WidgetDataPeer.Message.Content.VideoMessage(duration: Int32(duration)))
} else {