From 969724de40977e7b6b40e4822a2f1975799199e0 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 23 May 2023 23:53:43 +0400 Subject: [PATCH] [WIP] Stories --- .../Sources/IsMediaStreamable.swift | 4 +- .../Sources/AvatarVideoNode.swift | 2 +- .../Sources/ChatImportActivityScreen.swift | 2 +- .../Sources/ChatListController.swift | 187 +++++- .../Sources/Node/ChatListItemStrings.swift | 2 +- .../ChatItemGalleryFooterContentNode.swift | 2 +- .../Items/UniversalVideoGalleryItem.swift | 2 +- .../Sources/InstantPageMediaPlaylist.swift | 4 +- .../Sources/LegacyMediaPickers.swift | 6 +- .../Sources/PeerAvatarImageGalleryItem.swift | 4 +- .../Sources/PeerInfoAvatarListNode.swift | 2 +- submodules/Postbox/Sources/Postbox.swift | 102 +++- .../Postbox/Sources/PostboxTransaction.swift | 17 +- .../Postbox/Sources/StoryItemsTable.swift | 101 ++++ .../Postbox/Sources/StoryItemsView.swift | 53 ++ .../Postbox/Sources/StoryStatesTable.swift | 70 +++ .../Postbox/Sources/StoryStatesView.swift | 84 +++ .../Sources/StorySubscriptionsTable.swift | 67 +++ .../Sources/StorySubscriptionsView.swift | 50 ++ submodules/Postbox/Sources/Views.swift | 33 ++ .../Sources/ShareLoadingContainerNode.swift | 2 +- .../ShareItems/Sources/ShareItems.swift | 4 +- .../Sources/Account/Account.swift | 9 + .../ApiUtils/StoreMessage_Telegram.swift | 2 +- .../Sources/ApiUtils/TelegramMediaFile.swift | 8 +- .../PendingMessageUploadedContent.swift | 9 +- .../ManagedSecretChatOutgoingOperations.swift | 8 +- ...ecretChatIncomingDecryptedOperations.swift | 22 +- .../SyncCore/SyncCore_TelegramMediaFile.swift | 22 +- ...OutgoingMessageWithChatContextResult.swift | 2 +- .../TelegramEngine/Messages/Stories.swift | 544 +++++++++++++++++- .../Messages/StoryListContext.swift | 433 ++++++++++++++ .../Messages/TelegramEngineMessages.swift | 232 +++++++- .../Sources/MessageContentKind.swift | 2 +- .../Sources/ServiceMessageStrings.swift | 2 +- .../Sources/ChatEntityKeyboardInputNode.swift | 2 +- .../Sources/ChatListHeaderComponent.swift | 8 +- .../LegacyInstantVideoController.swift | 2 +- .../Sources/MessageInputPanelComponent.swift | 4 +- .../Sources/StoryContent.swift | 6 + .../StoryItemSetContainerComponent.swift | 86 +-- .../Stories/StoryContentComponent/BUILD | 1 + .../Sources/StoryChatContent.swift | 511 ++++++++++++++++ .../Sources/StoryPeerListComponent.swift | 83 +-- .../ChatContextResultPeekContentNode.swift | 2 +- .../ChatInterfaceStateContextMenus.swift | 2 +- .../Sources/ChatMessageActionItemNode.swift | 2 +- .../ChatMessageInteractiveFileNode.swift | 4 +- .../TelegramUI/Sources/ChatMessageItem.swift | 4 +- .../Sources/ChatMessageItemView.swift | 2 +- ...ageProfilePhotoSuggestionContentNode.swift | 2 +- ...ListContextResultsChatInputPanelItem.swift | 2 +- .../Sources/PeerInfo/PeerInfoHeaderNode.swift | 4 +- .../Sources/PeerMessagesMediaPlaylist.swift | 4 +- .../Sources/TelegramRootController.swift | 7 +- .../WatchBridge/Sources/WatchBridge.swift | 2 +- .../Sources/WebSearchGalleryController.swift | 2 +- .../Sources/WidgetItemsUtils.swift | 2 +- 58 files changed, 2653 insertions(+), 186 deletions(-) create mode 100644 submodules/Postbox/Sources/StoryItemsTable.swift create mode 100644 submodules/Postbox/Sources/StoryItemsView.swift create mode 100644 submodules/Postbox/Sources/StoryStatesTable.swift create mode 100644 submodules/Postbox/Sources/StoryStatesView.swift create mode 100644 submodules/Postbox/Sources/StorySubscriptionsTable.swift create mode 100644 submodules/Postbox/Sources/StorySubscriptionsView.swift diff --git a/submodules/AccountContext/Sources/IsMediaStreamable.swift b/submodules/AccountContext/Sources/IsMediaStreamable.swift index 0c93c9ceb8..a562f0a32c 100644 --- a/submodules/AccountContext/Sources/IsMediaStreamable.swift +++ b/submodules/AccountContext/Sources/IsMediaStreamable.swift @@ -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 } diff --git a/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift b/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift index d0570afad1..a4b1f77650 100644 --- a/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift +++ b/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift @@ -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() diff --git a/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift b/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift index eaa0290715..9749970787 100644 --- a/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift +++ b/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift @@ -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) diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 1d11364e8d..298854e417 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -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, 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() - 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)) } } } diff --git a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift index 47e1f40e48..d1305205f3 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift @@ -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 diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index 892d0d3bb2..cd3fb84aa2 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -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 { diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 39da60db97..b6a7ea0300 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -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 } diff --git a/submodules/InstantPageUI/Sources/InstantPageMediaPlaylist.swift b/submodules/InstantPageUI/Sources/InstantPageMediaPlaylist.swift index 7a29e48b2d..3f7da6b890 100644 --- a/submodules/InstantPageUI/Sources/InstantPageMediaPlaylist.swift +++ b/submodules/InstantPageUI/Sources/InstantPageMediaPlaylist.swift @@ -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 { diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift index ebb832dd1f..0d69a83ee4 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift @@ -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) diff --git a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift index 85e5c5f39d..6a74a5da18 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift @@ -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 diff --git a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift index ad7b767433..d6d11bf2e9 100644 --- a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift +++ b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift @@ -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 { diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index 1f765a4334..cab35ba6d7 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -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] = [:] private var currentUpdatedHiddenPeerIds: Bool = false + private var currentStoryStatesEvents: [StoryStatesTable.Event] = [] + private var currentStorySubscriptionsEvents: [StorySubscriptionsTable.Event] = [] + private var currentStoryItemsEvents: [StoryItemsTable.Event] = [] + var hiddenChatIds: Set { 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 @@ -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() diff --git a/submodules/Postbox/Sources/PostboxTransaction.swift b/submodules/Postbox/Sources/PostboxTransaction.swift index 118532e4dd..9e224c884c 100644 --- a/submodules/Postbox/Sources/PostboxTransaction.swift +++ b/submodules/Postbox/Sources/PostboxTransaction.swift @@ -49,6 +49,9 @@ final class PostboxTransaction { let updatedPeerThreadsSummaries: Set let updatedPinnedThreads: Set 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, 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, currentGlobalTagsOperations: [GlobalMessageHistoryTagsOperation], currentLocalTagsOperations: [IntermediateMessageHistoryLocalTagsOperation], updatedMedia: [MediaId: Media?], replaceRemoteContactCount: Int32?, replaceContactPeerIds: Set?, currentPendingMessageActionsOperations: [PendingMessageActionsOperation], currentUpdatedMessageActionsSummaries: [PendingMessageActionsSummaryKey: Int32], currentUpdatedMessageTagSummaries: [MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary], currentInvalidateMessageTagSummaries: [InvalidatedMessageHistoryTagsSummaryEntryOperation], currentUpdatedPendingPeerNotificationSettings: Set, replacedAdditionalChatListItems: [AdditionalChatListItem]?, updatedNoticeEntryKeys: Set, updatedCacheEntryKeys: Set, currentUpdatedMasterClientId: Int64?, updatedFailedMessagePeerIds: Set, updatedFailedMessageIds: Set, updatedGlobalNotificationSettings: Bool, updatedPeerTimeoutAttributes: Bool, updatedMessageThreadPeerIds: Set, updatedPeerThreadCombinedStates: Set, updatedPeerThreadsSummaries: Set, updatedPinnedThreads: Set, 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, 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, currentGlobalTagsOperations: [GlobalMessageHistoryTagsOperation], currentLocalTagsOperations: [IntermediateMessageHistoryLocalTagsOperation], updatedMedia: [MediaId: Media?], replaceRemoteContactCount: Int32?, replaceContactPeerIds: Set?, currentPendingMessageActionsOperations: [PendingMessageActionsOperation], currentUpdatedMessageActionsSummaries: [PendingMessageActionsSummaryKey: Int32], currentUpdatedMessageTagSummaries: [MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary], currentInvalidateMessageTagSummaries: [InvalidatedMessageHistoryTagsSummaryEntryOperation], currentUpdatedPendingPeerNotificationSettings: Set, replacedAdditionalChatListItems: [AdditionalChatListItem]?, updatedNoticeEntryKeys: Set, updatedCacheEntryKeys: Set, currentUpdatedMasterClientId: Int64?, updatedFailedMessagePeerIds: Set, updatedFailedMessageIds: Set, updatedGlobalNotificationSettings: Bool, updatedPeerTimeoutAttributes: Bool, updatedMessageThreadPeerIds: Set, updatedPeerThreadCombinedStates: Set, updatedPeerThreadsSummaries: Set, updatedPinnedThreads: Set, 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 } } diff --git a/submodules/Postbox/Sources/StoryItemsTable.swift b/submodules/Postbox/Sources/StoryItemsTable.swift new file mode 100644 index 0000000000..05d1bfabcd --- /dev/null +++ b/submodules/Postbox/Sources/StoryItemsTable.swift @@ -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() { + } +} diff --git a/submodules/Postbox/Sources/StoryItemsView.swift b/submodules/Postbox/Sources/StoryItemsView.swift new file mode 100644 index 0000000000..e121ed3327 --- /dev/null +++ b/submodules/Postbox/Sources/StoryItemsView.swift @@ -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 + } +} diff --git a/submodules/Postbox/Sources/StoryStatesTable.swift b/submodules/Postbox/Sources/StoryStatesTable.swift new file mode 100644 index 0000000000..b34f2ddfab --- /dev/null +++ b/submodules/Postbox/Sources/StoryStatesTable.swift @@ -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() { + } +} diff --git a/submodules/Postbox/Sources/StoryStatesView.swift b/submodules/Postbox/Sources/StoryStatesView.swift new file mode 100644 index 0000000000..6969ba8d37 --- /dev/null +++ b/submodules/Postbox/Sources/StoryStatesView.swift @@ -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 + } +} diff --git a/submodules/Postbox/Sources/StorySubscriptionsTable.swift b/submodules/Postbox/Sources/StorySubscriptionsTable.swift new file mode 100644 index 0000000000..89a98762ab --- /dev/null +++ b/submodules/Postbox/Sources/StorySubscriptionsTable.swift @@ -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() { + } +} diff --git a/submodules/Postbox/Sources/StorySubscriptionsView.swift b/submodules/Postbox/Sources/StorySubscriptionsView.swift new file mode 100644 index 0000000000..34395d6e55 --- /dev/null +++ b/submodules/Postbox/Sources/StorySubscriptionsView.swift @@ -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 + } +} diff --git a/submodules/Postbox/Sources/Views.swift b/submodules/Postbox/Sources/Views.swift index eab363d562..aa604f7fa8 100644 --- a/submodules/Postbox/Sources/Views.swift +++ b/submodules/Postbox/Sources/Views.swift @@ -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) } } diff --git a/submodules/ShareController/Sources/ShareLoadingContainerNode.swift b/submodules/ShareController/Sources/ShareLoadingContainerNode.swift index 900046237b..dd015ecea0 100644 --- a/submodules/ShareController/Sources/ShareLoadingContainerNode.swift +++ b/submodules/ShareController/Sources/ShareLoadingContainerNode.swift @@ -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) diff --git a/submodules/ShareItems/Sources/ShareItems.swift b/submodules/ShareItems/Sources/ShareItems.swift index 4f36bdb7ed..e7c2ab9031 100644 --- a/submodules/ShareItems/Sources/ShareItems.swift +++ b/submodules/ShareItems/Sources/ShareItems.swift @@ -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")] diff --git a/submodules/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift index ef4ca53c0f..a108f7ffe7 100644 --- a/submodules/TelegramCore/Sources/Account/Account.swift +++ b/submodules/TelegramCore/Sources/Account/Account.swift @@ -973,6 +973,8 @@ public class Account { let networkStatsContext: NetworkStatsContext + public let storySubscriptionsContext: StorySubscriptionsContext? + public init(accountManager: AccountManager, 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) }) diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 4d452b2534..861eb1963b 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -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 { diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift index 04a4a91bdd..576a6fc5da 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift @@ -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() diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift index 433a97f15a..8b8e33eae6 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift @@ -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 { diff --git a/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift index 1d8a54e359..6cffff8581 100644 --- a/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift @@ -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 diff --git a/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift b/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift index bc3781c3a7..0ff2743d71 100644 --- a/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift +++ b/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift @@ -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)) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift index 76b96e9e1c..5475c9cdbc 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift @@ -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 { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift index c3174d50ab..9dcde2905b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift @@ -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)) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index ea5a948956..b2918994db 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -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 { 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 Signal { 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 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 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 { guard let inputUser = peer.inputUser else { return .single(nil) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index 4593eb9c47..55578cae03 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -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 + + 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() + 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 + + public var state: Signal { + 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 diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 8073859ad6..ee4de749e3 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -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 { + let basicPeerKey = PostboxViewKey.basicPeer(self.account.peerId) + return self.account.postbox.combinedView(keys: [ + basicPeerKey, + PostboxViewKey.storySubscriptions, + PostboxViewKey.storiesState(key: .subscriptions) + ]) + |> mapToSignal { views -> Signal 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 { + return _internal_getStoriesById(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, peerId: peerId, ids: ids) + |> mapToSignal { result -> Signal 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 { diff --git a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift index e306922d14..741f201b36 100644 --- a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift +++ b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift @@ -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 { diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 1c7581b5ea..f514957521 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index aefd014290..152f2497b3 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -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): diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift index b97ee5c3e4..76d8769390 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift @@ -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 } diff --git a/submodules/TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift b/submodules/TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift index 5b33cf7cbf..366662da63 100644 --- a/submodules/TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift +++ b/submodules/TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift @@ -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 diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 5ff550726c..c9f3f2b098 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -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 } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift index ebc5c4c493..74d3ac1581 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift @@ -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 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 ) { self.id = id self.focusedItemId = focusedItemId self.items = items self.totalCount = totalCount + self.previousItemId = previousItemId + self.nextItemId = nextItemId self.update = update } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 05fadcedb5..5cffd403f9 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/Stories/StoryContentComponent/BUILD b/submodules/TelegramUI/Components/Stories/StoryContentComponent/BUILD index f31c881253..de30c2ebad 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContentComponent/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContentComponent/BUILD @@ -17,6 +17,7 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit", "//submodules/AccountContext", "//submodules/TelegramCore", + "//submodules/Postbox", "//submodules/PhotoResources", "//submodules/MediaPlayer:UniversalMediaPlayer", "//submodules/TelegramUniversalVideoContent", diff --git a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift index 6b2932292e..99850e7bcb 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift @@ -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 { 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() + + 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() + + 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 { + return self.statePromise.get() + } + private let statePromise = Promise() + + 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() + 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 { diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift index fc8e982513..e43700239c 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift @@ -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() @@ -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 diff --git a/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift b/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift index 1bbde22823..c1dd40fcea 100644 --- a/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift @@ -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): diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 5235878a23..d4a294eff3 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -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 { diff --git a/submodules/TelegramUI/Sources/ChatMessageActionItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageActionItemNode.swift index 31d1d1060f..dcd36a252d 100644 --- a/submodules/TelegramUI/Sources/ChatMessageActionItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageActionItemNode.swift @@ -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 diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift index 5887353ef3..623b851752 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift @@ -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) diff --git a/submodules/TelegramUI/Sources/ChatMessageItem.swift b/submodules/TelegramUI/Sources/ChatMessageItem.swift index fd012872ba..269f23b620 100644 --- a/submodules/TelegramUI/Sources/ChatMessageItem.swift +++ b/submodules/TelegramUI/Sources/ChatMessageItem.swift @@ -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 diff --git a/submodules/TelegramUI/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Sources/ChatMessageItemView.swift index 5555d4e440..aefcf7c7c2 100644 --- a/submodules/TelegramUI/Sources/ChatMessageItemView.swift +++ b/submodules/TelegramUI/Sources/ChatMessageItemView.swift @@ -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 diff --git a/submodules/TelegramUI/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift index cec115a682..fcd0fd106c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift @@ -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 diff --git a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift index 0346088900..7c245e3530 100644 --- a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift +++ b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift @@ -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 } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index 8d88b5b792..80c6aabc17 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -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() diff --git a/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift b/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift index 5166e2061e..44d712c54c 100644 --- a/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift +++ b/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift @@ -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 { diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 46737ce2ab..0ee08d73e5 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -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() }) diff --git a/submodules/WatchBridge/Sources/WatchBridge.swift b/submodules/WatchBridge/Sources/WatchBridge.swift index 953ab45513..f5162f7b77 100644 --- a/submodules/WatchBridge/Sources/WatchBridge.swift +++ b/submodules/WatchBridge/Sources/WatchBridge.swift @@ -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) diff --git a/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift b/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift index 3574de778f..097eb64e41 100644 --- a/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift +++ b/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift @@ -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): diff --git a/submodules/WidgetItemsUtils/Sources/WidgetItemsUtils.swift b/submodules/WidgetItemsUtils/Sources/WidgetItemsUtils.swift index 64d45a51e4..a17977a4d4 100644 --- a/submodules/WidgetItemsUtils/Sources/WidgetItemsUtils.swift +++ b/submodules/WidgetItemsUtils/Sources/WidgetItemsUtils.swift @@ -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 {