From deaf722d85eba4ac6851e23e10c281c81c6cc063 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 19 Oct 2023 20:44:16 +0400 Subject: [PATCH 1/2] Camera fixes --- submodules/Camera/Sources/Camera.swift | 26 +--- submodules/Camera/Sources/CameraOutput.swift | 2 +- .../Camera/Sources/CameraPreviewNode.swift | 69 ----------- .../LegacyComponents/PGCameraCaptureSession.h | 1 + .../LegacyComponents/Sources/PGCamera.m | 1 + .../Sources/PGCameraCaptureSession.m | 4 + .../QrCodeUI/Sources/QrCodeScanScreen.swift | 33 +++-- .../CameraScreen/Sources/CameraScreen.swift | 113 +++++++++++++----- .../Sources/FlashTintControlComponent.swift | 33 +++++ .../ChatMessageAttachedContentNode.swift | 10 +- 10 files changed, 154 insertions(+), 138 deletions(-) delete mode 100644 submodules/Camera/Sources/CameraPreviewNode.swift diff --git a/submodules/Camera/Sources/Camera.swift b/submodules/Camera/Sources/Camera.swift index 3de1bfe9f3..f0bf380219 100644 --- a/submodules/Camera/Sources/Camera.swift +++ b/submodules/Camera/Sources/Camera.swift @@ -119,12 +119,6 @@ private final class CameraContext { private let detectedCodesPipe = ValuePipe<[CameraCode]>() fileprivate let modeChangePromise = ValuePromise(.none) - var previewNode: CameraPreviewNode? { - didSet { - self.previewNode?.prepare() - } - } - var previewView: CameraPreviewView? var simplePreviewView: CameraSimplePreviewView? @@ -310,9 +304,7 @@ private final class CameraContext { self.mainDeviceContext?.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in guard let self, let mainDeviceContext = self.mainDeviceContext else { return - } - self.previewNode?.enqueue(sampleBuffer) - + } let timestamp = CACurrentMediaTime() if timestamp > self.lastSnapshotTimestamp + 2.5, !mainDeviceContext.output.isRecording { var front = false @@ -350,8 +342,6 @@ private final class CameraContext { guard let self, let mainDeviceContext = self.mainDeviceContext else { return } - self.previewNode?.enqueue(sampleBuffer) - let timestamp = CACurrentMediaTime() if timestamp > self.lastSnapshotTimestamp + 2.5, !mainDeviceContext.output.isRecording { var front = false @@ -814,20 +804,6 @@ public final class Camera { return disposable } } - - public func attachPreviewNode(_ node: CameraPreviewNode) { - let nodeRef: Unmanaged = Unmanaged.passRetained(node) - self.queue.async { - if let context = self.contextRef?.takeUnretainedValue() { - context.previewNode = nodeRef.takeUnretainedValue() - nodeRef.release() - } else { - Queue.mainQueue().async { - nodeRef.release() - } - } - } - } public func attachPreviewView(_ view: CameraPreviewView) { self.previewView = view diff --git a/submodules/Camera/Sources/CameraOutput.swift b/submodules/Camera/Sources/CameraOutput.swift index c0209e34c2..e312b335f7 100644 --- a/submodules/Camera/Sources/CameraOutput.swift +++ b/submodules/Camera/Sources/CameraOutput.swift @@ -245,7 +245,7 @@ final class CameraOutput: NSObject { } let settings = AVCapturePhotoSettings(format: [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32BGRA)]) - settings.flashMode = flashMode + settings.flashMode = mirror ? .off : flashMode if let previewPhotoPixelFormatType = settings.availablePreviewPhotoPixelFormatTypes.first { settings.previewPhotoFormat = [kCVPixelBufferPixelFormatTypeKey as String: previewPhotoPixelFormatType] } diff --git a/submodules/Camera/Sources/CameraPreviewNode.swift b/submodules/Camera/Sources/CameraPreviewNode.swift deleted file mode 100644 index 019e73a47e..0000000000 --- a/submodules/Camera/Sources/CameraPreviewNode.swift +++ /dev/null @@ -1,69 +0,0 @@ -import Foundation -import AsyncDisplayKit -import Display -import AVFoundation -import SwiftSignalKit - -private final class CameraPreviewNodeLayerNullAction: NSObject, CAAction { - @objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) { - } -} - -private final class CameraPreviewNodeLayer: AVSampleBufferDisplayLayer { - override func action(forKey event: String) -> CAAction? { - return CameraPreviewNodeLayerNullAction() - } -} - -public final class CameraPreviewNode: ASDisplayNode { - private var displayLayer: AVSampleBufferDisplayLayer - - private let fadeNode: ASDisplayNode - private var fadedIn = false - - public override init() { - self.displayLayer = AVSampleBufferDisplayLayer() - self.displayLayer.videoGravity = .resizeAspectFill - - self.fadeNode = ASDisplayNode() - self.fadeNode.backgroundColor = .black - self.fadeNode.isUserInteractionEnabled = false - - super.init() - - self.clipsToBounds = true - - self.layer.addSublayer(self.displayLayer) - - self.addSubnode(self.fadeNode) - } - - func prepare() { - DispatchQueue.main.async { - self.displayLayer.flushAndRemoveImage() - } - } - - func enqueue(_ sampleBuffer: CMSampleBuffer) { - self.displayLayer.enqueue(sampleBuffer) - - if !self.fadedIn { - self.fadedIn = true - Queue.mainQueue().after(0.2) { - self.fadeNode.alpha = 0.0 - self.fadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) - } - } - } - - override public func layout() { - super.layout() - - var transform = CGAffineTransform(rotationAngle: CGFloat.pi / 2.0) - transform = transform.scaledBy(x: 1.0, y: 1.0) - self.displayLayer.setAffineTransform(transform) - - self.displayLayer.frame = self.bounds - self.fadeNode.frame = self.bounds - } -} diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraCaptureSession.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraCaptureSession.h index e8854fd941..2a816abd14 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraCaptureSession.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraCaptureSession.h @@ -20,6 +20,7 @@ @property (nonatomic, assign) bool alwaysSetFlash; @property (nonatomic, assign) PGCameraMode currentMode; @property (nonatomic, assign) PGCameraFlashMode currentFlashMode; +@property (nonatomic, readonly) AVCaptureFlashMode currentDeviceFlashMode; @property (nonatomic, assign) PGCameraPosition currentCameraPosition; @property (nonatomic, readonly) PGCameraPosition preferredCameraPosition; diff --git a/submodules/LegacyComponents/Sources/PGCamera.m b/submodules/LegacyComponents/Sources/PGCamera.m index bc18a47e01..2e4348a9eb 100644 --- a/submodules/LegacyComponents/Sources/PGCamera.m +++ b/submodules/LegacyComponents/Sources/PGCamera.m @@ -391,6 +391,7 @@ NSString *const PGCameraAdjustingFocusKey = @"adjustingFocus"; _currentPhotoOrientation = orientation; AVCapturePhotoSettings *photoSettings = [AVCapturePhotoSettings photoSettings]; + photoSettings.flashMode = self.captureSession.currentDeviceFlashMode; [self.captureSession.imageOutput capturePhotoWithSettings:photoSettings delegate:self]; }]; }; diff --git a/submodules/LegacyComponents/Sources/PGCameraCaptureSession.m b/submodules/LegacyComponents/Sources/PGCameraCaptureSession.m index f34f96244a..28b7f4a23c 100644 --- a/submodules/LegacyComponents/Sources/PGCameraCaptureSession.m +++ b/submodules/LegacyComponents/Sources/PGCameraCaptureSession.m @@ -703,6 +703,10 @@ const NSInteger PGCameraFrameRate = 30; #pragma mark - Flash +- (AVCaptureFlashMode)currentDeviceFlashMode { + return [PGCameraCaptureSession _deviceFlashModeForCameraFlashMode:self.currentFlashMode]; +} + - (PGCameraFlashMode)currentFlashMode { switch (self.currentMode) diff --git a/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift b/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift index 9cfd90de3f..b5dfcbedb5 100644 --- a/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift +++ b/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift @@ -390,7 +390,7 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie private weak var controller: QrCodeScanScreen? private let subject: QrCodeScanScreen.Subject - private let previewNode: CameraPreviewNode + private let previewView: CameraSimplePreviewView private let fadeNode: ASDisplayNode private let topDimNode: ASDisplayNode private let bottomDimNode: ASDisplayNode @@ -436,8 +436,8 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie self.controller = controller self.subject = subject - self.previewNode = CameraPreviewNode() - self.previewNode.backgroundColor = .black + self.previewView = CameraSimplePreviewView(frame: .zero, main: true) + self.previewView.backgroundColor = .black self.fadeNode = ASDisplayNode() self.fadeNode.alpha = 0.0 @@ -513,7 +513,7 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie self.errorTextNode.textAlignment = .center self.errorTextNode.isHidden = true - self.camera = Camera(configuration: .init(preset: .hd1920x1080, position: .back, audio: false, photo: true, metadata: true, preferredFps: 60)) + self.camera = Camera(configuration: .init(preset: .hd1920x1080, position: .back, audio: false, photo: true, metadata: true, preferredFps: 60), previewView: self.previewView) super.init() @@ -526,7 +526,6 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie } }) - self.addSubnode(self.previewNode) self.addSubnode(self.fadeNode) self.addSubnode(self.topDimNode) self.addSubnode(self.bottomDimNode) @@ -544,6 +543,20 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie self.galleryButtonNode.addTarget(self, action: #selector(self.galleryPressed), forControlEvents: .touchUpInside) self.torchButtonNode.addTarget(self, action: #selector(self.torchPressed), forControlEvents: .touchUpInside) + + self.previewView.resetPlaceholder(front: false) + if #available(iOS 13.0, *) { + let _ = (self.previewView.isPreviewing + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak self] _ in + self?.previewView.removePlaceholder(delay: 0.15) + }) + } else { + Queue.mainQueue().after(0.35) { + self.previewView.removePlaceholder(delay: 0.15) + } + } } deinit { @@ -564,7 +577,7 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie override func didLoad() { super.didLoad() - self.camera.attachPreviewNode(self.previewNode) + self.view.insertSubview(self.previewView, at: 0) self.camera.startCapture() let throttledSignal = self.camera.detectedCodes @@ -671,14 +684,14 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie if case .tablet = layout.deviceMetrics.type { if UIDevice.current.orientation == .landscapeLeft { - self.previewNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + self.previewView.layer.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0) } else if UIDevice.current.orientation == .landscapeRight { - self.previewNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + self.previewView.layer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) } else { - self.previewNode.transform = CATransform3DIdentity + self.previewView.layer.transform = CATransform3DIdentity } } - transition.updateFrame(node: self.previewNode, frame: bounds) + transition.updateFrame(view: self.previewView, frame: bounds) transition.updateFrame(node: self.fadeNode, frame: bounds) let frameSide = max(240.0, layout.size.width - sideInset * 2.0) diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 6df09dea53..54b97bfd79 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -409,17 +409,23 @@ private final class CameraScreenComponent: CombinedComponent { controller.updateCameraState({ $0.updatedMode(mode) }, transition: .spring(duration: 0.3)) + var flashOn = controller.cameraState.flashMode == .on if case .video = mode, case .auto = controller.cameraState.flashMode { camera.setFlashMode(.on) + flashOn = true } + + self.updateScreenBrightness(flashOn: flashOn) } func toggleFlashMode() { guard let controller = self.getController(), let camera = controller.camera else { return } + var flashOn = false switch controller.cameraState.flashMode { case .off: + flashOn = true camera.setFlashMode(.on) case .on: if controller.cameraState.mode == .video { @@ -431,6 +437,8 @@ private final class CameraScreenComponent: CombinedComponent { camera.setFlashMode(.off) } self.hapticFeedback.impact(.light) + + self.updateScreenBrightness(flashOn: flashOn) } func updateFlashTint(_ tint: CameraState.FlashTint?) { @@ -441,6 +449,7 @@ private final class CameraScreenComponent: CombinedComponent { controller.updateCameraState({ $0.updatedFlashTint(tint) }, transition: .easeInOut(duration: 0.2)) } else { camera.setFlashMode(.off) + self.updateScreenBrightness(flashOn: false) } } @@ -460,6 +469,8 @@ private final class CameraScreenComponent: CombinedComponent { self.displayingFlashTint = true self.updated(transition: .immediate) + + self.updateScreenBrightness(flashOn: true) } private var lastFlipTimestamp: Double? @@ -515,7 +526,7 @@ private final class CameraScreenComponent: CombinedComponent { self.updated(transition: .easeInOut(duration: 0.2)) } - private var isTakingPhoto = false + var isTakingPhoto = false func takePhoto() { guard let controller = self.getController(), let camera = controller.camera else { return @@ -527,20 +538,47 @@ private final class CameraScreenComponent: CombinedComponent { controller.node.dismissAllTooltips() - let takePhoto = camera.takePhoto() - |> mapToSignal { value -> Signal in - switch value { - case .began: - return .single(.pendingImage) - case let .finished(image, additionalImage, _): - return .single(.image(CameraScreen.Result.Image(image: image, additionalImage: additionalImage, additionalImagePosition: .topRight))) - case .failed: - return .complete() + let takePhoto = { + let takePhoto = camera.takePhoto() + |> mapToSignal { value -> Signal in + switch value { + case .began: + return .single(.pendingImage) + case let .finished(image, additionalImage, _): + return .single(.image(CameraScreen.Result.Image(image: image, additionalImage: additionalImage, additionalImagePosition: .topRight))) + case .failed: + return .complete() + } } + self.completion.invoke(takePhoto) } - self.completion.invoke(takePhoto) - Queue.mainQueue().after(1.0) { - self.isTakingPhoto = false + + let isFrontCamera = controller.cameraState.position == .front + let isFlashOn = controller.cameraState.flashMode == .on + + if isFrontCamera && isFlashOn { + let previousBrightness = UIScreen.main.brightness + UIScreen.main.brightness = 1.0 + + let flashController = CameraFrontFlashOverlayController(color: controller.cameraState.flashTint.color) + controller.presentInGlobalOverlay(flashController) + + Queue.mainQueue().after(0.1, { + takePhoto() + + Queue.mainQueue().after(0.5, { + self.isTakingPhoto = false + + self.brightnessArguments = (CACurrentMediaTime(), 0.25, UIScreen.main.brightness, previousBrightness) + self.animateBrightnessChange() + flashController.dismissAnimated() + }) + }) + } else { + takePhoto() + Queue.mainQueue().after(1.0) { + self.isTakingPhoto = false + } } } @@ -548,10 +586,33 @@ private final class CameraScreenComponent: CombinedComponent { private var brightnessArguments: (Double, Double, CGFloat, CGFloat)? private var brightnessAnimator: ConstantDisplayLinkAnimator? - private func updateBrightness() { + func updateScreenBrightness(flashOn: Bool?) { + guard let controller = self.getController() else { + return + } + let isFrontCamera = controller.cameraState.position == .front + let isVideo = controller.cameraState.mode == .video + let isFlashOn = flashOn ?? (controller.cameraState.flashMode == .on) + + if isFrontCamera && isVideo && isFlashOn { + if self.initialBrightness == nil { + self.initialBrightness = UIScreen.main.brightness + self.brightnessArguments = (CACurrentMediaTime(), 0.2, UIScreen.main.brightness, 1.0) + self.animateBrightnessChange() + } + } else { + if let initialBrightness = self.initialBrightness { + self.initialBrightness = nil + self.brightnessArguments = (CACurrentMediaTime(), 0.2, UIScreen.main.brightness, initialBrightness) + self.animateBrightnessChange() + } + } + } + + private func animateBrightnessChange() { if self.brightnessAnimator == nil { self.brightnessAnimator = ConstantDisplayLinkAnimator(update: { [weak self] in - self?.updateBrightness() + self?.animateBrightnessChange() }) self.brightnessAnimator?.isPaused = true } @@ -601,16 +662,7 @@ private final class CameraScreenComponent: CombinedComponent { controller.updateCameraState({ $0.updatedRecording(pressing ? .holding : .handsFree).updatedDuration(0.0) }, transition: .spring(duration: 0.4)) - if case .front = controller.cameraState.position { - self.initialBrightness = UIScreen.main.brightness - UIScreen.main.brightness = 1.0 - - Queue.mainQueue().after(0.2, { - startRecording() - }) - } else { - startRecording() - } + startRecording() } func stopVideoRecording() { @@ -635,7 +687,7 @@ private final class CameraScreenComponent: CombinedComponent { if case .front = controller.cameraState.position, let initialBrightness = self.initialBrightness { self.initialBrightness = nil self.brightnessArguments = (CACurrentMediaTime(), 0.2, UIScreen.main.brightness, initialBrightness) - self.updateBrightness() + self.animateBrightnessChange() } } @@ -776,7 +828,9 @@ private final class CameraScreenComponent: CombinedComponent { .disappear(.default(alpha: true)) ) - controlsTintColor = .black + if !state.isTakingPhoto { + controlsTintColor = .black + } } let shutterState: ShutterButtonState @@ -1190,6 +1244,7 @@ private final class CameraScreenComponent: CombinedComponent { .disappear(.default(alpha: true)) ) } + return availableSize } } @@ -1564,7 +1619,7 @@ public class CameraScreen: ViewController { self.additionalPreviewView.isPreviewing ) |> filter { $0 && $1 } - |> take(1)).start(next: { [weak self] _, _ in + |> take(1)).startStandalone(next: { [weak self] _, _ in self?.mainPreviewView.removePlaceholder(delay: 0.35) self?.additionalPreviewView.removePlaceholder(delay: 0.35) }) @@ -1572,7 +1627,7 @@ public class CameraScreen: ViewController { let _ = (self.mainPreviewView.isPreviewing |> filter { $0 } |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] _ in + |> deliverOnMainQueue).startStandalone(next: { [weak self] _ in self?.mainPreviewView.removePlaceholder(delay: 0.35) }) } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/FlashTintControlComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/FlashTintControlComponent.swift index 769817e9bc..55287c4c53 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/FlashTintControlComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/FlashTintControlComponent.swift @@ -1,6 +1,7 @@ import Foundation import UIKit import Display +import AsyncDisplayKit import ComponentFlow import RoundedRectWithTailPath @@ -431,3 +432,35 @@ private final class SliderView: UIView { func performAction() { } } + +final class CameraFrontFlashOverlayController: ViewController { + class Node: ASDisplayNode { + init(color: UIColor) { + super.init() + + self.backgroundColor = color + } + } + + private let color: UIColor + init(color: UIColor) { + self.color = color + + super.init(navigationBarPresentationData: nil) + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadDisplayNode() { + self.displayNode = Node(color: self.color) + self.displayNodeDidLoad() + } + + func dismissAnimated() { + self.displayNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + self.dismiss() + }) + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift index 197276317e..e685f45bba 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift @@ -158,15 +158,17 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { } let messageTheme = incoming ? presentationData.theme.theme.chat.message.incoming : presentationData.theme.theme.chat.message.outgoing - + let author = message.author let mainColor: UIColor var secondaryColor: UIColor? if !incoming { mainColor = messageTheme.accentTextColor + if let _ = author?.nameColor?.dashColors.1 { + secondaryColor = messageTheme.accentTextColor + } } else { var authorNameColor: UIColor? - let author = message.author - if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(message.id.peerId.namespace), author?.id.namespace == Namespaces.Peer.CloudUser { +// if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(message.id.peerId.namespace), author?.id.namespace == Namespaces.Peer.CloudUser { authorNameColor = author?.nameColor?.color secondaryColor = author?.nameColor?.dashColors.1 @@ -186,7 +188,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { // authorNameColor = UIColor(hue: hue, saturation: saturation * 0.7, brightness: min(1.0, brightness * 1.2), alpha: 1.0) // } // } - } +// } if let authorNameColor { mainColor = authorNameColor From 934c6f18ae19b7a88edc634e31d22a66422a8b21 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 19 Oct 2023 21:21:01 +0400 Subject: [PATCH 2/2] Update API --- .../Sources/AccountContext.swift | 2 +- .../Sources/ChannelStatsController.swift | 18 +- .../Sources/State/ChannelBoost.swift | 546 +++++++++--------- .../SyncCore/SyncCore_Namespaces.swift | 2 +- .../Peers/TelegramEnginePeers.swift | 12 +- .../TelegramUI/Sources/OpenResolvedUrl.swift | 105 ++-- .../UrlHandling/Sources/UrlHandling.swift | 6 +- 7 files changed, 347 insertions(+), 344 deletions(-) diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 92ea81b2b9..738c33846f 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -300,7 +300,7 @@ public enum ResolvedUrl { case premiumOffer(reference: String?) case chatFolder(slug: String) case story(peerId: PeerId, id: Int32) - case boost(peerId: PeerId, status: ChannelBoostStatus?, canApplyStatus: CanApplyBoostStatus) + case boost(peerId: PeerId, status: ChannelBoostStatus?, myBoostStatus: MyBoostStatus?) case premiumGiftCode(slug: String) } diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index 7e90cd03ec..76770ac26a 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -115,7 +115,7 @@ private enum StatsEntry: ItemListNodeEntry { case boostersTitle(PresentationTheme, String) case boostersPlaceholder(PresentationTheme, String) - case booster(Int32, PresentationTheme, PresentationDateTimeFormat, EnginePeer, Int32) + case booster(Int32, PresentationTheme, PresentationDateTimeFormat, EnginePeer?, Int32) case boostersExpand(PresentationTheme, String) case boostersInfo(PresentationTheme, String) @@ -534,10 +534,16 @@ private enum StatsEntry: ItemListNodeEntry { arguments.contextAction(message.id, node, gesture) }) case let .booster(_, _, dateTimeFormat, peer, expires): - let expiresValue = stringForMediumDate(timestamp: expires, strings: presentationData.strings, dateTimeFormat: dateTimeFormat) - return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: presentationData.nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: .text(presentationData.strings.Stats_Boosts_ExpiresOn(expiresValue).string, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: peer.id != arguments.context.account.peerId, sectionId: self.section, action: { - arguments.openPeer(peer) - }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }) + let _ = dateTimeFormat + let _ = peer + let _ = expires + return ItemListTextItem(presentationData: presentationData, text: .markdown("text"), sectionId: self.section) +// let expiresValue = stringForMediumDate(timestamp: expires, strings: presentationData.strings, dateTimeFormat: dateTimeFormat) +// return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: presentationData.nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: .text(presentationData.strings.Stats_Boosts_ExpiresOn(expiresValue).string, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: peer?.id != nil && peer?.id != arguments.context.account.peerId, sectionId: self.section, action: { +// if let peer { +// arguments.openPeer(peer) +// } +// }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }) case let .boostersExpand(theme, title): return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(theme), title: title, sectionId: self.section, editing: false, action: { arguments.expandBoosters() @@ -732,7 +738,7 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p if let boostersState { var boosterIndex: Int32 = 0 - var boosters: [ChannelBoostersContext.State.Booster] = boostersState.boosters + var boosters: [ChannelBoostersContext.State.Boost] = boostersState.boosts var effectiveExpanded = state.boostersExpanded if boosters.count > maxUsersDisplayedLimit && !state.boostersExpanded { boosters = Array(boosters.prefix(Int(maxUsersDisplayedLimit))) diff --git a/submodules/TelegramCore/Sources/State/ChannelBoost.swift b/submodules/TelegramCore/Sources/State/ChannelBoost.swift index 85bf8efd56..e2faa5d636 100644 --- a/submodules/TelegramCore/Sources/State/ChannelBoost.swift +++ b/submodules/TelegramCore/Sources/State/ChannelBoost.swift @@ -3,23 +3,39 @@ import TelegramApi import Postbox import SwiftSignalKit -public final class ChannelBoostStatus: Equatable { +public struct MyBoostStatus: Equatable { + public struct Boost: Equatable { + public let slot: Int32 + public let peer: EnginePeer? + public let date: Int32 + public let expires: Int32 + public let cooldownUntil: Int32? + } + + public let boosts: [Boost] +} + +public struct ChannelBoostStatus: Equatable { public let level: Int public let boosts: Int + public let giftBoosts: Int? public let currentLevelBoosts: Int public let nextLevelBoosts: Int? public let premiumAudience: StatsPercentValue? public let url: String public let prepaidGiveaways: [PrepaidGiveaway] + public let boostedByMe: Bool - public init(level: Int, boosts: Int, currentLevelBoosts: Int, nextLevelBoosts: Int?, premiumAudience: StatsPercentValue?, url: String, prepaidGiveaways: [PrepaidGiveaway]) { + public init(level: Int, boosts: Int, giftBoosts: Int?, currentLevelBoosts: Int, nextLevelBoosts: Int?, premiumAudience: StatsPercentValue?, url: String, prepaidGiveaways: [PrepaidGiveaway], boostedByMe: Bool) { self.level = level self.boosts = boosts + self.giftBoosts = giftBoosts self.currentLevelBoosts = currentLevelBoosts self.nextLevelBoosts = nextLevelBoosts self.premiumAudience = premiumAudience self.url = url self.prepaidGiveaways = prepaidGiveaways + self.boostedByMe = boostedByMe } public static func ==(lhs: ChannelBoostStatus, rhs: ChannelBoostStatus) -> Bool { @@ -29,6 +45,9 @@ public final class ChannelBoostStatus: Equatable { if lhs.boosts != rhs.boosts { return false } + if lhs.giftBoosts != rhs.giftBoosts { + return false + } if lhs.currentLevelBoosts != rhs.currentLevelBoosts { return false } @@ -44,133 +63,96 @@ public final class ChannelBoostStatus: Equatable { if lhs.prepaidGiveaways != rhs.prepaidGiveaways { return false } + if lhs.boostedByMe != rhs.boostedByMe { + return false + } return true } } func _internal_getChannelBoostStatus(account: Account, peerId: PeerId) -> Signal { - return .single(ChannelBoostStatus(level: 0, boosts: 0, currentLevelBoosts: 0, nextLevelBoosts: nil, premiumAudience: nil, url: "", prepaidGiveaways: [])) -// return account.postbox.transaction { transaction -> Api.InputPeer? in -// return transaction.getPeer(peerId).flatMap(apiInputPeer) -// } -// |> mapToSignal { inputPeer -> Signal in -// guard let inputPeer = inputPeer else { -// return .single(nil) -// } -// return account.network.request(Api.functions.stories.getBoostsStatus(peer: inputPeer)) -// |> map(Optional.init) -// |> `catch` { _ -> Signal in -// return .single(nil) -// } -// |> map { result -> ChannelBoostStatus? in -// guard let result = result else { -// return nil -// } -// -// switch result { -// case let .boostsStatus(_, level, currentLevelBoosts, boosts, nextLevelBoosts, premiumAudience, url, prepaidGiveaways): -// return ChannelBoostStatus(level: Int(level), boosts: Int(boosts), currentLevelBoosts: Int(currentLevelBoosts), nextLevelBoosts: nextLevelBoosts.flatMap(Int.init), premiumAudience: premiumAudience.flatMap({ StatsPercentValue(apiPercentValue: $0) }), url: url, prepaidGiveaways: prepaidGiveaways?.map({ PrepaidGiveaway(apiPrepaidGiveaway: $0) }) ?? []) -// } -// } -// } -} - -public enum CanApplyBoostStatus { - public enum ErrorReason { - case generic - case premiumRequired - case floodWait(Int32) - case peerBoostAlreadyActive - case giftedPremiumNotAllowed + return account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(peerId).flatMap(apiInputPeer) + } + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer = inputPeer else { + return .single(nil) + } + return account.network.request(Api.functions.premium.getBoostsStatus(peer: inputPeer)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> map { result -> ChannelBoostStatus? in + guard let result = result else { + return nil + } + switch result { + case let .boostsStatus(flags, level, currentLevelBoosts, boosts, giftBoosts, nextLevelBoosts, premiumAudience, boostUrl, prepaidGiveaways, myBoostSlots): + let _ = myBoostSlots + return ChannelBoostStatus(level: Int(level), boosts: Int(boosts), giftBoosts: giftBoosts.flatMap(Int.init), currentLevelBoosts: Int(currentLevelBoosts), nextLevelBoosts: nextLevelBoosts.flatMap(Int.init), premiumAudience: premiumAudience.flatMap({ StatsPercentValue(apiPercentValue: $0) }), url: boostUrl, prepaidGiveaways: prepaidGiveaways?.map({ PrepaidGiveaway(apiPrepaidGiveaway: $0) }) ?? [], boostedByMe: (flags & (1 << 2)) != 0) + } + } } - - case ok - case replace(currentBoost: EnginePeer) - case error(ErrorReason) } -func _internal_canApplyChannelBoost(account: Account, peerId: PeerId) -> Signal { - return .single(.error(.generic)) -// return account.postbox.transaction { transaction -> Api.InputPeer? in -// return transaction.getPeer(peerId).flatMap(apiInputPeer) -// } -// |> mapToSignal { inputPeer -> Signal in -// guard let inputPeer = inputPeer else { -// return .single(.error(.generic)) -// } -// return account.network.request(Api.functions.stories.canApplyBoost(peer: inputPeer), automaticFloodWait: false) -// |> map { result -> (Api.stories.CanApplyBoostResult?, CanApplyBoostStatus.ErrorReason?) in -// return (result, nil) -// } -// |> `catch` { error -> Signal<(Api.stories.CanApplyBoostResult?, CanApplyBoostStatus.ErrorReason?), NoError> in -// let reason: CanApplyBoostStatus.ErrorReason -// if error.errorDescription == "PREMIUM_ACCOUNT_REQUIRED" { -// reason = .premiumRequired -// } else if error.errorDescription.hasPrefix("FLOOD_WAIT_") { -// let errorText = error.errorDescription ?? "" -// if let underscoreIndex = errorText.lastIndex(of: "_") { -// let timeoutText = errorText[errorText.index(after: underscoreIndex)...] -// if let timeoutValue = Int32(String(timeoutText)) { -// reason = .floodWait(timeoutValue) -// } else { -// reason = .generic -// } -// } else { -// reason = .generic -// } -// } else if error.errorDescription == "SAME_BOOST_ALREADY_ACTIVE" || error.errorDescription == "BOOST_NOT_MODIFIED" { -// reason = .peerBoostAlreadyActive -// } else if error.errorDescription == "PREMIUM_GIFTED_NOT_ALLOWED" { -// reason = .giftedPremiumNotAllowed -// } else { -// reason = .generic -// } -// -// return .single((nil, reason)) -// } -// |> mapToSignal { result, errorReason -> Signal in -// guard let result = result else { -// return .single(.error(errorReason ?? .generic)) -// } -// -// return account.postbox.transaction { transaction -> CanApplyBoostStatus in -// switch result { -// case .canApplyBoostOk: -// return .ok -// case let .canApplyBoostReplace(currentBoost, chats): -// updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: AccumulatedPeers(transaction: transaction, chats: chats, users: [])) -// -// if let peer = transaction.getPeer(currentBoost.peerId) { -// return .replace(currentBoost: EnginePeer(peer)) -// } else { -// return .error(.generic) -// } -// } -// } -// } -// } +func _internal_applyChannelBoost(account: Account, peerId: PeerId, slots: [Int32]) -> Signal { + return account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(peerId).flatMap(apiInputPeer) + } + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer = inputPeer else { + return .single(false) + } + var flags: Int32 = 0 + if !slots.isEmpty { + flags |= (1 << 0) + } + + return account.network.request(Api.functions.premium.applyBoost(flags: flags, slots: !slots.isEmpty ? slots : nil, peer: inputPeer)) + |> `catch` { error -> Signal in + return .single(.boolFalse) + } + |> map { result -> Bool in + if case .boolTrue = result { + return true + } + return false + } + } } -func _internal_applyChannelBoost(account: Account, peerId: PeerId) -> Signal { - return .single(false) -// return account.postbox.transaction { transaction -> Api.InputPeer? in -// return transaction.getPeer(peerId).flatMap(apiInputPeer) -// } -// |> mapToSignal { inputPeer -> Signal in -// guard let inputPeer = inputPeer else { -// return .single(false) -// } -// return account.network.request(Api.functions.stories.applyBoost(peer: inputPeer)) -// |> `catch` { error -> Signal in -// return .single(.boolFalse) -// } -// |> map { result -> Bool in -// if case .boolTrue = result { -// return true -// } -// return false -// } -// } +func _internal_getMyBoostStatus(account: Account) -> Signal { + return account.network.request(Api.functions.premium.getMyBoosts()) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + guard let result = result else { + return .single(nil) + } + return account.postbox.transaction { transaction -> MyBoostStatus? in + var boostsResult: [MyBoostStatus.Boost] = [] + switch result { + case let .myBoosts(myBoosts, chats, users): + let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users) + updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: parsedPeers) + for boost in myBoosts { + let _ = boost + switch boost { + case let .myBoost(_, slot, peer, date, expires, cooldownUntilDate): + var boostPeer: EnginePeer? + if let peerId = peer?.peerId, let peer = transaction.getPeer(peerId) { + boostPeer = EnginePeer(peer) + } + boostsResult.append(MyBoostStatus.Boost(slot: slot, peer: boostPeer, date: date, expires: expires, cooldownUntil: cooldownUntilDate)) + } + } + } + return MyBoostStatus(boosts: boostsResult) + } + } } private final class ChannelBoostersContextImpl { @@ -183,7 +165,7 @@ private final class ChannelBoostersContextImpl { private var hasLoadedOnce: Bool = false private var canLoadMore: Bool = true private var loadedFromCache = false - private var results: [ChannelBoostersContext.State.Booster] = [] + private var results: [ChannelBoostersContext.State.Boost] = [] private var count: Int32 private var lastOffset: String? private var populateCache: Bool = true @@ -198,16 +180,13 @@ private final class ChannelBoostersContextImpl { self.count = 0 self.isLoadingMore = true - self.disposable.set((account.postbox.transaction { transaction -> (peers: [ChannelBoostersContext.State.Booster], count: Int32, canLoadMore: Bool)? in - let cachedResult = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedChannelBoosters, key: CachedChannelBoosters.key(peerId: peerId)))?.get(CachedChannelBoosters.self) + self.disposable.set((account.postbox.transaction { transaction -> (peers: [ChannelBoostersContext.State.Boost], count: Int32, canLoadMore: Bool)? in + let cachedResult = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedChannelBoosts, key: CachedChannelBoosters.key(peerId: peerId)))?.get(CachedChannelBoosters.self) if let cachedResult = cachedResult { - var result: [ChannelBoostersContext.State.Booster] = [] - for peerId in cachedResult.peerIds { - if let peer = transaction.getPeer(peerId), let expires = cachedResult.dates[peerId] { - result.append(ChannelBoostersContext.State.Booster(peer: EnginePeer(peer), expires: expires)) - } else { - return nil - } + var result: [ChannelBoostersContext.State.Boost] = [] + for boost in cachedResult.boosts { + let peer = boost.peerId.flatMap { transaction.getPeer($0) } + result.append(ChannelBoostersContext.State.Boost(flags: ChannelBoostersContext.State.Boost.Flags(rawValue: boost.flags), id: boost.id, peer: peer.flatMap { EnginePeer($0) }, date: boost.date, expires: boost.expires)) } return (result, cachedResult.count, true) } else { @@ -243,95 +222,105 @@ private final class ChannelBoostersContextImpl { } func loadMore() { -// if self.isLoadingMore { -// return -// } -// self.isLoadingMore = true -// let account = self.account -// let accountPeerId = account.peerId -// let peerId = self.peerId -// let populateCache = self.populateCache -// -// if self.loadedFromCache { -// self.loadedFromCache = false -// } -// let lastOffset = self.lastOffset -// -// self.disposable.set((self.account.postbox.transaction { transaction -> Api.InputPeer? in -// return transaction.getPeer(peerId).flatMap(apiInputPeer) -// } -// |> mapToSignal { inputPeer -> Signal<([ChannelBoostersContext.State.Booster], Int32, String?), NoError> in -// if let inputPeer = inputPeer { -// let offset = lastOffset ?? "" -// let limit: Int32 = lastOffset == nil ? 25 : 50 -// -// let signal = account.network.request(Api.functions.stories.getBoostersList(peer: inputPeer, offset: offset, limit: limit)) -// |> map(Optional.init) -// |> `catch` { _ -> Signal in -// return .single(nil) -// } -// |> mapToSignal { result -> Signal<([ChannelBoostersContext.State.Booster], Int32, String?), NoError> in -// return account.postbox.transaction { transaction -> ([ChannelBoostersContext.State.Booster], Int32, String?) in -// guard let result = result else { -// return ([], 0, nil) -// } -// switch result { -// case let .boostersList(_, count, boosters, nextOffset, users): -// updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users)) -// var resultBoosters: [ChannelBoostersContext.State.Booster] = [] -// for booster in boosters { -// let peerId: EnginePeer.Id -// let expires: Int32 -// switch booster { -// case let .booster(userId, expiresValue): -// peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) -// expires = expiresValue -// } -// if let peer = transaction.getPeer(peerId) { -// resultBoosters.append(ChannelBoostersContext.State.Booster(peer: EnginePeer(peer), expires: expires)) -// } -// } -// if populateCache { -// if let entry = CodableEntry(CachedChannelBoosters(boosters: resultBoosters, count: count)) { -// transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedChannelBoosters, key: CachedChannelBoosters.key(peerId: peerId)), entry: entry) -// } -// } -// return (resultBoosters, count, nextOffset) -// } -// } -// } -// return signal -// } else { -// return .single(([], 0, nil)) -// } -// } -// |> deliverOn(self.queue)).start(next: { [weak self] boosters, updatedCount, nextOffset in -// guard let strongSelf = self else { -// return -// } -// strongSelf.lastOffset = nextOffset -// if strongSelf.populateCache { -// strongSelf.populateCache = false -// strongSelf.results.removeAll() -// } -// var existingIds = Set(strongSelf.results.map { $0.peer.id }) -// for booster in boosters { -// if !existingIds.contains(booster.peer.id) { -// strongSelf.results.append(booster) -// existingIds.insert(booster.peer.id) -// } -// } -// strongSelf.isLoadingMore = false -// strongSelf.hasLoadedOnce = true -// strongSelf.canLoadMore = !boosters.isEmpty -// if strongSelf.canLoadMore { -// strongSelf.count = max(updatedCount, Int32(strongSelf.results.count)) -// } else { -// strongSelf.count = Int32(strongSelf.results.count) -// } -// strongSelf.updateState() -// })) -// self.updateState() + if self.isLoadingMore { + return + } + self.isLoadingMore = true + let account = self.account + let accountPeerId = account.peerId + let peerId = self.peerId + let populateCache = self.populateCache + + if self.loadedFromCache { + self.loadedFromCache = false + } + let lastOffset = self.lastOffset + + self.disposable.set((self.account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(peerId).flatMap(apiInputPeer) + } + |> mapToSignal { inputPeer -> Signal<([ChannelBoostersContext.State.Boost], Int32, String?), NoError> in + if let inputPeer = inputPeer { + let offset = lastOffset ?? "" + let limit: Int32 = lastOffset == nil ? 25 : 50 + + let flags: Int32 = 0 + let signal = account.network.request(Api.functions.premium.getBoostsList(flags: flags, peer: inputPeer, offset: offset, limit: limit)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal<([ChannelBoostersContext.State.Boost], Int32, String?), NoError> in + return account.postbox.transaction { transaction -> ([ChannelBoostersContext.State.Boost], Int32, String?) in + guard let result = result else { + return ([], 0, nil) + } + switch result { + case let .boostsList(_, count, boosts, nextOffset, users): + updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users)) + var resultBoosts: [ChannelBoostersContext.State.Boost] = [] + for boost in boosts { + switch boost { + case let .boost(flags, id, userId, giveawayMessageId, date, expires, usedGiftSlug): + let _ = giveawayMessageId + let _ = usedGiftSlug + var boostFlags: ChannelBoostersContext.State.Boost.Flags = [] + var boostPeer: EnginePeer? + if let userId = userId { + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) + if let peer = transaction.getPeer(peerId) { + boostPeer = EnginePeer(peer) + } + } + if (flags & (1 << 1)) != 0 { + boostFlags.insert(.isGift) + } + if (flags & (1 << 2)) != 0 { + boostFlags.insert(.isGiveaway) + } + if (flags & (1 << 3)) != 0 { + boostFlags.insert(.isUnclaimed) + } + resultBoosts.append(ChannelBoostersContext.State.Boost(flags: boostFlags, id: id, peer: boostPeer, date: date, expires: expires)) + } + } + if populateCache { + if let entry = CodableEntry(CachedChannelBoosters(boosts: resultBoosts, count: count)) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedChannelBoosts, key: CachedChannelBoosters.key(peerId: peerId)), entry: entry) + } + } + return (resultBoosts, count, nextOffset) + } + } + } + return signal + } else { + return .single(([], 0, nil)) + } + } + |> deliverOn(self.queue)).start(next: { [weak self] boosters, updatedCount, nextOffset in + guard let strongSelf = self else { + return + } + strongSelf.lastOffset = nextOffset + if strongSelf.populateCache { + strongSelf.populateCache = false + strongSelf.results.removeAll() + } + for booster in boosters { + strongSelf.results.append(booster) + } + strongSelf.isLoadingMore = false + strongSelf.hasLoadedOnce = true + strongSelf.canLoadMore = !boosters.isEmpty + if strongSelf.canLoadMore { + strongSelf.count = max(updatedCount, Int32(strongSelf.results.count)) + } else { + strongSelf.count = Int32(strongSelf.results.count) + } + strongSelf.updateState() + })) + self.updateState() } private func updateCache() { @@ -340,34 +329,49 @@ private final class ChannelBoostersContextImpl { } let peerId = self.peerId - let resultBoosters = Array(self.results.prefix(50)) + let resultBoosts = Array(self.results.prefix(50)) let count = self.count self.updateDisposables.add(self.account.postbox.transaction({ transaction in - if let entry = CodableEntry(CachedChannelBoosters(boosters: resultBoosters, count: count)) { - transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedChannelBoosters, key: CachedChannelBoosters.key(peerId: peerId)), entry: entry) + if let entry = CodableEntry(CachedChannelBoosters(boosts: resultBoosts, count: count)) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedChannelBoosts, key: CachedChannelBoosters.key(peerId: peerId)), entry: entry) } }).start()) } private func updateState() { - self.state.set(.single(ChannelBoostersContext.State(boosters: self.results, isLoadingMore: self.isLoadingMore, hasLoadedOnce: self.hasLoadedOnce, canLoadMore: self.canLoadMore, count: self.count))) + self.state.set(.single(ChannelBoostersContext.State(boosts: self.results, isLoadingMore: self.isLoadingMore, hasLoadedOnce: self.hasLoadedOnce, canLoadMore: self.canLoadMore, count: self.count))) } } public final class ChannelBoostersContext { public struct State: Equatable { - public struct Booster: Equatable { - public var peer: EnginePeer + public struct Boost: Equatable { + public struct Flags: OptionSet { + public var rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public static let isGift = Flags(rawValue: 1 << 0) + public static let isGiveaway = Flags(rawValue: 1 << 1) + public static let isUnclaimed = Flags(rawValue: 1 << 2) + } + + public var flags: Flags + public var id: String + public var peer: EnginePeer? + public var date: Int32 public var expires: Int32 } - public var boosters: [Booster] + public var boosts: [Boost] public var isLoadingMore: Bool public var hasLoadedOnce: Bool public var canLoadMore: Bool public var count: Int32 - public static var Empty = State(boosters: [], isLoadingMore: false, hasLoadedOnce: true, canLoadMore: false, count: 0) - public static var Loading = State(boosters: [], isLoadingMore: false, hasLoadedOnce: false, canLoadMore: false, count: 0) + public static var Empty = State(boosts: [], isLoadingMore: false, hasLoadedOnce: true, canLoadMore: false, count: 0) + public static var Loading = State(boosts: [], isLoadingMore: false, hasLoadedOnce: false, canLoadMore: false, count: 0) } @@ -408,38 +412,56 @@ public final class ChannelBoostersContext { private final class CachedChannelBoosters: Codable { private enum CodingKeys: String, CodingKey { - case peerIds - case expires + case boosts case count } - private struct DictionaryPair: Codable, Hashable { - var key: Int64 - var value: String + fileprivate struct CachedBoost: Codable, Hashable { + private enum CodingKeys: String, CodingKey { + case flags + case id + case peerId + case date + case expires + } - init(_ key: Int64, value: String) { - self.key = key - self.value = value + var flags: Int32 + var id: String + var peerId: EnginePeer.Id? + var date: Int32 + var expires: Int32 + + init(flags: Int32, id: String, peerId: EnginePeer.Id?, date: Int32, expires: Int32) { + self.flags = flags + self.id = id + self.peerId = peerId + self.date = date + self.expires = expires } init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: StringCodingKey.self) + let container = try decoder.container(keyedBy: CodingKeys.self) - self.key = try container.decode(Int64.self, forKey: "k") - self.value = try container.decode(String.self, forKey: "v") + self.flags = try container.decode(Int32.self, forKey: .flags) + self.id = try container.decode(String.self, forKey: .id) + self.peerId = try container.decodeIfPresent(Int64.self, forKey: .peerId).flatMap { EnginePeer.Id($0) } + self.date = try container.decode(Int32.self, forKey: .date) + self.expires = try container.decode(Int32.self, forKey: .expires) } func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: StringCodingKey.self) + var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(self.key, forKey: "k") - try container.encode(self.value, forKey: "v") + try container.encode(self.flags, forKey: .flags) + try container.encode(self.id, forKey: .id) + try container.encodeIfPresent(self.peerId?.toInt64(), forKey: .peerId) + try container.encode(self.date, forKey: .date) + try container.encode(self.expires, forKey: .expires) } } - let peerIds: [EnginePeer.Id] - let dates: [EnginePeer.Id: Int32] - let count: Int32 + fileprivate let boosts: [CachedBoost] + fileprivate let count: Int32 static func key(peerId: EnginePeer.Id) -> ValueBoxKey { let key = ValueBoxKey(length: 8) @@ -447,50 +469,22 @@ private final class CachedChannelBoosters: Codable { return key } - init(boosters: [ChannelBoostersContext.State.Booster], count: Int32) { - self.peerIds = boosters.map { $0.peer.id } - self.dates = boosters.reduce(into: [EnginePeer.Id: Int32]()) { - $0[$1.peer.id] = $1.expires - } - self.count = count - } - - init(peerIds: [PeerId], dates: [PeerId: Int32], count: Int32) { - self.peerIds = peerIds - self.dates = dates + init(boosts: [ChannelBoostersContext.State.Boost], count: Int32) { + self.boosts = boosts.map { CachedBoost(flags: $0.flags.rawValue, id: $0.id, peerId: $0.peer?.id, date: $0.date, expires: $0.expires) } self.count = count } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - self.peerIds = (try container.decode([Int64].self, forKey: .peerIds)).map(EnginePeer.Id.init) - - var dates: [EnginePeer.Id: Int32] = [:] - let datesArray = try container.decode([Int64].self, forKey: .expires) - for index in stride(from: 0, to: datesArray.endIndex, by: 2) { - let userId = datesArray[index] - let date = datesArray[index + 1] - let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) - dates[peerId] = Int32(clamping: date) - } - self.dates = dates - + self.boosts = (try container.decode([CachedBoost].self, forKey: .boosts)) self.count = try container.decode(Int32.self, forKey: .count) } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(self.peerIds.map { $0.toInt64() }, forKey: .peerIds) - - var dates: [Int64] = [] - for (peerId, date) in self.dates { - dates.append(peerId.id._internalGetInt64Value()) - dates.append(Int64(date)) - } - - try container.encode(dates, forKey: .expires) + try container.encode(self.boosts, forKey: .boosts) try container.encode(self.count, forKey: .count) } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index a09fe8fc2a..08fc704269 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -111,7 +111,7 @@ public struct Namespaces { public static let cachedPeerStoryListHeads: Int8 = 27 public static let displayedStoryNotifications: Int8 = 28 public static let storySendAsPeerIds: Int8 = 29 - public static let cachedChannelBoosters: Int8 = 30 + public static let cachedChannelBoosts: Int8 = 31 } public struct UnorderedItemList { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 343dc2d0f7..0f759e5fec 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -1196,13 +1196,13 @@ public extension TelegramEngine { public func getChannelBoostStatus(peerId: EnginePeer.Id) -> Signal { return _internal_getChannelBoostStatus(account: self.account, peerId: peerId) } - - public func canApplyChannelBoost(peerId: EnginePeer.Id) -> Signal { - return _internal_canApplyChannelBoost(account: self.account, peerId: peerId) - } - public func applyChannelBoost(peerId: EnginePeer.Id) -> Signal { - return _internal_applyChannelBoost(account: self.account, peerId: peerId) + public func getMyBoostStatus() -> Signal { + return _internal_getMyBoostStatus(account: self.account) + } + + public func applyChannelBoost(peerId: EnginePeer.Id, slots: [Int32]) -> Signal { + return _internal_applyChannelBoost(account: self.account, peerId: peerId, slots: slots) } } } diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 28ee088d69..976efdb1ac 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -835,7 +835,8 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur }), nil) } }) - case let .boost(peerId, status, canApplyStatus): + case let .boost(peerId, status, myBoostsStatus): + let _ = myBoostsStatus var forceDark = false if let updatedPresentationData, updatedPresentationData.initial.theme.overallDarkAppearance { forceDark = true @@ -847,7 +848,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur } var isBoosted = false - if case let .error(error) = canApplyStatus, case .peerBoostAlreadyActive = error { + if status.boostedByMe { isBoosted = true } @@ -876,54 +877,56 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur if isBoosted { return true } - var dismiss = false - switch canApplyStatus { - case .ok: - updateImpl?() - case let .replace(previousPeer): - let controller = replaceBoostConfirmationController(context: context, fromPeers: [previousPeer], toPeer: peer, commit: { - updateImpl?() - }) - present(controller, nil) - case let .error(error): - let title: String? - let text: String - - var actions: [TextAlertAction] = [ - TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {}) - ] - - switch error { - case .generic: - title = nil - text = presentationData.strings.Login_UnknownError - case let .floodWait(timeout): - title = presentationData.strings.ChannelBoost_Error_BoostTooOftenTitle - let valueText = timeIntervalString(strings: presentationData.strings, value: timeout, usage: .afterTime, preferLowerValue: false) - text = presentationData.strings.ChannelBoost_Error_BoostTooOftenText(valueText).string - dismiss = true - case .premiumRequired: - title = presentationData.strings.ChannelBoost_Error_PremiumNeededTitle - text = presentationData.strings.ChannelBoost_Error_PremiumNeededText - actions = [ - TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), - TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Yes, action: { - dismissImpl?() - let controller = context.sharedContext.makePremiumIntroController(context: context, source: .channelBoost(peerId), forceDark: false, dismissed: nil) - navigationController?.pushViewController(controller) - }) - ] - case .giftedPremiumNotAllowed: - title = presentationData.strings.ChannelBoost_Error_GiftedPremiumNotAllowedTitle - text = presentationData.strings.ChannelBoost_Error_GiftedPremiumNotAllowedText - dismiss = true - case .peerBoostAlreadyActive: - return true - } - - let controller = textAlertController(sharedContext: context.sharedContext, updatedPresentationData: updatedPresentationData, title: title, text: text, actions: actions, parseMarkdown: true) - present(controller, nil) - } + let dismiss = false + updateImpl?() + +// switch canApplyStatus { +// case .ok: +// updateImpl?() +// case let .replace(previousPeer): +// let controller = replaceBoostConfirmationController(context: context, fromPeers: [previousPeer], toPeer: peer, commit: { +// updateImpl?() +// }) +// present(controller, nil) +// case let .error(error): +// let title: String? +// let text: String +// +// var actions: [TextAlertAction] = [ +// TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {}) +// ] +// +// switch error { +// case .generic: +// title = nil +// text = presentationData.strings.Login_UnknownError +// case let .floodWait(timeout): +// title = presentationData.strings.ChannelBoost_Error_BoostTooOftenTitle +// let valueText = timeIntervalString(strings: presentationData.strings, value: timeout, usage: .afterTime, preferLowerValue: false) +// text = presentationData.strings.ChannelBoost_Error_BoostTooOftenText(valueText).string +// dismiss = true +// case .premiumRequired: +// title = presentationData.strings.ChannelBoost_Error_PremiumNeededTitle +// text = presentationData.strings.ChannelBoost_Error_PremiumNeededText +// actions = [ +// TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), +// TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Yes, action: { +// dismissImpl?() +// let controller = context.sharedContext.makePremiumIntroController(context: context, source: .channelBoost(peerId), forceDark: false, dismissed: nil) +// navigationController?.pushViewController(controller) +// }) +// ] +// case .giftedPremiumNotAllowed: +// title = presentationData.strings.ChannelBoost_Error_GiftedPremiumNotAllowedTitle +// text = presentationData.strings.ChannelBoost_Error_GiftedPremiumNotAllowedText +// dismiss = true +// case .peerBoostAlreadyActive: +// return true +// } +// +// let controller = textAlertController(sharedContext: context.sharedContext, updatedPresentationData: updatedPresentationData, title: title, text: text, actions: actions, parseMarkdown: true) +// present(controller, nil) +// } return dismiss }, openPeer: { peer in @@ -942,7 +945,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur updateImpl = { [weak controller] in if let _ = status.nextLevelBoosts { - let _ = context.engine.peers.applyChannelBoost(peerId: peerId).startStandalone() + let _ = context.engine.peers.applyChannelBoost(peerId: peerId, slots: []).startStandalone() controller?.updateSubject(nextSubject, count: nextCount) } else { dismissImpl?() diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index ee5db0ba6e..a069796020 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -739,10 +739,10 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) case .boost: return combineLatest( context.engine.peers.getChannelBoostStatus(peerId: peer.id), - context.engine.peers.canApplyChannelBoost(peerId: peer.id) + context.engine.peers.getMyBoostStatus() ) - |> map { boostStatus, canApplyStatus -> ResolvedUrl? in - return .boost(peerId: peer.id, status: boostStatus, canApplyStatus: canApplyStatus) + |> map { boostStatus, myBoostStatus -> ResolvedUrl? in + return .boost(peerId: peer.id, status: boostStatus, myBoostStatus: myBoostStatus) } } } else {