diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift index ae27df6f66..191f9b27c8 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift @@ -611,7 +611,7 @@ private final class ActionButtonPanelNode: ASDisplayNode { private(set) var isAccepted: Bool = false var isAcceptedUpdated: (() -> Void)? var openRecurrentTerms: (() -> Void)? - private var recurrentConfirmationNode: RecurrentConfirmationNode? + var recurrentConfirmationNode: RecurrentConfirmationNode? func update(presentationData: PresentationData, layout: ContainerViewLayout, invoice: BotPaymentInvoice?, botName: String?) -> (CGFloat, CGFloat) { let bottomPanelVerticalInset: CGFloat = 16.0 @@ -1211,7 +1211,8 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz payString = self.presentationData.strings.CheckoutInfo_Pay } - self.actionButton.isEnabled = isButtonEnabled + self.actionButton.isEnabled = true + self.actionButton.isImplicitlyDisabled = !isButtonEnabled if let currentPaymentMethod = self.currentPaymentMethod { switch currentPaymentMethod { @@ -1268,7 +1269,11 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz } @objc func actionButtonPressed() { - self.pay() + if let recurrentConfirmationNode = self.actionButtonPanelNode.recurrentConfirmationNode, !self.actionButtonPanelNode.isAccepted { + recurrentConfirmationNode.layer.addShakeAnimation() + } else { + self.pay() + } } private func pay(savedCredentialsToken: TemporaryTwoStepPasswordToken? = nil, liabilityNoticeAccepted: Bool = false, receivedCredentials: BotPaymentCredentials? = nil) { diff --git a/submodules/FFMpegBinding/Sources/FFMpegAVCodec.m b/submodules/FFMpegBinding/Sources/FFMpegAVCodec.m index 739ab5efb3..f50633c2a8 100644 --- a/submodules/FFMpegBinding/Sources/FFMpegAVCodec.m +++ b/submodules/FFMpegBinding/Sources/FFMpegAVCodec.m @@ -1,6 +1,6 @@ #import -#import +#import "libavcodec/avcodec.h" @interface FFMpegAVCodec () { AVCodec const *_impl; diff --git a/submodules/FFMpegBinding/Sources/FFMpegAVCodecContext.m b/submodules/FFMpegBinding/Sources/FFMpegAVCodecContext.m index 0fae131670..ee2cde92c0 100644 --- a/submodules/FFMpegBinding/Sources/FFMpegAVCodecContext.m +++ b/submodules/FFMpegBinding/Sources/FFMpegAVCodecContext.m @@ -3,8 +3,8 @@ #import #import -#import -#import +#import "libavformat/avformat.h" +#import "libavcodec/avcodec.h" static enum AVPixelFormat getPreferredPixelFormat(__unused AVCodecContext *ctx, __unused const enum AVPixelFormat *pix_fmts) { return AV_PIX_FMT_VIDEOTOOLBOX; diff --git a/submodules/FFMpegBinding/Sources/FFMpegAVFormatContext.m b/submodules/FFMpegBinding/Sources/FFMpegAVFormatContext.m index a98c295d6d..ebc0d4f963 100644 --- a/submodules/FFMpegBinding/Sources/FFMpegAVFormatContext.m +++ b/submodules/FFMpegBinding/Sources/FFMpegAVFormatContext.m @@ -4,8 +4,8 @@ #import #import -#import -#import +#import "libavcodec/avcodec.h" +#import "libavformat/avformat.h" int FFMpegCodecIdH264 = AV_CODEC_ID_H264; int FFMpegCodecIdHEVC = AV_CODEC_ID_HEVC; diff --git a/submodules/FFMpegBinding/Sources/FFMpegAVFrame.m b/submodules/FFMpegBinding/Sources/FFMpegAVFrame.m index 9c17b53c09..fda8224dc7 100644 --- a/submodules/FFMpegBinding/Sources/FFMpegAVFrame.m +++ b/submodules/FFMpegBinding/Sources/FFMpegAVFrame.m @@ -1,6 +1,6 @@ #import -#import +#import "libavformat/avformat.h" @interface FFMpegAVFrame () { AVFrame *_impl; diff --git a/submodules/FFMpegBinding/Sources/FFMpegAVIOContext.m b/submodules/FFMpegBinding/Sources/FFMpegAVIOContext.m index ebad7d28c9..be3cecf4bd 100644 --- a/submodules/FFMpegBinding/Sources/FFMpegAVIOContext.m +++ b/submodules/FFMpegBinding/Sources/FFMpegAVIOContext.m @@ -1,6 +1,6 @@ #import -#import +#import "libavformat/avformat.h" int FFMPEG_CONSTANT_AVERROR_EOF = AVERROR_EOF; diff --git a/submodules/FFMpegBinding/Sources/FFMpegGlobals.m b/submodules/FFMpegBinding/Sources/FFMpegGlobals.m index 459725e5ab..f0ff9091bb 100644 --- a/submodules/FFMpegBinding/Sources/FFMpegGlobals.m +++ b/submodules/FFMpegBinding/Sources/FFMpegGlobals.m @@ -1,6 +1,6 @@ #import -#import +#import "libavformat/avformat.h" @implementation FFMpegGlobals diff --git a/submodules/FFMpegBinding/Sources/FFMpegLiveMuxer.m b/submodules/FFMpegBinding/Sources/FFMpegLiveMuxer.m index 22531987c8..85ddce8a9e 100644 --- a/submodules/FFMpegBinding/Sources/FFMpegLiveMuxer.m +++ b/submodules/FFMpegBinding/Sources/FFMpegLiveMuxer.m @@ -1,10 +1,10 @@ #import #import -#include -#include -#include -#include +#include "libavutil/timestamp.h" +#include "libavformat/avformat.h" +#include "libavcodec/avcodec.h" +#include "libswresample/swresample.h" #define MOV_TIMESCALE 1000 diff --git a/submodules/FFMpegBinding/Sources/FFMpegPacket.m b/submodules/FFMpegBinding/Sources/FFMpegPacket.m index 4ef8b24dbe..7da5655c96 100644 --- a/submodules/FFMpegBinding/Sources/FFMpegPacket.m +++ b/submodules/FFMpegBinding/Sources/FFMpegPacket.m @@ -2,8 +2,8 @@ #import -#import -#import +#import "libavcodec/avcodec.h" +#import "libavformat/avformat.h" @interface FFMpegPacket () { AVPacket *_impl; diff --git a/submodules/FFMpegBinding/Sources/FFMpegRemuxer.m b/submodules/FFMpegBinding/Sources/FFMpegRemuxer.m index 722a21f4fe..df3c1adfb8 100644 --- a/submodules/FFMpegBinding/Sources/FFMpegRemuxer.m +++ b/submodules/FFMpegBinding/Sources/FFMpegRemuxer.m @@ -2,9 +2,9 @@ #import -#include -#include -#include +#include "libavutil/timestamp.h" +#include "libavformat/avformat.h" +#include "libavcodec/avcodec.h" #define MOV_TIMESCALE 1000 diff --git a/submodules/FFMpegBinding/Sources/FFMpegSWResample.m b/submodules/FFMpegBinding/Sources/FFMpegSWResample.m index de38cab260..c6771b6db3 100644 --- a/submodules/FFMpegBinding/Sources/FFMpegSWResample.m +++ b/submodules/FFMpegBinding/Sources/FFMpegSWResample.m @@ -2,9 +2,9 @@ #import -#import -#import -#import +#import "libavformat/avformat.h" +#import "libavcodec/avcodec.h" +#import "libswresample/swresample.h" @interface FFMpegSWResample () { int _sourceSampleRate; diff --git a/submodules/FFMpegBinding/Sources/FFMpegVideoWriter.m b/submodules/FFMpegBinding/Sources/FFMpegVideoWriter.m index 4274fc6d6c..4746828e62 100755 --- a/submodules/FFMpegBinding/Sources/FFMpegVideoWriter.m +++ b/submodules/FFMpegBinding/Sources/FFMpegVideoWriter.m @@ -1,9 +1,9 @@ #import #import -#include -#include -#include +#include "libavformat/avformat.h" +#include "libavcodec/avcodec.h" +#include "libavutil/imgutils.h" @interface FFMpegVideoWriter () diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index f8a7049e91..5d848b66c9 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -528,29 +528,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att if case let .assets(_, mode) = controller.subject, [.wallpaper, .story, .addImage, .cover, .createSticker, .createAvatar].contains(mode) { } else { - let selectionGesture = MediaPickerGridSelectionGesture() - selectionGesture.delegate = self.wrappedGestureRecognizerDelegate - selectionGesture.began = { [weak self] in - self?.controller?.cancelPanGesture() - } - selectionGesture.updateIsScrollEnabled = { [weak self] isEnabled in - self?.gridNode.scrollView.isScrollEnabled = isEnabled - } - selectionGesture.itemAt = { [weak self] point in - if let self, let itemNode = self.gridNode.itemNodeAtPoint(point) as? MediaPickerGridItemNode, let selectableItem = itemNode.selectableItem { - return (selectableItem, self.controller?.interaction?.selectionState?.isIdentifierSelected(selectableItem.uniqueIdentifier) ?? false) - } else { - return nil - } - } - selectionGesture.updateSelection = { [weak self] asset, selected in - if let strongSelf = self { - strongSelf.controller?.interaction?.selectionState?.setItem(asset, selected: selected, animated: true, sender: nil) - } - } - selectionGesture.sideInset = 44.0 - self.gridNode.view.addGestureRecognizer(selectionGesture) - self.selectionGesture = selectionGesture + self.setupSelectionGesture() } if let controller = self.controller, case let .assets(collection, _) = controller.subject, collection != nil { @@ -713,6 +691,35 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att } } + func setupSelectionGesture() { + guard self.selectionGesture == nil else { + return + } + let selectionGesture = MediaPickerGridSelectionGesture() + selectionGesture.delegate = self.wrappedGestureRecognizerDelegate + selectionGesture.began = { [weak self] in + self?.controller?.cancelPanGesture() + } + selectionGesture.updateIsScrollEnabled = { [weak self] isEnabled in + self?.gridNode.scrollView.isScrollEnabled = isEnabled + } + selectionGesture.itemAt = { [weak self] point in + if let self, let itemNode = self.gridNode.itemNodeAtPoint(point) as? MediaPickerGridItemNode, let selectableItem = itemNode.selectableItem { + return (selectableItem, self.controller?.interaction?.selectionState?.isIdentifierSelected(selectableItem.uniqueIdentifier) ?? false) + } else { + return nil + } + } + selectionGesture.updateSelection = { [weak self] asset, selected in + if let strongSelf = self { + strongSelf.controller?.interaction?.selectionState?.setItem(asset, selected: selected, animated: true, sender: nil) + } + } + selectionGesture.sideInset = 44.0 + self.gridNode.view.addGestureRecognizer(selectionGesture) + self.selectionGesture = selectionGesture + } + @objc private func cameraTapped() { guard let camera = self.modernCamera, let previewView = self.modernCameraView else { return @@ -2352,9 +2359,6 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att let transition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut) var moreIsVisible = false if case let .assets(_, mode) = self.subject, [.story, .createSticker].contains(mode) { - if count == 1 { - self.requestAttachmentMenuExpansion() - } moreIsVisible = true } else if case let .media(media) = self.subject { self.titleView.title = media.count == 1 ? self.presentationData.strings.Attachment_Pasteboard : self.presentationData.strings.Attachment_SelectedMedia(count) @@ -2618,6 +2622,8 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att self.navigationItem.setRightBarButton(nil, animated: true) self.explicitMultipleSelection = true + self.controllerNode.setupSelectionGesture() + self.requestAttachmentMenuExpansion() if let state = self.controllerNode.state { self.controllerNode.updateState(state) diff --git a/submodules/MediaPlayer/Sources/ChunkMediaPlayerV2.swift b/submodules/MediaPlayer/Sources/ChunkMediaPlayerV2.swift index 9825eb7b4c..2026c7b8c7 100644 --- a/submodules/MediaPlayer/Sources/ChunkMediaPlayerV2.swift +++ b/submodules/MediaPlayer/Sources/ChunkMediaPlayerV2.swift @@ -33,101 +33,6 @@ private final class ChunkMediaPlayerExternalSourceImpl: ChunkMediaPlayerSourceIm } public final class ChunkMediaPlayerV2: ChunkMediaPlayer { - public final class AudioContext { - fileprivate let audioSessionManager: ManagedAudioSession - private var audioSessionDisposable: Disposable? - private(set) var hasAudioSession: Bool = false - private(set) var isAmbientMode: Bool = false - private(set) var isInitialized: Bool = false - - private var updatedListeners = Bag<() -> Void>() - - public init( - audioSessionManager: ManagedAudioSession - ) { - self.audioSessionManager = audioSessionManager - } - - deinit { - self.audioSessionDisposable?.dispose() - } - - func onUpdated(_ f: @escaping () -> Void) -> Disposable { - let index = self.updatedListeners.add(f) - return ActionDisposable { [weak self] in - Queue.mainQueue().async { - guard let self else { - return - } - self.updatedListeners.remove(index) - } - } - } - - func setIsAmbient(isAmbient: Bool) { - self.hasAudioSession = false - - for f in self.updatedListeners.copyItems() { - f() - } - - self.audioSessionDisposable?.dispose() - self.audioSessionDisposable = nil - } - - func update(type: ManagedAudioSessionType?) { - if let type { - if self.audioSessionDisposable == nil { - self.isInitialized = true - - self.audioSessionDisposable = self.audioSessionManager.push(params: ManagedAudioSessionClientParams( - audioSessionType: type, - activateImmediately: false, - manualActivate: { [weak self] control in - control.setupAndActivate(synchronous: false, { state in - Queue.mainQueue().async { - guard let self else { - return - } - self.hasAudioSession = true - for f in self.updatedListeners.copyItems() { - f() - } - } - }) - }, - deactivate: { [weak self] _ in - return Signal { subscriber in - guard let self else { - subscriber.putCompletion() - return EmptyDisposable - } - - self.hasAudioSession = false - for f in self.updatedListeners.copyItems() { - f() - } - subscriber.putCompletion() - - return EmptyDisposable - } - |> runOn(.mainQueue()) - }, - headsetConnectionStatusChanged: { _ in }, - availableOutputsChanged: { _, _ in } - )) - } - } else { - if let audioSessionDisposable = self.audioSessionDisposable { - self.audioSessionDisposable = nil - audioSessionDisposable.dispose() - } - - self.hasAudioSession = false - } - } - } - public enum SourceDescription { public final class ResourceDescription { public let postbox: Postbox @@ -261,10 +166,10 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer { private let dataQueue: Queue private let mediaDataReaderParams: MediaDataReaderParams + private let audioSessionManager: ManagedAudioSession private let onSeeked: (() -> Void)? private weak var playerNode: MediaPlayerNode? - private let audioContext: AudioContext private let renderSynchronizer: AVSampleBufferRenderSynchronizer private var videoRenderer: AVSampleBufferDisplayLayer private var audioRenderer: AVSampleBufferAudioRenderer? @@ -293,20 +198,13 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer { } public var actionAtEnd: MediaPlayerActionAtEnd = .stop - public weak var migrateToNextPlayerOnEnd: ChunkMediaPlayerV2? { - didSet { - if self.migrateToNextPlayerOnEnd !== oldValue { - self.updateInternalState() - } - } - } private var didSeekOnce: Bool = false private var isPlaying: Bool = false private var baseRate: Double = 1.0 private var isSoundEnabled: Bool private var isMuted: Bool - private var initialIsAmbient: Bool + private var isAmbientMode: Bool private var seekId: Int = 0 private var seekTimestamp: Double = 0.0 @@ -325,11 +223,12 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer { private var partsStateDisposable: Disposable? private var updateTimer: Foundation.Timer? - private var audioContextUpdatedDisposable: Disposable? + private var audioSessionDisposable: Disposable? + private var hasAudioSession: Bool = false public init( params: MediaDataReaderParams, - audioContext: AudioContext, + audioSessionManager: ManagedAudioSession, source: SourceDescription, video: Bool, playAutomatically: Bool = false, @@ -348,7 +247,7 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer { self.dataQueue = ChunkMediaPlayerV2.sharedDataQueue self.mediaDataReaderParams = params - self.audioContext = audioContext + self.audioSessionManager = audioSessionManager self.onSeeked = onSeeked self.playerNode = playerNode @@ -358,7 +257,7 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer { self.isSoundEnabled = enableSound self.isMuted = soundMuted - self.initialIsAmbient = ambient + self.isAmbientMode = ambient self.baseRate = baseRate self.renderSynchronizer = AVSampleBufferRenderSynchronizer() @@ -397,19 +296,12 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer { } else { self.renderSynchronizer.addRenderer(self.videoRenderer) } - - self.audioContextUpdatedDisposable = self.audioContext.onUpdated({ [weak self] in - guard let self else { - return - } - self.updateInternalState() - }) } deinit { self.partsStateDisposable?.dispose() self.updateTimer?.invalidate() - self.audioContextUpdatedDisposable?.dispose() + self.audioSessionDisposable?.dispose() if #available(iOS 17.0, *) { self.videoRenderer.sampleBufferRenderer.stopRequestingMediaData() @@ -429,19 +321,51 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer { } private func updateInternalState() { - var audioSessionType: ManagedAudioSessionType? if self.isSoundEnabled && self.hasSound { - let isAmbient: Bool - if self.audioContext.isInitialized { - isAmbient = self.audioContext.isAmbientMode - } else { - isAmbient = self.initialIsAmbient + if self.audioSessionDisposable == nil { + self.audioSessionDisposable = self.audioSessionManager.push(params: ManagedAudioSessionClientParams( + audioSessionType: self.isAmbientMode ? .ambient : .play(mixWithOthers: false), + activateImmediately: false, + manualActivate: { [weak self] control in + control.setupAndActivate(synchronous: false, { state in + Queue.mainQueue().async { + guard let self else { + return + } + self.hasAudioSession = true + self.updateInternalState() + } + }) + }, + deactivate: { [weak self] _ in + return Signal { subscriber in + guard let self else { + subscriber.putCompletion() + return EmptyDisposable + } + + self.hasAudioSession = false + self.updateInternalState() + subscriber.putCompletion() + + return EmptyDisposable + } + |> runOn(.mainQueue()) + }, + headsetConnectionStatusChanged: { _ in }, + availableOutputsChanged: { _, _ in } + )) } - audioSessionType = isAmbient ? .ambient : .play(mixWithOthers: false) + } else { + if let audioSessionDisposable = self.audioSessionDisposable { + self.audioSessionDisposable = nil + audioSessionDisposable.dispose() + } + + self.hasAudioSession = false } - self.audioContext.update(type: audioSessionType) - if self.isSoundEnabled && self.hasSound && self.audioContext.hasAudioSession { + if self.isSoundEnabled && self.hasSound && self.hasAudioSession { if self.audioRenderer == nil { let audioRenderer = AVSampleBufferAudioRenderer() audioRenderer.isMuted = self.isMuted @@ -875,9 +799,13 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer { } public func continueWithOverridingAmbientMode(isAmbient: Bool) { - if self.audioContext.isAmbientMode != isAmbient { - self.initialIsAmbient = isAmbient - self.audioContext.setIsAmbient(isAmbient: isAmbient) + if self.isAmbientMode != isAmbient { + self.isAmbientMode = isAmbient + + self.hasAudioSession = false + self.updateInternalState() + self.audioSessionDisposable?.dispose() + self.audioSessionDisposable = nil let currentTimestamp: CMTime if let pendingSeekTimestamp = self.pendingSeekTimestamp { diff --git a/submodules/Pasteboard/Sources/Pasteboard.swift b/submodules/Pasteboard/Sources/Pasteboard.swift index bbd784a466..7ffa9fc9d4 100644 --- a/submodules/Pasteboard/Sources/Pasteboard.swift +++ b/submodules/Pasteboard/Sources/Pasteboard.swift @@ -53,15 +53,29 @@ private func chatInputStateString(attributedString: NSAttributedString) -> NSAtt } if let value = attributes[.font], let font = value as? UIFont { let fontName = font.fontName.lowercased() - if fontName.contains("bolditalic") { - string.addAttribute(ChatTextInputAttributes.bold, value: true as NSNumber, range: range) - string.addAttribute(ChatTextInputAttributes.italic, value: true as NSNumber, range: range) - } else if fontName.contains("bold") { - string.addAttribute(ChatTextInputAttributes.bold, value: true as NSNumber, range: range) - } else if fontName.contains("italic") { - string.addAttribute(ChatTextInputAttributes.italic, value: true as NSNumber, range: range) - } else if fontName.contains("menlo") || fontName.contains("courier") || fontName.contains("sfmono") { - string.addAttribute(ChatTextInputAttributes.monospace, value: true as NSNumber, range: range) + if fontName.hasPrefix(".sfui") { + let traits = font.fontDescriptor.symbolicTraits + if traits.contains(.traitMonoSpace) { + string.addAttribute(ChatTextInputAttributes.monospace, value: true as NSNumber, range: range) + } else { + if traits.contains(.traitBold) { + string.addAttribute(ChatTextInputAttributes.bold, value: true as NSNumber, range: range) + } + if traits.contains(.traitItalic) { + string.addAttribute(ChatTextInputAttributes.italic, value: true as NSNumber, range: range) + } + } + } else { + if fontName.contains("bolditalic") { + string.addAttribute(ChatTextInputAttributes.bold, value: true as NSNumber, range: range) + string.addAttribute(ChatTextInputAttributes.italic, value: true as NSNumber, range: range) + } else if fontName.contains("bold") { + string.addAttribute(ChatTextInputAttributes.bold, value: true as NSNumber, range: range) + } else if fontName.contains("italic") { + string.addAttribute(ChatTextInputAttributes.italic, value: true as NSNumber, range: range) + } else if fontName.contains("menlo") || fontName.contains("courier") || fontName.contains("sfmono") { + string.addAttribute(ChatTextInputAttributes.monospace, value: true as NSNumber, range: range) + } } } if let value = attributes[.backgroundColor] as? UIColor, value.rgb == UIColor.gray.rgb { diff --git a/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift index d9c5782795..f01f636801 100644 --- a/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift @@ -1108,6 +1108,10 @@ private final class SheetContent: CombinedComponent { func layoutLevel(_ level: Int32) { var perks: [LevelSectionComponent.Perk] = [] + if !isGroup && level >= requiredBoostSubjectLevel(subject: .autoTranslate, group: isGroup, context: component.context, configuration: premiumConfiguration) { + perks.append(.autoTranslate) + } + perks.append(.story(level)) if !isGroup { @@ -1171,12 +1175,6 @@ private final class SheetContent: CombinedComponent { if !isGroup && level >= requiredBoostSubjectLevel(subject: .noAds, group: isGroup, context: component.context, configuration: premiumConfiguration) { perks.append(.noAds) } - if !isGroup && level >= requiredBoostSubjectLevel(subject: .autoTranslate, group: isGroup, context: component.context, configuration: premiumConfiguration) { - perks.append(.autoTranslate) - } -// if !isGroup && level >= requiredBoostSubjectLevel(subject: .wearGift, group: isGroup, context: component.context, configuration: premiumConfiguration) { -// perks.append(.wearGift) -// } levelItems.append( AnyComponentWithIdentity( diff --git a/submodules/SSignalKit/SwiftSignalKit/Source/Signal.swift b/submodules/SSignalKit/SwiftSignalKit/Source/Signal.swift index 6c9f5e0361..a3deef60db 100644 --- a/submodules/SSignalKit/SwiftSignalKit/Source/Signal.swift +++ b/submodules/SSignalKit/SwiftSignalKit/Source/Signal.swift @@ -120,7 +120,7 @@ public final class Signal { } } -@available(iOS 13.0, *) +@available(iOS 13.0, macOS 10.15, *) public extension Signal where E == NoError { func get() async -> T { let disposable = MetaDisposable() diff --git a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift index db19e00c36..e2bbc23abb 100644 --- a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift +++ b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift @@ -167,7 +167,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP self.conferenceAddParticipant?() } - var enableVideoSharpening = true + var enableVideoSharpening = false if let data = call.context.currentAppConfiguration.with({ $0 }).data, let value = data["ios_call_video_sharpening"] as? Double { enableVideoSharpening = value != 0.0 } diff --git a/submodules/TelegramCallsUI/Sources/Components/LivestreamVideoViewV1.swift b/submodules/TelegramCallsUI/Sources/Components/LivestreamVideoViewV1.swift index 030ecff2b5..e9d61c78a6 100644 --- a/submodules/TelegramCallsUI/Sources/Components/LivestreamVideoViewV1.swift +++ b/submodules/TelegramCallsUI/Sources/Components/LivestreamVideoViewV1.swift @@ -70,7 +70,7 @@ final class LivestreamVideoViewV1: UIView { var onSeeked: (() -> Void)? self.player = ChunkMediaPlayerV2( params: ChunkMediaPlayerV2.MediaDataReaderParams(context: context), - audioContext: ChunkMediaPlayerV2.AudioContext(audioSessionManager: audioSessionManager), + audioSessionManager: audioSessionManager, source: .externalParts(self.chunkPlayerPartsState.get()), video: true, enableSound: true, diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index d65a9c0fdd..b18aa64aa1 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -1247,7 +1247,7 @@ final class VideoChatScreenComponent: Component { } self.callState = component.initialData.callState - self.enableVideoSharpening = true + self.enableVideoSharpening = false if let data = component.initialCall.accountContext.currentAppConfiguration.with({ $0 }).data, let value = data["ios_call_video_sharpening"] as? Double { self.enableVideoSharpening = value != 0.0 } diff --git a/submodules/TelegramCore/Sources/State/ManagedProxyInfoUpdates.swift b/submodules/TelegramCore/Sources/State/ManagedProxyInfoUpdates.swift index b745a5b107..1384758737 100644 --- a/submodules/TelegramCore/Sources/State/ManagedProxyInfoUpdates.swift +++ b/submodules/TelegramCore/Sources/State/ManagedProxyInfoUpdates.swift @@ -202,6 +202,16 @@ func managedPromoInfoUpdates(accountPeerId: PeerId, postbox: Postbox, network: N switch data { case .promoDataEmpty: transaction.replaceAdditionalChatListItems([]) + + let suggestionInfo = ServerSuggestionInfo( + legacyItems: [], + items: [], + dismissedIds: [] + ) + + transaction.updatePreferencesEntry(key: PreferencesKeys.serverSuggestionInfo(), { _ in + return PreferencesEntry(suggestionInfo) + }) case let .promoData(flags, expires, peer, psaType, psaMessage, pendingSuggestions, dismissedSuggestions, customPendingSuggestion, chats, users): let _ = expires diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index 7b5a1c63b5..e7ca7d8f5c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -2463,7 +2463,7 @@ private final class ResaleGiftsContextImpl { let filterAttributes = self.filterAttributes let currentAttributesHash = self.attributesHash - let dataState = self.dataState + let dataState = self.dataState if case let .ready(true, initialNextOffset) = dataState { self.dataState = .loading diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index ea79ba170c..7c0ed52fed 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -70,6 +70,7 @@ swift_library( "//submodules/MediaPlayer:UniversalMediaPlayer", "//submodules/TelegramVoip:TelegramVoip", "//submodules/DeviceAccess:DeviceAccess", + "//submodules/Utils/DeviceModel", "//submodules/WatchCommon/Host:WatchCommon", "//submodules/BuildConfig:BuildConfig", "//submodules/BuildConfigExtra:BuildConfigExtra", diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 5b57038105..c647baa75a 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -2553,6 +2553,8 @@ public class CameraScreenImpl: ViewController, CameraScreen { transitionCircleLayer.animateScale(from: sourceLocalFrame.width / 320.0, to: 6.0, duration: 0.6, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in self.view.mask = nil colorFillView.removeFromSuperview() + + self.requestUpdateLayout(hasAppeared: true, transition: .immediate) }) } else { if case .story = controller.mode { diff --git a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift index 993fab330b..5caccf8eb1 100644 --- a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift @@ -469,13 +469,7 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { self.giftButton.isHidden = false self.helpButton.isHidden = true - //TODO:release - self.suggestedPostButton.isHidden = false - self.presentGiftOrSuggestTooltip() - } else if case .broadcast = peer.info { - self.giftButton.isHidden = true - self.helpButton.isHidden = true - self.suggestedPostButton.isHidden = false + self.suggestedPostButton.isHidden = true self.presentGiftOrSuggestTooltip() } else if peer.flags.contains(.isGigagroup), self.action == .muteNotifications || self.action == .unmuteNotifications { self.giftButton.isHidden = true diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift index 38c0ff482f..f749c3c46a 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift @@ -295,10 +295,6 @@ public func canAddMessageReactions(message: Message) -> Bool { return true } } - } else if let story = media as? TelegramMediaStory { - if story.isMention { - return false - } } } return true diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index 001fa6f4fe..ae3d159ef2 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -559,7 +559,7 @@ public final class GiftItemComponent: Component { let price: String switch component.subject { case let .premium(_, priceValue), let .starGift(_, priceValue): - if priceValue.containsEmoji { + if priceValue.contains("#") { buttonColor = component.theme.overallDarkAppearance ? UIColor(rgb: 0xffc337) : UIColor(rgb: 0xd3720a) if !component.isSoldOut { starsColor = UIColor(rgb: 0xffbe27) @@ -867,10 +867,12 @@ public final class GiftItemComponent: Component { } ) let dateTimeFormat = component.context.sharedContext.currentPresentationData.with { $0 }.dateTimeFormat - let labelText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("#\(presentationStringsFormattedNumber(Int32(resellPrice), dateTimeFormat.groupingSeparator))", attributes: attributes)) - if let range = labelText.string.range(of: "#") { - labelText.addAttribute(NSAttributedString.Key.font, value: Font.semibold(10.0), range: NSRange(range, in: labelText.string)) - labelText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: true)), range: NSRange(range, in: labelText.string)) + let labelText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("# \(presentationStringsFormattedNumber(Int32(resellPrice), dateTimeFormat.groupingSeparator))", attributes: attributes)) + let range = (labelText.string as NSString).range(of: "#") + if range.location != NSNotFound { + labelText.addAttribute(NSAttributedString.Key.font, value: Font.semibold(10.0), range: range) + labelText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: true)), range: range) + labelText.addAttribute(.kern, value: -1.5, range: NSRange(location: range.upperBound, length: 1)) } let resellSize = self.reselLabel.update( @@ -1048,11 +1050,13 @@ private final class ButtonContentComponent: Component { self.componentState = state let attributedText = NSMutableAttributedString(string: component.text, font: Font.semibold(11.0), textColor: component.color) - let range = (attributedText.string as NSString).range(of: "⭐️") + let range = (attributedText.string as NSString).range(of: "#") if range.location != NSNotFound { attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: component.tinted)), range: range) - attributedText.addAttribute(.font, value: Font.semibold(15.0), range: range) - attributedText.addAttribute(.baselineOffset, value: 2.0, range: NSRange(location: range.upperBound, length: attributedText.length - range.upperBound)) + attributedText.addAttribute(.font, value: Font.semibold(component.tinted ? 14.0 : 15.0), range: range) + attributedText.addAttribute(.baselineOffset, value: -3.0, range: range) + attributedText.addAttribute(.baselineOffset, value: 1.5, range: NSRange(location: range.upperBound + 1, length: attributedText.length - range.upperBound - 1)) + attributedText.addAttribute(.kern, value: -1.5, range: NSRange(location: range.upperBound, length: 1)) } let titleSize = self.title.update( diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index 5b79de4903..643d1f7131 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -412,12 +412,12 @@ final class GiftOptionsScreenComponent: Component { if let availability = gift.availability, availability.remains == 0, let minResaleStars = availability.minResaleStars { let priceString = presentationStringsFormattedNumber(Int32(minResaleStars), environment.dateTimeFormat.groupingSeparator) if let resaleConfiguration = self.resaleConfiguration, minResaleStars == resaleConfiguration.starGiftResaleMaxAmount || availability.resale == 1 { - subject = .starGift(gift: gift, price: "⭐️ \(priceString)") + subject = .starGift(gift: gift, price: "# \(priceString)") } else { - subject = .starGift(gift: gift, price: "⭐️ \(priceString)+") + subject = .starGift(gift: gift, price: "# \(priceString)+") } } else { - subject = .starGift(gift: gift, price: "⭐️ \(presentationStringsFormattedNumber(Int32(gift.price), environment.dateTimeFormat.groupingSeparator))") + subject = .starGift(gift: gift, price: "# \(presentationStringsFormattedNumber(Int32(gift.price), environment.dateTimeFormat.groupingSeparator))") } case let .unique(gift): subject = .uniqueGift(gift: gift, price: nil) @@ -1567,6 +1567,9 @@ final class GiftOptionsScreenComponent: Component { } } } + if disallowedGifts.contains(.unique) && gift.availability?.remains == 0 { + return false + } } return true } diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift index ae889947fc..4ca433b12f 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift @@ -95,7 +95,8 @@ final class GiftStoreScreenComponent: Component { private var starsStateDisposable: Disposable? private var starsState: StarsContext.State? - private var initialCount: Int? + private var initialCount: Int32? + private var showLoading = true private var component: GiftStoreScreenComponent? private(set) weak var state: State? @@ -230,7 +231,7 @@ final class GiftStoreScreenComponent: Component { color: ribbonColor ) - let subject: GiftItemComponent.Subject = .uniqueGift(gift: uniqueGift, price: "⭐️\(presentationStringsFormattedNumber(Int32(uniqueGift.resellStars ?? 0), environment.dateTimeFormat.groupingSeparator))") + let subject: GiftItemComponent.Subject = .uniqueGift(gift: uniqueGift, price: "# \(presentationStringsFormattedNumber(Int32(uniqueGift.resellStars ?? 0), environment.dateTimeFormat.groupingSeparator))") let _ = visibleItem.update( transition: itemTransition, component: AnyComponent( @@ -338,7 +339,9 @@ final class GiftStoreScreenComponent: Component { guard let self else { return } + self.showLoading = true self.state?.starGiftsContext.updateFilterAttributes([]) + self.scrollToTop() }, animateScale: false ) @@ -357,7 +360,7 @@ final class GiftStoreScreenComponent: Component { var emptyResultsActionFrame = CGRect( origin: CGPoint( x: floorToScreenPixels((availableWidth - emptyResultsActionSize.width) / 2.0), - y: max(self.scrollView.contentSize.height - 8.0, availableHeight - bottomInset - emptyResultsActionSize.height - 16.0) + y: max(self.scrollView.contentSize.height - 70.0, availableHeight - bottomInset - emptyResultsActionSize.height - 16.0) ), size: emptyResultsActionSize ) @@ -435,7 +438,7 @@ final class GiftStoreScreenComponent: Component { if view.superview == nil { view.alpha = 0.0 fadeTransition.setAlpha(view: view, alpha: 1.0) - self.insertSubview(view, belowSubview: self.loadingNode.view) + self.scrollView.addSubview(view) } view.bounds = CGRect(origin: .zero, size: emptyResultsActionFrame.size) ComponentTransition.immediate.setPosition(view: view, position: emptyResultsActionFrame.center) @@ -451,7 +454,7 @@ final class GiftStoreScreenComponent: Component { } let bottomContentOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height) - if interactive, bottomContentOffset < 320.0 { + if interactive, bottomContentOffset < 1000.0 { self.state?.starGiftsContext.loadMore() } } @@ -471,6 +474,7 @@ final class GiftStoreScreenComponent: Component { guard let self else { return } + self.showLoading = true self.state?.starGiftsContext.updateSorting(.value) self.scrollToTop() }))) @@ -481,6 +485,7 @@ final class GiftStoreScreenComponent: Component { guard let self else { return } + self.showLoading = true self.state?.starGiftsContext.updateSorting(.date) self.scrollToTop() }))) @@ -491,6 +496,7 @@ final class GiftStoreScreenComponent: Component { guard let self else { return } + self.showLoading = true self.state?.starGiftsContext.updateSorting(.number) self.scrollToTop() }))) @@ -514,7 +520,13 @@ final class GiftStoreScreenComponent: Component { } else { return false } - } + }.sorted(by: { lhs, rhs in + if case let .model(_, lhsFile, _) = lhs, case let .model(_, rhsFile, _) = rhs, let lhsCount = self.state?.starGiftsState?.attributeCount[.model(lhsFile.fileId.id)], let rhsCount = self.state?.starGiftsState?.attributeCount[.model(rhsFile.fileId.id)] { + return lhsCount > rhsCount + } else { + return false + } + }) let currentFilterAttributes = self.state?.starGiftsState?.filterAttributes ?? [] let selectedModelAttributes = currentFilterAttributes.filter { attribute in @@ -564,6 +576,7 @@ final class GiftStoreScreenComponent: Component { updatedFilterAttributes.append(attribute) } } + self.showLoading = true self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) self.scrollToTop() }, @@ -577,6 +590,7 @@ final class GiftStoreScreenComponent: Component { } return true } + self.showLoading = true self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) self.scrollToTop() } @@ -607,7 +621,13 @@ final class GiftStoreScreenComponent: Component { } else { return false } - } + }.sorted(by: { lhs, rhs in + if case let .backdrop(_, lhsId, _, _, _, _, _) = lhs, case let .backdrop(_, rhsId, _, _, _, _, _) = rhs, let lhsCount = self.state?.starGiftsState?.attributeCount[.backdrop(lhsId)], let rhsCount = self.state?.starGiftsState?.attributeCount[.backdrop(rhsId)] { + return lhsCount > rhsCount + } else { + return false + } + }) let currentFilterAttributes = self.state?.starGiftsState?.filterAttributes ?? [] let selectedBackdropAttributes = currentFilterAttributes.filter { attribute in @@ -657,6 +677,7 @@ final class GiftStoreScreenComponent: Component { updatedFilterAttributes.append(attribute) } } + self.showLoading = true self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) self.scrollToTop() }, @@ -670,6 +691,7 @@ final class GiftStoreScreenComponent: Component { } return true } + self.showLoading = true self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) self.scrollToTop() } @@ -700,7 +722,13 @@ final class GiftStoreScreenComponent: Component { } else { return false } - } + }.sorted(by: { lhs, rhs in + if case let .pattern(_, lhsFile, _) = lhs, case let .pattern(_, rhsFile, _) = rhs, let lhsCount = self.state?.starGiftsState?.attributeCount[.pattern(lhsFile.fileId.id)], let rhsCount = self.state?.starGiftsState?.attributeCount[.pattern(rhsFile.fileId.id)] { + return lhsCount > rhsCount + } else { + return false + } + }) let currentFilterAttributes = self.state?.starGiftsState?.filterAttributes ?? [] let selectedPatternAttributes = currentFilterAttributes.filter { attribute in @@ -750,6 +778,7 @@ final class GiftStoreScreenComponent: Component { updatedFilterAttributes.append(attribute) } } + self.showLoading = true self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) self.scrollToTop() }, @@ -763,6 +792,7 @@ final class GiftStoreScreenComponent: Component { } return true } + self.showLoading = true self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) self.scrollToTop() } @@ -804,6 +834,12 @@ final class GiftStoreScreenComponent: Component { self.component = component let isLoading = self.effectiveIsLoading + if case let .ready(loadMore, nextOffset) = self.state?.starGiftsState?.dataState { + if loadMore && nextOffset == nil { + } else { + self.showLoading = false + } + } let theme = environment.theme let strings = environment.strings @@ -812,7 +848,7 @@ final class GiftStoreScreenComponent: Component { self.backgroundColor = environment.theme.list.blocksBackgroundColor } - let bottomContentInset: CGFloat = 24.0 + let bottomContentInset: CGFloat = 56.0 let sideInset: CGFloat = 16.0 + environment.safeInsets.left let headerSideInset: CGFloat = 24.0 + environment.safeInsets.left @@ -927,7 +963,7 @@ final class GiftStoreScreenComponent: Component { } let effectiveCount: Int32 - if let count = self.effectiveGifts?.count, count > 0 || self.initialCount != nil { + if let count = self.state?.starGiftsState?.count, count > 0 || self.initialCount != nil { if self.initialCount == nil { self.initialCount = count } @@ -1047,6 +1083,7 @@ final class GiftStoreScreenComponent: Component { let loadingTransition: ComponentTransition = .easeInOut(duration: 0.25) + var showingFilters = false let filterSize = self.filterSelector.update( transition: transition, component: AnyComponent(FilterSelectorComponent( @@ -1069,6 +1106,7 @@ final class GiftStoreScreenComponent: Component { if let initialCount = self.initialCount, initialCount >= minimumCountToDisplayFilters { loadingTransition.setAlpha(view: filterSelectorView, alpha: 1.0) + showingFilters = true } } @@ -1112,8 +1150,8 @@ final class GiftStoreScreenComponent: Component { self.updateScrolling(transition: transition) - if isLoading { - self.loadingNode.update(size: availableSize, theme: environment.theme, transition: .immediate) + if isLoading && self.showLoading { + self.loadingNode.update(size: availableSize, theme: environment.theme, showFilters: !showingFilters, transition: .immediate) loadingTransition.setAlpha(view: self.loadingNode.view, alpha: 1.0) } else { loadingTransition.setAlpha(view: self.loadingNode.view, alpha: 0.0) diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/LoadingShimmerComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/LoadingShimmerComponent.swift index 73e314820f..af253d4b3e 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/LoadingShimmerComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/LoadingShimmerComponent.swift @@ -125,7 +125,7 @@ final class LoadingShimmerNode: ASDisplayNode { private let backgroundColorNode: ASDisplayNode private let effectNode: SearchShimmerEffectNode private let maskNode: ASImageNode - private var currentParams: (size: CGSize, theme: PresentationTheme)? + private var currentParams: (size: CGSize, theme: PresentationTheme, showFilters: Bool)? override init() { self.backgroundColorNode = ASDisplayNode() @@ -142,11 +142,11 @@ final class LoadingShimmerNode: ASDisplayNode { self.addSubnode(self.maskNode) } - func update(size: CGSize, theme: PresentationTheme, transition: ContainedViewLayoutTransition) { + func update(size: CGSize, theme: PresentationTheme, showFilters: Bool, transition: ContainedViewLayoutTransition) { let color = theme.list.itemSecondaryTextColor.mixedWith(theme.list.blocksBackgroundColor, alpha: 0.85) - if self.currentParams?.size != size || self.currentParams?.theme !== theme { - self.currentParams = (size, theme) + if self.currentParams?.size != size || self.currentParams?.theme !== theme || self.currentParams?.showFilters != showFilters { + self.currentParams = (size, theme, showFilters) self.backgroundColorNode.backgroundColor = color @@ -156,10 +156,12 @@ final class LoadingShimmerNode: ASDisplayNode { let sideInset: CGFloat = 16.0 - let filterSpacing: CGFloat = 6.0 - let filterWidth = (size.width - sideInset * 2.0 - filterSpacing * 3.0) / 4.0 - for i in 0 ..< 4 { - context.addPath(CGPath(roundedRect: CGRect(origin: CGPoint(x: sideInset + (filterWidth + filterSpacing) * CGFloat(i), y: 0.0), size: CGSize(width: filterWidth, height: 28.0)), cornerWidth: 14.0, cornerHeight: 14.0, transform: nil)) + if showFilters { + let filterSpacing: CGFloat = 6.0 + let filterWidth = (size.width - sideInset * 2.0 - filterSpacing * 3.0) / 4.0 + for i in 0 ..< 4 { + context.addPath(CGPath(roundedRect: CGRect(origin: CGPoint(x: sideInset + (filterWidth + filterSpacing) * CGFloat(i), y: 0.0), size: CGSize(width: filterWidth, height: 28.0)), cornerWidth: 14.0, cornerHeight: 14.0, transform: nil)) + } } var currentY: CGFloat = 39.0 + 7.0 diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 3d8134dfbc..c07308affd 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -309,17 +309,29 @@ private final class GiftViewSheetContent: CombinedComponent { let context = self.context let action = { if gifts { - if let profileController = context.sharedContext.makePeerInfoController( - context: context, - updatedPresentationData: nil, - peer: peer._asPeer(), - mode: peer.id == context.account.peerId ? .myProfileGifts : .gifts, - avatarInitiallyExpanded: false, - fromChat: false, - requestsContext: nil - ) { - navigationController.pushViewController(profileController) + let profileGifts = ProfileGiftsContext(account: context.account, peerId: peer.id) + let _ = (profileGifts.state + |> filter { state in + if case .ready = state.dataState { + return true + } + return false } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak navigationController] _ in + if let profileController = context.sharedContext.makePeerInfoController( + context: context, + updatedPresentationData: nil, + peer: peer._asPeer(), + mode: peer.id == context.account.peerId ? .myProfileGifts : .gifts, + avatarInitiallyExpanded: false, + fromChat: false, + requestsContext: nil + ) { + navigationController?.pushViewController(profileController) + } + let _ = profileGifts + }) } else { context.sharedContext.navigateToChatController(NavigateToChatControllerParams( navigationController: navigationController, @@ -946,7 +958,7 @@ private final class GiftViewSheetContent: CombinedComponent { let location = CGRect(origin: CGPoint(x: absoluteLocation.x, y: absoluteLocation.y - 12.0), size: CGSize()) let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: text), style: .wide, location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in - return .ignore + return .dismiss(consume: false) }) controller.present(tooltipController, in: .current) } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift index 47b4d4c792..373483fe9b 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift @@ -230,7 +230,7 @@ public enum MediaCropOrientation: Int32 { } } -public final class MediaEditorValues: Codable, Equatable { +public final class MediaEditorValues: Codable, Equatable, CustomStringConvertible { public static func == (lhs: MediaEditorValues, rhs: MediaEditorValues) -> Bool { if lhs.peerId != rhs.peerId { return false @@ -1010,6 +1010,114 @@ public final class MediaEditorValues: Codable, Equatable { } return false } + + public var description: String { + var components: [String] = [] + + components.append("originalDimensions: \(self.originalDimensions.width)x\(self.originalDimensions.height)") + + if self.cropOffset != .zero { + components.append("cropOffset: \(cropOffset)") + } + + if let cropRect = self.cropRect { + components.append("cropRect: \(cropRect)") + } + + if self.cropScale != 1.0 { + components.append("cropScale: \(self.cropScale)") + } + + if self.cropRotation != 0.0 { + components.append("cropRotation: \(self.cropRotation)") + } + + if self.cropMirroring { + components.append("cropMirroring: true") + } + + if let cropOrientation = self.cropOrientation { + components.append("cropOrientation: \(cropOrientation)") + } + + if let gradientColors = self.gradientColors, !gradientColors.isEmpty { + components.append("gradientColors: \(gradientColors.count) colors") + } + + if let videoTrimRange = self.videoTrimRange { + components.append("videoTrimRange: \(videoTrimRange.lowerBound) - \(videoTrimRange.upperBound)") + } + + if self.videoIsMuted { + components.append("videoIsMuted: true") + } + + if self.videoIsFullHd { + components.append("videoIsFullHd: true") + } + + if self.videoIsMirrored { + components.append("videoIsMirrored: true") + } + + if let videoVolume = self.videoVolume, videoVolume != 1.0 { + components.append("videoVolume: \(videoVolume)") + } + + if let additionalVideoPath = self.additionalVideoPath { + components.append("additionalVideo: \(additionalVideoPath)") + } + + if let position = self.additionalVideoPosition { + components.append("additionalVideoPosition: \(position)") + } + + if let scale = self.additionalVideoScale { + components.append("additionalVideoScale: \(scale)") + } + + if let rotation = self.additionalVideoRotation { + components.append("additionalVideoRotation: \(rotation)") + } + + if !self.additionalVideoPositionChanges.isEmpty { + components.append("additionalVideoPositionChanges: \(additionalVideoPositionChanges.count) changes") + } + + if !self.collage.isEmpty { + components.append("collage: \(collage.count) items") + } + + if self.nightTheme { + components.append("nightTheme: true") + } + + if self.drawing != nil { + components.append("drawing: true") + } + + if self.maskDrawing != nil { + components.append("maskDrawing: true") + } + + if !self.entities.isEmpty { + components.append("entities: \(self.entities.count) items") + } + + if !self.toolValues.isEmpty { + components.append("toolValues: \(self.toolValues.count) tools") + } + + if let audioTrack = self.audioTrack { + components.append("audioTrack: \(audioTrack.path)") + } + + if let qualityPreset = self.qualityPreset { + components.append("qualityPreset: \(qualityPreset)") + } + + return "MediaEditorValues(\(components.joined(separator: ", ")))" + } } public struct TintValue: Equatable, Codable { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift index 6061765718..89bfd124cc 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift @@ -264,6 +264,11 @@ public final class MediaEditorVideoExport { self.outputPath = outputPath self.textScale = textScale + Logger.shared.log("VideoExport", "Init") + Logger.shared.log("VideoExport", "Subject: \(subject)") + Logger.shared.log("VideoExport", "Output Path: \(outputPath)") + Logger.shared.log("VideoExport", "Configuration: \(configuration)") + if FileManager.default.fileExists(atPath: outputPath) { try? FileManager.default.removeItem(atPath: outputPath) } @@ -297,6 +302,9 @@ public final class MediaEditorVideoExport { } private func setup() { + Logger.shared.log("VideoExport", "Setting up") + + var mainAsset: AVAsset? var signals: [Signal] = [] @@ -948,11 +956,6 @@ public final class MediaEditorVideoExport { return false } } - } else { -// if !writer.appendVideoBuffer(sampleBuffer) { -// writer.markVideoAsFinished() -// return false -// } } } return true @@ -983,17 +986,21 @@ public final class MediaEditorVideoExport { } private func start() { + Logger.shared.log("VideoExport", "Start") guard self.internalStatus == .idle, let writer = self.writer else { + Logger.shared.log("VideoExport", "Failed with invalid state") self.statusValue = .failed(.invalid) return } guard writer.startWriting() else { + Logger.shared.log("VideoExport", "Failed on startWriting") self.statusValue = .failed(.writing(nil)) return } if let reader = self.reader, !reader.startReading() { + Logger.shared.log("VideoExport", "Failed on startReading") self.statusValue = .failed(.reading(nil)) return } @@ -1067,6 +1074,7 @@ public final class MediaEditorVideoExport { } if cancelled { + Logger.shared.log("VideoExport", "Cancelled") try? FileManager.default.removeItem(at: outputUrl) self.internalStatus = .finished self.statusValue = .failed(.cancelled) @@ -1108,6 +1116,7 @@ public final class MediaEditorVideoExport { let exportDuration = end - self.startTimestamp print("video processing took \(exportDuration)s") if duration.seconds > 0 { + Logger.shared.log("VideoExport", "Completed with path \(self.outputPath)") Logger.shared.log("VideoExport", "Video processing took \(exportDuration / duration.seconds)") } }) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift index 81ba25209d..4f32eca5a3 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift @@ -184,7 +184,9 @@ public final class PeerInfoGiftsCoverComponent: Component { } } + private var scheduledAnimateIn = false public func willAnimateIn() { + self.scheduledAnimateIn = true for (_, layer) in self.iconLayers { layer.opacity = 0.0 } @@ -194,6 +196,7 @@ public final class PeerInfoGiftsCoverComponent: Component { guard let _ = self.currentSize, let component = self.component else { return } + self.scheduledAnimateIn = false for (_, layer) in self.iconLayers { layer.opacity = 1.0 @@ -319,8 +322,12 @@ public final class PeerInfoGiftsCoverComponent: Component { self.iconLayers[id] = iconLayer self.layer.addSublayer(iconLayer) - iconLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - iconLayer.animateScale(from: 0.01, to: 1.0, duration: 0.2) + if self.scheduledAnimateIn { + iconLayer.opacity = 0.0 + } else { + iconLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + iconLayer.animateScale(from: 0.01, to: 1.0, duration: 0.2) + } iconLayer.startAnimations(index: index) } @@ -349,7 +356,10 @@ public final class PeerInfoGiftsCoverComponent: Component { iconTransition.setPosition(layer: iconLayer, position: absolutePosition) iconLayer.updateRotation(effectiveAngle, transition: iconTransition) iconTransition.setScale(layer: iconLayer, scale: iconPosition.scale * (1.0 - itemScaleFraction)) - iconTransition.setAlpha(layer: iconLayer, alpha: 1.0 - itemScaleFraction) + + if !self.scheduledAnimateIn { + iconTransition.setAlpha(layer: iconLayer, alpha: 1.0 - itemScaleFraction) + } index += 1 } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index 2a88b1ced1..51dd286db5 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -488,7 +488,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr switch product.gift { case let .generic(gift): - subject = .starGift(gift: gift, price: "⭐️ \(gift.price)") + subject = .starGift(gift: gift, price: "# \(gift.price)") peer = product.fromPeer.flatMap { .peer($0) } ?? .anonymous if let availability = gift.availability { diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift index 237d089380..3f42f70678 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift @@ -279,7 +279,7 @@ private final class SheetContent: CombinedComponent { case .starGiftResell: let amountInfoString: NSAttributedString if let value = state.amount?.value, value > 0 { - let starsValue = Int32(floor(Float(value) * Float(resaleConfiguration.paidMessageCommissionPermille) / 1000.0)) + let starsValue = Int32(floor(Float(value) * Float(resaleConfiguration.starGiftCommissionPermille) / 1000.0)) let starsString = environment.strings.Stars_SellGift_AmountInfo_Stars(starsValue) amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.Stars_SellGift_AmountInfo(starsString).string, attributes: amountMarkdownAttributes, textAlignment: .natural)) @@ -288,7 +288,7 @@ private final class SheetContent: CombinedComponent { amountRightLabel = "≈\(formatTonUsdValue(Int64(starsValue), divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat))" } } else { - amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.Stars_SellGift_AmountInfo("\(resaleConfiguration.paidMessageCommissionPermille / 10)%").string, attributes: amountMarkdownAttributes, textAlignment: .natural)) + amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.Stars_SellGift_AmountInfo("\(resaleConfiguration.starGiftCommissionPermille / 10)%").string, attributes: amountMarkdownAttributes, textAlignment: .natural)) } amountFooter = AnyComponent(MultilineTextComponent( text: .plain(amountInfoString), diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift index 725a27a253..b4e4c11784 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift @@ -6,7 +6,6 @@ import SwiftSignalKit import TelegramCore import Postbox import TelegramPresentationData -import UniversalMediaPlayer public final class StoryContentItem: Equatable { public final class ExternalState { @@ -33,7 +32,6 @@ public final class StoryContentItem: Equatable { public final class SharedState { public var replyDrafts: [StoryId: NSAttributedString] = [:] public var baseRate: Double = 1.0 - public var audioContext: ChunkMediaPlayerV2.AudioContext? public init() { } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index 513d067018..f7adeea7cd 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -15,275 +15,6 @@ import ButtonComponent import MultilineTextComponent import TelegramPresentationData -private protocol StoryVideoView: UIView { - var audioMode: StoryContentItem.AudioMode { get set } - var playbackCompleted: (() -> Void)? { get set } - var status: Signal { get } - - func play() - func pause() - func seek(timestamp: Double) - func setSoundMuted(soundMuted: Bool) - func continueWithOverridingAmbientMode(isAmbient: Bool) - func setBaseRate(baseRate: Double) - func update(size: CGSize, transition: ComponentTransition) -} - -private final class LegacyStoryVideoView: UIView, StoryVideoView { - private let videoNode: UniversalVideoNode - - var audioMode: StoryContentItem.AudioMode - var playbackCompleted: (() -> Void)? - - var status: Signal { - return self.videoNode.status - } - - init( - context: AccountContext, - file: FileMediaReference, - audioMode: StoryContentItem.AudioMode, - baseRate: Double, - isCaptureProtected: Bool - ) { - self.audioMode = audioMode - - var userLocation: MediaResourceUserLocation = .other - switch file { - case let .story(peer, _, _): - userLocation = .peer(peer.id) - default: - break - } - var hasSentFramesToDisplay: (() -> Void)? - self.videoNode = UniversalVideoNode( - context: context, - postbox: context.account.postbox, - audioSession: context.sharedContext.mediaManager.audioSession, - manager: context.sharedContext.mediaManager.universalVideoManager, - decoration: StoryVideoDecoration(), - content: NativeVideoContent( - id: .contextResult(0, "\(UInt64.random(in: 0 ... UInt64.max))"), - userLocation: userLocation, - fileReference: file, - imageReference: nil, - streamVideo: .story, - loopVideo: true, - enableSound: true, - soundMuted: audioMode == .off, - beginWithAmbientSound: audioMode == .ambient, - mixWithOthers: true, - useLargeThumbnail: false, - autoFetchFullSizeThumbnail: false, - tempFilePath: nil, - captureProtected: isCaptureProtected, - hintDimensions: file.media.dimensions?.cgSize, - storeAfterDownload: nil, - displayImage: false, - hasSentFramesToDisplay: { - hasSentFramesToDisplay?() - } - ), - priority: .gallery - ) - self.videoNode.isHidden = true - self.videoNode.setBaseRate(baseRate) - - super.init(frame: CGRect()) - - hasSentFramesToDisplay = { [weak self] in - guard let self else { - return - } - self.videoNode.isHidden = false - } - - self.videoNode.playbackCompleted = { [weak self] in - guard let self else { - return - } - self.playbackCompleted?() - } - - self.addSubview(self.videoNode.view) - - self.videoNode.ownsContentNodeUpdated = { [weak self] value in - guard let self else { - return - } - if value { - self.videoNode.seek(0.0) - if self.audioMode != .off { - self.videoNode.playOnceWithSound(playAndRecord: false, actionAtEnd: .stop) - } else { - self.videoNode.play() - } - } - } - self.videoNode.canAttachContent = true - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func play() { - self.videoNode.play() - } - - func pause() { - self.videoNode.pause() - } - - func seek(timestamp: Double) { - self.videoNode.seek(timestamp) - } - - func setSoundMuted(soundMuted: Bool) { - self.videoNode.setSoundMuted(soundMuted: soundMuted) - } - - func continueWithOverridingAmbientMode(isAmbient: Bool) { - self.videoNode.continueWithOverridingAmbientMode(isAmbient: isAmbient) - } - - func setBaseRate(baseRate: Double) { - self.videoNode.setBaseRate(baseRate) - } - - func update(size: CGSize, transition: ComponentTransition) { - transition.setFrame(view: self.videoNode.view, frame: CGRect(origin: CGPoint(), size: size)) - self.videoNode.updateLayout(size: size, transition: transition.containedViewLayoutTransition) - } -} - -private final class ModernStoryVideoView: UIView, StoryVideoView { - private let player: ChunkMediaPlayerV2 - private let playerNode: MediaPlayerNode - - var audioMode: StoryContentItem.AudioMode - var playbackCompleted: (() -> Void)? - var isFirstPlay: Bool = true - - var status: Signal { - return self.player.status |> map(Optional.init) - } - - init( - context: AccountContext, - audioContext: ChunkMediaPlayerV2.AudioContext, - file: FileMediaReference, - audioMode: StoryContentItem.AudioMode, - baseRate: Double, - isCaptureProtected: Bool - ) { - self.audioMode = audioMode - - self.playerNode = MediaPlayerNode( - backgroundThread: false, - captureProtected: isCaptureProtected - ) - - var userLocation: MediaResourceUserLocation = .other - switch file { - case let .story(peer, _, _): - userLocation = .peer(peer.id) - default: - break - } - - self.player = ChunkMediaPlayerV2( - params: ChunkMediaPlayerV2.MediaDataReaderParams(context: context), - audioContext: audioContext, - source: .directFetch(ChunkMediaPlayerV2.SourceDescription.ResourceDescription( - postbox: context.account.postbox, - size: file.media.size ?? 0, - reference: file.resourceReference(file.media.resource), - userLocation: userLocation, - userContentType: .story, - statsCategory: statsCategoryForFileWithAttributes(file.media.attributes), - fetchAutomatically: false - )), - video: true, - playAutomatically: false, - enableSound: true, - baseRate: baseRate, - soundMuted: audioMode == .off, - ambient: audioMode == .ambient, - mixWithOthers: true, - continuePlayingWithoutSoundOnLostAudioSession: false, - isAudioVideoMessage: false, - playerNode: self.playerNode - ) - self.playerNode.isHidden = true - self.player.setBaseRate(baseRate) - - super.init(frame: CGRect()) - - self.addSubview(self.playerNode.view) - - self.playerNode.hasSentFramesToDisplay = { [weak self] in - guard let self else { - return - } - self.playerNode.isHidden = false - } - - self.player.actionAtEnd = .action({ [weak self] in - guard let self else { - return - } - self.playbackCompleted?() - }) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func play() { - if self.isFirstPlay { - self.isFirstPlay = false - - if self.audioMode != .off { - self.player.playOnceWithSound(playAndRecord: false, seek: .start) - } else { - self.player.play() - } - } else { - self.player.play() - } - } - - func pause() { - self.player.pause() - } - - func seek(timestamp: Double) { - self.player.seek(timestamp: timestamp, play: nil) - } - - func setSoundMuted(soundMuted: Bool) { - self.player.setSoundMuted(soundMuted: soundMuted) - } - - func continueWithOverridingAmbientMode(isAmbient: Bool) { - self.player.continueWithOverridingAmbientMode(isAmbient: isAmbient) - } - - func setBaseRate(baseRate: Double) { - self.player.setBaseRate(baseRate) - } - - func update(size: CGSize, transition: ComponentTransition) { - transition.containedViewLayoutTransition.updateFrame(node: self.playerNode, frame: CGRect(origin: CGPoint(), size: size)) - } - - func updateNext(nextVideoView: ModernStoryVideoView?) { - self.player.migrateToNextPlayerOnEnd = nextVideoView?.player - } -} - final class StoryItemContentComponent: Component { typealias EnvironmentType = StoryContentItem.Environment @@ -360,11 +91,10 @@ final class StoryItemContentComponent: Component { final class View: StoryContentItem.View { private let imageView: StoryItemImageView private let overlaysView: StoryItemOverlaysView + private var videoNode: UniversalVideoNode? private var loadingEffectView: StoryItemLoadingEffectView? private var loadingEffectAppearanceTimer: SwiftSignalKit.Timer? - private var videoView: StoryVideoView? - private var mediaAreasEffectView: StoryItemLoadingEffectView? private var currentMessageMedia: EngineMedia? @@ -399,8 +129,6 @@ final class StoryItemContentComponent: Component { private var fetchPriorityResourceId: String? private var currentFetchPriority: (isMain: Bool, disposable: Disposable)? - private weak var nextItemView: StoryItemContentComponent.View? - override init(frame: CGRect) { self.hierarchyTrackingLayer = HierarchyTrackingLayer() self.imageView = StoryItemImageView() @@ -458,7 +186,10 @@ final class StoryItemContentComponent: Component { } private func initializeVideoIfReady(update: Bool) { - if self.videoView != nil { + if self.videoNode != nil { + return + } + if case .pause = self.progressMode { return } @@ -466,49 +197,48 @@ final class StoryItemContentComponent: Component { return } - var useLegacyImplementation = true - if let data = component.context.currentAppConfiguration.with({ $0 }).data, let value = data["ios_video_legacystoryplayer"] as? Double { - useLegacyImplementation = value != 0.0 - } - - if case .pause = self.progressMode { - if useLegacyImplementation { - return - } - } - if case let .file(file) = currentMessageMedia, let peerReference = PeerReference(component.peer._asPeer()) { - if self.videoView == nil { - let videoView: StoryVideoView - if useLegacyImplementation { - videoView = LegacyStoryVideoView( - context: component.context, - file: .story(peer: peerReference, id: component.item.id, media: file), - audioMode: component.audioMode, - baseRate: component.baseRate, - isCaptureProtected: component.item.isForwardingDisabled - ) - } else { - let audioContext: ChunkMediaPlayerV2.AudioContext - if let current = self.environment?.sharedState.audioContext { - audioContext = current - } else { - audioContext = ChunkMediaPlayerV2.AudioContext(audioSessionManager: component.context.sharedContext.mediaManager.audioSession) - self.environment?.sharedState.audioContext = audioContext - } - videoView = ModernStoryVideoView( - context: component.context, - audioContext: audioContext, - file: .story(peer: peerReference, id: component.item.id, media: file), - audioMode: component.audioMode, - baseRate: component.baseRate, - isCaptureProtected: component.item.isForwardingDisabled - ) - } - self.videoView = videoView - self.insertSubview(videoView, aboveSubview: self.imageView) + if self.videoNode == nil { + let videoNode = UniversalVideoNode( + context: component.context, + postbox: component.context.account.postbox, + audioSession: component.context.sharedContext.mediaManager.audioSession, + manager: component.context.sharedContext.mediaManager.universalVideoManager, + decoration: StoryVideoDecoration(), + content: NativeVideoContent( + id: .contextResult(0, "\(UInt64.random(in: 0 ... UInt64.max))"), + userLocation: .peer(peerReference.id), + fileReference: .story(peer: peerReference, id: component.item.id, media: file), + imageReference: nil, + streamVideo: .story, + loopVideo: true, + enableSound: true, + soundMuted: component.audioMode == .off, + beginWithAmbientSound: component.audioMode == .ambient, + mixWithOthers: true, + useLargeThumbnail: false, + autoFetchFullSizeThumbnail: false, + tempFilePath: nil, + captureProtected: component.item.isForwardingDisabled, + hintDimensions: file.dimensions?.cgSize, + storeAfterDownload: nil, + displayImage: false, + hasSentFramesToDisplay: { [weak self] in + guard let self else { + return + } + self.videoNode?.isHidden = false + } + ), + priority: .gallery + ) + videoNode.isHidden = true + videoNode.setBaseRate(component.baseRate) - videoView.playbackCompleted = { [weak self] in + self.videoNode = videoNode + self.insertSubview(videoNode.view, aboveSubview: self.imageView) + + videoNode.playbackCompleted = { [weak self] in guard let self else { return } @@ -523,24 +253,38 @@ final class StoryItemContentComponent: Component { if shouldLoop { self.rewind() - if let videoView = self.videoView { + if let videoNode = self.videoNode { if self.contentLoaded { - videoView.play() + videoNode.play() } } } else { self.environment?.presentationProgressUpdated(1.0, false, true) } } + videoNode.ownsContentNodeUpdated = { [weak self] value in + guard let self, let component = self.component else { + return + } + if value { + self.videoNode?.seek(0.0) + if component.audioMode != .off { + self.videoNode?.playOnceWithSound(playAndRecord: false, actionAtEnd: .stop) + } else { + self.videoNode?.play() + } + } + } + videoNode.canAttachContent = true if update { self.state?.updated(transition: .immediate) } } } - if let videoView = self.videoView { + if let videoNode = self.videoNode { if self.videoProgressDisposable == nil { - self.videoProgressDisposable = (videoView.status + self.videoProgressDisposable = (videoNode.status |> deliverOnMainQueue).start(next: { [weak self] status in guard let self, let status else { return @@ -552,17 +296,7 @@ final class StoryItemContentComponent: Component { } }) } - - let canPlay = self.progressMode != .pause && self.contentLoaded && self.hierarchyTrackingLayer.isInHierarchy - - if canPlay { - videoView.play() - } else { - videoView.pause() - } } - - self.updateVideoNextItem() } override func setProgressMode(_ progressMode: StoryContentItem.ProgressMode) { @@ -576,62 +310,48 @@ final class StoryItemContentComponent: Component { } } - func setNextItemView(nextItemView: StoryItemContentComponent.View?) { - if self.nextItemView !== nextItemView { - self.nextItemView = nextItemView - self.updateVideoNextItem() - } - } - - private func updateVideoNextItem() { - if let videoView = self.videoView as? ModernStoryVideoView { - let nextVideoView = self.nextItemView?.videoView as? ModernStoryVideoView - videoView.updateNext(nextVideoView: nextVideoView) - } - } - override func rewind() { self.currentProgressTimerValue = 0.0 - if let videoView = self.videoView { + if let videoNode = self.videoNode { if self.contentLoaded { - videoView.seek(timestamp: 0.0) + videoNode.seek(0.0) } } } override func leaveAmbientMode() { - if let videoView = self.videoView { + if let videoNode = self.videoNode { self.ignoreBufferingTimestamp = CFAbsoluteTimeGetCurrent() - videoView.setSoundMuted(soundMuted: false) - videoView.continueWithOverridingAmbientMode(isAmbient: false) + videoNode.setSoundMuted(soundMuted: false) + videoNode.continueWithOverridingAmbientMode(isAmbient: false) } } override func enterAmbientMode(ambient: Bool) { - if let videoView = self.videoView { + if let videoNode = self.videoNode { self.ignoreBufferingTimestamp = CFAbsoluteTimeGetCurrent() if ambient { - videoView.continueWithOverridingAmbientMode(isAmbient: true) + videoNode.continueWithOverridingAmbientMode(isAmbient: true) } else { - videoView.setSoundMuted(soundMuted: true) + videoNode.setSoundMuted(soundMuted: true) } } } override func setBaseRate(_ baseRate: Double) { - if let videoView = self.videoView { - videoView.setBaseRate(baseRate: baseRate) + if let videoNode = self.videoNode { + videoNode.setBaseRate(baseRate) } } private func updateProgressMode(update: Bool) { - if let videoView = self.videoView { + if let videoNode = self.videoNode { let canPlay = self.progressMode != .pause && self.contentLoaded && self.hierarchyTrackingLayer.isInHierarchy if canPlay { - videoView.play() + videoNode.play() } else { - videoView.pause() + videoNode.pause() } } @@ -846,11 +566,11 @@ final class StoryItemContentComponent: Component { private var isSeeking = false func seekTo(_ timestamp: Double, apply: Bool) { - guard let videoView = self.videoView else { + guard let videoNode = self.videoNode else { return } if apply { - videoView.seek(timestamp: min(timestamp, self.effectiveDuration - 0.3)) + videoNode.seek(min(timestamp, self.effectiveDuration - 0.3)) } self.isSeeking = true self.updateVideoPlaybackProgress(timestamp) @@ -868,10 +588,6 @@ final class StoryItemContentComponent: Component { let environment = environment[StoryContentItem.Environment.self].value self.environment = environment - if let videoView = self.videoView { - videoView.audioMode = component.audioMode - } - var synchronousLoad = false if let hint = transition.userData(Hint.self) { synchronousLoad = hint.synchronousLoad @@ -916,12 +632,12 @@ final class StoryItemContentComponent: Component { self.currentMessageMedia = messageMedia reloadMedia = true - if let videoView = self.videoView { + if let videoNode = self.videoNode { self.videoProgressDisposable?.dispose() self.videoProgressDisposable = nil - self.videoView = nil - videoView.removeFromSuperview() + self.videoNode = nil + videoNode.view.removeFromSuperview() } } self.currentMessageMetadataMedia = component.item.media @@ -1051,10 +767,10 @@ final class StoryItemContentComponent: Component { } let _ = imageSize - if let videoView = self.videoView { + if let videoNode = self.videoNode { let videoSize = dimensions.aspectFilled(availableSize) - videoView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - videoSize.width) * 0.5), y: floor((availableSize.height - videoSize.height) * 0.5)), size: videoSize) - videoView.update(size: videoSize, transition: .immediate) + videoNode.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - videoSize.width) * 0.5), y: floor((availableSize.height - videoSize.height) * 0.5)), size: videoSize) + videoNode.updateLayout(size: videoSize, transition: .immediate) } } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index f88ddc8fd8..856df6d79b 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -1478,7 +1478,7 @@ public final class StoryItemSetContainerComponent: Component { } if itemLayout.contentScaleFraction <= 0.0001 && !self.preparingToDisplayViewList { - if index != centralIndex && index != centralIndex + 1 { + if index != centralIndex { itemVisible = false } } @@ -1870,19 +1870,6 @@ public final class StoryItemSetContainerComponent: Component { } } - for i in 0 ..< component.slice.allItems.count { - guard let visibleItem = self.visibleItems[component.slice.allItems[i].id] else { - continue - } - var nextVisibleItem: VisibleItem? - if i != component.slice.allItems.count - 1 { - nextVisibleItem = self.visibleItems[component.slice.allItems[i + 1].id] - } - if let itemView = visibleItem.view.view as? StoryItemContentComponent.View { - itemView.setNextItemView(nextItemView: nextVisibleItem?.view.view as? StoryItemContentComponent.View) - } - } - self.trulyValidIds = trulyValidIds var removeIds: [StoryId] = [] diff --git a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift index f107fd6366..efce5db5c6 100644 --- a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift +++ b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift @@ -117,6 +117,8 @@ public final class TabSelectorComponent: Component { private let selectionView: UIImageView private var visibleItems: [AnyHashable: VisibleItem] = [:] + private var didInitiallyScroll = false + override init(frame: CGRect) { self.selectionView = UIImageView() @@ -238,11 +240,15 @@ public final class TabSelectorComponent: Component { )), effectAlignment: .center, minSize: nil, - action: { [weak self] in + action: { [weak self, weak itemView] in guard let self, let component = self.component else { return } component.setSelectedId(itemId) + + if let view = itemView?.title.view, allowScroll && self.contentSize.width > self.bounds.width { + self.scrollRectToVisible(view.frame.insetBy(dx: -64.0, dy: 0.0), animated: true) + } }, animateScale: !isLineSelection )), @@ -336,11 +342,15 @@ public final class TabSelectorComponent: Component { self.selectionView.alpha = 0.0 } - self.contentSize = CGSize(width: contentWidth, height: baseHeight + verticalInset * 2.0) + let contentSize = CGSize(width: contentWidth, height: baseHeight + verticalInset * 2.0) + if self.contentSize != contentSize { + self.contentSize = contentSize + } self.disablesInteractiveTransitionGestureRecognizer = contentWidth > availableSize.width - if let selectedBackgroundRect, self.bounds.width > 0.0 { + if let selectedBackgroundRect, self.bounds.width > 0.0 && !self.didInitiallyScroll { self.scrollRectToVisible(selectedBackgroundRect.insetBy(dx: -spacing, dy: 0.0), animated: false) + self.didInitiallyScroll = true } return CGSize(width: min(contentWidth, availableSize.width), height: baseHeight + verticalInset * 2.0) diff --git a/submodules/TelegramUI/Images.xcassets/Wallet/QrIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Wallet/QrIcon.imageset/Contents.json deleted file mode 100644 index f8015db3bc..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Wallet/QrIcon.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "ic_qrcode.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Wallet/QrIcon.imageset/ic_qrcode.pdf b/submodules/TelegramUI/Images.xcassets/Wallet/QrIcon.imageset/ic_qrcode.pdf deleted file mode 100644 index cf234b8223..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Wallet/QrIcon.imageset/ic_qrcode.pdf and /dev/null differ diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index 6223b0d385..010ccc7f6e 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -127,2042 +127,6 @@ import PostSuggestionsSettingsScreen import ChatSendStarsScreen extension ChatControllerImpl { - func reloadChatLocation() { - let context = self.context - let chatLocation = self.chatLocation - let chatLocationPeerId: PeerId? = self.chatLocation.peerId - let mode = self.mode - let subject = self.subject - let peerId = chatLocationPeerId - - switch chatLocation { - case .peer: - self.chatLocationInfoData = .peer(Promise()) - case let .replyThread(replyThreadMessage): - let promise = Promise() - if let effectiveMessageId = replyThreadMessage.effectiveMessageId { - promise.set(context.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.Message(id: effectiveMessageId)) - |> map { message -> Message? in - guard let message = message else { - return nil - } - return message._asMessage() - }) - } else { - promise.set(.single(nil)) - } - self.chatLocationInfoData = .replyThread(promise) - case .customChatContents: - self.chatLocationInfoData = .customChatContents - } - - let managingBot: Signal - if let peerId = self.chatLocation.peerId, peerId.namespace == Namespaces.Peer.CloudUser { - managingBot = self.context.engine.data.subscribe( - TelegramEngine.EngineData.Item.Peer.ChatManagingBot(id: peerId) - ) - |> mapToSignal { result -> Signal in - guard let result else { - return .single(nil) - } - return context.engine.data.subscribe( - TelegramEngine.EngineData.Item.Peer.Peer(id: result.id) - ) - |> map { botPeer -> ChatManagingBot? in - guard let botPeer else { - return nil - } - - return ChatManagingBot(bot: botPeer, isPaused: result.isPaused, canReply: result.canReply, settingsUrl: result.manageUrl) - } - } - |> distinctUntilChanged - } else { - managingBot = .single(nil) - } - - if case let .peer(peerView) = self.chatLocationInfoData, let peerId = peerId { - peerView.set(context.account.viewTracker.peerView(peerId)) - var onlineMemberCount: Signal<(total: Int32?, recent: Int32?), NoError> = .single((nil, nil)) - var hasScheduledMessages: Signal = .single(false) - - if peerId.namespace == Namespaces.Peer.CloudChannel { - let recentOnlineSignal: Signal<(total: Int32?, recent: Int32?), NoError> = peerView.get() - |> map { view -> Bool? in - if let cachedData = view.cachedData as? CachedChannelData, let peer = peerViewMainPeer(view) as? TelegramChannel { - if case .broadcast = peer.info { - return nil - } else if let memberCount = cachedData.participantsSummary.memberCount, memberCount > 50 { - return true - } else { - return false - } - } else { - return false - } - } - |> distinctUntilChanged - |> mapToSignal { isLarge -> Signal<(total: Int32?, recent: Int32?), NoError> in - if let isLarge = isLarge { - if isLarge { - return context.peerChannelMemberCategoriesContextsManager.recentOnline(account: context.account, accountPeerId: context.account.peerId, peerId: peerId) - |> map { value -> (total: Int32?, recent: Int32?) in - return (nil, value) - } - } else { - return context.peerChannelMemberCategoriesContextsManager.recentOnlineSmall(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId) - |> map { value -> (total: Int32?, recent: Int32?) in - return (value.total, value.recent) - } - } - } else { - return .single((nil, nil)) - } - } - onlineMemberCount = recentOnlineSignal - - self.reportIrrelvantGeoNoticePromise.set(context.engine.data.get(TelegramEngine.EngineData.Item.Notices.Notice(key: ApplicationSpecificNotice.irrelevantPeerGeoReportKey(peerId: peerId))) - |> map { entry -> Bool? in - if let _ = entry?.get(ApplicationSpecificBoolNotice.self) { - return true - } else { - return false - } - }) - } else { - self.reportIrrelvantGeoNoticePromise.set(.single(nil)) - } - - var isScheduledOrPinnedMessages = false - switch self.subject { - case .scheduledMessages, .pinnedMessages, .messageOptions: - isScheduledOrPinnedMessages = true - default: - break - } - - if chatLocation.peerId != nil, !isScheduledOrPinnedMessages, peerId.namespace != Namespaces.Peer.SecretChat { - let chatLocationContextHolder = self.chatLocationContextHolder - hasScheduledMessages = peerView.get() - |> take(1) - |> mapToSignal { view -> Signal in - if let peer = peerViewMainPeer(view) as? TelegramChannel, !peer.hasPermission(.sendSomething) { - return .single(false) - } else { - return context.account.viewTracker.scheduledMessagesViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder)) - |> map { view, _, _ in - return !view.entries.isEmpty - } - } - } - } - - var displayedCountSignal: Signal = .single(nil) - var subtitleTextSignal: Signal = .single(nil) - if case .pinnedMessages = subject { - displayedCountSignal = self.topPinnedMessageSignal(latest: true) - |> map { message -> Int? in - return message?.totalCount - } - |> distinctUntilChanged - } else if case let .messageOptions(peerIds, messageIds, info) = subject { - displayedCountSignal = self.presentationInterfaceStatePromise.get() - |> map { state -> Int? in - if let selectionState = state.interfaceState.selectionState { - return selectionState.selectedIds.count - } else { - return messageIds.count - } - } - |> distinctUntilChanged - - let peers = self.context.account.postbox.multiplePeersView(peerIds) - |> take(1) - - let presentationData = self.presentationData - - switch info { - case let .forward(forward): - subtitleTextSignal = combineLatest(peers, forward.options, displayedCountSignal) - |> map { peersView, options, count in - let peers = peersView.peers.values - if !peers.isEmpty { - if peers.count == 1, let peer = peers.first { - if let peer = peer as? TelegramUser { - let displayName = EnginePeer(peer).compactDisplayTitle - if count == 1 { - if options.hideNames { - return presentationData.strings.Conversation_ForwardOptions_UserMessageForwardHidden(displayName).string - } else { - return presentationData.strings.Conversation_ForwardOptions_UserMessageForwardVisible(displayName).string - } - } else { - if options.hideNames { - return presentationData.strings.Conversation_ForwardOptions_UserMessagesForwardHidden(displayName).string - } else { - return presentationData.strings.Conversation_ForwardOptions_UserMessagesForwardVisible(displayName).string - } - } - } else if let peer = peer as? TelegramChannel, case .broadcast = peer.info { - if count == 1 { - if options.hideNames { - return presentationData.strings.Conversation_ForwardOptions_ChannelMessageForwardHidden - } else { - return presentationData.strings.Conversation_ForwardOptions_ChannelMessageForwardVisible - } - } else { - if options.hideNames { - return presentationData.strings.Conversation_ForwardOptions_ChannelMessagesForwardHidden - } else { - return presentationData.strings.Conversation_ForwardOptions_ChannelMessagesForwardVisible - } - } - } else { - if count == 1 { - if options.hideNames { - return presentationData.strings.Conversation_ForwardOptions_GroupMessageForwardHidden - } else { - return presentationData.strings.Conversation_ForwardOptions_GroupMessageForwardVisible - } - } else { - if options.hideNames { - return presentationData.strings.Conversation_ForwardOptions_GroupMessagesForwardHidden - } else { - return presentationData.strings.Conversation_ForwardOptions_GroupMessagesForwardVisible - } - } - } - } else { - if count == 1 { - if options.hideNames { - return presentationData.strings.Conversation_ForwardOptions_RecipientsMessageForwardHidden - } else { - return presentationData.strings.Conversation_ForwardOptions_RecipientsMessageForwardVisible - } - } else { - if options.hideNames { - return presentationData.strings.Conversation_ForwardOptions_RecipientsMessagesForwardHidden - } else { - return presentationData.strings.Conversation_ForwardOptions_RecipientsMessagesForwardVisible - } - } - } - } else { - return nil - } - } - case let .reply(reply): - subtitleTextSignal = reply.selectionState.get() - |> map { selectionState -> String? in - if !selectionState.canQuote { - return nil - } - return presentationData.strings.Chat_SubtitleQuoteSelectionTip - } - case let .link(link): - subtitleTextSignal = link.options - |> map { options -> String? in - if options.hasAlternativeLinks { - return presentationData.strings.Chat_SubtitleLinkListTip - } else { - return nil - } - } - |> distinctUntilChanged - } - } - - let hasPeerInfo: Signal - if peerId == context.account.peerId { - hasPeerInfo = .single(true) - |> then( - hasAvailablePeerInfoMediaPanes(context: context, peerId: peerId) - ) - } else { - hasPeerInfo = .single(true) - } - - enum MessageOptionsTitleInfo { - case reply(hasQuote: Bool) - } - let messageOptionsTitleInfo: Signal - if case let .messageOptions(_, _, info) = self.subject { - switch info { - case .forward, .link: - messageOptionsTitleInfo = .single(nil) - case let .reply(reply): - messageOptionsTitleInfo = reply.selectionState.get() - |> map { selectionState -> Bool in - return selectionState.quote != nil - } - |> distinctUntilChanged - |> map { hasQuote -> MessageOptionsTitleInfo in - return .reply(hasQuote: hasQuote) - } - } - } else { - messageOptionsTitleInfo = .single(nil) - } - - self.titleDisposable.set((combineLatest(queue: Queue.mainQueue(), peerView.get(), onlineMemberCount, displayedCountSignal, subtitleTextSignal, self.presentationInterfaceStatePromise.get(), hasPeerInfo, messageOptionsTitleInfo) - |> deliverOnMainQueue).startStrict(next: { [weak self] peerView, onlineMemberCount, displayedCount, subtitleText, presentationInterfaceState, hasPeerInfo, messageOptionsTitleInfo in - if let strongSelf = self { - var isScheduledMessages = false - if case .scheduledMessages = presentationInterfaceState.subject { - isScheduledMessages = true - } - - if case let .messageOptions(_, _, info) = presentationInterfaceState.subject { - if case .reply = info { - let titleContent: ChatTitleContent - if case let .reply(hasQuote) = messageOptionsTitleInfo, hasQuote { - titleContent = .custom(presentationInterfaceState.strings.Chat_TitleQuoteSelection, subtitleText, false) - } else { - titleContent = .custom(presentationInterfaceState.strings.Chat_TitleReply, subtitleText, false) - } - if strongSelf.chatTitleView?.titleContent != titleContent { - if strongSelf.chatTitleView?.titleContent != nil { - strongSelf.chatTitleView?.animateLayoutTransition() - } - strongSelf.chatTitleView?.titleContent = titleContent - } - } else if case .link = info { - strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Chat_TitleLinkOptions, subtitleText, false) - } else if displayedCount == 1 { - strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Conversation_ForwardOptions_ForwardTitleSingle, subtitleText, false) - } else { - strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Conversation_ForwardOptions_ForwardTitle(Int32(displayedCount ?? 1)), subtitleText, false) - } - } else if let selectionState = presentationInterfaceState.interfaceState.selectionState { - if selectionState.selectedIds.count > 0 { - strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Conversation_SelectedMessages(Int32(selectionState.selectedIds.count)), nil, false) - } else { - if let (title, _, _) = presentationInterfaceState.reportReason { - strongSelf.chatTitleView?.titleContent = .custom(title, presentationInterfaceState.strings.Conversation_SelectMessages, false) - } else { - strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Conversation_SelectMessages, nil, false) - } - } - } else if let peer = peerViewMainPeer(peerView) { - if case .pinnedMessages = presentationInterfaceState.subject { - strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Chat_TitlePinnedMessages(Int32(displayedCount ?? 1)), nil, false) - } else { - strongSelf.chatTitleView?.titleContent = .peer(peerView: ChatTitleContent.PeerData(peerView: peerView), customTitle: nil, onlineMemberCount: onlineMemberCount, isScheduledMessages: isScheduledMessages, isMuted: nil, customMessageCount: nil, isEnabled: hasPeerInfo) - let imageOverride: AvatarNodeImageOverride? - if strongSelf.context.account.peerId == peer.id { - imageOverride = .savedMessagesIcon - } else if peer.id.isReplies { - imageOverride = .repliesIcon - } else if peer.id.isAnonymousSavedMessages { - imageOverride = .anonymousSavedMessagesIcon(isColored: true) - } else if peer.isDeleted { - imageOverride = .deletedIcon - } else { - imageOverride = nil - } - (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.setPeer(context: strongSelf.context, theme: strongSelf.presentationData.theme, peer: EnginePeer(peer), overrideImage: imageOverride) - if case .standard(.previewing) = strongSelf.mode { - (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.contextActionIsEnabled = false - } else { - (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.contextActionIsEnabled = peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil - } - strongSelf.chatInfoNavigationButton?.buttonItem.accessibilityLabel = presentationInterfaceState.strings.Conversation_ContextMenuOpenProfile - - strongSelf.storyStats = peerView.storyStats - if let avatarNode = strongSelf.avatarNode { - avatarNode.avatarNode.setStoryStats(storyStats: peerView.storyStats.flatMap { storyStats -> AvatarNode.StoryStats? in - if storyStats.totalCount == 0 { - return nil - } - if storyStats.unseenCount == 0 { - return nil - } - return AvatarNode.StoryStats( - totalCount: storyStats.totalCount, - unseenCount: storyStats.unseenCount, - hasUnseenCloseFriendsItems: storyStats.hasUnseenCloseFriends - ) - }, presentationParams: AvatarNode.StoryPresentationParams( - colors: AvatarNode.Colors(theme: strongSelf.presentationData.theme), - lineWidth: 1.5, - inactiveLineWidth: 1.5 - ), transition: .immediate) - } - } - } - } - })) - - let threadInfo: Signal - if let threadId = self.chatLocation.threadId { - let viewKey: PostboxViewKey = .messageHistoryThreadInfo(peerId: peerId, threadId: threadId) - threadInfo = context.account.postbox.combinedView(keys: [viewKey]) - |> map { views -> EngineMessageHistoryThread.Info? in - guard let view = views.views[viewKey] as? MessageHistoryThreadInfoView else { - return nil - } - guard let data = view.info?.data.get(MessageHistoryThreadData.self) else { - return nil - } - return data.info - } - |> distinctUntilChanged - } else { - threadInfo = .single(nil) - } - - let hasSearchTags: Signal - if let peerId = self.chatLocation.peerId, peerId == context.account.peerId { - hasSearchTags = context.engine.data.subscribe( - TelegramEngine.EngineData.Item.Messages.SavedMessageTagStats(peerId: context.account.peerId, threadId: self.chatLocation.threadId) - ) - |> map { tags -> Bool in - return !tags.isEmpty - } - |> distinctUntilChanged - } else { - hasSearchTags = .single(false) - } - - let hasSavedChats: Signal - if case .peer(context.account.peerId) = self.chatLocation { - hasSavedChats = context.engine.messages.savedMessagesHasPeersOtherThanSaved() - } else { - hasSavedChats = .single(false) - } - - let isPremiumRequiredForMessaging: Signal - if let peerId = self.chatLocation.peerId { - isPremiumRequiredForMessaging = context.engine.peers.subscribeIsPremiumRequiredForMessaging(id: peerId) - |> distinctUntilChanged - } else { - isPremiumRequiredForMessaging = .single(false) - } - - let adMessage: Signal - if let adMessagesContext = self.chatDisplayNode.historyNode.adMessagesContext { - adMessage = adMessagesContext.state |> map { $0.messages.first } - } else { - adMessage = .single(nil) - } - - let displayedPeerVerification: Signal - if let peerId = self.chatLocation.peerId { - displayedPeerVerification = ApplicationSpecificNotice.displayedPeerVerification(accountManager: context.sharedContext.accountManager, peerId: peerId) - |> take(1) - } else { - displayedPeerVerification = .single(false) - } - - let globalPrivacySettings = context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.GlobalPrivacy()) - - self.peerDisposable.set(combineLatest( - queue: Queue.mainQueue(), - peerView.get(), - context.engine.data.subscribe(TelegramEngine.EngineData.Item.NotificationSettings.Global()), - onlineMemberCount, - hasScheduledMessages, - self.reportIrrelvantGeoNoticePromise.get(), - displayedCountSignal, - threadInfo, - hasSearchTags, - hasSavedChats, - isPremiumRequiredForMessaging, - managingBot, - adMessage, - displayedPeerVerification, - globalPrivacySettings - ).startStrict(next: { [weak self] peerView, globalNotificationSettings, onlineMemberCount, hasScheduledMessages, peerReportNotice, pinnedCount, threadInfo, hasSearchTags, hasSavedChats, isPremiumRequiredForMessaging, managingBot, adMessage, displayedPeerVerification, globalPrivacySettings in - if let strongSelf = self { - if strongSelf.peerView === peerView && strongSelf.reportIrrelvantGeoNotice == peerReportNotice && strongSelf.hasScheduledMessages == hasScheduledMessages && strongSelf.threadInfo == threadInfo && strongSelf.presentationInterfaceState.hasSearchTags == hasSearchTags && strongSelf.presentationInterfaceState.hasSavedChats == hasSavedChats && strongSelf.presentationInterfaceState.isPremiumRequiredForMessaging == isPremiumRequiredForMessaging && managingBot == strongSelf.presentationInterfaceState.contactStatus?.managingBot && adMessage?.id == strongSelf.presentationInterfaceState.adMessage?.id { - return - } - - strongSelf.reportIrrelvantGeoNotice = peerReportNotice - strongSelf.hasScheduledMessages = hasScheduledMessages - - var upgradedToPeerId: PeerId? - var movedToForumTopics = false - if let previous = strongSelf.peerView, let group = previous.peers[previous.peerId] as? TelegramGroup, group.migrationReference == nil, let updatedGroup = peerView.peers[peerView.peerId] as? TelegramGroup, let migrationReference = updatedGroup.migrationReference { - upgradedToPeerId = migrationReference.peerId - } - if let previous = strongSelf.peerView, let channel = previous.peers[previous.peerId] as? TelegramChannel, !channel.flags.contains(.isForum), let updatedChannel = peerView.peers[peerView.peerId] as? TelegramChannel, updatedChannel.flags.contains(.isForum) { - movedToForumTopics = true - } - - var shouldDismiss = false - if let previous = strongSelf.peerView, let group = previous.peers[previous.peerId] as? TelegramGroup, group.membership != .Removed, let updatedGroup = peerView.peers[peerView.peerId] as? TelegramGroup, updatedGroup.membership == .Removed { - shouldDismiss = true - } else if let previous = strongSelf.peerView, let channel = previous.peers[previous.peerId] as? TelegramChannel, channel.participationStatus != .kicked, let updatedChannel = peerView.peers[peerView.peerId] as? TelegramChannel, updatedChannel.participationStatus == .kicked { - shouldDismiss = true - } else if let previous = strongSelf.peerView, let secretChat = previous.peers[previous.peerId] as? TelegramSecretChat, case .active = secretChat.embeddedState, let updatedSecretChat = peerView.peers[peerView.peerId] as? TelegramSecretChat, case .terminated = updatedSecretChat.embeddedState { - shouldDismiss = true - } - - var wasGroupChannel: Bool? - if let previousPeerView = strongSelf.peerView, let info = (previousPeerView.peers[previousPeerView.peerId] as? TelegramChannel)?.info { - if case .group = info { - wasGroupChannel = true - } else { - wasGroupChannel = false - } - } - var isGroupChannel: Bool? - if let info = (peerView.peers[peerView.peerId] as? TelegramChannel)?.info { - if case .group = info { - isGroupChannel = true - } else { - isGroupChannel = false - } - } - let firstTime = strongSelf.peerView == nil - strongSelf.peerView = peerView - strongSelf.threadInfo = threadInfo - if wasGroupChannel != isGroupChannel { - if let isGroupChannel = isGroupChannel, isGroupChannel { - let (recentDisposable, _) = strongSelf.context.peerChannelMemberCategoriesContextsManager.recent(engine: strongSelf.context.engine, postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, accountPeerId: context.account.peerId, peerId: peerView.peerId, updated: { _ in }) - let (adminsDisposable, _) = strongSelf.context.peerChannelMemberCategoriesContextsManager.admins(engine: strongSelf.context.engine, postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, accountPeerId: context.account.peerId, peerId: peerView.peerId, updated: { _ in }) - let disposable = DisposableSet() - disposable.add(recentDisposable) - disposable.add(adminsDisposable) - strongSelf.chatAdditionalDataDisposable.set(disposable) - } else { - strongSelf.chatAdditionalDataDisposable.set(nil) - } - } - if strongSelf.isNodeLoaded { - strongSelf.chatDisplayNode.overlayTitle = strongSelf.overlayTitle - } - var peerIsMuted = false - if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings { - if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { - peerIsMuted = true - } else if case .default = notificationSettings.muteState { - if let peer = peerView.peers[peerView.peerId] { - if peer is TelegramUser { - peerIsMuted = !globalNotificationSettings.privateChats.enabled - } else if peer is TelegramGroup { - peerIsMuted = !globalNotificationSettings.groupChats.enabled - } else if let channel = peer as? TelegramChannel { - switch channel.info { - case .group: - peerIsMuted = !globalNotificationSettings.groupChats.enabled - case .broadcast: - peerIsMuted = !globalNotificationSettings.channels.enabled - } - } - } - } - } - var starGiftsAvailable = false - var peerDiscussionId: PeerId? - var peerGeoLocation: PeerGeoLocation? - if let peer = peerView.peers[peerView.peerId] as? TelegramChannel, let cachedData = peerView.cachedData as? CachedChannelData { - if case .broadcast = peer.info { - starGiftsAvailable = cachedData.flags.contains(.starGiftsAvailable) - } else { - peerGeoLocation = cachedData.peerGeoLocation - } - if case let .known(value) = cachedData.linkedDiscussionPeerId { - peerDiscussionId = value - } - } - var renderedPeer: RenderedPeer? - var contactStatus: ChatContactStatus? - var businessIntro: TelegramBusinessIntro? - var sendPaidMessageStars: StarsAmount? - var alwaysShowGiftButton = false - var disallowedGifts: TelegramDisallowedGifts? - if let peer = peerView.peers[peerView.peerId] { - if let cachedData = peerView.cachedData as? CachedUserData { - contactStatus = ChatContactStatus(canAddContact: !peerView.peerIsContact, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: nil, managingBot: managingBot) - if case let .known(value) = cachedData.businessIntro { - businessIntro = value - } - if case let .peer(peerId) = chatLocation, peerId.namespace == Namespaces.Peer.SecretChat { - } else { - sendPaidMessageStars = cachedData.sendPaidMessageStars - if cachedData.disallowedGifts != .All { - alwaysShowGiftButton = globalPrivacySettings.displayGiftButton || cachedData.flags.contains(.displayGiftButton) - } - disallowedGifts = cachedData.disallowedGifts - } - } else if let cachedData = peerView.cachedData as? CachedGroupData { - var invitedBy: Peer? - if let invitedByPeerId = cachedData.invitedBy { - if let peer = peerView.peers[invitedByPeerId] { - invitedBy = peer - } - } - contactStatus = ChatContactStatus(canAddContact: false, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy, managingBot: managingBot) - } else if let cachedData = peerView.cachedData as? CachedChannelData { - var canReportIrrelevantLocation = true - if let peer = peerView.peers[peerView.peerId] as? TelegramChannel, peer.participationStatus == .member { - canReportIrrelevantLocation = false - } - if let peerReportNotice = peerReportNotice, peerReportNotice { - canReportIrrelevantLocation = false - } - var invitedBy: Peer? - if let invitedByPeerId = cachedData.invitedBy { - if let peer = peerView.peers[invitedByPeerId] { - invitedBy = peer - } - } - contactStatus = ChatContactStatus(canAddContact: false, canReportIrrelevantLocation: canReportIrrelevantLocation, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy, managingBot: managingBot) - - if let channel = peerView.peers[peerView.peerId] as? TelegramChannel { - if channel.flags.contains(.isCreator) || channel.adminRights != nil { - } else { - sendPaidMessageStars = channel.sendPaidMessageStars - } - } - } - - var peers = SimpleDictionary() - peers[peer.id] = peer - if let associatedPeerId = peer.associatedPeerId, let associatedPeer = peerView.peers[associatedPeerId] { - peers[associatedPeer.id] = associatedPeer - } - renderedPeer = RenderedPeer(peerId: peer.id, peers: peers, associatedMedia: peerView.media) - } - - var isNotAccessible: Bool = false - if let cachedChannelData = peerView.cachedData as? CachedChannelData { - isNotAccessible = cachedChannelData.isNotAccessible - } - - if firstTime && isNotAccessible { - strongSelf.context.account.viewTracker.forceUpdateCachedPeerData(peerId: peerView.peerId) - } - - var hasBots: Bool = false - var hasBotCommands: Bool = false - var botMenuButton: BotMenuButton = .commands - var currentSendAsPeerId: PeerId? - var autoremoveTimeout: Int32? - var copyProtectionEnabled: Bool = false - var hasBirthdayToday = false - var peerVerification: PeerVerification? - if let peer = peerView.peers[peerView.peerId] { - if !displayedPeerVerification { - if let cachedUserData = peerView.cachedData as? CachedUserData { - peerVerification = cachedUserData.verification - } else if let cachedChannelData = peerView.cachedData as? CachedChannelData { - peerVerification = cachedChannelData.verification - } - } - copyProtectionEnabled = peer.isCopyProtectionEnabled - if let cachedGroupData = peerView.cachedData as? CachedGroupData { - if !cachedGroupData.botInfos.isEmpty { - hasBots = true - } - let botCommands = cachedGroupData.botInfos.reduce(into: [], { result, info in - result.append(contentsOf: info.botInfo.commands) - }) - if !botCommands.isEmpty { - hasBotCommands = true - } - if case let .known(value) = cachedGroupData.autoremoveTimeout { - autoremoveTimeout = value?.effectiveValue - } - } else if let cachedChannelData = peerView.cachedData as? CachedChannelData { - currentSendAsPeerId = cachedChannelData.sendAsPeerId - if let channel = peer as? TelegramChannel, case .group = channel.info { - if !cachedChannelData.botInfos.isEmpty { - hasBots = true - } - let botCommands = cachedChannelData.botInfos.reduce(into: [], { result, info in - result.append(contentsOf: info.botInfo.commands) - }) - if !botCommands.isEmpty { - hasBotCommands = true - } - } - if case let .known(value) = cachedChannelData.autoremoveTimeout { - autoremoveTimeout = value?.effectiveValue - } - } else if let cachedUserData = peerView.cachedData as? CachedUserData { - botMenuButton = cachedUserData.botInfo?.menuButton ?? .commands - if case let .known(value) = cachedUserData.autoremoveTimeout { - autoremoveTimeout = value?.effectiveValue - } - if let botInfo = cachedUserData.botInfo, !botInfo.commands.isEmpty { - hasBotCommands = true - } - if let birthday = cachedUserData.birthday { - let today = Calendar.current.dateComponents(Set([.day, .month]), from: Date()) - if today.day == Int(birthday.day) && today.month == Int(birthday.month) { - hasBirthdayToday = true - } - } - } - } - - let isArchived: Bool = peerView.groupId == Namespaces.PeerGroup.archive - - var explicitelyCanPinMessages: Bool = false - if let cachedUserData = peerView.cachedData as? CachedUserData { - explicitelyCanPinMessages = cachedUserData.canPinMessages - } else if peerView.peerId == context.account.peerId { - explicitelyCanPinMessages = true - } - - var animated = false - if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat, let updated = renderedPeer?.peer as? TelegramSecretChat, peer.embeddedState != updated.embeddedState { - animated = true - } - if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, let updated = renderedPeer?.peer as? TelegramChannel { - if peer.participationStatus != updated.participationStatus { - animated = true - } - } - - var didDisplayActionsPanel = false - if let contactStatus = strongSelf.presentationInterfaceState.contactStatus, !contactStatus.isEmpty, let peerStatusSettings = contactStatus.peerStatusSettings { - if !peerStatusSettings.flags.isEmpty { - if contactStatus.canAddContact && peerStatusSettings.contains(.canAddContact) { - didDisplayActionsPanel = true - } else if peerStatusSettings.contains(.canReport) || peerStatusSettings.contains(.canBlock) { - didDisplayActionsPanel = true - } else if peerStatusSettings.contains(.canShareContact) { - didDisplayActionsPanel = true - } else if contactStatus.canReportIrrelevantLocation && peerStatusSettings.contains(.canReportIrrelevantGeoLocation) { - didDisplayActionsPanel = true - } else if peerStatusSettings.contains(.suggestAddMembers) { - didDisplayActionsPanel = true - } - } - } - if let contactStatus = strongSelf.presentationInterfaceState.contactStatus, contactStatus.managingBot != nil { - didDisplayActionsPanel = true - } - if strongSelf.presentationInterfaceState.search != nil && strongSelf.presentationInterfaceState.hasSearchTags { - didDisplayActionsPanel = true - } - - var displayActionsPanel = false - if let contactStatus = contactStatus, !contactStatus.isEmpty, let peerStatusSettings = contactStatus.peerStatusSettings { - if !peerStatusSettings.flags.isEmpty { - if contactStatus.canAddContact && peerStatusSettings.contains(.canAddContact) { - displayActionsPanel = true - } else if peerStatusSettings.contains(.canReport) || peerStatusSettings.contains(.canBlock) { - displayActionsPanel = true - } else if peerStatusSettings.contains(.canShareContact) { - displayActionsPanel = true - } else if contactStatus.canReportIrrelevantLocation && peerStatusSettings.contains(.canReportIrrelevantGeoLocation) { - displayActionsPanel = true - } else if peerStatusSettings.contains(.suggestAddMembers) { - displayActionsPanel = true - } - } - } - if let contactStatus, contactStatus.managingBot != nil { - displayActionsPanel = true - } - if strongSelf.presentationInterfaceState.search != nil && hasSearchTags { - displayActionsPanel = true - } - - if displayActionsPanel != didDisplayActionsPanel { - animated = true - } - - if strongSelf.preloadHistoryPeerId != peerDiscussionId { - strongSelf.preloadHistoryPeerId = peerDiscussionId - if let peerDiscussionId = peerDiscussionId, let channel = peerView.peers[peerView.peerId] as? TelegramChannel, case .broadcast = channel.info { - let combinedDisposable = DisposableSet() - strongSelf.preloadHistoryPeerIdDisposable.set(combinedDisposable) - combinedDisposable.add(strongSelf.context.account.viewTracker.polledChannel(peerId: peerDiscussionId).startStrict()) - combinedDisposable.add(strongSelf.context.account.addAdditionalPreloadHistoryPeerId(peerId: peerDiscussionId)) - } else { - strongSelf.preloadHistoryPeerIdDisposable.set(nil) - } - } - - var appliedBoosts: Int32? - var boostsToUnrestrict: Int32? - if let cachedChannelData = peerView.cachedData as? CachedChannelData { - appliedBoosts = cachedChannelData.appliedBoosts - boostsToUnrestrict = cachedChannelData.boostsToUnrestrict - } - - if strongSelf.premiumOrStarsRequiredDisposable == nil, sendPaidMessageStars != nil, let peerId = strongSelf.chatLocation.peerId { - strongSelf.premiumOrStarsRequiredDisposable = ((strongSelf.context.engine.peers.isPremiumRequiredToContact([peerId]) |> then(.complete() |> suspendAwareDelay(60.0, queue: Queue.concurrentDefaultQueue()))) |> restart).startStandalone() - } - - var adMessage = adMessage - if let peer = peerView.peers[peerView.peerId] as? TelegramUser, peer.botInfo != nil { - } else { - adMessage = nil - } - - if strongSelf.presentationInterfaceState.adMessage?.id != adMessage?.id { - animated = true - } - - strongSelf.updateChatPresentationInterfaceState(animated: animated, interactive: false, { - return $0.updatedPeer { _ in - return renderedPeer - }.updatedIsNotAccessible(isNotAccessible) - .updatedContactStatus(contactStatus) - .updatedHasBots(hasBots) - .updatedHasBotCommands(hasBotCommands) - .updatedBotMenuButton(botMenuButton) - .updatedIsArchived(isArchived) - .updatedPeerIsMuted(peerIsMuted) - .updatedPeerDiscussionId(peerDiscussionId) - .updatedPeerGeoLocation(peerGeoLocation) - .updatedExplicitelyCanPinMessages(explicitelyCanPinMessages) - .updatedHasScheduledMessages(hasScheduledMessages) - .updatedAutoremoveTimeout(autoremoveTimeout) - .updatedCurrentSendAsPeerId(currentSendAsPeerId) - .updatedCopyProtectionEnabled(copyProtectionEnabled) - .updatedHasSearchTags(hasSearchTags) - .updatedIsPremiumRequiredForMessaging(isPremiumRequiredForMessaging) - .updatedSendPaidMessageStars(sendPaidMessageStars) - .updatedAlwaysShowGiftButton(alwaysShowGiftButton) - .updatedDisallowedGifts(disallowedGifts) - .updatedHasSavedChats(hasSavedChats) - .updatedAppliedBoosts(appliedBoosts) - .updatedBoostsToUnrestrict(boostsToUnrestrict) - .updatedHasBirthdayToday(hasBirthdayToday) - .updatedBusinessIntro(businessIntro) - .updatedAdMessage(adMessage) - .updatedPeerVerification(peerVerification) - .updatedStarGiftsAvailable(starGiftsAvailable) - .updatedInterfaceState { interfaceState in - var interfaceState = interfaceState - - if let channel = renderedPeer?.peer as? TelegramChannel { - if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil { - interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) - } else if channel.hasBannedPermission(.banSendVoice) != nil { - if channel.hasBannedPermission(.banSendInstantVideos) == nil { - interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video) - } - } else if channel.hasBannedPermission(.banSendInstantVideos) != nil { - if channel.hasBannedPermission(.banSendVoice) == nil { - interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) - } - } - } else if let group = renderedPeer?.peer as? TelegramGroup { - if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) { - interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) - } else if group.hasBannedPermission(.banSendVoice) { - if !group.hasBannedPermission(.banSendInstantVideos) { - interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video) - } - } else if group.hasBannedPermission(.banSendInstantVideos) { - if !group.hasBannedPermission(.banSendVoice) { - interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) - } - } - } - - return interfaceState - } - }) - - if case .standard(.default) = mode, let channel = renderedPeer?.chatMainPeer as? TelegramChannel, case .broadcast = channel.info { - var isRegularChat = false - if let subject = subject { - if case .message = subject { - isRegularChat = true - } - } else { - isRegularChat = true - } - if strongSelf.nextChannelToReadDisposable == nil, let peerId = strongSelf.chatLocation.peerId, let customChatNavigationStack = strongSelf.customChatNavigationStack { - if let index = customChatNavigationStack.firstIndex(of: peerId), index != customChatNavigationStack.count - 1 { - let nextPeerId = customChatNavigationStack[index + 1] - strongSelf.nextChannelToReadDisposable = (combineLatest(queue: .mainQueue(), - strongSelf.context.engine.data.subscribe( - TelegramEngine.EngineData.Item.Peer.Peer(id: nextPeerId) - ), - ApplicationSpecificNotice.getNextChatSuggestionTip(accountManager: strongSelf.context.sharedContext.accountManager) - ) - |> then(.complete() |> delay(1.0, queue: .mainQueue())) - |> restart).startStrict(next: { nextPeer, nextChatSuggestionTip in - guard let strongSelf = self else { - return - } - - strongSelf.offerNextChannelToRead = true - strongSelf.chatDisplayNode.historyNode.nextChannelToRead = nextPeer.flatMap { nextPeer -> (peer: EnginePeer, threadData: (id: Int64, data: MessageHistoryThreadData)?, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation) in - return (peer: nextPeer, threadData: nil, unreadCount: 0, location: .same) - } - strongSelf.chatDisplayNode.historyNode.nextChannelToReadDisplayName = nextChatSuggestionTip >= 3 - - let nextPeerId = nextPeer?.id - - if strongSelf.preloadNextChatPeerId != nextPeerId { - strongSelf.preloadNextChatPeerId = nextPeerId - if let nextPeerId = nextPeerId { - let combinedDisposable = DisposableSet() - strongSelf.preloadNextChatPeerIdDisposable.set(combinedDisposable) - combinedDisposable.add(strongSelf.context.account.viewTracker.polledChannel(peerId: nextPeerId).startStrict()) - combinedDisposable.add(strongSelf.context.account.addAdditionalPreloadHistoryPeerId(peerId: nextPeerId)) - } else { - strongSelf.preloadNextChatPeerIdDisposable.set(nil) - } - } - - strongSelf.updateNextChannelToReadVisibility() - }) - } - } else if isRegularChat, strongSelf.nextChannelToReadDisposable == nil { - //TODO:loc optimize - let accountPeerId = strongSelf.context.account.peerId - strongSelf.nextChannelToReadDisposable = (combineLatest(queue: .mainQueue(), - strongSelf.context.engine.peers.getNextUnreadChannel(peerId: channel.id, chatListFilterId: strongSelf.currentChatListFilter, getFilterPredicate: { data in - return chatListFilterPredicate(filter: data, accountPeerId: accountPeerId) - }), - ApplicationSpecificNotice.getNextChatSuggestionTip(accountManager: strongSelf.context.sharedContext.accountManager) - ) - |> then(.complete() |> delay(1.0, queue: .mainQueue())) - |> restart).startStrict(next: { nextPeer, nextChatSuggestionTip in - guard let strongSelf = self else { - return - } - - strongSelf.offerNextChannelToRead = true - strongSelf.chatDisplayNode.historyNode.nextChannelToRead = nextPeer.flatMap { nextPeer -> (peer: EnginePeer, threadData: (id: Int64, data: MessageHistoryThreadData)?, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation) in - return (peer: nextPeer.peer, threadData: nil, unreadCount: nextPeer.unreadCount, location: nextPeer.location) - } - strongSelf.chatDisplayNode.historyNode.nextChannelToReadDisplayName = nextChatSuggestionTip >= 3 - - let nextPeerId = nextPeer?.peer.id - - if strongSelf.preloadNextChatPeerId != nextPeerId { - strongSelf.preloadNextChatPeerId = nextPeerId - if let nextPeerId = nextPeerId { - let combinedDisposable = DisposableSet() - strongSelf.preloadNextChatPeerIdDisposable.set(combinedDisposable) - combinedDisposable.add(strongSelf.context.account.viewTracker.polledChannel(peerId: nextPeerId).startStrict()) - combinedDisposable.add(strongSelf.context.account.addAdditionalPreloadHistoryPeerId(peerId: nextPeerId)) - } else { - strongSelf.preloadNextChatPeerIdDisposable.set(nil) - } - } - - strongSelf.updateNextChannelToReadVisibility() - }) - } - } - - if !strongSelf.didSetChatLocationInfoReady { - strongSelf.didSetChatLocationInfoReady = true - strongSelf._chatLocationInfoReady.set(.single(true)) - } - strongSelf.updateReminderActivity() - if let upgradedToPeerId = upgradedToPeerId { - if let navigationController = strongSelf.effectiveNavigationController { - var viewControllers = navigationController.viewControllers - if let index = viewControllers.firstIndex(where: { $0 === strongSelf }) { - viewControllers[index] = ChatControllerImpl(context: strongSelf.context, chatLocation: .peer(id: upgradedToPeerId)) - navigationController.setViewControllers(viewControllers, animated: false) - } - } - } else if movedToForumTopics { - if let navigationController = strongSelf.effectiveNavigationController { - let chatListController = strongSelf.context.sharedContext.makeChatListController(context: strongSelf.context, location: .forum(peerId: peerView.peerId), controlsHistoryPreload: false, hideNetworkActivityStatus: false, previewing: false, enableDebugActions: false) - navigationController.replaceController(strongSelf, with: chatListController, animated: true) - } - } else if shouldDismiss { - strongSelf.dismiss() - } - } - })) - - if peerId == context.account.peerId { - self.preloadSavedMessagesChatsDisposable = context.engine.messages.savedMessagesPeerListHead().start() - } - } else if case let .replyThread(messagePromise) = self.chatLocationInfoData, let peerId = peerId { - self.reportIrrelvantGeoNoticePromise.set(.single(nil)) - - let replyThreadType: ChatTitleContent.ReplyThreadType - var replyThreadId: Int64? - switch chatLocation { - case .peer: - replyThreadType = .replies - case let .replyThread(replyThreadMessage): - if replyThreadMessage.peerId == context.account.peerId { - replyThreadId = replyThreadMessage.threadId - replyThreadType = .replies - } else { - replyThreadId = replyThreadMessage.threadId - if replyThreadMessage.isChannelPost { - replyThreadType = .comments - } else { - replyThreadType = .replies - } - } - case .customChatContents: - replyThreadType = .replies - } - - let peerView = context.account.viewTracker.peerView(peerId) - - let messageAndTopic = messagePromise.get() - |> mapToSignal { message -> Signal<(message: Message?, threadData: MessageHistoryThreadData?, messageCount: Int), NoError> in - guard let replyThreadId = replyThreadId else { - return .single((message, nil, 0)) - } - let viewKey: PostboxViewKey = .messageHistoryThreadInfo(peerId: peerId, threadId: replyThreadId) - let countViewKey: PostboxViewKey = .historyTagSummaryView(tag: MessageTags(), peerId: peerId, threadId: replyThreadId, namespace: Namespaces.Message.Cloud, customTag: nil) - let localCountViewKey: PostboxViewKey = .historyTagSummaryView(tag: MessageTags(), peerId: peerId, threadId: replyThreadId, namespace: Namespaces.Message.Local, customTag: nil) - return context.account.postbox.combinedView(keys: [viewKey, countViewKey, localCountViewKey]) - |> map { views -> (message: Message?, threadData: MessageHistoryThreadData?, messageCount: Int) in - guard let view = views.views[viewKey] as? MessageHistoryThreadInfoView else { - return (message, nil, 0) - } - var messageCount = 0 - if let summaryView = views.views[countViewKey] as? MessageHistoryTagSummaryView, let count = summaryView.count { - if replyThreadId == 1 { - messageCount += Int(count) - } else { - messageCount += max(Int(count) - 1, 0) - } - } - if let summaryView = views.views[localCountViewKey] as? MessageHistoryTagSummaryView, let count = summaryView.count { - messageCount += Int(count) - } - return (message, view.info?.data.get(MessageHistoryThreadData.self), messageCount) - } - } - - let savedMessagesPeerId: PeerId? - if case let .replyThread(replyThreadMessage) = chatLocation, replyThreadMessage.peerId == context.account.peerId { - savedMessagesPeerId = PeerId(replyThreadMessage.threadId) - } else { - savedMessagesPeerId = nil - } - - let savedMessagesPeer: Signal<(peer: EnginePeer?, messageCount: Int)?, NoError> - if let savedMessagesPeerId { - let threadPeerId = savedMessagesPeerId - let basicPeerKey: PostboxViewKey = .basicPeer(threadPeerId) - let countViewKey: PostboxViewKey = .historyTagSummaryView(tag: MessageTags(), peerId: peerId, threadId: savedMessagesPeerId.toInt64(), namespace: Namespaces.Message.Cloud, customTag: nil) - savedMessagesPeer = context.account.postbox.combinedView(keys: [basicPeerKey, countViewKey]) - |> map { views -> (peer: EnginePeer?, messageCount: Int)? in - let peer = ((views.views[basicPeerKey] as? BasicPeerView)?.peer).flatMap(EnginePeer.init) - - var messageCount = 0 - if let summaryView = views.views[countViewKey] as? MessageHistoryTagSummaryView, let count = summaryView.count { - messageCount += Int(count) - } - - return (peer, messageCount) - } - } else { - savedMessagesPeer = .single(nil) - } - - var isScheduledOrPinnedMessages = false - switch subject { - case .scheduledMessages, .pinnedMessages, .messageOptions: - isScheduledOrPinnedMessages = true - default: - break - } - - var hasScheduledMessages: Signal = .single(false) - if chatLocation.peerId != nil, !isScheduledOrPinnedMessages, peerId.namespace != Namespaces.Peer.SecretChat { - let chatLocationContextHolder = self.chatLocationContextHolder - hasScheduledMessages = peerView - |> take(1) - |> mapToSignal { view -> Signal in - if let peer = peerViewMainPeer(view) as? TelegramChannel, !peer.hasPermission(.sendSomething) { - return .single(false) - } else { - if case let .replyThread(message) = chatLocation, message.peerId == context.account.peerId { - return context.account.viewTracker.scheduledMessagesViewForLocation(context.chatLocationInput(for: .peer(id: context.account.peerId), contextHolder: Atomic(value: nil))) - |> map { view, _, _ in - return !view.entries.isEmpty - } - |> distinctUntilChanged - } else { - return context.account.viewTracker.scheduledMessagesViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder)) - |> map { view, _, _ in - return !view.entries.isEmpty - } - |> distinctUntilChanged - } - } - } - } - - var onlineMemberCount: Signal<(total: Int32?, recent: Int32?), NoError> = .single((nil, nil)) - if peerId.namespace == Namespaces.Peer.CloudChannel { - let recentOnlineSignal: Signal<(total: Int32?, recent: Int32?), NoError> = peerView - |> map { view -> Bool? in - if let cachedData = view.cachedData as? CachedChannelData, let peer = peerViewMainPeer(view) as? TelegramChannel { - if case .broadcast = peer.info { - return nil - } else if let memberCount = cachedData.participantsSummary.memberCount, memberCount > 50 { - return true - } else { - return false - } - } else { - return false - } - } - |> distinctUntilChanged - |> mapToSignal { isLarge -> Signal<(total: Int32?, recent: Int32?), NoError> in - if let isLarge = isLarge { - if isLarge { - return context.peerChannelMemberCategoriesContextsManager.recentOnline(account: context.account, accountPeerId: context.account.peerId, peerId: peerId) - |> map { value -> (total: Int32?, recent: Int32?) in - return (nil, value) - } - } else { - return context.peerChannelMemberCategoriesContextsManager.recentOnlineSmall(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId) - |> map { value -> (total: Int32?, recent: Int32?) in - return (value.total, value.recent) - } - } - } else { - return .single((nil, nil)) - } - } - onlineMemberCount = recentOnlineSignal - } - - let hasSearchTags: Signal - if let peerId = self.chatLocation.peerId, peerId == context.account.peerId { - hasSearchTags = context.engine.data.subscribe( - TelegramEngine.EngineData.Item.Messages.SavedMessageTagStats(peerId: context.account.peerId, threadId: self.chatLocation.threadId) - ) - |> map { tags -> Bool in - return !tags.isEmpty - } - |> distinctUntilChanged - } else { - hasSearchTags = .single(false) - } - - let hasSavedChats: Signal - if case .peer(context.account.peerId) = self.chatLocation { - hasSavedChats = context.engine.messages.savedMessagesHasPeersOtherThanSaved() - } else { - hasSavedChats = .single(false) - } - - let isPremiumRequiredForMessaging: Signal - if let peerId = self.chatLocation.peerId { - isPremiumRequiredForMessaging = context.engine.peers.subscribeIsPremiumRequiredForMessaging(id: peerId) - |> distinctUntilChanged - } else { - isPremiumRequiredForMessaging = .single(false) - } - - let globalPrivacySettings = context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.GlobalPrivacy()) - - self.titleDisposable.set(nil) - self.peerDisposable.set((combineLatest(queue: Queue.mainQueue(), - peerView, - messageAndTopic, - savedMessagesPeer, - onlineMemberCount, - hasScheduledMessages, - hasSearchTags, - hasSavedChats, - isPremiumRequiredForMessaging, - managingBot, - globalPrivacySettings - ) - |> deliverOnMainQueue).startStrict(next: { [weak self] peerView, messageAndTopic, savedMessagesPeer, onlineMemberCount, hasScheduledMessages, hasSearchTags, hasSavedChats, isPremiumRequiredForMessaging, managingBot, globalPrivacySettings in - if let strongSelf = self { - strongSelf.hasScheduledMessages = hasScheduledMessages - - var renderedPeer: RenderedPeer? - var contactStatus: ChatContactStatus? - var copyProtectionEnabled = false - var businessIntro: TelegramBusinessIntro? - var sendPaidMessageStars: StarsAmount? - var alwaysShowGiftButton = false - var disallowedGifts: TelegramDisallowedGifts? - if let peer = peerView.peers[peerView.peerId] { - copyProtectionEnabled = peer.isCopyProtectionEnabled - if let cachedData = peerView.cachedData as? CachedUserData { - contactStatus = ChatContactStatus(canAddContact: !peerView.peerIsContact, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: nil, managingBot: managingBot) - if case let .known(value) = cachedData.businessIntro { - businessIntro = value - } - if cachedData.disallowedGifts != .All { - alwaysShowGiftButton = globalPrivacySettings.displayGiftButton || cachedData.flags.contains(.displayGiftButton) - } - disallowedGifts = cachedData.disallowedGifts - } else if let cachedData = peerView.cachedData as? CachedGroupData { - var invitedBy: Peer? - if let invitedByPeerId = cachedData.invitedBy { - if let peer = peerView.peers[invitedByPeerId] { - invitedBy = peer - } - } - contactStatus = ChatContactStatus(canAddContact: false, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy, managingBot: managingBot) - } else if let cachedData = peerView.cachedData as? CachedChannelData { - var canReportIrrelevantLocation = true - if let peer = peerView.peers[peerView.peerId] as? TelegramChannel, peer.participationStatus == .member { - canReportIrrelevantLocation = false - } - canReportIrrelevantLocation = false - var invitedBy: Peer? - if let invitedByPeerId = cachedData.invitedBy { - if let peer = peerView.peers[invitedByPeerId] { - invitedBy = peer - } - } - contactStatus = ChatContactStatus(canAddContact: false, canReportIrrelevantLocation: canReportIrrelevantLocation, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy, managingBot: managingBot) - - if let channel = peerView.peers[peerView.peerId] as? TelegramChannel { - if channel.flags.contains(.isCreator) || channel.adminRights != nil { - } else { - sendPaidMessageStars = channel.sendPaidMessageStars - } - } - } - - var peers = SimpleDictionary() - peers[peer.id] = peer - if let associatedPeerId = peer.associatedPeerId, let associatedPeer = peerView.peers[associatedPeerId] { - peers[associatedPeer.id] = associatedPeer - } - renderedPeer = RenderedPeer(peerId: peer.id, peers: peers, associatedMedia: peerView.media) - } - - if let savedMessagesPeerId { - let mappedPeerData = ChatTitleContent.PeerData( - peerId: savedMessagesPeerId, - peer: savedMessagesPeer?.peer?._asPeer(), - isContact: true, - isSavedMessages: true, - notificationSettings: nil, - peerPresences: [:], - cachedData: nil - ) - strongSelf.chatTitleView?.titleContent = .peer(peerView: mappedPeerData, customTitle: nil, onlineMemberCount: (nil, nil), isScheduledMessages: false, isMuted: false, customMessageCount: savedMessagesPeer?.messageCount ?? 0, isEnabled: true) - - strongSelf.peerView = peerView - - let imageOverride: AvatarNodeImageOverride? - if strongSelf.context.account.peerId == savedMessagesPeerId { - imageOverride = .myNotesIcon - } else if savedMessagesPeerId.isReplies { - imageOverride = .repliesIcon - } else if savedMessagesPeerId.isAnonymousSavedMessages { - imageOverride = .anonymousSavedMessagesIcon(isColored: true) - } else if let peer = savedMessagesPeer?.peer, peer.isDeleted { - imageOverride = .deletedIcon - } else { - imageOverride = nil - } - - if strongSelf.isNodeLoaded { - strongSelf.chatDisplayNode.overlayTitle = strongSelf.overlayTitle - } - - let animated = false - strongSelf.updateChatPresentationInterfaceState(animated: animated, interactive: false, { - return $0.updatedPeer { _ in - return renderedPeer - }.updatedSavedMessagesTopicPeer(savedMessagesPeer?.peer) - .updatedHasSearchTags(hasSearchTags) - .updatedHasSavedChats(hasSavedChats) - .updatedHasScheduledMessages(hasScheduledMessages) - }) - - (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.setPeer(context: strongSelf.context, theme: strongSelf.presentationData.theme, peer: savedMessagesPeer?.peer, overrideImage: imageOverride) - (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.contextActionIsEnabled = false - strongSelf.chatInfoNavigationButton?.buttonItem.accessibilityLabel = strongSelf.presentationData.strings.Conversation_ContextMenuOpenProfile - } else { - let message = messageAndTopic.message - - var count = 0 - if let message = message { - for attribute in message.attributes { - if let attribute = attribute as? ReplyThreadMessageAttribute { - count = Int(attribute.count) - break - } - } - } - - var peerIsMuted = false - if let threadData = messageAndTopic.threadData { - if case let .muted(until) = threadData.notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { - peerIsMuted = true - } - } else if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings { - if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { - peerIsMuted = true - } - } - - if let threadInfo = messageAndTopic.threadData?.info { - strongSelf.chatTitleView?.titleContent = .peer(peerView: ChatTitleContent.PeerData(peerView: peerView), customTitle: threadInfo.title, onlineMemberCount: onlineMemberCount, isScheduledMessages: false, isMuted: peerIsMuted, customMessageCount: messageAndTopic.messageCount == 0 ? nil : messageAndTopic.messageCount, isEnabled: true) - - let avatarContent: EmojiStatusComponent.Content - if strongSelf.chatLocation.threadId == 1 { - avatarContent = .image(image: PresentationResourcesChat.chatGeneralThreadIcon(strongSelf.presentationData.theme)) - } else if let fileId = threadInfo.icon { - avatarContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 48.0, height: 48.0), placeholderColor: strongSelf.presentationData.theme.list.mediaPlaceholderColor, themeColor: strongSelf.presentationData.theme.list.itemAccentColor, loopMode: .count(1)) - } else { - avatarContent = .topic(title: String(threadInfo.title.prefix(1)), color: threadInfo.iconColor, size: CGSize(width: 32.0, height: 32.0)) - } - (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.setStatus(context: strongSelf.context, content: avatarContent) - } else { - strongSelf.chatTitleView?.titleContent = .replyThread(type: replyThreadType, count: count) - } - - var wasGroupChannel: Bool? - if let previousPeerView = strongSelf.peerView, let info = (previousPeerView.peers[previousPeerView.peerId] as? TelegramChannel)?.info { - if case .group = info { - wasGroupChannel = true - } else { - wasGroupChannel = false - } - } - var isGroupChannel: Bool? - if let info = (peerView.peers[peerView.peerId] as? TelegramChannel)?.info { - if case .group = info { - isGroupChannel = true - } else { - isGroupChannel = false - } - } - let firstTime = strongSelf.peerView == nil - - if wasGroupChannel != isGroupChannel { - if let isGroupChannel = isGroupChannel, isGroupChannel { - let (recentDisposable, _) = strongSelf.context.peerChannelMemberCategoriesContextsManager.recent(engine: strongSelf.context.engine, postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, accountPeerId: context.account.peerId, peerId: peerView.peerId, updated: { _ in }) - let (adminsDisposable, _) = strongSelf.context.peerChannelMemberCategoriesContextsManager.admins(engine: strongSelf.context.engine, postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, accountPeerId: context.account.peerId, peerId: peerView.peerId, updated: { _ in }) - let disposable = DisposableSet() - disposable.add(recentDisposable) - disposable.add(adminsDisposable) - strongSelf.chatAdditionalDataDisposable.set(disposable) - } else { - strongSelf.chatAdditionalDataDisposable.set(nil) - } - } - - strongSelf.peerView = peerView - strongSelf.threadInfo = messageAndTopic.threadData?.info - - if strongSelf.isNodeLoaded { - strongSelf.chatDisplayNode.overlayTitle = strongSelf.overlayTitle - } - if case .standard(.previewing) = strongSelf.mode { - (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.contextActionIsEnabled = false - } else { - (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.contextActionIsEnabled = true - } - - var peerDiscussionId: PeerId? - var peerGeoLocation: PeerGeoLocation? - var currentSendAsPeerId: PeerId? - if let peer = peerView.peers[peerView.peerId] as? TelegramChannel, let cachedData = peerView.cachedData as? CachedChannelData { - currentSendAsPeerId = cachedData.sendAsPeerId - if case .group = peer.info { - peerGeoLocation = cachedData.peerGeoLocation - } - if case let .known(value) = cachedData.linkedDiscussionPeerId { - peerDiscussionId = value - } - } - - var isNotAccessible: Bool = false - if let cachedChannelData = peerView.cachedData as? CachedChannelData { - isNotAccessible = cachedChannelData.isNotAccessible - } - - if firstTime && isNotAccessible { - strongSelf.context.account.viewTracker.forceUpdateCachedPeerData(peerId: peerView.peerId) - } - - var hasBots: Bool = false - if let peer = peerView.peers[peerView.peerId] { - if let cachedGroupData = peerView.cachedData as? CachedGroupData { - if !cachedGroupData.botInfos.isEmpty { - hasBots = true - } - } else if let cachedChannelData = peerView.cachedData as? CachedChannelData, let channel = peer as? TelegramChannel, case .group = channel.info { - if !cachedChannelData.botInfos.isEmpty { - hasBots = true - } - } - } - - let isArchived: Bool = peerView.groupId == Namespaces.PeerGroup.archive - - var explicitelyCanPinMessages: Bool = false - if let cachedUserData = peerView.cachedData as? CachedUserData { - explicitelyCanPinMessages = cachedUserData.canPinMessages - } else if peerView.peerId == context.account.peerId { - explicitelyCanPinMessages = true - } - - var animated = false - if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat, let updated = renderedPeer?.peer as? TelegramSecretChat, peer.embeddedState != updated.embeddedState { - animated = true - } - if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, let updated = renderedPeer?.peer as? TelegramChannel { - if peer.participationStatus != updated.participationStatus { - animated = true - } - } - - var didDisplayActionsPanel = false - if let contactStatus = strongSelf.presentationInterfaceState.contactStatus, !contactStatus.isEmpty, let peerStatusSettings = contactStatus.peerStatusSettings { - if !peerStatusSettings.flags.isEmpty { - if contactStatus.canAddContact && peerStatusSettings.contains(.canAddContact) { - didDisplayActionsPanel = true - } else if peerStatusSettings.contains(.canReport) || peerStatusSettings.contains(.canBlock) { - didDisplayActionsPanel = true - } else if peerStatusSettings.contains(.canShareContact) { - didDisplayActionsPanel = true - } else if contactStatus.canReportIrrelevantLocation && peerStatusSettings.contains(.canReportIrrelevantGeoLocation) { - didDisplayActionsPanel = true - } else if peerStatusSettings.contains(.suggestAddMembers) { - didDisplayActionsPanel = true - } - } - } - if let contactStatus = strongSelf.presentationInterfaceState.contactStatus, contactStatus.managingBot != nil { - didDisplayActionsPanel = true - } - - var displayActionsPanel = false - if let contactStatus = contactStatus, !contactStatus.isEmpty, let peerStatusSettings = contactStatus.peerStatusSettings { - if !peerStatusSettings.flags.isEmpty { - if contactStatus.canAddContact && peerStatusSettings.contains(.canAddContact) { - displayActionsPanel = true - } else if peerStatusSettings.contains(.canReport) || peerStatusSettings.contains(.canBlock) { - displayActionsPanel = true - } else if peerStatusSettings.contains(.canShareContact) { - displayActionsPanel = true - } else if contactStatus.canReportIrrelevantLocation && peerStatusSettings.contains(.canReportIrrelevantGeoLocation) { - displayActionsPanel = true - } else if peerStatusSettings.contains(.suggestAddMembers) { - displayActionsPanel = true - } - } - } - if let contactStatus, contactStatus.managingBot != nil { - displayActionsPanel = true - } - - if displayActionsPanel != didDisplayActionsPanel { - animated = true - } - - if strongSelf.preloadHistoryPeerId != peerDiscussionId { - strongSelf.preloadHistoryPeerId = peerDiscussionId - if let peerDiscussionId = peerDiscussionId { - strongSelf.preloadHistoryPeerIdDisposable.set(strongSelf.context.account.addAdditionalPreloadHistoryPeerId(peerId: peerDiscussionId)) - } else { - strongSelf.preloadHistoryPeerIdDisposable.set(nil) - } - } - - var appliedBoosts: Int32? - var boostsToUnrestrict: Int32? - if let cachedChannelData = peerView.cachedData as? CachedChannelData { - appliedBoosts = cachedChannelData.appliedBoosts - boostsToUnrestrict = cachedChannelData.boostsToUnrestrict - } - - if strongSelf.premiumOrStarsRequiredDisposable == nil, sendPaidMessageStars != nil, let peerId = strongSelf.chatLocation.peerId { - strongSelf.premiumOrStarsRequiredDisposable = ((strongSelf.context.engine.peers.isPremiumRequiredToContact([peerId]) |> then(.complete() |> suspendAwareDelay(60.0, queue: Queue.concurrentDefaultQueue()))) |> restart).startStandalone() - } - - strongSelf.updateChatPresentationInterfaceState(animated: animated, interactive: false, { - return $0.updatedPeer { _ in - return renderedPeer - }.updatedIsNotAccessible(isNotAccessible).updatedContactStatus(contactStatus).updatedHasBots(hasBots).updatedIsArchived(isArchived).updatedPeerIsMuted(peerIsMuted).updatedPeerDiscussionId(peerDiscussionId).updatedPeerGeoLocation(peerGeoLocation).updatedExplicitelyCanPinMessages(explicitelyCanPinMessages).updatedHasScheduledMessages(hasScheduledMessages).updatedCurrentSendAsPeerId(currentSendAsPeerId) - .updatedCopyProtectionEnabled(copyProtectionEnabled) - .updatedHasSearchTags(hasSearchTags) - .updatedIsPremiumRequiredForMessaging(isPremiumRequiredForMessaging) - .updatedHasSavedChats(hasSavedChats) - .updatedAppliedBoosts(appliedBoosts) - .updatedBoostsToUnrestrict(boostsToUnrestrict) - .updatedBusinessIntro(businessIntro) - .updatedSendPaidMessageStars(sendPaidMessageStars) - .updatedAlwaysShowGiftButton(alwaysShowGiftButton) - .updatedDisallowedGifts(disallowedGifts) - .updatedInterfaceState { interfaceState in - var interfaceState = interfaceState - - if let channel = renderedPeer?.peer as? TelegramChannel { - if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil { - interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) - } else if channel.hasBannedPermission(.banSendVoice) != nil { - if channel.hasBannedPermission(.banSendInstantVideos) == nil { - interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video) - } - } else if channel.hasBannedPermission(.banSendInstantVideos) != nil { - if channel.hasBannedPermission(.banSendVoice) == nil { - interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) - } - } - } else if let group = renderedPeer?.peer as? TelegramGroup { - if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) { - interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) - } else if group.hasBannedPermission(.banSendVoice) { - if !group.hasBannedPermission(.banSendInstantVideos) { - interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video) - } - } else if group.hasBannedPermission(.banSendInstantVideos) { - if !group.hasBannedPermission(.banSendVoice) { - interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) - } - } - } - - return interfaceState - } - }) - - if let replyThreadId, let channel = renderedPeer?.peer as? TelegramChannel, channel.isForum, strongSelf.nextChannelToReadDisposable == nil { - strongSelf.nextChannelToReadDisposable = (combineLatest(queue: .mainQueue(), - strongSelf.context.engine.peers.getNextUnreadForumTopic(peerId: channel.id, topicId: Int32(clamping: replyThreadId)), - ApplicationSpecificNotice.getNextChatSuggestionTip(accountManager: strongSelf.context.sharedContext.accountManager) - ) - |> then(.complete() |> delay(1.0, queue: .mainQueue())) - |> restart).startStrict(next: { nextThreadData, nextChatSuggestionTip in - guard let strongSelf = self else { - return - } - - strongSelf.offerNextChannelToRead = true - strongSelf.chatDisplayNode.historyNode.nextChannelToRead = nextThreadData.flatMap { nextThreadData -> (peer: EnginePeer, threadData: (id: Int64, data: MessageHistoryThreadData)?, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation) in - return (peer: EnginePeer(channel), threadData: nextThreadData, unreadCount: Int(nextThreadData.data.incomingUnreadCount), location: .same) - } - strongSelf.chatDisplayNode.historyNode.nextChannelToReadDisplayName = nextChatSuggestionTip >= 3 - - strongSelf.updateNextChannelToReadVisibility() - }) - } - } - if !strongSelf.didSetChatLocationInfoReady { - strongSelf.didSetChatLocationInfoReady = true - strongSelf._chatLocationInfoReady.set(.single(true)) - } - } - })) - } else if case .customChatContents = self.chatLocationInfoData { - self.reportIrrelvantGeoNoticePromise.set(.single(nil)) - self.titleDisposable.set(nil) - - var peerView: Signal = .single(nil) - - if case let .customChatContents(customChatContents) = self.subject { - switch customChatContents.kind { - case .hashTagSearch: - break - case let .quickReplyMessageInput(shortcut, shortcutType): - switch shortcutType { - case .generic: - self.chatTitleView?.titleContent = .custom("\(shortcut)", nil, false) - case .greeting: - self.chatTitleView?.titleContent = .custom(self.presentationData.strings.QuickReply_TitleGreetingMessage, nil, false) - case .away: - self.chatTitleView?.titleContent = .custom(self.presentationData.strings.QuickReply_TitleAwayMessage, nil, false) - } - case let .businessLinkSetup(link): - let linkUrl: String - if link.url.hasPrefix("https://") { - linkUrl = String(link.url[link.url.index(link.url.startIndex, offsetBy: "https://".count)...]) - } else { - linkUrl = link.url - } - - self.chatTitleView?.titleContent = .custom(link.title ?? self.presentationData.strings.Business_Links_EditLinkTitle, linkUrl, false) - case .postSuggestions: - if let customChatContents = customChatContents as? PostSuggestionsChatContents { - peerView = context.account.viewTracker.peerView(customChatContents.peerId) |> map(Optional.init) - } - - //TODO:localize - self.chatTitleView?.titleContent = .custom("Message Suggestions", nil, false) - } - } else { - self.chatTitleView?.titleContent = .custom(" ", nil, false) - } - - self.peerDisposable.set((peerView - |> deliverOnMainQueue).startStrict(next: { [weak self] peerView in - guard let self else { - return - } - - var renderedPeer: RenderedPeer? - if let peerView, let peer = peerView.peers[peerView.peerId] { - var peers = SimpleDictionary() - peers[peer.id] = peer - if let associatedPeerId = peer.associatedPeerId, let associatedPeer = peerView.peers[associatedPeerId] { - peers[associatedPeer.id] = associatedPeer - } - renderedPeer = RenderedPeer(peerId: peer.id, peers: peers, associatedMedia: peerView.media) - - (self.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.setPeer(context: self.context, theme: self.presentationData.theme, peer: EnginePeer(peer), overrideImage: nil) - } - - self.peerView = peerView - - if self.isNodeLoaded { - self.chatDisplayNode.overlayTitle = self.overlayTitle - } - (self.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.contextActionIsEnabled = false - - self.updateChatPresentationInterfaceState(animated: false, interactive: false, { - return $0.updatedPeer { _ in - return renderedPeer - }.updatedInterfaceState { interfaceState in - return interfaceState - } - }) - - if !self.didSetChatLocationInfoReady { - self.didSetChatLocationInfoReady = true - self._chatLocationInfoReady.set(.single(true)) - } - })) - } - } - - func reloadCachedData() { - if let peerId = self.chatLocation.peerId { - let customEmojiAvailable: Signal = self.context.engine.data.subscribe( - TelegramEngine.EngineData.Item.Peer.SecretChatLayer(id: peerId) - ) - |> map { layer -> Bool in - guard let layer = layer else { - return true - } - - return layer >= 144 - } - |> distinctUntilChanged - - let isForum = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) - |> map { peer -> Bool in - if case let .channel(channel) = peer { - return channel.flags.contains(.isForum) - } else { - return false - } - } - |> distinctUntilChanged - - let context = self.context - let threadData: Signal - let forumTopicData: Signal - if let threadId = self.chatLocation.threadId { - let viewKey: PostboxViewKey = .messageHistoryThreadInfo(peerId: peerId, threadId: threadId) - threadData = context.account.postbox.combinedView(keys: [viewKey]) - |> map { views -> ChatPresentationInterfaceState.ThreadData? in - guard let view = views.views[viewKey] as? MessageHistoryThreadInfoView else { - return nil - } - guard let data = view.info?.data.get(MessageHistoryThreadData.self) else { - return nil - } - return ChatPresentationInterfaceState.ThreadData(title: data.info.title, icon: data.info.icon, iconColor: data.info.iconColor, isOwnedByMe: data.isOwnedByMe, isClosed: data.isClosed) - } - |> distinctUntilChanged - forumTopicData = .single(nil) - } else { - forumTopicData = isForum - |> mapToSignal { isForum -> Signal in - if isForum { - let viewKey: PostboxViewKey = .messageHistoryThreadInfo(peerId: peerId, threadId: 1) - return context.account.postbox.combinedView(keys: [viewKey]) - |> map { views -> ChatPresentationInterfaceState.ThreadData? in - guard let view = views.views[viewKey] as? MessageHistoryThreadInfoView else { - return nil - } - guard let data = view.info?.data.get(MessageHistoryThreadData.self) else { - return nil - } - return ChatPresentationInterfaceState.ThreadData(title: data.info.title, icon: data.info.icon, iconColor: data.info.iconColor, isOwnedByMe: data.isOwnedByMe, isClosed: data.isClosed) - } - |> distinctUntilChanged - } else { - return .single(nil) - } - } - threadData = .single(nil) - } - - if case .standard(.previewing) = self.presentationInterfaceState.mode { - - } else if peerId.namespace != Namespaces.Peer.SecretChat && peerId != context.account.peerId && self.subject != .scheduledMessages { - self.premiumGiftSuggestionDisposable?.dispose() - self.premiumGiftSuggestionDisposable = (ApplicationSpecificNotice.dismissedPremiumGiftSuggestion(accountManager: self.context.sharedContext.accountManager, peerId: peerId) - |> deliverOnMainQueue).startStrict(next: { [weak self] timestamp in - if let strongSelf = self { - let currentTime = Int32(Date().timeIntervalSince1970) - strongSelf.updateChatPresentationInterfaceState(animated: strongSelf.willAppear, interactive: strongSelf.willAppear, { state in - var suggest = true - if let timestamp, currentTime < timestamp + 60 * 60 * 24 { - suggest = false - } - return state.updatedSuggestPremiumGift(suggest) - }) - } - }) - - var baseLanguageCode = self.presentationData.strings.baseLanguageCode - if baseLanguageCode.contains("-") { - baseLanguageCode = baseLanguageCode.components(separatedBy: "-").first ?? baseLanguageCode - } - let isPremium = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) - |> map { peer -> Bool in - return peer?.isPremium ?? false - } |> distinctUntilChanged - - let isHidden = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.TranslationHidden(id: peerId)) - |> distinctUntilChanged - - let hasAutoTranslate = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.AutoTranslateEnabled(id: peerId)) - |> distinctUntilChanged - - self.translationStateDisposable?.dispose() - let chatLocation = self.chatLocation - self.translationStateDisposable = (combineLatest( - queue: .concurrentDefaultQueue(), - isPremium, - isHidden, - hasAutoTranslate, - ApplicationSpecificNotice.translationSuggestion(accountManager: self.context.sharedContext.accountManager) - ) |> mapToSignal { isPremium, isHidden, hasAutoTranslate, counterAndTimestamp -> Signal in - var maybeSuggestPremium = false - if counterAndTimestamp.0 >= 3 { - maybeSuggestPremium = true - } - if (isPremium || maybeSuggestPremium || hasAutoTranslate) && !isHidden { - return chatTranslationState(context: context, peerId: peerId, threadId: chatLocation.threadId) - |> map { translationState -> ChatPresentationTranslationState? in - if let translationState, !translationState.fromLang.isEmpty && (translationState.fromLang != baseLanguageCode || translationState.isEnabled) { - return ChatPresentationTranslationState(isEnabled: translationState.isEnabled, fromLang: translationState.fromLang, toLang: translationState.toLang ?? baseLanguageCode) - } else { - return nil - } - } - |> distinctUntilChanged - } else { - return .single(nil) - } - } - |> deliverOnMainQueue).startStrict(next: { [weak self] chatTranslationState in - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: strongSelf.willAppear, interactive: strongSelf.willAppear, { state in - return state.updatedTranslationState(chatTranslationState) - }) - } - }) - } - - let premiumGiftOptions: Signal<[CachedPremiumGiftOption], NoError> = .single([]) - |> then( - self.context.engine.payments.premiumGiftCodeOptions(peerId: peerId, onlyCached: true) - |> map { options in - return options.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) } - } - ) - - let isTopReplyThreadMessageShown: Signal = self.chatDisplayNode.historyNode.isTopReplyThreadMessageShown.get() - |> distinctUntilChanged - - let hasPendingMessages: Signal - let chatLocationPeerId = self.chatLocation.peerId - - if let chatLocationPeerId = chatLocationPeerId { - hasPendingMessages = self.context.account.pendingMessageManager.hasPendingMessages - |> mapToSignal { peerIds -> Signal in - let value = peerIds.contains(chatLocationPeerId) - if value { - return .single(true) - } else { - return .single(false) - } - } - |> distinctUntilChanged - } else { - hasPendingMessages = .single(false) - } - - let topPinnedMessage: Signal - if let subject = self.subject { - switch subject { - case .messageOptions, .pinnedMessages, .scheduledMessages: - topPinnedMessage = .single(nil) - default: - topPinnedMessage = self.topPinnedMessageSignal(latest: false) - } - } else { - topPinnedMessage = self.topPinnedMessageSignal(latest: false) - } - - self.cachedDataDisposable?.dispose() - self.cachedDataDisposable = combineLatest(queue: .mainQueue(), self.chatDisplayNode.historyNode.cachedPeerDataAndMessages |> debug_measureTimeToFirstEvent(label: "cachedData_cachedPeerDataAndMessages"), - hasPendingMessages |> debug_measureTimeToFirstEvent(label: "cachedData_hasPendingMessages"), - isTopReplyThreadMessageShown |> debug_measureTimeToFirstEvent(label: "cachedData_isTopReplyThreadMessageShown"), - topPinnedMessage |> debug_measureTimeToFirstEvent(label: "cachedData_topPinnedMessage"), - customEmojiAvailable |> debug_measureTimeToFirstEvent(label: "cachedData_customEmojiAvailable"), - isForum |> debug_measureTimeToFirstEvent(label: "cachedData_isForum"), - threadData |> debug_measureTimeToFirstEvent(label: "cachedData_threadData"), - forumTopicData |> debug_measureTimeToFirstEvent(label: "cachedData_forumTopicData"), - premiumGiftOptions |> debug_measureTimeToFirstEvent(label: "cachedData_premiumGiftOptions") - ).startStrict(next: { [weak self] cachedDataAndMessages, hasPendingMessages, isTopReplyThreadMessageShown, topPinnedMessage, customEmojiAvailable, isForum, threadData, forumTopicData, premiumGiftOptions in - if let strongSelf = self { - let (cachedData, messages) = cachedDataAndMessages - - if cachedData != nil { - var themeEmoticon: String? = nil - var chatWallpaper: TelegramWallpaper? - if let cachedData = cachedData as? CachedUserData { - themeEmoticon = cachedData.themeEmoticon - chatWallpaper = cachedData.wallpaper - } else if let cachedData = cachedData as? CachedGroupData { - themeEmoticon = cachedData.themeEmoticon - } else if let cachedData = cachedData as? CachedChannelData { - themeEmoticon = cachedData.themeEmoticon - chatWallpaper = cachedData.wallpaper - } - - strongSelf.chatThemeEmoticonPromise.set(.single(themeEmoticon)) - strongSelf.chatWallpaperPromise.set(.single(chatWallpaper)) - } - - var pinnedMessageId: MessageId? - var peerIsBlocked: Bool = false - var callsAvailable: Bool = false - var callsPrivate: Bool = false - var voiceMessagesAvailable: Bool = true - var slowmodeState: ChatSlowmodeState? - var activeGroupCallInfo: ChatActiveGroupCallInfo? - var inviteRequestsPending: Int32? - if let cachedData = cachedData as? CachedChannelData { - pinnedMessageId = cachedData.pinnedMessageId - if !canBypassRestrictions(chatPresentationInterfaceState: strongSelf.presentationInterfaceState) { - if let channel = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isRestrictedBySlowmode, let timeout = cachedData.slowModeTimeout { - if hasPendingMessages { - slowmodeState = ChatSlowmodeState(timeout: timeout, variant: .pendingMessages) - } else if let slowmodeUntilTimestamp = calculateSlowmodeActiveUntilTimestamp(account: strongSelf.context.account, untilTimestamp: cachedData.slowModeValidUntilTimestamp) { - slowmodeState = ChatSlowmodeState(timeout: timeout, variant: .timestamp(slowmodeUntilTimestamp)) - } - } - } - if let activeCall = cachedData.activeCall { - activeGroupCallInfo = ChatActiveGroupCallInfo(activeCall: activeCall) - } - inviteRequestsPending = cachedData.inviteRequestsPending - } else if let cachedData = cachedData as? CachedUserData { - peerIsBlocked = cachedData.isBlocked - callsAvailable = cachedData.voiceCallsAvailable - callsPrivate = cachedData.callsPrivate - pinnedMessageId = cachedData.pinnedMessageId - voiceMessagesAvailable = cachedData.voiceMessagesAvailable - } else if let cachedData = cachedData as? CachedGroupData { - pinnedMessageId = cachedData.pinnedMessageId - if let activeCall = cachedData.activeCall { - activeGroupCallInfo = ChatActiveGroupCallInfo(activeCall: activeCall) - } - inviteRequestsPending = cachedData.inviteRequestsPending - } else if let _ = cachedData as? CachedSecretChatData { - } - - var pinnedMessage: ChatPinnedMessage? - switch strongSelf.chatLocation { - case let .replyThread(replyThreadMessage): - if isForum { - pinnedMessageId = topPinnedMessage?.message.id - pinnedMessage = topPinnedMessage - } else { - if isTopReplyThreadMessageShown { - pinnedMessageId = nil - } else { - pinnedMessageId = replyThreadMessage.effectiveTopId - } - if let pinnedMessageId = pinnedMessageId { - if let message = messages?[pinnedMessageId] { - pinnedMessage = ChatPinnedMessage(message: message, index: 0, totalCount: 1, topMessageId: message.id) - } - } - } - case .peer: - pinnedMessageId = topPinnedMessage?.message.id - pinnedMessage = topPinnedMessage - case .customChatContents: - pinnedMessageId = nil - pinnedMessage = nil - } - - var pinnedMessageUpdated = false - if let current = strongSelf.presentationInterfaceState.pinnedMessage, let updated = pinnedMessage { - if current != updated { - pinnedMessageUpdated = true - } - } else if (strongSelf.presentationInterfaceState.pinnedMessage != nil) != (pinnedMessage != nil) { - pinnedMessageUpdated = true - } - - let callsDataUpdated = strongSelf.presentationInterfaceState.callsAvailable != callsAvailable || strongSelf.presentationInterfaceState.callsPrivate != callsPrivate - - let voiceMessagesAvailableUpdated = strongSelf.presentationInterfaceState.voiceMessagesAvailable != voiceMessagesAvailable - - var canManageInvitations = false - if let channel = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.flags.contains(.isCreator) || (channel.adminRights?.rights.contains(.canInviteUsers) == true) { - canManageInvitations = true - } else if let group = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramGroup { - if case .creator = group.role { - canManageInvitations = true - } else if case let .admin(rights, _) = group.role, rights.rights.contains(.canInviteUsers) { - canManageInvitations = true - } - } - - if canManageInvitations, let inviteRequestsPending = inviteRequestsPending, inviteRequestsPending >= 0 { - if strongSelf.inviteRequestsContext == nil { - let inviteRequestsContext = strongSelf.context.engine.peers.peerInvitationImporters(peerId: peerId, subject: .requests(query: nil)) - strongSelf.inviteRequestsContext = inviteRequestsContext - - strongSelf.inviteRequestsDisposable.set((combineLatest(queue: Queue.mainQueue(), inviteRequestsContext.state, ApplicationSpecificNotice.dismissedInvitationRequests(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peerId))).startStrict(next: { [weak self] requestsState, dismissedInvitationRequests in - guard let strongSelf = self else { - return - } - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { state in - return state - .updatedTitlePanelContext({ context in - let peers: [EnginePeer] = Array(requestsState.importers.compactMap({ $0.peer.peer.flatMap({ EnginePeer($0) }) }).prefix(3)) - - var peersDismissed = false - if let dismissedInvitationRequests = dismissedInvitationRequests, Set(peers.map({ $0.id.toInt64() })) == Set(dismissedInvitationRequests) { - peersDismissed = true - } - - if requestsState.count > 0 && !peersDismissed { - if !context.contains(where: { - switch $0 { - case .inviteRequests(peers, requestsState.count): - return true - default: - return false - } - }) { - var updatedContexts = context.filter { c in - if case .inviteRequests = c { - return false - } else { - return true - } - } - updatedContexts.append(.inviteRequests(peers, requestsState.count)) - return updatedContexts.sorted() - } else { - return context - } - } else { - if let index = context.firstIndex(where: { - switch $0 { - case .inviteRequests: - return true - default: - return false - } - }) { - var updatedContexts = context - updatedContexts.remove(at: index) - return updatedContexts - } else { - return context - } - } - }) - .updatedSlowmodeState(slowmodeState) - }) - })) - } else if let inviteRequestsContext = strongSelf.inviteRequestsContext { - let _ = (inviteRequestsContext.state - |> take(1) - |> deliverOnMainQueue).startStandalone(next: { [weak inviteRequestsContext] state in - if state.count != inviteRequestsPending { - inviteRequestsContext?.loadMore() - } - }) - } - } - - if strongSelf.presentationInterfaceState.pinnedMessageId != pinnedMessageId || strongSelf.presentationInterfaceState.pinnedMessage != pinnedMessage || strongSelf.presentationInterfaceState.peerIsBlocked != peerIsBlocked || pinnedMessageUpdated || callsDataUpdated || voiceMessagesAvailableUpdated || strongSelf.presentationInterfaceState.slowmodeState != slowmodeState || strongSelf.presentationInterfaceState.activeGroupCallInfo != activeGroupCallInfo || customEmojiAvailable != strongSelf.presentationInterfaceState.customEmojiAvailable || threadData != strongSelf.presentationInterfaceState.threadData || forumTopicData != strongSelf.presentationInterfaceState.forumTopicData || premiumGiftOptions != strongSelf.presentationInterfaceState.premiumGiftOptions { - strongSelf.updateChatPresentationInterfaceState(animated: strongSelf.willAppear, interactive: strongSelf.willAppear, { state in - return state - .updatedPinnedMessageId(pinnedMessageId) - .updatedActiveGroupCallInfo(activeGroupCallInfo) - .updatedPinnedMessage(pinnedMessage) - .updatedPeerIsBlocked(peerIsBlocked) - .updatedCallsAvailable(callsAvailable) - .updatedCallsPrivate(callsPrivate) - .updatedVoiceMessagesAvailable(voiceMessagesAvailable) - .updatedCustomEmojiAvailable(customEmojiAvailable) - .updatedThreadData(threadData) - .updatedForumTopicData(forumTopicData) - .updatedIsGeneralThreadClosed(forumTopicData?.isClosed) - .updatedPremiumGiftOptions(premiumGiftOptions) - .updatedTitlePanelContext({ context in - if pinnedMessageId != nil { - if !context.contains(where: { - switch $0 { - case .pinnedMessage: - return true - default: - return false - } - }) { - var updatedContexts = context - updatedContexts.append(.pinnedMessage) - return updatedContexts.sorted() - } else { - return context - } - } else { - if let index = context.firstIndex(where: { - switch $0 { - case .pinnedMessage: - return true - default: - return false - } - }) { - var updatedContexts = context - updatedContexts.remove(at: index) - return updatedContexts - } else { - return context - } - } - }) - .updatedSlowmodeState(slowmodeState) - }) - } - - if !strongSelf.didSetCachedDataReady { - strongSelf.didSetCachedDataReady = true - strongSelf.cachedDataReady.set(.single(true)) - } - } - }) - } else { - if !self.didSetCachedDataReady { - self.didSetCachedDataReady = true - self.cachedDataReady.set(.single(true)) - } - } - } - func loadDisplayNodeImpl() { if #available(iOS 18.0, *) { if self.context.sharedContext.immediateExperimentalUISettings.enableLocalTranslation { @@ -2579,6 +543,39 @@ extension ChatControllerImpl { } }) + let hasPendingMessages: Signal + let chatLocationPeerId = self.chatLocation.peerId + + if let chatLocationPeerId = chatLocationPeerId { + hasPendingMessages = self.context.account.pendingMessageManager.hasPendingMessages + |> mapToSignal { peerIds -> Signal in + let value = peerIds.contains(chatLocationPeerId) + if value { + return .single(true) + } else { + return .single(false) + } + } + |> distinctUntilChanged + } else { + hasPendingMessages = .single(false) + } + + let isTopReplyThreadMessageShown: Signal = self.chatDisplayNode.historyNode.isTopReplyThreadMessageShown.get() + |> distinctUntilChanged + + let topPinnedMessage: Signal + if let subject = self.subject { + switch subject { + case .messageOptions, .pinnedMessages, .scheduledMessages: + topPinnedMessage = .single(nil) + default: + topPinnedMessage = self.topPinnedMessageSignal(latest: false) + } + } else { + topPinnedMessage = self.topPinnedMessageSignal(latest: false) + } + if let peerId = self.chatLocation.peerId { self.chatThemeEmoticonPromise.set(self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.ThemeEmoticon(id: peerId))) let chatWallpaper = self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Wallpaper(id: peerId)) @@ -2589,7 +586,397 @@ extension ChatControllerImpl { self.chatWallpaperPromise.set(.single(nil)) } - self.reloadCachedData() + if let peerId = self.chatLocation.peerId { + let customEmojiAvailable: Signal = self.context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.SecretChatLayer(id: peerId) + ) + |> map { layer -> Bool in + guard let layer = layer else { + return true + } + + return layer >= 144 + } + |> distinctUntilChanged + + let isForum = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> map { peer -> Bool in + if case let .channel(channel) = peer { + return channel.flags.contains(.isForum) + } else { + return false + } + } + |> distinctUntilChanged + + let context = self.context + let threadData: Signal + let forumTopicData: Signal + if let threadId = self.chatLocation.threadId { + let viewKey: PostboxViewKey = .messageHistoryThreadInfo(peerId: peerId, threadId: threadId) + threadData = context.account.postbox.combinedView(keys: [viewKey]) + |> map { views -> ChatPresentationInterfaceState.ThreadData? in + guard let view = views.views[viewKey] as? MessageHistoryThreadInfoView else { + return nil + } + guard let data = view.info?.data.get(MessageHistoryThreadData.self) else { + return nil + } + return ChatPresentationInterfaceState.ThreadData(title: data.info.title, icon: data.info.icon, iconColor: data.info.iconColor, isOwnedByMe: data.isOwnedByMe, isClosed: data.isClosed) + } + |> distinctUntilChanged + forumTopicData = .single(nil) + } else { + forumTopicData = isForum + |> mapToSignal { isForum -> Signal in + if isForum { + let viewKey: PostboxViewKey = .messageHistoryThreadInfo(peerId: peerId, threadId: 1) + return context.account.postbox.combinedView(keys: [viewKey]) + |> map { views -> ChatPresentationInterfaceState.ThreadData? in + guard let view = views.views[viewKey] as? MessageHistoryThreadInfoView else { + return nil + } + guard let data = view.info?.data.get(MessageHistoryThreadData.self) else { + return nil + } + return ChatPresentationInterfaceState.ThreadData(title: data.info.title, icon: data.info.icon, iconColor: data.info.iconColor, isOwnedByMe: data.isOwnedByMe, isClosed: data.isClosed) + } + |> distinctUntilChanged + } else { + return .single(nil) + } + } + threadData = .single(nil) + } + + if case .standard(.previewing) = self.presentationInterfaceState.mode { + + } else if peerId.namespace != Namespaces.Peer.SecretChat && peerId != context.account.peerId && self.subject != .scheduledMessages { + self.premiumGiftSuggestionDisposable = (ApplicationSpecificNotice.dismissedPremiumGiftSuggestion(accountManager: self.context.sharedContext.accountManager, peerId: peerId) + |> deliverOnMainQueue).startStrict(next: { [weak self] timestamp in + if let strongSelf = self { + let currentTime = Int32(Date().timeIntervalSince1970) + strongSelf.updateChatPresentationInterfaceState(animated: strongSelf.willAppear, interactive: strongSelf.willAppear, { state in + var suggest = true + if let timestamp, currentTime < timestamp + 60 * 60 * 24 { + suggest = false + } + return state.updatedSuggestPremiumGift(suggest) + }) + } + }) + + var baseLanguageCode = self.presentationData.strings.baseLanguageCode + if baseLanguageCode.contains("-") { + baseLanguageCode = baseLanguageCode.components(separatedBy: "-").first ?? baseLanguageCode + } + let isPremium = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> map { peer -> Bool in + return peer?.isPremium ?? false + } |> distinctUntilChanged + + let isHidden = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.TranslationHidden(id: peerId)) + |> distinctUntilChanged + + let hasAutoTranslate = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.AutoTranslateEnabled(id: peerId)) + |> distinctUntilChanged + + let chatLocation = self.chatLocation + self.translationStateDisposable = (combineLatest( + queue: .concurrentDefaultQueue(), + isPremium, + isHidden, + hasAutoTranslate, + ApplicationSpecificNotice.translationSuggestion(accountManager: self.context.sharedContext.accountManager) + ) |> mapToSignal { isPremium, isHidden, hasAutoTranslate, counterAndTimestamp -> Signal in + var maybeSuggestPremium = false + if counterAndTimestamp.0 >= 3 { + maybeSuggestPremium = true + } + if (isPremium || maybeSuggestPremium || hasAutoTranslate) && !isHidden { + return chatTranslationState(context: context, peerId: peerId, threadId: chatLocation.threadId) + |> map { translationState -> ChatPresentationTranslationState? in + if let translationState, !translationState.fromLang.isEmpty && (translationState.fromLang != baseLanguageCode || translationState.isEnabled) { + return ChatPresentationTranslationState(isEnabled: translationState.isEnabled, fromLang: translationState.fromLang, toLang: translationState.toLang ?? baseLanguageCode) + } else { + return nil + } + } + |> distinctUntilChanged + } else { + return .single(nil) + } + } + |> deliverOnMainQueue).startStrict(next: { [weak self] chatTranslationState in + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: strongSelf.willAppear, interactive: strongSelf.willAppear, { state in + return state.updatedTranslationState(chatTranslationState) + }) + } + }) + } + + let premiumGiftOptions: Signal<[CachedPremiumGiftOption], NoError> = .single([]) + |> then( + self.context.engine.payments.premiumGiftCodeOptions(peerId: peerId, onlyCached: true) + |> map { options in + return options.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) } + } + ) + + self.cachedDataDisposable = combineLatest(queue: .mainQueue(), self.chatDisplayNode.historyNode.cachedPeerDataAndMessages |> debug_measureTimeToFirstEvent(label: "cachedData_cachedPeerDataAndMessages"), + hasPendingMessages |> debug_measureTimeToFirstEvent(label: "cachedData_hasPendingMessages"), + isTopReplyThreadMessageShown |> debug_measureTimeToFirstEvent(label: "cachedData_isTopReplyThreadMessageShown"), + topPinnedMessage |> debug_measureTimeToFirstEvent(label: "cachedData_topPinnedMessage"), + customEmojiAvailable |> debug_measureTimeToFirstEvent(label: "cachedData_customEmojiAvailable"), + isForum |> debug_measureTimeToFirstEvent(label: "cachedData_isForum"), + threadData |> debug_measureTimeToFirstEvent(label: "cachedData_threadData"), + forumTopicData |> debug_measureTimeToFirstEvent(label: "cachedData_forumTopicData"), + premiumGiftOptions |> debug_measureTimeToFirstEvent(label: "cachedData_premiumGiftOptions") + ).startStrict(next: { [weak self] cachedDataAndMessages, hasPendingMessages, isTopReplyThreadMessageShown, topPinnedMessage, customEmojiAvailable, isForum, threadData, forumTopicData, premiumGiftOptions in + if let strongSelf = self { + let (cachedData, messages) = cachedDataAndMessages + + if cachedData != nil { + var themeEmoticon: String? = nil + var chatWallpaper: TelegramWallpaper? + if let cachedData = cachedData as? CachedUserData { + themeEmoticon = cachedData.themeEmoticon + chatWallpaper = cachedData.wallpaper + } else if let cachedData = cachedData as? CachedGroupData { + themeEmoticon = cachedData.themeEmoticon + } else if let cachedData = cachedData as? CachedChannelData { + themeEmoticon = cachedData.themeEmoticon + chatWallpaper = cachedData.wallpaper + } + + strongSelf.chatThemeEmoticonPromise.set(.single(themeEmoticon)) + strongSelf.chatWallpaperPromise.set(.single(chatWallpaper)) + } + + var pinnedMessageId: MessageId? + var peerIsBlocked: Bool = false + var callsAvailable: Bool = false + var callsPrivate: Bool = false + var voiceMessagesAvailable: Bool = true + var slowmodeState: ChatSlowmodeState? + var activeGroupCallInfo: ChatActiveGroupCallInfo? + var inviteRequestsPending: Int32? + if let cachedData = cachedData as? CachedChannelData { + pinnedMessageId = cachedData.pinnedMessageId + if !canBypassRestrictions(chatPresentationInterfaceState: strongSelf.presentationInterfaceState) { + if let channel = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isRestrictedBySlowmode, let timeout = cachedData.slowModeTimeout { + if hasPendingMessages { + slowmodeState = ChatSlowmodeState(timeout: timeout, variant: .pendingMessages) + } else if let slowmodeUntilTimestamp = calculateSlowmodeActiveUntilTimestamp(account: strongSelf.context.account, untilTimestamp: cachedData.slowModeValidUntilTimestamp) { + slowmodeState = ChatSlowmodeState(timeout: timeout, variant: .timestamp(slowmodeUntilTimestamp)) + } + } + } + if let activeCall = cachedData.activeCall { + activeGroupCallInfo = ChatActiveGroupCallInfo(activeCall: activeCall) + } + inviteRequestsPending = cachedData.inviteRequestsPending + } else if let cachedData = cachedData as? CachedUserData { + peerIsBlocked = cachedData.isBlocked + callsAvailable = cachedData.voiceCallsAvailable + callsPrivate = cachedData.callsPrivate + pinnedMessageId = cachedData.pinnedMessageId + voiceMessagesAvailable = cachedData.voiceMessagesAvailable + } else if let cachedData = cachedData as? CachedGroupData { + pinnedMessageId = cachedData.pinnedMessageId + if let activeCall = cachedData.activeCall { + activeGroupCallInfo = ChatActiveGroupCallInfo(activeCall: activeCall) + } + inviteRequestsPending = cachedData.inviteRequestsPending + } else if let _ = cachedData as? CachedSecretChatData { + } + + var pinnedMessage: ChatPinnedMessage? + switch strongSelf.chatLocation { + case let .replyThread(replyThreadMessage): + if isForum { + pinnedMessageId = topPinnedMessage?.message.id + pinnedMessage = topPinnedMessage + } else { + if isTopReplyThreadMessageShown { + pinnedMessageId = nil + } else { + pinnedMessageId = replyThreadMessage.effectiveTopId + } + if let pinnedMessageId = pinnedMessageId { + if let message = messages?[pinnedMessageId] { + pinnedMessage = ChatPinnedMessage(message: message, index: 0, totalCount: 1, topMessageId: message.id) + } + } + } + case .peer: + pinnedMessageId = topPinnedMessage?.message.id + pinnedMessage = topPinnedMessage + case .customChatContents: + pinnedMessageId = nil + pinnedMessage = nil + } + + var pinnedMessageUpdated = false + if let current = strongSelf.presentationInterfaceState.pinnedMessage, let updated = pinnedMessage { + if current != updated { + pinnedMessageUpdated = true + } + } else if (strongSelf.presentationInterfaceState.pinnedMessage != nil) != (pinnedMessage != nil) { + pinnedMessageUpdated = true + } + + let callsDataUpdated = strongSelf.presentationInterfaceState.callsAvailable != callsAvailable || strongSelf.presentationInterfaceState.callsPrivate != callsPrivate + + let voiceMessagesAvailableUpdated = strongSelf.presentationInterfaceState.voiceMessagesAvailable != voiceMessagesAvailable + + var canManageInvitations = false + if let channel = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.flags.contains(.isCreator) || (channel.adminRights?.rights.contains(.canInviteUsers) == true) { + canManageInvitations = true + } else if let group = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramGroup { + if case .creator = group.role { + canManageInvitations = true + } else if case let .admin(rights, _) = group.role, rights.rights.contains(.canInviteUsers) { + canManageInvitations = true + } + } + + if canManageInvitations, let inviteRequestsPending = inviteRequestsPending, inviteRequestsPending >= 0 { + if strongSelf.inviteRequestsContext == nil { + let inviteRequestsContext = strongSelf.context.engine.peers.peerInvitationImporters(peerId: peerId, subject: .requests(query: nil)) + strongSelf.inviteRequestsContext = inviteRequestsContext + + strongSelf.inviteRequestsDisposable.set((combineLatest(queue: Queue.mainQueue(), inviteRequestsContext.state, ApplicationSpecificNotice.dismissedInvitationRequests(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peerId))).startStrict(next: { [weak self] requestsState, dismissedInvitationRequests in + guard let strongSelf = self else { + return + } + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { state in + return state + .updatedTitlePanelContext({ context in + let peers: [EnginePeer] = Array(requestsState.importers.compactMap({ $0.peer.peer.flatMap({ EnginePeer($0) }) }).prefix(3)) + + var peersDismissed = false + if let dismissedInvitationRequests = dismissedInvitationRequests, Set(peers.map({ $0.id.toInt64() })) == Set(dismissedInvitationRequests) { + peersDismissed = true + } + + if requestsState.count > 0 && !peersDismissed { + if !context.contains(where: { + switch $0 { + case .inviteRequests(peers, requestsState.count): + return true + default: + return false + } + }) { + var updatedContexts = context.filter { c in + if case .inviteRequests = c { + return false + } else { + return true + } + } + updatedContexts.append(.inviteRequests(peers, requestsState.count)) + return updatedContexts.sorted() + } else { + return context + } + } else { + if let index = context.firstIndex(where: { + switch $0 { + case .inviteRequests: + return true + default: + return false + } + }) { + var updatedContexts = context + updatedContexts.remove(at: index) + return updatedContexts + } else { + return context + } + } + }) + .updatedSlowmodeState(slowmodeState) + }) + })) + } else if let inviteRequestsContext = strongSelf.inviteRequestsContext { + let _ = (inviteRequestsContext.state + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak inviteRequestsContext] state in + if state.count != inviteRequestsPending { + inviteRequestsContext?.loadMore() + } + }) + } + } + + if strongSelf.presentationInterfaceState.pinnedMessageId != pinnedMessageId || strongSelf.presentationInterfaceState.pinnedMessage != pinnedMessage || strongSelf.presentationInterfaceState.peerIsBlocked != peerIsBlocked || pinnedMessageUpdated || callsDataUpdated || voiceMessagesAvailableUpdated || strongSelf.presentationInterfaceState.slowmodeState != slowmodeState || strongSelf.presentationInterfaceState.activeGroupCallInfo != activeGroupCallInfo || customEmojiAvailable != strongSelf.presentationInterfaceState.customEmojiAvailable || threadData != strongSelf.presentationInterfaceState.threadData || forumTopicData != strongSelf.presentationInterfaceState.forumTopicData || premiumGiftOptions != strongSelf.presentationInterfaceState.premiumGiftOptions { + strongSelf.updateChatPresentationInterfaceState(animated: strongSelf.willAppear, interactive: strongSelf.willAppear, { state in + return state + .updatedPinnedMessageId(pinnedMessageId) + .updatedActiveGroupCallInfo(activeGroupCallInfo) + .updatedPinnedMessage(pinnedMessage) + .updatedPeerIsBlocked(peerIsBlocked) + .updatedCallsAvailable(callsAvailable) + .updatedCallsPrivate(callsPrivate) + .updatedVoiceMessagesAvailable(voiceMessagesAvailable) + .updatedCustomEmojiAvailable(customEmojiAvailable) + .updatedThreadData(threadData) + .updatedForumTopicData(forumTopicData) + .updatedIsGeneralThreadClosed(forumTopicData?.isClosed) + .updatedPremiumGiftOptions(premiumGiftOptions) + .updatedTitlePanelContext({ context in + if pinnedMessageId != nil { + if !context.contains(where: { + switch $0 { + case .pinnedMessage: + return true + default: + return false + } + }) { + var updatedContexts = context + updatedContexts.append(.pinnedMessage) + return updatedContexts.sorted() + } else { + return context + } + } else { + if let index = context.firstIndex(where: { + switch $0 { + case .pinnedMessage: + return true + default: + return false + } + }) { + var updatedContexts = context + updatedContexts.remove(at: index) + return updatedContexts + } else { + return context + } + } + }) + .updatedSlowmodeState(slowmodeState) + }) + } + + if !strongSelf.didSetCachedDataReady { + strongSelf.didSetCachedDataReady = true + strongSelf.cachedDataReady.set(.single(true)) + } + } + }) + } else { + if !self.didSetCachedDataReady { + self.didSetCachedDataReady = true + self.cachedDataReady.set(.single(true)) + } + } self.historyStateDisposable = self.chatDisplayNode.historyNode.historyState.get().startStrict(next: { [weak self] state in if let strongSelf = self { @@ -5744,36 +4131,15 @@ extension ChatControllerImpl { strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Conversation_GigagroupDescription, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return true }), in: .current) } }, openSuggestPost: { [weak self] in - guard let self else { + let _ = self + /*guard let self else { return - } - guard let peerId = self.chatLocation.peerId else { + } + guard let peerId = self.chatLocation.peerId else { return - } - - let _ = (self.context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.LinkedMonoforumPeerId(id: peerId) - ) - |> deliverOnMainQueue).startStandalone(next: { [weak self] monoforumPeerIdValue in - guard let self, case let .known(monoforumPeerIdValue) = monoforumPeerIdValue, let monoforumPeerId = monoforumPeerIdValue else { - return - } - - let _ = (self.context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: monoforumPeerId) - ) - |> deliverOnMainQueue).startStandalone(next: { [weak self] monoforumPeer in - guard let self, let monoforumPeer else { - return - } - guard let navigationController = self.effectiveNavigationController else { - return - } - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(monoforumPeer), keepStack: .always)) - }) - }) - - /*let contents = PostSuggestionsChatContents( + } + + let contents = PostSuggestionsChatContents( context: self.context, peerId: peerId ) @@ -6224,58 +4590,6 @@ extension ChatControllerImpl { } else { apply() } - }, updateChatLocationThread: { [weak self] threadId in - guard let self else { - return - } - guard let peerId = self.chatLocation.peerId else { - return - } - guard let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer else { - return - } - let updatedChatLocation: ChatLocation - if let threadId { - var isMonoforum = false - if let channel = peer as? TelegramChannel, channel.flags.contains(.isMonoforum) { - isMonoforum = true - } - - updatedChatLocation = .replyThread(message: ChatReplyThreadMessage( - peerId: peerId, - threadId: threadId, - channelMessageId: nil, - isChannelPost: false, - isForumPost: true, - isMonoforum: isMonoforum, - maxMessage: nil, - maxReadIncomingMessageId: nil, - maxReadOutgoingMessageId: nil, - unreadCount: 0, - initialFilledHoles: IndexSet(), - initialAnchor: .automatic, - isNotAvailable: false - )) - } else { - updatedChatLocation = .peer(id: peerId) - } - self.updateChatPresentationInterfaceState(animated: true, interactive: false, { presentationInterfaceState in - return presentationInterfaceState.updatedChatLocation(updatedChatLocation) - }) - }, toggleChatSidebarMode: { [weak self] in - guard let self else { - return - } - self.updateChatPresentationInterfaceState(animated: true, interactive: true, { presentationInterfaceState in - let topicListDisplayMode: ChatPresentationInterfaceState.TopicListDisplayMode - switch presentationInterfaceState.topicListDisplayMode ?? .top { - case .top: - topicListDisplayMode = .side - case .side: - topicListDisplayMode = .top - } - return presentationInterfaceState.updatedTopicListDisplayMode(topicListDisplayMode) - }) }, updateDisplayHistoryFilterAsList: { [weak self] displayAsList in guard let self else { return @@ -6718,7 +5032,6 @@ extension ChatControllerImpl { channelMessageId: nil, isChannelPost: false, isForumPost: true, - isMonoforum: false, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 46681e3139..d92b509e98 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -1,4 +1,5 @@ import Foundation +import UniformTypeIdentifiers import UIKit import Display import AsyncDisplayKit @@ -44,6 +45,7 @@ import TelegramNotices import AnimatedCountLabelNode import TelegramStringFormatting import TextNodeWithEntities +import DeviceModel private let accessoryButtonFont = Font.medium(14.0) private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers]) @@ -4473,10 +4475,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch var attributedString: NSAttributedString? if let data = pasteboard.data(forPasteboardType: "private.telegramtext"), let value = chatInputStateStringFromAppSpecificString(data: data) { attributedString = value - } else if let data = pasteboard.data(forPasteboardType: kUTTypeRTF as String) { + } else if let data = pasteboard.data(forPasteboardType: "public.rtf") { attributedString = chatInputStateStringFromRTF(data, type: NSAttributedString.DocumentType.rtf) } else if let data = pasteboard.data(forPasteboardType: "com.apple.flat-rtfd") { - attributedString = chatInputStateStringFromRTF(data, type: NSAttributedString.DocumentType.rtfd) + if let _ = pasteboard.data(forPasteboardType: "com.apple.notes.richtext"), DeviceModel.current.isIpad, let htmlData = pasteboard.data(forPasteboardType: "public.html") { + attributedString = chatInputStateStringFromRTF(htmlData, type: NSAttributedString.DocumentType.html) + } else { + attributedString = chatInputStateStringFromRTF(data, type: NSAttributedString.DocumentType.rtfd) + } } if let attributedString = attributedString { diff --git a/submodules/TelegramUniversalVideoContent/Sources/HLSVideoJSNativeContentNode.swift b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoJSNativeContentNode.swift index e4dca05f71..2115c94488 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/HLSVideoJSNativeContentNode.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoJSNativeContentNode.swift @@ -1093,7 +1093,7 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod var onSeeked: (() -> Void)? self.player = ChunkMediaPlayerV2( params: ChunkMediaPlayerV2.MediaDataReaderParams(context: context), - audioContext: ChunkMediaPlayerV2.AudioContext(audioSessionManager: audioSessionManager), + audioSessionManager: audioSessionManager, source: .externalParts(self.chunkPlayerPartsState.get()), video: true, enableSound: self.enableSound, diff --git a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift index afb8dadbba..e8b67bd497 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift @@ -520,7 +520,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent } else { let mediaPlayer = ChunkMediaPlayerV2( params: ChunkMediaPlayerV2.MediaDataReaderParams(context: context), - audioContext: ChunkMediaPlayerV2.AudioContext(audioSessionManager: audioSessionManager), + audioSessionManager: audioSessionManager, source: .directFetch(ChunkMediaPlayerV2.SourceDescription.ResourceDescription( postbox: postbox, size: selectedFile.size ?? 0, diff --git a/submodules/TranslateUI/Sources/ChatTranslation.swift b/submodules/TranslateUI/Sources/ChatTranslation.swift index 8370e509f0..98b317cd03 100644 --- a/submodules/TranslateUI/Sources/ChatTranslation.swift +++ b/submodules/TranslateUI/Sources/ChatTranslation.swift @@ -78,7 +78,7 @@ public struct ChatTranslationState: Codable { private func cachedChatTranslationState(engine: TelegramEngine, peerId: EnginePeer.Id, threadId: Int64?) -> Signal { let key: EngineDataBuffer if let threadId { - key = EngineDataBuffer(length: 8 + 8) + key = EngineDataBuffer(length: 16) key.setInt64(0, value: peerId.id._internalGetInt64Value()) key.setInt64(8, value: threadId) } else { @@ -95,7 +95,7 @@ private func cachedChatTranslationState(engine: TelegramEngine, peerId: EnginePe private func updateChatTranslationState(engine: TelegramEngine, peerId: EnginePeer.Id, threadId: Int64?, state: ChatTranslationState?) -> Signal { let key: EngineDataBuffer if let threadId { - key = EngineDataBuffer(length: 8 + 8) + key = EngineDataBuffer(length: 16) key.setInt64(0, value: peerId.id._internalGetInt64Value()) key.setInt64(8, value: threadId) } else { @@ -111,10 +111,14 @@ private func updateChatTranslationState(engine: TelegramEngine, peerId: EnginePe } public func updateChatTranslationStateInteractively(engine: TelegramEngine, peerId: EnginePeer.Id, threadId: Int64?, _ f: @escaping (ChatTranslationState?) -> ChatTranslationState?) -> Signal { - let key = EngineDataBuffer(length: 8) - key.setInt64(0, value: peerId.id._internalGetInt64Value()) + let key: EngineDataBuffer if let threadId { + key = EngineDataBuffer(length: 16) + key.setInt64(0, value: peerId.id._internalGetInt64Value()) key.setInt64(8, value: threadId) + } else { + key = EngineDataBuffer(length: 8) + key.setInt64(0, value: peerId.id._internalGetInt64Value()) } return engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.translationState, id: key)) diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 4324257087..dda6079112 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -828,7 +828,11 @@ public final class WebAppController: ViewController, AttachmentContainable { } if let webView = self.webView { - var scrollInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0) + let inputHeight = self.validLayout?.0.inputHeight ?? 0.0 + + let intrinsicBottomInset = layout.intrinsicInsets.bottom > 40.0 ? layout.intrinsicInsets.bottom : 0.0 + + var scrollInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: max(inputHeight, intrinsicBottomInset), right: 0.0) var frameBottomInset: CGFloat = 0.0 if scrollInset.bottom > 40.0 { frameBottomInset = scrollInset.bottom @@ -841,12 +845,12 @@ public final class WebAppController: ViewController, AttachmentContainable { if !webView.frame.width.isZero && webView.frame != webViewFrame { self.updateWebViewWhenStable = true } - - var bottomInset = layout.intrinsicInsets.bottom + layout.additionalInsets.bottom - if let inputHeight = self.validLayout?.0.inputHeight, inputHeight > 44.0 { - bottomInset = max(bottomInset, inputHeight) + + var viewportBottomInset = max(frameBottomInset, scrollInset.bottom) + if (self.validLayout?.0.inputHeight ?? 0.0) < 44.0 { + viewportBottomInset += layout.additionalInsets.bottom } - let viewportFrame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: topInset), size: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: max(1.0, layout.size.height - topInset - bottomInset))) + let viewportFrame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: topInset), size: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: max(1.0, layout.size.height - topInset - viewportBottomInset))) if webView.scrollView.contentInset != scrollInset { webView.scrollView.contentInset = scrollInset @@ -1061,6 +1065,10 @@ public final class WebAppController: ViewController, AttachmentContainable { } else { self.lastExpansionTimestamp = currentTimestamp controller.requestAttachmentMenuExpansion() + + Queue.mainQueue().after(0.4) { + self.webView?.setNeedsLayout() + } } case "web_app_close": controller.dismiss()