mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
[WIP] Stories
This commit is contained in:
parent
d086a8f674
commit
969724de40
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
||||
|
@ -245,8 +245,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
private var powerSavingMonitoringDisposable: Disposable?
|
||||
|
||||
public private(set) var storyListContext: StoryListContext?
|
||||
private var storyListState: StoryListContext.State?
|
||||
private var storyListStateDisposable: Disposable?
|
||||
private var storySubscriptions: EngineStorySubscriptions?
|
||||
|
||||
private var storySubscriptionsDisposable: Disposable?
|
||||
private var preloadStorySubscriptionsDisposable: Disposable?
|
||||
private var preloadStoryResourceDisposables: [MediaResourceId: Disposable] = [:]
|
||||
|
||||
private var storyListHeight: CGFloat
|
||||
|
||||
@ -802,7 +805,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
self.joinForumDisposable.dispose()
|
||||
self.actionDisposables.dispose()
|
||||
self.powerSavingMonitoringDisposable?.dispose()
|
||||
self.storyListStateDisposable?.dispose()
|
||||
self.storySubscriptionsDisposable?.dispose()
|
||||
self.preloadStorySubscriptionsDisposable?.dispose()
|
||||
}
|
||||
|
||||
private func updateNavigationMetadata() {
|
||||
@ -2123,30 +2127,59 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
})
|
||||
|
||||
let storyListContext = self.context.engine.messages.allStories()
|
||||
self.storyListContext = storyListContext
|
||||
self.storyListStateDisposable = (storyListContext.state
|
||||
|> deliverOnMainQueue).start(next: { [weak self] state in
|
||||
self.preloadStorySubscriptionsDisposable = (self.context.engine.messages.preloadStorySubscriptions()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] resources in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
var validIds: [MediaResourceId] = []
|
||||
for (_, info) in resources.sorted(by: { $0.value.priority < $1.value.priority }) {
|
||||
let resource = info.resource
|
||||
validIds.append(resource.resource.id)
|
||||
if preloadStoryResourceDisposables[resource.resource.id] == nil {
|
||||
var fetchRange: (Range<Int64>, MediaBoxFetchPriority)?
|
||||
if let size = info.size {
|
||||
fetchRange = (0 ..< Int64(size), .default)
|
||||
}
|
||||
preloadStoryResourceDisposables[resource.resource.id] = fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: resource, range: fetchRange).start()
|
||||
}
|
||||
}
|
||||
|
||||
var removeIds: [MediaResourceId] = []
|
||||
for (id, disposable) in self.preloadStoryResourceDisposables {
|
||||
if !validIds.contains(id) {
|
||||
removeIds.append(id)
|
||||
disposable.dispose()
|
||||
}
|
||||
}
|
||||
for id in removeIds {
|
||||
self.preloadStoryResourceDisposables.removeValue(forKey: id)
|
||||
}
|
||||
})
|
||||
self.storySubscriptionsDisposable = (self.context.engine.messages.storySubscriptions()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] storySubscriptions in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
var wasEmpty = true
|
||||
if let storyListState = self.storyListState, !storyListState.itemSets.isEmpty {
|
||||
if let storySubscriptions = self.storySubscriptions, !storySubscriptions.items.isEmpty {
|
||||
wasEmpty = false
|
||||
}
|
||||
self.storyListState = state
|
||||
let isEmpty = state.itemSets.isEmpty
|
||||
self.storySubscriptions = storySubscriptions
|
||||
let isEmpty = storySubscriptions.items.isEmpty
|
||||
|
||||
self.chatListDisplayNode.mainContainerNode.currentItemNode.updateState { chatListState in
|
||||
var chatListState = chatListState
|
||||
|
||||
var peersWithNewStories = Set<EnginePeer.Id>()
|
||||
for itemSet in state.itemSets {
|
||||
if itemSet.peerId == self.context.account.peerId {
|
||||
for item in storySubscriptions.items {
|
||||
if item.peer.id == self.context.account.peerId {
|
||||
continue
|
||||
}
|
||||
if itemSet.items.contains(where: { $0.id > itemSet.maxReadId }) {
|
||||
peersWithNewStories.insert(itemSet.peerId)
|
||||
if item.hasUnseen {
|
||||
peersWithNewStories.insert(item.peer.id)
|
||||
}
|
||||
}
|
||||
chatListState.peersWithNewStories = peersWithNewStories
|
||||
@ -2156,14 +2189,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
|
||||
self.storyListHeight = isEmpty ? 0.0 : 94.0
|
||||
|
||||
let tabsIsEmpty: Bool
|
||||
if let (resolvedItems, displayTabsAtBottom, _) = self.tabContainerData {
|
||||
tabsIsEmpty = resolvedItems.count <= 1 || displayTabsAtBottom
|
||||
} else {
|
||||
tabsIsEmpty = true
|
||||
}
|
||||
|
||||
self.navigationBar?.secondaryContentHeight = (!tabsIsEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0)
|
||||
if case .chatList(.root) = self.location {
|
||||
self.searchContentNode?.additionalHeight = 94.0
|
||||
}
|
||||
@ -2413,11 +2438,121 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
if let searchContentNode = self.searchContentNode, case .chatList(.root) = self.location {
|
||||
if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
|
||||
componentView.storyPeerAction = { [weak self] peer in
|
||||
guard let self, let storyListContext = self.storyListContext else {
|
||||
guard let self, let peer else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = (StoryChatContent.stories(
|
||||
let storyFocusContext = self.context.engine.messages.peerStoryFocusContext(id: peer.id, focusItemId: nil)
|
||||
|
||||
let _ = (storyFocusContext.state
|
||||
|> filter { state -> Bool in
|
||||
if state.isLoading {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] state in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = storyFocusContext
|
||||
let _ = self
|
||||
|
||||
if let item = state.item {
|
||||
let _ = item
|
||||
|
||||
let _ = (StoryChatContent.subscriptionsStories(
|
||||
context: self.context,
|
||||
peerId: peer.id
|
||||
)
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] initialContent in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
var transitionIn: StoryContainerScreen.TransitionIn?
|
||||
if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
|
||||
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peer.id) {
|
||||
transitionIn = StoryContainerScreen.TransitionIn(
|
||||
sourceView: transitionView,
|
||||
sourceRect: transitionView.bounds,
|
||||
sourceCornerRadius: transitionView.bounds.height * 0.5
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var cameraTransitionIn: StoryCameraTransitionIn?
|
||||
if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
|
||||
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
|
||||
cameraTransitionIn = StoryCameraTransitionIn(
|
||||
sourceView: transitionView,
|
||||
sourceRect: transitionView.bounds,
|
||||
sourceCornerRadius: transitionView.bounds.height * 0.5
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var initialFocusedId: AnyHashable?
|
||||
//if let peer {
|
||||
initialFocusedId = AnyHashable(peer.id)
|
||||
//}
|
||||
|
||||
if initialFocusedId == AnyHashable(self.context.account.peerId), let firstItem = initialContent.first, firstItem.id == initialFocusedId && firstItem.items.isEmpty {
|
||||
if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
|
||||
rootController.openStoryCamera(transitionIn: cameraTransitionIn, transitionOut: { [weak self] _ in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
|
||||
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
|
||||
return StoryCameraTransitionOut(
|
||||
destinationView: transitionView,
|
||||
destinationRect: transitionView.bounds,
|
||||
destinationCornerRadius: transitionView.bounds.height * 0.5
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let storyContainerScreen = StoryContainerScreen(
|
||||
context: self.context,
|
||||
initialFocusedId: initialFocusedId,
|
||||
initialContent: initialContent,
|
||||
transitionIn: transitionIn,
|
||||
transitionOut: { [weak self] peerId, _ in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
|
||||
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) {
|
||||
return StoryContainerScreen.TransitionOut(
|
||||
destinationView: transitionView,
|
||||
destinationRect: transitionView.bounds,
|
||||
destinationCornerRadius: transitionView.bounds.height * 0.5,
|
||||
destinationIsAvatar: true,
|
||||
completed: {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
)
|
||||
self.push(storyContainerScreen)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
/*let _ = (StoryChatContent.stories(
|
||||
context: self.context,
|
||||
storyList: storyListContext,
|
||||
focusItem: nil
|
||||
@ -2503,7 +2638,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
)
|
||||
self.push(storyContainerScreen)
|
||||
})
|
||||
})*/
|
||||
}
|
||||
|
||||
let fraction = 94.0 / (navigationBarSearchContentHeight + 94.0)
|
||||
@ -2512,7 +2647,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
visibleProgress = (1.0 / fraction) * visibleProgress
|
||||
visibleProgress = max(0.0, min(1.0, visibleProgress))
|
||||
|
||||
componentView.updateStories(offset: visibleProgress, context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, storyListState: self.storyListState, transition: Transition(transition))
|
||||
componentView.updateStories(offset: visibleProgress, context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, storySubscriptions: self.storySubscriptions, transition: Transition(transition))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -1255,6 +1255,50 @@ public final class Transaction {
|
||||
assert(!self.disposed)
|
||||
return self.postbox!.removeChatHidden(peerId: peerId, index: index)
|
||||
}
|
||||
|
||||
public func getAllStorySubscriptions() -> (state: CodableEntry?, peerIds: [PeerId]) {
|
||||
assert(!self.disposed)
|
||||
return self.postbox!.getAllStorySubscriptions()
|
||||
}
|
||||
|
||||
public func replaceAllStorySubscriptions(state: CodableEntry?, peerIds: [PeerId]) {
|
||||
assert(!self.disposed)
|
||||
self.postbox!.replaceAllStorySubscriptions(state: state, peerIds: peerIds)
|
||||
}
|
||||
|
||||
public func setSubscriptionsStoriesState(state: CodableEntry?) {
|
||||
assert(!self.disposed)
|
||||
self.postbox!.setSubscriptionsStoriesState(state: state)
|
||||
}
|
||||
|
||||
public func getLocalStoryState() -> CodableEntry? {
|
||||
assert(!self.disposed)
|
||||
return self.postbox!.getLocalStoryState()
|
||||
}
|
||||
|
||||
public func setLocalStoryState(state: CodableEntry?) {
|
||||
assert(!self.disposed)
|
||||
self.postbox!.setLocalStoryState(state: state)
|
||||
}
|
||||
|
||||
public func getPeerStoryState(peerId: PeerId) -> CodableEntry? {
|
||||
assert(!self.disposed)
|
||||
return self.postbox!.getPeerStoryState(peerId: peerId)
|
||||
}
|
||||
|
||||
public func setPeerStoryState(peerId: PeerId, state: CodableEntry?) {
|
||||
assert(!self.disposed)
|
||||
self.postbox!.setPeerStoryState(peerId: peerId, state: state)
|
||||
}
|
||||
|
||||
public func setStoryItems(peerId: PeerId, items: [StoryItemsTableEntry]) {
|
||||
assert(!self.disposed)
|
||||
self.postbox!.setStoryItems(peerId: peerId, items: items)
|
||||
}
|
||||
|
||||
public func getStoryItems(peerId: PeerId) -> [StoryItemsTableEntry] {
|
||||
return self.postbox!.getStoryItems(peerId: peerId)
|
||||
}
|
||||
}
|
||||
|
||||
public enum PostboxResult {
|
||||
@ -1492,6 +1536,10 @@ final class PostboxImpl {
|
||||
private var currentHiddenChatIds: [PeerId: Bag<Void>] = [:]
|
||||
private var currentUpdatedHiddenPeerIds: Bool = false
|
||||
|
||||
private var currentStoryStatesEvents: [StoryStatesTable.Event] = []
|
||||
private var currentStorySubscriptionsEvents: [StorySubscriptionsTable.Event] = []
|
||||
private var currentStoryItemsEvents: [StoryItemsTable.Event] = []
|
||||
|
||||
var hiddenChatIds: Set<PeerId> {
|
||||
if self.currentHiddenChatIds.isEmpty {
|
||||
return Set()
|
||||
@ -1610,6 +1658,9 @@ final class PostboxImpl {
|
||||
let messageHistoryHoleIndexTable: MessageHistoryHoleIndexTable
|
||||
let groupMessageStatsTable: GroupMessageStatsTable
|
||||
let peerTimeoutPropertiesTable: PeerTimeoutPropertiesTable
|
||||
let storyStatesTable: StoryStatesTable
|
||||
let storySubscriptionsTable: StorySubscriptionsTable
|
||||
let storyItemsTable: StoryItemsTable
|
||||
|
||||
//temporary
|
||||
let peerRatingTable: RatingTable<PeerId>
|
||||
@ -1699,6 +1750,9 @@ final class PostboxImpl {
|
||||
self.noticeTable = NoticeTable(valueBox: self.valueBox, table: NoticeTable.tableSpec(43), useCaches: useCaches)
|
||||
self.deviceContactImportInfoTable = DeviceContactImportInfoTable(valueBox: self.valueBox, table: DeviceContactImportInfoTable.tableSpec(54), useCaches: useCaches)
|
||||
self.groupMessageStatsTable = GroupMessageStatsTable(valueBox: self.valueBox, table: GroupMessageStatsTable.tableSpec(58), useCaches: useCaches)
|
||||
self.storyStatesTable = StoryStatesTable(valueBox: self.valueBox, table: StoryStatesTable.tableSpec(65), useCaches: useCaches)
|
||||
self.storySubscriptionsTable = StorySubscriptionsTable(valueBox: self.valueBox, table: StorySubscriptionsTable.tableSpec(66), useCaches: useCaches)
|
||||
self.storyItemsTable = StoryItemsTable(valueBox: self.valueBox, table: StoryItemsTable.tableSpec(69), useCaches: useCaches)
|
||||
|
||||
var tables: [Table] = []
|
||||
tables.append(self.metadataTable)
|
||||
@ -1766,6 +1820,9 @@ final class PostboxImpl {
|
||||
tables.append(self.messageHistoryHoleIndexTable)
|
||||
tables.append(self.groupMessageStatsTable)
|
||||
tables.append(self.peerTimeoutPropertiesTable)
|
||||
tables.append(self.storyStatesTable)
|
||||
tables.append(self.storySubscriptionsTable)
|
||||
tables.append(self.storyItemsTable)
|
||||
|
||||
self.tables = tables
|
||||
|
||||
@ -2104,6 +2161,46 @@ final class PostboxImpl {
|
||||
self.synchronizeGroupMessageStatsTable.set(groupId: groupId, namespace: namespace, needsValidation: false, operations: &self.currentUpdatedGroupSummarySynchronizeOperations)
|
||||
}
|
||||
|
||||
fileprivate func getAllStorySubscriptions() -> (state: CodableEntry?, peerIds: [PeerId]) {
|
||||
return (
|
||||
self.storyStatesTable.get(key: .subscriptions),
|
||||
self.storySubscriptionsTable.getAll()
|
||||
)
|
||||
}
|
||||
|
||||
fileprivate func replaceAllStorySubscriptions(state: CodableEntry?, peerIds: [PeerId]) {
|
||||
self.storyStatesTable.set(key: .subscriptions, value: state, events: &self.currentStoryStatesEvents)
|
||||
self.storySubscriptionsTable.replaceAll(peerIds: peerIds, events: &self.currentStorySubscriptionsEvents)
|
||||
}
|
||||
|
||||
fileprivate func getLocalStoryState() -> CodableEntry? {
|
||||
return self.storyStatesTable.get(key: .local)
|
||||
}
|
||||
|
||||
fileprivate func setSubscriptionsStoriesState(state: CodableEntry?) {
|
||||
self.storyStatesTable.set(key: .subscriptions, value: state, events: &self.currentStoryStatesEvents)
|
||||
}
|
||||
|
||||
fileprivate func setLocalStoryState(state: CodableEntry?) {
|
||||
self.storyStatesTable.set(key: .local, value: state, events: &self.currentStoryStatesEvents)
|
||||
}
|
||||
|
||||
fileprivate func getPeerStoryState(peerId: PeerId) -> CodableEntry? {
|
||||
return self.storyStatesTable.get(key: .peer(peerId))
|
||||
}
|
||||
|
||||
fileprivate func setPeerStoryState(peerId: PeerId, state: CodableEntry?) {
|
||||
self.storyStatesTable.set(key: .peer(peerId), value: state, events: &self.currentStoryStatesEvents)
|
||||
}
|
||||
|
||||
fileprivate func setStoryItems(peerId: PeerId, items: [StoryItemsTableEntry]) {
|
||||
self.storyItemsTable.replace(peerId: peerId, entries: items, events: &self.currentStoryItemsEvents)
|
||||
}
|
||||
|
||||
fileprivate func getStoryItems(peerId: PeerId) -> [StoryItemsTableEntry] {
|
||||
return self.storyItemsTable.get(peerId: peerId)
|
||||
}
|
||||
|
||||
func renderIntermediateMessage(_ message: IntermediateMessage) -> Message {
|
||||
let renderedMessage = self.messageHistoryTable.renderMessage(message, peerTable: self.peerTable, threadIndexTable: self.messageHistoryThreadIndexTable)
|
||||
|
||||
@ -2170,7 +2267,7 @@ final class PostboxImpl {
|
||||
|
||||
let updatedPeerTimeoutAttributes = self.peerTimeoutPropertiesTable.hasUpdates
|
||||
|
||||
let transaction = PostboxTransaction(currentUpdatedState: self.currentUpdatedState, currentPeerHoleOperations: self.currentPeerHoleOperations, currentOperationsByPeerId: self.currentOperationsByPeerId, chatListOperations: self.currentChatListOperations, currentUpdatedChatListInclusions: self.currentUpdatedChatListInclusions, currentUpdatedPeers: self.currentUpdatedPeers, currentUpdatedPeerNotificationSettings: self.currentUpdatedPeerNotificationSettings, currentUpdatedPeerNotificationBehaviorTimestamps: self.currentUpdatedPeerNotificationBehaviorTimestamps, currentUpdatedCachedPeerData: self.currentUpdatedCachedPeerData, currentUpdatedPeerPresences: currentUpdatedPeerPresences, currentUpdatedPeerChatListEmbeddedStates: self.currentUpdatedPeerChatListEmbeddedStates, currentUpdatedTotalUnreadStates: self.currentUpdatedTotalUnreadStates, currentUpdatedTotalUnreadSummaries: self.currentUpdatedGroupTotalUnreadSummaries, alteredInitialPeerCombinedReadStates: alteredInitialPeerCombinedReadStates, currentPeerMergedOperationLogOperations: self.currentPeerMergedOperationLogOperations, currentTimestampBasedMessageAttributesOperations: self.currentTimestampBasedMessageAttributesOperations, unsentMessageOperations: self.currentUnsentOperations, updatedSynchronizePeerReadStateOperations: self.currentUpdatedSynchronizeReadStateOperations, currentUpdatedGroupSummarySynchronizeOperations: self.currentUpdatedGroupSummarySynchronizeOperations, currentPreferencesOperations: self.currentPreferencesOperations, currentOrderedItemListOperations: self.currentOrderedItemListOperations, currentItemCollectionItemsOperations: self.currentItemCollectionItemsOperations, currentItemCollectionInfosOperations: self.currentItemCollectionInfosOperations, currentUpdatedPeerChatStates: self.currentUpdatedPeerChatStates, currentGlobalTagsOperations: self.currentGlobalTagsOperations, currentLocalTagsOperations: self.currentLocalTagsOperations, updatedMedia: self.currentUpdatedMedia, replaceRemoteContactCount: self.currentReplaceRemoteContactCount, replaceContactPeerIds: self.currentReplacedContactPeerIds, currentPendingMessageActionsOperations: self.currentPendingMessageActionsOperations, currentUpdatedMessageActionsSummaries: self.currentUpdatedMessageActionsSummaries, currentUpdatedMessageTagSummaries: self.currentUpdatedMessageTagSummaries, currentInvalidateMessageTagSummaries: self.currentInvalidateMessageTagSummaries, currentUpdatedPendingPeerNotificationSettings: self.currentUpdatedPendingPeerNotificationSettings, replacedAdditionalChatListItems: self.currentReplacedAdditionalChatListItems, updatedNoticeEntryKeys: self.currentUpdatedNoticeEntryKeys, updatedCacheEntryKeys: self.currentUpdatedCacheEntryKeys, currentUpdatedMasterClientId: currentUpdatedMasterClientId, updatedFailedMessagePeerIds: self.messageHistoryFailedTable.updatedPeerIds, updatedFailedMessageIds: self.messageHistoryFailedTable.updatedMessageIds, updatedGlobalNotificationSettings: self.currentNeedsReindexUnreadCounters, updatedPeerTimeoutAttributes: updatedPeerTimeoutAttributes, updatedMessageThreadPeerIds: updatedMessageThreadPeerIds, updatedPeerThreadCombinedStates: self.currentUpdatedPeerThreadCombinedStates, updatedPeerThreadsSummaries: Set(alteredInitialPeerThreadsSummaries.keys), updatedPinnedThreads: self.currentUpdatedPinnedThreads, updatedHiddenPeerIds: self.currentUpdatedHiddenPeerIds)
|
||||
let transaction = PostboxTransaction(currentUpdatedState: self.currentUpdatedState, currentPeerHoleOperations: self.currentPeerHoleOperations, currentOperationsByPeerId: self.currentOperationsByPeerId, chatListOperations: self.currentChatListOperations, currentUpdatedChatListInclusions: self.currentUpdatedChatListInclusions, currentUpdatedPeers: self.currentUpdatedPeers, currentUpdatedPeerNotificationSettings: self.currentUpdatedPeerNotificationSettings, currentUpdatedPeerNotificationBehaviorTimestamps: self.currentUpdatedPeerNotificationBehaviorTimestamps, currentUpdatedCachedPeerData: self.currentUpdatedCachedPeerData, currentUpdatedPeerPresences: currentUpdatedPeerPresences, currentUpdatedPeerChatListEmbeddedStates: self.currentUpdatedPeerChatListEmbeddedStates, currentUpdatedTotalUnreadStates: self.currentUpdatedTotalUnreadStates, currentUpdatedTotalUnreadSummaries: self.currentUpdatedGroupTotalUnreadSummaries, alteredInitialPeerCombinedReadStates: alteredInitialPeerCombinedReadStates, currentPeerMergedOperationLogOperations: self.currentPeerMergedOperationLogOperations, currentTimestampBasedMessageAttributesOperations: self.currentTimestampBasedMessageAttributesOperations, unsentMessageOperations: self.currentUnsentOperations, updatedSynchronizePeerReadStateOperations: self.currentUpdatedSynchronizeReadStateOperations, currentUpdatedGroupSummarySynchronizeOperations: self.currentUpdatedGroupSummarySynchronizeOperations, currentPreferencesOperations: self.currentPreferencesOperations, currentOrderedItemListOperations: self.currentOrderedItemListOperations, currentItemCollectionItemsOperations: self.currentItemCollectionItemsOperations, currentItemCollectionInfosOperations: self.currentItemCollectionInfosOperations, currentUpdatedPeerChatStates: self.currentUpdatedPeerChatStates, currentGlobalTagsOperations: self.currentGlobalTagsOperations, currentLocalTagsOperations: self.currentLocalTagsOperations, updatedMedia: self.currentUpdatedMedia, replaceRemoteContactCount: self.currentReplaceRemoteContactCount, replaceContactPeerIds: self.currentReplacedContactPeerIds, currentPendingMessageActionsOperations: self.currentPendingMessageActionsOperations, currentUpdatedMessageActionsSummaries: self.currentUpdatedMessageActionsSummaries, currentUpdatedMessageTagSummaries: self.currentUpdatedMessageTagSummaries, currentInvalidateMessageTagSummaries: self.currentInvalidateMessageTagSummaries, currentUpdatedPendingPeerNotificationSettings: self.currentUpdatedPendingPeerNotificationSettings, replacedAdditionalChatListItems: self.currentReplacedAdditionalChatListItems, updatedNoticeEntryKeys: self.currentUpdatedNoticeEntryKeys, updatedCacheEntryKeys: self.currentUpdatedCacheEntryKeys, currentUpdatedMasterClientId: currentUpdatedMasterClientId, updatedFailedMessagePeerIds: self.messageHistoryFailedTable.updatedPeerIds, updatedFailedMessageIds: self.messageHistoryFailedTable.updatedMessageIds, updatedGlobalNotificationSettings: self.currentNeedsReindexUnreadCounters, updatedPeerTimeoutAttributes: updatedPeerTimeoutAttributes, updatedMessageThreadPeerIds: updatedMessageThreadPeerIds, updatedPeerThreadCombinedStates: self.currentUpdatedPeerThreadCombinedStates, updatedPeerThreadsSummaries: Set(alteredInitialPeerThreadsSummaries.keys), updatedPinnedThreads: self.currentUpdatedPinnedThreads, updatedHiddenPeerIds: self.currentUpdatedHiddenPeerIds, storyStatesEvents: self.currentStoryStatesEvents, storySubscriptionsEvents: self.currentStorySubscriptionsEvents, storyItemsEvents: self.currentStoryItemsEvents)
|
||||
var updatedTransactionState: Int64?
|
||||
var updatedMasterClientId: Int64?
|
||||
if !transaction.isEmpty {
|
||||
@ -2226,6 +2323,9 @@ final class PostboxImpl {
|
||||
self.currentUpdatedPinnedThreads.removeAll()
|
||||
self.currentUpdatedHiddenPeerIds = false
|
||||
self.currentNeedsReindexUnreadCounters = false
|
||||
self.currentStoryStatesEvents.removeAll()
|
||||
self.currentStorySubscriptionsEvents.removeAll()
|
||||
self.currentStoryItemsEvents.removeAll()
|
||||
|
||||
for table in self.tables {
|
||||
table.beforeCommit()
|
||||
|
@ -49,6 +49,9 @@ final class PostboxTransaction {
|
||||
let updatedPeerThreadsSummaries: Set<PeerId>
|
||||
let updatedPinnedThreads: Set<PeerId>
|
||||
let updatedHiddenPeerIds: Bool
|
||||
let storyStatesEvents: [StoryStatesTable.Event]
|
||||
let storySubscriptionsEvents: [StorySubscriptionsTable.Event]
|
||||
let storyItemsEvents: [StoryItemsTable.Event]
|
||||
|
||||
var isEmpty: Bool {
|
||||
if currentUpdatedState != nil {
|
||||
@ -195,10 +198,19 @@ final class PostboxTransaction {
|
||||
if self.updatedHiddenPeerIds {
|
||||
return false
|
||||
}
|
||||
if !self.storyStatesEvents.isEmpty {
|
||||
return false
|
||||
}
|
||||
if !self.storySubscriptionsEvents.isEmpty {
|
||||
return false
|
||||
}
|
||||
if !self.storyItemsEvents.isEmpty {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
init(currentUpdatedState: PostboxCoding?, currentPeerHoleOperations: [MessageHistoryIndexHoleOperationKey: [MessageHistoryIndexHoleOperation]] = [:], currentOperationsByPeerId: [PeerId: [MessageHistoryOperation]], chatListOperations: [PeerGroupId: [ChatListOperation]], currentUpdatedChatListInclusions: [PeerId: PeerChatListInclusion], currentUpdatedPeers: [PeerId: Peer], currentUpdatedPeerNotificationSettings: [PeerId: (PeerNotificationSettings?, PeerNotificationSettings)], currentUpdatedPeerNotificationBehaviorTimestamps: [PeerId: PeerNotificationSettingsBehaviorTimestamp], currentUpdatedCachedPeerData: [PeerId: CachedPeerData], currentUpdatedPeerPresences: [PeerId: PeerPresence], currentUpdatedPeerChatListEmbeddedStates: Set<PeerId>, currentUpdatedTotalUnreadStates: [PeerGroupId: ChatListTotalUnreadState], currentUpdatedTotalUnreadSummaries: [PeerGroupId: PeerGroupUnreadCountersCombinedSummary], alteredInitialPeerCombinedReadStates: [PeerId: CombinedPeerReadState], currentPeerMergedOperationLogOperations: [PeerMergedOperationLogOperation], currentTimestampBasedMessageAttributesOperations: [TimestampBasedMessageAttributesOperation], unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], updatedSynchronizePeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?], currentUpdatedGroupSummarySynchronizeOperations: [PeerGroupAndNamespace: Bool], currentPreferencesOperations: [PreferencesOperation], currentOrderedItemListOperations: [Int32: [OrderedItemListOperation]], currentItemCollectionItemsOperations: [ItemCollectionId: [ItemCollectionItemsOperation]], currentItemCollectionInfosOperations: [ItemCollectionInfosOperation], currentUpdatedPeerChatStates: Set<PeerId>, currentGlobalTagsOperations: [GlobalMessageHistoryTagsOperation], currentLocalTagsOperations: [IntermediateMessageHistoryLocalTagsOperation], updatedMedia: [MediaId: Media?], replaceRemoteContactCount: Int32?, replaceContactPeerIds: Set<PeerId>?, currentPendingMessageActionsOperations: [PendingMessageActionsOperation], currentUpdatedMessageActionsSummaries: [PendingMessageActionsSummaryKey: Int32], currentUpdatedMessageTagSummaries: [MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary], currentInvalidateMessageTagSummaries: [InvalidatedMessageHistoryTagsSummaryEntryOperation], currentUpdatedPendingPeerNotificationSettings: Set<PeerId>, replacedAdditionalChatListItems: [AdditionalChatListItem]?, updatedNoticeEntryKeys: Set<NoticeEntryKey>, updatedCacheEntryKeys: Set<ItemCacheEntryId>, currentUpdatedMasterClientId: Int64?, updatedFailedMessagePeerIds: Set<PeerId>, updatedFailedMessageIds: Set<MessageId>, updatedGlobalNotificationSettings: Bool, updatedPeerTimeoutAttributes: Bool, updatedMessageThreadPeerIds: Set<PeerId>, updatedPeerThreadCombinedStates: Set<PeerId>, updatedPeerThreadsSummaries: Set<PeerId>, updatedPinnedThreads: Set<PeerId>, updatedHiddenPeerIds: Bool) {
|
||||
init(currentUpdatedState: PostboxCoding?, currentPeerHoleOperations: [MessageHistoryIndexHoleOperationKey: [MessageHistoryIndexHoleOperation]] = [:], currentOperationsByPeerId: [PeerId: [MessageHistoryOperation]], chatListOperations: [PeerGroupId: [ChatListOperation]], currentUpdatedChatListInclusions: [PeerId: PeerChatListInclusion], currentUpdatedPeers: [PeerId: Peer], currentUpdatedPeerNotificationSettings: [PeerId: (PeerNotificationSettings?, PeerNotificationSettings)], currentUpdatedPeerNotificationBehaviorTimestamps: [PeerId: PeerNotificationSettingsBehaviorTimestamp], currentUpdatedCachedPeerData: [PeerId: CachedPeerData], currentUpdatedPeerPresences: [PeerId: PeerPresence], currentUpdatedPeerChatListEmbeddedStates: Set<PeerId>, currentUpdatedTotalUnreadStates: [PeerGroupId: ChatListTotalUnreadState], currentUpdatedTotalUnreadSummaries: [PeerGroupId: PeerGroupUnreadCountersCombinedSummary], alteredInitialPeerCombinedReadStates: [PeerId: CombinedPeerReadState], currentPeerMergedOperationLogOperations: [PeerMergedOperationLogOperation], currentTimestampBasedMessageAttributesOperations: [TimestampBasedMessageAttributesOperation], unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], updatedSynchronizePeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?], currentUpdatedGroupSummarySynchronizeOperations: [PeerGroupAndNamespace: Bool], currentPreferencesOperations: [PreferencesOperation], currentOrderedItemListOperations: [Int32: [OrderedItemListOperation]], currentItemCollectionItemsOperations: [ItemCollectionId: [ItemCollectionItemsOperation]], currentItemCollectionInfosOperations: [ItemCollectionInfosOperation], currentUpdatedPeerChatStates: Set<PeerId>, currentGlobalTagsOperations: [GlobalMessageHistoryTagsOperation], currentLocalTagsOperations: [IntermediateMessageHistoryLocalTagsOperation], updatedMedia: [MediaId: Media?], replaceRemoteContactCount: Int32?, replaceContactPeerIds: Set<PeerId>?, currentPendingMessageActionsOperations: [PendingMessageActionsOperation], currentUpdatedMessageActionsSummaries: [PendingMessageActionsSummaryKey: Int32], currentUpdatedMessageTagSummaries: [MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary], currentInvalidateMessageTagSummaries: [InvalidatedMessageHistoryTagsSummaryEntryOperation], currentUpdatedPendingPeerNotificationSettings: Set<PeerId>, replacedAdditionalChatListItems: [AdditionalChatListItem]?, updatedNoticeEntryKeys: Set<NoticeEntryKey>, updatedCacheEntryKeys: Set<ItemCacheEntryId>, currentUpdatedMasterClientId: Int64?, updatedFailedMessagePeerIds: Set<PeerId>, updatedFailedMessageIds: Set<MessageId>, updatedGlobalNotificationSettings: Bool, updatedPeerTimeoutAttributes: Bool, updatedMessageThreadPeerIds: Set<PeerId>, updatedPeerThreadCombinedStates: Set<PeerId>, updatedPeerThreadsSummaries: Set<PeerId>, updatedPinnedThreads: Set<PeerId>, updatedHiddenPeerIds: Bool, storyStatesEvents: [StoryStatesTable.Event], storySubscriptionsEvents: [StorySubscriptionsTable.Event], storyItemsEvents: [StoryItemsTable.Event]) {
|
||||
self.currentUpdatedState = currentUpdatedState
|
||||
self.currentPeerHoleOperations = currentPeerHoleOperations
|
||||
self.currentOperationsByPeerId = currentOperationsByPeerId
|
||||
@ -246,5 +258,8 @@ final class PostboxTransaction {
|
||||
self.updatedPeerThreadsSummaries = updatedPeerThreadsSummaries
|
||||
self.updatedPinnedThreads = updatedPinnedThreads
|
||||
self.updatedHiddenPeerIds = updatedHiddenPeerIds
|
||||
self.storyStatesEvents = storyStatesEvents
|
||||
self.storySubscriptionsEvents = storySubscriptionsEvents
|
||||
self.storyItemsEvents = storyItemsEvents
|
||||
}
|
||||
}
|
||||
|
101
submodules/Postbox/Sources/StoryItemsTable.swift
Normal file
101
submodules/Postbox/Sources/StoryItemsTable.swift
Normal file
@ -0,0 +1,101 @@
|
||||
import Foundation
|
||||
|
||||
public final class StoryItemsTableEntry: Equatable {
|
||||
public let value: CodableEntry
|
||||
public let id: Int32
|
||||
|
||||
public init(
|
||||
value: CodableEntry,
|
||||
id: Int32
|
||||
) {
|
||||
self.value = value
|
||||
self.id = id
|
||||
}
|
||||
|
||||
public static func ==(lhs: StoryItemsTableEntry, rhs: StoryItemsTableEntry) -> Bool {
|
||||
if lhs === rhs {
|
||||
return true
|
||||
}
|
||||
if lhs.id != rhs.id {
|
||||
return false
|
||||
}
|
||||
if lhs.value != rhs.value {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
final class StoryItemsTable: Table {
|
||||
enum Event {
|
||||
case replace(peerId: PeerId)
|
||||
}
|
||||
|
||||
private struct Key: Hashable {
|
||||
var peerId: PeerId
|
||||
var id: Int32
|
||||
}
|
||||
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: false)
|
||||
}
|
||||
|
||||
private let sharedKey = ValueBoxKey(length: 8 + 4)
|
||||
|
||||
private func key(_ key: Key) -> ValueBoxKey {
|
||||
self.sharedKey.setInt64(0, value: key.peerId.toInt64())
|
||||
self.sharedKey.setInt32(8, value: key.id)
|
||||
return self.sharedKey
|
||||
}
|
||||
|
||||
private func lowerBound(peerId: PeerId) -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 8)
|
||||
key.setInt64(0, value: peerId.toInt64())
|
||||
return key
|
||||
}
|
||||
|
||||
private func upperBound(peerId: PeerId) -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 8)
|
||||
key.setInt64(0, value: peerId.toInt64())
|
||||
return key.successor
|
||||
}
|
||||
|
||||
public func get(peerId: PeerId) -> [StoryItemsTableEntry] {
|
||||
var result: [StoryItemsTableEntry] = []
|
||||
|
||||
self.valueBox.range(self.table, start: self.lowerBound(peerId: peerId), end: self.upperBound(peerId: peerId), values: { key, value in
|
||||
let id = key.getInt32(8)
|
||||
|
||||
let entry = CodableEntry(data: value.makeData())
|
||||
result.append(StoryItemsTableEntry(value: entry, id: id))
|
||||
|
||||
return true
|
||||
}, limit: 10000)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
public func replace(peerId: PeerId, entries: [StoryItemsTableEntry], events: inout [Event]) {
|
||||
var previousKeys: [ValueBoxKey] = []
|
||||
self.valueBox.range(self.table, start: self.lowerBound(peerId: peerId), end: self.upperBound(peerId: peerId), keys: { key in
|
||||
previousKeys.append(key)
|
||||
|
||||
return true
|
||||
}, limit: 10000)
|
||||
for key in previousKeys {
|
||||
self.valueBox.remove(self.table, key: key, secure: true)
|
||||
}
|
||||
|
||||
for entry in entries {
|
||||
self.valueBox.set(self.table, key: self.key(Key(peerId: peerId, id: entry.id)), value: MemoryBuffer(data: entry.value.data))
|
||||
}
|
||||
|
||||
events.append(.replace(peerId: peerId))
|
||||
}
|
||||
|
||||
override func clearMemoryCache() {
|
||||
}
|
||||
|
||||
override func beforeCommit() {
|
||||
}
|
||||
}
|
53
submodules/Postbox/Sources/StoryItemsView.swift
Normal file
53
submodules/Postbox/Sources/StoryItemsView.swift
Normal file
@ -0,0 +1,53 @@
|
||||
import Foundation
|
||||
|
||||
final class MutableStoryItemsView: MutablePostboxView {
|
||||
let peerId: PeerId
|
||||
var items: [StoryItemsTableEntry]
|
||||
|
||||
init(postbox: PostboxImpl, peerId: PeerId) {
|
||||
self.peerId = peerId
|
||||
self.items = postbox.storyItemsTable.get(peerId: peerId)
|
||||
}
|
||||
|
||||
func replay(postbox: PostboxImpl, transaction: PostboxTransaction) -> Bool {
|
||||
var updated = false
|
||||
if !transaction.storyStatesEvents.isEmpty {
|
||||
loop: for event in transaction.storyItemsEvents {
|
||||
switch event {
|
||||
case .replace(peerId):
|
||||
let items = postbox.storyItemsTable.get(peerId: self.peerId)
|
||||
if self.items != items {
|
||||
self.items = items
|
||||
updated = true
|
||||
}
|
||||
break loop
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
func refreshDueToExternalTransaction(postbox: PostboxImpl) -> Bool {
|
||||
let items = postbox.storyItemsTable.get(peerId: self.peerId)
|
||||
if self.items != items {
|
||||
self.items = items
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func immutableView() -> PostboxView {
|
||||
return StoryItemsView(self)
|
||||
}
|
||||
}
|
||||
|
||||
public final class StoryItemsView: PostboxView {
|
||||
public let items: [StoryItemsTableEntry]
|
||||
|
||||
init(_ view: MutableStoryItemsView) {
|
||||
self.items = view.items
|
||||
}
|
||||
}
|
70
submodules/Postbox/Sources/StoryStatesTable.swift
Normal file
70
submodules/Postbox/Sources/StoryStatesTable.swift
Normal file
@ -0,0 +1,70 @@
|
||||
import Foundation
|
||||
|
||||
final class StoryStatesTable: Table {
|
||||
enum Event {
|
||||
case set(Key)
|
||||
}
|
||||
|
||||
enum Key: Hashable {
|
||||
case local
|
||||
case subscriptions
|
||||
case peer(PeerId)
|
||||
|
||||
init(key: ValueBoxKey) {
|
||||
switch key.getUInt8(0) {
|
||||
case 0:
|
||||
self = .local
|
||||
case 1:
|
||||
self = .subscriptions
|
||||
case 2:
|
||||
self = .peer(PeerId(key.getInt64(1)))
|
||||
default:
|
||||
assertionFailure()
|
||||
self = .peer(PeerId(namespace: PeerId.Namespace._internalFromInt32Value(0), id: ._internalFromInt64Value(0)))
|
||||
}
|
||||
}
|
||||
|
||||
func asKey() -> ValueBoxKey {
|
||||
switch self {
|
||||
case .local:
|
||||
let key = ValueBoxKey(length: 1)
|
||||
key.setUInt8(0, value: 0)
|
||||
return key
|
||||
case .subscriptions:
|
||||
let key = ValueBoxKey(length: 1)
|
||||
key.setUInt8(0, value: 1)
|
||||
return key
|
||||
case let .peer(peerId):
|
||||
let key = ValueBoxKey(length: 1 + 8)
|
||||
key.setUInt8(0, value: 2)
|
||||
key.setInt64(1, value: peerId.toInt64())
|
||||
return key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: false)
|
||||
}
|
||||
|
||||
private let sharedKey = ValueBoxKey(length: 8 + 4)
|
||||
|
||||
func get(key: Key) -> CodableEntry? {
|
||||
return self.valueBox.get(self.table, key: key.asKey()).flatMap { CodableEntry(data: $0.makeData()) }
|
||||
}
|
||||
|
||||
func set(key: Key, value: CodableEntry?, events: inout [Event]) {
|
||||
if let value = value {
|
||||
self.valueBox.set(self.table, key: key.asKey(), value: MemoryBuffer(data: value.data))
|
||||
} else {
|
||||
self.valueBox.remove(self.table, key: key.asKey(), secure: true)
|
||||
}
|
||||
events.append(.set(key))
|
||||
}
|
||||
|
||||
override func clearMemoryCache() {
|
||||
}
|
||||
|
||||
override func beforeCommit() {
|
||||
}
|
||||
}
|
84
submodules/Postbox/Sources/StoryStatesView.swift
Normal file
84
submodules/Postbox/Sources/StoryStatesView.swift
Normal file
@ -0,0 +1,84 @@
|
||||
import Foundation
|
||||
|
||||
public enum PostboxStoryStatesKey: Hashable {
|
||||
case local
|
||||
case subscriptions
|
||||
case peer(PeerId)
|
||||
}
|
||||
|
||||
private extension PostboxStoryStatesKey {
|
||||
init(tableKey: StoryStatesTable.Key) {
|
||||
switch tableKey {
|
||||
case .local:
|
||||
self = .local
|
||||
case .subscriptions:
|
||||
self = .subscriptions
|
||||
case let .peer(peerId):
|
||||
self = .peer(peerId)
|
||||
}
|
||||
}
|
||||
|
||||
var tableKey: StoryStatesTable.Key {
|
||||
switch self {
|
||||
case .local:
|
||||
return .local
|
||||
case .subscriptions:
|
||||
return .subscriptions
|
||||
case let .peer(peerId):
|
||||
return .peer(peerId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class MutableStoryStatesView: MutablePostboxView {
|
||||
let key: PostboxStoryStatesKey
|
||||
var value: CodableEntry?
|
||||
|
||||
init(postbox: PostboxImpl, key: PostboxStoryStatesKey) {
|
||||
self.key = key
|
||||
self.value = postbox.storyStatesTable.get(key: key.tableKey)
|
||||
}
|
||||
|
||||
func replay(postbox: PostboxImpl, transaction: PostboxTransaction) -> Bool {
|
||||
var updated = false
|
||||
if !transaction.storyStatesEvents.isEmpty {
|
||||
let tableKey = self.key.tableKey
|
||||
loop: for event in transaction.storyStatesEvents {
|
||||
switch event {
|
||||
case .set(tableKey):
|
||||
let value = postbox.storyStatesTable.get(key: self.key.tableKey)
|
||||
if value != self.value {
|
||||
self.value = value
|
||||
updated = true
|
||||
}
|
||||
break loop
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
func refreshDueToExternalTransaction(postbox: PostboxImpl) -> Bool {
|
||||
let value = postbox.storyStatesTable.get(key: self.key.tableKey)
|
||||
if value != self.value {
|
||||
self.value = value
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func immutableView() -> PostboxView {
|
||||
return StoryStatesView(self)
|
||||
}
|
||||
}
|
||||
|
||||
public final class StoryStatesView: PostboxView {
|
||||
public let value: CodableEntry?
|
||||
|
||||
init(_ view: MutableStoryStatesView) {
|
||||
self.value = view.value
|
||||
}
|
||||
}
|
67
submodules/Postbox/Sources/StorySubscriptionsTable.swift
Normal file
67
submodules/Postbox/Sources/StorySubscriptionsTable.swift
Normal file
@ -0,0 +1,67 @@
|
||||
import Foundation
|
||||
|
||||
final class StorySubscriptionsTable: Table {
|
||||
enum Event {
|
||||
case replaceAll
|
||||
}
|
||||
|
||||
private struct Key: Hashable {
|
||||
var peerId: PeerId
|
||||
}
|
||||
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: false)
|
||||
}
|
||||
|
||||
private let sharedKey = ValueBoxKey(length: 8)
|
||||
|
||||
private func key(_ key: Key) -> ValueBoxKey {
|
||||
self.sharedKey.setInt64(0, value: key.peerId.toInt64())
|
||||
return self.sharedKey
|
||||
}
|
||||
|
||||
private func getAllKeys() -> [Key] {
|
||||
var result: [Key] = []
|
||||
|
||||
self.valueBox.scan(self.table, keys: { key in
|
||||
let peerId = PeerId(key.getInt64(0))
|
||||
|
||||
result.append(Key(peerId: peerId))
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
public func getAll() -> [PeerId] {
|
||||
var result: [PeerId] = []
|
||||
|
||||
self.valueBox.scan(self.table, keys: { key in
|
||||
let peerId = PeerId(key.getInt64(0))
|
||||
result.append(peerId)
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
public func replaceAll(peerIds: [PeerId], events: inout [Event]) {
|
||||
for key in self.getAllKeys() {
|
||||
self.valueBox.remove(self.table, key: self.key(key), secure: true)
|
||||
}
|
||||
|
||||
for peerId in peerIds {
|
||||
self.valueBox.set(self.table, key: self.key(Key(peerId: peerId)), value: MemoryBuffer())
|
||||
}
|
||||
|
||||
events.append(.replaceAll)
|
||||
}
|
||||
|
||||
override func clearMemoryCache() {
|
||||
}
|
||||
|
||||
override func beforeCommit() {
|
||||
}
|
||||
}
|
50
submodules/Postbox/Sources/StorySubscriptionsView.swift
Normal file
50
submodules/Postbox/Sources/StorySubscriptionsView.swift
Normal file
@ -0,0 +1,50 @@
|
||||
import Foundation
|
||||
|
||||
final class MutableStorySubscriptionsView: MutablePostboxView {
|
||||
var peerIds: [PeerId]
|
||||
|
||||
init(postbox: PostboxImpl) {
|
||||
self.peerIds = postbox.storySubscriptionsTable.getAll()
|
||||
}
|
||||
|
||||
func replay(postbox: PostboxImpl, transaction: PostboxTransaction) -> Bool {
|
||||
var updated = false
|
||||
if !transaction.storySubscriptionsEvents.isEmpty {
|
||||
loop: for event in transaction.storySubscriptionsEvents {
|
||||
switch event {
|
||||
case .replaceAll:
|
||||
let peerIds = postbox.storySubscriptionsTable.getAll()
|
||||
if self.peerIds != peerIds {
|
||||
updated = true
|
||||
self.peerIds = peerIds
|
||||
}
|
||||
|
||||
break loop
|
||||
}
|
||||
}
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
func refreshDueToExternalTransaction(postbox: PostboxImpl) -> Bool {
|
||||
let peerIds = postbox.storySubscriptionsTable.getAll()
|
||||
if self.peerIds != peerIds {
|
||||
self.peerIds = peerIds
|
||||
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func immutableView() -> PostboxView {
|
||||
return StorySubscriptionsView(self)
|
||||
}
|
||||
}
|
||||
|
||||
public final class StorySubscriptionsView: PostboxView {
|
||||
public let peerIds: [PeerId]
|
||||
|
||||
init(_ view: MutableStorySubscriptionsView) {
|
||||
self.peerIds = view.peerIds
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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")]
|
||||
|
@ -973,6 +973,8 @@ public class Account {
|
||||
|
||||
let networkStatsContext: NetworkStatsContext
|
||||
|
||||
public let storySubscriptionsContext: StorySubscriptionsContext?
|
||||
|
||||
public init(accountManager: AccountManager<TelegramAccountManagerTypes>, id: AccountRecordId, basePath: String, testingEnvironment: Bool, postbox: Postbox, network: Network, networkArguments: NetworkInitializationArguments, peerId: PeerId, auxiliaryMethods: AccountAuxiliaryMethods, supplementary: Bool) {
|
||||
self.accountManager = accountManager
|
||||
self.id = id
|
||||
@ -989,6 +991,13 @@ public class Account {
|
||||
self.networkStatsContext = NetworkStatsContext(postbox: postbox)
|
||||
|
||||
self.peerInputActivityManager = PeerInputActivityManager()
|
||||
|
||||
if !supplementary {
|
||||
self.storySubscriptionsContext = StorySubscriptionsContext(accountPeerId: peerId, postbox: postbox, network: network)
|
||||
} else {
|
||||
self.storySubscriptionsContext = nil
|
||||
}
|
||||
|
||||
self.callSessionManager = CallSessionManager(postbox: postbox, network: network, maxLayer: networkArguments.voipMaxLayer, versions: networkArguments.voipVersions, addUpdates: { [weak self] updates in
|
||||
self?.stateManager?.addUpdates(updates)
|
||||
})
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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 {
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,423 @@ public struct EngineStoryPrivacy: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum Stories {
|
||||
public final class Item: Codable, Equatable {
|
||||
public struct Views: Codable, Equatable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case seenCount = "seenCount"
|
||||
case seenPeerIds = "seenPeerIds"
|
||||
}
|
||||
|
||||
public var seenCount: Int
|
||||
public var seenPeerIds: [PeerId]
|
||||
|
||||
public init(seenCount: Int, seenPeerIds: [PeerId]) {
|
||||
self.seenCount = seenCount
|
||||
self.seenPeerIds = seenPeerIds
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self.seenCount = Int(try container.decode(Int32.self, forKey: .seenCount))
|
||||
self.seenPeerIds = try container.decode([Int64].self, forKey: .seenPeerIds).map(PeerId.init)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(Int32(clamping: self.seenCount), forKey: .seenCount)
|
||||
try container.encode(self.seenPeerIds.map { $0.toInt64() }, forKey: .seenPeerIds)
|
||||
}
|
||||
}
|
||||
|
||||
public struct Privacy: Codable, Equatable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case base = "base"
|
||||
case additionallyIncludePeers = "addPeers"
|
||||
}
|
||||
|
||||
public enum Base: Int32 {
|
||||
case everyone = 0
|
||||
case contacts = 1
|
||||
case closeFriends = 2
|
||||
case nobody = 3
|
||||
}
|
||||
|
||||
public var base: Base
|
||||
public var additionallyIncludePeers: [PeerId]
|
||||
|
||||
public init(base: Base, additionallyIncludePeers: [PeerId]) {
|
||||
self.base = base
|
||||
self.additionallyIncludePeers = additionallyIncludePeers
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self.base = Base(rawValue: try container.decode(Int32.self, forKey: .base)) ?? .nobody
|
||||
self.additionallyIncludePeers = try container.decode([Int64].self, forKey: .additionallyIncludePeers).map(PeerId.init)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(self.base.rawValue, forKey: .base)
|
||||
try container.encode(self.additionallyIncludePeers.map { $0.toInt64() }, forKey: .additionallyIncludePeers)
|
||||
}
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case timestamp
|
||||
case media
|
||||
case text
|
||||
case entities
|
||||
case views
|
||||
case privacy
|
||||
}
|
||||
|
||||
public let id: Int32
|
||||
public let timestamp: Int32
|
||||
public let media: Media?
|
||||
public let text: String
|
||||
public let entities: [MessageTextEntity]
|
||||
public let views: Views?
|
||||
public let privacy: Privacy?
|
||||
|
||||
public init(
|
||||
id: Int32,
|
||||
timestamp: Int32,
|
||||
media: Media?,
|
||||
text: String,
|
||||
entities: [MessageTextEntity],
|
||||
views: Views?,
|
||||
privacy: Privacy?
|
||||
) {
|
||||
self.id = id
|
||||
self.timestamp = timestamp
|
||||
self.media = media
|
||||
self.text = text
|
||||
self.entities = entities
|
||||
self.views = views
|
||||
self.privacy = privacy
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self.id = try container.decode(Int32.self, forKey: .id)
|
||||
self.timestamp = try container.decode(Int32.self, forKey: .timestamp)
|
||||
|
||||
if let mediaData = try container.decodeIfPresent(Data.self, forKey: .media) {
|
||||
self.media = PostboxDecoder(buffer: MemoryBuffer(data: mediaData)).decodeRootObject() as? Media
|
||||
} else {
|
||||
self.media = nil
|
||||
}
|
||||
|
||||
self.text = try container.decode(String.self, forKey: .text)
|
||||
self.entities = try container.decode([MessageTextEntity].self, forKey: .entities)
|
||||
self.views = try container.decodeIfPresent(Views.self, forKey: .views)
|
||||
self.privacy = try container.decodeIfPresent(Privacy.self, forKey: .privacy)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(self.id, forKey: .id)
|
||||
try container.encode(self.timestamp, forKey: .timestamp)
|
||||
|
||||
if let media = self.media {
|
||||
let encoder = PostboxEncoder()
|
||||
encoder.encodeRootObject(media)
|
||||
let mediaData = encoder.makeData()
|
||||
try container.encode(mediaData, forKey: .media)
|
||||
}
|
||||
|
||||
try container.encode(self.text, forKey: .text)
|
||||
try container.encode(self.entities, forKey: .entities)
|
||||
try container.encodeIfPresent(self.views, forKey: .views)
|
||||
try container.encodeIfPresent(self.privacy, forKey: .privacy)
|
||||
}
|
||||
|
||||
public static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||
if lhs.id != rhs.id {
|
||||
return false
|
||||
}
|
||||
if lhs.timestamp != rhs.timestamp {
|
||||
return false
|
||||
}
|
||||
|
||||
if let lhsMedia = lhs.media, let rhsMedia = rhs.media {
|
||||
if !lhsMedia.isEqual(to: rhsMedia) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if (lhs.media == nil) != (rhs.media == nil) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
if lhs.entities != rhs.entities {
|
||||
return false
|
||||
}
|
||||
if lhs.views != rhs.views {
|
||||
return false
|
||||
}
|
||||
if lhs.privacy != rhs.privacy {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public final class Placeholder: Codable, Equatable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case timestamp
|
||||
}
|
||||
|
||||
public let id: Int32
|
||||
public let timestamp: Int32
|
||||
|
||||
public init(
|
||||
id: Int32,
|
||||
timestamp: Int32
|
||||
) {
|
||||
self.id = id
|
||||
self.timestamp = timestamp
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self.id = try container.decode(Int32.self, forKey: .id)
|
||||
self.timestamp = try container.decode(Int32.self, forKey: .timestamp)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(self.id, forKey: .id)
|
||||
try container.encode(self.timestamp, forKey: .timestamp)
|
||||
}
|
||||
|
||||
public static func ==(lhs: Placeholder, rhs: Placeholder) -> Bool {
|
||||
if lhs.id != rhs.id {
|
||||
return false
|
||||
}
|
||||
if lhs.timestamp != rhs.timestamp {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public enum StoredItem: Codable, Equatable {
|
||||
public enum DecodingError: Error {
|
||||
case generic
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case discriminator = "d"
|
||||
case item = "i"
|
||||
case placeholder = "p"
|
||||
}
|
||||
|
||||
case item(Item)
|
||||
case placeholder(Placeholder)
|
||||
|
||||
public var id: Int32 {
|
||||
switch self {
|
||||
case let .item(item):
|
||||
return item.id
|
||||
case let .placeholder(placeholder):
|
||||
return placeholder.id
|
||||
}
|
||||
}
|
||||
|
||||
public var timestamp: Int32 {
|
||||
switch self {
|
||||
case let .item(item):
|
||||
return item.timestamp
|
||||
case let .placeholder(placeholder):
|
||||
return placeholder.timestamp
|
||||
}
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
switch try container.decode(Int32.self, forKey: .discriminator) {
|
||||
case 0:
|
||||
self = .item(try container.decode(Item.self, forKey: .item))
|
||||
case 1:
|
||||
self = .placeholder(try container.decode(Placeholder.self, forKey: .placeholder))
|
||||
default:
|
||||
throw DecodingError.generic
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
switch self {
|
||||
case let .item(item):
|
||||
try container.encode(0 as Int32, forKey: .discriminator)
|
||||
try container.encode(item, forKey: .item)
|
||||
case let .placeholder(placeholder):
|
||||
try container.encode(1 as Int32, forKey: .discriminator)
|
||||
try container.encode(placeholder, forKey: .placeholder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class PeerState: Equatable, Codable {
|
||||
private enum CodingKeys: CodingKey {
|
||||
case subscriptionsOpaqueState
|
||||
case maxReadId
|
||||
}
|
||||
|
||||
public let subscriptionsOpaqueState: String?
|
||||
public let maxReadId: Int32
|
||||
|
||||
public init(
|
||||
subscriptionsOpaqueState: String?,
|
||||
maxReadId: Int32
|
||||
){
|
||||
self.subscriptionsOpaqueState = subscriptionsOpaqueState
|
||||
self.maxReadId = maxReadId
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self.subscriptionsOpaqueState = try container.decodeIfPresent(String.self, forKey: .subscriptionsOpaqueState)
|
||||
self.maxReadId = try container.decode(Int32.self, forKey: .maxReadId)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encodeIfPresent(self.subscriptionsOpaqueState, forKey: .subscriptionsOpaqueState)
|
||||
try container.encode(self.maxReadId, forKey: .maxReadId)
|
||||
}
|
||||
|
||||
public static func ==(lhs: PeerState, rhs: PeerState) -> Bool {
|
||||
if lhs.subscriptionsOpaqueState != rhs.subscriptionsOpaqueState {
|
||||
return false
|
||||
}
|
||||
if lhs.maxReadId != rhs.maxReadId {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public final class SubscriptionsState: Equatable, Codable {
|
||||
private enum CodingKeys: CodingKey {
|
||||
case opaqueState
|
||||
case hasMore
|
||||
}
|
||||
|
||||
public let opaqueState: String
|
||||
public let hasMore: Bool
|
||||
|
||||
public init(
|
||||
opaqueState: String,
|
||||
hasMore: Bool
|
||||
) {
|
||||
self.opaqueState = opaqueState
|
||||
self.hasMore = hasMore
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self.opaqueState = try container.decode(String.self, forKey: .opaqueState)
|
||||
self.hasMore = try container.decode(Bool.self, forKey: .hasMore)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(self.opaqueState, forKey: .opaqueState)
|
||||
try container.encode(self.hasMore, forKey: .hasMore)
|
||||
}
|
||||
|
||||
public static func ==(lhs: SubscriptionsState, rhs: SubscriptionsState) -> Bool {
|
||||
if lhs.opaqueState != rhs.opaqueState {
|
||||
return false
|
||||
}
|
||||
if lhs.hasMore != rhs.hasMore {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class EngineStorySubscriptions: Equatable {
|
||||
public final class Item: Equatable {
|
||||
public let peer: EnginePeer
|
||||
public let hasUnseen: Bool
|
||||
public let storyCount: Int
|
||||
public let lastTimestamp: Int32
|
||||
|
||||
public init(
|
||||
peer: EnginePeer,
|
||||
hasUnseen: Bool,
|
||||
storyCount: Int,
|
||||
lastTimestamp: Int32
|
||||
) {
|
||||
self.peer = peer
|
||||
self.hasUnseen = hasUnseen
|
||||
self.storyCount = storyCount
|
||||
self.lastTimestamp = lastTimestamp
|
||||
}
|
||||
|
||||
public static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
if lhs.hasUnseen != rhs.hasUnseen {
|
||||
return false
|
||||
}
|
||||
if lhs.storyCount != rhs.storyCount {
|
||||
return false
|
||||
}
|
||||
if lhs.lastTimestamp != rhs.lastTimestamp {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public let items: [Item]
|
||||
public let hasMoreToken: String?
|
||||
|
||||
public init(items: [Item], hasMoreToken: String?) {
|
||||
self.items = items
|
||||
self.hasMoreToken = hasMoreToken
|
||||
}
|
||||
|
||||
public static func ==(lhs: EngineStorySubscriptions, rhs: EngineStorySubscriptions) -> Bool {
|
||||
if lhs.items != rhs.items {
|
||||
return false
|
||||
}
|
||||
if lhs.hasMoreToken != rhs.hasMoreToken {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], privacy: EngineStoryPrivacy) -> Signal<Never, NoError> {
|
||||
let originalMedia: Media
|
||||
let contentToUpload: MessageContentToUpload
|
||||
@ -71,7 +488,7 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text:
|
||||
mimeType: "video/mp4",
|
||||
size: nil,
|
||||
attributes: [
|
||||
TelegramMediaFileAttribute.Video(duration: duration, size: dimensions, flags: .supportsStreaming)
|
||||
TelegramMediaFileAttribute.Video(duration: duration, size: dimensions, flags: .supportsStreaming, preloadSize: nil)
|
||||
]
|
||||
)
|
||||
originalMedia = fileMedia
|
||||
@ -227,6 +644,13 @@ func _internal_deleteStory(account: Account, id: Int32) -> Signal<Never, NoError
|
||||
|
||||
func _internal_markStoryAsSeen(account: Account, peerId: PeerId, id: Int32) -> Signal<Never, NoError> {
|
||||
return account.postbox.transaction { transaction -> Api.InputUser? in
|
||||
if let peerStoryState = transaction.getPeerStoryState(peerId: peerId)?.get(Stories.PeerState.self) {
|
||||
transaction.setPeerStoryState(peerId: peerId, state: CodableEntry(Stories.PeerState(
|
||||
subscriptionsOpaqueState: peerStoryState.subscriptionsOpaqueState,
|
||||
maxReadId: max(peerStoryState.maxReadId, id)
|
||||
)))
|
||||
}
|
||||
|
||||
return transaction.getPeer(peerId).flatMap(apiInputUser)
|
||||
}
|
||||
|> mapToSignal { inputUser -> Signal<Never, NoError> in
|
||||
@ -236,6 +660,12 @@ func _internal_markStoryAsSeen(account: Account, peerId: PeerId, id: Int32) -> S
|
||||
|
||||
account.stateManager.injectStoryUpdates(updates: [.read(peerId: peerId, maxId: id)])
|
||||
|
||||
#if DEBUG
|
||||
if "".isEmpty {
|
||||
return .complete()
|
||||
}
|
||||
#endif
|
||||
|
||||
return account.network.request(Api.functions.stories.readStories(userId: inputUser, maxId: id))
|
||||
|> `catch` { _ -> Signal<[Int32], NoError> in
|
||||
return .single([])
|
||||
@ -257,6 +687,74 @@ extension Api.StoryItem {
|
||||
}
|
||||
}
|
||||
|
||||
extension Stories.Item.Views {
|
||||
init(apiViews: Api.StoryViews) {
|
||||
switch apiViews {
|
||||
case let .storyViews(recentViewers, viewsCount):
|
||||
self.init(seenCount: Int(viewsCount), seenPeerIds: recentViewers.map { PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Stories.StoredItem {
|
||||
init?(apiStoryItem: Api.StoryItem, peerId: PeerId, transaction: Transaction) {
|
||||
switch apiStoryItem {
|
||||
case let .storyItem(flags, id, date, caption, entities, media, privacy, views):
|
||||
let _ = flags
|
||||
let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, peerId)
|
||||
if let parsedMedia = parsedMedia {
|
||||
var parsedPrivacy: Stories.Item.Privacy?
|
||||
if let privacy = privacy {
|
||||
var base: Stories.Item.Privacy.Base = .everyone
|
||||
var additionalPeerIds: [PeerId] = []
|
||||
for rule in privacy {
|
||||
switch rule {
|
||||
case .privacyValueAllowAll:
|
||||
base = .everyone
|
||||
case .privacyValueAllowContacts:
|
||||
base = .contacts
|
||||
case .privacyValueAllowCloseFriends:
|
||||
base = .closeFriends
|
||||
case let .privacyValueAllowUsers(users):
|
||||
for id in users {
|
||||
additionalPeerIds.append(EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(id)))
|
||||
}
|
||||
case let .privacyValueAllowChatParticipants(chats):
|
||||
for id in chats {
|
||||
if let peer = transaction.getPeer(EnginePeer.Id(namespace: Namespaces.Peer.CloudGroup, id: EnginePeer.Id.Id._internalFromInt64Value(id))) {
|
||||
additionalPeerIds.append(peer.id)
|
||||
} else if let peer = transaction.getPeer(EnginePeer.Id(namespace: Namespaces.Peer.CloudChannel, id: EnginePeer.Id.Id._internalFromInt64Value(id))) {
|
||||
additionalPeerIds.append(peer.id)
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
parsedPrivacy = Stories.Item.Privacy(base: base, additionallyIncludePeers: additionalPeerIds)
|
||||
}
|
||||
|
||||
let item = Stories.Item(
|
||||
id: id,
|
||||
timestamp: date,
|
||||
media: parsedMedia,
|
||||
text: caption ?? "",
|
||||
entities: entities.flatMap { entities in return messageTextEntitiesFromApiEntities(entities) } ?? [],
|
||||
views: views.flatMap(Stories.Item.Views.init(apiViews:)),
|
||||
privacy: parsedPrivacy
|
||||
)
|
||||
self = .item(item)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
case let .storyItemSkipped(id, date):
|
||||
self = .placeholder(Stories.Placeholder(id: id, timestamp: date))
|
||||
case .storyItemDeleted:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_parseApiStoryItem(transaction: Transaction, peerId: PeerId, apiStory: Api.StoryItem) -> StoryListContext.Item? {
|
||||
switch apiStory {
|
||||
case let .storyItem(flags, id, date, caption, entities, media, privacy, views):
|
||||
@ -323,6 +821,50 @@ func _internal_parseApiStoryViews(transaction: Transaction, views: Api.StoryView
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_getStoriesById(accountPeerId: PeerId, postbox: Postbox, network: Network, peerId: PeerId, ids: [Int32]) -> Signal<[Stories.StoredItem], NoError> {
|
||||
return postbox.transaction { transaction -> Api.InputUser? in
|
||||
return transaction.getPeer(peerId).flatMap(apiInputUser)
|
||||
}
|
||||
|> mapToSignal { inputUser -> Signal<[Stories.StoredItem], NoError> in
|
||||
guard let inputUser = inputUser else {
|
||||
return .single([])
|
||||
}
|
||||
|
||||
return network.request(Api.functions.stories.getStoriesByID(userId: inputUser, id: ids))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.stories.Stories?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { result -> Signal<[Stories.StoredItem], NoError> in
|
||||
guard let result = result else {
|
||||
return .single([])
|
||||
}
|
||||
return postbox.transaction { transaction -> [Stories.StoredItem] in
|
||||
switch result {
|
||||
case let .stories(_, stories, users):
|
||||
var peers: [Peer] = []
|
||||
var peerPresences: [PeerId: Api.User] = [:]
|
||||
|
||||
for user in users {
|
||||
let telegramUser = TelegramUser(user: user)
|
||||
peers.append(telegramUser)
|
||||
peerPresences[telegramUser.id] = user
|
||||
}
|
||||
|
||||
updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in
|
||||
return updated
|
||||
})
|
||||
updatePeerPresences(transaction: transaction, accountPeerId: accountPeerId, peerPresences: peerPresences)
|
||||
|
||||
return stories.compactMap { apiStoryItem -> Stories.StoredItem? in
|
||||
return Stories.StoredItem(apiStoryItem: apiStoryItem, peerId: peerId, transaction: transaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_getStoryById(accountPeerId: PeerId, postbox: Postbox, network: Network, peer: PeerReference, id: Int32) -> Signal<StoryListContext.Item?, NoError> {
|
||||
guard let inputUser = peer.inputUser else {
|
||||
return .single(nil)
|
||||
|
@ -9,6 +9,439 @@ enum InternalStoryUpdate {
|
||||
case read(peerId: PeerId, maxId: Int32)
|
||||
}
|
||||
|
||||
public final class StorySubscriptionsContext {
|
||||
private enum OpaqueStateMark: Equatable {
|
||||
case empty
|
||||
case value(String)
|
||||
}
|
||||
|
||||
private struct TaskState {
|
||||
var isRefreshScheduled: Bool = false
|
||||
var isLoadMoreScheduled: Bool = false
|
||||
}
|
||||
|
||||
private final class Impl {
|
||||
private let accountPeerId: PeerId
|
||||
private let queue: Queue
|
||||
private let postbox: Postbox
|
||||
private let network: Network
|
||||
|
||||
private var taskState = TaskState()
|
||||
|
||||
private var isLoading: Bool = false
|
||||
|
||||
private var loadedStateMark: OpaqueStateMark?
|
||||
private var stateDisposable: Disposable?
|
||||
private let loadMoreDisposable = MetaDisposable()
|
||||
|
||||
init(queue: Queue, accountPeerId: PeerId, postbox: Postbox, network: Network) {
|
||||
self.accountPeerId = accountPeerId
|
||||
self.queue = queue
|
||||
self.postbox = postbox
|
||||
self.network = network
|
||||
|
||||
self.taskState.isRefreshScheduled = true
|
||||
|
||||
self.updateTasks()
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.stateDisposable?.dispose()
|
||||
self.loadMoreDisposable.dispose()
|
||||
}
|
||||
|
||||
func loadMore() {
|
||||
self.taskState.isLoadMoreScheduled = true
|
||||
self.updateTasks()
|
||||
}
|
||||
|
||||
private func updateTasks() {
|
||||
if self.isLoading {
|
||||
return
|
||||
}
|
||||
|
||||
if self.taskState.isRefreshScheduled {
|
||||
self.isLoading = true
|
||||
|
||||
self.stateDisposable = (postbox.combinedView(keys: [PostboxViewKey.storiesState(key: .subscriptions)])
|
||||
|> take(1)
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] views in
|
||||
guard let `self` = self else {
|
||||
return
|
||||
}
|
||||
guard let storiesStateView = views.views[PostboxViewKey.storiesState(key: .subscriptions)] as? StoryStatesView else {
|
||||
return
|
||||
}
|
||||
|
||||
let stateMark: OpaqueStateMark
|
||||
if let subscriptionsState = storiesStateView.value?.get(Stories.SubscriptionsState.self) {
|
||||
stateMark = .value(subscriptionsState.opaqueState)
|
||||
} else {
|
||||
stateMark = .empty
|
||||
}
|
||||
|
||||
self.loadImpl(isRefresh: true, stateMark: stateMark)
|
||||
})
|
||||
} else if self.taskState.isLoadMoreScheduled {
|
||||
self.isLoading = true
|
||||
|
||||
self.stateDisposable = (postbox.combinedView(keys: [PostboxViewKey.storiesState(key: .subscriptions)])
|
||||
|> take(1)
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] views in
|
||||
guard let `self` = self else {
|
||||
return
|
||||
}
|
||||
guard let storiesStateView = views.views[PostboxViewKey.storiesState(key: .subscriptions)] as? StoryStatesView else {
|
||||
return
|
||||
}
|
||||
|
||||
let hasMore: Bool
|
||||
let stateMark: OpaqueStateMark
|
||||
if let subscriptionsState = storiesStateView.value?.get(Stories.SubscriptionsState.self) {
|
||||
hasMore = subscriptionsState.hasMore
|
||||
stateMark = .value(subscriptionsState.opaqueState)
|
||||
} else {
|
||||
stateMark = .empty
|
||||
hasMore = true
|
||||
}
|
||||
|
||||
if hasMore && self.loadedStateMark != stateMark {
|
||||
self.loadImpl(isRefresh: false, stateMark: stateMark)
|
||||
} else {
|
||||
self.isLoading = false
|
||||
self.taskState.isLoadMoreScheduled = false
|
||||
self.updateTasks()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func loadImpl(isRefresh: Bool, stateMark: OpaqueStateMark) {
|
||||
var flags: Int32 = 0
|
||||
var state: String?
|
||||
switch stateMark {
|
||||
case .empty:
|
||||
break
|
||||
case let .value(value):
|
||||
state = value
|
||||
flags |= 1 << 0
|
||||
|
||||
if !isRefresh {
|
||||
flags |= 1 << 1
|
||||
}
|
||||
}
|
||||
|
||||
let accountPeerId = self.accountPeerId
|
||||
|
||||
self.loadMoreDisposable.set((self.network.request(Api.functions.stories.getAllStories(flags: flags, state: state))
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = (self.postbox.transaction { transaction -> Void in
|
||||
switch result {
|
||||
case let .allStoriesNotModified(state):
|
||||
self.loadedStateMark = .value(state)
|
||||
let (currentStateValue, _) = transaction.getAllStorySubscriptions()
|
||||
let currentState = currentStateValue.flatMap { $0.get(Stories.SubscriptionsState.self) }
|
||||
|
||||
var hasMore = false
|
||||
if let currentState = currentState {
|
||||
hasMore = currentState.hasMore
|
||||
}
|
||||
|
||||
transaction.setSubscriptionsStoriesState(state: CodableEntry(Stories.SubscriptionsState(opaqueState: state, hasMore: hasMore)))
|
||||
case let .allStories(flags, state, userStories, users):
|
||||
var peers: [Peer] = []
|
||||
var peerPresences: [PeerId: Api.User] = [:]
|
||||
|
||||
for user in users {
|
||||
let telegramUser = TelegramUser(user: user)
|
||||
peers.append(telegramUser)
|
||||
peerPresences[telegramUser.id] = user
|
||||
}
|
||||
|
||||
updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in
|
||||
return updated
|
||||
})
|
||||
updatePeerPresences(transaction: transaction, accountPeerId: accountPeerId, peerPresences: peerPresences)
|
||||
|
||||
let hasMore: Bool = (flags & (1 << 0)) != 0
|
||||
|
||||
let (_, currentPeerItems) = transaction.getAllStorySubscriptions()
|
||||
var peerEntries: [PeerId] = []
|
||||
|
||||
for userStorySet in userStories {
|
||||
switch userStorySet {
|
||||
case let .userStories(_, userId, maxReadId, stories):
|
||||
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))
|
||||
|
||||
let previousPeerEntries: [StoryItemsTableEntry] = transaction.getStoryItems(peerId: peerId)
|
||||
|
||||
var updatedPeerEntries: [StoryItemsTableEntry] = []
|
||||
for story in stories {
|
||||
if let storedItem = Stories.StoredItem(apiStoryItem: story, peerId: peerId, transaction: transaction) {
|
||||
if case .placeholder = storedItem, let previousEntry = previousPeerEntries.first(where: { $0.id == storedItem.id }) {
|
||||
updatedPeerEntries.append(previousEntry)
|
||||
} else {
|
||||
if let codedEntry = CodableEntry(storedItem) {
|
||||
updatedPeerEntries.append(StoryItemsTableEntry(value: codedEntry, id: storedItem.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
peerEntries.append(peerId)
|
||||
|
||||
transaction.setStoryItems(peerId: peerId, items: updatedPeerEntries)
|
||||
transaction.setPeerStoryState(peerId: peerId, state: CodableEntry(Stories.PeerState(
|
||||
subscriptionsOpaqueState: state,
|
||||
maxReadId: maxReadId ?? 0
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
if !isRefresh {
|
||||
let leftPeerIds = currentPeerItems.filter({ !peerEntries.contains($0) })
|
||||
if !leftPeerIds.isEmpty {
|
||||
peerEntries = leftPeerIds + peerEntries
|
||||
}
|
||||
}
|
||||
|
||||
transaction.replaceAllStorySubscriptions(state: CodableEntry(Stories.SubscriptionsState(opaqueState: state, hasMore: hasMore)), peerIds: peerEntries)
|
||||
}
|
||||
}
|
||||
|> deliverOn(self.queue)).start(completed: { [weak self] in
|
||||
guard let `self` = self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.isLoading = false
|
||||
if isRefresh {
|
||||
self.taskState.isRefreshScheduled = false
|
||||
} else {
|
||||
self.taskState.isLoadMoreScheduled = false
|
||||
}
|
||||
|
||||
self.updateTasks()
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
private let queue = Queue(name: "StorySubscriptionsContext")
|
||||
private let impl: QueueLocalObject<Impl>
|
||||
|
||||
init(accountPeerId: PeerId, postbox: Postbox, network: Network) {
|
||||
let queue = self.queue
|
||||
self.impl = QueueLocalObject(queue: queue, generate: {
|
||||
Impl(queue: queue, accountPeerId: accountPeerId, postbox: postbox, network: network)
|
||||
})
|
||||
}
|
||||
|
||||
public func loadMore() {
|
||||
self.impl.with { impl in
|
||||
impl.loadMore()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class PeerStoryFocusContext {
|
||||
private final class Impl {
|
||||
private let queue: Queue
|
||||
private let account: Account
|
||||
private let peerId: PeerId
|
||||
private let focusItemId: Int32?
|
||||
|
||||
let state = Promise<State>()
|
||||
private var disposable: Disposable?
|
||||
|
||||
private var isLoadingPlaceholder: Bool = false
|
||||
private let loadDisposable = MetaDisposable()
|
||||
private var placeholderLoadWasUnsuccessful: Bool = false
|
||||
|
||||
init(
|
||||
queue: Queue,
|
||||
account: Account,
|
||||
peerId: PeerId,
|
||||
focusItemId: Int32?
|
||||
) {
|
||||
self.queue = queue
|
||||
self.account = account
|
||||
self.peerId = peerId
|
||||
self.focusItemId = focusItemId
|
||||
|
||||
self.disposable = (account.postbox.combinedView(keys: [
|
||||
PostboxViewKey.storiesState(key: .peer(peerId)),
|
||||
PostboxViewKey.storyItems(peerId: peerId)
|
||||
])
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] views in
|
||||
guard let `self` = self else {
|
||||
return
|
||||
}
|
||||
guard let peerStateView = views.views[PostboxViewKey.storiesState(key: .peer(peerId))] as? StoryStatesView else {
|
||||
return
|
||||
}
|
||||
guard let itemsView = views.views[PostboxViewKey.storyItems(peerId: peerId)] as? StoryItemsView else {
|
||||
return
|
||||
}
|
||||
|
||||
let peerState = peerStateView.value?.get(Stories.PeerState.self)
|
||||
|
||||
let items = itemsView.items.compactMap { $0.value.get(Stories.StoredItem.self) }
|
||||
var item: (value: Stories.StoredItem, position: Int, previousId: Int32?, nextId: Int32?)?
|
||||
|
||||
var focusItemId = self.focusItemId
|
||||
if focusItemId == nil {
|
||||
if let peerState = peerState {
|
||||
focusItemId = peerState.maxReadId + 1
|
||||
}
|
||||
}
|
||||
|
||||
if let focusItemId = focusItemId {
|
||||
if let index = items.firstIndex(where: { $0.id >= focusItemId }) {
|
||||
var previousId: Int32?
|
||||
var nextId: Int32?
|
||||
if index != 0 {
|
||||
previousId = items[index - 1].id
|
||||
}
|
||||
if index != items.count - 1 {
|
||||
nextId = items[index + 1].id
|
||||
}
|
||||
|
||||
item = (items[index], index, previousId, nextId)
|
||||
} else {
|
||||
if !items.isEmpty {
|
||||
var nextId: Int32?
|
||||
if 0 != items.count - 1 {
|
||||
nextId = items[0 + 1].id
|
||||
}
|
||||
item = (items[0], 0, nil, nextId)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !items.isEmpty {
|
||||
var nextId: Int32?
|
||||
if 0 != items.count - 1 {
|
||||
nextId = items[0 + 1].id
|
||||
}
|
||||
item = (items[0], 0, nil, nextId)
|
||||
}
|
||||
}
|
||||
|
||||
if let (item, position, previousId, nextId) = item {
|
||||
switch item {
|
||||
case let .item(item):
|
||||
self.state.set(.single(State(item: item, previousId: previousId, nextId: nextId, isLoading: false, count: items.count, position: position)))
|
||||
case .placeholder:
|
||||
let count = items.count
|
||||
|
||||
if !self.isLoadingPlaceholder {
|
||||
self.isLoadingPlaceholder = true
|
||||
self.loadDisposable.set((_internal_getStoriesById(
|
||||
accountPeerId: self.account.peerId,
|
||||
postbox: self.account.postbox,
|
||||
network: self.account.network,
|
||||
peerId: self.peerId,
|
||||
ids: [item.id]
|
||||
)
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let loadedItem = result.first, case let .item(item) = loadedItem {
|
||||
self.state.set(.single(State(item: item, previousId: previousId, nextId: nextId, isLoading: false, count: count, position: position)))
|
||||
} else {
|
||||
self.placeholderLoadWasUnsuccessful = false
|
||||
self.state.set(.single(State(item: nil, previousId: nil, nextId: nil, isLoading: !self.placeholderLoadWasUnsuccessful, count: count, position: position)))
|
||||
}
|
||||
}))
|
||||
}
|
||||
self.state.set(.single(State(item: nil, previousId: nil, nextId: nil, isLoading: !self.placeholderLoadWasUnsuccessful, count: count, position: position)))
|
||||
}
|
||||
} else {
|
||||
self.state.set(.single(State(item: nil, previousId: nil, nextId: nil, isLoading: false, count: 0, position: 0)))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable?.dispose()
|
||||
self.loadDisposable.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
public final class State: Equatable {
|
||||
public let item: Stories.Item?
|
||||
public let previousId: Int32?
|
||||
public let nextId: Int32?
|
||||
public let isLoading: Bool
|
||||
public let count: Int
|
||||
public let position: Int
|
||||
|
||||
public init(
|
||||
item: Stories.Item?,
|
||||
previousId: Int32?,
|
||||
nextId: Int32?,
|
||||
isLoading: Bool,
|
||||
count: Int,
|
||||
position: Int
|
||||
) {
|
||||
self.item = item
|
||||
self.previousId = previousId
|
||||
self.nextId = nextId
|
||||
self.isLoading = isLoading
|
||||
self.count = count
|
||||
self.position = position
|
||||
}
|
||||
|
||||
public static func ==(lhs: State, rhs: State) -> Bool {
|
||||
if lhs.item != rhs.item {
|
||||
return false
|
||||
}
|
||||
if lhs.previousId != rhs.previousId {
|
||||
return false
|
||||
}
|
||||
if lhs.nextId != rhs.nextId {
|
||||
return false
|
||||
}
|
||||
if lhs.isLoading != rhs.isLoading {
|
||||
return false
|
||||
}
|
||||
if lhs.count != rhs.count {
|
||||
return false
|
||||
}
|
||||
if lhs.position != rhs.position {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private static let sharedQueue = Queue(name: "PeerStoryFocusContext")
|
||||
private let queue: Queue
|
||||
private let impl: QueueLocalObject<Impl>
|
||||
|
||||
public var state: Signal<State, NoError> {
|
||||
return self.impl.signalWith { impl, subscriber in
|
||||
return impl.state.get().start(next: subscriber.putNext)
|
||||
}
|
||||
}
|
||||
|
||||
init(
|
||||
account: Account,
|
||||
peerId: PeerId,
|
||||
focusItemId: Int32?
|
||||
) {
|
||||
let queue = PeerStoryFocusContext.sharedQueue
|
||||
self.queue = queue
|
||||
self.impl = QueueLocalObject(queue: queue, generate: {
|
||||
return Impl(queue: queue, account: account, peerId: peerId, focusItemId: focusItemId)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public final class StoryListContext {
|
||||
public enum Scope {
|
||||
case all
|
||||
|
@ -7,6 +7,27 @@ public enum EngineOutgoingMessageContent {
|
||||
case text(String)
|
||||
}
|
||||
|
||||
public final class StoryPreloadInfo {
|
||||
public enum Priority: Comparable {
|
||||
case top(position: Int)
|
||||
case next(position: Int)
|
||||
}
|
||||
|
||||
public let resource: MediaResourceReference
|
||||
public let size: Int32?
|
||||
public let priority: Priority
|
||||
|
||||
public init(
|
||||
resource: MediaResourceReference,
|
||||
size: Int32?,
|
||||
priority: Priority
|
||||
) {
|
||||
self.resource = resource
|
||||
self.size = size
|
||||
self.priority = priority
|
||||
}
|
||||
}
|
||||
|
||||
public extension TelegramEngine {
|
||||
final class Messages {
|
||||
private let account: Account
|
||||
@ -569,8 +590,215 @@ public extension TelegramEngine {
|
||||
}).start()
|
||||
}
|
||||
|
||||
public func allStories() -> StoryListContext {
|
||||
return StoryListContext(account: self.account, scope: .all)
|
||||
public func storySubscriptions() -> Signal<EngineStorySubscriptions, NoError> {
|
||||
let basicPeerKey = PostboxViewKey.basicPeer(self.account.peerId)
|
||||
return self.account.postbox.combinedView(keys: [
|
||||
basicPeerKey,
|
||||
PostboxViewKey.storySubscriptions,
|
||||
PostboxViewKey.storiesState(key: .subscriptions)
|
||||
])
|
||||
|> mapToSignal { views -> Signal<EngineStorySubscriptions, NoError> in
|
||||
guard let basicPeerView = views.views[basicPeerKey] as? BasicPeerView, let accountPeer = basicPeerView.peer else {
|
||||
return .single(EngineStorySubscriptions(items: [], hasMoreToken: nil))
|
||||
}
|
||||
guard let storySubscriptionsView = views.views[PostboxViewKey.storySubscriptions] as? StorySubscriptionsView else {
|
||||
return .single(EngineStorySubscriptions(items: [], hasMoreToken: nil))
|
||||
}
|
||||
guard let storiesStateView = views.views[PostboxViewKey.storiesState(key: .subscriptions)] as? StoryStatesView else {
|
||||
return .single(EngineStorySubscriptions(items: [], hasMoreToken: nil))
|
||||
}
|
||||
|
||||
var additionalDataKeys: [PostboxViewKey] = []
|
||||
additionalDataKeys.append(contentsOf: storySubscriptionsView.peerIds.map { peerId -> PostboxViewKey in
|
||||
return PostboxViewKey.storyItems(peerId: peerId)
|
||||
})
|
||||
additionalDataKeys.append(contentsOf: storySubscriptionsView.peerIds.map { peerId -> PostboxViewKey in
|
||||
return PostboxViewKey.storiesState(key: .peer(peerId))
|
||||
})
|
||||
additionalDataKeys.append(contentsOf: storySubscriptionsView.peerIds.map { peerId -> PostboxViewKey in
|
||||
return PostboxViewKey.basicPeer(peerId)
|
||||
})
|
||||
|
||||
return self.account.postbox.combinedView(keys: additionalDataKeys)
|
||||
|> map { views -> EngineStorySubscriptions in
|
||||
let _ = accountPeer
|
||||
let _ = storySubscriptionsView
|
||||
let _ = storiesStateView
|
||||
|
||||
var hasMoreToken: String?
|
||||
if let subscriptionsState = storiesStateView.value?.get(Stories.SubscriptionsState.self) {
|
||||
if subscriptionsState.hasMore {
|
||||
hasMoreToken = subscriptionsState.opaqueState
|
||||
} else {
|
||||
hasMoreToken = nil
|
||||
}
|
||||
} else {
|
||||
hasMoreToken = ""
|
||||
}
|
||||
|
||||
var items: [EngineStorySubscriptions.Item] = []
|
||||
for peerId in storySubscriptionsView.peerIds {
|
||||
guard let peerView = views.views[PostboxViewKey.basicPeer(peerId)] as? BasicPeerView else {
|
||||
continue
|
||||
}
|
||||
guard let peer = peerView.peer else {
|
||||
continue
|
||||
}
|
||||
guard let itemsView = views.views[PostboxViewKey.storyItems(peerId: peerId)] as? StoryItemsView else {
|
||||
continue
|
||||
}
|
||||
guard let stateView = views.views[PostboxViewKey.storiesState(key: .peer(peerId))] as? StoryStatesView else {
|
||||
continue
|
||||
}
|
||||
guard let lastEntry = itemsView.items.last?.value.get(Stories.StoredItem.self) else {
|
||||
continue
|
||||
}
|
||||
|
||||
let peerState: Stories.PeerState? = stateView.value?.get(Stories.PeerState.self)
|
||||
var hasUnseen = false
|
||||
if let peerState = peerState {
|
||||
hasUnseen = peerState.maxReadId < lastEntry.id
|
||||
}
|
||||
|
||||
items.append(EngineStorySubscriptions.Item(
|
||||
peer: EnginePeer(peer),
|
||||
hasUnseen: hasUnseen,
|
||||
storyCount: itemsView.items.count,
|
||||
lastTimestamp: lastEntry.timestamp
|
||||
))
|
||||
}
|
||||
|
||||
items.sort(by: { lhs, rhs in
|
||||
return lhs.lastTimestamp > rhs.lastTimestamp
|
||||
})
|
||||
|
||||
return EngineStorySubscriptions(items: items, hasMoreToken: hasMoreToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func preloadStorySubscriptions() -> Signal<[EngineMediaResource.Id: StoryPreloadInfo], NoError> {
|
||||
let basicPeerKey = PostboxViewKey.basicPeer(self.account.peerId)
|
||||
return self.account.postbox.combinedView(keys: [
|
||||
basicPeerKey,
|
||||
PostboxViewKey.storySubscriptions,
|
||||
PostboxViewKey.storiesState(key: .subscriptions)
|
||||
])
|
||||
|> mapToSignal { views -> Signal<[EngineMediaResource.Id: StoryPreloadInfo], NoError> in
|
||||
guard let basicPeerView = views.views[basicPeerKey] as? BasicPeerView, let accountPeer = basicPeerView.peer else {
|
||||
return .single([:])
|
||||
}
|
||||
guard let storySubscriptionsView = views.views[PostboxViewKey.storySubscriptions] as? StorySubscriptionsView else {
|
||||
return .single([:])
|
||||
}
|
||||
guard let storiesStateView = views.views[PostboxViewKey.storiesState(key: .subscriptions)] as? StoryStatesView else {
|
||||
return .single([:])
|
||||
}
|
||||
|
||||
var additionalDataKeys: [PostboxViewKey] = []
|
||||
additionalDataKeys.append(contentsOf: storySubscriptionsView.peerIds.map { peerId -> PostboxViewKey in
|
||||
return PostboxViewKey.storyItems(peerId: peerId)
|
||||
})
|
||||
additionalDataKeys.append(contentsOf: storySubscriptionsView.peerIds.map { peerId -> PostboxViewKey in
|
||||
return PostboxViewKey.storiesState(key: .peer(peerId))
|
||||
})
|
||||
additionalDataKeys.append(contentsOf: storySubscriptionsView.peerIds.map { peerId -> PostboxViewKey in
|
||||
return PostboxViewKey.basicPeer(peerId)
|
||||
})
|
||||
|
||||
return self.account.postbox.combinedView(keys: additionalDataKeys)
|
||||
|> map { views -> [EngineMediaResource.Id: StoryPreloadInfo] in
|
||||
let _ = accountPeer
|
||||
let _ = storySubscriptionsView
|
||||
let _ = storiesStateView
|
||||
|
||||
var nextPriority: Int = 0
|
||||
|
||||
var resultResources: [EngineMediaResource.Id: StoryPreloadInfo] = [:]
|
||||
|
||||
for peerId in storySubscriptionsView.peerIds {
|
||||
guard let peerView = views.views[PostboxViewKey.basicPeer(peerId)] as? BasicPeerView else {
|
||||
continue
|
||||
}
|
||||
guard let peer = peerView.peer else {
|
||||
continue
|
||||
}
|
||||
guard let itemsView = views.views[PostboxViewKey.storyItems(peerId: peerId)] as? StoryItemsView else {
|
||||
continue
|
||||
}
|
||||
guard let stateView = views.views[PostboxViewKey.storiesState(key: .peer(peerId))] as? StoryStatesView else {
|
||||
continue
|
||||
}
|
||||
|
||||
var nextItem: Stories.StoredItem? = itemsView.items.first?.value.get(Stories.StoredItem.self)
|
||||
|
||||
let peerState: Stories.PeerState? = stateView.value?.get(Stories.PeerState.self)
|
||||
if let peerState = peerState {
|
||||
if let item = itemsView.items.first(where: { $0.id >= peerState.maxReadId }) {
|
||||
nextItem = item.value.get(Stories.StoredItem.self)
|
||||
}
|
||||
}
|
||||
|
||||
if let peerReference = PeerReference(peer) {
|
||||
if let nextItem = nextItem, case let .item(item) = nextItem, let media = item.media {
|
||||
if let image = media as? TelegramMediaImage, let resource = image.representations.last?.resource {
|
||||
let resource = MediaResourceReference.media(media: .story(peer: peerReference, id: item.id, media: media), resource: resource)
|
||||
resultResources[EngineMediaResource.Id(resource.resource.id)] = StoryPreloadInfo(
|
||||
resource: resource,
|
||||
size: nil,
|
||||
priority: .top(position: nextPriority)
|
||||
)
|
||||
nextPriority += 1
|
||||
} else if let file = media as? TelegramMediaFile {
|
||||
if let preview = file.previewRepresentations.last {
|
||||
let resource = MediaResourceReference.media(media: .story(peer: peerReference, id: item.id, media: file), resource: preview.resource)
|
||||
resultResources[EngineMediaResource.Id(resource.resource.id)] = StoryPreloadInfo(
|
||||
resource: resource,
|
||||
size: nil,
|
||||
priority: .top(position: nextPriority)
|
||||
)
|
||||
nextPriority += 1
|
||||
}
|
||||
|
||||
let resource = MediaResourceReference.media(media: .story(peer: peerReference, id: item.id, media: file), resource: file.resource)
|
||||
resultResources[EngineMediaResource.Id(resource.resource.id)] = StoryPreloadInfo(
|
||||
resource: resource,
|
||||
size: nil,
|
||||
priority: .top(position: nextPriority)
|
||||
)
|
||||
nextPriority += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resultResources
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func peerStoryFocusContext(id: EnginePeer.Id, focusItemId: Int32?) -> PeerStoryFocusContext {
|
||||
return PeerStoryFocusContext(account: self.account, peerId: id, focusItemId: focusItemId)
|
||||
}
|
||||
|
||||
public func refreshStories(peerId: EnginePeer.Id, ids: [Int32]) -> Signal<Never, NoError> {
|
||||
return _internal_getStoriesById(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, peerId: peerId, ids: ids)
|
||||
|> mapToSignal { result -> Signal<Never, NoError> in
|
||||
return self.account.postbox.transaction { transaction -> Void in
|
||||
var currentItems = transaction.getStoryItems(peerId: peerId)
|
||||
for i in 0 ..< currentItems.count {
|
||||
if let updatedItem = result.first(where: { $0.id == currentItems[i].id }) {
|
||||
if case .item = updatedItem {
|
||||
if let entry = CodableEntry(updatedItem) {
|
||||
currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
transaction.setStoryItems(peerId: peerId, items: currentItems)
|
||||
}
|
||||
|> ignoreValues
|
||||
}
|
||||
}
|
||||
|
||||
public func peerStories(id: EnginePeer.Id) -> StoryListContext {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -83,6 +83,8 @@ public final class StoryContentItemSlice {
|
||||
public let focusedItemId: AnyHashable?
|
||||
public let items: [StoryContentItem]
|
||||
public let totalCount: Int
|
||||
public let previousItemId: AnyHashable?
|
||||
public let nextItemId: AnyHashable?
|
||||
public let update: (StoryContentItemSlice, AnyHashable) -> Signal<StoryContentItemSlice, NoError>
|
||||
|
||||
public init(
|
||||
@ -90,12 +92,16 @@ public final class StoryContentItemSlice {
|
||||
focusedItemId: AnyHashable?,
|
||||
items: [StoryContentItem],
|
||||
totalCount: Int,
|
||||
previousItemId: AnyHashable?,
|
||||
nextItemId: AnyHashable?,
|
||||
update: @escaping (StoryContentItemSlice, AnyHashable) -> Signal<StoryContentItemSlice, NoError>
|
||||
) {
|
||||
self.id = id
|
||||
self.focusedItemId = focusedItemId
|
||||
self.items = items
|
||||
self.totalCount = totalCount
|
||||
self.previousItemId = previousItemId
|
||||
self.nextItemId = nextItemId
|
||||
self.update = update
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -17,6 +17,7 @@ swift_library(
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/PhotoResources",
|
||||
"//submodules/MediaPlayer:UniversalMediaPlayer",
|
||||
"//submodules/TelegramUniversalVideoContent",
|
||||
|
@ -5,9 +5,518 @@ import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import AccountContext
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import StoryContainerScreen
|
||||
|
||||
public final class StoryContentContextState {
|
||||
public final class FocusedSlice {
|
||||
public let peer: EnginePeer
|
||||
public let item: StoryContentItem
|
||||
public let totalCount: Int
|
||||
public let previousItemId: Int32?
|
||||
public let nextItemId: Int32?
|
||||
|
||||
public init(
|
||||
peer: EnginePeer,
|
||||
item: StoryContentItem,
|
||||
totalCount: Int,
|
||||
previousItemId: Int32?,
|
||||
nextItemId: Int32?
|
||||
) {
|
||||
self.peer = peer
|
||||
self.item = item
|
||||
self.totalCount = totalCount
|
||||
self.previousItemId = previousItemId
|
||||
self.nextItemId = nextItemId
|
||||
}
|
||||
}
|
||||
|
||||
public let slice: FocusedSlice?
|
||||
public let previousSlice: FocusedSlice?
|
||||
public let nextSlice: FocusedSlice?
|
||||
|
||||
public init(
|
||||
slice: FocusedSlice?,
|
||||
previousSlice: FocusedSlice?,
|
||||
nextSlice: FocusedSlice?
|
||||
) {
|
||||
self.slice = slice
|
||||
self.previousSlice = previousSlice
|
||||
self.nextSlice = nextSlice
|
||||
}
|
||||
}
|
||||
|
||||
public enum StoryContentContextNavigation {
|
||||
public enum Direction {
|
||||
case previous
|
||||
case next
|
||||
}
|
||||
|
||||
case item(Direction)
|
||||
case peer(Direction)
|
||||
}
|
||||
|
||||
public protocol StoryContentContext {
|
||||
var stateValue: StoryContentContextState? { get }
|
||||
var state: Signal<StoryContentContextState, NoError> { get }
|
||||
|
||||
func navigate(navigation: StoryContentContextNavigation)
|
||||
}
|
||||
|
||||
public final class StoryContentContextImpl: StoryContentContext {
|
||||
private struct StoryKey: Hashable {
|
||||
var peerId: EnginePeer.Id
|
||||
var id: Int32
|
||||
}
|
||||
|
||||
private final class PeerContext {
|
||||
private let context: AccountContext
|
||||
private let peerId: EnginePeer.Id
|
||||
|
||||
private(set) var sliceValue: StoryContentContextState.FocusedSlice?
|
||||
|
||||
let updated = Promise<Void>()
|
||||
|
||||
var isReady: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
private var disposable: Disposable?
|
||||
private var loadDisposable: Disposable?
|
||||
|
||||
init(context: AccountContext, peerId: EnginePeer.Id, focusedId: Int32?, loadIds: @escaping ([StoryKey]) -> Void) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
|
||||
self.disposable = (context.account.postbox.combinedView(
|
||||
keys: [
|
||||
PostboxViewKey.basicPeer(peerId),
|
||||
PostboxViewKey.storiesState(key: .peer(peerId)),
|
||||
PostboxViewKey.storyItems(peerId: peerId)
|
||||
]
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] views in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
guard let peerView = views.views[PostboxViewKey.basicPeer(peerId)] as? BasicPeerView else {
|
||||
return
|
||||
}
|
||||
guard let stateView = views.views[PostboxViewKey.storiesState(key: .peer(peerId))] as? StoryStatesView else {
|
||||
return
|
||||
}
|
||||
guard let itemsView = views.views[PostboxViewKey.storyItems(peerId: peerId)] as? StoryItemsView else {
|
||||
return
|
||||
}
|
||||
guard let peer = peerView.peer.flatMap(EnginePeer.init) else {
|
||||
return
|
||||
}
|
||||
let state = stateView.value?.get(Stories.PeerState.self)
|
||||
|
||||
var focusedIndex: Int?
|
||||
if let focusedId {
|
||||
focusedIndex = itemsView.items.firstIndex(where: { $0.id == focusedId })
|
||||
}
|
||||
if focusedIndex == nil, let state {
|
||||
focusedIndex = itemsView.items.firstIndex(where: { $0.id >= state.maxReadId })
|
||||
}
|
||||
if focusedIndex == nil {
|
||||
if !itemsView.items.isEmpty {
|
||||
focusedIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
if let focusedIndex {
|
||||
var previousItemId: Int32?
|
||||
var nextItemId: Int32?
|
||||
|
||||
if focusedIndex != 0 {
|
||||
previousItemId = itemsView.items[focusedIndex - 1].id
|
||||
}
|
||||
if focusedIndex != itemsView.items.count - 1 {
|
||||
nextItemId = itemsView.items[focusedIndex + 1].id
|
||||
}
|
||||
|
||||
var loadKeys: [StoryKey] = []
|
||||
for index in (focusedIndex - 2) ... (focusedIndex + 2) {
|
||||
if index >= 0 && index < itemsView.items.count {
|
||||
if let item = itemsView.items[focusedIndex].value.get(Stories.StoredItem.self), case .placeholder = item {
|
||||
loadKeys.append(StoryKey(peerId: peerId, id: item.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let item = itemsView.items[focusedIndex].value.get(Stories.StoredItem.self), case let .item(item) = item, let media = item.media {
|
||||
let mappedItem = StoryListContext.Item(
|
||||
id: item.id,
|
||||
timestamp: item.timestamp,
|
||||
media: EngineMedia(media),
|
||||
text: item.text,
|
||||
entities: item.entities,
|
||||
views: nil,
|
||||
privacy: nil
|
||||
)
|
||||
|
||||
self.sliceValue = StoryContentContextState.FocusedSlice(
|
||||
peer: peer,
|
||||
item: StoryContentItem(
|
||||
id: AnyHashable(item.id),
|
||||
position: focusedIndex,
|
||||
component: AnyComponent(StoryItemContentComponent(
|
||||
context: context,
|
||||
peer: peer,
|
||||
item: mappedItem
|
||||
)),
|
||||
centerInfoComponent: AnyComponent(StoryAuthorInfoComponent(
|
||||
context: context,
|
||||
peer: peer,
|
||||
timestamp: item.timestamp
|
||||
)),
|
||||
rightInfoComponent: AnyComponent(StoryAvatarInfoComponent(
|
||||
context: context,
|
||||
peer: peer
|
||||
)),
|
||||
peerId: peer.id,
|
||||
storyItem: mappedItem,
|
||||
preload: nil,
|
||||
delete: { [weak context] in
|
||||
guard let context else {
|
||||
return
|
||||
}
|
||||
let _ = context
|
||||
},
|
||||
markAsSeen: { [weak context] in
|
||||
guard let context else {
|
||||
return
|
||||
}
|
||||
let _ = context.engine.messages.markStoryAsSeen(peerId: peerId, id: item.id).start()
|
||||
},
|
||||
hasLike: false,
|
||||
isMy: peerId == context.account.peerId
|
||||
),
|
||||
totalCount: itemsView.items.count,
|
||||
previousItemId: previousItemId,
|
||||
nextItemId: nextItemId
|
||||
)
|
||||
self.updated.set(.single(Void()))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable?.dispose()
|
||||
self.loadDisposable?.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
private final class StateContext {
|
||||
let centralPeerContext: PeerContext
|
||||
let previousPeerContext: PeerContext?
|
||||
let nextPeerContext: PeerContext?
|
||||
|
||||
let updated = Promise<Void>()
|
||||
|
||||
var isReady: Bool {
|
||||
if !self.centralPeerContext.isReady {
|
||||
return false
|
||||
}
|
||||
if let previousPeerContext = self.previousPeerContext, !previousPeerContext.isReady {
|
||||
return false
|
||||
}
|
||||
if let nextPeerContext = self.nextPeerContext, !nextPeerContext.isReady {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private var centralDisposable: Disposable?
|
||||
private var previousDisposable: Disposable?
|
||||
private var nextDisposable: Disposable?
|
||||
|
||||
init(
|
||||
centralPeerContext: PeerContext,
|
||||
previousPeerContext: PeerContext?,
|
||||
nextPeerContext: PeerContext?
|
||||
) {
|
||||
self.centralPeerContext = centralPeerContext
|
||||
self.previousPeerContext = previousPeerContext
|
||||
self.nextPeerContext = nextPeerContext
|
||||
|
||||
self.centralDisposable = (centralPeerContext.updated.get()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.updated.set(.single(Void()))
|
||||
})
|
||||
|
||||
if let previousPeerContext {
|
||||
self.previousDisposable = (previousPeerContext.updated.get()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.updated.set(.single(Void()))
|
||||
})
|
||||
}
|
||||
|
||||
if let nextPeerContext {
|
||||
self.nextDisposable = (nextPeerContext.updated.get()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.updated.set(.single(Void()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.centralDisposable?.dispose()
|
||||
self.previousDisposable?.dispose()
|
||||
self.nextDisposable?.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
|
||||
public private(set) var stateValue: StoryContentContextState?
|
||||
public var state: Signal<StoryContentContextState, NoError> {
|
||||
return self.statePromise.get()
|
||||
}
|
||||
private let statePromise = Promise<StoryContentContextState>()
|
||||
|
||||
private var focusedItem: (peerId: EnginePeer.Id, storyId: Int32?)?
|
||||
|
||||
private var currentState: StateContext?
|
||||
private var currentStateUpdatedDisposable: Disposable?
|
||||
|
||||
private var pendingState: StateContext?
|
||||
private var pendingStateReadyDisposable: Disposable?
|
||||
|
||||
private var storySubscriptions: EngineStorySubscriptions?
|
||||
private var storySubscriptionsDisposable: Disposable?
|
||||
|
||||
private var requestedStoryKeys = Set<StoryKey>()
|
||||
private var requestStoryDisposables = DisposableSet()
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
focusedPeerId: EnginePeer.Id?
|
||||
) {
|
||||
self.context = context
|
||||
if let focusedPeerId {
|
||||
self.focusedItem = (focusedPeerId, nil)
|
||||
}
|
||||
|
||||
self.storySubscriptionsDisposable = (context.engine.messages.storySubscriptions()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] storySubscriptions in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.storySubscriptions = storySubscriptions
|
||||
self.updatePeerContexts()
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.storySubscriptionsDisposable?.dispose()
|
||||
self.requestStoryDisposables.dispose()
|
||||
}
|
||||
|
||||
private func updatePeerContexts() {
|
||||
if let currentState = self.currentState {
|
||||
let _ = currentState
|
||||
} else {
|
||||
self.switchToFocusedPeerId()
|
||||
}
|
||||
}
|
||||
|
||||
private func switchToFocusedPeerId() {
|
||||
if let storySubscriptions = self.storySubscriptions {
|
||||
if self.pendingState == nil {
|
||||
var centralIndex: Int?
|
||||
if let (focusedPeerId, _) = self.focusedItem {
|
||||
if let index = storySubscriptions.items.firstIndex(where: { $0.peer.id == focusedPeerId }) {
|
||||
centralIndex = index
|
||||
}
|
||||
}
|
||||
if centralIndex == nil {
|
||||
if !storySubscriptions.items.isEmpty {
|
||||
centralIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
if let centralIndex {
|
||||
let loadIds: ([StoryKey]) -> Void = { [weak self] keys in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let missingKeys = Set(keys).subtracting(self.requestedStoryKeys)
|
||||
if !missingKeys.isEmpty {
|
||||
var idsByPeerId: [EnginePeer.Id: [Int32]] = [:]
|
||||
for key in missingKeys {
|
||||
if idsByPeerId[key.peerId] == nil {
|
||||
idsByPeerId[key.peerId] = [key.id]
|
||||
} else {
|
||||
idsByPeerId[key.peerId]?.append(key.id)
|
||||
}
|
||||
}
|
||||
for (peerId, ids) in idsByPeerId {
|
||||
self.requestStoryDisposables.add(self.context.engine.messages.refreshStories(peerId: peerId, ids: ids).start())
|
||||
}
|
||||
}
|
||||
}
|
||||
let pendingState = StateContext(
|
||||
centralPeerContext: PeerContext(context: self.context, peerId: storySubscriptions.items[centralIndex].peer.id, focusedId: nil, loadIds: loadIds),
|
||||
previousPeerContext: centralIndex == 0 ? nil : PeerContext(context: self.context, peerId: storySubscriptions.items[centralIndex - 1].peer.id, focusedId: nil, loadIds: loadIds),
|
||||
nextPeerContext: (centralIndex == storySubscriptions.items.count - 1) ? nil : PeerContext(context: self.context, peerId: storySubscriptions.items[centralIndex + 1].peer.id, focusedId: nil, loadIds: loadIds)
|
||||
)
|
||||
self.pendingState = pendingState
|
||||
self.pendingStateReadyDisposable = (pendingState.updated.get()
|
||||
|> deliverOnMainQueue).start(next: { [weak self, weak pendingState] _ in
|
||||
guard let self, let pendingState, self.pendingState === pendingState, pendingState.isReady else {
|
||||
return
|
||||
}
|
||||
self.pendingState = nil
|
||||
self.pendingStateReadyDisposable?.dispose()
|
||||
self.pendingStateReadyDisposable = nil
|
||||
|
||||
self.currentState = pendingState
|
||||
|
||||
self.updateState()
|
||||
|
||||
self.currentStateUpdatedDisposable?.dispose()
|
||||
self.currentStateUpdatedDisposable = (pendingState.updated.get()
|
||||
|> deliverOnMainQueue).start(next: { [weak self, weak pendingState] _ in
|
||||
guard let self, let pendingState, self.currentState === pendingState else {
|
||||
return
|
||||
}
|
||||
self.updateState()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateState() {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
public func navigate(navigation: StoryContentContextNavigation) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public enum StoryChatContent {
|
||||
public static func peerStories(context: AccountContext, peerId: EnginePeer.Id, focusItem: Int32?) -> Signal<[StoryContentItemSlice], NoError> {
|
||||
let focusContext = context.engine.messages.peerStoryFocusContext(id: peerId, focusItemId: focusItem)
|
||||
return combineLatest(
|
||||
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)),
|
||||
focusContext.state
|
||||
)
|
||||
|> mapToSignal { peer, state -> Signal<[StoryContentItemSlice], NoError> in
|
||||
let _ = focusContext
|
||||
|
||||
if let peer, let item = state.item, let media = item.media {
|
||||
let mappedItem = StoryListContext.Item(
|
||||
id: item.id,
|
||||
timestamp: item.timestamp,
|
||||
media: EngineMedia(media),
|
||||
text: item.text,
|
||||
entities: item.entities,
|
||||
views: nil,
|
||||
privacy: nil
|
||||
)
|
||||
|
||||
let slice = StoryContentItemSlice(
|
||||
id: AnyHashable(peerId),
|
||||
focusedItemId: AnyHashable(item.id),
|
||||
items: [
|
||||
StoryContentItem(
|
||||
id: AnyHashable(item.id),
|
||||
position: state.position,
|
||||
component: AnyComponent(StoryItemContentComponent(
|
||||
context: context,
|
||||
peer: peer,
|
||||
item: mappedItem
|
||||
)),
|
||||
centerInfoComponent: AnyComponent(StoryAuthorInfoComponent(
|
||||
context: context,
|
||||
peer: peer,
|
||||
timestamp: item.timestamp
|
||||
)),
|
||||
rightInfoComponent: AnyComponent(StoryAvatarInfoComponent(
|
||||
context: context,
|
||||
peer: peer
|
||||
)),
|
||||
peerId: peer.id,
|
||||
storyItem: mappedItem,
|
||||
preload: nil,
|
||||
delete: { [weak context] in
|
||||
guard let context else {
|
||||
return
|
||||
}
|
||||
let _ = context
|
||||
},
|
||||
markAsSeen: { [weak context] in
|
||||
guard let context else {
|
||||
return
|
||||
}
|
||||
let _ = context.engine.messages.markStoryAsSeen(peerId: peerId, id: item.id).start()
|
||||
},
|
||||
hasLike: false,
|
||||
isMy: peerId == context.account.peerId
|
||||
)
|
||||
],
|
||||
totalCount: state.count,
|
||||
previousItemId: state.previousId,
|
||||
nextItemId: state.nextId,
|
||||
update: { requestedItemSet, itemId in
|
||||
var focusItem: Int32?
|
||||
if let id = itemId.base as? Int32 {
|
||||
focusItem = id
|
||||
}
|
||||
|
||||
return StoryChatContent.peerStories(context: context, peerId: peerId, focusItem: focusItem)
|
||||
|> mapToSignal { result in
|
||||
if let first = result.first {
|
||||
return .single(first)
|
||||
} else {
|
||||
return .never()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
return .single([slice])
|
||||
} else {
|
||||
return .single([])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func subscriptionsStories(context: AccountContext, peerId: EnginePeer.Id?) -> Signal<[StoryContentItemSlice], NoError> {
|
||||
return context.engine.messages.storySubscriptions()
|
||||
|> mapToSignal { subscriptions -> Signal<[StoryContentItemSlice], NoError> in
|
||||
var signals: [Signal<[StoryContentItemSlice], NoError>] = []
|
||||
for item in subscriptions.items {
|
||||
signals.append(peerStories(context: context, peerId: item.peer.id, focusItem: nil))
|
||||
}
|
||||
return combineLatest(queue: .mainQueue(), signals)
|
||||
|> map { peerItems -> [StoryContentItemSlice] in
|
||||
var result: [StoryContentItemSlice] = []
|
||||
|
||||
for item in peerItems {
|
||||
result.append(contentsOf: item)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func stories(context: AccountContext, storyList: StoryListContext, focusItem: Int32?) -> Signal<[StoryContentItemSlice], NoError> {
|
||||
return storyList.state
|
||||
|> map { state -> [StoryContentItemSlice] in
|
||||
@ -72,6 +581,8 @@ public enum StoryChatContent {
|
||||
focusedItemId: sliceFocusedItemId,
|
||||
items: items,
|
||||
totalCount: items.count,
|
||||
previousItemId: nil,
|
||||
nextItemId: nil,
|
||||
update: { requestedItemSet, itemId in
|
||||
var focusItem: Int32?
|
||||
if let id = itemId.base as? Int32 {
|
||||
|
@ -13,7 +13,7 @@ public final class StoryPeerListComponent: Component {
|
||||
public let context: AccountContext
|
||||
public let theme: PresentationTheme
|
||||
public let strings: PresentationStrings
|
||||
public let state: StoryListContext.State?
|
||||
public let storySubscriptions: EngineStorySubscriptions?
|
||||
public let collapseFraction: CGFloat
|
||||
public let peerAction: (EnginePeer?) -> Void
|
||||
|
||||
@ -21,14 +21,14 @@ public final class StoryPeerListComponent: Component {
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
state: StoryListContext.State?,
|
||||
storySubscriptions: EngineStorySubscriptions?,
|
||||
collapseFraction: CGFloat,
|
||||
peerAction: @escaping (EnginePeer?) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.state = state
|
||||
self.storySubscriptions = storySubscriptions
|
||||
self.collapseFraction = collapseFraction
|
||||
self.peerAction = peerAction
|
||||
}
|
||||
@ -43,7 +43,7 @@ public final class StoryPeerListComponent: Component {
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.state != rhs.state {
|
||||
if lhs.storySubscriptions != rhs.storySubscriptions {
|
||||
return false
|
||||
}
|
||||
if lhs.collapseFraction != rhs.collapseFraction {
|
||||
@ -102,7 +102,7 @@ public final class StoryPeerListComponent: Component {
|
||||
private var ignoreScrolling: Bool = false
|
||||
private var itemLayout: ItemLayout?
|
||||
|
||||
private var sortedItemSets: [StoryListContext.PeerItemSet] = []
|
||||
private var sortedItems: [EngineStorySubscriptions.Item] = []
|
||||
private var visibleItems: [EnginePeer.Id: VisibleItem] = [:]
|
||||
|
||||
private let title = ComponentView<Empty>()
|
||||
@ -110,6 +110,9 @@ public final class StoryPeerListComponent: Component {
|
||||
private var component: StoryPeerListComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
private var requestedLoadMoreToken: String?
|
||||
private let loadMoreDisposable = MetaDisposable()
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
self.collapsedButton = HighlightableButton()
|
||||
|
||||
@ -153,6 +156,10 @@ public final class StoryPeerListComponent: Component {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.loadMoreDisposable.dispose()
|
||||
}
|
||||
|
||||
@objc private func collapsedButtonPressed() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
@ -182,14 +189,14 @@ public final class StoryPeerListComponent: Component {
|
||||
}
|
||||
|
||||
var hasStories: Bool = false
|
||||
if let state = component.state, state.itemSets.count > 1 {
|
||||
if let storySubscriptions = component.storySubscriptions, !storySubscriptions.items.isEmpty {
|
||||
hasStories = true
|
||||
}
|
||||
|
||||
let titleSpacing: CGFloat = 8.0
|
||||
|
||||
let titleText: String
|
||||
let storyCount = self.sortedItemSets.count - 1
|
||||
let storyCount = self.sortedItems.count
|
||||
if storyCount <= 0 {
|
||||
titleText = "No Stories"
|
||||
} else {
|
||||
@ -211,7 +218,7 @@ public final class StoryPeerListComponent: Component {
|
||||
|
||||
let collapsedItemWidth: CGFloat = 24.0
|
||||
let collapsedItemDistance: CGFloat = 14.0
|
||||
let collapsedItemCount: CGFloat = CGFloat(min(self.sortedItemSets.count - collapseStartIndex, 3))
|
||||
let collapsedItemCount: CGFloat = CGFloat(min(self.sortedItems.count - collapseStartIndex, 3))
|
||||
var collapsedContentWidth: CGFloat = 0.0
|
||||
if collapsedItemCount > 0 {
|
||||
collapsedContentWidth = 1.0 * collapsedItemWidth + (collapsedItemDistance) * max(0.0, collapsedItemCount - 1.0)
|
||||
@ -257,11 +264,9 @@ public final class StoryPeerListComponent: Component {
|
||||
let visibleBounds = self.scrollView.bounds
|
||||
|
||||
var validIds: [EnginePeer.Id] = []
|
||||
for i in 0 ..< self.sortedItemSets.count {
|
||||
let itemSet = self.sortedItemSets[i]
|
||||
guard let peer = itemSet.peer else {
|
||||
continue
|
||||
}
|
||||
for i in 0 ..< self.sortedItems.count {
|
||||
let itemSet = self.sortedItems[i]
|
||||
let peer = itemSet.peer
|
||||
|
||||
let regularItemFrame = itemLayout.frame(at: i)
|
||||
if !visibleBounds.intersects(regularItemFrame) {
|
||||
@ -271,32 +276,29 @@ public final class StoryPeerListComponent: Component {
|
||||
//}
|
||||
}
|
||||
|
||||
validIds.append(itemSet.peerId)
|
||||
validIds.append(itemSet.peer.id)
|
||||
|
||||
let visibleItem: VisibleItem
|
||||
var itemTransition = transition
|
||||
if let current = self.visibleItems[itemSet.peerId] {
|
||||
if let current = self.visibleItems[itemSet.peer.id] {
|
||||
visibleItem = current
|
||||
} else {
|
||||
itemTransition = .immediate
|
||||
visibleItem = VisibleItem()
|
||||
self.visibleItems[itemSet.peerId] = visibleItem
|
||||
self.visibleItems[itemSet.peer.id] = visibleItem
|
||||
}
|
||||
|
||||
var hasUnseen = false
|
||||
let hasItems = !itemSet.items.isEmpty
|
||||
hasUnseen = itemSet.hasUnseen
|
||||
|
||||
let hasItems = true
|
||||
var itemProgress: CGFloat?
|
||||
if peer.id == component.context.account.peerId {
|
||||
itemProgress = component.state?.uploadProgress
|
||||
itemProgress = nil
|
||||
//itemProgress = component.state?.uploadProgress
|
||||
//itemProgress = 0.0
|
||||
}
|
||||
|
||||
for item in itemSet.items {
|
||||
if item.id > itemSet.maxReadId {
|
||||
hasUnseen = true
|
||||
}
|
||||
}
|
||||
|
||||
let collapsedItemFrame = CGRect(origin: CGPoint(x: collapsedContentOrigin + CGFloat(i - collapseStartIndex) * collapsedItemDistance, y: regularItemFrame.minY + collapsedItemOffsetY), size: CGSize(width: collapsedItemWidth, height: regularItemFrame.height))
|
||||
|
||||
let itemFrame = regularItemFrame.interpolate(to: collapsedItemFrame, amount: component.collapseFraction)
|
||||
@ -406,24 +408,27 @@ public final class StoryPeerListComponent: Component {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
if let storySubscriptions = component.storySubscriptions, let hasMoreToken = storySubscriptions.hasMoreToken {
|
||||
if self.requestedLoadMoreToken != hasMoreToken {
|
||||
self.requestedLoadMoreToken = hasMoreToken
|
||||
if let storySubscriptionsContext = component.context.account.storySubscriptionsContext {
|
||||
storySubscriptionsContext.loadMore()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.collapsedButton.isUserInteractionEnabled = component.collapseFraction >= 1.0 - .ulpOfOne
|
||||
|
||||
self.sortedItemSets.removeAll(keepingCapacity: true)
|
||||
if let state = component.state {
|
||||
if let myIndex = state.itemSets.firstIndex(where: { $0.peerId == component.context.account.peerId }) {
|
||||
self.sortedItemSets.append(state.itemSets[myIndex])
|
||||
self.sortedItems.removeAll(keepingCapacity: true)
|
||||
if let storySubscriptions = component.storySubscriptions {
|
||||
if let myIndex = storySubscriptions.items.firstIndex(where: { $0.peer.id == component.context.account.peerId }) {
|
||||
self.sortedItems.append(storySubscriptions.items[myIndex])
|
||||
}
|
||||
for i in 0 ..< 1 {
|
||||
for itemSet in state.itemSets {
|
||||
if itemSet.peerId == component.context.account.peerId {
|
||||
continue
|
||||
}
|
||||
if i == 0 {
|
||||
self.sortedItemSets.append(itemSet)
|
||||
} else {
|
||||
self.sortedItemSets.append(StoryListContext.PeerItemSet(peerId: EnginePeer.Id(namespace: itemSet.peerId.namespace, id: EnginePeer.Id.Id._internalFromInt64Value(itemSet.peerId.id._internalGetInt64Value() + Int64(i))), peer: itemSet.peer, maxReadId: itemSet.maxReadId, items: itemSet.items, totalCount: itemSet.totalCount))
|
||||
}
|
||||
for itemSet in storySubscriptions.items {
|
||||
if itemSet.peer.id == component.context.account.peerId {
|
||||
continue
|
||||
}
|
||||
self.sortedItems.append(itemSet)
|
||||
}
|
||||
}
|
||||
|
||||
@ -432,7 +437,7 @@ public final class StoryPeerListComponent: Component {
|
||||
containerInsets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 10.0),
|
||||
itemSize: CGSize(width: 60.0, height: 77.0),
|
||||
itemSpacing: 24.0,
|
||||
itemCount: self.sortedItemSets.count
|
||||
itemCount: self.sortedItems.count
|
||||
)
|
||||
self.itemLayout = itemLayout
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
})
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user