From 923587b0da791897fae7a0aa8db75df2f5caf9d1 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Wed, 18 Sep 2024 01:04:29 +0800 Subject: [PATCH] [WIP] Dynamic video streaming --- .../Sources/NotificationService.swift | 2 +- .../Sources/FetchMediaUtils.swift | 4 +- .../Sources/IsMediaStreamable.swift | 4 +- .../Sources/PresentationCallManager.swift | 1 + .../Sources/UniversalVideoNode.swift | 25 + .../Sources/AvatarVideoNode.swift | 2 +- .../Sources/ChatImportActivityScreen.swift | 2 +- .../Sources/ChatListSearchListPaneNode.swift | 6 +- .../Sources/Node/ChatListItem.swift | 2 +- .../Sources/Node/ChatListItemStrings.swift | 2 +- ...tControllerExtractedPresentationNode.swift | 1 + .../Sources/DebugController.swift | 36 +- .../ChatItemGalleryFooterContentNode.swift | 2 +- .../GalleryUI/Sources/GalleryController.swift | 6 +- .../Items/UniversalVideoGalleryItem.swift | 121 +- .../Sources/InAppPurchaseManager.swift | 2 +- .../Sources/InstantPageMediaPlaylist.swift | 4 +- .../Sources/LegacyMediaPickers.swift | 18 +- .../Sources/PeerAvatarImageGalleryItem.swift | 4 +- .../Sources/PeerInfoAvatarListNode.swift | 2 +- submodules/Postbox/Sources/Media.swift | 12 + .../PremiumUI/Sources/PremiumDemoScreen.swift | 3 +- .../Sources/PremiumLimitsListScreen.swift | 3 +- .../BubbleSettingsController.swift | 2 +- .../TextSizeSelectionController.swift | 2 +- .../Sources/Themes/EditThemeController.swift | 2 +- .../Themes/ThemePreviewControllerNode.swift | 2 +- .../Sources/ShareLoadingContainerNode.swift | 2 +- .../ShareItems/Sources/ShareItems.swift | 4 +- submodules/TelegramApi/Sources/Api0.swift | 5 +- submodules/TelegramApi/Sources/Api13.swift | 24 + submodules/TelegramApi/Sources/Api15.swift | 24 +- submodules/TelegramApi/Sources/Api36.swift | 7 +- submodules/TelegramApi/Sources/Api5.swift | 18 +- .../Sources/CallRatingController.swift | 2 +- .../Sources/PresentationGroupCall.swift | 5 + ...pandedParticipantThumbnailsComponent.swift | 2 +- .../VideoChatParticipantAvatarComponent.swift | 105 +- .../VideoChatParticipantVideoComponent.swift | 14 +- .../VideoChatParticipantsComponent.swift | 1 + .../Sources/VideoChatScreen.swift | 1619 +---------------- .../VideoChatScreenInviteMembers.swift | 343 ++++ .../Sources/VideoChatScreenMoreMenu.swift | 560 ++++++ ...ideoChatScreenParticipantContextMenu.swift | 667 +++++++ .../Sources/ApiUtils/BotInfo.swift | 2 +- .../Sources/ApiUtils/ChatContextResult.swift | 2 +- .../Sources/ApiUtils/InstantPage.swift | 2 +- .../ReplyMarkupMessageAttribute.swift | 3 + .../ApiUtils/StoreMessage_Telegram.swift | 6 +- .../Sources/ApiUtils/TelegramMediaFile.swift | 17 +- .../Sources/ApiUtils/TelegramMediaGame.swift | 2 +- .../ApiUtils/TelegramMediaWebpage.swift | 6 +- .../TelegramCore/Sources/ApiUtils/Theme.swift | 2 +- .../Sources/ApiUtils/Wallpaper.swift | 2 +- .../Network/FetchedMediaResource.swift | 19 +- .../PendingMessages/EnqueueMessage.swift | 2 +- .../PendingMessageUploadedContent.swift | 13 +- .../StandaloneUploadedMedia.swift | 6 +- .../State/AccountStateManagementUtils.swift | 6 +- .../Sources/State/AccountViewTracker.swift | 2 +- .../State/AvailableMessageEffects.swift | 2 +- .../Sources/State/AvailableReactions.swift | 17 +- .../TelegramCore/Sources/State/Holes.swift | 2 +- ...agedPremiumPromoConfigurationUpdates.swift | 2 +- .../Sources/State/ManagedRecentStickers.swift | 12 +- .../ManagedSecretChatOutgoingOperations.swift | 13 +- ...onizeInstalledStickerPacksOperations.swift | 2 +- ...ecretChatIncomingDecryptedOperations.swift | 54 +- .../Sources/State/Serialization.swift | 2 +- .../Sources/State/StickerManagement.swift | 6 +- .../Sources/Statistics/StoryStatistics.swift | 2 +- .../SyncCore/SyncCore_MediaReference.swift | 29 + .../SyncCore/SyncCore_TelegramMediaFile.swift | 48 +- .../SyncCore/SyncCore_TelegramWallpaper.swift | 2 +- .../TelegramEngine/Messages/AdMessages.swift | 2 +- .../Messages/AttachMenuBots.swift | 8 +- .../Messages/EngineStoryViewListContext.swift | 8 +- ...OutgoingMessageWithChatContextResult.swift | 4 +- .../Messages/QuickReplyMessages.swift | 2 +- .../TelegramEngine/Messages/Stories.swift | 70 +- .../Messages/StoryListContext.swift | 40 +- .../Messages/TelegramEngineMessages.swift | 6 +- .../Peers/NotificationSoundList.swift | 4 +- .../Stickers/ImportStickers.swift | 16 +- .../Stickers/LoadedStickerPack.swift | 2 +- .../Stickers/SearchStickers.swift | 4 +- .../Stickers/StickerSetInstallation.swift | 4 +- .../Stickers/TelegramEngineStickers.swift | 2 +- submodules/TelegramCore/Sources/Themes.swift | 2 +- .../Utils/ImageRepresentationsUtils.swift | 2 +- .../Utils/MediaResourceNetworkStatsTag.swift | 2 +- .../Sources/Utils/MessageUtils.swift | 2 +- .../Sources/DefaultDayPresentationTheme.swift | 3 +- .../Sources/PresentationThemeCodable.swift | 2 +- .../Sources/MessageContentKind.swift | 2 +- .../Sources/ServiceMessageStrings.swift | 2 +- .../ChatContextResultPeekContent.swift | 2 +- .../ChatMessageActionBubbleContentNode.swift | 2 +- .../ChatMessageAnimatedStickerItemNode.swift | 2 +- .../ChatMessageInteractiveFileNode.swift | 4 +- .../ChatMessageInteractiveMediaNode.swift | 45 +- .../Sources/ChatMessageItemImpl.swift | 4 +- .../Sources/ChatMessageItemView.swift | 2 +- ...ageProfilePhotoSuggestionContentNode.swift | 2 +- .../ChatSendAudioMessageContextPreview.swift | 2 +- .../Sources/GifContext.swift | 2 +- .../GroupStickerPackSetupController.swift | 2 +- .../LegacyInstantVideoController.swift | 2 +- .../Sources/EditStories.swift | 6 +- .../Sources/MediaEditorScreen.swift | 4 +- .../Sources/StickerPackListContextItem.swift | 2 +- ...PeerInfoAvatarTransformContainerNode.swift | 2 +- .../Sources/PeerInfoEditingAvatarNode.swift | 2 +- .../Sources/PeerInfoVisualMediaPaneNode.swift | 6 +- .../Sources/ChannelAppearanceScreen.swift | 4 +- .../ThemeAccentColorControllerNode.swift | 2 +- .../Sources/StoryChatContent.swift | 28 +- .../Sources/StoryContainerScreen.swift | 2 +- .../Sources/StoryItemContentComponent.swift | 6 +- .../StoryItemSetContainerComponent.swift | 6 +- ...StoryItemSetContainerViewSendMessage.swift | 8 +- .../Sources/VideoMessageCameraScreen.swift | 4 +- .../Chat/ChatControllerMediaRecording.swift | 4 +- .../Sources/Chat/ChatControllerPaste.swift | 6 +- .../ChatControllerOpenAttachmentMenu.swift | 2 +- .../ChatInterfaceStateContextMenus.swift | 4 +- ...ListContextResultsChatInputPanelItem.swift | 2 +- .../Sources/PeerMessagesMediaPlaylist.swift | 4 +- .../Sources/ExperimentalUISettings.swift | 10 +- .../TelegramUniversalVideoContent/BUILD | 1 + .../Sources/HLSVideoContent.swift | 725 ++++++++ .../Sources/NativeVideoContent.swift | 153 +- .../Sources/PlatformVideoContent.swift | 9 +- .../Sources/SystemVideoContent.swift | 7 + .../Sources/WebEmbedVideoContent.swift | 7 + .../WrappedMediaStreamingContext.swift | 111 +- .../Sources/OngoingCallThreadLocalContext.mm | 6 +- .../WatchBridge/Sources/WatchBridge.swift | 2 +- .../Sources/WatchRequestHandlers.swift | 2 +- .../Sources/WebSearchGalleryController.swift | 2 +- .../Sources/WidgetItemsUtils.swift | 2 +- 141 files changed, 3365 insertions(+), 1995 deletions(-) create mode 100644 submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift create mode 100644 submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift create mode 100644 submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift create mode 100644 submodules/TelegramUniversalVideoContent/Sources/HLSVideoContent.swift diff --git a/Telegram/NotificationService/Sources/NotificationService.swift b/Telegram/NotificationService/Sources/NotificationService.swift index 3ffcd997c6..74adf27ecc 100644 --- a/Telegram/NotificationService/Sources/NotificationService.swift +++ b/Telegram/NotificationService/Sources/NotificationService.swift @@ -1707,7 +1707,7 @@ private final class NotificationServiceHandler { } else if let file = media as? TelegramMediaFile { resource = file.resource for attribute in file.attributes { - if case let .Video(_, _, _, preloadSize, _) = attribute { + if case let .Video(_, _, _, preloadSize, _, _) = attribute { fetchSize = preloadSize.flatMap(Int64.init) } } diff --git a/submodules/AccountContext/Sources/FetchMediaUtils.swift b/submodules/AccountContext/Sources/FetchMediaUtils.swift index 08c3069e65..d23937221a 100644 --- a/submodules/AccountContext/Sources/FetchMediaUtils.swift +++ b/submodules/AccountContext/Sources/FetchMediaUtils.swift @@ -20,8 +20,8 @@ public func freeMediaFileResourceInteractiveFetched(account: Account, userLocati return fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(resource)) } -public func freeMediaFileResourceInteractiveFetched(postbox: Postbox, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, resource: MediaResource) -> Signal { - return fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(resource)) +public func freeMediaFileResourceInteractiveFetched(postbox: Postbox, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, resource: MediaResource, range: (Range, MediaBoxFetchPriority)? = nil) -> Signal { + return fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(resource), range: range) } public func cancelFreeMediaFileInteractiveFetch(account: Account, file: TelegramMediaFile) { diff --git a/submodules/AccountContext/Sources/IsMediaStreamable.swift b/submodules/AccountContext/Sources/IsMediaStreamable.swift index 650911e79a..5eb69dcf94 100644 --- a/submodules/AccountContext/Sources/IsMediaStreamable.swift +++ b/submodules/AccountContext/Sources/IsMediaStreamable.swift @@ -18,7 +18,7 @@ public func isMediaStreamable(message: Message, media: TelegramMediaFile) -> Boo return false } for attribute in media.attributes { - if case let .Video(_, _, flags, _, _) = attribute { + if case let .Video(_, _, flags, _, _, _) = attribute { if flags.contains(.supportsStreaming) { return true } @@ -41,7 +41,7 @@ public func isMediaStreamable(media: TelegramMediaFile) -> Bool { return false } for attribute in media.attributes { - if case let .Video(_, _, flags, _, _) = attribute { + if case let .Video(_, _, flags, _, _, _) = attribute { if flags.contains(.supportsStreaming) { return true } diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index d3e44953ea..310c6846ce 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -413,6 +413,7 @@ public protocol PresentationGroupCall: AnyObject { var members: Signal { get } var audioLevels: Signal<[(EnginePeer.Id, UInt32, Float, Bool)], NoError> { get } var myAudioLevel: Signal { get } + var myAudioLevelAndSpeaking: Signal<(Float, Bool), NoError> { get } var isMuted: Signal { get } var isNoiseSuppressionEnabled: Signal { get } diff --git a/submodules/AccountContext/Sources/UniversalVideoNode.swift b/submodules/AccountContext/Sources/UniversalVideoNode.swift index c9333755ff..d224b6aa78 100644 --- a/submodules/AccountContext/Sources/UniversalVideoNode.swift +++ b/submodules/AccountContext/Sources/UniversalVideoNode.swift @@ -10,6 +10,11 @@ import UniversalMediaPlayer import AVFoundation import RangeSet +public enum UniversalVideoContentVideoQuality: Equatable { + case auto + case quality(Int) +} + public protocol UniversalVideoContentNode: AnyObject { var ready: Signal { get } var status: Signal { get } @@ -29,6 +34,8 @@ public protocol UniversalVideoContentNode: AnyObject { func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool) func setBaseRate(_ baseRate: Double) + func setVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) + func videoQualityState() -> (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])? func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int func removePlaybackCompleted(_ index: Int) func fetchControl(_ control: UniversalVideoNodeFetchControl) @@ -329,6 +336,24 @@ public final class UniversalVideoNode: ASDisplayNode { }) } + public func setVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) { + self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in + if let contentNode = contentNode { + contentNode.setVideoQuality(videoQuality) + } + }) + } + + public func videoQualityState() -> (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])? { + var result: (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])? + self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in + if let contentNode { + result = contentNode.videoQualityState() + } + }) + return result + } + public func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd = .loopDisablingSound) { self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in if let contentNode = contentNode { diff --git a/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift b/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift index 15fc596bea..6cc94e44f2 100644 --- a/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift +++ b/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift @@ -206,7 +206,7 @@ public final class AvatarVideoNode: ASDisplayNode { self.backgroundNode.image = nil let videoId = photo.id?.id ?? peer.id.id._internalGetInt64Value() - let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [], preloadSize: nil, coverTime: nil)])) + 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, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])) let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: false, storeAfterDownload: nil) if videoContent.id != self.videoContent?.id { self.videoNode?.removeFromSupernode() diff --git a/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift b/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift index 68e3aa5abd..fcc1047125 100644 --- a/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift +++ b/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift @@ -460,7 +460,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: [], preloadSize: nil, coverTime: nil)]) + 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, coverTime: nil, videoCodec: nil)], alternativeRepresentations: []) let videoContent = NativeVideoContent(id: .message(1, EngineMedia.Id(namespace: 0, id: 1)), userLocation: .other, fileReference: .standalone(media: dummyFile), streamVideo: .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .black, storeAfterDownload: nil) diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 5f4b035ec6..cd0e1be846 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -4749,7 +4749,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode { return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), context: context, chatLocation: .peer(id: peer1.id), interaction: ListMessageItemInteraction.default, message: message._asMessage(), selection: hasSelection ? .selectable(selected: false) : .none, displayHeader: false, customHeader: nil, hintIsLink: true, isGlobalSearchResult: true) case .files: var media: [EngineMedia] = [] - media.append(.file(TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: 0, attributes: [.FileName(fileName: "Text.txt")]))) + media.append(.file(TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: 0, attributes: [.FileName(fileName: "Text.txt")], alternativeRepresentations: []))) let message = EngineMessage( stableId: 0, stableVersion: 0, @@ -4780,7 +4780,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode { return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), context: context, chatLocation: .peer(id: peer1.id), interaction: ListMessageItemInteraction.default, message: message._asMessage(), selection: hasSelection ? .selectable(selected: false) : .none, displayHeader: false, customHeader: nil, hintIsLink: false, isGlobalSearchResult: true) case .music: var media: [EngineMedia] = [] - media.append(.file(TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: [.Audio(isVoice: false, duration: 0, title: nil, performer: nil, waveform: Data())]))) + media.append(.file(TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: [.Audio(isVoice: false, duration: 0, title: nil, performer: nil, waveform: Data())], alternativeRepresentations: []))) let message = EngineMessage( stableId: 0, stableVersion: 0, @@ -4811,7 +4811,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode { return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), context: context, chatLocation: .peer(id: peer1.id), interaction: ListMessageItemInteraction.default, message: message._asMessage(), selection: hasSelection ? .selectable(selected: false) : .none, displayHeader: false, customHeader: nil, hintIsLink: false, isGlobalSearchResult: true) case .voice: var media: [EngineMedia] = [] - media.append(.file(TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: [.Audio(isVoice: true, duration: 0, title: nil, performer: nil, waveform: Data())]))) + media.append(.file(TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: [.Audio(isVoice: true, duration: 0, title: nil, performer: nil, waveform: Data())], alternativeRepresentations: []))) let message = EngineMessage( stableId: 0, stableVersion: 0, diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 7d071155d1..0f3ea9d162 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -2584,7 +2584,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { case let .preview(dimensions, immediateThumbnailData, videoDuration): if let immediateThumbnailData { if let videoDuration { - let thumbnailMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: index), partialReference: nil, resource: EmptyMediaResource(), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Video(duration: Double(videoDuration), size: dimensions ?? PixelDimensions(width: 1, height: 1), flags: [], preloadSize: nil, coverTime: nil)]) + let thumbnailMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: index), partialReference: nil, resource: EmptyMediaResource(), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Video(duration: Double(videoDuration), size: dimensions ?? PixelDimensions(width: 1, height: 1), flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: []) contentImageSpecs.append(ContentImageSpec(message: message, media: .file(thumbnailMedia), size: fitSize)) } else { let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: index), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) diff --git a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift index 4dc4de1c29..0882e448cd 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift @@ -246,7 +246,7 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: processed = true break inner } - case let .Video(_, _, flags, _, _): + case let .Video(_, _, flags, _, _, _): if flags.contains(.instantRoundVideo) { messageText = strings.Message_VideoMessage processed = true diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index f58c2993ad..6aa034bbe7 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -795,6 +795,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo if case .animateOut = stateTransition { contentRect.origin.y = self.contentRectDebugNode.frame.maxY - contentRect.size.height } + contentRect.size.height = 200.0 } else { return } diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index b42a95bdb1..db37d2bed4 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -105,6 +105,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case disableCallV2(Bool) case experimentalCallMute(Bool) case liveStreamV2(Bool) + case dynamicStreaming(Bool) case preferredVideoCodec(Int, String, String?, Bool) case disableVideoAspectScaling(Bool) case enableNetworkFramework(Bool) @@ -129,7 +130,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.web.rawValue case .keepChatNavigationStack, .skipReadHistory, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure: return DebugControllerSection.experiments.rawValue - case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .disableCallV2, .experimentalCallMute, .liveStreamV2: + case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .disableCallV2, .experimentalCallMute, .liveStreamV2, .dynamicStreaming: return DebugControllerSection.experiments.rawValue case .logTranslationRecognition, .resetTranslationStates: return DebugControllerSection.translation.rawValue @@ -248,8 +249,10 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 52 case .liveStreamV2: return 53 + case .dynamicStreaming: + return 54 case let .preferredVideoCodec(index, _, _, _): - return 54 + index + return 55 + index case .disableVideoAspectScaling: return 100 case .enableNetworkFramework: @@ -338,7 +341,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { let fileResource = LocalFileMediaResource(fileId: id, size: Int64(gzippedData.count), isSecretRelated: false) context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData) - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() @@ -418,7 +421,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { let fileResource = LocalFileMediaResource(fileId: id, size: Int64(logData.count), isSecretRelated: false) context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: logData) - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(logData.count), attributes: [.FileName(fileName: "Log-iOS-Short.txt")]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(logData.count), attributes: [.FileName(fileName: "Log-iOS-Short.txt")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() @@ -504,7 +507,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { let fileResource = LocalFileMediaResource(fileId: id, size: Int64(gzippedData.count), isSecretRelated: false) context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData) - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() @@ -588,7 +591,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { let fileResource = LocalFileMediaResource(fileId: id, size: Int64(gzippedData.count), isSecretRelated: false) context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData) - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() @@ -673,7 +676,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { let fileResource = LocalFileMediaResource(fileId: id, size: Int64(gzippedData.count), isSecretRelated: false) context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData) - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() @@ -726,7 +729,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { let messages = logs.map { (name, path) -> EnqueueMessage in let id = Int64.random(in: Int64.min ... Int64.max) - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: id), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: nil, attributes: [.FileName(fileName: name)]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: id), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: nil, attributes: [.FileName(fileName: name)], alternativeRepresentations: []) return .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) } let _ = enqueueMessages(account: context.account, peerId: peerId, messages: messages).start() @@ -835,7 +838,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { let fileResource = LocalFileMediaResource(fileId: id, size: Int64(gzippedData.count), isSecretRelated: false) context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData) - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/zip", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-All.txt.zip")]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/zip", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-All.txt.zip")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() @@ -890,7 +893,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { let fileResource = LocalFileMediaResource(fileId: id, size: Int64(allStatsData.count), isSecretRelated: false) context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: allStatsData) - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/zip", size: Int64(allStatsData.count), attributes: [.FileName(fileName: "StorageReport.txt")]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/zip", size: Int64(allStatsData.count), attributes: [.FileName(fileName: "StorageReport.txt")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() @@ -1348,6 +1351,16 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) }).start() }) + case let .dynamicStreaming(value): + return ItemListSwitchItem(presentationData: presentationData, title: "Dynamic Streaming", value: value, sectionId: self.section, style: .blocks, updated: { value in + let _ = arguments.sharedContext.accountManager.transaction ({ transaction in + transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in + var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings + settings.dynamicStreaming = value + return PreferencesEntry(settings) + }) + }).start() + }) case let .preferredVideoCodec(_, title, value, isSelected): return ItemListCheckboxItem(presentationData: presentationData, title: title, style: .right, checked: isSelected, zeroSeparatorInsets: false, sectionId: self.section, action: { let _ = arguments.sharedContext.accountManager.transaction ({ transaction in @@ -1505,6 +1518,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present entries.append(.disableCallV2(experimentalSettings.disableCallV2)) entries.append(.experimentalCallMute(experimentalSettings.experimentalCallMute)) entries.append(.liveStreamV2(experimentalSettings.liveStreamV2)) + entries.append(.dynamicStreaming(experimentalSettings.dynamicStreaming)) } /*let codecs: [(String, String?)] = [ @@ -1673,7 +1687,7 @@ public func triggerDebugSendLogsUI(context: AccountContext, additionalInfo: Stri let fileResource = LocalFileMediaResource(fileId: id, size: Int64(gzippedData.count), isSecretRelated: false) context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData) - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index 127cce67fa..d47d42bd3f 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -859,7 +859,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } else if let media = media as? TelegramMediaFile, !media.isAnimated { for attribute in media.attributes { switch attribute { - case let .Video(_, dimensions, _, _, _): + case let .Video(_, dimensions, _, _, _, _): isVideo = true if dimensions.height > 0 { if CGFloat(dimensions.width) / CGFloat(dimensions.height) > 1.33 { diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index 5c65e2a77d..c5e76739a1 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -245,7 +245,11 @@ public func galleryItemForEntry( content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), loopVideo: true, enableSound: false, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file)) } else { if true || (file.mimeType == "video/mpeg4" || file.mimeType == "video/mov" || file.mimeType == "video/mp4") { - content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file)) + if NativeVideoContent.isHLSVideo(file: file), context.sharedContext.immediateExperimentalUISettings.dynamicStreaming { + content = HLSVideoContent(id: .message(message.id, message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: loopVideos) + } else { + content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file)) + } } else { content = PlatformVideoContent(id: .message(message.id, message.stableId, file.fileId), userLocation: .peer(message.id.peerId), content: .file(.message(message: MessageReference(message), media: file)), streamVideo: streamVideos, loopVideo: loopVideos) } diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 074f443cda..fd7be6047b 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -1096,6 +1096,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { var hasLinkedStickers = false if let content = item.content as? NativeVideoContent { hasLinkedStickers = content.fileReference.media.hasLinkedStickers + } else if let content = item.content as? HLSVideoContent { + hasLinkedStickers = content.fileReference.media.hasLinkedStickers } var disablePictureInPicture = false @@ -1241,7 +1243,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 } @@ -1532,6 +1534,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if let _ = item.content as? NativeVideoContent { self.playbackRate = item.playbackRate() + } else if let _ = item.content as? HLSVideoContent { + self.playbackRate = item.playbackRate() } else if let _ = item.content as? WebEmbedVideoContent { self.playbackRate = item.playbackRate() } @@ -1602,6 +1606,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if isLocal || isStreamable { return true } + } else if let item = self.item, let _ = item.content as? HLSVideoContent { + return true } else if let item = self.item, let _ = item.content as? PlatformVideoContent { return true } @@ -1619,6 +1625,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { var isAnimated = false if let item = self.item, let content = item.content as? NativeVideoContent { isAnimated = content.fileReference.media.isAnimated + } else if let item = self.item, let content = item.content as? HLSVideoContent { + isAnimated = content.fileReference.media.isAnimated } self.hideStatusNodeUntilCentrality = false @@ -1712,6 +1720,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if let time = item.timecode { seek = .timecode(time) } + } else if let content = item.content as? HLSVideoContent { + isAnimated = content.fileReference.media.isAnimated + if let time = item.timecode { + seek = .timecode(time) + } } else if let _ = item.content as? WebEmbedVideoContent { if let time = item.timecode { seek = .timecode(time) @@ -1743,6 +1756,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if !item.isSecret, let content = item.content as? NativeVideoContent, content.duration <= 30 { return .loop } + if !item.isSecret, let content = item.content as? HLSVideoContent, content.duration <= 30 { + return .loop + } } return .stop } @@ -2700,6 +2716,35 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { items.append(.separator) + if let videoQualityState = strongSelf.videoNode?.videoQualityState(), !videoQualityState.available.isEmpty { + //TODO:localize + + let qualityText: String + switch videoQualityState.preferred { + case .auto: + if videoQualityState.current != 0 { + qualityText = "Auto (\(videoQualityState.current)p)" + } else { + qualityText = "Auto" + } + case let .quality(value): + qualityText = "\(value)p" + } + + items.append(.action(ContextMenuActionItem(text: "Video Quality", textLayout: .secondLineWithValue(qualityText), icon: { _ in + return nil + }, action: { c, _ in + guard let strongSelf = self else { + c?.dismiss(completion: nil) + return + } + + c?.setItems(.single(ContextController.Items(content: .list(strongSelf.contextMenuVideoQualityItems(dismiss: dismiss)))), minHeight: nil, animated: true) + }))) + + items.append(.separator) + } + if let (message, _, _) = strongSelf.contentInfo() { let context = strongSelf.context items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in @@ -2881,6 +2926,80 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } + private func contextMenuVideoQualityItems(dismiss: @escaping () -> Void) -> [ContextMenuItem] { + guard let videoNode = self.videoNode else { + return [] + } + guard let qualityState = videoNode.videoQualityState(), !qualityState.available.isEmpty else { + return [] + } + + var items: [ContextMenuItem] = [] + + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Common_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) + }, iconPosition: .left, action: { [weak self] c, _ in + guard let self else { + c?.dismiss(completion: nil) + return + } + c?.setItems(self.contextMenuMainItems(dismiss: dismiss) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) + }))) + + do { + let isSelected = qualityState.preferred == .auto + let qualityText: String + if qualityState.current != 0 { + qualityText = "Auto (\(qualityState.current)p)" + } else { + qualityText = "Auto" + } + items.append(.action(ContextMenuActionItem(text: qualityText, icon: { _ in + if isSelected { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white) + } else { + return nil + } + }, action: { [weak self] _, f in + f(.default) + + guard let self, let videoNode = self.videoNode else { + return + } + videoNode.setVideoQuality(.auto) + + /*if let controller = strongSelf.galleryController() as? GalleryController { + controller.updateSharedPlaybackRate(rate) + }*/ + }))) + } + + for quality in qualityState.available { + //TODO:release + let isSelected = qualityState.preferred == .quality(quality) + items.append(.action(ContextMenuActionItem(text: "\(quality)p", icon: { _ in + if isSelected { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white) + } else { + return nil + } + }, action: { [weak self] _, f in + f(.default) + + guard let self, let videoNode = self.videoNode else { + return + } + videoNode.setVideoQuality(.quality(quality)) + + /*if let controller = strongSelf.galleryController() as? GalleryController { + controller.updateSharedPlaybackRate(rate) + }*/ + }))) + } + + return items + } + private var isAirPlayActive = false private var externalVideoPlayer: ExternalVideoPlayer? func beginAirPlaySetup() { diff --git a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift index cc8f78669a..b607216341 100644 --- a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift +++ b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift @@ -590,7 +590,7 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver { let fileResource = LocalFileMediaResource(fileId: id, size: Int64(receiptData.count), isSecretRelated: false) self.engine.account.postbox.mediaBox.storeResourceData(fileResource.id, data: receiptData) - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(receiptData.count), attributes: [.FileName(fileName: "Receipt.dat")]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(receiptData.count), attributes: [.FileName(fileName: "Receipt.dat")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: self.engine.account, peerId: self.engine.account.peerId, messages: [message]).start() diff --git a/submodules/InstantPageUI/Sources/InstantPageMediaPlaylist.swift b/submodules/InstantPageUI/Sources/InstantPageMediaPlaylist.swift index c8fcfe9691..7d939eeb13 100644 --- a/submodules/InstantPageUI/Sources/InstantPageMediaPlaylist.swift +++ b/submodules/InstantPageUI/Sources/InstantPageMediaPlaylist.swift @@ -53,7 +53,7 @@ final class InstantPageMediaPlaylistItem: SharedMediaPlaylistItem { } else { return SharedMediaPlaybackData(type: .music, source: .telegramFile(reference: .webPage(webPage: WebpageReference(self.webPage), media: file), isCopyProtected: false, isViewOnce: 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, isViewOnce: false)) } else { @@ -99,7 +99,7 @@ final class InstantPageMediaPlaylistItem: SharedMediaPlaylistItem { return SharedMediaPlaybackDisplayData.music(title: updatedTitle, performer: updatedPerformer, albumArt: albumArt, long: false, caption: nil) } - case let .Video(_, _, flags, _, _): + case let .Video(_, _, flags, _, _, _): if flags.contains(.instantRoundVideo) { return SharedMediaPlaybackDisplayData.instantVideo(author: nil, peer: nil, timestamp: 0) } else { diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift index 7425988c60..86c841110f 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift @@ -294,11 +294,11 @@ 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: 0.0, size: PixelDimensions(finalDimensions), flags: [.supportsStreaming], preloadSize: nil, coverTime: nil)) + fileAttributes.append(.Video(duration: 0.0, size: PixelDimensions(finalDimensions), flags: [.supportsStreaming], preloadSize: nil, coverTime: nil, videoCodec: nil)) fileAttributes.append(.FileName(fileName: fileName)) fileAttributes.append(.Animated) - 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: fileAttributes) + 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: fileAttributes, alternativeRepresentations: []) subscriber.putNext(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])) subscriber.putCompletion() } else { @@ -336,11 +336,11 @@ 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: 0.0, size: PixelDimensions(finalDimensions), flags: [.supportsStreaming], preloadSize: nil, coverTime: nil)) + fileAttributes.append(.Video(duration: 0.0, size: PixelDimensions(finalDimensions), flags: [.supportsStreaming], preloadSize: nil, coverTime: nil, videoCodec: nil)) fileAttributes.append(.FileName(fileName: fileName)) fileAttributes.append(.Animated) - 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: fileAttributes) + 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: fileAttributes, alternativeRepresentations: []) subscriber.putNext(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])) subscriber.putCompletion() } else { @@ -506,7 +506,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: format == .jxl ? "image/jxl" : "image/jpeg", size: nil, attributes: [ .FileName(fileName: format == .jxl ? "image\(sizeSide)-q\(quality).jxl" : "image\(sizeSide)-q\(quality).jpg"), .ImageSize(size: PixelDimensions(scaledSize)) - ]) + ], alternativeRepresentations: []) var attributes: [MessageAttribute] = [] if let timer = item.timer, timer > 0 && (timer <= 60 || timer == viewOnceTimeout) { @@ -651,7 +651,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A var randomId: Int64 = 0 arc4random_buf(&randomId, 8) let resource = LocalFileReferenceMediaResource(localFilePath: path, randomId: randomId) - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: nil, attributes: [.FileName(fileName: name)]) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: nil, attributes: [.FileName(fileName: name)], alternativeRepresentations: []) var attributes: [MessageAttribute] = [] let text = trimChatInputText(convertMarkdownToAttributes(caption ?? NSAttributedString())) @@ -704,7 +704,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A var randomId: Int64 = 0 arc4random_buf(&randomId, 8) let resource = PhotoLibraryMediaResource(localIdentifier: asset.localIdentifier, uniqueId: Int64.random(in: Int64.min ... Int64.max)) - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: nil, attributes: [.FileName(fileName: name)]) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: nil, attributes: [.FileName(fileName: name)], alternativeRepresentations: []) var attributes: [MessageAttribute] = [] let text = trimChatInputText(convertMarkdownToAttributes(caption ?? NSAttributedString())) @@ -857,7 +857,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A fileAttributes.append(.Animated) } if !asFile { - fileAttributes.append(.Video(duration: finalDuration, size: PixelDimensions(finalDimensions), flags: [.supportsStreaming], preloadSize: nil, coverTime: nil)) + fileAttributes.append(.Video(duration: finalDuration, size: PixelDimensions(finalDimensions), flags: [.supportsStreaming], preloadSize: nil, coverTime: nil, videoCodec: nil)) if let adjustments = adjustments { if adjustments.sendAsGif { fileAttributes.append(.Animated) @@ -891,7 +891,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A fileAttributes.append(.HasLinkedStickers) } - 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: fileAttributes) + 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: fileAttributes, alternativeRepresentations: []) if let timer = item.timer, timer > 0 && (timer <= 60 || timer == viewOnceTimeout) { attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil)) diff --git a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift index 37b5bc76e6..5d15ce66df 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift @@ -187,7 +187,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { let subject: ShareControllerSubject var actionCompletionText: String? if let video = entry.videoRepresentations.last, let peerReference = PeerReference(peer._asPeer()) { - let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil)])) + 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, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])) 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: [], preloadSize: nil, coverTime: nil)])) + 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, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])) let videoContent = NativeVideoContent(id: .profileVideo(id, category), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: true, useLargeThumbnail: true, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, storeAfterDownload: nil) let videoNode = UniversalVideoNode(postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .overlay) videoNode.isUserInteractionEnabled = false diff --git a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift index a3ba818d23..b9f3a37afc 100644 --- a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift +++ b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift @@ -515,7 +515,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: [], preloadSize: nil, coverTime: nil)])) + 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, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])) let videoContent = NativeVideoContent(id: .profileVideo(id, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: fullSizeOnly, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, storeAfterDownload: nil) if videoContent.id != self.videoContent?.id { diff --git a/submodules/Postbox/Sources/Media.swift b/submodules/Postbox/Sources/Media.swift index 066afb81ec..4c0e3e28eb 100644 --- a/submodules/Postbox/Sources/Media.swift +++ b/submodules/Postbox/Sources/Media.swift @@ -101,6 +101,18 @@ public func areMediaArraysEqual(_ lhs: [Media], _ rhs: [Media]) -> Bool { return true } +public func areMediaArraysSemanticallyEqual(_ lhs: [Media], _ rhs: [Media]) -> Bool { + if lhs.count != rhs.count { + return false + } + for i in 0 ..< lhs.count { + if !lhs[i].isSemanticallyEqual(to: rhs[i]) { + return false + } + } + return true +} + public func areMediaDictionariesEqual(_ lhs: [MediaId: Media], _ rhs: [MediaId: Media]) -> Bool { if lhs.count != rhs.count { return false diff --git a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift index fc876e6e81..51a2d5e99b 100644 --- a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift @@ -645,7 +645,8 @@ private final class DemoSheetContent: CombinedComponent { immediateThumbnailData: file.immediateThumbnailData, mimeType: file.mimeType, size: file.size, - attributes: file.attributes + attributes: file.attributes, + alternativeRepresentations: file.alternativeRepresentations ) } default: diff --git a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift index 3f1fa037fd..77609c80f6 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift @@ -158,7 +158,8 @@ public class PremiumLimitsListScreen: ViewController { immediateThumbnailData: file.immediateThumbnailData, mimeType: file.mimeType, size: file.size, - attributes: file.attributes + attributes: file.attributes, + alternativeRepresentations: file.alternativeRepresentations ) } default: diff --git a/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift b/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift index 312ebb57a8..c039babd7d 100644 --- a/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift +++ b/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift @@ -177,7 +177,7 @@ private final class BubbleSettingsControllerNode: ASDisplayNode, ASScrollViewDel let waveformBase64 = "DAAOAAkACQAGAAwADwAMABAADQAPABsAGAALAA0AGAAfABoAHgATABgAGQAYABQADAAVABEAHwANAA0ACQAWABkACQAOAAwACQAfAAAAGQAVAAAAEwATAAAACAAfAAAAHAAAABwAHwAAABcAGQAAABQADgAAABQAHwAAAB8AHwAAAAwADwAAAB8AEwAAABoAFwAAAB8AFAAAAAAAHwAAAAAAHgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAAAA=" let voiceAttributes: [TelegramMediaFileAttribute] = [.Audio(isVoice: true, duration: 23, title: nil, performer: nil, waveform: Data(base64Encoded: waveformBase64)!)] - let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes) + let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes, alternativeRepresentations: []) let message3 = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message3], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local), tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false)) diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index 347f115d99..0ea046eeaa 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -450,7 +450,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, ASScrollView let waveformBase64 = "DAAOAAkACQAGAAwADwAMABAADQAPABsAGAALAA0AGAAfABoAHgATABgAGQAYABQADAAVABEAHwANAA0ACQAWABkACQAOAAwACQAfAAAAGQAVAAAAEwATAAAACAAfAAAAHAAAABwAHwAAABcAGQAAABQADgAAABQAHwAAAB8AHwAAAAwADwAAAB8AEwAAABoAFwAAAB8AFAAAAAAAHwAAAAAAHgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAAAA=" let voiceAttributes: [TelegramMediaFileAttribute] = [.Audio(isVoice: true, duration: 23, title: nil, performer: nil, waveform: Data(base64Encoded: waveformBase64)!)] - let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes) + let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes, alternativeRepresentations: []) let message3 = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message3], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local), tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false)) diff --git a/submodules/SettingsUI/Sources/Themes/EditThemeController.swift b/submodules/SettingsUI/Sources/Themes/EditThemeController.swift index c8030edd2e..0b009def43 100644 --- a/submodules/SettingsUI/Sources/Themes/EditThemeController.swift +++ b/submodules/SettingsUI/Sources/Themes/EditThemeController.swift @@ -538,7 +538,7 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll let _ = (combineLatest(queue: Queue.mainQueue(), previewThemePromise.get(), settingsPromise.get()) |> take(1)).start(next: { previewTheme, settings in let saveThemeTemplateFile: (String, LocalFileMediaResource, @escaping () -> Void) -> Void = { title, resource, completion in - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: resource.fileId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/x-tgtheme-ios", size: nil, attributes: [.FileName(fileName: "\(title).tgios-theme")]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: resource.fileId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/x-tgtheme-ios", size: nil, attributes: [.FileName(fileName: "\(title).tgios-theme")], alternativeRepresentations: []) let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: context.account.peerId, messages: [message]).start() diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index 974bf7976f..61fdf2c69f 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -615,7 +615,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, ASScrollViewDelegate { let waveformBase64 = "DAAOAAkACQAGAAwADwAMABAADQAPABsAGAALAA0AGAAfABoAHgATABgAGQAYABQADAAVABEAHwANAA0ACQAWABkACQAOAAwACQAfAAAAGQAVAAAAEwATAAAACAAfAAAAHAAAABwAHwAAABcAGQAAABQADgAAABQAHwAAAB8AHwAAAAwADwAAAB8AEwAAABoAFwAAAB8AFAAAAAAAHwAAAAAAHgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAAAA=" let voiceAttributes: [TelegramMediaFileAttribute] = [.Audio(isVoice: true, duration: 23, title: nil, performer: nil, waveform: Data(base64Encoded: waveformBase64)!)] - let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes) + let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes, alternativeRepresentations: []) let message6 = Message(stableId: 6, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 6), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66005, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) sampleMessages.append(message6) diff --git a/submodules/ShareController/Sources/ShareLoadingContainerNode.swift b/submodules/ShareController/Sources/ShareLoadingContainerNode.swift index 292cc01841..ebf18f121b 100644 --- a/submodules/ShareController/Sources/ShareLoadingContainerNode.swift +++ b/submodules/ShareController/Sources/ShareLoadingContainerNode.swift @@ -279,7 +279,7 @@ public final class ShareProlongedLoadingContainerNode: ASDisplayNode, ShareConte if let postbox, let mediaManager = environment.mediaManager, 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: [], preloadSize: nil, coverTime: nil)]) + 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, coverTime: nil, videoCodec: nil)], alternativeRepresentations: []) let videoContent = NativeVideoContent(id: .message(1, EngineMedia.Id(namespace: 0, id: 1)), userLocation: .other, fileReference: .standalone(media: dummyFile), streamVideo: .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .black, storeAfterDownload: nil) diff --git a/submodules/ShareItems/Sources/ShareItems.swift b/submodules/ShareItems/Sources/ShareItems.swift index 71ae6be423..84c74e819c 100644 --- a/submodules/ShareItems/Sources/ShareItems.swift +++ b/submodules/ShareItems/Sources/ShareItems.swift @@ -144,7 +144,7 @@ private func preparedShareItem(postbox: Postbox, network: Network, to peerId: Pe 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(postbox: postbox, network: network, peerId: peerId, text: "", source: .resource(.standalone(resource: resource)), mimeType: "video/mp4", attributes: [.Video(duration: finalDuration, size: PixelDimensions(width: Int32(finalDimensions.width), height: Int32(finalDimensions.height)), flags: flags, preloadSize: nil, coverTime: nil)], hintFileIsLarge: estimatedSize > 10 * 1024 * 1024) + return standaloneUploadedFile(postbox: postbox, network: network, peerId: peerId, text: "", source: .resource(.standalone(resource: resource)), mimeType: "video/mp4", attributes: [.Video(duration: finalDuration, size: PixelDimensions(width: Int32(finalDimensions.width), height: Int32(finalDimensions.height)), flags: flags, preloadSize: nil, coverTime: nil, videoCodec: nil)], hintFileIsLarge: estimatedSize > 10 * 1024 * 1024) |> mapError { _ -> PreparedShareItemError in return .generic } @@ -210,7 +210,7 @@ private func preparedShareItem(postbox: Postbox, network: Network, to peerId: Pe let mimeType: String if converted { mimeType = "video/mp4" - attributes = [.Video(duration: duration, size: PixelDimensions(width: Int32(dimensions.width), height: Int32(dimensions.height)), flags: [.supportsStreaming], preloadSize: nil, coverTime: nil), .Animated, .FileName(fileName: "animation.mp4")] + attributes = [.Video(duration: duration, size: PixelDimensions(width: Int32(dimensions.width), height: Int32(dimensions.height)), flags: [.supportsStreaming], preloadSize: nil, coverTime: nil, videoCodec: nil), .Animated, .FileName(fileName: "animation.mp4")] } else { mimeType = "animation/gif" attributes = [.ImageSize(size: PixelDimensions(width: Int32(dimensions.width), height: Int32(dimensions.height))), .Animated, .FileName(fileName: fileName ?? "animation.gif")] diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index f093c8b330..2f4860f756 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -245,7 +245,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1744710921] = { return Api.DocumentAttribute.parse_documentAttributeHasStickers($0) } dict[1815593308] = { return Api.DocumentAttribute.parse_documentAttributeImageSize($0) } dict[1662637586] = { return Api.DocumentAttribute.parse_documentAttributeSticker($0) } - dict[389652397] = { return Api.DocumentAttribute.parse_documentAttributeVideo($0) } + dict[1137015880] = { return Api.DocumentAttribute.parse_documentAttributeVideo($0) } dict[761606687] = { return Api.DraftMessage.parse_draftMessage($0) } dict[453805082] = { return Api.DraftMessage.parse_draftMessageEmpty($0) } dict[-1764723459] = { return Api.EmailVerification.parse_emailVerificationApple($0) } @@ -500,6 +500,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1560655744] = { return Api.KeyboardButton.parse_keyboardButton($0) } dict[-1344716869] = { return Api.KeyboardButton.parse_keyboardButtonBuy($0) } dict[901503851] = { return Api.KeyboardButton.parse_keyboardButtonCallback($0) } + dict[1976723854] = { return Api.KeyboardButton.parse_keyboardButtonCopy($0) } dict[1358175439] = { return Api.KeyboardButton.parse_keyboardButtonGame($0) } dict[-59151553] = { return Api.KeyboardButton.parse_keyboardButtonRequestGeoLocation($0) } dict[1406648280] = { return Api.KeyboardButton.parse_keyboardButtonRequestPeer($0) } @@ -603,7 +604,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1313731771] = { return Api.MessageFwdHeader.parse_messageFwdHeader($0) } dict[1882335561] = { return Api.MessageMedia.parse_messageMediaContact($0) } dict[1065280907] = { return Api.MessageMedia.parse_messageMediaDice($0) } - dict[1291114285] = { return Api.MessageMedia.parse_messageMediaDocument($0) } + dict[-581497899] = { return Api.MessageMedia.parse_messageMediaDocument($0) } dict[1038967584] = { return Api.MessageMedia.parse_messageMediaEmpty($0) } dict[-38694904] = { return Api.MessageMedia.parse_messageMediaGame($0) } dict[1457575028] = { return Api.MessageMedia.parse_messageMediaGeo($0) } diff --git a/submodules/TelegramApi/Sources/Api13.swift b/submodules/TelegramApi/Sources/Api13.swift index 3f76fc2bee..c5c11feab8 100644 --- a/submodules/TelegramApi/Sources/Api13.swift +++ b/submodules/TelegramApi/Sources/Api13.swift @@ -674,6 +674,7 @@ public extension Api { case keyboardButton(text: String) case keyboardButtonBuy(text: String) case keyboardButtonCallback(flags: Int32, text: String, data: Buffer) + case keyboardButtonCopy(text: String, copyText: String) case keyboardButtonGame(text: String) case keyboardButtonRequestGeoLocation(text: String) case keyboardButtonRequestPeer(text: String, buttonId: Int32, peerType: Api.RequestPeerType, maxQuantity: Int32) @@ -735,6 +736,13 @@ public extension Api { serializeString(text, buffer: buffer, boxed: false) serializeBytes(data, buffer: buffer, boxed: false) break + case .keyboardButtonCopy(let text, let copyText): + if boxed { + buffer.appendInt32(1976723854) + } + serializeString(text, buffer: buffer, boxed: false) + serializeString(copyText, buffer: buffer, boxed: false) + break case .keyboardButtonGame(let text): if boxed { buffer.appendInt32(1358175439) @@ -838,6 +846,8 @@ public extension Api { return ("keyboardButtonBuy", [("text", text as Any)]) case .keyboardButtonCallback(let flags, let text, let data): return ("keyboardButtonCallback", [("flags", flags as Any), ("text", text as Any), ("data", data as Any)]) + case .keyboardButtonCopy(let text, let copyText): + return ("keyboardButtonCopy", [("text", text as Any), ("copyText", copyText as Any)]) case .keyboardButtonGame(let text): return ("keyboardButtonGame", [("text", text as Any)]) case .keyboardButtonRequestGeoLocation(let text): @@ -968,6 +978,20 @@ public extension Api { return nil } } + public static func parse_keyboardButtonCopy(_ reader: BufferReader) -> KeyboardButton? { + var _1: String? + _1 = parseString(reader) + var _2: String? + _2 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.KeyboardButton.keyboardButtonCopy(text: _1!, copyText: _2!) + } + else { + return nil + } + } public static func parse_keyboardButtonGame(_ reader: BufferReader) -> KeyboardButton? { var _1: String? _1 = parseString(reader) diff --git a/submodules/TelegramApi/Sources/Api15.swift b/submodules/TelegramApi/Sources/Api15.swift index eedd139355..77e5198937 100644 --- a/submodules/TelegramApi/Sources/Api15.swift +++ b/submodules/TelegramApi/Sources/Api15.swift @@ -710,7 +710,7 @@ public extension Api { indirect enum MessageMedia: TypeConstructorDescription { case messageMediaContact(phoneNumber: String, firstName: String, lastName: String, vcard: String, userId: Int64) case messageMediaDice(value: Int32, emoticon: String) - case messageMediaDocument(flags: Int32, document: Api.Document?, altDocument: Api.Document?, ttlSeconds: Int32?) + case messageMediaDocument(flags: Int32, document: Api.Document?, altDocuments: [Api.Document]?, ttlSeconds: Int32?) case messageMediaEmpty case messageMediaGame(game: Api.Game) case messageMediaGeo(geo: Api.GeoPoint) @@ -745,13 +745,17 @@ public extension Api { serializeInt32(value, buffer: buffer, boxed: false) serializeString(emoticon, buffer: buffer, boxed: false) break - case .messageMediaDocument(let flags, let document, let altDocument, let ttlSeconds): + case .messageMediaDocument(let flags, let document, let altDocuments, let ttlSeconds): if boxed { - buffer.appendInt32(1291114285) + buffer.appendInt32(-581497899) } serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {document!.serialize(buffer, true)} - if Int(flags) & Int(1 << 5) != 0 {altDocument!.serialize(buffer, true)} + if Int(flags) & Int(1 << 5) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(altDocuments!.count)) + for item in altDocuments! { + item.serialize(buffer, true) + }} if Int(flags) & Int(1 << 2) != 0 {serializeInt32(ttlSeconds!, buffer: buffer, boxed: false)} break case .messageMediaEmpty: @@ -905,8 +909,8 @@ public extension Api { return ("messageMediaContact", [("phoneNumber", phoneNumber as Any), ("firstName", firstName as Any), ("lastName", lastName as Any), ("vcard", vcard as Any), ("userId", userId as Any)]) case .messageMediaDice(let value, let emoticon): return ("messageMediaDice", [("value", value as Any), ("emoticon", emoticon as Any)]) - case .messageMediaDocument(let flags, let document, let altDocument, let ttlSeconds): - return ("messageMediaDocument", [("flags", flags as Any), ("document", document as Any), ("altDocument", altDocument as Any), ("ttlSeconds", ttlSeconds as Any)]) + case .messageMediaDocument(let flags, let document, let altDocuments, let ttlSeconds): + return ("messageMediaDocument", [("flags", flags as Any), ("document", document as Any), ("altDocuments", altDocuments as Any), ("ttlSeconds", ttlSeconds as Any)]) case .messageMediaEmpty: return ("messageMediaEmpty", []) case .messageMediaGame(let game): @@ -982,9 +986,9 @@ public extension Api { if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.Document } } - var _3: Api.Document? - if Int(_1!) & Int(1 << 5) != 0 {if let signature = reader.readInt32() { - _3 = Api.parse(reader, signature: signature) as? Api.Document + var _3: [Api.Document]? + if Int(_1!) & Int(1 << 5) != 0 {if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) } } var _4: Int32? if Int(_1!) & Int(1 << 2) != 0 {_4 = reader.readInt32() } @@ -993,7 +997,7 @@ public extension Api { let _c3 = (Int(_1!) & Int(1 << 5) == 0) || _3 != nil let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { - return Api.MessageMedia.messageMediaDocument(flags: _1!, document: _2, altDocument: _3, ttlSeconds: _4) + return Api.MessageMedia.messageMediaDocument(flags: _1!, document: _2, altDocuments: _3, ttlSeconds: _4) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api36.swift b/submodules/TelegramApi/Sources/Api36.swift index 8d34e43419..a45ef83846 100644 --- a/submodules/TelegramApi/Sources/Api36.swift +++ b/submodules/TelegramApi/Sources/Api36.swift @@ -2608,12 +2608,13 @@ public extension Api.functions.channels { } } public extension Api.functions.channels { - static func clickSponsoredMessage(channel: Api.InputChannel, randomId: Buffer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func clickSponsoredMessage(flags: Int32, channel: Api.InputChannel, randomId: Buffer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(414170259) + buffer.appendInt32(21257589) + serializeInt32(flags, buffer: buffer, boxed: false) channel.serialize(buffer, true) serializeBytes(randomId, buffer: buffer, boxed: false) - return (FunctionDescription(name: "channels.clickSponsoredMessage", parameters: [("channel", String(describing: channel)), ("randomId", String(describing: randomId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + return (FunctionDescription(name: "channels.clickSponsoredMessage", parameters: [("flags", String(describing: flags)), ("channel", String(describing: channel)), ("randomId", String(describing: randomId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in let reader = BufferReader(buffer) var result: Api.Bool? if let signature = reader.readInt32() { diff --git a/submodules/TelegramApi/Sources/Api5.swift b/submodules/TelegramApi/Sources/Api5.swift index bd121a4a0b..98c4ef46fc 100644 --- a/submodules/TelegramApi/Sources/Api5.swift +++ b/submodules/TelegramApi/Sources/Api5.swift @@ -1473,7 +1473,7 @@ public extension Api { case documentAttributeHasStickers case documentAttributeImageSize(w: Int32, h: Int32) case documentAttributeSticker(flags: Int32, alt: String, stickerset: Api.InputStickerSet, maskCoords: Api.MaskCoords?) - case documentAttributeVideo(flags: Int32, duration: Double, w: Int32, h: Int32, preloadPrefixSize: Int32?, videoStartTs: Double?) + case documentAttributeVideo(flags: Int32, duration: Double, w: Int32, h: Int32, preloadPrefixSize: Int32?, videoStartTs: Double?, videoCodec: String?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -1529,9 +1529,9 @@ public extension Api { stickerset.serialize(buffer, true) if Int(flags) & Int(1 << 0) != 0 {maskCoords!.serialize(buffer, true)} break - case .documentAttributeVideo(let flags, let duration, let w, let h, let preloadPrefixSize, let videoStartTs): + case .documentAttributeVideo(let flags, let duration, let w, let h, let preloadPrefixSize, let videoStartTs, let videoCodec): if boxed { - buffer.appendInt32(389652397) + buffer.appendInt32(1137015880) } serializeInt32(flags, buffer: buffer, boxed: false) serializeDouble(duration, buffer: buffer, boxed: false) @@ -1539,6 +1539,7 @@ public extension Api { serializeInt32(h, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 2) != 0 {serializeInt32(preloadPrefixSize!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 4) != 0 {serializeDouble(videoStartTs!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 5) != 0 {serializeString(videoCodec!, buffer: buffer, boxed: false)} break } } @@ -1559,8 +1560,8 @@ public extension Api { return ("documentAttributeImageSize", [("w", w as Any), ("h", h as Any)]) case .documentAttributeSticker(let flags, let alt, let stickerset, let maskCoords): return ("documentAttributeSticker", [("flags", flags as Any), ("alt", alt as Any), ("stickerset", stickerset as Any), ("maskCoords", maskCoords as Any)]) - case .documentAttributeVideo(let flags, let duration, let w, let h, let preloadPrefixSize, let videoStartTs): - return ("documentAttributeVideo", [("flags", flags as Any), ("duration", duration as Any), ("w", w as Any), ("h", h as Any), ("preloadPrefixSize", preloadPrefixSize as Any), ("videoStartTs", videoStartTs as Any)]) + case .documentAttributeVideo(let flags, let duration, let w, let h, let preloadPrefixSize, let videoStartTs, let videoCodec): + return ("documentAttributeVideo", [("flags", flags as Any), ("duration", duration as Any), ("w", w as Any), ("h", h as Any), ("preloadPrefixSize", preloadPrefixSize as Any), ("videoStartTs", videoStartTs as Any), ("videoCodec", videoCodec as Any)]) } } @@ -1674,14 +1675,17 @@ public extension Api { if Int(_1!) & Int(1 << 2) != 0 {_5 = reader.readInt32() } var _6: Double? if Int(_1!) & Int(1 << 4) != 0 {_6 = reader.readDouble() } + var _7: String? + if Int(_1!) & Int(1 << 5) != 0 {_7 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil let _c6 = (Int(_1!) & Int(1 << 4) == 0) || _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.DocumentAttribute.documentAttributeVideo(flags: _1!, duration: _2!, w: _3!, h: _4!, preloadPrefixSize: _5, videoStartTs: _6) + let _c7 = (Int(_1!) & Int(1 << 5) == 0) || _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.DocumentAttribute.documentAttributeVideo(flags: _1!, duration: _2!, w: _3!, h: _4!, preloadPrefixSize: _5, videoStartTs: _6, videoCodec: _7) } else { return nil diff --git a/submodules/TelegramCallsUI/Sources/CallRatingController.swift b/submodules/TelegramCallsUI/Sources/CallRatingController.swift index d9c9d9b892..fa86baa08a 100644 --- a/submodules/TelegramCallsUI/Sources/CallRatingController.swift +++ b/submodules/TelegramCallsUI/Sources/CallRatingController.swift @@ -291,7 +291,7 @@ func rateCallAndSendLogs(engine: TelegramEngine, callId: CallId, starsCount: Int let id = Int64.random(in: Int64.min ... Int64.max) let name = "\(callId.id)_\(callId.accessHash).log.json" let path = callLogsPath(account: engine.account) + "/" + name - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: id), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: nil, attributes: [.FileName(fileName: name)]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: id), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: nil, attributes: [.FileName(fileName: name)], alternativeRepresentations: []) let message = EnqueueMessage.message(text: comment, attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) return rate |> then(enqueueMessages(account: engine.account, peerId: peerId, messages: [message]) diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index f270f3ab29..4bcace59a3 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -730,6 +730,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { public var myAudioLevel: Signal { return self.myAudioLevelPipe.signal() } + private let myAudioLevelAndSpeakingPipe = ValuePipe<(Float, Bool)>() + public var myAudioLevelAndSpeaking: Signal<(Float, Bool), NoError> { + return self.myAudioLevelAndSpeakingPipe.signal() + } private var myAudioLevelDisposable = MetaDisposable() private var audioSessionControl: ManagedAudioSessionControl? @@ -1957,6 +1961,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { let mappedLevel = myLevel * 1.5 strongSelf.myAudioLevelPipe.putNext(mappedLevel) + strongSelf.myAudioLevelAndSpeakingPipe.putNext((mappedLevel, myLevelHasVoice)) strongSelf.processMyAudioLevel(level: mappedLevel, hasVoice: myLevelHasVoice) strongSelf.isSpeakingPromise.set(orignalMyLevelHasVoice) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatExpandedParticipantThumbnailsComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatExpandedParticipantThumbnailsComponent.swift index c75453e7cf..355c75d06f 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatExpandedParticipantThumbnailsComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatExpandedParticipantThumbnailsComponent.swift @@ -201,7 +201,7 @@ final class VideoChatParticipantThumbnailComponent: Component { text: .plain(NSAttributedString(string: EnginePeer(component.participant.peer).compactDisplayTitle, font: Font.semibold(13.0), textColor: .white)) )), environment: {}, - containerSize: CGSize(width: availableSize.width - 6.0 * 2.0 - 8.0, height: 100.0) + containerSize: CGSize(width: availableSize.width - 6.0 * 2.0 - 12.0, height: 100.0) ) let titleFrame = CGRect(origin: CGPoint(x: 6.0, y: availableSize.height - 6.0 - titleSize.height), size: titleSize) if let titleView = self.title.view { diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift index 53d5df260f..2369a08d5c 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift @@ -134,17 +134,20 @@ private final class BlobView: UIView { final class VideoChatParticipantAvatarComponent: Component { let call: PresentationGroupCall let peer: EnginePeer + let myPeerId: EnginePeer.Id let isSpeaking: Bool let theme: PresentationTheme init( call: PresentationGroupCall, peer: EnginePeer, + myPeerId: EnginePeer.Id, isSpeaking: Bool, theme: PresentationTheme ) { self.call = call self.peer = peer + self.myPeerId = myPeerId self.isSpeaking = isSpeaking self.theme = theme } @@ -159,6 +162,9 @@ final class VideoChatParticipantAvatarComponent: Component { if lhs.isSpeaking != rhs.isSpeaking { return false } + if lhs.myPeerId != rhs.myPeerId { + return false + } if lhs.theme !== rhs.theme { return false } @@ -175,6 +181,7 @@ final class VideoChatParticipantAvatarComponent: Component { private var wasSpeaking: Bool? private var noAudioTimer: Foundation.Timer? + private var lastAudioLevelTimestamp: Double = 0.0 override init(frame: CGRect) { super.init(frame: frame) @@ -189,6 +196,31 @@ final class VideoChatParticipantAvatarComponent: Component { self.noAudioTimer?.invalidate() } + private func checkNoAudio() { + let timestamp = CFAbsoluteTimeGetCurrent() + if self.lastAudioLevelTimestamp + 1.0 < timestamp { + self.noAudioTimer?.invalidate() + self.noAudioTimer = nil + + if let blobView = self.blobView { + let transition: ComponentTransition = .easeInOut(duration: 0.3) + transition.setAlpha(view: blobView, alpha: 0.0, completion: { [weak self, weak blobView] completed in + guard let self, let blobView, completed else { + return + } + if self.blobView === blobView { + self.blobView = nil + } + blobView.removeFromSuperview() + }) + transition.setScale(layer: blobView.layer, scale: 0.5) + if let avatarNode = self.avatarNode { + transition.setScale(view: avatarNode.view, scale: 1.0) + } + } + } + } + func update(component: VideoChatParticipantAvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -254,35 +286,52 @@ final class VideoChatParticipantAvatarComponent: Component { let blobScale: CGFloat = 1.5 if self.audioLevelDisposable == nil { - let peerId = component.peer.id struct Level { var value: Float var isSpeaking: Bool } - self.audioLevelDisposable = (component.call.audioLevels - |> map { levels -> Level? in - for level in levels { - if level.0 == peerId { - return Level(value: level.2, isSpeaking: level.3) + + let peerId = component.peer.id + let levelSignal: Signal + if peerId == component.myPeerId { + levelSignal = component.call.myAudioLevelAndSpeaking + |> map { value, isSpeaking -> Level? in + if value == 0.0 { + return nil + } else { + return Level(value: value, isSpeaking: isSpeaking) } } - return nil + } else { + levelSignal = component.call.audioLevels + |> map { levels -> Level? in + for level in levels { + if level.0 == peerId { + return Level(value: level.2, isSpeaking: level.3) + } + } + return nil + } } + + self.audioLevelDisposable = (levelSignal |> distinctUntilChanged(isEqual: { lhs, rhs in if (lhs == nil) != (rhs == nil) { return false } if lhs != nil { - return true - } else { return false + } else { + return true } }) |> deliverOnMainQueue).startStrict(next: { [weak self] level in guard let self, let component = self.component, let avatarNode = self.avatarNode else { return } - if let level { + if let level, level.value >= 0.1 { + self.lastAudioLevelTimestamp = CFAbsoluteTimeGetCurrent() + let blobView: BlobView if let current = self.blobView { blobView = current @@ -316,6 +365,11 @@ final class VideoChatParticipantAvatarComponent: Component { ComponentTransition.immediate.setTintColor(layer: blobView.blobsLayer, color: component.isSpeaking ? UIColor(rgb: 0x33C758) : component.theme.list.itemAccentColor) } + if blobView.alpha == 0.0 { + let transition: ComponentTransition = .easeInOut(duration: 0.3) + transition.setAlpha(view: blobView, alpha: 1.0) + transition.setScale(view: blobView, scale: 1.0 / blobScale) + } blobView.updateLevel(CGFloat(level.value), immediately: false) if let noAudioTimer = self.noAudioTimer { @@ -323,28 +377,19 @@ final class VideoChatParticipantAvatarComponent: Component { noAudioTimer.invalidate() } } else { - if self.noAudioTimer == nil { - self.noAudioTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.4, repeats: false, block: { [weak self] _ in - guard let self else { - return - } - self.noAudioTimer?.invalidate() - self.noAudioTimer = nil - - if let blobView = self.blobView { - self.blobView = nil - blobView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak blobView] _ in - blobView?.removeFromSuperview() - }) - blobView.layer.animateScale(from: 1.0 / blobScale, to: 0.5, duration: 0.3, removeOnCompletion: false) - let transition: ComponentTransition = .easeInOut(duration: 0.1) - if let avatarNode = self.avatarNode { - transition.setScale(view: avatarNode.view, scale: 1.0) - } - } - }) + if let blobView = self.blobView { + blobView.updateLevel(0.0, immediately: false) } } + + if self.noAudioTimer == nil { + self.noAudioTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.4, repeats: true, block: { [weak self] _ in + guard let self else { + return + } + self.checkNoAudio() + }) + } }) } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift index 836747c627..d295c40a7f 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift @@ -307,6 +307,12 @@ final class VideoChatParticipantVideoComponent: Component { if muteStatusView.superview == nil { self.addSubview(muteStatusView) muteStatusView.alpha = controlsAlpha + + //TODO:release + muteStatusView.layer.shadowOpacity = 0.7 + muteStatusView.layer.shadowColor = UIColor(white: 0.0, alpha: 1.0).cgColor + muteStatusView.layer.shadowOffset = CGSize(width: 0.0, height: 1.0) + muteStatusView.layer.shadowRadius = 8.0 } transition.setPosition(view: muteStatusView, position: muteStatusFrame.center) transition.setBounds(view: muteStatusView, bounds: CGRect(origin: CGPoint(), size: muteStatusFrame.size)) @@ -320,7 +326,7 @@ final class VideoChatParticipantVideoComponent: Component { text: .plain(NSAttributedString(string: component.participant.peer.debugDisplayTitle, font: Font.semibold(16.0), textColor: .white)) )), environment: {}, - containerSize: CGSize(width: availableSize.width - 8.0 * 2.0, height: 100.0) + containerSize: CGSize(width: availableSize.width - 8.0 * 2.0 - 4.0, height: 100.0) ) let titleFrame: CGRect if component.isExpanded { @@ -333,6 +339,12 @@ final class VideoChatParticipantVideoComponent: Component { titleView.layer.anchorPoint = CGPoint() self.addSubview(titleView) titleView.alpha = controlsAlpha + + //TODO:release + titleView.layer.shadowOpacity = 0.7 + titleView.layer.shadowColor = UIColor(white: 0.0, alpha: 1.0).cgColor + titleView.layer.shadowOffset = CGSize(width: 0.0, height: 1.0) + titleView.layer.shadowRadius = 8.0 } transition.setPosition(view: titleView, position: titleFrame.origin) titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift index 18cd09bd5d..c5c197f6e2 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift @@ -1111,6 +1111,7 @@ final class VideoChatParticipantsComponent: Component { avatarComponent: AnyComponent(VideoChatParticipantAvatarComponent( call: component.call, peer: EnginePeer(participant.peer), + myPeerId: component.participants?.myPeerId ?? component.call.accountContext.account.peerId, isSpeaking: component.speakingParticipants.contains(participant.peer.id), theme: component.theme )), diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index 2ca6153719..a193b72312 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -21,18 +21,9 @@ import UndoUI import ShareController import AvatarNode import TelegramAudio - -import PeerInfoUI - -import DeleteChatPeerActionSheetItem -import PeerListItemComponent import LegacyComponents -import LegacyUI -import WebSearchUI -import MapResourceToAvatarSizes -import LegacyMediaPickerUI -private final class VideoChatScreenComponent: Component { +final class VideoChatScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let initialData: VideoChatScreenV2Impl.InitialData @@ -59,60 +50,60 @@ private final class VideoChatScreenComponent: Component { } final class View: UIView { - private let containerView: UIView + let containerView: UIView - private var component: VideoChatScreenComponent? - private var environment: ViewControllerComponentContainer.Environment? - private weak var state: EmptyComponentState? - private var isUpdating: Bool = false + var component: VideoChatScreenComponent? + var environment: ViewControllerComponentContainer.Environment? + weak var state: EmptyComponentState? + var isUpdating: Bool = false private var panGestureState: PanGestureState? - private var notifyDismissedInteractivelyOnPanGestureApply: Bool = false - private var completionOnPanGestureApply: (() -> Void)? + var notifyDismissedInteractivelyOnPanGestureApply: Bool = false + var completionOnPanGestureApply: (() -> Void)? - private let videoRenderingContext = VideoRenderingContext() + let videoRenderingContext = VideoRenderingContext() - private let title = ComponentView() - private let navigationLeftButton = ComponentView() - private let navigationRightButton = ComponentView() - private var navigationSidebarButton: ComponentView? + let title = ComponentView() + let navigationLeftButton = ComponentView() + let navigationRightButton = ComponentView() + var navigationSidebarButton: ComponentView? - private let videoButton = ComponentView() - private let leaveButton = ComponentView() - private let microphoneButton = ComponentView() + let videoButton = ComponentView() + let leaveButton = ComponentView() + let microphoneButton = ComponentView() - private let participants = ComponentView() + let participants = ComponentView() - private var reconnectedAsEventsDisposable: Disposable? + var reconnectedAsEventsDisposable: Disposable? - private var peer: EnginePeer? - private var callState: PresentationGroupCallState? - private var stateDisposable: Disposable? + var peer: EnginePeer? + var callState: PresentationGroupCallState? + var stateDisposable: Disposable? - private var audioOutputState: ([AudioSessionOutput], AudioSessionOutput?)? - private var audioOutputStateDisposable: Disposable? + var audioOutputState: ([AudioSessionOutput], AudioSessionOutput?)? + var audioOutputStateDisposable: Disposable? - private var displayAsPeers: [FoundPeer]? - private var displayAsPeersDisposable: Disposable? + var displayAsPeers: [FoundPeer]? + var displayAsPeersDisposable: Disposable? - private var inviteLinks: GroupCallInviteLinks? - private var inviteLinksDisposable: Disposable? + var inviteLinks: GroupCallInviteLinks? + var inviteLinksDisposable: Disposable? - private var isPushToTalkActive: Bool = false + var isPushToTalkActive: Bool = false - private var members: PresentationGroupCallMembers? - private var membersDisposable: Disposable? + var members: PresentationGroupCallMembers? + var membersDisposable: Disposable? - private let isPresentedValue = ValuePromise(false, ignoreRepeated: true) - private var applicationStateDisposable: Disposable? + let isPresentedValue = ValuePromise(false, ignoreRepeated: true) + var applicationStateDisposable: Disposable? - private var expandedParticipantsVideoState: VideoChatParticipantsComponent.ExpandedVideoState? - private var isTwoColumnSidebarHidden: Bool = false + var expandedParticipantsVideoState: VideoChatParticipantsComponent.ExpandedVideoState? + var isTwoColumnSidebarHidden: Bool = false - private let inviteDisposable = MetaDisposable() - private let currentAvatarMixin = Atomic(value: nil) - private let updateAvatarDisposable = MetaDisposable() - private var currentUpdatingAvatar: (TelegramMediaImageRepresentation, Float)? + let inviteDisposable = MetaDisposable() + let currentAvatarMixin = Atomic(value: nil) + let updateAvatarDisposable = MetaDisposable() + var currentUpdatingAvatar: (TelegramMediaImageRepresentation, Float)? override init(frame: CGRect) { self.containerView = UIView() @@ -191,1180 +182,7 @@ private final class VideoChatScreenComponent: Component { } } - private func openMoreMenu() { - guard let sourceView = self.navigationLeftButton.view else { - return - } - guard let component = self.component, let environment = self.environment, let controller = environment.controller() else { - return - } - guard let peer = self.peer else { - return - } - guard let callState = self.callState else { - return - } - - let canManageCall = callState.canManageCall - - var items: [ContextMenuItem] = [] - - if let displayAsPeers = self.displayAsPeers, displayAsPeers.count > 1 { - for peer in displayAsPeers { - if peer.peer.id == callState.myPeerId { - let avatarSize = CGSize(width: 28.0, height: 28.0) - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_DisplayAs, textLayout: .secondLineWithValue(EnginePeer(peer.peer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: component.call.accountContext.account, peer: EnginePeer(peer.peer), size: avatarSize)), action: { [weak self] c, _ in - guard let self else { - return - } - c?.pushItems(items: .single(ContextController.Items(content: .list(self.contextMenuDisplayAsItems())))) - }))) - items.append(.separator) - break - } - } - } - - if let (availableOutputs, currentOutput) = self.audioOutputState, availableOutputs.count > 1 { - var currentOutputTitle = "" - for output in availableOutputs { - if output == currentOutput { - let title: String - switch output { - case .builtin: - title = UIDevice.current.model - case .speaker: - title = environment.strings.Call_AudioRouteSpeaker - case .headphones: - title = environment.strings.Call_AudioRouteHeadphones - case let .port(port): - title = port.name - } - currentOutputTitle = title - break - } - } - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_ContextAudio, textLayout: .secondLineWithValue(currentOutputTitle), icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Audio"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] c, _ in - guard let self else { - return - } - c?.pushItems(items: .single(ContextController.Items(content: .list(self.contextMenuAudioItems())))) - }))) - } - - if canManageCall { - let text: String - if case let .channel(channel) = peer, case .broadcast = channel.info { - text = environment.strings.LiveStream_EditTitle - } else { - text = environment.strings.VoiceChat_EditTitle - } - items.append(.action(ContextMenuActionItem(text: text, icon: { theme -> UIImage? in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pencil"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] _, f in - f(.default) - - guard let self else { - return - } - self.openTitleEditing() - }))) - - var hasPermissions = true - if case let .channel(chatPeer) = peer { - if case .broadcast = chatPeer.info { - hasPermissions = false - } else if chatPeer.flags.contains(.isGigagroup) { - hasPermissions = false - } - } - if hasPermissions { - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_EditPermissions, icon: { theme -> UIImage? in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] c, _ in - guard let self else { - return - } - c?.pushItems(items: .single(ContextController.Items(content: .list(self.contextMenuPermissionItems())))) - }))) - } - } - - if let inviteLinks = self.inviteLinks { - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_Share, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] _, f in - f(.default) - - guard let self else { - return - } - self.presentShare(inviteLinks) - }))) - } - - //let isScheduled = strongSelf.isScheduled - //TODO:release - let isScheduled: Bool = !"".isEmpty - - let canSpeak: Bool - if let muteState = callState.muteState { - canSpeak = muteState.canUnmute - } else { - canSpeak = true - } - - if !isScheduled && canSpeak { - if #available(iOS 15.0, *) { - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_MicrophoneModes, textColor: .primary, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Noise"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - f(.dismissWithoutContent) - AVCaptureDevice.showSystemUserInterface(.microphoneModes) - }))) - } - } - - if callState.isVideoEnabled && (callState.muteState?.canUnmute ?? true) { - if component.call.hasScreencast { - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_StopScreenSharing, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ShareScreen"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] _, f in - f(.default) - - guard let self, let component = self.component else { - return - } - component.call.disableScreencast() - }))) - } else { - items.append(.custom(VoiceChatShareScreenContextItem(context: component.call.accountContext, text: environment.strings.VoiceChat_ShareScreen, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ShareScreen"), color: theme.actionSheet.primaryTextColor) - }, action: { _, _ in }), false)) - } - } - - if canManageCall { - if let recordingStartTimestamp = callState.recordingStartTimestamp { - items.append(.custom(VoiceChatRecordingContextItem(timestamp: recordingStartTimestamp, action: { [weak self] _, f in - f(.dismissWithoutContent) - - guard let self, let component = self.component, let environment = self.environment else { - return - } - - let alertController = textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: nil, text: environment.strings.VoiceChat_StopRecordingTitle, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: environment.strings.VoiceChat_StopRecordingStop, action: { [weak self] in - guard let self, let component = self.component, let environment = self.environment else { - return - } - component.call.setShouldBeRecording(false, title: nil, videoOrientation: nil) - - Queue.mainQueue().after(0.88) { - HapticFeedback().success() - } - - let text: String - if case let .channel(channel) = self.peer, case .broadcast = channel.info { - text = environment.strings.LiveStream_RecordingSaved - } else { - text = environment.strings.VideoChat_RecordingSaved - } - self.presentUndoOverlay(content: .forward(savedMessages: true, text: text), action: { [weak self] value in - if case .info = value, let self, let component = self.component, let environment = self.environment, let navigationController = environment.controller()?.navigationController as? NavigationController { - let context = component.call.accountContext - environment.controller()?.dismiss(completion: { [weak navigationController] in - Queue.mainQueue().justDispatch { - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) - |> deliverOnMainQueue).start(next: { peer in - guard let peer, let navigationController else { - return - } - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), keepStack: .always, purposefulAction: {}, peekData: nil)) - }) - } - }) - - return true - } - return false - }) - })]) - environment.controller()?.present(alertController, in: .window(.root)) - }), false)) - } else { - let text: String - if case let .channel(channel) = peer, case .broadcast = channel.info { - text = environment.strings.LiveStream_StartRecording - } else { - text = environment.strings.VoiceChat_StartRecording - } - if callState.scheduleTimestamp == nil { - items.append(.action(ContextMenuActionItem(text: text, icon: { theme -> UIImage? in - return generateStartRecordingIcon(color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] _, f in - f(.dismissWithoutContent) - - guard let self, let component = self.component, let environment = self.environment, let peer = self.peer else { - return - } - - let controller = VoiceChatRecordingSetupController(context: component.call.accountContext, peer: peer, completion: { [weak self] videoOrientation in - guard let self, let component = self.component, let environment = self.environment, let peer = self.peer else { - return - } - let title: String - let text: String - let placeholder: String - if let _ = videoOrientation { - placeholder = environment.strings.VoiceChat_RecordingTitlePlaceholderVideo - } else { - placeholder = environment.strings.VoiceChat_RecordingTitlePlaceholder - } - if case let .channel(channel) = peer, case .broadcast = channel.info { - title = environment.strings.LiveStream_StartRecordingTitle - if let _ = videoOrientation { - text = environment.strings.LiveStream_StartRecordingTextVideo - } else { - text = environment.strings.LiveStream_StartRecordingText - } - } else { - title = environment.strings.VoiceChat_StartRecordingTitle - if let _ = videoOrientation { - text = environment.strings.VoiceChat_StartRecordingTextVideo - } else { - text = environment.strings.VoiceChat_StartRecordingText - } - } - - let controller = voiceChatTitleEditController(sharedContext: component.call.accountContext.sharedContext, account: component.call.account, forceTheme: environment.theme, title: title, text: text, placeholder: placeholder, value: nil, maxLength: 40, apply: { [weak self] title in - guard let self, let component = self.component, let environment = self.environment, let peer = self.peer, let title else { - return - } - - component.call.setShouldBeRecording(true, title: title, videoOrientation: videoOrientation) - - let text: String - if case let .channel(channel) = peer, case .broadcast = channel.info { - text = environment.strings.LiveStream_RecordingStarted - } else { - text = environment.strings.VoiceChat_RecordingStarted - } - - self.presentUndoOverlay(content: .voiceChatRecording(text: text), action: { _ in return false }) - component.call.playTone(.recordingStarted) - }) - environment.controller()?.present(controller, in: .window(.root)) - }) - environment.controller()?.present(controller, in: .window(.root)) - }))) - } - } - } - - if canManageCall { - let text: String - if case let .channel(channel) = peer, case .broadcast = channel.info { - text = isScheduled ? environment.strings.VoiceChat_CancelLiveStream : environment.strings.VoiceChat_EndLiveStream - } else { - text = isScheduled ? environment.strings.VoiceChat_CancelVoiceChat : environment.strings.VoiceChat_EndVoiceChat - } - items.append(.action(ContextMenuActionItem(text: text, textColor: .destructive, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor) - }, action: { [weak self] _, f in - f(.dismissWithoutContent) - - guard let self, let component = self.component, let environment = self.environment else { - return - } - - let action: () -> Void = { [weak self] in - guard let self, let component = self.component else { - return - } - - let _ = (component.call.leave(terminateIfPossible: true) - |> filter { $0 } - |> take(1) - |> deliverOnMainQueue).start(completed: { [weak self] in - guard let self, let environment = self.environment else { - return - } - environment.controller()?.dismiss() - }) - } - - let title: String - let text: String - if case let .channel(channel) = self.peer, case .broadcast = channel.info { - title = isScheduled ? environment.strings.LiveStream_CancelConfirmationTitle : environment.strings.LiveStream_EndConfirmationTitle - text = isScheduled ? environment.strings.LiveStream_CancelConfirmationText : environment.strings.LiveStream_EndConfirmationText - } else { - title = isScheduled ? environment.strings.VoiceChat_CancelConfirmationTitle : environment.strings.VoiceChat_EndConfirmationTitle - text = isScheduled ? environment.strings.VoiceChat_CancelConfirmationText : environment.strings.VoiceChat_EndConfirmationText - } - - let alertController = textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: isScheduled ? environment.strings.VoiceChat_CancelConfirmationEnd : environment.strings.VoiceChat_EndConfirmationEnd, action: { - action() - })]) - environment.controller()?.present(alertController, in: .window(.root)) - }))) - } else { - let leaveText: String - if case let .channel(channel) = peer, case .broadcast = channel.info { - leaveText = environment.strings.LiveStream_LeaveVoiceChat - } else { - leaveText = environment.strings.VoiceChat_LeaveVoiceChat - } - items.append(.action(ContextMenuActionItem(text: leaveText, textColor: .destructive, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor) - }, action: { [weak self] _, f in - f(.dismissWithoutContent) - - guard let self, let component = self.component else { - return - } - - let _ = (component.call.leave(terminateIfPossible: false) - |> filter { $0 } - |> take(1) - |> deliverOnMainQueue).start(completed: { [weak self] in - guard let self, let environment = self.environment else { - return - } - environment.controller()?.dismiss() - }) - }))) - } - - let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) - let contextController = ContextController(presentationData: presentationData, source: .reference(VoiceChatContextReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) - controller.presentInGlobalOverlay(contextController) - } - - private func contextMenuDisplayAsItems() -> [ContextMenuItem] { - guard let component = self.component, let environment = self.environment else { - return [] - } - guard let callState = self.callState else { - return [] - } - let myPeerId = callState.myPeerId - - let avatarSize = CGSize(width: 28.0, height: 28.0) - - var items: [ContextMenuItem] = [] - - items.append(.action(ContextMenuActionItem(text: environment.strings.Common_Back, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) - }, iconPosition: .left, action: { (c, _) in - c?.popItems() - }))) - items.append(.separator) - - var isGroup = false - if let displayAsPeers = self.displayAsPeers { - for peer in displayAsPeers { - if peer.peer is TelegramGroup { - isGroup = true - break - } else if let peer = peer.peer as? TelegramChannel, case .group = peer.info { - isGroup = true - break - } - } - } - - items.append(.custom(VoiceChatInfoContextItem(text: isGroup ? environment.strings.VoiceChat_DisplayAsInfoGroup : environment.strings.VoiceChat_DisplayAsInfo, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Accounts"), color: theme.actionSheet.primaryTextColor) - }), true)) - - if let displayAsPeers = self.displayAsPeers { - for peer in displayAsPeers { - var subtitle: String? - if peer.peer.id.namespace == Namespaces.Peer.CloudUser { - subtitle = environment.strings.VoiceChat_PersonalAccount - } else if let subscribers = peer.subscribers { - if let peer = peer.peer as? TelegramChannel, case .broadcast = peer.info { - subtitle = environment.strings.Conversation_StatusSubscribers(subscribers) - } else { - subtitle = environment.strings.Conversation_StatusMembers(subscribers) - } - } - - let isSelected = peer.peer.id == myPeerId - let extendedAvatarSize = CGSize(width: 35.0, height: 35.0) - let theme = environment.theme - let avatarSignal = peerAvatarCompleteImage(account: component.call.accountContext.account, peer: EnginePeer(peer.peer), size: avatarSize) - |> map { image -> UIImage? in - if isSelected, let image = image { - return generateImage(extendedAvatarSize, rotatedContext: { size, context in - let bounds = CGRect(origin: CGPoint(), size: size) - context.clear(bounds) - context.translateBy(x: size.width / 2.0, y: size.height / 2.0) - context.scaleBy(x: 1.0, y: -1.0) - context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) - context.draw(image.cgImage!, in: CGRect(x: (extendedAvatarSize.width - avatarSize.width) / 2.0, y: (extendedAvatarSize.height - avatarSize.height) / 2.0, width: avatarSize.width, height: avatarSize.height)) - - let lineWidth = 1.0 + UIScreenPixel - context.setLineWidth(lineWidth) - context.setStrokeColor(theme.actionSheet.controlAccentColor.cgColor) - context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0)) - }) - } else { - return image - } - } - - items.append(.action(ContextMenuActionItem(text: EnginePeer(peer.peer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: isSelected ? extendedAvatarSize : avatarSize, signal: avatarSignal), action: { [weak self] _, f in - f(.default) - - guard let self, let component = self.component else { - return - } - - if peer.peer.id != myPeerId { - component.call.reconnect(as: peer.peer.id) - } - }))) - - if peer.peer.id.namespace == Namespaces.Peer.CloudUser { - items.append(.separator) - } - } - } - return items - } - - private func contextMenuAudioItems() -> [ContextMenuItem] { - guard let environment = self.environment else { - return [] - } - guard let (availableOutputs, currentOutput) = self.audioOutputState else { - return [] - } - - var items: [ContextMenuItem] = [] - - items.append(.action(ContextMenuActionItem(text: environment.strings.Common_Back, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) - }, iconPosition: .left, action: { (c, _) in - c?.popItems() - }))) - items.append(.separator) - - for output in availableOutputs { - let title: String - switch output { - case .builtin: - title = UIDevice.current.model - case .speaker: - title = environment.strings.Call_AudioRouteSpeaker - case .headphones: - title = environment.strings.Call_AudioRouteHeadphones - case let .port(port): - title = port.name - } - items.append(.action(ContextMenuActionItem(text: title, icon: { theme in - if output == currentOutput { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor) - } else { - return nil - } - }, action: { [weak self] _, f in - f(.default) - - guard let self, let component = self.component else { - return - } - - component.call.setCurrentAudioOutput(output) - }))) - } - - return items - } - - private func contextMenuPermissionItems() -> [ContextMenuItem] { - guard let environment = self.environment, let callState = self.callState else { - return [] - } - - var items: [ContextMenuItem] = [] - if callState.canManageCall, let defaultParticipantMuteState = callState.defaultParticipantMuteState { - let isMuted = defaultParticipantMuteState == .muted - - items.append(.action(ContextMenuActionItem(text: environment.strings.Common_Back, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) - }, iconPosition: .left, action: { (c, _) in - c?.popItems() - }))) - items.append(.separator) - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_SpeakPermissionEveryone, icon: { theme in - if isMuted { - return nil - } else { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor) - } - }, action: { [weak self] _, f in - f(.dismissWithoutContent) - - guard let self, let component = self.component else { - return - } - component.call.updateDefaultParticipantsAreMuted(isMuted: false) - }))) - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_SpeakPermissionAdmin, icon: { theme in - if !isMuted { - return nil - } else { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor) - } - }, action: { [weak self] _, f in - f(.dismissWithoutContent) - - guard let self, let component = self.component else { - return - } - component.call.updateDefaultParticipantsAreMuted(isMuted: true) - }))) - } - return items - } - - private func openParticipantContextMenu(id: EnginePeer.Id, sourceView: ContextExtractedContentContainingView, gesture: ContextGesture?) { - guard let component = self.component, let environment = self.environment else { - return - } - guard let members = self.members, let participant = members.participants.first(where: { $0.peer.id == id }) else { - return - } - - let muteStatePromise = Promise(participant.muteState) - - let itemsForEntry: (GroupCallParticipantsContext.Participant.MuteState?) -> [ContextMenuItem] = { [weak self] muteState in - guard let self, let component = self.component, let environment = self.environment else { - return [] - } - guard let callState = self.callState else { - return [] - } - - var items: [ContextMenuItem] = [] - - var hasVolumeSlider = false - let peer = participant.peer - if let muteState = muteState, !muteState.canUnmute || muteState.mutedByYou { - } else { - if callState.canManageCall || callState.myPeerId != id { - hasVolumeSlider = true - - let minValue: CGFloat - if callState.canManageCall && callState.adminIds.contains(peer.id) && muteState != nil { - minValue = 0.01 - } else { - minValue = 0.0 - } - items.append(.custom(VoiceChatVolumeContextItem(minValue: minValue, value: participant.volume.flatMap { CGFloat($0) / 10000.0 } ?? 1.0, valueChanged: { [weak self] newValue, finished in - guard let self, let component = self.component else { - return - } - - if finished && newValue.isZero { - let updatedMuteState = component.call.updateMuteState(peerId: peer.id, isMuted: true) - muteStatePromise.set(.single(updatedMuteState)) - } else { - component.call.setVolume(peerId: peer.id, volume: Int32(newValue * 10000), sync: finished) - } - }), true)) - } - } - - if callState.myPeerId == id && !hasVolumeSlider && ((participant.about?.isEmpty ?? true) || participant.peer.smallProfileImage == nil) { - items.append(.custom(VoiceChatInfoContextItem(text: environment.strings.VoiceChat_ImproveYourProfileText, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Tip"), color: theme.actionSheet.primaryTextColor) - }), true)) - } - - if peer.id == callState.myPeerId { - if participant.hasRaiseHand { - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_CancelSpeakRequest, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/RevokeSpeak"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] _, f in - guard let self, let component = self.component else { - return - } - component.call.lowerHand() - - f(.default) - }))) - } - items.append(.action(ContextMenuActionItem(text: peer.smallProfileImage == nil ? environment.strings.VoiceChat_AddPhoto : environment.strings.VoiceChat_ChangePhoto, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Camera"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] _, f in - f(.default) - Queue.mainQueue().after(0.1) { - guard let self else { - return - } - - self.openAvatarForEditing(fromGallery: false, completion: {}) - } - }))) - - items.append(.action(ContextMenuActionItem(text: (participant.about?.isEmpty ?? true) ? environment.strings.VoiceChat_AddBio : environment.strings.VoiceChat_EditBio, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] _, f in - f(.default) - - Queue.mainQueue().after(0.1) { - guard let self, let component = self.component, let environment = self.environment else { - return - } - let maxBioLength: Int - if peer.id.namespace == Namespaces.Peer.CloudUser { - maxBioLength = 70 - } else { - maxBioLength = 100 - } - let controller = voiceChatTitleEditController(sharedContext: component.call.accountContext.sharedContext, account: component.call.accountContext.account, forceTheme: environment.theme, title: environment.strings.VoiceChat_EditBioTitle, text: environment.strings.VoiceChat_EditBioText, placeholder: environment.strings.VoiceChat_EditBioPlaceholder, doneButtonTitle: environment.strings.VoiceChat_EditBioSave, value: participant.about, maxLength: maxBioLength, apply: { [weak self] bio in - guard let self, let component = self.component, let environment = self.environment, let bio else { - return - } - if peer.id.namespace == Namespaces.Peer.CloudUser { - let _ = (component.call.accountContext.engine.accountData.updateAbout(about: bio) - |> `catch` { _ -> Signal in - return .complete() - }).start() - } else { - let _ = (component.call.accountContext.engine.peers.updatePeerDescription(peerId: peer.id, description: bio) - |> `catch` { _ -> Signal in - return .complete() - }).start() - } - - self.presentUndoOverlay(content: .info(title: nil, text: environment.strings.VoiceChat_EditBioSuccess, timeout: nil, customUndoText: nil), action: { _ in return false }) - }) - environment.controller()?.present(controller, in: .window(.root)) - } - }))) - - if let peer = peer as? TelegramUser { - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_ChangeName, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ChangeName"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] _, f in - f(.default) - - Queue.mainQueue().after(0.1) { - guard let self, let component = self.component, let environment = self.environment else { - return - } - let controller = voiceChatUserNameController(sharedContext: component.call.accountContext.sharedContext, account: component.call.accountContext.account, forceTheme: environment.theme, title: environment.strings.VoiceChat_ChangeNameTitle, firstNamePlaceholder: environment.strings.UserInfo_FirstNamePlaceholder, lastNamePlaceholder: environment.strings.UserInfo_LastNamePlaceholder, doneButtonTitle: environment.strings.VoiceChat_EditBioSave, firstName: peer.firstName, lastName: peer.lastName, maxLength: 128, apply: { [weak self] firstAndLastName in - guard let self, let component = self.component, let environment = self.environment, let (firstName, lastName) = firstAndLastName else { - return - } - let _ = component.call.accountContext.engine.accountData.updateAccountPeerName(firstName: firstName, lastName: lastName).startStandalone() - - self.presentUndoOverlay(content: .info(title: nil, text: environment.strings.VoiceChat_EditNameSuccess, timeout: nil, customUndoText: nil), action: { _ in return false }) - }) - environment.controller()?.present(controller, in: .window(.root)) - } - }))) - } - } else { - if (callState.canManageCall || callState.adminIds.contains(component.call.accountContext.account.peerId)) { - if callState.adminIds.contains(peer.id) { - if let _ = muteState { - } else { - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_MutePeer, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] _, f in - guard let self, let component = self.component else { - return - } - - let _ = component.call.updateMuteState(peerId: peer.id, isMuted: true) - f(.default) - }))) - } - } else { - if let muteState = muteState, !muteState.canUnmute { - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_UnmutePeer, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: participant.hasRaiseHand ? "Call/Context Menu/AllowToSpeak" : "Call/Context Menu/Unmute"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] _, f in - guard let self, let component = self.component, let environment = self.environment else { - return - } - - let _ = component.call.updateMuteState(peerId: peer.id, isMuted: false) - f(.default) - - self.presentUndoOverlay(content: .voiceChatCanSpeak(text: environment.strings.VoiceChat_UserCanNowSpeak(EnginePeer(participant.peer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string), action: { _ in return true }) - }))) - } else { - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_MutePeer, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] _, f in - guard let self, let component = self.component else { - return - } - - let _ = component.call.updateMuteState(peerId: peer.id, isMuted: true) - f(.default) - }))) - } - } - } else { - if let muteState = muteState, muteState.mutedByYou { - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_UnmuteForMe, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Unmute"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] _, f in - guard let self, let component = self.component else { - return - } - - let _ = component.call.updateMuteState(peerId: peer.id, isMuted: false) - f(.default) - }))) - } else { - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_MuteForMe, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] _, f in - guard let self, let component = self.component else { - return - } - - let _ = component.call.updateMuteState(peerId: peer.id, isMuted: true) - f(.default) - }))) - } - } - - let openTitle: String - let openIcon: UIImage? - if [Namespaces.Peer.CloudChannel, Namespaces.Peer.CloudGroup].contains(peer.id.namespace) { - if let peer = peer as? TelegramChannel, case .broadcast = peer.info { - openTitle = environment.strings.VoiceChat_OpenChannel - openIcon = UIImage(bundleImageName: "Chat/Context Menu/Channels") - } else { - openTitle = environment.strings.VoiceChat_OpenGroup - openIcon = UIImage(bundleImageName: "Chat/Context Menu/Groups") - } - } else { - openTitle = environment.strings.Conversation_ContextMenuSendMessage - openIcon = UIImage(bundleImageName: "Chat/Context Menu/Message") - } - items.append(.action(ContextMenuActionItem(text: openTitle, icon: { theme in - return generateTintedImage(image: openIcon, color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] _, f in - guard let self, let component = self.component, let environment = self.environment else { - return - } - - guard let controller = environment.controller() as? VideoChatScreenV2Impl, let navigationController = controller.parentNavigationController else { - return - } - - let context = component.call.accountContext - environment.controller()?.dismiss(completion: { [weak navigationController] in - Queue.mainQueue().after(0.3) { - guard let navigationController else { - return - } - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(EnginePeer(peer)), keepStack: .always, purposefulAction: {}, peekData: nil)) - } - }) - - f(.dismissWithoutContent) - }))) - - if (callState.canManageCall && !callState.adminIds.contains(peer.id)), peer.id != component.call.peerId { - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_RemovePeer, textColor: .destructive, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor) - }, action: { [weak self] c, _ in - c?.dismiss(completion: { - guard let self, let component = self.component else { - return - } - - let _ = (component.call.accountContext.account.postbox.loadedPeerWithId(component.call.peerId) - |> deliverOnMainQueue).start(next: { [weak self] chatPeer in - guard let self, let component = self.component, let environment = self.environment else { - return - } - - let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) - let actionSheet = ActionSheetController(presentationData: presentationData) - var items: [ActionSheetItem] = [] - - let nameDisplayOrder = presentationData.nameDisplayOrder - items.append(DeleteChatPeerActionSheetItem(context: component.call.accountContext, peer: EnginePeer(peer), chatPeer: EnginePeer(chatPeer), action: .removeFromGroup, strings: environment.strings, nameDisplayOrder: nameDisplayOrder)) - - items.append(ActionSheetButtonItem(title: environment.strings.VoiceChat_RemovePeerRemove, color: .destructive, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - - guard let self, let component = self.component, let environment = self.environment else { - return - } - - let _ = component.call.accountContext.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: component.call.accountContext.engine, peerId: component.call.peerId, memberId: peer.id, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max)).start() - component.call.removedPeer(peer.id) - - self.presentUndoOverlay(content: .banned(text: environment.strings.VoiceChat_RemovedPeerText(EnginePeer(peer).displayTitle(strings: environment.strings, displayOrder: nameDisplayOrder)).string), action: { _ in return false }) - })) - - actionSheet.setItemGroups([ - ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: environment.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ]) - ]) - environment.controller()?.present(actionSheet, in: .window(.root)) - }) - }) - }))) - } - } - return items - } - - let items = muteStatePromise.get() - |> map { muteState -> [ContextMenuItem] in - return itemsForEntry(muteState) - } - - let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) - let contextController = ContextController( - presentationData: presentationData, - source: .extracted(ParticipantExtractedContentSource(contentView: sourceView)), - items: items |> map { items in - return ContextController.Items(content: .list(items)) - }, - recognizer: nil, - gesture: gesture - ) - - environment.controller()?.forEachController({ controller in - if let controller = controller as? UndoOverlayController { - controller.dismiss() - } - return true - }) - - environment.controller()?.presentInGlobalOverlay(contextController) - } - - private func openAvatarForEditing(fromGallery: Bool = false, completion: @escaping () -> Void = {}) { - guard let component = self.component else { - return - } - guard let callState = self.callState else { - return - } - let peerId = callState.myPeerId - - let _ = (component.call.accountContext.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), - TelegramEngine.EngineData.Item.Configuration.SearchBots() - ) - |> deliverOnMainQueue).start(next: { [weak self] peer, searchBotsConfiguration in - guard let self, let component = self.component, let environment = self.environment else { - return - } - guard let peer else { - return - } - - let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) - - let legacyController = LegacyController(presentation: .custom, theme: environment.theme) - legacyController.statusBar.statusBarStyle = .Ignore - - let emptyController = LegacyEmptyController(context: legacyController.context)! - let navigationController = makeLegacyNavigationController(rootController: emptyController) - navigationController.setNavigationBarHidden(true, animated: false) - navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0) - - legacyController.bind(controller: navigationController) - - self.endEditing(true) - environment.controller()?.present(legacyController, in: .window(.root)) - - var hasPhotos = false - if !peer.profileImageRepresentations.isEmpty { - hasPhotos = true - } - - let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasPhotos && !fromGallery, hasViewButton: false, personalPhoto: peerId.namespace == Namespaces.Peer.CloudUser, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: false, title: nil, isSuggesting: false)! - mixin.forceDark = true - mixin.stickersContext = LegacyPaintStickersContext(context: component.call.accountContext) - let _ = self.currentAvatarMixin.swap(mixin) - mixin.requestSearchController = { [weak self] assetsController in - guard let self, let component = self.component, let environment = self.environment else { - return - } - let controller = WebSearchController(context: component.call.accountContext, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: peer.id.namespace == Namespaces.Peer.CloudUser ? nil : peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder), completion: { [weak self] result in - assetsController?.dismiss() - - guard let self else { - return - } - self.updateProfilePhoto(result) - })) - controller.navigationPresentation = .modal - environment.controller()?.push(controller) - - if fromGallery { - completion() - } - } - mixin.didFinishWithImage = { [weak self] image in - if let image = image { - completion() - self?.updateProfilePhoto(image) - } - } - mixin.didFinishWithVideo = { [weak self] image, asset, adjustments in - if let image = image, let asset = asset { - completion() - self?.updateProfileVideo(image, asset: asset, adjustments: adjustments) - } - } - mixin.didFinishWithDelete = { [weak self] in - guard let self, let environment = self.environment else { - return - } - - let proceed = { [weak self] in - guard let self, let component = self.component else { - return - } - - let _ = self.currentAvatarMixin.swap(nil) - let postbox = component.call.accountContext.account.postbox - self.updateAvatarDisposable.set((component.call.accountContext.engine.peers.updatePeerPhoto(peerId: peerId, photo: nil, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) - }) - |> deliverOnMainQueue).start()) - } - - let actionSheet = ActionSheetController(presentationData: presentationData) - let items: [ActionSheetItem] = [ - ActionSheetButtonItem(title: environment.strings.Settings_RemoveConfirmation, color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - proceed() - }) - ] - - actionSheet.setItemGroups([ - ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ]) - ]) - environment.controller()?.present(actionSheet, in: .window(.root)) - } - mixin.didDismiss = { [weak self, weak legacyController] in - guard let self else { - return - } - let _ = self.currentAvatarMixin.swap(nil) - legacyController?.dismiss() - } - let menuController = mixin.present() - if let menuController = menuController { - menuController.customRemoveFromParentViewController = { [weak legacyController] in - legacyController?.dismiss() - } - } - }) - } - - private func updateProfilePhoto(_ image: UIImage) { - guard let component = self.component else { - return - } - guard let callState = self.callState else { - return - } - guard let data = image.jpegData(compressionQuality: 0.6) else { - return - } - - let peerId = callState.myPeerId - - let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - component.call.account.postbox.mediaBox.storeResourceData(resource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) - - self.currentUpdatingAvatar = (representation, 0.0) - - let postbox = component.call.account.postbox - let signal = peerId.namespace == Namespaces.Peer.CloudUser ? component.call.accountContext.engine.accountData.updateAccountPhoto(resource: resource, videoResource: nil, videoStartTimestamp: nil, markup: nil, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) - }) : component.call.accountContext.engine.peers.updatePeerPhoto(peerId: peerId, photo: component.call.accountContext.engine.peers.uploadedPeerPhoto(resource: resource), mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) - }) - - self.updateAvatarDisposable.set((signal - |> deliverOnMainQueue).start(next: { [weak self] result in - guard let self else { - return - } - switch result { - case .complete: - self.currentUpdatingAvatar = nil - self.state?.updated(transition: .spring(duration: 0.4)) - case let .progress(value): - self.currentUpdatingAvatar = (representation, value) - } - })) - - self.state?.updated(transition: .spring(duration: 0.4)) - } - - private func updateProfileVideo(_ image: UIImage, asset: Any?, adjustments: TGVideoEditAdjustments?) { - guard let component = self.component else { - return - } - guard let callState = self.callState else { - return - } - guard let data = image.jpegData(compressionQuality: 0.6) else { - return - } - let peerId = callState.myPeerId - - let photoResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - component.call.accountContext.account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) - - self.currentUpdatingAvatar = (representation, 0.0) - - var videoStartTimestamp: Double? = nil - if let adjustments = adjustments, adjustments.videoStartValue > 0.0 { - videoStartTimestamp = adjustments.videoStartValue - adjustments.trimStartValue - } - - let context = component.call.accountContext - let account = context.account - let signal = Signal { [weak self] subscriber in - let entityRenderer: LegacyPaintEntityRenderer? = adjustments.flatMap { adjustments in - if let paintingData = adjustments.paintingData, paintingData.hasAnimation { - return LegacyPaintEntityRenderer(postbox: account.postbox, adjustments: adjustments) - } else { - return nil - } - } - - let tempFile = EngineTempBox.shared.tempFile(fileName: "video.mp4") - let uploadInterface = LegacyLiveUploadInterface(context: context) - let signal: SSignal - if let url = asset as? URL, url.absoluteString.hasSuffix(".jpg"), let data = try? Data(contentsOf: url, options: [.mappedRead]), let image = UIImage(data: data), let entityRenderer = entityRenderer { - let durationSignal: SSignal = SSignal(generator: { subscriber in - let disposable = (entityRenderer.duration()).start(next: { duration in - subscriber.putNext(duration) - subscriber.putCompletion() - }) - - return SBlockDisposable(block: { - disposable.dispose() - }) - }) - signal = durationSignal.map(toSignal: { duration -> SSignal in - if let duration = duration as? Double { - return TGMediaVideoConverter.renderUIImage(image, duration: duration, adjustments: adjustments, path: tempFile.path, watcher: nil, entityRenderer: entityRenderer)! - } else { - return SSignal.single(nil) - } - }) - - } else if let asset = asset as? AVAsset { - signal = TGMediaVideoConverter.convert(asset, adjustments: adjustments, path: tempFile.path, watcher: uploadInterface, entityRenderer: entityRenderer)! - } else { - signal = SSignal.complete() - } - - let signalDisposable = signal.start(next: { next in - if let result = next as? TGMediaVideoConversionResult { - if let image = result.coverImage, let data = image.jpegData(compressionQuality: 0.7) { - account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) - } - - if let timestamp = videoStartTimestamp { - videoStartTimestamp = max(0.0, min(timestamp, result.duration - 0.05)) - } - - var value = stat() - if stat(result.fileURL.path, &value) == 0 { - if let data = try? Data(contentsOf: result.fileURL) { - let resource: TelegramMediaResource - if let liveUploadData = result.liveUploadData as? LegacyLiveUploadInterfaceResult { - resource = LocalFileMediaResource(fileId: liveUploadData.id) - } else { - resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - } - account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) - subscriber.putNext(resource) - - EngineTempBox.shared.dispose(tempFile) - } - } - subscriber.putCompletion() - } else if let progress = next as? NSNumber { - Queue.mainQueue().async { [weak self] in - guard let self else { - return - } - self.currentUpdatingAvatar = (representation, Float(truncating: progress) * 0.25) - self.state?.updated(transition: .spring(duration: 0.4)) - } - } - }, error: { _ in - }, completed: nil) - - let disposable = ActionDisposable { - signalDisposable?.dispose() - } - - return ActionDisposable { - disposable.dispose() - } - } - - self.updateAvatarDisposable.set((signal - |> mapToSignal { videoResource -> Signal in - if peerId.namespace == Namespaces.Peer.CloudUser { - return context.engine.accountData.updateAccountPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: nil, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) - }) - } else { - return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: photoResource), video: context.engine.peers.uploadedPeerVideo(resource: videoResource) |> map(Optional.init), videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) - }) - } - } - |> deliverOnMainQueue).start(next: { [weak self] result in - guard let self else { - return - } - switch result { - case .complete: - self.currentUpdatingAvatar = nil - self.state?.updated(transition: .spring(duration: 0.4)) - case let .progress(value): - self.currentUpdatingAvatar = (representation, 0.25 + value * 0.75) - self.state?.updated(transition: .spring(duration: 0.4)) - } - })) - } - - private func openTitleEditing() { + func openTitleEditing() { guard let component = self.component else { return } @@ -1413,7 +231,7 @@ private final class VideoChatScreenComponent: Component { }) } - private func presentUndoOverlay(content: UndoOverlayContent, action: @escaping (UndoOverlayAction) -> Bool) { + func presentUndoOverlay(content: UndoOverlayContent, action: @escaping (UndoOverlayAction) -> Bool) { guard let component = self.component, let environment = self.environment else { return } @@ -1429,340 +247,7 @@ private final class VideoChatScreenComponent: Component { environment.controller()?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: action), in: .current) } - private func openInviteMembers() { - guard let component = self.component else { - return - } - - var canInvite = true - var inviteIsLink = false - if case let .channel(peer) = self.peer { - if peer.flags.contains(.isGigagroup) { - if peer.flags.contains(.isCreator) || peer.adminRights != nil { - } else { - canInvite = false - } - } - if case .broadcast = peer.info, !(peer.addressName?.isEmpty ?? true) { - inviteIsLink = true - } - } - var inviteType: VideoChatParticipantsComponent.Participants.InviteType? - if canInvite { - if inviteIsLink { - inviteType = .shareLink - } else { - inviteType = .invite - } - } - - guard let inviteType else { - return - } - - switch inviteType { - case .invite: - let groupPeer = component.call.accountContext.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: component.call.peerId)) - let _ = (groupPeer - |> deliverOnMainQueue).start(next: { [weak self] groupPeer in - guard let self, let component = self.component, let environment = self.environment, let groupPeer else { - return - } - let inviteLinks = self.inviteLinks - - if case let .channel(groupPeer) = groupPeer { - var canInviteMembers = true - if case .broadcast = groupPeer.info, !(groupPeer.addressName?.isEmpty ?? true) { - canInviteMembers = false - } - if !canInviteMembers { - if let inviteLinks { - self.presentShare(inviteLinks) - } - return - } - } - - var filters: [ChannelMembersSearchFilter] = [] - if let members = self.members { - filters.append(.disable(Array(members.participants.map { $0.peer.id }))) - } - if case let .channel(groupPeer) = groupPeer { - if !groupPeer.hasPermission(.inviteMembers) && inviteLinks?.listenerLink == nil { - filters.append(.excludeNonMembers) - } - } else if case let .legacyGroup(groupPeer) = groupPeer { - if groupPeer.hasBannedPermission(.banAddMembers) { - filters.append(.excludeNonMembers) - } - } - filters.append(.excludeBots) - - var dismissController: (() -> Void)? - let controller = ChannelMembersSearchController(context: component.call.accountContext, peerId: groupPeer.id, forceTheme: environment.theme, mode: .inviteToCall, filters: filters, openPeer: { [weak self] peer, participant in - guard let self, let component = self.component, let environment = self.environment else { - dismissController?() - return - } - guard let callState = self.callState else { - return - } - - let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) - if peer.id == callState.myPeerId { - return - } - if let participant { - dismissController?() - - if component.call.invitePeer(participant.peer.id) { - let text: String - if case let .channel(channel) = self.peer, case .broadcast = channel.info { - text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string - } else { - text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string - } - self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: EnginePeer(participant.peer), title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) - } - } else { - if case let .channel(groupPeer) = groupPeer, let listenerLink = inviteLinks?.listenerLink, !groupPeer.hasPermission(.inviteMembers) { - let text = environment.strings.VoiceChat_SendPublicLinkText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), EnginePeer(groupPeer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string - - environment.controller()?.present(textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: environment.strings.VoiceChat_SendPublicLinkSend, action: { [weak self] in - dismissController?() - - guard let self, let component = self.component else { - return - } - - let _ = (enqueueMessages(account: component.call.accountContext.account, peerId: peer.id, messages: [.message(text: listenerLink, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) - |> deliverOnMainQueue).start(next: { [weak self] _ in - guard let self, let environment = self.environment else { - return - } - self.presentUndoOverlay(content: .forward(savedMessages: false, text: environment.strings.UserInfo_LinkForwardTooltip_Chat_One(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string), action: { _ in return true }) - }) - })]), in: .window(.root)) - } else { - let text: String - if case let .channel(groupPeer) = groupPeer, case .broadcast = groupPeer.info { - text = environment.strings.VoiceChat_InviteMemberToChannelFirstText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), EnginePeer(groupPeer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string - } else { - text = environment.strings.VoiceChat_InviteMemberToGroupFirstText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), groupPeer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string - } - - environment.controller()?.present(textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: environment.strings.VoiceChat_InviteMemberToGroupFirstAdd, action: { [weak self] in - guard let self, let component = self.component, let environment = self.environment else { - return - } - - if case let .channel(groupPeer) = groupPeer { - guard let selfController = environment.controller() else { - return - } - let inviteDisposable = self.inviteDisposable - var inviteSignal = component.call.accountContext.peerChannelMemberCategoriesContextsManager.addMembers(engine: component.call.accountContext.engine, peerId: groupPeer.id, memberIds: [peer.id]) - var cancelImpl: (() -> Void)? - let progressSignal = Signal { [weak selfController] subscriber in - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { - cancelImpl?() - })) - selfController?.present(controller, in: .window(.root)) - return ActionDisposable { [weak controller] in - Queue.mainQueue().async() { - controller?.dismiss() - } - } - } - |> runOn(Queue.mainQueue()) - |> delay(0.15, queue: Queue.mainQueue()) - let progressDisposable = progressSignal.start() - - inviteSignal = inviteSignal - |> afterDisposed { - Queue.mainQueue().async { - progressDisposable.dispose() - } - } - cancelImpl = { - inviteDisposable.set(nil) - } - - inviteDisposable.set((inviteSignal |> deliverOnMainQueue).start(error: { [weak self] error in - dismissController?() - guard let self, let component = self.component, let environment = self.environment else { - return - } - - let text: String - switch error { - case .limitExceeded: - text = environment.strings.Channel_ErrorAddTooMuch - case .tooMuchJoined: - text = environment.strings.Invite_ChannelsTooMuch - case .generic: - text = environment.strings.Login_UnknownError - case .restricted: - text = environment.strings.Channel_ErrorAddBlocked - case .notMutualContact: - if case .broadcast = groupPeer.info { - text = environment.strings.Channel_AddUserLeftError - } else { - text = environment.strings.GroupInfo_AddUserLeftError - } - case .botDoesntSupportGroups: - text = environment.strings.Channel_BotDoesntSupportGroups - case .tooMuchBots: - text = environment.strings.Channel_TooMuchBots - case .bot: - text = environment.strings.Login_UnknownError - case .kicked: - text = environment.strings.Channel_AddUserKickedError - } - environment.controller()?.present(textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root)) - }, completed: { [weak self] in - guard let self, let component = self.component, let environment = self.environment else { - dismissController?() - return - } - dismissController?() - - if component.call.invitePeer(peer.id) { - let text: String - if case let .channel(channel) = self.peer, case .broadcast = channel.info { - text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string - } else { - text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string - } - self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) - } - })) - } else if case let .legacyGroup(groupPeer) = groupPeer { - guard let selfController = environment.controller() else { - return - } - let inviteDisposable = self.inviteDisposable - var inviteSignal = component.call.accountContext.engine.peers.addGroupMember(peerId: groupPeer.id, memberId: peer.id) - var cancelImpl: (() -> Void)? - let progressSignal = Signal { [weak selfController] subscriber in - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { - cancelImpl?() - })) - selfController?.present(controller, in: .window(.root)) - return ActionDisposable { [weak controller] in - Queue.mainQueue().async() { - controller?.dismiss() - } - } - } - |> runOn(Queue.mainQueue()) - |> delay(0.15, queue: Queue.mainQueue()) - let progressDisposable = progressSignal.start() - - inviteSignal = inviteSignal - |> afterDisposed { - Queue.mainQueue().async { - progressDisposable.dispose() - } - } - cancelImpl = { - inviteDisposable.set(nil) - } - - inviteDisposable.set((inviteSignal |> deliverOnMainQueue).start(error: { [weak self] error in - dismissController?() - guard let self, let component = self.component, let environment = self.environment else { - return - } - let context = component.call.accountContext - - switch error { - case .privacy: - let _ = (component.call.accountContext.account.postbox.loadedPeerWithId(peer.id) - |> deliverOnMainQueue).start(next: { [weak self] peer in - guard let self, let component = self.component, let environment = self.environment else { - return - } - environment.controller()?.present(textAlertController(context: component.call.accountContext, title: nil, text: environment.strings.Privacy_GroupsAndChannels_InviteToGroupError(EnginePeer(peer).compactDisplayTitle, EnginePeer(peer).compactDisplayTitle).string, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root)) - }) - case .notMutualContact: - environment.controller()?.present(textAlertController(context: context, title: nil, text: environment.strings.GroupInfo_AddUserLeftError, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root)) - case .tooManyChannels: - environment.controller()?.present(textAlertController(context: context, title: nil, text: environment.strings.Invite_ChannelsTooMuch, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root)) - case .groupFull, .generic: - environment.controller()?.present(textAlertController(context: context, forceTheme: environment.theme, title: nil, text: environment.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root)) - } - }, completed: { [weak self] in - guard let self, let component = self.component, let environment = self.environment else { - dismissController?() - return - } - dismissController?() - - if component.call.invitePeer(peer.id) { - let text: String - if case let .channel(channel) = self.peer, case .broadcast = channel.info { - text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string - } else { - text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string - } - self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) - } - })) - } - })]), in: .window(.root)) - } - } - }) - controller.copyInviteLink = { [weak self] in - dismissController?() - - guard let self, let component = self.component else { - return - } - let callPeerId = component.call.peerId - - let _ = (component.call.accountContext.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: callPeerId), - TelegramEngine.EngineData.Item.Peer.ExportedInvitation(id: callPeerId) - ) - |> map { peer, exportedInvitation -> String? in - if let link = inviteLinks?.listenerLink { - return link - } else if let peer = peer, let addressName = peer.addressName, !addressName.isEmpty { - return "https://t.me/\(addressName)" - } else if let link = exportedInvitation?.link { - return link - } else { - return nil - } - } - |> deliverOnMainQueue).start(next: { [weak self] link in - guard let self, let environment = self.environment else { - return - } - - if let link { - UIPasteboard.general.string = link - - self.presentUndoOverlay(content: .linkCopied(text: environment.strings.VoiceChat_InviteLinkCopiedText), action: { _ in return false }) - } - }) - } - dismissController = { [weak controller] in - controller?.dismiss() - } - environment.controller()?.push(controller) - }) - case .shareLink: - guard let inviteLinks = self.inviteLinks else { - return - } - self.presentShare(inviteLinks) - } - } - - private func presentShare(_ inviteLinks: GroupCallInviteLinks) { + func presentShare(_ inviteLinks: GroupCallInviteLinks) { guard let component = self.component else { return } @@ -3200,25 +1685,3 @@ final class VideoChatScreenV2Impl: ViewControllerComponentContainer, VoiceChatCo } } } - -private final class ParticipantExtractedContentSource: ContextExtractedContentSource { - let keepInPlace: Bool = false - let ignoreContentTouches: Bool = false - let blurBackground: Bool = true - - //let actionsHorizontalAlignment: ContextActionsHorizontalAlignment = .center - - private let contentView: ContextExtractedContentContainingView - - init(contentView: ContextExtractedContentContainingView) { - self.contentView = contentView - } - - func takeView() -> ContextControllerTakeViewInfo? { - return ContextControllerTakeViewInfo(containingItem: .view(self.contentView), contentAreaInScreenSpace: UIScreen.main.bounds) - } - - func putBack() -> ContextControllerPutBackViewInfo? { - return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) - } -} diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift new file mode 100644 index 0000000000..70e52ce32e --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift @@ -0,0 +1,343 @@ +import Foundation +import UIKit +import Display +import TelegramCore +import SwiftSignalKit +import PeerInfoUI +import OverlayStatusController +import PresentationDataUtils + +extension VideoChatScreenComponent.View { + func openInviteMembers() { + guard let component = self.component else { + return + } + + var canInvite = true + var inviteIsLink = false + if case let .channel(peer) = self.peer { + if peer.flags.contains(.isGigagroup) { + if peer.flags.contains(.isCreator) || peer.adminRights != nil { + } else { + canInvite = false + } + } + if case .broadcast = peer.info, !(peer.addressName?.isEmpty ?? true) { + inviteIsLink = true + } + } + var inviteType: VideoChatParticipantsComponent.Participants.InviteType? + if canInvite { + if inviteIsLink { + inviteType = .shareLink + } else { + inviteType = .invite + } + } + + guard let inviteType else { + return + } + + switch inviteType { + case .invite: + let groupPeer = component.call.accountContext.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: component.call.peerId)) + let _ = (groupPeer + |> deliverOnMainQueue).start(next: { [weak self] groupPeer in + guard let self, let component = self.component, let environment = self.environment, let groupPeer else { + return + } + let inviteLinks = self.inviteLinks + + if case let .channel(groupPeer) = groupPeer { + var canInviteMembers = true + if case .broadcast = groupPeer.info, !(groupPeer.addressName?.isEmpty ?? true) { + canInviteMembers = false + } + if !canInviteMembers { + if let inviteLinks { + self.presentShare(inviteLinks) + } + return + } + } + + var filters: [ChannelMembersSearchFilter] = [] + if let members = self.members { + filters.append(.disable(Array(members.participants.map { $0.peer.id }))) + } + if case let .channel(groupPeer) = groupPeer { + if !groupPeer.hasPermission(.inviteMembers) && inviteLinks?.listenerLink == nil { + filters.append(.excludeNonMembers) + } + } else if case let .legacyGroup(groupPeer) = groupPeer { + if groupPeer.hasBannedPermission(.banAddMembers) { + filters.append(.excludeNonMembers) + } + } + filters.append(.excludeBots) + + var dismissController: (() -> Void)? + let controller = ChannelMembersSearchController(context: component.call.accountContext, peerId: groupPeer.id, forceTheme: environment.theme, mode: .inviteToCall, filters: filters, openPeer: { [weak self] peer, participant in + guard let self, let component = self.component, let environment = self.environment else { + dismissController?() + return + } + guard let callState = self.callState else { + return + } + + let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) + if peer.id == callState.myPeerId { + return + } + if let participant { + dismissController?() + + if component.call.invitePeer(participant.peer.id) { + let text: String + if case let .channel(channel) = self.peer, case .broadcast = channel.info { + text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string + } else { + text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string + } + self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: EnginePeer(participant.peer), title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) + } + } else { + if case let .channel(groupPeer) = groupPeer, let listenerLink = inviteLinks?.listenerLink, !groupPeer.hasPermission(.inviteMembers) { + let text = environment.strings.VoiceChat_SendPublicLinkText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), EnginePeer(groupPeer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string + + environment.controller()?.present(textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: environment.strings.VoiceChat_SendPublicLinkSend, action: { [weak self] in + dismissController?() + + guard let self, let component = self.component else { + return + } + + let _ = (enqueueMessages(account: component.call.accountContext.account, peerId: peer.id, messages: [.message(text: listenerLink, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let self, let environment = self.environment else { + return + } + self.presentUndoOverlay(content: .forward(savedMessages: false, text: environment.strings.UserInfo_LinkForwardTooltip_Chat_One(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string), action: { _ in return true }) + }) + })]), in: .window(.root)) + } else { + let text: String + if case let .channel(groupPeer) = groupPeer, case .broadcast = groupPeer.info { + text = environment.strings.VoiceChat_InviteMemberToChannelFirstText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), EnginePeer(groupPeer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string + } else { + text = environment.strings.VoiceChat_InviteMemberToGroupFirstText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), groupPeer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string + } + + environment.controller()?.present(textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: environment.strings.VoiceChat_InviteMemberToGroupFirstAdd, action: { [weak self] in + guard let self, let component = self.component, let environment = self.environment else { + return + } + + if case let .channel(groupPeer) = groupPeer { + guard let selfController = environment.controller() else { + return + } + let inviteDisposable = self.inviteDisposable + var inviteSignal = component.call.accountContext.peerChannelMemberCategoriesContextsManager.addMembers(engine: component.call.accountContext.engine, peerId: groupPeer.id, memberIds: [peer.id]) + var cancelImpl: (() -> Void)? + let progressSignal = Signal { [weak selfController] subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + selfController?.present(controller, in: .window(.root)) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.15, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + + inviteSignal = inviteSignal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + cancelImpl = { + inviteDisposable.set(nil) + } + + inviteDisposable.set((inviteSignal |> deliverOnMainQueue).start(error: { [weak self] error in + dismissController?() + guard let self, let component = self.component, let environment = self.environment else { + return + } + + let text: String + switch error { + case .limitExceeded: + text = environment.strings.Channel_ErrorAddTooMuch + case .tooMuchJoined: + text = environment.strings.Invite_ChannelsTooMuch + case .generic: + text = environment.strings.Login_UnknownError + case .restricted: + text = environment.strings.Channel_ErrorAddBlocked + case .notMutualContact: + if case .broadcast = groupPeer.info { + text = environment.strings.Channel_AddUserLeftError + } else { + text = environment.strings.GroupInfo_AddUserLeftError + } + case .botDoesntSupportGroups: + text = environment.strings.Channel_BotDoesntSupportGroups + case .tooMuchBots: + text = environment.strings.Channel_TooMuchBots + case .bot: + text = environment.strings.Login_UnknownError + case .kicked: + text = environment.strings.Channel_AddUserKickedError + } + environment.controller()?.present(textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root)) + }, completed: { [weak self] in + guard let self, let component = self.component, let environment = self.environment else { + dismissController?() + return + } + dismissController?() + + if component.call.invitePeer(peer.id) { + let text: String + if case let .channel(channel) = self.peer, case .broadcast = channel.info { + text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string + } else { + text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string + } + self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) + } + })) + } else if case let .legacyGroup(groupPeer) = groupPeer { + guard let selfController = environment.controller() else { + return + } + let inviteDisposable = self.inviteDisposable + var inviteSignal = component.call.accountContext.engine.peers.addGroupMember(peerId: groupPeer.id, memberId: peer.id) + var cancelImpl: (() -> Void)? + let progressSignal = Signal { [weak selfController] subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + selfController?.present(controller, in: .window(.root)) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.15, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + + inviteSignal = inviteSignal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + cancelImpl = { + inviteDisposable.set(nil) + } + + inviteDisposable.set((inviteSignal |> deliverOnMainQueue).start(error: { [weak self] error in + dismissController?() + guard let self, let component = self.component, let environment = self.environment else { + return + } + let context = component.call.accountContext + + switch error { + case .privacy: + let _ = (component.call.accountContext.account.postbox.loadedPeerWithId(peer.id) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let component = self.component, let environment = self.environment else { + return + } + environment.controller()?.present(textAlertController(context: component.call.accountContext, title: nil, text: environment.strings.Privacy_GroupsAndChannels_InviteToGroupError(EnginePeer(peer).compactDisplayTitle, EnginePeer(peer).compactDisplayTitle).string, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root)) + }) + case .notMutualContact: + environment.controller()?.present(textAlertController(context: context, title: nil, text: environment.strings.GroupInfo_AddUserLeftError, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root)) + case .tooManyChannels: + environment.controller()?.present(textAlertController(context: context, title: nil, text: environment.strings.Invite_ChannelsTooMuch, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root)) + case .groupFull, .generic: + environment.controller()?.present(textAlertController(context: context, forceTheme: environment.theme, title: nil, text: environment.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root)) + } + }, completed: { [weak self] in + guard let self, let component = self.component, let environment = self.environment else { + dismissController?() + return + } + dismissController?() + + if component.call.invitePeer(peer.id) { + let text: String + if case let .channel(channel) = self.peer, case .broadcast = channel.info { + text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string + } else { + text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string + } + self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) + } + })) + } + })]), in: .window(.root)) + } + } + }) + controller.copyInviteLink = { [weak self] in + dismissController?() + + guard let self, let component = self.component else { + return + } + let callPeerId = component.call.peerId + + let _ = (component.call.accountContext.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: callPeerId), + TelegramEngine.EngineData.Item.Peer.ExportedInvitation(id: callPeerId) + ) + |> map { peer, exportedInvitation -> String? in + if let link = inviteLinks?.listenerLink { + return link + } else if let peer = peer, let addressName = peer.addressName, !addressName.isEmpty { + return "https://t.me/\(addressName)" + } else if let link = exportedInvitation?.link { + return link + } else { + return nil + } + } + |> deliverOnMainQueue).start(next: { [weak self] link in + guard let self, let environment = self.environment else { + return + } + + if let link { + UIPasteboard.general.string = link + + self.presentUndoOverlay(content: .linkCopied(text: environment.strings.VoiceChat_InviteLinkCopiedText), action: { _ in return false }) + } + }) + } + dismissController = { [weak controller] in + controller?.dismiss() + } + environment.controller()?.push(controller) + }) + case .shareLink: + guard let inviteLinks = self.inviteLinks else { + return + } + self.presentShare(inviteLinks) + } + } +} diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift new file mode 100644 index 0000000000..16ecfecd1b --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift @@ -0,0 +1,560 @@ +import Foundation +import UIKit +import Display +import ContextUI +import TelegramCore +import SwiftSignalKit +import DeleteChatPeerActionSheetItem +import PeerListItemComponent +import LegacyComponents +import LegacyUI +import WebSearchUI +import MapResourceToAvatarSizes +import LegacyMediaPickerUI +import AvatarNode +import PresentationDataUtils +import AccountContext + +extension VideoChatScreenComponent.View { + func openMoreMenu() { + guard let sourceView = self.navigationLeftButton.view else { + return + } + guard let component = self.component, let environment = self.environment, let controller = environment.controller() else { + return + } + guard let peer = self.peer else { + return + } + guard let callState = self.callState else { + return + } + + let canManageCall = callState.canManageCall + + var items: [ContextMenuItem] = [] + + if let displayAsPeers = self.displayAsPeers, displayAsPeers.count > 1 { + for peer in displayAsPeers { + if peer.peer.id == callState.myPeerId { + let avatarSize = CGSize(width: 28.0, height: 28.0) + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_DisplayAs, textLayout: .secondLineWithValue(EnginePeer(peer.peer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: component.call.accountContext.account, peer: EnginePeer(peer.peer), size: avatarSize)), action: { [weak self] c, _ in + guard let self else { + return + } + c?.pushItems(items: .single(ContextController.Items(content: .list(self.contextMenuDisplayAsItems())))) + }))) + items.append(.separator) + break + } + } + } + + if let (availableOutputs, currentOutput) = self.audioOutputState, availableOutputs.count > 1 { + var currentOutputTitle = "" + for output in availableOutputs { + if output == currentOutput { + let title: String + switch output { + case .builtin: + title = UIDevice.current.model + case .speaker: + title = environment.strings.Call_AudioRouteSpeaker + case .headphones: + title = environment.strings.Call_AudioRouteHeadphones + case let .port(port): + title = port.name + } + currentOutputTitle = title + break + } + } + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_ContextAudio, textLayout: .secondLineWithValue(currentOutputTitle), icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Audio"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] c, _ in + guard let self else { + return + } + c?.pushItems(items: .single(ContextController.Items(content: .list(self.contextMenuAudioItems())))) + }))) + } + + if canManageCall { + let text: String + if case let .channel(channel) = peer, case .broadcast = channel.info { + text = environment.strings.LiveStream_EditTitle + } else { + text = environment.strings.VoiceChat_EditTitle + } + items.append(.action(ContextMenuActionItem(text: text, icon: { theme -> UIImage? in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pencil"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + f(.default) + + guard let self else { + return + } + self.openTitleEditing() + }))) + + var hasPermissions = true + if case let .channel(chatPeer) = peer { + if case .broadcast = chatPeer.info { + hasPermissions = false + } else if chatPeer.flags.contains(.isGigagroup) { + hasPermissions = false + } + } + if hasPermissions { + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_EditPermissions, icon: { theme -> UIImage? in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] c, _ in + guard let self else { + return + } + c?.pushItems(items: .single(ContextController.Items(content: .list(self.contextMenuPermissionItems())))) + }))) + } + } + + if let inviteLinks = self.inviteLinks { + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_Share, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + f(.default) + + guard let self else { + return + } + self.presentShare(inviteLinks) + }))) + } + + //let isScheduled = strongSelf.isScheduled + //TODO:release + let isScheduled: Bool = !"".isEmpty + + let canSpeak: Bool + if let muteState = callState.muteState { + canSpeak = muteState.canUnmute + } else { + canSpeak = true + } + + if !isScheduled && canSpeak { + if #available(iOS 15.0, *) { + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_MicrophoneModes, textColor: .primary, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Noise"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.dismissWithoutContent) + AVCaptureDevice.showSystemUserInterface(.microphoneModes) + }))) + } + } + + if callState.isVideoEnabled && (callState.muteState?.canUnmute ?? true) { + if component.call.hasScreencast { + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_StopScreenSharing, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ShareScreen"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + f(.default) + + guard let self, let component = self.component else { + return + } + component.call.disableScreencast() + }))) + } else { + items.append(.custom(VoiceChatShareScreenContextItem(context: component.call.accountContext, text: environment.strings.VoiceChat_ShareScreen, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ShareScreen"), color: theme.actionSheet.primaryTextColor) + }, action: { _, _ in }), false)) + } + } + + if canManageCall { + if let recordingStartTimestamp = callState.recordingStartTimestamp { + items.append(.custom(VoiceChatRecordingContextItem(timestamp: recordingStartTimestamp, action: { [weak self] _, f in + f(.dismissWithoutContent) + + guard let self, let component = self.component, let environment = self.environment else { + return + } + + let alertController = textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: nil, text: environment.strings.VoiceChat_StopRecordingTitle, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: environment.strings.VoiceChat_StopRecordingStop, action: { [weak self] in + guard let self, let component = self.component, let environment = self.environment else { + return + } + component.call.setShouldBeRecording(false, title: nil, videoOrientation: nil) + + Queue.mainQueue().after(0.88) { + HapticFeedback().success() + } + + let text: String + if case let .channel(channel) = self.peer, case .broadcast = channel.info { + text = environment.strings.LiveStream_RecordingSaved + } else { + text = environment.strings.VideoChat_RecordingSaved + } + self.presentUndoOverlay(content: .forward(savedMessages: true, text: text), action: { [weak self] value in + if case .info = value, let self, let component = self.component, let environment = self.environment, let navigationController = environment.controller()?.navigationController as? NavigationController { + let context = component.call.accountContext + environment.controller()?.dismiss(completion: { [weak navigationController] in + Queue.mainQueue().justDispatch { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { peer in + guard let peer, let navigationController else { + return + } + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), keepStack: .always, purposefulAction: {}, peekData: nil)) + }) + } + }) + + return true + } + return false + }) + })]) + environment.controller()?.present(alertController, in: .window(.root)) + }), false)) + } else { + let text: String + if case let .channel(channel) = peer, case .broadcast = channel.info { + text = environment.strings.LiveStream_StartRecording + } else { + text = environment.strings.VoiceChat_StartRecording + } + if callState.scheduleTimestamp == nil { + items.append(.action(ContextMenuActionItem(text: text, icon: { theme -> UIImage? in + return generateStartRecordingIcon(color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + f(.dismissWithoutContent) + + guard let self, let component = self.component, let environment = self.environment, let peer = self.peer else { + return + } + + let controller = VoiceChatRecordingSetupController(context: component.call.accountContext, peer: peer, completion: { [weak self] videoOrientation in + guard let self, let component = self.component, let environment = self.environment, let peer = self.peer else { + return + } + let title: String + let text: String + let placeholder: String + if let _ = videoOrientation { + placeholder = environment.strings.VoiceChat_RecordingTitlePlaceholderVideo + } else { + placeholder = environment.strings.VoiceChat_RecordingTitlePlaceholder + } + if case let .channel(channel) = peer, case .broadcast = channel.info { + title = environment.strings.LiveStream_StartRecordingTitle + if let _ = videoOrientation { + text = environment.strings.LiveStream_StartRecordingTextVideo + } else { + text = environment.strings.LiveStream_StartRecordingText + } + } else { + title = environment.strings.VoiceChat_StartRecordingTitle + if let _ = videoOrientation { + text = environment.strings.VoiceChat_StartRecordingTextVideo + } else { + text = environment.strings.VoiceChat_StartRecordingText + } + } + + let controller = voiceChatTitleEditController(sharedContext: component.call.accountContext.sharedContext, account: component.call.account, forceTheme: environment.theme, title: title, text: text, placeholder: placeholder, value: nil, maxLength: 40, apply: { [weak self] title in + guard let self, let component = self.component, let environment = self.environment, let peer = self.peer, let title else { + return + } + + component.call.setShouldBeRecording(true, title: title, videoOrientation: videoOrientation) + + let text: String + if case let .channel(channel) = peer, case .broadcast = channel.info { + text = environment.strings.LiveStream_RecordingStarted + } else { + text = environment.strings.VoiceChat_RecordingStarted + } + + self.presentUndoOverlay(content: .voiceChatRecording(text: text), action: { _ in return false }) + component.call.playTone(.recordingStarted) + }) + environment.controller()?.present(controller, in: .window(.root)) + }) + environment.controller()?.present(controller, in: .window(.root)) + }))) + } + } + } + + if canManageCall { + let text: String + if case let .channel(channel) = peer, case .broadcast = channel.info { + text = isScheduled ? environment.strings.VoiceChat_CancelLiveStream : environment.strings.VoiceChat_EndLiveStream + } else { + text = isScheduled ? environment.strings.VoiceChat_CancelVoiceChat : environment.strings.VoiceChat_EndVoiceChat + } + items.append(.action(ContextMenuActionItem(text: text, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor) + }, action: { [weak self] _, f in + f(.dismissWithoutContent) + + guard let self, let component = self.component, let environment = self.environment else { + return + } + + let action: () -> Void = { [weak self] in + guard let self, let component = self.component else { + return + } + + let _ = (component.call.leave(terminateIfPossible: true) + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).start(completed: { [weak self] in + guard let self, let environment = self.environment else { + return + } + environment.controller()?.dismiss() + }) + } + + let title: String + let text: String + if case let .channel(channel) = self.peer, case .broadcast = channel.info { + title = isScheduled ? environment.strings.LiveStream_CancelConfirmationTitle : environment.strings.LiveStream_EndConfirmationTitle + text = isScheduled ? environment.strings.LiveStream_CancelConfirmationText : environment.strings.LiveStream_EndConfirmationText + } else { + title = isScheduled ? environment.strings.VoiceChat_CancelConfirmationTitle : environment.strings.VoiceChat_EndConfirmationTitle + text = isScheduled ? environment.strings.VoiceChat_CancelConfirmationText : environment.strings.VoiceChat_EndConfirmationText + } + + let alertController = textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: isScheduled ? environment.strings.VoiceChat_CancelConfirmationEnd : environment.strings.VoiceChat_EndConfirmationEnd, action: { + action() + })]) + environment.controller()?.present(alertController, in: .window(.root)) + }))) + } else { + let leaveText: String + if case let .channel(channel) = peer, case .broadcast = channel.info { + leaveText = environment.strings.LiveStream_LeaveVoiceChat + } else { + leaveText = environment.strings.VoiceChat_LeaveVoiceChat + } + items.append(.action(ContextMenuActionItem(text: leaveText, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor) + }, action: { [weak self] _, f in + f(.dismissWithoutContent) + + guard let self, let component = self.component else { + return + } + + let _ = (component.call.leave(terminateIfPossible: false) + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).start(completed: { [weak self] in + guard let self, let environment = self.environment else { + return + } + environment.controller()?.dismiss() + }) + }))) + } + + let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) + let contextController = ContextController(presentationData: presentationData, source: .reference(VoiceChatContextReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) + controller.presentInGlobalOverlay(contextController) + } + + private func contextMenuDisplayAsItems() -> [ContextMenuItem] { + guard let component = self.component, let environment = self.environment else { + return [] + } + guard let callState = self.callState else { + return [] + } + let myPeerId = callState.myPeerId + + let avatarSize = CGSize(width: 28.0, height: 28.0) + + var items: [ContextMenuItem] = [] + + items.append(.action(ContextMenuActionItem(text: environment.strings.Common_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) + }, iconPosition: .left, action: { (c, _) in + c?.popItems() + }))) + items.append(.separator) + + var isGroup = false + if let displayAsPeers = self.displayAsPeers { + for peer in displayAsPeers { + if peer.peer is TelegramGroup { + isGroup = true + break + } else if let peer = peer.peer as? TelegramChannel, case .group = peer.info { + isGroup = true + break + } + } + } + + items.append(.custom(VoiceChatInfoContextItem(text: isGroup ? environment.strings.VoiceChat_DisplayAsInfoGroup : environment.strings.VoiceChat_DisplayAsInfo, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Accounts"), color: theme.actionSheet.primaryTextColor) + }), true)) + + if let displayAsPeers = self.displayAsPeers { + for peer in displayAsPeers { + var subtitle: String? + if peer.peer.id.namespace == Namespaces.Peer.CloudUser { + subtitle = environment.strings.VoiceChat_PersonalAccount + } else if let subscribers = peer.subscribers { + if let peer = peer.peer as? TelegramChannel, case .broadcast = peer.info { + subtitle = environment.strings.Conversation_StatusSubscribers(subscribers) + } else { + subtitle = environment.strings.Conversation_StatusMembers(subscribers) + } + } + + let isSelected = peer.peer.id == myPeerId + let extendedAvatarSize = CGSize(width: 35.0, height: 35.0) + let theme = environment.theme + let avatarSignal = peerAvatarCompleteImage(account: component.call.accountContext.account, peer: EnginePeer(peer.peer), size: avatarSize) + |> map { image -> UIImage? in + if isSelected, let image = image { + return generateImage(extendedAvatarSize, rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + context.draw(image.cgImage!, in: CGRect(x: (extendedAvatarSize.width - avatarSize.width) / 2.0, y: (extendedAvatarSize.height - avatarSize.height) / 2.0, width: avatarSize.width, height: avatarSize.height)) + + let lineWidth = 1.0 + UIScreenPixel + context.setLineWidth(lineWidth) + context.setStrokeColor(theme.actionSheet.controlAccentColor.cgColor) + context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0)) + }) + } else { + return image + } + } + + items.append(.action(ContextMenuActionItem(text: EnginePeer(peer.peer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: isSelected ? extendedAvatarSize : avatarSize, signal: avatarSignal), action: { [weak self] _, f in + f(.default) + + guard let self, let component = self.component else { + return + } + + if peer.peer.id != myPeerId { + component.call.reconnect(as: peer.peer.id) + } + }))) + + if peer.peer.id.namespace == Namespaces.Peer.CloudUser { + items.append(.separator) + } + } + } + return items + } + + private func contextMenuAudioItems() -> [ContextMenuItem] { + guard let environment = self.environment else { + return [] + } + guard let (availableOutputs, currentOutput) = self.audioOutputState else { + return [] + } + + var items: [ContextMenuItem] = [] + + items.append(.action(ContextMenuActionItem(text: environment.strings.Common_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) + }, iconPosition: .left, action: { (c, _) in + c?.popItems() + }))) + items.append(.separator) + + for output in availableOutputs { + let title: String + switch output { + case .builtin: + title = UIDevice.current.model + case .speaker: + title = environment.strings.Call_AudioRouteSpeaker + case .headphones: + title = environment.strings.Call_AudioRouteHeadphones + case let .port(port): + title = port.name + } + items.append(.action(ContextMenuActionItem(text: title, icon: { theme in + if output == currentOutput { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor) + } else { + return nil + } + }, action: { [weak self] _, f in + f(.default) + + guard let self, let component = self.component else { + return + } + + component.call.setCurrentAudioOutput(output) + }))) + } + + return items + } + + private func contextMenuPermissionItems() -> [ContextMenuItem] { + guard let environment = self.environment, let callState = self.callState else { + return [] + } + + var items: [ContextMenuItem] = [] + if callState.canManageCall, let defaultParticipantMuteState = callState.defaultParticipantMuteState { + let isMuted = defaultParticipantMuteState == .muted + + items.append(.action(ContextMenuActionItem(text: environment.strings.Common_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) + }, iconPosition: .left, action: { (c, _) in + c?.popItems() + }))) + items.append(.separator) + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_SpeakPermissionEveryone, icon: { theme in + if isMuted { + return nil + } else { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor) + } + }, action: { [weak self] _, f in + f(.dismissWithoutContent) + + guard let self, let component = self.component else { + return + } + component.call.updateDefaultParticipantsAreMuted(isMuted: false) + }))) + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_SpeakPermissionAdmin, icon: { theme in + if !isMuted { + return nil + } else { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor) + } + }, action: { [weak self] _, f in + f(.dismissWithoutContent) + + guard let self, let component = self.component else { + return + } + component.call.updateDefaultParticipantsAreMuted(isMuted: true) + }))) + } + return items + } +} diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift new file mode 100644 index 0000000000..6ee08d0a90 --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift @@ -0,0 +1,667 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import AccountContext +import TelegramCore +import ContextUI +import DeleteChatPeerActionSheetItem +import UndoUI +import LegacyComponents +import WebSearchUI +import MapResourceToAvatarSizes +import LegacyUI +import LegacyMediaPickerUI + +extension VideoChatScreenComponent.View { + func openParticipantContextMenu(id: EnginePeer.Id, sourceView: ContextExtractedContentContainingView, gesture: ContextGesture?) { + guard let component = self.component, let environment = self.environment else { + return + } + guard let members = self.members, let participant = members.participants.first(where: { $0.peer.id == id }) else { + return + } + + let muteStatePromise = Promise(participant.muteState) + + let itemsForEntry: (GroupCallParticipantsContext.Participant.MuteState?) -> [ContextMenuItem] = { [weak self] muteState in + guard let self, let component = self.component, let environment = self.environment else { + return [] + } + guard let callState = self.callState else { + return [] + } + + var items: [ContextMenuItem] = [] + + var hasVolumeSlider = false + let peer = participant.peer + if let muteState = muteState, !muteState.canUnmute || muteState.mutedByYou { + } else { + if callState.canManageCall || callState.myPeerId != id { + hasVolumeSlider = true + + let minValue: CGFloat + if callState.canManageCall && callState.adminIds.contains(peer.id) && muteState != nil { + minValue = 0.01 + } else { + minValue = 0.0 + } + items.append(.custom(VoiceChatVolumeContextItem(minValue: minValue, value: participant.volume.flatMap { CGFloat($0) / 10000.0 } ?? 1.0, valueChanged: { [weak self] newValue, finished in + guard let self, let component = self.component else { + return + } + + if finished && newValue.isZero { + let updatedMuteState = component.call.updateMuteState(peerId: peer.id, isMuted: true) + muteStatePromise.set(.single(updatedMuteState)) + } else { + component.call.setVolume(peerId: peer.id, volume: Int32(newValue * 10000), sync: finished) + } + }), true)) + } + } + + if callState.myPeerId == id && !hasVolumeSlider && ((participant.about?.isEmpty ?? true) || participant.peer.smallProfileImage == nil) { + items.append(.custom(VoiceChatInfoContextItem(text: environment.strings.VoiceChat_ImproveYourProfileText, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Tip"), color: theme.actionSheet.primaryTextColor) + }), true)) + } + + if peer.id == callState.myPeerId { + if participant.hasRaiseHand { + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_CancelSpeakRequest, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/RevokeSpeak"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + guard let self, let component = self.component else { + return + } + component.call.lowerHand() + + f(.default) + }))) + } + items.append(.action(ContextMenuActionItem(text: peer.smallProfileImage == nil ? environment.strings.VoiceChat_AddPhoto : environment.strings.VoiceChat_ChangePhoto, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Camera"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + f(.default) + Queue.mainQueue().after(0.1) { + guard let self else { + return + } + + self.openAvatarForEditing(fromGallery: false, completion: {}) + } + }))) + + items.append(.action(ContextMenuActionItem(text: (participant.about?.isEmpty ?? true) ? environment.strings.VoiceChat_AddBio : environment.strings.VoiceChat_EditBio, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + f(.default) + + Queue.mainQueue().after(0.1) { + guard let self, let component = self.component, let environment = self.environment else { + return + } + let maxBioLength: Int + if peer.id.namespace == Namespaces.Peer.CloudUser { + maxBioLength = 70 + } else { + maxBioLength = 100 + } + let controller = voiceChatTitleEditController(sharedContext: component.call.accountContext.sharedContext, account: component.call.accountContext.account, forceTheme: environment.theme, title: environment.strings.VoiceChat_EditBioTitle, text: environment.strings.VoiceChat_EditBioText, placeholder: environment.strings.VoiceChat_EditBioPlaceholder, doneButtonTitle: environment.strings.VoiceChat_EditBioSave, value: participant.about, maxLength: maxBioLength, apply: { [weak self] bio in + guard let self, let component = self.component, let environment = self.environment, let bio else { + return + } + if peer.id.namespace == Namespaces.Peer.CloudUser { + let _ = (component.call.accountContext.engine.accountData.updateAbout(about: bio) + |> `catch` { _ -> Signal in + return .complete() + }).start() + } else { + let _ = (component.call.accountContext.engine.peers.updatePeerDescription(peerId: peer.id, description: bio) + |> `catch` { _ -> Signal in + return .complete() + }).start() + } + + self.presentUndoOverlay(content: .info(title: nil, text: environment.strings.VoiceChat_EditBioSuccess, timeout: nil, customUndoText: nil), action: { _ in return false }) + }) + environment.controller()?.present(controller, in: .window(.root)) + } + }))) + + if let peer = peer as? TelegramUser { + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_ChangeName, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ChangeName"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + f(.default) + + Queue.mainQueue().after(0.1) { + guard let self, let component = self.component, let environment = self.environment else { + return + } + let controller = voiceChatUserNameController(sharedContext: component.call.accountContext.sharedContext, account: component.call.accountContext.account, forceTheme: environment.theme, title: environment.strings.VoiceChat_ChangeNameTitle, firstNamePlaceholder: environment.strings.UserInfo_FirstNamePlaceholder, lastNamePlaceholder: environment.strings.UserInfo_LastNamePlaceholder, doneButtonTitle: environment.strings.VoiceChat_EditBioSave, firstName: peer.firstName, lastName: peer.lastName, maxLength: 128, apply: { [weak self] firstAndLastName in + guard let self, let component = self.component, let environment = self.environment, let (firstName, lastName) = firstAndLastName else { + return + } + let _ = component.call.accountContext.engine.accountData.updateAccountPeerName(firstName: firstName, lastName: lastName).startStandalone() + + self.presentUndoOverlay(content: .info(title: nil, text: environment.strings.VoiceChat_EditNameSuccess, timeout: nil, customUndoText: nil), action: { _ in return false }) + }) + environment.controller()?.present(controller, in: .window(.root)) + } + }))) + } + } else { + if (callState.canManageCall || callState.adminIds.contains(component.call.accountContext.account.peerId)) { + if callState.adminIds.contains(peer.id) { + if let _ = muteState { + } else { + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_MutePeer, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + guard let self, let component = self.component else { + return + } + + let _ = component.call.updateMuteState(peerId: peer.id, isMuted: true) + f(.default) + }))) + } + } else { + if let muteState = muteState, !muteState.canUnmute { + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_UnmutePeer, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: participant.hasRaiseHand ? "Call/Context Menu/AllowToSpeak" : "Call/Context Menu/Unmute"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + guard let self, let component = self.component, let environment = self.environment else { + return + } + + let _ = component.call.updateMuteState(peerId: peer.id, isMuted: false) + f(.default) + + self.presentUndoOverlay(content: .voiceChatCanSpeak(text: environment.strings.VoiceChat_UserCanNowSpeak(EnginePeer(participant.peer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string), action: { _ in return true }) + }))) + } else { + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_MutePeer, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + guard let self, let component = self.component else { + return + } + + let _ = component.call.updateMuteState(peerId: peer.id, isMuted: true) + f(.default) + }))) + } + } + } else { + if let muteState = muteState, muteState.mutedByYou { + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_UnmuteForMe, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Unmute"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + guard let self, let component = self.component else { + return + } + + let _ = component.call.updateMuteState(peerId: peer.id, isMuted: false) + f(.default) + }))) + } else { + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_MuteForMe, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + guard let self, let component = self.component else { + return + } + + let _ = component.call.updateMuteState(peerId: peer.id, isMuted: true) + f(.default) + }))) + } + } + + let openTitle: String + let openIcon: UIImage? + if [Namespaces.Peer.CloudChannel, Namespaces.Peer.CloudGroup].contains(peer.id.namespace) { + if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + openTitle = environment.strings.VoiceChat_OpenChannel + openIcon = UIImage(bundleImageName: "Chat/Context Menu/Channels") + } else { + openTitle = environment.strings.VoiceChat_OpenGroup + openIcon = UIImage(bundleImageName: "Chat/Context Menu/Groups") + } + } else { + openTitle = environment.strings.Conversation_ContextMenuSendMessage + openIcon = UIImage(bundleImageName: "Chat/Context Menu/Message") + } + items.append(.action(ContextMenuActionItem(text: openTitle, icon: { theme in + return generateTintedImage(image: openIcon, color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + guard let self, let component = self.component, let environment = self.environment else { + return + } + + guard let controller = environment.controller() as? VideoChatScreenV2Impl, let navigationController = controller.parentNavigationController else { + return + } + + let context = component.call.accountContext + environment.controller()?.dismiss(completion: { [weak navigationController] in + Queue.mainQueue().after(0.3) { + guard let navigationController else { + return + } + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(EnginePeer(peer)), keepStack: .always, purposefulAction: {}, peekData: nil)) + } + }) + + f(.dismissWithoutContent) + }))) + + if (callState.canManageCall && !callState.adminIds.contains(peer.id)), peer.id != component.call.peerId { + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_RemovePeer, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor) + }, action: { [weak self] c, _ in + c?.dismiss(completion: { + guard let self, let component = self.component else { + return + } + + let _ = (component.call.accountContext.account.postbox.loadedPeerWithId(component.call.peerId) + |> deliverOnMainQueue).start(next: { [weak self] chatPeer in + guard let self, let component = self.component, let environment = self.environment else { + return + } + + let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) + var items: [ActionSheetItem] = [] + + let nameDisplayOrder = presentationData.nameDisplayOrder + items.append(DeleteChatPeerActionSheetItem(context: component.call.accountContext, peer: EnginePeer(peer), chatPeer: EnginePeer(chatPeer), action: .removeFromGroup, strings: environment.strings, nameDisplayOrder: nameDisplayOrder)) + + items.append(ActionSheetButtonItem(title: environment.strings.VoiceChat_RemovePeerRemove, color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + + guard let self, let component = self.component, let environment = self.environment else { + return + } + + let _ = component.call.accountContext.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: component.call.accountContext.engine, peerId: component.call.peerId, memberId: peer.id, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max)).start() + component.call.removedPeer(peer.id) + + self.presentUndoOverlay(content: .banned(text: environment.strings.VoiceChat_RemovedPeerText(EnginePeer(peer).displayTitle(strings: environment.strings, displayOrder: nameDisplayOrder)).string), action: { _ in return false }) + })) + + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: environment.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) + ]) + environment.controller()?.present(actionSheet, in: .window(.root)) + }) + }) + }))) + } + } + return items + } + + let items = muteStatePromise.get() + |> map { muteState -> [ContextMenuItem] in + return itemsForEntry(muteState) + } + + let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) + let contextController = ContextController( + presentationData: presentationData, + source: .extracted(ParticipantExtractedContentSource(contentView: sourceView)), + items: items |> map { items in + return ContextController.Items(content: .list(items)) + }, + recognizer: nil, + gesture: gesture + ) + + environment.controller()?.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismiss() + } + return true + }) + + environment.controller()?.presentInGlobalOverlay(contextController) + } + + private func openAvatarForEditing(fromGallery: Bool = false, completion: @escaping () -> Void = {}) { + guard let component = self.component else { + return + } + guard let callState = self.callState else { + return + } + let peerId = callState.myPeerId + + let _ = (component.call.accountContext.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), + TelegramEngine.EngineData.Item.Configuration.SearchBots() + ) + |> deliverOnMainQueue).start(next: { [weak self] peer, searchBotsConfiguration in + guard let self, let component = self.component, let environment = self.environment else { + return + } + guard let peer else { + return + } + + let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) + + let legacyController = LegacyController(presentation: .custom, theme: environment.theme) + legacyController.statusBar.statusBarStyle = .Ignore + + let emptyController = LegacyEmptyController(context: legacyController.context)! + let navigationController = makeLegacyNavigationController(rootController: emptyController) + navigationController.setNavigationBarHidden(true, animated: false) + navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0) + + legacyController.bind(controller: navigationController) + + self.endEditing(true) + environment.controller()?.present(legacyController, in: .window(.root)) + + var hasPhotos = false + if !peer.profileImageRepresentations.isEmpty { + hasPhotos = true + } + + let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasPhotos && !fromGallery, hasViewButton: false, personalPhoto: peerId.namespace == Namespaces.Peer.CloudUser, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: false, title: nil, isSuggesting: false)! + mixin.forceDark = true + mixin.stickersContext = LegacyPaintStickersContext(context: component.call.accountContext) + let _ = self.currentAvatarMixin.swap(mixin) + mixin.requestSearchController = { [weak self] assetsController in + guard let self, let component = self.component, let environment = self.environment else { + return + } + let controller = WebSearchController(context: component.call.accountContext, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: peer.id.namespace == Namespaces.Peer.CloudUser ? nil : peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder), completion: { [weak self] result in + assetsController?.dismiss() + + guard let self else { + return + } + self.updateProfilePhoto(result) + })) + controller.navigationPresentation = .modal + environment.controller()?.push(controller) + + if fromGallery { + completion() + } + } + mixin.didFinishWithImage = { [weak self] image in + if let image = image { + completion() + self?.updateProfilePhoto(image) + } + } + mixin.didFinishWithVideo = { [weak self] image, asset, adjustments in + if let image = image, let asset = asset { + completion() + self?.updateProfileVideo(image, asset: asset, adjustments: adjustments) + } + } + mixin.didFinishWithDelete = { [weak self] in + guard let self, let environment = self.environment else { + return + } + + let proceed = { [weak self] in + guard let self, let component = self.component else { + return + } + + let _ = self.currentAvatarMixin.swap(nil) + let postbox = component.call.accountContext.account.postbox + self.updateAvatarDisposable.set((component.call.accountContext.engine.peers.updatePeerPhoto(peerId: peerId, photo: nil, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) + }) + |> deliverOnMainQueue).start()) + } + + let actionSheet = ActionSheetController(presentationData: presentationData) + let items: [ActionSheetItem] = [ + ActionSheetButtonItem(title: environment.strings.Settings_RemoveConfirmation, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + proceed() + }) + ] + + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) + ]) + environment.controller()?.present(actionSheet, in: .window(.root)) + } + mixin.didDismiss = { [weak self, weak legacyController] in + guard let self else { + return + } + let _ = self.currentAvatarMixin.swap(nil) + legacyController?.dismiss() + } + let menuController = mixin.present() + if let menuController = menuController { + menuController.customRemoveFromParentViewController = { [weak legacyController] in + legacyController?.dismiss() + } + } + }) + } + + private func updateProfilePhoto(_ image: UIImage) { + guard let component = self.component else { + return + } + guard let callState = self.callState else { + return + } + guard let data = image.jpegData(compressionQuality: 0.6) else { + return + } + + let peerId = callState.myPeerId + + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + component.call.account.postbox.mediaBox.storeResourceData(resource.id, data: data) + let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) + + self.currentUpdatingAvatar = (representation, 0.0) + + let postbox = component.call.account.postbox + let signal = peerId.namespace == Namespaces.Peer.CloudUser ? component.call.accountContext.engine.accountData.updateAccountPhoto(resource: resource, videoResource: nil, videoStartTimestamp: nil, markup: nil, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) + }) : component.call.accountContext.engine.peers.updatePeerPhoto(peerId: peerId, photo: component.call.accountContext.engine.peers.uploadedPeerPhoto(resource: resource), mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) + }) + + self.updateAvatarDisposable.set((signal + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { + return + } + switch result { + case .complete: + self.currentUpdatingAvatar = nil + self.state?.updated(transition: .spring(duration: 0.4)) + case let .progress(value): + self.currentUpdatingAvatar = (representation, value) + } + })) + + self.state?.updated(transition: .spring(duration: 0.4)) + } + + private func updateProfileVideo(_ image: UIImage, asset: Any?, adjustments: TGVideoEditAdjustments?) { + guard let component = self.component else { + return + } + guard let callState = self.callState else { + return + } + guard let data = image.jpegData(compressionQuality: 0.6) else { + return + } + let peerId = callState.myPeerId + + let photoResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + component.call.accountContext.account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) + let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) + + self.currentUpdatingAvatar = (representation, 0.0) + + var videoStartTimestamp: Double? = nil + if let adjustments = adjustments, adjustments.videoStartValue > 0.0 { + videoStartTimestamp = adjustments.videoStartValue - adjustments.trimStartValue + } + + let context = component.call.accountContext + let account = context.account + let signal = Signal { [weak self] subscriber in + let entityRenderer: LegacyPaintEntityRenderer? = adjustments.flatMap { adjustments in + if let paintingData = adjustments.paintingData, paintingData.hasAnimation { + return LegacyPaintEntityRenderer(postbox: account.postbox, adjustments: adjustments) + } else { + return nil + } + } + + let tempFile = EngineTempBox.shared.tempFile(fileName: "video.mp4") + let uploadInterface = LegacyLiveUploadInterface(context: context) + let signal: SSignal + if let url = asset as? URL, url.absoluteString.hasSuffix(".jpg"), let data = try? Data(contentsOf: url, options: [.mappedRead]), let image = UIImage(data: data), let entityRenderer = entityRenderer { + let durationSignal: SSignal = SSignal(generator: { subscriber in + let disposable = (entityRenderer.duration()).start(next: { duration in + subscriber.putNext(duration) + subscriber.putCompletion() + }) + + return SBlockDisposable(block: { + disposable.dispose() + }) + }) + signal = durationSignal.map(toSignal: { duration -> SSignal in + if let duration = duration as? Double { + return TGMediaVideoConverter.renderUIImage(image, duration: duration, adjustments: adjustments, path: tempFile.path, watcher: nil, entityRenderer: entityRenderer)! + } else { + return SSignal.single(nil) + } + }) + + } else if let asset = asset as? AVAsset { + signal = TGMediaVideoConverter.convert(asset, adjustments: adjustments, path: tempFile.path, watcher: uploadInterface, entityRenderer: entityRenderer)! + } else { + signal = SSignal.complete() + } + + let signalDisposable = signal.start(next: { next in + if let result = next as? TGMediaVideoConversionResult { + if let image = result.coverImage, let data = image.jpegData(compressionQuality: 0.7) { + account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) + } + + if let timestamp = videoStartTimestamp { + videoStartTimestamp = max(0.0, min(timestamp, result.duration - 0.05)) + } + + var value = stat() + if stat(result.fileURL.path, &value) == 0 { + if let data = try? Data(contentsOf: result.fileURL) { + let resource: TelegramMediaResource + if let liveUploadData = result.liveUploadData as? LegacyLiveUploadInterfaceResult { + resource = LocalFileMediaResource(fileId: liveUploadData.id) + } else { + resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + } + account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + subscriber.putNext(resource) + + EngineTempBox.shared.dispose(tempFile) + } + } + subscriber.putCompletion() + } else if let progress = next as? NSNumber { + Queue.mainQueue().async { [weak self] in + guard let self else { + return + } + self.currentUpdatingAvatar = (representation, Float(truncating: progress) * 0.25) + self.state?.updated(transition: .spring(duration: 0.4)) + } + } + }, error: { _ in + }, completed: nil) + + let disposable = ActionDisposable { + signalDisposable?.dispose() + } + + return ActionDisposable { + disposable.dispose() + } + } + + self.updateAvatarDisposable.set((signal + |> mapToSignal { videoResource -> Signal in + if peerId.namespace == Namespaces.Peer.CloudUser { + return context.engine.accountData.updateAccountPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: nil, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) + }) + } else { + return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: photoResource), video: context.engine.peers.uploadedPeerVideo(resource: videoResource) |> map(Optional.init), videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) + }) + } + } + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { + return + } + switch result { + case .complete: + self.currentUpdatingAvatar = nil + self.state?.updated(transition: .spring(duration: 0.4)) + case let .progress(value): + self.currentUpdatingAvatar = (representation, 0.25 + value * 0.75) + self.state?.updated(transition: .spring(duration: 0.4)) + } + })) + } +} + +private final class ParticipantExtractedContentSource: ContextExtractedContentSource { + let keepInPlace: Bool = false + let ignoreContentTouches: Bool = false + let blurBackground: Bool = true + + private let contentView: ContextExtractedContentContainingView + + init(contentView: ContextExtractedContentContainingView) { + self.contentView = contentView + } + + func takeView() -> ContextControllerTakeViewInfo? { + return ContextControllerTakeViewInfo(containingItem: .view(self.contentView), contentAreaInScreenSpace: UIScreen.main.bounds) + } + + func putBack() -> ContextControllerPutBackViewInfo? { + return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) + } +} diff --git a/submodules/TelegramCore/Sources/ApiUtils/BotInfo.swift b/submodules/TelegramCore/Sources/ApiUtils/BotInfo.swift index 440f522cfe..92ee56bc26 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/BotInfo.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/BotInfo.swift @@ -18,7 +18,7 @@ extension BotInfo { switch apiBotInfo { case let .botInfo(_, _, description, descriptionPhoto, descriptionDocument, apiCommands, apiMenuButton, privacyPolicyUrl): let photo: TelegramMediaImage? = descriptionPhoto.flatMap(telegramMediaImageFromApiPhoto) - let video: TelegramMediaFile? = descriptionDocument.flatMap(telegramMediaFileFromApiDocument) + let video: TelegramMediaFile? = descriptionDocument.flatMap { telegramMediaFileFromApiDocument($0, altDocuments: []) } var commands: [BotCommand] = [] if let apiCommands = apiCommands { commands = apiCommands.map { command in diff --git a/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift b/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift index 65fa28f513..40db07ab3f 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift @@ -598,7 +598,7 @@ extension ChatContextResult { if let photo = photo, let parsedImage = telegramMediaImageFromApiPhoto(photo) { image = parsedImage } - if let document = document, let parsedFile = telegramMediaFileFromApiDocument(document) { + if let document = document, let parsedFile = telegramMediaFileFromApiDocument(document, altDocuments: []) { file = parsedFile } self = .internalReference(ChatContextResult.InternalReference(queryId: queryId, id: id, type: type, title: title, description: description, image: image, file: file, message: ChatContextResultMessage(apiMessage: sendMessage))) diff --git a/submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift b/submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift index 09360ff164..ea4bf38bc7 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift @@ -192,7 +192,7 @@ extension InstantPage { } } for file in files { - if let file = telegramMediaFileFromApiDocument(file), let id = file.id { + if let file = telegramMediaFileFromApiDocument(file, altDocuments: []), let id = file.id { media[id] = file } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/ReplyMarkupMessageAttribute.swift b/submodules/TelegramCore/Sources/ApiUtils/ReplyMarkupMessageAttribute.swift index 9c1e70ad3d..13231746a9 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ReplyMarkupMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ReplyMarkupMessageAttribute.swift @@ -89,6 +89,9 @@ extension ReplyMarkupButton { )) } self.init(title: text, titleWhenForwarded: nil, action: .requestPeer(peerType: mappedPeerType, buttonId: buttonId, maxQuantity: maxQuantity)) + case let .keyboardButtonCopy(text, _): + //TODO:release + self.init(title: text, titleWhenForwarded: nil, action: .text) } } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 663cf616b5..5a86902dbf 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -50,7 +50,7 @@ public func tagsForStoreMessage(incoming: Bool, attributes: [MessageAttribute], var isAnimated = false inner: for attribute in file.attributes { switch attribute { - case let .Video(_, _, flags, _, _): + case let .Video(_, _, flags, _, _, _): if flags.contains(.instantRoundVideo) { refinedTag = .voiceOrInstantVideo } else { @@ -350,9 +350,9 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI case let .messageMediaGeoLive(_, geo, heading, period, proximityNotificationRadius): let mediaMap = telegramMediaMapFromApiGeoPoint(geo, title: nil, address: nil, provider: nil, venueId: nil, venueType: nil, liveBroadcastingTimeout: period, liveProximityNotificationRadius: proximityNotificationRadius, heading: heading) return (mediaMap, nil, nil, nil, nil) - case let .messageMediaDocument(flags, document, _, ttlSeconds): + case let .messageMediaDocument(flags, document, altDocuments, ttlSeconds): if let document = document { - if let mediaFile = telegramMediaFileFromApiDocument(document) { + if let mediaFile = telegramMediaFileFromApiDocument(document, altDocuments: altDocuments) { return (mediaFile, ttlSeconds, (flags & (1 << 3)) != 0, (flags & (1 << 4)) != 0, nil) } } else { diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift index 611db7cedc..76b2a46ea6 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift @@ -6,7 +6,7 @@ import TelegramApi func dimensionsForFileAttributes(_ attributes: [TelegramMediaFileAttribute]) -> PixelDimensions? { for attribute in attributes { switch attribute { - case let .Video(_, size, _, _, _): + case let .Video(_, size, _, _, _, _): return size case let .ImageSize(size): return size @@ -20,7 +20,7 @@ func dimensionsForFileAttributes(_ attributes: [TelegramMediaFileAttribute]) -> func durationForFileAttributes(_ attributes: [TelegramMediaFileAttribute]) -> Double? { for attribute in attributes { switch attribute { - case let .Video(duration, _, _, _, _): + case let .Video(duration, _, _, _, _, _): return duration case let .Audio(_, duration, _, _, _): return Double(duration) @@ -99,7 +99,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, preloadSize, videoStart): + case let .documentAttributeVideo(flags, duration, w, h, preloadSize, videoStart, videoCodec): var videoFlags = TelegramMediaVideoFlags() if (flags & (1 << 0)) != 0 { videoFlags.insert(.instantRoundVideo) @@ -110,7 +110,7 @@ func telegramMediaFileAttributesFromApiAttributes(_ attributes: [Api.DocumentAtt if (flags & (1 << 3)) != 0 { videoFlags.insert(.isSilent) } - result.append(.Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags, preloadSize: preloadSize, coverTime: videoStart)) + result.append(.Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags, preloadSize: preloadSize, coverTime: videoStart, videoCodec: videoCodec)) case let .documentAttributeAudio(flags, duration, title, performer, waveform): let isVoice = (flags & (1 << 10)) != 0 let waveformBuffer: Data? = waveform?.makeData() @@ -158,7 +158,7 @@ func telegramMediaFileThumbnailRepresentationsFromApiSizes(datacenterId: Int32, return (immediateThumbnailData, representations) } -func telegramMediaFileFromApiDocument(_ document: Api.Document) -> TelegramMediaFile? { +func telegramMediaFileFromApiDocument(_ document: Api.Document, altDocuments: [Api.Document]?) -> TelegramMediaFile? { switch document { case let .document(_, id, accessHash, fileReference, _, mimeType, size, thumbs, videoThumbs, dcId, attributes): var parsedAttributes = telegramMediaFileAttributesFromApiAttributes(attributes) @@ -182,8 +182,13 @@ func telegramMediaFileFromApiDocument(_ document: Api.Document) -> TelegramMedia } } } + + var alternativeRepresentations: [Media] = [] + if let altDocuments { + alternativeRepresentations = altDocuments.compactMap { telegramMediaFileFromApiDocument($0, altDocuments: []) } + } - return TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: id), partialReference: nil, resource: CloudDocumentMediaResource(datacenterId: Int(dcId), fileId: id, accessHash: accessHash, size: size, fileReference: fileReference.makeData(), fileName: fileNameFromFileAttributes(parsedAttributes)), previewRepresentations: previewRepresentations, videoThumbnails: videoThumbnails, immediateThumbnailData: immediateThumbnail, mimeType: mimeType, size: size, attributes: parsedAttributes) + return TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: id), partialReference: nil, resource: CloudDocumentMediaResource(datacenterId: Int(dcId), fileId: id, accessHash: accessHash, size: size, fileReference: fileReference.makeData(), fileName: fileNameFromFileAttributes(parsedAttributes)), previewRepresentations: previewRepresentations, videoThumbnails: videoThumbnails, immediateThumbnailData: immediateThumbnail, mimeType: mimeType, size: size, attributes: parsedAttributes, alternativeRepresentations: alternativeRepresentations) case .documentEmpty: return nil } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaGame.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaGame.swift index 31d988351b..e4365a2a0e 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaGame.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaGame.swift @@ -9,7 +9,7 @@ extension TelegramMediaGame { case let .game(_, id, accessHash, shortName, title, description, photo, document): var file: TelegramMediaFile? if let document = document { - file = telegramMediaFileFromApiDocument(document) + file = telegramMediaFileFromApiDocument(document, altDocuments: []) } self.init(gameId: id, accessHash: accessHash, name: shortName, title: title, description: description, image: telegramMediaImageFromApiPhoto(photo), file: file) } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift index b5c63bcf8b..a27d6773f8 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift @@ -9,7 +9,7 @@ func telegramMediaWebpageAttributeFromApiWebpageAttribute(_ attribute: Api.WebPa case let .webPageAttributeTheme(_, documents, settings): var files: [TelegramMediaFile] = [] if let documents = documents { - files = documents.compactMap { telegramMediaFileFromApiDocument($0) } + files = documents.compactMap { telegramMediaFileFromApiDocument($0, altDocuments: []) } } return .theme(TelegraMediaWebpageThemeAttribute(files: files, settings: settings.flatMap { TelegramThemeSettings(apiThemeSettings: $0) })) case let .webPageAttributeStickerSet(apiFlags, stickers): @@ -21,7 +21,7 @@ func telegramMediaWebpageAttributeFromApiWebpageAttribute(_ attribute: Api.WebPa flags.insert(.isTemplate) } var files: [TelegramMediaFile] = [] - files = stickers.compactMap { telegramMediaFileFromApiDocument($0) } + files = stickers.compactMap { telegramMediaFileFromApiDocument($0, altDocuments: []) } return .stickerPack(TelegramMediaWebpageStickerPackAttribute(flags: flags, files: files)) case .webPageAttributeStory: return nil @@ -50,7 +50,7 @@ func telegramMediaWebpageFromApiWebpage(_ webpage: Api.WebPage) -> TelegramMedia } var file: TelegramMediaFile? if let document = document { - file = telegramMediaFileFromApiDocument(document) + file = telegramMediaFileFromApiDocument(document, altDocuments: []) } var story: TelegramMediaStory? var webpageAttributes: [TelegramMediaWebpageAttribute] = [] diff --git a/submodules/TelegramCore/Sources/ApiUtils/Theme.swift b/submodules/TelegramCore/Sources/ApiUtils/Theme.swift index 38f4fb1421..01feb7b6b1 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/Theme.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/Theme.swift @@ -8,7 +8,7 @@ extension TelegramTheme { convenience init(apiTheme: Api.Theme) { switch apiTheme { case let .theme(flags, id, accessHash, slug, title, document, settings, emoticon, installCount): - self.init(id: id, accessHash: accessHash, slug: slug, emoticon: emoticon, title: title, file: document.flatMap { telegramMediaFileFromApiDocument($0) }, settings: settings?.compactMap(TelegramThemeSettings.init(apiThemeSettings:)), isCreator: (flags & 1 << 0) != 0, isDefault: (flags & 1 << 1) != 0, installCount: installCount) + self.init(id: id, accessHash: accessHash, slug: slug, emoticon: emoticon, title: title, file: document.flatMap { telegramMediaFileFromApiDocument($0, altDocuments: []) }, settings: settings?.compactMap(TelegramThemeSettings.init(apiThemeSettings:)), isCreator: (flags & 1 << 0) != 0, isDefault: (flags & 1 << 1) != 0, installCount: installCount) } } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/Wallpaper.swift b/submodules/TelegramCore/Sources/ApiUtils/Wallpaper.swift index 9bb1a7ae3f..a7eebe3809 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/Wallpaper.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/Wallpaper.swift @@ -67,7 +67,7 @@ extension TelegramWallpaper { init(apiWallpaper: Api.WallPaper) { switch apiWallpaper { case let .wallPaper(id, flags, accessHash, slug, document, settings): - if let file = telegramMediaFileFromApiDocument(document) { + if let file = telegramMediaFileFromApiDocument(document, altDocuments: []) { let wallpaperSettings: WallpaperSettings if let settings = settings { wallpaperSettings = WallpaperSettings(apiWallpaperSettings: settings) diff --git a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift index e01f826118..113edc7caf 100644 --- a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift +++ b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift @@ -13,12 +13,12 @@ extension MediaResourceReference { } } -final class TelegramCloudMediaResourceFetchInfo: MediaResourceFetchInfo { - let reference: MediaResourceReference - let preferBackgroundReferenceRevalidation: Bool - let continueInBackground: Bool +public final class TelegramCloudMediaResourceFetchInfo: MediaResourceFetchInfo { + public let reference: MediaResourceReference + public let preferBackgroundReferenceRevalidation: Bool + public let continueInBackground: Bool - init(reference: MediaResourceReference, preferBackgroundReferenceRevalidation: Bool, continueInBackground: Bool) { + public init(reference: MediaResourceReference, preferBackgroundReferenceRevalidation: Bool, continueInBackground: Bool) { self.reference = reference self.preferBackgroundReferenceRevalidation = preferBackgroundReferenceRevalidation self.continueInBackground = continueInBackground @@ -493,7 +493,7 @@ final class MediaReferenceRevalidationContext { return .fail(.generic) } for document in result { - if let file = telegramMediaFileFromApiDocument(document) { + if let file = telegramMediaFileFromApiDocument(document, altDocuments: []) { return .single(file) } } @@ -956,9 +956,12 @@ func revalidateMediaResourceReference(accountPeerId: PeerId, postbox: Postbox, n } if let updatedResource = findUpdatedMediaResource(media: media, previousMedia: nil, resource: resource) { return .single(RevalidatedMediaResource(updatedResource: updatedResource, updatedReference: nil)) - } else if let alternativeMedia = item.alternativeMedia, let updatedResource = findUpdatedMediaResource(media: alternativeMedia, previousMedia: nil, resource: resource) { - return .single(RevalidatedMediaResource(updatedResource: updatedResource, updatedReference: nil)) } else { + for alternativeMediaValue in item.alternativeMediaList { + if let updatedResource = findUpdatedMediaResource(media: alternativeMediaValue, previousMedia: nil, resource: resource) { + return .single(RevalidatedMediaResource(updatedResource: updatedResource, updatedReference: nil)) + } + } return .fail(.generic) } } diff --git a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift index 7cafce774d..a5ac46ba0b 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift @@ -205,7 +205,7 @@ func augmentMediaWithReference(_ mediaReference: AnyMediaReference) -> Media { private func convertForwardedMediaForSecretChat(_ media: Media) -> Media { if let file = media as? TelegramMediaFile { - return TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: file.partialReference, resource: file.resource, previewRepresentations: file.previewRepresentations, videoThumbnails: file.videoThumbnails, immediateThumbnailData: file.immediateThumbnailData, mimeType: file.mimeType, size: file.size, attributes: file.attributes) + return TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: file.partialReference, resource: file.resource, previewRepresentations: file.previewRepresentations, videoThumbnails: file.videoThumbnails, immediateThumbnailData: file.immediateThumbnailData, mimeType: file.mimeType, size: file.size, attributes: file.attributes, alternativeRepresentations: []) } else if let image = media as? TelegramMediaImage { return TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: image.reference, partialReference: image.partialReference, flags: []) } else { diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift index 63df2625db..8346d55cb0 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift @@ -703,7 +703,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, preloadSize, coverTime): + case let .Video(duration, size, videoFlags, preloadSize, coverTime, videoCodec): var flags: Int32 = 0 if videoFlags.contains(.instantRoundVideo) { flags |= (1 << 0) @@ -720,7 +720,10 @@ func inputDocumentAttributesFromFileAttributes(_ fileAttributes: [TelegramMediaF if let coverTime = coverTime, coverTime > 0.0 { flags |= (1 << 4) } - attributes.append(.documentAttributeVideo(flags: flags, duration: duration, w: Int32(size.width), h: Int32(size.height), preloadPrefixSize: preloadSize, videoStartTs: coverTime)) + if videoCodec != nil { + flags |= (1 << 5) + } + attributes.append(.documentAttributeVideo(flags: flags, duration: duration, w: Int32(size.width), h: Int32(size.height), preloadPrefixSize: preloadSize, videoStartTs: coverTime, videoCodec: videoCodec)) case let .Audio(isVoice, duration, title, performer, waveform): var flags: Int32 = 0 if isVoice { @@ -790,7 +793,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 { @@ -1065,8 +1068,8 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili |> mapError { _ -> PendingMessageUploadError in return .generic } |> mapToSignal { result -> Signal in switch result { - case let .messageMediaDocument(_, document, _, _): - if let document = document, let mediaFile = telegramMediaFileFromApiDocument(document), let resource = mediaFile.resource as? CloudDocumentMediaResource, let fileReference = resource.fileReference { + case let .messageMediaDocument(_, document, altDocuments, _): + if let document = document, let mediaFile = telegramMediaFileFromApiDocument(document, altDocuments: altDocuments), let resource = mediaFile.resource as? CloudDocumentMediaResource, let fileReference = resource.fileReference { var flags: Int32 = 0 var ttlSeconds: Int32? if let autoclearMessageAttribute = autoclearMessageAttribute { diff --git a/submodules/TelegramCore/Sources/PendingMessages/StandaloneUploadedMedia.swift b/submodules/TelegramCore/Sources/PendingMessages/StandaloneUploadedMedia.swift index 0479620b2d..2f99126412 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/StandaloneUploadedMedia.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/StandaloneUploadedMedia.swift @@ -162,9 +162,9 @@ public func standaloneUploadedFile(postbox: Postbox, network: Network, peerId: P |> mapError { _ -> StandaloneUploadMediaError in return .generic } |> mapToSignal { media -> Signal in switch media { - case let .messageMediaDocument(_, document, _, _): + case let .messageMediaDocument(_, document, altDocuments, _): if let document = document { - if let mediaFile = telegramMediaFileFromApiDocument(document) { + if let mediaFile = telegramMediaFileFromApiDocument(document, altDocuments: altDocuments) { return .single(.result(.media(.standalone(media: mediaFile)))) } } @@ -194,7 +194,7 @@ public func standaloneUploadedFile(postbox: Postbox, network: Network, peerId: P |> mapToSignal { result -> Signal in switch result { case let .encryptedFile(id, accessHash, size, dcId, _): - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: SecretFileMediaResource(fileId: id, accessHash: accessHash, containerSize: size, decryptedSize: size, datacenterId: Int(dcId), key: key), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: size, attributes: attributes) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: SecretFileMediaResource(fileId: id, accessHash: accessHash, containerSize: size, decryptedSize: size, datacenterId: Int(dcId), key: key), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: size, attributes: attributes, alternativeRepresentations: []) return .single(.result(.media(.standalone(media: media)))) case .encryptedFileEmpty: diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index e5b49e557c..ac6cdb6fc3 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -4762,7 +4762,7 @@ func replayFinalState( timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, - alternativeMedia: item.alternativeMedia, + alternativeMediaList: item.alternativeMediaList, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -4796,7 +4796,7 @@ func replayFinalState( timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, - alternativeMedia: item.alternativeMedia, + alternativeMediaList: item.alternativeMediaList, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -4980,7 +4980,7 @@ func replayFinalState( } for apiDocument in documents { - if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id { + if let file = telegramMediaFileFromApiDocument(apiDocument, altDocuments: []), let id = file.id { let fileIndexKeys: [MemoryBuffer] if let indexKeys = indexKeysByFile[id] { fileIndexKeys = indexKeys diff --git a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift index 15d15fb579..9262048466 100644 --- a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift +++ b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift @@ -1196,7 +1196,7 @@ public final class AccountViewTracker { switch result { case let .stickerSet(_, _, _, documents)?: for document in documents { - if let file = telegramMediaFileFromApiDocument(document) { + if let file = telegramMediaFileFromApiDocument(document, altDocuments: []) { if transaction.getMedia(file.fileId) != nil { let _ = transaction.updateMedia(file.fileId, update: file) } diff --git a/submodules/TelegramCore/Sources/State/AvailableMessageEffects.swift b/submodules/TelegramCore/Sources/State/AvailableMessageEffects.swift index 5842b06a9b..8070509f5e 100644 --- a/submodules/TelegramCore/Sources/State/AvailableMessageEffects.swift +++ b/submodules/TelegramCore/Sources/State/AvailableMessageEffects.swift @@ -216,7 +216,7 @@ func managedSynchronizeAvailableMessageEffects(postbox: Postbox, network: Networ case let .availableEffects(hash, effects, documents): var files: [Int64: TelegramMediaFile] = [:] for document in documents { - if let file = telegramMediaFileFromApiDocument(document) { + if let file = telegramMediaFileFromApiDocument(document, altDocuments: []) { files[file.fileId.id] = file } } diff --git a/submodules/TelegramCore/Sources/State/AvailableReactions.swift b/submodules/TelegramCore/Sources/State/AvailableReactions.swift index d0a4f149a0..82e652ade4 100644 --- a/submodules/TelegramCore/Sources/State/AvailableReactions.swift +++ b/submodules/TelegramCore/Sources/State/AvailableReactions.swift @@ -22,7 +22,8 @@ private func generateStarsReactionFile(kind: Int, isAnimatedSticker: Bool) -> Te immediateThumbnailData: nil, mimeType: isAnimatedSticker ? "application/x-tgsticker" : "image/webp", size: nil, - attributes: attributes + attributes: attributes, + alternativeRepresentations: [] ) } @@ -261,23 +262,23 @@ private extension AvailableReactions.Reaction { convenience init?(apiReaction: Api.AvailableReaction) { switch apiReaction { case let .availableReaction(flags, reaction, title, staticIcon, appearAnimation, selectAnimation, activateAnimation, effectAnimation, aroundAnimation, centerIcon): - guard let staticIconFile = telegramMediaFileFromApiDocument(staticIcon) else { + guard let staticIconFile = telegramMediaFileFromApiDocument(staticIcon, altDocuments: []) else { return nil } - guard let appearAnimationFile = telegramMediaFileFromApiDocument(appearAnimation) else { + guard let appearAnimationFile = telegramMediaFileFromApiDocument(appearAnimation, altDocuments: []) else { return nil } - guard let selectAnimationFile = telegramMediaFileFromApiDocument(selectAnimation) else { + guard let selectAnimationFile = telegramMediaFileFromApiDocument(selectAnimation, altDocuments: []) else { return nil } - guard let activateAnimationFile = telegramMediaFileFromApiDocument(activateAnimation) else { + guard let activateAnimationFile = telegramMediaFileFromApiDocument(activateAnimation, altDocuments: []) else { return nil } - guard let effectAnimationFile = telegramMediaFileFromApiDocument(effectAnimation) else { + guard let effectAnimationFile = telegramMediaFileFromApiDocument(effectAnimation, altDocuments: []) else { return nil } - let aroundAnimationFile = aroundAnimation.flatMap { telegramMediaFileFromApiDocument($0) } - let centerAnimationFile = centerIcon.flatMap { telegramMediaFileFromApiDocument($0) } + let aroundAnimationFile = aroundAnimation.flatMap { telegramMediaFileFromApiDocument($0, altDocuments: []) } + let centerAnimationFile = centerIcon.flatMap { telegramMediaFileFromApiDocument($0, altDocuments: []) } let isEnabled = (flags & (1 << 0)) == 0 let isPremium = (flags & (1 << 2)) != 0 self.init( diff --git a/submodules/TelegramCore/Sources/State/Holes.swift b/submodules/TelegramCore/Sources/State/Holes.swift index 2eff15a2db..30b75e8580 100644 --- a/submodules/TelegramCore/Sources/State/Holes.swift +++ b/submodules/TelegramCore/Sources/State/Holes.swift @@ -170,7 +170,7 @@ func resolveUnknownEmojiFiles(postbox: Postbox, source: FetchMessageHistoryHo for documentSet in documentSets { if let documentSet = documentSet { for document in documentSet { - if let file = telegramMediaFileFromApiDocument(document) { + if let file = telegramMediaFileFromApiDocument(document, altDocuments: []) { transaction.storeMediaIfNotPresent(media: file) } } diff --git a/submodules/TelegramCore/Sources/State/ManagedPremiumPromoConfigurationUpdates.swift b/submodules/TelegramCore/Sources/State/ManagedPremiumPromoConfigurationUpdates.swift index 7c26efb038..9f1018bef7 100644 --- a/submodules/TelegramCore/Sources/State/ManagedPremiumPromoConfigurationUpdates.swift +++ b/submodules/TelegramCore/Sources/State/ManagedPremiumPromoConfigurationUpdates.swift @@ -65,7 +65,7 @@ private extension PremiumPromoConfiguration { var videos: [String: TelegramMediaFile] = [:] for (key, document) in zip(videoSections, videoFiles) { - if let file = telegramMediaFileFromApiDocument(document) { + if let file = telegramMediaFileFromApiDocument(document, altDocuments: []) { videos[key] = file } } diff --git a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift index 8e48ce953d..8974f82500 100644 --- a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift +++ b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift @@ -53,7 +53,7 @@ func managedRecentStickers(postbox: Postbox, network: Network, forceFetch: Bool case let .recentStickers(_, _, stickers, _): var items: [OrderedItemListEntry] = [] for sticker in stickers { - if let file = telegramMediaFileFromApiDocument(sticker), let id = file.id { + if let file = telegramMediaFileFromApiDocument(sticker, altDocuments: []), let id = file.id { if let entry = CodableEntry(RecentMediaItem(file)) { items.append(OrderedItemListEntry(id: RecentMediaItemId(id).rawValue, contents: entry)) } @@ -76,7 +76,7 @@ func managedRecentGifs(postbox: Postbox, network: Network, forceFetch: Bool = fa case let .savedGifs(_, gifs): var items: [OrderedItemListEntry] = [] for gif in gifs { - if let file = telegramMediaFileFromApiDocument(gif), let id = file.id { + if let file = telegramMediaFileFromApiDocument(gif, altDocuments: []), let id = file.id { if let entry = CodableEntry(RecentMediaItem(file)) { items.append(OrderedItemListEntry(id: RecentMediaItemId(id).rawValue, contents: entry)) } @@ -114,7 +114,7 @@ func managedSavedStickers(postbox: Postbox, network: Network, forceFetch: Bool = var items: [OrderedItemListEntry] = [] for sticker in stickers { - if let file = telegramMediaFileFromApiDocument(sticker), let id = file.id { + if let file = telegramMediaFileFromApiDocument(sticker, altDocuments: []), let id = file.id { var stringRepresentations: [String] = [] if let representations = fileStringRepresentations[id] { stringRepresentations = representations @@ -141,7 +141,7 @@ func managedGreetingStickers(postbox: Postbox, network: Network) -> Signal Signal Signal UInt { - return 187 + return 188 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/State/StickerManagement.swift b/submodules/TelegramCore/Sources/State/StickerManagement.swift index 42c8bf5adb..a4ac5b9360 100644 --- a/submodules/TelegramCore/Sources/State/StickerManagement.swift +++ b/submodules/TelegramCore/Sources/State/StickerManagement.swift @@ -291,7 +291,7 @@ func parsePreviewStickerSet(_ set: Api.StickerSetCovered, namespace: ItemCollect case let .stickerSetCovered(set, cover): let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace) var items: [StickerPackItem] = [] - if let file = telegramMediaFileFromApiDocument(cover), let id = file.id { + if let file = telegramMediaFileFromApiDocument(cover, altDocuments: []), let id = file.id { items.append(StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: id.id), file: file, indexKeys: [])) } return (info, items) @@ -299,7 +299,7 @@ func parsePreviewStickerSet(_ set: Api.StickerSetCovered, namespace: ItemCollect let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace) var items: [StickerPackItem] = [] for cover in covers { - if let file = telegramMediaFileFromApiDocument(cover), let id = file.id { + if let file = telegramMediaFileFromApiDocument(cover, altDocuments: []), let id = file.id { items.append(StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: id.id), file: file, indexKeys: [])) } } @@ -339,7 +339,7 @@ func parsePreviewStickerSet(_ set: Api.StickerSetCovered, namespace: ItemCollect let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace) var items: [StickerPackItem] = [] for document in documents { - if let file = telegramMediaFileFromApiDocument(document), let id = file.id { + if let file = telegramMediaFileFromApiDocument(document, altDocuments: []), let id = file.id { let fileIndexKeys: [MemoryBuffer] if let indexKeys = indexKeysByFile[id] { fileIndexKeys = indexKeys diff --git a/submodules/TelegramCore/Sources/Statistics/StoryStatistics.swift b/submodules/TelegramCore/Sources/Statistics/StoryStatistics.swift index d734f0a14d..29dd3fbfcb 100644 --- a/submodules/TelegramCore/Sources/Statistics/StoryStatistics.swift +++ b/submodules/TelegramCore/Sources/Statistics/StoryStatistics.swift @@ -321,7 +321,7 @@ private final class StoryStatsPublicForwardsContextImpl { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift index 64adab4a10..fb8c128a80 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift @@ -788,6 +788,35 @@ public enum MediaReference { } } + public func withMedia(_ media: T) -> MediaReference { + switch self { + case .standalone: + return .standalone(media: media) + case let .message(message, _): + return .message(message: message, media: media) + case let .webPage(webPage, _): + return .webPage(webPage: webPage, media: media) + case let .stickerPack(stickerPack, _): + return .stickerPack(stickerPack: stickerPack, media: media) + case .savedGif: + return .savedGif(media: media) + case .savedSticker: + return .savedSticker(media: media) + case .recentSticker: + return .recentSticker(media: media) + case let .avatarList(peer, _): + return .avatarList(peer: peer, media: media) + case let .attachBot(peer, _): + return .attachBot(peer: peer, media: media) + case .customEmoji: + return .customEmoji(media: media) + case let .story(peer, id, _): + return .story(peer: peer, id: id, media: media) + case let .starsTransaction(transaction, _): + return .starsTransaction(transaction: transaction, media: media) + } + } + public func resourceReference(_ resource: MediaResource) -> MediaResourceReference { return .media(media: self.abstract, resource: resource) } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift index aaddf2debb..4e0c73441d 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift @@ -235,7 +235,7 @@ public enum TelegramMediaFileAttribute: PostboxCoding, Equatable { case Sticker(displayText: String, packReference: StickerPackReference?, maskData: StickerMaskCoords?) case ImageSize(size: PixelDimensions) case Animated - case Video(duration: Double, size: PixelDimensions, flags: TelegramMediaVideoFlags, preloadSize: Int32?, coverTime: Double?) + case Video(duration: Double, size: PixelDimensions, flags: TelegramMediaVideoFlags, preloadSize: Int32?, coverTime: Double?, videoCodec: String?) case Audio(isVoice: Bool, duration: Int, title: String?, performer: String?, waveform: Data?) case HasLinkedStickers case hintFileIsLarge @@ -262,7 +262,7 @@ public enum TelegramMediaFileAttribute: PostboxCoding, Equatable { duration = Double(decoder.decodeInt32ForKey("du", orElse: 0)) } - self = .Video(duration: duration, 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"), coverTime: decoder.decodeOptionalDoubleForKey("ct")) + self = .Video(duration: duration, 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"), coverTime: decoder.decodeOptionalDoubleForKey("ct"), videoCodec: decoder.decodeOptionalStringForKey("vc")) case typeAudio: let waveformBuffer = decoder.decodeBytesForKeyNoCopy("wf") var waveform: Data? @@ -309,7 +309,7 @@ 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, preloadSize, coverTime): + case let .Video(duration, size, flags, preloadSize, coverTime, videoCodec): encoder.encodeInt32(typeVideo, forKey: "t") encoder.encodeDouble(duration, forKey: "dur") encoder.encodeInt32(Int32(size.width), forKey: "w") @@ -325,6 +325,11 @@ public enum TelegramMediaFileAttribute: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "ct") } + if let videoCodec { + encoder.encodeString(videoCodec, forKey: "vc") + } else { + encoder.encodeNil(forKey: "vc") + } case let .Audio(isVoice, duration, title, performer, waveform): encoder.encodeInt32(typeAudio, forKey: "t") encoder.encodeInt32(isVoice ? 1 : 0, forKey: "iv") @@ -440,6 +445,7 @@ public final class TelegramMediaFile: Media, Equatable, Codable { public let mimeType: String public let size: Int64? public let attributes: [TelegramMediaFileAttribute] + public let alternativeRepresentations: [Media] public let peerIds: [PeerId] = [] public var id: MediaId? { @@ -459,7 +465,7 @@ public final class TelegramMediaFile: Media, Equatable, Codable { return result.isEmpty ? nil : result } - public init(fileId: MediaId, partialReference: PartialMediaReference?, resource: TelegramMediaResource, previewRepresentations: [TelegramMediaImageRepresentation], videoThumbnails: [TelegramMediaFile.VideoThumbnail], immediateThumbnailData: Data?, mimeType: String, size: Int64?, attributes: [TelegramMediaFileAttribute]) { + public init(fileId: MediaId, partialReference: PartialMediaReference?, resource: TelegramMediaResource, previewRepresentations: [TelegramMediaImageRepresentation], videoThumbnails: [TelegramMediaFile.VideoThumbnail], immediateThumbnailData: Data?, mimeType: String, size: Int64?, attributes: [TelegramMediaFileAttribute], alternativeRepresentations: [Media]) { self.fileId = fileId self.partialReference = partialReference self.resource = resource @@ -469,6 +475,7 @@ public final class TelegramMediaFile: Media, Equatable, Codable { self.mimeType = mimeType self.size = size self.attributes = attributes + self.alternativeRepresentations = alternativeRepresentations } public init(decoder: PostboxDecoder) { @@ -487,6 +494,13 @@ public final class TelegramMediaFile: Media, Equatable, Codable { self.size = nil } self.attributes = decoder.decodeObjectArrayForKey("at") + if let altMedia = try? decoder.decodeObjectArrayWithCustomDecoderForKey("arep", decoder: { d in + return d.decodeRootObject() as! Media + }) { + self.alternativeRepresentations = altMedia + } else { + self.alternativeRepresentations = [] + } } public func encode(_ encoder: PostboxEncoder) { @@ -513,6 +527,9 @@ public final class TelegramMediaFile: Media, Equatable, Codable { encoder.encodeNil(forKey: "s64") } encoder.encodeObjectArray(self.attributes, forKey: "at") + encoder.encodeObjectArrayWithEncoder(self.alternativeRepresentations, forKey: "arep", encoder: { v, e in + e.encodeRootObject(v) + }) } public required init(from decoder: Decoder) throws { @@ -531,6 +548,7 @@ public final class TelegramMediaFile: Media, Equatable, Codable { self.mimeType = object.mimeType self.size = object.size self.attributes = object.attributes + self.alternativeRepresentations = object.alternativeRepresentations } public func encode(to encoder: Encoder) throws { @@ -597,7 +615,7 @@ 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) } } @@ -606,7 +624,7 @@ public final class TelegramMediaFile: Media, Equatable, Codable { public var preloadSize: Int32? { for attribute in self.attributes { - if case .Video(_, _, _, let preloadSize, _) = attribute { + if case .Video(_, _, _, let preloadSize, _, _) = attribute { return preloadSize } } @@ -803,6 +821,10 @@ public final class TelegramMediaFile: Media, Equatable, Codable { return false } + if !areMediaArraysEqual(self.alternativeRepresentations, other.alternativeRepresentations) { + return false + } + return true } @@ -849,27 +871,31 @@ public final class TelegramMediaFile: Media, Equatable, Codable { return false } + if !areMediaArraysSemanticallyEqual(self.alternativeRepresentations, other.alternativeRepresentations) { + return false + } + return true } public func withUpdatedPartialReference(_ partialReference: PartialMediaReference?) -> TelegramMediaFile { - return TelegramMediaFile(fileId: self.fileId, partialReference: partialReference, resource: self.resource, previewRepresentations: self.previewRepresentations, videoThumbnails: self.videoThumbnails, immediateThumbnailData: self.immediateThumbnailData, mimeType: self.mimeType, size: self.size, attributes: self.attributes) + return TelegramMediaFile(fileId: self.fileId, partialReference: partialReference, resource: self.resource, previewRepresentations: self.previewRepresentations, videoThumbnails: self.videoThumbnails, immediateThumbnailData: self.immediateThumbnailData, mimeType: self.mimeType, size: self.size, attributes: self.attributes, alternativeRepresentations: self.alternativeRepresentations) } public func withUpdatedResource(_ resource: TelegramMediaResource) -> TelegramMediaFile { - return TelegramMediaFile(fileId: self.fileId, partialReference: self.partialReference, resource: resource, previewRepresentations: self.previewRepresentations, videoThumbnails: self.videoThumbnails, immediateThumbnailData: self.immediateThumbnailData, mimeType: self.mimeType, size: self.size, attributes: self.attributes) + return TelegramMediaFile(fileId: self.fileId, partialReference: self.partialReference, resource: resource, previewRepresentations: self.previewRepresentations, videoThumbnails: self.videoThumbnails, immediateThumbnailData: self.immediateThumbnailData, mimeType: self.mimeType, size: self.size, attributes: self.attributes, alternativeRepresentations: self.alternativeRepresentations) } public func withUpdatedSize(_ size: Int64?) -> TelegramMediaFile { - return TelegramMediaFile(fileId: self.fileId, partialReference: self.partialReference, resource: self.resource, previewRepresentations: self.previewRepresentations, videoThumbnails: self.videoThumbnails, immediateThumbnailData: self.immediateThumbnailData, mimeType: self.mimeType, size: size, attributes: self.attributes) + return TelegramMediaFile(fileId: self.fileId, partialReference: self.partialReference, resource: self.resource, previewRepresentations: self.previewRepresentations, videoThumbnails: self.videoThumbnails, immediateThumbnailData: self.immediateThumbnailData, mimeType: self.mimeType, size: size, attributes: self.attributes, alternativeRepresentations: self.alternativeRepresentations) } public func withUpdatedPreviewRepresentations(_ previewRepresentations: [TelegramMediaImageRepresentation]) -> TelegramMediaFile { - return TelegramMediaFile(fileId: self.fileId, partialReference: self.partialReference, resource: self.resource, previewRepresentations: previewRepresentations, videoThumbnails: self.videoThumbnails, immediateThumbnailData: self.immediateThumbnailData, mimeType: self.mimeType, size: self.size, attributes: self.attributes) + return TelegramMediaFile(fileId: self.fileId, partialReference: self.partialReference, resource: self.resource, previewRepresentations: previewRepresentations, videoThumbnails: self.videoThumbnails, immediateThumbnailData: self.immediateThumbnailData, mimeType: self.mimeType, size: self.size, attributes: self.attributes, alternativeRepresentations: self.alternativeRepresentations) } public func withUpdatedAttributes(_ attributes: [TelegramMediaFileAttribute]) -> TelegramMediaFile { - return TelegramMediaFile(fileId: self.fileId, partialReference: self.partialReference, resource: self.resource, previewRepresentations: self.previewRepresentations, videoThumbnails: self.videoThumbnails, immediateThumbnailData: self.immediateThumbnailData, mimeType: self.mimeType, size: self.size, attributes: attributes) + return TelegramMediaFile(fileId: self.fileId, partialReference: self.partialReference, resource: self.resource, previewRepresentations: self.previewRepresentations, videoThumbnails: self.videoThumbnails, immediateThumbnailData: self.immediateThumbnailData, mimeType: self.mimeType, size: self.size, attributes: attributes, alternativeRepresentations: self.alternativeRepresentations) } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramWallpaper.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramWallpaper.swift index 5c0518226e..0872978f07 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramWallpaper.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramWallpaper.swift @@ -179,7 +179,7 @@ public struct TelegramWallpaperNativeCodable: Codable { public enum TelegramWallpaper: Equatable { public static func emoticonWallpaper(emoticon: String) -> TelegramWallpaper { - return .file(File(id: -1, accessHash: -1, isCreator: false, isDefault: false, isPattern: false, isDark: false, slug: "", file: TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: EmptyMediaResource(), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "", size: nil, attributes: []), settings: WallpaperSettings(emoticon: emoticon))) + return .file(File(id: -1, accessHash: -1, isCreator: false, isDefault: false, isPattern: false, isDark: false, slug: "", file: TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: EmptyMediaResource(), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "", size: nil, attributes: [], alternativeRepresentations: []), settings: WallpaperSettings(emoticon: emoticon))) } public struct Gradient: Equatable { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift index acc75aa7db..96d3dd1d93 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift @@ -615,7 +615,7 @@ func _internal_markAdAction(account: Account, peerId: EnginePeer.Id, opaqueId: D guard let inputChannel = inputChannel else { return .complete() } - return account.network.request(Api.functions.channels.clickSponsoredMessage(channel: inputChannel, randomId: Buffer(data: opaqueId))) + return account.network.request(Api.functions.channels.clickSponsoredMessage(flags: 0, channel: inputChannel, randomId: Buffer(data: opaqueId))) |> `catch` { _ -> Signal in return .single(.boolFalse) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift index 0123ff375c..d353439b2c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift @@ -309,7 +309,7 @@ func managedSynchronizeAttachMenuBots(accountPeerId: PeerId, postbox: Postbox, n for icon in botIcons { switch icon { case let .attachMenuBotIcon(_, name, icon, _): - if let iconName = AttachMenuBots.Bot.IconName(string: name), let icon = telegramMediaFileFromApiDocument(icon) { + if let iconName = AttachMenuBots.Bot.IconName(string: name), let icon = telegramMediaFileFromApiDocument(icon, altDocuments: []) { icons[iconName] = icon } } @@ -544,7 +544,7 @@ func _internal_getAttachMenuBot(accountPeerId: PeerId, postbox: Postbox, network for icon in botIcons { switch icon { case let .attachMenuBotIcon(_, name, icon, _): - if let iconName = AttachMenuBots.Bot.IconName(string: name), let icon = telegramMediaFileFromApiDocument(icon) { + if let iconName = AttachMenuBots.Bot.IconName(string: name), let icon = telegramMediaFileFromApiDocument(icon, altDocuments: []) { icons[iconName] = icon } } @@ -755,7 +755,7 @@ func _internal_getBotApp(account: Account, reference: BotAppReference) -> Signal if (botAppFlags & (1 << 2)) != 0 { appFlags.insert(.hasSettings) } - return .single(BotApp(id: id, accessHash: accessHash, shortName: shortName, title: title, description: description, photo: telegramMediaImageFromApiPhoto(photo), document: document.flatMap(telegramMediaFileFromApiDocument), hash: hash, flags: appFlags)) + return .single(BotApp(id: id, accessHash: accessHash, shortName: shortName, title: title, description: description, photo: telegramMediaImageFromApiPhoto(photo), document: document.flatMap { telegramMediaFileFromApiDocument($0, altDocuments: []) }, hash: hash, flags: appFlags)) case .botAppNotModified: return .complete() } @@ -770,7 +770,7 @@ extension BotApp { convenience init?(apiBotApp: Api.BotApp) { switch apiBotApp { case let .botApp(_, id, accessHash, shortName, title, description, photo, document, hash): - self.init(id: id, accessHash: accessHash, shortName: shortName, title: title, description: description, photo: telegramMediaImageFromApiPhoto(photo), document: document.flatMap(telegramMediaFileFromApiDocument), hash: hash, flags: []) + self.init(id: id, accessHash: accessHash, shortName: shortName, title: title, description: description, photo: telegramMediaImageFromApiPhoto(photo), document: document.flatMap { telegramMediaFileFromApiDocument($0, altDocuments: []) }, hash: hash, flags: []) case .botAppNotModified: return nil } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift index 523354bb0f..efcd222dd5 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift @@ -530,7 +530,7 @@ public final class EngineStoryViewListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -575,7 +575,7 @@ public final class EngineStoryViewListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, - alternativeMedia: item.alternativeMedia, + alternativeMediaList: item.alternativeMediaList, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -615,7 +615,7 @@ public final class EngineStoryViewListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, - alternativeMedia: item.alternativeMedia, + alternativeMediaList: item.alternativeMediaList, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -727,7 +727,7 @@ public final class EngineStoryViewListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift index 019afadf9e..c93f33e990 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift @@ -118,7 +118,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: externalReference.content?.duration ?? 0.0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil)) + fileAttributes.append(.Video(duration: externalReference.content?.duration ?? 0.0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)) } } @@ -136,7 +136,7 @@ func _internal_outgoingMessageWithChatContextResult(to peerId: PeerId, threadId: resource = EmptyMediaResource() } - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: videoThumbnails, immediateThumbnailData: nil, mimeType: externalReference.content?.mimeType ?? "application/binary", size: nil, attributes: fileAttributes) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: videoThumbnails, immediateThumbnailData: nil, mimeType: externalReference.content?.mimeType ?? "application/binary", size: nil, attributes: fileAttributes, alternativeRepresentations: []) return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: file), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) } else { return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift index 989ff1ddd8..f282a1ad39 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift @@ -742,7 +742,7 @@ extension TelegramBusinessIntro { convenience init(apiBusinessIntro: Api.BusinessIntro) { switch apiBusinessIntro { case let .businessIntro(_, title, description, sticker): - self.init(title: title, text: description, stickerFile: sticker.flatMap(telegramMediaFileFromApiDocument)) + self.init(title: title, text: description, stickerFile: sticker.flatMap { telegramMediaFileFromApiDocument($0, altDocuments: []) }) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index d26c411426..2336fd44b7 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -245,6 +245,7 @@ public enum Stories { case expirationTimestamp case media case alternativeMedia + case alternativeMediaList case mediaAreas case text case entities @@ -268,7 +269,7 @@ public enum Stories { public let timestamp: Int32 public let expirationTimestamp: Int32 public let media: Media? - public let alternativeMedia: Media? + public let alternativeMediaList: [Media] public let mediaAreas: [MediaArea] public let text: String public let entities: [MessageTextEntity] @@ -292,7 +293,7 @@ public enum Stories { timestamp: Int32, expirationTimestamp: Int32, media: Media?, - alternativeMedia: Media?, + alternativeMediaList: [Media], mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], @@ -315,7 +316,7 @@ public enum Stories { self.timestamp = timestamp self.expirationTimestamp = expirationTimestamp self.media = media - self.alternativeMedia = alternativeMedia + self.alternativeMediaList = alternativeMediaList self.mediaAreas = mediaAreas self.text = text self.entities = entities @@ -348,10 +349,18 @@ public enum Stories { self.media = nil } - if let alternativeMediaData = try container.decodeIfPresent(Data.self, forKey: .alternativeMedia) { - self.alternativeMedia = PostboxDecoder(buffer: MemoryBuffer(data: alternativeMediaData)).decodeRootObject() as? Media + if let alternativeMediaListData = try container.decodeIfPresent([Data].self, forKey: .alternativeMediaList) { + self.alternativeMediaList = alternativeMediaListData.compactMap { data -> Media? in + return PostboxDecoder(buffer: MemoryBuffer(data: data)).decodeRootObject() as? Media + } + } else if let alternativeMediaData = try container.decodeIfPresent(Data.self, forKey: .alternativeMedia) { + if let value = PostboxDecoder(buffer: MemoryBuffer(data: alternativeMediaData)).decodeRootObject() as? Media { + self.alternativeMediaList = [value] + } else { + self.alternativeMediaList = [] + } } else { - self.alternativeMedia = nil + self.alternativeMediaList = [] } self.mediaAreas = try container.decodeIfPresent([MediaArea].self, forKey: .mediaAreas) ?? [] @@ -388,12 +397,12 @@ public enum Stories { try container.encode(mediaData, forKey: .media) } - if let alternativeMedia = self.alternativeMedia { + let alternativeMediaListData = self.alternativeMediaList.map { alternativeMediaValue -> Data in let encoder = PostboxEncoder() - encoder.encodeRootObject(alternativeMedia) - let alternativeMediaData = encoder.makeData() - try container.encode(alternativeMediaData, forKey: .alternativeMedia) + encoder.encodeRootObject(alternativeMediaValue) + return encoder.makeData() } + try container.encode(alternativeMediaListData, forKey: .alternativeMediaList) try container.encode(self.mediaAreas, forKey: .mediaAreas) @@ -436,14 +445,8 @@ public enum Stories { } } - if let lhsAlternativeMedia = lhs.alternativeMedia, let rhsAlternativeMedia = rhs.alternativeMedia { - if !lhsAlternativeMedia.isEqual(to: rhsAlternativeMedia) { - return false - } - } else { - if (lhs.alternativeMedia == nil) != (rhs.alternativeMedia == nil) { - return false - } + if !areMediaArraysEqual(lhs.alternativeMediaList, rhs.alternativeMediaList) { + return false } if lhs.mediaAreas != rhs.mediaAreas { @@ -871,8 +874,9 @@ private func prepareUploadStoryContent(account: Account, media: EngineStoryInput mimeType: "video/mp4", size: nil, attributes: [ - TelegramMediaFileAttribute.Video(duration: duration, size: dimensions, flags: .supportsStreaming, preloadSize: nil, coverTime: coverTime) - ] + TelegramMediaFileAttribute.Video(duration: duration, size: dimensions, flags: .supportsStreaming, preloadSize: nil, coverTime: coverTime, videoCodec: nil) + ], + alternativeRepresentations: [] ) return fileMedia @@ -1209,7 +1213,7 @@ func _internal_uploadStoryImpl( timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, - alternativeMedia: item.alternativeMedia, + alternativeMediaList: item.alternativeMediaList, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -1617,7 +1621,7 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, - alternativeMedia: item.alternativeMedia, + alternativeMediaList: item.alternativeMediaList, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -1649,7 +1653,7 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, - alternativeMedia: item.alternativeMedia, + alternativeMediaList: item.alternativeMediaList, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -1846,7 +1850,7 @@ func _internal_updateStoriesArePinned(account: Account, peerId: PeerId, ids: [In timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, - alternativeMedia: item.alternativeMedia, + alternativeMediaList: item.alternativeMediaList, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -1877,7 +1881,7 @@ func _internal_updateStoriesArePinned(account: Account, peerId: PeerId, ids: [In timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, - alternativeMedia: item.alternativeMedia, + alternativeMediaList: item.alternativeMediaList, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -2093,11 +2097,11 @@ extension Stories.StoredItem { mergedForwardInfo = forwardFrom.flatMap(Stories.Item.ForwardInfo.init(apiForwardInfo:)) } - var parsedAlternativeMedia: Media? + var parsedAlternativeMedia: [Media] = [] switch media { - case let .messageMediaDocument(_, _, altDocument, _): - if let altDocument = altDocument { - parsedAlternativeMedia = telegramMediaFileFromApiDocument(altDocument) + case let .messageMediaDocument(_, _, altDocuments, _): + if let altDocuments { + parsedAlternativeMedia = altDocuments.compactMap { telegramMediaFileFromApiDocument($0, altDocuments: []) } } default: break @@ -2108,7 +2112,7 @@ extension Stories.StoredItem { timestamp: date, expirationTimestamp: expireDate, media: parsedMedia, - alternativeMedia: parsedAlternativeMedia, + alternativeMediaList: parsedAlternativeMedia, mediaAreas: mediaAreas?.compactMap(mediaAreaFromApiMediaArea) ?? [], text: caption ?? "", entities: entities.flatMap { entities in return messageTextEntitiesFromApiEntities(entities) } ?? [], @@ -2173,7 +2177,7 @@ func _internal_getStoryById(accountPeerId: PeerId, postbox: Postbox, network: Ne timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -2656,7 +2660,7 @@ func _internal_setStoryReaction(account: Account, peerId: EnginePeer.Id, id: Int timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, - alternativeMedia: item.alternativeMedia, + alternativeMediaList: item.alternativeMediaList, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -2690,7 +2694,7 @@ func _internal_setStoryReaction(account: Account, peerId: EnginePeer.Id, id: Int timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, - alternativeMedia: item.alternativeMedia, + alternativeMediaList: item.alternativeMediaList, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index e22036f50a..77b650f91e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -67,7 +67,7 @@ public final class EngineStoryItem: Equatable { public let timestamp: Int32 public let expirationTimestamp: Int32 public let media: EngineMedia - public let alternativeMedia: EngineMedia? + public let alternativeMediaList: [EngineMedia] public let mediaAreas: [MediaArea] public let text: String public let entities: [MessageTextEntity] @@ -87,12 +87,12 @@ public final class EngineStoryItem: Equatable { public let forwardInfo: ForwardInfo? public let author: EnginePeer? - public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, alternativeMedia: EngineMedia?, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool, isPending: Bool, isCloseFriends: Bool, isContacts: Bool, isSelectedContacts: Bool, isForwardingDisabled: Bool, isEdited: Bool, isMy: Bool, myReaction: MessageReaction.Reaction?, forwardInfo: ForwardInfo?, author: EnginePeer?) { + public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, alternativeMediaList: [EngineMedia], mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool, isPending: Bool, isCloseFriends: Bool, isContacts: Bool, isSelectedContacts: Bool, isForwardingDisabled: Bool, isEdited: Bool, isMy: Bool, myReaction: MessageReaction.Reaction?, forwardInfo: ForwardInfo?, author: EnginePeer?) { self.id = id self.timestamp = timestamp self.expirationTimestamp = expirationTimestamp self.media = media - self.alternativeMedia = alternativeMedia + self.alternativeMediaList = alternativeMediaList self.mediaAreas = mediaAreas self.text = text self.entities = entities @@ -126,7 +126,7 @@ public final class EngineStoryItem: Equatable { if lhs.media != rhs.media { return false } - if lhs.alternativeMedia != rhs.alternativeMedia { + if lhs.alternativeMediaList != rhs.alternativeMediaList { return false } if lhs.mediaAreas != rhs.mediaAreas { @@ -205,7 +205,7 @@ public extension EngineStoryItem { timestamp: self.timestamp, expirationTimestamp: self.expirationTimestamp, media: self.media._asMedia(), - alternativeMedia: self.alternativeMedia?._asMedia(), + alternativeMediaList: self.alternativeMediaList.map { $0._asMedia() }, mediaAreas: self.mediaAreas, text: self.text, entities: self.entities, @@ -670,7 +670,7 @@ public final class PeerStoryListContext: StoryListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -839,7 +839,7 @@ public final class PeerStoryListContext: StoryListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -1013,7 +1013,7 @@ public final class PeerStoryListContext: StoryListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -1062,7 +1062,7 @@ public final class PeerStoryListContext: StoryListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -1113,7 +1113,7 @@ public final class PeerStoryListContext: StoryListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -1170,7 +1170,7 @@ public final class PeerStoryListContext: StoryListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -1416,7 +1416,7 @@ public final class SearchStoryListContext: StoryListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -1565,7 +1565,7 @@ public final class SearchStoryListContext: StoryListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -1637,7 +1637,7 @@ public final class SearchStoryListContext: StoryListContext { timestamp: item.storyItem.timestamp, expirationTimestamp: item.storyItem.expirationTimestamp, media: item.storyItem.media, - alternativeMedia: item.storyItem.alternativeMedia, + alternativeMediaList: item.storyItem.alternativeMediaList, mediaAreas: item.storyItem.mediaAreas, text: item.storyItem.text, entities: item.storyItem.entities, @@ -1755,7 +1755,7 @@ public final class PeerExpiringStoryListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -2211,7 +2211,7 @@ public final class BotPreviewStoryListContext: StoryListContext { timestamp: 0, expirationTimestamp: Int32.max, media: EngineMedia(item.media), - alternativeMedia: nil, + alternativeMediaList: [], mediaAreas: [], text: "", entities: [], @@ -2260,7 +2260,7 @@ public final class BotPreviewStoryListContext: StoryListContext { timestamp: item.timestamp, expirationTimestamp: Int32.max, media: EngineMedia(item.media), - alternativeMedia: nil, + alternativeMediaList: [], mediaAreas: [], text: "", entities: [], @@ -2371,7 +2371,7 @@ public final class BotPreviewStoryListContext: StoryListContext { timestamp: item.timestamp, expirationTimestamp: Int32.max, media: EngineMedia(item.media), - alternativeMedia: nil, + alternativeMediaList: [], mediaAreas: [], text: "", entities: [], @@ -2447,7 +2447,7 @@ public final class BotPreviewStoryListContext: StoryListContext { timestamp: 0, expirationTimestamp: Int32.max, media: EngineMedia(item.media), - alternativeMedia: nil, + alternativeMediaList: [], mediaAreas: [], text: "", entities: [], @@ -2510,7 +2510,7 @@ public final class BotPreviewStoryListContext: StoryListContext { timestamp: item.timestamp, expirationTimestamp: Int32.max, media: EngineMedia(item.media), - alternativeMedia: nil, + alternativeMediaList: [], mediaAreas: [], text: "", entities: [], diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index cec07550e2..41d9e9b22f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -1235,8 +1235,8 @@ public extension TelegramEngine { } var selectedMedia: EngineMedia - if let alternativeMedia = itemAndPeer.item.alternativeMedia.flatMap(EngineMedia.init), (!preferHighQuality && !itemAndPeer.item.isMy) { - selectedMedia = alternativeMedia + if let alternativeMediaValue = itemAndPeer.item.alternativeMediaList.first.flatMap(EngineMedia.init), (!preferHighQuality && !itemAndPeer.item.isMy) { + selectedMedia = alternativeMediaValue } else { selectedMedia = EngineMedia(media) } @@ -1277,7 +1277,7 @@ public extension TelegramEngine { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, - alternativeMedia: item.alternativeMedia, + alternativeMediaList: item.alternativeMediaList, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/NotificationSoundList.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/NotificationSoundList.swift index ce0e9ea56c..2ea20ee4b9 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/NotificationSoundList.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/NotificationSoundList.swift @@ -104,7 +104,7 @@ public final class NotificationSoundList: Equatable, Codable { private extension NotificationSoundList.NotificationSound { convenience init?(apiDocument: Api.Document) { - guard let file = telegramMediaFileFromApiDocument(apiDocument) else { + guard let file = telegramMediaFileFromApiDocument(apiDocument, altDocuments: []) else { return nil } self.init(file: file) @@ -313,7 +313,7 @@ func _internal_uploadNotificationSound(account: Account, title: String, data: Da return .generic } |> mapToSignal { result -> Signal in - guard let file = telegramMediaFileFromApiDocument(result) else { + guard let file = telegramMediaFileFromApiDocument(result, altDocuments: []) else { return .fail(.generic) } return account.postbox.transaction { transaction -> NotificationSoundList.NotificationSound in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift index 6ba7171906..f8a8e133b1 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift @@ -80,15 +80,15 @@ func _internal_uploadSticker(account: Account, peer: Peer, resource: MediaResour var attributes: [Api.DocumentAttribute] = [] attributes.append(.documentAttributeSticker(flags: 0, alt: alt, stickerset: .inputStickerSetEmpty, maskCoords: nil)) if let duration { - attributes.append(.documentAttributeVideo(flags: 0, duration: duration, w: dimensions.width, h: dimensions.height, preloadPrefixSize: nil, videoStartTs: nil)) + attributes.append(.documentAttributeVideo(flags: 0, duration: duration, w: dimensions.width, h: dimensions.height, preloadPrefixSize: nil, videoStartTs: nil, videoCodec: nil)) } attributes.append(.documentAttributeImageSize(w: dimensions.width, h: dimensions.height)) return account.network.request(Api.functions.messages.uploadMedia(flags: 0, businessConnectionId: nil, peer: inputPeer, media: Api.InputMedia.inputMediaUploadedDocument(flags: flags, file: file, thumb: thumbnailFile, mimeType: mimeType, attributes: attributes, stickers: nil, ttlSeconds: nil))) |> mapError { _ -> UploadStickerError in return .generic } |> mapToSignal { media -> Signal in switch media { - case let .messageMediaDocument(_, document, _, _): - if let document = document, let file = telegramMediaFileFromApiDocument(document), let uploadedResource = file.resource as? CloudDocumentMediaResource { + case let .messageMediaDocument(_, document, altDocuments, _): + if let document = document, let file = telegramMediaFileFromApiDocument(document, altDocuments: altDocuments), let uploadedResource = file.resource as? CloudDocumentMediaResource { account.postbox.mediaBox.copyResourceData(from: resource.id, to: uploadedResource.id, synchronous: true) if let thumbnail, let previewRepresentation = file.previewRepresentations.first(where: { $0.dimensions == PixelDimensions(width: 320, height: 320) }) { account.postbox.mediaBox.copyResourceData(from: thumbnail.id, to: previewRepresentation.resource.id, synchronous: true) @@ -144,7 +144,7 @@ public extension ImportSticker { fileAttributes.append(.FileName(fileName: "sticker.webm")) fileAttributes.append(.Animated) fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil)) - fileAttributes.append(.Video(duration: self.duration ?? 3.0, size: self.dimensions, flags: [], preloadSize: nil, coverTime: nil)) + fileAttributes.append(.Video(duration: self.duration ?? 3.0, size: self.dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)) } else if self.mimeType == "application/x-tgsticker" { fileAttributes.append(.FileName(fileName: "sticker.tgs")) fileAttributes.append(.Animated) @@ -159,7 +159,7 @@ public extension ImportSticker { previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 320, height: 320), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil)) } - return StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: 0), file: TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 0), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: self.mimeType, size: nil, attributes: fileAttributes), indexKeys: []) + return StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: 0), file: TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 0), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: self.mimeType, size: nil, attributes: fileAttributes, alternativeRepresentations: []), indexKeys: []) } } @@ -560,7 +560,7 @@ func _internal_getMyStickerSets(account: Account) -> Signal<[(StickerPackCollect } let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace) var firstItem: StickerPackItem? - if let file = telegramMediaFileFromApiDocument(cover), let id = file.id { + if let file = telegramMediaFileFromApiDocument(cover, altDocuments: []), let id = file.id { firstItem = StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: id.id), file: file, indexKeys: []) } infos.append((info, firstItem)) @@ -579,7 +579,7 @@ func _internal_getMyStickerSets(account: Account) -> Signal<[(StickerPackCollect let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace) var firstItem: StickerPackItem? if let apiDocument = documents.first { - if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id { + if let file = telegramMediaFileFromApiDocument(apiDocument, altDocuments: []), let id = file.id { firstItem = StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: id.id), file: file, indexKeys: []) } } @@ -642,7 +642,7 @@ private func parseStickerSetInfoAndItems(apiStickerSet: Api.messages.StickerSet) var items: [StickerPackItem] = [] for apiDocument in documents { - if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id { + if let file = telegramMediaFileFromApiDocument(apiDocument, altDocuments: []), let id = file.id { let fileIndexKeys: [MemoryBuffer] if let indexKeys = indexKeysByFile[id] { fileIndexKeys = indexKeys diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/LoadedStickerPack.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/LoadedStickerPack.swift index b89e44c713..e2f78fa910 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/LoadedStickerPack.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/LoadedStickerPack.swift @@ -100,7 +100,7 @@ func updatedRemoteStickerPack(postbox: Postbox, network: Network, reference: Sti } for apiDocument in documents { - if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id { + if let file = telegramMediaFileFromApiDocument(apiDocument, altDocuments: []), let id = file.id { let fileIndexKeys: [MemoryBuffer] if let indexKeys = indexKeysByFile[id] { fileIndexKeys = indexKeys diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/SearchStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/SearchStickers.swift index 70ff4f5190..66d4f6abf3 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/SearchStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/SearchStickers.swift @@ -320,7 +320,7 @@ func _internal_searchStickers(account: Account, query: [String], scope: SearchSt var files: [TelegramMediaFile] = [] for sticker in stickers { - if let file = telegramMediaFileFromApiDocument(sticker), let id = file.id { + if let file = telegramMediaFileFromApiDocument(sticker, altDocuments: []), let id = file.id { files.append(file) if !currentItemIds.contains(id) { if file.isPremiumSticker { @@ -705,7 +705,7 @@ func _internal_searchStickers(account: Account, category: EmojiSearchCategories. var files: [TelegramMediaFile] = [] for sticker in stickers { - if let file = telegramMediaFileFromApiDocument(sticker), let id = file.id { + if let file = telegramMediaFileFromApiDocument(sticker, altDocuments: []), let id = file.id { files.append(file) if !currentItemIds.contains(id) { if file.isPremiumSticker { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerSetInstallation.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerSetInstallation.swift index 657fb8c198..db64fe57ab 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerSetInstallation.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerSetInstallation.swift @@ -117,7 +117,7 @@ func _internal_requestStickerSet(postbox: Postbox, network: Network, reference: } for apiDocument in documents { - if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id { + if let file = telegramMediaFileFromApiDocument(apiDocument, altDocuments: []), let id = file.id { let fileIndexKeys: [MemoryBuffer] if let indexKeys = indexKeysByFile[id] { fileIndexKeys = indexKeys @@ -199,7 +199,7 @@ func _internal_installStickerSetInteractively(account: Account, info: StickerPac var items:[StickerPackItem] = [] for apiDocument in apiDocuments { - if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id { + if let file = telegramMediaFileFromApiDocument(apiDocument, altDocuments: []), let id = file.id { items.append(StickerPackItem(index: ItemCollectionItemIndex(index: Int32(items.count), id: id.id), file: file, indexKeys: [])) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift index a3cf7bfac5..9e1d3d0a6e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift @@ -361,7 +361,7 @@ public func _internal_resolveInlineStickers(postbox: Postbox, network: Network, for result in documentSets { if let result = result { for document in result { - if let file = telegramMediaFileFromApiDocument(document) { + if let file = telegramMediaFileFromApiDocument(document, altDocuments: []) { resultFiles[file.fileId.id] = file transaction.storeMediaIfNotPresent(media: file) } diff --git a/submodules/TelegramCore/Sources/Themes.swift b/submodules/TelegramCore/Sources/Themes.swift index 92d1992607..9bb600e89a 100644 --- a/submodules/TelegramCore/Sources/Themes.swift +++ b/submodules/TelegramCore/Sources/Themes.swift @@ -258,7 +258,7 @@ private func uploadTheme(account: Account, resource: MediaResource, thumbnailDat return account.network.request(Api.functions.account.uploadTheme(flags: flags, file: file, thumb: thumbnailFile, fileName: fileName, mimeType: mimeType)) |> mapError { _ in return UploadThemeError.generic } |> mapToSignal { document -> Signal in - if let file = telegramMediaFileFromApiDocument(document) { + if let file = telegramMediaFileFromApiDocument(document, altDocuments: []) { return .single(.complete(file)) } else { return .fail(.generic) diff --git a/submodules/TelegramCore/Sources/Utils/ImageRepresentationsUtils.swift b/submodules/TelegramCore/Sources/Utils/ImageRepresentationsUtils.swift index 6b71965fb9..20a3890c13 100644 --- a/submodules/TelegramCore/Sources/Utils/ImageRepresentationsUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/ImageRepresentationsUtils.swift @@ -112,7 +112,7 @@ public func parseMediaData(data: Data) -> Media? { if let photo = object as? Api.Photo { return telegramMediaImageFromApiPhoto(photo) } else if let document = object as? Api.Document { - return telegramMediaFileFromApiDocument(document) + return telegramMediaFileFromApiDocument(document, altDocuments: []) } } return nil diff --git a/submodules/TelegramCore/Sources/Utils/MediaResourceNetworkStatsTag.swift b/submodules/TelegramCore/Sources/Utils/MediaResourceNetworkStatsTag.swift index 818111988f..618da9b69a 100644 --- a/submodules/TelegramCore/Sources/Utils/MediaResourceNetworkStatsTag.swift +++ b/submodules/TelegramCore/Sources/Utils/MediaResourceNetworkStatsTag.swift @@ -11,7 +11,7 @@ public enum MediaResourceStatsCategory { case voiceMessages } -final class TelegramMediaResourceFetchTag: MediaResourceFetchTag { +public final class TelegramMediaResourceFetchTag: MediaResourceFetchTag { public let statsCategory: MediaResourceStatsCategory public init(statsCategory: MediaResourceStatsCategory, userContentType: MediaResourceUserContentType?) { diff --git a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift index acc484da19..7a3e295fa4 100644 --- a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift @@ -599,7 +599,7 @@ public func _internal_parseMediaAttachment(data: Data) -> Media? { if let photo = object as? Api.Photo { return telegramMediaImageFromApiPhoto(photo) } else if let file = object as? Api.Document { - return telegramMediaFileFromApiDocument(file) + return telegramMediaFileFromApiDocument(file, altDocuments: []) } else { return nil } diff --git a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift index d32cae1ad7..67cc4355b2 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift @@ -1380,7 +1380,8 @@ public func defaultBuiltinWallpaper(data: BuiltinWallpaperData, colors: [UInt32] attributes: [ .ImageSize(size: PixelDimensions(width: 1440, height: 2960)), .FileName(fileName: "pattern.tgv") - ] + ], + alternativeRepresentations: [] ), settings: WallpaperSettings(colors: colors, intensity: intensity, rotation: rotation) )) diff --git a/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift b/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift index a5e38bb28f..8af0bd3bc0 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift @@ -122,7 +122,7 @@ struct TelegramWallpaperStandardizedCodable: Codable { } if let slug = slug { - self.value = .file(TelegramWallpaper.File(id: 0, accessHash: 0, isCreator: false, isDefault: false, isPattern: !colors.isEmpty, isDark: false, slug: slug, file: TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 0), partialReference: nil, resource: WallpaperDataResource(slug: slug), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "", size: nil, attributes: []), settings: WallpaperSettings(blur: blur, motion: motion, colors: colors.map { $0.argb }, intensity: intensity, rotation: rotation))) + self.value = .file(TelegramWallpaper.File(id: 0, accessHash: 0, isCreator: false, isDefault: false, isPattern: !colors.isEmpty, isDark: false, slug: slug, file: TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 0), partialReference: nil, resource: WallpaperDataResource(slug: slug), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "", size: nil, attributes: [], alternativeRepresentations: []), settings: WallpaperSettings(blur: blur, motion: motion, colors: colors.map { $0.argb }, intensity: intensity, rotation: rotation))) } else if colors.count > 1 { self.value = .gradient(TelegramWallpaper.Gradient(id: nil, colors: colors.map { $0.argb }, settings: WallpaperSettings(blur: blur, motion: motion, rotation: rotation))) } else { diff --git a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift index a548f9aac1..e289c562b6 100644 --- a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift +++ b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift @@ -330,7 +330,7 @@ public func mediaContentKind(_ media: EngineMedia, message: EngineMessage? = nil return .file(performer) } } - case let .Video(_, _, flags, _, _): + case let .Video(_, _, flags, _, _, _): if file.isAnimated { result = .animation } else { diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 6e8bc0a898..55613a5264 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -235,7 +235,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, } else { for attribute in file.attributes { switch attribute { - case let .Video(_, _, flags, _, _): + case let .Video(_, _, flags, _, _, _): if flags.contains(.instantRoundVideo) { type = .round } else { diff --git a/submodules/TelegramUI/Components/Chat/ChatContextResultPeekContent/Sources/ChatContextResultPeekContent.swift b/submodules/TelegramUI/Components/Chat/ChatContextResultPeekContent/Sources/ChatContextResultPeekContent.swift index 67891754b9..5e55312bd9 100644 --- a/submodules/TelegramUI/Components/Chat/ChatContextResultPeekContent/Sources/ChatContextResultPeekContent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatContextResultPeekContent/Sources/ChatContextResultPeekContent.swift @@ -172,7 +172,7 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont imageDimensions = externalReference.content?.dimensions?.cgSize if let content = externalReference.content, externalReference.type == "gif", let thumbnailResource = imageResource , let dimensions = content.dimensions { - videoFileReference = .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil)])) + 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, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])) imageResource = nil } case let .internalReference(internalReference): diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift index feaacffdb1..cebcfdff62 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift @@ -272,7 +272,7 @@ public 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: [], preloadSize: nil, coverTime: nil)])) + 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, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])) let videoContent = NativeVideoContent(id: .profileVideo(id, "action"), userLocation: .peer(item.message.id.peerId), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, storeAfterDownload: nil) if videoContent.id != strongSelf.videoContent?.id { let mediaManager = item.context.sharedContext.mediaManager diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index d43450b988..acf4bc4dbd 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -3061,7 +3061,7 @@ public struct AnimatedEmojiSoundsConfiguration { if let idString = dict["id"], let id = Int64(idString), let accessHashString = dict["access_hash"], let accessHash = Int64(accessHashString), let fileReference = Data(base64Encoded: fileReferenceString) { let resource = CloudDocumentMediaResource(datacenterId: 1, fileId: id, accessHash: accessHash, size: nil, fileReference: fileReference, fileName: nil) - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: nil, attributes: []) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: nil, attributes: [], alternativeRepresentations: []) sounds[key] = file } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift index 6ae1859216..ada3bff052 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift @@ -650,7 +650,7 @@ public 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 @@ -1558,7 +1558,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { var isVoice = false var audioDuration: Int32? for attribute in file.attributes { - if case let .Video(duration, _, flags, _, _) = attribute, flags.contains(.instantRoundVideo) { + if case let .Video(duration, _, flags, _, _, _) = attribute, flags.contains(.instantRoundVideo) { isAudio = true isVoice = true audioDuration = Int32(duration) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index 88b33b6c8d..e1586b4821 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -414,8 +414,8 @@ private class ExtendedMediaOverlayNode: ASDisplayNode { } private func selectStoryMedia(item: Stories.Item, preferredHighQuality: Bool) -> Media? { - if !preferredHighQuality, let alternativeMedia = item.alternativeMedia { - return alternativeMedia + if !preferredHighQuality, let alternativeMediaValue = item.alternativeMediaList.first { + return alternativeMediaValue } else { return item.media } @@ -430,7 +430,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr private var highQualityImageNode: TransformImageNode? private var videoNode: UniversalVideoNode? - private var videoContent: NativeVideoContent? + private var videoContent: UniversalVideoContent? private var animatedStickerNode: AnimatedStickerNode? private var statusNode: RadialStatusNode? public var videoNodeDecoration: ChatBubbleVideoDecoration? @@ -678,10 +678,10 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } else if let media = media as? TelegramMediaImage, let resource = largestImageRepresentation(media.representations)?.resource { messageMediaImageCancelInteractiveFetch(context: context, messageId: message.id, image: media, resource: resource) } - if let alternativeMedia = item.alternativeMedia { - if let media = alternativeMedia as? TelegramMediaFile { + if let alternativeMediaValue = item.alternativeMediaList.first { + if let media = alternativeMediaValue as? TelegramMediaFile { messageMediaFileCancelInteractiveFetch(context: context, messageId: message.id, file: media) - } else if let media = alternativeMedia as? TelegramMediaImage, let resource = largestImageRepresentation(media.representations)?.resource { + } else if let media = alternativeMediaValue as? TelegramMediaImage, let resource = largestImageRepresentation(media.representations)?.resource { messageMediaImageCancelInteractiveFetch(context: context, messageId: message.id, image: media, resource: resource) } } @@ -715,7 +715,20 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } else if let fetchStatus = self.fetchStatus, case .Local = fetchStatus { var videoContentMatch = true - if let content = self.videoContent, case let .message(stableId, mediaId) = content.nativeId { + if let content = self.videoContent as? NativeVideoContent, case let .message(stableId, mediaId) = content.nativeId { + var media = self.media + if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia { + media = fullMedia + } + + if let storyMedia = media as? TelegramMediaStory, let storyItem = self.message?.associatedStories[storyMedia.storyId]?.get(Stories.StoredItem.self) { + if case let .item(item) = storyItem, let _ = item.media { + media = selectStoryMedia(item: item, preferredHighQuality: self.preferredStoryHighQuality) + } + } + + videoContentMatch = self.message?.stableId == stableId && media?.id == mediaId + } else if let content = self.videoContent as? PlatformVideoContent, case let .message(_, stableId, mediaId) = content.nativeId { var media = self.media if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia { media = fullMedia @@ -1644,12 +1657,18 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr let streamVideo = isMediaStreamable(message: message, media: updatedVideoFile) let loopVideo = updatedVideoFile.isAnimated - let videoContent = NativeVideoContent(id: .message(message.stableId, updatedVideoFile.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), streamVideo: streamVideo ? .conservative : .none, loopVideo: loopVideo, enableSound: false, fetchAutomatically: false, onlyFullSizeThumbnail: (onlyFullSizeVideoThumbnail ?? false), continuePlayingWithoutSoundOnLostAudioSession: isInlinePlayableVideo, placeholderColor: emptyColor, captureProtected: message.isCopyProtected() || isExtendedMedia, storeAfterDownload: { [weak context] in - guard let context, let peerId else { - return - } - let _ = storeDownloadedMedia(storeManager: context.downloadedMediaStoreManager, media: .message(message: MessageReference(message), media: updatedVideoFile), peerId: peerId).startStandalone() - }) + + let videoContent: UniversalVideoContent + if NativeVideoContent.isHLSVideo(file: updatedVideoFile), context.sharedContext.immediateExperimentalUISettings.dynamicStreaming { + videoContent = HLSVideoContent(id: .message(message.id, message.stableId, updatedVideoFile.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), streamVideo: true, loopVideo: loopVideo) + } else { + videoContent = NativeVideoContent(id: .message(message.stableId, updatedVideoFile.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), streamVideo: streamVideo ? .conservative : .none, loopVideo: loopVideo, enableSound: false, fetchAutomatically: false, onlyFullSizeThumbnail: (onlyFullSizeVideoThumbnail ?? false), continuePlayingWithoutSoundOnLostAudioSession: isInlinePlayableVideo, placeholderColor: emptyColor, captureProtected: message.isCopyProtected() || isExtendedMedia, storeAfterDownload: { [weak context] in + guard let context, let peerId else { + return + } + let _ = storeDownloadedMedia(storeManager: context.downloadedMediaStoreManager, media: .message(message: MessageReference(message), media: updatedVideoFile), peerId: peerId).startStandalone() + }) + } let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded) videoNode.isUserInteractionEnabled = false videoNode.ownsContentNodeUpdated = { [weak self] owns in diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift index 465993c444..a90de7819d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift @@ -27,7 +27,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 } @@ -437,7 +437,7 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible viewClassName = ChatMessageStickerItemNode.self } break loop - case let .Video(_, _, flags, _, _): + case let .Video(_, _, flags, _, _, _): if flags.contains(.instantRoundVideo) { viewClassName = ChatMessageBubbleItemNode.self break loop diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift index 9424d985ab..e0b982fdf0 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift @@ -203,7 +203,7 @@ public final class ChatMessageAccessibilityData { text = item.presentationData.strings.VoiceOver_Chat_MusicTitle(title, performer).string text.append(item.presentationData.strings.VoiceOver_Chat_Duration(durationString).string) } - case let .Video(duration, _, flags, _, _): + case let .Video(duration, _, flags, _, _, _): isSpecialFile = true if isSelected == nil { hint = item.presentationData.strings.VoiceOver_Chat_PlayHint diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageProfilePhotoSuggestionContentNode/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageProfilePhotoSuggestionContentNode/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift index 7f8236c6ee..47c902badc 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageProfilePhotoSuggestionContentNode/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageProfilePhotoSuggestionContentNode/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift @@ -218,7 +218,7 @@ public class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleCont } 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: [], preloadSize: nil, coverTime: nil)])) + 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, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])) let videoContent = NativeVideoContent(id: .profileVideo(id, "action"), userLocation: .peer(item.message.id.peerId), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, storeAfterDownload: nil) if videoContent.id != strongSelf.videoContent?.id { let mediaManager = item.context.sharedContext.mediaManager diff --git a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift index 714ce82b87..bb3a2dfabf 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift @@ -257,7 +257,7 @@ public final class ChatSendAudioMessageContextPreview: UIView, ChatSendMessageCo public func update(containerSize: CGSize, transition: ComponentTransition) -> CGSize { let voiceAttributes: [TelegramMediaFileAttribute] = [.Audio(isVoice: true, duration: 23, title: nil, performer: nil, waveform: self.waveform.makeBitstream())] - let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes) + let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes, alternativeRepresentations: []) let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: self.context.account.peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [voiceMedia], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) diff --git a/submodules/TelegramUI/Components/EntityKeyboardGifContent/Sources/GifContext.swift b/submodules/TelegramUI/Components/EntityKeyboardGifContent/Sources/GifContext.swift index e133ecbacb..5178c9659e 100644 --- a/submodules/TelegramUI/Components/EntityKeyboardGifContent/Sources/GifContext.swift +++ b/submodules/TelegramUI/Components/EntityKeyboardGifContent/Sources/GifContext.swift @@ -126,7 +126,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: [], preloadSize: nil, coverTime: nil)]) + 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, coverTime: nil, videoCodec: nil)], alternativeRepresentations: []) references.append(MultiplexedVideoNodeFile(file: FileMediaReference.standalone(media: file), contextResult: (collection, result))) } case let .internalReference(internalReference): diff --git a/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerPackSetupController.swift b/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerPackSetupController.swift index 3f12c31673..3fa698bbbd 100644 --- a/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerPackSetupController.swift +++ b/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerPackSetupController.swift @@ -302,7 +302,7 @@ private func groupStickerPackSetupControllerEntries(context: AccountContext, pre let thumbnail: StickerPackItem? if let thumbnailRep = info.thumbnail { - thumbnail = StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: 0), file: TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: thumbnailRep.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: info.immediateThumbnailData, mimeType: "", size: nil, attributes: []), indexKeys: []) + thumbnail = StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: 0), file: TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: thumbnailRep.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: info.immediateThumbnailData, mimeType: "", size: nil, attributes: [], alternativeRepresentations: []), indexKeys: []) } else { thumbnail = entry.firstItem as? StickerPackItem } diff --git a/submodules/TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift b/submodules/TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift index 0dc69ee662..b6b6b4ffdf 100644 --- a/submodules/TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift +++ b/submodules/TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift @@ -231,7 +231,7 @@ public func legacyInstantVideoController(theme: PresentationTheme, forStory: Boo } } - 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: finalDuration, size: PixelDimensions(finalDimensions), flags: [.instantRoundVideo], preloadSize: nil, coverTime: nil)]) + 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: finalDuration, size: PixelDimensions(finalDimensions), flags: [.instantRoundVideo], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: []) var message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let scheduleTime: Int32? = scheduleTimestamp > 0 ? scheduleTimestamp : nil diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift index 9b259b362c..aee5cbbcb9 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift @@ -87,7 +87,7 @@ public extension MediaEditorScreen { if cover, case let .file(file) = storyItem.media { videoPlaybackPosition = 0.0 for attribute in file.attributes { - if case let .Video(_, _, _, _, coverTime) = attribute { + if case let .Video(_, _, _, _, coverTime, _) = attribute { videoPlaybackPosition = coverTime break } @@ -258,8 +258,8 @@ public extension MediaEditorScreen { if case let .file(file) = storyItem.media { var updatedAttributes: [TelegramMediaFileAttribute] = [] for attribute in file.attributes { - if case let .Video(duration, size, flags, preloadSize, _) = attribute { - updatedAttributes.append(.Video(duration: duration, size: size, flags: flags, preloadSize: preloadSize, coverTime: min(duration, updatedCoverTimestamp))) + if case let .Video(duration, size, flags, preloadSize, _, videoCodec) = attribute { + updatedAttributes.append(.Video(duration: duration, size: size, flags: flags, preloadSize: preloadSize, coverTime: min(duration, updatedCoverTimestamp), videoCodec: videoCodec)) } else { updatedAttributes.append(attribute) } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 0adc9b1edd..bc9ec74e11 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -8305,7 +8305,7 @@ private func stickerFile(resource: TelegramMediaResource, thumbnailResource: Tel fileAttributes.append(.FileName(fileName: isVideo ? "sticker.webm" : "sticker.webp")) fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil)) if isVideo { - fileAttributes.append(.Video(duration: duration ?? 3.0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil)) + fileAttributes.append(.Video(duration: duration ?? 3.0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)) } else { fileAttributes.append(.ImageSize(size: dimensions)) } @@ -8314,7 +8314,7 @@ private func stickerFile(resource: TelegramMediaResource, thumbnailResource: Tel previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil)) } - return 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: isVideo ? "video/webm" : "image/webp", size: size, attributes: fileAttributes) + return 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: isVideo ? "video/webm" : "image/webp", size: size, attributes: fileAttributes, alternativeRepresentations: []) } private struct MediaEditorConfiguration { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerPackListContextItem.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerPackListContextItem.swift index 67da5b8fe1..d14432ef69 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerPackListContextItem.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerPackListContextItem.swift @@ -60,7 +60,7 @@ private final class StickerPackListContextItemNode: ASDisplayNode, ContextMenuCu if let resource = thumbnailResource as? CloudDocumentMediaResource { resourceId = resource.fileId } - let thumbnailFile = topItem?.file ?? TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: resourceId), partialReference: nil, resource: thumbnailResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: thumbnailResource.size ?? 0, attributes: []) + let thumbnailFile = topItem?.file ?? TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: resourceId), partialReference: nil, resource: thumbnailResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: thumbnailResource.size ?? 0, attributes: [], alternativeRepresentations: []) let _ = freeMediaFileInteractiveFetched(account: item.context.account, userLocation: .other, fileReference: .stickerPack(stickerPack: .id(id: pack.id.id, accessHash: pack.accessHash), media: thumbnailFile)).start() thumbnailIconSource = ContextMenuActionItemIconSource( diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift index 8922ed2a8d..827ec82b64 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift @@ -327,7 +327,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: [], preloadSize: nil, coverTime: nil)])) + 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, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])) let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: peer.isCopyProtectionEnabled, storeAfterDownload: nil) if videoContent.id != self.videoContent?.id { self.videoNode?.removeFromSupernode() diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift index a2e196d1e4..ea1c47a629 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift @@ -162,7 +162,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: [], preloadSize: nil, coverTime: nil)])) + 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, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])) let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: peer.isCopyProtectionEnabled, storeAfterDownload: nil) if videoContent.id != self.videoContent?.id { self.videoNode?.removeFromSupernode() diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift index f43cb26bca..2f4b5209c0 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift @@ -709,7 +709,8 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme immediateThumbnailData: nil, mimeType: "image/jpeg", size: nil, - attributes: [.FileName(fileName: "file")] + attributes: [.FileName(fileName: "file")], + alternativeRepresentations: [] ) let fakeMessage = Message( stableId: 1, @@ -2126,7 +2127,8 @@ public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, immediateThumbnailData: nil, mimeType: "image/jpeg", size: nil, - attributes: [.FileName(fileName: "file")] + attributes: [.FileName(fileName: "file")], + alternativeRepresentations: [] ) let fakeMessage = Message( stableId: 1, diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift index 2ca28c9923..8554d99355 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift @@ -1310,7 +1310,7 @@ final class ChannelAppearanceScreenComponent: Component { var emojiPackFile: TelegramMediaFile? if let thumbnail = emojiPack?.thumbnail { - emojiPackFile = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: thumbnail.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: thumbnail.immediateThumbnailData, mimeType: "", size: nil, attributes: []) + emojiPackFile = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: thumbnail.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: thumbnail.immediateThumbnailData, mimeType: "", size: nil, attributes: [], alternativeRepresentations: []) } let emojiPackSectionSize = self.emojiPackSection.update( @@ -1438,7 +1438,7 @@ final class ChannelAppearanceScreenComponent: Component { var stickerPackFile: TelegramMediaFile? if let peerStickerPack = contentsData.peerStickerPack, let thumbnail = peerStickerPack.thumbnail { - stickerPackFile = TelegramMediaFile(fileId: MediaId(namespace: 0, id: peerStickerPack.id.id), partialReference: nil, resource: thumbnail.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: thumbnail.immediateThumbnailData, mimeType: "", size: nil, attributes: []) + stickerPackFile = TelegramMediaFile(fileId: MediaId(namespace: 0, id: peerStickerPack.id.id), partialReference: nil, resource: thumbnail.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: thumbnail.immediateThumbnailData, mimeType: "", size: nil, attributes: [], alternativeRepresentations: []) } let stickerPackSectionSize = self.stickerPackSection.update( diff --git a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift index 819d491cbd..6eeecc59bd 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift @@ -1072,7 +1072,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate let waveformBase64 = "DAAOAAkACQAGAAwADwAMABAADQAPABsAGAALAA0AGAAfABoAHgATABgAGQAYABQADAAVABEAHwANAA0ACQAWABkACQAOAAwACQAfAAAAGQAVAAAAEwATAAAACAAfAAAAHAAAABwAHwAAABcAGQAAABQADgAAABQAHwAAAB8AHwAAAAwADwAAAB8AEwAAABoAFwAAAB8AFAAAAAAAHwAAAAAAHgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAAAA=" let voiceAttributes: [TelegramMediaFileAttribute] = [.Audio(isVoice: true, duration: 23, title: nil, performer: nil, waveform: Data(base64Encoded: waveformBase64)!)] - let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes) + let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes, alternativeRepresentations: []) let message6 = Message(stableId: 6, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 6), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66005, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) sampleMessages.append(message6) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift index f5857b2e40..212e669dbf 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift @@ -281,7 +281,7 @@ public final class StoryContentContextImpl: StoryContentContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -329,7 +329,7 @@ public final class StoryContentContextImpl: StoryContentContext { timestamp: item.timestamp, expirationTimestamp: Int32.max, media: EngineMedia(item.media), - alternativeMedia: nil, + alternativeMediaList: [], mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -998,8 +998,8 @@ public final class StoryContentContextImpl: StoryContentContext { } var selectedMedia: EngineMedia - if let slice = stateValue.slice, let alternativeMedia = item.alternativeMedia, (!slice.additionalPeerData.preferHighQualityStories && !item.isMy) { - selectedMedia = alternativeMedia + if let slice = stateValue.slice, let alternativeMediaValue = item.alternativeMediaList.first, (!slice.additionalPeerData.preferHighQualityStories && !item.isMy) { + selectedMedia = alternativeMediaValue } else { selectedMedia = item.media } @@ -1315,7 +1315,7 @@ public final class SingleStoryContentContextImpl: StoryContentContext { timestamp: itemValue.timestamp, expirationTimestamp: itemValue.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: itemValue.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: itemValue.alternativeMediaList.map(EngineMedia.init), mediaAreas: itemValue.mediaAreas, text: itemValue.text, entities: itemValue.entities, @@ -1692,8 +1692,8 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { } var selectedMedia: EngineMedia - if let alternativeMedia = item.storyItem.alternativeMedia, (!preferHighQualityStories && !item.storyItem.isMy) { - selectedMedia = alternativeMedia + if let alternativeMediaValue = item.storyItem.alternativeMediaList.first, (!preferHighQualityStories && !item.storyItem.isMy) { + selectedMedia = alternativeMediaValue } else { selectedMedia = item.storyItem.media } @@ -1820,7 +1820,7 @@ public func preloadStoryMedia(context: AccountContext, info: StoryPreloadInfo) - case let .file(file): var fetchRange: (Range, MediaBoxFetchPriority)? for attribute in file.attributes { - if case let .Video(_, _, _, preloadSize, _) = attribute { + if case let .Video(_, _, _, preloadSize, _, _) = attribute { if let preloadSize { fetchRange = (0 ..< Int64(preloadSize), .default) } @@ -2004,8 +2004,8 @@ public func waitUntilStoryMediaPreloaded(context: AccountContext, peerId: Engine var fetchPriorityDisposable: Disposable? let selectedMedia: EngineMedia - if !preferHighQualityStories, let alternativeMedia = storyItem.alternativeMedia { - selectedMedia = alternativeMedia + if !preferHighQualityStories, let alternativeMediaValue = storyItem.alternativeMediaList.first { + selectedMedia = alternativeMediaValue } else { selectedMedia = storyItem.media } @@ -2047,7 +2047,7 @@ public func waitUntilStoryMediaPreloaded(context: AccountContext, peerId: Engine case let .file(file): var fetchRange: (Range, MediaBoxFetchPriority)? for attribute in file.attributes { - if case let .Video(_, _, _, preloadSize, _) = attribute { + if case let .Video(_, _, _, preloadSize, _, _) = attribute { if let preloadSize { fetchRange = (0 ..< Int64(preloadSize), .default) } @@ -2247,7 +2247,7 @@ private func getCachedStory(storyId: StoryId, transaction: Transaction) -> Engin timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), - alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -2940,8 +2940,8 @@ public final class RepostStoriesContentContextImpl: StoryContentContext { } var selectedMedia: EngineMedia - if let slice = stateValue.slice, let alternativeMedia = item.alternativeMedia, (!slice.additionalPeerData.preferHighQualityStories && !item.isMy) { - selectedMedia = alternativeMedia + if let slice = stateValue.slice, let alternativeMediaValue = item.alternativeMediaList.first, (!slice.additionalPeerData.preferHighQualityStories && !item.isMy) { + selectedMedia = alternativeMediaValue } else { selectedMedia = item.media } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 178920de70..4d26f2e715 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -1136,7 +1136,7 @@ private final class StoryContainerScreenComponent: Component { var isSilentVideo = false if case let .file(file) = slice.item.storyItem.media { for attribute in file.attributes { - if case let .Video(_, _, flags, _, _) = attribute { + if case let .Video(_, _, flags, _, _, _) = attribute { if flags.contains(.isSilent) { isSilentVideo = true } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index fd2bef3098..a5b630e67b 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -598,10 +598,10 @@ final class StoryItemContentComponent: Component { let selectedMedia: EngineMedia var messageMedia: EngineMedia? - if !component.preferHighQuality, !component.item.isMy, let alternativeMedia = component.item.alternativeMedia { - selectedMedia = alternativeMedia + if !component.preferHighQuality, !component.item.isMy, let alternativeMediaValue = component.item.alternativeMediaList.first { + selectedMedia = alternativeMediaValue - switch alternativeMedia { + switch alternativeMediaValue { case let .image(image): messageMedia = .image(image) case let .file(file): diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 6d68c120bb..b194377d78 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -3820,7 +3820,7 @@ public final class StoryItemSetContainerComponent: Component { isVideo = true soundAlpha = 1.0 for attribute in file.attributes { - if case let .Video(_, _, flags, _, _) = attribute { + if case let .Video(_, _, flags, _, _, _) = attribute { if flags.contains(.isSilent) { isSilentVideo = true soundAlpha = 0.5 @@ -3853,7 +3853,7 @@ public final class StoryItemSetContainerComponent: Component { var isSilentVideo = false if case let .file(file) = component.slice.item.storyItem.media { for attribute in file.attributes { - if case let .Video(_, _, flags, _, _) = attribute { + if case let .Video(_, _, flags, _, _, _) = attribute { if flags.contains(.isSilent) { isSilentVideo = true } @@ -5392,7 +5392,7 @@ public final class StoryItemSetContainerComponent: Component { if cover { if case let .file(file) = component.slice.item.storyItem.media { for attribute in file.attributes { - if case let .Video(_, _, _, _, coverTime) = attribute { + if case let .Video(_, _, _, _, coverTime, _) = attribute { videoPlaybackPosition = coverTime } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index bb57628710..58e24479c6 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -580,7 +580,7 @@ final class StoryItemSetContainerSendMessage { let waveformBuffer = audio.waveform.makeBitstream() - let messages: [EnqueueMessage] = [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: audio.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(audio.fileSize), attributes: [.Audio(isVoice: true, duration: Int(audio.duration), title: nil, performer: nil, waveform: waveformBuffer)])), threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] + let messages: [EnqueueMessage] = [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: audio.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(audio.fileSize), attributes: [.Audio(isVoice: true, duration: Int(audio.duration), title: nil, performer: nil, waveform: waveformBuffer)], alternativeRepresentations: [])), threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] let _ = enqueueMessages(account: component.context.account, peerId: peerId, messages: messages).start() @@ -791,7 +791,7 @@ final class StoryItemSetContainerSendMessage { fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil)) fileAttributes.append(.ImageSize(size: PixelDimensions(size))) - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: Int64(data.count), attributes: fileAttributes) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: Int64(data.count), attributes: fileAttributes, alternativeRepresentations: []) let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) self.sendMessages(view: view, peer: peer, messages: [message], silentPosting: false) @@ -896,7 +896,7 @@ final class StoryItemSetContainerSendMessage { guard let self, let view else { return } - self.sendMessages(view: view, peer: peer, messages: [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.compressedData.count), attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)])), threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + self.sendMessages(view: view, peer: peer, messages: [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.compressedData.count), attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)], alternativeRepresentations: [])), threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) HapticFeedback().tap() }) @@ -2176,7 +2176,7 @@ final class StoryItemSetContainerSendMessage { attributes.append(.Audio(isVoice: false, duration: audioMetadata.duration, title: audioMetadata.title, performer: audioMetadata.performer, waveform: nil)) } - let file = TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData, thumbnail: false), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(item.fileSize), attributes: attributes) + let file = TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData, thumbnail: false), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(item.fileSize), attributes: attributes, alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: replyMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: replyToStoryId, localGroupingKey: groupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: []) messages.append(message) } diff --git a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift index 402b226194..d8d2908bae 100644 --- a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift +++ b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift @@ -1221,7 +1221,7 @@ public class VideoMessageCameraScreen: ViewController { let id = Int64.random(in: Int64.min ... Int64.max) let fileResource = LocalFileReferenceMediaResource(localFilePath: path, randomId: id) - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: Int64(data.count), attributes: [.FileName(fileName: "video.mp4")]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: Int64(data.count), attributes: [.FileName(fileName: "video.mp4")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: self.context.engine.account, peerId: self.context.engine.account.peerId, messages: [message]).start() @@ -1877,7 +1877,7 @@ public class VideoMessageCameraScreen: ViewController { context.account.postbox.mediaBox.storeCachedResourceRepresentation(resource, representation: CachedVideoFirstFrameRepresentation(), data: data) } - 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: finalDuration, size: video.dimensions, flags: [.instantRoundVideo], preloadSize: nil, coverTime: nil)]) + 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: finalDuration, size: video.dimensions, flags: [.instantRoundVideo], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: []) var attributes: [MessageAttribute] = [] if self.cameraState.isViewOnceEnabled { diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift index 143746e193..ac4358c04a 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift @@ -346,7 +346,7 @@ extension ChatControllerImpl { attributes.append(AutoremoveTimeoutMessageAttribute(timeout: viewOnceTimeout, countdownBeginTime: nil)) } - strongSelf.sendMessages([.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.compressedData.count), attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)])), threadId: strongSelf.chatLocation.threadId, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])]) + strongSelf.sendMessages([.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.compressedData.count), attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)], alternativeRepresentations: [])), threadId: strongSelf.chatLocation.threadId, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])]) strongSelf.recorderFeedback?.tap() strongSelf.recorderFeedback = nil @@ -521,7 +521,7 @@ extension ChatControllerImpl { attributes.append(EffectMessageAttribute(id: messageEffect.id)) } - let messages: [EnqueueMessage] = [.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: audio.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(audio.fileSize), attributes: [.Audio(isVoice: true, duration: Int(audio.duration), title: nil, performer: nil, waveform: waveformBuffer)])), threadId: self.chatLocation.threadId, replyToMessageId: self.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] + let messages: [EnqueueMessage] = [.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: audio.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(audio.fileSize), attributes: [.Audio(isVoice: true, duration: Int(audio.duration), title: nil, performer: nil, waveform: waveformBuffer)], alternativeRepresentations: [])), threadId: self.chatLocation.threadId, replyToMessageId: self.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] let transformedMessages: [EnqueueMessage] if let silentPosting = silentPosting { diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift index 8eacbdcc2a..602ed24931 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift @@ -88,7 +88,7 @@ extension ChatControllerImpl { fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil)) fileAttributes.append(.ImageSize(size: PixelDimensions(size))) - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: Int64(data.count), attributes: fileAttributes) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: Int64(data.count), attributes: fileAttributes, alternativeRepresentations: []) let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject @@ -211,7 +211,7 @@ extension ChatControllerImpl { var fileAttributes: [TelegramMediaFileAttribute] = [] fileAttributes.append(.FileName(fileName: "sticker.webm")) fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil)) - fileAttributes.append(.Video(duration: animatedImage.duration, size: PixelDimensions(width: 512, height: 512), flags: [], preloadSize: nil, coverTime: nil)) + fileAttributes.append(.Video(duration: animatedImage.duration, size: PixelDimensions(width: 512, height: 512), flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)) let previewRepresentations: [TelegramMediaImageRepresentation] = [] // if let thumbnailResource { @@ -220,7 +220,7 @@ extension ChatControllerImpl { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) self.context.account.postbox.mediaBox.copyResourceData(resource.id, fromTempPath: path) - let file = 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/webm", size: 0, attributes: fileAttributes) + let file = 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/webm", size: 0, attributes: fileAttributes, alternativeRepresentations: []) self.enqueueStickerFile(file) default: break diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index f8d76a3a32..e3ba2c90f7 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -1109,7 +1109,7 @@ extension ChatControllerImpl { attributes.append(.Audio(isVoice: false, duration: audioMetadata.duration, title: audioMetadata.title, performer: audioMetadata.performer, waveform: nil)) } - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData, thumbnail: false), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(item.fileSize), attributes: attributes) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData, thumbnail: false), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(item.fileSize), attributes: attributes, alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: groupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: []) messages.append(message) } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 0dd3f9bda6..159114aeb2 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -397,7 +397,7 @@ func messageMediaEditingOptions(message: Message) -> MessageMediaEditingOptions return [] case .Animated: break - case let .Video(_, _, flags, _, _): + case let .Video(_, _, flags, _, _, _): if flags.contains(.instantRoundVideo) { return [] } else { @@ -983,7 +983,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState strongController.dismiss() let id = Int64.random(in: Int64.min ... Int64.max) - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: logPath, randomId: id), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: nil, attributes: [.FileName(fileName: "CallStats.log")]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: logPath, randomId: id), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: nil, attributes: [.FileName(fileName: "CallStats.log")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).startStandalone() diff --git a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift index d41b9923b6..94770716c2 100644 --- a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift +++ b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift @@ -260,7 +260,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode } imageDimensions = externalReference.content?.dimensions?.cgSize if externalReference.type == "gif", let thumbnailResource = externalReference.thumbnail?.resource, let content = externalReference.content, let dimensions = content.dimensions { - videoFile = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: thumbnailResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil)]) + 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, coverTime: nil, videoCodec: nil)], alternativeRepresentations: []) imageResource = nil } diff --git a/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift b/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift index 9b28b54b49..9eb5ba1858 100644 --- a/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift +++ b/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift @@ -72,7 +72,7 @@ final class MessageMediaPlaylistItem: SharedMediaPlaylistItem { } else { return SharedMediaPlaybackData(type: .music, source: source) } - case let .Video(_, _, flags, _, _): + case let .Video(_, _, flags, _, _, _): if flags.contains(.instantRoundVideo) { return SharedMediaPlaybackData(type: .instantVideo, source: source) } else { @@ -129,7 +129,7 @@ final class MessageMediaPlaylistItem: SharedMediaPlaylistItem { displayData = SharedMediaPlaybackDisplayData.music(title: updatedTitle, performer: updatedPerformer, albumArt: albumArt, long: CGFloat(duration) > 10.0 * 60.0, caption: caption) } return displayData - case let .Video(_, _, flags, _, _): + case let .Video(_, _, flags, _, _, _): if flags.contains(.instantRoundVideo) { return SharedMediaPlaybackDisplayData.instantVideo(author: self.message.effectiveAuthor.flatMap(EnginePeer.init), peer: self.message.peers[self.message.id.peerId].flatMap(EnginePeer.init), timestamp: self.message.timestamp) } else { diff --git a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift index cefaf5e573..011ccc1132 100644 --- a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift +++ b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift @@ -59,6 +59,7 @@ public struct ExperimentalUISettings: Codable, Equatable { public var allowWebViewInspection: Bool public var disableReloginTokens: Bool public var liveStreamV2: Bool + public var dynamicStreaming: Bool public static var defaultSettings: ExperimentalUISettings { return ExperimentalUISettings( @@ -95,7 +96,8 @@ public struct ExperimentalUISettings: Codable, Equatable { experimentalCallMute: false, allowWebViewInspection: false, disableReloginTokens: false, - liveStreamV2: false + liveStreamV2: false, + dynamicStreaming: false ) } @@ -133,7 +135,8 @@ public struct ExperimentalUISettings: Codable, Equatable { experimentalCallMute: Bool, allowWebViewInspection: Bool, disableReloginTokens: Bool, - liveStreamV2: Bool + liveStreamV2: Bool, + dynamicStreaming: Bool ) { self.keepChatNavigationStack = keepChatNavigationStack self.skipReadHistory = skipReadHistory @@ -169,6 +172,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.allowWebViewInspection = allowWebViewInspection self.disableReloginTokens = disableReloginTokens self.liveStreamV2 = liveStreamV2 + self.dynamicStreaming = dynamicStreaming } public init(from decoder: Decoder) throws { @@ -208,6 +212,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.allowWebViewInspection = try container.decodeIfPresent(Bool.self, forKey: "allowWebViewInspection") ?? false self.disableReloginTokens = try container.decodeIfPresent(Bool.self, forKey: "disableReloginTokens") ?? false self.liveStreamV2 = try container.decodeIfPresent(Bool.self, forKey: "liveStreamV2") ?? false + self.dynamicStreaming = try container.decodeIfPresent(Bool.self, forKey: "dynamicStreaming") ?? false } public func encode(to encoder: Encoder) throws { @@ -247,6 +252,7 @@ public struct ExperimentalUISettings: Codable, Equatable { try container.encode(self.allowWebViewInspection, forKey: "allowWebViewInspection") try container.encode(self.disableReloginTokens, forKey: "disableReloginTokens") try container.encode(self.liveStreamV2, forKey: "liveStreamV2") + try container.encode(self.dynamicStreaming, forKey: "dynamicStreaming") } } diff --git a/submodules/TelegramUniversalVideoContent/BUILD b/submodules/TelegramUniversalVideoContent/BUILD index 78bb6c366e..9baefbc0b0 100644 --- a/submodules/TelegramUniversalVideoContent/BUILD +++ b/submodules/TelegramUniversalVideoContent/BUILD @@ -23,6 +23,7 @@ swift_library( "//submodules/RadialStatusNode:RadialStatusNode", "//submodules/AppBundle:AppBundle", "//submodules/Utils/RangeSet:RangeSet", + "//submodules/TelegramVoip", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUniversalVideoContent/Sources/HLSVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoContent.swift new file mode 100644 index 0000000000..f04db2abf9 --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoContent.swift @@ -0,0 +1,725 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import AVFoundation +import UniversalMediaPlayer +import TelegramAudio +import AccountContext +import PhotoResources +import RangeSet +import TelegramVoip + +public final class HLSVideoContent: UniversalVideoContent { + public let id: AnyHashable + public let nativeId: PlatformVideoContentId + let userLocation: MediaResourceUserLocation + public let fileReference: FileMediaReference + public let dimensions: CGSize + public let duration: Double + let streamVideo: Bool + let loopVideo: Bool + let enableSound: Bool + let baseRate: Double + let fetchAutomatically: Bool + + public init(id: PlatformVideoContentId, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, streamVideo: Bool = false, loopVideo: Bool = false, enableSound: Bool = true, baseRate: Double = 1.0, fetchAutomatically: Bool = true) { + self.id = id + self.userLocation = userLocation + self.nativeId = id + self.fileReference = fileReference + self.dimensions = self.fileReference.media.dimensions?.cgSize ?? CGSize(width: 480, height: 320) + self.duration = self.fileReference.media.duration ?? 0.0 + self.streamVideo = streamVideo + self.loopVideo = loopVideo + self.enableSound = enableSound + self.baseRate = baseRate + self.fetchAutomatically = fetchAutomatically + } + + public func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { + return HLSVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, fileReference: self.fileReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically) + } + + public func isEqual(to other: UniversalVideoContent) -> Bool { + if let other = other as? HLSVideoContent { + if case let .message(_, stableId, _) = self.nativeId { + if case .message(_, stableId, _) = other.nativeId { + if self.fileReference.media.isInstantVideo { + return true + } + } + } + } + return false + } +} + +private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNode { + private final class HLSServerSource: SharedHLSServer.Source { + let id: UUID + let postbox: Postbox + let userLocation: MediaResourceUserLocation + let playlistFiles: [Int: FileMediaReference] + let qualityFiles: [Int: FileMediaReference] + + private var playlistFetchDisposables: [Int: Disposable] = [:] + + init(id: UUID, postbox: Postbox, userLocation: MediaResourceUserLocation, playlistFiles: [Int: FileMediaReference], qualityFiles: [Int: FileMediaReference]) { + self.id = id + self.postbox = postbox + self.userLocation = userLocation + self.playlistFiles = playlistFiles + self.qualityFiles = qualityFiles + } + + deinit { + for (_, disposable) in self.playlistFetchDisposables { + disposable.dispose() + } + } + + func masterPlaylistData() -> Signal { + var playlistString: String = "" + playlistString.append("#EXTM3U\n") + + for (quality, file) in self.qualityFiles.sorted(by: { $0.key > $1.key }) { + let width = file.media.dimensions?.width ?? 1280 + let height = file.media.dimensions?.height ?? 720 + + let bandwidth: Int + if let size = file.media.size, let duration = file.media.duration, duration != 0.0 { + bandwidth = Int(Double(size) / duration) * 8 + } else { + bandwidth = 1000000 + } + + playlistString.append("#EXT-X-STREAM-INF:BANDWIDTH=\(bandwidth),RESOLUTION=\(width)x\(height)\n") + playlistString.append("hls_level_\(quality).m3u8\n") + } + return .single(playlistString) + } + + func playlistData(quality: Int) -> Signal { + guard let playlistFile = self.playlistFiles[quality] else { + return .never() + } + if self.playlistFetchDisposables[quality] == nil { + self.playlistFetchDisposables[quality] = freeMediaFileResourceInteractiveFetched(postbox: self.postbox, userLocation: self.userLocation, fileReference: playlistFile, resource: playlistFile.media.resource).startStrict() + } + + return self.postbox.mediaBox.resourceData(playlistFile.media.resource) + |> filter { data in + return data.complete + } + |> map { data -> String in + guard data.complete else { + return "" + } + guard let data = try? Data(contentsOf: URL(fileURLWithPath: data.path)) else { + return "" + } + guard var playlistString = String(data: data, encoding: .utf8) else { + return "" + } + let partRegex = try! NSRegularExpression(pattern: "mtproto:([\\d]+)", options: []) + let results = partRegex.matches(in: playlistString, range: NSRange(playlistString.startIndex..., in: playlistString)) + for result in results.reversed() { + if let range = Range(result.range, in: playlistString) { + if let fileIdRange = Range(result.range(at: 1), in: playlistString) { + let fileId = String(playlistString[fileIdRange]) + playlistString.replaceSubrange(range, with: "partfile\(fileId).mp4") + } + } + } + return playlistString + } + } + + func partData(index: Int, quality: Int) -> Signal { + return .never() + } + + func fileData(id: Int64, range: Range) -> Signal<(Data, Int)?, NoError> { + guard let file = self.qualityFiles.values.first(where: { $0.media.fileId.id == id }) else { + return .single(nil) + } + guard let size = file.media.size else { + return .single(nil) + } + + let postbox = self.postbox + let userLocation = self.userLocation + + let mappedRange: Range = Int64(range.lowerBound) ..< Int64(range.upperBound) + + return Signal { subscriber in + if let fetchResource = postbox.mediaBox.fetchResource { + let location = MediaResourceStorageLocation(userLocation: userLocation, reference: file.resourceReference(file.media.resource)) + let params = MediaResourceFetchParameters( + tag: TelegramMediaResourceFetchTag(statsCategory: .video, userContentType: .video), + info: TelegramCloudMediaResourceFetchInfo(reference: file.resourceReference(file.media.resource), preferBackgroundReferenceRevalidation: true, continueInBackground: true), + location: location, + contentType: .video, + isRandomAccessAllowed: true + ) + + final class StoredState { + let range: Range + var data: Data + var ranges: RangeSet + + init(range: Range) { + self.range = range + self.data = Data(count: Int(range.upperBound - range.lowerBound)) + self.ranges = RangeSet(range) + } + } + let storedState = Atomic(value: StoredState(range: mappedRange)) + + return fetchResource(file.media.resource, .single([(mappedRange, .elevated)]), params).start(next: { result in + switch result { + case let .dataPart(resourceOffset, data, _, _): + if !data.isEmpty { + let partRange = resourceOffset ..< (resourceOffset + Int64(data.count)) + var isReady = false + storedState.with { storedState in + let overlapRange = partRange.clamped(to: storedState.range) + guard !overlapRange.isEmpty else { + return + } + let innerRange = (overlapRange.lowerBound - storedState.range.lowerBound) ..< (overlapRange.upperBound - storedState.range.lowerBound) + let dataStart = overlapRange.lowerBound - partRange.lowerBound + let dataEnd = overlapRange.upperBound - partRange.lowerBound + let innerData = data.subdata(in: Int(dataStart) ..< Int(dataEnd)) + storedState.data.replaceSubrange(Int(innerRange.lowerBound) ..< Int(innerRange.upperBound), with: innerData) + storedState.ranges.subtract(RangeSet(overlapRange)) + if storedState.ranges.isEmpty { + isReady = true + } + } + if isReady { + subscriber.putNext((storedState.with({ $0.data }), Int(size))) + subscriber.putCompletion() + } + } + default: + break + } + }) + } else { + return EmptyDisposable + } + + /*let fetchDisposable = freeMediaFileResourceInteractiveFetched(postbox: postbox, userLocation: userLocation, fileReference: file, resource: file.media.resource, range: (mappedRange, .elevated)).startStandalone() + + let dataDisposable = postbox.mediaBox.resourceData(file.media.resource, size: size, in: mappedRange).startStandalone(next: { value, isComplete in + if isComplete { + subscriber.putNext((value, Int(size))) + subscriber.putCompletion() + } + }) + return ActionDisposable { + fetchDisposable.dispose() + dataDisposable.dispose() + }*/ + } + } + } + + private let postbox: Postbox + private let userLocation: MediaResourceUserLocation + private let fileReference: FileMediaReference + private let approximateDuration: Double + private let intrinsicDimensions: CGSize + + private let audioSessionManager: ManagedAudioSession + private let audioSessionDisposable = MetaDisposable() + private var hasAudioSession = false + + private let playbackCompletedListeners = Bag<() -> Void>() + + private var initializedStatus = false + private var statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused, soundEnabled: true) + private var isBuffering = false + private var seekId: Int = 0 + private let _status = ValuePromise() + var status: Signal { + return self._status.get() + } + + private let _bufferingStatus = Promise<(RangeSet, Int64)?>() + var bufferingStatus: Signal<(RangeSet, Int64)?, NoError> { + return self._bufferingStatus.get() + } + + private let _ready = Promise() + var ready: Signal { + return self._ready.get() + } + + private let _preloadCompleted = ValuePromise() + var preloadCompleted: Signal { + return self._preloadCompleted.get() + } + + private var playerSource: HLSServerSource? + private var serverDisposable: Disposable? + + private let imageNode: TransformImageNode + + private var playerItem: AVPlayerItem? + private let player: AVPlayer + private let playerNode: ASDisplayNode + + private var loadProgressDisposable: Disposable? + private var statusDisposable: Disposable? + + private var didPlayToEndTimeObserver: NSObjectProtocol? + private var didBecomeActiveObserver: NSObjectProtocol? + private var willResignActiveObserver: NSObjectProtocol? + private var failureObserverId: NSObjectProtocol? + private var errorObserverId: NSObjectProtocol? + private var playerItemFailedToPlayToEndTimeObserver: NSObjectProtocol? + + private let fetchDisposable = MetaDisposable() + + private var dimensions: CGSize? + private let dimensionsPromise = ValuePromise(CGSize()) + + private var validLayout: CGSize? + + private var statusTimer: Foundation.Timer? + + private var preferredVideoQuality: UniversalVideoContentVideoQuality = .auto + + init(postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, streamVideo: Bool, loopVideo: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool) { + self.postbox = postbox + self.fileReference = fileReference + self.approximateDuration = fileReference.media.duration ?? 0.0 + self.audioSessionManager = audioSessionManager + self.userLocation = userLocation + + self.imageNode = TransformImageNode() + + var startTime = CFAbsoluteTimeGetCurrent() + + let player = AVPlayer(playerItem: nil) + self.player = player + if !enableSound { + player.volume = 0.0 + } + + print("Player created in \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") + + self.playerNode = ASDisplayNode() + self.playerNode.setLayerBlock({ + return AVPlayerLayer(player: player) + }) + + self.intrinsicDimensions = fileReference.media.dimensions?.cgSize ?? CGSize(width: 480.0, height: 320.0) + + self.playerNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicDimensions) + + var qualityFiles: [Int: FileMediaReference] = [:] + for alternativeRepresentation in fileReference.media.alternativeRepresentations { + if let alternativeFile = alternativeRepresentation as? TelegramMediaFile { + for attribute in alternativeFile.attributes { + if case let .Video(_, size, _, _, _, videoCodec) = attribute { + let _ = size + if let videoCodec, NativeVideoContent.isVideoCodecSupported(videoCodec: videoCodec) { + qualityFiles[Int(size.height)] = fileReference.withMedia(alternativeFile) + } + } + } + } + } + /*for key in Array(qualityFiles.keys) { + if key != 144 && key != 720 { + qualityFiles.removeValue(forKey: key) + } + }*/ + var playlistFiles: [Int: FileMediaReference] = [:] + for alternativeRepresentation in fileReference.media.alternativeRepresentations { + if let alternativeFile = alternativeRepresentation as? TelegramMediaFile { + if alternativeFile.mimeType == "application/x-mpegurl" { + if let fileName = alternativeFile.fileName { + if fileName.hasPrefix("mtproto:") { + let fileIdString = String(fileName[fileName.index(fileName.startIndex, offsetBy: "mtproto:".count)...]) + if let fileId = Int64(fileIdString) { + for (quality, file) in qualityFiles { + if file.media.fileId.id == fileId { + playlistFiles[quality] = fileReference.withMedia(alternativeFile) + break + } + } + } + } + } + } + } + } + if !playlistFiles.isEmpty && playlistFiles.keys == qualityFiles.keys { + self.playerSource = HLSServerSource(id: UUID(), postbox: postbox, userLocation: userLocation, playlistFiles: playlistFiles, qualityFiles: qualityFiles) + } + + + super.init() + + self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, userLocation: self.userLocation, videoReference: fileReference) |> map { [weak self] getSize, getData in + Queue.mainQueue().async { + if let strongSelf = self, strongSelf.dimensions == nil { + if let dimensions = getSize() { + strongSelf.dimensions = dimensions + strongSelf.dimensionsPromise.set(dimensions) + if let size = strongSelf.validLayout { + strongSelf.updateLayout(size: size, transition: .immediate) + } + } + } + } + return getData + }) + + self.addSubnode(self.imageNode) + self.addSubnode(self.playerNode) + self.player.actionAtItemEnd = .pause + + self.imageNode.imageUpdated = { [weak self] _ in + self?._ready.set(.single(Void())) + } + + self.player.addObserver(self, forKeyPath: "rate", options: [], context: nil) + + self._bufferingStatus.set(.single(nil)) + + startTime = CFAbsoluteTimeGetCurrent() + + if let playerSource = self.playerSource { + self.serverDisposable = SharedHLSServer.shared.registerPlayer(source: playerSource) + + let playerItem: AVPlayerItem + let assetUrl = "http://127.0.0.1:\(SharedHLSServer.shared.port)/\(playerSource.id)/master.m3u8" + #if DEBUG + print("HLSVideoContentNode: playing \(assetUrl)") + #endif + playerItem = AVPlayerItem(url: URL(string: assetUrl)!) + print("Player item created in \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") + + if #available(iOS 14.0, *) { + playerItem.startsOnFirstEligibleVariant = true + } + + startTime = CFAbsoluteTimeGetCurrent() + self.setPlayerItem(playerItem) + print("Set player item in \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") + } + + self.didPlayToEndTimeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: self.player.currentItem, queue: nil, using: { [weak self] notification in + self?.performActionAtEnd() + }) + + self.failureObserverId = NotificationCenter.default.addObserver(forName: AVPlayerItem.failedToPlayToEndTimeNotification, object: self.player.currentItem, queue: .main, using: { notification in + print("Player Error: \(notification.description)") + }) + self.errorObserverId = NotificationCenter.default.addObserver(forName: AVPlayerItem.newErrorLogEntryNotification, object: self.player.currentItem, queue: .main, using: { notification in + print("Player Error: \(notification.description)") + }) + + self.didBecomeActiveObserver = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: nil, using: { [weak self] _ in + guard let strongSelf = self, let layer = strongSelf.playerNode.layer as? AVPlayerLayer else { + return + } + layer.player = strongSelf.player + }) + self.willResignActiveObserver = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil, using: { [weak self] _ in + guard let strongSelf = self, let layer = strongSelf.playerNode.layer as? AVPlayerLayer else { + return + } + layer.player = nil + }) + if let currentItem = self.player.currentItem { + currentItem.addObserver(self, forKeyPath: "presentationSize", options: [], context: nil) + } + } + + deinit { + self.player.removeObserver(self, forKeyPath: "rate") + if let currentItem = self.player.currentItem { + currentItem.removeObserver(self, forKeyPath: "presentationSize") + } + + self.setPlayerItem(nil) + + self.audioSessionDisposable.dispose() + + self.loadProgressDisposable?.dispose() + self.statusDisposable?.dispose() + + if let didPlayToEndTimeObserver = self.didPlayToEndTimeObserver { + NotificationCenter.default.removeObserver(didPlayToEndTimeObserver) + } + if let didBecomeActiveObserver = self.didBecomeActiveObserver { + NotificationCenter.default.removeObserver(didBecomeActiveObserver) + } + if let willResignActiveObserver = self.willResignActiveObserver { + NotificationCenter.default.removeObserver(willResignActiveObserver) + } + if let failureObserverId = self.failureObserverId { + NotificationCenter.default.removeObserver(failureObserverId) + } + if let errorObserverId = self.errorObserverId { + NotificationCenter.default.removeObserver(errorObserverId) + } + + self.serverDisposable?.dispose() + + self.statusTimer?.invalidate() + } + + private func setPlayerItem(_ item: AVPlayerItem?) { + if let playerItem = self.playerItem { + playerItem.removeObserver(self, forKeyPath: "playbackBufferEmpty") + playerItem.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp") + playerItem.removeObserver(self, forKeyPath: "playbackBufferFull") + playerItem.removeObserver(self, forKeyPath: "status") + if let playerItemFailedToPlayToEndTimeObserver = self.playerItemFailedToPlayToEndTimeObserver { + NotificationCenter.default.removeObserver(playerItemFailedToPlayToEndTimeObserver) + self.playerItemFailedToPlayToEndTimeObserver = nil + } + } + + self.playerItem = item + + if let playerItem = self.playerItem { + playerItem.addObserver(self, forKeyPath: "playbackBufferEmpty", options: .new, context: nil) + playerItem.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: .new, context: nil) + playerItem.addObserver(self, forKeyPath: "playbackBufferFull", options: .new, context: nil) + playerItem.addObserver(self, forKeyPath: "status", options: .new, context: nil) + self.playerItemFailedToPlayToEndTimeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime, object: playerItem, queue: OperationQueue.main, using: { [weak self] _ in + guard let self else { + return + } + let _ = self + }) + } + + self.player.replaceCurrentItem(with: self.playerItem) + } + + private func updateStatus() { + let isPlaying = !self.player.rate.isZero + let status: MediaPlayerPlaybackStatus + if self.isBuffering { + status = .buffering(initial: false, whilePlaying: isPlaying, progress: 0.0, display: true) + } else { + status = isPlaying ? .playing : .paused + } + var timestamp = self.player.currentTime().seconds + if timestamp.isFinite && !timestamp.isNaN { + } else { + timestamp = 0.0 + } + self.statusValue = MediaPlayerStatus(generationTimestamp: CACurrentMediaTime(), duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: timestamp, baseRate: Double(self.player.rate), seekId: self.seekId, status: status, soundEnabled: true) + self._status.set(self.statusValue) + + if case .playing = status { + if self.statusTimer == nil { + self.statusTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 1.0 / 30.0, repeats: true, block: { [weak self] _ in + guard let self else { + return + } + self.updateStatus() + }) + } + } else if let statusTimer = self.statusTimer { + self.statusTimer = nil + statusTimer.invalidate() + } + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if keyPath == "rate" { + let isPlaying = !self.player.rate.isZero + if isPlaying { + self.isBuffering = false + } + self.updateStatus() + } else if keyPath == "playbackBufferEmpty" { + self.isBuffering = true + self.updateStatus() + } else if keyPath == "playbackLikelyToKeepUp" || keyPath == "playbackBufferFull" { + self.isBuffering = false + self.updateStatus() + } else if keyPath == "presentationSize" { + if let currentItem = self.player.currentItem { + print("Presentation size: \(Int(currentItem.presentationSize.height))") + } + } + } + + private func performActionAtEnd() { + for listener in self.playbackCompletedListeners.copyItems() { + listener() + } + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + transition.updatePosition(node: self.playerNode, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0)) + transition.updateTransformScale(node: self.playerNode, scale: size.width / self.intrinsicDimensions.width) + + transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(), size: size)) + + let makeImageLayout = self.imageNode.asyncLayout() + let applyImageLayout = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: UIEdgeInsets())) + applyImageLayout() + } + + func play() { + assert(Queue.mainQueue().isCurrent()) + if !self.initializedStatus { + self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: self.seekId, status: .buffering(initial: true, whilePlaying: true, progress: 0.0, display: true), soundEnabled: true)) + } + if !self.hasAudioSession { + if self.player.volume != 0.0 { + self.audioSessionDisposable.set(self.audioSessionManager.push(audioSessionType: .play(mixWithOthers: false), activate: { [weak self] _ in + self?.hasAudioSession = true + self?.player.play() + }, deactivate: { [weak self] _ in + self?.hasAudioSession = false + self?.player.pause() + return .complete() + })) + } else { + self.player.play() + } + } else { + self.player.play() + } + } + + func pause() { + assert(Queue.mainQueue().isCurrent()) + self.player.pause() + } + + func togglePlayPause() { + assert(Queue.mainQueue().isCurrent()) + if self.player.rate.isZero { + self.play() + } else { + self.pause() + } + } + + func setSoundEnabled(_ value: Bool) { + assert(Queue.mainQueue().isCurrent()) + if value { + if !self.hasAudioSession { + self.audioSessionDisposable.set(self.audioSessionManager.push(audioSessionType: .play(mixWithOthers: false), activate: { [weak self] _ in + self?.hasAudioSession = true + self?.player.volume = 1.0 + }, deactivate: { [weak self] _ in + self?.hasAudioSession = false + self?.player.pause() + return .complete() + })) + } + } else { + self.player.volume = 0.0 + self.hasAudioSession = false + self.audioSessionDisposable.set(nil) + } + } + + func seek(_ timestamp: Double) { + assert(Queue.mainQueue().isCurrent()) + self.seekId += 1 + self.player.seek(to: CMTime(seconds: timestamp, preferredTimescale: 30)) + } + + func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { + self.player.volume = 1.0 + self.play() + } + + func setSoundMuted(soundMuted: Bool) { + self.player.volume = soundMuted ? 0.0 : 1.0 + } + + func continueWithOverridingAmbientMode(isAmbient: Bool) { + } + + func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) { + } + + func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { + self.player.volume = 0.0 + self.hasAudioSession = false + self.audioSessionDisposable.set(nil) + } + + func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool) { + } + + func setBaseRate(_ baseRate: Double) { + self.player.rate = Float(baseRate) + } + + func setVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) { + self.preferredVideoQuality = videoQuality + + guard let currentItem = self.player.currentItem else { + return + } + guard let playerSource = self.playerSource else { + return + } + + switch videoQuality { + case .auto: + currentItem.preferredPeakBitRate = 0.0 + case let .quality(qualityValue): + if let file = playerSource.qualityFiles[qualityValue] { + if let size = file.media.size, let duration = file.media.duration, duration != 0.0 { + let bandwidth = Int(Double(size) / duration) * 8 + currentItem.preferredPeakBitRate = Double(bandwidth) + } + } + } + + } + + func videoQualityState() -> (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])? { + guard let currentItem = self.player.currentItem else { + return nil + } + guard let playerSource = self.playerSource else { + return nil + } + let current = Int(currentItem.presentationSize.height) + var available: [Int] = Array(playerSource.qualityFiles.keys) + available.sort(by: { $0 > $1 }) + return (current, self.preferredVideoQuality, available) + } + + func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int { + return self.playbackCompletedListeners.add(f) + } + + func removePlaybackCompleted(_ index: Int) { + self.playbackCompletedListeners.remove(index) + } + + func fetchControl(_ control: UniversalVideoNodeFetchControl) { + } + + func notifyPlaybackControlsHidden(_ hidden: Bool) { + } + + func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) { + } +} diff --git a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift index 3df84b6729..6bc07d1c0c 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift @@ -40,6 +40,7 @@ public final class NativeVideoContent: UniversalVideoContent { public let beginWithAmbientSound: Bool public let mixWithOthers: Bool public let baseRate: Double + public let baseVideoQuality: UniversalVideoContentVideoQuality let fetchAutomatically: Bool let onlyFullSizeThumbnail: Bool let useLargeThumbnail: Bool @@ -56,7 +57,42 @@ public final class NativeVideoContent: UniversalVideoContent { let displayImage: Bool let hasSentFramesToDisplay: (() -> Void)? - public init(id: NativeVideoContentId, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, imageReference: ImageMediaReference? = nil, streamVideo: MediaPlayerStreaming = .none, loopVideo: Bool = false, enableSound: Bool = true, soundMuted: Bool = false, beginWithAmbientSound: Bool = false, mixWithOthers: Bool = false, baseRate: Double = 1.0, fetchAutomatically: Bool = true, onlyFullSizeThumbnail: Bool = false, useLargeThumbnail: Bool = false, autoFetchFullSizeThumbnail: Bool = false, startTimestamp: Double? = nil, endTimestamp: Double? = nil, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor = .white, tempFilePath: String? = nil, isAudioVideoMessage: Bool = false, captureProtected: Bool = false, hintDimensions: CGSize? = nil, storeAfterDownload: (() -> Void)?, displayImage: Bool = true, hasSentFramesToDisplay: (() -> Void)? = nil) { + public static func isVideoCodecSupported(videoCodec: String) -> Bool { + return videoCodec == "h264" || videoCodec == "h265" || videoCodec == "avc" || videoCodec == "hevc" + } + + public static func isHLSVideo(file: TelegramMediaFile) -> Bool { + for alternativeRepresentation in file.alternativeRepresentations { + if let alternativeFile = alternativeRepresentation as? TelegramMediaFile { + if alternativeFile.mimeType == "application/x-mpegurl" { + return true + } + } + } + return false + } + + public static func selectVideoQualityFile(file: TelegramMediaFile, quality: UniversalVideoContentVideoQuality) -> TelegramMediaFile { + guard case let .quality(qualityHeight) = quality else { + return file + } + for alternativeRepresentation in file.alternativeRepresentations { + if let alternativeFile = alternativeRepresentation as? TelegramMediaFile { + for attribute in alternativeFile.attributes { + if case let .Video(_, size, _, _, _, videoCodec) = attribute { + if let videoCodec, isVideoCodecSupported(videoCodec: videoCodec) { + if size.height == qualityHeight { + return alternativeFile + } + } + } + } + } + } + return file + } + + public init(id: NativeVideoContentId, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, imageReference: ImageMediaReference? = nil, streamVideo: MediaPlayerStreaming = .none, loopVideo: Bool = false, enableSound: Bool = true, soundMuted: Bool = false, beginWithAmbientSound: Bool = false, mixWithOthers: Bool = false, baseRate: Double = 1.0, baseVideoQuality: UniversalVideoContentVideoQuality = .auto, fetchAutomatically: Bool = true, onlyFullSizeThumbnail: Bool = false, useLargeThumbnail: Bool = false, autoFetchFullSizeThumbnail: Bool = false, startTimestamp: Double? = nil, endTimestamp: Double? = nil, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor = .white, tempFilePath: String? = nil, isAudioVideoMessage: Bool = false, captureProtected: Bool = false, hintDimensions: CGSize? = nil, storeAfterDownload: (() -> Void)?, displayImage: Bool = true, hasSentFramesToDisplay: (() -> Void)? = nil) { self.id = id self.nativeId = id self.userLocation = userLocation @@ -83,6 +119,7 @@ public final class NativeVideoContent: UniversalVideoContent { self.beginWithAmbientSound = beginWithAmbientSound self.mixWithOthers = mixWithOthers self.baseRate = baseRate + self.baseVideoQuality = baseVideoQuality self.fetchAutomatically = fetchAutomatically self.onlyFullSizeThumbnail = onlyFullSizeThumbnail self.useLargeThumbnail = useLargeThumbnail @@ -101,7 +138,7 @@ public final class NativeVideoContent: UniversalVideoContent { } public func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { - return NativeVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, fileReference: self.fileReference, imageReference: self.imageReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, soundMuted: self.soundMuted, beginWithAmbientSound: self.beginWithAmbientSound, mixWithOthers: self.mixWithOthers, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically, onlyFullSizeThumbnail: self.onlyFullSizeThumbnail, useLargeThumbnail: self.useLargeThumbnail, autoFetchFullSizeThumbnail: self.autoFetchFullSizeThumbnail, startTimestamp: self.startTimestamp, endTimestamp: self.endTimestamp, continuePlayingWithoutSoundOnLostAudioSession: self.continuePlayingWithoutSoundOnLostAudioSession, placeholderColor: self.placeholderColor, tempFilePath: self.tempFilePath, isAudioVideoMessage: self.isAudioVideoMessage, captureProtected: self.captureProtected, hintDimensions: self.hintDimensions, storeAfterDownload: self.storeAfterDownload, displayImage: self.displayImage, hasSentFramesToDisplay: self.hasSentFramesToDisplay) + return NativeVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, fileReference: self.fileReference, imageReference: self.imageReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, soundMuted: self.soundMuted, beginWithAmbientSound: self.beginWithAmbientSound, mixWithOthers: self.mixWithOthers, baseRate: self.baseRate, baseVideoQuality: self.baseVideoQuality, fetchAutomatically: self.fetchAutomatically, onlyFullSizeThumbnail: self.onlyFullSizeThumbnail, useLargeThumbnail: self.useLargeThumbnail, autoFetchFullSizeThumbnail: self.autoFetchFullSizeThumbnail, startTimestamp: self.startTimestamp, endTimestamp: self.endTimestamp, continuePlayingWithoutSoundOnLostAudioSession: self.continuePlayingWithoutSoundOnLostAudioSession, placeholderColor: self.placeholderColor, tempFilePath: self.tempFilePath, isAudioVideoMessage: self.isAudioVideoMessage, captureProtected: self.captureProtected, hintDimensions: self.hintDimensions, storeAfterDownload: self.storeAfterDownload, displayImage: self.displayImage, hasSentFramesToDisplay: self.hasSentFramesToDisplay) } public func isEqual(to other: UniversalVideoContent) -> Bool { @@ -122,18 +159,21 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent private let postbox: Postbox private let userLocation: MediaResourceUserLocation private let fileReference: FileMediaReference + private let streamVideo: MediaPlayerStreaming private let enableSound: Bool private let soundMuted: Bool private let beginWithAmbientSound: Bool private let mixWithOthers: Bool private let loopVideo: Bool private let baseRate: Double + private var baseVideoQuality: UniversalVideoContentVideoQuality private let audioSessionManager: ManagedAudioSession private let isAudioVideoMessage: Bool private let captureProtected: Bool + private let continuePlayingWithoutSoundOnLostAudioSession: Bool private let displayImage: Bool - private let player: MediaPlayer + private var player: MediaPlayer private var thumbnailPlayer: MediaPlayer? private let imageNode: TransformImageNode private let playerNode: MediaPlayerNode @@ -183,10 +223,11 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent private let hasSentFramesToDisplay: (() -> Void)? - init(postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, imageReference: ImageMediaReference?, streamVideo: MediaPlayerStreaming, loopVideo: Bool, enableSound: Bool, soundMuted: Bool, beginWithAmbientSound: Bool, mixWithOthers: Bool, baseRate: Double, fetchAutomatically: Bool, onlyFullSizeThumbnail: Bool, useLargeThumbnail: Bool, autoFetchFullSizeThumbnail: Bool, startTimestamp: Double?, endTimestamp: Double?, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor, tempFilePath: String?, isAudioVideoMessage: Bool, captureProtected: Bool, hintDimensions: CGSize?, storeAfterDownload: (() -> Void)? = nil, displayImage: Bool, hasSentFramesToDisplay: (() -> Void)?) { + init(postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, imageReference: ImageMediaReference?, streamVideo: MediaPlayerStreaming, loopVideo: Bool, enableSound: Bool, soundMuted: Bool, beginWithAmbientSound: Bool, mixWithOthers: Bool, baseRate: Double, baseVideoQuality: UniversalVideoContentVideoQuality, fetchAutomatically: Bool, onlyFullSizeThumbnail: Bool, useLargeThumbnail: Bool, autoFetchFullSizeThumbnail: Bool, startTimestamp: Double?, endTimestamp: Double?, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor, tempFilePath: String?, isAudioVideoMessage: Bool, captureProtected: Bool, hintDimensions: CGSize?, storeAfterDownload: (() -> Void)? = nil, displayImage: Bool, hasSentFramesToDisplay: (() -> Void)?) { self.postbox = postbox self.userLocation = userLocation self.fileReference = fileReference + self.streamVideo = streamVideo self.placeholderColor = placeholderColor self.enableSound = enableSound self.soundMuted = soundMuted @@ -194,9 +235,11 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent self.mixWithOthers = mixWithOthers self.loopVideo = loopVideo self.baseRate = baseRate + self.baseVideoQuality = baseVideoQuality self.audioSessionManager = audioSessionManager self.isAudioVideoMessage = isAudioVideoMessage self.captureProtected = captureProtected + self.continuePlayingWithoutSoundOnLostAudioSession = continuePlayingWithoutSoundOnLostAudioSession self.displayImage = displayImage self.hasSentFramesToDisplay = hasSentFramesToDisplay @@ -210,7 +253,9 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent break } - self.player = MediaPlayer(audioSessionManager: audioSessionManager, postbox: postbox, userLocation: userLocation, userContentType: userContentType, resourceReference: fileReference.resourceReference(fileReference.media.resource), tempFilePath: tempFilePath, streamable: streamVideo, video: true, preferSoftwareDecoding: false, playAutomatically: false, enableSound: enableSound, baseRate: baseRate, fetchAutomatically: fetchAutomatically, soundMuted: soundMuted, ambient: beginWithAmbientSound, mixWithOthers: mixWithOthers, continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession, storeAfterDownload: storeAfterDownload, isAudioVideoMessage: isAudioVideoMessage) + let selectedFile = NativeVideoContent.selectVideoQualityFile(file: fileReference.media, quality: self.baseVideoQuality) + + self.player = MediaPlayer(audioSessionManager: audioSessionManager, postbox: postbox, userLocation: userLocation, userContentType: userContentType, resourceReference: fileReference.resourceReference(selectedFile.resource), tempFilePath: tempFilePath, streamable: streamVideo, video: true, preferSoftwareDecoding: false, playAutomatically: false, enableSound: enableSound, baseRate: baseRate, fetchAutomatically: fetchAutomatically, soundMuted: soundMuted, ambient: beginWithAmbientSound, mixWithOthers: mixWithOthers, continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession, storeAfterDownload: storeAfterDownload, isAudioVideoMessage: isAudioVideoMessage) var actionAtEndImpl: (() -> Void)? if enableSound && !loopVideo { @@ -279,7 +324,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent return MediaPlayerStatus(generationTimestamp: status.generationTimestamp, duration: status.duration, dimensions: dimensions, timestamp: status.timestamp, baseRate: status.baseRate, seekId: status.seekId, status: status.status, soundEnabled: status.soundEnabled) }) - self.fetchStatusDisposable.set((postbox.mediaBox.resourceStatus(fileReference.media.resource) + self.fetchStatusDisposable.set((postbox.mediaBox.resourceStatus(selectedFile.resource) |> deliverOnMainQueue).start(next: { [weak self] status in guard let strongSelf = self else { return @@ -294,8 +339,8 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent } })) - if let size = fileReference.media.size { - self._bufferingStatus.set(postbox.mediaBox.resourceRangesStatus(fileReference.media.resource) |> map { ranges in + if let size = selectedFile.size { + self._bufferingStatus.set(postbox.mediaBox.resourceRangesStatus(selectedFile.resource) |> map { ranges in return (ranges, size) }) } else { @@ -503,6 +548,98 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent self.player.setBaseRate(baseRate) } + func setVideoQuality(_ quality: UniversalVideoContentVideoQuality) { + let _ = (self._status.get() + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak self] status in + guard let self else { + return + } + + if self.baseVideoQuality == quality { + return + } + self.baseVideoQuality = quality + + let selectedFile = NativeVideoContent.selectVideoQualityFile(file: self.fileReference.media, quality: self.baseVideoQuality) + + let updatedFileReference: FileMediaReference = self.fileReference.withMedia(selectedFile) + + var userContentType = MediaResourceUserContentType(file: selectedFile) + switch updatedFileReference { + case .story: + userContentType = .story + default: + break + } + + self._status.set(.never()) + self.player.pause() + + //TODO:release coordinate fetchAutomatically + self.player = MediaPlayer(audioSessionManager: self.audioSessionManager, postbox: self.postbox, userLocation: self.userLocation, userContentType: userContentType, resourceReference: updatedFileReference.resourceReference(selectedFile.resource), tempFilePath: nil, streamable: self.streamVideo, video: true, preferSoftwareDecoding: false, playAutomatically: false, enableSound: self.enableSound, baseRate: self.baseRate, fetchAutomatically: true, soundMuted: self.soundMuted, ambient: beginWithAmbientSound, mixWithOthers: mixWithOthers, continuePlayingWithoutSoundOnLostAudioSession: self.continuePlayingWithoutSoundOnLostAudioSession, storeAfterDownload: nil, isAudioVideoMessage: self.isAudioVideoMessage) + + var actionAtEndImpl: (() -> Void)? + if self.enableSound && !self.loopVideo { + self.player.actionAtEnd = .action({ + actionAtEndImpl?() + }) + } else { + self.player.actionAtEnd = .loop({ + actionAtEndImpl?() + }) + } + actionAtEndImpl = { [weak self] in + self?.performActionAtEnd() + } + + self._status.set(combineLatest(self.dimensionsPromise.get(), self.player.status) + |> map { dimensions, status in + return MediaPlayerStatus(generationTimestamp: status.generationTimestamp, duration: status.duration, dimensions: dimensions, timestamp: status.timestamp, baseRate: status.baseRate, seekId: status.seekId, status: status.status, soundEnabled: status.soundEnabled) + }) + + self.fetchStatusDisposable.set((self.postbox.mediaBox.resourceStatus(selectedFile.resource) + |> deliverOnMainQueue).start(next: { [weak self] status in + guard let strongSelf = self else { + return + } + switch status { + case .Local: + break + default: + if strongSelf.thumbnailPlayer == nil { + strongSelf.createThumbnailPlayer() + } + } + })) + + if let size = updatedFileReference.media.size { + self._bufferingStatus.set(postbox.mediaBox.resourceRangesStatus(selectedFile.resource) |> map { ranges in + return (ranges, size) + }) + } else { + self._bufferingStatus.set(.single(nil)) + } + + self.player.attachPlayerNode(self.playerNode) + + var play = false + switch status.status { + case .playing: + play = true + case let .buffering(_, whilePlaying, _, _): + play = whilePlaying + case .paused: + break + } + self.player.seek(timestamp: status.timestamp, play: play) + }) + } + + func videoQualityState() -> (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])? { + return nil + } + func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { assert(Queue.mainQueue().isCurrent()) let action = { [weak self] in diff --git a/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift index 30bac1bc9c..f7a6529cb1 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift @@ -70,7 +70,7 @@ public final class PlatformVideoContent: UniversalVideoContent { } public let id: AnyHashable - let nativeId: PlatformVideoContentId + public let nativeId: PlatformVideoContentId let userLocation: MediaResourceUserLocation let content: Content public let dimensions: CGSize @@ -448,6 +448,13 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte func setBaseRate(_ baseRate: Double) { } + func setVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) { + } + + func videoQualityState() -> (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])? { + return nil + } + func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int { return self.playbackCompletedListeners.add(f) } diff --git a/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift index 756fcf51e9..66bca4d094 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift @@ -285,6 +285,13 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent func setBaseRate(_ baseRate: Double) { } + func setVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) { + } + + func videoQualityState() -> (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])? { + return nil + } + func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int { return self.playbackCompletedListeners.add(f) } diff --git a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift index d45c70364b..2cba6fdf64 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift @@ -183,6 +183,13 @@ final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode { self.playerNode.setBaseRate(baseRate) } + func setVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) { + } + + func videoQualityState() -> (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])? { + return nil + } + func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int { return self.playbackCompletedListeners.add(f) } diff --git a/submodules/TelegramVoip/Sources/WrappedMediaStreamingContext.swift b/submodules/TelegramVoip/Sources/WrappedMediaStreamingContext.swift index 115ee7edb8..1acafab9a5 100644 --- a/submodules/TelegramVoip/Sources/WrappedMediaStreamingContext.swift +++ b/submodules/TelegramVoip/Sources/WrappedMediaStreamingContext.swift @@ -138,7 +138,7 @@ public final class WrappedMediaStreamingContext { } } @available(iOS 12.0, macOS 14.0, *) -public final class ExternalMediaStreamingContext { +public final class ExternalMediaStreamingContext: SharedHLSServerSource { private final class Impl { let queue: Queue @@ -274,21 +274,29 @@ public final class ExternalMediaStreamingContext { ) } } + + func fileData(id: Int64, range: Range) -> Signal<(Data, Int)?, NoError> { + return .never() + } } private let queue = Queue() - let id: CallSessionInternalId + let internalId: CallSessionInternalId private let impl: QueueLocalObject private var hlsServerDisposable: Disposable? + public var id: UUID { + return self.internalId + } + public init(id: CallSessionInternalId, rejoinNeeded: @escaping () -> Void) { - self.id = id + self.internalId = id let queue = self.queue self.impl = QueueLocalObject(queue: queue, generate: { return Impl(queue: queue, rejoinNeeded: rejoinNeeded) }) - self.hlsServerDisposable = SharedHLSServer.shared.registerPlayer(streamingContext: self) + self.hlsServerDisposable = SharedHLSServer.shared.registerPlayer(source: self) } deinit { @@ -322,9 +330,27 @@ public final class ExternalMediaStreamingContext { impl.partData(index: index, quality: quality).start(next: subscriber.putNext) } } + + public func fileData(id: Int64, range: Range) -> Signal<(Data, Int)?, NoError> { + return self.impl.signalWith { impl, subscriber in + impl.fileData(id: id, range: range).start(next: subscriber.putNext) + } + } } + +public protocol SharedHLSServerSource: AnyObject { + var id: UUID { get } + + func masterPlaylistData() -> Signal + func playlistData(quality: Int) -> Signal + func partData(index: Int, quality: Int) -> Signal + func fileData(id: Int64, range: Range) -> Signal<(Data, Int)?, NoError> +} + @available(iOS 12.0, macOS 14.0, *) public final class SharedHLSServer { + public typealias Source = SharedHLSServerSource + public static let shared: SharedHLSServer = { return SharedHLSServer() }() @@ -346,11 +372,11 @@ public final class SharedHLSServer { } } - private final class ContextReference { - weak var streamingContext: ExternalMediaStreamingContext? + private final class SourceReference { + weak var source: SharedHLSServerSource? - init(streamingContext: ExternalMediaStreamingContext) { - self.streamingContext = streamingContext + init(source: SharedHLSServerSource) { + self.source = source } } @available(iOS 12.0, macOS 14.0, *) @@ -360,7 +386,7 @@ public final class SharedHLSServer { private let port: NWEndpoint.Port private var listener: NWListener? - private var contextReferences = Bag() + private var sourceReferences = Bag() init(queue: Queue, port: UInt16) { self.queue = queue @@ -443,6 +469,20 @@ public final class SharedHLSServer { } let requestPath = String(firstLine[firstLine.startIndex ..< firstLine.index(firstLine.endIndex, offsetBy: -" HTTP/1.1".count)]) + var requestRange: Range? + if let rangeRange = requestString.range(of: "Range: bytes=") { + if let endRange = requestString.range(of: "\r\n", range: rangeRange.upperBound ..< requestString.endIndex) { + let rangeString = String(requestString[rangeRange.upperBound ..< endRange.lowerBound]) + if let dashRange = rangeString.range(of: "-") { + let lowerBoundString = String(rangeString[rangeString.startIndex ..< dashRange.lowerBound]) + let upperBoundString = String(rangeString[dashRange.upperBound ..< rangeString.endIndex]) + + if let lowerBound = Int(lowerBoundString), let upperBound = Int(upperBoundString) { + requestRange = lowerBound ..< upperBound + } + } + } + } guard let firstSlash = requestPath.range(of: "/") else { self.sendErrorAndClose(connection: connection, error: .notFound) @@ -452,14 +492,14 @@ public final class SharedHLSServer { self.sendErrorAndClose(connection: connection) return } - guard let streamingContext = self.contextReferences.copyItems().first(where: { $0.streamingContext?.id == streamId })?.streamingContext else { + guard let source = self.sourceReferences.copyItems().first(where: { $0.source?.id == streamId })?.source else { self.sendErrorAndClose(connection: connection) return } let filePath = String(requestPath[firstSlash.upperBound...]) if filePath == "master.m3u8" { - let _ = (streamingContext.masterPlaylistData() + let _ = (source.masterPlaylistData() |> deliverOn(self.queue) |> take(1)).start(next: { [weak self] result in guard let self else { @@ -474,7 +514,7 @@ public final class SharedHLSServer { return } - let _ = (streamingContext.playlistData(quality: levelIndex) + let _ = (source.playlistData(quality: levelIndex) |> deliverOn(self.queue) |> take(1)).start(next: { [weak self] result in guard let self else { @@ -497,7 +537,7 @@ public final class SharedHLSServer { self.sendErrorAndClose(connection: connection) return } - let _ = (streamingContext.partData(index: partIndex, quality: levelIndex) + let _ = (source.partData(index: partIndex, quality: levelIndex) |> deliverOn(self.queue) |> take(1)).start(next: { [weak self] result in guard let self else { @@ -529,6 +569,29 @@ public final class SharedHLSServer { self.sendErrorAndClose(connection: connection, error: .notFound) } }) + } else if filePath.hasPrefix("partfile") && filePath.hasSuffix(".mp4") { + let fileId = String(filePath[filePath.index(filePath.startIndex, offsetBy: "partfile".count) ..< filePath.index(filePath.endIndex, offsetBy: -".mp4".count)]) + guard let fileIdValue = Int64(fileId) else { + self.sendErrorAndClose(connection: connection) + return + } + guard let requestRange else { + self.sendErrorAndClose(connection: connection) + return + } + let _ = (source.fileData(id: fileIdValue, range: requestRange.lowerBound ..< requestRange.upperBound + 1) + |> deliverOn(self.queue) + |> take(1)).start(next: { [weak self] result in + guard let self else { + return + } + + if let (data, totalSize) = result { + self.sendResponseAndClose(connection: connection, data: data, range: requestRange, totalSize: totalSize) + } else { + self.sendErrorAndClose(connection: connection, error: .internalServerError) + } + }) } else { self.sendErrorAndClose(connection: connection, error: .notFound) } @@ -544,8 +607,16 @@ public final class SharedHLSServer { }) } - private func sendResponseAndClose(connection: NWConnection, data: Data) { - let responseHeaders = "HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nConnection: close\r\n\r\n" + private func sendResponseAndClose(connection: NWConnection, data: Data, range: Range? = nil, totalSize: Int? = nil) { + var responseHeaders = "HTTP/1.1 200 OK\r\n" + responseHeaders.append("Content-Length: \(data.count)\r\n") + if let range, let totalSize { + responseHeaders.append("Content-Range: bytes \(range.lowerBound)-\(range.upperBound)/\(totalSize)\r\n") + } + responseHeaders.append("Content-Type: application/octet-stream\r\n") + responseHeaders.append("Connection: close\r\n") + responseHeaders.append("Access-Control-Allow-Origin: *\r\n") + responseHeaders.append("\r\n") var responseData = Data() responseData.append(responseHeaders.data(using: .utf8)!) responseData.append(data) @@ -557,16 +628,16 @@ public final class SharedHLSServer { }) } - func registerPlayer(streamingContext: ExternalMediaStreamingContext) -> Disposable { + func registerPlayer(source: SharedHLSServerSource) -> Disposable { let queue = self.queue - let index = self.contextReferences.add(ContextReference(streamingContext: streamingContext)) + let index = self.sourceReferences.add(SourceReference(source: source)) return ActionDisposable { [weak self] in queue.async { guard let self else { return } - self.contextReferences.remove(index) + self.sourceReferences.remove(index) } } } @@ -584,11 +655,11 @@ public final class SharedHLSServer { }) } - fileprivate func registerPlayer(streamingContext: ExternalMediaStreamingContext) -> Disposable { + public func registerPlayer(source: SharedHLSServerSource) -> Disposable { let disposable = MetaDisposable() self.impl.with { impl in - disposable.set(impl.registerPlayer(streamingContext: streamingContext)) + disposable.set(impl.registerPlayer(source: source)) } return disposable diff --git a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm index 66b6284805..4237c162a5 100644 --- a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm +++ b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm @@ -1781,7 +1781,11 @@ audioDevice:(SharedCallAudioDevice * _Nullable)audioDevice { NSMutableArray *result = [[NSMutableArray alloc] init]; for (auto &it : levels.updates) { [result addObject:@(it.ssrc)]; - [result addObject:@(it.value.level)]; + auto level = it.value.level; + if (it.value.isMuted) { + level = 0.0; + } + [result addObject:@(level)]; [result addObject:@(it.value.voice)]; } audioLevelsUpdated(result); diff --git a/submodules/WatchBridge/Sources/WatchBridge.swift b/submodules/WatchBridge/Sources/WatchBridge.swift index c7c75e42e1..8f88b40ff7 100644 --- a/submodules/WatchBridge/Sources/WatchBridge.swift +++ b/submodules/WatchBridge/Sources/WatchBridge.swift @@ -172,7 +172,7 @@ func makeBridgeMedia(message: Message, strings: PresentationStrings, chatPeer: P for attribute in file.attributes { switch attribute { - case let .Video(duration, size, flags, _, _): + case let .Video(duration, size, flags, _, _, _): bridgeVideo.duration = Int32(duration) bridgeVideo.dimensions = size.cgSize bridgeVideo.round = flags.contains(.instantRoundVideo) diff --git a/submodules/WatchBridge/Sources/WatchRequestHandlers.swift b/submodules/WatchBridge/Sources/WatchRequestHandlers.swift index 5c986acfb2..964990c99b 100644 --- a/submodules/WatchBridge/Sources/WatchRequestHandlers.swift +++ b/submodules/WatchBridge/Sources/WatchRequestHandlers.swift @@ -720,7 +720,7 @@ final class WatchAudioHandler: WatchRequestHandler { replyMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: replyToMid) } - let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.count), attributes: [.Audio(isVoice: true, duration: Int(duration), title: nil, performer: nil, waveform: nil)])), threadId: nil, replyToMessageId: replyMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).start() + let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.count), attributes: [.Audio(isVoice: true, duration: Int(duration), title: nil, performer: nil, waveform: nil)], alternativeRepresentations: [])), threadId: nil, replyToMessageId: replyMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).start() } }) } else { diff --git a/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift b/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift index f6b4715142..b9e7363af9 100644 --- a/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift +++ b/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift @@ -37,7 +37,7 @@ struct WebSearchGalleryEntry: Equatable { switch self.result { case let .externalReference(externalReference): if let content = externalReference.content, externalReference.type == "gif", let thumbnailResource = externalReference.thumbnail?.resource, let dimensions = content.dimensions { - let fileReference = FileMediaReference.standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil)])) + 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, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])) return WebSearchVideoGalleryItem(context: context, presentationData: presentationData, index: self.index, result: self.result, content: NativeVideoContent(id: .contextResult(self.result.queryId, self.result.id), userLocation: .other, fileReference: fileReference, loopVideo: true, enableSound: false, fetchAutomatically: true, storeAfterDownload: nil), controllerInteraction: controllerInteraction) } case let .internalReference(internalReference): diff --git a/submodules/WidgetItemsUtils/Sources/WidgetItemsUtils.swift b/submodules/WidgetItemsUtils/Sources/WidgetItemsUtils.swift index e350f2ec7c..c643a025e0 100644 --- a/submodules/WidgetItemsUtils/Sources/WidgetItemsUtils.swift +++ b/submodules/WidgetItemsUtils/Sources/WidgetItemsUtils.swift @@ -22,7 +22,7 @@ public extension WidgetDataPeer.Message { switch attribute { case let .Sticker(altText, _, _): content = .sticker(WidgetDataPeer.Message.Content.Sticker(altText: altText)) - case let .Video(duration, _, flags, _, _): + case let .Video(duration, _, flags, _, _, _): if flags.contains(.instantRoundVideo) { content = .videoMessage(WidgetDataPeer.Message.Content.VideoMessage(duration: Int32(duration))) } else {