From cdc04c733c5f52156a4c0b92313e555752cd2ccb Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 5 Nov 2021 20:35:30 +0400 Subject: [PATCH] Wallpaper experiment --- .../Sources/DebugController.swift | 14 +- .../Items/UniversalVideoGalleryItem.swift | 56 +- .../Sources/SoftwareGradientBackground.swift | 23 +- .../Sources/PasscodeEntryControllerNode.swift | 16 +- .../BubbleSettingsController.swift | 2 +- .../ForwardPrivacyChatPreviewItem.swift | 2 +- .../TextSizeSelectionController.swift | 2 +- .../Themes/SettingsThemeWallpaperNode.swift | 2 +- .../ThemeAccentColorControllerNode.swift | 2 +- .../Themes/ThemePreviewControllerNode.swift | 4 +- .../Themes/ThemeSettingsChatPreviewItem.swift | 2 +- .../Sources/Themes/WallpaperGalleryItem.swift | 2 +- .../Sources/ShareController.swift | 2 +- submodules/Svg/PublicHeaders/Svg/Svg.h | 2 +- submodules/Svg/Sources/Svg.m | 6 +- .../State/AccountStateManagementUtils.swift | 2 +- .../TelegramUI/Sources/ChatController.swift | 2 +- .../Sources/ChatHistoryListNode.swift | 2 +- .../ChatRecentActionsControllerNode.swift | 2 +- .../Sources/ExperimentalUISettings.swift | 12 +- submodules/WallpaperBackgroundNode/BUILD | 3 + .../Sources/WallpaperBackgroundNode.swift | 770 ++++++++++++++++-- .../Sources/WallpaperResources.swift | 2 +- 23 files changed, 834 insertions(+), 98 deletions(-) diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index 8b043d797e..3ada505314 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -80,7 +80,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case experimentalCompatibility(Bool) case enableDebugDataDisplay(Bool) case acceleratedStickers(Bool) - case mockICE(Bool) + case experimentalBackground(Bool) case playerEmbedding(Bool) case playlistPlayback(Bool) case voiceConference @@ -102,7 +102,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.logging.rawValue case .enableRaiseToSpeak, .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries: return DebugControllerSection.experiments.rawValue - case .clearTips, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .playerEmbedding, .playlistPlayback, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .mockICE: + case .clearTips, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .playerEmbedding, .playlistPlayback, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .experimentalBackground: return DebugControllerSection.experiments.rawValue case .preferredVideoCodec: return DebugControllerSection.videoExperiments.rawValue @@ -171,7 +171,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 27 case .acceleratedStickers: return 29 - case .mockICE: + case .experimentalBackground: return 30 case .playerEmbedding: return 31 @@ -804,12 +804,12 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) }).start() }) - case let .mockICE(value): - return ItemListSwitchItem(presentationData: presentationData, title: "mockICE", value: value, sectionId: self.section, style: .blocks, updated: { value in + case let .experimentalBackground(value): + return ItemListSwitchItem(presentationData: presentationData, title: "Background Experiment", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings - settings.mockICE = value + settings.experimentalBackground = value return PreferencesEntry(settings) }) }).start() @@ -926,7 +926,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present entries.append(.experimentalCompatibility(experimentalSettings.experimentalCompatibility)) entries.append(.enableDebugDataDisplay(experimentalSettings.enableDebugDataDisplay)) entries.append(.acceleratedStickers(experimentalSettings.acceleratedStickers)) - entries.append(.mockICE(experimentalSettings.mockICE)) + entries.append(.experimentalBackground(experimentalSettings.experimentalBackground)) entries.append(.playerEmbedding(experimentalSettings.playerEmbedding)) entries.append(.playlistPlayback(experimentalSettings.playlistPlayback)) } diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 2e12e5845f..7da6c75921 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -518,7 +518,22 @@ private final class PictureInPictureContentImpl: NSObject, PictureInPictureConte } public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, skipByInterval skipInterval: CMTime, completion completionHandler: @escaping () -> Void) { - completionHandler() + let node = self.node + let _ = (self.node.status + |> take(1) + |> deliverOnMainQueue).start(next: { [weak node] status in + if let node = node, let timestamp = status?.timestamp, let duration = status?.duration { + let nextTimestamp = timestamp + skipInterval.seconds + if nextTimestamp > duration { + node.seek(0.0) + node.pause() + } else { + node.seek(min(duration, nextTimestamp)) + } + } + + completionHandler() + }) } public func pictureInPictureControllerShouldProhibitBackgroundAudioPlayback(_ pictureInPictureController: AVPictureInPictureController) -> Bool { @@ -539,7 +554,9 @@ private final class PictureInPictureContentImpl: NSObject, PictureInPictureConte private var hiddenMediaManagerIndex: Int? - init(overlayController: OverlayMediaController, mediaManager: MediaManager, accountId: AccountRecordId, hiddenMedia: (MessageId, Media)?, videoNode: UniversalVideoNode, willBegin: @escaping (PictureInPictureContentImpl) -> Void, didEnd: @escaping (PictureInPictureContentImpl) -> Void, expand: @escaping (@escaping () -> Void) -> Void) { + private var messageRemovedDisposable: Disposable? + + init(context: AccountContext, overlayController: OverlayMediaController, mediaManager: MediaManager, accountId: AccountRecordId, hiddenMedia: (MessageId, Media)?, videoNode: UniversalVideoNode, canSkip: Bool, willBegin: @escaping (PictureInPictureContentImpl) -> Void, didEnd: @escaping (PictureInPictureContentImpl) -> Void, expand: @escaping (@escaping () -> Void) -> Void) { self.overlayController = overlayController self.mediaManager = mediaManager self.node = videoNode @@ -559,7 +576,7 @@ private final class PictureInPictureContentImpl: NSObject, PictureInPictureConte contentDelegate.pictureInPictureController = pictureInPictureController pictureInPictureController.canStartPictureInPictureAutomaticallyFromInline = false - pictureInPictureController.requiresLinearPlayback = true + pictureInPictureController.requiresLinearPlayback = !canSkip pictureInPictureController.delegate = self self.pictureInPictureController = pictureInPictureController let timer = SwiftSignalKit.Timer(timeout: 0.005, repeat: true, completion: { [weak self] in @@ -586,9 +603,31 @@ private final class PictureInPictureContentImpl: NSObject, PictureInPictureConte } }) } + + if let (messageId, _) = hiddenMedia { + self.messageRemovedDisposable = (context.account.postbox.combinedView(keys: [PostboxViewKey.messages([messageId])]) + |> map { views -> Bool in + if let view = views.views[PostboxViewKey.messages([messageId])] as? MessagesView { + if view.messages[messageId] == nil { + return true + } + } + return false + } + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let strongSelf = self else { + return + } + overlayController.removePictureInPictureContent(content: strongSelf) + strongSelf.node.canAttachContent = false + }) + } } deinit { + self.messageRemovedDisposable?.dispose() self.pictureInPictureTimer?.invalidate() self.node.setCanPlaybackWithoutHierarchy(false) @@ -698,6 +737,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private var dismissOnOrientationChange = false private var keepSoundOnDismiss = false private var hasPictureInPicture = false + + private var pictureInPictureButton: UIBarButtonItem? private var requiresDownload = false @@ -1328,6 +1369,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } if forceEnablePiP || (!isAnimated && !disablePlayerControls && !disablePictureInPicture) { let rightBarButtonItem = UIBarButtonItem(image: pictureInPictureButtonImage, style: .plain, target: self, action: #selector(self.pictureInPictureButtonPressed)) + self.pictureInPictureButton = rightBarButtonItem barButtonItems.append(rightBarButtonItem) self.hasPictureInPicture = true } else { @@ -1427,6 +1469,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { pictureInPictureNode.removeFromSupernode() self.videoNode?.backgroundColor = .black } + + self.pictureInPictureButton?.isEnabled = self.pictureInPictureNode == nil } private func shouldAutoplayOnCentrality() -> Bool { @@ -2069,7 +2113,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { case let .message(message): for media in message.media { if let media = media as? TelegramMediaFile, media.isVideo { - isNativePictureInPictureSupported = true + if message.id.namespace == Namespaces.Message.Cloud { + isNativePictureInPictureSupported = true + } } } default: @@ -2106,7 +2152,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { break } - let content = PictureInPictureContentImpl(overlayController: overlayController, mediaManager: self.context.sharedContext.mediaManager, accountId: self.context.account.id, hiddenMedia: hiddenMedia, videoNode: overlayVideoNode, willBegin: { [weak self] content in + let content = PictureInPictureContentImpl(context: self.context, overlayController: overlayController, mediaManager: self.context.sharedContext.mediaManager, accountId: self.context.account.id, hiddenMedia: hiddenMedia, videoNode: overlayVideoNode, canSkip: true, willBegin: { [weak self] content in guard let strongSelf = self else { return } diff --git a/submodules/GradientBackground/Sources/SoftwareGradientBackground.swift b/submodules/GradientBackground/Sources/SoftwareGradientBackground.swift index d9fa846ecf..0c22d1e273 100644 --- a/submodules/GradientBackground/Sources/SoftwareGradientBackground.swift +++ b/submodules/GradientBackground/Sources/SoftwareGradientBackground.swift @@ -275,7 +275,7 @@ public final class GradientBackgroundNode: ASDisplayNode { deinit { } - public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition, extendAnimation: Bool = false, backwards: Bool = false) { + public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition, extendAnimation: Bool, backwards: Bool, completion: @escaping () -> Void) { let sizeUpdated = self.validLayout != size self.validLayout = size @@ -372,6 +372,10 @@ public final class GradientBackgroundNode: ASDisplayNode { animation.beginTime = self.contentView.layer.convertTime(CACurrentMediaTime(), from: nil) + 0.25 } + animation.completion = { _ in + completion() + } + self.contentView.layer.removeAnimation(forKey: "contents") self.contentView.layer.add(animation, forKey: "contents") @@ -405,7 +409,11 @@ public final class GradientBackgroundNode: ASDisplayNode { for cloneNode in self.cloneNodes { cloneNode.value?.image = dimmedImage } + + completion() } + } else { + completion() } } else if sizeUpdated { let image = generateGradient(size: imageSize, colors: self.colors, positions: positions) @@ -419,6 +427,10 @@ public final class GradientBackgroundNode: ASDisplayNode { } self.validPhase = self.phase + + completion() + } else { + completion() } transition.updateFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: size)) @@ -440,13 +452,14 @@ public final class GradientBackgroundNode: ASDisplayNode { self.colors = colors self.invalidated = true if let size = self.validLayout { - self.updateLayout(size: size, transition: .immediate) + self.updateLayout(size: size, transition: .immediate, extendAnimation: false, backwards: false, completion: {}) } } } - public func animateEvent(transition: ContainedViewLayoutTransition, extendAnimation: Bool = false, backwards: Bool = false) { + public func animateEvent(transition: ContainedViewLayoutTransition, extendAnimation: Bool, backwards: Bool, completion: @escaping () -> Void) { guard case let .animated(duration, _) = transition, duration > 0.001 else { + completion() return } @@ -463,7 +476,9 @@ public final class GradientBackgroundNode: ASDisplayNode { GradientBackgroundNode.sharedPhase = self.phase } if let size = self.validLayout { - self.updateLayout(size: size, transition: transition, extendAnimation: extendAnimation, backwards: backwards) + self.updateLayout(size: size, transition: transition, extendAnimation: extendAnimation, backwards: backwards, completion: completion) + } else { + completion() } } } diff --git a/submodules/PasscodeUI/Sources/PasscodeEntryControllerNode.swift b/submodules/PasscodeUI/Sources/PasscodeEntryControllerNode.swift index 43f382b3cc..fea2c0773c 100644 --- a/submodules/PasscodeUI/Sources/PasscodeEntryControllerNode.swift +++ b/submodules/PasscodeUI/Sources/PasscodeEntryControllerNode.swift @@ -94,7 +94,7 @@ final class PasscodeEntryControllerNode: ASDisplayNode { if let strongSelf = self { strongSelf.inputFieldNode.append(character) if let gradientNode = strongSelf.backgroundCustomNode as? GradientBackgroundNode { - gradientNode.animateEvent(transition: .animated(duration: 0.55, curve: .spring)) + gradientNode.animateEvent(transition: .animated(duration: 0.55, curve: .spring), extendAnimation: false, backwards: false, completion: {}) } } } @@ -102,7 +102,7 @@ final class PasscodeEntryControllerNode: ASDisplayNode { if let strongSelf = self { let _ = strongSelf.inputFieldNode.delete() if let gradientNode = strongSelf.backgroundCustomNode as? GradientBackgroundNode { - gradientNode.animateEvent(transition: .animated(duration: 0.55, curve: .spring), backwards: true) + gradientNode.animateEvent(transition: .animated(duration: 0.55, curve: .spring), extendAnimation: false, backwards: true, completion: {}) } } } @@ -167,7 +167,7 @@ final class PasscodeEntryControllerNode: ASDisplayNode { self.hapticFeedback.tap() let result = self.inputFieldNode.delete() if result, let gradientNode = self.backgroundCustomNode as? GradientBackgroundNode { - gradientNode.animateEvent(transition: .animated(duration: 0.55, curve: .spring), backwards: true) + gradientNode.animateEvent(transition: .animated(duration: 0.55, curve: .spring), extendAnimation: false, backwards: true, completion: {}) } } @@ -337,7 +337,7 @@ final class PasscodeEntryControllerNode: ASDisplayNode { if let gradientNode = self.backgroundCustomNode as? GradientBackgroundNode { gradientNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) self.backgroundDimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - gradientNode.animateEvent(transition: .animated(duration: 1.0, curve: .spring), extendAnimation: true) + gradientNode.animateEvent(transition: .animated(duration: 1.0, curve: .spring), extendAnimation: true, backwards: false, completion: {}) } } self.titleNode.setAttributedText(NSAttributedString(string: self.strings.EnterPasscode_EnterPasscode, font: titleFont, textColor: .white), animation: .none) @@ -355,7 +355,7 @@ final class PasscodeEntryControllerNode: ASDisplayNode { self.backgroundImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) if let gradientNode = self.backgroundCustomNode as? GradientBackgroundNode { gradientNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - gradientNode.animateEvent(transition: .animated(duration: 0.35, curve: .spring)) + gradientNode.animateEvent(transition: .animated(duration: 0.35, curve: .spring), extendAnimation: false, backwards: false, completion: {}) self.backgroundDimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } if !iconFrame.isEmpty { @@ -385,7 +385,7 @@ final class PasscodeEntryControllerNode: ASDisplayNode { self.subtitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) if let gradientNode = self.backgroundCustomNode as? GradientBackgroundNode { - gradientNode.animateEvent(transition: .animated(duration: 1.0, curve: .spring)) + gradientNode.animateEvent(transition: .animated(duration: 1.0, curve: .spring), extendAnimation: false, backwards: false, completion: {}) } self.inputFieldNode.animateIn() self.keyboardNode.animateIn() @@ -425,7 +425,7 @@ final class PasscodeEntryControllerNode: ASDisplayNode { self.hapticFeedback.error() if let gradientNode = self.backgroundCustomNode as? GradientBackgroundNode { - gradientNode.animateEvent(transition: .animated(duration: 1.5, curve: .spring), extendAnimation: true, backwards: true) + gradientNode.animateEvent(transition: .animated(duration: 1.5, curve: .spring), extendAnimation: true, backwards: true, completion: {}) } } @@ -440,7 +440,7 @@ final class PasscodeEntryControllerNode: ASDisplayNode { if let backgroundCustomNode = self.backgroundCustomNode { transition.updateFrame(node: backgroundCustomNode, frame: bounds) if let gradientBackgroundNode = backgroundCustomNode as? GradientBackgroundNode { - gradientBackgroundNode.updateLayout(size: bounds.size, transition: transition) + gradientBackgroundNode.updateLayout(size: bounds.size, transition: transition, extendAnimation: false, backwards: false, completion: {}) } } transition.updateFrame(view: self.effectView, frame: bounds) diff --git a/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift b/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift index 3c0b2ef2c5..a45d4764e9 100644 --- a/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift +++ b/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift @@ -62,7 +62,7 @@ private final class BubbleSettingsControllerNode: ASDisplayNode, UIScrollViewDel self.scrollNode = ASScrollNode() - self.chatBackgroundNode = createWallpaperBackgroundNode(context: context) + self.chatBackgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: false) self.chatBackgroundNode.displaysAsynchronously = false self.messagesContainerNode = ASDisplayNode() diff --git a/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift b/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift index 6db765af58..2bab48b1c3 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift @@ -132,7 +132,7 @@ class ForwardPrivacyChatPreviewItemNode: ListViewItemNode { return { item, params, neighbors in if currentBackgroundNode == nil { - currentBackgroundNode = createWallpaperBackgroundNode(context: item.context) + currentBackgroundNode = createWallpaperBackgroundNode(context: item.context, forChatDisplay: false) } currentBackgroundNode?.update(wallpaper: item.wallpaper) currentBackgroundNode?.updateBubbleTheme(bubbleTheme: item.theme, bubbleCorners: item.chatBubbleCorners) diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index ea3fb677f8..f3db9bc29d 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -74,7 +74,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView self.pageControlNode = PageControlNode(dotSpacing: 7.0, dotColor: .white, inactiveDotColor: UIColor.white.withAlphaComponent(0.4)) self.chatListBackgroundNode = ASDisplayNode() - self.chatBackgroundNode = createWallpaperBackgroundNode(context: context) + self.chatBackgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: false) self.chatBackgroundNode.displaysAsynchronously = false self.messagesContainerNode = ASDisplayNode() diff --git a/submodules/SettingsUI/Sources/Themes/SettingsThemeWallpaperNode.swift b/submodules/SettingsUI/Sources/Themes/SettingsThemeWallpaperNode.swift index 6ed9876771..a1c8bb2e8b 100644 --- a/submodules/SettingsUI/Sources/Themes/SettingsThemeWallpaperNode.swift +++ b/submodules/SettingsUI/Sources/Themes/SettingsThemeWallpaperNode.swift @@ -174,7 +174,7 @@ final class SettingsThemeWallpaperNode: ASDisplayNode { if let gradientNode = self.gradientNode { gradientNode.frame = CGRect(origin: CGPoint(), size: size) - gradientNode.updateLayout(size: size, transition: .immediate) + gradientNode.updateLayout(size: size, transition: .immediate, extendAnimation: false, backwards: false, completion: {}) } let progressDiameter: CGFloat = 50.0 diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift index 08afc2236b..ddac2cdc46 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift @@ -285,7 +285,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate self.backgroundContainerNode = ASDisplayNode() self.backgroundContainerNode.clipsToBounds = true self.backgroundWrapperNode = ASDisplayNode() - self.backgroundNode = createWallpaperBackgroundNode(context: context) + self.backgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: false) self.messagesContainerNode = ASDisplayNode() self.messagesContainerNode.clipsToBounds = true diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index 7cb2af7755..850fad8014 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -107,7 +107,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { self.messagesContainerNode.clipsToBounds = true self.messagesContainerNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) - self.instantChatBackgroundNode = createWallpaperBackgroundNode(context: context) + self.instantChatBackgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: false) self.instantChatBackgroundNode.displaysAsynchronously = false self.ready.set(.single(true)) @@ -121,7 +121,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { self.blurredNode = BlurredImageNode() self.blurredNode.blurView.contentMode = .scaleAspectFill - self.wallpaperNode = createWallpaperBackgroundNode(context: context) + self.wallpaperNode = createWallpaperBackgroundNode(context: context, forChatDisplay: false) self.toolbarNode = WallpaperGalleryToolbarNode(theme: self.previewTheme, strings: self.presentationData.strings, doneButtonType: .set) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift index 064fa3e245..bbbfedb957 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift @@ -138,7 +138,7 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode { return { item, params, neighbors in if currentBackgroundNode == nil { - currentBackgroundNode = createWallpaperBackgroundNode(context: item.context) + currentBackgroundNode = createWallpaperBackgroundNode(context: item.context, forChatDisplay: false) } currentBackgroundNode?.update(wallpaper: item.wallpaper) currentBackgroundNode?.updateBubbleTheme(bubbleTheme: item.componentTheme, bubbleCorners: item.chatBubbleCorners) diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift index 8d5a1921dc..68f9a32b3b 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift +++ b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift @@ -136,7 +136,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { self.wrapperNode = ASDisplayNode() self.imageNode = TransformImageNode() self.imageNode.contentAnimations = .subsequentUpdates - self.nativeNode = createWallpaperBackgroundNode(context: context) + self.nativeNode = createWallpaperBackgroundNode(context: context, forChatDisplay: false) self.cropNode = WallpaperCropNode() self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.6)) self.statusNode.frame = CGRect(x: 0.0, y: 0.0, width: progressDiameter, height: progressDiameter) diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index 424eb02b38..2e7780300f 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -964,7 +964,7 @@ final class MessageStoryRenderer { self.containerNode = ASDisplayNode() - self.instantChatBackgroundNode = createWallpaperBackgroundNode(context: context) + self.instantChatBackgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: false) self.instantChatBackgroundNode.displaysAsynchronously = false self.messagesContainerNode = ASDisplayNode() diff --git a/submodules/Svg/PublicHeaders/Svg/Svg.h b/submodules/Svg/PublicHeaders/Svg/Svg.h index 952fecb850..48e51ef4ff 100755 --- a/submodules/Svg/PublicHeaders/Svg/Svg.h +++ b/submodules/Svg/PublicHeaders/Svg/Svg.h @@ -7,6 +7,6 @@ NSData * _Nullable prepareSvgImage(NSData * _Nonnull data); UIImage * _Nullable renderPreparedImage(NSData * _Nonnull data, CGSize size); -UIImage * _Nullable drawSvgImage(NSData * _Nonnull data, CGSize size, UIColor * _Nullable backgroundColor, UIColor * _Nullable foregroundColor); +UIImage * _Nullable drawSvgImage(NSData * _Nonnull data, CGSize size, UIColor * _Nullable backgroundColor, UIColor * _Nullable foregroundColor, bool opaque); #endif /* Lottie_h */ diff --git a/submodules/Svg/Sources/Svg.m b/submodules/Svg/Sources/Svg.m index 2e8e07a2ee..23fd601265 100755 --- a/submodules/Svg/Sources/Svg.m +++ b/submodules/Svg/Sources/Svg.m @@ -83,7 +83,7 @@ CGSize aspectFillSize(CGSize size, CGSize bounds) { @end -UIImage * _Nullable drawSvgImage(NSData * _Nonnull data, CGSize size, UIColor *backgroundColor, UIColor *foregroundColor) { +UIImage * _Nullable drawSvgImage(NSData * _Nonnull data, CGSize size, UIColor *backgroundColor, UIColor *foregroundColor, bool opaque) { NSDate *startTime = [NSDate date]; NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data]; @@ -115,8 +115,8 @@ UIImage * _Nullable drawSvgImage(NSData * _Nonnull data, CGSize size, UIColor *b printf("parseTime = %f\n", deltaTime); startTime = [NSDate date]; - - UIGraphicsBeginImageContextWithOptions(size, true, 1.0); + + UIGraphicsBeginImageContextWithOptions(size, opaque, 1.0); CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSetFillColorWithColor(context, backgroundColor.CGColor); CGContextFillRect(context, CGRectMake(0.0f, 0.0f, size.width, size.height)); diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 9d600828e6..76a5bcdf76 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -2001,7 +2001,7 @@ private func pollChannel(network: Network, peer: Peer, state: AccountMutableStat } else { pollPts = 1 } - return (network.request(Api.functions.updates.getChannelDifference(flags: 0, channel: inputChannel, filter: .channelMessagesFilterEmpty, pts: pollPts, limit: limit)) + return (network.request(Api.functions.updates.getChannelDifference(flags: 0, channel: inputChannel, filter: .channelMessagesFilterEmpty, pts: max(pollPts, 1), limit: limit)) |> map(Optional.init) |> `catch` { error -> Signal in switch error.errorDescription { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 208e5d0d30..f4fb597d67 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -508,7 +508,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G default: break } - self.chatBackgroundNode = createWallpaperBackgroundNode(context: context, useSharedAnimationPhase: useSharedAnimationPhase) + self.chatBackgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: true, useSharedAnimationPhase: useSharedAnimationPhase, useExperimentalImplementation: self.context.sharedContext.immediateExperimentalUISettings.experimentalBackground) self.wallpaperReady.set(self.chatBackgroundNode.isReady) var locationBroadcastPanelSource: LocationBroadcastPanelSource diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index ec89174c88..3f9511ba86 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -611,7 +611,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { return } var adPeerId: PeerId? - adPeerId = nil + adPeerId = messages.first?.author?.id if strongSelf.preloadAdPeerId != adPeerId { strongSelf.preloadAdPeerId = adPeerId diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index 7268d74939..82a45ea6ee 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -99,7 +99,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { self.automaticMediaDownloadSettings = context.sharedContext.currentAutomaticMediaDownloadSettings.with { $0 } - self.backgroundNode = createWallpaperBackgroundNode(context: context) + self.backgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: true) self.backgroundNode.isUserInteractionEnabled = false self.panelBackgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.chat.inputPanel.panelBackgroundColor) diff --git a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift index 6aa915a490..0fad7a6334 100644 --- a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift +++ b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift @@ -18,7 +18,7 @@ public struct ExperimentalUISettings: Codable, Equatable { public var experimentalCompatibility: Bool public var enableDebugDataDisplay: Bool public var acceleratedStickers: Bool - public var mockICE: Bool + public var experimentalBackground: Bool public static var defaultSettings: ExperimentalUISettings { return ExperimentalUISettings( @@ -36,7 +36,7 @@ public struct ExperimentalUISettings: Codable, Equatable { experimentalCompatibility: false, enableDebugDataDisplay: false, acceleratedStickers: false, - mockICE: false + experimentalBackground: false ) } @@ -55,7 +55,7 @@ public struct ExperimentalUISettings: Codable, Equatable { experimentalCompatibility: Bool, enableDebugDataDisplay: Bool, acceleratedStickers: Bool, - mockICE: Bool + experimentalBackground: Bool ) { self.keepChatNavigationStack = keepChatNavigationStack self.skipReadHistory = skipReadHistory @@ -71,7 +71,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.experimentalCompatibility = experimentalCompatibility self.enableDebugDataDisplay = enableDebugDataDisplay self.acceleratedStickers = acceleratedStickers - self.mockICE = mockICE + self.experimentalBackground = experimentalBackground } public init(from decoder: Decoder) throws { @@ -91,7 +91,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.experimentalCompatibility = (try container.decodeIfPresent(Int32.self, forKey: "experimentalCompatibility") ?? 0) != 0 self.enableDebugDataDisplay = (try container.decodeIfPresent(Int32.self, forKey: "enableDebugDataDisplay") ?? 0) != 0 self.acceleratedStickers = (try container.decodeIfPresent(Int32.self, forKey: "acceleratedStickers") ?? 0) != 0 - self.mockICE = (try container.decodeIfPresent(Int32.self, forKey: "mockICE") ?? 0) != 0 + self.experimentalBackground = (try container.decodeIfPresent(Int32.self, forKey: "experimentalBackground") ?? 0) != 0 } public func encode(to encoder: Encoder) throws { @@ -111,7 +111,7 @@ public struct ExperimentalUISettings: Codable, Equatable { try container.encode((self.experimentalCompatibility ? 1 : 0) as Int32, forKey: "experimentalCompatibility") try container.encode((self.enableDebugDataDisplay ? 1 : 0) as Int32, forKey: "enableDebugDataDisplay") try container.encode((self.acceleratedStickers ? 1 : 0) as Int32, forKey: "acceleratedStickers") - try container.encode((self.mockICE ? 1 : 0) as Int32, forKey: "mockICE") + try container.encode((self.experimentalBackground ? 1 : 0) as Int32, forKey: "experimentalBackground") } } diff --git a/submodules/WallpaperBackgroundNode/BUILD b/submodules/WallpaperBackgroundNode/BUILD index 574f61d5db..1a0c4147ac 100644 --- a/submodules/WallpaperBackgroundNode/BUILD +++ b/submodules/WallpaperBackgroundNode/BUILD @@ -19,6 +19,9 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/WallpaperResources:WallpaperResources", "//submodules/FastBlur:FastBlur", + "//submodules/Svg:Svg", + "//submodules/GZip:GZip", + "//submodules/AppBundle:AppBundle", ], visibility = [ "//visibility:public", diff --git a/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift b/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift index 47fc43592d..453b6ca4e5 100644 --- a/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift +++ b/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift @@ -9,6 +9,9 @@ import AccountContext import SwiftSignalKit import WallpaperResources import FastBlur +import Svg +import GZip +import AppBundle private let motionAmount: CGFloat = 32.0 @@ -781,12 +784,12 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode if let gradientBackgroundNode = self.gradientBackgroundNode { transition.updateFrame(node: gradientBackgroundNode, frame: CGRect(origin: CGPoint(), size: size)) - gradientBackgroundNode.updateLayout(size: size, transition: transition) + gradientBackgroundNode.updateLayout(size: size, transition: transition, extendAnimation: false, backwards: false, completion: {}) } if let outgoingBubbleGradientBackgroundNode = self.outgoingBubbleGradientBackgroundNode { transition.updateFrame(node: outgoingBubbleGradientBackgroundNode, frame: CGRect(origin: CGPoint(), size: size)) - outgoingBubbleGradientBackgroundNode.updateLayout(size: size, transition: transition) + outgoingBubbleGradientBackgroundNode.updateLayout(size: size, transition: transition, extendAnimation: false, backwards: false, completion: {}) } self.loadPatternForSizeIfNeeded(size: size, transition: transition) @@ -797,21 +800,8 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode } func animateEvent(transition: ContainedViewLayoutTransition, extendAnimation: Bool) { - self.gradientBackgroundNode?.animateEvent(transition: transition, extendAnimation: extendAnimation) - self.outgoingBubbleGradientBackgroundNode?.animateEvent(transition: transition, extendAnimation: extendAnimation) - } - - private func updateBakedBackground() { - guard let size = self.validLayout else { - return - } - let context = DrawingContext(size: size, scale: UIScreenScale, opaque: true) - - context.withContext { context in - context.clear(CGRect(origin: CGPoint(), size: size)) - } - - self.bakedBackgroundView.image = context.generateImage() + self.gradientBackgroundNode?.animateEvent(transition: transition, extendAnimation: extendAnimation, backwards: false, completion: {}) + self.outgoingBubbleGradientBackgroundNode?.animateEvent(transition: transition, extendAnimation: extendAnimation, backwards: false, completion: {}) } func updateBubbleTheme(bubbleTheme: PresentationTheme, bubbleCorners: PresentationChatBubbleCorners) { @@ -824,7 +814,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode let outgoingBubbleGradientBackgroundNode = GradientBackgroundNode(adjustSaturation: false) if let size = self.validLayout { outgoingBubbleGradientBackgroundNode.frame = CGRect(origin: CGPoint(), size: size) - outgoingBubbleGradientBackgroundNode.updateLayout(size: size, transition: .immediate) + outgoingBubbleGradientBackgroundNode.updateLayout(size: size, transition: .immediate, extendAnimation: false, backwards: false, completion: {}) } self.outgoingBubbleGradientBackgroundNode = outgoingBubbleGradientBackgroundNode } @@ -898,32 +888,473 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode } } +private protocol WallpaperComponentView: AnyObject { + var view: UIView { get } + + func update(size: CGSize, transition: ContainedViewLayoutTransition) +} + final class WallpaperBackgroundNodeMergedImpl: ASDisplayNode, WallpaperBackgroundNode { final class SharedStorage { } - private class WallpaperComponentView: UIView { - let updated: () -> Void + final class BubbleBackgroundNodeImpl: ASDisplayNode, WallpaperBubbleBackgroundNode { + private let bubbleType: WallpaperBubbleType + private let contentNode: ASImageNode - init(updated: @escaping () -> Void) { - self.updated = updated + private var cleanWallpaperNode: ASDisplayNode? + private var gradientWallpaperNode: GradientBackgroundNode.CloneNode? + private weak var backgroundNode: WallpaperBackgroundNodeMergedImpl? + private var index: SparseBag.Index? - super.init(frame: CGRect()) + private var currentLayout: (rect: CGRect, containerSize: CGSize)? + + override var frame: CGRect { + didSet { + if oldValue.size != self.bounds.size { + self.contentNode.frame = self.bounds + if let cleanWallpaperNode = self.cleanWallpaperNode { + cleanWallpaperNode.frame = self.bounds + } + if let gradientWallpaperNode = self.gradientWallpaperNode { + gradientWallpaperNode.frame = self.bounds + } + } + } + } + + init(backgroundNode: WallpaperBackgroundNodeMergedImpl, bubbleType: WallpaperBubbleType) { + self.backgroundNode = backgroundNode + self.bubbleType = bubbleType + + self.contentNode = ASImageNode() + self.contentNode.displaysAsynchronously = false + self.contentNode.isUserInteractionEnabled = false + + super.init() + + self.addSubnode(self.contentNode) + + self.index = backgroundNode.bubbleBackgroundNodeReferences.add(BubbleBackgroundNodeReference(node: self)) + } + + deinit { + if let index = self.index, let backgroundNode = self.backgroundNode { + backgroundNode.bubbleBackgroundNodeReferences.remove(index) + } + } + + func updateContents() { + guard let backgroundNode = self.backgroundNode else { + return + } + + if let bubbleTheme = backgroundNode.bubbleTheme, let bubbleCorners = backgroundNode.bubbleCorners { + let wallpaper = backgroundNode.wallpaper ?? bubbleTheme.chat.defaultWallpaper + + let graphics = PresentationResourcesChat.principalGraphics(theme: bubbleTheme, wallpaper: wallpaper, bubbleCorners: bubbleCorners) + var needsCleanBackground = false + switch self.bubbleType { + case .incoming: + self.contentNode.image = graphics.incomingBubbleGradientImage + if graphics.incomingBubbleGradientImage == nil { + self.contentNode.backgroundColor = bubbleTheme.chat.message.incoming.bubble.withWallpaper.fill[0] + } else { + self.contentNode.backgroundColor = nil + } + needsCleanBackground = bubbleTheme.chat.message.incoming.bubble.withWallpaper.fill.contains(where: { $0.alpha <= 0.99 }) + case .outgoing: + if backgroundNode.outgoingBubbleGradientBackgroundNode != nil { + self.contentNode.image = nil + self.contentNode.backgroundColor = nil + } else { + self.contentNode.image = graphics.outgoingBubbleGradientImage + if graphics.outgoingBubbleGradientImage == nil { + self.contentNode.backgroundColor = bubbleTheme.chat.message.outgoing.bubble.withWallpaper.fill[0] + } else { + self.contentNode.backgroundColor = nil + } + needsCleanBackground = bubbleTheme.chat.message.outgoing.bubble.withWallpaper.fill.contains(where: { $0.alpha <= 0.99 }) + } + case .free: + self.contentNode.image = nil + self.contentNode.backgroundColor = nil + needsCleanBackground = true + } + + var isInvertedGradient = false + var hasComplexGradient = false + switch wallpaper { + case let .file(file): + hasComplexGradient = file.settings.colors.count >= 3 + if let intensity = file.settings.intensity, intensity < 0 { + isInvertedGradient = true + } + case let .gradient(gradient): + hasComplexGradient = gradient.colors.count >= 3 + default: + break + } + + var needsGradientBackground = false + var needsWallpaperBackground = false + + if isInvertedGradient { + switch self.bubbleType { + case .free: + needsCleanBackground = false + case .incoming, .outgoing: + break + } + } + + if needsCleanBackground { + if hasComplexGradient { + needsGradientBackground = backgroundNode.gradient != nil + } else { + needsWallpaperBackground = true + } + } + + var gradientBackgroundSource: GradientBackgroundNode? = backgroundNode.gradient?.gradientBackground + + if case .outgoing = self.bubbleType { + if let outgoingBubbleGradientBackgroundNode = backgroundNode.outgoingBubbleGradientBackgroundNode { + gradientBackgroundSource = outgoingBubbleGradientBackgroundNode + needsWallpaperBackground = false + needsGradientBackground = true + } + } + + if needsWallpaperBackground { + if self.cleanWallpaperNode == nil { + let cleanWallpaperNode = ASImageNode() + cleanWallpaperNode.displaysAsynchronously = false + self.cleanWallpaperNode = cleanWallpaperNode + cleanWallpaperNode.frame = self.bounds + self.insertSubnode(cleanWallpaperNode, at: 0) + } + if let blurredBackgroundContents = backgroundNode.blurredBackgroundContents { + self.cleanWallpaperNode?.contents = blurredBackgroundContents.cgImage + self.cleanWallpaperNode?.backgroundColor = backgroundNode.backgroundColor + } else { + self.cleanWallpaperNode?.contents = nil + self.cleanWallpaperNode?.backgroundColor = backgroundNode.backgroundColor + } + } else { + if let cleanWallpaperNode = self.cleanWallpaperNode { + self.cleanWallpaperNode = nil + cleanWallpaperNode.removeFromSupernode() + } + } + + if needsGradientBackground, let gradientBackgroundNode = gradientBackgroundSource { + if self.gradientWallpaperNode == nil { + let gradientWallpaperNode = GradientBackgroundNode.CloneNode(parentNode: gradientBackgroundNode) + gradientWallpaperNode.frame = self.bounds + self.gradientWallpaperNode = gradientWallpaperNode + self.insertSubnode(gradientWallpaperNode, at: 0) + } + } else { + if let gradientWallpaperNode = self.gradientWallpaperNode { + self.gradientWallpaperNode = nil + gradientWallpaperNode.removeFromSupernode() + } + } + } else { + self.contentNode.image = nil + if let cleanWallpaperNode = self.cleanWallpaperNode { + self.cleanWallpaperNode = nil + cleanWallpaperNode.removeFromSupernode() + } + } + + if let (rect, containerSize) = self.currentLayout { + self.update(rect: rect, within: containerSize) + } + } + + func update(rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition = .immediate) { + self.currentLayout = (rect, containerSize) + + let shiftedContentsRect = CGRect(origin: CGPoint(x: rect.minX / containerSize.width, y: rect.minY / containerSize.height), size: CGSize(width: rect.width / containerSize.width, height: rect.height / containerSize.height)) + + transition.updateFrame(layer: self.contentNode.layer, frame: self.bounds) + transition.animateView { + self.contentNode.layer.contentsRect = shiftedContentsRect + } + if let cleanWallpaperNode = self.cleanWallpaperNode { + transition.updateFrame(layer: cleanWallpaperNode.layer, frame: self.bounds) + transition.animateView { + cleanWallpaperNode.layer.contentsRect = shiftedContentsRect + } + } + if let gradientWallpaperNode = self.gradientWallpaperNode { + transition.updateFrame(layer: gradientWallpaperNode.layer, frame: self.bounds) + transition.animateView { + gradientWallpaperNode.layer.contentsRect = shiftedContentsRect + } + } + } + + func update(rect: CGRect, within containerSize: CGSize, transition: CombinedTransition) { + self.currentLayout = (rect, containerSize) + + let shiftedContentsRect = CGRect(origin: CGPoint(x: rect.minX / containerSize.width, y: rect.minY / containerSize.height), size: CGSize(width: rect.width / containerSize.width, height: rect.height / containerSize.height)) + + transition.updateFrame(layer: self.contentNode.layer, frame: self.bounds) + self.contentNode.layer.contentsRect = shiftedContentsRect + if let cleanWallpaperNode = self.cleanWallpaperNode { + transition.updateFrame(layer: cleanWallpaperNode.layer, frame: self.bounds) + cleanWallpaperNode.layer.contentsRect = shiftedContentsRect + } + if let gradientWallpaperNode = self.gradientWallpaperNode { + transition.updateFrame(layer: gradientWallpaperNode.layer, frame: self.bounds) + gradientWallpaperNode.layer.contentsRect = shiftedContentsRect + } + } + + func offset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { + guard let (_, containerSize) = self.currentLayout else { + return + } + let transition: ContainedViewLayoutTransition = .animated(duration: duration, curve: animationCurve) + + let scaledOffset = CGPoint(x: value.x / containerSize.width, y: value.y / containerSize.height) + transition.animateContentsRectPositionAdditive(layer: self.contentNode.layer, offset: scaledOffset) + + if let cleanWallpaperNode = self.cleanWallpaperNode { + transition.animateContentsRectPositionAdditive(layer: cleanWallpaperNode.layer, offset: scaledOffset) + } + if let gradientWallpaperNode = self.gradientWallpaperNode { + transition.animateContentsRectPositionAdditive(layer: gradientWallpaperNode.layer, offset: scaledOffset) + } + } + + func offsetSpring(value: CGFloat, duration: Double, damping: CGFloat) { + guard let (_, containerSize) = self.currentLayout else { + return + } + + let scaledOffset = CGPoint(x: 0.0, y: -value / containerSize.height) + + self.contentNode.layer.animateSpring(from: NSValue(cgPoint: scaledOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "contentsRect.position", duration: duration, initialVelocity: 0.0, damping: damping, additive: true) + if let cleanWallpaperNode = self.cleanWallpaperNode { + cleanWallpaperNode.layer.animateSpring(from: NSValue(cgPoint: scaledOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "contentsRect.position", duration: duration, initialVelocity: 0.0, damping: damping, additive: true) + } + if let gradientWallpaperNode = self.gradientWallpaperNode { + gradientWallpaperNode.layer.animateSpring(from: NSValue(cgPoint: scaledOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "contentsRect.position", duration: duration, initialVelocity: 0.0, damping: damping, additive: true) + } + } + } + + private final class BubbleBackgroundNodeReference { + weak var node: BubbleBackgroundNodeImpl? + + init(node: BubbleBackgroundNodeImpl) { + self.node = node + } + } + + private final class WallpaperGradiendComponentView: WallpaperComponentView { + struct Spec: Equatable { + var colors: [UInt32] + } + + let spec: Spec + let gradientBackground: GradientBackgroundNode + + var view: UIView { + return self.gradientBackground.view + } + + init(spec: Spec, updated: @escaping () -> Void) { + self.spec = spec + + self.gradientBackground = GradientBackgroundNode(colors: spec.colors.map(UIColor.init(rgb:)), useSharedAnimationPhase: true, adjustSaturation: false) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - } - private final class WallpaperGradiendComponentView: WallpaperComponentView { - struct Spec { - var colors: [UInt32] + func update(size: CGSize, transition: ContainedViewLayoutTransition) { + self.gradientBackground.frame = CGRect(origin: CGPoint(), size: size) + self.gradientBackground.updateLayout(size: size, transition: transition, extendAnimation: false, backwards: false, completion: {}) } } - private final class WallpaperPatternComponentView: WallpaperComponentView { - struct Spec { + private final class WallpaperColorComponentView: WallpaperComponentView { + struct Spec: Equatable { + var color: UInt32 + } + + let spec: Spec + let backgroundView: UIView + + var view: UIView { + return self.backgroundView + } + + init(spec: Spec, updated: @escaping () -> Void) { + self.spec = spec + + self.backgroundView = UIView() + self.backgroundView.backgroundColor = UIColor(rgb: spec.color) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(size: CGSize, transition: ContainedViewLayoutTransition) { + self.backgroundView.frame = CGRect(origin: CGPoint(), size: size) + } + } + + private final class WallpaperImageComponentView: WallpaperComponentView { + enum Spec: Equatable { + case image( + representation: TelegramMediaImageRepresentation, + isPattern: Bool, + intensity: CGFloat + ) + case builtin + } + + let spec: Spec + let updated: () -> Void + let imageView: UIImageView + var fetchDisposable: Disposable? + var dataDisposable: Disposable? + + var imageData: Data? + + private var validSize: CGSize? + + var view: UIView { + return self.imageView + } + + init(context: AccountContext, spec: Spec, updated: @escaping () -> Void) { + self.spec = spec + self.updated = updated + + self.imageView = UIImageView() + self.imageView.contentMode = .scaleAspectFill + + switch spec { + case let .image(representation, _, _): + self.fetchDisposable = (fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: MediaResourceReference.standalone(resource: representation.resource)) + |> deliverOnMainQueue).start() + self.dataDisposable = (context.account.postbox.mediaBox.resourceData(representation.resource) + |> deliverOnMainQueue).start(next: { [weak self] dataValue in + guard let strongSelf = self else { + return + } + + if dataValue.complete, let data = try? Data(contentsOf: URL(fileURLWithPath: dataValue.path)) { + strongSelf.imageData = data + if let size = strongSelf.validSize { + strongSelf.updateImage(size: size, data: data) + } + } + }) + case .builtin: + if let filePath = getAppBundle().path(forResource: "ChatWallpaperBuiltin0", ofType: "jpg"), let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) { + self.imageData = data + if let size = self.validSize { + self.updateImage(size: size, data: data) + } + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.fetchDisposable?.dispose() + self.dataDisposable?.dispose() + } + + func update(size: CGSize, transition: ContainedViewLayoutTransition) { + let sizeUpdated = self.validSize != size + self.validSize = size + + self.imageView.frame = CGRect(origin: CGPoint(), size: size) + + if sizeUpdated || self.imageView.image == nil { + if let imageData = self.imageData { + self.updateImage(size: size, data: imageData) + } + } + } + + private func updateImage(size: CGSize, data: Data) { + let scale: CGFloat + if UIScreenScale >= 2.9 { + scale = 2.5 + } else { + scale = UIScreenScale + } + + switch self.spec { + case let .image(_, isPattern, intensity): + if isPattern { + let patternBackgroundColor: UIColor + let patternForegroundColor: UIColor + if intensity < 0.0 { + patternBackgroundColor = .clear + patternForegroundColor = .black + } else { + patternBackgroundColor = .clear + patternForegroundColor = .black + } + + if let unpackedData = TGGUnzipData(data, 2 * 1024 * 1024), let patternImage = drawSvgImage(unpackedData, CGSize(width: floor(size.width * scale), height: floor(size.height * scale)), patternBackgroundColor, patternForegroundColor, false) { + if intensity < 0.0 { + self.imageView.image = generateImage(patternImage.size, scale: patternImage.scale, rotatedContext: { size, context in + context.setFillColor(UIColor.black.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + + if let cgImage = patternImage.cgImage { + context.setBlendMode(.destinationOut) + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + context.draw(cgImage, in: CGRect(origin: CGPoint(), size: size)) + } + }) + self.imageView.alpha = 1.0 + self.imageView.layer.compositingFilter = nil + self.imageView.backgroundColor = UIColor(white: 0.0, alpha: 1.0 - abs(intensity)) + } else { + self.imageView.image = patternImage + self.imageView.alpha = abs(intensity) + self.imageView.layer.compositingFilter = "softLightBlendMode" + self.imageView.backgroundColor = nil + } + } + + self.updated() + } else if let image = UIImage(data: data) { + self.imageView.image = image + self.imageView.layer.compositingFilter = nil + self.imageView.alpha = 1.0 + + self.updated() + } + case .builtin: + if let image = UIImage(data: data) { + self.imageView.image = image + self.imageView.layer.compositingFilter = nil + self.imageView.alpha = 1.0 + + self.updated() + } + } } } @@ -931,8 +1362,17 @@ final class WallpaperBackgroundNodeMergedImpl: ASDisplayNode, WallpaperBackgroun private let storage: SharedStorage private let staticView: UIImageView + private let dynamicView: UIView + private var color: WallpaperColorComponentView? private var gradient: WallpaperGradiendComponentView? - private var pattern: WallpaperPatternComponentView? + private var image: WallpaperImageComponentView? + + private var blurredBackgroundContents: UIImage? + + private var isSettingUpWallpaper: Bool = false + + private var wallpaper: TelegramWallpaper? + private var validLayout: CGSize? private let _isReady = ValuePromise(false, ignoreRepeated: true) var isReady: Signal { @@ -944,11 +1384,19 @@ final class WallpaperBackgroundNodeMergedImpl: ASDisplayNode, WallpaperBackgroun } } + private var isAnimating: Bool = false + + private var bubbleTheme: PresentationTheme? + private var bubbleCorners: PresentationChatBubbleCorners? + private var bubbleBackgroundNodeReferences = SparseBag() + private var outgoingBubbleGradientBackgroundNode: GradientBackgroundNode? + init(context: AccountContext, storage: SharedStorage?) { self.context = context self.storage = storage ?? SharedStorage() self.staticView = UIImageView() + self.dynamicView = UIView() super.init() @@ -956,70 +1404,294 @@ final class WallpaperBackgroundNodeMergedImpl: ASDisplayNode, WallpaperBackgroun } func update(wallpaper: TelegramWallpaper) { + self.wallpaper = wallpaper + + var colorSpec: WallpaperColorComponentView.Spec? var gradientSpec: WallpaperGradiendComponentView.Spec? + var imageSpec: WallpaperImageComponentView.Spec? switch wallpaper { - case let .builtin(wallpaperSettings): - let _ = wallpaperSettings + case .builtin: + imageSpec = WallpaperImageComponentView.Spec.builtin case let .color(color): - let _ = color + colorSpec = WallpaperColorComponentView.Spec(color: color) case let .gradient(gradient): if gradient.colors.count >= 3 { gradientSpec = WallpaperGradiendComponentView.Spec(colors: gradient.colors) } case let .image(representations, settings): - let _ = representations + if let representation = representations.last { + imageSpec = WallpaperImageComponentView.Spec.image(representation: representation, isPattern: false, intensity: 1.0) + } let _ = settings case let .file(file): if file.settings.colors.count >= 3 { gradientSpec = WallpaperGradiendComponentView.Spec(colors: file.settings.colors) } + if let dimensions = file.file.dimensions { + let representation = TelegramMediaImageRepresentation(dimensions: dimensions, resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: file.file.immediateThumbnailData) + imageSpec = WallpaperImageComponentView.Spec.image(representation: representation, isPattern: file.isPattern, intensity: CGFloat(file.settings.intensity ?? 100) / 100.0) + } } - if let gradientSpec = gradientSpec { - let gradient: WallpaperGradiendComponentView - if let current = self.gradient { - gradient = current - } else { - gradient = WallpaperGradiendComponentView(updated: { [weak self] in - self?.componentsUpdated() + if self.color?.spec != colorSpec { + if let color = self.color { + self.color = nil + color.view.removeFromSuperview() + } + if let colorSpec = colorSpec { + let color = WallpaperColorComponentView(spec: colorSpec, updated: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.componentsUpdated() }) + self.color = color + if let size = self.validLayout { + color.update(size: size, transition: .immediate) + } + self.dynamicView.insertSubview(color.view, at: 0) + + self.componentsUpdated() + } + } + + if self.gradient?.spec != gradientSpec { + if let gradient = self.gradient { + self.gradient = nil + gradient.view.removeFromSuperview() + } + if let gradientSpec = gradientSpec { + let gradient = WallpaperGradiendComponentView(spec: gradientSpec, updated: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.componentsUpdated() + }) + self.gradient = gradient + if let size = self.validLayout { + gradient.update(size: size, transition: .immediate) + } + self.dynamicView.insertSubview(gradient.view, at: 0) + } + } + + if self.image?.spec != imageSpec { + if let image = self.image { + self.image = nil + image.view.removeFromSuperview() + } + if let imageSpec = imageSpec { + let image = WallpaperImageComponentView(context: self.context, spec: imageSpec, updated: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.componentsUpdated() + }) + self.image = image + if let size = self.validLayout { + image.update(size: size, transition: .immediate) + } + if let gradient = self.gradient { + self.dynamicView.insertSubview(image.view, aboveSubview: gradient.view) + } else { + self.dynamicView.insertSubview(image.view, at: 0) + } } - let _ = gradient - let _ = gradientSpec - } else if let gradient = self.gradient { - self.gradient = nil - gradient.removeFromSuperview() } } private func componentsUpdated() { + if self.isAnimating { + if self.dynamicView.superview == nil { + self.view.addSubview(self.dynamicView) + self.staticView.isHidden = true + } + self._isReady.set(true) + } else { + self.staticView.isHidden = false + self.dynamicView.removeFromSuperview() + + if let size = self.validLayout { + if let color = self.color { + self.staticView.image = nil + self.staticView.backgroundColor = color.backgroundView.backgroundColor + } else { + let gradientImage = self.gradient?.gradientBackground.contentView.image + let gradientFrame = self.gradient?.gradientBackground.frame + + let imageImage = self.image?.imageView.image + let imageBackgroundColor = self.image?.imageView.backgroundColor + let imageFrame = self.image?.imageView.frame + let imageAlpha = self.image?.imageView.alpha + let imageFilter = self.image?.imageView.layer.compositingFilter as? String + + self.staticView.image = generateImage(size, opaque: true, scale: nil, rotatedContext: { size, context in + UIGraphicsPushContext(context) + + if let gradientImage = gradientImage, let gradientFrame = gradientFrame { + gradientImage.draw(in: gradientFrame) + } + + if let imageImage = imageImage, let imageFrame = imageFrame, let imageAlpha = imageAlpha { + if imageFilter == "softLightBlendMode" { + context.setBlendMode(.softLight) + } + + if let imageBackgroundColor = imageBackgroundColor { + context.setFillColor(imageBackgroundColor.cgColor) + context.fill(imageFrame) + } + + context.setAlpha(imageAlpha) + + context.translateBy(x: imageFrame.midX, y: imageFrame.midY) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -imageFrame.midX, y: -imageFrame.midY) + if let cgImage = imageImage.cgImage { + let drawingSize = imageImage.size.aspectFilled(imageFrame.size) + context.draw(cgImage, in: CGRect(origin: CGPoint(x: imageFrame.minX + (imageFrame.width - drawingSize.width) / 2.0, y: imageFrame.minX + (imageFrame.height - drawingSize.height) / 2.0), size: drawingSize)) + } + context.translateBy(x: imageFrame.midX, y: imageFrame.midY) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -imageFrame.midX, y: -imageFrame.midY) + + context.setBlendMode(.normal) + context.setAlpha(1.0) + } + + UIGraphicsPopContext() + }) + } + + self._isReady.set(true) + } + } } func _internalUpdateIsSettingUpWallpaper() { + self.isSettingUpWallpaper = true } func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = size + self.staticView.frame = CGRect(origin: CGPoint(), size: size) + + if let gradient = self.gradient { + gradient.update(size: size, transition: transition) + } + if let image = self.image { + image.update(size: size, transition: transition) + } } func animateEvent(transition: ContainedViewLayoutTransition, extendAnimation: Bool) { - + if let gradient = self.gradient { + self.isAnimating = true + self.componentsUpdated() + gradient.gradientBackground.animateEvent(transition: transition, extendAnimation: extendAnimation, backwards: false, completion: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.isAnimating = false + strongSelf.componentsUpdated() + }) + } else { + self.isAnimating = false + } } func updateBubbleTheme(bubbleTheme: PresentationTheme, bubbleCorners: PresentationChatBubbleCorners) { + if self.bubbleTheme !== bubbleTheme || self.bubbleCorners != bubbleCorners { + self.bubbleTheme = bubbleTheme + self.bubbleCorners = bubbleCorners + if bubbleTheme.chat.message.outgoing.bubble.withoutWallpaper.fill.count >= 3 && bubbleTheme.chat.animateMessageColors { + if self.outgoingBubbleGradientBackgroundNode == nil { + let outgoingBubbleGradientBackgroundNode = GradientBackgroundNode(adjustSaturation: false) + if let size = self.validLayout { + outgoingBubbleGradientBackgroundNode.frame = CGRect(origin: CGPoint(), size: size) + outgoingBubbleGradientBackgroundNode.updateLayout(size: size, transition: .immediate, extendAnimation: false, backwards: false, completion: {}) + } + self.outgoingBubbleGradientBackgroundNode = outgoingBubbleGradientBackgroundNode + } + self.outgoingBubbleGradientBackgroundNode?.updateColors(colors: bubbleTheme.chat.message.outgoing.bubble.withoutWallpaper.fill) + } else if let _ = self.outgoingBubbleGradientBackgroundNode { + self.outgoingBubbleGradientBackgroundNode = nil + } + + self.updateBubbles() + } + } + + private func updateBubbles() { + for reference in self.bubbleBackgroundNodeReferences { + reference.node?.updateContents() + } } func hasBubbleBackground(for type: WallpaperBubbleType) -> Bool { + guard let bubbleTheme = self.bubbleTheme, let bubbleCorners = self.bubbleCorners else { + return false + } + if self.wallpaper == nil && !self.isSettingUpWallpaper { + return false + } + + var hasPlainWallpaper = false + let graphicsWallpaper: TelegramWallpaper + if let wallpaper = self.wallpaper { + switch wallpaper { + case .color: + hasPlainWallpaper = true + default: + break + } + graphicsWallpaper = wallpaper + } else { + graphicsWallpaper = bubbleTheme.chat.defaultWallpaper + } + + let graphics = PresentationResourcesChat.principalGraphics(theme: bubbleTheme, wallpaper: graphicsWallpaper, bubbleCorners: bubbleCorners) + switch type { + case .incoming: + if graphics.incomingBubbleGradientImage != nil { + return true + } + if bubbleTheme.chat.message.incoming.bubble.withWallpaper.fill.contains(where: { $0.alpha <= 0.99 }) { + return !hasPlainWallpaper + } + case .outgoing: + if graphics.outgoingBubbleGradientImage != nil { + return true + } + if bubbleTheme.chat.message.outgoing.bubble.withWallpaper.fill.contains(where: { $0.alpha <= 0.99 }) { + return !hasPlainWallpaper + } + case .free: + return true + } + return false } func makeBubbleBackground(for type: WallpaperBubbleType) -> WallpaperBubbleBackgroundNode? { - return nil + if !self.hasBubbleBackground(for: type) { + return nil + } + let node = WallpaperBackgroundNodeMergedImpl.BubbleBackgroundNodeImpl(backgroundNode: self, bubbleType: type) + node.updateContents() + return node } } -public func createWallpaperBackgroundNode(context: AccountContext, useSharedAnimationPhase: Bool = false) -> WallpaperBackgroundNode { +private let sharedStorage = WallpaperBackgroundNodeMergedImpl.SharedStorage() + +public func createWallpaperBackgroundNode(context: AccountContext, forChatDisplay: Bool, useSharedAnimationPhase: Bool = false, useExperimentalImplementation: Bool = false) -> WallpaperBackgroundNode { + if forChatDisplay && useExperimentalImplementation { + return WallpaperBackgroundNodeMergedImpl(context: context, storage: useSharedAnimationPhase ? sharedStorage : nil) + } + return WallpaperBackgroundNodeImpl(context: context, useSharedAnimationPhase: useSharedAnimationPhase) } diff --git a/submodules/WallpaperResources/Sources/WallpaperResources.swift b/submodules/WallpaperResources/Sources/WallpaperResources.swift index fa925fec9b..b6e7549af1 100644 --- a/submodules/WallpaperResources/Sources/WallpaperResources.swift +++ b/submodules/WallpaperResources/Sources/WallpaperResources.swift @@ -1272,7 +1272,7 @@ public func themeImage(account: Account, accountManager: AccountManager