From 9cdad135c01108c4a5c9b5584e312aafabb44fe7 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 20 Sep 2024 22:33:27 +0800 Subject: [PATCH 01/17] Test vfsoverlay --- .bazelrc | 1 + 1 file changed, 1 insertion(+) diff --git a/.bazelrc b/.bazelrc index 43ccf92e1f..5ecdbaff0d 100644 --- a/.bazelrc +++ b/.bazelrc @@ -29,6 +29,7 @@ build --features=debug_prefix_map_pwd_is_dot build --features=swift.cacheable_swiftmodules build --features=swift.debug_prefix_map build --features=swift.enable_vfsoverlays +build --features=swift.vfsoverlay build --strategy=Genrule=standalone build --spawn_strategy=standalone From af267cae64c46c9e01cdbb03ec20bf0bf019d915 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 20 Sep 2024 22:33:36 +0800 Subject: [PATCH 02/17] Video player and calls --- .../Sources/AccountContext.swift | 2 +- .../Sources/PresentationCallManager.swift | 3 +- .../Sources/UniversalVideoNode.swift | 9 +- .../Sources/AvatarVideoNode.swift | 2 +- .../Sources/ChatImportActivityScreen.swift | 2 +- .../Sources/DrawingStickerEntityView.swift | 1 + .../ChatVideoGalleryItemScrubberView.swift | 1 + .../GalleryUI/Sources/GalleryController.swift | 11 + .../Items/UniversalVideoGalleryItem.swift | 368 +++++++------- .../InstantPagePlayableVideoNode.swift | 2 +- .../Sources/PeerAvatarImageGalleryItem.swift | 2 +- .../Sources/PeerInfoAvatarListNode.swift | 2 +- submodules/Postbox/Sources/MediaBox.swift | 67 +-- .../Sources/MediaBoxFileContextV2Impl.swift | 28 +- .../Postbox/Sources/MediaBoxFileManager.swift | 8 +- .../Sources/PhoneDemoComponent.swift | 2 +- .../Sources/ShareLoadingContainerNode.swift | 2 +- .../Sources/PresentationCallManager.swift | 122 +++-- .../ScheduleVideoChatSheetScreen.swift | 466 ++++++++++++++++++ .../VideoChatActionButtonComponent.swift | 5 +- .../Sources/VideoChatMicButtonComponent.swift | 49 +- .../VideoChatScheduledInfoComponent.swift | 213 ++++++++ .../Sources/VideoChatScreen.swift | 124 ++++- .../Sources/VoiceChatController.swift | 6 +- .../Network/FetchedMediaResource.swift | 12 + .../Sources/ChatBotInfoItem.swift | 2 +- .../ChatMessageActionBubbleContentNode.swift | 2 +- ...atMessageInteractiveInstantVideoNode.swift | 2 +- .../ChatMessageInteractiveMediaNode.swift | 23 +- ...ageProfilePhotoSuggestionContentNode.swift | 2 +- .../Sources/ChatQrCodeScreen.swift | 2 +- ...PeerInfoAvatarTransformContainerNode.swift | 2 +- .../Sources/PeerInfoEditingAvatarNode.swift | 2 +- .../Sources/PeerInfoScreen.swift | 5 +- .../Sources/StoryItemContentComponent.swift | 1 + .../NavigationSettings.imageset/Contents.json | 12 + .../videosettings_30.pdf | Bin 0 -> 5753 bytes .../Contents.json | 12 + .../videosettingsauto_30.pdf | Bin 0 -> 5233 bytes .../Contents.json | 12 + .../videosettingshd_30.pdf | Bin 0 -> 5183 bytes .../Contents.json | 12 + .../videosettingssd_30.pdf | Bin 0 -> 5423 bytes .../TelegramUI/Sources/AccountContext.swift | 4 +- .../TelegramUI/Sources/AppDelegate.swift | 2 +- .../TelegramUI/Sources/ChatController.swift | 2 +- .../Sources/OverlayInstantVideoNode.swift | 4 +- .../Sources/SharedMediaPlayer.swift | 2 +- .../TelegramUniversalVideoContent/BUILD | 1 + .../Sources/HLSVideoContent.swift | 391 +++++++++------ .../Sources/NativeVideoContent.swift | 2 +- .../Sources/OverlayUniversalVideoNode.swift | 4 +- .../Sources/PlatformVideoContent.swift | 2 +- .../Sources/SystemVideoContent.swift | 2 +- .../Sources/WebEmbedVideoContent.swift | 2 +- submodules/TelegramVoip/BUILD | 1 + .../WrappedMediaStreamingContext.swift | 160 +++++- .../Sources/WebSearchVideoGalleryItem.swift | 2 +- 58 files changed, 1643 insertions(+), 538 deletions(-) create mode 100644 submodules/TelegramCallsUI/Sources/ScheduleVideoChatSheetScreen.swift create mode 100644 submodules/TelegramCallsUI/Sources/VideoChatScheduledInfoComponent.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettings.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettings.imageset/videosettings_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQAuto.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQAuto.imageset/videosettingsauto_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQHD.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQHD.imageset/videosettingshd_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQSD.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQSD.imageset/videosettingssd_30.pdf diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 8a704ace67..366086569a 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1135,7 +1135,7 @@ public protocol AccountContext: AnyObject { func chatLocationUnreadCount(for location: ChatLocation, contextHolder: Atomic) -> Signal func applyMaxReadIndex(for location: ChatLocation, contextHolder: Atomic, messageIndex: MessageIndex) - func scheduleGroupCall(peerId: PeerId) + func scheduleGroupCall(peerId: PeerId, parentController: ViewController) func joinGroupCall(peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, activeCall: EngineGroupCallDescription) func requestCall(peerId: PeerId, isVideo: Bool, completion: @escaping () -> Void) } diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index 310c6846ce..d4d605a69a 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -4,6 +4,7 @@ import AsyncDisplayKit import TelegramCore import SwiftSignalKit import TelegramAudio +import Display public enum RequestCallResult { case requested @@ -472,5 +473,5 @@ public protocol PresentationCallManager: AnyObject { func requestCall(context: AccountContext, peerId: EnginePeer.Id, isVideo: Bool, endCurrentIfAny: Bool) -> RequestCallResult func joinGroupCall(context: AccountContext, peerId: EnginePeer.Id, invite: String?, requestJoinAsPeerId: ((@escaping (EnginePeer.Id?) -> Void) -> Void)?, initialCall: EngineGroupCallDescription, endCurrentIfAny: Bool) -> JoinGroupCallManagerResult - func scheduleGroupCall(context: AccountContext, peerId: EnginePeer.Id, endCurrentIfAny: Bool) -> RequestScheduleGroupCallResult + func scheduleGroupCall(context: AccountContext, peerId: EnginePeer.Id, endCurrentIfAny: Bool, parentController: ViewController) -> RequestScheduleGroupCallResult } diff --git a/submodules/AccountContext/Sources/UniversalVideoNode.swift b/submodules/AccountContext/Sources/UniversalVideoNode.swift index d224b6aa78..788431c2be 100644 --- a/submodules/AccountContext/Sources/UniversalVideoNode.swift +++ b/submodules/AccountContext/Sources/UniversalVideoNode.swift @@ -48,7 +48,7 @@ public protocol UniversalVideoContent { var dimensions: CGSize { get } var duration: Double { get } - func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode + func makeContentNode(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode func isEqual(to other: UniversalVideoContent) -> Bool } @@ -90,6 +90,7 @@ public enum UniversalVideoNodeFetchControl { } public final class UniversalVideoNode: ASDisplayNode { + private let accountId: AccountRecordId private let postbox: Postbox private let audioSession: ManagedAudioSession private let manager: UniversalVideoManager @@ -135,11 +136,12 @@ public final class UniversalVideoNode: ASDisplayNode { if self.canAttachContent { assert(self.contentRequestIndex == nil) + let accountId = self.accountId let content = self.content let postbox = self.postbox let audioSession = self.audioSession self.contentRequestIndex = self.manager.attachUniversalVideoContent(content: self.content, priority: self.priority, create: { - return content.makeContentNode(postbox: postbox, audioSession: audioSession) + return content.makeContentNode(accountId: accountId, postbox: postbox, audioSession: audioSession) }, update: { [weak self] contentNodeAndFlags in if let strongSelf = self { strongSelf.updateContentNode(contentNodeAndFlags) @@ -160,7 +162,8 @@ public final class UniversalVideoNode: ASDisplayNode { return self.contentNode != nil } - public init(postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoManager, decoration: UniversalVideoDecoration, content: UniversalVideoContent, priority: UniversalVideoPriority, autoplay: Bool = false, snapshotContentWhenGone: Bool = false) { + public init(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoManager, decoration: UniversalVideoDecoration, content: UniversalVideoContent, priority: UniversalVideoPriority, autoplay: Bool = false, snapshotContentWhenGone: Bool = false) { + self.accountId = accountId self.postbox = postbox self.audioSession = audioSession self.manager = manager diff --git a/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift b/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift index 6cc94e44f2..85c701d480 100644 --- a/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift +++ b/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift @@ -234,7 +234,7 @@ public final class AvatarVideoNode: ASDisplayNode { if self.videoNode == nil { let context = self.context let mediaManager = context.sharedContext.mediaManager - let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: VideoDecoration(), content: videoContent, priority: .embedded) + let videoNode = UniversalVideoNode(accountId: context.account.id, postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: VideoDecoration(), content: videoContent, priority: .embedded) videoNode.clipsToBounds = true videoNode.isUserInteractionEnabled = false videoNode.isHidden = true diff --git a/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift b/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift index fcc1047125..9634edd0a8 100644 --- a/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift +++ b/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift @@ -464,7 +464,7 @@ public final class ChatImportActivityScreen: ViewController { 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) - let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded) + let videoNode = UniversalVideoNode(accountId: context.account.id, postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded) videoNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 2.0, height: 2.0)) videoNode.alpha = 0.01 self.videoNode = videoNode diff --git a/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift b/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift index ae5a1f2e6e..c1c8f5d686 100644 --- a/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift @@ -330,6 +330,7 @@ public class DrawingStickerEntityView: DrawingEntityView { private func setupWithVideo(_ file: TelegramMediaFile) { let videoNode = UniversalVideoNode( + accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: self.context.sharedContext.mediaManager.audioSession, manager: self.context.sharedContext.mediaManager.universalVideoManager, diff --git a/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift b/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift index bee6431d99..b98b543865 100644 --- a/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift +++ b/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift @@ -9,6 +9,7 @@ import UniversalMediaPlayer import TelegramPresentationData import RangeSet import ShimmerEffect +import TelegramUniversalVideoContent private let textFont = Font.with(size: 13.0, design: .regular, weight: .regular, traits: [.monospacedNumbers]) diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index c5e76739a1..1448c0f424 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -578,6 +578,7 @@ public class GalleryController: ViewController, StandalonePresentableController, private let landscape: Bool private let timecode: Double? private var playbackRate: Double? + private var videoQuality: UniversalVideoContentVideoQuality = .auto private let accountInUseDisposable = MetaDisposable() private let disposable = MetaDisposable() @@ -1757,6 +1758,16 @@ public class GalleryController: ViewController, StandalonePresentableController, } } + func updateSharedVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) { + self.videoQuality = videoQuality + + self.galleryNode.pager.forEachItemNode { itemNode in + if let itemNode = itemNode as? UniversalVideoGalleryItemNode { + itemNode.updateVideoQuality(videoQuality) + } + } + } + public var keyShortcuts: [KeyShortcut] { var keyShortcuts: [KeyShortcut] = [] keyShortcuts.append( diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index fd7be6047b..9f361c67be 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -769,6 +769,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private var moreBarButtonRate: Double = 1.0 private var moreBarButtonRateTimestamp: Double? + private let settingsBarButton: MoreHeaderButton + private var videoNode: UniversalVideoNode? private var videoNodeUserInteractionEnabled: Bool = false private var videoFramePreview: FramePreview? @@ -798,6 +800,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private var item: UniversalVideoGalleryItem? private var playbackRate: Double? + private var videoQuality: UniversalVideoContentVideoQuality = .auto private let playbackRatePromise = ValuePromise() private let statusDisposable = MetaDisposable() @@ -849,11 +852,15 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.moreBarButton.isUserInteractionEnabled = true self.moreBarButton.setContent(.more(optionsCircleImage(dark: false))) + self.settingsBarButton = MoreHeaderButton() + self.settingsBarButton.isUserInteractionEnabled = true + super.init() self.clipsToBounds = true self.moreBarButton.addTarget(self, action: #selector(self.moreButtonPressed), forControlEvents: .touchUpInside) + self.settingsBarButton.addTarget(self, action: #selector(self.settingsButtonPressed), forControlEvents: .touchUpInside) self.footerContentNode.interacting = { [weak self] value in self?.isInteractingPromise.set(value) @@ -966,7 +973,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } self.moreBarButton.contextAction = { [weak self] sourceNode, gesture in - self?.openMoreMenu(sourceNode: sourceNode, gesture: gesture) + guard let self else { + return + } + self.openMoreMenu(sourceNode: self.moreBarButton.referenceNode, gesture: gesture, isSettings: false) } self.titleContentView = GalleryTitleView(frame: CGRect()) @@ -1106,6 +1116,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { var forceEnableUserInteraction = false var isAnimated = false var isEnhancedWebPlayer = false + var isAdaptive = false if let content = item.content as? NativeVideoContent { isAnimated = content.fileReference.media.isAnimated self.videoFramePreview = MediaPlayerFramePreview(postbox: item.context.account.postbox, userLocation: content.userLocation, userContentType: .video, fileReference: content.fileReference) @@ -1129,6 +1140,14 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } else if let _ = item.content as? PlatformVideoContent { disablePlayerControls = true forceEnablePiP = true + } else if let _ = item.content as? HLSVideoContent { + isAdaptive = true + } + + if isAdaptive { + self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQAuto"), color: .white))) + } else { + self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettings"), color: .white))) } let dimensions = item.content.dimensions @@ -1149,7 +1168,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { let mediaManager = item.context.sharedContext.mediaManager - let videoNode = UniversalVideoNode(postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .gallery) + let videoNode = UniversalVideoNode(accountId: item.context.account.id, postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .gallery) let videoScale: CGFloat if item.content is WebEmbedVideoContent { videoScale = 1.0 @@ -1250,7 +1269,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } let status = messageMediaFileStatus(context: item.context, messageId: message.id, file: file) if !isWebpage { - scrubberView.setFetchStatusSignal(status, strings: self.presentationData.strings, decimalSeparator: self.presentationData.dateTimeFormat.decimalSeparator, fileSize: file.size) + if !NativeVideoContent.isHLSVideo(file: file) { + scrubberView.setFetchStatusSignal(status, strings: self.presentationData.strings, decimalSeparator: self.presentationData.dateTimeFormat.decimalSeparator, fileSize: file.size) + } } self.requiresDownload = !isMediaStreamable(message: message, media: file) @@ -1443,6 +1464,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { rightBarButtonItem.accessibilityLabel = self.presentationData.strings.Gallery_VoiceOver_Stickers barButtonItems.append(rightBarButtonItem) } + if forceEnablePiP || (!isAnimated && !disablePlayerControls && !disablePictureInPicture) { let rightBarButtonItem = UIBarButtonItem(image: pictureInPictureButtonImage, style: .plain, target: self, action: #selector(self.pictureInPictureButtonPressed)) rightBarButtonItem.accessibilityLabel = self.presentationData.strings.Gallery_VoiceOver_PictureInPicture @@ -1487,6 +1509,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { hasMoreButton = true } + if !isAnimated && !disablePlayerControls { + let settingsMenuItem = UIBarButtonItem(customDisplayNode: self.settingsBarButton)! + settingsMenuItem.accessibilityLabel = self.presentationData.strings.Settings_Title + barButtonItems.append(settingsMenuItem) + } + if hasMoreButton { let moreMenuItem = UIBarButtonItem(customDisplayNode: self.moreBarButton)! moreMenuItem.accessibilityLabel = self.presentationData.strings.Common_More @@ -2169,7 +2197,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { let baseNavigationController = self.baseNavigationController() let mediaManager = self.context.sharedContext.mediaManager var expandImpl: (() -> Void)? - let overlayNode = OverlayUniversalVideoNode(postbox: self.context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, content: item.content, expand: { + let overlayNode = OverlayUniversalVideoNode(accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, content: item.content, expand: { expandImpl?() }, close: { [weak mediaManager] in mediaManager?.setOverlayVideoNode(nil) @@ -2271,7 +2299,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if #available(iOSApplicationExtension 15.0, iOS 15.0, *), AVPictureInPictureController.isPictureInPictureSupported(), isNativePictureInPictureSupported { self.disablePictureInPicturePlaceholder = true - let overlayVideoNode = UniversalVideoNode(postbox: self.context.account.postbox, audioSession: self.context.sharedContext.mediaManager.audioSession, manager: self.context.sharedContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .overlay) + let overlayVideoNode = UniversalVideoNode(accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: self.context.sharedContext.mediaManager.audioSession, manager: self.context.sharedContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .overlay) let absoluteRect = videoNode.view.convert(videoNode.view.bounds, to: nil) overlayVideoNode.frame = absoluteRect overlayVideoNode.updateLayout(size: absoluteRect.size, transition: .immediate) @@ -2354,7 +2382,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { shouldBeDismissed = .single(false) } - let overlayNode = OverlayUniversalVideoNode(postbox: self.context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, content: item.content, shouldBeDismissed: shouldBeDismissed, expand: { + let overlayNode = OverlayUniversalVideoNode(accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, content: item.content, shouldBeDismissed: shouldBeDismissed, expand: { expandImpl?() }, close: { [weak mediaManager] in mediaManager?.setOverlayVideoNode(nil) @@ -2501,7 +2529,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.moreBarButton.contextAction?(self.moreBarButton.containerNode, nil) } - private func openMoreMenu(sourceNode: ASDisplayNode, gesture: ContextGesture?) { + private func openMoreMenu(sourceNode: ContextReferenceContentNode, gesture: ContextGesture?, isSettings: Bool) { guard let controller = self.baseNavigationController()?.topViewController as? ViewController else { return } @@ -2510,12 +2538,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if case let .message(message, _) = self.item?.contentInfo, let _ = message.adAttribute { items = self.adMenuMainItems() } else { - items = self.contextMenuMainItems(dismiss: { + items = self.contextMenuMainItems(isSettings: isSettings, dismiss: { dismissImpl?() }) } - let contextController = ContextController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.moreBarButton.referenceNode)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + let contextController = ContextController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) self.isShowingContextMenuPromise.set(true) controller.presentInGlobalOverlay(contextController) dismissImpl = { [weak contextController] in @@ -2666,7 +2694,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } - private func contextMenuMainItems(dismiss: @escaping () -> Void) -> Signal<[ContextMenuItem], NoError> { + private func contextMenuMainItems(isSettings: Bool, dismiss: @escaping () -> Void) -> Signal<[ContextMenuItem], NoError> { guard let videoNode = self.videoNode, let item = self.item else { return .single([]) } @@ -2687,172 +2715,172 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { var items: [ContextMenuItem] = [] - var speedValue: String = strongSelf.presentationData.strings.PlaybackSpeed_Normal - var speedIconText: String = "1x" - var didSetSpeedValue = false - for (text, iconText, speed) in strongSelf.speedList(strings: strongSelf.presentationData.strings) { - if abs(speed - status.baseRate) < 0.01 { - speedValue = text - speedIconText = iconText - didSetSpeedValue = true - break - } - } - if !didSetSpeedValue && status.baseRate != 1.0 { - speedValue = String(format: "%.1fx", status.baseRate) - speedIconText = speedValue - } - - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PlaybackSpeed_Title, textLayout: .secondLineWithValue(speedValue), icon: { theme in - return optionsRateImage(rate: speedIconText, isLarge: false, color: theme.contextMenu.primaryColor) - }, action: { c, _ in - guard let strongSelf = self else { - c?.dismiss(completion: nil) - return - } - - c?.setItems(strongSelf.contextMenuSpeedItems(dismiss: dismiss) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) - }))) - - 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" + if isSettings { + var speedValue: String = strongSelf.presentationData.strings.PlaybackSpeed_Normal + var speedIconText: String = "1x" + var didSetSpeedValue = false + for (text, iconText, speed) in strongSelf.speedList(strings: strongSelf.presentationData.strings) { + if abs(speed - status.baseRate) < 0.01 { + speedValue = text + speedIconText = iconText + didSetSpeedValue = true + break } - case let .quality(value): - qualityText = "\(value)p" + } + if !didSetSpeedValue && status.baseRate != 1.0 { + speedValue = String(format: "%.1fx", status.baseRate) + speedIconText = speedValue } - items.append(.action(ContextMenuActionItem(text: "Video Quality", textLayout: .secondLineWithValue(qualityText), icon: { _ in - return nil + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PlaybackSpeed_Title, textLayout: .secondLineWithValue(speedValue), icon: { theme in + return optionsRateImage(rate: speedIconText, isLarge: false, color: theme.contextMenu.primaryColor) }, 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) + + c?.pushItems(items: strongSelf.contextMenuSpeedItems(dismiss: dismiss) |> map { ContextController.Items(content: .list($0)) }) }))) - 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 - guard let strongSelf = self, let peer = peer else { - return + if let videoQualityState = strongSelf.videoNode?.videoQualityState(), !videoQualityState.available.isEmpty { + items.append(.separator) + + //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" } - if let navigationController = strongSelf.baseNavigationController() { - strongSelf.beginCustomDismiss(true) + + 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 + } - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false))) - - Queue.mainQueue().after(0.3) { - strongSelf.completeCustomDismiss() + c?.pushItems(items: .single(ContextController.Items(content: .list(strongSelf.contextMenuVideoQualityItems(dismiss: dismiss))))) + }))) + } + } else { + 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 + guard let strongSelf = self, let peer = peer else { + return + } + if let navigationController = strongSelf.baseNavigationController() { + strongSelf.beginCustomDismiss(true) + + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false))) + + Queue.mainQueue().after(0.3) { + strongSelf.completeCustomDismiss() + } + } + f(.default) + }))) + } + + // if #available(iOS 11.0, *) { + // items.append(.action(ContextMenuActionItem(text: "AirPlay", textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/AirPlay"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + // f(.default) + // guard let strongSelf = self else { + // return + // } + // strongSelf.beginAirPlaySetup() + // }))) + // } + + if let (message, _, _) = strongSelf.contentInfo() { + for media in message.media { + if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { + let url = content.url + + let item = OpenInItem.url(url: url) + let openText = strongSelf.presentationData.strings.Conversation_FileOpenIn + items.append(.action(ContextMenuActionItem(text: openText, textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) }, action: { _, f in + f(.default) + + if let strongSelf = self, let controller = strongSelf.galleryController() { + var presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + if !presentationData.theme.overallDarkAppearance { + presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) + } + let actionSheet = OpenInActionSheetController(context: strongSelf.context, forceTheme: presentationData.theme, item: item, openUrl: { [weak self] url in + if let strongSelf = self { + strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: strongSelf.baseNavigationController(), dismissInput: {}) + } + }) + controller.present(actionSheet, in: .window(.root)) + } + }))) + break } } - f(.default) - }))) - } - -// if #available(iOS 11.0, *) { -// items.append(.action(ContextMenuActionItem(text: "AirPlay", textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/AirPlay"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in -// f(.default) -// guard let strongSelf = self else { -// return -// } -// strongSelf.beginAirPlaySetup() -// }))) -// } - - if let (message, _, _) = strongSelf.contentInfo() { - for media in message.media { - if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { - let url = content.url - - let item = OpenInItem.url(url: url) - let openText = strongSelf.presentationData.strings.Conversation_FileOpenIn - items.append(.action(ContextMenuActionItem(text: openText, textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) }, action: { _, f in - f(.default) - - if let strongSelf = self, let controller = strongSelf.galleryController() { - var presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - if !presentationData.theme.overallDarkAppearance { - presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) - } - let actionSheet = OpenInActionSheetController(context: strongSelf.context, forceTheme: presentationData.theme, item: item, openUrl: { [weak self] url in - if let strongSelf = self { - strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: strongSelf.baseNavigationController(), dismissInput: {}) - } - }) - controller.present(actionSheet, in: .window(.root)) - } - }))) - break - } } - } - - if let (message, maybeFile, _) = strongSelf.contentInfo(), let file = maybeFile, !message.isCopyProtected() && !item.peerIsCopyProtected && message.paidContent == nil { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Gallery_SaveVideo, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in - f(.default) - - if let strongSelf = self { - switch strongSelf.fetchStatus { - case .Local: - let _ = (SaveToCameraRoll.saveToCameraRoll(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: file)) - |> deliverOnMainQueue).start(completed: { - guard let strongSelf = self else { - return - } + + if let (message, maybeFile, _) = strongSelf.contentInfo(), let file = maybeFile, !message.isCopyProtected() && !item.peerIsCopyProtected && message.paidContent == nil { + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Gallery_SaveVideo, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in + f(.default) + + if let strongSelf = self { + switch strongSelf.fetchStatus { + case .Local: + let _ = (SaveToCameraRoll.saveToCameraRoll(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: file)) + |> deliverOnMainQueue).start(completed: { + guard let strongSelf = self else { + return + } + guard let controller = strongSelf.galleryController() else { + return + } + controller.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .mediaSaved(text: strongSelf.presentationData.strings.Gallery_VideoSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + }) + default: guard let controller = strongSelf.galleryController() else { return } - controller.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .mediaSaved(text: strongSelf.presentationData.strings.Gallery_VideoSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) - }) - default: - guard let controller = strongSelf.galleryController() else { - return + controller.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Gallery_WaitForVideoDownoad, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { + })]), in: .window(.root)) } - controller.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Gallery_WaitForVideoDownoad, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { - })]), in: .window(.root)) } - } - }))) - } - - if let peer, let (message, _, _) = strongSelf.contentInfo(), canSendMessagesToPeer(peer._asPeer()) { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_ContextMenuReply, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reply"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in - if let self, let navigationController = self.baseNavigationController() { - self.beginCustomDismiss(true) - - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: true))) - - Queue.mainQueue().after(0.3) { - self.completeCustomDismiss() + }))) + } + + if let peer, let (message, _, _) = strongSelf.contentInfo(), canSendMessagesToPeer(peer._asPeer()) { + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_ContextMenuReply, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reply"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in + if let self, let navigationController = self.baseNavigationController() { + self.beginCustomDismiss(true) + + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: true))) + + Queue.mainQueue().after(0.3) { + self.completeCustomDismiss() + } } - } - f(.default) - }))) - } - - if strongSelf.canDelete() { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { _, f in - f(.default) - - if let strongSelf = self { - strongSelf.footerContentNode.deleteButtonPressed() - } - }))) + f(.default) + }))) + } + + if strongSelf.canDelete() { + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { _, f in + f(.default) + + if let strongSelf = self { + strongSelf.footerContentNode.deleteButtonPressed() + } + }))) + } } return items @@ -2877,11 +2905,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Back, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) }, iconPosition: .left, action: { c, _ in - guard let strongSelf = self else { - c?.dismiss(completion: nil) - return - } - c?.setItems(strongSelf.contextMenuMainItems(dismiss: dismiss) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) + c?.popItems() }))) let sliderValuePromise = ValuePromise(nil) @@ -2938,12 +2962,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { 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) + }, iconPosition: .left, action: { c, _ in + c?.popItems() }))) do { @@ -2967,6 +2987,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { return } videoNode.setVideoQuality(.auto) + self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQAuto"), color: .white))) /*if let controller = strongSelf.galleryController() as? GalleryController { controller.updateSharedPlaybackRate(rate) @@ -2990,6 +3011,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { return } videoNode.setVideoQuality(.quality(quality)) + if quality >= 700 { + self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQHD"), color: .white))) + } else { + self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQSD"), color: .white))) + } /*if let controller = strongSelf.galleryController() as? GalleryController { controller.updateSharedPlaybackRate(rate) @@ -3082,6 +3108,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { }) } + @objc private func settingsButtonPressed() { + self.openMoreMenu(sourceNode: self.settingsBarButton.referenceNode, gesture: nil, isSettings: true) + } + override func adjustForPreviewing() { super.adjustForPreviewing() @@ -3102,6 +3132,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.playbackRatePromise.set(self.playbackRate ?? 1.0) } + func updateVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) { + self.videoQuality = videoQuality + + self.videoNode?.setVideoQuality(videoQuality) + } + public func seekToStart() { self.videoNode?.seek(0.0) self.videoNode?.play() diff --git a/submodules/InstantPageUI/Sources/InstantPagePlayableVideoNode.swift b/submodules/InstantPageUI/Sources/InstantPagePlayableVideoNode.swift index df36f4c416..cdbc22dc5f 100644 --- a/submodules/InstantPageUI/Sources/InstantPagePlayableVideoNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPagePlayableVideoNode.swift @@ -58,7 +58,7 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler fileValue = file } - self.videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: NativeVideoContent(id: .instantPage(webPage.webpageId, media.media.id!), userLocation: userLocation, fileReference: .webPage(webPage: WebpageReference(webPage), media: fileValue!), imageReference: imageReference, streamVideo: streamVideo ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, placeholderColor: theme.pageBackgroundColor, storeAfterDownload: nil), priority: .embedded, autoplay: true) + self.videoNode = UniversalVideoNode(accountId: context.account.id, postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: NativeVideoContent(id: .instantPage(webPage.webpageId, media.media.id!), userLocation: userLocation, fileReference: .webPage(webPage: WebpageReference(webPage), media: fileValue!), imageReference: imageReference, streamVideo: streamVideo ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, placeholderColor: theme.pageBackgroundColor, storeAfterDownload: nil), priority: .embedded, autoplay: true) self.videoNode.isUserInteractionEnabled = false self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.6)) diff --git a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift index 5d15ce66df..759746fba5 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift @@ -281,7 +281,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { 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, 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) + let videoNode = UniversalVideoNode(accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .overlay) videoNode.isUserInteractionEnabled = false videoNode.isHidden = true self.videoStartTimestamp = video.representation.startTimestamp diff --git a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift index b9f3a37afc..dfb059861b 100644 --- a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift +++ b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift @@ -366,7 +366,7 @@ public final class PeerInfoAvatarListItemNode: ASDisplayNode { } let mediaManager = self.context.sharedContext.mediaManager - let videoNode = UniversalVideoNode(postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay) + let videoNode = UniversalVideoNode(accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay) videoNode.isUserInteractionEnabled = false videoNode.canAttachContent = true videoNode.isHidden = true diff --git a/submodules/Postbox/Sources/MediaBox.swift b/submodules/Postbox/Sources/MediaBox.swift index 06c4fbe654..ac98ad54e3 100644 --- a/submodules/Postbox/Sources/MediaBox.swift +++ b/submodules/Postbox/Sources/MediaBox.swift @@ -140,8 +140,8 @@ public final class MediaBox { private let statusQueue = Queue() private let concurrentQueue = Queue.concurrentDefaultQueue() - private let dataQueue = Queue(name: "MediaBox-Data") - private let dataFileManager: MediaBoxFileManager + public let dataQueue = Queue(name: "MediaBox-Data") + public let dataFileManager: MediaBoxFileManager private let cacheQueue = Queue() private let timeBasedCleanup: TimeBasedCleanup @@ -209,60 +209,6 @@ public final class MediaBox { self.dataFileManager = MediaBoxFileManager(queue: self.dataQueue) let _ = self.ensureDirectoryCreated - - //self.updateResourceIndex() - - /*#if DEBUG - self.dataQueue.async { - for _ in 0 ..< 5 { - let tempFile = TempBox.shared.tempFile(fileName: "file") - print("MediaBox test: file \(tempFile.path)") - let queue2 = Queue.concurrentDefaultQueue() - if let fileContext = MediaBoxFileContextV2Impl(queue: self.dataQueue, manager: self.dataFileManager, storageBox: self.storageBox, resourceId: tempFile.path.data(using: .utf8)!, path: tempFile.path + "_complete", partialPath: tempFile.path + "_partial", metaPath: tempFile.path + "_partial" + ".meta") { - let _ = fileContext.fetched( - range: 0 ..< Int64.max, - priority: .default, - fetch: { ranges in - return ranges - |> filter { !$0.isEmpty } - |> take(1) - |> castError(MediaResourceDataFetchError.self) - |> mapToSignal { _ in - return Signal { subscriber in - queue2.async { - subscriber.putNext(.resourceSizeUpdated(524288)) - } - queue2.async { - subscriber.putNext(.resourceSizeUpdated(393216)) - } - queue2.async { - subscriber.putNext(.resourceSizeUpdated(655360)) - } - queue2.async { - subscriber.putNext(.resourceSizeUpdated(169608)) - } - queue2.async { - subscriber.putNext(.dataPart(resourceOffset: 131072, data: Data(repeating: 0xbb, count: 38536), range: 0 ..< 38536, complete: true)) - } - queue2.async { - subscriber.putNext(.dataPart(resourceOffset: 0, data: Data(repeating: 0xaa, count: 131072), range: 0 ..< 131072, complete: false)) - } - - return EmptyDisposable - } - } - }, - error: { _ in - }, - completed: { - assert(try! Data(contentsOf: URL(fileURLWithPath: tempFile.path + "_complete")) == Data(repeating: 0xaa, count: 131072) + Data(repeating: 0xbb, count: 38536)) - let _ = fileContext.addReference() - } - ) - } - } - } - #endif*/ } public func setMaxStoreTimes(general: Int32, shortLived: Int32, gigabytesLimit: Int32) { @@ -641,21 +587,12 @@ public final class MediaBox { paths.partial + ".meta" ]) - #if true if let fileContext = MediaBoxFileContextV2Impl(queue: self.dataQueue, manager: self.dataFileManager, storageBox: self.storageBox, resourceId: id.stringRepresentation.data(using: .utf8)!, path: paths.complete, partialPath: paths.partial, metaPath: paths.partial + ".meta") { context = fileContext self.fileContexts[resourceId] = fileContext } else { return nil } - #else - if let fileContext = MediaBoxFileContextImpl(queue: self.dataQueue, manager: self.dataFileManager, storageBox: self.storageBox, resourceId: id.stringRepresentation.data(using: .utf8)!, path: paths.complete, partialPath: paths.partial, metaPath: paths.partial + ".meta") { - context = fileContext - self.fileContexts[resourceId] = fileContext - } else { - return nil - } - #endif } if let context = context { let index = context.addReference() diff --git a/submodules/Postbox/Sources/MediaBoxFileContextV2Impl.swift b/submodules/Postbox/Sources/MediaBoxFileContextV2Impl.swift index 6bc400325a..3d1e70b889 100644 --- a/submodules/Postbox/Sources/MediaBoxFileContextV2Impl.swift +++ b/submodules/Postbox/Sources/MediaBoxFileContextV2Impl.swift @@ -2,7 +2,7 @@ import Foundation import RangeSet import SwiftSignalKit -final class MediaBoxFileContextV2Impl: MediaBoxFileContext { +public final class MediaBoxFileContextV2Impl: MediaBoxFileContext { private final class RangeRequest { let value: Range let priority: MediaBoxFetchPriority @@ -99,7 +99,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext { private final class PartialState { private let queue: Queue private let manager: MediaBoxFileManager - private let storageBox: StorageBox + private let storageBox: StorageBox? private let resourceId: Data private let partialPath: String private let fullPath: String @@ -124,7 +124,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext { init( queue: Queue, manager: MediaBoxFileManager, - storageBox: StorageBox, + storageBox: StorageBox?, resourceId: Data, partialPath: String, fullPath: String, @@ -461,7 +461,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext { self.fileMap.fill(range) self.fileMap.serialize(manager: self.manager, to: self.metaPath) - self.storageBox.update(id: self.resourceId, size: self.fileMap.sum) + self.storageBox?.update(id: self.resourceId, size: self.fileMap.sum) } else { postboxLog("MediaBoxFileContextV2Impl: error seeking file to \(resourceOffset) at \(self.partialPath)") } @@ -474,7 +474,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext { private func processMovedFile() { if let size = fileSize(self.fullPath) { self.isComplete = true - self.storageBox.update(id: self.resourceId, size: size) + self.storageBox?.update(id: self.resourceId, size: size) } } @@ -623,7 +623,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext { private let queue: Queue private let manager: MediaBoxFileManager - private let storageBox: StorageBox + private let storageBox: StorageBox? private let resourceId: Data private let path: String private let partialPath: String @@ -637,10 +637,10 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext { return self.references.isEmpty } - init?( + public init?( queue: Queue, manager: MediaBoxFileManager, - storageBox: StorageBox, + storageBox: StorageBox?, resourceId: Data, path: String, partialPath: String, @@ -683,7 +683,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext { } } - func data(range: Range, waitUntilAfterInitialFetch: Bool, next: @escaping (MediaResourceData) -> Void) -> Disposable { + public func data(range: Range, waitUntilAfterInitialFetch: Bool, next: @escaping (MediaResourceData) -> Void) -> Disposable { assert(self.queue.isCurrent()) if let size = fileSize(self.path) { @@ -708,7 +708,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext { } } - func fetched( + public func fetched( range: Range, priority: MediaBoxFetchPriority, fetch: @escaping (Signal<[(Range, MediaBoxFetchPriority)], NoError>) -> Signal, @@ -734,7 +734,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext { } } - func fetchedFullRange( + public func fetchedFullRange( fetch: @escaping (Signal<[(Range, MediaBoxFetchPriority)], NoError>) -> Signal, error: @escaping (MediaResourceDataFetchError) -> Void, completed: @escaping () -> Void @@ -758,7 +758,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext { } } - func cancelFullRangeFetches() { + public func cancelFullRangeFetches() { assert(self.queue.isCurrent()) if let partialState = self.partialState { @@ -766,7 +766,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext { } } - func rangeStatus(next: @escaping (RangeSet) -> Void, completed: @escaping () -> Void) -> Disposable { + public func rangeStatus(next: @escaping (RangeSet) -> Void, completed: @escaping () -> Void) -> Disposable { assert(self.queue.isCurrent()) if let size = fileSize(self.path) { @@ -781,7 +781,7 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext { } } - func status(next: @escaping (MediaResourceStatus) -> Void, completed: @escaping () -> Void, size: Int64?) -> Disposable { + public func status(next: @escaping (MediaResourceStatus) -> Void, completed: @escaping () -> Void, size: Int64?) -> Disposable { assert(self.queue.isCurrent()) if let _ = fileSize(self.path) { diff --git a/submodules/Postbox/Sources/MediaBoxFileManager.swift b/submodules/Postbox/Sources/MediaBoxFileManager.swift index bc963b8e4c..22799bc0a5 100644 --- a/submodules/Postbox/Sources/MediaBoxFileManager.swift +++ b/submodules/Postbox/Sources/MediaBoxFileManager.swift @@ -2,13 +2,13 @@ import Foundation import SwiftSignalKit import ManagedFile -final class MediaBoxFileManager { - enum Mode { +public final class MediaBoxFileManager { + public enum Mode { case read case readwrite } - enum AccessError: Error { + public enum AccessError: Error { case generic } @@ -129,7 +129,7 @@ final class MediaBoxFileManager { private var nextItemId: Int = 0 private let maxOpenFiles: Int - init(queue: Queue?) { + public init(queue: Queue?) { self.queue = queue self.maxOpenFiles = 16 } diff --git a/submodules/PremiumUI/Sources/PhoneDemoComponent.swift b/submodules/PremiumUI/Sources/PhoneDemoComponent.swift index 34b2ff7a8e..c3bb78f7ee 100644 --- a/submodules/PremiumUI/Sources/PhoneDemoComponent.swift +++ b/submodules/PremiumUI/Sources/PhoneDemoComponent.swift @@ -233,7 +233,7 @@ private final class PhoneView: UIView { hintDimensions: CGSize(width: 1170, height: 1754), storeAfterDownload: nil ) - let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: VideoDecoration(), content: videoContent, priority: .embedded) + let videoNode = UniversalVideoNode(accountId: context.account.id, postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: VideoDecoration(), content: videoContent, priority: .embedded) videoNode.canAttachContent = true self.videoNode = videoNode diff --git a/submodules/ShareController/Sources/ShareLoadingContainerNode.swift b/submodules/ShareController/Sources/ShareLoadingContainerNode.swift index ebf18f121b..3b5bbc6448 100644 --- a/submodules/ShareController/Sources/ShareLoadingContainerNode.swift +++ b/submodules/ShareController/Sources/ShareLoadingContainerNode.swift @@ -283,7 +283,7 @@ public final class ShareProlongedLoadingContainerNode: ASDisplayNode, ShareConte 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) - let videoNode = UniversalVideoNode(postbox: postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded) + let videoNode = UniversalVideoNode(accountId: AccountRecordId(rawValue: 0), postbox: postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded) videoNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 2.0, height: 2.0)) videoNode.alpha = 0.01 self.videoNode = videoNode diff --git a/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift b/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift index de7135864c..527a5a3074 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift @@ -634,7 +634,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager { } } - private func requestScheduleGroupCall(accountContext: AccountContext, peerId: PeerId, internalId: CallSessionInternalId = CallSessionInternalId()) -> Signal { + private func requestScheduleGroupCall(accountContext: AccountContext, peerId: PeerId, internalId: CallSessionInternalId = CallSessionInternalId(), parentController: ViewController) -> Signal { let (presentationData, present, openSettings) = self.getDeviceAccessData() let isVideo = false @@ -668,7 +668,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager { accountContext.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) ) |> deliverOnMainQueue - |> mapToSignal { [weak self] accessEnabled, peer -> Signal in + |> mapToSignal { [weak self, weak parentController] accessEnabled, peer -> Signal in guard let strongSelf = self else { return .single(false) } @@ -681,46 +681,98 @@ public final class PresentationCallManagerImpl: PresentationCallManager { if let peer = peer, case let .channel(channel) = peer, case .broadcast = channel.info { isChannel = true } - - let call = PresentationGroupCallImpl( - accountContext: accountContext, - audioSession: strongSelf.audioSession, - callKitIntegration: nil, - getDeviceAccessData: strongSelf.getDeviceAccessData, - initialCall: nil, - internalId: internalId, - peerId: peerId, - isChannel: isChannel, - invite: nil, - joinAsPeerId: nil, - isStream: false - ) - strongSelf.updateCurrentGroupCall(call) - strongSelf.currentGroupCallPromise.set(.single(call)) - strongSelf.hasActiveGroupCallsPromise.set(true) - strongSelf.removeCurrentGroupCallDisposable.set((call.canBeRemoved - |> filter { $0 } - |> take(1) - |> deliverOnMainQueue).start(next: { [weak call] value in - guard let strongSelf = self, let call = call else { - return + + if shouldUseV2VideoChatImpl(context: accountContext) { + if let parentController { + parentController.push(ScheduleVideoChatSheetScreen( + context: accountContext, + scheduleAction: { timestamp in + guard let self else { + return + } + + let call = PresentationGroupCallImpl( + accountContext: accountContext, + audioSession: self.audioSession, + callKitIntegration: nil, + getDeviceAccessData: self.getDeviceAccessData, + initialCall: nil, + internalId: internalId, + peerId: peerId, + isChannel: isChannel, + invite: nil, + joinAsPeerId: nil, + isStream: false + ) + call.schedule(timestamp: timestamp) + + self.updateCurrentGroupCall(call) + self.currentGroupCallPromise.set(.single(call)) + self.hasActiveGroupCallsPromise.set(true) + self.removeCurrentGroupCallDisposable.set((call.canBeRemoved + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self, weak call] value in + guard let self, let call else { + return + } + if value { + if self.currentGroupCall === call { + self.updateCurrentGroupCall(nil) + self.currentGroupCallPromise.set(.single(nil)) + self.hasActiveGroupCallsPromise.set(false) + } + } + })) + } + )) } - if value { - if strongSelf.currentGroupCall === call { - strongSelf.updateCurrentGroupCall(nil) - strongSelf.currentGroupCallPromise.set(.single(nil)) - strongSelf.hasActiveGroupCallsPromise.set(false) + + return .single(true) + } else { + let call = PresentationGroupCallImpl( + accountContext: accountContext, + audioSession: strongSelf.audioSession, + callKitIntegration: nil, + getDeviceAccessData: strongSelf.getDeviceAccessData, + initialCall: nil, + internalId: internalId, + peerId: peerId, + isChannel: isChannel, + invite: nil, + joinAsPeerId: nil, + isStream: false + ) + strongSelf.updateCurrentGroupCall(call) + strongSelf.currentGroupCallPromise.set(.single(call)) + strongSelf.hasActiveGroupCallsPromise.set(true) + strongSelf.removeCurrentGroupCallDisposable.set((call.canBeRemoved + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak call] value in + guard let strongSelf = self, let call = call else { + return } - } - })) + if value { + if strongSelf.currentGroupCall === call { + strongSelf.updateCurrentGroupCall(nil) + strongSelf.currentGroupCallPromise.set(.single(nil)) + strongSelf.hasActiveGroupCallsPromise.set(false) + } + } + })) + } return .single(true) } } - public func scheduleGroupCall(context: AccountContext, peerId: PeerId, endCurrentIfAny: Bool) -> RequestScheduleGroupCallResult { - let begin: () -> Void = { [weak self] in - let _ = self?.requestScheduleGroupCall(accountContext: context, peerId: peerId).start() + public func scheduleGroupCall(context: AccountContext, peerId: PeerId, endCurrentIfAny: Bool, parentController: ViewController) -> RequestScheduleGroupCallResult { + let begin: () -> Void = { [weak self, weak parentController] in + guard let parentController else { + return + } + let _ = self?.requestScheduleGroupCall(accountContext: context, peerId: peerId, parentController: parentController).start() } if let currentGroupCall = self.currentGroupCallValue { diff --git a/submodules/TelegramCallsUI/Sources/ScheduleVideoChatSheetScreen.swift b/submodules/TelegramCallsUI/Sources/ScheduleVideoChatSheetScreen.swift new file mode 100644 index 0000000000..08b7019d97 --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/ScheduleVideoChatSheetScreen.swift @@ -0,0 +1,466 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import ViewControllerComponent +import AccountContext +import SheetComponent +import ButtonComponent +import TelegramCore +import AnimatedTextComponent +import MultilineTextComponent +import BalancedTextComponent +import TelegramPresentationData +import TelegramStringFormatting +import Markdown + +private final class ScheduleVideoChatSheetContentComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let scheduleAction: (Int32) -> Void + let dismiss: () -> Void + + init( + scheduleAction: @escaping (Int32) -> Void, + dismiss: @escaping () -> Void + ) { + self.scheduleAction = scheduleAction + self.dismiss = dismiss + } + + static func ==(lhs: ScheduleVideoChatSheetContentComponent, rhs: ScheduleVideoChatSheetContentComponent) -> Bool { + return true + } + + final class View: UIView { + private let button = ComponentView() + private let cancelButton = ComponentView() + + private let title = ComponentView() + private let mainText = ComponentView() + private var pickerView: UIDatePicker? + + private let calendar = Calendar(identifier: .gregorian) + private let dateFormatter: DateFormatter + + private var component: ScheduleVideoChatSheetContentComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.dateFormatter = DateFormatter() + self.dateFormatter.timeStyle = .none + self.dateFormatter.dateStyle = .short + self.dateFormatter.timeZone = TimeZone.current + + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + @objc private func scheduleDatePickerUpdated() { + self.state?.updated(transition: .immediate) + } + + private func updateSchedulePickerLimits() { + let timeZone = TimeZone(secondsFromGMT: 0)! + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = timeZone + let currentDate = Date() + var components = calendar.dateComponents(Set([.era, .year, .month, .day, .hour, .minute, .second]), from: currentDate) + components.second = 0 + + let roundedDate = calendar.date(from: components)! + let next1MinDate = calendar.date(byAdding: .minute, value: 1, to: roundedDate) + + let minute = components.minute ?? 0 + components.minute = 0 + let roundedToHourDate = calendar.date(from: components)! + components.hour = 0 + + let roundedToMidnightDate = calendar.date(from: components)! + let nextTwoHourDate = calendar.date(byAdding: .hour, value: minute > 30 ? 4 : 3, to: roundedToHourDate) + let maxDate = calendar.date(byAdding: .day, value: 8, to: roundedToMidnightDate) + + if let date = calendar.date(byAdding: .day, value: 365, to: currentDate) { + self.pickerView?.maximumDate = date + } + if let next1MinDate = next1MinDate, let nextTwoHourDate = nextTwoHourDate { + self.pickerView?.minimumDate = next1MinDate + self.pickerView?.maximumDate = maxDate + self.pickerView?.date = nextTwoHourDate + } + } + + func update(component: ScheduleVideoChatSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let previousComponent = self.component + let _ = previousComponent + + self.component = component + self.state = state + + let environment = environment[EnvironmentType.self].value + + let sideInset: CGFloat = 16.0 + + var contentHeight: CGFloat = 0.0 + contentHeight += 16.0 + + //TODO:localize + let titleString = NSMutableAttributedString() + titleString.append(NSAttributedString(string: "Schedule Video Chat", font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(titleString), + maximumNumberOfLines: 1 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentHeight), size: titleSize)) + } + contentHeight += titleSize.height + contentHeight += 16.0 + + let pickerView: UIDatePicker + if let current = self.pickerView { + pickerView = current + } else { + let textColor = UIColor.white + UILabel.setDateLabel(textColor) + + pickerView = UIDatePicker() + pickerView.timeZone = TimeZone(secondsFromGMT: 0) + pickerView.datePickerMode = .countDownTimer + pickerView.datePickerMode = .dateAndTime + pickerView.locale = Locale.current + pickerView.timeZone = TimeZone.current + pickerView.minuteInterval = 1 + self.addSubview(pickerView) + pickerView.addTarget(self, action: #selector(self.scheduleDatePickerUpdated), for: .valueChanged) + if #available(iOS 13.4, *) { + pickerView.preferredDatePickerStyle = .wheels + } + pickerView.setValue(textColor, forKey: "textColor") + self.pickerView = pickerView + self.addSubview(pickerView) + + self.updateSchedulePickerLimits() + } + + let pickerFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: 216.0)) + transition.setFrame(view: pickerView, frame: pickerFrame) + contentHeight += pickerFrame.height + contentHeight += 26.0 + + let date = pickerView.date + let calendar = Calendar(identifier: .gregorian) + let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + let timestamp = Int32(date.timeIntervalSince1970) + let time = stringForMessageTimestamp(timestamp: timestamp, dateTimeFormat: PresentationDateTimeFormat()) + let buttonTitle: String + if calendar.isDateInToday(date) { + buttonTitle = environment.strings.ScheduleVoiceChat_ScheduleToday(time).string + } else if calendar.isDateInTomorrow(date) { + buttonTitle = environment.strings.ScheduleVoiceChat_ScheduleTomorrow(time).string + } else { + buttonTitle = environment.strings.ScheduleVoiceChat_ScheduleOn(self.dateFormatter.string(from: date), time).string + } + + let delta = timestamp - currentTimestamp + + let isGroup = "".isEmpty + let intervalString = scheduledTimeIntervalString(strings: environment.strings, value: max(60, delta)) + + let text: String = isGroup ? environment.strings.ScheduleVoiceChat_GroupText(intervalString).string : environment.strings.ScheduleLiveStream_ChannelText(intervalString).string + + let mainText = NSMutableAttributedString() + mainText.append(parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes( + body: MarkdownAttributeSet( + font: Font.regular(14.0), + textColor: UIColor(rgb: 0x8e8e93) + ), + bold: MarkdownAttributeSet( + font: Font.semibold(14.0), + textColor: UIColor(rgb: 0x8e8e93) + ), + link: MarkdownAttributeSet( + font: Font.regular(14.0), + textColor: environment.theme.list.itemAccentColor, + additionalAttributes: [:] + ), + linkAttribute: { attributes in + return ("URL", "") + } + ))) + + let mainTextSize = self.mainText.update( + transition: .immediate, + component: AnyComponent(BalancedTextComponent( + text: .plain(mainText), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + if let mainTextView = self.mainText.view { + if mainTextView.superview == nil { + self.addSubview(mainTextView) + } + transition.setFrame(view: mainTextView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - mainTextSize.width) * 0.5), y: contentHeight), size: mainTextSize)) + } + contentHeight += mainTextSize.height + contentHeight += 10.0 + + var buttonContents: [AnyComponentWithIdentity] = [] + buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( + Text(text: buttonTitle, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor) + ))) + let buttonTransition = transition + let buttonSize = self.button.update( + transition: buttonTransition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: UIColor(rgb: 0x3252EF), + foreground: .white, + pressedColor: UIColor(rgb: 0x3252EF).withMultipliedAlpha(0.8) + ), + content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( + HStack(buttonContents, spacing: 5.0) + )), + isEnabled: true, + tintWhenDisabled: false, + displaysProgress: false, + action: { [weak self] in + guard let self, let component = self.component, let pickerView = self.pickerView else { + return + } + component.scheduleAction(Int32(pickerView.date.timeIntervalSince1970)) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + ) + let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: buttonSize) + if let buttonView = self.button.view { + if buttonView.superview == nil { + self.addSubview(buttonView) + } + transition.setFrame(view: buttonView, frame: buttonFrame) + } + contentHeight += buttonSize.height + contentHeight += 10.0 + + let cancelButtonSize = self.cancelButton.update( + transition: buttonTransition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: UIColor(rgb: 0x2B2B2F), + foreground: .white, + pressedColor: UIColor(rgb: 0x2B2B2F).withMultipliedAlpha(0.8) + ), + content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( + Text(text: "Cancel", font: Font.semibold(17.0), color: environment.theme.list.itemPrimaryTextColor) + )), + isEnabled: true, + tintWhenDisabled: false, + displaysProgress: false, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.dismiss() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + ) + let cancelButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: cancelButtonSize) + if let cancelButtonView = self.cancelButton.view { + if cancelButtonView.superview == nil { + self.addSubview(cancelButtonView) + } + transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame) + } + contentHeight += cancelButtonSize.height + + if environment.safeInsets.bottom.isZero { + contentHeight += 16.0 + } else { + contentHeight += environment.safeInsets.bottom + 14.0 + } + + return CGSize(width: availableSize.width, height: contentHeight) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class ScheduleVideoChatSheetScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let scheduleAction: (Int32) -> Void + + init( + context: AccountContext, + scheduleAction: @escaping (Int32) -> Void + ) { + self.context = context + self.scheduleAction = scheduleAction + } + + static func ==(lhs: ScheduleVideoChatSheetScreenComponent, rhs: ScheduleVideoChatSheetScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + final class View: UIView { + private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, SheetComponentEnvironment)>() + private let sheetAnimateOut = ActionSlot>() + + private var component: ScheduleVideoChatSheetScreenComponent? + private var environment: EnvironmentType? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: ScheduleVideoChatSheetScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + self.environment = environment + + let sheetEnvironment = SheetComponentEnvironment( + isDisplaying: environment.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { [weak self] _ in + guard let self, let environment = self.environment else { + return + } + self.sheetAnimateOut.invoke(Action { _ in + if let controller = environment.controller() { + controller.dismiss(completion: nil) + } + }) + } + ) + let _ = self.sheet.update( + transition: transition, + component: AnyComponent(SheetComponent( + content: AnyComponent(ScheduleVideoChatSheetContentComponent( + scheduleAction: { [weak self] timestamp in + guard let self else { + return + } + self.sheetAnimateOut.invoke(Action { [weak self] _ in + guard let self, let component = self.component else { + return + } + if let controller = self.environment?.controller() { + controller.dismiss(completion: nil) + } + + component.scheduleAction(timestamp) + }) + }, + dismiss: { [weak self] in + guard let self else { + return + } + self.sheetAnimateOut.invoke(Action { [weak self] _ in + guard let self else { + return + } + if let controller = self.environment?.controller() { + controller.dismiss(completion: nil) + } + }) + } + )), + backgroundColor: .color(UIColor(rgb: 0x1C1C1E)), + animateOut: self.sheetAnimateOut + )), + environment: { + environment + sheetEnvironment + }, + containerSize: availableSize + ) + if let sheetView = self.sheet.view { + if sheetView.superview == nil { + self.addSubview(sheetView) + } + transition.setFrame(view: sheetView, frame: CGRect(origin: CGPoint(), size: availableSize)) + } + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public class ScheduleVideoChatSheetScreen: ViewControllerComponentContainer { + public init(context: AccountContext, scheduleAction: @escaping (Int32) -> Void) { + super.init(context: context, component: ScheduleVideoChatSheetScreenComponent( + context: context, + scheduleAction: scheduleAction + ), navigationBarAppearance: .none, theme: .dark) + + self.statusBar.statusBarStyle = .Ignore + self.navigationPresentation = .flatModal + self.blocksBackgroundWhenInOverlay = true + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.view.disablesInteractiveModalDismiss = true + } +} diff --git a/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift index 13c69b2cf2..8d6b3f69bd 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift @@ -66,6 +66,7 @@ final class VideoChatActionButtonComponent: Component { case muted case unmuted case raiseHand + case scheduled } let strings: PresentationStrings @@ -156,7 +157,7 @@ final class VideoChatActionButtonComponent: Component { backgroundColor = !isActive ? UIColor(rgb: 0x002E5D) : UIColor(rgb: 0x027FFF) case .unmuted: backgroundColor = !isActive ? UIColor(rgb: 0x124B21) : UIColor(rgb: 0x34C659) - case .raiseHand: + case .raiseHand, .scheduled: backgroundColor = UIColor(rgb: 0x3252EF) } iconDiameter = 60.0 @@ -169,7 +170,7 @@ final class VideoChatActionButtonComponent: Component { backgroundColor = !isActive ? UIColor(rgb: 0x002E5D) : UIColor(rgb: 0x027FFF) case .unmuted: backgroundColor = !isActive ? UIColor(rgb: 0x124B21) : UIColor(rgb: 0x34C659) - case .raiseHand: + case .raiseHand, .scheduled: backgroundColor = UIColor(rgb: 0x3252EF) } iconDiameter = 60.0 diff --git a/submodules/TelegramCallsUI/Sources/VideoChatMicButtonComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatMicButtonComponent.swift index 9e7c87b5bb..8ecf340e20 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatMicButtonComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatMicButtonComponent.swift @@ -175,11 +175,17 @@ private final class GlowView: UIView { } final class VideoChatMicButtonComponent: Component { + enum ScheduledState: Equatable { + case start + case toggleSubscription(isSubscribed: Bool) + } + enum Content: Equatable { case connecting case muted case unmuted(pushToTalk: Bool) case raiseHand + case scheduled(state: ScheduledState) } let call: PresentationGroupCall @@ -187,19 +193,22 @@ final class VideoChatMicButtonComponent: Component { let isCollapsed: Bool let updateUnmutedStateIsPushToTalk: (Bool?) -> Void let raiseHand: () -> Void + let scheduleAction: () -> Void init( call: PresentationGroupCall, content: Content, isCollapsed: Bool, updateUnmutedStateIsPushToTalk: @escaping (Bool?) -> Void, - raiseHand: @escaping () -> Void + raiseHand: @escaping () -> Void, + scheduleAction: @escaping () -> Void ) { self.call = call self.content = content self.isCollapsed = isCollapsed self.updateUnmutedStateIsPushToTalk = updateUnmutedStateIsPushToTalk self.raiseHand = raiseHand + self.scheduleAction = scheduleAction } static func ==(lhs: VideoChatMicButtonComponent, rhs: VideoChatMicButtonComponent) -> Bool { @@ -245,7 +254,7 @@ final class VideoChatMicButtonComponent: Component { self.beginTrackingTimestamp = CFAbsoluteTimeGetCurrent() if let component = self.component { switch component.content { - case .connecting, .unmuted, .raiseHand: + case .connecting, .unmuted, .raiseHand, .scheduled: self.beginTrackingWasPushToTalk = false case .muted: self.beginTrackingWasPushToTalk = true @@ -291,6 +300,8 @@ final class VideoChatMicButtonComponent: Component { self.icon.playRandomAnimation() component.raiseHand() + case .scheduled: + component.scheduleAction() } } } @@ -322,6 +333,17 @@ final class VideoChatMicButtonComponent: Component { titleText = isPushToTalk ? "You are Live" : "Tap to Mute" case .raiseHand: titleText = "Raise Hand" + case let .scheduled(state): + switch state { + case .start: + titleText = "Start Now" + case let .toggleSubscription(isSubscribed): + if isSubscribed { + titleText = "Clear Reminder" + } else { + titleText = "Set Reminder" + } + } } self.isEnabled = isEnabled @@ -390,12 +412,14 @@ final class VideoChatMicButtonComponent: Component { case .connecting: context.setFillColor(UIColor(white: 0.1, alpha: 1.0).cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) - case .muted, .unmuted, .raiseHand: + case .muted, .unmuted, .raiseHand, .scheduled: let colors: [UIColor] if case .muted = component.content { colors = [UIColor(rgb: 0x0080FF), UIColor(rgb: 0x00A1FE)] } else if case .raiseHand = component.content { colors = [UIColor(rgb: 0x3252EF), UIColor(rgb: 0xC64688)] + } else if case .scheduled = component.content { + colors = [UIColor(rgb: 0x3252EF), UIColor(rgb: 0xC64688)] } else { colors = [UIColor(rgb: 0x33C659), UIColor(rgb: 0x0BA8A5)] } @@ -477,10 +501,21 @@ final class VideoChatMicButtonComponent: Component { self.icon.enqueueState(.unmute) case .raiseHand: self.icon.enqueueState(.hand) + case let .scheduled(state): + switch state { + case .start: + self.icon.enqueueState(.start) + case let .toggleSubscription(isSubscribed): + if isSubscribed { + self.icon.enqueueState(.unsubscribe) + } else { + self.icon.enqueueState(.subscribe) + } + } } switch component.content { - case .muted, .unmuted, .raiseHand: + case .muted, .unmuted, .raiseHand, .scheduled: let blobSize = CGRect(origin: CGPoint(), size: CGSize(width: 116.0, height: 116.0)).insetBy(dx: -40.0, dy: -40.0).size let blobTintTransition: ComponentTransition @@ -512,6 +547,8 @@ final class VideoChatMicButtonComponent: Component { blobsColor = UIColor(rgb: 0x0086FF) } else if case .raiseHand = component.content { blobsColor = UIColor(rgb: 0x914BAD) + } else if case .scheduled = component.content { + blobsColor = UIColor(rgb: 0x914BAD) } else { blobsColor = UIColor(rgb: 0x33C758) } @@ -528,7 +565,7 @@ final class VideoChatMicButtonComponent: Component { blobView.updateLevel(CGFloat(value), immediately: false) }) } - case .connecting, .muted, .raiseHand: + case .connecting, .muted, .raiseHand, .scheduled: if let audioLevelDisposable = self.audioLevelDisposable { self.audioLevelDisposable = nil audioLevelDisposable.dispose() @@ -561,6 +598,8 @@ final class VideoChatMicButtonComponent: Component { glowColor = UIColor(rgb: 0x0086FF) } else if case .raiseHand = component.content { glowColor = UIColor(rgb: 0x3252EF) + } else if case .scheduled = component.content { + glowColor = UIColor(rgb: 0x3252EF) } else { glowColor = UIColor(rgb: 0x33C758) } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScheduledInfoComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatScheduledInfoComponent.swift new file mode 100644 index 0000000000..3c97c76306 --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/VideoChatScheduledInfoComponent.swift @@ -0,0 +1,213 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import MultilineTextComponent +import TelegramPresentationData +import TelegramStringFormatting +import HierarchyTrackingLayer + +private let purple = UIColor(rgb: 0x3252ef) +private let pink = UIColor(rgb: 0xef436c) + +private let latePurple = UIColor(rgb: 0x974aa9) +private let latePink = UIColor(rgb: 0xf0436c) + +final class VideoChatScheduledInfoComponent: Component { + let timestamp: Int32 + let strings: PresentationStrings + + init( + timestamp: Int32, + strings: PresentationStrings + ) { + self.timestamp = timestamp + self.strings = strings + } + + static func ==(lhs: VideoChatScheduledInfoComponent, rhs: VideoChatScheduledInfoComponent) -> Bool { + if lhs.timestamp != rhs.timestamp { + return false + } + if lhs.strings !== rhs.strings { + return false + } + return true + } + + final class View: UIView { + private let title = ComponentView() + private let countdownText = ComponentView() + private let dateText = ComponentView() + + private let countdownContainerView: UIView + private let countdownMaskView: UIView + private let countdownGradientLayer: SimpleGradientLayer + private let hierarchyTrackingLayer: HierarchyTrackingLayer + + private var component: VideoChatScheduledInfoComponent? + private var isUpdating: Bool = false + + override init(frame: CGRect) { + self.countdownContainerView = UIView() + self.countdownMaskView = UIView() + + self.countdownGradientLayer = SimpleGradientLayer() + self.countdownGradientLayer.type = .radial + self.countdownGradientLayer.colors = [pink.cgColor, purple.cgColor, purple.cgColor] + self.countdownGradientLayer.locations = [0.0, 0.85, 1.0] + self.countdownGradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0) + self.countdownGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0) + + self.hierarchyTrackingLayer = HierarchyTrackingLayer() + + super.init(frame: frame) + + self.layer.addSublayer(self.hierarchyTrackingLayer) + + self.countdownContainerView.layer.addSublayer(self.countdownGradientLayer) + self.addSubview(self.countdownContainerView) + + self.countdownContainerView.mask = self.countdownMaskView + + self.hierarchyTrackingLayer.isInHierarchyUpdated = { [weak self] value in + guard let self else { + return + } + if value { + self.updateAnimations() + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func updateAnimations() { + if let _ = self.countdownGradientLayer.animation(forKey: "movement") { + } else { + let previousValue = self.countdownGradientLayer.startPoint + let newValue = CGPoint(x: CGFloat.random(in: 0.65 ..< 0.85), y: CGFloat.random(in: 0.1 ..< 0.45)) + self.countdownGradientLayer.startPoint = newValue + + CATransaction.begin() + + let animation = CABasicAnimation(keyPath: "startPoint") + animation.duration = Double.random(in: 0.8 ..< 1.4) + animation.fromValue = previousValue + animation.toValue = newValue + + CATransaction.setCompletionBlock { [weak self] in + guard let self else { + return + } + if self.hierarchyTrackingLayer.isInHierarchy { + self.updateAnimations() + } + } + + self.countdownGradientLayer.add(animation, forKey: "movement") + CATransaction.commit() + } + } + + func update(component: VideoChatScheduledInfoComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + self.component = component + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "Starts in", font: Font.with(size: 23.0, design: .round, weight: .semibold), textColor: .white)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 200.0) + ) + + let remainingSeconds: Int32 = max(0, component.timestamp - Int32(Date().timeIntervalSince1970)) + let countdownText: String + if remainingSeconds >= 86400 { + countdownText = scheduledTimeIntervalString(strings: component.strings, value: remainingSeconds) + } else { + countdownText = textForTimeout(value: abs(remainingSeconds)) + /*if remainingSeconds < 0 && !self.isLate { + self.isLate = true + self.foregroundGradientLayer.colors = [latePink.cgColor, latePurple.cgColor, latePurple.cgColor] + }*/ + } + + let countdownTextSize = self.countdownText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: countdownText, font: Font.with(size: 68.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: .white)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 400.0) + ) + + let dateText = humanReadableStringForTimestamp(strings: component.strings, dateTimeFormat: PresentationDateTimeFormat(), timestamp: component.timestamp, alwaysShowTime: true).string + + let dateTextSize = self.dateText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: dateText, font: Font.with(size: 23.0, design: .round, weight: .semibold), textColor: .white)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 400.0) + ) + + let titleSpacing: CGFloat = 5.0 + let dateSpacing: CGFloat = 5.0 + + let contentHeight: CGFloat = titleSize.height + titleSpacing + countdownTextSize.height + dateSpacing + dateTextSize.height + + let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: floor((availableSize.height - contentHeight) * 0.5)), size: titleSize) + let countdownTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - countdownTextSize.width) * 0.5), y: titleFrame.maxY + titleSpacing), size: countdownTextSize) + let dateTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - dateTextSize.width) * 0.5), y: countdownTextFrame.maxY + dateSpacing), size: dateTextSize) + + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setPosition(view: titleView, position: titleFrame.center) + titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + } + + if let countdownTextView = self.countdownText.view { + if countdownTextView.superview == nil { + self.countdownMaskView.addSubview(countdownTextView) + } + transition.setFrame(view: countdownTextView, frame: CGRect(origin: CGPoint(), size: countdownTextFrame.size)) + } + + transition.setFrame(view: self.countdownContainerView, frame: countdownTextFrame) + transition.setFrame(view: self.countdownMaskView, frame: CGRect(origin: CGPoint(), size: countdownTextFrame.size)) + transition.setFrame(layer: self.countdownGradientLayer, frame: CGRect(origin: CGPoint(), size: countdownTextFrame.size)) + + if let dateTextView = self.dateText.view { + if dateTextView.superview == nil { + self.addSubview(dateTextView) + } + transition.setPosition(view: dateTextView, position: dateTextFrame.center) + dateTextView.bounds = CGRect(origin: CGPoint(), size: dateTextFrame.size) + } + + self.updateAnimations() + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index a193b72312..906f913916 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -73,6 +73,7 @@ final class VideoChatScreenComponent: Component { let microphoneButton = ComponentView() let participants = ComponentView() + var scheduleInfo: ComponentView? var reconnectedAsEventsDisposable: Disposable? @@ -561,6 +562,13 @@ final class VideoChatScreenComponent: Component { self.isUpdating = false } + let alphaTransition: ComponentTransition + if transition.animation.isImmediate { + alphaTransition = .immediate + } else { + alphaTransition = .easeInOut(duration: 0.25) + } + let environment = environment[ViewControllerComponentContainer.Environment.self].value let themeUpdated = self.environment?.theme !== environment.theme @@ -1058,10 +1066,16 @@ final class VideoChatScreenComponent: Component { } let idleTitleStatusText: String - if let callState = self.callState, callState.networkState == .connected, let members = self.members { - idleTitleStatusText = environment.strings.VoiceChat_Panel_Members(Int32(max(1, members.totalCount))) + if let callState = self.callState { + if callState.networkState == .connected, let members = self.members { + idleTitleStatusText = environment.strings.VoiceChat_Panel_Members(Int32(max(1, members.totalCount))) + } else if callState.scheduleTimestamp != nil { + idleTitleStatusText = "scheduled" + } else { + idleTitleStatusText = "connecting..." + } } else { - idleTitleStatusText = "connecting..." + idleTitleStatusText = " " } let titleSize = self.title.update( transition: transition, @@ -1324,35 +1338,88 @@ final class VideoChatScreenComponent: Component { let participantsFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: participantsSize) if let participantsView = self.participants.view { if participantsView.superview == nil { + participantsView.layer.allowsGroupOpacity = true self.containerView.addSubview(participantsView) } transition.setFrame(view: participantsView, frame: participantsFrame) + var participantsAlpha: CGFloat = 1.0 + if let callState = self.callState, callState.scheduleTimestamp != nil { + participantsAlpha = 0.0 + } + alphaTransition.setAlpha(view: participantsView, alpha: participantsAlpha) + } + + if let callState = self.callState, let scheduleTimestamp = callState.scheduleTimestamp { + let scheduleInfo: ComponentView + var scheduleInfoTransition = transition + if let current = self.scheduleInfo { + scheduleInfo = current + } else { + scheduleInfoTransition = scheduleInfoTransition.withAnimation(.none) + scheduleInfo = ComponentView() + self.scheduleInfo = scheduleInfo + } + let scheduleInfoSize = scheduleInfo.update( + transition: scheduleInfoTransition, + component: AnyComponent(VideoChatScheduledInfoComponent( + timestamp: scheduleTimestamp, + strings: environment.strings + )), + environment: {}, + containerSize: participantsSize + ) + let scheduleInfoFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: scheduleInfoSize) + if let scheduleInfoView = scheduleInfo.view { + if scheduleInfoView.superview == nil { + scheduleInfoView.isUserInteractionEnabled = false + self.containerView.addSubview(scheduleInfoView) + } + scheduleInfoTransition.setFrame(view: scheduleInfoView, frame: scheduleInfoFrame) + } + } else if let scheduleInfo = self.scheduleInfo { + self.scheduleInfo = nil + if let scheduleInfoView = scheduleInfo.view { + alphaTransition.setAlpha(view: scheduleInfoView, alpha: 0.0, completion: { [weak scheduleInfoView] _ in + scheduleInfoView?.removeFromSuperview() + }) + } } let micButtonContent: VideoChatMicButtonComponent.Content let actionButtonMicrophoneState: VideoChatActionButtonComponent.MicrophoneState if let callState = self.callState { - switch callState.networkState { - case .connecting: - micButtonContent = .connecting - actionButtonMicrophoneState = .connecting - case .connected: - if let callState = callState.muteState { - if callState.canUnmute { - if self.isPushToTalkActive { - micButtonContent = .unmuted(pushToTalk: self.isPushToTalkActive) - actionButtonMicrophoneState = .unmuted + if callState.scheduleTimestamp != nil { + let scheduledState: VideoChatMicButtonComponent.ScheduledState + if callState.canManageCall { + scheduledState = .start + } else { + scheduledState = .toggleSubscription(isSubscribed: callState.subscribedToScheduled) + } + micButtonContent = .scheduled(state: scheduledState) + actionButtonMicrophoneState = .scheduled + } else { + switch callState.networkState { + case .connecting: + micButtonContent = .connecting + actionButtonMicrophoneState = .connecting + case .connected: + if let callState = callState.muteState { + if callState.canUnmute { + if self.isPushToTalkActive { + micButtonContent = .unmuted(pushToTalk: self.isPushToTalkActive) + actionButtonMicrophoneState = .unmuted + } else { + micButtonContent = .muted + actionButtonMicrophoneState = .muted + } } else { - micButtonContent = .muted - actionButtonMicrophoneState = .muted + micButtonContent = .raiseHand + actionButtonMicrophoneState = .raiseHand } } else { - micButtonContent = .raiseHand - actionButtonMicrophoneState = .raiseHand + micButtonContent = .unmuted(pushToTalk: false) + actionButtonMicrophoneState = .unmuted } - } else { - micButtonContent = .unmuted(pushToTalk: false) - actionButtonMicrophoneState = .unmuted } } } else { @@ -1412,6 +1479,23 @@ final class VideoChatScreenComponent: Component { if !callState.raisedHand { component.call.raiseHand() } + }, + scheduleAction: { [weak self] in + guard let self, let component = self.component else { + return + } + guard let callState = self.callState else { + return + } + guard callState.scheduleTimestamp != nil else { + return + } + + if callState.canManageCall { + component.call.startScheduled() + } else { + component.call.toggleScheduledSubscription(!callState.subscribedToScheduled) + } } )), environment: {}, diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index 533e8b89ca..db52366c58 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -7097,7 +7097,7 @@ final class VoiceChatContextReferenceContentSource: ContextReferenceContentSourc } } -private func calculateUseV2(context: AccountContext) -> Bool { +public func shouldUseV2VideoChatImpl(context: AccountContext) -> Bool { var useV2 = true if context.sharedContext.immediateExperimentalUISettings.disableCallV2 { useV2 = false @@ -7109,7 +7109,7 @@ private func calculateUseV2(context: AccountContext) -> Bool { } public func makeVoiceChatControllerInitialData(sharedContext: SharedAccountContext, accountContext: AccountContext, call: PresentationGroupCall) -> Signal { - let useV2 = calculateUseV2(context: accountContext) + let useV2 = shouldUseV2VideoChatImpl(context: accountContext) if useV2 { return VideoChatScreenV2Impl.initialData(call: call) |> map { $0 as Any } @@ -7119,7 +7119,7 @@ public func makeVoiceChatControllerInitialData(sharedContext: SharedAccountConte } public func makeVoiceChatController(sharedContext: SharedAccountContext, accountContext: AccountContext, call: PresentationGroupCall, initialData: Any) -> VoiceChatController { - let useV2 = calculateUseV2(context: accountContext) + let useV2 = shouldUseV2VideoChatImpl(context: accountContext) if useV2 { return VideoChatScreenV2Impl(initialData: initialData as! VideoChatScreenV2Impl.InitialData, call: call) diff --git a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift index 113edc7caf..af95079f06 100644 --- a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift +++ b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift @@ -184,6 +184,12 @@ private func findMediaResource(media: Media, previousMedia: Media?, resource: Me return representation.resource } } + + for alternativeRepresentation in file.alternativeRepresentations { + if let result = findMediaResource(media: alternativeRepresentation, previousMedia: previousMedia, resource: resource) { + return result + } + } } } else if let webPage = media as? TelegramMediaWebpage, case let .Loaded(content) = webPage.content { if let image = content.image, let result = findMediaResource(media: image, previousMedia: previousMedia, resource: resource) { @@ -254,6 +260,12 @@ func findMediaResourceById(media: Media, resourceId: MediaResourceId) -> Telegra return representation.resource } } + + for alternativeRepresentation in file.alternativeRepresentations { + if let result = findMediaResourceById(media: alternativeRepresentation, resourceId: resourceId) { + return result + } + } } else if let webPage = media as? TelegramMediaWebpage, case let .Loaded(content) = webPage.content { if let image = content.image, let result = findMediaResourceById(media: image, resourceId: resourceId) { return result diff --git a/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift b/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift index 8ee63a28ac..2bff980564 100644 --- a/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift @@ -151,7 +151,7 @@ public final class ChatBotInfoItemNode: ListViewItemNode { continuePlayingWithoutSoundOnLostAudioSession: false, storeAfterDownload: nil ) - let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: VideoDecoration(), content: videoContent, priority: .embedded) + let videoNode = UniversalVideoNode(accountId: context.account.id, postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: VideoDecoration(), content: videoContent, priority: .embedded) videoNode.canAttachContent = true self.videoNode = videoNode diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift index cebcfdff62..7b56c5b951 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift @@ -276,7 +276,7 @@ public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { 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 - let videoNode = UniversalVideoNode(postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay) + let videoNode = UniversalVideoNode(accountId: item.context.account.id, postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay) videoNode.isUserInteractionEnabled = false videoNode.ownsContentNodeUpdated = { [weak self] owns in if let strongSelf = self { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift index 5dcd3a1f81..9bd38d928d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -763,7 +763,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { }) } let mediaManager = item.context.sharedContext.mediaManager - let videoNode = UniversalVideoNode(postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: ChatBubbleInstantVideoDecoration(inset: 2.0, backgroundImage: instantVideoBackgroundImage, tapped: { + let videoNode = UniversalVideoNode(accountId: item.context.account.id, postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: ChatBubbleInstantVideoDecoration(inset: 2.0, backgroundImage: instantVideoBackgroundImage, tapped: { if let strongSelf = self { if let item = strongSelf.item { if strongSelf.infoBackgroundNode.alpha.isZero { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index e1586b4821..d37ea5fcd8 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -1659,7 +1659,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr let loopVideo = updatedVideoFile.isAnimated let videoContent: UniversalVideoContent - if NativeVideoContent.isHLSVideo(file: updatedVideoFile), context.sharedContext.immediateExperimentalUISettings.dynamicStreaming { + if !"".isEmpty && 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 @@ -1669,7 +1669,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr 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) + let videoNode = UniversalVideoNode(accountId: context.account.id, 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 if let strongSelf = self { @@ -2162,10 +2162,15 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr if let duration = file.duration, !message.flags.contains(.Unsent) { let durationString = file.isAnimated ? gifTitle : stringForDuration(playerDuration > 0 ? playerDuration : Int32(duration), position: playerPosition) if isMediaStreamable(message: message, media: file) { - badgeContent = .mediaDownload(backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, duration: durationString, size: active ? sizeString : nil, muted: muted, active: active) - mediaDownloadState = .fetching(progress: automaticPlayback ? nil : adjustedProgress) - if self.playerStatus?.status == .playing { - mediaDownloadState = nil + if NativeVideoContent.isHLSVideo(file: file) { + mediaDownloadState = .fetching(progress: nil) + badgeContent = .text(inset: 12.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: durationString), iconName: nil) + } else { + badgeContent = .mediaDownload(backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, duration: durationString, size: active ? sizeString : nil, muted: muted, active: active) + mediaDownloadState = .fetching(progress: automaticPlayback ? nil : adjustedProgress) + if self.playerStatus?.status == .playing { + mediaDownloadState = nil + } } state = automaticPlayback ? .none : .play(messageTheme.mediaOverlayControlColors.foregroundColor) } else { @@ -2264,7 +2269,11 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr do { let durationString = file.isAnimated ? gifTitle : stringForDuration(playerDuration > 0 ? playerDuration : (file.duration.flatMap { Int32(floor($0)) } ?? 0), position: playerPosition) if wideLayout { - if isMediaStreamable(message: message, media: file), let fileSize = file.size, fileSize > 0 && fileSize != .max { + if NativeVideoContent.isHLSVideo(file: file) { + state = automaticPlayback ? .none : .play(messageTheme.mediaOverlayControlColors.foregroundColor) + mediaDownloadState = nil + badgeContent = .text(inset: 12.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: durationString), iconName: nil) + } else if isMediaStreamable(message: message, media: file), let fileSize = file.size, fileSize > 0 && fileSize != .max { state = automaticPlayback ? .none : .play(messageTheme.mediaOverlayControlColors.foregroundColor) badgeContent = .mediaDownload(backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, duration: durationString, size: dataSizeString(fileSize, formatting: formatting), muted: muted, active: true) mediaDownloadState = .remote diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageProfilePhotoSuggestionContentNode/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageProfilePhotoSuggestionContentNode/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift index 47c902badc..cbcbc37fc4 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageProfilePhotoSuggestionContentNode/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageProfilePhotoSuggestionContentNode/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift @@ -222,7 +222,7 @@ public class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleCont 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 - let videoNode = UniversalVideoNode(postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay) + let videoNode = UniversalVideoNode(accountId: item.context.account.id, postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay) videoNode.isUserInteractionEnabled = false videoNode.ownsContentNodeUpdated = { [weak self] owns in if let strongSelf = self { diff --git a/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift b/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift index 86eb90094c..9cef08d2d2 100644 --- a/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift @@ -2286,7 +2286,7 @@ private class MessageContentNode: ASDisplayNode, ContentNode { } } else { let videoContent = NativeVideoContent(id: .message(message.stableId, video.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: video), streamVideo: .conservative, loopVideo: true, enableSound: false, fetchAutomatically: false, onlyFullSizeThumbnail: self.isStatic, continuePlayingWithoutSoundOnLostAudioSession: true, placeholderColor: .clear, captureProtected: false, storeAfterDownload: nil) - let videoNode = UniversalVideoNode(postbox: self.context.account.postbox, audioSession: self.context.sharedContext.mediaManager.audioSession, manager: self.context.sharedContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .overlay, autoplay: !self.isStatic) + let videoNode = UniversalVideoNode(accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: self.context.sharedContext.mediaManager.audioSession, manager: self.context.sharedContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .overlay, autoplay: !self.isStatic) self.videoStatusDisposable.set((videoNode.status |> deliverOnMainQueue).startStrict(next: { [weak self] status in diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift index 827ec82b64..a07718b06f 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift @@ -333,7 +333,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { self.videoNode?.removeFromSupernode() let mediaManager = self.context.sharedContext.mediaManager - let videoNode = UniversalVideoNode(postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .embedded) + let videoNode = UniversalVideoNode(accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .embedded) videoNode.isUserInteractionEnabled = false videoNode.isHidden = true diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift index ea1c47a629..9d34dd106c 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift @@ -168,7 +168,7 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode { self.videoNode?.removeFromSupernode() let mediaManager = self.context.sharedContext.mediaManager - let videoNode = UniversalVideoNode(postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .gallery) + let videoNode = UniversalVideoNode(accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .gallery) videoNode.isUserInteractionEnabled = false self.videoStartTimestamp = video.representation.startTimestamp self.videoContent = videoContent diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 37a38e0da3..c253385710 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -7067,7 +7067,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } private func scheduleGroupCall() { - self.context.scheduleGroupCall(peerId: self.peerId) + guard let controller = self.controller else { + return + } + self.context.scheduleGroupCall(peerId: self.peerId, parentController: controller) } private func createExternalStream(credentialsPromise: Promise?) { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index a5b630e67b..18c6d85990 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -200,6 +200,7 @@ final class StoryItemContentComponent: Component { if case let .file(file) = currentMessageMedia, let peerReference = PeerReference(component.peer._asPeer()) { if self.videoNode == nil { let videoNode = UniversalVideoNode( + accountId: component.context.account.id, postbox: component.context.account.postbox, audioSession: component.context.sharedContext.mediaManager.audioSession, manager: component.context.sharedContext.mediaManager.universalVideoManager, diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettings.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettings.imageset/Contents.json new file mode 100644 index 0000000000..cc44172ecd --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettings.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "videosettings_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettings.imageset/videosettings_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettings.imageset/videosettings_30.pdf new file mode 100644 index 0000000000000000000000000000000000000000..028e8b9ab0560c94a99440d10e540916ef2f1022 GIT binary patch literal 5753 zcmZXYWmr^Q+lEC-dT1nMNa+}wht8oHx?zAB8gv*slu|$>hi;_1y9H@bN& zgC~66_xT?iCmG zpPX08!5Ild0C|<1p-7kl%*M?YcDv)Yc-3L9c1U}m!0qQcx9Ii+cO(J^b;0#Ud%Z9c zEht8e`8hyZu__KcIiBv02QCj%whxj?la&KYNUz}7>Hp%R@jdyNam+$XyrnQF)b-=W z^5@M(RoC?nQP<7&Qnqy9$&IDuugmXS%BHiIS7w&BF zqo=>xlfK-0JFzm?8g(_-bl$@4vJ`XjqWdO*=oyn`qwkBZ8hWYgL&HN%&_vhIz)zbG ze+q9%UcdGaXn)yNbMewL@XX@#-kKZzvzzY&1N6Tv2Og<8c`e$EHXd9A{-Ss7NDi#H z3Jg4lPqsdKZ5iZc>E{=+x1~kzVt#(oIdJiw(zj`?ec3*Jd#2Jaiqh9LySmNI^bdbDdRNqMQ3pZE4MzE*Hyw{_~kz;+IGB z?`kcQ#DiU(#VeyalA!xL3Rk^R4M%4U`)d-joO3?WjkVh*26M}GQ#Db0t$ifE&g#o` ztf!u%5nn&cX*tX^g;ab*oL5wxOZ0Di;>`+M8la%hX6$qHvlvO7=l&yeWC*Ivdn$#{ znG@wQ=8Tl-D)iX*ul97efm>RyfAbvO|DaS6xO}sE=zm-xaio$HZLxOlcNz#kH!`tP z`eBpMHk4X}^cz@K)$7{ikYv5@pIv+8ry~q1| z>wds-f&FYtZgjf4JWZ435iDgRc9VXj(;VTMx5)AA0K?_t1!maS3Ngp`%Q10vE3=Nq zcrTs@cnQzViEu`oHBW4!&a2Fvh8hA|;@_RO&vOxH22ie?F#pl>{h-lHZ_R!!HGsgs zPQ-k7k()H+%#UW%d8A>PkaE8nGQY4`dr_l7C~L^hves1K_K})1<7i=AA$&WAfvcxl zVj+%J`m28Q$NkwwVX+rn2Z9K7!s$ULI!VaGMB)gkz^p4@Y5uN!m0J8^dBYmT^Ff12 zFdgj`;8#TaVRh*Ee*4&}?aFwP)t;}L@h=+Fu_XIMdg(kPegmmA&dBbL!FVB>U}qOq z#@eh|fu?elO7@THuc7f`QY#-7SN%mN$|Q3VNO?cA{*_GP<|PheF;OHCF(wO zE36yG<|acvCLkL&kPgARGf90;8&U2%1{@Y@A)%T=LAFi%w+ym6a&l{vzRLoE9~mz9 z9tY(G%%U>uRX-N>DFk$+Vx4t&M%KpxO2;C!xSIj^7WuD9-V6C$?m=O0S zphMtGZ=cKuoTyKUIhrB00$U_xEK!%jmE|SR6Y+$5)Cg24!1eM{&WP4|5nZw5c<5DQ zMtiq5fUY$Z)oGWJ@_~6c+y*EmW1_1jN2B}g>!zylaLaL8SyaLp1Kn1pskd2*uL<7m6|wK`$Zwix23k;Z;=Aw%0XJ}2n#C? zIvRNYanDmmBm2W@d#%UKb|`s%GeVl8SegN;Jxo2mG2?g5}U~i7`kEvpH^kwegKzNtyOi5v$jV}&C@KFShn%zjQ zbhBm&#o*97doja|dF^oVd&#v7VW;O=7TC6iJeVb%4v{i0YWV7}AvC#!+c6U|#aU#j z0KyW zNhr2*8rvHXW`yjpAj10fo9`WQrx8qT*yS8S^*T;Tdp_jrx>NTKAuV zEAi#CcdHW|ly1n>Q{_phRCSK5wgNeOdiavxk%XcrFrsfu?s8sc`NKne-q{Asg6pjM zf&puJv$-%2G08#v)ooc*XR9_w_#-8=q!=BA?BdKTT#Cq;n|g+w_~Hs`RHD6ky%7mW z>1;wGevY=>c)GL&@52~43owW;eDtZ`5?y^)0zE1lr_19jF^o;p z$&6QOA!sV6hr1cgzCMH37lrU+D>U}8V!(wJQ9E7%}{V-9mEh^+iU#Xre; zu!H{WIe;6HP~9PeHvORl4t({L66zzyg^gD-LAo3H zNg}bYWRiPQIXrgMImGE7lhXDUM~(E$m15x%yvOe&uzgb)*`In&WzVRg?kvw`-cTsJ zD|U#%uRV_wPAE#tr;vz}Nz5r)7;Ea@2X8}qT%N|yUe^9d_73K@+R6qb&Fq!(?Zi@NP}Ks0j!(zNjQvGS4Bn<9jsl- zk3N0FqPqlQOqtf3B~z%}aa>1j7Y?GkR5em7rz#T?OhQy8ec8q`0Qm{h9Bx6a;Voen z`sssS)?xLp552?Sy3x<)I)`a;_Bv|S73Fq(Ot!$0Pa5B-(6|(D1(#pjw5ZB!2-aE1 zc)SkluAguDiVoq>B*--vGK_rFtLQ7E!xM&W3svQ_lxBQCzh2TTGr??TPs@93kYoOH zQRIt2!y`=;fyjPZWG%5!PHQqxoL8cNa+w`%0+V8K&!r3xf6L>dDWE&ERE{PSrE)Et zN9p`Wvj)80ncOQ_hIfe3|N|(4MizbSZwsFWP!vlAwp^SoXGrAX@ zBRpfp#SxXKF|@r!$+++1Zyj0H(eZ$iMYq#O7{J1!?H3vI2>3n>XXBniVkop=j0)H> zrO*I{Em&KQe^x%$%qsM_+6+^j+~H!G3YEy8mrj$dZ8sp_1~JM+;x6TqVa9cwg0p_yos4$eR3!+=vuz^zRwCf zua-6zB^xiiB!RO$HK+Nh@_3w}-d8s%aDkFe&=>F6KocojNhB&9&1V{OSQITeKWOsO zDpp6ikyIc+BqFYC2$m~1jbv*J%M@wZr}bK80D0!p4f&X-pOGnBGhRhe(>N+0hUx|mouXEg|s-{pY?*x=DU4QU&d*a z!3)h2+J>0|rAk=EY!cOiA@1B%$7Gu>=f-7T$7u3Jr=b83kbt2a2Q>g0_Xcp52)C=S z)<4z5oSb_YCYf#!HBBPK*l$>`wcs!(h_#RZ1(KQu(A{;Epou(U!*<`wLq5==2nR6- zkBsO_PNSRSm2AeAE83<|laC)2!F}?%%`G}q+4}*nww9G{%*D1Ogw!<2lbX8cfRz$x zYYm@bic}YfYi&Fjin|qK$2^#i*wxFvTZP9rgbNsFQk{&^9+Mq~h$`+gp32eK&Ze%j zWA8GCIB>@r84wPP?S}6>Bw}P4i=zKDwXvR@&6drGs#Rb>uzwjgF4IZYln(ta^R_My z!b1w`eQA6_IKHu5e{AcWq`a3NmW5O87~iBUt5aGKBtfPaPQ4%YzTHc)@HtAK&Og}& zvC*L#DGT=~l5@DUzG@@bM#N7tcR_LUtARP`lD^X1m^~jhlq-TIGO=yGBIk`c2_hTv z^9*&~$*iw(-M@r9#s{TmqB`}8(;i!0Yg6^}(pZcoynZjxF;(_f4(3K5hs$C7It7EZ zur4I1V-P=W3lb4O%cm+Ez~F2BO76U!Snktv@@}A6*~W-Ti`~c@E68;`C!JsU%O?VT zlHH1XG(8#t)#u{+zc3kFY1n@>&d^}^*q;29V6eoROuq6c26{3#B2#h4FUU6Y;Q8Vt z@gw2xmk&RB1tGOO)W1F%6`LX4##nC# ziIG?I4@`G7Wv^d?6Ca#D(M;4c!O!vkFflDkq_`vKJ+oAYuelh(GbBj>ImIcy`WQaS zd=^Z(f~FFTmN-(GoVpJkCh`Q9aOvy(h-DWk9Kw53{_;|_K!WX3VOYl^bpVoJQ)B+B z+ozX`gQIrClXHsa;mcyS4CIFyZR5CT=bM1<{UmwIW0M(N)}0Y9%`u? zk}tnQp{&YR#SupQ)3o6B9vw(b7f$SIWk(#REh>CiRrdxf zgs7D#>#QvZX&c#=R|FwmH(e~EAQk-8N_AWzsQNXiPmXRKM@WCbXisI(o3{A0bPE$w zD+B8G>qoB<1rSCa2qc~tJiZ6AO3RDfO-8>w0+wRr-4~iCHPfL+O_ef7R~VJNYOs(Q zXX9Jd-xIUBeDPfHsiwh#0#<%i&r2D(oOOdM(!v?vq)j4V#GsZW5KXJ7AS?Dw>~tHG zyibcb&wOHu&(8EW1*YuTDE3P7B^L26@6xh`U$*r9!*6gjPD5hWiSqq6JCGsE4yuOg zXDOu=dQ)IJ(0E-Jq1dGz59GVs{!+-#=Y_@_Q48OV0jygH9VV>%napMFAIyPSImvYCS^hx;J;hhc%7_i zMVzXu#U9&+#tXyUJ?~yBx0pMGua+mRCAbFccoIasDyO!i4-zWPmFP7y+aqJ!;P!Yo zO&}sIJ(Gq6Q&QE^Y($hM{M>r-lH4TsT#&iLrV20M zgYUkhv$6{Oa;SPIkd3%WhRt#bN?fqBbs7t>HFdqSR+5Sf7#JyPml$={3IH}pJ>6vr z>(gXlFqa}0B^S${rfEslnu~REmQooQ;{B+oIqJ+0^qS}QAb5JLjs|1a$0_yPgwnCL zmPGVi8^tAz)EP@am)-2WXZzxeVF&9;#o2`Es97?bl*=6rK`JpjUB0|M>eTte<40Ys zdd=9~Sv>b%*o(*Y!rShhSd&A`=8yG#rD9BZ^28>*@N}xIdgFdXm1~A6wCJ=|W4%K6 zDYKZFB>R}AkpCEOCuyrGgNvIa@f4?!;$>Y=eJWQlvF)0p=X+F)+}i6}u&Ta8DDJb? zl*4G3PG&e^)Mr!nl*h{4J{34;>w=K}=I?m=hJq`y8y0%_g+b{>$g81_xYAB>4Jrtk zv{PVHzN`>6m#?^CK-x>IXG5Qg`aSghZ7c)9mGC*5Nt$xYWV?xLg2Gs~Lh>xTgu|bI z0GxDU?b3O?(NbMtu`fUzxCrs1dj%WWZ<^6mjy?szwxbJp?PN_@8qPsn=2CLdVAxY&HM*yLlL(ODD*e_ z59fJ@e!Ea!H3!?!IHxdvN{a+*~^6y3@0_xxlL*U;1Jr#x9aj?0&xxm&QRj;@F;Zc#Gul?4aeB(Jj@zg*AE`U+D`lDfX+^`u@m*Y%eETI z-Ci+RwG3s0IMYKYhV*0aW2khh5Hk4N*Yn4Co-g@len2cnQ+}6n^vL0|QRI$KI-Q z&z8$X(_TkY15|uSZh5wc+@TSjPgNUK@s(w3ORC3^XcoOg%kKLz=Dowh0!k6;55d32 z_=yq~s;sPn_CQ`CowwEq4(C_O8}2DI3MbXB7^02m-Jur?2NUB#Csuu1>xUIJg45!J zP7^3%g=X)x{6Ao(_HjLDd(*3~KRsltS+Hn72lK${>a{l7DleRtKOPs)I#&*sf5GS+ zTUcYuo1vOGirROHG)~;2pPF>dB46J(w{V$3kd~L3g@`5RU_!SRT4}5f#b|JE8dc!p$WA}FZ!8| zIXMAe1IsR%)RyJ31Bvr4#yJx-B*ha6-43@59^ljmH0#xbh4Gv>y}x@5Hv%}k5^O-l zt~AABIOWAZR}Sax_RX1*~{x{ej(01&{0@XT4H3Z#~1l zDZk)Bwz^hSP^^*&PIYP)qx=lYTq+?K?cIeX$B^XS){iVC7XOj2g8yg)U6}`e=wy8Q z%BNUQ-tR!Pf{=v=c0gyVBN;ec>w_qkyIPe-OPN+~&3bVPvn#1`x|bve>%ZV@%~_8e zej%xD3;mR4c@eNQVd>ReCosb|-8#dQ3Q&kBpakI$tFS1m`e?QAtVuOLx9YUYu6?8v zA4;0eB~02bVbUXZnVkIjLnoyk2m4t4&Nv(S3ROYS5TPEV_cRw!0p0rgO@+m+1V5pJ zopcIK62SvKaq3teDFYz#Ta(;jFQg^QbMCs#sfMT-s@LBQ2r9Vu0j?Xgp)JCfhSZ1& z7E=>QRC|;8w9p-S$|d{dVW7G%SUUlcK* z9iLgG{Dj6>tbp+$k;#nheIvmix><_nVF68g<<>0#8)ZYn_QeUR(N}yds=ei(M)EMg z>@n;<+~BGMmL%KldJMsEzI1nY4AE!s?mZ?Ozb3iu~g8s$uP+^%9oVwBq zP9Te<%)1Yz?Io(oon8!2$18#hh(a|Jv^Pz3h{Ovx8wP-{;yF6}&ZFdu3tME!l%$kW zz!?#D0>3D-zt+je2 zh+gK(oIZ-DHx$Bg42A`UbPjIrLnTe<2P8{!k2l6?zu!+ZX<%Vt1tz!>z2yGUGiz6SsbeQm^L<{>B8;1e5Pq@Y)CX zl(F<`E_cAI1MPEWb*d6|<>h{W%?|3--MTO}CU&ogK*E`&Aj{qwm)D4<;sin^tWXza zAh*&n**@uo9Dx&du<~4WS4v&Qrx603i%PjQ&r_=oF(<{p8a7?HDAI0^cIW-cZQT0~!g zVWQSh09scvNArSn7EK&$^YE3~Vq^1^ieVNVq2L@QrCtULT=AwZ>|B*q$qaLEgxG2)Wn?2|ls z2euyi2;e@BtR*NqlDBkz)smi*sbyt$WSncQUyx05!^@MN?an=Y!y?ROo@TsN_egc<=gs!hVe5F@kOSYmv*&Z-8z0v4| zuOCGMs7*hW>(FGvIH&{QLpEcy>tP{>O>cA6+*#dJs;g0wUJC|YY@E*Y772dceJWW4 zCkh%*JJk=zHu>-22RmAG%H?UwBH+?t_YQ_4>mEHG@6SE7;kJiZNVtNy>`Y~AiDsUT zgfE@aaEWRP&jHK|gp7x6?uPb0%)=ZMmoKV_t9mS~_EmnvHL}sY>1Vmc%iwd?-i?#` zNK4GuqhFsb3qirGtIytBVV;oRaaC}|z`@iypqtN&lUp74lY27asS!`}<*Bt+HY+Yh zTn*VEV39=^79)R}$T#&!y*<~`* zvY#QAW|E+|@M zDZJZ)P}!z@6-sMUTA6qE3BR224HDN4Xc8q#CeDo%F-^P+!+HWIDchI^h)@v|XAC}M zxfDj^#t}Z>`HtD)i7>kZ&7=!fV5w_0(V4_K*Ky|}E(k0Qh^Nmi$1l*zGt^Fu1{Fz> z9xA*Kra#zW9vPDWtnru?g+Rf zcKAZ%s(^uL^97?X$z~8Ko5D-la&d$T1@GfD4GxktKWoS7vI!OjjObWt9tVBG>PYG#gA<16z4Yds=BIA@cwG>B`Wu9On& z$*OPucMAI(rL5?P1}O#v_Zj>%AMX%#z1Hsxhph%s=ak_YHc1&1g`J!|O7k2zek&`s zSYuS2+&r3T(f=09o-dDR+`}+jyd%M>z4q-Xx8S(aSv+tXG|c^Gx@Kz&DeBAo-g1-6 zNX*yTPi|1@MY|%e{n_C~iSg|ILC6OOEk6gP*em$UYAx)8s25hosA}Tw_LM>aN^-Yn z2$GzeIn@w2J3s1^b;)|rWng2fBl_YDrKd(I1s0J2MoV>nBIB;t37~ei7JipH=3bLN)n)8< z1}-#DCJ=Zh2H zM_wq2$~UAuiEcNe4CP)&*r{EvY^F4vF0ya8HUs-fhL+VktOaDcn;SE^qSM8lGBG{x z=+S_j-}vZmM()-SZ1#j8sd3*M6_k?bf@xw+90vZTbhjNpF>i5I>^SAN)qhc#4s3#o4=bXKC zfFBNn_xD$sv4}h zh_6LkGq^@TMn+2W?mGPfbR|QI>K14r3$zu6I6^jQRGzMVRP54y?R6yprd$2vAtf%4 zlZD&8FsqJ;a}b)F)`vg>y%xha#vf`?Ohg~ z^#N5;*7W^|v&aN3qNC=!46y;@Pa=%`}v_NWc%{lYNQ0;g9I3 zl2J|Vp*;D*76mJ@Foy!nwYamU1ha*Q)OMR>Y~KUw`W#|*k(FR%?K$o7w#ssXtiXmB zlg@)CFKdp0cj11*8AIp@&T1q+vnMEDlEe;3LDV>dMVm+DrrtX4;2^f~`jGV*D0=^e zaG^K}!Td)HnO`Sb*ExZonZExz6`MY<9L_ntxdv6qOohtI$OZ^yB@Pc$yY0XHh!GXS z;1h2@eF<8tjH9T&KA-GHeA8D7l=(h!=fm*DFnsSEcGoUXl zb=s0@(!RfNG&g><|7(|KKWsA(DKr03F@`HQe=H8ISV`v9=HC-$$|9?mE)+GLJbU&= z4R=uQ7b-zL_s+wNvB&A zDkJj%NBYGlFwZK3j6EJr<1?rf zqu3$K!%G{Hr^uEw%eIb8sT%P?BHPmK%!0+RoT~X5wSc*p!@!?kDh_Yy(BF9HA5L@& ze*WS|!Xl!=zwwdqZ}|o*@yolqy1_jFCT4#_qh~LlLje5RR`&l;Bn^nIqtz2PZ-5Cv zfKTAh`0qjJxAfoBKb61`4>wPk4a5U*3$ktr)Gd@!aC3!2TyHwwQmlVOZ7bM~1`zpM z`XAPHEB$6*{A!N2H=_b>9e@G<^igp0yvY^(fAf3%#k~I6hUmX4-5T`x-SBp>zlZ50 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQHD.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQHD.imageset/Contents.json new file mode 100644 index 0000000000..1f7bfc2bb2 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQHD.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "videosettingshd_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQHD.imageset/videosettingshd_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQHD.imageset/videosettingshd_30.pdf new file mode 100644 index 0000000000000000000000000000000000000000..735db423a85e5782578d6a4063aeb6b1b112f554 GIT binary patch literal 5183 zcmZXYXEbq#4tk$gV7>H^b&;VEkp~^iQc;?A&5>A zF?!Sp676H}oxR`p{q7&@c-FO^yIl9New{oBW!0O)w3R}Pvlg}$?Fq_NaN>ciJEKAu91_7K)Kqle_1{9diSk_Nh8wrOm6;7 zJPOK?d_!U7bhnQE;$$a={o?0QTl2+_`_6K*7n@5{br;QveIkB4gR5OxI|V0}GkzBb zdq1|=j!(kw%lUcF$z9l-`5yo3PEyXsEP4LM;S-acJ(Jz2sLp%C}{K5R!JwVJ>3w(A`SEljRV}FOz{O5NbI-}*B z%HW4@v*u#!YMnm^b!}sU{7GY(=gT*PjpjD~TBgnC`^HDs|K05D)xbqA>q0!Uybzba)okw~I<^ zp2!bkDZ1MeR{)7_%Jdr9>c z<+H2RFwC*NbW$+HtqFUW&t^s&H9-6X4^5IAF?O%wOB{uX&;{~pQVzv78=MKcMIZY< zZT4H;$Bp~#C-wK)2t5(T`g@p4qx}c5&|bdj6?G6fKBh135mDKoDQ=vTJNYGt!XWRd zKw-veWo(_?ELekL<66%r2w48c2+SbaX$F$K)9fR@8CcT)N(SuK5l_Rch75c6!`Jo0VE0}@$dz4KP6;G7c(OyQYg)a`_Tbhmk(~K?kJfECE1gWi zvut)Z?KGsjHYUxdpZ|1nvhwsv02W6E!yS_}VmVN9Q=yN-mc`8^vMW8QSbvf=W9d~b zB*biKgu=DM-8X1hh8^}zpVi#tNcP?X7scx4XqVj2OopbE^K5FfP*t_v^~oO)^8)nq zGj>#QvU1K&-oT2<(kUZ~jAB{&nBu*yy&eig`88$aH2!gys_2_k_bqO3`6VTQh*=fZ zIw3WMC9+a-2?X%Oq%yAZ5~9mvcZl!nXqUwicgdecXdFk(JgUf(+K6Yi<4SdN?FiV< z58Y+*juA<4315$ zq-~xK0SlAh8UA;^)TcGmdrqeBPeh} zK-Me_7$1x9SP=y)_*)JxIKf8(5;k~nu{Y;Q_LSbI_8FmdhRe}Zyo|I#wwcE|Q%8Xn z3XkiAovk}-Nat{Pf~G7o*V>;aty!sbP|h@RlVSG{D>CUPBL%0addWP)=igD=J`T0w zQ;j+NMYc^c#xui)noE!km(}s3FdfWp?i!Bsg#xFlGw+lR{MgMl>)`}gu1*P~R*()I z0;)_{rtWJtQs(hCf|Om>X<8Z(fH1G!*NU7NbB=W1`(IB!y6{Nlj@7HncfS?!WqOcZ zuBE{4QD?z+sH*IKWa7mZI$;S2dz6>m_%*vIoio_~b@&Qx)x8FZhR)9dOPaApZYd^7 zFUh+?*rjVHJ{JY5rzIMc!VoVK6kJWO~<=Hj-H5#?<`dmwC3D| zN-?8ajzAA}wd^PY;hZq`>L#;u4lRmG2gh-5{#oZ>fr+T%=`|YT>JL;v)RdpT&}mUg zo~gbm^>_qv#$fORn(S|aDT+favl`bRGOBB&Y|)f?%zy~Nz1LcFg8HgAhxv^^5af{5 zC=n5Zl?h!HUo5+^-5Qil`GZCJ0Xu;wRE2Hv=*D(cmYbagG*XL#2ktr;+8tX&xKPKuUanRt463DI-=`pjC3lr}9f2`q__q@QDk ze?~_pb_Kve?%l3D@<(F3RwkoTrYN0LFN#DAS6T=bts*@$1X)rZvhS|OcU=fCZcC&g1EB9yTd7woPR%y58jOpLSVQ>+7rp_7(7X>)WHSYr zZWbbRaElBJe(gG2j5Ktdn#XHUG&LN*lE0V$8w?B}`%t+A=vhL9bg`Zi7izKQ(Z)67 zz^bPJhq*Ftf-#hJ`8#}LoLhjk(Ee_+=Mlhr90f7;Rx}Rxd-REXz~7w~qGFw@Vvq-* zOXkp^hjGagTC?_1AgWSKuHT+_nF?dRxwzbBjSj`SzXpoaQi{_cNJSjxjYa^AXzlp0 zJBRX*Ay5885BO6A>4JKbBW$24zOemu{@|=o3MUWyyr4+ZH~v{ontXe=uTGQ z-kTq>h!I3H=#%EjQrPPX`id7&XG}9&VZlXtUF~o1S|%y{as1ZA%krGY88gZcf3e0I z;8RD+e(BG>;m*yxzrlO3g6pyuKB;(GDFs?p)W%uxo5o^5JZcLu%`6A9L@4j|z_OaJ zi&^p%M5O9b_VV{Y{UwnfA%#_XG%|Th3nWhzkSts3u*yoHHqVYWqu>TvDrSa;)SE@@ zu@9wr{ze6|mhU_;|EK}Fg%B6LgZDT)lGdJ}rk;Qj!luHjeHTHk*+m!xwR#5^=sec^ z!7LIa?W<#GBZ4l|8Id!7c41?mP|(iXpq7%*%_PXdtMD4@>AJXrT|}Q1Rvk}o8Jjm_dZH=5 z^sf8n8;`Hvw&>HGj635#CUJMa`@b+EI(whEOf&3Q5|d2bf(*c9F|IQ#sv@mSx`vTV z%ynS=_$b9m#{}i92J!9DHI!VVC|`4f^obWSpB_^n{G55{wbW4j{w5KpUUvIB%x8Qz zn^{l92!YcuYT58-rd_>|^*GFOUXDIoOe;-K-wD!u!qgKg6-PlM#s8wh(EA|~>6QJbjLwy#xHFJt&M9tPb2`5#5!6T`ANG_N*yC@1WvYz`Y z1V&{E^-ko^fQ@!gdFJf|?xmUKSf?$ZNM$Ny+6v#kZIr=CPw-iL$JRuJfPcb+r8hlG z$Zs51_VVv8j&m4v?;{Z}{fk$k+#@v&q2%~4Z)vaTYO9ks>3x*KAnNUQ9oS2Qy6YIth_2#OU@%^|afpSt#USMIcA4W2 z(F{ic@TJPL=O?;+XQVU>Q868s4r}%D1Lt3y?)sHE2_BIj&MycQak1Q$FtD-)?r7eu zq#hbr@AY_96h45ity-T>hp(N8uf7$#p+H-QgMXwIFrP16amac88cU zFAlNrJTTpi%K`SyIIU1$d}b7G%kWdx?w0(^3tos^J-CoT-m?FJj@ob?G>j_7TM=1p)RqjPFzc8h&-aR?Nm9aHCMsdP*H;~n@O`jd zvjvnHpNe@1dds0?@$f=sOZ;}`H#}pgA+`tGH~9u$R`M8zNjr7ZZm1w6Y^y2t?AT}h zh3Rr-nC%GZ`a|D$Ca%`w{Ac*+qoa1kOI|Nc#fQ6X%5d8oiRxQ0YEYyq)Qs6G{P@FB z0#TZ7Md6Uuz)TkU%+64Y#BMyG!K}CI`&==vTN#e#L9xO@*Ltq#7?tuM)li*9H#J!n zk0=t*5l4KcwQe|Yfs)nkI=x`Jv#y4pdk7Rr++!)l#Mm6w;iygkM zx2FevSTGx#>-gqP$FT}3J^H(Er|U9NOKVuye9hcEU(zQtiO-R?OE+>yQpm~q;CE_+ zhlej{bsVfkR=4PPSL?FQ4=zrb0Ub**S-GVbKTe)MRrY7-6H4Fh+c_&Yw_yr!} zQ@G4i-<9Cx?HkE-{zjMXtaSeC&Vj+kYS-BYn;+LVubr<%sg$&KKj(*(PVdz|8GNEx zXSI*nHBM<}Za(^9@wMTk;Fg}F1zprld!n=}8aIX; zJLuhB_&c-i*2^QJMc9_Pt^B?cuw0QGX z#)`LTH~(5l)`!Juc$EAna%tAFux&kiVAIYd1$xBD`GG~1_79Apm-rMf=G&*jUqk9c z2Z8k)Rzn<{otPUvE=iUKf!MocPX?9F857&c&bt-I&WT8$qM9mJad#2qdkvNnu0bHa z+Ng0@;lL+&w#k5zs-bV7lo~m!!YslPyf(Jnut6UE)Kz@2=i-mG-S+t_*7G;4`AvPU zFwS2zNDL+{_74ma`$xWnL_!J}jI+BN$lUU81Z3##f(8j8kaoYBkPaGUk5qJi1TqIf zZ$bZze-EPnNdKOGSJFqjIeTDj&~BhBEOo_`t`L)wGsYc_x$Jnwq<)JCB=%AR!Ty&1 zgIrxne|fkXR?8lBIV$MNfj;PWA0=mx%Ut3AH^19o1nc)U#Q&?(l|i?E8eR?dmvJSe zJJQkF?!QwXVbQigC`bbMm;I|JNE`+eg@G>Rze5-b`tt5dr%hDHzu7$gUzOO)>JRvKv#q*0MZL8KAsW)M(H zVm^HGecx~WIM+V+b=KZ%t@GoY>*ml=QR5d9kN^UOz!0#Dl>-nAmXQI=$pMilo8PxU z;s4}@bYT+S=XFn#p3DFkL7}bvQ6{(K+Mn8>DtFj z-}0z zujgu?ICHq5FUH%SFwL2H%T9HgzbLVAPJQjQA6WWGP-OI-;Yk;x^Fnv+^b|zRre6|}cTq1SWryAz2cUP`<~&OUc2RMFjAoy!;c0v7=9WN-9GML3hd`$* zvx_s-bfut@wd?L|{Q6Filhxzc)uvdq11|rjXYAISPQS&|>bFI`Mr&B3Po=o;n4LEE zI?^N6AImQd`ir-9o~*VE5PIana7?A4ljXzuM-3VeYr;IS`|u*3Gv2Xd)~CWUd)@Wm z;jX{puv=P>(RH0#g`2tc!?UcD)y#I;(aIkdY(?fm6$`>73xNa}cWDu^u6U5heL6r*K$EoKY74C9+}jzaf{ z6jtD&AStgx1*rueBS1zJ`uxv}2ax}8uz zqN6tY!D3Vk7;)1tKXds&RMA7nJjQ)T#g*X_PhyATrPiEZAjtGk)~ZT12@daw%hqtR zFpNH#_H3@?$U|(3`_06kJ6A*6W#iAfpHi8|b< z)yyPBZ7BpJVUZqm6nu|8&-r?Pr6&$|oKYBP+|}irav6ovj7+C^avFy2EtYk{k=fEg zHmR|eCP~Dk&bzo$_n2<8a!v9K#FNJj%G9?XQ>t~FKhQOQUP(~4C-p&q-B)2%86a%h z;AWMYmB|oOFS!Z^csXa$ZgS!xYZICAE$+i=lJR>KR^v3!qGo;Siluf^=#B1Xxj%Uq zbW{?4@TxgFz5-!Zb)Ead zDVr!wpPr03D{dx$Yx2aPLj$r6ALK@)YSt6!O+v2OO-zybkzK7V=!Kr3{B?(N8YHg@ zzG7@Fx zM|~HXkiJAI!1vyX!bXnp+GOvnxq9OYp83i+ygjwMiV%AEp><+o6&iV6RVS#zm>f2u zG?LXEC&7lPnSRoionyBt6Ushe^t>GL~8 zS-zR32hmCKxzRKTzQ>MkvFiw9m^-(oD!6YA#xV8s39n7ccW6KXT@CwVeL8BwNFT#+ zZEXIZp4?ljU->g4I>|{FWb>s#J)g`05kAE)+g6tKB~%Hp%t=4;+H|_PxP^5a^`(I; zHz4(V($e9#<%ygsyA5!YM0v|h9eMObaV#Dwo)oQCulY`=RWL*7Q$=@UddO%L+Z@az zO(pw(Do+l|%&?5!<@}~cD9CEe#0i1D<&#wV4*aa70S`;TFJTY$r+7CI%+ec`H*L^B zO!<%@9axbftBnV}XQ}iNcJh`_m zoVs-5zDb!Jt74V&`r60@p~$V!PnXsa@$pqvpH)wd*&9ga5>b%2f{;6oQcI~ni==zm z( zTJ>kG#L01m#5OquoMT-Me!^)K$-0H82{60W4`g$=$n#&GdARNM{J#in%uDb-;C{-& z^`z>!syJq+E2rb-@9WJQvIgVta^Y${6nZJZx~czygx__bAKoiRVsiYAp)@)qlx{dU z)J9WW%j*kqajSv`2PkukTZgb?Jxli}x86=Kg+piXp{|~-4fU@Ig*K)=izA*2hg?8? zUvio+1IS9&N`Y)52t3Ad#i7t(&iMwa;4mtFGC0Mvd>HURgE%YlL7#eLHW04yS%zyZ zi6gNo9yqDO6#p5G|8*k2u#QCcy?O+9$`);;stHFTn?6CYLVr9xMM4QMLi&ul9Lhfq z)ckft!4`;h>}*5^uotes`DkjdxrbN@9vx(Oo#Od^vEecCRWghW^!av2OR)`@##%p8 z$PLHJ8BQ#Y$tKMQ)W=)nbD6;S2$)eVyAkyK%j+)n!pc(j^l)>c@#%r(xD|BX(~Sx~ zv|rX<4l!wbk4PS5^E2${V}&6F@2l~#5dpSnp z-cRti>Af#Bd#U5M0cPHIKuW>(Ti-qcYCd0q>)MOzVv?w?Uej!$WEkGe7~WFP=gRQH zV#Maldtz_ZLJy@4p8#+taD0pk8{N&o?n7aF*! zjwJ1Vo-j3QO*SB!?_LL&oVE&~b;LzWz&22 zdm>OWgZhd|cMXnfuu=NCm>;4u&$`ki<|6!?QK&8u&#^rP{f34qmzxeNqT*dF>2~U)`lU#Owfu zcT5>$b0^O&5_#Prt;N)QBj`t&(A90L`?Gvmg@OhIujo6~?+tB9hGacec_VT1SptJM znQA_0Fq0a&^!1qYb$*Eug&-m&n>R94+EwrV<^+Fe4t(W+ryGE{RcOFs#B|yao!`zo z@2}uBP@(iVsc^OHizWt7RbtiJHG}S+Kw^VM2YP6YCcH{w5C+z3dwWi4Mx?}%u#LdM`vn%$ou4Jh=G+X5Jr@D^Fo zu9s~<+4^5vJ}9qZhOzNXG|DwT@|wmVm`Y2BIjGyL1U!2tw$PI=#rQ7<6*yOiHaLa!zs;uKn;8J3j^U_vt51dnNjJ-&@Q_9Y| zt`gIpTGD1)zM6^&z6WH@jrCwK4izN2-JUj_l4-pubKXJ`lUaS$dm2Tj-KB(dc7KgI z8%?=+39?egvjO=N=)HGNZ6+6LPp`E+5HwSfvcmIXxA~1X-V-%N6W<4an%&1gSqpCx zWR~nSf)d19AZR%KbtJ;^o=d=?#hM=lL{HPn*FVjWy;9B-UjZJomeY&OMU*vyoE74F zw?6fm1gSP48eP{7G8iBiYlTAuEHXLX{qb{}CBdy7LFRJl8L1hV_G}KA>)gg;+q)HwzAfTkekxpLhH`%61uF%X zQxBS0)Mw#(A%`&o;`E#6p{ql8{GNd#S{>iC78v_EE?nAW#}&{#br2WL{i!xfc263k zEm{#ZnEOWlZFk_+dcn0kCS2j*tY`G|6z1Y8=$XoMn%#{1XFcC9jD}kcH2N>`3h)`0 zS~zN$RA5u1`OJHyIWEfAKZ~}sE99B;jYbdf5#5ieK?4EhlEa&}Oe?4LX!U)1Kb(}) z=LzrEWS1IiPszS7bbI<*dz$EUgCLLe(o#u0r}#5lGJLw}UfEXgnT4DwFSIdF8cz|p zEUwj3#{B*GUCGBVSqhU#qsNjNk?kssW!4=n9CGa6cGXP0x7bti_` z73WzAWd{b!;Tbf)6owfh-{zUxSs47Vq-VK1w27Ma9S8%xY@6QOz}6i(A~6AlgJ&kV zo+in{&&4BT9Y1a#fLjlYF5V}D(!*$9)+ChOy1i*hrgEE57<9V9EjT$(&^z|}3!5J@ z*^C{zVIRY zzs51D#xkj@8%6$7^!!15T%x3Yp5?b82eld@%nsYj`dpgAU~2nZHnK>|E0Y2eO-}YK^?^7za7|pq>Vj7$;Ai!5DXE3{1N{yME*hluKwN$j&yhN zbhAdfgMVwd-~Q|-1eIM-9!S*9iN77(-%tnPcEiA8f1&>{b-&R+22T)bZ*x;B_;&|z z@ZaYsyLjH@3jM$O-TyRpe@{dFzjpfFp!+`u{x0m##+4Bs2q%}v|DEdNhO`Ajz!Jbe z+y8w9i;Ibgh=FhLUm*km|FHt2{y`!)b^eZjkf@}@P1pZ}Zhk8N8;L>0CI1%*iT<1D z;fAnxLb?HezdcQrn{jZtyEr4c!Cdw>+ip{lL+e}BulhyVZp literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Sources/AccountContext.swift b/submodules/TelegramUI/Sources/AccountContext.swift index 31c602eaee..2b14a05883 100644 --- a/submodules/TelegramUI/Sources/AccountContext.swift +++ b/submodules/TelegramUI/Sources/AccountContext.swift @@ -578,8 +578,8 @@ public final class AccountContextImpl: AccountContext { } } - public func scheduleGroupCall(peerId: PeerId) { - let _ = self.sharedContext.callManager?.scheduleGroupCall(context: self, peerId: peerId, endCurrentIfAny: true) + public func scheduleGroupCall(peerId: PeerId, parentController: ViewController) { + let _ = self.sharedContext.callManager?.scheduleGroupCall(context: self, peerId: peerId, endCurrentIfAny: true, parentController: parentController) } public func joinGroupCall(peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, activeCall: EngineGroupCallDescription) { diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index a5ce5c753c..488dc74dc3 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -1753,7 +1753,7 @@ private func extractAccountManagerState(records: AccountRecordsView Void)? - init(postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoManager, content: UniversalVideoContent, close: @escaping () -> Void) { + init(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoManager, content: UniversalVideoContent, close: @escaping () -> Void) { self.close = close self.content = content var togglePlayPauseImpl: (() -> Void)? let decoration = OverlayInstantVideoDecoration(tapped: { togglePlayPauseImpl?() }) - self.videoNode = UniversalVideoNode(postbox: postbox, audioSession: audioSession, manager: manager, decoration: decoration, content: content, priority: .secondaryOverlay, snapshotContentWhenGone: true) + self.videoNode = UniversalVideoNode(accountId: accountId, postbox: postbox, audioSession: audioSession, manager: manager, decoration: decoration, content: content, priority: .secondaryOverlay, snapshotContentWhenGone: true) self.decoration = decoration super.init() diff --git a/submodules/TelegramUI/Sources/SharedMediaPlayer.swift b/submodules/TelegramUI/Sources/SharedMediaPlayer.swift index a5961e2876..6c6229f201 100644 --- a/submodules/TelegramUI/Sources/SharedMediaPlayer.swift +++ b/submodules/TelegramUI/Sources/SharedMediaPlayer.swift @@ -236,7 +236,7 @@ final class SharedMediaPlayer { if let mediaManager = strongSelf.mediaManager, let item = item as? MessageMediaPlaylistItem { switch playbackData.source { case let .telegramFile(fileReference, _, _): - let videoNode = OverlayInstantVideoNode(postbox: strongSelf.account.postbox, audioSession: strongSelf.audioSession, manager: mediaManager.universalVideoManager, content: NativeVideoContent(id: .message(item.message.stableId, fileReference.media.fileId), userLocation: .peer(item.message.id.peerId), fileReference: fileReference, enableSound: false, baseRate: rateValue, isAudioVideoMessage: true, captureProtected: item.message.isCopyProtected(), storeAfterDownload: nil), close: { [weak mediaManager] in + let videoNode = OverlayInstantVideoNode(accountId: strongSelf.account.id, postbox: strongSelf.account.postbox, audioSession: strongSelf.audioSession, manager: mediaManager.universalVideoManager, content: NativeVideoContent(id: .message(item.message.stableId, fileReference.media.fileId), userLocation: .peer(item.message.id.peerId), fileReference: fileReference, enableSound: false, baseRate: rateValue, isAudioVideoMessage: true, captureProtected: item.message.isCopyProtected(), storeAfterDownload: nil), close: { [weak mediaManager] in mediaManager?.setPlaylist(nil, type: .voice, control: .playback(.pause)) }) strongSelf.playbackItem = .instantVideo(videoNode) diff --git a/submodules/TelegramUniversalVideoContent/BUILD b/submodules/TelegramUniversalVideoContent/BUILD index 9baefbc0b0..b705ae876b 100644 --- a/submodules/TelegramUniversalVideoContent/BUILD +++ b/submodules/TelegramUniversalVideoContent/BUILD @@ -24,6 +24,7 @@ swift_library( "//submodules/AppBundle:AppBundle", "//submodules/Utils/RangeSet:RangeSet", "//submodules/TelegramVoip", + "//submodules/ManagedFile", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUniversalVideoContent/Sources/HLSVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoContent.swift index f04db2abf9..3cd8c3b4e3 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/HLSVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoContent.swift @@ -12,6 +12,7 @@ import AccountContext import PhotoResources import RangeSet import TelegramVoip +import ManagedFile public final class HLSVideoContent: UniversalVideoContent { public let id: AnyHashable @@ -40,8 +41,8 @@ public final class HLSVideoContent: UniversalVideoContent { 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 makeContentNode(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { + return HLSVideoContentNode(accountId: accountId, 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 { @@ -60,7 +61,7 @@ public final class HLSVideoContent: UniversalVideoContent { private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNode { private final class HLSServerSource: SharedHLSServer.Source { - let id: UUID + let id: String let postbox: Postbox let userLocation: MediaResourceUserLocation let playlistFiles: [Int: FileMediaReference] @@ -68,8 +69,8 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod private var playlistFetchDisposables: [Int: Disposable] = [:] - init(id: UUID, postbox: Postbox, userLocation: MediaResourceUserLocation, playlistFiles: [Int: FileMediaReference], qualityFiles: [Int: FileMediaReference]) { - self.id = id + init(accountId: Int64, fileId: Int64, postbox: Postbox, userLocation: MediaResourceUserLocation, playlistFiles: [Int: FileMediaReference], qualityFiles: [Int: FileMediaReference]) { + self.id = "\(UInt64(bitPattern: accountId))_\(fileId)" self.postbox = postbox self.userLocation = userLocation self.playlistFiles = playlistFiles @@ -143,10 +144,11 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod 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 { + func fileData(id: Int64, range: Range) -> Signal<(TempBoxFile, Range, Int)?, NoError> { + guard let (quality, file) = self.qualityFiles.first(where: { $0.value.media.fileId.id == id }) else { return .single(nil) } + let _ = quality guard let size = file.media.size else { return .single(nil) } @@ -156,77 +158,80 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod 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 { + let queue = postbox.mediaBox.dataQueue + return Signal<(TempBoxFile, Range, Int)?, NoError> { subscriber in + guard let fetchResource = postbox.mediaBox.fetchResource else { return EmptyDisposable } - /*let fetchDisposable = freeMediaFileResourceInteractiveFetched(postbox: postbox, userLocation: userLocation, fileReference: file, resource: file.media.resource, range: (mappedRange, .elevated)).startStandalone() + 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 + ) - 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() + let completeFile = TempBox.shared.tempFile(fileName: "data") + let partialFile = TempBox.shared.tempFile(fileName: "data") + let metaFile = TempBox.shared.tempFile(fileName: "data") + + guard let fileContext = MediaBoxFileContextV2Impl( + queue: queue, + manager: postbox.mediaBox.dataFileManager, + storageBox: nil, + resourceId: file.media.resource.id.stringRepresentation.data(using: .utf8)!, + path: completeFile.path, + partialPath: partialFile.path, + metaPath: metaFile.path + ) else { + return EmptyDisposable + } + + let fetchDisposable = fileContext.fetched( + range: mappedRange, + priority: .default, + fetch: { intervals in + return fetchResource(file.media.resource, intervals, params) + }, + error: { _ in + }, + completed: { } - }) + ) + + #if DEBUG + let startTime = CFAbsoluteTimeGetCurrent() + #endif + + let dataDisposable = fileContext.data( + range: mappedRange, + waitUntilAfterInitialFetch: true, + next: { result in + if result.complete { + #if DEBUG + let fetchTime = CFAbsoluteTimeGetCurrent() - startTime + print("Fetching \(quality)p part took \(fetchTime * 1000.0) ms") + #endif + subscriber.putNext((partialFile, Int(result.offset) ..< Int(result.offset + result.size), Int(size))) + subscriber.putCompletion() + } + } + ) + return ActionDisposable { - fetchDisposable.dispose() - dataDisposable.dispose() - }*/ + queue.async { + fetchDisposable.dispose() + dataDisposable.dispose() + fileContext.cancelFullRangeFetches() + + TempBox.shared.dispose(completeFile) + TempBox.shared.dispose(metaFile) + } + } } + |> runOn(queue) } } @@ -244,6 +249,7 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod 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 baseRate: Double = 1.0 private var isBuffering = false private var seekId: Int = 0 private let _status = ValuePromise() @@ -272,7 +278,7 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod private let imageNode: TransformImageNode private var playerItem: AVPlayerItem? - private let player: AVPlayer + private var player: AVPlayer? private let playerNode: ASDisplayNode private var loadProgressDisposable: Disposable? @@ -296,24 +302,38 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod private var preferredVideoQuality: UniversalVideoContentVideoQuality = .auto - init(postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, streamVideo: Bool, loopVideo: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool) { + init(accountId: AccountRecordId, 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.baseRate = baseRate + + if var dimensions = fileReference.media.dimensions { + if let thumbnail = fileReference.media.previewRepresentations.first { + let dimensionsVertical = dimensions.width < dimensions.height + let thumbnailVertical = thumbnail.dimensions.width < thumbnail.dimensions.height + if dimensionsVertical != thumbnailVertical { + dimensions = PixelDimensions(width: dimensions.height, height: dimensions.width) + } + } + self.dimensions = dimensions.cgSize + } else { + self.dimensions = CGSize(width: 128.0, height: 128.0) + } self.imageNode = TransformImageNode() - var startTime = CFAbsoluteTimeGetCurrent() - - let player = AVPlayer(playerItem: nil) + var player: AVPlayer? + player = AVPlayer(playerItem: nil) self.player = player - if !enableSound { - player.volume = 0.0 + if #available(iOS 16.0, *) { + player?.defaultRate = Float(baseRate) + } + if !enableSound { + player?.volume = 0.0 } - - print("Player created in \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") self.playerNode = ASDisplayNode() self.playerNode.setLayerBlock({ @@ -363,10 +383,9 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod } } if !playlistFiles.isEmpty && playlistFiles.keys == qualityFiles.keys { - self.playerSource = HLSServerSource(id: UUID(), postbox: postbox, userLocation: userLocation, playlistFiles: playlistFiles, qualityFiles: qualityFiles) + self.playerSource = HLSServerSource(accountId: accountId.int64, fileId: fileReference.media.fileId.id, 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 @@ -386,49 +405,39 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod self.addSubnode(self.imageNode) self.addSubnode(self.playerNode) - self.player.actionAtItemEnd = .pause + 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.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.serverDisposable = SharedHLSServer.shared.registerPlayer(source: playerSource, completion: { [weak self] in + Queue.mainQueue().async { + guard let self else { + return + } + + 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)!) + + if #available(iOS 14.0, *) { + playerItem.startsOnFirstEligibleVariant = true + } + + self.setPlayerItem(playerItem) + } + }) } - 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 @@ -441,16 +450,10 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod } 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.player?.removeObserver(self, forKeyPath: "rate") self.setPlayerItem(nil) @@ -459,15 +462,16 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod 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 didPlayToEndTimeObserver = self.didPlayToEndTimeObserver { + NotificationCenter.default.removeObserver(didPlayToEndTimeObserver) + } if let failureObserverId = self.failureObserverId { NotificationCenter.default.removeObserver(failureObserverId) } @@ -486,14 +490,53 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod 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 - } + playerItem.removeObserver(self, forKeyPath: "presentationSize") + } + + if let playerItemFailedToPlayToEndTimeObserver = self.playerItemFailedToPlayToEndTimeObserver { + self.playerItemFailedToPlayToEndTimeObserver = nil + NotificationCenter.default.removeObserver(playerItemFailedToPlayToEndTimeObserver) + } + + if let didPlayToEndTimeObserver = self.didPlayToEndTimeObserver { + self.didPlayToEndTimeObserver = nil + NotificationCenter.default.removeObserver(didPlayToEndTimeObserver) + } + if let failureObserverId = self.failureObserverId { + self.failureObserverId = nil + NotificationCenter.default.removeObserver(failureObserverId) + } + if let errorObserverId = self.errorObserverId { + self.errorObserverId = nil + NotificationCenter.default.removeObserver(errorObserverId) } self.playerItem = item + if let item { + self.didPlayToEndTimeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: item, queue: nil, using: { [weak self] notification in + self?.performActionAtEnd() + }) + + self.failureObserverId = NotificationCenter.default.addObserver(forName: AVPlayerItem.failedToPlayToEndTimeNotification, object: item, queue: .main, using: { notification in +#if DEBUG + print("Player Error: \(notification.description)") +#endif + }) + self.errorObserverId = NotificationCenter.default.addObserver(forName: AVPlayerItem.newErrorLogEntryNotification, object: item, queue: .main, using: { [weak item] notification in + if let item { + let event = item.errorLog()?.events.last + if let event { + let _ = event +#if DEBUG + print("Player Error: \(event.errorComment ?? "")") +#endif + } + } + }) + item.addObserver(self, forKeyPath: "presentationSize", options: [], context: nil) + } + if let playerItem = self.playerItem { playerItem.addObserver(self, forKeyPath: "playbackBufferEmpty", options: .new, context: nil) playerItem.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: .new, context: nil) @@ -507,23 +550,26 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod }) } - self.player.replaceCurrentItem(with: self.playerItem) + self.player?.replaceCurrentItem(with: self.playerItem) } private func updateStatus() { - let isPlaying = !self.player.rate.isZero + guard let player = self.player else { + return + } + let isPlaying = !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 + var timestamp = 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.statusValue = MediaPlayerStatus(generationTimestamp: CACurrentMediaTime(), duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: timestamp, baseRate: self.baseRate, seekId: self.seekId, status: status, soundEnabled: true) self._status.set(self.statusValue) if case .playing = status { @@ -543,9 +589,11 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod 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 + if let player = self.player { + let isPlaying = !player.rate.isZero + if isPlaying { + self.isBuffering = false + } } self.updateStatus() } else if keyPath == "playbackBufferEmpty" { @@ -555,7 +603,7 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod self.isBuffering = false self.updateStatus() } else if keyPath == "presentationSize" { - if let currentItem = self.player.currentItem { + if let currentItem = self.player?.currentItem { print("Presentation size: \(Int(currentItem.presentationSize.height))") } } @@ -573,42 +621,57 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod 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() + if let dimensions = self.dimensions { + let imageSize = CGSize(width: floor(dimensions.width / 2.0), height: floor(dimensions.height / 2.0)) + let makeLayout = self.imageNode.asyncLayout() + let applyLayout = makeLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: .clear)) + applyLayout() + } } 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)) + self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, baseRate: self.baseRate, 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 { + 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() + guard let self else { + return + } + self.hasAudioSession = true + self.player?.play() }, deactivate: { [weak self] _ in - self?.hasAudioSession = false - self?.player.pause() + guard let self else { + return .complete() + } + self.hasAudioSession = false + self.player?.pause() + return .complete() })) } else { - self.player.play() + self.player?.play() } } else { - self.player.play() + self.player?.play() } } func pause() { assert(Queue.mainQueue().isCurrent()) - self.player.pause() + self.player?.pause() } func togglePlayPause() { assert(Queue.mainQueue().isCurrent()) - if self.player.rate.isZero { + + guard let player = self.player else { + return + } + + if player.rate.isZero { self.play() } else { self.pause() @@ -621,15 +684,15 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod 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 + self?.player?.volume = 1.0 }, deactivate: { [weak self] _ in self?.hasAudioSession = false - self?.player.pause() + self?.player?.pause() return .complete() })) } } else { - self.player.volume = 0.0 + self.player?.volume = 0.0 self.hasAudioSession = false self.audioSessionDisposable.set(nil) } @@ -638,16 +701,16 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod func seek(_ timestamp: Double) { assert(Queue.mainQueue().isCurrent()) self.seekId += 1 - self.player.seek(to: CMTime(seconds: timestamp, preferredTimescale: 30)) + self.player?.seek(to: CMTime(seconds: timestamp, preferredTimescale: 30)) } func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { - self.player.volume = 1.0 + self.player?.volume = 1.0 self.play() } func setSoundMuted(soundMuted: Bool) { - self.player.volume = soundMuted ? 0.0 : 1.0 + self.player?.volume = soundMuted ? 0.0 : 1.0 } func continueWithOverridingAmbientMode(isAmbient: Bool) { @@ -657,7 +720,7 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod } func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { - self.player.volume = 0.0 + self.player?.volume = 0.0 self.hasAudioSession = false self.audioSessionDisposable.set(nil) } @@ -666,13 +729,23 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod } func setBaseRate(_ baseRate: Double) { - self.player.rate = Float(baseRate) + guard let player = self.player else { + return + } + self.baseRate = baseRate + if #available(iOS 16.0, *) { + player.defaultRate = Float(baseRate) + } + if player.rate != 0.0 { + player.rate = Float(baseRate) + } + self.updateStatus() } func setVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) { self.preferredVideoQuality = videoQuality - guard let currentItem = self.player.currentItem else { + guard let currentItem = self.player?.currentItem else { return } guard let playerSource = self.playerSource else { @@ -694,7 +767,7 @@ private final class HLSVideoContentNode: ASDisplayNode, UniversalVideoContentNod } func videoQualityState() -> (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])? { - guard let currentItem = self.player.currentItem else { + guard let currentItem = self.player?.currentItem else { return nil } guard let playerSource = self.playerSource else { diff --git a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift index 6bc07d1c0c..e7fd264a92 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift @@ -137,7 +137,7 @@ public final class NativeVideoContent: UniversalVideoContent { self.hasSentFramesToDisplay = hasSentFramesToDisplay } - public func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { + public func makeContentNode(accountId: AccountRecordId, 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, 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) } diff --git a/submodules/TelegramUniversalVideoContent/Sources/OverlayUniversalVideoNode.swift b/submodules/TelegramUniversalVideoContent/Sources/OverlayUniversalVideoNode.swift index 1d8cc7c36a..d71b1eec99 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/OverlayUniversalVideoNode.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/OverlayUniversalVideoNode.swift @@ -41,7 +41,7 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode, AVPictureInP private var statusDisposable: Disposable? private var status: MediaPlayerStatus? - public init(postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoManager, content: UniversalVideoContent, shouldBeDismissed: Signal = .single(false), expand: @escaping () -> Void, close: @escaping () -> Void) { + public init(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoManager, content: UniversalVideoContent, shouldBeDismissed: Signal = .single(false), expand: @escaping () -> Void, close: @escaping () -> Void) { self.content = content self.defaultExpand = expand @@ -62,7 +62,7 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode, AVPictureInP }, controlsAreShowingUpdated: { value in controlsAreShowingUpdatedImpl?(value) }) - self.videoNode = UniversalVideoNode(postbox: postbox, audioSession: audioSession, manager: manager, decoration: decoration, content: content, priority: .overlay) + self.videoNode = UniversalVideoNode(accountId: accountId, postbox: postbox, audioSession: audioSession, manager: manager, decoration: decoration, content: content, priority: .overlay) self.decoration = decoration super.init() diff --git a/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift index f7a6529cb1..e673a81ed4 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift @@ -95,7 +95,7 @@ public final class PlatformVideoContent: UniversalVideoContent { self.fetchAutomatically = fetchAutomatically } - public func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { + public func makeContentNode(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { return PlatformVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, content: self.content, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically) } diff --git a/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift index 66bca4d094..8f436da4c4 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift @@ -29,7 +29,7 @@ public final class SystemVideoContent: UniversalVideoContent { self.duration = duration } - public func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { + public func makeContentNode(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { return SystemVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, url: self.url, imageReference: self.imageReference, intrinsicDimensions: self.dimensions, approximateDuration: self.duration) } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift index 2cba6fdf64..5b204ce3d0 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift @@ -36,7 +36,7 @@ public final class WebEmbedVideoContent: UniversalVideoContent { self.openUrl = openUrl } - public func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { + public func makeContentNode(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { return WebEmbedVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, webPage: self.webPage, webpageContent: self.webpageContent, forcedTimestamp: self.forcedTimestamp, openUrl: self.openUrl) } } diff --git a/submodules/TelegramVoip/BUILD b/submodules/TelegramVoip/BUILD index 62595d2fb6..ab815d0e7f 100644 --- a/submodules/TelegramVoip/BUILD +++ b/submodules/TelegramVoip/BUILD @@ -18,6 +18,7 @@ swift_library( "//submodules/TgVoip:TgVoip", "//submodules/TgVoipWebrtc:TgVoipWebrtc", "//submodules/FFMpegBinding", + "//submodules/ManagedFile", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramVoip/Sources/WrappedMediaStreamingContext.swift b/submodules/TelegramVoip/Sources/WrappedMediaStreamingContext.swift index 1acafab9a5..8610235ba8 100644 --- a/submodules/TelegramVoip/Sources/WrappedMediaStreamingContext.swift +++ b/submodules/TelegramVoip/Sources/WrappedMediaStreamingContext.swift @@ -5,7 +5,7 @@ import TelegramCore import Network import Postbox import FFMpegBinding - +import ManagedFile @available(iOS 12.0, macOS 14.0, *) public final class WrappedMediaStreamingContext { @@ -275,7 +275,7 @@ public final class ExternalMediaStreamingContext: SharedHLSServerSource { } } - func fileData(id: Int64, range: Range) -> Signal<(Data, Int)?, NoError> { + func fileData(id: Int64, range: Range) -> Signal<(TempBoxFile, Range, Int)?, NoError> { return .never() } } @@ -285,8 +285,8 @@ public final class ExternalMediaStreamingContext: SharedHLSServerSource { private let impl: QueueLocalObject private var hlsServerDisposable: Disposable? - public var id: UUID { - return self.internalId + public var id: String { + return self.internalId.uuidString } public init(id: CallSessionInternalId, rejoinNeeded: @escaping () -> Void) { @@ -296,7 +296,7 @@ public final class ExternalMediaStreamingContext: SharedHLSServerSource { return Impl(queue: queue, rejoinNeeded: rejoinNeeded) }) - self.hlsServerDisposable = SharedHLSServer.shared.registerPlayer(source: self) + self.hlsServerDisposable = SharedHLSServer.shared.registerPlayer(source: self, completion: {}) } deinit { @@ -331,7 +331,7 @@ public final class ExternalMediaStreamingContext: SharedHLSServerSource { } } - public func fileData(id: Int64, range: Range) -> Signal<(Data, Int)?, NoError> { + public func fileData(id: Int64, range: Range) -> Signal<(TempBoxFile, Range, Int)?, NoError> { return self.impl.signalWith { impl, subscriber in impl.fileData(id: id, range: range).start(next: subscriber.putNext) } @@ -339,12 +339,12 @@ public final class ExternalMediaStreamingContext: SharedHLSServerSource { } public protocol SharedHLSServerSource: AnyObject { - var id: UUID { get } + var id: String { 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> + func fileData(id: Int64, range: Range) -> Signal<(TempBoxFile, Range, Int)?, NoError> } @available(iOS 12.0, macOS 14.0, *) @@ -387,14 +387,66 @@ public final class SharedHLSServer { private var listener: NWListener? private var sourceReferences = Bag() + private var referenceCheckTimer: SwiftSignalKit.Timer? + private var shutdownTimer: SwiftSignalKit.Timer? init(queue: Queue, port: UInt16) { self.queue = queue self.port = NWEndpoint.Port(rawValue: port)! - self.start() } - func start() { + deinit { + self.referenceCheckTimer?.invalidate() + self.shutdownTimer?.invalidate() + } + + private func updateNeedsListener() { + var isEmpty = true + for item in self.sourceReferences.copyItems() { + if let _ = item.source { + isEmpty = false + break + } + } + + if isEmpty { + if self.listener != nil { + if self.shutdownTimer == nil { + self.shutdownTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: false, completion: { [weak self] in + guard let self else { + return + } + self.shutdownTimer = nil + self.stopListener() + }, queue: self.queue) + self.shutdownTimer?.start() + } + } + if let referenceCheckTimer = self.referenceCheckTimer { + self.referenceCheckTimer = nil + referenceCheckTimer.invalidate() + } + } else { + if let shutdownTimer = self.shutdownTimer { + self.shutdownTimer = nil + shutdownTimer.invalidate() + } + if self.listener == nil { + self.startListener() + } + if self.referenceCheckTimer == nil { + self.referenceCheckTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in + guard let self else { + return + } + self.updateNeedsListener() + }, queue: self.queue) + self.referenceCheckTimer?.start() + } + } + } + + private func startListener() { let listener: NWListener do { listener = try NWListener(using: .tcp, on: self.port) @@ -411,8 +463,8 @@ public final class SharedHLSServer { self.handleConnection(connection: connection) } - listener.stateUpdateHandler = { [weak self] state in - guard let self else { + listener.stateUpdateHandler = { [weak self, weak listener] state in + guard let self, let listener else { return } switch state { @@ -420,9 +472,9 @@ public final class SharedHLSServer { Logger.shared.log("SharedHLSServer", "Server is ready on port \(self.port)") case let .failed(error): Logger.shared.log("SharedHLSServer", "Server failed with error: \(error)") - self.listener?.cancel() + listener.cancel() - self.listener?.start(queue: self.queue.queue) + listener.start(queue: self.queue.queue) default: break } @@ -431,9 +483,17 @@ public final class SharedHLSServer { listener.start(queue: self.queue.queue) } + private func stopListener() { + guard let listener = self.listener else { + return + } + self.listener = nil + listener.cancel() + } + private func handleConnection(connection: NWConnection) { connection.start(queue: self.queue.queue) - connection.receive(minimumIncompleteLength: 1, maximumLength: 1024, completion: { [weak self] data, _, isComplete, error in + connection.receive(minimumIncompleteLength: 1, maximumLength: 32 * 1024, completion: { [weak self] data, _, isComplete, error in guard let self else { return } @@ -488,10 +548,7 @@ public final class SharedHLSServer { self.sendErrorAndClose(connection: connection, error: .notFound) return } - guard let streamId = UUID(uuidString: String(requestPath[requestPath.startIndex ..< firstSlash.lowerBound])) else { - self.sendErrorAndClose(connection: connection) - return - } + let streamId = String(requestPath[requestPath.startIndex ..< firstSlash.lowerBound]) guard let source = self.sourceReferences.copyItems().first(where: { $0.source?.id == streamId })?.source else { self.sendErrorAndClose(connection: connection) return @@ -581,13 +638,14 @@ public final class SharedHLSServer { } let _ = (source.fileData(id: fileIdValue, range: requestRange.lowerBound ..< requestRange.upperBound + 1) |> deliverOn(self.queue) + //|> timeout(5.0, queue: self.queue, alternate: .single(nil)) |> 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) + if let (tempFile, tempFileRange, totalSize) = result { + self.sendResponseFileAndClose(connection: connection, file: tempFile, fileRange: tempFileRange, range: requestRange, totalSize: totalSize) } else { self.sendErrorAndClose(connection: connection, error: .internalServerError) } @@ -628,9 +686,62 @@ public final class SharedHLSServer { }) } - func registerPlayer(source: SharedHLSServerSource) -> Disposable { + private static func sendRemainingFileRange(queue: Queue, connection: NWConnection, tempFile: TempBoxFile, managedFile: ManagedFile, remainingRange: Range, fileSize: Int) -> Void { + let blockSize = 256 * 1024 + + let clippedLowerBound = min(remainingRange.lowerBound, fileSize) + var clippedUpperBound = min(remainingRange.upperBound, fileSize) + clippedUpperBound = min(clippedUpperBound, clippedLowerBound + blockSize) + + if clippedUpperBound == clippedLowerBound { + TempBox.shared.dispose(tempFile) + connection.cancel() + } else { + let _ = managedFile.seek(position: Int64(clippedLowerBound)) + let data = managedFile.readData(count: Int(clippedUpperBound - clippedLowerBound)) + let nextRange = clippedUpperBound ..< remainingRange.upperBound + + connection.send(content: data, completion: .contentProcessed { error in + queue.async { + if let error { + Logger.shared.log("SharedHLSServer", "Failed to send response: \(error)") + connection.cancel() + TempBox.shared.dispose(tempFile) + } else { + sendRemainingFileRange(queue: queue, connection: connection, tempFile: tempFile, managedFile: managedFile, remainingRange: nextRange, fileSize: fileSize) + } + } + }) + } + } + + private func sendResponseFileAndClose(connection: NWConnection, file: TempBoxFile, fileRange: Range, range: Range, totalSize: Int) { + let queue = self.queue + + guard let managedFile = ManagedFile(queue: nil, path: file.path, mode: .read), let fileSize = managedFile.getSize() else { + self.sendErrorAndClose(connection: connection, error: .internalServerError) + TempBox.shared.dispose(file) + return + } + + var responseHeaders = "HTTP/1.1 200 OK\r\n" + responseHeaders.append("Content-Length: \(fileRange.upperBound - fileRange.lowerBound)\r\n") + 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") + + connection.send(content: responseHeaders.data(using: .utf8)!, completion: .contentProcessed({ _ in })) + + Impl.sendRemainingFileRange(queue: queue, connection: connection, tempFile: file, managedFile: managedFile, remainingRange: fileRange, fileSize: Int(fileSize)) + } + + func registerPlayer(source: SharedHLSServerSource, completion: @escaping () -> Void) -> Disposable { let queue = self.queue let index = self.sourceReferences.add(SourceReference(source: source)) + self.updateNeedsListener() + completion() return ActionDisposable { [weak self] in queue.async { @@ -638,6 +749,7 @@ public final class SharedHLSServer { return } self.sourceReferences.remove(index) + self.updateNeedsListener() } } } @@ -655,11 +767,11 @@ public final class SharedHLSServer { }) } - public func registerPlayer(source: SharedHLSServerSource) -> Disposable { + public func registerPlayer(source: SharedHLSServerSource, completion: @escaping () -> Void) -> Disposable { let disposable = MetaDisposable() self.impl.with { impl in - disposable.set(impl.registerPlayer(source: source)) + disposable.set(impl.registerPlayer(source: source, completion: completion)) } return disposable diff --git a/submodules/WebSearchUI/Sources/WebSearchVideoGalleryItem.swift b/submodules/WebSearchUI/Sources/WebSearchVideoGalleryItem.swift index 142323c37d..d3696d5d0d 100644 --- a/submodules/WebSearchUI/Sources/WebSearchVideoGalleryItem.swift +++ b/submodules/WebSearchUI/Sources/WebSearchVideoGalleryItem.swift @@ -165,7 +165,7 @@ final class WebSearchVideoGalleryItemNode: ZoomableContentGalleryItemNode { let mediaManager = item.context.sharedContext.mediaManager - let videoNode = UniversalVideoNode(postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .gallery) + let videoNode = UniversalVideoNode(accountId: item.context.account.id, postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .gallery) let videoSize = CGSize(width: item.content.dimensions.width * 2.0, height: item.content.dimensions.height * 2.0) videoNode.updateLayout(size: videoSize, transition: .immediate) self.videoNode = videoNode From 87b2d223f8d42d972d4008b575df58cb69a78a3e Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 20 Sep 2024 18:35:17 +0400 Subject: [PATCH 03/17] Ads and codes improvements --- Telegram/Telegram-iOS/en.lproj/Localizable.strings | 2 ++ .../ChatListUI/Sources/Node/ChatListItem.swift | 6 +++--- .../Sources/DeviceLocationManager.swift | 2 +- submodules/Display/Source/WindowContent.swift | 12 +++++++++++- submodules/Display/Source/WindowPanRecognizer.swift | 11 +++++++++++ .../Sources/ChatMessageAttachedContentNode.swift | 8 +++++--- submodules/TelegramUI/Sources/ChatController.swift | 4 ++-- .../Sources/ChatHistoryEntriesForView.swift | 2 ++ .../Sources/MediaAutoDownloadSettings.swift | 5 ++++- 9 files changed, 41 insertions(+), 11 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 2eb8b2e591..27c0caee78 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12939,3 +12939,5 @@ Sorry for the inconvenience."; "Notification.StarsGiveaway.Subtitle" = "You won a prize in a giveaway organized by **%1$@**.\n\nYour prize is **%2$@**."; "Notification.StarsGiveaway.Subtitle.Stars_1" = "%@ Star"; "Notification.StarsGiveaway.Subtitle.Stars_any" = "%@ Stars"; + +"VerificationCodes.DescriptionText" = "This chat is used to receive verification codes from third-party services."; diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index b86e567d5c..8628c5f602 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -576,7 +576,7 @@ private enum RevealOptionKey: Int32 { } private func canArchivePeer(id: EnginePeer.Id, accountPeerId: EnginePeer.Id) -> Bool { - if id.namespace == Namespaces.Peer.CloudUser && id.id._internalGetInt64Value() == 777000 { + if id.isTelegramNotifications { return false } if id == accountPeerId { @@ -913,7 +913,7 @@ private final class ChatListMediaPreviewNode: ASDisplayNode { } } -private let loginCodeRegex = try? NSRegularExpression(pattern: "[\\d\\-]{5,7}", options: []) +private let loginCodeRegex = try? NSRegularExpression(pattern: "\\b\\d{5,8}\\b", options: []) public class ChatListItemNode: ItemListRevealOptionsItemNode { final class TopicItemNode: ASDisplayNode { @@ -2371,7 +2371,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } } - if message.id.peerId.namespace == Namespaces.Peer.CloudUser && message.id.peerId.id._internalGetInt64Value() == 777000 { + if message.id.peerId.isTelegramNotifications || message.id.peerId.isVerificationCodes { if let cached = currentCustomTextEntities, cached.matches(text: message.text) { customTextEntities = cached } else if let matches = loginCodeRegex?.matches(in: message.text, options: [], range: NSMakeRange(0, (message.text as NSString).length)) { diff --git a/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift b/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift index 7f6756ad14..a3fcbc3fad 100644 --- a/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift +++ b/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift @@ -54,7 +54,7 @@ public final class DeviceLocationManager: NSObject { self.manager.delegate = self self.manager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters -// self.manager.distanceFilter = 5.0 + self.manager.distanceFilter = kCLDistanceFilterNone self.manager.activityType = .other self.manager.pausesLocationUpdatesAutomatically = false self.manager.headingFilter = 2.0 diff --git a/submodules/Display/Source/WindowContent.swift b/submodules/Display/Source/WindowContent.swift index e1bbea3b17..b716d17fb4 100644 --- a/submodules/Display/Source/WindowContent.swift +++ b/submodules/Display/Source/WindowContent.swift @@ -231,6 +231,16 @@ private func layoutMetricsForScreenSize(size: CGSize, orientation: UIInterfaceOr } public final class WindowKeyboardGestureRecognizerDelegate: NSObject, UIGestureRecognizerDelegate { + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + if let view = gestureRecognizer.view { + let location = touch.location(in: gestureRecognizer.view) + if location.y > view.bounds.height - 44.0 { + return false + } + } + return true + } + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } @@ -1299,7 +1309,7 @@ public class Window1 { } } - @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { + @objc func panGesture(_ recognizer: WindowPanRecognizer) { switch recognizer.state { case .began: self.panGestureBegan(location: recognizer.location(in: recognizer.view)) diff --git a/submodules/Display/Source/WindowPanRecognizer.swift b/submodules/Display/Source/WindowPanRecognizer.swift index 53ed394912..7ef93a5ca6 100644 --- a/submodules/Display/Source/WindowPanRecognizer.swift +++ b/submodules/Display/Source/WindowPanRecognizer.swift @@ -7,6 +7,7 @@ public final class WindowPanRecognizer: UIGestureRecognizer { public var ended: ((CGPoint, CGPoint?) -> Void)? private var previousPoints: [(CGPoint, Double)] = [] + private var previousVelocity: CGFloat = 0.0 override public func reset() { super.reset() @@ -45,6 +46,11 @@ public final class WindowPanRecognizer: UIGestureRecognizer { } } + func velocity(in view: UIView?) -> CGPoint { + let point = CGPoint(x: 0.0, y: self.previousVelocity) + return self.view?.convert(point, to: view) ?? .zero + } + override public func touchesBegan(_ touches: Set, with event: UIEvent) { super.touchesBegan(touches, with: event) @@ -68,9 +74,12 @@ public final class WindowPanRecognizer: UIGestureRecognizer { override public func touchesEnded(_ touches: Set, with event: UIEvent) { super.touchesEnded(touches, with: event) + self.state = .ended + if let touch = touches.first { let location = touch.location(in: self.view) self.addPoint(location) + self.previousVelocity = self.estimateVerticalVelocity() self.ended?(location, CGPoint(x: 0.0, y: self.estimateVerticalVelocity())) } } @@ -78,6 +87,8 @@ public final class WindowPanRecognizer: UIGestureRecognizer { override public func touchesCancelled(_ touches: Set, with event: UIEvent) { super.touchesCancelled(touches, with: event) + self.state = .cancelled + if let touch = touches.first { self.ended?(touch.location(in: self.view), nil) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift index 6ef71e157f..bcac257337 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift @@ -221,6 +221,8 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { } } + let isAd = message.adAttribute != nil + var isReplyThread = false if case .replyThread = chatLocation { isReplyThread = true @@ -352,7 +354,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { contentFileValue = file } - if shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file) { + if shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file, isAd: isAd) { contentMediaAutomaticDownload = .full } else if shouldPredownloadMedia(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, media: file) { contentMediaAutomaticDownload = .prefetch @@ -404,7 +406,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { } else { let contentMode: InteractiveMediaNodeContentMode = contentMediaAspectFilled ? .aspectFill : .aspectFit - let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: contentMediaValue) + let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: contentMediaValue, isAd: isAd) let (_, initialImageWidth, refineLayout) = makeContentMedia( context, @@ -435,7 +437,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { let contentFileContinueLayout: ChatMessageInteractiveFileNode.ContinueLayout? if let contentFileValue { - let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: contentFileValue) + let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: contentFileValue, isAd: isAd) let (_, refineLayout) = makeContentFile(ChatMessageInteractiveFileNode.Arguments( context: context, diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index c3d1f77ce9..3a8317ab07 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -7786,10 +7786,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return false } }) - } else if peerId.namespace == Namespaces.Peer.CloudUser && peerId.id._internalGetInt64Value() == 777000 { + } else if peerId.isTelegramNotifications { self.screenCaptureManager = ScreenCaptureDetectionManager(check: { [weak self] in if let strongSelf = self, strongSelf.traceVisibility() { - let loginCodeRegex = try? NSRegularExpression(pattern: "[\\d\\-]{5,7}", options: []) + let loginCodeRegex = try? NSRegularExpression(pattern: "\\b\\d{5,7}\\b", options: []) var loginCodesToInvalidate: [String] = [] strongSelf.chatDisplayNode.historyNode.forEachVisibleMessageItemNode({ itemNode in if let text = itemNode.item?.message.text, let matches = loginCodeRegex?.matches(in: text, options: [], range: NSMakeRange(0, (text as NSString).length)), let match = matches.first { diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift index 0c8f47b758..7b756fe096 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift @@ -446,6 +446,8 @@ func chatHistoryEntriesForView( } if case let .peer(peerId) = location, peerId.isReplies { entries.insert(.ChatInfoEntry("", presentationData.strings.RepliesChat_DescriptionText, nil, nil, presentationData), at: 0) + } else if case let .peer(peerId) = location, peerId.isVerificationCodes { + entries.insert(.ChatInfoEntry("", presentationData.strings.VerificationCodes_DescriptionText, nil, nil, presentationData), at: 0) } else if let cachedPeerData = cachedPeerData as? CachedUserData, let botInfo = cachedPeerData.botInfo, !botInfo.description.isEmpty { entries.insert(.ChatInfoEntry(presentationData.strings.Bot_DescriptionTitle, botInfo.description, botInfo.photo, botInfo.video, presentationData), at: 0) } else { diff --git a/submodules/TelegramUIPreferences/Sources/MediaAutoDownloadSettings.swift b/submodules/TelegramUIPreferences/Sources/MediaAutoDownloadSettings.swift index 5e2beb637c..fc8fceb504 100644 --- a/submodules/TelegramUIPreferences/Sources/MediaAutoDownloadSettings.swift +++ b/submodules/TelegramUIPreferences/Sources/MediaAutoDownloadSettings.swift @@ -564,7 +564,10 @@ public func isAutodownloadEnabledForAnyPeerType(category: MediaAutoDownloadCateg return category.contacts || category.otherPrivate || category.groups || category.channels } -public func shouldDownloadMediaAutomatically(settings: MediaAutoDownloadSettings, peerType: MediaAutoDownloadPeerType, networkType: MediaAutoDownloadNetworkType, authorPeerId: PeerId? = nil, contactsPeerIds: Set = Set(), media: Media?, isStory: Bool = false) -> Bool { +public func shouldDownloadMediaAutomatically(settings: MediaAutoDownloadSettings, peerType: MediaAutoDownloadPeerType, networkType: MediaAutoDownloadNetworkType, authorPeerId: PeerId? = nil, contactsPeerIds: Set = Set(), media: Media?, isStory: Bool = false, isAd: Bool = false) -> Bool { + if isAd { + return true + } if (networkType == .cellular && !settings.cellular.enabled) || (networkType == .wifi && !settings.wifi.enabled) { return false } From 5a8506c3a4bcea0fe4b6695f8e5a7a0cccec00cf Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 20 Sep 2024 18:52:28 +0400 Subject: [PATCH 04/17] Fix build --- submodules/TelegramCore/Sources/Utils/PeerUtils.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift index 0eef90a045..be6962553a 100644 --- a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift @@ -433,6 +433,15 @@ public func isServicePeer(_ peer: Peer) -> Bool { } public extension PeerId { + var isTelegramNotifications: Bool { + if self.namespace == Namespaces.Peer.CloudUser { + if self.id._internalGetInt64Value() == 777000 { + return true + } + } + return false + } + var isReplies: Bool { if self.namespace == Namespaces.Peer.CloudUser { if self.id._internalGetInt64Value() == 708513 || self.id._internalGetInt64Value() == 1271266957 { From ab6d94b4eb607367675c6ea61626175eccfdbb54 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 21 Sep 2024 02:24:36 +0400 Subject: [PATCH 05/17] Codes improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 4 ++-- .../Sources/ChatListSearchContainerNode.swift | 2 +- .../Sources/ChatListSearchListPaneNode.swift | 2 +- .../Sources/Node/ChatListItem.swift | 22 ++++++++++++------- .../Sources/ChatMessageDateHeader.swift | 6 ++++- .../ChatMessageTextBubbleContentNode.swift | 4 ++-- .../Sources/PeerInfoScreen.swift | 4 ++-- .../Chat/ChatControllerLoadDisplayNode.swift | 4 ++-- .../TelegramUI/Sources/ChatController.swift | 7 +++++- .../Sources/ChatControllerNode.swift | 2 +- 10 files changed, 36 insertions(+), 21 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 27c0caee78..cdb5e338f9 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -5822,8 +5822,6 @@ Sorry for the inconvenience."; "Conversation.EditingPhotoPanelTitle" = "Edit Photo"; -"Conversation.TextCopied" = "Text copied to clipboard"; - "Media.LimitedAccessTitle" = "Limited Access to Media"; "Media.LimitedAccessText" = "You've given Telegram access only to select number of photos."; "Media.LimitedAccessManage" = "Manage"; @@ -12941,3 +12939,5 @@ Sorry for the inconvenience."; "Notification.StarsGiveaway.Subtitle.Stars_any" = "%@ Stars"; "VerificationCodes.DescriptionText" = "This chat is used to receive verification codes from third-party services."; + +"Conversation.CodeCopied" = "Code copied to clipboard"; diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 5538521545..b5168b166d 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -806,7 +806,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo var type: PeerType = .group for message in messages { if let user = message.author?._asPeer() as? TelegramUser { - if user.botInfo != nil { + if user.botInfo != nil && !user.id.isVerificationCodes { type = .bot } else { type = .user diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 55526da10d..3620d86c21 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -764,7 +764,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { } var status: ContactsPeerItemStatus = .none - if case let .user(user) = primaryPeer, let _ = user.botInfo { + if case let .user(user) = primaryPeer, let _ = user.botInfo, !primaryPeer.id.isVerificationCodes { if let subscriberCount = user.subscriberCount { status = .custom(string: presentationData.strings.Conversation_StatusBotSubscribers(subscriberCount), multiline: false, isActive: false, icon: nil) } else { diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 8628c5f602..852175365b 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -2270,14 +2270,20 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { if let messagePeer = itemPeer.chatMainPeer { peerText = messagePeer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) } - } else if let message = messages.last, let author = message.author?._asPeer(), let peer = itemPeer.chatMainPeer, !isUser { - if case let .channel(peer) = peer, case .broadcast = peer.info { - } else if !displayAsMessage { - if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported), let authorSignature = forwardInfo.authorSignature { - peerText = authorSignature - } else { - peerText = author.id == account.peerId ? item.presentationData.strings.DialogList_You : EnginePeer(author).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) - authorIsCurrentChat = author.id == peer.id + } else if let message = messages.last, let author = message.author?._asPeer(), let peer = itemPeer.chatMainPeer { + if peer.id.isVerificationCodes { + if let message = messages.last, let forwardInfo = message.forwardInfo, let author = forwardInfo.author { + peerText = EnginePeer(author).compactDisplayTitle + } + } else if !isUser { + if case let .channel(peer) = peer, case .broadcast = peer.info { + } else if !displayAsMessage { + if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported), let authorSignature = forwardInfo.authorSignature { + peerText = authorSignature + } else { + peerText = author.id == account.peerId ? item.presentationData.strings.DialogList_You : EnginePeer(author).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) + authorIsCurrentChat = author.id == peer.id + } } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift index 6df74b6802..d104205259 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift @@ -551,7 +551,11 @@ public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode, Chat } public func setPeer(context: AccountContext, theme: PresentationTheme, synchronousLoad: Bool, peer: Peer, authorOfMessage: MessageReference?, emptyColor: UIColor) { - self.containerNode.isGestureEnabled = true + if let messageReference = self.messageReference, let id = messageReference.id { + self.containerNode.isGestureEnabled = !id.peerId.isVerificationCodes + } else { + self.containerNode.isGestureEnabled = true + } var overrideImage: AvatarNodeImageOverride? if peer.isDeleted { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index d41a6440f9..2c88943faf 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -1376,7 +1376,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } - let enableCopy = !item.associatedData.isCopyProtectionEnabled && !item.message.isCopyProtected() + let enableCopy = (!item.associatedData.isCopyProtectionEnabled && !item.message.isCopyProtected()) || item.message.id.peerId.isVerificationCodes textSelectionNode.enableCopy = enableCopy var enableQuote = !item.message.text.isEmpty @@ -1390,7 +1390,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { if !item.controllerInteraction.canSendMessages() && !enableCopy { enableQuote = false } - if item.message.id.peerId.namespace == Namespaces.Peer.SecretChat { + if item.message.id.peerId.namespace == Namespaces.Peer.SecretChat || item.message.id.peerId.isVerificationCodes { enableQuote = false } if item.message.containsSecretMedia { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index a2b47e3eb4..f486e49542 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -11666,7 +11666,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro var isBot = false for message in messages { if let author = message.author, case let .user(user) = author { - if user.botInfo != nil { + if user.botInfo != nil && !user.id.isVerificationCodes { isBot = true } break @@ -11676,7 +11676,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if isBot { type = .bot } else if let user = peer as? TelegramUser { - if user.botInfo != nil { + if user.botInfo != nil && !user.id.isVerificationCodes { type = .bot } else { type = .user diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index 5a303be715..d3bd5649e0 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -4062,7 +4062,7 @@ extension ChatControllerImpl { } var isBot = false for message in messages { - if let author = message.author, case let .user(user) = author, user.botInfo != nil { + if let author = message.author, case let .user(user) = author, user.botInfo != nil && !user.id.isVerificationCodes { isBot = true break } @@ -4071,7 +4071,7 @@ extension ChatControllerImpl { if isBot { type = .bot } else if let user = peer as? TelegramUser { - if user.botInfo != nil { + if user.botInfo != nil && !user.id.isVerificationCodes { type = .bot } else { type = .user diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index a5b9502fa4..4b3f85aaf9 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -3800,8 +3800,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self { storeMessageTextInPasteboard(text, entities: nil) + var infoText = presentationData.strings.Conversation_TextCopied + if let peerId = strongSelf.chatLocation.peerId, peerId.isVerificationCodes && text.rangeOfCharacter(from: CharacterSet.decimalDigits.inverted) == nil { + infoText = presentationData.strings.Conversation_CodeCopied + } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } - strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .copy(text: infoText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return true }), in: .current) } diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 33d49ba476..d461f2a2d1 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -1110,7 +1110,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } } - let isSecret = self.chatPresentationInterfaceState.copyProtectionEnabled || self.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat + let isSecret = self.chatPresentationInterfaceState.copyProtectionEnabled || self.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat || self.chatLocation.peerId?.isVerificationCodes == true if self.historyNodeContainer.isSecret != isSecret { self.historyNodeContainer.isSecret = isSecret setLayerDisableScreenshots(self.titleAccessoryPanelContainer.layer, isSecret) From a5021d298f1ba3f25fb9e56185433dcbfae70762 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Sat, 21 Sep 2024 19:02:56 +0800 Subject: [PATCH 06/17] Bump xcode and macos version --- versions.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/versions.json b/versions.json index 2d777b2e9e..010b3179b1 100644 --- a/versions.json +++ b/versions.json @@ -1,6 +1,6 @@ { "app": "11.1.1", - "xcode": "15.2", + "xcode": "16.0", "bazel": "7.3.1", - "macos": "13.0" + "macos": "15.0" } From 8b50a9ed23784f05eb77ec560a7fab43a1812460 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 21 Sep 2024 16:39:01 +0400 Subject: [PATCH 07/17] Bump version --- versions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/versions.json b/versions.json index 010b3179b1..bc1c16abd1 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "11.1.1", + "app": "11.1.2", "xcode": "16.0", "bazel": "7.3.1", "macos": "15.0" From 4880be00f2c374eed442d989da4f15fc279cd938 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Sun, 22 Sep 2024 01:52:23 +0800 Subject: [PATCH 08/17] Trigger build --- Random.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Random.txt b/Random.txt index 397d9dd933..3578a816e4 100644 --- a/Random.txt +++ b/Random.txt @@ -1 +1 @@ -3a64b94cc76109006741756f85403c85 +3a64b94cc76109006741756f85403c86 From 93aab4587532c29b86b300f329cab35eb7a42991 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Mon, 23 Sep 2024 22:56:22 +0800 Subject: [PATCH 09/17] Disable new calls for the release --- submodules/TelegramCallsUI/Sources/VoiceChatController.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index db52366c58..4e0facc053 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -7098,14 +7098,15 @@ final class VoiceChatContextReferenceContentSource: ContextReferenceContentSourc } public func shouldUseV2VideoChatImpl(context: AccountContext) -> Bool { - var useV2 = true + /*var useV2 = true if context.sharedContext.immediateExperimentalUISettings.disableCallV2 { useV2 = false } if let data = context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_videochatui_v2"] { useV2 = false } - return useV2 + return useV2*/ + return false } public func makeVoiceChatControllerInitialData(sharedContext: SharedAccountContext, accountContext: AccountContext, call: PresentationGroupCall) -> Signal { From 2c56786809e04d77d56ad18208abc7478c08b2e7 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 23 Sep 2024 22:44:29 +0400 Subject: [PATCH 10/17] Gifts improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 5 + .../Sources/AccountContext.swift | 1 + .../AccountContext/Sources/Premium.swift | 1 + .../TelegramEngine/Payments/StarGifts.swift | 49 ++ submodules/TelegramUI/BUILD | 1 + .../ChatMessageGiftBubbleContentNode.swift | 169 ++++-- .../Sources/GiftItemComponent.swift | 108 +++- .../Sources/GiftOptionsScreen.swift | 201 +++++- .../Sources/ChatGiftPreviewItem.swift | 15 +- .../Sources/GiftSetupScreen.swift | 100 ++- .../Components/Gifts/GiftViewScreen/BUILD | 1 + .../Sources/GiftViewScreen.swift | 206 ++++--- .../PeerInfoRecommendedChannelsPane.swift | 1 - .../PeerInfoVisualMediaPaneNode/BUILD | 1 + .../Sources/PeerInfoGiftsPaneNode.swift | 195 ++++-- .../Components/Stars/StarsIntroScreen/BUILD | 44 ++ .../Sources/StarsIntroScreen.swift | 573 ++++++++++++++++++ .../Sources/StarsPurchaseScreen.swift | 10 +- .../HiddenIcon.imageset/Contents.json | 12 + .../HiddenIcon.imageset/hidden_30.pdf | Bin 0 -> 1543 bytes .../Premium/StarsPerk/Contents.json | 9 + .../StarsPerk/Gift.imageset/Contents.json | 12 + .../StarsPerk/Gift.imageset/gift_30 (4).pdf | Bin 0 -> 4312 bytes .../StarsPerk/Media.imageset/Contents.json | 12 + .../StarsPerk/Media.imageset/unlock_30.pdf | Bin 0 -> 2124 bytes .../StarsPerk/Miniapp.imageset/Contents.json | 12 + .../StarsPerk/Miniapp.imageset/bot_30.pdf | Bin 0 -> 2702 bytes .../StarsPerk/Reaction.imageset/Contents.json | 12 + .../StarsPerk/Reaction.imageset/cash_30.pdf | 62 ++ .../Sources/SharedAccountContext.swift | 11 +- 30 files changed, 1564 insertions(+), 259 deletions(-) create mode 100644 submodules/TelegramUI/Components/Stars/StarsIntroScreen/BUILD create mode 100644 submodules/TelegramUI/Components/Stars/StarsIntroScreen/Sources/StarsIntroScreen.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Peer Info/HiddenIcon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Peer Info/HiddenIcon.imageset/hidden_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Gift.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Gift.imageset/gift_30 (4).pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Media.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Media.imageset/unlock_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Miniapp.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Miniapp.imageset/bot_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Reaction.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Reaction.imageset/cash_30.pdf diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index cdb5e338f9..60ec2f47b2 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12941,3 +12941,8 @@ Sorry for the inconvenience."; "VerificationCodes.DescriptionText" = "This chat is used to receive verification codes from third-party services."; "Conversation.CodeCopied" = "Code copied to clipboard"; + +"Stars.Purchase.StarGiftInfo" = "Buy Stars to send **%@** gifts that can be kept on the profile or converted to Stars."; + +"SharedMedia.GiftCount_1" = "%@ gift"; +"SharedMedia.GiftCount_any" = "%@ gifts"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index fef2ed9501..b0d5799d9a 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1017,6 +1017,7 @@ public protocol SharedAccountContext: AnyObject { func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController func makeStarsGiveawayBoostScreen(context: AccountContext, peerId: EnginePeer.Id, boost: ChannelBoostersContext.State.Boost) -> ViewController + func makeStarsIntroScreen(context: AccountContext) -> ViewController func makeGiftViewScreen(context: AccountContext, message: EngineMessage) -> ViewController func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index 7f8d9fdaaa..0edf8c349c 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -130,6 +130,7 @@ public enum StarsPurchasePurpose: Equatable { case subscription(peerId: EnginePeer.Id, requiredStars: Int64, renew: Bool) case gift(peerId: EnginePeer.Id) case unlockMedia(requiredStars: Int64) + case starGift(peerId: EnginePeer.Id, requiredStars: Int64) } public struct PremiumConfiguration { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index 2ebfdbddb5..b297f7ba13 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -238,6 +238,7 @@ private final class ProfileGiftsContextImpl { private let peerId: PeerId private let disposable = MetaDisposable() + private let actionDisposable = MetaDisposable() private var gifts: [ProfileGiftsContext.State.StarGift] = [] private var count: Int32? @@ -258,6 +259,7 @@ private final class ProfileGiftsContextImpl { deinit { self.disposable.dispose() + self.actionDisposable.dispose() } func loadMore() { @@ -315,6 +317,27 @@ private final class ProfileGiftsContextImpl { } } + func updateStarGiftAddedToProfile(messageId: EngineMessage.Id, added: Bool) { + self.actionDisposable.set( + _internal_updateStarGiftAddedToProfile(account: self.account, messageId: messageId, added: added).startStrict() + ) + if let index = self.gifts.firstIndex(where: { $0.messageId == messageId }) { + self.gifts[index] = self.gifts[index].withSavedToProfile(added) + } + self.pushState() + } + + func convertStarGift(messageId: EngineMessage.Id) { + self.actionDisposable.set( + _internal_convertStarGift(account: self.account, messageId: messageId).startStrict() + ) + if let count = self.count { + self.count = max(0, count - 1) + } + self.gifts.removeAll(where: { $0.messageId == messageId }) + self.pushState() + } + private func pushState() { self.stateValue.set(.single(ProfileGiftsContext.State(gifts: self.gifts, count: self.count, dataState: self.dataState))) } @@ -332,6 +355,20 @@ public final class ProfileGiftsContext { public let nameHidden: Bool public let savedToProfile: Bool public let convertStars: Int64? + + public func withSavedToProfile(_ savedToProfile: Bool) -> StarGift { + return StarGift( + gift: self.gift, + fromPeer: self.fromPeer, + date: self.date, + text: self.text, + entities: self.entities, + messageId: self.messageId, + nameHidden: self.nameHidden, + savedToProfile: savedToProfile, + convertStars: self.convertStars + ) + } } public enum DataState: Equatable { @@ -373,6 +410,18 @@ public final class ProfileGiftsContext { impl.loadMore() } } + + public func updateStarGiftAddedToProfile(messageId: EngineMessage.Id, added: Bool) { + self.impl.with { impl in + impl.updateStarGiftAddedToProfile(messageId: messageId, added: added) + } + } + + public func convertStarGift(messageId: EngineMessage.Id) { + self.impl.with { impl in + impl.convertStarGift(messageId: messageId) + } + } } private extension ProfileGiftsContext.State.StarGift { diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 328418a38e..f4d9204e2a 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -459,6 +459,7 @@ swift_library( "//submodules/TelegramUI/Components/MinimizedContainer", "//submodules/TelegramUI/Components/SpaceWarpView", "//submodules/TelegramUI/Components/MiniAppListScreen", + "//submodules/TelegramUI/Components/Stars/StarsIntroScreen", "//submodules/TelegramUI/Components/Gifts/GiftOptionsScreen", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index 576330bd64..6f86b3e5d8 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -31,13 +31,16 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { private let backgroundMaskNode: ASImageNode private var linkHighlightingNode: LinkHighlightingNode? + private let mediaBackgroundMaskNode: ASImageNode private var mediaBackgroundContent: WallpaperBubbleBackgroundNode? - private let mediaBackgroundNode: NavigationBackgroundNode private let titleNode: TextNode private let subtitleNode: TextNode private let placeholderNode: StickerShimmerEffectNode private let animationNode: AnimatedStickerNode + private let ribbonBackgroundNode: ASImageNode + private let ribbonTextNode: TextNode + private var shimmerEffectNode: ShimmerEffectForegroundNode? private let buttonNode: HighlightTrackingButtonNode private let buttonStarsNode: PremiumStarsNode @@ -79,9 +82,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { self.backgroundMaskNode = ASImageNode() - self.mediaBackgroundNode = NavigationBackgroundNode(color: .clear) - self.mediaBackgroundNode.clipsToBounds = true - self.mediaBackgroundNode.cornerRadius = 24.0 + self.mediaBackgroundMaskNode = ASImageNode() self.titleNode = TextNode() self.titleNode.isUserInteractionEnabled = false @@ -107,19 +108,30 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { self.buttonTitleNode.isUserInteractionEnabled = false self.buttonTitleNode.displaysAsynchronously = false + self.ribbonBackgroundNode = ASImageNode() + self.ribbonBackgroundNode.displaysAsynchronously = false + + self.ribbonTextNode = TextNode() + self.ribbonTextNode.isUserInteractionEnabled = false + self.ribbonTextNode.displaysAsynchronously = false + super.init() self.addSubnode(self.labelNode) - self.addSubnode(self.mediaBackgroundNode) self.addSubnode(self.titleNode) self.addSubnode(self.subtitleNode) + self.addSubnode(self.subtitleNode) + self.addSubnode(self.placeholderNode) self.addSubnode(self.animationNode) self.addSubnode(self.buttonNode) self.buttonNode.addSubnode(self.buttonStarsNode) self.addSubnode(self.buttonTitleNode) + self.addSubnode(self.ribbonBackgroundNode) + self.addSubnode(self.ribbonTextNode) + self.buttonNode.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { @@ -226,7 +238,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode) - + let makeRibbonTextLayout = TextNode.asyncLayout(self.ribbonTextNode) + let cachedMaskBackgroundImage = self.cachedMaskBackgroundImage return { item, layoutConstants, _, _, _, _ in @@ -247,6 +260,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { var title = item.presentationData.strings.Notification_PremiumGift_Title var text = "" var buttonTitle = item.presentationData.strings.Notification_PremiumGift_View + var ribbonTitle = "" var hasServiceMessage = true var textSpacing: CGFloat = 0.0 for media in item.message.media { @@ -315,8 +329,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { buttonTitle = item.presentationData.strings.Notification_PremiumPrize_View hasServiceMessage = false } - case let .starGift(gift, convertStars, giftText, entities, nameHidden, savedToProfile, converted)://(amount, giftId, nameHidden, limitNumber, limitTotal, giftText, _): + case let .starGift(gift, convertStars, giftText, entities, nameHidden, savedToProfile, converted): let _ = nameHidden + //TODO:localize let authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? "" title = "Gift from \(authorName)" if let giftText, !giftText.isEmpty { @@ -344,6 +359,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } } animationFile = gift.file + if let availability = gift.availability { + ribbonTitle = "1 of \(availability.total)" + } default: break } @@ -377,6 +395,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) let (buttonTitleLayout, buttonTitleApply) = makeButtonTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: buttonTitle, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + let (ribbonTextLayout, ribbonTextApply) = makeRibbonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: ribbonTitle, font: Font.semibold(11.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) giftSize.height = titleLayout.size.height + textSpacing + subtitleLayout.size.height + 212.0 @@ -424,31 +444,10 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { return (backgroundSize.width, { boundingWidth in return (backgroundSize, { [weak self] animation, synchronousLoads, _ in if let strongSelf = self { - if strongSelf.item == nil { - strongSelf.animationNode.autoplay = true - - if let file = animationFile { - strongSelf.animationNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource), width: 384, height: 384, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) - if strongSelf.fetchDisposable == nil { - strongSelf.fetchDisposable = freeMediaFileResourceInteractiveFetched(postbox: item.context.account.postbox, userLocation: .other, fileReference: .message(message: MessageReference(item.message), media: file), resource: file.resource).start() - } - } else if animationName.hasPrefix("Gift") { - strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: animationName), width: 384, height: 384, playbackMode: .still(.end), mode: .direct(cachePathPrefix: nil)) - } - } - strongSelf.item = item - - strongSelf.updateVisibility() + let overlayColor = item.presentationData.theme.theme.overallDarkAppearance ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12) - strongSelf.labelNode.isHidden = !hasServiceMessage - let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - giftSize.width) / 2.0), y: hasServiceMessage ? labelLayout.size.height + 16.0 : 0.0), size: giftSize) let mediaBackgroundFrame = imageFrame.insetBy(dx: -2.0, dy: -2.0) - strongSelf.mediaBackgroundNode.frame = mediaBackgroundFrame - - strongSelf.mediaBackgroundNode.updateColor(color: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: item.controllerInteraction.enableFullTranslucency && dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), transition: .immediate) - strongSelf.mediaBackgroundNode.update(size: mediaBackgroundFrame.size, transition: .immediate) - strongSelf.buttonNode.backgroundColor = item.presentationData.theme.theme.overallDarkAppearance ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12) var iconSize = CGSize(width: 160.0, height: 160.0) var iconOffset: CGFloat = 0.0 @@ -456,13 +455,56 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { iconSize = CGSize(width: 120.0, height: 120.0) iconOffset = 32.0 } - strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - iconSize.width) / 2.0), y: mediaBackgroundFrame.minY - 16.0 + iconOffset), size: iconSize) + let animationFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - iconSize.width) / 2.0), y: mediaBackgroundFrame.minY - 16.0 + iconOffset), size: iconSize) + strongSelf.animationNode.frame = animationFrame + + if strongSelf.item == nil { + strongSelf.animationNode.started = { [weak self] in + if let strongSelf = self { + let current = CACurrentMediaTime() + if let setupTimestamp = strongSelf.setupTimestamp, current - setupTimestamp > 0.3 { + if !strongSelf.placeholderNode.alpha.isZero { + strongSelf.animationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + strongSelf.removePlaceholder(animated: true) + } + } else { + strongSelf.removePlaceholder(animated: false) + } + } + } + + strongSelf.animationNode.autoplay = true + + if let file = animationFile { + strongSelf.animationNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource), width: 384, height: 384, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) + if strongSelf.fetchDisposable == nil { + strongSelf.fetchDisposable = freeMediaFileResourceInteractiveFetched(postbox: item.context.account.postbox, userLocation: .other, fileReference: .message(message: MessageReference(item.message), media: file), resource: file.resource).start() + } + + if let immediateThumbnailData = file.immediateThumbnailData { + let shimmeringColor = bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.stickerPlaceholderShimmerColor, wallpaper: item.presentationData.theme.wallpaper) + strongSelf.placeholderNode.update(backgroundColor: nil, foregroundColor: overlayColor, shimmeringColor: shimmeringColor, data: immediateThumbnailData, size: animationFrame.size, enableEffect: item.context.sharedContext.energyUsageSettings.fullTranslucency) + } + } else if animationName.hasPrefix("Gift") { + strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: animationName), width: 384, height: 384, playbackMode: .still(.end), mode: .direct(cachePathPrefix: nil)) + } + } + strongSelf.item = item + + strongSelf.updateVisibility() + + strongSelf.labelNode.isHidden = !hasServiceMessage + + strongSelf.buttonNode.backgroundColor = overlayColor + strongSelf.animationNode.updateLayout(size: iconSize) + strongSelf.placeholderNode.frame = animationFrame let _ = labelApply() let _ = titleApply() let _ = subtitleApply() let _ = buttonTitleApply() + let _ = ribbonTextApply() let labelFrame = CGRect(origin: CGPoint(x: 8.0, y: 2.0), size: labelLayout.size) strongSelf.labelNode.frame = labelFrame @@ -479,23 +521,58 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let buttonSize = CGSize(width: buttonTitleLayout.size.width + 38.0, height: 34.0) strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonSize.width) / 2.0), y: subtitleFrame.maxY + 10.0), size: buttonSize) strongSelf.buttonStarsNode.frame = CGRect(origin: .zero, size: buttonSize) - - if item.controllerInteraction.presentationContext.backgroundNode?.hasExtraBubbleBackground() == true { - if strongSelf.mediaBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { - strongSelf.mediaBackgroundNode.isHidden = true - backgroundContent.clipsToBounds = true - backgroundContent.allowsGroupOpacity = true - backgroundContent.cornerRadius = 24.0 - - strongSelf.mediaBackgroundContent = backgroundContent - strongSelf.insertSubnode(backgroundContent, at: 0) + + if ribbonTextLayout.size.width > 0.0 { + if strongSelf.ribbonBackgroundNode.image == nil { + let ribbonImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/GiftRibbon"), color: overlayColor) + strongSelf.ribbonBackgroundNode.image = ribbonImage } + if let ribbonImage = strongSelf.ribbonBackgroundNode.image { + let ribbonFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.maxX - ribbonImage.size.width + 2.0, y: mediaBackgroundFrame.minY - 2.0), size: ribbonImage.size) + strongSelf.ribbonBackgroundNode.frame = ribbonFrame + + strongSelf.ribbonTextNode.transform = CATransform3DMakeRotation(.pi / 4.0, 0.0, 0.0, 1.0) + strongSelf.ribbonTextNode.bounds = CGRect(origin: .zero, size: ribbonTextLayout.size) + strongSelf.ribbonTextNode.position = ribbonFrame.center.offsetBy(dx: 7.0, dy: -6.0) + } + } + + if strongSelf.mediaBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { + backgroundContent.clipsToBounds = true + backgroundContent.cornerRadius = 24.0 - strongSelf.mediaBackgroundContent?.frame = mediaBackgroundFrame - } else { - strongSelf.mediaBackgroundNode.isHidden = false - strongSelf.mediaBackgroundContent?.removeFromSupernode() - strongSelf.mediaBackgroundContent = nil + strongSelf.mediaBackgroundContent = backgroundContent + strongSelf.insertSubnode(backgroundContent, at: 0) + } + + if let backgroundContent = strongSelf.mediaBackgroundContent { + if ribbonTextLayout.size.width > 0.0 { + let backgroundMaskFrame = mediaBackgroundFrame.insetBy(dx: -2.0, dy: -2.0) + backgroundContent.frame = backgroundMaskFrame + backgroundContent.cornerRadius = 0.0 + + if strongSelf.mediaBackgroundMaskNode.image?.size != mediaBackgroundFrame.size { + strongSelf.mediaBackgroundMaskNode.image = generateImage(backgroundMaskFrame.size, contextGenerator: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.clear(bounds) + + context.setFillColor(UIColor.black.cgColor) + context.addPath(UIBezierPath(roundedRect: bounds.insetBy(dx: 2.0, dy: 2.0), cornerRadius: 24.0).cgPath) + context.fillPath() + + if let ribbonImage = UIImage(bundleImageName: "Chat/Message/GiftRibbon"), let cgImage = ribbonImage.cgImage { + context.draw(cgImage, in: CGRect(origin: CGPoint(x: bounds.width - ribbonImage.size.width, y: bounds.height - ribbonImage.size.height), size: ribbonImage.size), byTiling: false) + } + }) + } + backgroundContent.view.mask = strongSelf.mediaBackgroundMaskNode.view + strongSelf.mediaBackgroundMaskNode.frame = CGRect(origin: .zero, size: backgroundMaskFrame.size) + } else { + backgroundContent.frame = mediaBackgroundFrame + backgroundContent.clipsToBounds = true + backgroundContent.cornerRadius = 24.0 + backgroundContent.view.mask = nil + } } let baseBackgroundFrame = labelFrame.offsetBy(dx: 0.0, dy: -11.0) @@ -645,7 +722,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { return ChatMessageBubbleContentTapAction(content: .ignore) } else if let backgroundNode = self.backgroundNode, backgroundNode.frame.contains(point) { return ChatMessageBubbleContentTapAction(content: .openMessage) - } else if self.mediaBackgroundNode.frame.contains(point) { + } else if self.mediaBackgroundContent?.frame.contains(point) == true { return ChatMessageBubbleContentTapAction(content: .openMessage) } else { return ChatMessageBubbleContentTapAction(content: .none) diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index 340460ce72..2f1b3612be 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -20,35 +20,62 @@ public final class GiftItemComponent: Component { } public struct Ribbon: Equatable { + public enum Color { + case red + case blue + + var colors: [UIColor] { + switch self { + case .red: + return [ + UIColor(rgb: 0xed1c26), + UIColor(rgb: 0xff5c55) + + ] + case .blue: + return [ + UIColor(rgb: 0x34a4fc), + UIColor(rgb: 0x6fd3ff) + ] + } + } + } public let text: String - public let color: UIColor + public let color: Color - public init(text: String, color: UIColor) { + public init(text: String, color: Color) { self.text = text self.color = color } } + public enum Peer: Equatable { + case peer(EnginePeer) + case anonymous + } + let context: AccountContext let theme: PresentationTheme - let peer: EnginePeer? - let subject: Subject + let peer: GiftItemComponent.Peer? + let subject: GiftItemComponent.Subject let title: String? let subtitle: String? let price: String let ribbon: Ribbon? let isLoading: Bool + let isHidden: Bool public init( context: AccountContext, theme: PresentationTheme, - peer: EnginePeer?, - subject: Subject, + peer: GiftItemComponent.Peer?, + subject: GiftItemComponent.Subject, title: String? = nil, subtitle: String? = nil, price: String, ribbon: Ribbon? = nil, - isLoading: Bool = false + isLoading: Bool = false, + isHidden: Bool = false ) { self.context = context self.theme = theme @@ -59,6 +86,7 @@ public final class GiftItemComponent: Component { self.price = price self.ribbon = ribbon self.isLoading = isLoading + self.isHidden = isHidden } public static func ==(lhs: GiftItemComponent, rhs: GiftItemComponent) -> Bool { @@ -89,6 +117,9 @@ public final class GiftItemComponent: Component { if lhs.isLoading != rhs.isLoading { return false } + if lhs.isHidden != rhs.isHidden { + return false + } return true } @@ -108,6 +139,9 @@ public final class GiftItemComponent: Component { private var animationLayer: InlineStickerItemLayer? + private var hiddenIconBackground: UIVisualEffectView? + private var hiddenIcon: UIImageView? + override init(frame: CGRect) { super.init(frame: frame) @@ -125,6 +159,8 @@ public final class GiftItemComponent: Component { } func update(component: GiftItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let isFirstTime = self.component == nil + self.component = component self.componentState = state @@ -201,8 +237,9 @@ public final class GiftItemComponent: Component { self.layer.addSublayer(animationLayer) } + let animationFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - iconSize.width) / 2.0), y: animationOffset), size: iconSize) if let animationLayer = self.animationLayer { - transition.setFrame(layer: animationLayer, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - iconSize.width) / 2.0), y: animationOffset), size: iconSize)) + transition.setFrame(layer: animationLayer, frame: animationFrame) } if let title = component.title { @@ -287,7 +324,7 @@ public final class GiftItemComponent: Component { ribbonTextView.bounds = CGRect(origin: .zero, size: ribbonTextSize) if self.ribbon.image == nil { - self.ribbon.image = generateGradientTintedImage(image: UIImage(bundleImageName: "Premium/GiftRibbon"), colors: [ribbon.color.withMultipliedBrightnessBy(1.1), ribbon.color.withMultipliedBrightnessBy(0.9)], direction: .diagonal) + self.ribbon.image = generateGradientTintedImage(image: UIImage(bundleImageName: "Premium/GiftRibbon"), colors: ribbon.color.colors, direction: .diagonal) } if let ribbonImage = self.ribbon.image { self.ribbon.frame = CGRect(origin: CGPoint(x: size.width - ribbonImage.size.width + 2.0, y: -2.0), size: ribbonImage.size) @@ -312,13 +349,64 @@ public final class GiftItemComponent: Component { self.avatarNode = avatarNode } - avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, displayDimensions: CGSize(width: 20.0, height: 20.0)) + switch peer { + case let .peer(peer): + avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, displayDimensions: CGSize(width: 20.0, height: 20.0)) + case .anonymous: + avatarNode.setPeer(context: component.context, theme: component.theme, peer: nil, overrideImage: .anonymousSavedMessagesIcon(isColored: true)) + } + avatarNode.frame = CGRect(origin: CGPoint(x: 2.0, y: 2.0), size: CGSize(width: 20.0, height: 20.0)) } self.backgroundLayer.backgroundColor = component.theme.list.itemBlocksBackgroundColor.cgColor transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: .zero, size: size)) + if component.isHidden { + let hiddenIconBackground: UIVisualEffectView + let hiddenIcon: UIImageView + if let currentBackground = self.hiddenIconBackground, let currentIcon = self.hiddenIcon { + hiddenIconBackground = currentBackground + hiddenIcon = currentIcon + } else { + let blurEffect: UIBlurEffect + if #available(iOS 13.0, *) { + blurEffect = UIBlurEffect(style: .systemThinMaterialDark) + } else { + blurEffect = UIBlurEffect(style: .dark) + } + hiddenIconBackground = UIVisualEffectView(effect: blurEffect) + hiddenIconBackground.clipsToBounds = true + hiddenIconBackground.layer.cornerRadius = 15.0 + self.hiddenIconBackground = hiddenIconBackground + + hiddenIcon = UIImageView(image: generateTintedImage(image: UIImage(bundleImageName: "Peer Info/HiddenIcon"), color: .white)) + self.hiddenIcon = hiddenIcon + + self.addSubview(hiddenIconBackground) + hiddenIconBackground.contentView.addSubview(hiddenIcon) + + if !isFirstTime { + hiddenIconBackground.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2) + hiddenIconBackground.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + + let iconSize = CGSize(width: 30.0, height: 30.0) + hiddenIconBackground.frame = iconSize.centered(around: animationFrame.center) + hiddenIcon.frame = CGRect(origin: .zero, size: iconSize) + } else { + if let hiddenIconBackground = self.hiddenIconBackground { + self.hiddenIconBackground = nil + self.hiddenIcon = nil + + hiddenIconBackground.layer.animateAlpha(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false, completion: { _ in + hiddenIconBackground.removeFromSuperview() + }) + hiddenIconBackground.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + } + } + return size } } diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index 78a2fdb53a..eb54cd350d 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -30,15 +30,18 @@ final class GiftOptionsScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext + let starsContext: StarsContext let peerId: EnginePeer.Id let premiumOptions: [CachedPremiumGiftOption] init( context: AccountContext, + starsContext: StarsContext, peerId: EnginePeer.Id, premiumOptions: [CachedPremiumGiftOption] ) { self.context = context + self.starsContext = starsContext self.peerId = peerId self.premiumOptions = premiumOptions } @@ -100,10 +103,15 @@ final class GiftOptionsScreenComponent: Component { private let header = ComponentView() + private let balanceTitle = ComponentView() + private let balanceValue = ComponentView() + private let balanceIcon = ComponentView() + private let premiumTitle = ComponentView() private let premiumDescription = ComponentView() private var premiumItems: [AnyHashable: ComponentView] = [:] - private var selectedPremiumGift: String? + private var inProgressPremiumGift: String? + private let purchaseDisposable = MetaDisposable() private let starsTitle = ComponentView() private let starsDescription = ComponentView() @@ -113,6 +121,9 @@ final class GiftOptionsScreenComponent: Component { private var isUpdating: Bool = false + private var starsStateDisposable: Disposable? + private var starsState: StarsContext.State? + private var component: GiftOptionsScreenComponent? private(set) weak var state: State? private var environment: EnvironmentType? @@ -147,6 +158,8 @@ final class GiftOptionsScreenComponent: Component { } deinit { + self.starsStateDisposable?.dispose() + self.purchaseDisposable.dispose() } func scrollToTop() { @@ -205,7 +218,6 @@ final class GiftOptionsScreenComponent: Component { transition.setScale(view: premiumTitleView, scale: premiumTitleScale) } - if let headerView = self.header.view { transition.setPosition(view: headerView, position: CGPoint(x: availableWidth / 2.0, y: topInset + headerView.bounds.height / 2.0 - 30.0 - premiumTitleOffset * premiumTitleScale)) transition.setScale(view: headerView, scale: premiumTitleScale) @@ -273,7 +285,7 @@ final class GiftOptionsScreenComponent: Component { ribbon: gift.availability != nil ? GiftItemComponent.Ribbon( text: "Limited", - color: UIColor(rgb: 0x58c1fe) + color: .blue ) : nil ) @@ -330,6 +342,88 @@ final class GiftOptionsScreenComponent: Component { } } + private func buyPremium(_ product: PremiumGiftProduct) { + guard let component = self.component, let inAppPurchaseManager = self.component?.context.inAppPurchaseManager, self.inProgressPremiumGift == nil else { + return + } + + self.inProgressPremiumGift = product.id + self.state?.updated() + + let (currency, amount) = product.storeProduct.priceCurrencyAndAmount + + addAppLogEvent(postbox: component.context.account.postbox, type: "premium_gift.promo_screen_accept") + + let purpose: AppStoreTransactionPurpose = .giftCode(peerIds: [component.peerId], boostPeer: nil, currency: currency, amount: amount) + let quantity: Int32 = 1 + + let _ = (component.context.engine.payments.canPurchasePremium(purpose: purpose) + |> deliverOnMainQueue).start(next: { [weak self] available in + if let strongSelf = self { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + if available { + strongSelf.purchaseDisposable.set((inAppPurchaseManager.buyProduct(product.storeProduct, quantity: quantity, purpose: purpose) + |> deliverOnMainQueue).start(next: { [weak self] status in + guard let self, case .purchased = status, let controller = self.environment?.controller(), let navigationController = controller.navigationController as? NavigationController else { + return + } + + var controllers = navigationController.viewControllers + controllers = controllers.filter { !($0 is GiftOptionsScreen) } + var foundController = false + for controller in controllers.reversed() { + if let chatController = controller as? ChatController, case .peer(id: component.peerId) = chatController.chatLocation { + chatController.hintPlayNextOutgoingGift() + foundController = true + break + } + } + if !foundController { + let chatController = component.context.sharedContext.makeChatController(context: component.context, chatLocation: .peer(id: component.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil) + chatController.hintPlayNextOutgoingGift() + controllers.append(chatController) + } + navigationController.setViewControllers(controllers, animated: true) + }, error: { [weak self] error in + guard let self, let controller = self.environment?.controller() else { + return + } + self.inProgressPremiumGift = nil + self.state?.updated(transition: .immediate) + + var errorText: String? + switch error { + case .generic: + errorText = presentationData.strings.Premium_Purchase_ErrorUnknown + case .network: + errorText = presentationData.strings.Premium_Purchase_ErrorNetwork + case .notAllowed: + errorText = presentationData.strings.Premium_Purchase_ErrorNotAllowed + case .cantMakePayments: + errorText = presentationData.strings.Premium_Purchase_ErrorCantMakePayments + case .assignFailed: + errorText = presentationData.strings.Premium_Purchase_ErrorUnknown + case .tryLater: + errorText = presentationData.strings.Premium_Purchase_ErrorUnknown + case .cancelled: + break + } + + if let errorText { + addAppLogEvent(postbox: component.context.account.postbox, type: "premium_gift.promo_screen_fail") + + let alertController = textAlertController(context: component.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + controller.present(alertController, in: .window(.root)) + } + })) + } else { + self?.inProgressPremiumGift = nil + self?.state?.updated(transition: .immediate) + } + } + }) + } + func update(component: GiftOptionsScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -340,13 +434,21 @@ final class GiftOptionsScreenComponent: Component { let controller = environment.controller let themeUpdated = self.environment?.theme !== environment.theme self.environment = environment + self.state = state if self.component == nil { - + self.starsStateDisposable = (component.starsContext.state + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let self else { + return + } + self.starsState = state + if !self.isUpdating { + self.state?.updated() + } + }) } - self.component = component - self.state = state if themeUpdated { self.backgroundColor = environment.theme.list.blocksBackgroundColor @@ -451,6 +553,55 @@ final class GiftOptionsScreenComponent: Component { transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame) } + let balanceTitleSize = self.balanceTitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.Stars_Purchase_Balance, + font: Font.regular(14.0), + textColor: environment.theme.actionSheet.primaryTextColor + )), + maximumNumberOfLines: 1 + )), + environment: {}, + containerSize: availableSize + ) + let balanceValueSize = self.balanceValue.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: presentationStringsFormattedNumber(Int32(self.starsState?.balance ?? 0), environment.dateTimeFormat.groupingSeparator), + font: Font.semibold(14.0), + textColor: environment.theme.actionSheet.primaryTextColor + )), + maximumNumberOfLines: 1 + )), + environment: {}, + containerSize: availableSize + ) + let balanceIconSize = self.balanceIcon.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent(name: "Premium/Stars/StarSmall", tintColor: nil)), + environment: {}, + containerSize: availableSize + ) + + if let balanceTitleView = self.balanceTitle.view, let balanceValueView = self.balanceValue.view, let balanceIconView = self.balanceIcon.view { + if balanceTitleView.superview == nil { + self.addSubview(balanceTitleView) + self.addSubview(balanceValueView) + self.addSubview(balanceIconView) + } + let navigationHeight = environment.navigationHeight - environment.statusBarHeight + let topBalanceOriginY = environment.statusBarHeight + (navigationHeight - balanceTitleSize.height - balanceValueSize.height) / 2.0 + balanceTitleView.center = CGPoint(x: availableSize.width - 16.0 - environment.safeInsets.right - balanceTitleSize.width / 2.0, y: topBalanceOriginY + balanceTitleSize.height / 2.0) + balanceTitleView.bounds = CGRect(origin: .zero, size: balanceTitleSize) + balanceValueView.center = CGPoint(x: availableSize.width - 16.0 - environment.safeInsets.right - balanceValueSize.width / 2.0, y: topBalanceOriginY + balanceTitleSize.height + balanceValueSize.height / 2.0) + balanceValueView.bounds = CGRect(origin: .zero, size: balanceValueSize) + balanceIconView.center = CGPoint(x: availableSize.width - 16.0 - environment.safeInsets.right - balanceValueSize.width - balanceIconSize.width / 2.0 - 2.0, y: topBalanceOriginY + balanceTitleSize.height + balanceValueSize.height / 2.0 - UIScreenPixel) + balanceIconView.bounds = CGRect(origin: .zero, size: balanceIconSize) + } + let premiumTitleSize = self.premiumTitle.update( transition: transition, component: AnyComponent(MultilineTextComponent( @@ -494,8 +645,13 @@ final class GiftOptionsScreenComponent: Component { return nil } }, - tapAction: { _, _ in - + tapAction: { [weak self] _, _ in + guard let self, let component = self.component, let environment = self.environment else { + return + } + let introController = component.context.sharedContext.makePremiumIntroController(context: component.context, source: .settings, forceDark: false, dismissed: nil) + introController.navigationPresentation = .modal + environment.controller()?.push(introController) } )), environment: {}, @@ -561,21 +717,15 @@ final class GiftOptionsScreenComponent: Component { ribbon: product.discount.flatMap { GiftItemComponent.Ribbon( text: "-\($0)%", - color: UIColor(rgb: 0xfa4846) + color: .red ) }, - isLoading: self.selectedPremiumGift == product.id + isLoading: self.inProgressPremiumGift == product.id ) ), effectAlignment: .center, action: { [weak self] in - self?.selectedPremiumGift = product.id - self?.state?.updated() - - Queue.mainQueue().after(4.0, { - self?.selectedPremiumGift = nil - self?.state?.updated() - }) + self?.buyPremium(product) }, animateAlpha: false ) @@ -658,8 +808,13 @@ final class GiftOptionsScreenComponent: Component { return nil } }, - tapAction: { _, _ in - + tapAction: { [weak self] _, _ in + guard let self, let component = self.component, let environment = self.environment else { + return + } + let introController = component.context.sharedContext.makeStarsIntroScreen(context: component.context) + introController.navigationPresentation = .modal + environment.controller()?.push(introController) } )), environment: {}, @@ -859,11 +1014,17 @@ final class GiftOptionsScreenComponent: Component { public final class GiftOptionsScreen: ViewControllerComponentContainer, GiftOptionsScreenProtocol { private let context: AccountContext - public init(context: AccountContext, peerId: EnginePeer.Id, premiumOptions: [CachedPremiumGiftOption]) { + public init( + context: AccountContext, + starsContext: StarsContext, + peerId: EnginePeer.Id, + premiumOptions: [CachedPremiumGiftOption] + ) { self.context = context super.init(context: context, component: GiftOptionsScreenComponent( context: context, + starsContext: starsContext, peerId: peerId, premiumOptions: premiumOptions ), navigationBarAppearance: .none, theme: .default, updatedPresentationData: nil) diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift index 8a37f061b5..35f48321e7 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift @@ -148,6 +148,8 @@ final class ChatGiftPreviewItemNode: ListViewItemNode { private let disposable = MetaDisposable() + private var initialBubbleHeight: CGFloat? + init() { self.topStripeNode = ASDisplayNode() self.topStripeNode.isLayerBacked = true @@ -235,14 +237,13 @@ final class ChatGiftPreviewItemNode: ListViewItemNode { }) itemNode!.isUserInteractionEnabled = false messageNodes.append(itemNode!) + + self.initialBubbleHeight = itemNode?.frame.height } nodes = messageNodes } var contentSize = CGSize(width: params.width, height: 4.0 + 4.0) -// for node in nodes { -// contentSize.height += node.frame.size.height -// } contentSize.height = 346.0 insets = itemListNeighborsGroupedInsets(neighbors, params) if params.width <= 320.0 { @@ -269,7 +270,13 @@ final class ChatGiftPreviewItemNode: ListViewItemNode { if node.supernode == nil { strongSelf.containerNode.addSubnode(node) } - node.updateFrame(CGRect(origin: CGPoint(x: 0.0, y: floor((contentSize.height - node.frame.size.height) / 2.0)), size: node.frame.size), within: layoutSize) + let bubbleHeight: CGFloat + if let initialBubbleHeight = strongSelf.initialBubbleHeight { + bubbleHeight = max(node.frame.height, initialBubbleHeight) + } else { + bubbleHeight = node.frame.height + } + node.updateFrame(CGRect(origin: CGPoint(x: 0.0, y: floor((contentSize.height - bubbleHeight) / 2.0)), size: node.frame.size), within: layoutSize) //topOffset += node.frame.size.height } diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index f26cb3deb2..805b8aaf6c 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -90,6 +90,14 @@ final class GiftSetupScreenComponent: Component { private var starImage: (UIImage, PresentationTheme)? + private var optionsDisposable: Disposable? + private(set) var options: [StarsTopUpOption] = [] { + didSet { + self.optionsPromise.set(self.options) + } + } + private let optionsPromise = ValuePromise<[StarsTopUpOption]?>(nil) + override init(frame: CGRect) { self.scrollView = ScrollView() self.scrollView.showsVerticalScrollIndicator = true @@ -159,45 +167,77 @@ final class GiftSetupScreenComponent: Component { } func proceed() { - guard let component = self.component else { + guard let component = self.component, let starsContext = component.context.starsContext, let starsState = starsContext.currentState else { return } - let source: BotPaymentInvoiceSource = .starGift(hideName: self.hideName, peerId: component.peerId, giftId: component.gift.id, text: self.textInputState.text.string, entities: []) - let inputData = BotCheckoutController.InputData.fetch(context: component.context, source: source) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } - let _ = (inputData - |> deliverOnMainQueue).startStandalone(next: { [weak self] inputData in - guard let inputData else { + let proceed = { [weak self] in + guard let self else { return } - let _ = (component.context.engine.payments.sendStarsPaymentForm(formId: inputData.form.id, source: source) - |> deliverOnMainQueue).start(next: { [weak self] result in - guard let self, let controller = self.environment?.controller(), let navigationController = controller.navigationController as? NavigationController else { + let source: BotPaymentInvoiceSource = .starGift(hideName: self.hideName, peerId: component.peerId, giftId: component.gift.id, text: self.textInputState.text.string, entities: []) + let inputData = BotCheckoutController.InputData.fetch(context: component.context, source: source) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + + let _ = (inputData + |> deliverOnMainQueue).startStandalone(next: { [weak self] inputData in + guard let inputData else { return } - - var controllers = navigationController.viewControllers - controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) } - var foundController = false - for controller in controllers.reversed() { - if let chatController = controller as? ChatController, case .peer(id: component.peerId) = chatController.chatLocation { - chatController.hintPlayNextOutgoingGift() - foundController = true - break + let _ = (component.context.engine.payments.sendStarsPaymentForm(formId: inputData.form.id, source: source) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self, let controller = self.environment?.controller(), let navigationController = controller.navigationController as? NavigationController else { + return } - } - if !foundController { - let chatController = component.context.sharedContext.makeChatController(context: component.context, chatLocation: .peer(id: component.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil) - chatController.hintPlayNextOutgoingGift() - controllers.append(chatController) - } - navigationController.setViewControllers(controllers, animated: true) + + var controllers = navigationController.viewControllers + controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) } + var foundController = false + for controller in controllers.reversed() { + if let chatController = controller as? ChatController, case .peer(id: component.peerId) = chatController.chatLocation { + chatController.hintPlayNextOutgoingGift() + foundController = true + break + } + } + if !foundController { + let chatController = component.context.sharedContext.makeChatController(context: component.context, chatLocation: .peer(id: component.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil) + chatController.hintPlayNextOutgoingGift() + controllers.append(chatController) + } + navigationController.setViewControllers(controllers, animated: true) + }) }) - }) + } + + if starsState.balance < component.gift.price { + let _ = (self.optionsPromise.get() + |> filter { $0 != nil } + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak self] options in + guard let self, let component = self.component, let controller = self.environment?.controller() else { + return + } + let purchaseController = component.context.sharedContext.makeStarsPurchaseScreen( + context: component.context, + starsContext: starsContext, + options: options ?? [], + purpose: .starGift(peerId: component.peerId, requiredStars: component.gift.price), + completion: { [weak starsContext] stars in + starsContext?.add(balance: stars) + Queue.mainQueue().after(0.1) { + proceed() + } + } + ) + controller.push(purchaseController) + }) + } else { + proceed() + } } func update(component: GiftSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD index 342a21824d..6bb84c21b4 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD @@ -25,6 +25,7 @@ swift_library( "//submodules/Components/ViewControllerComponent", "//submodules/Components/BundleIconComponent", "//submodules/Components/MultilineTextComponent", + "//submodules/Components/MultilineTextWithEntitiesComponent", "//submodules/Components/BalancedTextComponent", "//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/TelegramUI/Components/ListActionItemComponent", diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 28b3467edd..b569c9bb4e 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -12,6 +12,7 @@ import ComponentFlow import ViewControllerComponent import SheetComponent import MultilineTextComponent +import MultilineTextWithEntitiesComponent import BundleIconComponent import SolidRoundedButtonComponent import Markdown @@ -32,6 +33,7 @@ private final class GiftViewSheetContent: CombinedComponent { let openPeer: (EnginePeer) -> Void let updateSavedToProfile: (Bool) -> Void let convertToStars: () -> Void + let openStarsIntro: () -> Void init( context: AccountContext, @@ -39,7 +41,8 @@ private final class GiftViewSheetContent: CombinedComponent { cancel: @escaping (Bool) -> Void, openPeer: @escaping (EnginePeer) -> Void, updateSavedToProfile: @escaping (Bool) -> Void, - convertToStars: @escaping () -> Void + convertToStars: @escaping () -> Void, + openStarsIntro: @escaping () -> Void ) { self.context = context self.subject = subject @@ -47,6 +50,7 @@ private final class GiftViewSheetContent: CombinedComponent { self.openPeer = openPeer self.updateSavedToProfile = updateSavedToProfile self.convertToStars = convertToStars + self.openStarsIntro = openStarsIntro } static func ==(lhs: GiftViewSheetContent, rhs: GiftViewSheetContent) -> Bool { @@ -176,7 +180,7 @@ private final class GiftViewSheetContent: CombinedComponent { limitNumber = arguments.gift.availability?.remains limitTotal = arguments.gift.availability?.total convertStars = arguments.convertStars - incoming = arguments.incoming + incoming = arguments.incoming || arguments.peerId == component.context.account.peerId savedToProfile = arguments.savedToProfile converted = arguments.converted } else { @@ -259,7 +263,13 @@ private final class GiftViewSheetContent: CombinedComponent { ) let tableFont = Font.regular(15.0) + let tableBoldFont = Font.semibold(15.0) + let tableItalicFont = Font.italic(15.0) + let tableBoldItalicFont = Font.semiboldItalic(15.0) + let tableMonospaceFont = Font.monospace(15.0) + let tableTextColor = theme.list.itemPrimaryTextColor + let tableLinkColor = theme.list.itemAccentColor var tableItems: [TableComponent.Item] = [] if let peerId = component.subject.arguments?.peerId, let peer = state.peerMap[peerId] { @@ -291,6 +301,18 @@ private final class GiftViewSheetContent: CombinedComponent { ) ) )) + } else { + tableItems.append(.init( + id: "from", + title: strings.Stars_Transaction_From, + component: AnyComponent( + PeerCellComponent( + context: component.context, + theme: theme, + peer: nil + ) + ) + )) } tableItems.append(.init( @@ -312,11 +334,19 @@ private final class GiftViewSheetContent: CombinedComponent { } if let text { + let attributedText = stringWithAppliedEntities(text, entities: entities ?? [], baseColor: tableTextColor, linkColor: tableLinkColor, baseFont: tableFont, linkFont: tableFont, boldFont: tableBoldFont, italicFont: tableItalicFont, boldItalicFont: tableBoldItalicFont, fixedFont: tableMonospaceFont, blockQuoteFont: tableFont, message: nil) + tableItems.append(.init( id: "text", title: nil, component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: text, font: tableFont, textColor: tableTextColor))) + MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: theme.list.mediaPlaceholderColor, + text: .plain(attributedText) + ) ) )) } @@ -331,40 +361,7 @@ private final class GiftViewSheetContent: CombinedComponent { ) let textFont = Font.regular(15.0) -// let boldTextFont = Font.semibold(15.0) -// let textColor = theme.actionSheet.secondaryTextColor let linkColor = theme.actionSheet.controlAccentColor -// let destructiveColor = theme.actionSheet.destructiveActionTextColor -// let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in -// return (TelegramTextAttributes.URL, contents) -// }) -// let additional = additional.update( -// component: BalancedTextComponent( -// text: .markdown(text: additionalText, attributes: markdownAttributes), -// horizontalAlignment: .center, -// maximumNumberOfLines: 0, -// lineSpacing: 0.2, -// highlightColor: linkColor.withAlphaComponent(0.2), -// highlightAction: { attributes in -// if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { -// return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) -// } else { -// return nil -// } -// }, -// tapAction: { attributes, _ in -// if let controller = controller() as? GiftViewScreen, let navigationController = controller.navigationController as? NavigationController { -// let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } -// component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_Transaction_Terms_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) -// component.cancel(true) -// } -// } -// ), -// availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), -// transition: .immediate -// ) - - context.add(title .position(CGPoint(x: context.availableSize.width / 2.0, y: 177.0)) @@ -417,7 +414,7 @@ private final class GiftViewSheetContent: CombinedComponent { } }, tapAction: { _, _ in - + component.openStarsIntro() } ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), @@ -467,31 +464,7 @@ private final class GiftViewSheetContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + table.size.height / 2.0)) ) originY += table.size.height + 23.0 - -// context.add(additional -// .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + additional.size.height / 2.0)) -// ) -// originY += additional.size.height + 23.0 - -// if let statusText { -// originY += 7.0 -// let status = status.update( -// component: BalancedTextComponent( -// text: .plain(NSAttributedString(string: statusText, font: textFont, textColor: statusIsDestructive ? destructiveColor : textColor)), -// horizontalAlignment: .center, -// maximumNumberOfLines: 0, -// lineSpacing: 0.1 -// ), -// availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), -// transition: .immediate -// ) -// context.add(status -// .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + status.size.height / 2.0)) -// ) -// originY += status.size.height + (statusIsDestructive ? 23.0 : 13.0) -// } - - + if incoming && !converted { let button = button.update( component: SolidRoundedButtonComponent( @@ -545,6 +518,33 @@ private final class GiftViewSheetContent: CombinedComponent { .position(CGPoint(x: secondaryButtonFrame.midX, y: secondaryButtonFrame.midY)) ) originY += secondaryButton.size.height + } else { + let button = button.update( + component: SolidRoundedButtonComponent( + title: strings.Common_OK, + theme: SolidRoundedButtonComponent.Theme(theme: theme), + font: .bold, + fontSize: 17.0, + height: 50.0, + cornerRadius: 10.0, + gloss: false, + iconName: nil, + animationName: nil, + iconPosition: .left, + isLoading: state.inProgress, + action: { + component.cancel(true) + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + transition: context.transition + ) + let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: button.size) + context.add(button + .position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY)) + ) + originY += button.size.height + originY += 7.0 } context.add(closeButton @@ -566,19 +566,22 @@ private final class GiftViewSheetComponent: CombinedComponent { let openPeer: (EnginePeer) -> Void let updateSavedToProfile: (Bool) -> Void let convertToStars: () -> Void + let openStarsIntro: () -> Void init( context: AccountContext, subject: GiftViewScreen.Subject, openPeer: @escaping (EnginePeer) -> Void, updateSavedToProfile: @escaping (Bool) -> Void, - convertToStars: @escaping () -> Void + convertToStars: @escaping () -> Void, + openStarsIntro: @escaping () -> Void ) { self.context = context self.subject = subject self.openPeer = openPeer self.updateSavedToProfile = updateSavedToProfile self.convertToStars = convertToStars + self.openStarsIntro = openStarsIntro } static func ==(lhs: GiftViewSheetComponent, rhs: GiftViewSheetComponent) -> Bool { @@ -620,7 +623,8 @@ private final class GiftViewSheetComponent: CombinedComponent { }, openPeer: context.component.openPeer, updateSavedToProfile: context.component.updateSavedToProfile, - convertToStars: context.component.convertToStars + convertToStars: context.component.convertToStars, + openStarsIntro: context.component.openStarsIntro )), backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), followContentSizeChanges: true, @@ -698,7 +702,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { return (message.id.peerId, message.id, message.flags.contains(.Incoming), gift, convertStars, text, entities, nameHidden, savedToProfile, converted) } case let .profileGift(peerId, gift): - return (peerId, gift.messageId, false, gift.gift, gift.convertStars ?? 0, gift.text, gift.entities, gift.nameHidden, true, false) + return (peerId, gift.messageId, false, gift.gift, gift.convertStars ?? 0, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, false) } return nil } @@ -712,13 +716,17 @@ public class GiftViewScreen: ViewControllerComponentContainer { public init( context: AccountContext, subject: GiftViewScreen.Subject, - forceDark: Bool = false + forceDark: Bool = false, + updateSavedToProfile: ((Bool) -> Void)? = nil, + convertToStars: (() -> Void)? = nil ) { self.context = context var openPeerImpl: ((EnginePeer) -> Void)? var updateSavedToProfileImpl: ((Bool) -> Void)? var convertToStarsImpl: (() -> Void)? + var openStarsIntroImpl: (() -> Void)? + super.init( context: context, component: GiftViewSheetComponent( @@ -732,6 +740,9 @@ public class GiftViewScreen: ViewControllerComponentContainer { }, convertToStars: { convertToStarsImpl?() + }, + openStarsIntro: { + openStarsIntroImpl?() } ), navigationBarAppearance: .none, @@ -764,8 +775,12 @@ public class GiftViewScreen: ViewControllerComponentContainer { guard let self, let arguments = subject.arguments, let messageId = arguments.messageId else { return } - let _ = (context.engine.payments.updateStarGiftAddedToProfile(messageId: messageId, added: added) - |> deliverOnMainQueue).startStandalone() + if let updateSavedToProfile { + updateSavedToProfile(added) + } else { + let _ = (context.engine.payments.updateStarGiftAddedToProfile(messageId: messageId, added: added) + |> deliverOnMainQueue).startStandalone() + } self.dismissAnimated() @@ -774,7 +789,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { if let lastController = navigationController.viewControllers.last as? ViewController { let resultController = UndoOverlayController( presentationData: presentationData, - content: .sticker(context: context, file: arguments.gift.file, loop: false, title: "Gift Saved to Profile", text: "The gift is now displayed in your profile.", undoText: nil, customAction: nil), + content: .sticker(context: context, file: arguments.gift.file, loop: false, title: added ? "Gift Saved to Profile" : "Gift Removed from Profile", text: added ? "The gift is now displayed in your profile." : "The gift is no longer displayed in your profile.", undoText: nil, customAction: nil), elevatedLayout: lastController is ChatController, action: { _ in return true} ) @@ -795,9 +810,12 @@ public class GiftViewScreen: ViewControllerComponentContainer { actions: [ TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: "Convert", action: { [weak self, weak navigationController] in - let _ = (context.engine.payments.convertStarGift(messageId: messageId) - |> deliverOnMainQueue).startStandalone() - + if let convertToStars { + convertToStars() + } else { + let _ = (context.engine.payments.convertStarGift(messageId: messageId) + |> deliverOnMainQueue).startStandalone() + } self?.dismissAnimated() if let navigationController { @@ -827,6 +845,14 @@ public class GiftViewScreen: ViewControllerComponentContainer { ) self.present(controller, in: .window(.root)) } + openStarsIntroImpl = { [weak self] in + guard let self else { + return + } + let introController = context.sharedContext.makeStarsIntroScreen(context: context) + introController.navigationPresentation = .modal + self.push(introController) + } } required public init(coder aDecoder: NSCoder) { @@ -1130,14 +1156,18 @@ private final class PeerCellComponent: Component { } final class View: UIView { - private let avatar = ComponentView() + private let avatarNode: AvatarNode private let text = ComponentView() private var component: PeerCellComponent? private weak var state: EmptyComponentState? override init(frame: CGRect) { + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 8.0)) + super.init(frame: frame) + + self.addSubnode(self.avatarNode) } required init?(coder: NSCoder) { @@ -1152,29 +1182,25 @@ private final class PeerCellComponent: Component { let spacing: CGFloat = 6.0 let peerName: String - let peer: StarsContext.State.Transaction.Peer + let avatarOverride: AvatarNodeImageOverride? if let peerValue = component.peer { peerName = peerValue.compactDisplayTitle - peer = .peer(peerValue) + avatarOverride = nil } else { + //TODO:localize peerName = "Hidden Name" - peer = .fragment + avatarOverride = .anonymousSavedMessagesIcon(isColored: true) } - let avatarNaturalSize = self.avatar.update( - transition: .immediate, - component: AnyComponent( - StarsAvatarComponent(context: component.context, theme: component.theme, peer: peer, photo: nil, media: [], backgroundColor: .clear) - ), - environment: {}, - containerSize: CGSize(width: 40.0, height: 40.0) - ) + let avatarNaturalSize = CGSize(width: 40.0, height: 40.0) + self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer, overrideImage: avatarOverride) + self.avatarNode.bounds = CGRect(origin: .zero, size: avatarNaturalSize) let textSize = self.text.update( transition: .immediate, component: AnyComponent( MultilineTextComponent( - text: .plain(NSAttributedString(string: peerName, font: Font.regular(15.0), textColor: component.theme.list.itemAccentColor, paragraphAlignment: .left)) + text: .plain(NSAttributedString(string: peerName, font: Font.regular(15.0), textColor: component.peer != nil ? component.theme.list.itemAccentColor : component.theme.list.itemPrimaryTextColor, paragraphAlignment: .left)) ) ), environment: {}, @@ -1184,15 +1210,7 @@ private final class PeerCellComponent: Component { let size = CGSize(width: avatarSize.width + textSize.width + spacing, height: textSize.height) let avatarFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - avatarSize.height) / 2.0)), size: avatarSize) - - if let view = self.avatar.view { - if view.superview == nil { - self.addSubview(view) - } - let scale = avatarSize.width / avatarNaturalSize.width - view.transform = CGAffineTransform(scaleX: scale, y: scale) - view.frame = avatarFrame - } + self.avatarNode.frame = avatarFrame if let view = self.text.view { if view.superview == nil { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedChannelsPane.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedChannelsPane.swift index 991453a817..2dd9c7b988 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedChannelsPane.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedChannelsPane.swift @@ -100,7 +100,6 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode private let listNode: ListView private var currentEntries: [RecommendedChannelsListEntry] = [] private var currentState: (RecommendedChannels?, Bool)? - private var canLoadMore: Bool = false private var enqueuedTransactions: [RecommendedChannelsListTransaction] = [] private var unlockBackground: UIImageView? diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD index 6517691155..27132baef3 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD @@ -51,6 +51,7 @@ swift_library( "//submodules/TelegramUI/Components/Gifts/GiftItemComponent", "//submodules/TelegramUI/Components/Gifts/GiftViewScreen", "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/Components/BalancedTextComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index 9a4c7f8122..f99b353467 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -15,12 +15,13 @@ import MergeLists import ItemListUI import ChatControllerInteraction import MultilineTextComponent +import BalancedTextComponent import Markdown import PeerInfoPaneNode import GiftItemComponent import PlainButtonComponent import GiftViewScreen -import ButtonComponent +import SolidRoundedButtonNode public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate { private let context: AccountContext @@ -37,6 +38,10 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr private let backgroundNode: ASDisplayNode private let scrollNode: ASScrollNode + private var unlockBackground: UIImageView? + private var unlockText: ComponentView? + private var unlockButton: SolidRoundedButtonNode? + private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData)? private var theme: PresentationTheme? @@ -82,7 +87,8 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr guard let self else { return } - self.statusPromise.set(.single(PeerInfoStatusData(text: "\(state.count ?? 0) gifts", isActivity: true, key: .gifts))) + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + self.statusPromise.set(.single(PeerInfoStatusData(text: presentationData.strings.SharedMedia_GiftCount(state.count ?? 0), isActivity: true, key: .gifts))) self.starsProducts = state.gifts if !self.didSetReady { @@ -149,6 +155,13 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } if isVisible { + let ribbonText: String? + if let availability = product.gift.availability { + //TODO:localize + ribbonText = "1 of \(compactNumericCountString(Int(availability.total)))" + } else { + ribbonText = nil + } let _ = visibleItem.update( transition: itemTransition, component: AnyComponent( @@ -157,26 +170,36 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr GiftItemComponent( context: self.context, theme: params.presentationData.theme, - peer: product.fromPeer, + peer: product.fromPeer.flatMap { .peer($0) } ?? .anonymous, subject: .starGift(product.gift.id, product.gift.file), price: "â­ï¸ \(product.gift.price)", - ribbon: product.gift.availability != nil ? - GiftItemComponent.Ribbon( - text: "1 of 1K", - color: UIColor(rgb: 0x58c1fe) - ) - : nil + ribbon: ribbonText.flatMap { GiftItemComponent.Ribbon(text: $0, color: .blue) }, + isHidden: !product.savedToProfile ) ), effectAlignment: .center, action: { [weak self] in - if let self { - let controller = GiftViewScreen( - context: self.context, - subject: .profileGift(self.peerId, product) - ) - self.parentController?.push(controller) + guard let self else { + return } + let controller = GiftViewScreen( + context: self.context, + subject: .profileGift(self.peerId, product), + updateSavedToProfile: { [weak self] added in + guard let self, let messageId = product.messageId else { + return + } + self.profileGifts.updateStarGiftAddedToProfile(messageId: messageId, added: added) + }, + convertToStars: { [weak self] in + guard let self, let messageId = product.messageId else { + return + } + self.profileGifts.convertStarGift(messageId: messageId) + } + ) + self.parentController?.push(controller) + }, animateAlpha: false ) @@ -198,46 +221,124 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } } - let contentHeight = ceil(CGFloat(starsProducts.count) / 3.0) * starsOptionSize.height + 60.0 + params.bottomInset + 16.0 + var contentHeight = ceil(CGFloat(starsProducts.count) / 3.0) * starsOptionSize.height + 60.0 + 16.0 -// //TODO:localize -// let buttonSize = self.button.update( -// transition: .immediate, -// component: AnyComponent(ButtonComponent( -// background: ButtonComponent.Background( -// color: params.presentationData.theme.list.itemCheckColors.fillColor, -// foreground: params.presentationData.theme.list.itemCheckColors.foregroundColor, -// pressedColor: params.presentationData.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), -// cornerRadius: 10.0 -// ), -// content: AnyComponentWithIdentity( -// id: AnyHashable(0), -// component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "Send Gifts to Friends", font: Font.semibold(17.0), textColor: )params.presentationData.theme.list.itemCheckColors.foregroundColor))) -// ), -// isEnabled: true, -// displaysProgress: false, -// action: { -// -// } -// )), -// environment: {}, -// containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50) -// ) -// if let buttonView = self.button.view { -// if buttonView.superview == nil { -// self.addSubview(buttonView) -// } -// buttonView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - buttonSize.height), size: buttonSize) -// } - -// contentHeight += 100.0 + if self.peerId == self.context.account.peerId { + let transition = ComponentTransition.immediate + + let size = params.size + let sideInset = params.sideInset + let bottomInset = params.bottomInset + let presentationData = params.presentationData + + let themeUpdated = self.theme !== presentationData.theme + self.theme = presentationData.theme + + let unlockText: ComponentView + let unlockBackground: UIImageView + let unlockButton: SolidRoundedButtonNode + if let current = self.unlockText { + unlockText = current + } else { + unlockText = ComponentView() + self.unlockText = unlockText + } + + if let current = self.unlockBackground { + unlockBackground = current + } else { + unlockBackground = UIImageView() + unlockBackground.contentMode = .scaleToFill + self.view.addSubview(unlockBackground) + self.unlockBackground = unlockBackground + } + + if let current = self.unlockButton { + unlockButton = current + } else { + unlockButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: presentationData.theme), height: 50.0, cornerRadius: 10.0) + self.view.addSubview(unlockButton.view) + self.unlockButton = unlockButton + + //TODO:localize + unlockButton.title = "Send Gifts to Friends" + + unlockButton.pressed = { [weak self] in + self?.buttonPressed() + } + } + if themeUpdated { + let topColor = presentationData.theme.list.plainBackgroundColor.withAlphaComponent(0.0) + let bottomColor = presentationData.theme.list.plainBackgroundColor + unlockBackground.image = generateGradientImage(size: CGSize(width: 1.0, height: 170.0), colors: [topColor, bottomColor, bottomColor], locations: [0.0, 0.3, 1.0]) + unlockButton.updateTheme(SolidRoundedButtonTheme(theme: presentationData.theme)) + } + + let textFont = Font.regular(13.0) + let boldTextFont = Font.semibold(13.0) + let textColor = presentationData.theme.list.itemSecondaryTextColor + let linkColor = presentationData.theme.list.itemAccentColor + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: boldTextFont, textColor: linkColor), linkAttribute: { _ in + return nil + }) + + let scrollOffset: CGFloat = min(0.0, self.scrollNode.view.contentOffset.y + bottomInset + 80.0) + + transition.setFrame(view: unlockBackground, frame: CGRect(x: 0.0, y: size.height - bottomInset - 170.0 + scrollOffset, width: size.width, height: bottomInset + 170.0)) + + let buttonSideInset = sideInset + 16.0 + let buttonSize = CGSize(width: size.width - buttonSideInset * 2.0, height: 50.0) + transition.setFrame(view: unlockButton.view, frame: CGRect(origin: CGPoint(x: buttonSideInset, y: size.height - bottomInset - buttonSize.height - 26.0), size: buttonSize)) + let _ = unlockButton.updateLayout(width: buttonSize.width, transition: .immediate) + + let unlockSize = unlockText.update( + transition: .immediate, + component: AnyComponent( + BalancedTextComponent( + text: .markdown(text: "These gifts were sent to you by other users. Tap on a gift to exchange it for Stars or change its privacy settings.", attributes: markdownAttributes), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ) + ), + environment: {}, + containerSize: CGSize(width: size.width - 32.0, height: 200.0) + ) + if let view = unlockText.view { + if view.superview == nil { + view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.buttonPressed))) + self.scrollNode.view.addSubview(view) + } + transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: floor((size.width - unlockSize.width) / 2.0), y: contentHeight), size: unlockSize)) + } + contentHeight += unlockSize.height + } + contentHeight += params.bottomInset let contentSize = CGSize(width: params.size.width, height: contentHeight) if self.scrollNode.view.contentSize != contentSize { self.scrollNode.view.contentSize = contentSize } } + + let bottomOffset = max(0.0, self.scrollNode.view.contentSize.height - self.scrollNode.view.contentOffset.y - self.scrollNode.view.frame.height) + if bottomOffset < 100.0 { + self.profileGifts.loadMore() + } + } + + @objc private func buttonPressed() { + let _ = (self.context.account.stateManager.contactBirthdays + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] birthdays in + guard let self else { + return + } + let controller = self.context.sharedContext.makePremiumGiftController(context: self.context, source: .settings(birthdays), completion: nil) + controller.navigationPresentation = .modal + self.chatControllerInteraction.navigationController()?.pushViewController(controller) + }) } public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { diff --git a/submodules/TelegramUI/Components/Stars/StarsIntroScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsIntroScreen/BUILD new file mode 100644 index 0000000000..598da6fc38 --- /dev/null +++ b/submodules/TelegramUI/Components/Stars/StarsIntroScreen/BUILD @@ -0,0 +1,44 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "StarsIntroScreen", + module_name = "StarsIntroScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/ComponentFlow", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BalancedTextComponent", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/AppBundle", + "//submodules/ItemListUI", + "//submodules/TelegramStringFormatting", + "//submodules/PresentationDataUtils", + "//submodules/Components/SheetComponent", + "//submodules/UndoUI", + "//submodules/TextFormat", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/TelegramUI/Components/ScrollComponent", + "//submodules/TelegramUI/Components/Premium/PremiumStarComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/Components/SolidRoundedButtonComponent", + "//submodules/Components/BlurredBackgroundComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Stars/StarsIntroScreen/Sources/StarsIntroScreen.swift b/submodules/TelegramUI/Components/Stars/StarsIntroScreen/Sources/StarsIntroScreen.swift new file mode 100644 index 0000000000..db13e8975a --- /dev/null +++ b/submodules/TelegramUI/Components/Stars/StarsIntroScreen/Sources/StarsIntroScreen.swift @@ -0,0 +1,573 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import TelegramCore +import Markdown +import TextFormat +import TelegramPresentationData +import ViewControllerComponent +import ScrollComponent +import BundleIconComponent +import BalancedTextComponent +import MultilineTextComponent +import SolidRoundedButtonComponent +import AccountContext +import ScrollComponent +import BlurredBackgroundComponent +import PremiumStarComponent + +private final class ScrollContent: CombinedComponent { + typealias EnvironmentType = (ViewControllerComponentContainer.Environment, ScrollChildEnvironment) + + let context: AccountContext + let openExamples: () -> Void + let dismiss: () -> Void + + init( + context: AccountContext, + openExamples: @escaping () -> Void, + dismiss: @escaping () -> Void + ) { + self.context = context + self.openExamples = openExamples + self.dismiss = dismiss + } + + static func ==(lhs: ScrollContent, rhs: ScrollContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + static var body: Body { + let star = Child(PremiumStarComponent.self) + + let title = Child(BalancedTextComponent.self) + let text = Child(BalancedTextComponent.self) + let list = Child(List.self) + + return { context in + let environment = context.environment[ViewControllerComponentContainer.Environment.self].value + let component = context.component + + let theme = environment.theme + //let strings = environment.strings + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let textSideInset: CGFloat = 30.0 + environment.safeInsets.left + + let titleFont = Font.semibold(20.0) + let textFont = Font.regular(15.0) + + let textColor = theme.actionSheet.primaryTextColor + let secondaryTextColor = theme.actionSheet.secondaryTextColor + let linkColor = theme.actionSheet.controlAccentColor + + let spacing: CGFloat = 16.0 + var contentSize = CGSize(width: context.availableSize.width, height: 152.0) + + let star = star.update( + component: PremiumStarComponent( + theme: environment.theme, + isIntro: true, + isVisible: true, + hasIdleAnimations: true, + colors: [ + UIColor(rgb: 0xe57d02), + UIColor(rgb: 0xf09903), + UIColor(rgb: 0xf9b004), + UIColor(rgb: 0xfdd219) + ], + particleColor: UIColor(rgb: 0xf9b004), + backgroundColor: environment.theme.list.plainBackgroundColor + ), + availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0), + transition: context.transition + ) + context.add(star + .position(CGPoint(x: context.availableSize.width / 2.0, y: environment.navigationHeight + 24.0)) + ) + + let title = title.update( + component: BalancedTextComponent( + text: .plain(NSAttributedString(string: "What are Stars?", font: titleFont, textColor: textColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.1 + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0)) + ) + contentSize.height += title.size.height + contentSize.height += spacing - 8.0 + + let text = text.update( + component: BalancedTextComponent( + text: .plain(NSAttributedString(string: "Buy packages of Stars on Telegram that let you do following:", font: textFont, textColor: secondaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(text + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + text.size.height / 2.0)) + ) + contentSize.height += text.size.height + contentSize.height += spacing + + var items: [AnyComponentWithIdentity] = [] + items.append( + AnyComponentWithIdentity( + id: "gift", + component: AnyComponent(ParagraphComponent( + title: "Send Gifts to Friends", + titleColor: textColor, + text: "Give your friends gifts that can be kept on their profiles or converted to Stars.", + textColor: secondaryTextColor, + accentColor: linkColor, + iconName: "Premium/StarsPerk/Gift", + iconColor: linkColor + )) + ) + ) + items.append( + AnyComponentWithIdentity( + id: "miniapp", + component: AnyComponent(ParagraphComponent( + title: "Use Stars in Miniapps", + titleColor: textColor, + text: "Buy additional content and services in Telegram miniapps. [See Examples >]()", + textColor: secondaryTextColor, + accentColor: linkColor, + iconName: "Premium/StarsPerk/Miniapp", + iconColor: linkColor, + action: { + component.openExamples() + } + )) + ) + ) + items.append( + AnyComponentWithIdentity( + id: "media", + component: AnyComponent(ParagraphComponent( + title: "Unlock Content in Channels", + titleColor: textColor, + text: "Get access to paid content and services in Telegram channels.", + textColor: secondaryTextColor, + accentColor: linkColor, + iconName: "Premium/StarsPerk/Media", + iconColor: linkColor + )) + ) + ) + items.append( + AnyComponentWithIdentity( + id: "reaction", + component: AnyComponent(ParagraphComponent( + title: "Send Star Reactions", + titleColor: textColor, + text: "Support your favorite channels by sending Star reactions to their posts.", + textColor: secondaryTextColor, + accentColor: linkColor, + iconName: "Premium/StarsPerk/Reaction", + iconColor: linkColor + )) + ) + ) + + let list = list.update( + component: List(items), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 10000.0), + transition: context.transition + ) + context.add(list + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + list.size.height / 2.0)) + ) + contentSize.height += list.size.height + contentSize.height += spacing - 9.0 + + contentSize.height += 12.0 + 50.0 + if environment.safeInsets.bottom > 0 { + contentSize.height += environment.safeInsets.bottom + 5.0 + } else { + contentSize.height += 12.0 + } + + return contentSize + } + } +} + +private final class ContainerComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let openExamples: () -> Void + + init( + context: AccountContext, + openExamples: @escaping () -> Void + ) { + self.context = context + self.openExamples = openExamples + } + + static func ==(lhs: ContainerComponent, rhs: ContainerComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + final class State: ComponentState { + var topContentOffset: CGFloat? + var bottomContentOffset: CGFloat? + } + + func makeState() -> State { + return State() + } + + static var body: Body { + let background = Child(Rectangle.self) + let scroll = Child(ScrollComponent.self) + let bottomPanel = Child(BlurredBackgroundComponent.self) + let bottomSeparator = Child(Rectangle.self) + let actionButton = Child(SolidRoundedButtonComponent.self) + let scrollExternalState = ScrollComponent.ExternalState() + + return { context in + let environment = context.environment[EnvironmentType.self] + let theme = environment.theme + //let strings = environment.strings + let state = context.state + + let controller = environment.controller + + let background = background.update( + component: Rectangle(color: environment.theme.list.plainBackgroundColor), + environment: {}, + availableSize: context.availableSize, + transition: context.transition + ) + context.add(background + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + let scroll = scroll.update( + component: ScrollComponent( + content: AnyComponent(ScrollContent( + context: context.component.context, + openExamples: context.component.openExamples, + dismiss: { + controller()?.dismiss() + } + )), + externalState: scrollExternalState, + contentInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 1.0, right: 0.0), + contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in + state?.topContentOffset = topContentOffset + state?.bottomContentOffset = bottomContentOffset + Queue.mainQueue().justDispatch { + state?.updated(transition: .immediate) + } + }, + contentOffsetWillCommit: { targetContentOffset in + } + ), + environment: { environment }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(scroll + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + let buttonHeight: CGFloat = 50.0 + let bottomPanelPadding: CGFloat = 12.0 + let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding + let bottomPanelHeight = bottomPanelPadding + buttonHeight + bottomInset + + let bottomPanelAlpha: CGFloat + if scrollExternalState.contentHeight > context.availableSize.height { + if let bottomContentOffset = state.bottomContentOffset { + bottomPanelAlpha = min(16.0, bottomContentOffset) / 16.0 + } else { + bottomPanelAlpha = 1.0 + } + } else { + bottomPanelAlpha = 0.0 + } + + let bottomPanel = bottomPanel.update( + component: BlurredBackgroundComponent( + color: theme.rootController.tabBar.backgroundColor + ), + availableSize: CGSize(width: context.availableSize.width, height: bottomPanelHeight), + transition: context.transition + ) + let bottomSeparator = bottomSeparator.update( + component: Rectangle( + color: theme.rootController.tabBar.separatorColor + ), + availableSize: CGSize(width: context.availableSize.width, height: UIScreenPixel), + transition: context.transition + ) + + context.add(bottomPanel + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height / 2.0)) + .opacity(bottomPanelAlpha) + ) + context.add(bottomSeparator + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height)) + .opacity(bottomPanelAlpha) + ) + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let actionButton = actionButton.update( + component: SolidRoundedButtonComponent( + title: "Got It", + theme: SolidRoundedButtonComponent.Theme( + backgroundColor: theme.list.itemCheckColors.fillColor, + backgroundColors: [], + foregroundColor: theme.list.itemCheckColors.foregroundColor + ), + font: .bold, + fontSize: 17.0, + height: buttonHeight, + cornerRadius: 10.0, + gloss: false, + iconName: nil, + animationName: nil, + iconPosition: .left, + action: { + controller()?.dismiss() + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + transition: context.transition + ) + context.add(actionButton + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanelHeight + bottomPanelPadding + actionButton.size.height / 2.0)) + ) + + return context.availableSize + } + } +} + +public final class StarsIntroScreen: ViewControllerComponentContainer { + private let context: AccountContext + + public init( + context: AccountContext, + forceDark: Bool = false + ) { + self.context = context + + var openExamplesImpl: (() -> Void)? + super.init( + context: context, + component: ContainerComponent( + context: context, + openExamples: { + openExamplesImpl?() + } + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: forceDark ? .dark : .default + ) + + self.navigationPresentation = .modal + + openExamplesImpl = { [weak self] in + guard let self else { + return + } + let _ = (context.sharedContext.makeMiniAppListScreenInitialData(context: context) + |> deliverOnMainQueue).startStandalone(next: { [weak self] initialData in + guard let self, let navigationController = self.navigationController as? NavigationController else { + return + } + navigationController.pushViewController(context.sharedContext.makeMiniAppListScreen(context: context, initialData: initialData)) + }) + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private final class ParagraphComponent: CombinedComponent { + let title: String + let titleColor: UIColor + let text: String + let textColor: UIColor + let accentColor: UIColor + let iconName: String + let iconColor: UIColor + let action: () -> Void + + public init( + title: String, + titleColor: UIColor, + text: String, + textColor: UIColor, + accentColor: UIColor, + iconName: String, + iconColor: UIColor, + action: @escaping () -> Void = {} + ) { + self.title = title + self.titleColor = titleColor + self.text = text + self.textColor = textColor + self.accentColor = accentColor + self.iconName = iconName + self.iconColor = iconColor + self.action = action + } + + static func ==(lhs: ParagraphComponent, rhs: ParagraphComponent) -> Bool { + if lhs.title != rhs.title { + return false + } + if lhs.titleColor != rhs.titleColor { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.textColor != rhs.textColor { + return false + } + if lhs.accentColor != rhs.accentColor { + return false + } + if lhs.iconName != rhs.iconName { + return false + } + if lhs.iconColor != rhs.iconColor { + return false + } + return true + } + + final class State: ComponentState { + var cachedChevronImage: (UIImage, UIColor)? + } + + func makeState() -> State { + return State() + } + + static var body: Body { + let title = Child(MultilineTextComponent.self) + let text = Child(MultilineTextComponent.self) + let icon = Child(BundleIconComponent.self) + + return { context in + let component = context.component + let state = context.state + + let leftInset: CGFloat = 32.0 + let rightInset: CGFloat = 24.0 + let textSideInset: CGFloat = leftInset + 8.0 + let spacing: CGFloat = 5.0 + + let textTopInset: CGFloat = 9.0 + + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.title, + font: Font.semibold(15.0), + textColor: component.titleColor, + paragraphAlignment: .natural + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + + let textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) + let textColor = component.textColor + let accentColor = component.accentColor + let markdownAttributes = MarkdownAttributes( + body: MarkdownAttributeSet(font: textFont, textColor: textColor), + bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), + link: MarkdownAttributeSet(font: textFont, textColor: accentColor), + linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + } + ) + + if state.cachedChevronImage == nil || state.cachedChevronImage?.1 != accentColor { + state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: accentColor)!, accentColor) + } + let textAttributedString = parseMarkdownIntoAttributedString(component.text, attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString + if let range = textAttributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 { + textAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: textAttributedString.string)) + } + + let text = text.update( + component: MultilineTextComponent( + text: .plain(textAttributedString), + horizontalAlignment: .natural, + maximumNumberOfLines: 0, + lineSpacing: 0.2, + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { _, _ in + component.action() + } + ), + availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: context.availableSize.height), + transition: .immediate + ) + + let icon = icon.update( + component: BundleIconComponent( + name: component.iconName, + tintColor: component.iconColor + ), + availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), + transition: .immediate + ) + + context.add(title + .position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0)) + ) + + context.add(text + .position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0)) + ) + + context.add(icon + .position(CGPoint(x: 15.0, y: textTopInset + 18.0)) + ) + + return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 20.0) + } + } +} diff --git a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift index 6dfa8bb2d5..c49448d6af 100644 --- a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift @@ -237,6 +237,8 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { textString = renew ? strings.Stars_Purchase_SubscriptionRenewInfo(component.peers.first?.value.compactDisplayTitle ?? "").string : strings.Stars_Purchase_SubscriptionInfo(component.peers.first?.value.compactDisplayTitle ?? "").string case .unlockMedia: textString = strings.Stars_Purchase_StarsNeededUnlockInfo + case .starGift: + textString = strings.Stars_Purchase_StarGiftInfo(component.peers.first?.value.compactDisplayTitle ?? "").string } let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: accentColor), linkAttribute: { contents in @@ -815,11 +817,9 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { switch context.component.purpose { case .generic: titleText = strings.Stars_Purchase_GetStars - case let .topUp(requiredStars, _): - titleText = strings.Stars_Purchase_StarsNeeded(Int32(requiredStars)) case .gift: titleText = strings.Stars_Purchase_GiftStars - case let .transfer(_, requiredStars), let .reactions(_, requiredStars), let .subscription(_, requiredStars, _), let .unlockMedia(requiredStars): + case let .topUp(requiredStars, _), let .transfer(_, requiredStars), let .reactions(_, requiredStars), let .subscription(_, requiredStars, _), let .unlockMedia(requiredStars), let .starGift(_, requiredStars): titleText = strings.Stars_Purchase_StarsNeeded(Int32(requiredStars)) } @@ -1239,6 +1239,8 @@ private extension StarsPurchasePurpose { return [peerId] case let .subscription(peerId, _, _): return [peerId] + case let .starGift(peerId, _): + return [peerId] default: return [] } @@ -1256,6 +1258,8 @@ private extension StarsPurchasePurpose { return requiredStars case let .unlockMedia(requiredStars): return requiredStars + case let .starGift(_, requiredStars): + return requiredStars default: return nil } diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/HiddenIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/HiddenIcon.imageset/Contents.json new file mode 100644 index 0000000000..126065aee3 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/HiddenIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "hidden_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/HiddenIcon.imageset/hidden_30.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/HiddenIcon.imageset/hidden_30.pdf new file mode 100644 index 0000000000000000000000000000000000000000..7ff4831d79107a982fae465761b9a552df3a1842 GIT binary patch literal 1543 zcmY!laBvK;I`dFTEr~!5FAK2q*+Jp}3?dH8Gc~f-!gM*>$(wcv`<#ajoSJnW<>` z=jJxnU3tGb+`7bea0alv`22kFI%RH;JsbaIo_ILt@Iw1P-#-4YmzV$l=kN5t*Y|JZ zUmxGEf4ly+&9R?1k8f|Ezkd1ax$^J!wSAm0OK+FGUGDWw@8$Q|*iDW&awv6vU61O% zoc}B`%C>$Q^Ag=AtPe@tE^Q=yQP|d5K1co0<%Vvhn#%Rnii_8*T#&!;^#qB{-gm!5 zWLXx?FFMnanq8l*p(j%96xb%P(-64^iPEDL!;RpXt- zlT#0G3AN5Rr?fA_s$3@!Tif$x)d{=N&K76xu&GCpBcVs`l}FE^F3wa-R3y zQsv#@XRaPDCvrq^<0^|LshtTr@tY;{FLQDjp110B>rvaqw$+UDq>8v{SG9t{uUkc5 zYONo76*#Y5a!a`PtIM_7W{aX`_o|#-^qDhV%)Kt9_syx3?HigOM;xB=ZR^E3>xz_4 z)s*`^JK`MRtH}QI#NT&jyZ6rawNt6f*AR(ovrd;@SYJ}T=fY;&^#PwOf4V7&wdXA zL|lh!0pqiCVn$hNf*AsqY1^At&AxXr`S3wgx55+Li#!W_)+lwlxl7b;&6qgNEb70~ z7xwAja(}n?n6-Uijy7h>-f_Bc%DG94oVKQx_>|v@2;9?_dg_he?4`~;NyS=M%g<&# z)&F$k)q_6OUomS6{>4mXfAu|N$HZx!rn2Q5FFslm_tZIcVg9pIrIRG*h#U&PBr!+$ z^%|Dlx6V9S6=-w8L|yq{KgU&bP7wR3h^+)aLJjX7+1e%Kj>rhC6@0qh@@|Fl9$ti%9RGeIl(% z*>8$6LN|8kPKcYyXnS+Q(F&X6j-R%%-=1MOBi2XKYtedzLBATb@5w|!GnG83KhD-@y?4D<}Z0KqVZ3n5rAlY&x<^Gl18Q;QWq#S5rh z02LL^`FSO&c|aRMWeZF&Ah8H2reF#cLMnkkLhzDF-#aq}=u`!e4}uh6_BiL40`(eV zxW5=u62St)48Ql_d%qBnW?Fogjb^va|$735t-Rfg#X~s6vJo7U)7oX29Trswyc;%*;tG;sV8< zrwcGRG>Y?cQ#BPdGE+1mfubLjpI@Q?@-R4n^@B63Qh}}q7t4u7C1CFu8XFmMsj9mA Gy8!@9=2D^n literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Contents.json new file mode 100644 index 0000000000..6e965652df --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Gift.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Gift.imageset/Contents.json new file mode 100644 index 0000000000..923ba188ab --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Gift.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "gift_30 (4).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Gift.imageset/gift_30 (4).pdf b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Gift.imageset/gift_30 (4).pdf new file mode 100644 index 0000000000000000000000000000000000000000..0d6d36c6032106b352f30cc3e5ed1452079ff59a GIT binary patch literal 4312 zcmZXYc{G%L8^?QEB$5y%OJm>HVPsGCP$-hI4aPcSVrIx5vV}opYpmIK29vQRS!3+G zp=4jPC(=tj^*rzUUVq%@y1(bTe%ErJbD#71ap|h435f|yQBa5hL;(m(2MPc{P7a`; zKmm2KK6#}O|Fv6#FbE2&0<}U|Lyt3#O+*{&WQ(!`0FT4(v3^!^M&x-==&Oe(XdxeBU28Kic2OnfFa?@HyOjU7LNh2PY^TZXBU}@wIdA(nFhG zUk~>``>fz^_WVFO=LhcY>oI#IJ`BE9!tlbXIw52gpYN7t&$*U$1QG8a8$7#5KINt`Q}2= zg?*2Ru!_W?*_+>p$~EpiBer0XrO~@JesM7=I9E#3!jk%nbm-j`T-}m>&Y7YTtHkkX zm1Rd`x3C~-H^GoW_|&BX>v&gYc=y7~i`gC{z1w3s;xCUl4X2YAp^+f3^1O;t@l__t z@ZOz;9L@x&gEX3_M2cz_GxS zPd!C2t7yefs4H9zU-*VtpQXxRmK{E6rnLrIo>Hu z7O^Y`w)HGpO#m$=n~m3N0}Ws$W4tN&uV@7guDH>V67Z!WVvmgJ5@Azs#Df`!=7pAM zy(KgRMgd5af zZxzW)80Wz_EbZZ?BX#v-y%Yk0^l?xfMhnuj1EbV_kh4E6?bTULg{fEQ4#%kvDx?HN zJAb$zXG?g>d%D(Atk_W{dsQ*I%g5*y&wEOn)t%C+YxxJ-(cUPu_5K;st~XtPk$q7j zPq!n<1LAD9D!|Av@R_QV2|>z2pCFVSY4_^Mfgvvlyv=q39Lqfl z<6mg#C{51$y#cVtBjXG%-7^u@%JAc(W?E2oJ6M4<#Z~Ueu^GP5b+r@kL+Xw%b8E0g zH+uvIPKUVI;q zxx9mZ=d~iGk`LH+mZ}->!l~~mzNG;u(KdN9pX_EgHk_1_ zM)OveX3BekWpT*Z+dj8M{Z15(E8`S115;uEiRWb_W>WuR@T!D$sO(jxmg1r3Ldk+_ zWe!$a3q-grBK}ak{3F5SyV;`PXt}BJ@bzT-m-*ia`!(s+4ZePE#y%gxWIs+1ca1fi zS)D(q{1ED_Rs@KPQFVHqz3>6xP(?)^CEg;8M!?wnM)(XI77|y}a0z0Cp|5otwRP&2 zRC`U^E_~Btt;y8nS}4p=?#~WfbCFdMa?QLhTp{EUk$>)5W?QBg^`o$+v;rcKV6i2{ zo0rLU?H!SpCn=3rmY*dAMc3q)cT4J!RpS)mUN#&8EYMUnhX{pXM8CiStuxx!4lJ^S zSYk5!@6aEVuab;6J~+c3sl>AzkXK{KfxL->yRz^6QAUf43Pl}}a-)=IV>Z;v6V7jO z@?M2!OG9kVcE^>!E~HFO&e?A{x-wIvik6T!t9?uLIzEDV<$*K-JQ-nSEg6zgGHE1W z3%+|n+#t!WKHl$*UE2281eVMUJfN4f*&UhM7Z*SH%AcdeKQz5yn6(1`1$HynnU-40 zk%B!*Nj}V2@#w(MIPzd+DLtV_cZ3X_^r);zI4pxH&OU=~?%~m{9St<`S@f9Z$i)Eo zrv6+gkUp}Jf6)!Tm2+KVJ~uoy<5?c7j%xFqvg<~XS_KW^UeZc4|HfH<7-&}s>K`{+ zX2bG&+;NzV=dEb;wNOyrqR|^@IC1_cQ;r$&xDL6$yqK=>lzEFtjOD0c?Ktz$)#J4# zB6%m+dk4u<;;1iT8_Xv?TK$$(L9OSL+olm0WfbNjrS{=!yGqArf``a546@yL1#Vv% zIY~_=#dLYDTzH?AvMOa`3OI~ zb;G*$QWtX0D_2F!QnREx+#$1Xr5#_1u-V3SAOC$RFw(xkm~CFK`rC})5Nj^>@JpN* ziY0GqG$|#-vdM0djKJU0KZaP??8wcR(#Uid<6?Vy2RQs|#(Z*}ym`fTdOo?Gu~X7_ zEr=oqS~z{yc~GK!x+mj7n_*D)ba(>&w_>)=Mw4Z7IMtBw2QvG69+#qaX7R!-)lN+9 z>>{@@(N88^rMYlQWfwAW8hvk~(-kP?=XW8usD%&0dM*+ z4l_%YYw&M!&&%naniYvmjv>vM0rB!036&*Xd6tS;_GXpICFrhYV%5eppo|vk=KApW&e3B~di``#EujHln6Y_`gk7YyYozM-@KysopMDB60)<@BgOxpGb zEF*8?4Os15$XL6RmAiZ(@^19e+;9F0 z?^8yha@5Y5IRaB;k-6e&S7{;K()Ww#3`r{LZg<`&Cg-xYw^{70I19l{Q0^Y)J*1 zPBsIcFP(XdbGV%16!_Qlik`^;F6#?Dr}Ie1wL9gtRA#gMx#Q1zi21G@wAW_Wna4hv z@~N&d$!R&LdxzF+-YzT1nbbnvrE&Lnpo6#z`$f;Eyg;nCeETxV6tx}_k`ua67L7&i zU0>n9Noz_wG#FdDBIAFVnWQDyV&lCqIwOkts&PyC?B}P9NvFgBy61X9V%?B4f(!$Z z8p%0pPO+0;jOzBcsZvEB4%X4vrb?JweCny4q8dq^A!BJAUi(s0>^vj7sOC70zK_qc zXO6-uQ7taS1jgRqPRg+f=M-gN^7IZ!GW66*by1!jYK|L~wS3Lkw<@luzEzQLka}Yj zD@E^scFtJE+LCbb-u&d{&MAJm5c;!8duWKx`>9qbWD-iGTTle{gH zvdBVK!KjCBD%GY_%J6BsLJCcjVvlbu?w4YV_d(3(TjGnq7( zleVgZp76$<;U+ypUw~Ale_B}Si&E)}97&Z^eKs539*%!r>Z7ruqm~{Frj#Y12ND~I zciH4SrCDrv7b)el1|Fr-SQXgu!3rZtWNO`}3Mp@i{T4Y*DM%~4ZEyeWbmh|{_U%Mr ztrU=NLa#W(YIUtA?-1A6OXsd!29*zoVI;=`=H5M?64Q>_&I1Wj7^9dqqDM(vqm(MhoWj-cq#w&GV zD7SHc#`x}RC-&|q+d7o;>A`bxK22C@=atmtg^Sz`n(u#LJDL)T>s1JutCm8Bl~(k= znOuEyIoP&jBeD0B6Sd!jxa;D2Clc;Ixshl%x&mLFF|!$0na6dl`w7MlZ7M`!MJlW; z&NH>feW}=prIlETAm_j-ic9F^FzQF*qxpgZ0;0iJmhv$*^-DHeoCh1;7= z-G{YK<<+nZ6TU7%uHw3!$1iO2ljmtcbTt9=88$IN>@#c9Zk|5jYZWpR~g}if;t^n zJb~rkqAtYcSOZA@E&U6rPozJnEuv*_ecUSGWCDG_?>fo|^l`4(|Midjh1S2f0s60# zP6kB&DR|P@ufdfeCPCrUyT_)j9r1!50_x=@^aJx!J4b?_n)a3~*u*WQ}% r*HT3E5eO9EWZ<6*7P)Wl0X^>i=RE_&1@&_r2~lZL3N9|ydusmy%o+Bo literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Media.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Media.imageset/Contents.json new file mode 100644 index 0000000000..6a409d155b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Media.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "unlock_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Media.imageset/unlock_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Media.imageset/unlock_30.pdf new file mode 100644 index 0000000000000000000000000000000000000000..67593f36987371d8d76c0f5e07d27005ec365ec5 GIT binary patch literal 2124 zcmZXVc|4T)AHa(hhLFvf<8hDHF@td|Bj;e;V~8A?$qZA>$c%fW=|}E$Y&miyjACN# zQksy>#Fi1|itVsv97QOxWLG;wwC(Tn$MgC=@7MGBem|ej^Ll;X8Xk5CU8tT32m}Qn z010~%1OOHmfTbk}M-1V;f(-u6^$~<{3N8xJM}%W2I6K@4QV5Ql!3}*E95Iv<1{iWb zM{yB%2QnoJhlv2i?Tsj8H$FEOwV&Ap<%*yWXrOWl{*{wAFGuF73KZ+d-1abedz4ls|3Eth^hTNH$CUdu}cA)1Mz>{z`OApIpq|csVooI^n2IW`~TK z7r3sqaNtSNqA-eZBL#C_^wJ z-$)`Y9U+#VXP+%##YP&bd+YwPdeEj38BqDGt&jtTO@->@Sj)dWa5)8k$(N?ofR{^W z!QJwv$9&~&y}oVG3Ct8JX}&RhX~(W z*r{@_jofU7Ba2Hy-8MDoSc=_zw-s0hhdW<(VPGkJ(OBTVN~tuu6OGhMQ2wX~?I@(9 zB#yVj#BZSU`&fFSK03V&#u*`kH1uksT{EYusbGK7y7wI1M{Bqy>p_Jm zlD*J{{^{CdczWlYV9m9Wu+2bxUd$@vU~v525GG|Gi)A2o2pKQ@%5Hv7hhbAf?vg6(C=EQc(r;A^_?1T7_rbu*!PMNtB?C|wagEoeJEKL1GL9$YHe zS$|AksYyd;sYv{%%q z3X2}L_WA}d9znc%sR2SiXgMejdD2H`1`9+$YwFiLOGSM}SCTpvP&pT`%);{P31;rESK;gSvB>rtaU$9kkPrYp2p=Vrmya-AMyR63zGE^3ZmxUKx(0U_vt=xZCVlm~vVlMKqtDKC>c)>hpv?rv#u&quCPZz4c<@bL0^)6ywRyGeD+ zhYQHz*%F-}TSc<`?v+=@zViGA+nbc^8mD;Kqg4X0nQdopH}+nn$;ld<2A{9T zm?of7Aa7n4?4Q2+M|E+kl6-g?zkNx7!=ayUVJMAIOxeQ^!Px>tzZx~M$My{ zBXI_O!2LNF$#1i?5EW^6$6vVyjirqgt6)W9W){3TO9b=!fbn=1MzK5J93SE0Vheh@8EQzdr?5ESg=_1U}uLso|keq98i?1^yAR6#C)s@6)^Hz2lOXt&LMHn^8 zvoDpdmpv7Gk!@S_P~}9jTfpLb#;@ybe&n&N;TEXl^d_|LP}i>uhHl#$WBk zYmmHqAg{3P#%(baOgJg@>(sa?93BJ#OhDW1^PYe)3}y%excGBGA;8uOAnqbVZk@c? zMNpUt_n-U+nQ;sI8krcI{TG=UeMzK5VF=;4C=l=OIoWZ?0Vb0oaM}Qv5Td<36nzwl jL;-jWZw*-Ai;#fhR=?$TOcZ6S9jF<^0HmQ|?~eEf03~5- literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Miniapp.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Miniapp.imageset/Contents.json new file mode 100644 index 0000000000..e582efcfd3 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Miniapp.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "bot_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Miniapp.imageset/bot_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Miniapp.imageset/bot_30.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e682166cc8a986ec536d2dd519b2bb3415f3b00d GIT binary patch literal 2702 zcmZXWdpwhiAIB~CMeg_8<2Foevk5aZ-rJ~4Gr%c5VP9n+Kx;VG*AJ6Oid|u!C^T+en#@b>G5QZjTFam%B)F3h#04yv3 zOG_}3Lg2oFjsC8~Fr)}Nkp{pp5qLV$mKaPW5IGqfgSikXq4Y2S$q9GoNSq2WbQ%#K z3629rUL46%HkLm3VOzQj!+80sv+Yd3VqCHpHXD+I)w*#`Nh|q~oAKNnt@64DB6F(l;yZ7n3JbFV&v?WiCQ66BKoMioTu`2UM zKw;;T=LarK@1w4Vn?~ZPijCT&g;ir;?zu zKXfGdqCYasR5?~Mx|u!@q`?!l=vnD{cATkvWc*Zq>cH_<$@2Sb z&3J=^W4z%$kinY@$gRVQlvlp@!xUtLS{cON{vnUeycCxx%3OQgx}ntPvvI{&Sr2np z0&n`BmJw#aXHS+Vq5i1JBuqdf*p12`dZzlZz|v4cilXHQzKZqe#oFYKB%fXTi4IxRM3YPigvG_zL19SI}WCG2r z65)aphx*~eU2s}sYav#{AxdO1UU?r+YeQd?dOm2@33vWA(O~x;o4v2bNio=RI=-4E zPd=pTI=l7zAz;gC%?g!Mg6?-+vG(?D1oCT$V2#3zY>=#dr3Ve#1ivp8&p7k0qza55 zlc+R}i}cS{a-Ta~EnkE-VNUTFTenN}nTJ~$X%$H-*4Py$XlR1#MJnVfXK8yZr6ztz ztkSjB)hj_a_V4ZT^?E78Z7k7B+2u~A_2*=|(2Kecy!^OA|AeDF zGEYToV(DUlHu3>9YMO1-q2va_2){PSx}{WnFg3CM8vlS#mL88&;?kmMk)UP{#3Ris zs+9d^2Hxs?T#VeM=Qd0%MP>_B3d80drS7%dF7vPLdvJ;sF@KidNcKSIj_J^f1I9+v zfPR@!HbP)nJ)U$6IevvUCUpPSb+Zg%yW77QiKVL--mN*PXV!)Bbw4Ayy;Oh!*R%|y zaG_})#%#n?qkEs~MABjV4TNL2OUihzYn4k%uUvE=Il~%?ZqLZ-#W{C{=R9cf$bjyk z4p(M9kg8rCSN)%W$RU{u21$dC|3y39ZL5CIGgo$^L_wS&Mi)ZEW=LelB`S2K$j^6@ z?BnbZn619Khe-~zMnM_%P&qg@e_6L&#I-8{U0=v>A6mSsVXcpdX7HWBG@>sgg?1i1 za1w4FJb82j^m4BDWTPD`iJ&mRt>K#@3$S|IV&M^S6II`KwZ=}CA_Emq&1l-ER0LCFu7GG zs%zTx8LN;liA=h2tppU#CN6 zIGAA+Z-jKn#5$~|23SgXdKb4vjajAJ=903q_rBkhg8jN-*mymGS}QF!Q~A2|O|UY8R87W3x3 zQuj0YdlwUnaV8b0RRyLT!!caZ_lJx9*Y`b%9L_1YR@UIJ+F>;egZZ?J1vqrupm>)+ zM?X?XN-Bew#MK{Co&BCnRvBvPi~E^oe5{=D1uYw#@UV4+q|4maI+tz$f)HCN2zHNJ z)5S=fimY8$ML4T4&gfuc*32s(`bxXHS}Q(c#;_|2sr*so*PI7{QUqUA92&${gl^p(zw9JqGae|3(zJ_$VR( z!{S4~BZe!HK*C#7;{ZPZZV3Nke>uoq=`ZhlC3j*BHI^1ki~+cm!^IOWvDi>4bRvb* zk&8IrMJ%4i(E#Ib(%(RcEA0Xj%$Y>sj0$i!a0kBkv7yFtauNTTKjteCeZR(me^lZw z7_-}uJJ{F7ZSZt_1U2-Zsc|%72pA5SfWNNKeF6uJjgiIxNB(9AIPk>-D7zApGbh(} zB_o6}=M?{vO#TyvM49|M3T5j)<(xw|h8juK19V9Qy{|`sxl^fh ifV=RQ1H(K>2}I8HU+54|qkmb)2xSTfYirxNVg3Zve`;+2 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Reaction.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Reaction.imageset/Contents.json new file mode 100644 index 0000000000..a7e458b55c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Reaction.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "cash_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Reaction.imageset/cash_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Reaction.imageset/cash_30.pdf new file mode 100644 index 0000000000..ccfbed1a33 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/StarsPerk/Reaction.imageset/cash_30.pdf @@ -0,0 +1,62 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Filter /FlateDecode + /Length 3 0 R + >> +stream +xe•KŽT1 EçYE‘‰óõKxjRwK¨%ÖÏÉç%ÔÈïĹvl'õáóÓŸ_×Ó·/Ÿ¾›ûëz3¿·®ÿî6Þ/k­EBÔì·q½˜åö¿ñv½"6*Îæœ\©—&[Åå”É&§Qä!c;~sn›]flIØ›N ϱÚ6tð»Ì¦=`r~ni‚#­fŒx-t™¾¡e¸Ø0ðêK]ë&S§Ëü4ó .Ãþb¢-%Ö¸²óÙ¦”¼¦ ¼®]ô®Ñ:Ñ&6–J¾ýqZˆê ]_6œÑ &¶ÑéA:TÈnZèÍU<çoÖ«À—D/”)[õ}³°i±R%Ôò€9ÉA9t±©:ïÇq’N¿É8³¢¾¦“ªUýGp£ù2&¬\Š<›jcФß`õ¥’;*IK$…ØûìãĪ0p\sÂ{*ÏôÖ‡Ø\mЬ1´#Q¥æ§¶èðZˆ³y›|J=}6ˆûš 7…éžJú5Tªhw/Ç CpÂb#íb†ˆ¬¡ºàÙæ(™ÍÓ,„ÞêÉ„©éÑ’Œ^¾aËZmˆ>ĺY¥]ýÈ…rDñu +$>äá`¹Öš!*ŽHÕjM@%ŸIZ/R*‘é^l5b)dÅ[,Û’Äa1fï¸ö #0ëÔxÎMb_Ô¤ì[,ÚØæ«â`.–Šh+͘ր|o +h–p!êºæAÁšßV;Ð Ú·NHv¼Ò9’ -æÄ©`òx™e—f—xþŠp×âÌc:Ð;àœ#ô$}¯­!mà”*rë[ +¡{bݼX«Õ=ì ¶byîFÜWïAµP¶ÖŒ­+vÌp^Ævo,å¡oÌ´xž¯ÈpƒHËYZÚ“¶g^=Ù”Cs<©p }-zPؼQ×ìTY=™Ë1óÚiS±™|Ûë’E: íaê,(¹'c7Ó‘yÂ9ä¢dq·f+n†öн)™S7áâ3(‰vbñüi)ÊsNCÆDñx:§™R²û¤óI}æ‘ÿñμš¯æ/²ô¤r +endstream +endobj + +3 0 obj + 797 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000915 00000 n +0000000937 00000 n +0000001110 00000 n +0000001184 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1243 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 1fcc5ef531..aefebec13e 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -72,6 +72,7 @@ import StarsWithdrawalScreen import MiniAppListScreen import GiftOptionsScreen import GiftViewScreen +import StarsIntroScreen private final class AccountUserInterfaceInUseContext { let subscribers = Bag<(Bool) -> Void>() @@ -2205,8 +2206,6 @@ public final class SharedAccountContextImpl: SharedAccountContext { public func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (([EnginePeer.Id]) -> Void)?) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } -// let limit: Int32 = 10 -// var reachedLimitImpl: ((Int32) -> Void)? var presentBirthdayPickerImpl: (() -> Void)? var starsMode: ContactSelectionControllerMode = .generic var currentBirthdays: [EnginePeer.Id: TelegramBirthday]? @@ -2268,9 +2267,9 @@ public final class SharedAccountContextImpl: SharedAccountContext { )) let _ = combineLatest(queue: Queue.mainQueue(), contactsController.result, options.get()) .startStandalone(next: { [weak contactsController] result, options in - if let (peers, _, _, _, _, _) = result, let contactPeer = peers.first, case let .peer(peer, _, _) = contactPeer { + if let (peers, _, _, _, _, _) = result, let contactPeer = peers.first, case let .peer(peer, _, _) = contactPeer, let starsContext = context.starsContext { let premiumOptions = options.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) } - let giftController = GiftOptionsScreen(context: context, peerId: peer.id, premiumOptions: premiumOptions) + let giftController = GiftOptionsScreen(context: context, starsContext: starsContext, peerId: peer.id, premiumOptions: premiumOptions) giftController.navigationPresentation = .modal contactsController?.push(giftController) @@ -2812,6 +2811,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { return StarsTransactionScreen(context: context, subject: .boost(peerId, boost)) } + public func makeStarsIntroScreen(context: AccountContext) -> ViewController { + return StarsIntroScreen(context: context) + } + public func makeGiftViewScreen(context: AccountContext, message: EngineMessage) -> ViewController { return GiftViewScreen(context: context, subject: .message(message)) } From aecd3655c4ec3d5e2b438e183d073485055d18c7 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 24 Sep 2024 12:34:21 +0800 Subject: [PATCH 11/17] Roll back --- submodules/TelegramCallsUI/Sources/VoiceChatController.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index 4e0facc053..db52366c58 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -7098,15 +7098,14 @@ final class VoiceChatContextReferenceContentSource: ContextReferenceContentSourc } public func shouldUseV2VideoChatImpl(context: AccountContext) -> Bool { - /*var useV2 = true + var useV2 = true if context.sharedContext.immediateExperimentalUISettings.disableCallV2 { useV2 = false } if let data = context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_videochatui_v2"] { useV2 = false } - return useV2*/ - return false + return useV2 } public func makeVoiceChatControllerInitialData(sharedContext: SharedAccountContext, accountContext: AccountContext, call: PresentationGroupCall) -> Signal { From 762eb19cc1c508465ba90944f69bb93e1663a99c Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 24 Sep 2024 21:07:21 +0800 Subject: [PATCH 12/17] Various improvements --- .../GalleryUI/Sources/GalleryController.swift | 2 +- ...eoChatExpandedSpeakingToastComponent.swift | 185 ++++++++++++ .../Sources/VideoChatMicButtonComponent.swift | 62 +++- .../VideoChatParticipantAvatarComponent.swift | 2 +- .../VideoChatParticipantsComponent.swift | 201 ++++++++++++- .../Sources/VideoChatScreen.swift | 274 +++++++++++++++--- ...ideoChatScreenParticipantContextMenu.swift | 4 +- .../Sources/VideoChatTitleComponent.swift | 79 ++++- .../Sources/PrivateCallScreen.swift | 16 +- .../ChatMessageInteractiveMediaNode.swift | 2 +- 10 files changed, 747 insertions(+), 80 deletions(-) create mode 100644 submodules/TelegramCallsUI/Sources/VideoChatExpandedSpeakingToastComponent.swift diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index 1448c0f424..1f1e17ba77 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -245,7 +245,7 @@ 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") { - if NativeVideoContent.isHLSVideo(file: file), context.sharedContext.immediateExperimentalUISettings.dynamicStreaming { + if NativeVideoContent.isHLSVideo(file: file) { 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)) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatExpandedSpeakingToastComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatExpandedSpeakingToastComponent.swift new file mode 100644 index 0000000000..8170418a48 --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/VideoChatExpandedSpeakingToastComponent.swift @@ -0,0 +1,185 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import MultilineTextComponent +import AvatarNode +import TelegramPresentationData +import AccountContext +import TelegramCore +import Markdown +import TextFormat + +final class VideoChatExpandedSpeakingToastComponent: Component { + let context: AccountContext + let peer: EnginePeer + let strings: PresentationStrings + let theme: PresentationTheme + let action: (EnginePeer) -> Void + + init( + context: AccountContext, + peer: EnginePeer, + strings: PresentationStrings, + theme: PresentationTheme, + action: @escaping (EnginePeer) -> Void + ) { + self.context = context + self.peer = peer + self.strings = strings + self.theme = theme + self.action = action + } + + static func ==(lhs: VideoChatExpandedSpeakingToastComponent, rhs: VideoChatExpandedSpeakingToastComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.theme !== rhs.theme { + return false + } + return true + } + + final class View: HighlightTrackingButton { + private let background = ComponentView() + private let title = ComponentView() + private var avatarNode: AvatarNode? + + private var component: VideoChatExpandedSpeakingToastComponent? + private var isUpdating: Bool = false + + override init(frame: CGRect) { + super.init(frame: frame) + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + if let component = self.component { + component.action(component.peer) + } + } + + func update(component: VideoChatExpandedSpeakingToastComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + self.component = component + + let avatarLeftInset: CGFloat = 3.0 + let avatarVerticalInset: CGFloat = 3.0 + let avatarSpacing: CGFloat = 12.0 + let rightInset: CGFloat = 16.0 + let avatarWidth: CGFloat = 32.0 + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) + let bodyAttributes = MarkdownAttributeSet(font: Font.regular(15.0), textColor: .white, additionalAttributes: [:]) + let boldAttributes = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: .white, additionalAttributes: [:]) + let titleText = addAttributesToStringWithRanges(component.strings.VoiceChat_ParticipantIsSpeaking(component.peer.displayTitle(strings: component.strings, displayOrder: presentationData.nameDisplayOrder))._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(titleText) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - avatarLeftInset - avatarWidth - avatarSpacing - rightInset, height: 100.0) + ) + + let size = CGSize(width: avatarLeftInset + avatarWidth + avatarSpacing + titleSize.width + rightInset, height: avatarWidth + avatarVerticalInset * 2.0) + + let _ = self.background.update( + transition: transition, + component: AnyComponent(FilledRoundedRectangleComponent( + color: UIColor(white: 0.0, alpha: 0.9), + cornerRadius: size.height * 0.5, + smoothCorners: false + )), + environment: {}, + containerSize: size + ) + let backgroundFrame = CGRect(origin: CGPoint(), size: size) + if let backgroundView = self.background.view { + if backgroundView.superview == nil { + backgroundView.isUserInteractionEnabled = false + self.addSubview(backgroundView) + } + transition.setFrame(view: backgroundView, frame: backgroundFrame) + } + + let titleFrame = CGRect(origin: CGPoint(x: avatarLeftInset + avatarWidth + avatarSpacing, y: floor((size.height - titleSize.height) * 0.5)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + titleView.layer.anchorPoint = CGPoint() + self.addSubview(titleView) + } + transition.setPosition(view: titleView, position: titleFrame.origin) + titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + } + + let avatarNode: AvatarNode + if let current = self.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 15.0)) + self.avatarNode = avatarNode + self.addSubview(avatarNode.view) + avatarNode.isUserInteractionEnabled = false + } + + let avatarSize = CGSize(width: avatarWidth, height: avatarWidth) + + let clipStyle: AvatarNodeClipStyle + if case let .channel(channel) = component.peer, channel.flags.contains(.isForum) { + clipStyle = .roundedRect + } else { + clipStyle = .round + } + + if component.peer.smallProfileImage != nil { + avatarNode.setPeerV2( + context: component.context, + theme: component.theme, + peer: component.peer, + authorOfMessage: nil, + overrideImage: nil, + emptyColor: nil, + clipStyle: .round, + synchronousLoad: false, + displayDimensions: avatarSize + ) + } else { + avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer, clipStyle: clipStyle, synchronousLoad: false, displayDimensions: avatarSize) + } + + let avatarFrame = CGRect(origin: CGPoint(x: avatarLeftInset, y: avatarVerticalInset), size: avatarSize) + transition.setPosition(view: avatarNode.view, position: avatarFrame.center) + transition.setBounds(view: avatarNode.view, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size)) + avatarNode.updateSize(size: avatarSize) + + return size + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramCallsUI/Sources/VideoChatMicButtonComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatMicButtonComponent.swift index 8ecf340e20..d7db6cc15f 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatMicButtonComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatMicButtonComponent.swift @@ -184,7 +184,7 @@ final class VideoChatMicButtonComponent: Component { case connecting case muted case unmuted(pushToTalk: Bool) - case raiseHand + case raiseHand(isRaised: Bool) case scheduled(state: ScheduledState) } @@ -226,6 +226,7 @@ final class VideoChatMicButtonComponent: Component { private var disappearingBackgrounds: [UIImageView] = [] private var progressIndicator: RadialStatusNode? private let title = ComponentView() + private var subtitle: ComponentView? private let icon: VoiceChatActionButtonIconNode private var glowView: GlowView? @@ -322,6 +323,7 @@ final class VideoChatMicButtonComponent: Component { let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.2) let titleText: String + var subtitleText: String? var isEnabled = true switch component.content { case .connecting: @@ -331,8 +333,14 @@ final class VideoChatMicButtonComponent: Component { titleText = "Unmute" case let .unmuted(isPushToTalk): titleText = isPushToTalk ? "You are Live" : "Tap to Mute" - case .raiseHand: - titleText = "Raise Hand" + case let .raiseHand(isRaised): + if isRaised { + titleText = "You asked to speak" + subtitleText = "We let the speakers know" + } else { + titleText = "Muted by Admin" + subtitleText = "Tap if you want to speak" + } case let .scheduled(state): switch state { case .start: @@ -353,7 +361,7 @@ final class VideoChatMicButtonComponent: Component { text: .plain(NSAttributedString(string: titleText, font: Font.regular(15.0), textColor: .white)) )), environment: {}, - containerSize: CGSize(width: 120.0, height: 100.0) + containerSize: CGSize(width: 180.0, height: 100.0) ) let size = CGSize(width: availableSize.width, height: availableSize.height) @@ -470,7 +478,10 @@ final class VideoChatMicButtonComponent: Component { transition.setScale(view: disappearingBackground, scale: size.width / 116.0) } - let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) * 0.5), y: size.height + 16.0), size: titleSize) + var titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) * 0.5), y: size.height + 16.0), size: titleSize) + if subtitleText != nil { + titleFrame.origin.y -= 5.0 + } if let titleView = self.title.view { if titleView.superview == nil { titleView.isUserInteractionEnabled = false @@ -481,6 +492,47 @@ final class VideoChatMicButtonComponent: Component { alphaTransition.setAlpha(view: titleView, alpha: component.isCollapsed ? 0.0 : 1.0) } + if let subtitleText { + let subtitle: ComponentView + var subtitleTransition = transition + if let current = self.subtitle { + subtitle = current + } else { + subtitleTransition = subtitleTransition.withAnimation(.none) + subtitle = ComponentView() + self.subtitle = subtitle + } + let subtitleSize = subtitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: subtitleText, font: Font.regular(13.0), textColor: .white)) + )), + environment: {}, + containerSize: CGSize(width: 180.0, height: 100.0) + ) + let subtitleFrame = CGRect(origin: CGPoint(x: floor((size.width - subtitleSize.width) * 0.5), y: titleFrame.maxY + 1.0), size: subtitleSize) + if let subtitleView = subtitle.view { + if subtitleView.superview == nil { + subtitleView.isUserInteractionEnabled = false + self.addSubview(subtitleView) + + subtitleView.alpha = 0.0 + transition.animateScale(view: subtitleView, from: 0.001, to: 1.0) + } + subtitleTransition.setPosition(view: subtitleView, position: subtitleFrame.center) + subtitleView.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size) + alphaTransition.setAlpha(view: subtitleView, alpha: component.isCollapsed ? 0.0 : 1.0) + } + } else if let subtitle = self.subtitle { + self.subtitle = nil + if let subtitleView = subtitle.view { + transition.setScale(view: subtitleView, scale: 0.001) + alphaTransition.setAlpha(view: subtitleView, alpha: 0.0, completion: { [weak subtitleView] _ in + subtitleView?.removeFromSuperview() + }) + } + } + if self.icon.view.superview == nil { self.icon.view.isUserInteractionEnabled = false self.addSubview(self.icon.view) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift index 2369a08d5c..834fc03318 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift @@ -283,7 +283,7 @@ final class VideoChatParticipantAvatarComponent: Component { transition.setBounds(view: avatarNode.view, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size)) avatarNode.updateSize(size: avatarSize) - let blobScale: CGFloat = 1.5 + let blobScale: CGFloat = 2.0 if self.audioLevelDisposable == nil { struct Level { diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift index c5c197f6e2..dda1b187c8 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift @@ -117,6 +117,13 @@ final class VideoChatParticipantsComponent: Component { } } + final class EventCycleState { + var ignoreScrolling: Bool = false + + init() { + } + } + let call: PresentationGroupCall let participants: Participants? let speakingParticipants: Set @@ -132,6 +139,7 @@ final class VideoChatParticipantsComponent: Component { let updateIsMainParticipantPinned: (Bool) -> Void let updateIsExpandedUIHidden: (Bool) -> Void let openInviteMembers: () -> Void + let visibleParticipantsUpdated: (Set) -> Void init( call: PresentationGroupCall, @@ -148,7 +156,8 @@ final class VideoChatParticipantsComponent: Component { updateMainParticipant: @escaping (VideoParticipantKey?, Bool?) -> Void, updateIsMainParticipantPinned: @escaping (Bool) -> Void, updateIsExpandedUIHidden: @escaping (Bool) -> Void, - openInviteMembers: @escaping () -> Void + openInviteMembers: @escaping () -> Void, + visibleParticipantsUpdated: @escaping (Set) -> Void ) { self.call = call self.participants = participants @@ -165,6 +174,7 @@ final class VideoChatParticipantsComponent: Component { self.updateIsMainParticipantPinned = updateIsMainParticipantPinned self.updateIsExpandedUIHidden = updateIsExpandedUIHidden self.openInviteMembers = openInviteMembers + self.visibleParticipantsUpdated = visibleParticipantsUpdated } static func ==(lhs: VideoChatParticipantsComponent, rhs: VideoChatParticipantsComponent) -> Bool { @@ -477,7 +487,7 @@ final class VideoChatParticipantsComponent: Component { self.scrollClippingFrame = CGRect(origin: CGPoint(x: self.listFrame.minX, y: layout.mainColumn.insets.top), size: CGSize(width: self.listFrame.width, height: containerSize.height - layout.mainColumn.insets.top)) } else { self.listFrame = CGRect(origin: CGPoint(x: floor((containerSize.width - listWidth) * 0.5), y: 0.0), size: CGSize(width: listWidth, height: containerSize.height)) - self.scrollClippingFrame = CGRect(origin: CGPoint(x: self.listFrame.minX, y: layout.mainColumn.insets.top), size: CGSize(width: listWidth, height: containerSize.height - layout.mainColumn.insets.top - layout.mainColumn.insets.bottom)) + self.scrollClippingFrame = CGRect(origin: CGPoint(x: self.listFrame.minX + layout.mainColumn.insets.left, y: layout.mainColumn.insets.top), size: CGSize(width: listWidth - layout.mainColumn.insets.left - layout.mainColumn.insets.right, height: containerSize.height - layout.mainColumn.insets.top)) self.separateVideoGridFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 0.0, height: containerSize.height)) self.separateVideoScrollClippingFrame = CGRect(origin: CGPoint(x: self.separateVideoGridFrame.minX, y: layout.mainColumn.insets.top), size: CGSize(width: self.separateVideoGridFrame.width, height: containerSize.height - layout.mainColumn.insets.top)) @@ -599,6 +609,7 @@ final class VideoChatParticipantsComponent: Component { final class View: UIView, UIScrollViewDelegate { private let scrollViewClippingContainer: SolidRoundedCornersContainer private let scrollView: ScrollView + private let scrollViewBottomShadowView: UIImageView private let separateVideoScrollViewClippingContainer: SolidRoundedCornersContainer private let separateVideoScrollView: ScrollView @@ -622,6 +633,7 @@ final class VideoChatParticipantsComponent: Component { private let expandedGridItemContainer: UIView private var expandedControlsView: ComponentView? private var expandedThumbnailsView: ComponentView? + private var expandedSpeakingToast: ComponentView? private var listItemViews: [EnginePeer.Id: ListItem] = [:] private let listItemViewContainer: UIView @@ -635,9 +647,13 @@ final class VideoChatParticipantsComponent: Component { private var currentLoadMoreToken: String? + private var mainScrollViewEventCycleState: EventCycleState? + private var separateVideoScrollViewEventCycleState: EventCycleState? + override init(frame: CGRect) { self.scrollViewClippingContainer = SolidRoundedCornersContainer() self.scrollView = ScrollView() + self.scrollViewBottomShadowView = UIImageView() self.separateVideoScrollViewClippingContainer = SolidRoundedCornersContainer() self.separateVideoScrollView = ScrollView() @@ -687,6 +703,7 @@ final class VideoChatParticipantsComponent: Component { self.scrollViewClippingContainer.addSubview(self.scrollView) self.addSubview(self.scrollViewClippingContainer) self.addSubview(self.scrollViewClippingContainer.cornersView) + self.addSubview(self.scrollViewBottomShadowView) self.separateVideoScrollViewClippingContainer.addSubview(self.separateVideoScrollView) self.addSubview(self.separateVideoScrollViewClippingContainer) @@ -765,10 +782,46 @@ final class VideoChatParticipantsComponent: Component { func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { + if scrollView == self.scrollView { + if let eventCycleState = self.mainScrollViewEventCycleState { + if eventCycleState.ignoreScrolling { + self.ignoreScrolling = true + scrollView.contentOffset = CGPoint() + self.ignoreScrolling = false + return + } + } + } else if scrollView == self.separateVideoScrollView { + if let eventCycleState = self.separateVideoScrollViewEventCycleState { + if eventCycleState.ignoreScrolling { + self.ignoreScrolling = true + scrollView.contentOffset = CGPoint() + self.ignoreScrolling = false + return + } + } + } + self.updateScrolling(transition: .immediate) } } + func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + if scrollView == self.scrollView { + if let eventCycleState = self.mainScrollViewEventCycleState { + if eventCycleState.ignoreScrolling { + targetContentOffset.pointee.y = 0.0 + } + } + } else if scrollView == self.separateVideoScrollView { + if let eventCycleState = self.separateVideoScrollViewEventCycleState { + if eventCycleState.ignoreScrolling { + targetContentOffset.pointee.y = 0.0 + } + } + } + } + private func updateScrolling(transition: ComponentTransition) { guard let component = self.component, let itemLayout = self.itemLayout else { return @@ -832,11 +885,18 @@ final class VideoChatParticipantsComponent: Component { var validGridItemIds: [VideoParticipantKey] = [] var validGridItemIndices: [Int] = [] + var clippedScrollViewBounds = self.scrollView.bounds + clippedScrollViewBounds.origin.y += component.layout.mainColumn.insets.top + clippedScrollViewBounds.size.height -= component.layout.mainColumn.insets.top + component.layout.mainColumn.insets.bottom + let visibleGridItemRange: (minIndex: Int, maxIndex: Int) + let clippedVisibleGridItemRange: (minIndex: Int, maxIndex: Int) if itemLayout.layout.videoColumn == nil { visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.scrollView.bounds) + clippedVisibleGridItemRange = itemLayout.visibleGridItemRange(for: clippedScrollViewBounds) } else { visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.separateVideoScrollView.bounds) + clippedVisibleGridItemRange = visibleGridItemRange } if visibleGridItemRange.maxIndex >= visibleGridItemRange.minIndex { for index in visibleGridItemRange.minIndex ... visibleGridItemRange.maxIndex { @@ -852,6 +912,8 @@ final class VideoChatParticipantsComponent: Component { validGridItemIndices.append(index) } } + + var visibleParticipants: [EnginePeer.Id] = [] for index in validGridItemIndices { let videoParticipant = self.gridParticipants[index] @@ -879,6 +941,10 @@ final class VideoChatParticipantsComponent: Component { } } + if isItemExpanded || (index >= clippedVisibleGridItemRange.minIndex && index <= clippedVisibleGridItemRange.maxIndex) { + visibleParticipants.append(videoParticipant.key.id) + } + var suppressItemExpansionCollapseAnimation = false if isItemExpanded { if let previousExpandedItemId, previousExpandedItemId != videoParticipantKey { @@ -1066,11 +1132,16 @@ final class VideoChatParticipantsComponent: Component { var validListItemIds: [EnginePeer.Id] = [] let visibleListItemRange = itemLayout.visibleListItemRange(for: self.scrollView.bounds) + let clippedVisibleListItemRange = itemLayout.visibleListItemRange(for: clippedScrollViewBounds) if visibleListItemRange.maxIndex >= visibleListItemRange.minIndex { for i in visibleListItemRange.minIndex ... visibleListItemRange.maxIndex { let participant = self.listParticipants[i] validListItemIds.append(participant.peer.id) + if i >= clippedVisibleListItemRange.minIndex && i <= clippedVisibleListItemRange.maxIndex { + visibleParticipants.append(participant.peer.id) + } + var itemTransition = transition let itemView: ListItem if let current = self.listItemViews[participant.peer.id] { @@ -1087,9 +1158,15 @@ final class VideoChatParticipantsComponent: Component { if participant.peer.id == component.call.accountContext.account.peerId { subtitle = PeerListItemComponent.Subtitle(text: "this is you", color: .accent) } else if component.speakingParticipants.contains(participant.peer.id) { - subtitle = PeerListItemComponent.Subtitle(text: "speaking", color: .constructive) + if let volume = participant.volume, volume != 10000 { + subtitle = PeerListItemComponent.Subtitle(text: "\(volume / 100)% speaking", color: .constructive) + } else { + subtitle = PeerListItemComponent.Subtitle(text: "speaking", color: .constructive) + } + } else if let about = participant.about, !about.isEmpty { + subtitle = PeerListItemComponent.Subtitle(text: about, color: .neutral) } else { - subtitle = PeerListItemComponent.Subtitle(text: participant.about ?? "listening", color: .neutral) + subtitle = PeerListItemComponent.Subtitle(text: "listening", color: .neutral) } let rightAccessoryComponent: AnyComponent = AnyComponent(VideoChatParticipantStatusComponent( @@ -1412,12 +1489,86 @@ final class VideoChatParticipantsComponent: Component { } } + if let expandedVideoState = component.expandedVideoState, expandedVideoState.isMainParticipantPinned, let participants = component.participants, !component.speakingParticipants.isEmpty, let firstOther = component.speakingParticipants.first(where: { $0 != expandedVideoState.mainParticipant.id }), let speakingPeer = participants.participants.first(where: { $0.peer.id == firstOther })?.peer { + let expandedSpeakingToast: ComponentView + var expandedSpeakingToastTransition = transition + if let current = self.expandedSpeakingToast { + expandedSpeakingToast = current + } else { + expandedSpeakingToastTransition = expandedSpeakingToastTransition.withAnimation(.none) + expandedSpeakingToast = ComponentView() + self.expandedSpeakingToast = expandedSpeakingToast + } + let expandedSpeakingToastSize = expandedSpeakingToast.update( + transition: expandedSpeakingToastTransition, + component: AnyComponent(VideoChatExpandedSpeakingToastComponent( + context: component.call.accountContext, + peer: EnginePeer(speakingPeer), + strings: component.strings, + theme: component.theme, + action: { [weak self] peer in + guard let self, let component = self.component, let participants = component.participants else { + return + } + guard let participant = participants.participants.first(where: { $0.peer.id == peer.id }) else { + return + } + var key: VideoParticipantKey? + if participant.presentationDescription != nil { + key = VideoParticipantKey(id: peer.id, isPresentation: true) + } else if participant.videoDescription != nil { + key = VideoParticipantKey(id: peer.id, isPresentation: false) + } + if let key { + component.updateMainParticipant(key, nil) + } + } + )), + environment: {}, + containerSize: itemLayout.expandedGrid.itemContainerFrame().size + ) + let expandedSpeakingToastFrame = CGRect(origin: CGPoint(x: floor((itemLayout.expandedGrid.itemContainerFrame().size.width - expandedSpeakingToastSize.width) * 0.5), y: 44.0), size: expandedSpeakingToastSize) + if let expandedSpeakingToastView = expandedSpeakingToast.view { + var animateIn = false + if expandedSpeakingToastView.superview == nil { + animateIn = true + self.expandedGridItemContainer.addSubview(expandedSpeakingToastView) + } + expandedSpeakingToastTransition.setFrame(view: expandedSpeakingToastView, frame: expandedSpeakingToastFrame) + + if animateIn { + alphaTransition.animateAlpha(view: expandedSpeakingToastView, from: 0.0, to: 1.0) + transition.animateScale(view: expandedSpeakingToastView, from: 0.6, to: 1.0) + } + } + } else { + if let expandedSpeakingToast = self.expandedSpeakingToast { + self.expandedSpeakingToast = nil + if let expandedSpeakingToastView = expandedSpeakingToast.view { + alphaTransition.setAlpha(view: expandedSpeakingToastView, alpha: 0.0, completion: { [weak expandedSpeakingToastView] _ in + expandedSpeakingToastView?.removeFromSuperview() + }) + transition.setScale(view: expandedSpeakingToastView, scale: 0.6) + } + } + } + if let participants = component.participants, let loadMoreToken = participants.loadMoreToken, visibleListItemRange.maxIndex >= self.listParticipants.count - 5 { if self.currentLoadMoreToken != loadMoreToken { self.currentLoadMoreToken = loadMoreToken component.call.loadMoreMembers(token: loadMoreToken) } } + + component.visibleParticipantsUpdated(Set(visibleParticipants)) + } + + func setEventCycleState(scrollView: UIScrollView, eventCycleState: EventCycleState?) { + if scrollView == self.scrollView { + self.mainScrollViewEventCycleState = eventCycleState + } else if scrollView == self.separateVideoScrollView { + self.separateVideoScrollViewEventCycleState = eventCycleState + } } func update(component: VideoChatParticipantsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { @@ -1482,11 +1633,16 @@ final class VideoChatParticipantsComponent: Component { var listParticipants: [GroupCallParticipantsContext.Participant] = [] if let participants = component.participants { for participant in participants.participants { + var isFullyMuted = false + if let muteState = participant.muteState, !muteState.canUnmute { + isFullyMuted = true + } + var hasVideo = false if participant.videoDescription != nil { hasVideo = true let videoParticipant = VideoParticipant(participant: participant, isPresentation: false) - if participant.peer.id == component.call.accountContext.account.peerId || participant.peer.id == participants.myPeerId { + if participant.peer.id == participants.myPeerId { gridParticipants.insert(videoParticipant, at: 0) } else { gridParticipants.append(videoParticipant) @@ -1495,14 +1651,14 @@ final class VideoChatParticipantsComponent: Component { if participant.presentationDescription != nil { hasVideo = true let videoParticipant = VideoParticipant(participant: participant, isPresentation: true) - if participant.peer.id == component.call.accountContext.account.peerId { + if participant.peer.id == participants.myPeerId { gridParticipants.insert(videoParticipant, at: 0) } else { gridParticipants.append(videoParticipant) } } if !hasVideo || component.layout.videoColumn != nil { - if participant.peer.id == component.call.accountContext.account.peerId { + if participant.peer.id == participants.myPeerId && !isFullyMuted { listParticipants.insert(participant, at: 0) } else { listParticipants.append(participant) @@ -1594,6 +1750,37 @@ final class VideoChatParticipantsComponent: Component { smoothCorners: false ), transition: transition) + if self.scrollViewBottomShadowView.image == nil { + let height: CGFloat = 80.0 + let baseGradientAlpha: CGFloat = 1.0 + let numSteps = 8 + let firstStep = 0 + let firstLocation = 0.0 + let colors = (0 ..< numSteps).map { i -> UIColor in + if i < firstStep { + return UIColor(white: 1.0, alpha: 1.0) + } else { + let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1) + let value: CGFloat = 1.0 - bezierPoint(0.42, 0.0, 0.58, 1.0, step) + return UIColor(white: 0.0, alpha: baseGradientAlpha * value) + } + } + let locations = (0 ..< numSteps).map { i -> CGFloat in + if i < firstStep { + return 0.0 + } else { + let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1) + return (firstLocation + (1.0 - firstLocation) * step) + } + } + + self.scrollViewBottomShadowView.image = generateGradientImage(size: CGSize(width: 8.0, height: height), colors: colors.reversed(), locations: locations.reversed().map { 1.0 - $0 })!.withRenderingMode(.alwaysTemplate).stretchableImage(withLeftCapWidth: 0, topCapHeight: Int(height - 1.0)) + self.scrollViewBottomShadowView.tintColor = .black + } + let scrollViewBottomShadowOverflow: CGFloat = 30.0 + let scrollViewBottomShadowFrame = CGRect(origin: CGPoint(x: itemLayout.scrollClippingFrame.minX, y: itemLayout.scrollClippingFrame.maxY - component.layout.mainColumn.insets.bottom - scrollViewBottomShadowOverflow), size: CGSize(width: itemLayout.scrollClippingFrame.width, height: component.layout.mainColumn.insets.bottom + scrollViewBottomShadowOverflow)) + transition.setFrame(view: self.scrollViewBottomShadowView, frame: scrollViewBottomShadowFrame) + transition.setPosition(view: self.separateVideoScrollViewClippingContainer, position: itemLayout.separateVideoScrollClippingFrame.center) transition.setBounds(view: self.separateVideoScrollViewClippingContainer, bounds: CGRect(origin: CGPoint(x: itemLayout.separateVideoScrollClippingFrame.minX - itemLayout.separateVideoGridFrame.minX, y: itemLayout.separateVideoScrollClippingFrame.minY - itemLayout.separateVideoGridFrame.minY), size: itemLayout.separateVideoScrollClippingFrame.size)) transition.setFrame(view: self.separateVideoScrollViewClippingContainer.cornersView, frame: itemLayout.separateVideoScrollClippingFrame) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index 906f913916..85227b283c 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -41,15 +41,22 @@ final class VideoChatScreenComponent: Component { return true } - private struct PanGestureState { - var offsetFraction: CGFloat + private final class PanState { + var fraction: CGFloat + weak var scrollView: UIScrollView? + var startContentOffsetY: CGFloat = 0.0 + var accumulatedOffset: CGFloat = 0.0 + var dismissedTooltips: Bool = false + var didLockScrolling: Bool = false + var contentOffset: CGFloat? - init(offsetFraction: CGFloat) { - self.offsetFraction = offsetFraction + init(fraction: CGFloat, scrollView: UIScrollView?) { + self.fraction = fraction + self.scrollView = scrollView } } - final class View: UIView { + final class View: UIView, UIGestureRecognizerDelegate { let containerView: UIView var component: VideoChatScreenComponent? @@ -57,7 +64,7 @@ final class VideoChatScreenComponent: Component { weak var state: EmptyComponentState? var isUpdating: Bool = false - private var panGestureState: PanGestureState? + private var verticalPanState: PanState? var notifyDismissedInteractivelyOnPanGestureApply: Bool = false var completionOnPanGestureApply: (() -> Void)? @@ -95,6 +102,9 @@ final class VideoChatScreenComponent: Component { var members: PresentationGroupCallMembers? var membersDisposable: Disposable? + var speakingParticipantPeers: [EnginePeer] = [] + var visibleParticipants: Set = Set() + let isPresentedValue = ValuePromise(false, ignoreRepeated: true) var applicationStateDisposable: Disposable? @@ -117,9 +127,11 @@ final class VideoChatScreenComponent: Component { self.addSubview(self.containerView) - self.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) + let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + panGestureRecognizer.delegate = self + self.addGestureRecognizer(panGestureRecognizer) - self.panGestureState = PanGestureState(offsetFraction: 1.0) + self.verticalPanState = PanState(fraction: 1.0, scrollView: nil) } required init?(coder: NSCoder) { @@ -139,37 +151,159 @@ final class VideoChatScreenComponent: Component { } func animateIn() { - self.panGestureState = PanGestureState(offsetFraction: 1.0) + self.verticalPanState = PanState(fraction: 1.0, scrollView: nil) self.state?.updated(transition: .immediate) - self.panGestureState = nil + self.verticalPanState = nil self.state?.updated(transition: .spring(duration: 0.5)) } func animateOut(completion: @escaping () -> Void) { - self.panGestureState = PanGestureState(offsetFraction: 1.0) + self.verticalPanState = PanState(fraction: 1.0, scrollView: nil) self.completionOnPanGestureApply = completion self.state?.updated(transition: .spring(duration: 0.5)) } + @objc public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer is UITapGestureRecognizer { + if otherGestureRecognizer is UIPanGestureRecognizer { + return true + } + return false + } else { + return false + } + } + + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer is UIPanGestureRecognizer { + if let otherGestureRecognizer = otherGestureRecognizer as? UIPanGestureRecognizer { + if otherGestureRecognizer.view is UIScrollView { + return true + } + if let participantsView = self.participants.view as? VideoChatParticipantsComponent.View { + if otherGestureRecognizer.view === participantsView { + return true + } + } + } + return false + } else { + return false + } + } + + @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { switch recognizer.state { case .began, .changed: if !self.bounds.height.isZero && !self.notifyDismissedInteractivelyOnPanGestureApply { let translation = recognizer.translation(in: self) - self.panGestureState = PanGestureState(offsetFraction: translation.y / self.bounds.height) - self.state?.updated(transition: .immediate) + let fraction = max(0.0, translation.y / self.bounds.height) + if let verticalPanState = self.verticalPanState { + verticalPanState.fraction = fraction + } else { + var targetScrollView: UIScrollView? + if case .began = recognizer.state, let participantsView = self.participants.view as? VideoChatParticipantsComponent.View { + if let hitResult = participantsView.hitTest(self.convert(recognizer.location(in: self), to: participantsView), with: nil) { + func findTargetScrollView(target: UIView, minParent: UIView) -> UIScrollView? { + if target === participantsView { + return nil + } + if let target = target as? UIScrollView { + return target + } + if let parent = target.superview { + return findTargetScrollView(target: parent, minParent: minParent) + } else { + return nil + } + } + targetScrollView = findTargetScrollView(target: hitResult, minParent: participantsView) + } + } + self.verticalPanState = PanState(fraction: fraction, scrollView: targetScrollView) + if let targetScrollView { + self.verticalPanState?.contentOffset = targetScrollView.contentOffset.y + self.verticalPanState?.startContentOffsetY = recognizer.translation(in: self).y + } + } + + if let verticalPanState = self.verticalPanState { + /*if abs(verticalPanState.fraction) >= 0.1 && !verticalPanState.dismissedTooltips { + verticalPanState.dismissedTooltips = true + self.dismissAllTooltips() + }*/ + + if let scrollView = verticalPanState.scrollView { + let relativeTranslationY = recognizer.translation(in: self).y - verticalPanState.startContentOffsetY + let overflowY = scrollView.contentOffset.y - relativeTranslationY + + if !verticalPanState.didLockScrolling { + if scrollView.contentOffset.y == 0.0 { + verticalPanState.didLockScrolling = true + } + if let previousContentOffset = verticalPanState.contentOffset, (previousContentOffset < 0.0) != (scrollView.contentOffset.y < 0.0) { + verticalPanState.didLockScrolling = true + } + } + + var resetContentOffset = false + if verticalPanState.didLockScrolling { + verticalPanState.accumulatedOffset += -overflowY + + if verticalPanState.accumulatedOffset < 0.0 { + verticalPanState.accumulatedOffset = 0.0 + } + if scrollView.contentOffset.y < 0.0 { + resetContentOffset = true + } + } else { + verticalPanState.accumulatedOffset += -overflowY + verticalPanState.accumulatedOffset = max(0.0, verticalPanState.accumulatedOffset) + } + + if verticalPanState.accumulatedOffset > 0.0 || resetContentOffset { + scrollView.contentOffset = CGPoint() + + if let participantsView = self.participants.view as? VideoChatParticipantsComponent.View { + let eventCycleState = VideoChatParticipantsComponent.EventCycleState() + eventCycleState.ignoreScrolling = true + participantsView.setEventCycleState(scrollView: scrollView, eventCycleState: eventCycleState) + + DispatchQueue.main.async { [weak scrollView, weak participantsView] in + guard let participantsView, let scrollView else { + return + } + participantsView.setEventCycleState(scrollView: scrollView, eventCycleState: nil) + } + } + } + + verticalPanState.contentOffset = scrollView.contentOffset.y + verticalPanState.startContentOffsetY = recognizer.translation(in: self).y + } + + self.state?.updated(transition: .immediate) + } } case .cancelled, .ended: - if !self.bounds.height.isZero { + if !self.bounds.height.isZero, let verticalPanState = self.verticalPanState { let translation = recognizer.translation(in: self) - let panGestureState = PanGestureState(offsetFraction: translation.y / self.bounds.height) + verticalPanState.fraction = max(0.0, translation.y / self.bounds.height) + + let effectiveFraction: CGFloat + if verticalPanState.scrollView != nil { + effectiveFraction = verticalPanState.accumulatedOffset / self.bounds.height + } else { + effectiveFraction = verticalPanState.fraction + } let velocity = recognizer.velocity(in: self) - self.panGestureState = nil - if abs(panGestureState.offsetFraction) > 0.6 || abs(velocity.y) >= 100.0 { - self.panGestureState = PanGestureState(offsetFraction: panGestureState.offsetFraction < 0.0 ? -1.0 : 1.0) + self.verticalPanState = nil + if effectiveFraction > 0.6 || (effectiveFraction > 0.0 && velocity.y >= 100.0) { + self.verticalPanState = PanState(fraction: effectiveFraction < 0.0 ? -1.0 : 1.0, scrollView: nil) self.notifyDismissedInteractivelyOnPanGestureApply = true if let controller = self.environment?.controller() as? VideoChatScreenV2Impl { controller.notifyDismissed() @@ -556,6 +690,39 @@ final class VideoChatScreenComponent: Component { } } + private func onVisibleParticipantsUpdated(ids: Set) { + if self.visibleParticipants == ids { + return + } + self.visibleParticipants = ids + self.updateTitleSpeakingStatus() + } + + private func updateTitleSpeakingStatus() { + guard let titleView = self.title.view as? VideoChatTitleComponent.View else { + return + } + + if self.speakingParticipantPeers.isEmpty { + titleView.updateActivityStatus(value: nil, transition: .easeInOut(duration: 0.2)) + } else { + var titleSpeakingStatusValue = "" + for participant in self.speakingParticipantPeers { + if !self.visibleParticipants.contains(participant.id) { + if !titleSpeakingStatusValue.isEmpty { + titleSpeakingStatusValue.append(", ") + } + titleSpeakingStatusValue.append(participant.compactDisplayTitle) + } + } + if titleSpeakingStatusValue.isEmpty { + titleView.updateActivityStatus(value: nil, transition: .easeInOut(duration: 0.2)) + } else { + titleView.updateActivityStatus(value: titleSpeakingStatusValue, transition: .easeInOut(duration: 0.2)) + } + } + } + func update(component: VideoChatScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -585,7 +752,7 @@ final class VideoChatScreenComponent: Component { if self.members != members { var members = members - #if DEBUG && false + #if DEBUG && true if let membersValue = members { var participants = membersValue.participants for i in 1 ... 20 { @@ -640,25 +807,7 @@ final class VideoChatScreenComponent: Component { #endif if let membersValue = members { - var participants = membersValue.participants - participants = participants.sorted(by: { lhs, rhs in - guard let lhsIndex = membersValue.participants.firstIndex(where: { $0.peer.id == lhs.peer.id }) else { - return false - } - guard let rhsIndex = membersValue.participants.firstIndex(where: { $0.peer.id == rhs.peer.id }) else { - return false - } - - if let lhsActivityRank = lhs.activityRank, let rhsActivityRank = rhs.activityRank { - if lhsActivityRank != rhsActivityRank { - return lhsActivityRank < rhsActivityRank - } - } else if (lhs.activityRank == nil) != (rhs.activityRank == nil) { - return lhs.activityRank != nil - } - - return lhsIndex < rhsIndex - }) + let participants = membersValue.participants members = PresentationGroupCallMembers( participants: participants, speakingParticipants: membersValue.speakingParticipants, @@ -746,6 +895,19 @@ final class VideoChatScreenComponent: Component { if !self.isUpdating { self.state?.updated(transition: .spring(duration: 0.4)) } + + var speakingParticipantPeers: [EnginePeer] = [] + if let members, !members.speakingParticipants.isEmpty { + for participant in members.participants { + if members.speakingParticipants.contains(participant.peer.id) { + speakingParticipantPeers.append(EnginePeer(participant.peer)) + } + } + } + if self.speakingParticipantPeers != speakingParticipantPeers { + self.speakingParticipantPeers = speakingParticipantPeers + self.updateTitleSpeakingStatus() + } } }) @@ -898,8 +1060,12 @@ final class VideoChatScreenComponent: Component { } var containerOffset: CGFloat = 0.0 - if let panGestureState = self.panGestureState { - containerOffset = panGestureState.offsetFraction * availableSize.height + if let verticalPanState = self.verticalPanState { + if verticalPanState.scrollView != nil { + containerOffset = verticalPanState.accumulatedOffset + } else { + containerOffset = verticalPanState.fraction * availableSize.height + } self.containerView.layer.cornerRadius = environment.deviceMetrics.screenCornerRadius } @@ -907,7 +1073,7 @@ final class VideoChatScreenComponent: Component { guard let self, completed else { return } - if self.panGestureState == nil { + if self.verticalPanState == nil { self.containerView.layer.cornerRadius = 0.0 } if self.notifyDismissedInteractivelyOnPanGestureApply { @@ -1141,11 +1307,19 @@ final class VideoChatScreenComponent: Component { } } - let buttonsSideInset: CGFloat = 42.0 + let buttonsSideInset: CGFloat = 26.0 let buttonsWidth: CGFloat = actionButtonDiameter * 2.0 + microphoneButtonDiameter let remainingButtonsSpace: CGFloat = availableSize.width - buttonsSideInset * 2.0 - buttonsWidth - let actionMicrophoneButtonSpacing = min(maxActionMicrophoneButtonSpacing, floor(remainingButtonsSpace * 0.5)) + + let effectiveMaxActionMicrophoneButtonSpacing: CGFloat + if areButtonsCollapsed { + effectiveMaxActionMicrophoneButtonSpacing = 80.0 + } else { + effectiveMaxActionMicrophoneButtonSpacing = maxActionMicrophoneButtonSpacing + } + + let actionMicrophoneButtonSpacing = min(effectiveMaxActionMicrophoneButtonSpacing, floor(remainingButtonsSpace * 0.5)) var collapsedMicrophoneButtonFrame: CGRect = CGRect(origin: CGPoint(x: floor((availableSize.width - collapsedMicrophoneButtonDiameter) * 0.5), y: availableSize.height - 48.0 - environment.safeInsets.bottom - collapsedMicrophoneButtonDiameter), size: CGSize(width: collapsedMicrophoneButtonDiameter, height: collapsedMicrophoneButtonDiameter)) var expandedMicrophoneButtonFrame: CGRect = CGRect(origin: CGPoint(x: floor((availableSize.width - expandedMicrophoneButtonDiameter) * 0.5), y: availableSize.height - environment.safeInsets.bottom - expandedMicrophoneButtonDiameter - 12.0), size: CGSize(width: expandedMicrophoneButtonDiameter, height: expandedMicrophoneButtonDiameter)) @@ -1330,6 +1504,12 @@ final class VideoChatScreenComponent: Component { return } self.openInviteMembers() + }, + visibleParticipantsUpdated: { [weak self] visibleParticipants in + guard let self else { + return + } + self.onVisibleParticipantsUpdated(ids: visibleParticipants) } )), environment: {}, @@ -1403,8 +1583,8 @@ final class VideoChatScreenComponent: Component { micButtonContent = .connecting actionButtonMicrophoneState = .connecting case .connected: - if let callState = callState.muteState { - if callState.canUnmute { + if let muteState = callState.muteState { + if muteState.canUnmute { if self.isPushToTalkActive { micButtonContent = .unmuted(pushToTalk: self.isPushToTalkActive) actionButtonMicrophoneState = .unmuted @@ -1413,7 +1593,7 @@ final class VideoChatScreenComponent: Component { actionButtonMicrophoneState = .muted } } else { - micButtonContent = .raiseHand + micButtonContent = .raiseHand(isRaised: callState.raisedHand) actionButtonMicrophoneState = .raiseHand } } else { @@ -1741,9 +1921,11 @@ final class VideoChatScreenV2Impl: ViewControllerComponentContainer, VoiceChatCo } self.isAnimatingDismiss = false self.superDismiss() + completion?() }) } else { self.superDismiss() + completion?() } } } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift index 6ee08d0a90..dd81e23a91 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift @@ -248,8 +248,8 @@ extension VideoChatScreenComponent.View { } let context = component.call.accountContext - environment.controller()?.dismiss(completion: { [weak navigationController] in - Queue.mainQueue().after(0.3) { + controller.dismiss(completion: { [weak navigationController] in + Queue.mainQueue().after(0.1) { guard let navigationController else { return } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatTitleComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatTitleComponent.swift index 0f13e9d815..6b36289463 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatTitleComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatTitleComponent.swift @@ -5,6 +5,7 @@ import ComponentFlow import MultilineTextComponent import TelegramPresentationData import HierarchyTrackingLayer +import ChatTitleActivityNode final class VideoChatTitleComponent: Component { let title: String @@ -43,12 +44,17 @@ final class VideoChatTitleComponent: Component { final class View: UIView { private let hierarchyTrackingLayer: HierarchyTrackingLayer private let title = ComponentView() - private var status: ComponentView? + private let status = ComponentView() private var recordingImageView: UIImageView? + + private var activityStatusNode: ChatTitleActivityNode? private var component: VideoChatTitleComponent? private var isUpdating: Bool = false + private var currentActivityStatus: String? + private var currentSize: CGSize? + override init(frame: CGRect) { self.hierarchyTrackingLayer = HierarchyTrackingLayer() @@ -81,6 +87,64 @@ final class VideoChatTitleComponent: Component { } } + func updateActivityStatus(value: String?, transition: ComponentTransition) { + if self.currentActivityStatus == value { + return + } + self.currentActivityStatus = value + + guard let currentSize = self.currentSize, let statusView = self.status.view else { + return + } + + let alphaTransition: ComponentTransition + if transition.animation.isImmediate { + alphaTransition = .immediate + } else { + alphaTransition = .easeInOut(duration: 0.2) + } + + if let value { + let activityStatusNode: ChatTitleActivityNode + if let current = self.activityStatusNode { + activityStatusNode = current + } else { + activityStatusNode = ChatTitleActivityNode() + self.activityStatusNode = activityStatusNode + } + + let _ = activityStatusNode.transitionToState(.recordingVoice(NSAttributedString(string: value, font: Font.regular(13.0), textColor: UIColor(rgb: 0x34c759)), UIColor(rgb: 0x34c759)), animation: .none) + let activityStatusSize = activityStatusNode.updateLayout(CGSize(width: currentSize.width, height: 100.0), alignment: .center) + let activityStatusFrame = CGRect(origin: CGPoint(x: floor((currentSize.width - activityStatusSize.width) * 0.5), y: statusView.center.y - activityStatusSize.height * 0.5), size: activityStatusSize) + + let activityStatusNodeView = activityStatusNode.view + activityStatusNodeView.center = activityStatusFrame.center + activityStatusNodeView.bounds = CGRect(origin: CGPoint(), size: activityStatusFrame.size) + if activityStatusNodeView.superview == nil { + self.addSubview(activityStatusNode.view) + ComponentTransition.immediate.setTransform(view: activityStatusNodeView, transform: CATransform3DMakeTranslation(0.0, -10.0, 0.0)) + activityStatusNodeView.alpha = 0.0 + } + transition.setTransform(view: activityStatusNodeView, transform: CATransform3DIdentity) + alphaTransition.setAlpha(view: activityStatusNodeView, alpha: 1.0) + + transition.setTransform(view: statusView, transform: CATransform3DMakeTranslation(0.0, 10.0, 0.0)) + alphaTransition.setAlpha(view: statusView, alpha: 0.0) + } else { + if let activityStatusNode = self.activityStatusNode { + self.activityStatusNode = nil + let activityStatusNodeView = activityStatusNode.view + transition.setTransform(view: activityStatusNodeView, transform: CATransform3DMakeTranslation(0.0, -10.0, 0.0)) + alphaTransition.setAlpha(view: activityStatusNodeView, alpha: 0.0, completion: { [weak activityStatusNodeView] _ in + activityStatusNodeView?.removeFromSuperview() + }) + } + + transition.setTransform(view: statusView, transform: CATransform3DIdentity) + alphaTransition.setAlpha(view: statusView, alpha: 1.0) + } + } + func update(component: VideoChatTitleComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -100,19 +164,12 @@ final class VideoChatTitleComponent: Component { containerSize: CGSize(width: availableSize.width, height: 100.0) ) - let status: ComponentView - if let current = self.status { - status = current - } else { - status = ComponentView() - self.status = status - } let statusComponent: AnyComponent statusComponent = AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: component.status, font: Font.regular(13.0), textColor: UIColor(white: 1.0, alpha: 0.5))) )) - let statusSize = status.update( + let statusSize = self.status.update( transition: .immediate, component: statusComponent, environment: {}, @@ -131,7 +188,7 @@ final class VideoChatTitleComponent: Component { } let statusFrame = CGRect(origin: CGPoint(x: floor((size.width - statusSize.width) * 0.5), y: titleFrame.maxY + spacing), size: statusSize) - if let statusView = status.view { + if let statusView = self.status.view { if statusView.superview == nil { self.addSubview(statusView) } @@ -165,6 +222,8 @@ final class VideoChatTitleComponent: Component { } } + self.currentSize = size + return size } } diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift index 28fcc277ad..9474675578 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift @@ -324,13 +324,15 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu self.closeAction?() } - if #available(iOS 16.0, *) { - let pipVideoCallViewController = AVPictureInPictureVideoCallViewController() - pipVideoCallViewController.view.addSubview(self.pipView) - self.pipView.frame = pipVideoCallViewController.view.bounds - self.pipView.autoresizingMask = [.flexibleWidth, .flexibleHeight] - self.pipView.translatesAutoresizingMaskIntoConstraints = true - self.pipVideoCallViewController = pipVideoCallViewController + if !"".isEmpty { + if #available(iOS 16.0, *) { + let pipVideoCallViewController = AVPictureInPictureVideoCallViewController() + pipVideoCallViewController.view.addSubview(self.pipView) + self.pipView.frame = pipVideoCallViewController.view.bounds + self.pipView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + self.pipView.translatesAutoresizingMaskIntoConstraints = true + self.pipVideoCallViewController = pipVideoCallViewController + } } if let blurFilter = makeBlurFilter() { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index d37ea5fcd8..707944d151 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -1659,7 +1659,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr let loopVideo = updatedVideoFile.isAnimated let videoContent: UniversalVideoContent - if !"".isEmpty && NativeVideoContent.isHLSVideo(file: updatedVideoFile), context.sharedContext.immediateExperimentalUISettings.dynamicStreaming { + if !"".isEmpty && NativeVideoContent.isHLSVideo(file: updatedVideoFile) { 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 From 1a3f7dc42d61bc0fee0006cdc374fd0266d61f4a Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 24 Sep 2024 17:26:51 +0400 Subject: [PATCH 13/17] Content reporting --- .../Telegram-iOS/en.lproj/Localizable.strings | 2 + .../Sources/AccountContext.swift | 2 + submodules/TelegramApi/Sources/Api0.swift | 10 +- submodules/TelegramApi/Sources/Api16.swift | 124 +-- submodules/TelegramApi/Sources/Api17.swift | 84 ++ submodules/TelegramApi/Sources/Api21.swift | 82 ++ submodules/TelegramApi/Sources/Api23.swift | 20 +- submodules/TelegramApi/Sources/Api36.swift | 24 +- .../Messages/ReportContent.swift | 82 ++ .../Messages/TelegramEngineMessages.swift | 4 + .../TelegramEngine/Payments/Stars.swift | 14 +- .../TelegramEngine/Peers/ReportPeer.swift | 68 +- submodules/TelegramUI/BUILD | 1 + .../Sources/AdsReportScreen.swift | 1 + .../Components/ContentReportScreen/BUILD | 41 + .../Sources/ContentReportScreen.swift | 726 ++++++++++++++++++ .../Gifts/GiftAnimationComponent/BUILD | 31 + .../Sources/GiftAnimationComponent.swift | 98 +++ .../Components/Gifts/GiftViewScreen/BUILD | 1 + .../Sources/GiftViewScreen.swift | 108 +-- .../Sources/NavigationStackComponent.swift | 8 +- .../Sources/PeerInfoScreen.swift | 11 +- .../Stars/StarsTransactionScreen/BUILD | 1 + .../Sources/StarsTransactionScreen.swift | 32 +- .../StarsTransactionsListPanelComponent.swift | 5 +- .../StoryItemSetContainerComponent.swift | 83 +- .../Chat/ChatControllerLoadDisplayNode.swift | 32 +- .../Sources/SharedAccountContext.swift | 10 + 28 files changed, 1417 insertions(+), 288 deletions(-) create mode 100644 submodules/TelegramCore/Sources/TelegramEngine/Messages/ReportContent.swift create mode 100644 submodules/TelegramUI/Components/ContentReportScreen/BUILD create mode 100644 submodules/TelegramUI/Components/ContentReportScreen/Sources/ContentReportScreen.swift create mode 100644 submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/BUILD create mode 100644 submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftAnimationComponent.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 60ec2f47b2..7fc36c6c7b 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12285,6 +12285,8 @@ Sorry for the inconvenience."; "Stars.Intro.Transaction.FragmentWithdrawal.Subtitle" = "via Fragment"; "Stars.Intro.Transaction.TelegramAds.Title" = "Withdrawal"; "Stars.Intro.Transaction.TelegramAds.Subtitle" = "via Telegram Ads"; +"Stars.Intro.Transaction.Gift" = "Gift"; +"Stars.Intro.Transaction.ConvertedGift" = "Converted Gift"; "Stars.Intro.Transaction.Unsupported.Title" = "Unsupported"; "Stars.Intro.Transaction.Refund" = "Refund"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 2a44593c39..1f8fd53658 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1020,6 +1020,8 @@ public protocol SharedAccountContext: AnyObject { func makeStarsIntroScreen(context: AccountContext) -> ViewController func makeGiftViewScreen(context: AccountContext, message: EngineMessage) -> ViewController + func makeContentReportScreen(context: AccountContext, subject: ReportContentSubject, forceDark: Bool, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void) + func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 9faafe7bc0..aa22fb967b 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -631,6 +631,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-2083123262] = { return Api.MessageReplies.parse_messageReplies($0) } dict[-1346631205] = { return Api.MessageReplyHeader.parse_messageReplyHeader($0) } dict[240843065] = { return Api.MessageReplyHeader.parse_messageReplyStoryHeader($0) } + dict[2030298073] = { return Api.MessageReportOption.parse_messageReportOption($0) } dict[1163625789] = { return Api.MessageViews.parse_messageViews($0) } dict[975236280] = { return Api.MessagesFilter.parse_inputMessagesFilterChatPhotos($0) } dict[-530392189] = { return Api.MessagesFilter.parse_inputMessagesFilterContacts($0) } @@ -803,6 +804,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[777640226] = { return Api.ReportReason.parse_inputReportReasonPornography($0) } dict[1490799288] = { return Api.ReportReason.parse_inputReportReasonSpam($0) } dict[505595789] = { return Api.ReportReason.parse_inputReportReasonViolence($0) } + dict[1862904881] = { return Api.ReportResult.parse_reportResultAddComment($0) } + dict[-253435722] = { return Api.ReportResult.parse_reportResultChooseOption($0) } + dict[-1917633461] = { return Api.ReportResult.parse_reportResultReported($0) } dict[865857388] = { return Api.RequestPeerType.parse_requestPeerTypeBroadcast($0) } dict[-906990053] = { return Api.RequestPeerType.parse_requestPeerTypeChat($0) } dict[1597737472] = { return Api.RequestPeerType.parse_requestPeerTypeUser($0) } @@ -898,7 +902,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1401868056] = { return Api.StarsSubscription.parse_starsSubscription($0) } dict[88173912] = { return Api.StarsSubscriptionPricing.parse_starsSubscriptionPricing($0) } dict[198776256] = { return Api.StarsTopupOption.parse_starsTopupOption($0) } - dict[-294313259] = { return Api.StarsTransaction.parse_starsTransaction($0) } + dict[178185410] = { return Api.StarsTransaction.parse_starsTransaction($0) } dict[-670195363] = { return Api.StarsTransactionPeer.parse_starsTransactionPeer($0) } dict[1617438738] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerAds($0) } dict[-1269320843] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerAppStore($0) } @@ -1853,6 +1857,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.MessageReplyHeader: _1.serialize(buffer, boxed) + case let _1 as Api.MessageReportOption: + _1.serialize(buffer, boxed) case let _1 as Api.MessageViews: _1.serialize(buffer, boxed) case let _1 as Api.MessagesFilter: @@ -1969,6 +1975,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.ReportReason: _1.serialize(buffer, boxed) + case let _1 as Api.ReportResult: + _1.serialize(buffer, boxed) case let _1 as Api.RequestPeerType: _1.serialize(buffer, boxed) case let _1 as Api.RequestedPeer: diff --git a/submodules/TelegramApi/Sources/Api16.swift b/submodules/TelegramApi/Sources/Api16.swift index 4652a76a82..38015e3b82 100644 --- a/submodules/TelegramApi/Sources/Api16.swift +++ b/submodules/TelegramApi/Sources/Api16.swift @@ -482,6 +482,46 @@ public extension Api { } } +public extension Api { + enum MessageReportOption: TypeConstructorDescription { + case messageReportOption(text: String, option: Buffer) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .messageReportOption(let text, let option): + if boxed { + buffer.appendInt32(2030298073) + } + serializeString(text, buffer: buffer, boxed: false) + serializeBytes(option, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .messageReportOption(let text, let option): + return ("messageReportOption", [("text", text as Any), ("option", option as Any)]) + } + } + + public static func parse_messageReportOption(_ reader: BufferReader) -> MessageReportOption? { + var _1: String? + _1 = parseString(reader) + var _2: Buffer? + _2 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.MessageReportOption.messageReportOption(text: _1!, option: _2!) + } + else { + return nil + } + } + + } +} public extension Api { enum MessageViews: TypeConstructorDescription { case messageViews(flags: Int32, views: Int32?, forwards: Int32?, replies: Api.MessageReplies?) @@ -902,87 +942,3 @@ public extension Api { } } -public extension Api { - enum NotificationSound: TypeConstructorDescription { - case notificationSoundDefault - case notificationSoundLocal(title: String, data: String) - case notificationSoundNone - case notificationSoundRingtone(id: Int64) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .notificationSoundDefault: - if boxed { - buffer.appendInt32(-1746354498) - } - - break - case .notificationSoundLocal(let title, let data): - if boxed { - buffer.appendInt32(-2096391452) - } - serializeString(title, buffer: buffer, boxed: false) - serializeString(data, buffer: buffer, boxed: false) - break - case .notificationSoundNone: - if boxed { - buffer.appendInt32(1863070943) - } - - break - case .notificationSoundRingtone(let id): - if boxed { - buffer.appendInt32(-9666487) - } - serializeInt64(id, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .notificationSoundDefault: - return ("notificationSoundDefault", []) - case .notificationSoundLocal(let title, let data): - return ("notificationSoundLocal", [("title", title as Any), ("data", data as Any)]) - case .notificationSoundNone: - return ("notificationSoundNone", []) - case .notificationSoundRingtone(let id): - return ("notificationSoundRingtone", [("id", id as Any)]) - } - } - - public static func parse_notificationSoundDefault(_ reader: BufferReader) -> NotificationSound? { - return Api.NotificationSound.notificationSoundDefault - } - public static func parse_notificationSoundLocal(_ reader: BufferReader) -> NotificationSound? { - 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.NotificationSound.notificationSoundLocal(title: _1!, data: _2!) - } - else { - return nil - } - } - public static func parse_notificationSoundNone(_ reader: BufferReader) -> NotificationSound? { - return Api.NotificationSound.notificationSoundNone - } - public static func parse_notificationSoundRingtone(_ reader: BufferReader) -> NotificationSound? { - var _1: Int64? - _1 = reader.readInt64() - let _c1 = _1 != nil - if _c1 { - return Api.NotificationSound.notificationSoundRingtone(id: _1!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api17.swift b/submodules/TelegramApi/Sources/Api17.swift index 278c569f5c..11ea4e2a43 100644 --- a/submodules/TelegramApi/Sources/Api17.swift +++ b/submodules/TelegramApi/Sources/Api17.swift @@ -1,3 +1,87 @@ +public extension Api { + enum NotificationSound: TypeConstructorDescription { + case notificationSoundDefault + case notificationSoundLocal(title: String, data: String) + case notificationSoundNone + case notificationSoundRingtone(id: Int64) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .notificationSoundDefault: + if boxed { + buffer.appendInt32(-1746354498) + } + + break + case .notificationSoundLocal(let title, let data): + if boxed { + buffer.appendInt32(-2096391452) + } + serializeString(title, buffer: buffer, boxed: false) + serializeString(data, buffer: buffer, boxed: false) + break + case .notificationSoundNone: + if boxed { + buffer.appendInt32(1863070943) + } + + break + case .notificationSoundRingtone(let id): + if boxed { + buffer.appendInt32(-9666487) + } + serializeInt64(id, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .notificationSoundDefault: + return ("notificationSoundDefault", []) + case .notificationSoundLocal(let title, let data): + return ("notificationSoundLocal", [("title", title as Any), ("data", data as Any)]) + case .notificationSoundNone: + return ("notificationSoundNone", []) + case .notificationSoundRingtone(let id): + return ("notificationSoundRingtone", [("id", id as Any)]) + } + } + + public static func parse_notificationSoundDefault(_ reader: BufferReader) -> NotificationSound? { + return Api.NotificationSound.notificationSoundDefault + } + public static func parse_notificationSoundLocal(_ reader: BufferReader) -> NotificationSound? { + 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.NotificationSound.notificationSoundLocal(title: _1!, data: _2!) + } + else { + return nil + } + } + public static func parse_notificationSoundNone(_ reader: BufferReader) -> NotificationSound? { + return Api.NotificationSound.notificationSoundNone + } + public static func parse_notificationSoundRingtone(_ reader: BufferReader) -> NotificationSound? { + var _1: Int64? + _1 = reader.readInt64() + let _c1 = _1 != nil + if _c1 { + return Api.NotificationSound.notificationSoundRingtone(id: _1!) + } + else { + return nil + } + } + + } +} public extension Api { enum NotifyPeer: TypeConstructorDescription { case notifyBroadcasts diff --git a/submodules/TelegramApi/Sources/Api21.swift b/submodules/TelegramApi/Sources/Api21.swift index 73eedd5544..2e4266d3f5 100644 --- a/submodules/TelegramApi/Sources/Api21.swift +++ b/submodules/TelegramApi/Sources/Api21.swift @@ -134,6 +134,88 @@ public extension Api { } } +public extension Api { + enum ReportResult: TypeConstructorDescription { + case reportResultAddComment(flags: Int32, option: Buffer) + case reportResultChooseOption(title: String, options: [Api.MessageReportOption]) + case reportResultReported + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .reportResultAddComment(let flags, let option): + if boxed { + buffer.appendInt32(1862904881) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeBytes(option, buffer: buffer, boxed: false) + break + case .reportResultChooseOption(let title, let options): + if boxed { + buffer.appendInt32(-253435722) + } + serializeString(title, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(options.count)) + for item in options { + item.serialize(buffer, true) + } + break + case .reportResultReported: + if boxed { + buffer.appendInt32(-1917633461) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .reportResultAddComment(let flags, let option): + return ("reportResultAddComment", [("flags", flags as Any), ("option", option as Any)]) + case .reportResultChooseOption(let title, let options): + return ("reportResultChooseOption", [("title", title as Any), ("options", options as Any)]) + case .reportResultReported: + return ("reportResultReported", []) + } + } + + public static func parse_reportResultAddComment(_ reader: BufferReader) -> ReportResult? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Buffer? + _2 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.ReportResult.reportResultAddComment(flags: _1!, option: _2!) + } + else { + return nil + } + } + public static func parse_reportResultChooseOption(_ reader: BufferReader) -> ReportResult? { + var _1: String? + _1 = parseString(reader) + var _2: [Api.MessageReportOption]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageReportOption.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.ReportResult.reportResultChooseOption(title: _1!, options: _2!) + } + else { + return nil + } + } + public static func parse_reportResultReported(_ reader: BufferReader) -> ReportResult? { + return Api.ReportResult.reportResultReported + } + + } +} public extension Api { enum RequestPeerType: TypeConstructorDescription { case requestPeerTypeBroadcast(flags: Int32, hasUsername: Api.Bool?, userAdminRights: Api.ChatAdminRights?, botAdminRights: Api.ChatAdminRights?) diff --git a/submodules/TelegramApi/Sources/Api23.swift b/submodules/TelegramApi/Sources/Api23.swift index 5868481552..a89148bf05 100644 --- a/submodules/TelegramApi/Sources/Api23.swift +++ b/submodules/TelegramApi/Sources/Api23.swift @@ -1002,13 +1002,13 @@ public extension Api { } public extension Api { enum StarsTransaction: TypeConstructorDescription { - case starsTransaction(flags: Int32, id: String, stars: Int64, date: Int32, peer: Api.StarsTransactionPeer, title: String?, description: String?, photo: Api.WebDocument?, transactionDate: Int32?, transactionUrl: String?, botPayload: Buffer?, msgId: Int32?, extendedMedia: [Api.MessageMedia]?, subscriptionPeriod: Int32?, giveawayPostId: Int32?) + case starsTransaction(flags: Int32, id: String, stars: Int64, date: Int32, peer: Api.StarsTransactionPeer, title: String?, description: String?, photo: Api.WebDocument?, transactionDate: Int32?, transactionUrl: String?, botPayload: Buffer?, msgId: Int32?, extendedMedia: [Api.MessageMedia]?, subscriptionPeriod: Int32?, giveawayPostId: Int32?, stargift: Api.StarGift?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia, let subscriptionPeriod, let giveawayPostId): + case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia, let subscriptionPeriod, let giveawayPostId, let stargift): if boxed { - buffer.appendInt32(-294313259) + buffer.appendInt32(178185410) } serializeInt32(flags, buffer: buffer, boxed: false) serializeString(id, buffer: buffer, boxed: false) @@ -1029,14 +1029,15 @@ public extension Api { }} if Int(flags) & Int(1 << 12) != 0 {serializeInt32(subscriptionPeriod!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 13) != 0 {serializeInt32(giveawayPostId!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 14) != 0 {stargift!.serialize(buffer, true)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia, let subscriptionPeriod, let giveawayPostId): - return ("starsTransaction", [("flags", flags as Any), ("id", id as Any), ("stars", stars as Any), ("date", date as Any), ("peer", peer as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("transactionDate", transactionDate as Any), ("transactionUrl", transactionUrl as Any), ("botPayload", botPayload as Any), ("msgId", msgId as Any), ("extendedMedia", extendedMedia as Any), ("subscriptionPeriod", subscriptionPeriod as Any), ("giveawayPostId", giveawayPostId as Any)]) + case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia, let subscriptionPeriod, let giveawayPostId, let stargift): + return ("starsTransaction", [("flags", flags as Any), ("id", id as Any), ("stars", stars as Any), ("date", date as Any), ("peer", peer as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("transactionDate", transactionDate as Any), ("transactionUrl", transactionUrl as Any), ("botPayload", botPayload as Any), ("msgId", msgId as Any), ("extendedMedia", extendedMedia as Any), ("subscriptionPeriod", subscriptionPeriod as Any), ("giveawayPostId", giveawayPostId as Any), ("stargift", stargift as Any)]) } } @@ -1077,6 +1078,10 @@ public extension Api { if Int(_1!) & Int(1 << 12) != 0 {_14 = reader.readInt32() } var _15: Int32? if Int(_1!) & Int(1 << 13) != 0 {_15 = reader.readInt32() } + var _16: Api.StarGift? + if Int(_1!) & Int(1 << 14) != 0 {if let signature = reader.readInt32() { + _16 = Api.parse(reader, signature: signature) as? Api.StarGift + } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -1092,8 +1097,9 @@ public extension Api { let _c13 = (Int(_1!) & Int(1 << 9) == 0) || _13 != nil let _c14 = (Int(_1!) & Int(1 << 12) == 0) || _14 != nil let _c15 = (Int(_1!) & Int(1 << 13) == 0) || _15 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 { - return Api.StarsTransaction.starsTransaction(flags: _1!, id: _2!, stars: _3!, date: _4!, peer: _5!, title: _6, description: _7, photo: _8, transactionDate: _9, transactionUrl: _10, botPayload: _11, msgId: _12, extendedMedia: _13, subscriptionPeriod: _14, giveawayPostId: _15) + let _c16 = (Int(_1!) & Int(1 << 14) == 0) || _16 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 { + return Api.StarsTransaction.starsTransaction(flags: _1!, id: _2!, stars: _3!, date: _4!, peer: _5!, title: _6, description: _7, photo: _8, transactionDate: _9, transactionUrl: _10, botPayload: _11, msgId: _12, extendedMedia: _13, subscriptionPeriod: _14, giveawayPostId: _15, stargift: _16) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api36.swift b/submodules/TelegramApi/Sources/Api36.swift index 740c313af2..ed03072b36 100644 --- a/submodules/TelegramApi/Sources/Api36.swift +++ b/submodules/TelegramApi/Sources/Api36.swift @@ -7380,22 +7380,22 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func report(peer: Api.InputPeer, id: [Int32], reason: Api.ReportReason, message: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func report(peer: Api.InputPeer, id: [Int32], option: Buffer, message: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-1991005362) + buffer.appendInt32(-59199589) peer.serialize(buffer, true) buffer.appendInt32(481674261) buffer.appendInt32(Int32(id.count)) for item in id { serializeInt32(item, buffer: buffer, boxed: false) } - reason.serialize(buffer, true) + serializeBytes(option, buffer: buffer, boxed: false) serializeString(message, buffer: buffer, boxed: false) - return (FunctionDescription(name: "messages.report", parameters: [("peer", String(describing: peer)), ("id", String(describing: id)), ("reason", String(describing: reason)), ("message", String(describing: message))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + return (FunctionDescription(name: "messages.report", parameters: [("peer", String(describing: peer)), ("id", String(describing: id)), ("option", String(describing: option)), ("message", String(describing: message))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.ReportResult? in let reader = BufferReader(buffer) - var result: Api.Bool? + var result: Api.ReportResult? if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.Bool + result = Api.parse(reader, signature: signature) as? Api.ReportResult } return result }) @@ -10787,22 +10787,22 @@ public extension Api.functions.stories { } } public extension Api.functions.stories { - static func report(peer: Api.InputPeer, id: [Int32], reason: Api.ReportReason, message: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func report(peer: Api.InputPeer, id: [Int32], option: Buffer, message: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(421788300) + buffer.appendInt32(433646405) peer.serialize(buffer, true) buffer.appendInt32(481674261) buffer.appendInt32(Int32(id.count)) for item in id { serializeInt32(item, buffer: buffer, boxed: false) } - reason.serialize(buffer, true) + serializeBytes(option, buffer: buffer, boxed: false) serializeString(message, buffer: buffer, boxed: false) - return (FunctionDescription(name: "stories.report", parameters: [("peer", String(describing: peer)), ("id", String(describing: id)), ("reason", String(describing: reason)), ("message", String(describing: message))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + return (FunctionDescription(name: "stories.report", parameters: [("peer", String(describing: peer)), ("id", String(describing: id)), ("option", String(describing: option)), ("message", String(describing: message))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.ReportResult? in let reader = BufferReader(buffer) - var result: Api.Bool? + var result: Api.ReportResult? if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.Bool + result = Api.parse(reader, signature: signature) as? Api.ReportResult } return result }) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReportContent.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReportContent.swift new file mode 100644 index 0000000000..b79ed7c30c --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReportContent.swift @@ -0,0 +1,82 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramApi +import MtProtoKit + +public enum ReportContentResult { + public struct Option: Equatable { + public let text: String + public let option: Data + } + + case options(title: String, options: [Option]) + case addComment(optional: Bool, option: Data) + case reported +} + +public enum ReportContentError { + case generic + case messageIdRequired +} + +public enum ReportContentSubject: Equatable { + case peer(EnginePeer.Id) + case messages([EngineMessage.Id]) + case stories(EnginePeer.Id, [Int32]) + + var peerId: EnginePeer.Id { + switch self { + case let .peer(peerId): + return peerId + case let .messages(messageIds): + return messageIds.first!.peerId + case let .stories(peerId, _): + return peerId + } + } +} + +func _internal_reportContent(account: Account, subject: ReportContentSubject, option: Data?, message: String?) -> Signal { + return account.postbox.transaction { transaction -> Signal in + guard let peer = transaction.getPeer(subject.peerId), let inputPeer = apiInputPeer(peer) else { + return .fail(.generic) + } + + let request: Signal + if case let .stories(_, ids) = subject { + request = account.network.request(Api.functions.stories.report(peer: inputPeer, id: ids, option: Buffer(data: option), message: message ?? "")) + } else { + var ids: [Int32] = [] + if case let .messages(messageIds) = subject { + ids = messageIds.map { $0.id } + } + request = account.network.request(Api.functions.messages.report(peer: inputPeer, id: ids, option: Buffer(data: option), message: message ?? "")) + } + + return request + |> mapError { error -> ReportContentError in + if error.errorDescription == "MESSAGE_ID_REQUIRED" { + return .messageIdRequired + } + return .generic + } + |> map { result -> ReportContentResult in + switch result { + case let .reportResultChooseOption(title, options): + return .options(title: title, options: options.map { + switch $0 { + case let .messageReportOption(text, option): + return ReportContentResult.Option(text: text, option: option.makeData()) + } + }) + case let .reportResultAddComment(flags, option): + return .addComment(optional: (flags & (1 << 0)) != 0, option: option.makeData()) + case .reportResultReported: + return .reported + } + } + } + |> castError(ReportContentError.self) + |> switchToLatest +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 22de0f6824..5933679e31 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -1463,6 +1463,10 @@ public extension TelegramEngine { return _internal_reportAdMessage(account: self.account, peerId: peerId, opaqueId: opaqueId, option: option) } + public func reportContent(subject: ReportContentSubject, option: Data?, message: String?) -> Signal { + return _internal_reportContent(account: self.account, subject: subject, option: option, message: message) + } + public func updateExtendedMedia(messageIds: [EngineMessage.Id]) -> Signal { return _internal_updateExtendedMedia(account: self.account, messageIds: messageIds) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index f3b738d87a..6ae977572f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -468,7 +468,7 @@ private final class StarsContextImpl { } var transactions = state.transactions if addTransaction { - transactions.insert(.init(flags: [.isLocal], id: "\(arc4random())", count: balance, date: Int32(Date().timeIntervalSince1970), peer: .appStore, title: nil, description: nil, photo: nil, transactionDate: nil, transactionUrl: nil, paidMessageId: nil, giveawayMessageId: nil, media: [], subscriptionPeriod: nil), at: 0) + transactions.insert(.init(flags: [.isLocal], id: "\(arc4random())", count: balance, date: Int32(Date().timeIntervalSince1970), peer: .appStore, title: nil, description: nil, photo: nil, transactionDate: nil, transactionUrl: nil, paidMessageId: nil, giveawayMessageId: nil, media: [], subscriptionPeriod: nil, starGift: nil), at: 0) } self.updateState(StarsContext.State(flags: [.isPendingBalance], balance: max(0, state.balance + balance), subscriptions: state.subscriptions, canLoadMoreSubscriptions: state.canLoadMoreSubscriptions, transactions: transactions, canLoadMoreTransactions: state.canLoadMoreTransactions, isLoading: state.isLoading)) @@ -490,7 +490,7 @@ private final class StarsContextImpl { private extension StarsContext.State.Transaction { init?(apiTransaction: Api.StarsTransaction, peerId: EnginePeer.Id?, transaction: Transaction) { switch apiTransaction { - case let .starsTransaction(apiFlags, id, stars, date, transactionPeer, title, description, photo, transactionDate, transactionUrl, _, messageId, extendedMedia, subscriptionPeriod, giveawayPostId): + case let .starsTransaction(apiFlags, id, stars, date, transactionPeer, title, description, photo, transactionDate, transactionUrl, _, messageId, extendedMedia, subscriptionPeriod, giveawayPostId, starGift): let parsedPeer: StarsContext.State.Transaction.Peer var paidMessageId: MessageId? var giveawayMessageId: MessageId? @@ -544,7 +544,7 @@ private extension StarsContext.State.Transaction { let media = extendedMedia.flatMap({ $0.compactMap { textMediaAndExpirationTimerFromApiMedia($0, PeerId(0)).media } }) ?? [] let _ = subscriptionPeriod - self.init(flags: flags, id: id, count: stars, date: date, peer: parsedPeer, title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), transactionDate: transactionDate, transactionUrl: transactionUrl, paidMessageId: paidMessageId, giveawayMessageId: giveawayMessageId, media: media, subscriptionPeriod: subscriptionPeriod) + self.init(flags: flags, id: id, count: stars, date: date, peer: parsedPeer, title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), transactionDate: transactionDate, transactionUrl: transactionUrl, paidMessageId: paidMessageId, giveawayMessageId: giveawayMessageId, media: media, subscriptionPeriod: subscriptionPeriod, starGift: starGift.flatMap { StarGift(apiStarGift: $0) }) } } } @@ -613,6 +613,7 @@ public final class StarsContext { public let giveawayMessageId: MessageId? public let media: [Media] public let subscriptionPeriod: Int32? + public let starGift: StarGift? public init( flags: Flags, @@ -628,7 +629,8 @@ public final class StarsContext { paidMessageId: MessageId?, giveawayMessageId: MessageId?, media: [Media], - subscriptionPeriod: Int32? + subscriptionPeriod: Int32?, + starGift: StarGift? ) { self.flags = flags self.id = id @@ -644,6 +646,7 @@ public final class StarsContext { self.giveawayMessageId = giveawayMessageId self.media = media self.subscriptionPeriod = subscriptionPeriod + self.starGift = starGift } public static func == (lhs: Transaction, rhs: Transaction) -> Bool { @@ -689,6 +692,9 @@ public final class StarsContext { if lhs.subscriptionPeriod != rhs.subscriptionPeriod { return false } + if lhs.starGift != rhs.starGift { + return false + } return true } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ReportPeer.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ReportPeer.swift index 04ffbec511..5d84874fbd 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ReportPeer.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ReportPeer.swift @@ -148,42 +148,44 @@ func _internal_reportPeerPhoto(account: Account, peerId: PeerId, reason: ReportR } func _internal_reportPeerMessages(account: Account, messageIds: [MessageId], reason: ReportReason, message: String) -> Signal { - return account.postbox.transaction { transaction -> Signal in - let groupedIds = messagesIdsGroupedByPeerId(messageIds) - let signals = groupedIds.values.compactMap { ids -> Signal? in - guard let peerId = ids.first?.peerId, let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) else { - return nil - } - return account.network.request(Api.functions.messages.report(peer: inputPeer, id: ids.map { $0.id }, reason: reason.apiReason, message: message)) - |> `catch` { _ -> Signal in - return .single(.boolFalse) - } - |> mapToSignal { _ -> Signal in - return .complete() - } - } - - return combineLatest(signals) - |> mapToSignal { _ -> Signal in - return .complete() - } - } |> switchToLatest + return .complete() +// return account.postbox.transaction { transaction -> Signal in +// let groupedIds = messagesIdsGroupedByPeerId(messageIds) +// let signals = groupedIds.values.compactMap { ids -> Signal? in +// guard let peerId = ids.first?.peerId, let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) else { +// return nil +// } +// return account.network.request(Api.functions.messages.report(peer: inputPeer, id: ids.map { $0.id }, reason: reason.apiReason, message: message)) +// |> `catch` { _ -> Signal in +// return .single(.boolFalse) +// } +// |> mapToSignal { _ -> Signal in +// return .complete() +// } +// } +// +// return combineLatest(signals) +// |> mapToSignal { _ -> Signal in +// return .complete() +// } +// } |> switchToLatest } func _internal_reportPeerStory(account: Account, peerId: PeerId, storyId: Int32, reason: ReportReason, message: String) -> Signal { - return account.postbox.transaction { transaction -> Signal in - if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { - return account.network.request(Api.functions.stories.report(peer: inputPeer, id: [storyId], reason: reason.apiReason, message: message)) - |> `catch` { _ -> Signal in - return .single(.boolFalse) - } - |> mapToSignal { _ -> Signal in - return .complete() - } - } else { - return .complete() - } - } |> switchToLatest + return .complete() +// return account.postbox.transaction { transaction -> Signal in +// if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { +// return account.network.request(Api.functions.stories.report(peer: inputPeer, id: [storyId], reason: reason.apiReason, message: message)) +// |> `catch` { _ -> Signal in +// return .single(.boolFalse) +// } +// |> mapToSignal { _ -> Signal in +// return .complete() +// } +// } else { +// return .complete() +// } +// } |> switchToLatest } func _internal_reportPeerReaction(account: Account, authorId: PeerId, messageId: MessageId) -> Signal { diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index f4d9204e2a..ff1d488ded 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -461,6 +461,7 @@ swift_library( "//submodules/TelegramUI/Components/MiniAppListScreen", "//submodules/TelegramUI/Components/Stars/StarsIntroScreen", "//submodules/TelegramUI/Components/Gifts/GiftOptionsScreen", + "//submodules/TelegramUI/Components/ContentReportScreen", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift b/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift index 86ee3fe125..b3574cd921 100644 --- a/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift +++ b/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift @@ -396,6 +396,7 @@ private final class SheetContent: CombinedComponent { let navigation = navigation.update( component: NavigationStackComponent( items: items, + clipContent: false, requestPop: { [weak state] in state?.pushedOptions.removeLast() update(.spring(duration: 0.45)) diff --git a/submodules/TelegramUI/Components/ContentReportScreen/BUILD b/submodules/TelegramUI/Components/ContentReportScreen/BUILD new file mode 100644 index 0000000000..24d0da15cc --- /dev/null +++ b/submodules/TelegramUI/Components/ContentReportScreen/BUILD @@ -0,0 +1,41 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ContentReportScreen", + module_name = "ContentReportScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/ComponentFlow", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BalancedTextComponent", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/AppBundle", + "//submodules/ItemListUI", + "//submodules/TelegramStringFormatting", + "//submodules/PresentationDataUtils", + "//submodules/Components/SheetComponent", + "//submodules/UndoUI", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/TelegramUI/Components/NavigationStackComponent", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/ContentReportScreen/Sources/ContentReportScreen.swift b/submodules/TelegramUI/Components/ContentReportScreen/Sources/ContentReportScreen.swift new file mode 100644 index 0000000000..a248532726 --- /dev/null +++ b/submodules/TelegramUI/Components/ContentReportScreen/Sources/ContentReportScreen.swift @@ -0,0 +1,726 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import TelegramCore +import Markdown +import TextFormat +import TelegramPresentationData +import ViewControllerComponent +import SheetComponent +import BalancedTextComponent +import MultilineTextComponent +import ListSectionComponent +import ListActionItemComponent +import NavigationStackComponent +import ItemListUI +import UndoUI +import AccountContext +import LottieComponent +import TextFieldComponent +import ListMultilineTextFieldItemComponent +import ButtonComponent + +private enum ReportResult { + case reported +} + +private final class SheetPageContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + enum Content: Equatable { + struct Item: Equatable { + let title: String + let option: Data + } + + case options(items: [Item]) + case comment(isOptional: Bool, option: Data) + } + + let context: AccountContext + let isFirst: Bool + let title: String? + let subtitle: String + let content: Content + let action: (Content.Item, String?) -> Void + let pop: () -> Void + + init( + context: AccountContext, + isFirst: Bool, + title: String?, + subtitle: String, + content: Content, + action: @escaping (Content.Item, String?) -> Void, + pop: @escaping () -> Void + ) { + self.context = context + self.isFirst = isFirst + self.title = title + self.subtitle = subtitle + self.content = content + self.action = action + self.pop = pop + } + + static func ==(lhs: SheetPageContent, rhs: SheetPageContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.subtitle != rhs.subtitle { + return false + } + if lhs.content != rhs.content { + return false + } + return true + } + + final class State: ComponentState { + var backArrowImage: (UIImage, PresentationTheme)? + + let playOnce = ActionSlot() + private var didPlayAnimation = false + + let textInputState = ListMultilineTextFieldItemComponent.ExternalState() + + func playAnimationIfNeeded() { + guard !self.didPlayAnimation else { + return + } + self.didPlayAnimation = true + self.playOnce.invoke(Void()) + } + } + + func makeState() -> State { + return State() + } + + static var body: Body { + let background = Child(RoundedRectangle.self) + let back = Child(Button.self) + let title = Child(Text.self) + let animation = Child(LottieComponent.self) + let section = Child(ListSectionComponent.self) + let button = Child(ButtonComponent.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + let component = context.component + let state = context.state + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let theme = environment.theme + let strings = environment.strings + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + + var contentSize = CGSize(width: context.availableSize.width, height: 18.0) + + let background = background.update( + component: RoundedRectangle(color: theme.list.modalBlocksBackgroundColor, cornerRadius: 8.0), + availableSize: CGSize(width: context.availableSize.width, height: 1000.0), + transition: .immediate + ) + context.add(background + .position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0)) + ) + + let backArrowImage: UIImage + if let (cached, cachedTheme) = state.backArrowImage, cachedTheme === theme { + backArrowImage = cached + } else { + backArrowImage = NavigationBarTheme.generateBackArrowImage(color: theme.list.itemAccentColor)! + state.backArrowImage = (backArrowImage, theme) + } + + let backContents: AnyComponent + if component.isFirst { + backContents = AnyComponent(Text(text: strings.Common_Cancel, font: Font.regular(17.0), color: theme.list.itemAccentColor)) + } else { + backContents = AnyComponent( + HStack([ + AnyComponentWithIdentity(id: "arrow", component: AnyComponent(Image(image: backArrowImage, contentMode: .center))), + AnyComponentWithIdentity(id: "label", component: AnyComponent(Text(text: strings.Common_Back, font: Font.regular(17.0), color: theme.list.itemAccentColor))) + ], spacing: 6.0) + ) + } + let back = back.update( + component: Button( + content: backContents, + action: { + component.pop() + } + ), + availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), + transition: .immediate + ) + context.add(back + .position(CGPoint(x: sideInset + back.size.width / 2.0 - (component.title != nil ? 8.0 : 0.0), y: contentSize.height + back.size.height / 2.0)) + ) + + let constrainedTitleWidth = context.availableSize.width - (back.size.width + 16.0) * 2.0 + + let titleString: String + if let title = component.title { + titleString = title + } else { + titleString = "" + } + + let title = title.update( + component: Text(text: titleString, font: Font.semibold(17.0), color: theme.list.itemPrimaryTextColor), + availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height), + transition: .immediate + ) + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0)) + ) + contentSize.height += title.size.height + contentSize.height += 24.0 + + var items: [AnyComponentWithIdentity] = [] + var footer: AnyComponent? + + switch component.content { + case let .options(options): + for item in options { + items.append(AnyComponentWithIdentity(id: item.title, component: AnyComponent(ListActionItemComponent( + theme: theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: item.title, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + accessory: .arrow, + action: { _ in + component.action(item, nil) + } + )))) + } + case let .comment(isOptional, _): + contentSize.height -= 11.0 + + let animationHeight: CGFloat = 120.0 + let animation = animation.update( + component: LottieComponent( + content: LottieComponent.AppBundleContent(name: "Cop"), + startingPosition: .begin, + playOnce: state.playOnce + ), + environment: {}, + availableSize: CGSize(width: animationHeight, height: animationHeight), + transition: .immediate + ) + context.add(animation + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + animation.size.height / 2.0)) + ) + contentSize.height += animation.size.height + contentSize.height += 18.0 + + items.append( + AnyComponentWithIdentity(id: items.count, component: AnyComponent(ListMultilineTextFieldItemComponent( + externalState: state.textInputState, + context: component.context, + theme: theme, + strings: strings, + initialText: "", + resetText: nil, + placeholder: isOptional ? "Add Comment (Optional)" : "Add Comment", + autocapitalizationType: .none, + autocorrectionType: .no, + returnKeyType: .done, + characterLimit: 140, + displayCharacterLimit: true, + emptyLineHandling: .notAllowed, + updated: { [weak state] _ in + state?.updated() + }, + returnKeyAction: { +// guard let self else { +// return +// } +// if let titleView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View { +// titleView.endEditing(true) +// } + }, + textUpdateTransition: .spring(duration: 0.4), + tag: nil + ))) + ) + + footer = AnyComponent(MultilineTextComponent( + text: .plain( + NSAttributedString(string: "Please help us by telling what is wrong with the message you have selected.", font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: theme.list.freeTextColor) + ), + maximumNumberOfLines: 0 + )) + } + + let section = section.update( + component: ListSectionComponent( + theme: theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.subtitle.uppercased(), + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: footer, + items: items + ), + environment: {}, + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), + transition: context.transition + ) + context.add(section + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + section.size.height / 2.0)) + ) + contentSize.height += section.size.height + contentSize.height += 54.0 + + if case let .comment(isOptional, option) = component.content { + contentSize.height -= 16.0 + + let action = component.action + let button = button.update( + component: ButtonComponent( + background: ButtonComponent.Background( + color: theme.list.itemCheckColors.fillColor, + foreground: theme.list.itemCheckColors.foregroundColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) + ), + content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(Text(text: "Send Report", font: Font.semibold(17.0), color: theme.list.itemCheckColors.foregroundColor))), + isEnabled: isOptional || state.textInputState.hasText, + allowActionWhenDisabled: false, + displaysProgress: false, + action: { + action(SheetPageContent.Content.Item(title: "", option: option), state.textInputState.text.string) + } + ), + environment: {}, + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + transition: context.transition + ) + context.add(button + .clipsToBounds(true) + .cornerRadius(10.0) + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0)) + ) + contentSize.height += button.size.height + contentSize.height += 16.0 + + if environment.inputHeight.isZero && environment.safeInsets.bottom > 0.0 { + contentSize.height += environment.safeInsets.bottom + } + } + + contentSize.height += environment.inputHeight + + state.playAnimationIfNeeded() + + return contentSize + } + } +} + +private final class SheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let subject: ReportContentSubject + let title: String + let options: [ReportContentResult.Option] + let pts: Int + let openMore: () -> Void + let complete: (ReportResult) -> Void + let dismiss: () -> Void + let update: (ComponentTransition) -> Void + + init( + context: AccountContext, + subject: ReportContentSubject, + title: String, + options: [ReportContentResult.Option], + pts: Int, + openMore: @escaping () -> Void, + complete: @escaping (ReportResult) -> Void, + dismiss: @escaping () -> Void, + update: @escaping (ComponentTransition) -> Void + ) { + self.context = context + self.subject = subject + self.title = title + self.options = options + self.pts = pts + self.openMore = openMore + self.complete = complete + self.dismiss = dismiss + self.update = update + } + + static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.subject != rhs.subject { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.options != rhs.options { + return false + } + if lhs.pts != rhs.pts { + return false + } + return true + } + + final class State: ComponentState { + var pushedOptions: [(title: String, subtitle: String, content: SheetPageContent.Content)] = [] + let disposable = MetaDisposable() + + deinit { + self.disposable.dispose() + } + } + + func makeState() -> State { + return State() + } + + static var body: Body { + let navigation = Child(NavigationStackComponent.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + let component = context.component + let state = context.state + let update = component.update + + let accountContext = component.context + let subject = component.subject + let complete = component.complete + let action: (SheetPageContent.Content.Item, String?) -> Void = { [weak state] item, message in + guard let state else { + return + } + state.disposable.set( + (accountContext.engine.messages.reportContent(subject: subject, option: item.option, message: message) + |> deliverOnMainQueue).start(next: { [weak state] result in + switch result { + case let .options(title, options): + state?.pushedOptions.append((item.title, title, .options(items: options.map { SheetPageContent.Content.Item(title: $0.text, option: $0.option) }))) + state?.updated(transition: .spring(duration: 0.45)) + case let .addComment(isOptional, option): + state?.pushedOptions.append((item.title, "", .comment(isOptional: isOptional, option: option))) + state?.updated(transition: .spring(duration: 0.45)) + case .reported: + complete(.reported) + } + }, error: { error in +// if case .premiumRequired = error { +// complete(.premiumRequired) +// } + }) + ) + } + + let mainTitle: String + switch component.subject { + case .peer: + mainTitle = "Report Peer" + case .messages: + mainTitle = "Report Message" + case .stories: + mainTitle = "Report Story" + } + + var items: [AnyComponentWithIdentity] = [] + items.append(AnyComponentWithIdentity(id: items.count, component: AnyComponent( + SheetPageContent( + context: component.context, + isFirst: true, + title: mainTitle, + subtitle: component.title, + content: .options(items: component.options.map { + SheetPageContent.Content.Item(title: $0.text, option: $0.option) + }), + action: { item, message in + action(item, message) + }, + pop: { + component.dismiss() + } + ) + ))) + for pushedOption in state.pushedOptions { + items.append(AnyComponentWithIdentity(id: items.count, component: AnyComponent( + SheetPageContent( + context: component.context, + isFirst: false, + title: pushedOption.title, + subtitle: pushedOption.subtitle, + content: pushedOption.content, + action: { item, message in + action(item, message) + }, + pop: { [weak state] in + state?.pushedOptions.removeLast() + update(.spring(duration: 0.45)) + } + ) + ))) + } + + var contentSize = CGSize(width: context.availableSize.width, height: 0.0) + let navigation = navigation.update( + component: NavigationStackComponent( + items: items, + clipContent: false, + requestPop: { [weak state] in + state?.pushedOptions.removeLast() + update(.spring(duration: 0.45)) + } + ), + environment: { environment }, + availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), + transition: context.transition + ) + context.add(navigation + .position(CGPoint(x: context.availableSize.width / 2.0, y: navigation.size.height / 2.0)) + .clipsToBounds(true) + .cornerRadius(8.0) + ) + contentSize.height += navigation.size.height + + return contentSize + } + } +} + +private final class SheetContainerComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let subject: ReportContentSubject + let title: String + let options: [ReportContentResult.Option] + let openMore: () -> Void + let complete: (ReportResult) -> Void + + init( + context: AccountContext, + subject: ReportContentSubject, + title: String, + options: [ReportContentResult.Option], + openMore: @escaping () -> Void, + complete: @escaping (ReportResult) -> Void + ) { + self.context = context + self.subject = subject + self.title = title + self.options = options + self.openMore = openMore + self.complete = complete + } + + static func ==(lhs: SheetContainerComponent, rhs: SheetContainerComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.subject != rhs.subject { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.options != rhs.options { + return false + } + return true + } + + final class State: ComponentState { + var pts: Int = 0 + } + + func makeState() -> State { + return State() + } + + static var body: Body { + let sheet = Child(SheetComponent.self) + let animateOut = StoredActionSlot(Action.self) + + let sheetExternalState = SheetComponent.ExternalState() + + return { context in + let environment = context.environment[EnvironmentType.self] + let state = context.state + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(SheetContent( + context: context.component.context, + subject: context.component.subject, + title: context.component.title, + options: context.component.options, + pts: state.pts, + openMore: context.component.openMore, + complete: context.component.complete, + dismiss: { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + }, + update: { [weak state] transition in + state?.pts += 1 + state?.updated(transition: transition) + } + )), + backgroundColor: .color(environment.theme.list.modalBlocksBackgroundColor), + followContentSizeChanges: true, + externalState: sheetExternalState, + animateOut: animateOut + ), + environment: { + environment + SheetComponentEnvironment( + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + if animated { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else { + if let controller = controller() { + controller.dismiss(completion: nil) + } + } + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + if let controller = controller(), !controller.automaticallyControlPresentationContextLayout { + let layout = ContainerViewLayout( + size: context.availableSize, + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: max(environment.safeInsets.bottom, sheetExternalState.contentHeight), right: 0.0), + safeInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right), + additionalInsets: .zero, + statusBarHeight: environment.statusBarHeight, + inputHeight: nil, + inputHeightIsInteractivellyChanging: false, + inVoiceOver: false + ) + controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition) + } + + return context.availableSize + } + } +} + + +public final class ContentReportScreen: ViewControllerComponentContainer { + private let context: AccountContext + + public init( + context: AccountContext, + subject: ReportContentSubject, + title: String, + options: [ReportContentResult.Option], + forceDark: Bool = false, + completed: @escaping () -> Void + ) { + self.context = context + + var completeImpl: ((ReportResult) -> Void)? + super.init( + context: context, + component: SheetContainerComponent( + context: context, + subject: subject, + title: title, + options: options, + openMore: {}, + complete: { hidden in + completeImpl?(hidden) + } + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: forceDark ? .dark : .default + ) + + self.navigationPresentation = .flatModal + + completeImpl = { [weak self] result in + guard let self else { + return + } + let navigationController = self.navigationController + self.dismissAnimated() + + switch result { + case .reported: + Queue.mainQueue().after(0.1) { + completed() + } + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + Queue.mainQueue().after(0.4, { + (navigationController?.viewControllers.last as? ViewController)?.present(UndoOverlayController(presentationData: presentationData, content: .emoji(name: "PoliceCar", text: presentationData.strings.Report_Succeed), elevatedLayout: false, action: { _ in return true }), in: .current) + }) + } + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func viewDidLoad() { + super.viewDidLoad() + + self.view.disablesInteractiveModalDismiss = true + } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() + } + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/BUILD b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/BUILD new file mode 100644 index 0000000000..2fec2704be --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/BUILD @@ -0,0 +1,31 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "GiftAnimationComponent", + module_name = "GiftAnimationComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/ComponentFlow", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/AppBundle", + "//submodules/TelegramStringFormatting", + "//submodules/PresentationDataUtils", + "//submodules/TextFormat", + "//submodules/TelegramUI/Components/EmojiTextAttachmentView", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftAnimationComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftAnimationComponent.swift new file mode 100644 index 0000000000..c5d161e3de --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftAnimationComponent.swift @@ -0,0 +1,98 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramCore +import TelegramPresentationData +import AppBundle +import AccountContext +import EmojiTextAttachmentView +import TextFormat + +public final class GiftAnimationComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let file: TelegramMediaFile? + + public init( + context: AccountContext, + theme: PresentationTheme, + file: TelegramMediaFile? + ) { + self.context = context + self.theme = theme + self.file = file + } + + public static func ==(lhs: GiftAnimationComponent, rhs: GiftAnimationComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.file != rhs.file { + return false + } + return true + } + + public final class View: UIView { + private var component: GiftAnimationComponent? + private weak var componentState: EmptyComponentState? + + private var animationLayer: InlineStickerItemLayer? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: GiftAnimationComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.componentState = state + + let emoji = ChatTextInputTextCustomEmojiAttribute( + interactivelySelectedFromPackId: nil, + fileId: component.file?.fileId.id ?? 0, + file: component.file + ) + + let iconSize = availableSize + if self.animationLayer == nil { + let animationLayer = InlineStickerItemLayer( + context: .account(component.context), + userLocation: .other, + attemptSynchronousLoad: false, + emoji: emoji, + file: component.file, + cache: component.context.animationCache, + renderer: component.context.animationRenderer, + unique: true, + placeholderColor: component.theme.list.mediaPlaceholderColor, + pointSize: CGSize(width: iconSize.width * 1.2, height: iconSize.height * 1.2), + loopCount: 1 + ) + animationLayer.isVisibleForAnimations = true + self.animationLayer = animationLayer + self.layer.addSublayer(animationLayer) + } + if let animationLayer = self.animationLayer { + transition.setFrame(layer: animationLayer, frame: CGRect(origin: .zero, size: iconSize)) + } + + return iconSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD index 6bb84c21b4..a0742ca169 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD @@ -37,6 +37,7 @@ swift_library( "//submodules/Components/SolidRoundedButtonComponent", "//submodules/TelegramUI/Components/Stars/StarsAvatarComponent", "//submodules/TelegramUI/Components/EmojiTextAttachmentView", + "//submodules/TelegramUI/Components/Gifts/GiftAnimationComponent", "//submodules/UndoUI", ], visibility = [ diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index b569c9bb4e..d81772d75b 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -23,6 +23,7 @@ import TelegramStringFormatting import StarsAvatarComponent import EmojiTextAttachmentView import UndoUI +import GiftAnimationComponent private final class GiftViewSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -695,14 +696,14 @@ public class GiftViewScreen: ViewControllerComponentContainer { case message(EngineMessage) case profileGift(EnginePeer.Id, ProfileGiftsContext.State.StarGift) - var arguments: (peerId: EnginePeer.Id, messageId: EngineMessage.Id?, incoming: Bool, gift: StarGift, convertStars: Int64, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool)? { + var arguments: (peerId: EnginePeer.Id, fromPeerName: String?, messageId: EngineMessage.Id?, incoming: Bool, gift: StarGift, convertStars: Int64, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool)? { switch self { case let .message(message): if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted) = action.action { - return (message.id.peerId, message.id, message.flags.contains(.Incoming), gift, convertStars, text, entities, nameHidden, savedToProfile, converted) + return (message.id.peerId, message.author?.compactDisplayTitle, message.id, message.flags.contains(.Incoming), gift, convertStars, text, entities, nameHidden, savedToProfile, converted) } case let .profileGift(peerId, gift): - return (peerId, gift.messageId, false, gift.gift, gift.convertStars ?? 0, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, false) + return (peerId, gift.fromPeer?.compactDisplayTitle, gift.messageId, false, gift.gift, gift.convertStars ?? 0, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, false) } return nil } @@ -789,9 +790,14 @@ public class GiftViewScreen: ViewControllerComponentContainer { if let lastController = navigationController.viewControllers.last as? ViewController { let resultController = UndoOverlayController( presentationData: presentationData, - content: .sticker(context: context, file: arguments.gift.file, loop: false, title: added ? "Gift Saved to Profile" : "Gift Removed from Profile", text: added ? "The gift is now displayed in your profile." : "The gift is no longer displayed in your profile.", undoText: nil, customAction: nil), + content: .sticker(context: context, file: arguments.gift.file, loop: false, title: added ? "Gift Saved to Profile" : "Gift Removed from Profile", text: added ? "The gift is now displayed in [your profile]()." : "The gift is no longer displayed in [your profile]().", undoText: nil, customAction: nil), elevatedLayout: lastController is ChatController, - action: { _ in return true} + action: { action in + if case .info = action { + + } + return true + } ) lastController.present(resultController, in: .window(.root)) } @@ -800,13 +806,13 @@ public class GiftViewScreen: ViewControllerComponentContainer { } convertToStarsImpl = { [weak self] in - guard let self, case let .message(message) = subject, let arguments = subject.arguments, let messageId = arguments.messageId, let navigationController = self.navigationController as? NavigationController else { + guard let self, let arguments = subject.arguments, let messageId = arguments.messageId, let fromPeerName = arguments.fromPeerName, let navigationController = self.navigationController as? NavigationController else { return } let controller = textAlertController( context: self.context, title: "Convert Gift to Stars", - text: "Do you want to convert this gift from **\(message.author?.compactDisplayTitle ?? "")** to **\(arguments.convertStars) Stars**?\n\nThis action cannot be undone.", + text: "Do you want to convert this gift from **\(fromPeerName)** to **\(arguments.convertStars) Stars**?\n\nThis action cannot be undone.", actions: [ TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: "Convert", action: { [weak self, weak navigationController] in @@ -1253,91 +1259,3 @@ private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: context.strokePath() }) } - -private final class GiftAnimationComponent: Component { - let context: AccountContext - let theme: PresentationTheme - let file: TelegramMediaFile? - - public init( - context: AccountContext, - theme: PresentationTheme, - file: TelegramMediaFile? - ) { - self.context = context - self.theme = theme - self.file = file - } - - public static func ==(lhs: GiftAnimationComponent, rhs: GiftAnimationComponent) -> Bool { - if lhs.context !== rhs.context { - return false - } - if lhs.theme !== rhs.theme { - return false - } - if lhs.file != rhs.file { - return false - } - return true - } - - public final class View: UIView { - private var component: GiftAnimationComponent? - private weak var componentState: EmptyComponentState? - - private var animationLayer: InlineStickerItemLayer? - - override init(frame: CGRect) { - super.init(frame: frame) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func update(component: GiftAnimationComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - self.component = component - self.componentState = state - - let emoji = ChatTextInputTextCustomEmojiAttribute( - interactivelySelectedFromPackId: nil, - fileId: component.file?.fileId.id ?? 0, - file: component.file - ) - - let iconSize = availableSize - if self.animationLayer == nil { - let animationLayer = InlineStickerItemLayer( - context: .account(component.context), - userLocation: .other, - attemptSynchronousLoad: false, - emoji: emoji, - file: component.file, - cache: component.context.animationCache, - renderer: component.context.animationRenderer, - unique: true, - placeholderColor: component.theme.list.mediaPlaceholderColor, - pointSize: CGSize(width: iconSize.width * 1.2, height: iconSize.height * 1.2), - loopCount: 1 - ) - animationLayer.isVisibleForAnimations = true - self.animationLayer = animationLayer - self.layer.addSublayer(animationLayer) - } - if let animationLayer = self.animationLayer { - transition.setFrame(layer: animationLayer, frame: CGRect(origin: .zero, size: iconSize)) - } - - return iconSize - } - } - - public func makeView() -> View { - return View(frame: CGRect()) - } - - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } -} diff --git a/submodules/TelegramUI/Components/NavigationStackComponent/Sources/NavigationStackComponent.swift b/submodules/TelegramUI/Components/NavigationStackComponent/Sources/NavigationStackComponent.swift index e7a795459f..3a5aabd841 100644 --- a/submodules/TelegramUI/Components/NavigationStackComponent/Sources/NavigationStackComponent.swift +++ b/submodules/TelegramUI/Components/NavigationStackComponent/Sources/NavigationStackComponent.swift @@ -85,13 +85,16 @@ public final class NavigationStackComponent: Compon } public let items: [AnyComponentWithIdentity] + public let clipContent: Bool public let requestPop: () -> Void public init( items: [AnyComponentWithIdentity], + clipContent: Bool = true, requestPop: @escaping () -> Void ) { self.items = items + self.clipContent = clipContent self.requestPop = requestPop } @@ -99,6 +102,9 @@ public final class NavigationStackComponent: Compon if lhs.items != rhs.items { return false } + if lhs.clipContent != rhs.clipContent { + return false + } return true } @@ -198,7 +204,7 @@ public final class NavigationStackComponent: Compon } else { itemTransition = itemTransition.withAnimation(.none) itemView = ItemView() - itemView.clipsToBounds = true + itemView.clipsToBounds = component.clipContent self.itemViews[itemId] = itemView itemView.contents.parentState = state } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index f486e49542..e9a5f9f5c9 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -999,7 +999,8 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 103, label: .text(""), additionalBadgeLabel: presentationData.strings.Settings_New, text: presentationData.strings.Settings_Business, icon: PresentationResourcesSettings.business, action: { interaction.openSettings(.businessSetup) })) - items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 104, label: .text(""), text: presentationData.strings.Settings_PremiumGift, icon: PresentationResourcesSettings.premiumGift, action: { + //TODO:localize + items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 104, label: .text(""), text: "Send a Gift", icon: PresentationResourcesSettings.premiumGift, action: { interaction.openSettings(.premiumGift) })) } @@ -6098,8 +6099,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }))) } - if strongSelf.peerId.namespace == Namespaces.Peer.CloudUser, !user.isDeleted && user.botInfo == nil && !user.flags.contains(.isSupport), let cachedData = data.cachedData as? CachedUserData, !cachedData.premiumGiftOptions.isEmpty { - items.append(.action(ContextMenuActionItem(text: presentationData.strings.PeerInfo_GiftPremium, icon: { theme in + if strongSelf.peerId.namespace == Namespaces.Peer.CloudUser, !user.isDeleted && user.botInfo == nil && !user.flags.contains(.isSupport) { + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Send a Gift", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Gift"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.dismissWithoutContent) @@ -11642,11 +11644,14 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return } strongSelf.view.endEditing(true) + strongSelf.controller?.present(peerReportOptionsController(context: strongSelf.context, subject: .messages(Array(messageIds).sorted()), passthrough: false, present: { c, a in self?.controller?.present(c, in: .window(.root), with: a) }, push: { c in self?.controller?.push(c) }, completion: { _, _ in }), in: .window(.root)) + + }, displayCopyProtectionTip: { [weak self] node, save in if let strongSelf = self, let peer = strongSelf.data?.peer, let messageIds = strongSelf.state.selectedMessageIds, !messageIds.isEmpty { let _ = (strongSelf.context.engine.data.get(EngineDataMap( diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD index b814269b7d..ff340fd489 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD @@ -36,6 +36,7 @@ swift_library( "//submodules/GalleryUI", "//submodules/TelegramUI/Components/MiniAppListScreen", "//submodules/TelegramUI/Components/Premium/PremiumStarComponent", + "//submodules/TelegramUI/Components/Gifts/GiftAnimationComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index 4e8ba3c566..af129188fe 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -25,6 +25,7 @@ import GalleryUI import StarsAvatarComponent import MiniAppListScreen import PremiumStarComponent +import GiftAnimationComponent private final class StarsTransactionSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -145,6 +146,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { let title = Child(MultilineTextComponent.self) let star = Child(StarsImageComponent.self) let activeStar = Child(PremiumStarComponent.self) + let gift = Child(GiftAnimationComponent.self) let amountBackground = Child(RoundedRectangle.self) let amount = Child(BalancedTextComponent.self) let amountStar = Child(BundleIconComponent.self) @@ -225,6 +227,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { var isReaction = false var giveawayMessageId: MessageId? var isBoost = false + var giftAnimation: TelegramMediaFile? var delayedCloseOnOpenPeer = true switch subject { @@ -322,7 +325,18 @@ private final class StarsTransactionSheetContent: CombinedComponent { } } case let .transaction(transaction, parentPeer): - if let giveawayMessageIdValue = transaction.giveawayMessageId { + if let starGift = transaction.starGift { + titleText = "Gift" + descriptionText = "" + count = transaction.count + transactionId = transaction.id + date = transaction.date + if case let .peer(peer) = transaction.peer { + toPeer = peer + } + transactionPeer = transaction.peer + giftAnimation = starGift.file + } else if let giveawayMessageIdValue = transaction.giveawayMessageId { titleText = strings.Stars_Transaction_Giveaway_Title descriptionText = "" count = transaction.count @@ -572,7 +586,17 @@ private final class StarsTransactionSheetContent: CombinedComponent { imageIcon = nil } var starChild: _UpdatedChildComponent - if isBoost { + if let giftAnimation { + starChild = gift.update( + component: GiftAnimationComponent( + context: component.context, + theme: theme, + file: giftAnimation + ), + availableSize: CGSize(width: 128.0, height: 128.0), + transition: .immediate + ) + } else if isBoost { starChild = activeStar.update( component: PremiumStarComponent( theme: theme, @@ -877,7 +901,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { ) context.add(starChild - .position(CGPoint(x: context.availableSize.width / 2.0, y: starChild.size.height / 2.0 - 19.0)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: 200.0 / 2.0 - 19.0)) ) context.add(title @@ -885,7 +909,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { ) var originY: CGFloat = 0.0 - originY += starChild.size.height - 23.0 + originY += 200.0 - 23.0 var descriptionSize: CGSize = .zero if !descriptionText.isEmpty { diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift index 5431bef8d3..3200636923 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift @@ -209,7 +209,10 @@ final class StarsTransactionsListPanelComponent: Component { var itemPeer = item.peer switch item.peer { case let .peer(peer): - if let _ = item.giveawayMessageId { + if let _ = item.starGift { + itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) + itemSubtitle = item.count > 0 ? environment.strings.Stars_Intro_Transaction_ConvertedGift : environment.strings.Stars_Intro_Transaction_Gift + } else if let _ = item.giveawayMessageId { itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) itemSubtitle = environment.strings.Stars_Intro_Transaction_GiveawayPrize } else if !item.media.isEmpty { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index b194377d78..80b6b69fa2 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -6961,48 +6961,69 @@ public final class StoryItemSetContainerComponent: Component { if !component.slice.effectivePeer.isService { items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_Report, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Report"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] c, a in + }, action: { [weak self] _, f in guard let self, let component = self.component, let controller = component.controller() else { return } - let options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .illegalDrugs, .personalDetails, .other] - presentPeerReportOptions( + f(.default) + + self.isReporting = true + self.updateIsProgressPaused() + + component.context.sharedContext.makeContentReportScreen( context: component.context, - parent: controller, - contextController: c, - backAction: { _ in }, - subject: .story(component.slice.effectivePeer.id, component.slice.item.storyItem.id), - options: options, - passthrough: true, - forceTheme: defaultDarkPresentationTheme, - isDetailedReportingVisible: { [weak self] isReporting in + subject: .stories(component.slice.effectivePeer.id, [component.slice.item.storyItem.id]), + forceDark: true, + present: { c in + controller.push(c) + }, + completion: { [weak self] in guard let self else { return } - self.isReporting = isReporting + self.isReporting = false self.updateIsProgressPaused() - }, - completion: { [weak self] reason, _ in - guard let self, let component = self.component, let controller = component.controller(), let reason else { - return - } - let _ = component.context.engine.peers.reportPeerStory(peerId: component.slice.effectivePeer.id, storyId: component.slice.item.storyItem.id, reason: reason, message: "").startStandalone() - controller.present( - UndoOverlayController( - presentationData: presentationData, - content: .emoji( - name: "PoliceCar", - text: presentationData.strings.Report_Succeed - ), - elevatedLayout: false, - blurred: true, - action: { _ in return false } - ) - , in: .current - ) } ) + +// let options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .illegalDrugs, .personalDetails, .other] +// presentPeerReportOptions( +// context: component.context, +// parent: controller, +// contextController: c, +// backAction: { _ in }, +// subject: .story(component.slice.effectivePeer.id, component.slice.item.storyItem.id), +// options: options, +// passthrough: true, +// forceTheme: defaultDarkPresentationTheme, +// isDetailedReportingVisible: { [weak self] isReporting in +// guard let self else { +// return +// } +// self.isReporting = isReporting +// self.updateIsProgressPaused() +// }, +// completion: { [weak self] reason, _ in +// guard let self, let component = self.component, let controller = component.controller(), let reason else { +// return +// } +// let _ = component.context.engine.peers.reportPeerStory(peerId: component.slice.effectivePeer.id, storyId: component.slice.item.storyItem.id, reason: reason, message: "").startStandalone() +// controller.present( +// UndoOverlayController( +// presentationData: presentationData, +// content: .emoji( +// name: "PoliceCar", +// text: presentationData.strings.Report_Succeed +// ), +// elevatedLayout: false, +// blurred: true, +// action: { _ in return false } +// ) +// , in: .current +// ) +// } +// ) }))) } } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index d3bd5649e0..4627a1b812 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -1957,22 +1957,30 @@ extension ChatControllerImpl { ]) strongSelf.present(controller, in: .window(.root)) } else { - strongSelf.present(peerReportOptionsController(context: strongSelf.context, subject: .messages(Array(messageIds).sorted()), passthrough: false, present: { c, a in - self?.present(c, in: .window(.root), with: a) - }, push: { c in - self?.push(c) - }, completion: { _, done in - if done { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - } - }), in: .window(.root)) + strongSelf.context.sharedContext.makeContentReportScreen(context: strongSelf.context, subject: .messages(Array(messageIds).sorted()), forceDark: false, present: { [weak self] controller in + self?.push(controller) + }, completion: { [weak self] in + self?.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) + }) } } }, reportMessages: { [weak self] messages, contextController in - if let strongSelf = self, !messages.isEmpty { - let options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .illegalDrugs, .personalDetails, .other] - presentPeerReportOptions(context: strongSelf.context, parent: strongSelf, contextController: contextController, subject: .messages(messages.map({ $0.id }).sorted()), options: options, completion: { _, _ in }) + guard let self, !messages.isEmpty else { + return } + contextController?.dismiss() + self.context.sharedContext.makeContentReportScreen( + context: self.context, + subject: .messages(messages.map({ $0.id }).sorted()), + forceDark: false, + present: { [weak self] controller in + guard let self else { + return + } + self.push(controller) + }, + completion: {} + ) }, blockMessageAuthor: { [weak self] message, contextController in contextController?.dismiss(completion: { guard let strongSelf = self else { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index aefebec13e..5d212212f5 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -73,6 +73,7 @@ import MiniAppListScreen import GiftOptionsScreen import GiftViewScreen import StarsIntroScreen +import ContentReportScreen private final class AccountUserInterfaceInUseContext { let subscribers = Bag<(Bool) -> Void>() @@ -2819,6 +2820,15 @@ public final class SharedAccountContextImpl: SharedAccountContext { return GiftViewScreen(context: context, subject: .message(message)) } + public func makeContentReportScreen(context: AccountContext, subject: ReportContentSubject, forceDark: Bool, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void) { + let _ = (context.engine.messages.reportContent(subject: subject, option: nil, message: nil) + |> deliverOnMainQueue).startStandalone(next: { result in + if case let .options(title, options) = result { + present(ContentReportScreen(context: context, subject: subject, title: title, options: options, forceDark: forceDark, completed: completion)) + } + }) + } + public func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal { return MiniAppListScreen.initialData(context: context) } From 860f543fb09a3c26ea024331bb550a7a97b450f8 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 24 Sep 2024 21:28:43 +0800 Subject: [PATCH 14/17] iPhone 16 Pro/Max metrics --- submodules/Camera/Sources/Camera.swift | 2 +- submodules/Camera/Sources/CameraMetrics.swift | 4 +++ submodules/Display/Source/DeviceMetrics.swift | 28 +++++++++++++------ .../PasscodeUI/Sources/PasscodeLayout.swift | 8 +++--- .../DeviceModel/Sources/DeviceModel.swift | 15 +++++++++- 5 files changed, 43 insertions(+), 14 deletions(-) diff --git a/submodules/Camera/Sources/Camera.swift b/submodules/Camera/Sources/Camera.swift index ca84dca958..f0a4d76653 100644 --- a/submodules/Camera/Sources/Camera.swift +++ b/submodules/Camera/Sources/Camera.swift @@ -102,7 +102,7 @@ final class CameraDeviceContext { return 30.0 } switch DeviceModel.current { - case .iPhone15ProMax, .iPhone14ProMax, .iPhone13ProMax: + case .iPhone15ProMax, .iPhone14ProMax, .iPhone13ProMax, .iPhone16ProMax: return 60.0 default: return 30.0 diff --git a/submodules/Camera/Sources/CameraMetrics.swift b/submodules/Camera/Sources/CameraMetrics.swift index 4d5c684da8..01d68996e1 100644 --- a/submodules/Camera/Sources/CameraMetrics.swift +++ b/submodules/Camera/Sources/CameraMetrics.swift @@ -34,6 +34,10 @@ public extension Camera { self = .iPhone15Pro case .iPhone15ProMax: self = .iPhone15ProMax + case .iPhone16Pro: + self = .iPhone15Pro + case .iPhone16ProMax: + self = .iPhone15ProMax case .unknown: self = .unknown default: diff --git a/submodules/Display/Source/DeviceMetrics.swift b/submodules/Display/Source/DeviceMetrics.swift index b01c2a487d..302af10890 100644 --- a/submodules/Display/Source/DeviceMetrics.swift +++ b/submodules/Display/Source/DeviceMetrics.swift @@ -36,6 +36,8 @@ public enum DeviceMetrics: CaseIterable, Equatable { case iPhone14ProZoomed case iPhone14ProMax case iPhone14ProMaxZoomed + case iPhone16Pro + case iPhone16ProMax case iPad case iPadMini case iPad102Inch @@ -68,6 +70,8 @@ public enum DeviceMetrics: CaseIterable, Equatable { .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, + .iPhone16Pro, + .iPhone16ProMax, .iPad, .iPadMini, .iPad102Inch, @@ -171,6 +175,10 @@ public enum DeviceMetrics: CaseIterable, Equatable { return CGSize(width: 430.0, height: 932.0) case .iPhone14ProMaxZoomed: return CGSize(width: 375.0, height: 812.0) + case .iPhone16Pro: + return CGSize(width: 402.0, height: 874.0) + case .iPhone16ProMax: + return CGSize(width: 440.0, height: 956.0) case .iPad: return CGSize(width: 768.0, height: 1024.0) case .iPadMini: @@ -204,6 +212,8 @@ public enum DeviceMetrics: CaseIterable, Equatable { return 53.0 + UIScreenPixel case .iPhone14Pro, .iPhone14ProMax: return 55.0 + case .iPhone16Pro, .iPhone16ProMax: + return 55.0 case let .unknown(_, _, _, screenCornerRadius): return screenCornerRadius default: @@ -213,7 +223,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { func safeInsets(inLandscape: Bool) -> UIEdgeInsets { switch self { - case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed: + case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax: return inLandscape ? UIEdgeInsets(top: 0.0, left: 44.0, bottom: 0.0, right: 44.0) : UIEdgeInsets(top: 44.0, left: 0.0, bottom: 0.0, right: 0.0) default: return UIEdgeInsets.zero @@ -222,7 +232,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { public func onScreenNavigationHeight(inLandscape: Bool, systemOnScreenNavigationHeight: CGFloat?) -> CGFloat? { switch self { - case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProMax: + case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProMax, .iPhone16Pro, .iPhone16ProMax: return inLandscape ? 21.0 : 34.0 case .iPhone14ProZoomed: return inLandscape ? 21.0 : 28.0 @@ -262,6 +272,8 @@ public enum DeviceMetrics: CaseIterable, Equatable { return 54.0 case .iPhone14ProMaxZoomed: return 47.0 + case .iPhone16Pro, .iPhone16ProMax: + return 54.0 case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax: return 44.0 case .iPadPro11Inch, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen: @@ -280,7 +292,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { return 162.0 case .iPhone6, .iPhone6Plus: return 163.0 - case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed: + case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax: return 172.0 case .iPad, .iPad102Inch, .iPadPro10Inch: return 348.0 @@ -299,9 +311,9 @@ public enum DeviceMetrics: CaseIterable, Equatable { return 216.0 case .iPhone6Plus: return 226.0 - case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed: + case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed, .iPhone16Pro: return 292.0 - case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax: + case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone16ProMax: return 302.0 case .iPad, .iPad102Inch, .iPadPro10Inch: return 263.0 @@ -320,7 +332,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { func predictiveInputHeight(inLandscape: Bool) -> CGFloat { if inLandscape { switch self { - case .iPhone4, .iPhone5, .iPhone6, .iPhone6Plus, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed: + case .iPhone4, .iPhone5, .iPhone6, .iPhone6Plus, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax: return 37.0 case .iPad, .iPad102Inch, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen: return 50.0 @@ -331,7 +343,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { switch self { case .iPhone4, .iPhone5: return 37.0 - case .iPhone6, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed: + case .iPhone6, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax: return 44.0 case .iPhone6Plus: return 45.0 @@ -358,7 +370,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { public var hasDynamicIsland: Bool { switch self { - case .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed: + case .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax: return true default: return false diff --git a/submodules/PasscodeUI/Sources/PasscodeLayout.swift b/submodules/PasscodeUI/Sources/PasscodeLayout.swift index 5819862754..c5032937c6 100644 --- a/submodules/PasscodeUI/Sources/PasscodeLayout.swift +++ b/submodules/PasscodeUI/Sources/PasscodeLayout.swift @@ -67,7 +67,7 @@ struct PasscodeKeyboardLayout { self.topOffset = 226.0 self.biometricsOffset = 30.0 self.deleteOffset = 20.0 - case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed: + case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed, .iPhone16Pro: self.buttonSize = 75.0 self.horizontalSecond = 103.0 self.horizontalThird = 206.0 @@ -78,7 +78,7 @@ struct PasscodeKeyboardLayout { self.topOffset = 294.0 self.biometricsOffset = 30.0 self.deleteOffset = 20.0 - case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax: + case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone16ProMax: self.buttonSize = 85.0 self.horizontalSecond = 115.0 self.horizontalThird = 230.0 @@ -151,11 +151,11 @@ public struct PasscodeLayout { self.titleOffset = 112.0 self.subtitleOffset = -6.0 self.inputFieldOffset = 156.0 - case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed: + case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed, .iPhone16Pro: self.titleOffset = 162.0 self.subtitleOffset = 0.0 self.inputFieldOffset = 206.0 - case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax: + case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone16ProMax: self.titleOffset = 180.0 self.subtitleOffset = 0.0 self.inputFieldOffset = 226.0 diff --git a/submodules/Utils/DeviceModel/Sources/DeviceModel.swift b/submodules/Utils/DeviceModel/Sources/DeviceModel.swift index ba30871c3b..78cdfc5018 100644 --- a/submodules/Utils/DeviceModel/Sources/DeviceModel.swift +++ b/submodules/Utils/DeviceModel/Sources/DeviceModel.swift @@ -48,7 +48,9 @@ public enum DeviceModel: CaseIterable, Equatable { .iPhone15, .iPhone15Plus, .iPhone15Pro, - .iPhone15ProMax + .iPhone15ProMax, + .iPhone16Pro, + .iPhone16ProMax ] } @@ -116,6 +118,9 @@ public enum DeviceModel: CaseIterable, Equatable { case iPhone15Pro case iPhone15ProMax + case iPhone16Pro + case iPhone16ProMax + case unknown(String) public var modelId: [String] { @@ -218,6 +223,10 @@ public enum DeviceModel: CaseIterable, Equatable { return ["iPhone16,1"] case .iPhone15ProMax: return ["iPhone16,2"] + case .iPhone16Pro: + return ["iPhone17,1"] + case .iPhone16ProMax: + return ["iPhone17,2"] case let .unknown(modelId): return [modelId] } @@ -323,6 +332,10 @@ public enum DeviceModel: CaseIterable, Equatable { return "iPhone 15 Pro" case .iPhone15ProMax: return "iPhone 15 Pro Max" + case .iPhone16Pro: + return "iPhone 16 Pro" + case .iPhone16ProMax: + return "iPhone 16 Pro Max" case let .unknown(modelId): if modelId.hasPrefix("iPhone") { return "Unknown iPhone" From 99fd201507110ef2c035fb6bd95c4d837d38e115 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 24 Sep 2024 21:29:01 +0800 Subject: [PATCH 15/17] Disable camera button when connecting --- submodules/TelegramCallsUI/Sources/VideoChatScreen.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index 85227b283c..ff0435974f 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -481,6 +481,15 @@ final class VideoChatScreenComponent: Component { guard let component = self.component, let environment = self.environment else { return } + guard let callState = self.callState else { + return + } + if case .connecting = callState.networkState { + return + } + if let muteState = callState.muteState, !muteState.canUnmute { + return + } HapticFeedback().impact(.light) if component.call.hasVideo { From 3d509c7bdac108cf7e97af396b271d30a6eb2830 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Wed, 25 Sep 2024 01:14:28 +0800 Subject: [PATCH 16/17] Video chat improvements --- .../Sources/PresentationCallManager.swift | 5 +- submodules/TelegramCallsUI/BUILD | 1 + .../Sources/PresentationGroupCall.swift | 7 +- .../ScheduleVideoChatSheetScreen.swift | 78 ++++++++++++- .../VideoChatActionButtonComponent.swift | 23 +++- .../Sources/VideoChatMuteIconComponent.swift | 6 +- .../VideoChatParticipantAvatarComponent.swift | 28 ++++- .../VideoChatParticipantStatusComponent.swift | 4 +- .../VideoChatParticipantVideoComponent.swift | 5 +- .../VideoChatParticipantsComponent.swift | 8 ++ .../VideoChatScheduledInfoComponent.swift | 67 +++++++++-- .../Sources/VideoChatScreen.swift | 107 +++++++++++++++++- .../Sources/PeerListItemComponent.swift | 4 +- 13 files changed, 315 insertions(+), 28 deletions(-) diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index d4d605a69a..c285caa029 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -212,6 +212,7 @@ public struct PresentationGroupCallState: Equatable { public var subscribedToScheduled: Bool public var isVideoEnabled: Bool public var isVideoWatchersLimitReached: Bool + public var hasVideo: Bool public init( myPeerId: EnginePeer.Id, @@ -226,7 +227,8 @@ public struct PresentationGroupCallState: Equatable { scheduleTimestamp: Int32?, subscribedToScheduled: Bool, isVideoEnabled: Bool, - isVideoWatchersLimitReached: Bool + isVideoWatchersLimitReached: Bool, + hasVideo: Bool ) { self.myPeerId = myPeerId self.networkState = networkState @@ -241,6 +243,7 @@ public struct PresentationGroupCallState: Equatable { self.subscribedToScheduled = subscribedToScheduled self.isVideoEnabled = isVideoEnabled self.isVideoWatchersLimitReached = isVideoWatchersLimitReached + self.hasVideo = hasVideo } } diff --git a/submodules/TelegramCallsUI/BUILD b/submodules/TelegramCallsUI/BUILD index eb0553d7a1..55988887b0 100644 --- a/submodules/TelegramCallsUI/BUILD +++ b/submodules/TelegramCallsUI/BUILD @@ -115,6 +115,7 @@ swift_library( "//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/Stories/PeerListItemComponent", "//submodules/TelegramUI/Components/BackButtonComponent", + "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/DirectMediaImageCache", "//submodules/FastBlur", ], diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 4bcace59a3..f990bdf289 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -268,7 +268,8 @@ private extension PresentationGroupCallState { scheduleTimestamp: scheduleTimestamp, subscribedToScheduled: subscribedToScheduled, isVideoEnabled: false, - isVideoWatchersLimitReached: false + isVideoWatchersLimitReached: false, + hasVideo: false ) } } @@ -2971,11 +2972,12 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.updateLocalVideoState() } + self.stateValue.hasVideo = self.hasVideo } public func disableVideo() { self.hasVideo = false - self.useFrontCamera = true; + self.useFrontCamera = true if let _ = self.videoCapturer { self.videoCapturer = nil self.isVideoMutedDisposable.set(nil) @@ -2984,6 +2986,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.updateLocalVideoState() } + self.stateValue.hasVideo = self.hasVideo } private func updateLocalVideoState() { diff --git a/submodules/TelegramCallsUI/Sources/ScheduleVideoChatSheetScreen.swift b/submodules/TelegramCallsUI/Sources/ScheduleVideoChatSheetScreen.swift index 08b7019d97..a6445e05ff 100644 --- a/submodules/TelegramCallsUI/Sources/ScheduleVideoChatSheetScreen.swift +++ b/submodules/TelegramCallsUI/Sources/ScheduleVideoChatSheetScreen.swift @@ -13,6 +13,10 @@ import BalancedTextComponent import TelegramPresentationData import TelegramStringFormatting import Markdown +import HierarchyTrackingLayer + +private let purple = UIColor(rgb: 0x3252ef) +private let pink = UIColor(rgb: 0xef436c) private final class ScheduleVideoChatSheetContentComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -33,7 +37,11 @@ private final class ScheduleVideoChatSheetContentComponent: Component { } final class View: UIView { + private let hierarchyTrackingLayer: HierarchyTrackingLayer + private let button = ComponentView() + private let buttonBackgroundLayer: SimpleGradientLayer + private let cancelButton = ComponentView() private let title = ComponentView() @@ -52,7 +60,29 @@ private final class ScheduleVideoChatSheetContentComponent: Component { self.dateFormatter.dateStyle = .short self.dateFormatter.timeZone = TimeZone.current + self.hierarchyTrackingLayer = HierarchyTrackingLayer() + + self.buttonBackgroundLayer = SimpleGradientLayer() + self.buttonBackgroundLayer.type = .radial + self.buttonBackgroundLayer.colors = [pink.cgColor, purple.cgColor, purple.cgColor] + self.buttonBackgroundLayer.locations = [0.0, 0.85, 1.0] + self.buttonBackgroundLayer.startPoint = CGPoint(x: 1.0, y: 0.0) + let radius = CGSize(width: 1.0, height: 2.0) + let endEndPoint = CGPoint(x: (self.buttonBackgroundLayer.startPoint.x + radius.width) * 1.0, y: (self.buttonBackgroundLayer.startPoint.y + radius.height) * 1.0) + self.buttonBackgroundLayer.endPoint = endEndPoint + self.buttonBackgroundLayer.cornerRadius = 10.0 + super.init(frame: frame) + + self.layer.addSublayer(self.hierarchyTrackingLayer) + self.hierarchyTrackingLayer.isInHierarchyUpdated = { [weak self] value in + guard let self else { + return + } + if value { + self.updateAnimations() + } + } } required init?(coder: NSCoder) { @@ -96,6 +126,46 @@ private final class ScheduleVideoChatSheetContentComponent: Component { } } + private func updateAnimations() { + if let _ = self.buttonBackgroundLayer.animation(forKey: "movement") { + } else { + let previousValue = self.buttonBackgroundLayer.startPoint + let previousEndValue = self.buttonBackgroundLayer.endPoint + let newValue = CGPoint(x: CGFloat.random(in: 0.65 ..< 0.85), y: CGFloat.random(in: 0.1 ..< 0.45)) + self.buttonBackgroundLayer.startPoint = newValue + + let radius = CGSize(width: 1.0, height: 2.0) + let newEndValue = CGPoint(x: (self.buttonBackgroundLayer.startPoint.x + radius.width) * 1.0, y: (self.buttonBackgroundLayer.startPoint.y + radius.height) * 1.0) + + CATransaction.begin() + + let animation = CABasicAnimation(keyPath: "startPoint") + animation.duration = Double.random(in: 0.8 ..< 1.4) + animation.fromValue = previousValue + animation.toValue = newValue + + CATransaction.setCompletionBlock { [weak self] in + guard let self else { + return + } + if self.hierarchyTrackingLayer.isInHierarchy { + self.updateAnimations() + } + } + + self.buttonBackgroundLayer.add(animation, forKey: "movement") + + let endAnimation = CABasicAnimation(keyPath: "endPoint") + endAnimation.duration = animation.duration + endAnimation.fromValue = previousEndValue + endAnimation.toValue = newEndValue + + self.buttonBackgroundLayer.add(animation, forKey: "movementEnd") + + CATransaction.commit() + } + } + func update(component: ScheduleVideoChatSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousComponent = self.component let _ = previousComponent @@ -233,9 +303,9 @@ private final class ScheduleVideoChatSheetContentComponent: Component { transition: buttonTransition, component: AnyComponent(ButtonComponent( background: ButtonComponent.Background( - color: UIColor(rgb: 0x3252EF), + color: .clear, foreground: .white, - pressedColor: UIColor(rgb: 0x3252EF).withMultipliedAlpha(0.8) + pressedColor: UIColor(white: 1.0, alpha: 0.1) ), content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( HStack(buttonContents, spacing: 5.0) @@ -256,8 +326,10 @@ private final class ScheduleVideoChatSheetContentComponent: Component { let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: buttonSize) if let buttonView = self.button.view { if buttonView.superview == nil { + self.layer.addSublayer(self.buttonBackgroundLayer) self.addSubview(buttonView) } + transition.setFrame(layer: self.buttonBackgroundLayer, frame: buttonFrame) transition.setFrame(view: buttonView, frame: buttonFrame) } contentHeight += buttonSize.height @@ -302,6 +374,8 @@ private final class ScheduleVideoChatSheetContentComponent: Component { contentHeight += environment.safeInsets.bottom + 14.0 } + self.updateAnimations() + return CGSize(width: availableSize.width, height: contentHeight) } } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift index 8d6b3f69bd..435bdecdaf 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift @@ -34,11 +34,13 @@ final class VideoChatActionButtonComponent: Component { case audio(audio: Audio) case video case leave + case switchVideo } case audio(audio: Audio) case video(isActive: Bool) case leave + case switchVideo fileprivate var iconType: IconType { switch self { @@ -57,6 +59,8 @@ final class VideoChatActionButtonComponent: Component { return .video case .leave: return .leave + case .switchVideo: + return .switchVideo } } } @@ -174,6 +178,19 @@ final class VideoChatActionButtonComponent: Component { backgroundColor = UIColor(rgb: 0x3252EF) } iconDiameter = 60.0 + case .switchVideo: + titleText = "" + switch component.microphoneState { + case .connecting: + backgroundColor = UIColor(white: 0.1, alpha: 1.0) + case .muted: + backgroundColor = UIColor(rgb: 0x027FFF) + case .unmuted: + backgroundColor = UIColor(rgb: 0x34C659) + case .raiseHand, .scheduled: + backgroundColor = UIColor(rgb: 0x3252EF) + } + iconDiameter = 54.0 case .leave: titleText = "leave" backgroundColor = UIColor(rgb: 0x47191E) @@ -204,6 +221,8 @@ final class VideoChatActionButtonComponent: Component { self.contentImage = UIImage(bundleImageName: iconName)?.precomposed().withRenderingMode(.alwaysTemplate) case .video: self.contentImage = UIImage(bundleImageName: "Call/CallCameraButton")?.precomposed().withRenderingMode(.alwaysTemplate) + case .switchVideo: + self.contentImage = UIImage(bundleImageName: "Call/CallSwitchCameraButton")?.precomposed().withRenderingMode(.alwaysTemplate) case .leave: self.contentImage = generateImage(CGSize(width: 28.0, height: 28.0), opaque: false, rotatedContext: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) @@ -275,7 +294,9 @@ final class VideoChatActionButtonComponent: Component { if iconView.superview == nil { self.addSubview(iconView) } - transition.setFrame(view: iconView, frame: iconFrame) + transition.setPosition(view: iconView, position: iconFrame.center) + transition.setBounds(view: iconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size)) + transition.setScale(view: iconView, scale: availableSize.width / 56.0) } return size diff --git a/submodules/TelegramCallsUI/Sources/VideoChatMuteIconComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatMuteIconComponent.swift index 3bc92c4bcf..f50c490402 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatMuteIconComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatMuteIconComponent.swift @@ -62,6 +62,7 @@ final class VideoChatMuteIconComponent: Component { self.isUpdating = false } + let previousComponent = self.component self.component = component if case let .mute(isFilled, isMuted) = component.content { @@ -77,7 +78,10 @@ final class VideoChatMuteIconComponent: Component { let animationSize = availableSize let animationFrame = animationSize.centered(in: CGRect(origin: CGPoint(), size: availableSize)) transition.setFrame(view: icon.view, frame: animationFrame) - icon.update(state: VoiceChatMicrophoneNode.State(muted: isMuted, filled: isFilled, color: component.color), animated: !transition.animation.isImmediate) + if let previousComponent, previousComponent.content == component.content, previousComponent.color == component.color { + } else { + icon.update(state: VoiceChatMicrophoneNode.State(muted: isMuted, filled: isFilled, color: component.color), animated: !transition.animation.isImmediate) + } } else { if let icon = self.icon { self.icon = nil diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift index 834fc03318..bcc2c08954 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift @@ -136,6 +136,7 @@ final class VideoChatParticipantAvatarComponent: Component { let peer: EnginePeer let myPeerId: EnginePeer.Id let isSpeaking: Bool + let isMutedForMe: Bool let theme: PresentationTheme init( @@ -143,12 +144,14 @@ final class VideoChatParticipantAvatarComponent: Component { peer: EnginePeer, myPeerId: EnginePeer.Id, isSpeaking: Bool, + isMutedForMe: Bool, theme: PresentationTheme ) { self.call = call self.peer = peer self.myPeerId = myPeerId self.isSpeaking = isSpeaking + self.isMutedForMe = isMutedForMe self.theme = theme } @@ -159,10 +162,13 @@ final class VideoChatParticipantAvatarComponent: Component { if lhs.peer != rhs.peer { return false } + if lhs.myPeerId != rhs.myPeerId { + return false + } if lhs.isSpeaking != rhs.isSpeaking { return false } - if lhs.myPeerId != rhs.myPeerId { + if lhs.isMutedForMe != rhs.isMutedForMe { return false } if lhs.theme !== rhs.theme { @@ -259,7 +265,15 @@ final class VideoChatParticipantAvatarComponent: Component { } else { tintTransition = .immediate } - tintTransition.setTintColor(layer: blobView.blobsLayer, color: component.isSpeaking ? UIColor(rgb: 0x33C758) : component.theme.list.itemAccentColor) + let tintColor: UIColor + if component.isMutedForMe { + tintColor = UIColor(rgb: 0xff3b30) + } else if component.isSpeaking { + tintColor = UIColor(rgb: 0x33C758) + } else { + tintColor = component.theme.list.itemAccentColor + } + tintTransition.setTintColor(layer: blobView.blobsLayer, color: tintColor) } if component.peer.smallProfileImage != nil { @@ -362,7 +376,15 @@ final class VideoChatParticipantAvatarComponent: Component { avatarNode.layer.transform = CATransform3DMakeScale(1.0 + additionalScale, 1.0 + additionalScale, 1.0) } - ComponentTransition.immediate.setTintColor(layer: blobView.blobsLayer, color: component.isSpeaking ? UIColor(rgb: 0x33C758) : component.theme.list.itemAccentColor) + let tintColor: UIColor + if component.isMutedForMe { + tintColor = UIColor(rgb: 0xff3b30) + } else if component.isSpeaking { + tintColor = UIColor(rgb: 0x33C758) + } else { + tintColor = component.theme.list.itemAccentColor + } + ComponentTransition.immediate.setTintColor(layer: blobView.blobsLayer, color: tintColor) } if blobView.alpha == 0.0 { diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantStatusComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantStatusComponent.swift index 5b480bac46..7375afb149 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantStatusComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantStatusComponent.swift @@ -121,7 +121,9 @@ final class VideoChatParticipantStatusComponent: Component { } if let iconView = muteStatusView.iconView { let iconTintColor: UIColor - if component.isSpeaking { + if let muteState = component.muteState, muteState.mutedByYou { + iconTintColor = UIColor(rgb: 0xff3b30) + } else if component.isSpeaking { iconTintColor = UIColor(rgb: 0x33C758) } else { if let muteState = component.muteState { diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift index d295c40a7f..7fd4ae4af7 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift @@ -352,7 +352,10 @@ final class VideoChatParticipantVideoComponent: Component { alphaTransition.setAlpha(view: titleView, alpha: controlsAlpha) } - let videoDescription = component.isPresentation ? component.participant.presentationDescription : component.participant.videoDescription + var videoDescription = component.isPresentation ? component.participant.presentationDescription : component.participant.videoDescription + if component.isPresentation && component.isMyPeer { + videoDescription = nil + } var isEffectivelyPaused = false if let videoDescription, videoDescription.isPaused { diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift index dda1b187c8..e7e0c24831 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift @@ -1154,9 +1154,16 @@ final class VideoChatParticipantsComponent: Component { let itemFrame = itemLayout.listItemFrame(at: i) + var isMutedForMe = false + if let muteState = participant.muteState, muteState.mutedByYou { + isMutedForMe = true + } + let subtitle: PeerListItemComponent.Subtitle if participant.peer.id == component.call.accountContext.account.peerId { subtitle = PeerListItemComponent.Subtitle(text: "this is you", color: .accent) + } else if let muteState = participant.muteState, muteState.mutedByYou { + subtitle = PeerListItemComponent.Subtitle(text: "muted for you", color: .destructive) } else if component.speakingParticipants.contains(participant.peer.id) { if let volume = participant.volume, volume != 10000 { subtitle = PeerListItemComponent.Subtitle(text: "\(volume / 100)% speaking", color: .constructive) @@ -1190,6 +1197,7 @@ final class VideoChatParticipantsComponent: Component { peer: EnginePeer(participant.peer), myPeerId: component.participants?.myPeerId ?? component.call.accountContext.account.peerId, isSpeaking: component.speakingParticipants.contains(participant.peer.id), + isMutedForMe: isMutedForMe, theme: component.theme )), peer: EnginePeer(participant.peer), diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScheduledInfoComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatScheduledInfoComponent.swift index 3c97c76306..47e5a2407b 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScheduledInfoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScheduledInfoComponent.swift @@ -6,6 +6,7 @@ import MultilineTextComponent import TelegramPresentationData import TelegramStringFormatting import HierarchyTrackingLayer +import AnimatedTextComponent private let purple = UIColor(rgb: 0x3252ef) private let pink = UIColor(rgb: 0xef436c) @@ -13,6 +14,33 @@ private let pink = UIColor(rgb: 0xef436c) private let latePurple = UIColor(rgb: 0x974aa9) private let latePink = UIColor(rgb: 0xf0436c) +private func textItemsForTimeout(value: Int32) -> [AnimatedTextComponent.Item] { + if value < 3600 { + let minutes = value / 60 + let seconds = value % 60 + + var items: [AnimatedTextComponent.Item] = [] + items.append(AnimatedTextComponent.Item(id: AnyHashable(11), content: .number(Int(minutes), minDigits: 1))) + items.append(AnimatedTextComponent.Item(id: AnyHashable(12), content: .text(":"))) + items.append(AnimatedTextComponent.Item(id: AnyHashable(13), content: .number(Int(seconds), minDigits: 2))) + + return items + } else { + let hours = value / 3600 + let minutes = (value % 3600) / 60 + let seconds = value % 60 + + var items: [AnimatedTextComponent.Item] = [] + items.append(AnimatedTextComponent.Item(id: AnyHashable(9), content: .number(Int(hours), minDigits: 1))) + items.append(AnimatedTextComponent.Item(id: AnyHashable(10), content: .text(":"))) + items.append(AnimatedTextComponent.Item(id: AnyHashable(11), content: .number(Int(minutes), minDigits: 2))) + items.append(AnimatedTextComponent.Item(id: AnyHashable(12), content: .text(":"))) + items.append(AnimatedTextComponent.Item(id: AnyHashable(13), content: .number(Int(seconds), minDigits: 2))) + + return items + } +} + final class VideoChatScheduledInfoComponent: Component { let timestamp: Int32 let strings: PresentationStrings @@ -46,8 +74,11 @@ final class VideoChatScheduledInfoComponent: Component { private let hierarchyTrackingLayer: HierarchyTrackingLayer private var component: VideoChatScheduledInfoComponent? + private weak var state: EmptyComponentState? private var isUpdating: Bool = false + private var countdownTimer: Foundation.Timer? + override init(frame: CGRect) { self.countdownContainerView = UIView() self.countdownMaskView = UIView() @@ -76,6 +107,9 @@ final class VideoChatScheduledInfoComponent: Component { } if value { self.updateAnimations() + } else { + self.countdownTimer?.invalidate() + self.countdownTimer = nil } } } @@ -84,6 +118,10 @@ final class VideoChatScheduledInfoComponent: Component { fatalError("init(coder:) has not been implemented") } + deinit { + self.countdownTimer?.invalidate() + } + private func updateAnimations() { if let _ = self.countdownGradientLayer.animation(forKey: "movement") { } else { @@ -110,6 +148,15 @@ final class VideoChatScheduledInfoComponent: Component { self.countdownGradientLayer.add(animation, forKey: "movement") CATransaction.commit() } + + if self.countdownTimer == nil { + self.countdownTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self] _ in + guard let self else { + return + } + self.state?.updated(transition: .easeInOut(duration: 0.2)) + }) + } } func update(component: VideoChatScheduledInfoComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { @@ -119,6 +166,7 @@ final class VideoChatScheduledInfoComponent: Component { } self.component = component + self.state = state let titleSize = self.title.update( transition: .immediate, @@ -130,21 +178,20 @@ final class VideoChatScheduledInfoComponent: Component { ) let remainingSeconds: Int32 = max(0, component.timestamp - Int32(Date().timeIntervalSince1970)) - let countdownText: String + var items: [AnimatedTextComponent.Item] = [] if remainingSeconds >= 86400 { - countdownText = scheduledTimeIntervalString(strings: component.strings, value: remainingSeconds) + let countdownText = scheduledTimeIntervalString(strings: component.strings, value: remainingSeconds) + items.append(AnimatedTextComponent.Item(id: AnyHashable(0), content: .text(countdownText))) } else { - countdownText = textForTimeout(value: abs(remainingSeconds)) - /*if remainingSeconds < 0 && !self.isLate { - self.isLate = true - self.foregroundGradientLayer.colors = [latePink.cgColor, latePurple.cgColor, latePurple.cgColor] - }*/ + items = textItemsForTimeout(value: remainingSeconds) } let countdownTextSize = self.countdownText.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: countdownText, font: Font.with(size: 68.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: .white)) + transition: transition, + component: AnyComponent(AnimatedTextComponent( + font: Font.with(size: 68.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), + color: .white, + items: items )), environment: {}, containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 400.0) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index ff0435974f..2bc963014d 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -76,6 +76,7 @@ final class VideoChatScreenComponent: Component { var navigationSidebarButton: ComponentView? let videoButton = ComponentView() + var switchVideoButton: ComponentView? let leaveButton = ComponentView() let microphoneButton = ComponentView() @@ -1316,10 +1317,17 @@ final class VideoChatScreenComponent: Component { } } + let actionButtonPlacementArea: (x: CGFloat, width: CGFloat) + if isTwoColumnLayout { + actionButtonPlacementArea = (availableSize.width - sideInset - mainColumnWidth, mainColumnWidth) + } else { + actionButtonPlacementArea = (0.0, availableSize.width) + } + let buttonsSideInset: CGFloat = 26.0 let buttonsWidth: CGFloat = actionButtonDiameter * 2.0 + microphoneButtonDiameter - let remainingButtonsSpace: CGFloat = availableSize.width - buttonsSideInset * 2.0 - buttonsWidth + let remainingButtonsSpace: CGFloat = actionButtonPlacementArea.width - buttonsSideInset * 2.0 - buttonsWidth let effectiveMaxActionMicrophoneButtonSpacing: CGFloat if areButtonsCollapsed { @@ -1355,7 +1363,7 @@ final class VideoChatScreenComponent: Component { } } - let microphoneButtonFrame: CGRect + var microphoneButtonFrame: CGRect if areButtonsCollapsed { microphoneButtonFrame = expandedMicrophoneButtonFrame } else { @@ -1376,8 +1384,41 @@ final class VideoChatScreenComponent: Component { expandedParticipantsClippingY = expandedMicrophoneButtonFrame.minY - 24.0 } - let leftActionButtonFrame = CGRect(origin: CGPoint(x: microphoneButtonFrame.minX - actionMicrophoneButtonSpacing - actionButtonDiameter, y: microphoneButtonFrame.minY + floor((microphoneButtonFrame.height - actionButtonDiameter) * 0.5)), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter)) - let rightActionButtonFrame = CGRect(origin: CGPoint(x: microphoneButtonFrame.maxX + actionMicrophoneButtonSpacing, y: microphoneButtonFrame.minY + floor((microphoneButtonFrame.height - actionButtonDiameter) * 0.5)), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter)) + var leftActionButtonFrame = CGRect(origin: CGPoint(x: microphoneButtonFrame.minX - actionMicrophoneButtonSpacing - actionButtonDiameter, y: microphoneButtonFrame.minY + floor((microphoneButtonFrame.height - actionButtonDiameter) * 0.5)), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter)) + var rightActionButtonFrame = CGRect(origin: CGPoint(x: microphoneButtonFrame.maxX + actionMicrophoneButtonSpacing, y: microphoneButtonFrame.minY + floor((microphoneButtonFrame.height - actionButtonDiameter) * 0.5)), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter)) + + var additionalLeftActionButtonFrame: CGRect? + if let callState = self.callState, callState.hasVideo { + let additionalButtonDiameter: CGFloat + if areButtonsCollapsed { + additionalButtonDiameter = actionButtonDiameter + } else { + additionalButtonDiameter = floor(actionButtonDiameter * 0.64) + } + + if areButtonsCollapsed { + let buttonCount: CGFloat = 4.0 + + let buttonsWidth: CGFloat = actionButtonDiameter * buttonCount + let remainingButtonsSpace: CGFloat = actionButtonPlacementArea.width - buttonsSideInset * 2.0 - buttonsWidth + let maxSpacing: CGFloat = 80.0 + let effectiveSpacing = min(maxSpacing, floor(remainingButtonsSpace / (buttonCount - 1.0))) + + let totalButtonsWidth: CGFloat = buttonsWidth + (buttonCount - 1.0) * effectiveSpacing + let totalButtonsX: CGFloat = actionButtonPlacementArea.x + floor((actionButtonPlacementArea.width - totalButtonsWidth) * 0.5) + additionalLeftActionButtonFrame = CGRect(origin: CGPoint(x: totalButtonsX + CGFloat(0.0) * (actionButtonDiameter + effectiveSpacing), y: leftActionButtonFrame.minY), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter)) + leftActionButtonFrame = CGRect(origin: CGPoint(x: totalButtonsX + CGFloat(1.0) * (actionButtonDiameter + effectiveSpacing), y: leftActionButtonFrame.minY), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter)) + microphoneButtonFrame = CGRect(origin: CGPoint(x: totalButtonsX + CGFloat(2.0) * (actionButtonDiameter + effectiveSpacing), y: leftActionButtonFrame.minY), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter)) + rightActionButtonFrame = CGRect(origin: CGPoint(x: totalButtonsX + CGFloat(3.0) * (actionButtonDiameter + effectiveSpacing), y: leftActionButtonFrame.minY), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter)) + } else { + let additionalButtonSpacing = 12.0 + let totalLeftButtonHeight: CGFloat = leftActionButtonFrame.height + additionalButtonSpacing + additionalButtonDiameter + let totalLeftButtonOriginY: CGFloat = leftActionButtonFrame.minY + floor((leftActionButtonFrame.height - totalLeftButtonHeight) * 0.5) + leftActionButtonFrame.origin.y = totalLeftButtonOriginY + additionalButtonDiameter + additionalButtonSpacing + + additionalLeftActionButtonFrame = CGRect(origin: CGPoint(x: leftActionButtonFrame.minX + floor((leftActionButtonFrame.width - additionalButtonDiameter) * 0.5), y: leftActionButtonFrame.minY - additionalButtonSpacing - additionalButtonDiameter), size: CGSize(width: additionalButtonDiameter, height: additionalButtonDiameter)) + } + } let participantsSize = availableSize @@ -1728,7 +1769,7 @@ final class VideoChatScreenComponent: Component { videoButtonContent = .audio(audio: buttonAudio) } else { //TODO:release - videoButtonContent = .video(isActive: false) + videoButtonContent = .video(isActive: self.callState?.hasVideo ?? false) } let _ = self.videoButton.update( transition: transition, @@ -1763,6 +1804,62 @@ final class VideoChatScreenComponent: Component { transition.setBounds(view: videoButtonView, bounds: CGRect(origin: CGPoint(), size: leftActionButtonFrame.size)) } + if let additionalLeftActionButtonFrame { + let switchVideoButton: ComponentView + var switchVideoButtonTransition = transition + if let current = self.switchVideoButton { + switchVideoButton = current + } else { + switchVideoButtonTransition = switchVideoButtonTransition.withAnimation(.none) + switchVideoButton = ComponentView() + self.switchVideoButton = switchVideoButton + } + + let switchVideoButtonContent: VideoChatActionButtonComponent.Content = .switchVideo + + let _ = switchVideoButton.update( + transition: switchVideoButtonTransition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(VideoChatActionButtonComponent( + strings: environment.strings, + content: switchVideoButtonContent, + microphoneState: actionButtonMicrophoneState, + isCollapsed: areButtonsCollapsed + )), + effectAlignment: .center, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.call.switchVideoCamera() + }, + animateAlpha: false + )), + environment: {}, + containerSize: additionalLeftActionButtonFrame.size + ) + if let switchVideoButtonView = switchVideoButton.view { + var animateIn = false + if switchVideoButtonView.superview == nil { + self.containerView.addSubview(switchVideoButtonView) + animateIn = true + } + switchVideoButtonTransition.setFrame(view: switchVideoButtonView, frame: additionalLeftActionButtonFrame) + if animateIn { + alphaTransition.animateAlpha(view: switchVideoButtonView, from: 0.0, to: 1.0) + transition.animateScale(view: switchVideoButtonView, from: 0.001, to: 1.0) + } + } + } else if let switchVideoButton = self.switchVideoButton { + self.switchVideoButton = nil + if let switchVideoButtonView = switchVideoButton.view { + alphaTransition.setAlpha(view: switchVideoButtonView, alpha: 0.0, completion: { [weak switchVideoButtonView] _ in + switchVideoButtonView?.removeFromSuperview() + }) + transition.setScale(view: switchVideoButtonView, scale: 0.001) + } + } + let _ = self.leaveButton.update( transition: transition, component: AnyComponent(PlainButtonComponent( diff --git a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift index 349aa6da22..972ce43e09 100644 --- a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift @@ -169,6 +169,7 @@ public final class PeerListItemComponent: Component { case neutral case accent case constructive + case destructive } public var text: String @@ -937,8 +938,9 @@ public final class PeerListItemComponent: Component { case .accent: labelColor = component.theme.list.itemAccentColor case .constructive: - //TODO:release labelColor = UIColor(rgb: 0x33C758) + case .destructive: + labelColor = UIColor(rgb: 0xff3b30) } var animateLabelDirection: Bool? From b71275482055b2b59ab41da9080fde4a843f98c3 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Wed, 25 Sep 2024 01:26:17 +0800 Subject: [PATCH 17/17] Fix video chat action button tint transition --- .../Source/Base/Transition.swift | 21 +++++++++++++++++++ .../VideoChatActionButtonComponent.swift | 6 +++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index b42f21063e..837eacb44d 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -1189,6 +1189,27 @@ public struct ComponentTransition { } } + public func animateTintColor(layer: CALayer, from: UIColor, to: UIColor, completion: ((Bool) -> Void)? = nil) { + switch self.animation { + case .none: + completion?(true) + case let .curve(duration, curve): + let previousColor: CGColor = from.cgColor + + layer.animate( + from: previousColor, + to: to.cgColor, + keyPath: "contentsMultiplyColor", + duration: duration, + delay: 0.0, + curve: curve, + removeOnCompletion: true, + additive: false, + completion: completion + ) + } + } + public func setGradientColors(layer: CAGradientLayer, colors: [UIColor], completion: ((Bool) -> Void)? = nil) { if let current = layer.colors { if current.count == colors.count { diff --git a/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift index 435bdecdaf..e965986f30 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift @@ -267,7 +267,11 @@ final class VideoChatActionButtonComponent: Component { } else { tintTransition = .immediate } - tintTransition.setTintColor(layer: self.background.layer, color: backgroundColor) + let previousTintColor = self.background.tintColor + self.background.tintColor = backgroundColor + if let previousTintColor, previousTintColor != backgroundColor { + tintTransition.animateTintColor(layer: self.background.layer, from: previousTintColor, to: backgroundColor) + } let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) * 0.5), y: size.height + 8.0), size: titleSize) if let titleView = self.title.view {