diff --git a/Nicegram/NGData/Sources/NGSettings.swift b/Nicegram/NGData/Sources/NGSettings.swift index 76915b7ca61..38cebcb1cdd 100644 --- a/Nicegram/NGData/Sources/NGSettings.swift +++ b/Nicegram/NGData/Sources/NGSettings.swift @@ -134,7 +134,7 @@ public func isPremium() -> Bool { if #available(iOS 13.0, *) { return PremiumContainer.shared .getPremiumStatusUseCase() - .hasPremiumOnDeviceNonIsolated() + .hasPremiumOnDevice() } else { return false } diff --git a/Nicegram/NGEnv/Sources/NGEnv.swift b/Nicegram/NGEnv/Sources/NGEnv.swift index b6355e8a4c0..eda2d2515ee 100644 --- a/Nicegram/NGEnv/Sources/NGEnv.swift +++ b/Nicegram/NGEnv/Sources/NGEnv.swift @@ -17,7 +17,6 @@ public struct NGEnvObj: Decodable { public let remote_config_cache_duration_seconds: Double public let tapjoy_api_key: String public let telegram_auth_bot: String - public let google_cloud_api_key: String public let applovin_api_key: String public let applovin_ad_unit_id: String public let websocket_url: URL diff --git a/Nicegram/NGTranslate/BUILD b/Nicegram/NGTranslate/BUILD index 8071c2bb6b4..ea7ce64963d 100644 --- a/Nicegram/NGTranslate/BUILD +++ b/Nicegram/NGTranslate/BUILD @@ -7,6 +7,7 @@ swift_library( "Sources/**/*.swift", ]), deps = [ + "@swiftpkg_nicegram_assistant_ios//:Sources_FeatSpeechToText", "@swiftpkg_nicegram_assistant_ios//:Sources_NGCore", "//submodules/AccountContext:AccountContext", "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", diff --git a/Nicegram/NGTranslate/Sources/SpeechToText/GoogleSpeechToTextProcessor.swift b/Nicegram/NGTranslate/Sources/SpeechToText/GoogleSpeechToTextProcessor.swift deleted file mode 100644 index a9136d82887..00000000000 --- a/Nicegram/NGTranslate/Sources/SpeechToText/GoogleSpeechToTextProcessor.swift +++ /dev/null @@ -1,129 +0,0 @@ -import Foundation -import NGCore -import NGModels - -struct GoogleRecognitionConfig: Encodable { - let encoding: String - let sampleRateHertz: Int - let languageCode: String - let alternativeLanguageCodes: [String] - let enableAutomaticPunctuation: Bool -} - -struct GoogleRecognitionResult { - let parts: [Translate] - - struct Translate { - let text: String - let confidence: Double - } -} - -class GoogleSpeechToTextProcessor { - - // MARK: - Dependencies - - private let urlSession: URLSession - - // MARK: - Logic - - private let apiKey: String - - private let url = URL(string: "https://speech.googleapis.com/v1p1beta1/speech:recognize") - - // MARK: - Lifecycle - - init(apiKey: String, urlSession: URLSession = .shared) { - self.apiKey = apiKey - self.urlSession = urlSession - } - - // MARK: - Public Functions - - func recognize(audioData: Data, config: GoogleRecognitionConfig, completion: ((Result) -> ())?) { - guard var url = url else { - completion?(.failure(MessageError.unknown)) - return - } - url = url.appendingQuery(key: "key", value: apiKey) - - let body = makeBody(audioData: audioData, config: config) - - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.httpBody = try? JSONEncoder().encode(body) - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - - urlSession.dataTask(with: request) { data, response, error in - let result = self.mapApiResponse(data: data, response: response, error: error) - completion?(result) - }.resume() - } -} - -// MARK: - Private Functions - -private extension GoogleSpeechToTextProcessor { - func makeBody(audioData: Data, config: GoogleRecognitionConfig) -> BodyDTO { - return BodyDTO(config: config, audio: .init(content: audioData.base64EncodedString())) - } - - func mapApiResponse(data: Data?, response: URLResponse?, error: Error?) -> Result { - if let error = error { - return .failure(error) - } - - guard let code = (response as? HTTPURLResponse)?.statusCode else { - return .failure(MessageError.unknown) - } - - switch code { - case 200: - do { - guard let data = data else { - return .failure(MessageError.unknown) - } - - let dto = try JSONDecoder().decode(ResponseDTO.self, from: data) - let recognitionResult = mapResponseDto(dto) - return .success(recognitionResult) - } catch { - return .failure(error) - } - default: - return .failure(MessageError.unknown) - } - } - - func mapResponseDto(_ dto: ResponseDTO) -> GoogleRecognitionResult { - let results = dto.results ?? [] - let parts: [GoogleRecognitionResult.Translate] = results - .reduce([], { $0 + [$1.alternatives?.first].compactMap({$0}) }) - .map({ .init(text: $0.transcript, confidence: $0.confidence) }) - return GoogleRecognitionResult(parts: parts) - } -} - -// MARK: - DTO - -private struct BodyDTO: Encodable { - let config: GoogleRecognitionConfig - let audio: Audio - - struct Audio: Encodable { - let content: String - } -} - -private struct ResponseDTO: Decodable { - let results: [Result]? - - struct Result: Decodable { - let alternatives: [Alternative]? - - struct Alternative: Decodable { - let transcript: String - let confidence: Double - } - } -} diff --git a/Nicegram/NGTranslate/Sources/SpeechToText/TgSpeechToTextManager.swift b/Nicegram/NGTranslate/Sources/SpeechToText/TgSpeechToTextManager.swift new file mode 100644 index 00000000000..9396e8e6c96 --- /dev/null +++ b/Nicegram/NGTranslate/Sources/SpeechToText/TgSpeechToTextManager.swift @@ -0,0 +1,44 @@ +import FeatSpeechToText +import Foundation +import Postbox +import SwiftSignalKit +import TelegramCore + +@available(iOS 13.0, *) +public class TgSpeechToTextManager { + + // MARK: - Dependencies + + private let convertSpeechToTextUseCase: ConvertSpeechToTextUseCase + private let mediaBox: MediaBox + + // MARK: - Lifecycle + + public init(mediaBox: MediaBox) { + self.convertSpeechToTextUseCase = SpeechToTextContainer.shared.convertSpeechToTextUseCase() + self.mediaBox = mediaBox + } +} + +@available(iOS 13.0, *) +public extension TgSpeechToTextManager { + func convertSpeechToText( + mediaFile: TelegramMediaFile + ) async -> ConvertSpeechToTextResult { + await withCheckedContinuation { continuation in + let _ = (mediaBox.resourceData(mediaFile.resource) + |> take(1)).start { data in + let url = URL( + fileURLWithPath: data.path + ) + + Task { + let result = await self.convertSpeechToTextUseCase( + url: url + ) + continuation.resume(returning: result) + } + } + } + } +} diff --git a/Nicegram/NGTranslate/Sources/SpeechToText/TgVoiceToTextProcessor.swift b/Nicegram/NGTranslate/Sources/SpeechToText/TgVoiceToTextProcessor.swift deleted file mode 100644 index 3994dd2481b..00000000000 --- a/Nicegram/NGTranslate/Sources/SpeechToText/TgVoiceToTextProcessor.swift +++ /dev/null @@ -1,146 +0,0 @@ -import Foundation -import Postbox -import SwiftSignalKit -import TelegramCore -import NGData -import NGEnv -import NGStrings - -public enum TgVoiceToTextError: Error { - case needPremium - case lowAccuracy - case underlying(Error) -} - -extension TgVoiceToTextError: LocalizedError { - public var errorDescription: String? { - switch self { - case .needPremium, .lowAccuracy: - return nil - case .underlying(let error): - return error.localizedDescription - } - } -} - -public class TgVoiceToTextProcessor { - - // MARK: - Dependencies - - private let mediaBox: MediaBox - private let googleProcessor: GoogleSpeechToTextProcessor - - // MARK: - Logic - - private let additionalLanguageCodes: [String] - - private let encodingFormat = "OGG-OPUS" - private let sampleRateHertz = 48000 - private let thresholdAccuracy = 0.7 - - // MARK: - Lifecycle - - init(mediaBox: MediaBox, googleProcessor: GoogleSpeechToTextProcessor, additionalLanguageCodes: [String]) { - self.mediaBox = mediaBox - self.googleProcessor = googleProcessor - self.additionalLanguageCodes = additionalLanguageCodes - } - - // MARK: - Public Functions - - public func recognize(mediaFile: TelegramMediaFile, completion: ((Result) -> ())?) { - guard isPremium() else { - completion?(.failure(.needPremium)) - return - } - - let _ = (mediaBox.resourceData(mediaFile.resource) - |> take(1)).start { data in - do { - let audioData = try self.extractAudioData(data) - let config = self.makeRecognitionConfig() - - self.googleProcessor.recognize(audioData: audioData, config: config) { result in - let result = self.mapGoogleRecognitionResponse(result) - DispatchQueue.main.async { - completion?(result) - } - } - } catch { - DispatchQueue.main.async { - completion?(.failure(.underlying(error))) - } - } - } - } -} - -// MARK: - Convenience init - -public extension TgVoiceToTextProcessor { - convenience init(mediaBox: MediaBox, additionalLanguageCodes: [String]) { - let googleProcessor = GoogleSpeechToTextProcessor(apiKey: NGENV.google_cloud_api_key) - self.init(mediaBox: mediaBox, googleProcessor: googleProcessor, additionalLanguageCodes: additionalLanguageCodes) - } -} - -// MARK: - Private Functions - -private extension TgVoiceToTextProcessor { - func extractAudioData(_ data: MediaResourceData) throws -> Data { - let path = data.path - let url = URL(fileURLWithPath: path) - let data = try Data(contentsOf: url) - return data - } - - func makeRecognitionConfig() -> GoogleRecognitionConfig { - let mainCode = Bundle.main.preferredLocalizations.first ?? "en" - var additionalCodes = Set( - additionalLanguageCodes + [ - Locale.current.languageWithScriptCode, - "en" - ] - ) - additionalCodes.remove(mainCode) - - return GoogleRecognitionConfig( - encoding: encodingFormat, - sampleRateHertz: sampleRateHertz, - languageCode: mainCode, - alternativeLanguageCodes: Array(additionalCodes), - enableAutomaticPunctuation: true - ) - } - - func mapGoogleRecognitionResponse(_ result: Result) -> Result { - switch result { - case .success(let result): - return mapGoogleRecognitionResult(result) - case .failure(let error): - return .failure(.underlying(error)) - } - } - - func mapGoogleRecognitionResult(_ result: GoogleRecognitionResult) -> Result { - let accuracy = measureAccuracy(result) - guard accuracy > thresholdAccuracy else { - return .failure(.lowAccuracy) - } - - return .success(result.parts.map(\.text).joined()) - } - - func measureAccuracy(_ result: GoogleRecognitionResult) -> Double { - let sum = result.parts.reduce(0, { $0 + $1.confidence }) - - let averageAccuracy: Double - if result.parts.isEmpty { - averageAccuracy = 0 - } else { - averageAccuracy = sum / Double(result.parts.count) - } - - return averageAccuracy - } -} diff --git a/Nicegram/NGUI/BUILD b/Nicegram/NGUI/BUILD index 2511bf14389..a53dd2483fe 100644 --- a/Nicegram/NGUI/BUILD +++ b/Nicegram/NGUI/BUILD @@ -42,6 +42,7 @@ swift_library( "//Nicegram/NGStealthMode:NGStealthMode", "@swiftpkg_nicegram_assistant_ios//:Sources_NGAiChatUI", "@swiftpkg_nicegram_assistant_ios//:Sources_FeatImagesHubUI", + "@swiftpkg_nicegram_assistant_ios//:Sources_FeatSpeechToText", ], visibility = [ "//visibility:public", diff --git a/Nicegram/NGUI/Sources/PremiumController.swift b/Nicegram/NGUI/Sources/PremiumController.swift index 1bfd09d0c37..7a84ed0c42e 100644 --- a/Nicegram/NGUI/Sources/PremiumController.swift +++ b/Nicegram/NGUI/Sources/PremiumController.swift @@ -6,8 +6,9 @@ // Copyright © 2019 Nicegram. All rights reserved. // -import Foundation import Display +import FeatSpeechToText +import Foundation import SwiftSignalKit import Postbox import TelegramCore @@ -46,6 +47,7 @@ private enum premiumControllerSection: Int32 { case notifyMissed case manageFilters case other + case speechToText case stealthMode case test } @@ -82,6 +84,8 @@ private enum PremiumControllerEntry: ItemListNodeEntry { case testButton(PresentationTheme, String) case ignoretr(PresentationTheme, String) + case useOpenAi(Bool) + case stealthMode(Bool) var section: ItemListSectionId { @@ -98,6 +102,8 @@ private enum PremiumControllerEntry: ItemListNodeEntry { return premiumControllerSection.other.rawValue case .testButton: return premiumControllerSection.test.rawValue + case .useOpenAi: + return premiumControllerSection.speechToText.rawValue case .stealthMode: return premiumControllerSection.stealthMode.rawValue } @@ -130,8 +136,10 @@ private enum PremiumControllerEntry: ItemListNodeEntry { return 11000 case .ignoretr: return 12000 - case .stealthMode: + case .useOpenAi: return 13000 + case .stealthMode: + return 14000 case .testButton: return 999999 } @@ -225,6 +233,12 @@ private enum PremiumControllerEntry: ItemListNodeEntry { } else { return false } + case let .useOpenAi(lhsValue): + if case let .useOpenAi(rhsValue) = rhs, lhsValue == rhsValue { + return true + } else { + return false + } case let .stealthMode(lhsValue): if case let .stealthMode(rhsValue) = rhs, lhsValue == rhsValue { return true @@ -284,6 +298,17 @@ private enum PremiumControllerEntry: ItemListNodeEntry { return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: true, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleSetting(value, .rememberFilterOnExit) }) + case let .useOpenAi(value): + return ItemListSwitchItem(presentationData: presentationData, title: l("SpeechToText.UseOpenAi"), value: value, enabled: true, sectionId: self.section, style: .blocks, updated: { value in + if #available(iOS 13.0, *) { + Task { + let setPreferredProviderTypeUseCase = SpeechToTextContainer.shared.setPreferredProviderTypeUseCase() + await setPreferredProviderTypeUseCase( + value ? .openAi : .google + ) + } + } + }) case let .stealthMode(value): return ItemListSwitchItem(presentationData: presentationData, title: NGStealthMode.Resources.toggleTitle(), value: value, enabled: true, sectionId: self.section, style: .blocks, updated: { value in NGStealthMode.setStealthModeEnabled(value) @@ -293,7 +318,7 @@ private enum PremiumControllerEntry: ItemListNodeEntry { } -private func premiumControllerEntries(presentationData: PresentationData) -> [PremiumControllerEntry] { +private func premiumControllerEntries(presentationData: PresentationData, useOpenAi: Bool) -> [PremiumControllerEntry] { var entries: [PremiumControllerEntry] = [] let theme = presentationData.theme @@ -304,6 +329,8 @@ private func premiumControllerEntries(presentationData: PresentationData) -> [Pr entries.append(.onetaptr(theme, l("Premium.OnetapTranslate", locale), NGSettings.oneTapTr)) entries.append(.ignoretr(theme, l("Premium.IgnoreTranslate.Title", locale))) + entries.append(.useOpenAi(useOpenAi)) + entries.append(.stealthMode(NGStealthMode.isStealthModeEnabled())) #if DEBUG @@ -343,10 +370,18 @@ public func premiumController(context: AccountContext) -> ViewController { let updateState: ((SelectionState) -> SelectionState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } - - - func updateTabs() { - // + + let useOpenAiSignal: Signal + if #available(iOS 13.0, *) { + let getPreferredProviderTypeUseCase = SpeechToTextContainer.shared.getPreferredProviderTypeUseCase() + + useOpenAiSignal = getPreferredProviderTypeUseCase + .publisher() + .map { $0 == .openAi } + .toSignal() + .setNoError() + } else { + useOpenAiSignal = .single(false) } let arguments = PremiumControllerArguments(toggleSetting: { value, setting in @@ -454,10 +489,10 @@ public func premiumController(context: AccountContext) -> ViewController { - let signal = combineLatest(context.sharedContext.presentationData, statePromise.get()) - |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in + let signal = combineLatest(context.sharedContext.presentationData, statePromise.get(), useOpenAiSignal) + |> map { presentationData, state, useOpenAi -> (ItemListControllerState, (ItemListNodeState, Any)) in - let entries = premiumControllerEntries(presentationData: presentationData) + let entries = premiumControllerEntries(presentationData: presentationData, useOpenAi: useOpenAi) var _ = 0 var scrollToItem: ListViewScrollToItem? diff --git a/Package.resolved b/Package.resolved index b7da4a6383d..5b8d6022bf7 100644 --- a/Package.resolved +++ b/Package.resolved @@ -42,7 +42,7 @@ "location" : "git@bitbucket.org:mobyrix/nicegram-assistant-ios.git", "state" : { "branch" : "develop", - "revision" : "b0d993e6fcb6e48734d8361c26bd401752ac5513" + "revision" : "2cf7fd3fa9abeaee81fd2b970681ab3ae83043f4" } }, { @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/SDWebImage/SDWebImage.git", "state" : { - "revision" : "e278c13e46e8d20c895c221e922c6ac6b72aaca9", - "version" : "5.18.7" + "revision" : "fd010e54231331fc19338f81c6d072cd9ace2825", + "version" : "5.18.8" } }, { @@ -68,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/SnapKit/SnapKit.git", "state" : { - "revision" : "f222cbdf325885926566172f6f5f06af95473158", - "version" : "5.6.0" + "revision" : "e74fe2a978d1216c3602b129447c7301573cc2d8", + "version" : "5.7.0" } }, { diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 944e98280e7..91ef316175a 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -4418,6 +4418,7 @@ Sorry for the inconvenience."; "Call.Mute" = "mute"; "Call.Camera" = "camera"; +"Call.Video" = "video"; "Call.Flip" = "flip"; "Call.End" = "end"; "Call.Speaker" = "speaker"; @@ -5646,6 +5647,7 @@ Sorry for the inconvenience."; "Call.CameraConfirmationText" = "Switch to video call?"; "Call.CameraConfirmationConfirm" = "Switch"; +"Call.YourCameraOff" = "Your camera is off"; "Call.YourMicrophoneOff" = "Your microphone is off"; "Call.MicrophoneOff" = "%@'s microphone is off"; "Call.CameraOff" = "%@'s camera is off"; @@ -10379,6 +10381,8 @@ Sorry for the inconvenience."; "Chat.Giveaway.Message.PrizeText" = "%1$@ for %2$@."; "Chat.Giveaway.Message.Subscriptions_1" = "**%@** Telegram Premium Subscription"; "Chat.Giveaway.Message.Subscriptions_any" = "**%@** Telegram Premium Subscriptions"; +"Chat.Giveaway.Message.WithSubscriptions_1" = "**%@** Telegram Premium Subscription"; +"Chat.Giveaway.Message.WithSubscriptions_any" = "**%@** Telegram Premium Subscriptions"; "Chat.Giveaway.Message.Months_1" = "**%@** month"; "Chat.Giveaway.Message.Months_any" = "**%@** months"; "Chat.Giveaway.Message.ParticipantsTitle" = "Participants"; @@ -10392,6 +10396,8 @@ Sorry for the inconvenience."; "Chat.Giveaway.Message.DateTitle" = "Winners Selection Date"; "Chat.Giveaway.Message.LearnMore" = "LEARN MORE"; "Chat.Giveaway.Message.With" = "with"; +"Chat.Giveaway.Message.CustomPrizeQuantity_1" = "%@"; +"Chat.Giveaway.Message.CustomPrizeQuantity_any" = "%@"; "GiftLink.Title" = "Gift Link"; "GiftLink.UsedTitle" = "Used Gift Link"; @@ -10835,3 +10841,45 @@ Sorry for the inconvenience."; "RequestPeer.SelectUsers.SearchPlaceholder" = "Search"; "RequestPeer.ReachedMaximum_1" = "You can select up to %@ user."; "RequestPeer.ReachedMaximum_any" = "You can select up to %@ users."; + +"ChatList.DeleteSavedPeerConfirmation" = "Are you sure you want to delete saved messages from %@?"; + +"Message.VoiceMessageExpired" = "Expired voice message"; +"Message.VideoMessageExpired" = "Expired video message"; + +"Chat.PlayOnceVoiceMessageTooltip" = "This voice message can only be played once."; +"Chat.PlayOnceVoiceMessageYourTooltip" = "This message will disappear once **%@** plays it once."; + +"Chat.TapToPlayVoiceMessageOnceTooltip" = "Tap to set this message to **Play Once**"; +"Chat.PlayVoiceMessageOnceTooltip" = "The recipients will be able to listen to it only once."; + +"ChatList.AuthorHidden" = "Author Hidden"; +"SavedMessages.SubChatDeleted" = "Saved messages deleted."; +"PeerInfo.SavedMessagesTabTitle" = "Saved"; + +"Chat.PanelStatusAuthorHidden" = "Senders of these messages restricted to link\ntheir name when forwarding."; +"Chat.SavedMessagesChatsTooltip" = "Tap to view your Saved Messages organized by type or source"; + +"Chat.ConfirmationRemoveFromSavedMessages" = "Remove from Saved Messages"; + +"Premium.MaxSavedPinsText" = "Sorry, you can't pin more than **%1$@** chats to the top. Unpin some that are currently pinned or subscribe to **Telegram Premium** to double the limit to **%2$@** chats."; +"Premium.MaxSavedPinsNoPremiumText" = "Sorry, you can't pin more than **%@** chats to the top. Unpin some that are currently pinned."; +"Premium.MaxSavedPinsFinalText" = "Sorry, you can't pin more than **%@** chats to the top. Unpin some that are currently pinned."; + +"Chat.PlayOnceMesasgeClose" = "Close"; +"Chat.PlayOnceMesasgeCloseAndDelete" = "Close and Delete"; +"Chat.PlayOnceMesasge.DisableScreenCapture" = "Sorry, you can't play this message while screen recording is active."; + +"Call.EncryptedAlertTitle" = "This call is end-to-end encrypted"; +"Call.EncryptedAlertText" = "If the emoji on %@'s screen are the same, this call is 100% secure."; +"Call.EncryptionKeyTooltip" = "Encryption key of this call"; +"Call.StatusBusy" = "Line Busy"; +"Call.StatusDeclined" = "Call Declined"; +"Call.StatusFailed" = "Call Failed"; +"Call.StatusEnded" = "Call Ended"; +"Call.StatusMissed" = "Call Missed"; + +"Call.WaitingStatusRequesting" = "Requesting"; +"Call.WaitingStatusRinging" = "Ringing"; +"Call.WaitingStatusConnecting" = "Connecting"; +"Call.WaitingStatusReconnecting" = "Reconnecting"; diff --git a/Telegram/Telegram-iOS/en.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/en.lproj/NiceLocalizable.strings index 31a858c8037..4f25fb6335c 100644 --- a/Telegram/Telegram-iOS/en.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/NiceLocalizable.strings @@ -112,7 +112,6 @@ "Messages.TranslateError" = "Sorry, translation unavailable."; "Messages.SpeechToText" = "Speech2Text"; "Messages.UndoSpeechToText" = "Undo Speech2Text"; -"Messages.SpeechToText.LowAccuracyError" = "Speech recognition failed.\n\nThis could happen due to the poor quality of the audio recording or differences between the language of the application and of the audio recording."; "Messages.SelectAllFromUser" = "Select All From This User"; "Messages.ToLanguage" = "To Language"; "Messages.ToLanguage.WithCode" = "To Language: %@"; @@ -211,5 +210,12 @@ "RoundedVideos.MoreButtonTooltip" = "Convert square videos to send as circles with one tap."; "RoundedVideos.SendButtonTooltip" = "Long-press to send your video as a stylish circle."; +/*Speech to text*/ +"SpeechToText.UseOpenAi" = "OpenAI Speech2Text"; +"SpeechToText.Toast" = "[Subscribe to Nicegram Premium]() for faster and better OpenAI Speech2Text technologies!"; + /*Stealth Mode*/ "StealthMode.Toggle" = "Stealth Mode"; + +/*Confirm Call*/ +"ConfirmCall.Desc" = "Are you sure you want to make a call?"; diff --git a/Tests/CallUITest/BUILD b/Tests/CallUITest/BUILD index a809c0ad086..7cb591d9173 100644 --- a/Tests/CallUITest/BUILD +++ b/Tests/CallUITest/BUILD @@ -43,6 +43,7 @@ swift_library( deps = [ "//submodules/Display", "//submodules/MetalEngine", + "//submodules/TelegramPresentationData", "//submodules/TelegramUI/Components/Calls/CallScreen", ], ) @@ -188,5 +189,4 @@ xcodeproj( }, }, default_xcode_configuration = "Debug" - ) diff --git a/Tests/CallUITest/Sources/ViewController.swift b/Tests/CallUITest/Sources/ViewController.swift index 7641bd93e7b..3178bdc66b5 100644 --- a/Tests/CallUITest/Sources/ViewController.swift +++ b/Tests/CallUITest/Sources/ViewController.swift @@ -4,6 +4,7 @@ import MetalEngine import Display import CallScreen import ComponentFlow +import TelegramPresentationData private extension UIScreen { private static let cornerRadiusKey: String = { @@ -24,6 +25,7 @@ private extension UIScreen { public final class ViewController: UIViewController { private var callScreenView: PrivateCallScreen? private var callState: PrivateCallScreen.State = PrivateCallScreen.State( + strings: defaultPresentationStrings, lifecycleState: .connecting, name: "Emma Walters", shortName: "Emma", diff --git a/build-system/Make/BuildEnvironment.py b/build-system/Make/BuildEnvironment.py index 6fa5fe4ee99..8d31133fe14 100644 --- a/build-system/Make/BuildEnvironment.py +++ b/build-system/Make/BuildEnvironment.py @@ -140,6 +140,10 @@ def __init__( raise Exception('Missing xcode version in {}'.format(configuration_path)) else: self.xcode_version = configuration_dict['xcode'] + if configuration_dict['macos'] is None: + raise Exception('Missing macos version in {}'.format(configuration_path)) + else: + self.macos_version = configuration_dict['macos'] class BuildEnvironment: def __init__( @@ -179,3 +183,4 @@ def __init__( self.app_version = versions.app_version self.xcode_version = versions.xcode_version self.bazel_version = versions.bazel_version + self.macos_version = versions.macos_version diff --git a/build-system/Make/Make.py b/build-system/Make/Make.py index 69583071ca0..21f87a95022 100644 --- a/build-system/Make/Make.py +++ b/build-system/Make/Make.py @@ -9,7 +9,7 @@ import shutil import glob -from BuildEnvironment import resolve_executable, call_executable, run_executable_with_output, BuildEnvironment +from BuildEnvironment import resolve_executable, call_executable, run_executable_with_output, BuildEnvironmentVersions, BuildEnvironment from ProjectGeneration import generate from BazelLocation import locate_bazel from BuildConfiguration import CodesigningSource, GitCodesigningSource, DirectoryCodesigningSource, XcodeManagedCodesigningSource, BuildConfiguration, build_configuration_from_json @@ -1060,6 +1060,8 @@ def add_project_and_build_common_arguments(current_parser: argparse.ArgumentPars os.makedirs(remote_input_path + '/certs') os.makedirs(remote_input_path + '/profiles') + versions = BuildEnvironmentVersions(base_path=os.getcwd()) + resolve_configuration( base_path=os.getcwd(), bazel_command_line=None, @@ -1072,6 +1074,7 @@ def add_project_and_build_common_arguments(current_parser: argparse.ArgumentPars RemoteBuild.remote_build( darwin_containers_path=args.darwinContainers, darwin_containers_host=args.darwinContainersHost, + macos_version=versions.macos_version, bazel_cache_host=args.cacheHost, configuration=args.configuration, build_input_data_path=remote_input_path @@ -1106,18 +1109,24 @@ def add_project_and_build_common_arguments(current_parser: argparse.ArgumentPars print('APPSTORE_CONNECT_PASSWORD environment variable is not set') sys.exit(1) + versions = BuildEnvironmentVersions(base_path=os.getcwd()) + RemoteBuild.remote_deploy_testflight( darwin_containers_path=args.darwinContainers, darwin_containers_host=args.darwinContainersHost, + macos_version=versions.macos_version, ipa_path=args.ipa, dsyms_path=args.dsyms, username=env['APPSTORE_CONNECT_USERNAME'], password=env['APPSTORE_CONNECT_PASSWORD'] ) elif args.commandName == 'remote-ipa-diff': + versions = BuildEnvironmentVersions(base_path=os.getcwd()) + RemoteBuild.remote_ipa_diff( darwin_containers_path=args.darwinContainers, darwin_containers_host=args.darwinContainersHost, + macos_version=versions.macos_version, ipa1_path=args.ipa1, ipa2_path=args.ipa2 ) diff --git a/build-system/Make/RemoteBuild.py b/build-system/Make/RemoteBuild.py index e0a4810bf56..89ed9b0d13c 100644 --- a/build-system/Make/RemoteBuild.py +++ b/build-system/Make/RemoteBuild.py @@ -55,9 +55,7 @@ def session_ssh(session, command): return os.system(ssh_command) -def remote_build(darwin_containers_path, darwin_containers_host, bazel_cache_host, configuration, build_input_data_path): - macos_version = '13.0' - +def remote_build(darwin_containers_path, darwin_containers_host, macos_version, bazel_cache_host, configuration, build_input_data_path): DarwinContainers = import_module_from_file('darwin-containers', darwin_containers_path) base_dir = os.getcwd() @@ -181,9 +179,7 @@ def handle_stopped(): ) -def remote_deploy_testflight(darwin_containers_path, darwin_containers_host, ipa_path, dsyms_path, username, password): - macos_version = '13.0' - +def remote_deploy_testflight(darwin_containers_path, darwin_containers_host, macos_version, ipa_path, dsyms_path, username, password): DarwinContainers = import_module_from_file('darwin-containers', darwin_containers_path) configuration_path = 'versions.json' @@ -240,9 +236,7 @@ def handle_stopped(): ) -def remote_ipa_diff(darwin_containers_path, darwin_containers_host, ipa1_path, ipa2_path): - macos_version = '13.0' - +def remote_ipa_diff(darwin_containers_path, darwin_containers_host, macos_version, ipa1_path, ipa2_path): DarwinContainers = import_module_from_file('darwin-containers', darwin_containers_path) configuration_path = 'versions.json' diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index c75999e082d..602a3fc91bb 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -302,6 +302,7 @@ public enum ResolvedUrl { case story(peerId: PeerId, id: Int32) case boost(peerId: PeerId, status: ChannelBoostStatus?, myBoostStatus: MyBoostStatus?) case premiumGiftCode(slug: String) + case premiumMultiGift(reference: String?) } public enum ResolveUrlResult { @@ -422,7 +423,7 @@ public final class NavigateToChatControllerParams { case let .peer(peer): return peer.id case let .replyThread(message): - return message.messageId.peerId + return message.peerId } } @@ -431,7 +432,7 @@ public final class NavigateToChatControllerParams { case .peer: return nil case let .replyThread(message): - return Int64(message.messageId.id) + return message.threadId } } @@ -473,9 +474,10 @@ public final class NavigateToChatControllerParams { public let changeColors: Bool public let setupController: (ChatController) -> Void public let completion: (ChatController) -> Void + public let chatListCompletion: ((ChatListController) -> Void)? public let pushController: ((ChatController, Bool, @escaping () -> Void) -> Void)? - public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: Location, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, botAppStart: ChatControllerInitialBotAppStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: ChatControllerActivateInput? = nil, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, useBackAnimation: Bool = false, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, reportReason: ReportReason? = nil, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [ChatNavigationStackItem] = [], changeColors: Bool = false, setupController: @escaping (ChatController) -> Void = { _ in }, pushController: ((ChatController, Bool, @escaping () -> Void) -> Void)? = nil, completion: @escaping (ChatController) -> Void = { _ in }) { + public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: Location, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, botAppStart: ChatControllerInitialBotAppStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: ChatControllerActivateInput? = nil, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, useBackAnimation: Bool = false, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, reportReason: ReportReason? = nil, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [ChatNavigationStackItem] = [], changeColors: Bool = false, setupController: @escaping (ChatController) -> Void = { _ in }, pushController: ((ChatController, Bool, @escaping () -> Void) -> Void)? = nil, completion: @escaping (ChatController) -> Void = { _ in }, chatListCompletion: @escaping (ChatListController) -> Void = { _ in }) { self.navigationController = navigationController self.chatController = chatController self.chatLocationContextHolder = chatLocationContextHolder @@ -505,6 +507,7 @@ public final class NavigateToChatControllerParams { self.setupController = setupController self.pushController = pushController self.completion = completion + self.chatListCompletion = chatListCompletion } } @@ -901,7 +904,7 @@ public protocol SharedAccountContext: AnyObject { selectedMessages: Signal?, NoError>, mode: ChatHistoryListMode ) -> ChatHistoryListNode - func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: (() -> Void)?, backgroundNode: ASDisplayNode?, availableReactions: AvailableReactions?, accountPeer: Peer?, isCentered: Bool) -> ListViewItem + func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: (() -> Void)?, backgroundNode: ASDisplayNode?, availableReactions: AvailableReactions?, accountPeer: Peer?, isCentered: Bool, isPreview: Bool) -> ListViewItem func makeChatMessageDateHeaderItem(context: AccountContext, timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader func makeChatMessageAvatarHeaderItem(context: AccountContext, timestamp: Int32, peer: Peer, message: Message, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader func makePeerSharedMediaController(context: AccountContext, peerId: PeerId) -> ViewController? @@ -947,7 +950,7 @@ public protocol SharedAccountContext: AnyObject { func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController func makePremiumDemoController(context: AccountContext, subject: PremiumDemoSubject, action: @escaping () -> Void) -> ViewController func makePremiumLimitController(context: AccountContext, subject: PremiumLimitSubject, count: Int32, forceDark: Bool, cancel: @escaping () -> Void, action: @escaping () -> Bool) -> ViewController - func makePremiumGiftController(context: AccountContext) -> ViewController + func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource) -> ViewController func makeStickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], loadedStickerPacks: [LoadedStickerPack], parentNavigationController: NavigationController?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?) -> ViewController @@ -1014,6 +1017,15 @@ public enum PremiumIntroSource { case wallpapers } +public enum PremiumGiftSource: Equatable { + case profile + case attachMenu + case settings + case chatList + case channelBoost + case deeplink(String?) +} + public enum PremiumDemoSubject { case doubleLimits case moreUpload diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index d434b333eac..526954ed9d8 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -758,7 +758,13 @@ public enum ChatControllerSubject: Equatable { } public enum ChatControllerPresentationMode: Equatable { - case standard(previewing: Bool) + public enum StandardPresentation: Equatable { + case `default` + case previewing + case embedded(invertDirection: Bool) + } + + case standard(StandardPresentation) case overlay(NavigationController?) case inline(NavigationController?) } @@ -917,6 +923,10 @@ public protocol ChatController: ViewController { func cancelSelectingMessages() func activateSearch(domain: ChatSearchDomain, query: String) func beginClearHistory(type: InteractiveHistoryClearingType) + + func performScrollToTop() -> Bool + func transferScrollingVelocity(_ velocity: CGFloat) + func updateIsScrollingLockedAtTop(isScrollingLockedAtTop: Bool) } public protocol ChatMessagePreviewItemNode: AnyObject { diff --git a/submodules/AccountContext/Sources/MediaManager.swift b/submodules/AccountContext/Sources/MediaManager.swift index 3281f18dee7..6ca9d90f500 100644 --- a/submodules/AccountContext/Sources/MediaManager.swift +++ b/submodules/AccountContext/Sources/MediaManager.swift @@ -35,7 +35,7 @@ public enum PeerMessagesPlaylistLocation: Equatable, SharedMediaPlaylistLocation case let .peer(peerId): return .peer(peerId) case let .replyThread(replyThreaMessage): - return .peer(replyThreaMessage.messageId.peerId) + return .peer(replyThreaMessage.peerId) case let .feed(id): return .feed(id) } diff --git a/submodules/AccountContext/Sources/SharedMediaPlayer.swift b/submodules/AccountContext/Sources/SharedMediaPlayer.swift index 0beadc3572b..71d26fc45e9 100644 --- a/submodules/AccountContext/Sources/SharedMediaPlayer.swift +++ b/submodules/AccountContext/Sources/SharedMediaPlayer.swift @@ -12,18 +12,21 @@ public enum SharedMediaPlaybackDataType { } public enum SharedMediaPlaybackDataSource: Equatable { - case telegramFile(reference: FileMediaReference, isCopyProtected: Bool) + case telegramFile(reference: FileMediaReference, isCopyProtected: Bool, isViewOnce: Bool) public static func ==(lhs: SharedMediaPlaybackDataSource, rhs: SharedMediaPlaybackDataSource) -> Bool { switch lhs { - case let .telegramFile(lhsFileReference, lhsIsCopyProtected): - if case let .telegramFile(rhsFileReference, rhsIsCopyProtected) = rhs { + case let .telegramFile(lhsFileReference, lhsIsCopyProtected, lhsIsViewOnce): + if case let .telegramFile(rhsFileReference, rhsIsCopyProtected, rhsIsViewOnce) = rhs { if !lhsFileReference.media.isEqual(to: rhsFileReference.media) { return false } if lhsIsCopyProtected != rhsIsCopyProtected { return false } + if lhsIsViewOnce != rhsIsViewOnce { + return false + } return true } else { return false diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 60064a039dd..508fbedce88 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -737,7 +737,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { self.makeEntityInputView = makeEntityInputView - self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(previewing: false), chatLocation: chatLocation ?? .peer(id: context.account.peerId), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil) + self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(.default), chatLocation: chatLocation ?? .peer(id: context.account.peerId), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil) self.containerNode = ASDisplayNode() self.containerNode.clipsToBounds = true @@ -830,7 +830,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { }, stopMediaRecording: { }, lockMediaRecording: { }, deleteRecordedMedia: { - }, sendRecordedMedia: { _ in + }, sendRecordedMedia: { _, _ in }, displayRestrictedInfo: { _, _ in }, displayVideoUnmuteTip: { _ in }, switchMediaRecordingMode: { diff --git a/submodules/AvatarNode/Sources/AvatarNode.swift b/submodules/AvatarNode/Sources/AvatarNode.swift index 6a38691286f..0cf3ff2cd2a 100644 --- a/submodules/AvatarNode/Sources/AvatarNode.swift +++ b/submodules/AvatarNode/Sources/AvatarNode.swift @@ -24,6 +24,7 @@ public let savedMessagesIcon = generateTintedImage(image: UIImage(bundleImageNam public let repostStoryIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/RepostStoryIcon"), color: .white) private let archivedChatsIcon = UIImage(bundleImageName: "Avatar/ArchiveAvatarIcon")?.precomposed() private let repliesIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/RepliesMessagesIcon"), color: .white) +private let anonymousSavedMessagesIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/AnonymousSenderIcon"), color: .white) public func avatarPlaceholderFont(size: CGFloat) -> UIFont { return Font.with(size: size, design: .round, weight: .bold) @@ -95,6 +96,8 @@ private func calculateColors(context: AccountContext?, explicitColorIndex: Int?, colors = AvatarNode.repostColors } else if case .repliesIcon = icon { colors = AvatarNode.savedMessagesColors + } else if case .anonymousSavedMessagesIcon = icon { + colors = AvatarNode.savedMessagesColors } else if case .editAvatarIcon = icon, let theme { colors = [theme.list.itemAccentColor.withAlphaComponent(0.1), theme.list.itemAccentColor.withAlphaComponent(0.1)] } else if case let .archivedChatsIcon(hiddenByDefault) = icon, let theme = theme { @@ -175,6 +178,7 @@ private enum AvatarNodeIcon: Equatable { case none case savedMessagesIcon case repliesIcon + case anonymousSavedMessagesIcon case archivedChatsIcon(hiddenByDefault: Bool) case editAvatarIcon case deletedIcon @@ -187,6 +191,7 @@ public enum AvatarNodeImageOverride: Equatable { case image(TelegramMediaImageRepresentation) case savedMessagesIcon case repliesIcon + case anonymousSavedMessagesIcon case archivedChatsIcon(hiddenByDefault: Bool) case editAvatarIcon(forceNone: Bool) case deletedIcon @@ -473,32 +478,35 @@ public final class AvatarNode: ASDisplayNode { var icon = AvatarNodeIcon.none if let overrideImage = overrideImage { switch overrideImage { - case .none: - representation = nil - case let .image(image): - representation = image - synchronousLoad = false - case .savedMessagesIcon: - representation = nil - icon = .savedMessagesIcon - case .repostIcon: - representation = nil - icon = .repostIcon - case .repliesIcon: - representation = nil - icon = .repliesIcon - case let .archivedChatsIcon(hiddenByDefault): - representation = nil - icon = .archivedChatsIcon(hiddenByDefault: hiddenByDefault) - case let .editAvatarIcon(forceNone): - representation = forceNone ? nil : peer?.smallProfileImage - icon = .editAvatarIcon - case .deletedIcon: - representation = nil - icon = .deletedIcon - case .phoneIcon: - representation = nil - icon = .phoneIcon + case .none: + representation = nil + case let .image(image): + representation = image + synchronousLoad = false + case .savedMessagesIcon: + representation = nil + icon = .savedMessagesIcon + case .repostIcon: + representation = nil + icon = .repostIcon + case .repliesIcon: + representation = nil + icon = .repliesIcon + case .anonymousSavedMessagesIcon: + representation = nil + icon = .anonymousSavedMessagesIcon + case let .archivedChatsIcon(hiddenByDefault): + representation = nil + icon = .archivedChatsIcon(hiddenByDefault: hiddenByDefault) + case let .editAvatarIcon(forceNone): + representation = forceNone ? nil : peer?.smallProfileImage + icon = .editAvatarIcon + case .deletedIcon: + representation = nil + icon = .deletedIcon + case .phoneIcon: + representation = nil + icon = .phoneIcon } } else if peer?.restrictionText(platform: "ios", contentSettings: contentSettings) == nil { representation = peer?.smallProfileImage @@ -641,32 +649,35 @@ public final class AvatarNode: ASDisplayNode { var icon = AvatarNodeIcon.none if let overrideImage = overrideImage { switch overrideImage { - case .none: - representation = nil - case let .image(image): - representation = image - synchronousLoad = false - case .savedMessagesIcon: - representation = nil - icon = .savedMessagesIcon - case .repostIcon: - representation = nil - icon = .repostIcon - case .repliesIcon: - representation = nil - icon = .repliesIcon - case let .archivedChatsIcon(hiddenByDefault): - representation = nil - icon = .archivedChatsIcon(hiddenByDefault: hiddenByDefault) - case let .editAvatarIcon(forceNone): - representation = forceNone ? nil : peer?.smallProfileImage - icon = .editAvatarIcon - case .deletedIcon: - representation = nil - icon = .deletedIcon - case .phoneIcon: - representation = nil - icon = .phoneIcon + case .none: + representation = nil + case let .image(image): + representation = image + synchronousLoad = false + case .savedMessagesIcon: + representation = nil + icon = .savedMessagesIcon + case .repostIcon: + representation = nil + icon = .repostIcon + case .repliesIcon: + representation = nil + icon = .repliesIcon + case .anonymousSavedMessagesIcon: + representation = nil + icon = .anonymousSavedMessagesIcon + case let .archivedChatsIcon(hiddenByDefault): + representation = nil + icon = .archivedChatsIcon(hiddenByDefault: hiddenByDefault) + case let .editAvatarIcon(forceNone): + representation = forceNone ? nil : peer?.smallProfileImage + icon = .editAvatarIcon + case .deletedIcon: + representation = nil + icon = .deletedIcon + case .phoneIcon: + representation = nil + icon = .phoneIcon } } // MARK: Nicegram (isAllowedChat) @@ -903,6 +914,15 @@ public final class AvatarNode: ASDisplayNode { if let repliesIcon = repliesIcon { context.draw(repliesIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - repliesIcon.size.width) / 2.0), y: floor((bounds.size.height - repliesIcon.size.height) / 2.0)), size: repliesIcon.size)) } + } else if case .anonymousSavedMessagesIcon = parameters.icon { + let factor = bounds.size.width / 60.0 + context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0) + context.scaleBy(x: factor, y: -factor) + context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0) + + if let anonymousSavedMessagesIcon = anonymousSavedMessagesIcon { + context.draw(anonymousSavedMessagesIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - anonymousSavedMessagesIcon.size.width) / 2.0), y: floor((bounds.size.height - anonymousSavedMessagesIcon.size.height) / 2.0)), size: anonymousSavedMessagesIcon.size)) + } } else if case .editAvatarIcon = parameters.icon, let theme = parameters.theme, !parameters.hasImage { context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0) context.scaleBy(x: 1.0, y: -1.0) diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index 233e5db0fc1..890c78ec2cd 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -854,6 +854,44 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId: } } +public func savedMessagesPeerMenuItems(context: AccountContext, threadId: Int64, parentController: ViewController) -> Signal<[ContextMenuItem], NoError> { + let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) + let strings = presentationData.strings + + return combineLatest( + context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: PeerId(threadId)) + ), + context.account.postbox.transaction { transaction -> [Int64] in + return transaction.getPeerPinnedThreads(peerId: context.account.peerId) + } + ) + |> mapToSignal { [weak parentController] peer, pinnedThreadIds -> Signal<[ContextMenuItem], NoError> in + var items: [ContextMenuItem] = [] + + let isPinned = pinnedThreadIds.contains(threadId) + + items.append(.action(ContextMenuActionItem(text: isPinned ? strings.ChatList_Context_Unpin : strings.ChatList_Context_Pin, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isPinned ? "Chat/Context Menu/Unpin": "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { _, f in + f(.default) + + let _ = (context.engine.peers.toggleForumChannelTopicPinned(id: context.account.peerId, threadId: threadId) + |> deliverOnMainQueue).startStandalone(error: { error in + switch error { + case let .limitReached(count): + let controller = PremiumLimitScreen(context: context, subject: .pinnedSavedPeers, count: Int32(count), action: { + return true + }) + parentController?.push(controller) + default: + break + } + }) + }))) + + return .single(items) + } +} + private func openCustomMute(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, baseController: ViewController) { let controller = ChatTimerScreen(context: context, updatedPresentationData: nil, style: .default, mode: .mute, currentTime: nil, dismissByTapOutside: true, completion: { [weak baseController] value in let presentationData = context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 021189f1e65..67799fec4b1 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1404,8 +1404,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if let threadId = threadId { let source: ContextContentSource let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .replyThread(message: ChatReplyThreadMessage( - messageId: MessageId(peerId: peer.peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false - )), subject: nil, botStart: nil, mode: .standard(previewing: true)) + peerId: peer.peerId, threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false + )), subject: nil, botStart: nil, mode: .standard(.previewing)) chatController.canReadHistory.set(false) source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) @@ -1422,7 +1422,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if let location = location { source = .location(ChatListContextLocationContentSource(controller: strongSelf, location: location)) } else { - let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peer.peerId), subject: nil, botStart: nil, mode: .standard(previewing: true)) + let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peer.peerId), subject: nil, botStart: nil, mode: .standard(.previewing)) chatController.canReadHistory.set(false) source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) } @@ -1440,8 +1440,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } let source: ContextContentSource let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .replyThread(message: ChatReplyThreadMessage( - messageId: MessageId(peerId: peer.peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false - )), subject: nil, botStart: nil, mode: .standard(previewing: true)) + peerId: peer.peerId, threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false + )), subject: nil, botStart: nil, mode: .standard(.previewing)) chatController.canReadHistory.set(false) source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) @@ -1491,7 +1491,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if case let .search(messageId) = source, let id = messageId { subject = .message(id: .id(id), highlight: nil, timecode: nil) } - let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peer.id), subject: subject, botStart: nil, mode: .standard(previewing: true)) + let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peer.id), subject: subject, botStart: nil, mode: .standard(.previewing)) chatController.canReadHistory.set(false) contextContentSource = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) } @@ -3495,7 +3495,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } - let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(previewing: false)) + let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(.default)) if let sourceController = sourceController as? ChatListControllerImpl, case .forum(peerId) = sourceController.location { navigationController.replaceController(sourceController, with: chatController, animated: false) @@ -6686,7 +6686,7 @@ private final class ChatListLocationContext { if let channel = peerView.peers[peerView.peerId] as? TelegramChannel, !channel.flags.contains(.isForum) { if let parentController = self.parentController, let navigationController = parentController.navigationController as? NavigationController { - let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(previewing: false)) + let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(.default)) navigationController.replaceController(parentController, with: chatController, animated: true) } } else { diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 8f6e39e2039..28d5cb8daba 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -1152,7 +1152,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { return } - let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(previewing: false)) + let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(.default)) (controller.navigationController as? NavigationController)?.replaceController(controller, with: chatController, animated: false) } diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index fcf487666fa..86d48faeb5c 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -1559,7 +1559,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi }) }, peerContextAction: { peer, node, gesture, location in - let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(previewing: true)) + let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.previewing)) chatController.canReadHistory.set(false) let presentationData = context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 7e9ce3c6e93..0f3bc92b482 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -1450,7 +1450,21 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } } - (strongSelf.navigationController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + (strongSelf.navigationController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in + if savedMessages, let self, action == .info { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.navigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) + }) + } + return false + }), in: .current) } peerSelectionController.peerSelected = { [weak self, weak peerSelectionController] peer, threadId in let peerId = peer.id @@ -1461,7 +1475,22 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - (strongSelf.navigationController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: true, text: messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .window(.root)) + (strongSelf.navigationController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: true, text: messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many), elevatedLayout: false, animateInAsReplacement: true, action: { action in + if let self, action == .info { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.navigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) + }) + } + + return false + }), in: .window(.root)) let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: messageIds.map { id -> EnqueueMessage in return .forward(source: id, threadId: threadId, grouping: .auto, attributes: [], correlationId: nil) @@ -1536,7 +1565,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo proceed(chatController) }) } else { - proceed(strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(previewing: false))) + proceed(strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(.default))) } strongSelf.updateState { state in diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index ebcb16827f6..16c58d9e393 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -1228,6 +1228,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { enableRecentlySearched = true } } + if case .savedMessagesChats = location { + enableRecentlySearched = false + } if enableRecentlySearched { fixedRecentlySearchedPeers = context.engine.peers.recentlySearchedPeers() @@ -1406,7 +1409,25 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let accountPeer = context.account.postbox.loadedPeerWithId(context.account.peerId) |> take(1) let foundLocalPeers: Signal<(peers: [EngineRenderedPeer], unread: [EnginePeer.Id: (Int32, Bool)], recentlySearchedPeerIds: Set), NoError> - if let query = query, (key == .chats || key == .topics) { + + if case .savedMessagesChats = location { + if let query { + foundLocalPeers = context.engine.messages.searchLocalSavedMessagesPeers(query: query.lowercased(), indexNameMapping: [ + context.account.peerId: [ + PeerIndexNameRepresentation.title(title: "saved messages", addressNames: []), + PeerIndexNameRepresentation.title(title: presentationData.strings.DialogList_SavedMessages.lowercased(), addressNames: []) + ], + PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(2666000)): [ + PeerIndexNameRepresentation.title(title: presentationData.strings.ChatList_AuthorHidden.lowercased(), addressNames: []) + ] + ]) + |> map { peers -> (peers: [EngineRenderedPeer], unread: [EnginePeer.Id: (Int32, Bool)], recentlySearchedPeerIds: Set) in + return (peers.map(EngineRenderedPeer.init(peer:)), [:], Set()) + } + } else { + foundLocalPeers = .single(([], [:], Set())) + } + } else if let query = query, (key == .chats || key == .topics) { let fixedOrRemovedRecentlySearchedPeers = context.engine.peers.recentlySearchedPeers() |> map { peers -> [RecentlySearchedPeer] in let allIds = peers.map(\.peer.peerId) @@ -1524,7 +1545,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let foundRemotePeers: Signal<([FoundPeer], [FoundPeer], Bool), NoError> let currentRemotePeersValue: ([FoundPeer], [FoundPeer]) = currentRemotePeers.with { $0 } ?? ([], []) - if let query = query, case .chats = key { + if case .savedMessagesChats = location { + foundRemotePeers = .single(([], [], false)) + } else if let query = query, case .chats = key { foundRemotePeers = ( .single((currentRemotePeersValue.0, currentRemotePeersValue.1, true)) |> then( @@ -1539,9 +1562,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let searchLocations: [SearchMessagesLocation] if let options = options { if case let .forum(peerId) = location { - searchLocations = [.peer(peerId: peerId, fromId: nil, tags: tagMask, topMsgId: nil, minDate: options.date?.0, maxDate: options.date?.1), .general(tags: tagMask, minDate: options.date?.0, maxDate: options.date?.1)] + searchLocations = [.peer(peerId: peerId, fromId: nil, tags: tagMask, threadId: nil, minDate: options.date?.0, maxDate: options.date?.1), .general(tags: tagMask, minDate: options.date?.0, maxDate: options.date?.1)] } else if let (peerId, _, _) = options.peer { - searchLocations = [.peer(peerId: peerId, fromId: nil, tags: tagMask, topMsgId: nil, minDate: options.date?.0, maxDate: options.date?.1)] + searchLocations = [.peer(peerId: peerId, fromId: nil, tags: tagMask, threadId: nil, minDate: options.date?.0, maxDate: options.date?.1)] } else { if case let .chatList(groupId) = location, case .archive = groupId { searchLocations = [.group(groupId: groupId._asGroup(), tags: tagMask, minDate: options.date?.0, maxDate: options.date?.1)] @@ -1551,7 +1574,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } } else { if case let .forum(peerId) = location { - searchLocations = [.peer(peerId: peerId, fromId: nil, tags: tagMask, topMsgId: nil, minDate: nil, maxDate: nil), .general(tags: tagMask, minDate: nil, maxDate: nil)] + searchLocations = [.peer(peerId: peerId, fromId: nil, tags: tagMask, threadId: nil, minDate: nil, maxDate: nil), .general(tags: tagMask, minDate: nil, maxDate: nil)] } else if case let .chatList(groupId) = location, case .archive = groupId { searchLocations = [.group(groupId: groupId._asGroup(), tags: tagMask, minDate: nil, maxDate: nil)] } else { @@ -1579,7 +1602,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } let foundRemoteMessages: Signal<([FoundRemoteMessages], Bool), NoError> - if peersFilter.contains(.doNotSearchMessages) { + if case .savedMessagesChats = location { + foundRemoteMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) + } else if peersFilter.contains(.doNotSearchMessages) { foundRemoteMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) } else { if !finalQuery.isEmpty { @@ -1676,18 +1701,23 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { ) } - let resolvedMessage = .single(nil) - |> then(context.sharedContext.resolveUrl(context: context, peerId: nil, url: finalQuery, skipUrlAuth: true) - |> mapToSignal { resolvedUrl -> Signal in - if case let .channelMessage(_, messageId, _) = resolvedUrl { - return context.engine.messages.downloadMessage(messageId: messageId) - |> map { message -> EngineMessage? in - return message.flatMap(EngineMessage.init) + let resolvedMessage: Signal + if case .savedMessagesChats = location { + resolvedMessage = .single(nil) + } else { + resolvedMessage = .single(nil) + |> then(context.sharedContext.resolveUrl(context: context, peerId: nil, url: finalQuery, skipUrlAuth: true) + |> mapToSignal { resolvedUrl -> Signal in + if case let .channelMessage(_, messageId, _) = resolvedUrl { + return context.engine.messages.downloadMessage(messageId: messageId) + |> map { message -> EngineMessage? in + return message.flatMap(EngineMessage.init) + } + } else { + return .single(nil) } - } else { - return .single(nil) - } - }) + }) + } let foundThreads: Signal<[EngineChatList.Item], NoError> if case let .forum(peerId) = location, (key == .topics || key == .chats) { @@ -2174,7 +2204,12 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }, peerSelected: { [weak self] peer, chatPeer, threadId, _ in interaction.dismissInput() interaction.openPeer(peer, chatPeer, threadId, false) - let _ = context.engine.peers.addRecentlySearchedPeer(peerId: peer.id).startStandalone() + switch location { + case .chatList, .forum: + let _ = context.engine.peers.addRecentlySearchedPeer(peerId: peer.id).startStandalone() + case .savedMessagesChats: + break + } self?.listNode.clearHighlightAnimated(true) }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in @@ -2619,6 +2654,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { if peersFilter.contains(.excludeRecent) { recentItems = .single([]) } + if case .savedMessagesChats = location { + recentItems = .single([]) + } if case .chats = key, !peersFilter.contains(.excludeRecent) { self.updatedRecentPeersDisposable.set(context.engine.peers.managedUpdatedRecentPeers().startStrict()) @@ -2636,7 +2674,12 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let transition = chatListSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries, context: context, presentationData: presentationData, filter: peersFilter, peerSelected: { peer, threadId in interaction.openPeer(peer, nil, threadId, true) if threadId == nil { - let _ = context.engine.peers.addRecentlySearchedPeer(peerId: peer.id).startStandalone() + switch location { + case .chatList, .forum: + let _ = context.engine.peers.addRecentlySearchedPeer(peerId: peer.id).startStandalone() + case .savedMessagesChats: + break + } } self?.recentListNode.clearHighlightAnimated(true) }, disabledPeerSelected: { peer, threadId in diff --git a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift index 44c30394ce8..f3747fb993d 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift @@ -379,9 +379,10 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerD self.currentParams = (size, sideInset, bottomInset, visibleHeight, presentationData, availablePanes) - if case .forum = self.location { + switch self.location { + case .forum, .savedMessagesChats: self.backgroundColor = .clear - } else { + default: self.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor } let paneFrame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)) diff --git a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift index d07e4174d98..5bcc75170c0 100644 --- a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift @@ -124,13 +124,13 @@ final class ShimmerEffectNode: ASDisplayNode { } } -final class ChatListShimmerNode: ASDisplayNode { +public final class ChatListShimmerNode: ASDisplayNode { private let backgroundColorNode: ASDisplayNode private let effectNode: ShimmerEffectNode private let maskNode: ASImageNode private var currentParams: (size: CGSize, presentationData: PresentationData)? - override init() { + override public init() { self.backgroundColorNode = ASDisplayNode() self.effectNode = ShimmerEffectNode() self.maskNode = ASImageNode() @@ -144,7 +144,7 @@ final class ChatListShimmerNode: ASDisplayNode { self.addSubnode(self.maskNode) } - func update(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, size: CGSize, isInlineMode: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { + public func update(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, size: CGSize, isInlineMode: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { if self.currentParams?.size != size || self.currentParams?.presentationData !== presentationData { self.currentParams = (size, presentationData) diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 71343fc6359..c65735742c4 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -402,7 +402,13 @@ public struct ChatListItemFilterData: Equatable { private func revealOptions(strings: PresentationStrings, theme: PresentationTheme, isPinned: Bool, isMuted: Bool?, location: ChatListControllerLocation, peerId: EnginePeer.Id, accountPeerId: EnginePeer.Id, canDelete: Bool, isEditing: Bool, filterData: ChatListItemFilterData?) -> [ItemListRevealOption] { var options: [ItemListRevealOption] = [] if !isEditing { - if case .chatList(.archive) = location { + if case .savedMessagesChats = location { + if isPinned { + options.append(ItemListRevealOption(key: RevealOptionKey.unpin.rawValue, title: strings.DialogList_Unpin, icon: unpinIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, textColor: theme.list.itemDisclosureActions.constructive.foregroundColor)) + } else { + options.append(ItemListRevealOption(key: RevealOptionKey.pin.rawValue, title: strings.DialogList_Pin, icon: pinIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, textColor: theme.list.itemDisclosureActions.constructive.foregroundColor)) + } + } else if case .chatList(.archive) = location { if isPinned { options.append(ItemListRevealOption(key: RevealOptionKey.unpin.rawValue, title: strings.DialogList_Unpin, icon: unpinIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, textColor: theme.list.itemDisclosureActions.constructive.foregroundColor)) } else { @@ -421,28 +427,31 @@ private func revealOptions(strings: PresentationStrings, theme: PresentationThem if canDelete { options.append(ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: strings.Common_Delete, icon: deleteIcon, color: theme.list.itemDisclosureActions.destructive.fillColor, textColor: theme.list.itemDisclosureActions.destructive.foregroundColor)) } - if !isEditing { - var canArchive = false - var canUnarchive = false - if let filterData = filterData { - if filterData.excludesArchived { - canArchive = true - } - } else { - if case let .chatList(groupId) = location { - if case .root = groupId { + if case .savedMessagesChats = location { + } else { + if !isEditing { + var canArchive = false + var canUnarchive = false + if let filterData = filterData { + if filterData.excludesArchived { canArchive = true - } else { - canUnarchive = true + } + } else { + if case let .chatList(groupId) = location { + if case .root = groupId { + canArchive = true + } else { + canUnarchive = true + } } } - } - if canArchive { - if canArchivePeer(id: peerId, accountPeerId: accountPeerId) { - options.append(ItemListRevealOption(key: RevealOptionKey.archive.rawValue, title: strings.ChatList_ArchiveAction, icon: archiveIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, textColor: theme.list.itemDisclosureActions.inactive.foregroundColor)) + if canArchive { + if canArchivePeer(id: peerId, accountPeerId: accountPeerId) { + options.append(ItemListRevealOption(key: RevealOptionKey.archive.rawValue, title: strings.ChatList_ArchiveAction, icon: archiveIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, textColor: theme.list.itemDisclosureActions.inactive.foregroundColor)) + } + } else if canUnarchive { + options.append(ItemListRevealOption(key: RevealOptionKey.unarchive.rawValue, title: strings.ChatList_UnarchiveAction, icon: unarchiveIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, textColor: theme.list.itemDisclosureActions.inactive.foregroundColor)) } - } else if canUnarchive { - options.append(ItemListRevealOption(key: RevealOptionKey.unarchive.rawValue, title: strings.ChatList_UnarchiveAction, icon: unarchiveIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, textColor: theme.list.itemDisclosureActions.inactive.foregroundColor)) } } return options @@ -1429,6 +1438,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var overrideImage: AvatarNodeImageOverride? if peer.id.isReplies { overrideImage = .repliesIcon + } else if peer.id.isAnonymousSavedMessages { + overrideImage = .anonymousSavedMessagesIcon } else if peer.id == item.context.account.peerId && !displayAsMessage { overrideImage = .savedMessagesIcon } else if peer.isDeleted { @@ -2356,6 +2367,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { titleAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_SavedMessages, font: titleFont, textColor: theme.titleColor) } else if let id = itemPeer.chatMainPeer?.id, id.isReplies { titleAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_Replies, font: titleFont, textColor: theme.titleColor) + } else if let id = itemPeer.chatMainPeer?.id, id.isAnonymousSavedMessages { + titleAttributedString = NSAttributedString(string: item.presentationData.strings.ChatList_AuthorHidden, font: titleFont, textColor: theme.titleColor) } else if let displayTitle = itemPeer.chatMainPeer?.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) { let textColor: UIColor if case let .chatList(index) = item.index, index.messageIndex.id.peerId.namespace == Namespaces.Peer.SecretChat { diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 0d0549256f1..b85d60792ce 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -31,6 +31,7 @@ import StoryContainerScreen import ChatListHeaderComponent import UndoUI import NewSessionInfoScreen +import PresentationDataUtils public enum ChatListNodeMode { case chatList(appendContacts: Bool) @@ -1453,70 +1454,90 @@ public final class ChatListNode: ListView { } } }, setItemPinned: { [weak self] itemId, _ in - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) - |> deliverOnMainQueue).startStandalone(next: { peer in - guard let strongSelf = self else { - return - } - guard case let .chatList(groupId) = strongSelf.location else { - return - } - - let isPremium = peer?.isPremium ?? false - let location: TogglePeerChatPinnedLocation - if let chatListFilter = chatListFilter { - location = .filter(chatListFilter.id) - } else { - location = .group(groupId._asGroup()) - } - let _ = (context.engine.peers.toggleItemPinned(location: location, itemId: itemId) - |> deliverOnMainQueue).startStandalone(next: { result in - if let strongSelf = self { - switch result { - case .done: + if case .savedMessagesChats = location { + if case let .peer(itemPeerId) = itemId { + let _ = (context.engine.peers.toggleForumChannelTopicPinned(id: context.account.peerId, threadId: itemPeerId.toInt64()) + |> deliverOnMainQueue).start(error: { error in + guard let self else { + return + } + switch error { + case let .limitReached(count): + let controller = PremiumLimitScreen(context: context, subject: .pinnedSavedPeers, count: Int32(count), action: { + return true + }) + self.push?(controller) + default: break - case let .limitExceeded(count, _): - if isPremium { - if case .filter = location { - let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: { - return true - }) - strongSelf.push?(controller) - } else { - let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: { - return true - }) - strongSelf.push?(controller) - } - } else { - if case .filter = location { - var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: { - let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats) - replaceImpl?(premiumScreen) - return true - }) - strongSelf.push?(controller) - replaceImpl = { [weak controller] c in - controller?.replace(with: c) + } + }) + } + } else { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).startStandalone(next: { peer in + guard let strongSelf = self else { + return + } + guard case let .chatList(groupId) = strongSelf.location else { + return + } + + let isPremium = peer?.isPremium ?? false + let location: TogglePeerChatPinnedLocation + if let chatListFilter = chatListFilter { + location = .filter(chatListFilter.id) + } else { + location = .group(groupId._asGroup()) + } + let _ = (context.engine.peers.toggleItemPinned(location: location, itemId: itemId) + |> deliverOnMainQueue).startStandalone(next: { result in + if let strongSelf = self { + switch result { + case .done: + break + case let .limitExceeded(count, _): + if isPremium { + if case .filter = location { + let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: { + return true + }) + strongSelf.push?(controller) + } else { + let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: { + return true + }) + strongSelf.push?(controller) } } else { - var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: { - let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats) - replaceImpl?(premiumScreen) - return true - }) - strongSelf.push?(controller) - replaceImpl = { [weak controller] c in - controller?.replace(with: c) + if case .filter = location { + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: { + let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats) + replaceImpl?(premiumScreen) + return true + }) + strongSelf.push?(controller) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + } else { + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: { + let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats) + replaceImpl?(premiumScreen) + return true + }) + strongSelf.push?(controller) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } } } } } - } + }) }) - }) + } }, setPeerMuted: { [weak self] peerId, _ in guard let strongSelf = self else { return @@ -1645,7 +1666,7 @@ public final class ChatListNode: ListView { guard let self else { return } - let controller = self.context.sharedContext.makePremiumGiftController(context: self.context) + let controller = self.context.sharedContext.makePremiumGiftController(context: self.context, source: .chatList) self.push?(controller) }, openActiveSessions: { [weak self] in guard let self else { diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift index 18353e76f10..d84bfb41ce6 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift @@ -312,81 +312,49 @@ func chatListViewForLocation(chatListLocation: ChatListControllerLocation, locat return ChatListNodeViewUpdate(list: list, type: type, scrollPosition: nil) } case .savedMessagesChats: - var isFirst = true + let viewKey: PostboxViewKey = .savedMessagesIndex(peerId: account.peerId) - return account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: account.peerId, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 1000, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tagMask: nil, appendMessagesFromTheSameGroup: false, namespaces: .not(Set([Namespaces.Message.ScheduledCloud, Namespaces.Message.ScheduledLocal])), orderStatistics: []) - |> map { view, _, _ -> ChatListNodeViewUpdate in - let isLoading = view.isLoading + var isFirst = true + return account.postbox.combinedView(keys: [viewKey]) + |> map { views -> ChatListNodeViewUpdate in + guard let view = views.views[viewKey] as? MessageHistorySavedMessagesIndexView else { + preconditionFailure() + } var items: [EngineChatList.Item] = [] - - var topMessageByPeerId: [EnginePeer.Id: Message] = [:] - if !isLoading { - for entry in view.entries { - guard let threadId = entry.message.threadId else { - continue - } - let sourcePeerId = PeerId(threadId) - - if let currentTopMessage = topMessageByPeerId[sourcePeerId] { - if currentTopMessage.index < entry.index { - topMessageByPeerId[sourcePeerId] = entry.message - } - } else { - topMessageByPeerId[sourcePeerId] = entry.message - } + for item in view.items { + guard let sourcePeer = item.peer else { + continue } - for (_, message) in topMessageByPeerId.sorted(by: { $0.value.index > $1.value.index }) { - guard let threadId = message.threadId else { - continue - } - let sourceId = PeerId(threadId) - var sourcePeer = message.peers[sourceId] - if sourcePeer == nil, let forwardInfo = message.forwardInfo, let authorSignature = forwardInfo.authorSignature { - sourcePeer = TelegramUser( - id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(1)), - accessHash: nil, - firstName: authorSignature, - lastName: nil, - username: nil, - phone: nil, - photo: [], - botInfo: nil, - restrictionInfo: nil, - flags: [], - emojiStatus: nil, - usernames: [], - storiesHidden: nil, - nameColor: nil, - backgroundEmojiId: nil, - profileColor: nil, - profileBackgroundEmojiId: nil - ) - } - guard let sourcePeer else { - continue - } - let mappedMessageIndex = MessageIndex(id: MessageId(peerId: sourceId, namespace: message.index.id.namespace, id: message.index.id.id), timestamp: message.index.timestamp) - items.append(EngineChatList.Item( - id: .chatList(sourceId), - index: .chatList(ChatListIndex(pinningIndex: nil, messageIndex: mappedMessageIndex)), - messages: [EngineMessage(message)], - readCounters: nil, - isMuted: false, - draft: nil, - threadData: nil, - renderedPeer: EngineRenderedPeer(peer: EnginePeer(sourcePeer)), - presence: nil, - hasUnseenMentions: false, - hasUnseenReactions: false, - forumTopicData: nil, - topForumTopicItems: [], - hasFailed: false, - isContact: false, - autoremoveTimeout: nil, - storyStats: nil - )) + + let sourceId = PeerId(item.id) + + var messages: [EngineMessage] = [] + if let topMessage = item.topMessage { + messages.append(EngineMessage(topMessage)) } + + let mappedMessageIndex = MessageIndex(id: MessageId(peerId: sourceId, namespace: item.index.id.namespace, id: item.index.id.id), timestamp: item.index.timestamp) + + items.append(EngineChatList.Item( + id: .chatList(sourceId), + index: .chatList(ChatListIndex(pinningIndex: item.pinnedIndex.flatMap(UInt16.init), messageIndex: mappedMessageIndex)), + messages: messages, + readCounters: nil, + isMuted: false, + draft: nil, + threadData: nil, + renderedPeer: EngineRenderedPeer(peer: EnginePeer(sourcePeer)), + presence: nil, + hasUnseenMentions: false, + hasUnseenReactions: false, + forumTopicData: nil, + topForumTopicItems: [], + hasFailed: false, + isContact: false, + autoremoveTimeout: nil, + storyStats: nil + )) } let list = EngineChatList( @@ -395,7 +363,7 @@ func chatListViewForLocation(chatListLocation: ChatListControllerLocation, locat additionalItems: [], hasEarlier: false, hasLater: false, - isLoading: isLoading + isLoading: view.isLoading ) let type: ViewUpdateType diff --git a/submodules/ChatPresentationInterfaceState/BUILD b/submodules/ChatPresentationInterfaceState/BUILD index e8c07bfa747..5d3ea70cf2f 100644 --- a/submodules/ChatPresentationInterfaceState/BUILD +++ b/submodules/ChatPresentationInterfaceState/BUILD @@ -20,6 +20,7 @@ swift_library( "//submodules/TelegramUIPreferences:TelegramUIPreferences", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/ChatContextQuery", + "//submodules/TooltipUI", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift index 03b686da640..61d3b9bd075 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift @@ -7,6 +7,7 @@ import TelegramCore import Display import AccountContext import ContextUI +import TooltipUI public enum ChatLoadingMessageSubject { case generic @@ -16,7 +17,7 @@ public enum ChatLoadingMessageSubject { public enum ChatFinishMediaRecordingAction { case dismiss case preview - case send + case send(viewOnce: Bool) } public final class ChatPanelInterfaceInteractionStatuses { @@ -115,7 +116,7 @@ public final class ChatPanelInterfaceInteraction { public let stopMediaRecording: () -> Void public let lockMediaRecording: () -> Void public let deleteRecordedMedia: () -> Void - public let sendRecordedMedia: (Bool) -> Void + public let sendRecordedMedia: (Bool, Bool) -> Void public let displayRestrictedInfo: (ChatPanelRestrictionInfoSubject, ChatPanelRestrictionInfoDisplayType) -> Void public let displayVideoUnmuteTip: (CGPoint?) -> Void public let switchMediaRecordingMode: () -> Void @@ -228,7 +229,7 @@ public final class ChatPanelInterfaceInteraction { stopMediaRecording: @escaping () -> Void, lockMediaRecording: @escaping () -> Void, deleteRecordedMedia: @escaping () -> Void, - sendRecordedMedia: @escaping (Bool) -> Void, + sendRecordedMedia: @escaping (Bool, Bool) -> Void, displayRestrictedInfo: @escaping (ChatPanelRestrictionInfoSubject, ChatPanelRestrictionInfoDisplayType) -> Void, displayVideoUnmuteTip: @escaping (CGPoint?) -> Void, switchMediaRecordingMode: @escaping () -> Void, @@ -456,7 +457,7 @@ public final class ChatPanelInterfaceInteraction { }, stopMediaRecording: { }, lockMediaRecording: { }, deleteRecordedMedia: { - }, sendRecordedMedia: { _ in + }, sendRecordedMedia: { _, _ in }, displayRestrictedInfo: { _, _ in }, displayVideoUnmuteTip: { _ in }, switchMediaRecordingMode: { diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift index a37e42eff81..2809ca5db28 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift @@ -14,7 +14,7 @@ public extension ChatLocation { case let .peer(peerId): return peerId case let .replyThread(replyThreadMessage): - return replyThreadMessage.messageId.peerId + return replyThreadMessage.peerId case .feed: return nil } @@ -25,7 +25,7 @@ public extension ChatLocation { case .peer: return nil case let .replyThread(replyThreadMessage): - return Int64(replyThreadMessage.messageId.id) + return replyThreadMessage.threadId case .feed: return nil } diff --git a/submodules/ContactListUI/Sources/ContactsControllerNode.swift b/submodules/ContactListUI/Sources/ContactsControllerNode.swift index 5be7b54ceca..3a991bbbe0a 100644 --- a/submodules/ContactListUI/Sources/ContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsControllerNode.swift @@ -439,7 +439,7 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { let controller = ContextController(presentationData: self.presentationData, source: .extracted(ContactContextExtractedContentSource(sourceNode: node, shouldBeDismissed: .single(false))), items: items, recognizer: nil, gesture: gesture) contactsController.presentInGlobalOverlay(controller) } else { - let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(previewing: true)) + let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.previewing)) chatController.canReadHistory.set(false) let contextController = ContextController(presentationData: self.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: items, gesture: gesture) contactsController.presentInGlobalOverlay(contextController) diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index 8045723169e..5b834e9c73e 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -783,6 +783,8 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { titleAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_SavedMessages, font: titleBoldFont, textColor: textColor) } else if peer.id.isReplies { titleAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_Replies, font: titleBoldFont, textColor: textColor) + } else if peer.id.isAnonymousSavedMessages { + titleAttributedString = NSAttributedString(string: item.presentationData.strings.ChatList_AuthorHidden, font: titleBoldFont, textColor: textColor) } else if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty { let string = NSMutableAttributedString() switch item.displayOrder { @@ -1030,6 +1032,8 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { overrideImage = .savedMessagesIcon } else if peer.id.isReplies, case .generalSearch = item.peerMode { overrideImage = .repliesIcon + } else if peer.id.isAnonymousSavedMessages, case .generalSearch = item.peerMode { + overrideImage = .anonymousSavedMessagesIcon } else if peer.isDeleted { overrideImage = .deletedIcon } diff --git a/submodules/ContextUI/BUILD b/submodules/ContextUI/BUILD index a9e9d78fa6e..cfd7d60f059 100644 --- a/submodules/ContextUI/BUILD +++ b/submodules/ContextUI/BUILD @@ -29,6 +29,7 @@ swift_library( "//submodules/Components/ComponentDisplayAdapters", "//submodules/TelegramUI/Components/TabSelectorComponent", "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift index 59f89de751b..592efe01aee 100644 --- a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift +++ b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift @@ -199,7 +199,7 @@ private final class InnerActionsContainerNode: ASDisplayNode { if let minimalWidth = minimalWidth, minimalWidth > minActionsWidth { minActionsWidth = minimalWidth } - + switch widthClass { case .compact: minActionsWidth = max(minActionsWidth, floor(constrainedWidth / 3.0)) diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index c5e577943ef..024be3fb418 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -2214,12 +2214,16 @@ public final class ContextController: ViewController, StandalonePresentableContr public let title: String public let source: ContextContentSource public let items: Signal + public let closeActionTitle: String? + public let closeAction: (() -> Void)? - public init(id: AnyHashable, title: String, source: ContextContentSource, items: Signal) { + public init(id: AnyHashable, title: String, source: ContextContentSource, items: Signal, closeActionTitle: String? = nil, closeAction: (() -> Void)? = nil) { self.id = id self.title = title self.source = source self.items = items + self.closeActionTitle = closeActionTitle + self.closeAction = closeAction } } diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index ada6f68ffeb..0b1b90d0474 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -1635,7 +1635,12 @@ final class ContextControllerActionsStackNode: ASDisplayNode { topItemWidth = lastItemLayout.size.width * (1.0 - transitionFraction) + previousItemLayout.size.width * transitionFraction } - let navigationContainerFrame = CGRect(origin: CGPoint(), size: CGSize(width: topItemWidth, height: max(14 * 2.0, topItemApparentHeight))) + let navigationContainerFrame: CGRect + if topItemApparentHeight > 0.0 { + navigationContainerFrame = CGRect(origin: CGPoint(), size: CGSize(width: topItemWidth, height: max(14 * 2.0, topItemApparentHeight))) + } else { + navigationContainerFrame = .zero + } let previousNavigationContainerFrame = self.navigationContainer.frame transition.updateFrame(node: self.navigationContainer, frame: navigationContainerFrame, beginWithCurrentState: true) self.navigationContainer.update(presentationData: presentationData, presentation: presentation, size: navigationContainerFrame.size, transition: transition) diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 4cade32e470..1d4b9e0479c 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -1027,16 +1027,26 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo if let contentNode = itemContentNode { var contentFrame = CGRect(origin: CGPoint(x: contentParentGlobalFrame.minX + contentRect.minX - contentNode.containingItem.contentRect.minX, y: contentRect.minY - contentNode.containingItem.contentRect.minY + contentVerticalOffset + additionalVisibleOffsetY), size: contentNode.containingItem.view.bounds.size) - if case let .extracted(extracted) = self.source, extracted.centerVertically, contentFrame.midX > layout.size.width / 2.0 { - contentFrame.origin.x = layout.size.width - contentFrame.maxX + if case let .extracted(extracted) = self.source { + if extracted.centerVertically { + if combinedActionsFrame.height.isZero { + contentFrame.origin.y = floorToScreenPixels((layout.size.height - contentFrame.height) / 2.0) + } else if contentFrame.midX > layout.size.width / 2.0 { + contentFrame.origin.x = layout.size.width - contentFrame.maxX + } + } } contentTransition.updateFrame(node: contentNode, frame: contentFrame, beginWithCurrentState: true) } if let contentNode = controllerContentNode { //TODO: var contentFrame = CGRect(origin: CGPoint(x: contentRect.minX, y: contentRect.minY + contentVerticalOffset + additionalVisibleOffsetY), size: contentRect.size) - if case let .extracted(extracted) = self.source, extracted.centerVertically, contentFrame.midX > layout.size.width / 2.0 { - contentFrame.origin.x = layout.size.width - contentFrame.maxX + if case let .extracted(extracted) = self.source, extracted.centerVertically { + if combinedActionsFrame.height.isZero { + contentFrame.origin.y = floorToScreenPixels((layout.size.height - contentFrame.height) / 2.0) + } else if contentFrame.midX > layout.size.width / 2.0 { + contentFrame.origin.x = layout.size.width - contentFrame.maxX + } } contentTransition.updateFrame(node: contentNode, frame: contentFrame, beginWithCurrentState: true) @@ -1099,6 +1109,8 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo switch stateTransition { case .animateIn: + let actionsSize = self.actionsContainerNode.bounds.size + if let contentNode = itemContentNode { contentNode.takeContainingNode() } @@ -1108,7 +1120,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo self.scroller.contentOffset = CGPoint(x: 0.0, y: defaultScrollY) - let animationInContentYDistance: CGFloat + var animationInContentYDistance: CGFloat let currentContentScreenFrame: CGRect if let contentNode = itemContentNode { if let animateClippingFromContentAreaInScreenSpace = contentNode.animateClippingFromContentAreaInScreenSpace { @@ -1123,19 +1135,25 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo var animationInContentXDistance: CGFloat = 0.0 let contentX = contentParentGlobalFrame.minX + contentRect.minX - contentNode.containingItem.contentRect.minX let contentWidth = contentNode.containingItem.view.bounds.size.width - if case let .extracted(extracted) = self.source, extracted.centerVertically, contentX + contentWidth > layout.size.width / 2.0 { - let fixedContentX = layout.size.width - (contentX + contentWidth) - animationInContentXDistance = fixedContentX - contentX - - contentNode.layer.animateSpring( - from: -animationInContentXDistance as NSNumber, to: 0.0 as NSNumber, - keyPath: "position.x", - duration: duration, - delay: 0.0, - initialVelocity: 0.0, - damping: springDamping, - additive: true - ) + let contentHeight = contentNode.containingItem.view.bounds.size.height + if case let .extracted(extracted) = self.source, extracted.centerVertically { + if actionsSize.height.isZero { + let fixedContentY = floorToScreenPixels((layout.size.height - contentHeight) / 2.0) + animationInContentYDistance = fixedContentY - contentRect.minY + } else if contentX + contentWidth > layout.size.width / 2.0, actionsSize.height > 0.0 { + let fixedContentX = layout.size.width - (contentX + contentWidth) + animationInContentXDistance = fixedContentX - contentX + + contentNode.layer.animateSpring( + from: -animationInContentXDistance as NSNumber, to: 0.0 as NSNumber, + keyPath: "position.x", + duration: duration, + delay: 0.0, + initialVelocity: 0.0, + damping: springDamping, + additive: true + ) + } } contentNode.layer.animateSpring( @@ -1191,9 +1209,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo damping: springDamping, additive: false ) - - let actionsSize = self.actionsContainerNode.bounds.size - + var actionsPositionDeltaXDistance: CGFloat = 0.0 if case .center = actionsHorizontalAlignment { actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsContainerNode.frame.midX @@ -1275,6 +1291,8 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } } case let .animateOut(result, completion): + let actionsSize = self.actionsContainerNode.bounds.size + let duration: Double let timingFunction: String switch result { @@ -1380,19 +1398,24 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo var animationInContentXDistance: CGFloat = 0.0 let contentX = contentParentGlobalFrame.minX + contentRect.minX - contentNode.containingItem.contentRect.minX let contentWidth = contentNode.containingItem.view.bounds.size.width - if case let .extracted(extracted) = self.source, extracted.centerVertically, contentX + contentWidth > layout.size.width / 2.0 { - let fixedContentX = layout.size.width - (contentX + contentWidth) - animationInContentXDistance = contentX - fixedContentX - - contentNode.offsetContainerNode.layer.animate( - from: -animationInContentXDistance as NSNumber, - to: 0.0 as NSNumber, - keyPath: "position.x", - timingFunction: timingFunction, - duration: duration, - delay: 0.0, - additive: true - ) + if case let .extracted(extracted) = self.source, extracted.centerVertically { + if actionsSize.height.isZero { +// let fixedContentY = floorToScreenPixels((layout.size.height - contentHeight) / 2.0) + animationInContentYDistance = 0.0 //contentY - fixedContentY + } else if contentX + contentWidth > layout.size.width / 2.0{ + let fixedContentX = layout.size.width - (contentX + contentWidth) + animationInContentXDistance = contentX - fixedContentX + + contentNode.offsetContainerNode.layer.animate( + from: -animationInContentXDistance as NSNumber, + to: 0.0 as NSNumber, + keyPath: "position.x", + timingFunction: timingFunction, + duration: duration, + delay: 0.0, + additive: true + ) + } } contentNode.offsetContainerNode.position = contentNode.offsetContainerNode.position.offsetBy(dx: animationInContentXDistance, dy: -animationInContentYDistance) @@ -1472,9 +1495,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } } ) - - let actionsSize = self.actionsContainerNode.bounds.size - + var actionsPositionDeltaXDistance: CGFloat = 0.0 if case .center = actionsHorizontalAlignment { actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsContainerNode.frame.midX diff --git a/submodules/ContextUI/Sources/ContextSourceContainer.swift b/submodules/ContextUI/Sources/ContextSourceContainer.swift index dc620965b84..d6c7ec6e7e8 100644 --- a/submodules/ContextUI/Sources/ContextSourceContainer.swift +++ b/submodules/ContextUI/Sources/ContextSourceContainer.swift @@ -7,6 +7,7 @@ import TelegramCore import ReactionSelectionNode import ComponentFlow import TabSelectorComponent +import PlainButtonComponent import ComponentDisplayAdapters final class ContextSourceContainer: ASDisplayNode { @@ -16,6 +17,8 @@ final class ContextSourceContainer: ASDisplayNode { let id: AnyHashable let title: String let source: ContextContentSource + let closeActionTitle: String? + let closeAction: (() -> Void)? private var _presentationNode: ContextControllerPresentationNode? var presentationNode: ContextControllerPresentationNode { @@ -40,12 +43,16 @@ final class ContextSourceContainer: ASDisplayNode { id: AnyHashable, title: String, source: ContextContentSource, - items: Signal + items: Signal, + closeActionTitle: String? = nil, + closeAction: (() -> Void)? = nil ) { self.controller = controller self.id = id self.title = title self.source = source + self.closeActionTitle = closeActionTitle + self.closeAction = closeAction self.ready.set(combineLatest(queue: .mainQueue(), self.contentReady.get(), self.actionsReady.get()) |> map { a, b -> Bool in @@ -162,8 +169,11 @@ final class ContextSourceContainer: ASDisplayNode { guard let self, let controller = self.controller else { return } - controller.controllerNode.dismissedForCancel?() - controller.controllerNode.beginDismiss(result) + if let _ = self.closeActionTitle { + } else { + controller.controllerNode.dismissedForCancel?() + controller.controllerNode.beginDismiss(result) + } }, requestAnimateOut: { [weak self] result, completion in guard let self, let controller = self.controller else { @@ -341,6 +351,7 @@ final class ContextSourceContainer: ASDisplayNode { var activeIndex: Int = 0 private var tabSelector: ComponentView? + private var closeButton: ComponentView? private var presentationData: PresentationData? private var validLayout: ContainerViewLayout? @@ -376,7 +387,9 @@ final class ContextSourceContainer: ASDisplayNode { id: source.id, title: source.title, source: source.source, - items: source.items + items: source.items, + closeActionTitle: source.closeActionTitle, + closeAction: source.closeAction ) self.sources.append(mappedSource) self.addSubnode(mappedSource.presentationNode) @@ -457,17 +470,33 @@ final class ContextSourceContainer: ASDisplayNode { if let tabSelectorView = self.tabSelector?.view { tabSelectorView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } + if let closeButtonView = self.closeButton?.view { + closeButtonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } } func animateOut(result: ContextMenuActionResult, completion: @escaping () -> Void) { - self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + let delayDismissal = self.activeSource?.closeAction != nil + let delay: Double = delayDismissal ? 0.2 : 0.0 + let duration: Double = delayDismissal ? 0.35 : 0.2 + + self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, delay: delay, removeOnCompletion: false, completion: { _ in + if delayDismissal { + Queue.mainQueue().after(0.55) { + completion() + } + } + }) if let tabSelectorView = self.tabSelector?.view { - tabSelectorView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + tabSelectorView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, delay: delay, removeOnCompletion: false) + } + if let closeButtonView = self.closeButton?.view { + closeButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, delay: delay, removeOnCompletion: false) } if let activeSource = self.activeSource { - activeSource.animateOut(result: result, completion: completion) + activeSource.animateOut(result: result, completion: delayDismissal ? {} : completion) } else { completion() } @@ -636,6 +665,47 @@ final class ContextSourceContainer: ASDisplayNode { } transition.updateFrame(view: tabSelectorView, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - tabSelectorSize.width) * 0.5), y: layout.size.height - layout.intrinsicInsets.bottom - tabSelectorSize.height), size: tabSelectorSize)) } + } else if let source = self.sources.first, let closeActionTitle = source.closeActionTitle { + let closeButton: ComponentView + if let current = self.closeButton { + closeButton = current + } else { + closeButton = ComponentView() + self.closeButton = closeButton + } + + let closeButtonSize = closeButton.update( + transition: Transition(transition), + component: AnyComponent(PlainButtonComponent( + content: AnyComponent( + CloseButtonComponent( + backgroundColor: presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.1), + text: closeActionTitle + ) + ), + effectAlignment: .center, + action: { [weak self, weak source] in + guard let self else { + return + } + if let source, let closeAction = source.closeAction { + closeAction() + } else { + self.controller?.dismiss(result: .dismissWithoutContent, completion: nil) + } + }) + ), + environment: {}, + containerSize: CGSize(width: layout.size.width, height: 44.0) + ) + childLayout.intrinsicInsets.bottom += 30.0 + + if let closeButtonView = closeButton.view { + if closeButtonView.superview == nil { + self.view.addSubview(closeButtonView) + } + transition.updateFrame(view: closeButtonView, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - closeButtonSize.width) * 0.5), y: layout.size.height - layout.intrinsicInsets.bottom - closeButtonSize.height - 10.0), size: closeButtonSize)) + } } else if let tabSelector = self.tabSelector { self.tabSelector = nil tabSelector.view?.removeFromSuperview() @@ -664,6 +734,11 @@ final class ContextSourceContainer: ASDisplayNode { return result } } + if let closeButtonView = self.closeButton?.view { + if let result = closeButtonView.hitTest(self.view.convert(point, to: closeButtonView), with: event) { + return result + } + } guard let activeSource = self.activeSource else { return nil @@ -671,3 +746,61 @@ final class ContextSourceContainer: ASDisplayNode { return activeSource.presentationNode.view.hitTest(point, with: event) } } + + +private final class CloseButtonComponent: CombinedComponent { + let backgroundColor: UIColor + let text: String + + init( + backgroundColor: UIColor, + text: String + ) { + self.backgroundColor = backgroundColor + self.text = text + } + + static func ==(lhs: CloseButtonComponent, rhs: CloseButtonComponent) -> Bool { + if lhs.backgroundColor != rhs.backgroundColor { + return false + } + if lhs.text != rhs.text { + return false + } + return true + } + + static var body: Body { + let background = Child(RoundedRectangle.self) + let text = Child(Text.self) + + return { context in + let text = text.update( + component: Text( + text: "\(context.component.text)", + font: Font.regular(17.0), + color: .white + ), + availableSize: CGSize(width: 200.0, height: 100.0), + transition: .immediate + ) + + let backgroundSize = CGSize(width: text.size.width + 34.0, height: 36.0) + let background = background.update( + component: RoundedRectangle(color: context.component.backgroundColor, cornerRadius: 18.0), + availableSize: backgroundSize, + transition: .immediate + ) + + context.add(background + .position(CGPoint(x: backgroundSize.width / 2.0, y: backgroundSize.height / 2.0)) + ) + + context.add(text + .position(CGPoint(x: backgroundSize.width / 2.0, y: backgroundSize.height / 2.0)) + ) + + return backgroundSize + } + } +} diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index 7ecc6f8c35f..4a37fb10939 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -81,7 +81,8 @@ private enum DebugControllerEntry: ItemListNodeEntry { case logToConsole(PresentationTheme, Bool) case redactSensitiveData(PresentationTheme, Bool) case keepChatNavigationStack(PresentationTheme, Bool) - case unidirectionalSwipeToReply(Bool) + // MARK: Nicegram StealthMode, delete skipReadHistory + case dustEffect(Bool) case callV2(Bool) case alternativeStoryMedia(Bool) case crashOnSlowQueries(PresentationTheme, Bool) @@ -89,7 +90,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case clearTips(PresentationTheme) case resetNotifications case crash(PresentationTheme) - case resetData(PresentationTheme) + case fillLocalSavedMessageCache case resetDatabase(PresentationTheme) case resetDatabaseAndCache(PresentationTheme) case resetHoles(PresentationTheme) @@ -140,9 +141,10 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.logging.rawValue case .webViewInspection, .resetWebViewCache: return DebugControllerSection.web.rawValue - case .keepChatNavigationStack, .unidirectionalSwipeToReply, .callV2, .alternativeStoryMedia, .crashOnSlowQueries, .crashOnMemoryPressure: + // MARK: Nicegram StealthMode, delete skipReadHistory + case .keepChatNavigationStack, .dustEffect, .callV2, .alternativeStoryMedia, .crashOnSlowQueries, .crashOnMemoryPressure: return DebugControllerSection.experiments.rawValue - case .clearTips, .resetNotifications, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .inlineForums, .localTranscription, .enableReactionOverrides, .restorePurchases: + case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .inlineForums, .localTranscription, .enableReactionOverrides, .restorePurchases: return DebugControllerSection.experiments.rawValue case .logTranslationRecognition, .resetTranslationStates: return DebugControllerSection.translation.rawValue @@ -195,7 +197,8 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 14 case .keepChatNavigationStack: return 15 - case .unidirectionalSwipeToReply: + // MARK: Nicegram StealthMode, delete skipReadHistory + case .dustEffect: return 17 case .callV2: return 18 @@ -211,7 +214,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 23 case .crash: return 24 - case .resetData: + case .fillLocalSavedMessageCache: return 25 case .resetDatabase: return 26 @@ -987,11 +990,12 @@ private enum DebugControllerEntry: ItemListNodeEntry { return settings }).start() }) - case let .unidirectionalSwipeToReply(value): - return ItemListSwitchItem(presentationData: presentationData, title: "Legacy swipe to reply", value: value, sectionId: self.section, style: .blocks, updated: { value in + // MARK: Nicegram StealthMode, delete skipReadHistory + case let .dustEffect(value): + return ItemListSwitchItem(presentationData: presentationData, title: "Dust Debug", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = updateExperimentalUISettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in var settings = settings - settings.unidirectionalSwipeToReply = value + settings.dustEffect = value return settings }).start() }) @@ -1070,24 +1074,18 @@ private enum DebugControllerEntry: ItemListNodeEntry { return ItemListActionItem(presentationData: presentationData, title: "Crash", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { preconditionFailure() }) - case .resetData: - return ItemListActionItem(presentationData: presentationData, title: "Reset Data", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { + case .fillLocalSavedMessageCache: + return ItemListActionItem(presentationData: presentationData, title: "Reload Saved Messages", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + guard let context = arguments.context else { + return + } let presentationData = arguments.sharedContext.currentPresentationData.with { $0 } - let actionSheet = ActionSheetController(presentationData: presentationData) - actionSheet.setItemGroups([ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: "All data will be lost."), - ActionSheetButtonItem(title: "Reset Data", color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - let databasePath = arguments.sharedContext.accountManager.basePath + "/db" - let _ = try? FileManager.default.removeItem(atPath: databasePath) - preconditionFailure() - }), - ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - arguments.presentController(actionSheet, nil) + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) + arguments.presentController(controller, nil) + let _ = (_internal_fillSavedMessageHistory(accountPeerId: context.account.peerId, postbox: context.account.postbox, network: context.account.network) + |> deliverOnMainQueue).start(completed: { + controller.dismiss() + }) }) case .resetDatabase: return ItemListActionItem(presentationData: presentationData, title: "Clear Database", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { @@ -1488,7 +1486,8 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present entries.append(.resetWebViewCache(presentationData.theme)) entries.append(.keepChatNavigationStack(presentationData.theme, experimentalSettings.keepChatNavigationStack)) - entries.append(.unidirectionalSwipeToReply(experimentalSettings.unidirectionalSwipeToReply)) + // MARK: Nicegram StealthMode, delete skipReadHistory + entries.append(.dustEffect(experimentalSettings.dustEffect)) entries.append(.callV2(experimentalSettings.callV2)) entries.append(.alternativeStoryMedia(experimentalSettings.alternativeStoryMedia)) } @@ -1499,7 +1498,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present entries.append(.resetNotifications) } entries.append(.crash(presentationData.theme)) - entries.append(.resetData(presentationData.theme)) + entries.append(.fillLocalSavedMessageCache) entries.append(.resetDatabase(presentationData.theme)) entries.append(.resetDatabaseAndCache(presentationData.theme)) entries.append(.resetHoles(presentationData.theme)) diff --git a/submodules/DeleteChatPeerActionSheetItem/BUILD b/submodules/DeleteChatPeerActionSheetItem/BUILD index ca2b91ed1b2..40f03aff214 100644 --- a/submodules/DeleteChatPeerActionSheetItem/BUILD +++ b/submodules/DeleteChatPeerActionSheetItem/BUILD @@ -16,6 +16,9 @@ swift_library( "//submodules/AccountContext:AccountContext", "//submodules/AvatarNode:AvatarNode", "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/ComponentFlow", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BalancedTextComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/DeleteChatPeerActionSheetItem/Sources/DeleteChatPeerActionSheetItem.swift b/submodules/DeleteChatPeerActionSheetItem/Sources/DeleteChatPeerActionSheetItem.swift index 90cc87378e6..11e2107c4e7 100644 --- a/submodules/DeleteChatPeerActionSheetItem/Sources/DeleteChatPeerActionSheetItem.swift +++ b/submodules/DeleteChatPeerActionSheetItem/Sources/DeleteChatPeerActionSheetItem.swift @@ -6,6 +6,9 @@ import TelegramPresentationData import TelegramUIPreferences import AvatarNode import AccountContext +import ComponentFlow +import BalancedTextComponent +import MultilineTextComponent public enum DeleteChatPeerAction { case delete @@ -15,6 +18,7 @@ public enum DeleteChatPeerAction { case clearCacheSuggestion case removeFromGroup case removeFromChannel + case deleteSavedPeer } private let avatarFont = avatarPlaceholderFont(size: 26.0) @@ -26,18 +30,20 @@ public final class DeleteChatPeerActionSheetItem: ActionSheetItem { let action: DeleteChatPeerAction let strings: PresentationStrings let nameDisplayOrder: PresentationPersonNameOrder + let balancedLayout: Bool - public init(context: AccountContext, peer: EnginePeer, chatPeer: EnginePeer, action: DeleteChatPeerAction, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder) { + public init(context: AccountContext, peer: EnginePeer, chatPeer: EnginePeer, action: DeleteChatPeerAction, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, balancedLayout: Bool = false) { self.context = context self.peer = peer self.chatPeer = chatPeer self.action = action self.strings = strings self.nameDisplayOrder = nameDisplayOrder + self.balancedLayout = balancedLayout } public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { - return DeleteChatPeerActionSheetItemNode(theme: theme, strings: self.strings, nameOrder: self.nameDisplayOrder, context: self.context, peer: self.peer, chatPeer: self.chatPeer, action: self.action) + return DeleteChatPeerActionSheetItemNode(theme: theme, strings: self.strings, nameOrder: self.nameDisplayOrder, context: self.context, peer: self.peer, chatPeer: self.chatPeer, action: self.action, balancedLayout: self.balancedLayout) } public func updateNode(_ node: ActionSheetItemNode) { @@ -47,15 +53,19 @@ public final class DeleteChatPeerActionSheetItem: ActionSheetItem { private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode { private let theme: ActionSheetControllerTheme private let strings: PresentationStrings + private let balancedLayout: Bool private let avatarNode: AvatarNode - private let textNode: ImmediateTextNode + + private var text: NSAttributedString? + private let textView = ComponentView() private let accessibilityArea: AccessibilityAreaNode - init(theme: ActionSheetControllerTheme, strings: PresentationStrings, nameOrder: PresentationPersonNameOrder, context: AccountContext, peer: EnginePeer, chatPeer: EnginePeer, action: DeleteChatPeerAction) { + init(theme: ActionSheetControllerTheme, strings: PresentationStrings, nameOrder: PresentationPersonNameOrder, context: AccountContext, peer: EnginePeer, chatPeer: EnginePeer, action: DeleteChatPeerAction, balancedLayout: Bool) { self.theme = theme self.strings = strings + self.balancedLayout = balancedLayout let textFont = Font.regular(floor(theme.baseFontSize * 14.0 / 17.0)) let boldFont = Font.semibold(floor(theme.baseFontSize * 14.0 / 17.0)) @@ -63,24 +73,19 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode { self.avatarNode = AvatarNode(font: avatarFont) self.avatarNode.isAccessibilityElement = false - self.textNode = ImmediateTextNode() - self.textNode.displaysAsynchronously = false - self.textNode.maximumNumberOfLines = 0 - self.textNode.textAlignment = .center - self.textNode.isAccessibilityElement = false - self.accessibilityArea = AccessibilityAreaNode() super.init(theme: theme) self.addSubnode(self.avatarNode) - self.addSubnode(self.textNode) self.addSubnode(self.accessibilityArea) if chatPeer.id == context.account.peerId { self.avatarNode.setPeer(context: context, theme: (context.sharedContext.currentPresentationData.with { $0 }).theme, peer: peer, overrideImage: .savedMessagesIcon) } else if chatPeer.id.isReplies { self.avatarNode.setPeer(context: context, theme: (context.sharedContext.currentPresentationData.with { $0 }).theme, peer: peer, overrideImage: .repliesIcon) + } else if chatPeer.id.isAnonymousSavedMessages { + self.avatarNode.setPeer(context: context, theme: (context.sharedContext.currentPresentationData.with { $0 }).theme, peer: peer, overrideImage: .anonymousSavedMessagesIcon) } else { var overrideImage: AvatarNodeImageOverride? if chatPeer.isDeleted { @@ -127,6 +132,9 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode { } else { text = strings.ChatList_DeleteChatConfirmation(peer.displayTitle(strings: strings, displayOrder: nameOrder)) } + case .deleteSavedPeer: + let peerTitle = peer.displayTitle(strings: strings, displayOrder: nameOrder) + text = strings.ChatList_DeleteSavedPeerConfirmation(peerTitle) case let .clearHistory(canClearCache): if peer.id == context.account.peerId { text = PresentationStrings.FormattedString(string: strings.ChatList_ClearSavedMessagesConfirmation, ranges: []) @@ -162,7 +170,7 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode { } if let attributedText = attributedText { - self.textNode.attributedText = attributedText + self.text = attributedText self.accessibilityArea.accessibilityLabel = attributedText.string self.accessibilityArea.accessibilityTraits = .staticText @@ -170,7 +178,21 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode { } public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - let textSize = self.textNode.updateLayout(CGSize(width: constrainedSize.width - 20.0, height: .greatestFiniteMagnitude)) + let textComponent: AnyComponent + if self.balancedLayout { + textComponent = AnyComponent(BalancedTextComponent( + text: .plain(self.text ?? NSAttributedString()), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )) + } else { + textComponent = AnyComponent(MultilineTextComponent( + text: .plain(self.text ?? NSAttributedString()), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )) + } + let textSize = self.textView.update(transition: .immediate, component: textComponent, environment: {}, containerSize: CGSize(width: constrainedSize.width - 20.0, height: 1000.0)) let topInset: CGFloat = 16.0 let avatarSize: CGFloat = 60.0 @@ -178,7 +200,13 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode { let bottomInset: CGFloat = 15.0 self.avatarNode.frame = CGRect(origin: CGPoint(x: floor((constrainedSize.width - avatarSize) / 2.0), y: topInset), size: CGSize(width: avatarSize, height: avatarSize)) - self.textNode.frame = CGRect(origin: CGPoint(x: floor((constrainedSize.width - textSize.width) / 2.0), y: topInset + avatarSize + textSpacing), size: textSize) + + if let textComponentView = self.textView.view { + if textComponentView.superview == nil { + self.view.addSubview(textComponentView) + } + textComponentView.frame = CGRect(origin: CGPoint(x: floor((constrainedSize.width - textSize.width) / 2.0), y: topInset + avatarSize + textSpacing), size: textSize) + } let size = CGSize(width: constrainedSize.width, height: topInset + avatarSize + textSpacing + textSize.height + bottomInset) self.accessibilityArea.frame = CGRect(origin: CGPoint(), size: size) diff --git a/submodules/Display/Source/DeviceMetrics.swift b/submodules/Display/Source/DeviceMetrics.swift index 32b74325ee0..b01c2a487dc 100644 --- a/submodules/Display/Source/DeviceMetrics.swift +++ b/submodules/Display/Source/DeviceMetrics.swift @@ -44,7 +44,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { case iPadPro case iPadPro3rdGen case iPadMini6thGen - case unknown(screenSize: CGSize, statusBarHeight: CGFloat, onScreenNavigationHeight: CGFloat?) + case unknown(screenSize: CGSize, statusBarHeight: CGFloat, onScreenNavigationHeight: CGFloat?, screenCornerRadius: CGFloat) public static let performance = Performance() @@ -111,14 +111,24 @@ public enum DeviceMetrics: CaseIterable, Equatable { return } } - self = .unknown(screenSize: screenSize, statusBarHeight: statusBarHeight, onScreenNavigationHeight: onScreenNavigationHeight) + + let screenCornerRadius: CGFloat + if screenSize.width >= 1024.0 || screenSize.height >= 1024.0 { + screenCornerRadius = 0.0 + } else if onScreenNavigationHeight != nil { + screenCornerRadius = 39.0 + } else { + screenCornerRadius = 0.0 + } + + self = .unknown(screenSize: screenSize, statusBarHeight: statusBarHeight, onScreenNavigationHeight: onScreenNavigationHeight, screenCornerRadius: screenCornerRadius) } public var type: DeviceType { switch self { case .iPad, .iPad102Inch, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen: return .tablet - case let .unknown(screenSize, _, _) where screenSize.width >= 744.0 && screenSize.height >= 1024.0: + case let .unknown(screenSize, _, _, _) where screenSize.width >= 744.0 && screenSize.height >= 1024.0: return .tablet default: return .phone @@ -175,7 +185,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { return CGSize(width: 1024.0, height: 1366.0) case .iPadMini6thGen: return CGSize(width: 744.0, height: 1133.0) - case let .unknown(screenSize, _, _): + case let .unknown(screenSize, _, _, _): return screenSize } } @@ -194,12 +204,8 @@ public enum DeviceMetrics: CaseIterable, Equatable { return 53.0 + UIScreenPixel case .iPhone14Pro, .iPhone14ProMax: return 55.0 - case let .unknown(_, _, onScreenNavigationHeight): - if let _ = onScreenNavigationHeight { - return 39.0 - } else { - return 0.0 - } + case let .unknown(_, _, _, screenCornerRadius): + return screenCornerRadius default: return 0.0 } @@ -230,7 +236,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { } else { return nil } - case let .unknown(_, _, onScreenNavigationHeight): + case let .unknown(_, _, onScreenNavigationHeight, _): return onScreenNavigationHeight default: return nil @@ -260,7 +266,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { return 44.0 case .iPadPro11Inch, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen: return 24.0 - case let .unknown(_, statusBarHeight, _): + case let .unknown(_, statusBarHeight, _, _): return statusBarHeight default: return 20.0 diff --git a/submodules/Display/Source/DisplayLinkAnimator.swift b/submodules/Display/Source/DisplayLinkAnimator.swift index 8a3af9d8c52..c67d08ce942 100644 --- a/submodules/Display/Source/DisplayLinkAnimator.swift +++ b/submodules/Display/Source/DisplayLinkAnimator.swift @@ -1,5 +1,6 @@ import Foundation import UIKit +import Darwin public protocol SharedDisplayLinkDriverLink: AnyObject { var isPaused: Bool { get set } @@ -7,6 +8,24 @@ public protocol SharedDisplayLinkDriverLink: AnyObject { func invalidate() } +private let isIpad: Bool = { + var systemInfo = utsname() + uname(&systemInfo) + let modelCode = withUnsafePointer(to: &systemInfo.machine) { + $0.withMemoryRebound(to: CChar.self, capacity: 1) { + ptr in String.init(validatingUTF8: ptr) + } + } + + if let modelCode { + if modelCode.lowercased().hasPrefix("ipad") { + return true + } + } + + return false +}() + public final class SharedDisplayLinkDriver { public enum FramesPerSecond: Comparable { case fps(Int) @@ -146,7 +165,7 @@ public final class SharedDisplayLinkDriver { if #available(iOS 15.0, *) { let maxFps = Float(UIScreen.main.maximumFramesPerSecond) if maxFps > 61.0 { - let frameRateRange: CAFrameRateRange + var frameRateRange: CAFrameRateRange switch maxFramesPerSecond { case let .fps(fps): if fps > 60 { @@ -157,6 +176,11 @@ public final class SharedDisplayLinkDriver { case .max: frameRateRange = CAFrameRateRange(minimum: 30.0, maximum: 120.0, preferred: 120.0) } + + if isIpad { + frameRateRange = CAFrameRateRange(minimum: 30.0, maximum: 120.0, preferred: 120.0) + } + if displayLink.preferredFrameRateRange != frameRateRange { displayLink.preferredFrameRateRange = frameRateRange print("SharedDisplayLinkDriver: switch to \(frameRateRange)") diff --git a/submodules/Display/Source/Nodes/ASImageNode.swift b/submodules/Display/Source/Nodes/ASImageNode.swift index f4b8406aaa6..2ebf0e99870 100644 --- a/submodules/Display/Source/Nodes/ASImageNode.swift +++ b/submodules/Display/Source/Nodes/ASImageNode.swift @@ -47,8 +47,4 @@ open class ASImageNode: ASDisplayNode { override public func calculateSizeThatFits(_ contrainedSize: CGSize) -> CGSize { return self.image?.size ?? CGSize() } - - public var asdf: Int { - return 1234 - } } diff --git a/submodules/Display/Source/TooltipController.swift b/submodules/Display/Source/TooltipController.swift index 3cd74e0f200..ca34cecec9c 100644 --- a/submodules/Display/Source/TooltipController.swift +++ b/submodules/Display/Source/TooltipController.swift @@ -178,7 +178,7 @@ open class TooltipController: ViewController, StandalonePresentableController { override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - if self.layout != nil && self.layout! != layout { + if self.layout != nil && self.layout!.size != layout.size { if self.dismissImmediatelyOnLayoutUpdate { self.dismissImmediately() } else { diff --git a/submodules/DrawingUI/BUILD b/submodules/DrawingUI/BUILD index 108213a0bd6..2b6b32ab637 100644 --- a/submodules/DrawingUI/BUILD +++ b/submodules/DrawingUI/BUILD @@ -106,6 +106,7 @@ swift_library( "//submodules/TelegramUI/Components/EntityKeyboard", "//submodules/Camera", "//submodules/TelegramUI/Components/DustEffect", + "//submodules/TelegramUI/Components/DynamicCornerRadiusView", ], visibility = [ "//visibility:public", diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index 2e7f76740d4..b996d290e13 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -3082,7 +3082,6 @@ public final class DrawingToolsInteraction { return } - var isRectangleImage = false var isVideo = false var isAdditional = false var isMessage = false @@ -3090,8 +3089,6 @@ public final class DrawingToolsInteraction { if case let .dualVideoReference(isAdditionalValue) = entity.content { isVideo = true isAdditional = isAdditionalValue - } else if case let .image(_, type) = entity.content, case .rectangle = type { - isRectangleImage = true } else if case .message = entity.content { isMessage = true } @@ -3154,7 +3151,7 @@ public final class DrawingToolsInteraction { } - if #available(iOS 17.0, *), isRectangleImage { + if #available(iOS 17.0, *), let stickerEntity = entityView.entity as? DrawingStickerEntity, stickerEntity.canCutOut { actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_CutOut, accessibilityLabel: presentationData.strings.Paint_CutOut), action: { [weak self, weak entityView] in if let self, let entityView, let entity = entityView.entity as? DrawingStickerEntity, case let .image(image, _) = entity.content { let _ = (cutoutStickerImage(from: image) diff --git a/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift b/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift index 2b444556477..cd4bb1e43b7 100644 --- a/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift @@ -14,6 +14,7 @@ import UniversalMediaPlayer import TelegramPresentationData import TelegramUniversalVideoContent import DustEffect +import DynamicCornerRadiusView private class BlurView: UIVisualEffectView { private func setup() { @@ -66,7 +67,9 @@ public class DrawingStickerEntityView: DrawingEntityView { let imageNode: TransformImageNode var animationNode: DefaultAnimatedStickerNodeImpl? var videoNode: UniversalVideoNode? + var videoMaskView: DynamicCornerRadiusView? var animatedImageView: UIImageView? + var overlayImageView: UIImageView? var cameraPreviewView: UIView? let progressDisposable = MetaDisposable() @@ -154,7 +157,7 @@ public class DrawingStickerEntityView: DrawingEntityView { return file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0) case .dualVideoReference: return CGSize(width: 512.0, height: 512.0) - case let .message(_, _, size): + case let .message(_, size, _, _, _): return size } } @@ -276,14 +279,17 @@ public class DrawingStickerEntityView: DrawingEntityView { self.animatedImageView = imageView self.addSubview(imageView) self.setNeedsLayout() - } else if case .message = self.stickerEntity.content { + } else if case let .message(_, _, file, mediaRect, _) = self.stickerEntity.content { if let image = self.stickerEntity.renderImage { - self.setupWithImage(image) + self.setupWithImage(image, overlayImage: self.stickerEntity.overlayRenderImage) + } + if let file, let _ = mediaRect { + self.setupWithVideo(file) } } } - private func setupWithImage(_ image: UIImage) { + private func setupWithImage(_ image: UIImage, overlayImage: UIImage? = nil) { let imageView: UIImageView if let current = self.animatedImageView { imageView = current @@ -294,6 +300,20 @@ public class DrawingStickerEntityView: DrawingEntityView { self.animatedImageView = imageView } imageView.image = image + + if let overlayImage { + let imageView: UIImageView + if let current = self.overlayImageView { + imageView = current + } else { + imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + self.addSubview(imageView) + self.overlayImageView = imageView + } + imageView.image = overlayImage + } + self.currentSize = nil self.setNeedsLayout() } @@ -335,6 +355,9 @@ public class DrawingStickerEntityView: DrawingEntityView { videoNode.isUserInteractionEnabled = false videoNode.clipsToBounds = true self.addSubnode(videoNode) + if let overlayImageView = self.overlayImageView { + self.addSubview(overlayImageView) + } self.videoNode = videoNode self.setNeedsLayout() videoNode.play() @@ -579,21 +602,53 @@ public class DrawingStickerEntityView: DrawingEntityView { } if let videoNode = self.videoNode { - var imageSize = imageSize - if case let .message(_, file, _) = self.stickerEntity.content, let dimensions = file?.dimensions { - let fittedDimensions = dimensions.cgSize.aspectFitted(boundingSize) - imageSize = fittedDimensions - videoNode.cornerRadius = 0.0 + if case let .message(_, size, _, rect, cornerRadius) = self.stickerEntity.content, let rect, let cornerRadius { + let baseSize = self.stickerEntity.baseSize + let scale = baseSize.width / size.width + let scaledRect = CGRect(x: rect.minX * scale, y: rect.minY * scale, width: rect.width * scale, height: rect.height * scale) + videoNode.frame = scaledRect + videoNode.updateLayout(size: scaledRect.size, transition: .immediate) + + if cornerRadius > 100.0 { + videoNode.cornerRadius = cornerRadius * scale + } else { + videoNode.cornerRadius = 0.0 + + let hasRoundBottomCorners = scaledRect.maxY > baseSize.height - 6.0 + if hasRoundBottomCorners { + let maskView: DynamicCornerRadiusView + if let current = self.videoMaskView { + maskView = current + } else { + maskView = DynamicCornerRadiusView() + self.videoMaskView = maskView + videoNode.view.mask = maskView + } + + let corners = DynamicCornerRadiusView.Corners( + minXMinY: 0.0, + maxXMinY: 0.0, + minXMaxY: cornerRadius * scale, + maxXMaxY: cornerRadius * scale + ) + maskView.update(size: scaledRect.size, corners: corners, transition: .immediate) + } else { + videoNode.view.mask = nil + } + } } else { videoNode.cornerRadius = floor(imageSize.width * 0.03) + videoNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) * 0.5), y: floor((size.height - imageSize.height) * 0.5)), size: imageSize) + videoNode.updateLayout(size: imageSize, transition: .immediate) } - videoNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) * 0.5), y: floor((size.height - imageSize.height) * 0.5)), size: imageSize) - videoNode.updateLayout(size: imageSize, transition: .immediate) } if let animatedImageView = self.animatedImageView { animatedImageView.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) * 0.5), y: floor((size.height - imageSize.height) * 0.5)), size: imageSize) } + if let overlayImageView = self.overlayImageView { + overlayImageView.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) * 0.5), y: floor((size.height - imageSize.height) * 0.5)), size: imageSize) + } if let cameraPreviewView = self.cameraPreviewView { cameraPreviewView.layer.cornerRadius = imageSize.width / 2.0 @@ -717,11 +772,42 @@ public class DrawingStickerEntityView: DrawingEntityView { } func getRenderSubEntities() -> [DrawingEntity] { - guard case let .message(_, file, _) = self.stickerEntity.content else { - return [] + if case let .message(_, _, file, _, cornerRadius) = self.stickerEntity.content { + if let file, let cornerRadius, let videoNode = self.videoNode { + let _ = cornerRadius + let stickerSize = self.bounds.size + let stickerPosition = self.stickerEntity.position + let videoSize = videoNode.frame.size + let scale = self.stickerEntity.scale + let rotation = self.stickerEntity.rotation + + let videoPosition = videoNode.position.offsetBy(dx: -stickerSize.width / 2.0, dy: -stickerSize.height / 2.0) + let videoScale = videoSize.width / stickerSize.width + + let videoEntity = DrawingStickerEntity(content: .video(file)) + videoEntity.referenceDrawingSize = self.stickerEntity.referenceDrawingSize + videoEntity.position = stickerPosition.offsetBy( + dx: (videoPosition.x * cos(rotation) - videoPosition.y * sin(rotation)) * scale, + dy: (videoPosition.y * cos(rotation) + videoPosition.x * sin(rotation)) * scale + ) + videoEntity.scale = scale * videoScale + videoEntity.rotation = rotation + + var entities: [DrawingEntity] = [] + entities.append(videoEntity) + + if let overlayImage = self.stickerEntity.overlayRenderImage { + let overlayEntity = DrawingStickerEntity(content: .image(overlayImage, .sticker)) + overlayEntity.referenceDrawingSize = self.stickerEntity.referenceDrawingSize + overlayEntity.position = self.stickerEntity.position + overlayEntity.scale = self.stickerEntity.scale + overlayEntity.rotation = self.stickerEntity.rotation + entities.append(overlayEntity) + } + + return entities + } } - - let _ = file return [] } } diff --git a/submodules/DrawingUI/Sources/ImageObjectSeparation.swift b/submodules/DrawingUI/Sources/ImageObjectSeparation.swift index c27efa81de0..764975d4f6b 100644 --- a/submodules/DrawingUI/Sources/ImageObjectSeparation.swift +++ b/submodules/DrawingUI/Sources/ImageObjectSeparation.swift @@ -8,7 +8,7 @@ import VideoToolbox private let queue = Queue() -func cutoutStickerImage(from image: UIImage) -> Signal { +public func cutoutStickerImage(from image: UIImage) -> Signal { if #available(iOS 17.0, *) { guard let cgImage = image.cgImage else { return .single(nil) diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index 9e9b7b7d7cb..8241ea8e241 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -219,11 +219,12 @@ public func galleryItemForEntry( } else if let file = media as? TelegramMediaFile { if file.isVideo { let content: UniversalVideoContent + let captureProtected = message.isCopyProtected() || message.containsSecretMedia || message.minAutoremoveOrClearTimeout == viewOnceTimeout if file.isAnimated { - content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), loopVideo: true, enableSound: false, tempFilePath: tempFilePath, captureProtected: message.isCopyProtected(), storeAfterDownload: generateStoreAfterDownload?(message, file)) + content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), loopVideo: true, enableSound: false, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file)) } else { if true || (file.mimeType == "video/mpeg4" || file.mimeType == "video/mov" || file.mimeType == "video/mp4") { - content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: message.isCopyProtected(), storeAfterDownload: generateStoreAfterDownload?(message, file)) + content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file)) } else { content = PlatformVideoContent(id: .message(message.id, message.stableId, file.fileId), userLocation: .peer(message.id.peerId), content: .file(.message(message: MessageReference(message), media: file)), streamVideo: streamVideos, loopVideo: loopVideos) } @@ -326,11 +327,11 @@ public func galleryItemForEntry( var content: UniversalVideoContent? switch websiteType(of: webpageContent.websiteName) { case .instagram where webpageContent.file != nil && webpageContent.image != nil && webpageContent.file!.isVideo: - content = NativeVideoContent(id: .message(message.stableId, webpageContent.file?.id ?? webpage.webpageId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: webpageContent.file!), imageReference: webpageContent.image.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, enableSound: true, captureProtected: message.isCopyProtected(), storeAfterDownload: nil) + content = NativeVideoContent(id: .message(message.stableId, webpageContent.file?.id ?? webpage.webpageId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: webpageContent.file!), imageReference: webpageContent.image.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, enableSound: true, captureProtected: message.isCopyProtected() || message.containsSecretMedia, storeAfterDownload: nil) default: if let embedUrl = webpageContent.embedUrl, let image = webpageContent.image { if let file = webpageContent.file, file.isVideo { - content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: message.isCopyProtected(), storeAfterDownload: generateStoreAfterDownload?(message, file)) + content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: message.isCopyProtected() || message.containsSecretMedia, storeAfterDownload: generateStoreAfterDownload?(message, file)) } else if URL(string: embedUrl)?.pathExtension == "mp4" { content = SystemVideoContent(userLocation: .peer(message.id.peerId), url: embedUrl, imageReference: .webPage(webPage: WebpageReference(webpage), media: image), dimensions: webpageContent.embedSize?.cgSize ?? CGSize(width: 640.0, height: 640.0), duration: webpageContent.duration.flatMap(Double.init) ?? 0.0) } diff --git a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift index 775de8c0313..911712c5059 100644 --- a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift +++ b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift @@ -525,7 +525,7 @@ public final class SecretMediaPreviewController: ViewController { } } - guard let item = galleryItemForEntry(context: self.context, presentationData: self.presentationData, entry: MessageHistoryEntry(message: message, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)), streamVideos: false, hideControls: true, isSecret: true, playbackRate: { nil }, tempFilePath: tempFilePath, playbackCompleted: { [weak self] in + guard let item = galleryItemForEntry(context: self.context, presentationData: self.presentationData, entry: MessageHistoryEntry(message: message, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)), streamVideos: false, hideControls: true, isSecret: true, playbackRate: { nil }, peerIsCopyProtected: true, tempFilePath: tempFilePath, playbackCompleted: { [weak self] in if let self { if self.currentNodeMessageIsViewOnce || (duration < 30.0 && !self.currentMessageIsDismissed) { if let node = self.controllerNode.pager.centralItemNode() as? UniversalVideoGalleryItemNode { diff --git a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift index 5f19df40943..c8077369ec9 100644 --- a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift @@ -184,7 +184,21 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { text = "" } } - strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), nil) + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in + if savedMessages, let self, action == .info { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.getNavigationController() else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) + }) + } + return false + }), nil) } }) } diff --git a/submodules/InstantPageUI/Sources/InstantPageMediaPlaylist.swift b/submodules/InstantPageUI/Sources/InstantPageMediaPlaylist.swift index 3f7da6b8906..11484db8b47 100644 --- a/submodules/InstantPageUI/Sources/InstantPageMediaPlaylist.swift +++ b/submodules/InstantPageUI/Sources/InstantPageMediaPlaylist.swift @@ -49,13 +49,13 @@ final class InstantPageMediaPlaylistItem: SharedMediaPlaylistItem { switch attribute { case let .Audio(isVoice, _, _, _, _): if isVoice { - return SharedMediaPlaybackData(type: .voice, source: .telegramFile(reference: .webPage(webPage: WebpageReference(self.webPage), media: file), isCopyProtected: false)) + return SharedMediaPlaybackData(type: .voice, source: .telegramFile(reference: .webPage(webPage: WebpageReference(self.webPage), media: file), isCopyProtected: false, isViewOnce: false)) } else { - return SharedMediaPlaybackData(type: .music, source: .telegramFile(reference: .webPage(webPage: WebpageReference(self.webPage), media: file), isCopyProtected: false)) + return SharedMediaPlaybackData(type: .music, source: .telegramFile(reference: .webPage(webPage: WebpageReference(self.webPage), media: file), isCopyProtected: false, isViewOnce: false)) } case let .Video(_, _, flags, _): if flags.contains(.instantRoundVideo) { - return SharedMediaPlaybackData(type: .instantVideo, source: .telegramFile(reference: .webPage(webPage: WebpageReference(self.webPage), media: file), isCopyProtected: false)) + return SharedMediaPlaybackData(type: .instantVideo, source: .telegramFile(reference: .webPage(webPage: WebpageReference(self.webPage), media: file), isCopyProtected: false, isViewOnce: false)) } else { return nil } @@ -64,12 +64,12 @@ final class InstantPageMediaPlaylistItem: SharedMediaPlaylistItem { } } if file.mimeType.hasPrefix("audio/") { - return SharedMediaPlaybackData(type: .music, source: .telegramFile(reference: .webPage(webPage: WebpageReference(self.webPage), media: file), isCopyProtected: false)) + return SharedMediaPlaybackData(type: .music, source: .telegramFile(reference: .webPage(webPage: WebpageReference(self.webPage), media: file), isCopyProtected: false, isViewOnce: false)) } if let fileName = file.fileName { let ext = (fileName as NSString).pathExtension.lowercased() if ext == "wav" || ext == "opus" { - return SharedMediaPlaybackData(type: .music, source: .telegramFile(reference: .webPage(webPage: WebpageReference(self.webPage), media: file), isCopyProtected: false)) + return SharedMediaPlaybackData(type: .music, source: .telegramFile(reference: .webPage(webPage: WebpageReference(self.webPage), media: file), isCopyProtected: false, isViewOnce: false)) } } } diff --git a/submodules/InstantPageUI/Sources/InstantPageSlideshowItemNode.swift b/submodules/InstantPageUI/Sources/InstantPageSlideshowItemNode.swift index 335f060cc24..b584d5b1b6b 100644 --- a/submodules/InstantPageUI/Sources/InstantPageSlideshowItemNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageSlideshowItemNode.swift @@ -415,7 +415,7 @@ final class InstantPageSlideshowNode: ASDisplayNode, InstantPageNode { super.layout() self.pagerNode.frame = self.bounds - self.pagerNode.containerLayoutUpdated(ContainerViewLayout(size: self.bounds.size, metrics: LayoutMetrics(), deviceMetrics: .unknown(screenSize: CGSize(), statusBarHeight: 0.0, onScreenNavigationHeight: nil), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: .immediate) + self.pagerNode.containerLayoutUpdated(ContainerViewLayout(size: self.bounds.size, metrics: LayoutMetrics(), deviceMetrics: .unknown(screenSize: CGSize(), statusBarHeight: 0.0, onScreenNavigationHeight: nil, screenCornerRadius: 0.0), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: .immediate) self.pageControlNode.layer.transform = CATransform3DIdentity self.pageControlNode.frame = CGRect(origin: CGPoint(x: 0.0, y: self.bounds.size.height - 20.0), size: CGSize(width: self.bounds.size.width, height: 20.0)) diff --git a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift index f2e72ec1f96..9cf0cf48275 100644 --- a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift @@ -311,6 +311,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese var presentInGlobalOverlayImpl: ((ViewController) -> Void)? var dismissImpl: (() -> Void)? var attemptNavigationImpl: ((@escaping () -> Void) -> Bool)? + var navigationController: (() -> NavigationController?)? var dismissTooltipsImpl: (() -> Void)? @@ -385,7 +386,21 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese } } - presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), nil) + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in + if savedMessages, action == .info { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { peer in + guard let peer else { + return + } + guard let navigationController = navigationController?() else { + return + } + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer))) + }) + } + return false + }), nil) }) } shareController.actionCompleted = { @@ -746,6 +761,9 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese return true } } + navigationController = { [weak controller] in + return controller?.navigationController as? NavigationController + } pushControllerImpl = { [weak controller] c in if let controller = controller { (controller.navigationController as? NavigationController)?.pushViewController(c, animated: true) diff --git a/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift b/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift index 59a906df9fa..9b784ac5ae5 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift @@ -466,7 +466,21 @@ public final class InviteLinkInviteController: ViewController { } } - strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .window(.root)) + strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in + if savedMessages, let self, action == .info { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.controller?.navigationController as? NavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) + }) + } + return false + }), in: .window(.root)) } }) } diff --git a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift index 62f57045e60..5207eff92b8 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift @@ -397,6 +397,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio var pushControllerImpl: ((ViewController) -> Void)? var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? var presentInGlobalOverlayImpl: ((ViewController) -> Void)? + var navigationController: (() -> NavigationController?)? var dismissTooltipsImpl: (() -> Void)? @@ -463,7 +464,21 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio } } - presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), nil) + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in + if savedMessages, action == .info { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { peer in + guard let peer else { + return + } + guard let navigationController = navigationController?() else { + return + } + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer))) + }) + } + return false + }), nil) }) } shareController.actionCompleted = { @@ -665,7 +680,21 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio } } - presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), nil) + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in + if savedMessages, action == .info { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { peer in + guard let peer else { + return + } + guard let navigationController = navigationController?() else { + return + } + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer))) + }) + } + return false + }), nil) }) } shareController.actionCompleted = { @@ -925,6 +954,9 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio (controller.navigationController as? NavigationController)?.pushViewController(c, animated: true) } } + navigationController = { [weak controller] in + return controller?.navigationController as? NavigationController + } presentControllerImpl = { [weak controller] c, p in if let controller = controller { controller.present(c, in: .window(.root), with: p) diff --git a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift index ba8fe938f5c..d6271c8fc62 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift @@ -536,7 +536,21 @@ public final class InviteLinkViewController: ViewController { } } - strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .window(.root)) + strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in + if savedMessages, let self, action == .info { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.controller?.navigationController as? NavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) + }) + } + return false + }), in: .window(.root)) } }) } diff --git a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift index 7a17a43cf39..739937f0f7b 100644 --- a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift +++ b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift @@ -1041,6 +1041,8 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo titleAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_SavedMessages, font: currentBoldFont, textColor: titleColor) } else if item.peer.id.isReplies { titleAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_Replies, font: currentBoldFont, textColor: titleColor) + } else if item.peer.id.isAnonymousSavedMessages { + titleAttributedString = NSAttributedString(string: item.presentationData.strings.ChatList_AuthorHidden, font: currentBoldFont, textColor: titleColor) } else if case let .user(user) = item.peer { if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty { let string = NSMutableAttributedString() diff --git a/submodules/LegacyComponents/Sources/TGModernConversationInputMicButton.m b/submodules/LegacyComponents/Sources/TGModernConversationInputMicButton.m index 7161bb73140..30cf93a7bcb 100644 --- a/submodules/LegacyComponents/Sources/TGModernConversationInputMicButton.m +++ b/submodules/LegacyComponents/Sources/TGModernConversationInputMicButton.m @@ -6,6 +6,7 @@ #import "TGOverlayController.h" #import "TGColor.h" #import "TGImageUtils.h" +#import "TGPhotoEditorSparseView.h" static const CGFloat innerCircleRadius = 110.0f; static const CGFloat outerCircleRadius = innerCircleRadius + 50.0f; @@ -379,7 +380,7 @@ - (void)animateIn { }; } - _lockPanelWrapperView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 40.0f, 72.0f)]; + _lockPanelWrapperView = [[TGPhotoEditorSparseView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 40.0f, 72.0f)]; [[_presentation view] addSubview:_lockPanelWrapperView]; _lockPanelView = [self createLockPanelView]; diff --git a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift index 382ee3aa6ff..c91bb0375ab 100644 --- a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift @@ -818,7 +818,7 @@ public final class ListMessageFileItemNode: ListMessageNode { descriptionText = NSAttributedString(string: " ", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor) } - if let _ = item.message?.threadId, let threadInfo = item.message?.associatedThreadInfo { + if let _ = item.message?.threadId, item.message?.id.peerId.namespace == Namespaces.Peer.CloudChannel, let threadInfo = item.message?.associatedThreadInfo { if isInstantVideo || isVoice { titleExtraData = (NSAttributedString(string: threadInfo.title, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor), true, threadInfo.icon, threadInfo.iconColor) } else { diff --git a/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift index 9196c1b0891..a640ceb8d12 100644 --- a/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift @@ -509,7 +509,7 @@ public final class ListMessageSnippetItemNode: ListMessageNode { var forumThreadTitle: (title: NSAttributedString, showIcon: Bool, iconId: Int64?, iconColor: Int32)? = nil var authorString = "" - if let message = item.message, let _ = message.threadId, let threadInfo = message.associatedThreadInfo { + if let message = item.message, let _ = message.threadId, item.message?.id.peerId.namespace == Namespaces.Peer.CloudChannel, let threadInfo = message.associatedThreadInfo { let fullAuthorString = stringForFullAuthorName(message: EngineMessage(message), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId) authorString = fullAuthorString.first ?? "" forumThreadTitle = (NSAttributedString(string: threadInfo.title, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor), true, threadInfo.icon, threadInfo.iconColor) diff --git a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift index 9ab15c0373c..f48f1b2b86b 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift @@ -859,10 +859,10 @@ final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate, UI let previewText = groupLayouts.count > 1 ? presentationData.strings.Attachment_MessagesPreview : presentationData.strings.Attachment_MessagePreview let previewMessage = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [TelegramMediaAction(action: .customText(text: previewText, entities: [], additionalAttributes: nil))], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - let previewItem = self.context.sharedContext.makeChatMessagePreviewItem(context: context, messages: [previewMessage], theme: theme, strings: presentationData.strings, wallpaper: wallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: bubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.wallpaperBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: true) + let previewItem = self.context.sharedContext.makeChatMessagePreviewItem(context: context, messages: [previewMessage], theme: theme, strings: presentationData.strings, wallpaper: wallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: bubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.wallpaperBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: true, isPreview: true) let dragMessage = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [TelegramMediaAction(action: .customText(text: presentationData.strings.Attachment_DragToReorder, entities: [], additionalAttributes: nil))], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - let dragItem = self.context.sharedContext.makeChatMessagePreviewItem(context: context, messages: [dragMessage], theme: theme, strings: presentationData.strings, wallpaper: wallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: bubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.wallpaperBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: true) + let dragItem = self.context.sharedContext.makeChatMessagePreviewItem(context: context, messages: [dragMessage], theme: theme, strings: presentationData.strings, wallpaper: wallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: bubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.wallpaperBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: true, isPreview: true) let headerItems: [ListViewItem] = [previewItem, dragItem] diff --git a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift index 1fd43edf484..9e6ae6af5ec 100644 --- a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift @@ -2327,7 +2327,7 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta }) } else { if let navigationController = controller.navigationController as? NavigationController { - navigationController.replaceAllButRootController(context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(previewing: false)), animated: true) + navigationController.replaceAllButRootController(context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(.default)), animated: true) } } } diff --git a/submodules/PeerInfoUI/Sources/ConvertToSupergroupController.swift b/submodules/PeerInfoUI/Sources/ConvertToSupergroupController.swift index e5c9a37338c..5b13bed75d1 100644 --- a/submodules/PeerInfoUI/Sources/ConvertToSupergroupController.swift +++ b/submodules/PeerInfoUI/Sources/ConvertToSupergroupController.swift @@ -149,7 +149,7 @@ public func convertToSupergroupController(context: AccountContext, peerId: Engin if !alreadyConverting { convertDisposable.set((context.engine.peers.convertGroupToSupergroup(peerId: peerId) |> deliverOnMainQueue).start(next: { createdPeerId in - replaceControllerImpl?(context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: createdPeerId), subject: nil, botStart: nil, mode: .standard(previewing: false))) + replaceControllerImpl?(context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: createdPeerId), subject: nil, botStart: nil, mode: .standard(.default))) })) } })]), nil) diff --git a/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift b/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift index 990fb86e357..0a87d44f1d1 100644 --- a/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift +++ b/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift @@ -486,7 +486,7 @@ public func peersNearbyController(context: AccountContext) -> ViewController { })) }, contextAction: { peer, node, gesture in let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(previewing: true)) + let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.previewing)) chatController.canReadHistory.set(false) let contextController = ContextController(presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: peerNearbyContextMenuItems(context: context, peerId: peer.id, present: { c in presentControllerImpl?(c, nil) diff --git a/submodules/PhotoResources/Sources/PhotoResources.swift b/submodules/PhotoResources/Sources/PhotoResources.swift index 87a14b51137..450d9bb9ad9 100644 --- a/submodules/PhotoResources/Sources/PhotoResources.swift +++ b/submodules/PhotoResources/Sources/PhotoResources.swift @@ -2141,7 +2141,7 @@ public func chatSecretMessageVideo(account: Account, userLocation: MediaResource if blurredImage == nil { if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { let thumbnailSize = CGSize(width: image.width, height: image.height) - let thumbnailContextSize = thumbnailSize.aspectFilled(CGSize(width: 20.0, height: 20.0)) + let thumbnailContextSize = thumbnailSize.aspectFilled(CGSize(width: 40.0, height: 40.0)) if let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) { thumbnailContext.withFlippedContext { c in c.interpolationQuality = .none diff --git a/submodules/Postbox/Sources/Message.swift b/submodules/Postbox/Sources/Message.swift index a1300a3ec5e..af43f4f21b8 100644 --- a/submodules/Postbox/Sources/Message.swift +++ b/submodules/Postbox/Sources/Message.swift @@ -869,14 +869,14 @@ public enum StoreMessageId { } } -public func makeMessageThreadId(_ messageId: MessageId) -> Int64 { - return (Int64(messageId.namespace) << 32) | Int64(bitPattern: UInt64(UInt32(bitPattern: messageId.id))) -} - -public func makeThreadIdMessageId(peerId: PeerId, threadId: Int64) -> MessageId { - let namespace = Int32((threadId >> 32) & 0x7fffffff) - let id = Int32(bitPattern: UInt32(threadId & 0xffffffff)) - return MessageId(peerId: peerId, namespace: namespace, id: id) +public struct MessageThreadKey: Hashable { + public var peerId: PeerId + public var threadId: Int64 + + public init(peerId: PeerId, threadId: Int64) { + self.peerId = peerId + self.threadId = threadId + } } public final class StoreMessage { diff --git a/submodules/Postbox/Sources/MessageHistorySavedMessagesIndexView.swift b/submodules/Postbox/Sources/MessageHistorySavedMessagesIndexView.swift new file mode 100644 index 00000000000..61a46b3577e --- /dev/null +++ b/submodules/Postbox/Sources/MessageHistorySavedMessagesIndexView.swift @@ -0,0 +1,189 @@ +import Foundation + +final class MutableMessageHistorySavedMessagesIndexView: MutablePostboxView { + final class Item { + let id: Int64 + let peer: Peer? + let pinnedIndex: Int? + let index: MessageIndex + let topMessage: Message? + + init( + id: Int64, + peer: Peer?, + pinnedIndex: Int?, + index: MessageIndex, + topMessage: Message? + ) { + self.id = id + self.peer = peer + self.pinnedIndex = pinnedIndex + self.index = index + self.topMessage = topMessage + } + } + + fileprivate let peerId: PeerId + fileprivate var peer: Peer? + fileprivate var items: [Item] = [] + private var hole: ForumTopicListHolesEntry? + fileprivate var isLoading: Bool = false + + init(postbox: PostboxImpl, peerId: PeerId) { + self.peerId = peerId + + self.reload(postbox: postbox) + } + + private func reload(postbox: PostboxImpl) { + self.items.removeAll() + + self.peer = postbox.peerTable.get(self.peerId) + + let validIndexBoundary = postbox.peerThreadCombinedStateTable.get(peerId: self.peerId)?.validIndexBoundary + self.isLoading = validIndexBoundary == nil + + if let validIndexBoundary = validIndexBoundary { + if validIndexBoundary.messageId != 1 { + self.hole = ForumTopicListHolesEntry(peerId: self.peerId, index: validIndexBoundary) + } else { + self.hole = nil + } + } else { + self.hole = ForumTopicListHolesEntry(peerId: self.peerId, index: nil) + } + + if !self.isLoading { + let pinnedThreadIds = postbox.messageHistoryThreadPinnedTable.get(peerId: self.peerId) + + for item in postbox.messageHistoryThreadIndexTable.getAll(peerId: self.peerId) { + var pinnedIndex: Int? + if let index = pinnedThreadIds.firstIndex(of: item.threadId) { + pinnedIndex = index + } + + self.items.append(Item( + id: item.threadId, + peer: postbox.peerTable.get(PeerId(item.threadId)), + pinnedIndex: pinnedIndex, + index: item.index, + topMessage: postbox.getMessage(item.index.id) + )) + } + + self.items.sort(by: { lhs, rhs in + if let lhsPinnedIndex = lhs.pinnedIndex, let rhsPinnedIndex = rhs.pinnedIndex { + return lhsPinnedIndex < rhsPinnedIndex + } else if (lhs.pinnedIndex == nil) != (rhs.pinnedIndex == nil) { + if lhs.pinnedIndex != nil { + return true + } else { + return false + } + } + + return lhs.index > rhs.index + }) + } + } + + func replay(postbox: PostboxImpl, transaction: PostboxTransaction) -> Bool { + var updated = false + + if transaction.updatedMessageThreadPeerIds.contains(self.peerId) || transaction.updatedPinnedThreads.contains(self.peerId) || transaction.updatedPeerThreadCombinedStates.contains(self.peerId) || transaction.currentUpdatedMessageTagSummaries.contains(where: { $0.key.peerId == self.peerId }) || transaction.currentUpdatedMessageActionsSummaries.contains(where: { $0.key.peerId == self.peerId }) || transaction.currentUpdatedPeerChatListEmbeddedStates.contains(self.peerId) || transaction.currentUpdatedPeerNotificationSettings[self.peerId] != nil || transaction.updatedPinnedThreads.contains(self.peerId) { + self.reload(postbox: postbox) + updated = true + } + + return updated + } + + func topHole() -> ForumTopicListHolesEntry? { + return self.hole + } + + func refreshDueToExternalTransaction(postbox: PostboxImpl) -> Bool { + self.reload(postbox: postbox) + + return true + } + + func immutableView() -> PostboxView { + return MessageHistorySavedMessagesIndexView(self) + } +} + +public final class EngineMessageHistorySavedMessagesThread { + public final class Item: Equatable { + public let id: Int64 + public let peer: Peer? + public let pinnedIndex: Int? + public let index: MessageIndex + public let topMessage: Message? + + public init( + id: Int64, + peer: Peer?, + pinnedIndex: Int?, + index: MessageIndex, + topMessage: Message? + ) { + self.id = id + self.peer = peer + self.pinnedIndex = pinnedIndex + self.index = index + self.topMessage = topMessage + } + + public static func ==(lhs: Item, rhs: Item) -> Bool { + if lhs.id != rhs.id { + return false + } + if !arePeersEqual(lhs.peer, rhs.peer) { + return false + } + if lhs.pinnedIndex != rhs.pinnedIndex { + return false + } + if lhs.index != rhs.index { + return false + } + if let lhsMessage = lhs.topMessage, let rhsMessage = rhs.topMessage { + if lhsMessage.index != rhsMessage.index { + return false + } + if lhsMessage.stableVersion != rhsMessage.stableVersion { + return false + } + } else if (lhs.topMessage == nil) != (rhs.topMessage == nil) { + return false + } + + return true + } + } +} + +public final class MessageHistorySavedMessagesIndexView: PostboxView { + public let peer: Peer? + public let items: [EngineMessageHistorySavedMessagesThread.Item] + public let isLoading: Bool + + init(_ view: MutableMessageHistorySavedMessagesIndexView) { + self.peer = view.peer + + var items: [EngineMessageHistorySavedMessagesThread.Item] = [] + for item in view.items { + items.append(EngineMessageHistorySavedMessagesThread.Item( + id: item.id, + peer: item.peer, + pinnedIndex: item.pinnedIndex, + index: item.index, + topMessage: item.topMessage + )) + } + self.items = items + + self.isLoading = view.isLoading + } +} diff --git a/submodules/Postbox/Sources/MessageHistorySavedMessagesStatsView.swift b/submodules/Postbox/Sources/MessageHistorySavedMessagesStatsView.swift new file mode 100644 index 00000000000..014e0018f40 --- /dev/null +++ b/submodules/Postbox/Sources/MessageHistorySavedMessagesStatsView.swift @@ -0,0 +1,55 @@ +import Foundation + +final class MutableMessageHistorySavedMessagesStatsView: MutablePostboxView { + fileprivate let peerId: PeerId + fileprivate var count: Int = 0 + fileprivate var isLoading: Bool = false + + init(postbox: PostboxImpl, peerId: PeerId) { + self.peerId = peerId + + self.reload(postbox: postbox) + } + + private func reload(postbox: PostboxImpl) { + let validIndexBoundary = postbox.peerThreadCombinedStateTable.get(peerId: peerId)?.validIndexBoundary + self.isLoading = validIndexBoundary == nil + + if !self.isLoading { + self.count = postbox.messageHistoryThreadIndexTable.getCount(peerId: self.peerId) + } else { + self.count = 0 + } + } + + func replay(postbox: PostboxImpl, transaction: PostboxTransaction) -> Bool { + var updated = false + + if transaction.updatedMessageThreadPeerIds.contains(self.peerId) { + self.reload(postbox: postbox) + updated = true + } + + return updated + } + + func refreshDueToExternalTransaction(postbox: PostboxImpl) -> Bool { + self.reload(postbox: postbox) + + return true + } + + func immutableView() -> PostboxView { + return MessageHistorySavedMessagesStatsView(self) + } +} + +public final class MessageHistorySavedMessagesStatsView: PostboxView { + public let isLoading: Bool + public let count: Int + + init(_ view: MutableMessageHistorySavedMessagesStatsView) { + self.isLoading = view.isLoading + self.count = view.count + } +} diff --git a/submodules/Postbox/Sources/MessageHistoryTable.swift b/submodules/Postbox/Sources/MessageHistoryTable.swift index 1343de84d37..5af95e9c868 100644 --- a/submodules/Postbox/Sources/MessageHistoryTable.swift +++ b/submodules/Postbox/Sources/MessageHistoryTable.swift @@ -1554,7 +1554,7 @@ final class MessageHistoryTable: Table { let updatedGroupInfo = self.updateMovingGroupInfoInNamespace(index: updatedIndex, updatedIndex: updatedIndex, groupingKey: message.groupingKey, previousInfo: previousMessage.groupInfo, updatedGroupInfos: &updatedGroupInfos) - if previousMessage.tags != message.tags || index != updatedIndex { + if previousMessage.tags != message.tags || previousMessage.threadId != message.threadId || index != updatedIndex { if !previousMessage.tags.isEmpty { self.tagsTable.remove(tags: previousMessage.tags, index: index, updatedSummaries: &updatedMessageTagSummaries, invalidateSummaries: &invalidateMessageTagSummaries) if let threadId = previousMessage.threadId { diff --git a/submodules/Postbox/Sources/MessageThreadIndexTable.swift b/submodules/Postbox/Sources/MessageThreadIndexTable.swift index e0243c980c8..36cf7be63ff 100644 --- a/submodules/Postbox/Sources/MessageThreadIndexTable.swift +++ b/submodules/Postbox/Sources/MessageThreadIndexTable.swift @@ -119,13 +119,15 @@ class MessageHistoryThreadIndexTable: Table { } private let reverseIndexTable: MessageHistoryThreadReverseIndexTable + private let seedConfiguration: SeedConfiguration private let sharedKey = ValueBoxKey(length: 8 + 4 + 8 + 4 + 4) private var updatedInfoItems: [MessageHistoryThreadsTable.ItemId: UpdatedEntry] = [:] - init(valueBox: ValueBox, table: ValueBoxTable, reverseIndexTable: MessageHistoryThreadReverseIndexTable, useCaches: Bool) { + init(valueBox: ValueBox, table: ValueBoxTable, reverseIndexTable: MessageHistoryThreadReverseIndexTable, seedConfiguration: SeedConfiguration, useCaches: Bool) { self.reverseIndexTable = reverseIndexTable + self.seedConfiguration = seedConfiguration super.init(valueBox: valueBox, table: table, useCaches: useCaches) } @@ -219,6 +221,13 @@ class MessageHistoryThreadIndexTable: Table { info = nil } } + if info == nil { + if let value = self.seedConfiguration.automaticThreadIndexInfo(itemId.peerId, itemId.threadId) { + let encoder = PostboxEncoder() + value.encode(encoder) + info = encoder.makeReadBufferAndReset() + } + } if let topIndex = topIndex, let info = info { if let previousIndex = previousIndex { @@ -291,6 +300,10 @@ class MessageHistoryThreadIndexTable: Table { return result } + func getCount(peerId: PeerId) -> Int { + return self.valueBox.count(self.table, start: self.lowerBound(peerId: peerId), end: self.upperBound(peerId: peerId)) + } + override func beforeCommit() { super.beforeCommit() diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index edd4275bcd9..f8d2c16efc4 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -1084,6 +1084,11 @@ public final class Transaction { assert(!self.disposed) self.postbox?.scanMessages(peerId: peerId, namespace: namespace, tag: tag, f) } + + public func scanMessages(peerId: PeerId, threadId: Int64, namespace: MessageId.Namespace, tag: MessageTags, _ f: (Message) -> Bool) { + assert(!self.disposed) + self.postbox?.scanMessages(peerId: peerId, threadId: threadId, namespace: namespace, tag: tag, f) + } public func scanTopMessages(peerId: PeerId, namespace: MessageId.Namespace, limit: Int, _ f: (Message) -> Bool) { assert(!self.disposed) @@ -1346,6 +1351,26 @@ public final class Transaction { public func getPeerStoryStats(peerId: PeerId) -> PeerStoryStats? { return fetchPeerStoryStats(postbox: self.postbox!, peerId: peerId) } + + public func searchSubPeers(peerId: PeerId, query: String, indexNameMapping: [PeerId: [PeerIndexNameRepresentation]]) -> [Peer] { + let allThreads = self.postbox!.messageHistoryThreadIndexTable.getAll(peerId: peerId) + var matchingPeers: [(Peer, MessageIndex)] = [] + for (threadId, index, _) in allThreads { + if let peer = self.postbox!.peerTable.get(PeerId(threadId)) { + if let mappings = indexNameMapping[peer.id] { + inner: for mapping in mappings { + if mapping.matchesByTokens(query) { + matchingPeers.append((peer, index)) + break inner + } + } + } else if peer.indexName.matchesByTokens(query) { + matchingPeers.append((peer, index)) + } + } + } + return matchingPeers.sorted(by: { $0.1 > $1.1 }).map(\.0) + } } public enum PostboxResult { @@ -1756,7 +1781,7 @@ final class PostboxImpl { self.messageHistoryThreadTagsTable = MessageHistoryThreadTagsTable(valueBox: self.valueBox, table: MessageHistoryThreadTagsTable.tableSpec(71), useCaches: useCaches, seedConfiguration: self.seedConfiguration, summaryTable: self.messageHistoryTagsSummaryTable) self.messageHistoryThreadHoleIndexTable = MessageHistoryThreadHoleIndexTable(valueBox: self.valueBox, table: MessageHistoryThreadHoleIndexTable.tableSpec(63), useCaches: useCaches, metadataTable: self.messageHistoryMetadataTable, seedConfiguration: self.seedConfiguration) self.messageHistoryThreadReverseIndexTable = MessageHistoryThreadReverseIndexTable(valueBox: self.valueBox, table: MessageHistoryThreadReverseIndexTable.tableSpec(72), useCaches: useCaches) - self.messageHistoryThreadIndexTable = MessageHistoryThreadIndexTable(valueBox: self.valueBox, table: MessageHistoryThreadIndexTable.tableSpec(73), reverseIndexTable: self.messageHistoryThreadReverseIndexTable, useCaches: useCaches) + self.messageHistoryThreadIndexTable = MessageHistoryThreadIndexTable(valueBox: self.valueBox, table: MessageHistoryThreadIndexTable.tableSpec(73), reverseIndexTable: self.messageHistoryThreadReverseIndexTable, seedConfiguration: seedConfiguration, useCaches: useCaches) self.messageHistoryThreadPinnedTable = MessageHistoryThreadPinnedTable(valueBox: self.valueBox, table: MessageHistoryThreadPinnedTable.tableSpec(76), useCaches: useCaches) self.globalMessageHistoryTagsTable = GlobalMessageHistoryTagsTable(valueBox: self.valueBox, table: GlobalMessageHistoryTagsTable.tableSpec(39), useCaches: useCaches) self.localMessageHistoryTagsTable = LocalMessageHistoryTagsTable(valueBox: self.valueBox, table: GlobalMessageHistoryTagsTable.tableSpec(52), useCaches: useCaches) @@ -3962,6 +3987,28 @@ final class PostboxImpl { } } } + + fileprivate func scanMessages(peerId: PeerId, threadId: Int64, namespace: MessageId.Namespace, tag: MessageTags, _ f: (Message) -> Bool) { + var index = MessageIndex.lowerBound(peerId: peerId, namespace: namespace) + while true { + let indices = self.messageHistoryThreadTagsTable.laterIndices(tag: tag, threadId: threadId, peerId: peerId, namespace: namespace, index: index, includeFrom: false, count: 10) + for index in indices { + if let message = self.messageHistoryTable.getMessage(index) { + if !f(self.renderIntermediateMessage(message)) { + break + } + } else { + assertionFailure() + break + } + } + if let last = indices.last { + index = last + } else { + break + } + } + } fileprivate func scanTopMessages(peerId: PeerId, namespace: MessageId.Namespace, limit: Int, _ f: (Message) -> Bool) { let lowerBound = MessageIndex.lowerBound(peerId: peerId, namespace: namespace) diff --git a/submodules/Postbox/Sources/SeedConfiguration.swift b/submodules/Postbox/Sources/SeedConfiguration.swift index 9b9912281d0..1aff6733b7a 100644 --- a/submodules/Postbox/Sources/SeedConfiguration.swift +++ b/submodules/Postbox/Sources/SeedConfiguration.swift @@ -78,6 +78,7 @@ public final class SeedConfiguration { public let decodeAutoremoveTimeout: (CachedPeerData) -> Int32? public let decodeDisplayPeerAsRegularChat: (CachedPeerData) -> Bool public let isPeerUpgradeMessage: (Message) -> Bool + public let automaticThreadIndexInfo: (PeerId, Int64) -> StoredMessageHistoryThreadInfo? public init( globalMessageIdsPeerIdNamespaces: Set, @@ -105,7 +106,8 @@ public final class SeedConfiguration { decodeMessageThreadInfo: @escaping (CodableEntry) -> Message.AssociatedThreadInfo?, decodeAutoremoveTimeout: @escaping (CachedPeerData) -> Int32?, decodeDisplayPeerAsRegularChat: @escaping (CachedPeerData) -> Bool, - isPeerUpgradeMessage: @escaping (Message) -> Bool + isPeerUpgradeMessage: @escaping (Message) -> Bool, + automaticThreadIndexInfo: @escaping (PeerId, Int64) -> StoredMessageHistoryThreadInfo? ) { self.globalMessageIdsPeerIdNamespaces = globalMessageIdsPeerIdNamespaces self.initializeChatListWithHole = initializeChatListWithHole @@ -129,5 +131,6 @@ public final class SeedConfiguration { self.decodeAutoremoveTimeout = decodeAutoremoveTimeout self.decodeDisplayPeerAsRegularChat = decodeDisplayPeerAsRegularChat self.isPeerUpgradeMessage = isPeerUpgradeMessage + self.automaticThreadIndexInfo = automaticThreadIndexInfo } } diff --git a/submodules/Postbox/Sources/ViewTracker.swift b/submodules/Postbox/Sources/ViewTracker.swift index d724c7bea9a..fde70cefe66 100644 --- a/submodules/Postbox/Sources/ViewTracker.swift +++ b/submodules/Postbox/Sources/ViewTracker.swift @@ -453,6 +453,10 @@ final class ViewTracker { if let hole = view.topHole() { firstHoles.insert(hole) } + } else if case .savedMessagesIndex = key, let view = view as? MutableMessageHistorySavedMessagesIndexView { + if let hole = view.topHole() { + firstHoles.insert(hole) + } } } } diff --git a/submodules/Postbox/Sources/Views.swift b/submodules/Postbox/Sources/Views.swift index 779ce1c5e5c..67ccf5d34c1 100644 --- a/submodules/Postbox/Sources/Views.swift +++ b/submodules/Postbox/Sources/Views.swift @@ -46,6 +46,8 @@ public enum PostboxViewKey: Hashable { case storyExpirationTimeItems case peerStoryStats(peerIds: Set) case story(id: StoryId) + case savedMessagesIndex(peerId: PeerId) + case savedMessagesStats(peerId: PeerId) public func hash(into hasher: inout Hasher) { switch self { @@ -153,6 +155,10 @@ public enum PostboxViewKey: Hashable { hasher.combine(peerIds) case let .story(id): hasher.combine(id) + case let .savedMessagesIndex(peerId): + hasher.combine(peerId) + case let .savedMessagesStats(peerId): + hasher.combine(peerId) } } @@ -428,6 +434,18 @@ public enum PostboxViewKey: Hashable { } else { return false } + case let .savedMessagesIndex(peerId): + if case .savedMessagesIndex(peerId) = rhs { + return true + } else { + return false + } + case let .savedMessagesStats(peerId): + if case .savedMessagesStats(peerId) = rhs { + return true + } else { + return false + } } } } @@ -524,5 +542,9 @@ func postboxViewForKey(postbox: PostboxImpl, key: PostboxViewKey) -> MutablePost return MutablePeerStoryStatsView(postbox: postbox, peerIds: peerIds) case let .story(id): return MutableStoryView(postbox: postbox, id: id) + case let .savedMessagesIndex(peerId): + return MutableMessageHistorySavedMessagesIndexView(postbox: postbox, peerId: peerId) + case let .savedMessagesStats(peerId): + return MutableMessageHistorySavedMessagesStatsView(postbox: postbox, peerId: peerId) } } diff --git a/submodules/PremiumUI/Sources/PremiumBoostScreen.swift b/submodules/PremiumUI/Sources/PremiumBoostScreen.swift index 6a27077fafe..8cffe20868b 100644 --- a/submodules/PremiumUI/Sources/PremiumBoostScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumBoostScreen.swift @@ -88,10 +88,11 @@ public func PremiumBoostScreen( } } } - - let initialState = BoostState(level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), boosts: Int32(status.boosts)) + + let boosts = max(Int32(status.boosts), myBoostCount) + let initialState = BoostState(level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), boosts: boosts) let updatedState = Promise() - updatedState.set(.single(BoostState(level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), boosts: Int32(status.boosts + 1)))) + updatedState.set(.single(BoostState(level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), boosts: boosts + 1))) var updateImpl: (() -> Void)? var dismissImpl: (() -> Void)? @@ -236,8 +237,11 @@ public func PremiumBoostScreen( actions: [ TextAlertAction(type: .defaultAction, title: presentationData.strings.ChannelBoost_MoreBoosts_Gift, action: { dismissImpl?() - let controller = context.sharedContext.makePremiumGiftController(context: context) - pushController(controller) + + Queue.mainQueue().after(0.4) { + let controller = context.sharedContext.makePremiumGiftController(context: context, source: .channelBoost) + pushController(controller) + } }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Close, action: {}) ], diff --git a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift index b0f01da41fb..f217d65bb1d 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift @@ -21,11 +21,7 @@ import TextFormat import UniversalMediaPlayer import InstantPageCache -public enum PremiumGiftSource: Equatable { - case profile - case attachMenu - case settings - +extension PremiumGiftSource { var identifier: String? { switch self { case .profile: @@ -34,6 +30,16 @@ public enum PremiumGiftSource: Equatable { return "attach" case .settings: return "settings" + case .chatList: + return "chats" + case .channelBoost: + return "channel_boost" + case let .deeplink(reference): + if let reference = reference { + return "deeplink_\(reference)" + } else { + return "deeplink" + } } } } @@ -879,14 +885,14 @@ private final class PremiumGiftScreenComponent: CombinedComponent { let purpose: AppStoreTransactionPurpose var quantity: Int32 = 1 - if case .settings = self.source { - purpose = .giftCode(peerIds: self.peerIds, boostPeer: nil, currency: currency, amount: amount) - quantity = Int32(self.peerIds.count) - } else if let peerId = self.peerIds.first { + + if self.source == .profile || self.source == .attachMenu, let peerId = self.peerIds.first { purpose = .gift(peerId: peerId, currency: currency, amount: amount) } else { - fatalError() + purpose = .giftCode(peerIds: self.peerIds, boostPeer: nil, currency: currency, amount: amount) + quantity = Int32(self.peerIds.count) } + let _ = (self.context.engine.payments.canPurchasePremium(purpose: purpose) |> deliverOnMainQueue).start(next: { [weak self] available in if let strongSelf = self { diff --git a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift index 29c1e1dfbc1..5143f399c5d 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift @@ -1025,6 +1025,22 @@ private final class LimitSheetContent: CombinedComponent { badgeText = "\(limit)" string = strings.Premium_MaxPinsNoPremiumText("\(limit)").string } + case .pinnedSavedPeers: + let limit = state.limits.maxPinnedSavedChatCount + let premiumLimit = state.premiumLimits.maxPinnedSavedChatCount + iconName = "Premium/Pin" + badgeText = "\(component.count)" + string = component.count >= premiumLimit ? strings.Premium_MaxSavedPinsFinalText("\(premiumLimit)").string : strings.Premium_MaxSavedPinsText("\(limit)", "\(premiumLimit)").string + defaultValue = component.count > limit ? "\(limit)" : "" + premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" + badgePosition = max(0.15, min(0.85, CGFloat(component.count) / CGFloat(premiumLimit))) + badgeGraphPosition = badgePosition + buttonAnimationName = nil + + if isPremiumDisabled { + badgeText = "\(limit)" + string = strings.Premium_MaxSavedPinsNoPremiumText("\(limit)").string + } case .files: let limit = Int64(state.limits.maxUploadFileParts) * 512 * 1024 + 1024 * 1024 * 100 let premiumLimit = Int64(state.premiumLimits.maxUploadFileParts) * 512 * 1024 + 1024 * 1024 * 100 @@ -1771,6 +1787,7 @@ public class PremiumLimitScreen: ViewControllerComponentContainer { case folders case chatsPerFolder case pins + case pinnedSavedPeers case files case accounts case linksPerSharedFolder diff --git a/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift b/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift index 048e0fe72a9..453fdbc7edb 100644 --- a/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift +++ b/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift @@ -880,7 +880,7 @@ public class ReplaceBoostScreen: ViewController { } let navigationController = self.navigationController self.dismiss(animated: true, completion: { - let giftController = context.sharedContext.makePremiumGiftController(context: context) + let giftController = context.sharedContext.makePremiumGiftController(context: context, source: .channelBoost) navigationController?.pushViewController(giftController, animated: true) }) } diff --git a/submodules/RadialStatusNode/Sources/RadialStatusSecretTimeoutContentNode.swift b/submodules/RadialStatusNode/Sources/RadialStatusSecretTimeoutContentNode.swift index 03133297ea9..cc0bb47138b 100644 --- a/submodules/RadialStatusNode/Sources/RadialStatusSecretTimeoutContentNode.swift +++ b/submodules/RadialStatusNode/Sources/RadialStatusSecretTimeoutContentNode.swift @@ -143,7 +143,6 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode { return } - let absoluteTimestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 let alphaProgress: CGFloat diff --git a/submodules/SSignalKit/SwiftSignalKit/Source/Signal_SwiftCombine.swift b/submodules/SSignalKit/SwiftSignalKit/Source/Signal_SwiftCombine.swift new file mode 100644 index 00000000000..89e1ca74204 --- /dev/null +++ b/submodules/SSignalKit/SwiftSignalKit/Source/Signal_SwiftCombine.swift @@ -0,0 +1,45 @@ +import Combine + +// MARK: Nicegram + +@available(iOS 13.0, *) +public extension Publisher { + func toSignal() -> Signal { + Signal { subscriber in + let cancellable = self.sink( + receiveCompletion: { completion in + switch completion { + case .finished: + break + case .failure(let failure): + subscriber.putError(failure) + } + + subscriber.putCompletion() + }, + receiveValue: { value in + subscriber.putNext(value) + } + ) + + return ActionDisposable { + cancellable.cancel() + } + } + } +} + +public extension Signal { + func setNoError() -> Signal { + Signal { subscriber in + self.start( + next: { + subscriber.putNext($0) + }, + completed: { + subscriber.putCompletion() + } + ) + } + } +} diff --git a/submodules/ScreenCaptureDetection/Sources/ScreenCaptureDetection.swift b/submodules/ScreenCaptureDetection/Sources/ScreenCaptureDetection.swift index 3da80a49476..f0d89f73f64 100644 --- a/submodules/ScreenCaptureDetection/Sources/ScreenCaptureDetection.swift +++ b/submodules/ScreenCaptureDetection/Sources/ScreenCaptureDetection.swift @@ -81,6 +81,8 @@ public final class ScreenCaptureDetectionManager { private var screenRecordingDisposable: Disposable? private var screenRecordingCheckTimer: SwiftSignalKit.Timer? + public var isRecordingActive = false + public init(check: @escaping () -> Bool) { self.observer = NotificationCenter.default.addObserver(forName: UIApplication.userDidTakeScreenshotNotification, object: nil, queue: .main, using: { [weak self] _ in guard let _ = self else { @@ -94,6 +96,7 @@ public final class ScreenCaptureDetectionManager { guard let strongSelf = self else { return } + strongSelf.isRecordingActive = value if value { if strongSelf.screenRecordingCheckTimer == nil { strongSelf.screenRecordingCheckTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { diff --git a/submodules/SearchPeerMembers/Sources/SearchPeerMembers.swift b/submodules/SearchPeerMembers/Sources/SearchPeerMembers.swift index 25645782d0f..8cef0161a84 100644 --- a/submodules/SearchPeerMembers/Sources/SearchPeerMembers.swift +++ b/submodules/SearchPeerMembers/Sources/SearchPeerMembers.swift @@ -67,7 +67,7 @@ public func searchPeerMembers(context: AccountContext, peerId: EnginePeer.Id, ch disposable.dispose() } case let .replyThread(replyThreadMessage): - let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.mentions(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, threadMessageId: replyThreadMessage.messageId, searchQuery: normalizedQuery.isEmpty ? nil : normalizedQuery, updated: { state in + let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.mentions(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, threadMessageId: EngineMessage.Id(peerId: replyThreadMessage.peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: replyThreadMessage.threadId)), searchQuery: normalizedQuery.isEmpty ? nil : normalizedQuery, updated: { state in if case .ready = state.loadingState { subscriber.putNext((state.list.compactMap { participant in if participant.peer.isDeleted { diff --git a/submodules/SemanticStatusNode/Sources/SemanticStatusNode.swift b/submodules/SemanticStatusNode/Sources/SemanticStatusNode.swift index b84244351ca..01df0bddf29 100644 --- a/submodules/SemanticStatusNode/Sources/SemanticStatusNode.swift +++ b/submodules/SemanticStatusNode/Sources/SemanticStatusNode.swift @@ -6,7 +6,6 @@ import SwiftSignalKit import RLottieBinding import GZip import AppBundle -import ManagedAnimationNode public enum SemanticStatusNodeState: Equatable { public struct ProgressAppearance: Equatable { @@ -33,25 +32,27 @@ public enum SemanticStatusNodeState: Equatable { case pause case check(appearance: CheckAppearance?) case progress(value: CGFloat?, cancelEnabled: Bool, appearance: ProgressAppearance?) + case secretTimeout(position: Double, duration: Double, generationTimestamp: Double, appearance: ProgressAppearance?) case customIcon(UIImage) } -private protocol SemanticStatusNodeStateDrawingState: NSObjectProtocol { +protocol SemanticStatusNodeStateDrawingState: NSObjectProtocol { func draw(context: CGContext, size: CGSize, foregroundColor: UIColor) } -private protocol SemanticStatusNodeStateContext: AnyObject { +protocol SemanticStatusNodeStateContext: AnyObject { var isAnimating: Bool { get } var requestUpdate: () -> Void { get set } func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState } -private enum SemanticStatusNodeIcon: Equatable { +enum SemanticStatusNodeIcon: Equatable { case none case download case play case pause + case secretTimeout case custom(UIImage) } @@ -88,535 +89,6 @@ private func svgPath(_ path: StaticString, scale: CGPoint = CGPoint(x: 1.0, y: 1 return path } -private final class SemanticStatusNodeIconContext: SemanticStatusNodeStateContext { - final class DrawingState: NSObject, SemanticStatusNodeStateDrawingState { - let transitionFraction: CGFloat - let icon: SemanticStatusNodeIcon - let iconImage: UIImage? - let iconOffset: CGFloat - - init(transitionFraction: CGFloat, icon: SemanticStatusNodeIcon, iconImage: UIImage?, iconOffset: CGFloat) { - self.transitionFraction = transitionFraction - self.icon = icon - self.iconImage = iconImage - self.iconOffset = iconOffset - - super.init() - } - - func draw(context: CGContext, size: CGSize, foregroundColor: UIColor) { - context.saveGState() - context.translateBy(x: size.width / 2.0, y: size.height / 2.0) - context.scaleBy(x: max(0.01, self.transitionFraction), y: max(0.01, self.transitionFraction)) - context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) - - if foregroundColor.alpha.isZero { - context.setBlendMode(.destinationOut) - context.setFillColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor) - context.setStrokeColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor) - } else { - context.setBlendMode(.normal) - context.setFillColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor) - context.setStrokeColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor) - } - - switch self.icon { - case .none: - break - case .play: - let diameter = size.width - let factor = diameter / 50.0 - - let size: CGSize - var offset: CGFloat = 0.0 - if let iconImage = self.iconImage { - size = iconImage.size - offset = self.iconOffset - } else { - offset = 1.5 - size = CGSize(width: 15.0, height: 18.0) - } - context.translateBy(x: (diameter - size.width) / 2.0 + offset, y: (diameter - size.height) / 2.0) - if (diameter < 40.0) { - context.translateBy(x: size.width / 2.0, y: size.height / 2.0) - context.scaleBy(x: factor, y: factor) - context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) - } - if let iconImage = self.iconImage { - context.saveGState() - let iconRect = CGRect(origin: CGPoint(), size: iconImage.size) - context.translateBy(x: size.width / 2.0, y: size.height / 2.0) - context.scaleBy(x: 1.0, y: -1.0) - context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) - context.clip(to: iconRect, mask: iconImage.cgImage!) - context.fill(iconRect) - context.restoreGState() - } else { - let _ = try? drawSvgPath(context, path: "M1.71891969,0.209353049 C0.769586558,-0.350676705 0,0.0908839327 0,1.18800046 L0,16.8564753 C0,17.9569971 0.750549162,18.357187 1.67393713,17.7519379 L14.1073836,9.60224049 C15.0318735,8.99626906 15.0094718,8.04970371 14.062401,7.49100858 L1.71891969,0.209353049 ") - context.fillPath() - } - if (diameter < 40.0) { - context.translateBy(x: size.width / 2.0, y: size.height / 2.0) - context.scaleBy(x: 1.0 / 0.8, y: 1.0 / 0.8) - context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) - } - context.translateBy(x: -(diameter - size.width) / 2.0 - offset, y: -(diameter - size.height) / 2.0) - case .pause: - let diameter = size.width - let factor = diameter / 50.0 - - let size: CGSize - let offset: CGFloat - if let iconImage = self.iconImage { - size = iconImage.size - offset = self.iconOffset - } else { - size = CGSize(width: 15.0, height: 16.0) - offset = 0.0 - } - context.translateBy(x: (diameter - size.width) / 2.0 + offset, y: (diameter - size.height) / 2.0) - if (diameter < 40.0) { - context.translateBy(x: size.width / 2.0, y: size.height / 2.0) - context.scaleBy(x: factor, y: factor) - context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) - } - if let iconImage = self.iconImage { - context.saveGState() - let iconRect = CGRect(origin: CGPoint(), size: iconImage.size) - context.translateBy(x: size.width / 2.0, y: size.height / 2.0) - context.scaleBy(x: 1.0, y: -1.0) - context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) - context.clip(to: iconRect, mask: iconImage.cgImage!) - context.fill(iconRect) - context.restoreGState() - } else { - let _ = try? drawSvgPath(context, path: "M0,1.00087166 C0,0.448105505 0.443716645,0 0.999807492,0 L4.00019251,0 C4.55237094,0 5,0.444630861 5,1.00087166 L5,14.9991283 C5,15.5518945 4.55628335,16 4.00019251,16 L0.999807492,16 C0.447629061,16 0,15.5553691 0,14.9991283 L0,1.00087166 Z M10,1.00087166 C10,0.448105505 10.4437166,0 10.9998075,0 L14.0001925,0 C14.5523709,0 15,0.444630861 15,1.00087166 L15,14.9991283 C15,15.5518945 14.5562834,16 14.0001925,16 L10.9998075,16 C10.4476291,16 10,15.5553691 10,14.9991283 L10,1.00087166 ") - context.fillPath() - } - if (diameter < 40.0) { - context.translateBy(x: size.width / 2.0, y: size.height / 2.0) - context.scaleBy(x: 1.0 / 0.8, y: 1.0 / 0.8) - context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) - } - context.translateBy(x: -(diameter - size.width) / 2.0, y: -(diameter - size.height) / 2.0) - case let .custom(image): - let diameter = size.width - let imageRect = CGRect(origin: CGPoint(x: floor((diameter - image.size.width) / 2.0), y: floor((diameter - image.size.height) / 2.0)), size: image.size) - - context.saveGState() - context.translateBy(x: imageRect.midX, y: imageRect.midY) - context.scaleBy(x: 1.0, y: -1.0) - context.translateBy(x: -imageRect.midX, y: -imageRect.midY) - context.clip(to: imageRect, mask: image.cgImage!) - context.fill(imageRect) - context.restoreGState() - case .download: - let diameter = size.width - let factor = diameter / 50.0 - let lineWidth: CGFloat = max(1.6, 2.25 * factor) - - context.setLineWidth(lineWidth) - context.setLineCap(.round) - context.setLineJoin(.round) - - let arrowHeadSize: CGFloat = 15.0 * factor - let arrowLength: CGFloat = 18.0 * factor - let arrowHeadOffset: CGFloat = 1.0 * factor - - let leftPath = UIBezierPath() - leftPath.lineWidth = lineWidth - leftPath.lineCapStyle = .round - leftPath.lineJoinStyle = .round - leftPath.move(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 + arrowLength / 2.0 + arrowHeadOffset)) - leftPath.addLine(to: CGPoint(x: diameter / 2.0 - arrowHeadSize / 2.0, y: diameter / 2.0 + arrowLength / 2.0 - arrowHeadSize / 2.0 + arrowHeadOffset)) - leftPath.stroke() - - let rightPath = UIBezierPath() - rightPath.lineWidth = lineWidth - rightPath.lineCapStyle = .round - rightPath.lineJoinStyle = .round - rightPath.move(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 + arrowLength / 2.0 + arrowHeadOffset)) - rightPath.addLine(to: CGPoint(x: diameter / 2.0 + arrowHeadSize / 2.0, y: diameter / 2.0 + arrowLength / 2.0 - arrowHeadSize / 2.0 + arrowHeadOffset)) - rightPath.stroke() - - let bodyPath = UIBezierPath() - bodyPath.lineWidth = lineWidth - bodyPath.lineCapStyle = .round - bodyPath.lineJoinStyle = .round - bodyPath.move(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 - arrowLength / 2.0)) - bodyPath.addLine(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 + arrowLength / 2.0)) - bodyPath.stroke() - } - context.restoreGState() - } - } - - var icon: SemanticStatusNodeIcon { - didSet { - self.animationNode?.enqueueState(self.icon == .play ? .play : .pause, animated: self.iconImage != nil) - } - } - - var animationNode: PlayPauseIconNode? - var iconImage: UIImage? - var iconOffset: CGFloat = 0.0 - - init(icon: SemanticStatusNodeIcon) { - self.icon = icon - - if [.play, .pause].contains(icon) { - self.animationNode = PlayPauseIconNode() - self.animationNode?.imageUpdated = { [weak self] image in - if let strongSelf = self { - strongSelf.iconImage = image - if var position = strongSelf.animationNode?.state?.position { - position = position * 2.0 - if position > 1.0 { - position = 2.0 - position - } - strongSelf.iconOffset = (1.0 - position) * 1.5 - } - strongSelf.requestUpdate() - } - } - self.animationNode?.enqueueState(self.icon == .play ? .play : .pause, animated: false) - self.iconImage = self.animationNode?.image - self.iconOffset = 1.5 - } - } - - var isAnimating: Bool { - return false - } - - var requestUpdate: () -> Void = {} - - func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState { - return DrawingState(transitionFraction: transitionFraction, icon: self.icon, iconImage: self.iconImage, iconOffset: self.iconOffset) - } -} - -private final class SemanticStatusNodeProgressTransition { - let beginTime: Double - let initialValue: CGFloat - - init(beginTime: Double, initialValue: CGFloat) { - self.beginTime = beginTime - self.initialValue = initialValue - } - - func valueAt(timestamp: Double, actualValue: CGFloat) -> (CGFloat, Bool) { - let duration = 0.2 - var t = CGFloat((timestamp - self.beginTime) / duration) - t = min(1.0, max(0.0, t)) - return (t * actualValue + (1.0 - t) * self.initialValue, t >= 1.0 - 0.001) - } -} - -private final class SemanticStatusNodeProgressContext: SemanticStatusNodeStateContext { - final class DrawingState: NSObject, SemanticStatusNodeStateDrawingState { - let transitionFraction: CGFloat - let value: CGFloat? - let displayCancel: Bool - let appearance: SemanticStatusNodeState.ProgressAppearance? - let timestamp: Double - - init(transitionFraction: CGFloat, value: CGFloat?, displayCancel: Bool, appearance: SemanticStatusNodeState.ProgressAppearance?, timestamp: Double) { - self.transitionFraction = transitionFraction - self.value = value - self.displayCancel = displayCancel - self.appearance = appearance - self.timestamp = timestamp - - super.init() - } - - func draw(context: CGContext, size: CGSize, foregroundColor: UIColor) { - let diameter = size.width - - let factor = diameter / 50.0 - - context.saveGState() - - if foregroundColor.alpha.isZero { - context.setBlendMode(.destinationOut) - context.setFillColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor) - context.setStrokeColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor) - } else { - context.setBlendMode(.normal) - context.setFillColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor) - context.setStrokeColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor) - } - - var progress: CGFloat - var startAngle: CGFloat - var endAngle: CGFloat - if let value = self.value { - progress = value - startAngle = -CGFloat.pi / 2.0 - endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle - - if progress > 1.0 { - progress = 2.0 - progress - let tmp = startAngle - startAngle = endAngle - endAngle = tmp - } - progress = min(1.0, progress) - } else { - progress = CGFloat(1.0 + self.timestamp.remainder(dividingBy: 2.0)) - - startAngle = -CGFloat.pi / 2.0 - endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle - - if progress > 1.0 { - progress = 2.0 - progress - let tmp = startAngle - startAngle = endAngle - endAngle = tmp - } - progress = min(1.0, progress) - } - - let lineWidth: CGFloat - if let appearance = self.appearance { - lineWidth = appearance.lineWidth - } else { - lineWidth = max(1.6, 2.25 * factor) - } - - let pathDiameter: CGFloat - if let appearance = self.appearance { - pathDiameter = diameter - lineWidth - appearance.inset * 2.0 - } else { - pathDiameter = diameter - lineWidth - 2.5 * 2.0 - } - - var angle = self.timestamp.truncatingRemainder(dividingBy: Double.pi * 2.0) - angle *= 4.0 - - context.translateBy(x: diameter / 2.0, y: diameter / 2.0) - context.rotate(by: CGFloat(angle.truncatingRemainder(dividingBy: Double.pi * 2.0))) - context.translateBy(x: -diameter / 2.0, y: -diameter / 2.0) - - let path = UIBezierPath(arcCenter: CGPoint(x: diameter / 2.0, y: diameter / 2.0), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise: true) - path.lineWidth = lineWidth - path.lineCapStyle = .round - path.stroke() - - context.restoreGState() - - if self.displayCancel { - if foregroundColor.alpha.isZero { - context.setBlendMode(.destinationOut) - context.setFillColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor) - context.setStrokeColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor) - } else { - context.setBlendMode(.normal) - context.setFillColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor) - context.setStrokeColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor) - } - - context.saveGState() - context.translateBy(x: size.width / 2.0, y: size.height / 2.0) - context.scaleBy(x: max(0.01, self.transitionFraction), y: max(0.01, self.transitionFraction)) - context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) - - context.setLineWidth(max(1.3, 2.0 * factor)) - context.setLineCap(.round) - - let crossSize: CGFloat = 14.0 * factor - context.move(to: CGPoint(x: diameter / 2.0 - crossSize / 2.0, y: diameter / 2.0 - crossSize / 2.0)) - context.addLine(to: CGPoint(x: diameter / 2.0 + crossSize / 2.0, y: diameter / 2.0 + crossSize / 2.0)) - context.strokePath() - context.move(to: CGPoint(x: diameter / 2.0 + crossSize / 2.0, y: diameter / 2.0 - crossSize / 2.0)) - context.addLine(to: CGPoint(x: diameter / 2.0 - crossSize / 2.0, y: diameter / 2.0 + crossSize / 2.0)) - context.strokePath() - - context.restoreGState() - } - } - } - - var value: CGFloat? - let displayCancel: Bool - let appearance: SemanticStatusNodeState.ProgressAppearance? - var transition: SemanticStatusNodeProgressTransition? - - var isAnimating: Bool { - return true - } - - var requestUpdate: () -> Void = {} - - init(value: CGFloat?, displayCancel: Bool, appearance: SemanticStatusNodeState.ProgressAppearance?) { - self.value = value - self.displayCancel = displayCancel - self.appearance = appearance - } - - func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState { - let timestamp = CACurrentMediaTime() - - let resolvedValue: CGFloat? - if let value = self.value { - if let transition = self.transition { - let (v, isCompleted) = transition.valueAt(timestamp: timestamp, actualValue: value) - resolvedValue = v - if isCompleted { - self.transition = nil - } - } else { - resolvedValue = value - } - } else { - resolvedValue = nil - } - return DrawingState(transitionFraction: transitionFraction, value: resolvedValue, displayCancel: self.displayCancel, appearance: self.appearance, timestamp: timestamp) - } - - func maskView() -> UIView? { - return nil - } - - func updateValue(value: CGFloat?) { - if value != self.value { - let previousValue = self.value - self.value = value - let timestamp = CACurrentMediaTime() - if let _ = value, let previousValue = previousValue { - if let transition = self.transition { - self.transition = SemanticStatusNodeProgressTransition(beginTime: timestamp, initialValue: transition.valueAt(timestamp: timestamp, actualValue: previousValue).0) - } else { - self.transition = SemanticStatusNodeProgressTransition(beginTime: timestamp, initialValue: previousValue) - } - } else { - self.transition = nil - } - } - } -} - -private final class SemanticStatusNodeCheckContext: SemanticStatusNodeStateContext { - final class DrawingState: NSObject, SemanticStatusNodeStateDrawingState { - let transitionFraction: CGFloat - let value: CGFloat - let appearance: SemanticStatusNodeState.CheckAppearance? - - init(transitionFraction: CGFloat, value: CGFloat, appearance: SemanticStatusNodeState.CheckAppearance?) { - self.transitionFraction = transitionFraction - self.value = value - self.appearance = appearance - - super.init() - } - - func draw(context: CGContext, size: CGSize, foregroundColor: UIColor) { - let diameter = size.width - - let factor = diameter / 50.0 - - context.saveGState() - - if foregroundColor.alpha.isZero { - context.setBlendMode(.destinationOut) - context.setFillColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor) - context.setStrokeColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor) - } else { - context.setBlendMode(.normal) - context.setFillColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor) - context.setStrokeColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor) - } - - let center = CGPoint(x: diameter / 2.0, y: diameter / 2.0) - - let lineWidth: CGFloat - if let appearance = self.appearance { - lineWidth = appearance.lineWidth - } else { - lineWidth = max(1.6, 2.25 * factor) - } - - context.setLineWidth(max(1.7, lineWidth * factor)) - context.setLineCap(.round) - context.setLineJoin(.round) - context.setMiterLimit(10.0) - - let progress = self.value - let firstSegment: CGFloat = max(0.0, min(1.0, progress * 3.0)) - - var s = CGPoint(x: center.x - 10.0 * factor, y: center.y + 1.0 * factor) - var p1 = CGPoint(x: 7.0 * factor, y: 7.0 * factor) - var p2 = CGPoint(x: 13.0 * factor, y: -15.0 * factor) - - if diameter < 36.0 { - s = CGPoint(x: center.x - 7.0 * factor, y: center.y + 1.0 * factor) - p1 = CGPoint(x: 4.5 * factor, y: 4.5 * factor) - p2 = CGPoint(x: 10.0 * factor, y: -11.0 * factor) - } - - if !firstSegment.isZero { - if firstSegment < 1.0 { - context.move(to: CGPoint(x: s.x + p1.x * firstSegment, y: s.y + p1.y * firstSegment)) - context.addLine(to: s) - } else { - let secondSegment = (progress - 0.33) * 1.5 - context.move(to: CGPoint(x: s.x + p1.x + p2.x * secondSegment, y: s.y + p1.y + p2.y * secondSegment)) - context.addLine(to: CGPoint(x: s.x + p1.x, y: s.y + p1.y)) - context.addLine(to: s) - } - } - context.strokePath() - } - } - - var value: CGFloat - let appearance: SemanticStatusNodeState.CheckAppearance? - var transition: SemanticStatusNodeProgressTransition? - - var isAnimating: Bool { - return true - } - - var requestUpdate: () -> Void = {} - - init(value: CGFloat, appearance: SemanticStatusNodeState.CheckAppearance?) { - self.value = value - self.appearance = appearance - - self.animate() - } - - func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState { - let timestamp = CACurrentMediaTime() - - let resolvedValue: CGFloat - if let transition = self.transition { - let (v, isCompleted) = transition.valueAt(timestamp: timestamp, actualValue: value) - resolvedValue = v - if isCompleted { - self.transition = nil - } - } else { - resolvedValue = value - } - return DrawingState(transitionFraction: transitionFraction, value: resolvedValue, appearance: self.appearance) - } - - func maskView() -> UIView? { - return nil - } - - func animate() { - guard self.value < 1.0 else { - return - } - let timestamp = CACurrentMediaTime() - self.value = 1.0 - self.transition = SemanticStatusNodeProgressTransition(beginTime: timestamp, initialValue: 0.0) - } -} - private extension SemanticStatusNodeState { func context(current: SemanticStatusNodeStateContext?) -> SemanticStatusNodeStateContext { switch self { @@ -633,6 +105,8 @@ private extension SemanticStatusNodeState { icon = .pause case let .customIcon(image): icon = .custom(image) + case .secretTimeout: + icon = .none default: preconditionFailure() } @@ -654,6 +128,13 @@ private extension SemanticStatusNodeState { } else { return SemanticStatusNodeCheckContext(value: 0.0, appearance: appearance) } + case let .secretTimeout(position, duration, generationTimestamp, appearance): + if let current = current as? SemanticStatusNodeSecretTimeoutContext { + current.updateValue(position: position, duration: duration, generationTimestamp: generationTimestamp) + return current + } else { + return SemanticStatusNodeSecretTimeoutContext(position: position, duration: duration, generationTimestamp: generationTimestamp, appearance: appearance) + } case let .progress(value, cancelEnabled, appearance): if let current = current as? SemanticStatusNodeProgressContext, current.displayCancel == cancelEnabled { current.updateValue(value: value) @@ -1048,53 +529,3 @@ public final class SemanticStatusNode: ASControlNode { parameters.appearanceState.drawForeground(context: context, size: bounds.size) } } - -private enum PlayPauseIconNodeState: Equatable { - case play - case pause -} - -private final class PlayPauseIconNode: ManagedAnimationNode { - private let duration: Double = 0.35 - private var iconState: PlayPauseIconNodeState = .play - - init() { - super.init(size: CGSize(width: 36.0, height: 36.0)) - - self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01)) - } - - func enqueueState(_ state: PlayPauseIconNodeState, animated: Bool) { - guard self.iconState != state else { - return - } - - let previousState = self.iconState - self.iconState = state - - switch previousState { - case .pause: - switch state { - case .play: - if animated { - self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 83), duration: self.duration)) - } else { - self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01)) - } - case .pause: - break - } - case .play: - switch state { - case .pause: - if animated { - self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 41), duration: self.duration)) - } else { - self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01)) - } - case .play: - break - } - } - } -} diff --git a/submodules/SemanticStatusNode/Sources/SemanticStatusNodeCheckContext.swift b/submodules/SemanticStatusNode/Sources/SemanticStatusNodeCheckContext.swift new file mode 100644 index 00000000000..17b34d7cbc2 --- /dev/null +++ b/submodules/SemanticStatusNode/Sources/SemanticStatusNodeCheckContext.swift @@ -0,0 +1,123 @@ +import Foundation +import UIKit +import Display + +final class SemanticStatusNodeCheckContext: SemanticStatusNodeStateContext { + final class DrawingState: NSObject, SemanticStatusNodeStateDrawingState { + let transitionFraction: CGFloat + let value: CGFloat + let appearance: SemanticStatusNodeState.CheckAppearance? + + init(transitionFraction: CGFloat, value: CGFloat, appearance: SemanticStatusNodeState.CheckAppearance?) { + self.transitionFraction = transitionFraction + self.value = value + self.appearance = appearance + + super.init() + } + + func draw(context: CGContext, size: CGSize, foregroundColor: UIColor) { + let diameter = size.width + + let factor = diameter / 50.0 + + context.saveGState() + + if foregroundColor.alpha.isZero { + context.setBlendMode(.destinationOut) + context.setFillColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor) + context.setStrokeColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor) + } else { + context.setBlendMode(.normal) + context.setFillColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor) + context.setStrokeColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor) + } + + let center = CGPoint(x: diameter / 2.0, y: diameter / 2.0) + + let lineWidth: CGFloat + if let appearance = self.appearance { + lineWidth = appearance.lineWidth + } else { + lineWidth = max(1.6, 2.25 * factor) + } + + context.setLineWidth(max(1.7, lineWidth * factor)) + context.setLineCap(.round) + context.setLineJoin(.round) + context.setMiterLimit(10.0) + + let progress = self.value + let firstSegment: CGFloat = max(0.0, min(1.0, progress * 3.0)) + + var s = CGPoint(x: center.x - 10.0 * factor, y: center.y + 1.0 * factor) + var p1 = CGPoint(x: 7.0 * factor, y: 7.0 * factor) + var p2 = CGPoint(x: 13.0 * factor, y: -15.0 * factor) + + if diameter < 36.0 { + s = CGPoint(x: center.x - 7.0 * factor, y: center.y + 1.0 * factor) + p1 = CGPoint(x: 4.5 * factor, y: 4.5 * factor) + p2 = CGPoint(x: 10.0 * factor, y: -11.0 * factor) + } + + if !firstSegment.isZero { + if firstSegment < 1.0 { + context.move(to: CGPoint(x: s.x + p1.x * firstSegment, y: s.y + p1.y * firstSegment)) + context.addLine(to: s) + } else { + let secondSegment = (progress - 0.33) * 1.5 + context.move(to: CGPoint(x: s.x + p1.x + p2.x * secondSegment, y: s.y + p1.y + p2.y * secondSegment)) + context.addLine(to: CGPoint(x: s.x + p1.x, y: s.y + p1.y)) + context.addLine(to: s) + } + } + context.strokePath() + } + } + + var value: CGFloat + let appearance: SemanticStatusNodeState.CheckAppearance? + var transition: SemanticStatusNodeProgressTransition? + + var isAnimating: Bool { + return true + } + + var requestUpdate: () -> Void = {} + + init(value: CGFloat, appearance: SemanticStatusNodeState.CheckAppearance?) { + self.value = value + self.appearance = appearance + + self.animate() + } + + func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState { + let timestamp = CACurrentMediaTime() + + let resolvedValue: CGFloat + if let transition = self.transition { + let (v, isCompleted) = transition.valueAt(timestamp: timestamp, actualValue: value) + resolvedValue = v + if isCompleted { + self.transition = nil + } + } else { + resolvedValue = value + } + return DrawingState(transitionFraction: transitionFraction, value: resolvedValue, appearance: self.appearance) + } + + func maskView() -> UIView? { + return nil + } + + func animate() { + guard self.value < 1.0 else { + return + } + let timestamp = CACurrentMediaTime() + self.value = 1.0 + self.transition = SemanticStatusNodeProgressTransition(beginTime: timestamp, initialValue: 0.0) + } +} diff --git a/submodules/SemanticStatusNode/Sources/SemanticStatusNodeIconContext.swift b/submodules/SemanticStatusNode/Sources/SemanticStatusNodeIconContext.swift new file mode 100644 index 00000000000..9824108a51a --- /dev/null +++ b/submodules/SemanticStatusNode/Sources/SemanticStatusNodeIconContext.swift @@ -0,0 +1,262 @@ +import Foundation +import UIKit +import Display +import ManagedAnimationNode + +final class SemanticStatusNodeIconContext: SemanticStatusNodeStateContext { + final class DrawingState: NSObject, SemanticStatusNodeStateDrawingState { + let transitionFraction: CGFloat + let icon: SemanticStatusNodeIcon + let iconImage: UIImage? + let iconOffset: CGFloat + + init(transitionFraction: CGFloat, icon: SemanticStatusNodeIcon, iconImage: UIImage?, iconOffset: CGFloat) { + self.transitionFraction = transitionFraction + self.icon = icon + self.iconImage = iconImage + self.iconOffset = iconOffset + + super.init() + } + + func draw(context: CGContext, size: CGSize, foregroundColor: UIColor) { + context.saveGState() + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: max(0.01, self.transitionFraction), y: max(0.01, self.transitionFraction)) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + + if foregroundColor.alpha.isZero { + context.setBlendMode(.destinationOut) + context.setFillColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor) + context.setStrokeColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor) + } else { + context.setBlendMode(.normal) + context.setFillColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor) + context.setStrokeColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor) + } + + switch self.icon { + case .none, .secretTimeout: + break + case .play: + let diameter = size.width + let factor = diameter / 50.0 + + let size: CGSize + var offset: CGFloat = 0.0 + if let iconImage = self.iconImage { + size = iconImage.size + offset = self.iconOffset + } else { + offset = 1.5 + size = CGSize(width: 15.0, height: 18.0) + } + context.translateBy(x: (diameter - size.width) / 2.0 + offset, y: (diameter - size.height) / 2.0) + if (diameter < 40.0) { + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: factor, y: factor) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + } + if let iconImage = self.iconImage { + context.saveGState() + let iconRect = CGRect(origin: CGPoint(), size: iconImage.size) + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + context.clip(to: iconRect, mask: iconImage.cgImage!) + context.fill(iconRect) + context.restoreGState() + } else { + let _ = try? drawSvgPath(context, path: "M1.71891969,0.209353049 C0.769586558,-0.350676705 0,0.0908839327 0,1.18800046 L0,16.8564753 C0,17.9569971 0.750549162,18.357187 1.67393713,17.7519379 L14.1073836,9.60224049 C15.0318735,8.99626906 15.0094718,8.04970371 14.062401,7.49100858 L1.71891969,0.209353049 ") + context.fillPath() + } + if (diameter < 40.0) { + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0 / 0.8, y: 1.0 / 0.8) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + } + context.translateBy(x: -(diameter - size.width) / 2.0 - offset, y: -(diameter - size.height) / 2.0) + case .pause: + let diameter = size.width + let factor = diameter / 50.0 + + let size: CGSize + let offset: CGFloat + if let iconImage = self.iconImage { + size = iconImage.size + offset = self.iconOffset + } else { + size = CGSize(width: 15.0, height: 16.0) + offset = 0.0 + } + context.translateBy(x: (diameter - size.width) / 2.0 + offset, y: (diameter - size.height) / 2.0) + if (diameter < 40.0) { + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: factor, y: factor) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + } + if let iconImage = self.iconImage { + context.saveGState() + let iconRect = CGRect(origin: CGPoint(), size: iconImage.size) + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + context.clip(to: iconRect, mask: iconImage.cgImage!) + context.fill(iconRect) + context.restoreGState() + } else { + let _ = try? drawSvgPath(context, path: "M0,1.00087166 C0,0.448105505 0.443716645,0 0.999807492,0 L4.00019251,0 C4.55237094,0 5,0.444630861 5,1.00087166 L5,14.9991283 C5,15.5518945 4.55628335,16 4.00019251,16 L0.999807492,16 C0.447629061,16 0,15.5553691 0,14.9991283 L0,1.00087166 Z M10,1.00087166 C10,0.448105505 10.4437166,0 10.9998075,0 L14.0001925,0 C14.5523709,0 15,0.444630861 15,1.00087166 L15,14.9991283 C15,15.5518945 14.5562834,16 14.0001925,16 L10.9998075,16 C10.4476291,16 10,15.5553691 10,14.9991283 L10,1.00087166 ") + context.fillPath() + } + if (diameter < 40.0) { + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0 / 0.8, y: 1.0 / 0.8) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + } + context.translateBy(x: -(diameter - size.width) / 2.0, y: -(diameter - size.height) / 2.0) + case let .custom(image): + let diameter = size.width + let imageRect = CGRect(origin: CGPoint(x: floor((diameter - image.size.width) / 2.0), y: floor((diameter - image.size.height) / 2.0)), size: image.size) + + context.saveGState() + context.translateBy(x: imageRect.midX, y: imageRect.midY) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -imageRect.midX, y: -imageRect.midY) + context.clip(to: imageRect, mask: image.cgImage!) + context.fill(imageRect) + context.restoreGState() + case .download: + let diameter = size.width + let factor = diameter / 50.0 + let lineWidth: CGFloat = max(1.6, 2.25 * factor) + + context.setLineWidth(lineWidth) + context.setLineCap(.round) + context.setLineJoin(.round) + + let arrowHeadSize: CGFloat = 15.0 * factor + let arrowLength: CGFloat = 18.0 * factor + let arrowHeadOffset: CGFloat = 1.0 * factor + + let leftPath = UIBezierPath() + leftPath.lineWidth = lineWidth + leftPath.lineCapStyle = .round + leftPath.lineJoinStyle = .round + leftPath.move(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 + arrowLength / 2.0 + arrowHeadOffset)) + leftPath.addLine(to: CGPoint(x: diameter / 2.0 - arrowHeadSize / 2.0, y: diameter / 2.0 + arrowLength / 2.0 - arrowHeadSize / 2.0 + arrowHeadOffset)) + leftPath.stroke() + + let rightPath = UIBezierPath() + rightPath.lineWidth = lineWidth + rightPath.lineCapStyle = .round + rightPath.lineJoinStyle = .round + rightPath.move(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 + arrowLength / 2.0 + arrowHeadOffset)) + rightPath.addLine(to: CGPoint(x: diameter / 2.0 + arrowHeadSize / 2.0, y: diameter / 2.0 + arrowLength / 2.0 - arrowHeadSize / 2.0 + arrowHeadOffset)) + rightPath.stroke() + + let bodyPath = UIBezierPath() + bodyPath.lineWidth = lineWidth + bodyPath.lineCapStyle = .round + bodyPath.lineJoinStyle = .round + bodyPath.move(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 - arrowLength / 2.0)) + bodyPath.addLine(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 + arrowLength / 2.0)) + bodyPath.stroke() + } + context.restoreGState() + } + } + + var icon: SemanticStatusNodeIcon { + didSet { + self.animationNode?.enqueueState(self.icon == .play ? .play : .pause, animated: self.iconImage != nil) + } + } + + private var animationNode: PlayPauseIconNode? + private var iconImage: UIImage? + private var iconOffset: CGFloat = 0.0 + + init(icon: SemanticStatusNodeIcon) { + self.icon = icon + + if [.play, .pause].contains(icon) { + self.animationNode = PlayPauseIconNode() + self.animationNode?.imageUpdated = { [weak self] image in + if let strongSelf = self { + strongSelf.iconImage = image + if var position = strongSelf.animationNode?.state?.position { + position = position * 2.0 + if position > 1.0 { + position = 2.0 - position + } + strongSelf.iconOffset = (1.0 - position) * 1.5 + } + strongSelf.requestUpdate() + } + } + self.animationNode?.enqueueState(self.icon == .play ? .play : .pause, animated: false) + self.iconImage = self.animationNode?.image + self.iconOffset = 1.5 + } + } + + var isAnimating: Bool { + return false + } + + var requestUpdate: () -> Void = {} + + func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState { + return DrawingState(transitionFraction: transitionFraction, icon: self.icon, iconImage: self.iconImage, iconOffset: self.iconOffset) + } +} + +private enum PlayPauseIconNodeState: Equatable { + case play + case pause +} + +private final class PlayPauseIconNode: ManagedAnimationNode { + private let duration: Double = 0.35 + private var iconState: PlayPauseIconNodeState = .play + + init() { + super.init(size: CGSize(width: 36.0, height: 36.0)) + + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01)) + } + + func enqueueState(_ state: PlayPauseIconNodeState, animated: Bool) { + guard self.iconState != state else { + return + } + + let previousState = self.iconState + self.iconState = state + + switch previousState { + case .pause: + switch state { + case .play: + if animated { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 83), duration: self.duration)) + } else { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01)) + } + case .pause: + break + } + case .play: + switch state { + case .pause: + if animated { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 41), duration: self.duration)) + } else { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01)) + } + case .play: + break + } + } + } +} diff --git a/submodules/SemanticStatusNode/Sources/SemanticStatusNodeProgressContext.swift b/submodules/SemanticStatusNode/Sources/SemanticStatusNodeProgressContext.swift new file mode 100644 index 00000000000..671c33180f0 --- /dev/null +++ b/submodules/SemanticStatusNode/Sources/SemanticStatusNodeProgressContext.swift @@ -0,0 +1,204 @@ +import Foundation +import UIKit +import Display + +final class SemanticStatusNodeProgressTransition { + let beginTime: Double + let initialValue: CGFloat + + init(beginTime: Double, initialValue: CGFloat) { + self.beginTime = beginTime + self.initialValue = initialValue + } + + func valueAt(timestamp: Double, actualValue: CGFloat) -> (CGFloat, Bool) { + let duration = 0.2 + var t = CGFloat((timestamp - self.beginTime) / duration) + t = min(1.0, max(0.0, t)) + return (t * actualValue + (1.0 - t) * self.initialValue, t >= 1.0 - 0.001) + } +} + +final class SemanticStatusNodeProgressContext: SemanticStatusNodeStateContext { + final class DrawingState: NSObject, SemanticStatusNodeStateDrawingState { + let transitionFraction: CGFloat + let value: CGFloat? + let displayCancel: Bool + let appearance: SemanticStatusNodeState.ProgressAppearance? + let timestamp: Double + + init(transitionFraction: CGFloat, value: CGFloat?, displayCancel: Bool, appearance: SemanticStatusNodeState.ProgressAppearance?, timestamp: Double) { + self.transitionFraction = transitionFraction + self.value = value + self.displayCancel = displayCancel + self.appearance = appearance + self.timestamp = timestamp + + super.init() + } + + func draw(context: CGContext, size: CGSize, foregroundColor: UIColor) { + let diameter = size.width + + let factor = diameter / 50.0 + + context.saveGState() + + if foregroundColor.alpha.isZero { + context.setBlendMode(.destinationOut) + context.setFillColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor) + context.setStrokeColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor) + } else { + context.setBlendMode(.normal) + context.setFillColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor) + context.setStrokeColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor) + } + + var progress: CGFloat + var startAngle: CGFloat + var endAngle: CGFloat + if let value = self.value { + progress = value + startAngle = -CGFloat.pi / 2.0 + endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle + + if progress > 1.0 { + progress = 2.0 - progress + let tmp = startAngle + startAngle = endAngle + endAngle = tmp + } + progress = min(1.0, progress) + } else { + progress = CGFloat(1.0 + self.timestamp.remainder(dividingBy: 2.0)) + + startAngle = -CGFloat.pi / 2.0 + endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle + + if progress > 1.0 { + progress = 2.0 - progress + let tmp = startAngle + startAngle = endAngle + endAngle = tmp + } + progress = min(1.0, progress) + } + + let lineWidth: CGFloat + if let appearance = self.appearance { + lineWidth = appearance.lineWidth + } else { + lineWidth = max(1.6, 2.25 * factor) + } + + let pathDiameter: CGFloat + if let appearance = self.appearance { + pathDiameter = diameter - lineWidth - appearance.inset * 2.0 + } else { + pathDiameter = diameter - lineWidth - 2.5 * 2.0 + } + + var angle = self.timestamp.truncatingRemainder(dividingBy: Double.pi * 2.0) + angle *= 4.0 + + context.translateBy(x: diameter / 2.0, y: diameter / 2.0) + context.rotate(by: CGFloat(angle.truncatingRemainder(dividingBy: Double.pi * 2.0))) + context.translateBy(x: -diameter / 2.0, y: -diameter / 2.0) + + let path = UIBezierPath(arcCenter: CGPoint(x: diameter / 2.0, y: diameter / 2.0), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise: true) + path.lineWidth = lineWidth + path.lineCapStyle = .round + path.stroke() + + context.restoreGState() + + if self.displayCancel { + if foregroundColor.alpha.isZero { + context.setBlendMode(.destinationOut) + context.setFillColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor) + context.setStrokeColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor) + } else { + context.setBlendMode(.normal) + context.setFillColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor) + context.setStrokeColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor) + } + + context.saveGState() + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: max(0.01, self.transitionFraction), y: max(0.01, self.transitionFraction)) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + + context.setLineWidth(max(1.3, 2.0 * factor)) + context.setLineCap(.round) + + let crossSize: CGFloat = 14.0 * factor + context.move(to: CGPoint(x: diameter / 2.0 - crossSize / 2.0, y: diameter / 2.0 - crossSize / 2.0)) + context.addLine(to: CGPoint(x: diameter / 2.0 + crossSize / 2.0, y: diameter / 2.0 + crossSize / 2.0)) + context.strokePath() + context.move(to: CGPoint(x: diameter / 2.0 + crossSize / 2.0, y: diameter / 2.0 - crossSize / 2.0)) + context.addLine(to: CGPoint(x: diameter / 2.0 - crossSize / 2.0, y: diameter / 2.0 + crossSize / 2.0)) + context.strokePath() + + context.restoreGState() + } + } + } + + var value: CGFloat? + let displayCancel: Bool + let appearance: SemanticStatusNodeState.ProgressAppearance? + var transition: SemanticStatusNodeProgressTransition? + + var isAnimating: Bool { + return true + } + + var requestUpdate: () -> Void = {} + + init(value: CGFloat?, displayCancel: Bool, appearance: SemanticStatusNodeState.ProgressAppearance?) { + self.value = value + self.displayCancel = displayCancel + self.appearance = appearance + } + + func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState { + let timestamp = CACurrentMediaTime() + + let resolvedValue: CGFloat? + if let value = self.value { + if let transition = self.transition { + let (v, isCompleted) = transition.valueAt(timestamp: timestamp, actualValue: value) + resolvedValue = v + if isCompleted { + self.transition = nil + } + } else { + resolvedValue = value + } + } else { + resolvedValue = nil + } + return DrawingState(transitionFraction: transitionFraction, value: resolvedValue, displayCancel: self.displayCancel, appearance: self.appearance, timestamp: timestamp) + } + + func maskView() -> UIView? { + return nil + } + + func updateValue(value: CGFloat?) { + if value != self.value { + let previousValue = self.value + self.value = value + let timestamp = CACurrentMediaTime() + if let _ = value, let previousValue = previousValue { + if let transition = self.transition { + self.transition = SemanticStatusNodeProgressTransition(beginTime: timestamp, initialValue: transition.valueAt(timestamp: timestamp, actualValue: previousValue).0) + } else { + self.transition = SemanticStatusNodeProgressTransition(beginTime: timestamp, initialValue: previousValue) + } + } else { + self.transition = nil + } + } + } +} diff --git a/submodules/SemanticStatusNode/Sources/SemanticStatusNodeSecretTimeoutContext.swift b/submodules/SemanticStatusNode/Sources/SemanticStatusNodeSecretTimeoutContext.swift new file mode 100644 index 00000000000..103cb5a5ca2 --- /dev/null +++ b/submodules/SemanticStatusNode/Sources/SemanticStatusNodeSecretTimeoutContext.swift @@ -0,0 +1,226 @@ +import Foundation +import UIKit +import Display +import ManagedAnimationNode + +final class SemanticStatusNodeSecretTimeoutContext: SemanticStatusNodeStateContext { + final class DrawingState: NSObject, SemanticStatusNodeStateDrawingState { + let transitionFraction: CGFloat + let value: CGFloat + let appearance: SemanticStatusNodeState.ProgressAppearance? + let iconImage: UIImage? + fileprivate let particles: [ContentParticle] + + fileprivate init(transitionFraction: CGFloat, value: CGFloat, appearance: SemanticStatusNodeState.ProgressAppearance?, iconImage: UIImage?, particles: [ContentParticle]) { + self.transitionFraction = transitionFraction + self.value = value + self.appearance = appearance + self.iconImage = iconImage + self.particles = particles + + super.init() + } + + func draw(context: CGContext, size: CGSize, foregroundColor: UIColor) { + let diameter = size.width + + let factor = diameter / 50.0 + + context.saveGState() + + if foregroundColor.alpha.isZero { + context.setBlendMode(.destinationOut) + context.setFillColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor) + context.setStrokeColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor) + } else { + context.setBlendMode(.normal) + context.setFillColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor) + context.setStrokeColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor) + } + + var progress = self.value + progress = min(1.0, progress) + let endAngle = -CGFloat.pi / 2.0 + let startAngle = CGFloat(progress) * 2.0 * CGFloat.pi + endAngle + + let lineWidth: CGFloat + if let appearance = self.appearance { + lineWidth = appearance.lineWidth + } else { + lineWidth = max(1.6, 2.25 * factor) + } + + let pathDiameter: CGFloat + if let appearance = self.appearance { + pathDiameter = diameter - lineWidth - appearance.inset * 2.0 + } else { + pathDiameter = diameter - lineWidth - 2.5 * 2.0 + } + + let path = UIBezierPath(arcCenter: CGPoint(x: diameter / 2.0, y: diameter / 2.0), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise: true) + path.lineWidth = lineWidth + path.lineCapStyle = .round + path.stroke() + + if let iconImage = self.iconImage { + context.saveGState() + let iconRect = CGRect(origin: CGPoint(), size: iconImage.size) + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + context.translateBy(x: 6.0, y: 8.0) + context.clip(to: iconRect, mask: iconImage.cgImage!) + context.fill(iconRect) + context.restoreGState() + } + + for particle in self.particles { + let size: CGFloat = 1.3 + context.setAlpha(particle.alpha) + context.fillEllipse(in: CGRect(origin: CGPoint(x: particle.position.x - size / 2.0, y: particle.position.y - size / 2.0), size: CGSize(width: size, height: size))) + } + + context.restoreGState() + } + } + + var position: Double + var duration: Double + var generationTimestamp: Double + + let appearance: SemanticStatusNodeState.ProgressAppearance? + fileprivate var particles: [ContentParticle] = [] + + private var animationNode: FireIconNode? + private var iconImage: UIImage? + + var isAnimating: Bool { + return true + } + + var requestUpdate: () -> Void = {} + + init(position: Double, duration: Double, generationTimestamp: Double, appearance: SemanticStatusNodeState.ProgressAppearance?) { + self.position = position + self.duration = duration + self.generationTimestamp = generationTimestamp + self.appearance = appearance + + self.animationNode = FireIconNode() + self.animationNode?.imageUpdated = { [weak self] image in + if let strongSelf = self { + strongSelf.iconImage = image + strongSelf.requestUpdate() + } + } + self.iconImage = self.animationNode?.image + } + + func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState { + let timestamp = CACurrentMediaTime() + let position = self.position + (timestamp - self.generationTimestamp) + let resolvedValue: CGFloat + if self.duration > 0.0 { + resolvedValue = position / self.duration + } else { + resolvedValue = 0.0 + } + + let size = CGSize(width: 44.0, height: 44.0) + + + let lineWidth: CGFloat + let lineInset: CGFloat + if let appearance = self.appearance { + lineWidth = appearance.lineWidth + lineInset = appearance.inset + } else { + lineWidth = 2.0 + lineInset = 1.0 + } + + let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + let radius: CGFloat = (size.width - lineWidth - lineInset * 2.0) * 0.5 + + let endAngle: CGFloat = -CGFloat.pi / 2.0 + 2.0 * CGFloat.pi * resolvedValue + + let v = CGPoint(x: sin(endAngle), y: -cos(endAngle)) + let c = CGPoint(x: -v.y * radius + center.x, y: v.x * radius + center.y) + + let dt: CGFloat = 1.0 / 60.0 + var removeIndices: [Int] = [] + for i in 0 ..< self.particles.count { + let currentTime = timestamp - self.particles[i].beginTime + if currentTime > self.particles[i].lifetime { + removeIndices.append(i) + } else { + let input: CGFloat = CGFloat(currentTime / self.particles[i].lifetime) + let decelerated: CGFloat = (1.0 - (1.0 - input) * (1.0 - input)) + self.particles[i].alpha = 1.0 - decelerated + + var p = self.particles[i].position + let d = self.particles[i].direction + let v = self.particles[i].velocity + p = CGPoint(x: p.x + d.x * v * dt, y: p.y + d.y * v * dt) + self.particles[i].position = p + } + } + + for i in removeIndices.reversed() { + self.particles.remove(at: i) + } + + let newParticleCount = 1 + for _ in 0 ..< newParticleCount { + let degrees: CGFloat = CGFloat(arc4random_uniform(140)) - 70.0 + let angle: CGFloat = degrees * CGFloat.pi / 180.0 + + let direction = CGPoint(x: v.x * cos(angle) - v.y * sin(angle), y: v.x * sin(angle) + v.y * cos(angle)) + let velocity = (20.0 + (CGFloat(arc4random()) / CGFloat(UINT32_MAX)) * 4.0) * 0.5 + + let lifetime = Double(0.4 + CGFloat(arc4random_uniform(100)) * 0.01) + + let particle = ContentParticle(position: c, direction: direction, velocity: velocity, alpha: 1.0, lifetime: lifetime, beginTime: timestamp) + self.particles.append(particle) + } + + return DrawingState(transitionFraction: transitionFraction, value: resolvedValue, appearance: self.appearance, iconImage: self.iconImage, particles: self.particles) + } + + func maskView() -> UIView? { + return nil + } + + func updateValue(position: Double, duration: Double, generationTimestamp: Double) { + self.position = position + self.duration = duration + self.generationTimestamp = generationTimestamp + } +} + +private struct ContentParticle { + var position: CGPoint + var direction: CGPoint + var velocity: CGFloat + var alpha: CGFloat + var lifetime: Double + var beginTime: Double + + init(position: CGPoint, direction: CGPoint, velocity: CGFloat, alpha: CGFloat, lifetime: Double, beginTime: Double) { + self.position = position + self.direction = direction + self.velocity = velocity + self.alpha = alpha + self.lifetime = lifetime + self.beginTime = beginTime + } +} + +private final class FireIconNode: ManagedAnimationNode { + init() { + super.init(size: CGSize(width: 32.0, height: 32.0)) + + self.trackTo(item: ManagedAnimationItem(source: .local("anim_flame_1"), frames: .range(startFrame: 0, endFrame: 60), duration: 1.5)) + self.trackTo(item: ManagedAnimationItem(source: .local("anim_flame_2"), frames: .range(startFrame: 0, endFrame: 120), duration: 2.0, loop: true)) + } +} diff --git a/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift b/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift index 9dd7708f93e..75b5b852b34 100644 --- a/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift +++ b/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift @@ -170,20 +170,20 @@ private final class BubbleSettingsControllerNode: ASDisplayNode, UIScrollViewDel messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) let message1 = Message(stableId: 4, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 4), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66003, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true)) let message2 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66002, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true)) let waveformBase64 = "DAAOAAkACQAGAAwADwAMABAADQAPABsAGAALAA0AGAAfABoAHgATABgAGQAYABQADAAVABEAHwANAA0ACQAWABkACQAOAAwACQAfAAAAGQAVAAAAEwATAAAACAAfAAAAHAAAABwAHwAAABcAGQAAABQADgAAABQAHwAAAB8AHwAAAAwADwAAAB8AEwAAABoAFwAAAB8AFAAAAAAAHwAAAAAAHgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAAAA=" let voiceAttributes: [TelegramMediaFileAttribute] = [.Audio(isVoice: true, duration: 23, title: nil, performer: nil, waveform: Data(base64Encoded: waveformBase64)!)] let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes) let message3 = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message3], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local), tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message3], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local), tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true)) let message4 = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message4], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message4], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true)) let width: CGFloat if case .regular = layout.metrics.widthClass { diff --git a/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift b/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift index 3569643a28e..c4190b3f5ad 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift @@ -149,7 +149,7 @@ class ForwardPrivacyChatPreviewItemNode: ListViewItemNode { let forwardInfo = MessageForwardInfo(author: item.linkEnabled ? peers[peerId] : nil, source: nil, sourceMessageId: nil, date: 0, authorSignature: item.linkEnabled ? nil : item.peerName, psaType: nil, flags: []) - let messageItem = item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: forwardInfo, author: nil, text: item.strings.Privacy_Forwards_PreviewMessageText, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])], theme: item.theme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false) + let messageItem = item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: forwardInfo, author: nil, text: item.strings.Privacy_Forwards_PreviewMessageText, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])], theme: item.theme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true) var node: ListViewItemNode? if let current = currentNode { diff --git a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift index 39af380686f..8d4f0180bee 100644 --- a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift +++ b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift @@ -997,7 +997,7 @@ func settingsSearchableItems(context: AccountContext, notificationExceptionsList allItems.append(contentsOf: profileItems) let savedMessages = SettingsSearchableItem(id: .savedMessages(0), title: strings.Settings_SavedMessages, alternate: synonyms(strings.SettingsSearch_Synonyms_SavedMessages), icon: .savedMessages, breadcrumbs: [], present: { context, _, present in - present(.push, context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: context.account.peerId), subject: nil, botStart: nil, mode: .standard(previewing: false))) + present(.push, context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: context.account.peerId), subject: nil, botStart: nil, mode: .standard(.default))) }) allItems.append(savedMessages) @@ -1057,7 +1057,7 @@ func settingsSearchableItems(context: AccountContext, notificationExceptionsList let _ = (context.engine.peers.supportPeerId() |> deliverOnMainQueue).start(next: { peerId in if let peerId = peerId { - present(.push, context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(previewing: false))) + present(.push, context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(.default))) } }) }) diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index 39b8e378552..1e9f8351ef1 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -435,20 +435,20 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) let message1 = Message(stableId: 4, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 4), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66003, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true)) let message2 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66002, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true)) let waveformBase64 = "DAAOAAkACQAGAAwADwAMABAADQAPABsAGAALAA0AGAAfABoAHgATABgAGQAYABQADAAVABEAHwANAA0ACQAWABkACQAOAAwACQAfAAAAGQAVAAAAEwATAAAACAAfAAAAHAAAABwAHwAAABcAGQAAABQADgAAABQAHwAAAB8AHwAAAAwADwAAAB8AEwAAABoAFwAAAB8AFAAAAAAAHwAAAAAAHgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAAAA=" let voiceAttributes: [TelegramMediaFileAttribute] = [.Audio(isVoice: true, duration: 23, title: nil, performer: nil, waveform: Data(base64Encoded: waveformBase64)!)] let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes) let message3 = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message3], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local), tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message3], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local), tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true)) let message4 = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message4], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message4], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true)) let width: CGFloat if case .regular = layout.metrics.widthClass { diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index 9dd1e3c9647..c258319120c 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -619,7 +619,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { sampleMessages.append(message8) items = sampleMessages.reversed().map { message in - self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message], theme: self.previewTheme, strings: self.presentationData.strings, wallpaper: self.wallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: !message.media.isEmpty ? FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local) : nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.wallpaperNode, availableReactions: nil, accountPeer: nil, isCentered: false) + self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message], theme: self.previewTheme, strings: self.presentationData.strings, wallpaper: self.wallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: !message.media.isEmpty ? FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local) : nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.wallpaperNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true) } let width: CGFloat diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift index 3e9d9cdb35a..6de6a2ef22c 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift @@ -26,10 +26,10 @@ private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selec accentColor = UIColor(rgb: 0x999999) } context.setStrokeColor(accentColor.cgColor) - lineWidth = 2.0 + lineWidth = 2.0 - UIScreenPixel } else { context.setStrokeColor(theme.list.disclosureArrowColor.withAlphaComponent(0.4).cgColor) - lineWidth = 1.0 + lineWidth = 1.0 - UIScreenPixel } if bordered || selected { @@ -95,10 +95,13 @@ class ThemeSettingsAppIconItem: ListViewItem, ItemListItem { } } +private let badgeSize = CGSize(width: 24.0, height: 24.0) +private let badgeStrokeSize: CGFloat = 2.0 + private final class ThemeSettingsAppIconNode : ASDisplayNode { private let iconNode: ASImageNode private let overlayNode: ASImageNode - private let lockNode: ASImageNode + fileprivate let lockNode: ASImageNode private let textNode: ImmediateTextNode private var action: (() -> Void)? @@ -108,11 +111,11 @@ private final class ThemeSettingsAppIconNode : ASDisplayNode { override init() { self.iconNode = ASImageNode() - self.iconNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 62.0, height: 62.0)) + self.iconNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 63.0, height: 63.0)) self.iconNode.isLayerBacked = true self.overlayNode = ASImageNode() - self.overlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 62.0, height: 62.0)) + self.overlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 63.0, height: 63.0)) self.overlayNode.isLayerBacked = true self.lockNode = ASImageNode() @@ -141,7 +144,7 @@ private final class ThemeSettingsAppIconNode : ASDisplayNode { self.iconNode.image = icon self.textNode.attributedText = title self.overlayNode.image = generateBorderImage(theme: theme, bordered: bordered, selected: selected) - self.lockNode.image = locked ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: color) : nil + self.lockNode.isHidden = !locked self.action = { action() } @@ -172,26 +175,25 @@ private final class ThemeSettingsAppIconNode : ASDisplayNode { super.layout() let bounds = self.bounds + let iconSize = CGSize(width: 63.0, height: 63.0) - self.iconNode.frame = CGRect(origin: CGPoint(x: 9.0, y: 14.0), size: CGSize(width: 62.0, height: 62.0)) - self.overlayNode.frame = CGRect(origin: CGPoint(x: 9.0, y: 14.0), size: CGSize(width: 62.0, height: 62.0)) + self.iconNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - iconSize.width) / 2.0), y: 13.0), size: iconSize) + self.overlayNode.frame = self.iconNode.frame let textSize = self.textNode.updateLayout(bounds.size) - var textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - textSize.width)) / 2.0, y: 87.0), size: textSize) - if self.locked { - textFrame = textFrame.offsetBy(dx: 5.0, dy: 0.0) - } + let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - textSize.width) / 2.0), y: 81.0), size: textSize) self.textNode.frame = textFrame - self.lockNode.frame = CGRect(x: self.textNode.frame.minX - 10.0, y: 90.0, width: 6.0, height: 8.0) + let badgeFinalSize = CGSize(width: badgeSize.width + badgeStrokeSize * 2.0, height: badgeSize.height + badgeStrokeSize * 2.0) + self.lockNode.frame = CGRect(x: bounds.width - 24.0, y: 4.0, width: badgeFinalSize.width, height: badgeFinalSize.height) - self.activateAreaNode.frame = self.bounds + self.activateAreaNode.frame = bounds } } private let textFont = Font.regular(12.0) -private let selectedTextFont = Font.bold(12.0) +private let selectedTextFont = Font.medium(12.0) class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode { private let backgroundNode: ASDisplayNode @@ -199,7 +201,7 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode { private let bottomStripeNode: ASDisplayNode private let maskNode: ASImageNode - private let scrollNode: ASScrollNode + private let containerNode: ASDisplayNode private var nodes: [ThemeSettingsAppIconNode] = [] private var item: ThemeSettingsAppIconItem? @@ -209,6 +211,8 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode { return self.item?.tag } + private var lockImage: UIImage? + init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true @@ -221,35 +225,23 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode { self.maskNode = ASImageNode() - self.scrollNode = ASScrollNode() + self.containerNode = ASDisplayNode() super.init(layerBacked: false, dynamicBounce: false) - self.addSubnode(self.scrollNode) - } - - override func didLoad() { - super.didLoad() - self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true - self.scrollNode.view.showsHorizontalScrollIndicator = false - } - - private func scrollToNode(_ node: ThemeSettingsAppIconNode, animated: Bool) { - let bounds = self.scrollNode.view.bounds - let frame = node.frame.insetBy(dx: -48.0, dy: 0.0) - - if frame.minX < bounds.minX || frame.maxX > bounds.maxX { - self.scrollNode.view.scrollRectToVisible(frame, animated: animated) - } + self.addSubnode(self.containerNode) } - + func asyncLayout() -> (_ item: ThemeSettingsAppIconItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { return { item, params, neighbors in let contentSize: CGSize let insets: UIEdgeInsets let separatorHeight = UIScreenPixel - contentSize = CGSize(width: params.width, height: 116.0) + let nodeSize = CGSize(width: 74.0, height: 102.0) + let height: CGFloat = nodeSize.height * ceil(CGFloat(item.icons.count) / 4.0) + 12.0 + + contentSize = CGSize(width: params.width, height: height) insets = itemListNeighborsGroupedInsets(neighbors, params) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) @@ -257,10 +249,32 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode { return (layout, { [weak self] in if let strongSelf = self { + let previousItem = strongSelf.item strongSelf.item = item strongSelf.layoutParams = params - strongSelf.scrollNode.view.contentInset = UIEdgeInsets() + if previousItem?.theme !== item.theme { + strongSelf.lockImage = generateImage(CGSize(width: badgeSize.width + badgeStrokeSize, height: badgeSize.height + badgeStrokeSize), contextGenerator: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + context.setFillColor(item.theme.list.itemBlocksBackgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: .zero, size: size)) + + context.addEllipse(in: CGRect(origin: .zero, size: size).insetBy(dx: badgeStrokeSize, dy: badgeStrokeSize)) + context.clip() + + var locations: [CGFloat] = [0.0, 1.0] + let colors: [CGColor] = [UIColor(rgb: 0x9076FF).cgColor, UIColor(rgb: 0xB86DEA).cgColor] + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions()) + + if let icon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: .white) { + context.draw(icon.cgImage!, in: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - icon.size.width) / 2.0), y: floorToScreenPixels((size.height - icon.size.height) / 2.0)), size: icon.size), byTiling: false) + } + }) + } + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor @@ -309,33 +323,37 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode { strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) - strongSelf.scrollNode.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 2.0), size: CGSize(width: layoutSize.width - params.leftInset - params.rightInset, height: layoutSize.height)) + strongSelf.containerNode.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 2.0), size: CGSize(width: layoutSize.width - params.leftInset - params.rightInset, height: layoutSize.height)) - let nodeInset: CGFloat = 4.0 - let nodeSize = CGSize(width: 80.0, height: 112.0) - var nodeOffset = nodeInset + let sideInset: CGFloat = 8.0 + let spacing: CGFloat = floorToScreenPixels((params.width - sideInset * 2.0 - params.leftInset - params.rightInset - nodeSize.width * 4.0) / 3.0) + let verticalSpacing: CGFloat = 0.0 - var updated = false - var selectedNode: ThemeSettingsAppIconNode? + var x: CGFloat = sideInset + var y: CGFloat = 0.0 var i = 0 for icon in item.icons { + if i > 0 && i % 4 == 0 { + x = sideInset + y += nodeSize.height + verticalSpacing + } + let nodeFrame = CGRect(x: x, y: y, width: nodeSize.width, height: nodeSize.height) + x += nodeSize.width + spacing + let imageNode: ThemeSettingsAppIconNode if strongSelf.nodes.count > i { imageNode = strongSelf.nodes[i] } else { imageNode = ThemeSettingsAppIconNode() strongSelf.nodes.append(imageNode) - strongSelf.scrollNode.addSubnode(imageNode) - updated = true + strongSelf.containerNode.addSubnode(imageNode) } + imageNode.lockNode.image = strongSelf.lockImage if let image = UIImage(named: icon.imageName, in: getAppBundle(), compatibleWith: nil) { let selected = icon.name == item.currentIconName - if selected { - selectedNode = imageNode - } - + var name = "Icon" var bordered = true switch icon.name { @@ -382,30 +400,15 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode { name = icon.name } - imageNode.setup(theme: item.theme, icon: image, title: NSAttributedString(string: name, font: selected ? selectedTextFont : textFont, textColor: selected ? item.theme.list.itemAccentColor : item.theme.list.itemPrimaryTextColor, paragraphAlignment: .center), locked: !item.isPremium && icon.isPremium, color: item.theme.list.itemPrimaryTextColor, bordered: bordered, selected: selected, action: { [weak self, weak imageNode] in + imageNode.setup(theme: item.theme, icon: image, title: NSAttributedString(string: name, font: selected ? selectedTextFont : textFont, textColor: selected ? item.theme.list.itemAccentColor : item.theme.list.itemPrimaryTextColor, paragraphAlignment: .center), locked: !item.isPremium && icon.isPremium, color: item.theme.list.itemPrimaryTextColor, bordered: bordered, selected: selected, action: { item.updated(icon) - if let imageNode = imageNode { - self?.scrollToNode(imageNode, animated: true) - } }) } - imageNode.frame = CGRect(origin: CGPoint(x: nodeOffset, y: 0.0), size: nodeSize) - nodeOffset += nodeSize.width + 15.0 + imageNode.frame = nodeFrame i += 1 } - - if let lastNode = strongSelf.nodes.last { - let contentSize = CGSize(width: lastNode.frame.maxX + nodeInset, height: strongSelf.scrollNode.frame.height) - if strongSelf.scrollNode.view.contentSize != contentSize { - strongSelf.scrollNode.view.contentSize = contentSize - } - } - - if updated, let selectedNode = selectedNode { - strongSelf.scrollToNode(selectedNode, animated: false) - } } }) } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift index ee60dca8a3d..8e4c8921022 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift @@ -168,7 +168,7 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode { } let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: messageItem.outgoing ? otherPeerId : peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: messageItem.outgoing ? [] : [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: messageItem.outgoing ? TelegramUser(id: otherPeerId, accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) : nil, text: messageItem.text, attributes: messageItem.reply != nil ? [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false)] : [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false)) + items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true)) } var nodes: [ListViewItemNode] = [] diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index d67b8c88797..403492a7e3b 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -1147,7 +1147,18 @@ public func channelStatsController(context: AccountContext, updatedPresentationD } } - presentImpl?(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false })) + presentImpl?(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in + if savedMessages, action == .info { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { peer in + guard let peer else { + return + } + navigateToChatImpl?(peer) + }) + } + return false + })) }) } shareController.actionCompleted = { diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index d3776d015d4..d272aeb8031 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -6,6 +6,7 @@ public enum Api { public enum channels {} public enum chatlists {} public enum contacts {} + public enum feed {} public enum help {} public enum messages {} public enum payments {} @@ -26,6 +27,7 @@ public enum Api { public enum channels {} public enum chatlists {} public enum contacts {} + public enum feed {} public enum folders {} public enum help {} public enum langpack {} @@ -252,6 +254,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1103040667] = { return Api.ExportedContactToken.parse_exportedContactToken($0) } dict[1571494644] = { return Api.ExportedMessageLink.parse_exportedMessageLink($0) } dict[1070138683] = { return Api.ExportedStoryLink.parse_exportedStoryLink($0) } + dict[1348066419] = { return Api.FeedPosition.parse_feedPosition($0) } dict[-207944868] = { return Api.FileHash.parse_fileHash($0) } dict[-11252123] = { return Api.Folder.parse_folder($0) } dict[-373643672] = { return Api.FolderPeer.parse_folderPeer($0) } @@ -472,7 +475,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[340088945] = { return Api.MediaArea.parse_mediaAreaSuggestedReaction($0) } dict[-1098720356] = { return Api.MediaArea.parse_mediaAreaVenue($0) } dict[64088654] = { return Api.MediaAreaCoordinates.parse_mediaAreaCoordinates($0) } - dict[940666592] = { return Api.Message.parse_message($0) } + dict[1992213009] = { return Api.Message.parse_message($0) } dict[-1868117372] = { return Api.Message.parse_messageEmpty($0) } dict[721967202] = { return Api.Message.parse_messageService($0) } dict[-988359047] = { return Api.MessageAction.parse_messageActionBotAllowed($0) } @@ -539,7 +542,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1859134776] = { return Api.MessageEntity.parse_messageEntityUrl($0) } dict[-297296796] = { return Api.MessageExtendedMedia.parse_messageExtendedMedia($0) } dict[-1386050360] = { return Api.MessageExtendedMedia.parse_messageExtendedMediaPreview($0) } - dict[1601666510] = { return Api.MessageFwdHeader.parse_messageFwdHeader($0) } + dict[1313731771] = { return Api.MessageFwdHeader.parse_messageFwdHeader($0) } dict[1882335561] = { return Api.MessageMedia.parse_messageMediaContact($0) } dict[1065280907] = { return Api.MessageMedia.parse_messageMediaDice($0) } dict[1291114285] = { return Api.MessageMedia.parse_messageMediaDocument($0) } @@ -748,6 +751,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1054465340] = { return Api.RichText.parse_textUnderline($0) } dict[1009288385] = { return Api.RichText.parse_textUrl($0) } dict[289586518] = { return Api.SavedContact.parse_savedPhoneContact($0) } + dict[-1115174036] = { return Api.SavedDialog.parse_savedDialog($0) } dict[-911191137] = { return Api.SearchResultsCalendarPeriod.parse_searchResultsCalendarPeriod($0) } dict[2137295719] = { return Api.SearchResultsPosition.parse_searchResultPosition($0) } dict[871426631] = { return Api.SecureCredentialsEncrypted.parse_secureCredentialsEncrypted($0) } @@ -939,6 +943,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1538885128] = { return Api.Update.parse_updatePinnedChannelMessages($0) } dict[-99664734] = { return Api.Update.parse_updatePinnedDialogs($0) } dict[-309990731] = { return Api.Update.parse_updatePinnedMessages($0) } + dict[1751942566] = { return Api.Update.parse_updatePinnedSavedDialogs($0) } dict[-298113238] = { return Api.Update.parse_updatePrivacy($0) } dict[861169551] = { return Api.Update.parse_updatePtsChanged($0) } dict[-693004986] = { return Api.Update.parse_updateReadChannelDiscussionInbox($0) } @@ -947,6 +952,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1218471511] = { return Api.Update.parse_updateReadChannelOutbox($0) } dict[-78886548] = { return Api.Update.parse_updateReadFeaturedEmojiStickers($0) } dict[1461528386] = { return Api.Update.parse_updateReadFeaturedStickers($0) } + dict[1951948721] = { return Api.Update.parse_updateReadFeed($0) } dict[-1667805217] = { return Api.Update.parse_updateReadHistoryInbox($0) } dict[791617983] = { return Api.Update.parse_updateReadHistoryOutbox($0) } dict[-131960447] = { return Api.Update.parse_updateReadMessagesContents($0) } @@ -954,6 +960,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[821314523] = { return Api.Update.parse_updateRecentEmojiStatuses($0) } dict[1870160884] = { return Api.Update.parse_updateRecentReactions($0) } dict[-1706939360] = { return Api.Update.parse_updateRecentStickers($0) } + dict[-1364222348] = { return Api.Update.parse_updateSavedDialogPinned($0) } dict[-1821035490] = { return Api.Update.parse_updateSavedGifs($0) } dict[1960361625] = { return Api.Update.parse_updateSavedRingtones($0) } dict[2103604867] = { return Api.Update.parse_updateSentStoryReaction($0) } @@ -1085,6 +1092,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1891070632] = { return Api.contacts.TopPeers.parse_topPeers($0) } dict[-1255369827] = { return Api.contacts.TopPeers.parse_topPeersDisabled($0) } dict[-567906571] = { return Api.contacts.TopPeers.parse_topPeersNotModified($0) } + dict[-587770695] = { return Api.feed.FeedMessages.parse_feedMessages($0) } + dict[-619039485] = { return Api.feed.FeedMessages.parse_feedMessagesNotModified($0) } dict[-585598930] = { return Api.help.AppConfig.parse_appConfig($0) } dict[2094949405] = { return Api.help.AppConfig.parse_appConfigNotModified($0) } dict[-860107216] = { return Api.help.AppUpdate.parse_appUpdate($0) } @@ -1166,6 +1175,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1334846497] = { return Api.messages.Reactions.parse_reactionsNotModified($0) } dict[-1999405994] = { return Api.messages.RecentStickers.parse_recentStickers($0) } dict[186120336] = { return Api.messages.RecentStickers.parse_recentStickersNotModified($0) } + dict[-130358751] = { return Api.messages.SavedDialogs.parse_savedDialogs($0) } + dict[-1071681560] = { return Api.messages.SavedDialogs.parse_savedDialogsNotModified($0) } + dict[1153080793] = { return Api.messages.SavedDialogs.parse_savedDialogsSlice($0) } dict[-2069878259] = { return Api.messages.SavedGifs.parse_savedGifs($0) } dict[-402498398] = { return Api.messages.SavedGifs.parse_savedGifsNotModified($0) } dict[-398136321] = { return Api.messages.SearchCounter.parse_searchCounter($0) } @@ -1263,7 +1275,7 @@ public extension Api { return parser(reader) } else { - telegramApiLog("Type constructor \(String(signature, radix: 16, uppercase: false)) not found") + telegramApiLog("Type constructor \(String(UInt32(bitPattern: signature), radix: 16, uppercase: false)) not found") return nil } } @@ -1457,6 +1469,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.ExportedStoryLink: _1.serialize(buffer, boxed) + case let _1 as Api.FeedPosition: + _1.serialize(buffer, boxed) case let _1 as Api.FileHash: _1.serialize(buffer, boxed) case let _1 as Api.Folder: @@ -1745,6 +1759,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.SavedContact: _1.serialize(buffer, boxed) + case let _1 as Api.SavedDialog: + _1.serialize(buffer, boxed) case let _1 as Api.SearchResultsCalendarPeriod: _1.serialize(buffer, boxed) case let _1 as Api.SearchResultsPosition: @@ -1951,6 +1967,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.contacts.TopPeers: _1.serialize(buffer, boxed) + case let _1 as Api.feed.FeedMessages: + _1.serialize(buffer, boxed) case let _1 as Api.help.AppConfig: _1.serialize(buffer, boxed) case let _1 as Api.help.AppUpdate: @@ -2061,6 +2079,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.messages.RecentStickers: _1.serialize(buffer, boxed) + case let _1 as Api.messages.SavedDialogs: + _1.serialize(buffer, boxed) case let _1 as Api.messages.SavedGifs: _1.serialize(buffer, boxed) case let _1 as Api.messages.SearchCounter: diff --git a/submodules/TelegramApi/Sources/Api12.swift b/submodules/TelegramApi/Sources/Api12.swift index d5155f12fbf..c1740387cb6 100644 --- a/submodules/TelegramApi/Sources/Api12.swift +++ b/submodules/TelegramApi/Sources/Api12.swift @@ -424,20 +424,21 @@ public extension Api { } public extension Api { indirect enum Message: TypeConstructorDescription { - case message(flags: Int32, id: Int32, fromId: Api.Peer?, peerId: Api.Peer, fwdFrom: Api.MessageFwdHeader?, viaBotId: Int64?, replyTo: Api.MessageReplyHeader?, date: Int32, message: String, media: Api.MessageMedia?, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?, views: Int32?, forwards: Int32?, replies: Api.MessageReplies?, editDate: Int32?, postAuthor: String?, groupedId: Int64?, reactions: Api.MessageReactions?, restrictionReason: [Api.RestrictionReason]?, ttlPeriod: Int32?) + case message(flags: Int32, id: Int32, fromId: Api.Peer?, peerId: Api.Peer, savedPeerId: Api.Peer?, fwdFrom: Api.MessageFwdHeader?, viaBotId: Int64?, replyTo: Api.MessageReplyHeader?, date: Int32, message: String, media: Api.MessageMedia?, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?, views: Int32?, forwards: Int32?, replies: Api.MessageReplies?, editDate: Int32?, postAuthor: String?, groupedId: Int64?, reactions: Api.MessageReactions?, restrictionReason: [Api.RestrictionReason]?, ttlPeriod: Int32?) case messageEmpty(flags: Int32, id: Int32, peerId: Api.Peer?) case messageService(flags: Int32, id: Int32, fromId: Api.Peer?, peerId: Api.Peer, replyTo: Api.MessageReplyHeader?, date: Int32, action: Api.MessageAction, ttlPeriod: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .message(let flags, let id, let fromId, let peerId, let fwdFrom, let viaBotId, let replyTo, let date, let message, let media, let replyMarkup, let entities, let views, let forwards, let replies, let editDate, let postAuthor, let groupedId, let reactions, let restrictionReason, let ttlPeriod): + case .message(let flags, let id, let fromId, let peerId, let savedPeerId, let fwdFrom, let viaBotId, let replyTo, let date, let message, let media, let replyMarkup, let entities, let views, let forwards, let replies, let editDate, let postAuthor, let groupedId, let reactions, let restrictionReason, let ttlPeriod): if boxed { - buffer.appendInt32(940666592) + buffer.appendInt32(1992213009) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(id, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 8) != 0 {fromId!.serialize(buffer, true)} peerId.serialize(buffer, true) + if Int(flags) & Int(1 << 28) != 0 {savedPeerId!.serialize(buffer, true)} if Int(flags) & Int(1 << 2) != 0 {fwdFrom!.serialize(buffer, true)} if Int(flags) & Int(1 << 11) != 0 {serializeInt64(viaBotId!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 3) != 0 {replyTo!.serialize(buffer, true)} @@ -490,8 +491,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .message(let flags, let id, let fromId, let peerId, let fwdFrom, let viaBotId, let replyTo, let date, let message, let media, let replyMarkup, let entities, let views, let forwards, let replies, let editDate, let postAuthor, let groupedId, let reactions, let restrictionReason, let ttlPeriod): - return ("message", [("flags", flags as Any), ("id", id as Any), ("fromId", fromId as Any), ("peerId", peerId as Any), ("fwdFrom", fwdFrom as Any), ("viaBotId", viaBotId as Any), ("replyTo", replyTo as Any), ("date", date as Any), ("message", message as Any), ("media", media as Any), ("replyMarkup", replyMarkup as Any), ("entities", entities as Any), ("views", views as Any), ("forwards", forwards as Any), ("replies", replies as Any), ("editDate", editDate as Any), ("postAuthor", postAuthor as Any), ("groupedId", groupedId as Any), ("reactions", reactions as Any), ("restrictionReason", restrictionReason as Any), ("ttlPeriod", ttlPeriod as Any)]) + case .message(let flags, let id, let fromId, let peerId, let savedPeerId, let fwdFrom, let viaBotId, let replyTo, let date, let message, let media, let replyMarkup, let entities, let views, let forwards, let replies, let editDate, let postAuthor, let groupedId, let reactions, let restrictionReason, let ttlPeriod): + return ("message", [("flags", flags as Any), ("id", id as Any), ("fromId", fromId as Any), ("peerId", peerId as Any), ("savedPeerId", savedPeerId as Any), ("fwdFrom", fwdFrom as Any), ("viaBotId", viaBotId as Any), ("replyTo", replyTo as Any), ("date", date as Any), ("message", message as Any), ("media", media as Any), ("replyMarkup", replyMarkup as Any), ("entities", entities as Any), ("views", views as Any), ("forwards", forwards as Any), ("replies", replies as Any), ("editDate", editDate as Any), ("postAuthor", postAuthor as Any), ("groupedId", groupedId as Any), ("reactions", reactions as Any), ("restrictionReason", restrictionReason as Any), ("ttlPeriod", ttlPeriod as Any)]) case .messageEmpty(let flags, let id, let peerId): return ("messageEmpty", [("flags", flags as Any), ("id", id as Any), ("peerId", peerId as Any)]) case .messageService(let flags, let id, let fromId, let peerId, let replyTo, let date, let action, let ttlPeriod): @@ -512,79 +513,84 @@ public extension Api { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.Peer } - var _5: Api.MessageFwdHeader? + var _5: Api.Peer? + if Int(_1!) & Int(1 << 28) != 0 {if let signature = reader.readInt32() { + _5 = Api.parse(reader, signature: signature) as? Api.Peer + } } + var _6: Api.MessageFwdHeader? if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { - _5 = Api.parse(reader, signature: signature) as? Api.MessageFwdHeader + _6 = Api.parse(reader, signature: signature) as? Api.MessageFwdHeader } } - var _6: Int64? - if Int(_1!) & Int(1 << 11) != 0 {_6 = reader.readInt64() } - var _7: Api.MessageReplyHeader? + var _7: Int64? + if Int(_1!) & Int(1 << 11) != 0 {_7 = reader.readInt64() } + var _8: Api.MessageReplyHeader? if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() { - _7 = Api.parse(reader, signature: signature) as? Api.MessageReplyHeader + _8 = Api.parse(reader, signature: signature) as? Api.MessageReplyHeader } } - var _8: Int32? - _8 = reader.readInt32() - var _9: String? - _9 = parseString(reader) - var _10: Api.MessageMedia? + var _9: Int32? + _9 = reader.readInt32() + var _10: String? + _10 = parseString(reader) + var _11: Api.MessageMedia? if Int(_1!) & Int(1 << 9) != 0 {if let signature = reader.readInt32() { - _10 = Api.parse(reader, signature: signature) as? Api.MessageMedia + _11 = Api.parse(reader, signature: signature) as? Api.MessageMedia } } - var _11: Api.ReplyMarkup? + var _12: Api.ReplyMarkup? if Int(_1!) & Int(1 << 6) != 0 {if let signature = reader.readInt32() { - _11 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup + _12 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup } } - var _12: [Api.MessageEntity]? + var _13: [Api.MessageEntity]? if Int(_1!) & Int(1 << 7) != 0 {if let _ = reader.readInt32() { - _12 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) + _13 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) } } - var _13: Int32? - if Int(_1!) & Int(1 << 10) != 0 {_13 = reader.readInt32() } var _14: Int32? if Int(_1!) & Int(1 << 10) != 0 {_14 = reader.readInt32() } - var _15: Api.MessageReplies? + var _15: Int32? + if Int(_1!) & Int(1 << 10) != 0 {_15 = reader.readInt32() } + var _16: Api.MessageReplies? if Int(_1!) & Int(1 << 23) != 0 {if let signature = reader.readInt32() { - _15 = Api.parse(reader, signature: signature) as? Api.MessageReplies + _16 = Api.parse(reader, signature: signature) as? Api.MessageReplies } } - var _16: Int32? - if Int(_1!) & Int(1 << 15) != 0 {_16 = reader.readInt32() } - var _17: String? - if Int(_1!) & Int(1 << 16) != 0 {_17 = parseString(reader) } - var _18: Int64? - if Int(_1!) & Int(1 << 17) != 0 {_18 = reader.readInt64() } - var _19: Api.MessageReactions? + var _17: Int32? + if Int(_1!) & Int(1 << 15) != 0 {_17 = reader.readInt32() } + var _18: String? + if Int(_1!) & Int(1 << 16) != 0 {_18 = parseString(reader) } + var _19: Int64? + if Int(_1!) & Int(1 << 17) != 0 {_19 = reader.readInt64() } + var _20: Api.MessageReactions? if Int(_1!) & Int(1 << 20) != 0 {if let signature = reader.readInt32() { - _19 = Api.parse(reader, signature: signature) as? Api.MessageReactions + _20 = Api.parse(reader, signature: signature) as? Api.MessageReactions } } - var _20: [Api.RestrictionReason]? + var _21: [Api.RestrictionReason]? if Int(_1!) & Int(1 << 22) != 0 {if let _ = reader.readInt32() { - _20 = Api.parseVector(reader, elementSignature: 0, elementType: Api.RestrictionReason.self) + _21 = Api.parseVector(reader, elementSignature: 0, elementType: Api.RestrictionReason.self) } } - var _21: Int32? - if Int(_1!) & Int(1 << 25) != 0 {_21 = reader.readInt32() } + var _22: Int32? + if Int(_1!) & Int(1 << 25) != 0 {_22 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = (Int(_1!) & Int(1 << 8) == 0) || _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 11) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 3) == 0) || _7 != nil - let _c8 = _8 != nil + let _c5 = (Int(_1!) & Int(1 << 28) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil + let _c7 = (Int(_1!) & Int(1 << 11) == 0) || _7 != nil + let _c8 = (Int(_1!) & Int(1 << 3) == 0) || _8 != nil let _c9 = _9 != nil - let _c10 = (Int(_1!) & Int(1 << 9) == 0) || _10 != nil - let _c11 = (Int(_1!) & Int(1 << 6) == 0) || _11 != nil - let _c12 = (Int(_1!) & Int(1 << 7) == 0) || _12 != nil - let _c13 = (Int(_1!) & Int(1 << 10) == 0) || _13 != nil + let _c10 = _10 != nil + let _c11 = (Int(_1!) & Int(1 << 9) == 0) || _11 != nil + let _c12 = (Int(_1!) & Int(1 << 6) == 0) || _12 != nil + let _c13 = (Int(_1!) & Int(1 << 7) == 0) || _13 != nil let _c14 = (Int(_1!) & Int(1 << 10) == 0) || _14 != nil - let _c15 = (Int(_1!) & Int(1 << 23) == 0) || _15 != nil - let _c16 = (Int(_1!) & Int(1 << 15) == 0) || _16 != nil - let _c17 = (Int(_1!) & Int(1 << 16) == 0) || _17 != nil - let _c18 = (Int(_1!) & Int(1 << 17) == 0) || _18 != nil - let _c19 = (Int(_1!) & Int(1 << 20) == 0) || _19 != nil - let _c20 = (Int(_1!) & Int(1 << 22) == 0) || _20 != nil - let _c21 = (Int(_1!) & Int(1 << 25) == 0) || _21 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 { - return Api.Message.message(flags: _1!, id: _2!, fromId: _3, peerId: _4!, fwdFrom: _5, viaBotId: _6, replyTo: _7, date: _8!, message: _9!, media: _10, replyMarkup: _11, entities: _12, views: _13, forwards: _14, replies: _15, editDate: _16, postAuthor: _17, groupedId: _18, reactions: _19, restrictionReason: _20, ttlPeriod: _21) + let _c15 = (Int(_1!) & Int(1 << 10) == 0) || _15 != nil + let _c16 = (Int(_1!) & Int(1 << 23) == 0) || _16 != nil + let _c17 = (Int(_1!) & Int(1 << 15) == 0) || _17 != nil + let _c18 = (Int(_1!) & Int(1 << 16) == 0) || _18 != nil + let _c19 = (Int(_1!) & Int(1 << 17) == 0) || _19 != nil + let _c20 = (Int(_1!) & Int(1 << 20) == 0) || _20 != nil + let _c21 = (Int(_1!) & Int(1 << 22) == 0) || _21 != nil + let _c22 = (Int(_1!) & Int(1 << 25) == 0) || _22 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 { + return Api.Message.message(flags: _1!, id: _2!, fromId: _3, peerId: _4!, savedPeerId: _5, fwdFrom: _6, viaBotId: _7, replyTo: _8, date: _9!, message: _10!, media: _11, replyMarkup: _12, entities: _13, views: _14, forwards: _15, replies: _16, editDate: _17, postAuthor: _18, groupedId: _19, reactions: _20, restrictionReason: _21, ttlPeriod: _22) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api13.swift b/submodules/TelegramApi/Sources/Api13.swift index faeac515b0a..f82c179069d 100644 --- a/submodules/TelegramApi/Sources/Api13.swift +++ b/submodules/TelegramApi/Sources/Api13.swift @@ -618,13 +618,13 @@ public extension Api { } public extension Api { enum MessageFwdHeader: TypeConstructorDescription { - case messageFwdHeader(flags: Int32, fromId: Api.Peer?, fromName: String?, date: Int32, channelPost: Int32?, postAuthor: String?, savedFromPeer: Api.Peer?, savedFromMsgId: Int32?, psaType: String?) + case messageFwdHeader(flags: Int32, fromId: Api.Peer?, fromName: String?, date: Int32, channelPost: Int32?, postAuthor: String?, savedFromPeer: Api.Peer?, savedFromMsgId: Int32?, savedFromId: Api.Peer?, savedFromName: String?, savedDate: Int32?, psaType: String?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .messageFwdHeader(let flags, let fromId, let fromName, let date, let channelPost, let postAuthor, let savedFromPeer, let savedFromMsgId, let psaType): + case .messageFwdHeader(let flags, let fromId, let fromName, let date, let channelPost, let postAuthor, let savedFromPeer, let savedFromMsgId, let savedFromId, let savedFromName, let savedDate, let psaType): if boxed { - buffer.appendInt32(1601666510) + buffer.appendInt32(1313731771) } serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {fromId!.serialize(buffer, true)} @@ -634,6 +634,9 @@ public extension Api { if Int(flags) & Int(1 << 3) != 0 {serializeString(postAuthor!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 4) != 0 {savedFromPeer!.serialize(buffer, true)} if Int(flags) & Int(1 << 4) != 0 {serializeInt32(savedFromMsgId!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 8) != 0 {savedFromId!.serialize(buffer, true)} + if Int(flags) & Int(1 << 9) != 0 {serializeString(savedFromName!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 10) != 0 {serializeInt32(savedDate!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 6) != 0 {serializeString(psaType!, buffer: buffer, boxed: false)} break } @@ -641,8 +644,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .messageFwdHeader(let flags, let fromId, let fromName, let date, let channelPost, let postAuthor, let savedFromPeer, let savedFromMsgId, let psaType): - return ("messageFwdHeader", [("flags", flags as Any), ("fromId", fromId as Any), ("fromName", fromName as Any), ("date", date as Any), ("channelPost", channelPost as Any), ("postAuthor", postAuthor as Any), ("savedFromPeer", savedFromPeer as Any), ("savedFromMsgId", savedFromMsgId as Any), ("psaType", psaType as Any)]) + case .messageFwdHeader(let flags, let fromId, let fromName, let date, let channelPost, let postAuthor, let savedFromPeer, let savedFromMsgId, let savedFromId, let savedFromName, let savedDate, let psaType): + return ("messageFwdHeader", [("flags", flags as Any), ("fromId", fromId as Any), ("fromName", fromName as Any), ("date", date as Any), ("channelPost", channelPost as Any), ("postAuthor", postAuthor as Any), ("savedFromPeer", savedFromPeer as Any), ("savedFromMsgId", savedFromMsgId as Any), ("savedFromId", savedFromId as Any), ("savedFromName", savedFromName as Any), ("savedDate", savedDate as Any), ("psaType", psaType as Any)]) } } @@ -667,8 +670,16 @@ public extension Api { } } var _8: Int32? if Int(_1!) & Int(1 << 4) != 0 {_8 = reader.readInt32() } - var _9: String? - if Int(_1!) & Int(1 << 6) != 0 {_9 = parseString(reader) } + var _9: Api.Peer? + if Int(_1!) & Int(1 << 8) != 0 {if let signature = reader.readInt32() { + _9 = Api.parse(reader, signature: signature) as? Api.Peer + } } + var _10: String? + if Int(_1!) & Int(1 << 9) != 0 {_10 = parseString(reader) } + var _11: Int32? + if Int(_1!) & Int(1 << 10) != 0 {_11 = reader.readInt32() } + var _12: String? + if Int(_1!) & Int(1 << 6) != 0 {_12 = parseString(reader) } let _c1 = _1 != nil let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil let _c3 = (Int(_1!) & Int(1 << 5) == 0) || _3 != nil @@ -677,9 +688,12 @@ public extension Api { let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil let _c8 = (Int(_1!) & Int(1 << 4) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 6) == 0) || _9 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { - return Api.MessageFwdHeader.messageFwdHeader(flags: _1!, fromId: _2, fromName: _3, date: _4!, channelPost: _5, postAuthor: _6, savedFromPeer: _7, savedFromMsgId: _8, psaType: _9) + let _c9 = (Int(_1!) & Int(1 << 8) == 0) || _9 != nil + let _c10 = (Int(_1!) & Int(1 << 9) == 0) || _10 != nil + let _c11 = (Int(_1!) & Int(1 << 10) == 0) || _11 != nil + let _c12 = (Int(_1!) & Int(1 << 6) == 0) || _12 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 { + return Api.MessageFwdHeader.messageFwdHeader(flags: _1!, fromId: _2, fromName: _3, date: _4!, channelPost: _5, postAuthor: _6, savedFromPeer: _7, savedFromMsgId: _8, savedFromId: _9, savedFromName: _10, savedDate: _11, psaType: _12) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api19.swift b/submodules/TelegramApi/Sources/Api19.swift index 3b54211e639..61fa36ecb84 100644 --- a/submodules/TelegramApi/Sources/Api19.swift +++ b/submodules/TelegramApi/Sources/Api19.swift @@ -606,6 +606,52 @@ public extension Api { } } +public extension Api { + enum SavedDialog: TypeConstructorDescription { + case savedDialog(flags: Int32, peer: Api.Peer, topMessage: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .savedDialog(let flags, let peer, let topMessage): + if boxed { + buffer.appendInt32(-1115174036) + } + serializeInt32(flags, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + serializeInt32(topMessage, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .savedDialog(let flags, let peer, let topMessage): + return ("savedDialog", [("flags", flags as Any), ("peer", peer as Any), ("topMessage", topMessage as Any)]) + } + } + + public static func parse_savedDialog(_ reader: BufferReader) -> SavedDialog? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.Peer? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.Peer + } + var _3: Int32? + _3 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.SavedDialog.savedDialog(flags: _1!, peer: _2!, topMessage: _3!) + } + else { + return nil + } + } + + } +} public extension Api { enum SearchResultsCalendarPeriod: TypeConstructorDescription { case searchResultsCalendarPeriod(date: Int32, minMsgId: Int32, maxMsgId: Int32, count: Int32) diff --git a/submodules/TelegramApi/Sources/Api22.swift b/submodules/TelegramApi/Sources/Api22.swift index 2ed34c1d5b3..1242ceb8a0e 100644 --- a/submodules/TelegramApi/Sources/Api22.swift +++ b/submodules/TelegramApi/Sources/Api22.swift @@ -550,6 +550,7 @@ public extension Api { case updatePinnedChannelMessages(flags: Int32, channelId: Int64, messages: [Int32], pts: Int32, ptsCount: Int32) case updatePinnedDialogs(flags: Int32, folderId: Int32?, order: [Api.DialogPeer]?) case updatePinnedMessages(flags: Int32, peer: Api.Peer, messages: [Int32], pts: Int32, ptsCount: Int32) + case updatePinnedSavedDialogs(flags: Int32, order: [Api.DialogPeer]?) case updatePrivacy(key: Api.PrivacyKey, rules: [Api.PrivacyRule]) case updatePtsChanged case updateReadChannelDiscussionInbox(flags: Int32, channelId: Int64, topMsgId: Int32, readMaxId: Int32, broadcastId: Int64?, broadcastPost: Int32?) @@ -558,6 +559,7 @@ public extension Api { case updateReadChannelOutbox(channelId: Int64, maxId: Int32) case updateReadFeaturedEmojiStickers case updateReadFeaturedStickers + case updateReadFeed(flags: Int32, filterId: Int32, maxPosition: Api.FeedPosition, unreadCount: Int32?, unreadMutedCount: Int32?) case updateReadHistoryInbox(flags: Int32, folderId: Int32?, peer: Api.Peer, maxId: Int32, stillUnreadCount: Int32, pts: Int32, ptsCount: Int32) case updateReadHistoryOutbox(peer: Api.Peer, maxId: Int32, pts: Int32, ptsCount: Int32) case updateReadMessagesContents(flags: Int32, messages: [Int32], pts: Int32, ptsCount: Int32, date: Int32?) @@ -565,6 +567,7 @@ public extension Api { case updateRecentEmojiStatuses case updateRecentReactions case updateRecentStickers + case updateSavedDialogPinned(flags: Int32, peer: Api.DialogPeer) case updateSavedGifs case updateSavedRingtones case updateSentStoryReaction(peer: Api.Peer, storyId: Int32, reaction: Api.Reaction) @@ -1372,6 +1375,17 @@ public extension Api { serializeInt32(pts, buffer: buffer, boxed: false) serializeInt32(ptsCount, buffer: buffer, boxed: false) break + case .updatePinnedSavedDialogs(let flags, let order): + if boxed { + buffer.appendInt32(1751942566) + } + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(order!.count)) + for item in order! { + item.serialize(buffer, true) + }} + break case .updatePrivacy(let key, let rules): if boxed { buffer.appendInt32(-298113238) @@ -1437,6 +1451,16 @@ public extension Api { buffer.appendInt32(1461528386) } + break + case .updateReadFeed(let flags, let filterId, let maxPosition, let unreadCount, let unreadMutedCount): + if boxed { + buffer.appendInt32(1951948721) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(filterId, buffer: buffer, boxed: false) + maxPosition.serialize(buffer, true) + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(unreadCount!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(unreadMutedCount!, buffer: buffer, boxed: false)} break case .updateReadHistoryInbox(let flags, let folderId, let peer, let maxId, let stillUnreadCount, let pts, let ptsCount): if boxed { @@ -1497,6 +1521,13 @@ public extension Api { buffer.appendInt32(-1706939360) } + break + case .updateSavedDialogPinned(let flags, let peer): + if boxed { + buffer.appendInt32(-1364222348) + } + serializeInt32(flags, buffer: buffer, boxed: false) + peer.serialize(buffer, true) break case .updateSavedGifs: if boxed { @@ -1828,6 +1859,8 @@ public extension Api { return ("updatePinnedDialogs", [("flags", flags as Any), ("folderId", folderId as Any), ("order", order as Any)]) case .updatePinnedMessages(let flags, let peer, let messages, let pts, let ptsCount): return ("updatePinnedMessages", [("flags", flags as Any), ("peer", peer as Any), ("messages", messages as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)]) + case .updatePinnedSavedDialogs(let flags, let order): + return ("updatePinnedSavedDialogs", [("flags", flags as Any), ("order", order as Any)]) case .updatePrivacy(let key, let rules): return ("updatePrivacy", [("key", key as Any), ("rules", rules as Any)]) case .updatePtsChanged: @@ -1844,6 +1877,8 @@ public extension Api { return ("updateReadFeaturedEmojiStickers", []) case .updateReadFeaturedStickers: return ("updateReadFeaturedStickers", []) + case .updateReadFeed(let flags, let filterId, let maxPosition, let unreadCount, let unreadMutedCount): + return ("updateReadFeed", [("flags", flags as Any), ("filterId", filterId as Any), ("maxPosition", maxPosition as Any), ("unreadCount", unreadCount as Any), ("unreadMutedCount", unreadMutedCount as Any)]) case .updateReadHistoryInbox(let flags, let folderId, let peer, let maxId, let stillUnreadCount, let pts, let ptsCount): return ("updateReadHistoryInbox", [("flags", flags as Any), ("folderId", folderId as Any), ("peer", peer as Any), ("maxId", maxId as Any), ("stillUnreadCount", stillUnreadCount as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)]) case .updateReadHistoryOutbox(let peer, let maxId, let pts, let ptsCount): @@ -1858,6 +1893,8 @@ public extension Api { return ("updateRecentReactions", []) case .updateRecentStickers: return ("updateRecentStickers", []) + case .updateSavedDialogPinned(let flags, let peer): + return ("updateSavedDialogPinned", [("flags", flags as Any), ("peer", peer as Any)]) case .updateSavedGifs: return ("updateSavedGifs", []) case .updateSavedRingtones: @@ -3538,6 +3575,22 @@ public extension Api { return nil } } + public static func parse_updatePinnedSavedDialogs(_ reader: BufferReader) -> Update? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.DialogPeer]? + if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.DialogPeer.self) + } } + let _c1 = _1 != nil + let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + if _c1 && _c2 { + return Api.Update.updatePinnedSavedDialogs(flags: _1!, order: _2) + } + else { + return nil + } + } public static func parse_updatePrivacy(_ reader: BufferReader) -> Update? { var _1: Api.PrivacyKey? if let signature = reader.readInt32() { @@ -3648,6 +3701,31 @@ public extension Api { public static func parse_updateReadFeaturedStickers(_ reader: BufferReader) -> Update? { return Api.Update.updateReadFeaturedStickers } + public static func parse_updateReadFeed(_ reader: BufferReader) -> Update? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Api.FeedPosition? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.FeedPosition + } + var _4: Int32? + if Int(_1!) & Int(1 << 0) != 0 {_4 = reader.readInt32() } + var _5: Int32? + if Int(_1!) & Int(1 << 0) != 0 {_5 = reader.readInt32() } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.Update.updateReadFeed(flags: _1!, filterId: _2!, maxPosition: _3!, unreadCount: _4, unreadMutedCount: _5) + } + else { + return nil + } + } public static func parse_updateReadHistoryInbox(_ reader: BufferReader) -> Update? { var _1: Int32? _1 = reader.readInt32() @@ -3751,6 +3829,22 @@ public extension Api { public static func parse_updateRecentStickers(_ reader: BufferReader) -> Update? { return Api.Update.updateRecentStickers } + public static func parse_updateSavedDialogPinned(_ reader: BufferReader) -> Update? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.DialogPeer? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.DialogPeer + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.Update.updateSavedDialogPinned(flags: _1!, peer: _2!) + } + else { + return nil + } + } public static func parse_updateSavedGifs(_ reader: BufferReader) -> Update? { return Api.Update.updateSavedGifs } diff --git a/submodules/TelegramApi/Sources/Api26.swift b/submodules/TelegramApi/Sources/Api26.swift index 60f1674a87b..2e926c8b0ff 100644 --- a/submodules/TelegramApi/Sources/Api26.swift +++ b/submodules/TelegramApi/Sources/Api26.swift @@ -634,6 +634,102 @@ public extension Api.contacts { } } +public extension Api.feed { + enum FeedMessages: TypeConstructorDescription { + case feedMessages(flags: Int32, maxPosition: Api.FeedPosition?, minPosition: Api.FeedPosition?, readMaxPosition: Api.FeedPosition?, messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) + case feedMessagesNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .feedMessages(let flags, let maxPosition, let minPosition, let readMaxPosition, let messages, let chats, let users): + if boxed { + buffer.appendInt32(-587770695) + } + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {maxPosition!.serialize(buffer, true)} + if Int(flags) & Int(1 << 1) != 0 {minPosition!.serialize(buffer, true)} + if Int(flags) & Int(1 << 2) != 0 {readMaxPosition!.serialize(buffer, true)} + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(messages.count)) + for item in messages { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + case .feedMessagesNotModified: + if boxed { + buffer.appendInt32(-619039485) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .feedMessages(let flags, let maxPosition, let minPosition, let readMaxPosition, let messages, let chats, let users): + return ("feedMessages", [("flags", flags as Any), ("maxPosition", maxPosition as Any), ("minPosition", minPosition as Any), ("readMaxPosition", readMaxPosition as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) + case .feedMessagesNotModified: + return ("feedMessagesNotModified", []) + } + } + + public static func parse_feedMessages(_ reader: BufferReader) -> FeedMessages? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.FeedPosition? + if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.FeedPosition + } } + var _3: Api.FeedPosition? + if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.FeedPosition + } } + var _4: Api.FeedPosition? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.FeedPosition + } } + var _5: [Api.Message]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) + } + var _6: [Api.Chat]? + if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _7: [Api.User]? + if let _ = reader.readInt32() { + _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.feed.FeedMessages.feedMessages(flags: _1!, maxPosition: _2, minPosition: _3, readMaxPosition: _4, messages: _5!, chats: _6!, users: _7!) + } + else { + return nil + } + } + public static func parse_feedMessagesNotModified(_ reader: BufferReader) -> FeedMessages? { + return Api.feed.FeedMessages.feedMessagesNotModified + } + + } +} public extension Api.help { enum AppConfig: TypeConstructorDescription { case appConfig(hash: Int32, config: Api.JSONValue) @@ -1304,89 +1400,3 @@ public extension Api.help { } } -public extension Api.help { - enum PremiumPromo: TypeConstructorDescription { - case premiumPromo(statusText: String, statusEntities: [Api.MessageEntity], videoSections: [String], videos: [Api.Document], periodOptions: [Api.PremiumSubscriptionOption], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .premiumPromo(let statusText, let statusEntities, let videoSections, let videos, let periodOptions, let users): - if boxed { - buffer.appendInt32(1395946908) - } - serializeString(statusText, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(statusEntities.count)) - for item in statusEntities { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(videoSections.count)) - for item in videoSections { - serializeString(item, buffer: buffer, boxed: false) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(videos.count)) - for item in videos { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(periodOptions.count)) - for item in periodOptions { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .premiumPromo(let statusText, let statusEntities, let videoSections, let videos, let periodOptions, let users): - return ("premiumPromo", [("statusText", statusText as Any), ("statusEntities", statusEntities as Any), ("videoSections", videoSections as Any), ("videos", videos as Any), ("periodOptions", periodOptions as Any), ("users", users as Any)]) - } - } - - public static func parse_premiumPromo(_ reader: BufferReader) -> PremiumPromo? { - var _1: String? - _1 = parseString(reader) - var _2: [Api.MessageEntity]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) - } - var _3: [String]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self) - } - var _4: [Api.Document]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) - } - var _5: [Api.PremiumSubscriptionOption]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PremiumSubscriptionOption.self) - } - var _6: [Api.User]? - if let _ = reader.readInt32() { - _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.help.PremiumPromo.premiumPromo(statusText: _1!, statusEntities: _2!, videoSections: _3!, videos: _4!, periodOptions: _5!, users: _6!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api27.swift b/submodules/TelegramApi/Sources/Api27.swift index 90373cb39cb..badbd701453 100644 --- a/submodules/TelegramApi/Sources/Api27.swift +++ b/submodules/TelegramApi/Sources/Api27.swift @@ -1,3 +1,89 @@ +public extension Api.help { + enum PremiumPromo: TypeConstructorDescription { + case premiumPromo(statusText: String, statusEntities: [Api.MessageEntity], videoSections: [String], videos: [Api.Document], periodOptions: [Api.PremiumSubscriptionOption], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .premiumPromo(let statusText, let statusEntities, let videoSections, let videos, let periodOptions, let users): + if boxed { + buffer.appendInt32(1395946908) + } + serializeString(statusText, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(statusEntities.count)) + for item in statusEntities { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(videoSections.count)) + for item in videoSections { + serializeString(item, buffer: buffer, boxed: false) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(videos.count)) + for item in videos { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(periodOptions.count)) + for item in periodOptions { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .premiumPromo(let statusText, let statusEntities, let videoSections, let videos, let periodOptions, let users): + return ("premiumPromo", [("statusText", statusText as Any), ("statusEntities", statusEntities as Any), ("videoSections", videoSections as Any), ("videos", videos as Any), ("periodOptions", periodOptions as Any), ("users", users as Any)]) + } + } + + public static func parse_premiumPromo(_ reader: BufferReader) -> PremiumPromo? { + var _1: String? + _1 = parseString(reader) + var _2: [Api.MessageEntity]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) + } + var _3: [String]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self) + } + var _4: [Api.Document]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) + } + var _5: [Api.PremiumSubscriptionOption]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PremiumSubscriptionOption.self) + } + var _6: [Api.User]? + if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.help.PremiumPromo.premiumPromo(statusText: _1!, statusEntities: _2!, videoSections: _3!, videos: _4!, periodOptions: _5!, users: _6!) + } + else { + return nil + } + } + + } +} public extension Api.help { enum PromoData: TypeConstructorDescription { case promoData(flags: Int32, expires: Int32, peer: Api.Peer, chats: [Api.Chat], users: [Api.User], psaType: String?, psaMessage: String?) diff --git a/submodules/TelegramApi/Sources/Api28.swift b/submodules/TelegramApi/Sources/Api28.swift index afd23f9c90c..639e18a7b26 100644 --- a/submodules/TelegramApi/Sources/Api28.swift +++ b/submodules/TelegramApi/Sources/Api28.swift @@ -1425,59 +1425,153 @@ public extension Api.messages { } } public extension Api.messages { - enum SavedGifs: TypeConstructorDescription { - case savedGifs(hash: Int64, gifs: [Api.Document]) - case savedGifsNotModified + enum SavedDialogs: TypeConstructorDescription { + case savedDialogs(dialogs: [Api.SavedDialog], messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) + case savedDialogsNotModified(count: Int32) + case savedDialogsSlice(count: Int32, dialogs: [Api.SavedDialog], messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .savedGifs(let hash, let gifs): + case .savedDialogs(let dialogs, let messages, let chats, let users): if boxed { - buffer.appendInt32(-2069878259) + buffer.appendInt32(-130358751) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(dialogs.count)) + for item in dialogs { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(messages.count)) + for item in messages { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) } - serializeInt64(hash, buffer: buffer, boxed: false) buffer.appendInt32(481674261) - buffer.appendInt32(Int32(gifs.count)) - for item in gifs { + buffer.appendInt32(Int32(users.count)) + for item in users { item.serialize(buffer, true) } break - case .savedGifsNotModified: + case .savedDialogsNotModified(let count): if boxed { - buffer.appendInt32(-402498398) + buffer.appendInt32(-1071681560) + } + serializeInt32(count, buffer: buffer, boxed: false) + break + case .savedDialogsSlice(let count, let dialogs, let messages, let chats, let users): + if boxed { + buffer.appendInt32(1153080793) + } + serializeInt32(count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(dialogs.count)) + for item in dialogs { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(messages.count)) + for item in messages { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) } - break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .savedGifs(let hash, let gifs): - return ("savedGifs", [("hash", hash as Any), ("gifs", gifs as Any)]) - case .savedGifsNotModified: - return ("savedGifsNotModified", []) + case .savedDialogs(let dialogs, let messages, let chats, let users): + return ("savedDialogs", [("dialogs", dialogs as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) + case .savedDialogsNotModified(let count): + return ("savedDialogsNotModified", [("count", count as Any)]) + case .savedDialogsSlice(let count, let dialogs, let messages, let chats, let users): + return ("savedDialogsSlice", [("count", count as Any), ("dialogs", dialogs as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) } } - public static func parse_savedGifs(_ reader: BufferReader) -> SavedGifs? { - var _1: Int64? - _1 = reader.readInt64() - var _2: [Api.Document]? + public static func parse_savedDialogs(_ reader: BufferReader) -> SavedDialogs? { + var _1: [Api.SavedDialog]? if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.SavedDialog.self) + } + var _2: [Api.Message]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) + } + var _3: [Api.Chat]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _4: [Api.User]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } let _c1 = _1 != nil let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.SavedGifs.savedGifs(hash: _1!, gifs: _2!) + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.messages.SavedDialogs.savedDialogs(dialogs: _1!, messages: _2!, chats: _3!, users: _4!) + } + else { + return nil + } + } + public static func parse_savedDialogsNotModified(_ reader: BufferReader) -> SavedDialogs? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.messages.SavedDialogs.savedDialogsNotModified(count: _1!) } else { return nil } } - public static func parse_savedGifsNotModified(_ reader: BufferReader) -> SavedGifs? { - return Api.messages.SavedGifs.savedGifsNotModified + public static func parse_savedDialogsSlice(_ reader: BufferReader) -> SavedDialogs? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.SavedDialog]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.SavedDialog.self) + } + var _3: [Api.Message]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) + } + var _4: [Api.Chat]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _5: [Api.User]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.messages.SavedDialogs.savedDialogsSlice(count: _1!, dialogs: _2!, messages: _3!, chats: _4!, users: _5!) + } + else { + return nil + } } } diff --git a/submodules/TelegramApi/Sources/Api29.swift b/submodules/TelegramApi/Sources/Api29.swift index 0ff2e969750..c0a8e8f120a 100644 --- a/submodules/TelegramApi/Sources/Api29.swift +++ b/submodules/TelegramApi/Sources/Api29.swift @@ -1,3 +1,61 @@ +public extension Api.messages { + enum SavedGifs: TypeConstructorDescription { + case savedGifs(hash: Int64, gifs: [Api.Document]) + case savedGifsNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .savedGifs(let hash, let gifs): + if boxed { + buffer.appendInt32(-2069878259) + } + serializeInt64(hash, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(gifs.count)) + for item in gifs { + item.serialize(buffer, true) + } + break + case .savedGifsNotModified: + if boxed { + buffer.appendInt32(-402498398) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .savedGifs(let hash, let gifs): + return ("savedGifs", [("hash", hash as Any), ("gifs", gifs as Any)]) + case .savedGifsNotModified: + return ("savedGifsNotModified", []) + } + } + + public static func parse_savedGifs(_ reader: BufferReader) -> SavedGifs? { + var _1: Int64? + _1 = reader.readInt64() + var _2: [Api.Document]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.SavedGifs.savedGifs(hash: _1!, gifs: _2!) + } + else { + return nil + } + } + public static func parse_savedGifsNotModified(_ reader: BufferReader) -> SavedGifs? { + return Api.messages.SavedGifs.savedGifsNotModified + } + + } +} public extension Api.messages { enum SearchCounter: TypeConstructorDescription { case searchCounter(flags: Int32, filter: Api.MessagesFilter, count: Int32) @@ -1482,85 +1540,3 @@ public extension Api.phone { } } -public extension Api.phone { - enum GroupCallStreamChannels: TypeConstructorDescription { - case groupCallStreamChannels(channels: [Api.GroupCallStreamChannel]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .groupCallStreamChannels(let channels): - if boxed { - buffer.appendInt32(-790330702) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(channels.count)) - for item in channels { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .groupCallStreamChannels(let channels): - return ("groupCallStreamChannels", [("channels", channels as Any)]) - } - } - - public static func parse_groupCallStreamChannels(_ reader: BufferReader) -> GroupCallStreamChannels? { - var _1: [Api.GroupCallStreamChannel]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.GroupCallStreamChannel.self) - } - let _c1 = _1 != nil - if _c1 { - return Api.phone.GroupCallStreamChannels.groupCallStreamChannels(channels: _1!) - } - else { - return nil - } - } - - } -} -public extension Api.phone { - enum GroupCallStreamRtmpUrl: TypeConstructorDescription { - case groupCallStreamRtmpUrl(url: String, key: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .groupCallStreamRtmpUrl(let url, let key): - if boxed { - buffer.appendInt32(767505458) - } - serializeString(url, buffer: buffer, boxed: false) - serializeString(key, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .groupCallStreamRtmpUrl(let url, let key): - return ("groupCallStreamRtmpUrl", [("url", url as Any), ("key", key as Any)]) - } - } - - public static func parse_groupCallStreamRtmpUrl(_ reader: BufferReader) -> GroupCallStreamRtmpUrl? { - var _1: String? - _1 = parseString(reader) - var _2: String? - _2 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.phone.GroupCallStreamRtmpUrl.groupCallStreamRtmpUrl(url: _1!, key: _2!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api30.swift b/submodules/TelegramApi/Sources/Api30.swift index 0f744475ba3..472f613cd64 100644 --- a/submodules/TelegramApi/Sources/Api30.swift +++ b/submodules/TelegramApi/Sources/Api30.swift @@ -1,3 +1,85 @@ +public extension Api.phone { + enum GroupCallStreamChannels: TypeConstructorDescription { + case groupCallStreamChannels(channels: [Api.GroupCallStreamChannel]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .groupCallStreamChannels(let channels): + if boxed { + buffer.appendInt32(-790330702) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(channels.count)) + for item in channels { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .groupCallStreamChannels(let channels): + return ("groupCallStreamChannels", [("channels", channels as Any)]) + } + } + + public static func parse_groupCallStreamChannels(_ reader: BufferReader) -> GroupCallStreamChannels? { + var _1: [Api.GroupCallStreamChannel]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.GroupCallStreamChannel.self) + } + let _c1 = _1 != nil + if _c1 { + return Api.phone.GroupCallStreamChannels.groupCallStreamChannels(channels: _1!) + } + else { + return nil + } + } + + } +} +public extension Api.phone { + enum GroupCallStreamRtmpUrl: TypeConstructorDescription { + case groupCallStreamRtmpUrl(url: String, key: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .groupCallStreamRtmpUrl(let url, let key): + if boxed { + buffer.appendInt32(767505458) + } + serializeString(url, buffer: buffer, boxed: false) + serializeString(key, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .groupCallStreamRtmpUrl(let url, let key): + return ("groupCallStreamRtmpUrl", [("url", url as Any), ("key", key as Any)]) + } + } + + public static func parse_groupCallStreamRtmpUrl(_ reader: BufferReader) -> GroupCallStreamRtmpUrl? { + var _1: String? + _1 = parseString(reader) + var _2: String? + _2 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.phone.GroupCallStreamRtmpUrl.groupCallStreamRtmpUrl(url: _1!, key: _2!) + } + else { + return nil + } + } + + } +} public extension Api.phone { enum GroupParticipants: TypeConstructorDescription { case groupParticipants(count: Int32, participants: [Api.GroupCallParticipant], nextOffset: String, chats: [Api.Chat], users: [Api.User], version: Int32) @@ -1494,141 +1576,3 @@ public extension Api.stories { } } -public extension Api.stories { - enum StoryViews: TypeConstructorDescription { - case storyViews(views: [Api.StoryViews], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .storyViews(let views, let users): - if boxed { - buffer.appendInt32(-560009955) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(views.count)) - for item in views { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .storyViews(let views, let users): - return ("storyViews", [("views", views as Any), ("users", users as Any)]) - } - } - - public static func parse_storyViews(_ reader: BufferReader) -> StoryViews? { - var _1: [Api.StoryViews]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryViews.self) - } - var _2: [Api.User]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.stories.StoryViews.storyViews(views: _1!, users: _2!) - } - else { - return nil - } - } - - } -} -public extension Api.stories { - enum StoryViewsList: TypeConstructorDescription { - case storyViewsList(flags: Int32, count: Int32, viewsCount: Int32, forwardsCount: Int32, reactionsCount: Int32, views: [Api.StoryView], chats: [Api.Chat], users: [Api.User], nextOffset: String?) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .storyViewsList(let flags, let count, let viewsCount, let forwardsCount, let reactionsCount, let views, let chats, let users, let nextOffset): - if boxed { - buffer.appendInt32(1507299269) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(count, buffer: buffer, boxed: false) - serializeInt32(viewsCount, buffer: buffer, boxed: false) - serializeInt32(forwardsCount, buffer: buffer, boxed: false) - serializeInt32(reactionsCount, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(views.count)) - for item in views { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .storyViewsList(let flags, let count, let viewsCount, let forwardsCount, let reactionsCount, let views, let chats, let users, let nextOffset): - return ("storyViewsList", [("flags", flags as Any), ("count", count as Any), ("viewsCount", viewsCount as Any), ("forwardsCount", forwardsCount as Any), ("reactionsCount", reactionsCount as Any), ("views", views as Any), ("chats", chats as Any), ("users", users as Any), ("nextOffset", nextOffset as Any)]) - } - } - - public static func parse_storyViewsList(_ reader: BufferReader) -> StoryViewsList? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() - var _4: Int32? - _4 = reader.readInt32() - var _5: Int32? - _5 = reader.readInt32() - var _6: [Api.StoryView]? - if let _ = reader.readInt32() { - _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryView.self) - } - var _7: [Api.Chat]? - if let _ = reader.readInt32() { - _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _8: [Api.User]? - if let _ = reader.readInt32() { - _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - var _9: String? - if Int(_1!) & Int(1 << 0) != 0 {_9 = parseString(reader) } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - let _c7 = _7 != nil - let _c8 = _8 != nil - let _c9 = (Int(_1!) & Int(1 << 0) == 0) || _9 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { - return Api.stories.StoryViewsList.storyViewsList(flags: _1!, count: _2!, viewsCount: _3!, forwardsCount: _4!, reactionsCount: _5!, views: _6!, chats: _7!, users: _8!, nextOffset: _9) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api31.swift b/submodules/TelegramApi/Sources/Api31.swift index 0cfe25da84a..e5b0c6e49a8 100644 --- a/submodules/TelegramApi/Sources/Api31.swift +++ b/submodules/TelegramApi/Sources/Api31.swift @@ -1,3 +1,141 @@ +public extension Api.stories { + enum StoryViews: TypeConstructorDescription { + case storyViews(views: [Api.StoryViews], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .storyViews(let views, let users): + if boxed { + buffer.appendInt32(-560009955) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(views.count)) + for item in views { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .storyViews(let views, let users): + return ("storyViews", [("views", views as Any), ("users", users as Any)]) + } + } + + public static func parse_storyViews(_ reader: BufferReader) -> StoryViews? { + var _1: [Api.StoryViews]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryViews.self) + } + var _2: [Api.User]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.stories.StoryViews.storyViews(views: _1!, users: _2!) + } + else { + return nil + } + } + + } +} +public extension Api.stories { + enum StoryViewsList: TypeConstructorDescription { + case storyViewsList(flags: Int32, count: Int32, viewsCount: Int32, forwardsCount: Int32, reactionsCount: Int32, views: [Api.StoryView], chats: [Api.Chat], users: [Api.User], nextOffset: String?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .storyViewsList(let flags, let count, let viewsCount, let forwardsCount, let reactionsCount, let views, let chats, let users, let nextOffset): + if boxed { + buffer.appendInt32(1507299269) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(count, buffer: buffer, boxed: false) + serializeInt32(viewsCount, buffer: buffer, boxed: false) + serializeInt32(forwardsCount, buffer: buffer, boxed: false) + serializeInt32(reactionsCount, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(views.count)) + for item in views { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .storyViewsList(let flags, let count, let viewsCount, let forwardsCount, let reactionsCount, let views, let chats, let users, let nextOffset): + return ("storyViewsList", [("flags", flags as Any), ("count", count as Any), ("viewsCount", viewsCount as Any), ("forwardsCount", forwardsCount as Any), ("reactionsCount", reactionsCount as Any), ("views", views as Any), ("chats", chats as Any), ("users", users as Any), ("nextOffset", nextOffset as Any)]) + } + } + + public static func parse_storyViewsList(_ reader: BufferReader) -> StoryViewsList? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + var _6: [Api.StoryView]? + if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryView.self) + } + var _7: [Api.Chat]? + if let _ = reader.readInt32() { + _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _8: [Api.User]? + if let _ = reader.readInt32() { + _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + var _9: String? + if Int(_1!) & Int(1 << 0) != 0 {_9 = parseString(reader) } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + let _c9 = (Int(_1!) & Int(1 << 0) == 0) || _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.stories.StoryViewsList.storyViewsList(flags: _1!, count: _2!, viewsCount: _3!, forwardsCount: _4!, reactionsCount: _5!, views: _6!, chats: _7!, users: _8!, nextOffset: _9) + } + else { + return nil + } + } + + } +} public extension Api.updates { indirect enum ChannelDifference: TypeConstructorDescription { case channelDifference(flags: Int32, pts: Int32, timeout: Int32?, newMessages: [Api.Message], otherUpdates: [Api.Update], chats: [Api.Chat], users: [Api.User]) diff --git a/submodules/TelegramApi/Sources/Api32.swift b/submodules/TelegramApi/Sources/Api32.swift index 6e161a1f2bd..6983f441826 100644 --- a/submodules/TelegramApi/Sources/Api32.swift +++ b/submodules/TelegramApi/Sources/Api32.swift @@ -3770,6 +3770,44 @@ public extension Api.functions.contacts { }) } } +public extension Api.functions.feed { + static func getFeed(flags: Int32, filterId: Int32, offsetPosition: Api.FeedPosition?, addOffset: Int32, limit: Int32, maxPosition: Api.FeedPosition?, minPosition: Api.FeedPosition?, hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(2121717715) + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(filterId, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {offsetPosition!.serialize(buffer, true)} + serializeInt32(addOffset, buffer: buffer, boxed: false) + serializeInt32(limit, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {maxPosition!.serialize(buffer, true)} + if Int(flags) & Int(1 << 2) != 0 {minPosition!.serialize(buffer, true)} + serializeInt64(hash, buffer: buffer, boxed: false) + return (FunctionDescription(name: "feed.getFeed", parameters: [("flags", String(describing: flags)), ("filterId", String(describing: filterId)), ("offsetPosition", String(describing: offsetPosition)), ("addOffset", String(describing: addOffset)), ("limit", String(describing: limit)), ("maxPosition", String(describing: maxPosition)), ("minPosition", String(describing: minPosition)), ("hash", String(describing: hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.feed.FeedMessages? in + let reader = BufferReader(buffer) + var result: Api.feed.FeedMessages? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.feed.FeedMessages + } + return result + }) + } +} +public extension Api.functions.feed { + static func readFeed(filterId: Int32, maxPosition: Api.FeedPosition) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1271479809) + serializeInt32(filterId, buffer: buffer, boxed: false) + maxPosition.serialize(buffer, true) + return (FunctionDescription(name: "feed.readFeed", parameters: [("filterId", String(describing: filterId)), ("maxPosition", String(describing: maxPosition))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } +} public extension Api.functions.folders { static func editPeerFolders(folderPeers: [Api.InputFolderPeer]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -4545,6 +4583,25 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func deleteSavedHistory(flags: Int32, peer: Api.InputPeer, maxId: Int32, minDate: Int32?, maxDate: Int32?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1855459371) + serializeInt32(flags, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + serializeInt32(maxId, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {serializeInt32(minDate!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 3) != 0 {serializeInt32(maxDate!, buffer: buffer, boxed: false)} + return (FunctionDescription(name: "messages.deleteSavedHistory", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("maxId", String(describing: maxId)), ("minDate", String(describing: minDate)), ("maxDate", String(describing: maxDate))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.AffectedHistory? in + let reader = BufferReader(buffer) + var result: Api.messages.AffectedHistory? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.messages.AffectedHistory + } + return result + }) + } +} public extension Api.functions.messages { static func deleteScheduledMessages(peer: Api.InputPeer, id: [Int32]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -5670,6 +5727,21 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func getPinnedSavedDialogs() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-700607264) + + return (FunctionDescription(name: "messages.getPinnedSavedDialogs", parameters: []), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.SavedDialogs? in + let reader = BufferReader(buffer) + var result: Api.messages.SavedDialogs? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.messages.SavedDialogs + } + return result + }) + } +} public extension Api.functions.messages { static func getPollResults(peer: Api.InputPeer, msgId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -5778,6 +5850,26 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func getSavedDialogs(flags: Int32, offsetDate: Int32, offsetId: Int32, offsetPeer: Api.InputPeer, limit: Int32, hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1401016858) + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(offsetDate, buffer: buffer, boxed: false) + serializeInt32(offsetId, buffer: buffer, boxed: false) + offsetPeer.serialize(buffer, true) + serializeInt32(limit, buffer: buffer, boxed: false) + serializeInt64(hash, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.getSavedDialogs", parameters: [("flags", String(describing: flags)), ("offsetDate", String(describing: offsetDate)), ("offsetId", String(describing: offsetId)), ("offsetPeer", String(describing: offsetPeer)), ("limit", String(describing: limit)), ("hash", String(describing: hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.SavedDialogs? in + let reader = BufferReader(buffer) + var result: Api.messages.SavedDialogs? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.messages.SavedDialogs + } + return result + }) + } +} public extension Api.functions.messages { static func getSavedGifs(hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -5793,6 +5885,28 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func getSavedHistory(peer: Api.InputPeer, offsetId: Int32, offsetDate: Int32, addOffset: Int32, limit: Int32, maxId: Int32, minId: Int32, hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1033519437) + peer.serialize(buffer, true) + serializeInt32(offsetId, buffer: buffer, boxed: false) + serializeInt32(offsetDate, buffer: buffer, boxed: false) + serializeInt32(addOffset, buffer: buffer, boxed: false) + serializeInt32(limit, buffer: buffer, boxed: false) + serializeInt32(maxId, buffer: buffer, boxed: false) + serializeInt32(minId, buffer: buffer, boxed: false) + serializeInt64(hash, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.getSavedHistory", parameters: [("peer", String(describing: peer)), ("offsetId", String(describing: offsetId)), ("offsetDate", String(describing: offsetDate)), ("addOffset", String(describing: addOffset)), ("limit", String(describing: limit)), ("maxId", String(describing: maxId)), ("minId", String(describing: minId)), ("hash", String(describing: hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.Messages? in + let reader = BufferReader(buffer) + var result: Api.messages.Messages? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.messages.Messages + } + return result + }) + } +} public extension Api.functions.messages { static func getScheduledHistory(peer: Api.InputPeer, hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -5830,18 +5944,19 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func getSearchCounters(flags: Int32, peer: Api.InputPeer, topMsgId: Int32?, filters: [Api.MessagesFilter]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Api.messages.SearchCounter]>) { + static func getSearchCounters(flags: Int32, peer: Api.InputPeer, savedPeerId: Api.InputPeer?, topMsgId: Int32?, filters: [Api.MessagesFilter]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Api.messages.SearchCounter]>) { let buffer = Buffer() - buffer.appendInt32(11435201) + buffer.appendInt32(465367808) serializeInt32(flags, buffer: buffer, boxed: false) peer.serialize(buffer, true) + if Int(flags) & Int(1 << 2) != 0 {savedPeerId!.serialize(buffer, true)} if Int(flags) & Int(1 << 0) != 0 {serializeInt32(topMsgId!, buffer: buffer, boxed: false)} buffer.appendInt32(481674261) buffer.appendInt32(Int32(filters.count)) for item in filters { item.serialize(buffer, true) } - return (FunctionDescription(name: "messages.getSearchCounters", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("topMsgId", String(describing: topMsgId)), ("filters", String(describing: filters))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> [Api.messages.SearchCounter]? in + return (FunctionDescription(name: "messages.getSearchCounters", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("savedPeerId", String(describing: savedPeerId)), ("topMsgId", String(describing: topMsgId)), ("filters", String(describing: filters))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> [Api.messages.SearchCounter]? in let reader = BufferReader(buffer) var result: [Api.messages.SearchCounter]? if let _ = reader.readInt32() { @@ -5852,14 +5967,16 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func getSearchResultsCalendar(peer: Api.InputPeer, filter: Api.MessagesFilter, offsetId: Int32, offsetDate: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func getSearchResultsCalendar(flags: Int32, peer: Api.InputPeer, savedPeerId: Api.InputPeer?, filter: Api.MessagesFilter, offsetId: Int32, offsetDate: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1240514025) + buffer.appendInt32(1789130429) + serializeInt32(flags, buffer: buffer, boxed: false) peer.serialize(buffer, true) + if Int(flags) & Int(1 << 2) != 0 {savedPeerId!.serialize(buffer, true)} filter.serialize(buffer, true) serializeInt32(offsetId, buffer: buffer, boxed: false) serializeInt32(offsetDate, buffer: buffer, boxed: false) - return (FunctionDescription(name: "messages.getSearchResultsCalendar", parameters: [("peer", String(describing: peer)), ("filter", String(describing: filter)), ("offsetId", String(describing: offsetId)), ("offsetDate", String(describing: offsetDate))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.SearchResultsCalendar? in + return (FunctionDescription(name: "messages.getSearchResultsCalendar", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("savedPeerId", String(describing: savedPeerId)), ("filter", String(describing: filter)), ("offsetId", String(describing: offsetId)), ("offsetDate", String(describing: offsetDate))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.SearchResultsCalendar? in let reader = BufferReader(buffer) var result: Api.messages.SearchResultsCalendar? if let signature = reader.readInt32() { @@ -5870,14 +5987,16 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func getSearchResultsPositions(peer: Api.InputPeer, filter: Api.MessagesFilter, offsetId: Int32, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func getSearchResultsPositions(flags: Int32, peer: Api.InputPeer, savedPeerId: Api.InputPeer?, filter: Api.MessagesFilter, offsetId: Int32, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1855292323) + buffer.appendInt32(-1669386480) + serializeInt32(flags, buffer: buffer, boxed: false) peer.serialize(buffer, true) + if Int(flags) & Int(1 << 2) != 0 {savedPeerId!.serialize(buffer, true)} filter.serialize(buffer, true) serializeInt32(offsetId, buffer: buffer, boxed: false) serializeInt32(limit, buffer: buffer, boxed: false) - return (FunctionDescription(name: "messages.getSearchResultsPositions", parameters: [("peer", String(describing: peer)), ("filter", String(describing: filter)), ("offsetId", String(describing: offsetId)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.SearchResultsPositions? in + return (FunctionDescription(name: "messages.getSearchResultsPositions", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("savedPeerId", String(describing: savedPeerId)), ("filter", String(describing: filter)), ("offsetId", String(describing: offsetId)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.SearchResultsPositions? in let reader = BufferReader(buffer) var result: Api.messages.SearchResultsPositions? if let signature = reader.readInt32() { @@ -6384,6 +6503,26 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func reorderPinnedSavedDialogs(flags: Int32, order: [Api.InputDialogPeer]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1955502713) + serializeInt32(flags, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(order.count)) + for item in order { + item.serialize(buffer, true) + } + return (FunctionDescription(name: "messages.reorderPinnedSavedDialogs", parameters: [("flags", String(describing: flags)), ("order", String(describing: order))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.messages { static func reorderStickerSets(flags: Int32, order: [Int64]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -6646,13 +6785,14 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func search(flags: Int32, peer: Api.InputPeer, q: String, fromId: Api.InputPeer?, topMsgId: Int32?, filter: Api.MessagesFilter, minDate: Int32, maxDate: Int32, offsetId: Int32, addOffset: Int32, limit: Int32, maxId: Int32, minId: Int32, hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func search(flags: Int32, peer: Api.InputPeer, q: String, fromId: Api.InputPeer?, savedPeerId: Api.InputPeer?, topMsgId: Int32?, filter: Api.MessagesFilter, minDate: Int32, maxDate: Int32, offsetId: Int32, addOffset: Int32, limit: Int32, maxId: Int32, minId: Int32, hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-1593989278) + buffer.appendInt32(-1481316055) serializeInt32(flags, buffer: buffer, boxed: false) peer.serialize(buffer, true) serializeString(q, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {fromId!.serialize(buffer, true)} + if Int(flags) & Int(1 << 2) != 0 {savedPeerId!.serialize(buffer, true)} if Int(flags) & Int(1 << 1) != 0 {serializeInt32(topMsgId!, buffer: buffer, boxed: false)} filter.serialize(buffer, true) serializeInt32(minDate, buffer: buffer, boxed: false) @@ -6663,7 +6803,7 @@ public extension Api.functions.messages { serializeInt32(maxId, buffer: buffer, boxed: false) serializeInt32(minId, buffer: buffer, boxed: false) serializeInt64(hash, buffer: buffer, boxed: false) - return (FunctionDescription(name: "messages.search", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("q", String(describing: q)), ("fromId", String(describing: fromId)), ("topMsgId", String(describing: topMsgId)), ("filter", String(describing: filter)), ("minDate", String(describing: minDate)), ("maxDate", String(describing: maxDate)), ("offsetId", String(describing: offsetId)), ("addOffset", String(describing: addOffset)), ("limit", String(describing: limit)), ("maxId", String(describing: maxId)), ("minId", String(describing: minId)), ("hash", String(describing: hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.Messages? in + return (FunctionDescription(name: "messages.search", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("q", String(describing: q)), ("fromId", String(describing: fromId)), ("savedPeerId", String(describing: savedPeerId)), ("topMsgId", String(describing: topMsgId)), ("filter", String(describing: filter)), ("minDate", String(describing: minDate)), ("maxDate", String(describing: maxDate)), ("offsetId", String(describing: offsetId)), ("addOffset", String(describing: addOffset)), ("limit", String(describing: limit)), ("maxId", String(describing: maxId)), ("minId", String(describing: minId)), ("hash", String(describing: hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.Messages? in let reader = BufferReader(buffer) var result: Api.messages.Messages? if let signature = reader.readInt32() { @@ -7405,6 +7545,22 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func toggleSavedDialogPin(flags: Int32, peer: Api.InputDialogPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1400783906) + serializeInt32(flags, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + return (FunctionDescription(name: "messages.toggleSavedDialogPin", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.messages { static func toggleStickerSets(flags: Int32, stickersets: [Api.InputStickerSet]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramApi/Sources/Api6.swift b/submodules/TelegramApi/Sources/Api6.swift index 60cf7f42908..ecfcd55b204 100644 --- a/submodules/TelegramApi/Sources/Api6.swift +++ b/submodules/TelegramApi/Sources/Api6.swift @@ -74,6 +74,52 @@ public extension Api { } } +public extension Api { + enum FeedPosition: TypeConstructorDescription { + case feedPosition(date: Int32, peer: Api.Peer, id: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .feedPosition(let date, let peer, let id): + if boxed { + buffer.appendInt32(1348066419) + } + serializeInt32(date, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + serializeInt32(id, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .feedPosition(let date, let peer, let id): + return ("feedPosition", [("date", date as Any), ("peer", peer as Any), ("id", id as Any)]) + } + } + + public static func parse_feedPosition(_ reader: BufferReader) -> FeedPosition? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.Peer? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.Peer + } + var _3: Int32? + _3 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.FeedPosition.feedPosition(date: _1!, peer: _2!, id: _3!) + } + else { + return nil + } + } + + } +} public extension Api { enum FileHash: TypeConstructorDescription { case fileHash(offset: Int64, limit: Int32, hash: Buffer) diff --git a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift index 4538013b8ad..2ac5619743e 100644 --- a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift +++ b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift @@ -643,7 +643,12 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { } } - if let (item, previousItem, nextItem, order, type, _) = self.playlistStateAndType, !mediaAccessoryPanelHidden { + var isViewOnceMessage = false + if let (item, _, _, _, _, _) = self.playlistStateAndType, let source = item.playbackData?.source, case let .telegramFile(_, _, isViewOnce) = source, isViewOnce { + isViewOnceMessage = true + } + + if let (item, previousItem, nextItem, order, type, _) = self.playlistStateAndType, !mediaAccessoryPanelHidden && !isViewOnceMessage { let panelHeight = MediaNavigationAccessoryHeaderNode.minimizedHeight let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: panelStartY), size: CGSize(width: layout.size.width, height: panelHeight)) additionalHeight += panelHeight diff --git a/submodules/TelegramCallsUI/Sources/CallController.swift b/submodules/TelegramCallsUI/Sources/CallController.swift index e39e7167327..3aa0ee9a293 100644 --- a/submodules/TelegramCallsUI/Sources/CallController.swift +++ b/submodules/TelegramCallsUI/Sources/CallController.swift @@ -148,7 +148,7 @@ public final class CallController: ViewController { } override public func loadDisplayNode() { - var useV2 = self.call.context.sharedContext.immediateExperimentalUISettings.callV2 + var useV2 = true if let data = self.call.context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_callui_v2"] { useV2 = false } diff --git a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift index 89dd7adb28a..68fd311c244 100644 --- a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift +++ b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift @@ -42,6 +42,8 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP private var didInitializeIsReady: Bool = false private var callStartTimestamp: Double? + private var smoothSignalQuality: Double? + private var smoothSignalQualityTarget: Double? private var callState: PresentationCallState? var isMuted: Bool = false @@ -77,6 +79,8 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP private var panGestureState: PanGestureState? private var notifyDismissedInteractivelyOnPanGestureApply: Bool = false + private var signalQualityTimer: Foundation.Timer? + init( sharedContext: SharedAccountContext, account: Account, @@ -153,6 +157,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP } self.callScreenState = PrivateCallScreen.State( + strings: presentationData.strings, lifecycleState: .connecting, name: " ", shortName: " ", @@ -190,6 +195,22 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP }) self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) + + self.signalQualityTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { [weak self] _ in + guard let self else { + return + } + if let smoothSignalQuality = self.smoothSignalQuality, let smoothSignalQualityTarget = self.smoothSignalQualityTarget { + let updatedSmoothSignalQuality = (smoothSignalQuality + smoothSignalQualityTarget) * 0.5 + if abs(updatedSmoothSignalQuality - smoothSignalQuality) > 0.001 { + self.smoothSignalQuality = updatedSmoothSignalQuality + + if let callState = self.callState { + self.updateCallState(callState) + } + } + } + }) } deinit { @@ -197,6 +218,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP self.isMicrophoneMutedDisposable?.dispose() self.audioLevelDisposable?.dispose() self.audioOutputCheckTimer?.invalidate() + self.signalQualityTimer?.invalidate() } func updateAudioOutputs(availableOutputs: [AudioSessionOutput], currentOutput: AudioSessionOutput?) { @@ -211,8 +233,24 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP mappedOutput = .internalSpeaker case .speaker: mappedOutput = .speaker - case .headphones, .port: - mappedOutput = .speaker + case .headphones: + mappedOutput = .headphones + case let .port(port): + switch port.type { + case .wired: + mappedOutput = .headphones + default: + let portName = port.name.lowercased() + if portName.contains("airpods pro") { + mappedOutput = .airpodsPro + } else if portName.contains("airpods max") { + mappedOutput = .airpodsMax + } else if portName.contains("airpods") { + mappedOutput = .airpods + } else { + mappedOutput = .bluetooth + } + } } } else { mappedOutput = .internalSpeaker @@ -342,10 +380,16 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP case .connecting: mappedLifecycleState = .connecting case let .active(startTime, signalQuality, keyData): - self.callStartTimestamp = startTime + var signalQuality = signalQuality.flatMap(Int.init) + self.smoothSignalQualityTarget = Double(signalQuality ?? 4) - var signalQuality = signalQuality - signalQuality = 4 + if let smoothSignalQuality = self.smoothSignalQuality { + signalQuality = Int(round(smoothSignalQuality)) + } else { + signalQuality = 4 + } + + self.callStartTimestamp = startTime let _ = keyData mappedLifecycleState = .active(PrivateCallScreen.State.ActiveState( @@ -354,6 +398,9 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP emojiKey: self.resolvedEmojiKey(data: keyData) )) case let .reconnecting(startTime, _, keyData): + self.smoothSignalQuality = nil + self.smoothSignalQualityTarget = nil + if self.callStartTimestamp != nil { mappedLifecycleState = .active(PrivateCallScreen.State.ActiveState( startTime: startTime + kCFAbsoluteTimeIntervalSince1970, @@ -517,51 +564,31 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP callScreenState.name = peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) callScreenState.shortName = peer.compactDisplayTitle - if self.currentPeer?.smallProfileImage != peer.smallProfileImage { + if (self.currentPeer?.smallProfileImage != peer.smallProfileImage) || self.callScreenState?.avatarImage == nil { self.peerAvatarDisposable?.dispose() - if let smallProfileImage = peer.largeProfileImage, let peerReference = PeerReference(peer._asPeer()) { - if let thumbnailImage = smallProfileImage.immediateThumbnailData.flatMap(decodeTinyThumbnail).flatMap(UIImage.init(data:)), let cgImage = thumbnailImage.cgImage { - callScreenState.avatarImage = generateImage(CGSize(width: 128.0, height: 128.0), contextGenerator: { size, context in - context.draw(cgImage, in: CGRect(origin: CGPoint(), size: size)) - }, scale: 1.0).flatMap { image in - return blurredImage(image, radius: 10.0) - } - } - - let postbox = self.call.context.account.postbox - self.peerAvatarDisposable = (Signal { subscriber in - let fetchDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: .other, userContentType: .avatar, reference: .avatar(peer: peerReference, resource: smallProfileImage.resource)).start() - let dataDisposable = postbox.mediaBox.resourceData(smallProfileImage.resource).start(next: { data in - if data.complete, let image = UIImage(contentsOfFile: data.path)?.precomposed() { - subscriber.putNext(image) - subscriber.putCompletion() - } - }) - - return ActionDisposable { - fetchDisposable.dispose() - dataDisposable.dispose() - } - } - |> deliverOnMainQueue).start(next: { [weak self] image in + let size = CGSize(width: 128.0, height: 128.0) + if let representation = peer.largeProfileImage, let signal = peerAvatarImage(account: self.call.context.account, peerReference: PeerReference(peer._asPeer()), authorOfMessage: nil, representation: representation, displayDimensions: size, synchronousLoad: self.callScreenState?.avatarImage == nil) { + self.peerAvatarDisposable = (signal + |> deliverOnMainQueue).startStrict(next: { [weak self] imageVersions in guard let self else { return } - if var callScreenState = self.callScreenState { + let image = imageVersions?.0 + if let image { callScreenState.avatarImage = image self.callScreenState = callScreenState self.update(transition: .immediate) } }) } else { - self.peerAvatarDisposable?.dispose() - self.peerAvatarDisposable = nil - - callScreenState.avatarImage = generateImage(CGSize(width: 512, height: 512), scale: 1.0, rotatedContext: { size, context in + let image = generateImage(size, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - drawPeerAvatarLetters(context: context, size: size, font: Font.semibold(20.0), letters: peer.displayLetters, peerId: peer.id, nameColor: peer.nameColor) - }) + drawPeerAvatarLetters(context: context, size: size, font: avatarPlaceholderFont(size: 50.0), letters: peer.displayLetters, peerId: peer.id, nameColor: peer.nameColor) + })! + callScreenState.avatarImage = image + self.callScreenState = callScreenState + self.update(transition: .immediate) } } self.currentPeer = peer @@ -788,14 +815,53 @@ private final class AdaptedCallVideoSource: VideoSource { } } + final class PixelBufferPool { + let width: Int + let height: Int + let pool: CVPixelBufferPool + + init?(width: Int, height: Int) { + self.width = width + self.height = height + + let bufferOptions: [String: Any] = [ + kCVPixelBufferPoolMinimumBufferCountKey as String: 4 as NSNumber + ] + let pixelBufferOptions: [String: Any] = [ + kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange as NSNumber, + kCVPixelBufferWidthKey as String: width as NSNumber, + kCVPixelBufferHeightKey as String: height as NSNumber, + kCVPixelBufferIOSurfacePropertiesKey as String: [:] as NSDictionary + ] + + var pool: CVPixelBufferPool? + CVPixelBufferPoolCreate(nil, bufferOptions as CFDictionary, pixelBufferOptions as CFDictionary, &pool) + guard let pool else { + return nil + } + self.pool = pool + } + } + + final class PixelBufferPoolState { + var pool: PixelBufferPool? + } + private static let queue = Queue(name: "AdaptedCallVideoSource") private var onUpdatedListeners = Bag<() -> Void>() private(set) var currentOutput: Output? private var textureCache: CVMetalTextureCache? + private var pixelBufferPoolState: QueueLocalObject + private var videoFrameDisposable: Disposable? init(videoStreamSignal: Signal) { + let pixelBufferPoolState = QueueLocalObject(queue: AdaptedCallVideoSource.queue, generate: { + return PixelBufferPoolState() + }) + self.pixelBufferPoolState = pixelBufferPoolState + CVMetalTextureCacheCreate(nil, nil, MetalEngine.shared.device, nil, &self.textureCache) self.videoFrameDisposable = (videoStreamSignal @@ -890,34 +956,62 @@ private final class AdaptedCallVideoSource: VideoSource { sourceId: sourceId ) case let .i420(i420Buffer): + guard let pixelBufferPoolState = pixelBufferPoolState.unsafeGet() else { + return + } + let width = i420Buffer.width let height = i420Buffer.height - let _ = width - let _ = height - return + let pool: PixelBufferPool? + if let current = pixelBufferPoolState.pool, current.width == width, current.height == height { + pool = current + } else { + pool = PixelBufferPool(width: width, height: height) + pixelBufferPoolState.pool = pool + } + guard let pool else { + return + } - /*var cvMetalTextureY: CVMetalTexture? - var status = CVMetalTextureCacheCreateTextureFromImage(nil, textureCache, nativeBuffer.pixelBuffer, nil, .r8Unorm, width, height, 0, &cvMetalTextureY) + let auxAttributes: [String: Any] = [kCVPixelBufferPoolAllocationThresholdKey as String: 5 as NSNumber] + var pixelBuffer: CVPixelBuffer? + let result = CVPixelBufferPoolCreatePixelBufferWithAuxAttributes(kCFAllocatorDefault, pool.pool, auxAttributes as CFDictionary, &pixelBuffer) + if result == kCVReturnWouldExceedAllocationThreshold { + print("kCVReturnWouldExceedAllocationThreshold, dropping frame") + return + } + guard let pixelBuffer else { + return + } + + if !copyI420BufferToNV12Buffer(buffer: i420Buffer, pixelBuffer: pixelBuffer) { + return + } + + var cvMetalTextureY: CVMetalTexture? + var status = CVMetalTextureCacheCreateTextureFromImage(nil, textureCache, pixelBuffer, nil, .r8Unorm, width, height, 0, &cvMetalTextureY) guard status == kCVReturnSuccess, let yTexture = CVMetalTextureGetTexture(cvMetalTextureY!) else { return } var cvMetalTextureUV: CVMetalTexture? - status = CVMetalTextureCacheCreateTextureFromImage(nil, textureCache, nativeBuffer.pixelBuffer, nil, .rg8Unorm, width / 2, height / 2, 1, &cvMetalTextureUV) + status = CVMetalTextureCacheCreateTextureFromImage(nil, textureCache, pixelBuffer, nil, .rg8Unorm, width / 2, height / 2, 1, &cvMetalTextureUV) guard status == kCVReturnSuccess, let uvTexture = CVMetalTextureGetTexture(cvMetalTextureUV!) else { return } output = Output( resolution: CGSize(width: CGFloat(yTexture.width), height: CGFloat(yTexture.height)), - y: yTexture, - uv: uvTexture, - dataBuffer: Output.NativeDataBuffer(pixelBuffer: nativeBuffer.pixelBuffer), + textureLayout: .biPlanar(Output.BiPlanarTextureLayout( + y: yTexture, + uv: uvTexture + )), + dataBuffer: Output.NativeDataBuffer(pixelBuffer: pixelBuffer), rotationAngle: rotationAngle, followsDeviceOrientation: followsDeviceOrientation, mirrorDirection: mirrorDirection, sourceId: sourceId - )*/ + ) default: return } diff --git a/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift b/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift index a2f37daa0ff..19b7211e2d5 100644 --- a/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift +++ b/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift @@ -157,7 +157,7 @@ private class CallStatusBarBackgroundNode: ASDisplayNode { } public class CallStatusBarNodeImpl: CallStatusBarNode { - public enum Content { + public enum Content: Equatable { case call(SharedAccountContext, Account, PresentationCall) case groupCall(SharedAccountContext, Account, PresentationGroupCall) @@ -167,6 +167,23 @@ public class CallStatusBarNodeImpl: CallStatusBarNode { return sharedContext } } + + public static func ==(lhs: Content, rhs: Content) -> Bool { + switch lhs { + case let .call(sharedContext, account, call): + if case let .call(rhsSharedContext, rhsAccount, rhsCall) = rhs, sharedContext === rhsSharedContext, account === rhsAccount, call === rhsCall { + return true + } else { + return false + } + case let .groupCall(sharedContext, account, groupCall): + if case let .groupCall(rhsSharedContext, rhsAccount, rhsGroupCall) = rhs, sharedContext === rhsSharedContext, account === rhsAccount, groupCall === rhsGroupCall { + return true + } else { + return false + } + } + } } private let backgroundNode: CallStatusBarBackgroundNode @@ -236,10 +253,12 @@ public class CallStatusBarNodeImpl: CallStatusBarNode { } public func update(content: Content) { - self.currentContent = content - self.backgroundNode.animationsEnabled = content.sharedContext.energyUsageSettings.fullTranslucency - if self.isCurrentlyInHierarchy { - self.update() + if self.currentContent != content { + self.currentContent = content + self.backgroundNode.animationsEnabled = content.sharedContext.energyUsageSettings.fullTranslucency + if self.isCurrentlyInHierarchy { + self.update() + } } } diff --git a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift index 310b8b36de9..0d1df5f992f 100644 --- a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift @@ -93,6 +93,7 @@ enum AccountStateMutationOperation { case ReadSecretOutbox(peerId: PeerId, maxTimestamp: Int32, actionTimestamp: Int32) case AddPeerInputActivity(chatPeerId: PeerActivitySpace, peerId: PeerId?, activity: PeerInputActivity?) case UpdatePinnedItemIds(PeerGroupId, AccountStateUpdatePinnedItemIdsOperation) + case UpdatePinnedSavedItemIds(AccountStateUpdatePinnedItemIdsOperation) case UpdatePinnedTopic(peerId: PeerId, threadId: Int64, isPinned: Bool) case UpdatePinnedTopicOrder(peerId: PeerId, threadIds: [Int64]) case ReadMessageContents(peerIdsAndMessageIds: (PeerId?, [Int32]), date: Int32?) @@ -571,6 +572,10 @@ struct AccountMutableState { self.addOperation(.UpdatePinnedItemIds(groupId, operation)) } + mutating func addUpdatePinnedSavedItemIds(operation: AccountStateUpdatePinnedItemIdsOperation) { + self.addOperation(.UpdatePinnedSavedItemIds(operation)) + } + mutating func addUpdatePinnedTopic(peerId: PeerId, threadId: Int64, isPinned: Bool) { self.addOperation(.UpdatePinnedTopic(peerId: peerId, threadId: threadId, isPinned: isPinned)) } @@ -665,7 +670,7 @@ struct AccountMutableState { mutating func addOperation(_ operation: AccountStateMutationOperation) { switch operation { - case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .UpdateWallpaper, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization: + case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .UpdateWallpaper, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization: break case let .AddMessages(messages, location): for message in messages { diff --git a/submodules/TelegramCore/Sources/Account/AccountManager.swift b/submodules/TelegramCore/Sources/Account/AccountManager.swift index 2c95e1eac39..6519c2dc1ba 100644 --- a/submodules/TelegramCore/Sources/Account/AccountManager.swift +++ b/submodules/TelegramCore/Sources/Account/AccountManager.swift @@ -201,6 +201,7 @@ private var declaredEncodables: Void = { declareEncodable(OutgoingMessageInfoAttribute.self, f: { OutgoingMessageInfoAttribute(decoder: $0) }) declareEncodable(ForwardSourceInfoAttribute.self, f: { ForwardSourceInfoAttribute(decoder: $0) }) declareEncodable(SourceReferenceMessageAttribute.self, f: { SourceReferenceMessageAttribute(decoder: $0) }) + declareEncodable(SourceAuthorInfoMessageAttribute.self, f: { SourceAuthorInfoMessageAttribute(decoder: $0) }) declareEncodable(EditedMessageAttribute.self, f: { EditedMessageAttribute(decoder: $0) }) declareEncodable(ReplyMarkupMessageAttribute.self, f: { ReplyMarkupMessageAttribute(decoder: $0) }) declareEncodable(OutgoingChatContextResultMessageAttribute.self, f: { OutgoingChatContextResultMessageAttribute(decoder: $0) }) diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 60e9e513044..c77129451d5 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -126,7 +126,7 @@ public func tagsForStoreMessage(incoming: Bool, attributes: [MessageAttribute], func apiMessagePeerId(_ messsage: Api.Message) -> PeerId? { switch messsage { - case let .message(_, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): let chatPeerId = messagePeerId return chatPeerId.peerId case let .messageEmpty(_, _, peerId): @@ -142,7 +142,7 @@ func apiMessagePeerId(_ messsage: Api.Message) -> PeerId? { func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { switch message { - case let .message(_, _, fromId, chatPeerId, fwdHeader, viaBotId, replyTo, _, _, media, _, entities, _, _, _, _, _, _, _, _, _): + case let .message(_, _, fromId, chatPeerId, savedPeerId, fwdHeader, viaBotId, replyTo, _, _, media, _, entities, _, _, _, _, _, _, _, _, _): let peerId: PeerId = chatPeerId.peerId var result = [peerId] @@ -155,19 +155,26 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { if let fwdHeader = fwdHeader { switch fwdHeader { - case let .messageFwdHeader(_, fromId, _, _, _, _, savedFromPeer, _, _): + case let .messageFwdHeader(_, fromId, _, _, _, _, savedFromPeer, _, savedFromId, _, _, _): if let fromId = fromId { result.append(fromId.peerId) } if let savedFromPeer = savedFromPeer { result.append(savedFromPeer.peerId) } + if let savedFromId = savedFromId { + result.append(savedFromId.peerId) + } } } if let viaBotId = viaBotId { result.append(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(viaBotId))) } + + if let savedPeerId = savedPeerId { + result.append(savedPeerId.peerId) + } if let media = media { switch media { @@ -256,7 +263,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { func apiMessageAssociatedMessageIds(_ message: Api.Message) -> (replyIds: ReferencedReplyMessageIds, generalIds: [MessageId])? { switch message { - case let .message(_, id, _, chatPeerId, _, _, replyTo, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, id, _, chatPeerId, _, _, _, replyTo, _, _, _, _, _, _, _, _, _, _, _, _, _, _): if let replyTo = replyTo { let peerId: PeerId = chatPeerId.peerId @@ -342,7 +349,15 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI return (mediaFile, ttlSeconds, (flags & (1 << 3)) != 0, (flags & (1 << 4)) != 0, nil) } } else { - return (TelegramMediaExpiredContent(data: .file), nil, nil, nil, nil) + var data: TelegramMediaExpiredContentData + if (flags & (1 << 7)) != 0 { + data = .videoMessage + } else if (flags & (1 << 8)) != 0 { + data = .voiceMessage + } else { + data = .file + } + return (TelegramMediaExpiredContent(data: data), nil, nil, nil, nil) } case let .messageMediaWebPage(flags, webpage): if let mediaWebpage = telegramMediaWebpageFromApiWebpage(webpage) { @@ -582,7 +597,7 @@ func messageTextEntitiesFromApiEntities(_ entities: [Api.MessageEntity]) -> [Mes extension StoreMessage { convenience init?(apiMessage: Api.Message, accountPeerId: PeerId, peerIsForum: Bool, namespace: MessageId.Namespace = Namespaces.Message.Cloud) { switch apiMessage { - case let .message(flags, id, fromId, chatPeerId, fwdFrom, viaBotId, replyTo, date, message, media, replyMarkup, entities, views, forwards, replies, editDate, postAuthor, groupingId, reactions, restrictionReason, ttlPeriod): + case let .message(flags, id, fromId, chatPeerId, savedPeerId, fwdFrom, viaBotId, replyTo, date, message, media, replyMarkup, entities, views, forwards, replies, editDate, postAuthor, groupingId, reactions, restrictionReason, ttlPeriod): let resolvedFromId = fromId?.peerId ?? chatPeerId.peerId let peerId: PeerId @@ -622,17 +637,17 @@ extension StoreMessage { if isForumTopic { let threadIdValue = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: replyToTopId) threadMessageId = threadIdValue - threadId = makeMessageThreadId(threadIdValue) + threadId = Int64(threadIdValue.id) } } else { if peerId.namespace == Namespaces.Peer.CloudChannel { let threadIdValue = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: replyToTopId) threadMessageId = threadIdValue - threadId = makeMessageThreadId(threadIdValue) + threadId = Int64(threadIdValue.id) } else { let threadIdValue = MessageId(peerId: replyToPeerId?.peerId ?? peerId, namespace: Namespaces.Message.Cloud, id: replyToTopId) threadMessageId = threadIdValue - threadId = makeMessageThreadId(threadIdValue) + threadId = Int64(threadIdValue.id) } } } else if peerId.namespace == Namespaces.Peer.CloudChannel { @@ -641,11 +656,11 @@ extension StoreMessage { if peerIsForum { if isForumTopic { threadMessageId = threadIdValue - threadId = makeMessageThreadId(threadIdValue) + threadId = Int64(threadIdValue.id) } } else { threadMessageId = threadIdValue - threadId = makeMessageThreadId(threadIdValue) + threadId = Int64(threadIdValue.id) } } attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId, quote: quote, isQuote: isQuote)) @@ -663,10 +678,9 @@ extension StoreMessage { } var forwardInfo: StoreMessageForwardInfo? - var savedFromPeerId: PeerId? if let fwdFrom = fwdFrom { switch fwdFrom { - case let .messageFwdHeader(flags, fromId, fromName, date, channelPost, postAuthor, savedFromPeer, savedFromMsgId, psaType): + case let .messageFwdHeader(flags, fromId, fromName, date, channelPost, postAuthor, savedFromPeer, savedFromMsgId, savedFromId, savedFromName, savedDate, psaType): var forwardInfoFlags: MessageForwardInfo.Flags = [] let isImported = (flags & (1 << 7)) != 0 if isImported { @@ -690,16 +704,20 @@ extension StoreMessage { authorId = fromId.peerId } } + + let originalOutgoing = (flags & (1 << 11)) != 0 if let savedFromPeer = savedFromPeer, let savedFromMsgId = savedFromMsgId { let peerId: PeerId = savedFromPeer.peerId - savedFromPeerId = peerId let messageId: MessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: savedFromMsgId) attributes.append(SourceReferenceMessageAttribute(messageId: messageId)) } + if savedFromId != nil || savedFromName != nil || savedDate != nil || originalOutgoing { + attributes.append(SourceAuthorInfoMessageAttribute(originalAuthor: savedFromId?.peerId, originalAuthorName: savedFromName, orignalDate: savedDate, originalOutgoing: originalOutgoing)) + } if let authorId = authorId { - forwardInfo = StoreMessageForwardInfo(authorId: authorId, sourceId: sourceId, sourceMessageId: sourceMessageId, date: date, authorSignature: postAuthor, psaType: psaType, flags: forwardInfoFlags) + forwardInfo = StoreMessageForwardInfo(authorId: authorId, sourceId: sourceId, sourceMessageId: sourceMessageId, date: date, authorSignature: postAuthor, psaType: psaType, flags: forwardInfoFlags) } else if let sourceId = sourceId { forwardInfo = StoreMessageForwardInfo(authorId: sourceId, sourceId: sourceId, sourceMessageId: sourceMessageId, date: date, authorSignature: postAuthor, psaType: psaType, flags: forwardInfoFlags) } else if let postAuthor = postAuthor ?? fromName { @@ -708,8 +726,8 @@ extension StoreMessage { } } - if peerId == accountPeerId, let savedFromPeerId = savedFromPeerId { - threadId = savedFromPeerId.toInt64() + if peerId == accountPeerId, let savedPeerId = savedPeerId { + threadId = savedPeerId.peerId.toInt64() } let messageText = message @@ -911,12 +929,12 @@ extension StoreMessage { let threadIdValue = MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToTopId) threadMessageId = threadIdValue if replyPeerId == peerId { - threadId = makeMessageThreadId(threadIdValue) + threadId = Int64(threadIdValue.id) } } else if peerId.namespace == Namespaces.Peer.CloudChannel { let threadIdValue = MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId) threadMessageId = threadIdValue - threadId = makeMessageThreadId(threadIdValue) + threadId = Int64(threadIdValue.id) } switch action { case .messageActionTopicEdit: diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift index 15899cf081d..9daed9bc012 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift @@ -68,10 +68,10 @@ extension StickerPackReference { self = .emojiGenericAnimations case .inputStickerSetEmojiDefaultStatuses: self = .iconStatusEmoji + case .inputStickerSetEmojiChannelDefaultStatuses: + self = .iconChannelStatusEmoji case .inputStickerSetEmojiDefaultTopicIcons: self = .iconTopicEmoji - case .inputStickerSetEmojiChannelDefaultStatuses: - return nil } } } diff --git a/submodules/TelegramCore/Sources/ForumChannels.swift b/submodules/TelegramCore/Sources/ForumChannels.swift index db874759a91..97993eb59b5 100644 --- a/submodules/TelegramCore/Sources/ForumChannels.swift +++ b/submodules/TelegramCore/Sources/ForumChannels.swift @@ -458,33 +458,51 @@ public enum SetForumChannelTopicPinnedError { } func _internal_setForumChannelPinnedTopics(account: Account, id: EnginePeer.Id, threadIds: [Int64]) -> Signal { - return account.postbox.transaction { transaction -> Api.InputChannel? in - guard let inputChannel = transaction.getPeer(id).flatMap(apiInputChannel) else { - return nil + if id == account.peerId { + return account.postbox.transaction { transaction -> [Api.InputDialogPeer] in + transaction.setPeerPinnedThreads(peerId: id, threadIds: threadIds) + + return threadIds.compactMap { transaction.getPeer(PeerId($0)).flatMap(apiInputPeer).flatMap({ .inputDialogPeer(peer: $0) }) } } - - transaction.setPeerPinnedThreads(peerId: id, threadIds: threadIds) - - return inputChannel - } - |> castError(SetForumChannelTopicPinnedError.self) - |> mapToSignal { inputChannel -> Signal in - guard let inputChannel = inputChannel else { - return .fail(.generic) + |> castError(SetForumChannelTopicPinnedError.self) + |> mapToSignal { inputPeers -> Signal in + return account.network.request(Api.functions.messages.reorderPinnedSavedDialogs(flags: 1 << 0, order: inputPeers)) + |> mapError { _ -> SetForumChannelTopicPinnedError in + return .generic + } + |> mapToSignal { _ -> Signal in + return .complete() + } } - - return account.network.request(Api.functions.channels.reorderPinnedForumTopics( - flags: 1 << 0, - channel: inputChannel, - order: threadIds.map(Int32.init(clamping:)) - )) - |> mapError { _ -> SetForumChannelTopicPinnedError in - return .generic + } else { + return account.postbox.transaction { transaction -> Api.InputChannel? in + guard let inputChannel = transaction.getPeer(id).flatMap(apiInputChannel) else { + return nil + } + + transaction.setPeerPinnedThreads(peerId: id, threadIds: threadIds) + + return inputChannel } - |> mapToSignal { result -> Signal in - account.stateManager.addUpdates(result) + |> castError(SetForumChannelTopicPinnedError.self) + |> mapToSignal { inputChannel -> Signal in + guard let inputChannel = inputChannel else { + return .fail(.generic) + } - return .complete() + return account.network.request(Api.functions.channels.reorderPinnedForumTopics( + flags: 1 << 0, + channel: inputChannel, + order: threadIds.map(Int32.init(clamping:)) + )) + |> mapError { _ -> SetForumChannelTopicPinnedError in + return .generic + } + |> mapToSignal { result -> Signal in + account.stateManager.addUpdates(result) + + return .complete() + } } } } @@ -516,7 +534,7 @@ func _internal_setChannelForumMode(postbox: Postbox, network: Network, stateMana struct LoadMessageHistoryThreadsResult { struct Item { var threadId: Int64 - var data: MessageHistoryThreadData + var data: MessageHistoryThreadData? var topMessage: Int32 var unreadMentionsCount: Int32 var unreadReactionsCount: Int32 @@ -524,7 +542,7 @@ struct LoadMessageHistoryThreadsResult { init( threadId: Int64, - data: MessageHistoryThreadData, + data: MessageHistoryThreadData?, topMessage: Int32, unreadMentionsCount: Int32, unreadReactionsCount: Int32, @@ -570,95 +588,175 @@ enum LoadMessageHistoryThreadsError { case generic } -func _internal_requestMessageHistoryThreads(accountPeerId: PeerId, postbox: Postbox, network: Network, peerId: PeerId, query: String?, offsetIndex: StoredPeerThreadCombinedState.Index?, limit: Int) -> Signal { - let signal: Signal = postbox.transaction { transaction -> Api.InputChannel? in - guard let channel = transaction.getPeer(peerId) as? TelegramChannel else { - return nil - } - if !channel.flags.contains(.isForum) { - return nil - } - return apiInputChannel(channel) +public func _internal_fillSavedMessageHistory(accountPeerId: PeerId, postbox: Postbox, network: Network) -> Signal { + enum PassResult { + case restart } - |> castError(LoadMessageHistoryThreadsError.self) - |> mapToSignal { inputChannel -> Signal in - guard let inputChannel = inputChannel else { - return .fail(.generic) + let fillSignal = (postbox.transaction { transaction -> Range? in + let holes = transaction.getHoles(peerId: accountPeerId, namespace: Namespaces.Message.Cloud) + return holes.rangeView.last + } + |> castError(PassResult.self) + |> mapToSignal { range -> Signal in + if let range { + return fetchMessageHistoryHole( + accountPeerId: accountPeerId, + source: .network(network), + postbox: postbox, + peerInput: .direct(peerId: accountPeerId, threadId: nil), + namespace: Namespaces.Message.Cloud, + direction: .range( + start: MessageId(peerId: accountPeerId, namespace: Namespaces.Message.Cloud, id: Int32(range.upperBound) - 1), + end: MessageId(peerId: accountPeerId, namespace: Namespaces.Message.Cloud, id: Int32(range.lowerBound) - 1) + ), + space: .everywhere, + count: 100 + ) + |> ignoreValues + |> castError(PassResult.self) + |> then(.fail(.restart)) + } else { + return .complete() } - var flags: Int32 = 0 + }) + |> restartIfError + + let applySignal = postbox.transaction { transaction -> Void in + var topMessages: [Int64: Message] = [:] + transaction.scanTopMessages(peerId: accountPeerId, namespace: Namespaces.Message.Cloud, limit: 100000, { message in + if let threadId = message.threadId { + if let current = topMessages[threadId] { + if current.id < message.id { + topMessages[threadId] = message + } + } else { + topMessages[threadId] = message + } + } + + return true + }) + - if query != nil { - flags |= 1 << 0 + var items: [LoadMessageHistoryThreadsResult.Item] = [] + for message in topMessages.values.sorted(by: { $0.id > $1.id }) { + guard let threadId = message.threadId else { + continue + } + items.append(LoadMessageHistoryThreadsResult.Item( + threadId: threadId, + data: MessageHistoryThreadData( + creationDate: 0, + isOwnedByMe: true, + author: accountPeerId, + info: EngineMessageHistoryThread.Info(title: "", icon: nil, iconColor: 0), + incomingUnreadCount: 0, + maxIncomingReadId: 0, + maxKnownMessageId: 0, + maxOutgoingReadId: 0, + isClosed: false, + isHidden: false, + notificationSettings: TelegramPeerNotificationSettings.defaultSettings + ), + topMessage: message.id.id, + unreadMentionsCount: 0, + unreadReactionsCount: 0, + index: StoredPeerThreadCombinedState.Index(timestamp: message.timestamp, threadId: threadId, messageId: message.id.id) + )) } + let result = LoadMessageHistoryThreadsResult( + peerId: accountPeerId, + items: items, + messages: [], + pinnedThreadIds: nil, + combinedState: PeerThreadCombinedState(validIndexBoundary: StoredPeerThreadCombinedState.Index(timestamp: 0, threadId: 0, messageId: 1)), + users: [], + chats: [] + ) + + applyLoadMessageHistoryThreadsResults(accountPeerId: accountPeerId, transaction: transaction, results: [result]) + } + |> ignoreValues + + return fillSignal + |> then(applySignal) +} + +func _internal_requestMessageHistoryThreads(accountPeerId: PeerId, postbox: Postbox, network: Network, peerId: PeerId, query: String?, offsetIndex: StoredPeerThreadCombinedState.Index?, limit: Int) -> Signal { + if peerId == accountPeerId { + var flags: Int32 = 0 + flags = 0 + var offsetDate: Int32 = 0 var offsetId: Int32 = 0 - var offsetTopic: Int32 = 0 + var offsetPeer: Api.InputPeer = .inputPeerEmpty if let offsetIndex = offsetIndex { offsetDate = offsetIndex.timestamp offsetId = offsetIndex.messageId - offsetTopic = Int32(clamping: offsetIndex.threadId) + //TODO:api + offsetPeer = .inputPeerEmpty } - let signal: Signal = network.request(Api.functions.channels.getForumTopics( + let signal: Signal = network.request(Api.functions.messages.getSavedDialogs( flags: flags, - channel: inputChannel, - q: query, offsetDate: offsetDate, offsetId: offsetId, - offsetTopic: offsetTopic, - limit: Int32(limit) + offsetPeer: offsetPeer, + limit: Int32(limit), + hash: 0 )) - |> mapError { _ -> LoadMessageHistoryThreadsError in - return .generic + |> `catch` { error -> Signal in + if error.errorDescription == "SAVED_DIALOGS_UNSUPPORTED" { + return .never() + } else { + return .fail(.generic) + } } |> mapToSignal { result -> Signal in switch result { - case let .forumTopics(_, _, topics, messages, chats, users, pts): + case .savedDialogs(let dialogs, let messages, let chats, let users), .savedDialogsSlice(_, let dialogs, let messages, let chats, let users): var items: [LoadMessageHistoryThreadsResult.Item] = [] var pinnedIds: [Int64] = [] let addedMessages = messages.compactMap { message -> StoreMessage? in - return StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: true) + return StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: false) } - let _ = pts var minIndex: StoredPeerThreadCombinedState.Index? - for topic in topics { - switch topic { - case let .forumTopic(flags, id, date, title, iconColor, iconEmojiId, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, unreadMentionsCount, unreadReactionsCount, fromId, notifySettings, draft): - let _ = draft - - if (flags & (1 << 3)) != 0 { - pinnedIds.append(Int64(id)) + for dialog in dialogs { + switch dialog { + case let .savedDialog(flags, peer, topMessage): + if (flags & (1 << 2)) != 0 { + pinnedIds.append(peer.peerId.toInt64()) } let data = MessageHistoryThreadData( - creationDate: date, - isOwnedByMe: (flags & (1 << 1)) != 0, - author: fromId.peerId, + creationDate: 0, + isOwnedByMe: true, + author: peer.peerId, info: EngineMessageHistoryThread.Info( - title: title, - icon: iconEmojiId == 0 ? nil : iconEmojiId, - iconColor: iconColor + title: "", + icon: nil, + iconColor: 0 ), - incomingUnreadCount: unreadCount, - maxIncomingReadId: readInboxMaxId, + incomingUnreadCount: 0, + maxIncomingReadId: 0, maxKnownMessageId: topMessage, - maxOutgoingReadId: readOutboxMaxId, - isClosed: (flags & (1 << 2)) != 0, - isHidden: (flags & (1 << 6)) != 0, - notificationSettings: TelegramPeerNotificationSettings(apiSettings: notifySettings) + maxOutgoingReadId: 0, + isClosed: false, + isHidden: false, + notificationSettings: TelegramPeerNotificationSettings.defaultSettings ) - var topTimestamp = date + var topTimestamp: Int32 = 1 for message in addedMessages { - if message.id.peerId == peerId && message.threadId == Int64(id) { + if message.id.peerId == peerId && message.threadId == peer.peerId.toInt64() { topTimestamp = max(topTimestamp, message.timestamp) } } - let topicIndex = StoredPeerThreadCombinedState.Index(timestamp: topTimestamp, threadId: Int64(id), messageId: topMessage) + let topicIndex = StoredPeerThreadCombinedState.Index(timestamp: topTimestamp, threadId: peer.peerId.toInt64(), messageId: topMessage) if let minIndexValue = minIndex { if topicIndex < minIndexValue { minIndex = topicIndex @@ -668,15 +766,13 @@ func _internal_requestMessageHistoryThreads(accountPeerId: PeerId, postbox: Post } items.append(LoadMessageHistoryThreadsResult.Item( - threadId: Int64(id), + threadId: peer.peerId.toInt64(), data: data, topMessage: topMessage, - unreadMentionsCount: unreadMentionsCount, - unreadReactionsCount: unreadReactionsCount, + unreadMentionsCount: 0, + unreadReactionsCount: 0, index: topicIndex )) - case .forumTopicDeleted: - break } } @@ -686,7 +782,7 @@ func _internal_requestMessageHistoryThreads(accountPeerId: PeerId, postbox: Post } var nextIndex: StoredPeerThreadCombinedState.Index - if topics.count != 0 { + if dialogs.count != 0 { nextIndex = minIndex ?? StoredPeerThreadCombinedState.Index(timestamp: 0, threadId: 0, messageId: 1) } else { nextIndex = StoredPeerThreadCombinedState.Index(timestamp: 0, threadId: 0, messageId: 1) @@ -706,12 +802,154 @@ func _internal_requestMessageHistoryThreads(accountPeerId: PeerId, postbox: Post users: users, chats: chats )) + case .savedDialogsNotModified: + return .complete() + } + } + return signal + } else { + let signal: Signal = postbox.transaction { transaction -> Api.InputChannel? in + guard let channel = transaction.getPeer(peerId) as? TelegramChannel else { + return nil + } + if !channel.flags.contains(.isForum) { + return nil + } + return apiInputChannel(channel) + } + |> castError(LoadMessageHistoryThreadsError.self) + |> mapToSignal { inputChannel -> Signal in + guard let inputChannel = inputChannel else { + return .fail(.generic) + } + var flags: Int32 = 0 + + if query != nil { + flags |= 1 << 0 + } + + var offsetDate: Int32 = 0 + var offsetId: Int32 = 0 + var offsetTopic: Int32 = 0 + if let offsetIndex = offsetIndex { + offsetDate = offsetIndex.timestamp + offsetId = offsetIndex.messageId + offsetTopic = Int32(clamping: offsetIndex.threadId) + } + let signal: Signal = network.request(Api.functions.channels.getForumTopics( + flags: flags, + channel: inputChannel, + q: query, + offsetDate: offsetDate, + offsetId: offsetId, + offsetTopic: offsetTopic, + limit: Int32(limit) + )) + |> mapError { _ -> LoadMessageHistoryThreadsError in + return .generic + } + |> mapToSignal { result -> Signal in + switch result { + case let .forumTopics(_, _, topics, messages, chats, users, pts): + var items: [LoadMessageHistoryThreadsResult.Item] = [] + var pinnedIds: [Int64] = [] + + let addedMessages = messages.compactMap { message -> StoreMessage? in + return StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: true) + } + + let _ = pts + var minIndex: StoredPeerThreadCombinedState.Index? + + for topic in topics { + switch topic { + case let .forumTopic(flags, id, date, title, iconColor, iconEmojiId, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, unreadMentionsCount, unreadReactionsCount, fromId, notifySettings, draft): + let _ = draft + + if (flags & (1 << 3)) != 0 { + pinnedIds.append(Int64(id)) + } + + let data = MessageHistoryThreadData( + creationDate: date, + isOwnedByMe: (flags & (1 << 1)) != 0, + author: fromId.peerId, + info: EngineMessageHistoryThread.Info( + title: title, + icon: iconEmojiId == 0 ? nil : iconEmojiId, + iconColor: iconColor + ), + incomingUnreadCount: unreadCount, + maxIncomingReadId: readInboxMaxId, + maxKnownMessageId: topMessage, + maxOutgoingReadId: readOutboxMaxId, + isClosed: (flags & (1 << 2)) != 0, + isHidden: (flags & (1 << 6)) != 0, + notificationSettings: TelegramPeerNotificationSettings(apiSettings: notifySettings) + ) + + var topTimestamp = date + for message in addedMessages { + if message.id.peerId == peerId && message.threadId == Int64(id) { + topTimestamp = max(topTimestamp, message.timestamp) + } + } + + let topicIndex = StoredPeerThreadCombinedState.Index(timestamp: topTimestamp, threadId: Int64(id), messageId: topMessage) + if let minIndexValue = minIndex { + if topicIndex < minIndexValue { + minIndex = topicIndex + } + } else { + minIndex = topicIndex + } + + items.append(LoadMessageHistoryThreadsResult.Item( + threadId: Int64(id), + data: data, + topMessage: topMessage, + unreadMentionsCount: unreadMentionsCount, + unreadReactionsCount: unreadReactionsCount, + index: topicIndex + )) + case .forumTopicDeleted: + break + } + } + + var pinnedThreadIds: [Int64]? + if offsetIndex == nil { + pinnedThreadIds = pinnedIds + } + + var nextIndex: StoredPeerThreadCombinedState.Index + if topics.count != 0 { + nextIndex = minIndex ?? StoredPeerThreadCombinedState.Index(timestamp: 0, threadId: 0, messageId: 1) + } else { + nextIndex = StoredPeerThreadCombinedState.Index(timestamp: 0, threadId: 0, messageId: 1) + } + if let offsetIndex = offsetIndex, nextIndex == offsetIndex { + nextIndex = StoredPeerThreadCombinedState.Index(timestamp: 0, threadId: 0, messageId: 1) + } + + let combinedState = PeerThreadCombinedState(validIndexBoundary: nextIndex) + + return .single(LoadMessageHistoryThreadsResult( + peerId: peerId, + items: items, + messages: addedMessages, + pinnedThreadIds: pinnedThreadIds, + combinedState: combinedState, + users: users, + chats: chats + )) + } } + return signal } + return signal } - - return signal } func applyLoadMessageHistoryThreadsResults(accountPeerId: PeerId, transaction: Transaction, results: [LoadMessageHistoryThreadsResult]) { @@ -722,9 +960,16 @@ func applyLoadMessageHistoryThreadsResults(accountPeerId: PeerId, transaction: T let _ = InternalAccountState.addMessages(transaction: transaction, messages: result.messages, location: .Random) for item in result.items { - guard let info = StoredMessageHistoryThreadInfo(item.data) else { + let info: StoredMessageHistoryThreadInfo? + if let data = item.data { + info = StoredMessageHistoryThreadInfo(data) + } else { + info = telegramPostboxSeedConfiguration.automaticThreadIndexInfo(result.peerId, item.threadId) + } + guard let info else { continue } + transaction.setMessageHistoryThreadInfo(peerId: result.peerId, threadId: item.threadId, info: info) transaction.replaceMessageTagSummary(peerId: result.peerId, threadId: item.threadId, tagMask: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud, count: item.unreadMentionsCount, maxId: item.topMessage) @@ -864,6 +1109,9 @@ public func _internal_searchForumTopics(account: Account, peerId: EnginePeer.Id, guard let index = item.index else { continue } + guard let itemData = item.data else { + continue + } items.append(EngineChatList.Item( id: .forum(item.threadId), index: .forum(pinnedIndex: .none, timestamp: index.timestamp, threadId: index.threadId, namespace: Namespaces.Message.Cloud, id: index.messageId), @@ -878,10 +1126,10 @@ public func _internal_searchForumTopics(account: Account, peerId: EnginePeer.Id, hasUnseenReactions: false, forumTopicData: EngineChatList.ForumTopicData( id: item.threadId, - title: item.data.info.title, - iconFileId: item.data.info.icon, - iconColor: item.data.info.iconColor, - maxOutgoingReadMessageId: EngineMessage.Id(peerId: peerId, namespace: Namespaces.Message.Cloud, id: item.data.maxOutgoingReadId), + title: itemData.info.title, + iconFileId: itemData.info.icon, + iconColor: itemData.info.iconColor, + maxOutgoingReadMessageId: EngineMessage.Id(peerId: peerId, namespace: Namespaces.Message.Cloud, id: itemData.maxOutgoingReadId), isUnread: false ), topForumTopicItems: [], diff --git a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift index 1fb83a9ee5c..a54d8a540bf 100644 --- a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift +++ b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift @@ -839,6 +839,8 @@ func revalidateMediaResourceReference(accountPeerId: PeerId, postbox: Postbox, n } if let updatedResource = findUpdatedMediaResource(media: media, previousMedia: nil, resource: resource) { return .single(RevalidatedMediaResource(updatedResource: updatedResource, updatedReference: nil)) + } else if let alternativeMedia = item.alternativeMedia, let updatedResource = findUpdatedMediaResource(media: alternativeMedia, previousMedia: nil, resource: resource) { + return .single(RevalidatedMediaResource(updatedResource: updatedResource, updatedReference: nil)) } else { return .fail(.generic) } diff --git a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift index 1f5827d62b4..73a643cec61 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift @@ -145,6 +145,15 @@ public enum EnqueueMessage { return nil } } + + public var attributes: [MessageAttribute] { + switch self { + case let .message(_, attributes, _, _, _, _, _, _, _, _): + return attributes + case let .forward(_, _, _, attributes, _, _): + return attributes + } + } } private extension EnqueueMessage { @@ -579,7 +588,9 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, var quote = replyToMessageId.quote let isQuote = quote != nil if let replyMessage = transaction.getMessage(replyToMessageId.messageId) { - threadMessageId = replyMessage.effectiveReplyThreadMessageId + if replyMessage.id.namespace == Namespaces.Message.Cloud, let threadId = replyMessage.threadId { + threadMessageId = MessageId(peerId: replyMessage.id.peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)) + } if quote == nil, replyToMessageId.messageId.peerId != peerId { let nsText = replyMessage.text as NSString var replyMedia: Media? @@ -718,14 +729,14 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, threadId = threadIdValue } else { if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .group = channel.info { - threadId = makeMessageThreadId(replyToMessageId.messageId) + threadId = Int64(replyToMessageId.messageId.id) } } } else { threadId = threadIdValue } } else if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .group = channel.info { - threadId = makeMessageThreadId(replyToMessageId.messageId) + threadId = Int64(replyToMessageId.messageId.id) } } } @@ -908,7 +919,7 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, } } else if let attribute = attribute as? ReplyMessageAttribute { if let threadMessageId = attribute.threadMessageId { - threadId = makeMessageThreadId(threadMessageId) + threadId = Int64(threadMessageId.id) } } else if let attribute = attribute as? SendAsMessageAttribute { if let peer = transaction.getPeer(attribute.peerId) { diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift index 0c3352a2106..0c4dc8f3b34 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift @@ -98,6 +98,7 @@ func messageContentToUpload(accountPeerId: PeerId, network: Network, postbox: Po } else if let media = media.first as? TelegramMediaWebpage, case let .Loaded(content) = media.content { return .signal(postbox.transaction { transaction -> PendingMessageUploadedContentResult in var flags: Int32 = 0 + flags |= 1 << 2 if let attribute = attributes.first(where: { $0 is WebpagePreviewMessageAttribute }) as? WebpagePreviewMessageAttribute { if let forceLargeMedia = attribute.forceLargeMedia { if forceLargeMedia { @@ -241,6 +242,7 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(inputDice, text), reuploadInfo: nil, cacheReferenceKey: nil))) } else if let media = media as? TelegramMediaWebpage, case let .Loaded(content) = media.content { var flags: Int32 = 0 + flags |= 1 << 2 if let attribute = attributes.first(where: { $0 is WebpagePreviewMessageAttribute }) as? WebpagePreviewMessageAttribute { if let forceLargeMedia = attribute.forceLargeMedia { if forceLargeMedia { diff --git a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift index 5e928d1c815..2ca7884bc28 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift @@ -320,7 +320,7 @@ private func sendUploadedMessageContent( var forwardSourceInfoAttribute: ForwardSourceInfoAttribute? var messageEntities: [Api.MessageEntity]? var replyMessageId: Int32? = threadId.flatMap { threadId in - makeThreadIdMessageId(peerId: peerId, threadId: threadId).id + return Int32(clamping: threadId) } var replyToStoryId: StoryId? var scheduleTime: Int32? diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 9853581311e..b290d3f72d7 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1444,7 +1444,7 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: case let .updateChannelUserTyping(_, channelId, topMsgId, userId, type): if let date = updatesDate, date + 60 > serverTime { let channelPeerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)) - let threadId = topMsgId.flatMap { makeMessageThreadId(MessageId(peerId: channelPeerId, namespace: Namespaces.Message.Cloud, id: $0)) } + let threadId = topMsgId.flatMap { Int64($0) } let activity = PeerInputActivity(apiType: type, peerId: nil, timestamp: date) var category: PeerActivitySpace.Category = .global @@ -1490,6 +1490,27 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: } else { updatedState.addUpdatePinnedItemIds(groupId: groupId, operation: .sync) } + case let .updateSavedDialogPinned(flags, peer): + if case let .dialogPeer(peer) = peer { + if (flags & (1 << 0)) != 0 { + updatedState.addUpdatePinnedSavedItemIds(operation: .pin(.peer(peer.peerId))) + } else { + updatedState.addUpdatePinnedSavedItemIds(operation: .unpin(.peer(peer.peerId))) + } + } + case let .updatePinnedSavedDialogs(_, order): + if let order = order { + updatedState.addUpdatePinnedSavedItemIds(operation: .reorder(order.compactMap { + switch $0 { + case let .dialogPeer(peer): + return .peer(peer.peerId) + case .dialogPeerFolder: + return nil + } + })) + } else { + updatedState.addUpdatePinnedSavedItemIds(operation: .sync) + } case let .updateChannelPinnedTopic(flags, channelId, topicId): let isPinned = (flags & (1 << 0)) != 0 updatedState.addUpdatePinnedTopic(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), threadId: Int64(topicId), isPinned: isPinned) @@ -3231,7 +3252,7 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation]) var currentAddScheduledMessages: OptimizeAddMessagesState? for operation in operations { switch operation { - case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateWallpaper: + case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateWallpaper: if let currentAddMessages = currentAddMessages, !currentAddMessages.messages.isEmpty { result.append(.AddMessages(currentAddMessages.messages, currentAddMessages.location)) } @@ -3480,16 +3501,16 @@ func replayFinalState( var removedCount: Int = 0 var peers: [ReplyThreadUserMessage] = [] } - var messageThreadStatsDifferences: [MessageId: MessageThreadStatsRecord] = [:] - func addMessageThreadStatsDifference(threadMessageId: MessageId, remove: Int, addedMessagePeer: PeerId?, addedMessageId: MessageId?, isOutgoing: Bool) { - if let value = messageThreadStatsDifferences[threadMessageId] { + var messageThreadStatsDifferences: [MessageThreadKey: MessageThreadStatsRecord] = [:] + func addMessageThreadStatsDifference(threadKey: MessageThreadKey, remove: Int, addedMessagePeer: PeerId?, addedMessageId: MessageId?, isOutgoing: Bool) { + if let value = messageThreadStatsDifferences[threadKey] { value.removedCount += remove if let addedMessagePeer = addedMessagePeer, let addedMessageId = addedMessageId { value.peers.append(ReplyThreadUserMessage(id: addedMessagePeer, messageId: addedMessageId, isOutgoing: isOutgoing)) } } else { let value = MessageThreadStatsRecord() - messageThreadStatsDifferences[threadMessageId] = value + messageThreadStatsDifferences[threadKey] = value value.removedCount = remove if let addedMessagePeer = addedMessagePeer, let addedMessageId = addedMessageId { value.peers.append(ReplyThreadUserMessage(id: addedMessagePeer, messageId: addedMessageId, isOutgoing: isOutgoing)) @@ -3540,10 +3561,9 @@ func replayFinalState( } } - let messageThreadId = makeThreadIdMessageId(peerId: message.id.peerId, threadId: threadId) if id.peerId.namespace == Namespaces.Peer.CloudChannel { if !transaction.messageExists(id: id) { - addMessageThreadStatsDifference(threadMessageId: messageThreadId, remove: 0, addedMessagePeer: message.authorId, addedMessageId: id, isOutgoing: !message.flags.contains(.Incoming)) + addMessageThreadStatsDifference(threadKey: MessageThreadKey(peerId: message.id.peerId, threadId: threadId), remove: 0, addedMessagePeer: message.authorId, addedMessageId: id, isOutgoing: !message.flags.contains(.Incoming)) } } @@ -3736,7 +3756,7 @@ func replayFinalState( deletedMessageIds.append(contentsOf: ids.map { .global($0) }) case let .DeleteMessages(ids): _internal_deleteMessages(transaction: transaction, mediaBox: mediaBox, ids: ids, manualAddMessageThreadStatsDifference: { id, add, remove in - addMessageThreadStatsDifference(threadMessageId: id, remove: remove, addedMessagePeer: nil, addedMessageId: nil, isOutgoing: false) + addMessageThreadStatsDifference(threadKey: id, remove: remove, addedMessagePeer: nil, addedMessageId: nil, isOutgoing: false) }) deletedMessageIds.append(contentsOf: ids.map { .messageId($0) }) case let .UpdateMinAvailableMessage(id): @@ -4229,6 +4249,44 @@ func replayFinalState( case .sync: addSynchronizePinnedChatsOperation(transaction: transaction, groupId: groupId) } + case let .UpdatePinnedSavedItemIds(pinnedOperation): + switch pinnedOperation { + case let .pin(itemId): + switch itemId { + case let .peer(peerId): + var currentItemIds = transaction.getPeerPinnedThreads(peerId: accountPeerId) + if !currentItemIds.contains(peerId.toInt64()) { + currentItemIds.insert(peerId.toInt64(), at: 0) + transaction.setPeerPinnedThreads(peerId: accountPeerId, threadIds: currentItemIds) + } + } + case let .unpin(itemId): + switch itemId { + case let .peer(peerId): + var currentItemIds = transaction.getPeerPinnedThreads(peerId: accountPeerId) + if let index = currentItemIds.firstIndex(of: peerId.toInt64()) { + currentItemIds.remove(at: index) + transaction.setPeerPinnedThreads(peerId: accountPeerId, threadIds: currentItemIds) + } else { + addSynchronizePinnedSavedChatsOperation(transaction: transaction, accountPeerId: accountPeerId) + } + } + case let .reorder(itemIds): + let itemIds = itemIds.compactMap({ + switch $0 { + case let .peer(peerId): + return peerId + } + }) + let currentItemIds = transaction.getPeerPinnedThreads(peerId: accountPeerId) + if Set(itemIds) == Set(currentItemIds.map(PeerId.init)) { + transaction.setPeerPinnedThreads(peerId: accountPeerId, threadIds: itemIds.map { $0.toInt64() }) + } else { + addSynchronizePinnedSavedChatsOperation(transaction: transaction, accountPeerId: accountPeerId) + } + case .sync: + addSynchronizePinnedSavedChatsOperation(transaction: transaction, accountPeerId: accountPeerId) + } case let .UpdatePinnedTopic(peerId, threadId, isPinned): var currentThreadIds = transaction.getPeerPinnedThreads(peerId: peerId) if isPinned { @@ -4784,8 +4842,8 @@ func replayFinalState( // } // } - for (threadMessageId, difference) in messageThreadStatsDifferences { - updateMessageThreadStats(transaction: transaction, threadMessageId: threadMessageId, removedCount: difference.removedCount, addedMessagePeers: difference.peers) + for (threadKey, difference) in messageThreadStatsDifferences { + updateMessageThreadStats(transaction: transaction, threadKey: threadKey, removedCount: difference.removedCount, addedMessagePeers: difference.peers) } if !peerActivityTimestamps.isEmpty { diff --git a/submodules/TelegramCore/Sources/State/AccountStateManager.swift b/submodules/TelegramCore/Sources/State/AccountStateManager.swift index 3f7469dabe1..b630929c576 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManager.swift @@ -49,6 +49,41 @@ public enum DeletedMessageId: Hashable { case messageId(MessageId) } +final class MessagesRemovedContext { + private var messagesRemovedInteractively = Set() + private var messagesRemovedInteractivelyLock = NSLock() + + func synchronouslyIsMessageDeletedInteractively(ids: [MessageId]) -> [EngineMessage.Id] { + var result: [EngineMessage.Id] = [] + + self.messagesRemovedInteractivelyLock.lock() + for id in ids { + let mappedId: DeletedMessageId + if id.peerId.namespace == Namespaces.Peer.CloudUser || id.peerId.namespace == Namespaces.Peer.CloudGroup { + mappedId = .global(id.id) + } else { + mappedId = .messageId(id) + } + if self.messagesRemovedInteractively.contains(mappedId) { + result.append(id) + } + } + self.messagesRemovedInteractivelyLock.unlock() + + return result + } + + func addIsMessagesDeletedInteractively(ids: [DeletedMessageId]) { + if ids.isEmpty { + return + } + + self.messagesRemovedInteractivelyLock.lock() + self.messagesRemovedInteractively.formUnion(ids) + self.messagesRemovedInteractivelyLock.unlock() + } +} + public final class AccountStateManager { public final class IncomingCallUpdate { public let callId: Int64 @@ -227,6 +262,8 @@ public final class AccountStateManager { return self.deletedMessagesPipe.signal() } + let messagesRemovedContext: MessagesRemovedContext + fileprivate let storyUpdatesPipe = ValuePipe<[InternalStoryUpdate]>() public var storyUpdates: Signal<[InternalStoryUpdate], NoError> { return self.storyUpdatesPipe.signal() @@ -256,7 +293,8 @@ public final class AccountStateManager { peerInputActivityManager: PeerInputActivityManager?, auxiliaryMethods: AccountAuxiliaryMethods, updateConfigRequested: (() -> Void)?, - isPremiumUpdated: (() -> Void)? + isPremiumUpdated: (() -> Void)?, + messagesRemovedContext: MessagesRemovedContext ) { self.queue = queue self.accountPeerId = accountPeerId @@ -270,6 +308,7 @@ public final class AccountStateManager { self.auxiliaryMethods = auxiliaryMethods self.updateConfigRequested = updateConfigRequested self.isPremiumUpdated = isPremiumUpdated + self.messagesRemovedContext = messagesRemovedContext } deinit { @@ -591,13 +630,14 @@ public final class AccountStateManager { let network = self.network let auxiliaryMethods = self.auxiliaryMethods let events = channelOperationsContext.events + let messagesRemovedContext = self.messagesRemovedContext let _ = (self.postbox.transaction { transaction -> AccountReplayedFinalState? in if let state = transaction.getState() as? AuthorizedAccountState { transaction.setState(state.withInvalidatedChannels([])) } - return replayFinalState( + let result = replayFinalState( accountManager: accountManager, postbox: postbox, accountPeerId: accountPeerId, @@ -616,6 +656,12 @@ public final class AccountStateManager { ignoreDate: false, skipVerification: true ) + + if let result = result, !result.deletedMessageIds.isEmpty { + messagesRemovedContext.addIsMessagesDeletedInteractively(ids: result.deletedMessageIds) + } + + return result } |> deliverOn(self.queue)).start(next: { [weak self] finalState in guard let strongSelf = self else { @@ -663,6 +709,8 @@ public final class AccountStateManager { let mediaBox = postbox.mediaBox let accountPeerId = self.accountPeerId let auxiliaryMethods = self.auxiliaryMethods + let messagesRemovedContext = self.messagesRemovedContext + let signal = postbox.transaction { transaction -> (AuthorizedAccountState?, [(peer: Peer, pts: Int32?)], Bool) in let state = transaction.getState() as? AuthorizedAccountState @@ -749,6 +797,10 @@ public final class AccountStateManager { } if let replayedState = replayedState { + if !replayedState.deletedMessageIds.isEmpty { + messagesRemovedContext.addIsMessagesDeletedInteractively(ids: replayedState.deletedMessageIds) + } + return (difference, replayedState, false, false) } else { return (nil, nil, false, false) @@ -861,6 +913,8 @@ public final class AccountStateManager { let accountPeerId = self.accountPeerId let mediaBox = postbox.mediaBox let queue = self.queue + let messagesRemovedContext = self.messagesRemovedContext + let signal = initialStateWithUpdateGroups(postbox: postbox, groups: groups) |> mapToSignal { [weak self] state -> Signal<(AccountReplayedFinalState?, AccountFinalState), NoError> in return finalStateWithUpdateGroups(accountPeerId: accountPeerId, postbox: postbox, network: network, state: state, groups: groups, asyncResetChannels: nil) @@ -880,6 +934,11 @@ public final class AccountStateManager { } else { let startTime = CFAbsoluteTimeGetCurrent() let result = replayFinalState(accountManager: accountManager, postbox: postbox, accountPeerId: accountPeerId, mediaBox: mediaBox, encryptionProvider: network.encryptionProvider, transaction: transaction, auxiliaryMethods: auxiliaryMethods, finalState: finalState, removePossiblyDeliveredMessagesUniqueIds: removePossiblyDeliveredMessagesUniqueIds, ignoreDate: false, skipVerification: false) + + if let result = result, !result.deletedMessageIds.isEmpty { + messagesRemovedContext.addIsMessagesDeletedInteractively(ids: result.deletedMessageIds) + } + let deltaTime = CFAbsoluteTimeGetCurrent() - startTime if deltaTime > 1.0 { Logger.shared.log("State", "replayFinalState took \(deltaTime)s") @@ -1029,7 +1088,7 @@ public final class AccountStateManager { for attr in first.attributes { if let attribute = attr as? ReplyMessageAttribute { if let threadId = attribute.threadMessageId { - threadData = transaction.getMessageHistoryThreadInfo(peerId: first.id.peerId, threadId: makeMessageThreadId(threadId))?.data.get(MessageHistoryThreadData.self) + threadData = transaction.getMessageHistoryThreadInfo(peerId: first.id.peerId, threadId: Int64(threadId.id))?.data.get(MessageHistoryThreadData.self) } } } @@ -1133,6 +1192,7 @@ public final class AccountStateManager { let network = self.network let auxiliaryMethods = self.auxiliaryMethods let removePossiblyDeliveredMessagesUniqueIds = self.removePossiblyDeliveredMessagesUniqueIds + let messagesRemovedContext = self.messagesRemovedContext let signal = self.postbox.transaction { transaction -> AccountReplayedFinalState? in let startTime = CFAbsoluteTimeGetCurrent() let result = replayFinalState(accountManager: accountManager, postbox: postbox, accountPeerId: accountPeerId, mediaBox: mediaBox, encryptionProvider: network.encryptionProvider, transaction: transaction, auxiliaryMethods: auxiliaryMethods, finalState: finalState, removePossiblyDeliveredMessagesUniqueIds: removePossiblyDeliveredMessagesUniqueIds, ignoreDate: false, skipVerification: false) @@ -1140,6 +1200,11 @@ public final class AccountStateManager { if deltaTime > 1.0 { Logger.shared.log("State", "replayFinalState took \(deltaTime)s") } + + if let result = result, !result.deletedMessageIds.isEmpty { + messagesRemovedContext.addIsMessagesDeletedInteractively(ids: result.deletedMessageIds) + } + return result } |> map({ ($0, finalState) }) @@ -1178,9 +1243,16 @@ public final class AccountStateManager { let network = self.network let auxiliaryMethods = self.auxiliaryMethods let removePossiblyDeliveredMessagesUniqueIds = self.removePossiblyDeliveredMessagesUniqueIds + let messagesRemovedContext = self.messagesRemovedContext + let signal = self.postbox.transaction { transaction -> AccountReplayedFinalState? in let startTime = CFAbsoluteTimeGetCurrent() let result = replayFinalState(accountManager: accountManager, postbox: postbox, accountPeerId: accountPeerId, mediaBox: mediaBox, encryptionProvider: network.encryptionProvider, transaction: transaction, auxiliaryMethods: auxiliaryMethods, finalState: finalState, removePossiblyDeliveredMessagesUniqueIds: removePossiblyDeliveredMessagesUniqueIds, ignoreDate: false, skipVerification: false) + + if let result = result, !result.deletedMessageIds.isEmpty { + messagesRemovedContext.addIsMessagesDeletedInteractively(ids: result.deletedMessageIds) + } + let deltaTime = CFAbsoluteTimeGetCurrent() - startTime if deltaTime > 1.0 { Logger.shared.log("State", "replayFinalState took \(deltaTime)s") @@ -1202,6 +1274,7 @@ public final class AccountStateManager { let mediaBox = postbox.mediaBox let accountPeerId = self.accountPeerId let auxiliaryMethods = self.auxiliaryMethods + let messagesRemovedContext = self.messagesRemovedContext let signal = postbox.stateView() |> mapToSignal { view -> Signal in @@ -1267,6 +1340,11 @@ public final class AccountStateManager { ignoreDate: true, skipVerification: false ) + + if let replayedState = replayedState, !replayedState.deletedMessageIds.isEmpty { + messagesRemovedContext.addIsMessagesDeletedInteractively(ids: replayedState.deletedMessageIds) + } + let deltaTime = CFAbsoluteTimeGetCurrent() - startTime if deltaTime > 1.0 { Logger.shared.log("State", "replayFinalState took \(deltaTime)s") @@ -1653,6 +1731,8 @@ public final class AccountStateManager { var updateConfigRequested: (() -> Void)? var isPremiumUpdated: (() -> Void)? + let messagesRemovedContext = MessagesRemovedContext() + init( accountPeerId: PeerId, accountManager: AccountManager, @@ -1671,6 +1751,8 @@ public final class AccountStateManager { self.network = network self.auxiliaryMethods = auxiliaryMethods + let messagesRemovedContext = self.messagesRemovedContext + var updateConfigRequestedImpl: (() -> Void)? var isPremiumUpdatedImpl: (() -> Void)? @@ -1691,7 +1773,8 @@ public final class AccountStateManager { }, isPremiumUpdated: { isPremiumUpdatedImpl?() - } + }, + messagesRemovedContext: messagesRemovedContext ) }) @@ -1848,6 +1931,10 @@ public final class AccountStateManager { return nil } } + + public func synchronouslyIsMessageDeletedInteractively(ids: [EngineMessage.Id]) -> [EngineMessage.Id] { + return self.messagesRemovedContext.synchronouslyIsMessageDeletedInteractively(ids: ids) + } } func resolveNotificationSettings(list: [TelegramPeerNotificationSettings], defaultSettings: MessageNotificationSettings) -> (sound: PeerMessageSound, notify: Bool, displayContents: Bool) { @@ -1912,7 +1999,7 @@ public func messagesForNotification(transaction: Transaction, id: MessageId, alw } if let attribute = attribute as? ReplyMessageAttribute { if let threadId = attribute.threadMessageId { - threadData = transaction.getMessageHistoryThreadInfo(peerId: message.id.peerId, threadId: makeMessageThreadId(threadId))?.data.get(MessageHistoryThreadData.self) + threadData = transaction.getMessageHistoryThreadInfo(peerId: message.id.peerId, threadId: Int64(threadId.id))?.data.get(MessageHistoryThreadData.self) } } } diff --git a/submodules/TelegramCore/Sources/State/AccountTaskManager.swift b/submodules/TelegramCore/Sources/State/AccountTaskManager.swift index 2887726a174..bbd8f0c4545 100644 --- a/submodules/TelegramCore/Sources/State/AccountTaskManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountTaskManager.swift @@ -69,7 +69,8 @@ final class AccountTaskManager { tasks.add(managedSynchronizeGroupMessageStats(network: self.stateManager.network, postbox: self.stateManager.postbox, stateManager: self.stateManager).start()) tasks.add(managedGlobalNotificationSettings(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) - tasks.add(managedSynchronizePinnedChatsOperations(postbox: self.stateManager.postbox, network: self.stateManager.network, accountPeerId: self.stateManager.accountPeerId, stateManager: self.stateManager).start()) + tasks.add(managedSynchronizePinnedChatsOperations(postbox: self.stateManager.postbox, network: self.stateManager.network, accountPeerId: self.stateManager.accountPeerId, stateManager: self.stateManager, tag: OperationLogTags.SynchronizePinnedChats).start()) + tasks.add(managedSynchronizePinnedChatsOperations(postbox: self.stateManager.postbox, network: self.stateManager.network, accountPeerId: self.stateManager.accountPeerId, stateManager: self.stateManager, tag: OperationLogTags.SynchronizePinnedSavedChats).start()) tasks.add(managedSynchronizeGroupedPeersOperations(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start()) tasks.add(managedSynchronizeInstalledStickerPacksOperations(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager, namespace: .stickers).start()) tasks.add(managedSynchronizeInstalledStickerPacksOperations(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager, namespace: .masks).start()) @@ -102,11 +103,14 @@ final class AccountTaskManager { tasks.add(managedAllPremiumStickers(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) tasks.add(managedRecentStatusEmoji(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) tasks.add(managedFeaturedStatusEmoji(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) + tasks.add(managedFeaturedChannelStatusEmoji(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) tasks.add(managedProfilePhotoEmoji(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) tasks.add(managedGroupPhotoEmoji(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) tasks.add(managedBackgroundIconEmoji(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) tasks.add(managedRecentReactions(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) tasks.add(_internal_loadedStickerPack(postbox: self.stateManager.postbox, network: self.stateManager.network, reference: .iconStatusEmoji, forceActualized: true).start()) + tasks.add(_internal_loadedStickerPack(postbox: self.stateManager.postbox, network: self.stateManager.network, reference: .iconChannelStatusEmoji, forceActualized: true).start()) + tasks.add(managedDisabledChannelStatusIconEmoji(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) tasks.add(_internal_loadedStickerPack(postbox: self.stateManager.postbox, network: self.stateManager.network, reference: .iconTopicEmoji, forceActualized: true).start()) tasks.add(managedPeerColorUpdates(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) diff --git a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift index db150f34fb4..97a5986320e 100644 --- a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift +++ b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift @@ -1860,23 +1860,15 @@ public final class AccountViewTracker { return transaction.getMessageHistoryThreadInfo(peerId: peerId, threadId: threadId)?.data.get(MessageHistoryThreadData.self) } |> mapToSignal { threadInfo -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> in - if let threadInfo = threadInfo { - let anchor: HistoryViewInputAnchor - if threadInfo.maxIncomingReadId <= 1 { - anchor = .message(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: 1)) - } else if threadInfo.incomingUnreadCount > 0 && tagMask == nil { - let customUnreadMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: threadInfo.maxIncomingReadId) - anchor = .message(customUnreadMessageId) - } else { - anchor = .upperBound - } - + if peerId == account.peerId { return account.postbox.aroundMessageHistoryViewForLocation( chatLocation, - anchor: anchor, + anchor: .upperBound, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, count: count, - fixedCombinedReadStates: nil, + fixedCombinedReadStates: .peer([peerId: CombinedPeerReadState(states: [ + (Namespaces.Message.Cloud, PeerReadState.idBased(maxIncomingReadId: Int32.max - 1, maxOutgoingReadId: Int32.max - 1, maxKnownId: Int32.max - 1, count: 0, markedUnread: false)) + ])]), topTaggedMessageIdNamespaces: [], tagMask: tagMask, appendMessagesFromTheSameGroup: false, @@ -1884,6 +1876,32 @@ public final class AccountViewTracker { orderStatistics: orderStatistics, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData) ) + } else { + if let threadInfo = threadInfo { + let anchor: HistoryViewInputAnchor + if threadInfo.maxIncomingReadId <= 1 { + anchor = .message(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: 1)) + } else if threadInfo.incomingUnreadCount > 0 && tagMask == nil { + let customUnreadMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: threadInfo.maxIncomingReadId) + anchor = .message(customUnreadMessageId) + } else { + anchor = .upperBound + } + + return account.postbox.aroundMessageHistoryViewForLocation( + chatLocation, + anchor: anchor, + ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, + count: count, + fixedCombinedReadStates: nil, + topTaggedMessageIdNamespaces: [], + tagMask: tagMask, + appendMessagesFromTheSameGroup: false, + namespaces: .not(Namespaces.Message.allScheduled), + orderStatistics: orderStatistics, + additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData) + ) + } } return account.postbox.aroundMessageOfInterestHistoryViewForChatLocation(chatLocation, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, count: count, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tagMask: tagMask, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: .not(Namespaces.Message.allScheduled), orderStatistics: orderStatistics, customUnreadMessageId: nil, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData)) diff --git a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift index 5f6ac5c48ef..cd37ccc9329 100644 --- a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift +++ b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift @@ -96,7 +96,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes var updatedTimestamp: Int32? if let apiMessage = apiMessage { switch apiMessage { - case let .message(_, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, _, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _): updatedTimestamp = date case .messageEmpty: break @@ -287,9 +287,8 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes if let updatedMessage = updatedMessage, case let .Id(updatedId) = updatedMessage.id { if message.id.namespace == Namespaces.Message.Local && updatedId.namespace == Namespaces.Message.Cloud && updatedId.peerId.namespace == Namespaces.Peer.CloudChannel { if let threadId = updatedMessage.threadId { - let messageThreadId = makeThreadIdMessageId(peerId: updatedMessage.id.peerId, threadId: threadId) if let authorId = updatedMessage.authorId { - updateMessageThreadStats(transaction: transaction, threadMessageId: messageThreadId, removedCount: 0, addedMessagePeers: [ReplyThreadUserMessage(id: authorId, messageId: updatedId, isOutgoing: true)]) + updateMessageThreadStats(transaction: transaction, threadKey: MessageThreadKey(peerId: updatedMessage.id.peerId, threadId: threadId), removedCount: 0, addedMessagePeers: [ReplyThreadUserMessage(id: authorId, messageId: updatedId, isOutgoing: true)]) } } } diff --git a/submodules/TelegramCore/Sources/State/HistoryViewStateValidation.swift b/submodules/TelegramCore/Sources/State/HistoryViewStateValidation.swift index 266e2c68034..9a810131956 100644 --- a/submodules/TelegramCore/Sources/State/HistoryViewStateValidation.swift +++ b/submodules/TelegramCore/Sources/State/HistoryViewStateValidation.swift @@ -234,7 +234,7 @@ final class HistoryViewStateValidationContexts { context.batchReferences[messageId] = batch } - disposable.set((validateReplyThreadMessagesBatch(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, peerId: peerId, threadMessageId: makeThreadIdMessageId(peerId: peerId, threadId: threadId).id, tag: view.tagMask, messageIds: messages) + disposable.set((validateReplyThreadMessagesBatch(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, peerId: peerId, threadMessageId: Int32(clamping: threadId), tag: view.tagMask, messageIds: messages) |> deliverOn(self.queue)).start(completed: { [weak self, weak batch] in if let strongSelf = self, let context = strongSelf.contexts[id], let batch = batch { var completedMessageIds: [MessageId] = [] @@ -512,7 +512,7 @@ private func validateChannelMessagesBatch(postbox: Postbox, network: Network, ac } else if tag == MessageTags.unseenReaction { requestSignal = network.request(Api.functions.messages.getUnreadReactions(flags: 0, peer: inputPeer, topMsgId: nil, offsetId: messageIds[messageIds.count - 1].id + 1, addOffset: 0, limit: Int32(messageIds.count), maxId: messageIds[messageIds.count - 1].id + 1, minId: messageIds[0].id - 1)) } else if let filter = messageFilterForTagMask(tag) { - requestSignal = network.request(Api.functions.messages.search(flags: 0, peer: inputPeer, q: "", fromId: nil, topMsgId: nil, filter: filter, minDate: 0, maxDate: 0, offsetId: messageIds[messageIds.count - 1].id + 1, addOffset: 0, limit: Int32(messageIds.count), maxId: messageIds[messageIds.count - 1].id + 1, minId: messageIds[0].id - 1, hash: hash)) + requestSignal = network.request(Api.functions.messages.search(flags: 0, peer: inputPeer, q: "", fromId: nil, savedPeerId: nil, topMsgId: nil, filter: filter, minDate: 0, maxDate: 0, offsetId: messageIds[messageIds.count - 1].id + 1, addOffset: 0, limit: Int32(messageIds.count), maxId: messageIds[messageIds.count - 1].id + 1, minId: messageIds[0].id - 1, hash: hash)) } else { assertionFailure() requestSignal = .complete() @@ -582,7 +582,7 @@ private func validateReplyThreadMessagesBatch(postbox: Postbox, network: Network var flags: Int32 = 0 flags |= (1 << 1) - requestSignal = network.request(Api.functions.messages.search(flags: flags, peer: inputPeer, q: "", fromId: nil, topMsgId: threadMessageId, filter: filter, minDate: 0, maxDate: 0, offsetId: messageIds[messageIds.count - 1].id + 1, addOffset: 0, limit: Int32(messageIds.count), maxId: messageIds[messageIds.count - 1].id + 1, minId: messageIds[0].id - 1, hash: hash)) + requestSignal = network.request(Api.functions.messages.search(flags: flags, peer: inputPeer, q: "", fromId: nil, savedPeerId: nil, topMsgId: threadMessageId, filter: filter, minDate: 0, maxDate: 0, offsetId: messageIds[messageIds.count - 1].id + 1, addOffset: 0, limit: Int32(messageIds.count), maxId: messageIds[messageIds.count - 1].id + 1, minId: messageIds[0].id - 1, hash: hash)) } else { return .complete() } @@ -621,7 +621,7 @@ private func validateReplyThreadMessagesBatch(postbox: Postbox, network: Network return .complete() } - return validateReplyThreadBatch(postbox: postbox, network: network, transaction: transaction, accountPeerId: accountPeerId, peerId: peerId, threadId: makeMessageThreadId(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: threadMessageId)), signal: signal, previous: previous, messageNamespace: Namespaces.Message.Cloud) + return validateReplyThreadBatch(postbox: postbox, network: network, transaction: transaction, accountPeerId: accountPeerId, peerId: peerId, threadId: Int64(threadMessageId), signal: signal, previous: previous, messageNamespace: Namespaces.Message.Cloud) } |> switchToLatest } diff --git a/submodules/TelegramCore/Sources/State/Holes.swift b/submodules/TelegramCore/Sources/State/Holes.swift index d8537f8d103..7d4c93c6ea6 100644 --- a/submodules/TelegramCore/Sources/State/Holes.swift +++ b/submodules/TelegramCore/Sources/State/Holes.swift @@ -335,16 +335,29 @@ enum FetchMessageHistoryHoleThreadInput: CustomStringConvertible { } } - var requestThreadId: MessageId? { + func requestThreadId(accountPeerId: PeerId) -> Int64? { switch self { case let .direct(peerId, threadId): - if let threadId = threadId { - return makeThreadIdMessageId(peerId: peerId, threadId: threadId) + if let threadId = threadId, peerId != accountPeerId { + return threadId } else { return nil } case let .threadFromChannel(channelMessageId): - return channelMessageId + return Int64(channelMessageId.id) + } + } + + func requestSubPeerId(accountPeerId: PeerId) -> PeerId? { + switch self { + case let .direct(peerId, threadId): + if let threadId = threadId, peerId == accountPeerId { + return PeerId(threadId) + } else { + return nil + } + case .threadFromChannel: + return nil } } } @@ -360,10 +373,6 @@ struct FetchMessageHistoryHoleResult: Equatable { func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryHoleSource, postbox: Postbox, peerInput: FetchMessageHistoryHoleThreadInput, namespace: MessageId.Namespace, direction: MessageHistoryViewRelativeHoleDirection, space: MessageHistoryHoleSpace, count rawCount: Int) -> Signal { let count = min(100, rawCount) - if peerInput.requestThreadId != nil, case .everywhere = space, case .aroundId = direction { - assert(true) - } - return postbox.stateView() |> mapToSignal { view -> Signal in if let state = view.state as? AuthorizedAccountState { @@ -374,15 +383,15 @@ func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryH } |> take(1) |> mapToSignal { _ -> Signal in - return postbox.transaction { transaction -> (Peer?, Int64) in + return postbox.transaction { transaction -> (Peer?, Int64, Peer?) in switch peerInput { case let .direct(peerId, _): - return (transaction.getPeer(peerId), 0) + return (transaction.getPeer(peerId), 0, peerInput.requestSubPeerId(accountPeerId: accountPeerId).flatMap(transaction.getPeer)) case let .threadFromChannel(channelMessageId): - return (transaction.getPeer(channelMessageId.peerId), 0) + return (transaction.getPeer(channelMessageId.peerId), 0, nil) } } - |> mapToSignal { (peer, hash) -> Signal in + |> mapToSignal { (peer, hash, subPeer) -> Signal in guard let peer = peer else { return .single(FetchMessageHistoryHoleResult(removedIndices: IndexSet(), strictRemovedIndices: IndexSet(), actualPeerId: nil, actualThreadId: nil, ids: [])) } @@ -397,284 +406,345 @@ func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryH let minMaxRange: ClosedRange switch space { - case .everywhere: - if let requestThreadId = peerInput.requestThreadId { - let offsetId: Int32 - let addOffset: Int32 - let selectedLimit = count - let maxId: Int32 - let minId: Int32 - - switch direction { - case let .range(start, end): - if start.id <= end.id { - offsetId = start.id <= 1 ? 1 : (start.id - 1) - addOffset = Int32(-selectedLimit) - maxId = end.id - minId = start.id - 1 - - let rangeStartId = start.id - let rangeEndId = min(end.id, Int32.max - 1) - if rangeStartId <= rangeEndId { - minMaxRange = rangeStartId ... rangeEndId - } else { - minMaxRange = rangeStartId ... rangeStartId - assertionFailure() - } - } else { - offsetId = start.id == Int32.max ? start.id : (start.id + 1) - addOffset = 0 - maxId = start.id == Int32.max ? start.id : (start.id + 1) - minId = end.id - - let rangeStartId = end.id - let rangeEndId = min(start.id, Int32.max - 1) - if rangeStartId <= rangeEndId { - minMaxRange = rangeStartId ... rangeEndId - } else { - minMaxRange = rangeStartId ... rangeStartId - assertionFailure() - } - } - case let .aroundId(id): - offsetId = id.id - addOffset = Int32(-selectedLimit / 2) - maxId = Int32.max - minId = 1 + case .everywhere: + if let requestThreadId = peerInput.requestThreadId(accountPeerId: accountPeerId) { + let offsetId: Int32 + let addOffset: Int32 + let selectedLimit = count + let maxId: Int32 + let minId: Int32 + + switch direction { + case let .range(start, end): + if start.id <= end.id { + offsetId = start.id <= 1 ? 1 : (start.id - 1) + addOffset = Int32(-selectedLimit) + maxId = end.id + minId = start.id - 1 - minMaxRange = 1 ... (Int32.max - 1) - } - - request = source.request(Api.functions.messages.getReplies(peer: inputPeer, msgId: requestThreadId.id, offsetId: offsetId, offsetDate: 0, addOffset: addOffset, limit: Int32(selectedLimit), maxId: maxId, minId: minId, hash: hash)) - } else { - let offsetId: Int32 - let addOffset: Int32 - let selectedLimit = count - let maxId: Int32 - let minId: Int32 - - switch direction { - case let .range(start, end): - if start.id <= end.id { - offsetId = start.id <= 1 ? 1 : (start.id - 1) - addOffset = Int32(-selectedLimit) - maxId = end.id - minId = start.id - 1 - - let rangeStartId = start.id - let rangeEndId = min(end.id, Int32.max - 1) - if rangeStartId <= rangeEndId { - minMaxRange = rangeStartId ... rangeEndId - } else { - minMaxRange = rangeStartId ... rangeStartId - assertionFailure() - } - } else { - offsetId = start.id == Int32.max ? start.id : (start.id + 1) - addOffset = 0 - maxId = start.id == Int32.max ? start.id : (start.id + 1) - minId = end.id == 1 ? 0 : end.id - - let rangeStartId = end.id - let rangeEndId = min(start.id, Int32.max - 1) - if rangeStartId <= rangeEndId { - minMaxRange = rangeStartId ... rangeEndId - } else { - minMaxRange = rangeStartId ... rangeStartId - assertionFailure() - } - } - case let .aroundId(id): - offsetId = id.id - addOffset = Int32(-selectedLimit / 2) - maxId = Int32.max - minId = 1 - minMaxRange = 1 ... Int32.max - 1 + let rangeStartId = start.id + let rangeEndId = min(end.id, Int32.max - 1) + if rangeStartId <= rangeEndId { + minMaxRange = rangeStartId ... rangeEndId + } else { + minMaxRange = rangeStartId ... rangeStartId + assertionFailure() + } + } else { + offsetId = start.id == Int32.max ? start.id : (start.id + 1) + addOffset = 0 + maxId = start.id == Int32.max ? start.id : (start.id + 1) + minId = end.id + + let rangeStartId = end.id + let rangeEndId = min(start.id, Int32.max - 1) + if rangeStartId <= rangeEndId { + minMaxRange = rangeStartId ... rangeEndId + } else { + minMaxRange = rangeStartId ... rangeStartId + assertionFailure() + } } + case let .aroundId(id): + offsetId = id.id + addOffset = Int32(-selectedLimit / 2) + maxId = Int32.max + minId = 1 - request = source.request(Api.functions.messages.getHistory(peer: inputPeer, offsetId: offsetId, offsetDate: 0, addOffset: addOffset, limit: Int32(selectedLimit), maxId: maxId, minId: minId, hash: 0)) + minMaxRange = 1 ... (Int32.max - 1) } - case let .tag(tag): - assert(tag.containsSingleElement) - if tag == .unseenPersonalMessage { - let offsetId: Int32 - let addOffset: Int32 - let selectedLimit = count - let maxId: Int32 - let minId: Int32 - - switch direction { - case let .range(start, end): - if start.id <= end.id { - offsetId = start.id <= 1 ? 1 : (start.id - 1) - addOffset = Int32(-selectedLimit) - maxId = end.id - minId = start.id - 1 - - let rangeStartId = start.id - let rangeEndId = min(end.id, Int32.max - 1) - if rangeStartId <= rangeEndId { - minMaxRange = rangeStartId ... rangeEndId - } else { - minMaxRange = rangeStartId ... rangeStartId - assertionFailure() - } - } else { - offsetId = start.id == Int32.max ? start.id : (start.id + 1) - addOffset = 0 - maxId = start.id == Int32.max ? start.id : (start.id + 1) - minId = end.id - - let rangeStartId = end.id - let rangeEndId = min(start.id, Int32.max - 1) - if rangeStartId <= rangeEndId { - minMaxRange = rangeStartId ... rangeEndId - } else { - minMaxRange = rangeStartId ... rangeStartId - assertionFailure() - } - } - case let .aroundId(id): - offsetId = id.id - addOffset = Int32(-selectedLimit / 2) - maxId = Int32.max - minId = 1 + + request = source.request(Api.functions.messages.getReplies(peer: inputPeer, msgId: Int32(clamping: requestThreadId), offsetId: offsetId, offsetDate: 0, addOffset: addOffset, limit: Int32(selectedLimit), maxId: maxId, minId: minId, hash: hash)) + } else if let subPeerId = peerInput.requestSubPeerId(accountPeerId: accountPeerId) { + guard let subPeer, subPeer.id == subPeerId, let inputSubPeer = apiInputPeer(subPeer) else { + Logger.shared.log("fetchMessageHistoryHole", "subPeer not available") + return .never() + } + + let offsetId: Int32 + let addOffset: Int32 + let selectedLimit = count + let maxId: Int32 + let minId: Int32 + + switch direction { + case let .range(start, end): + if start.id <= end.id { + offsetId = start.id <= 1 ? 1 : (start.id - 1) + addOffset = Int32(-selectedLimit) + maxId = end.id + minId = start.id - 1 - minMaxRange = 1 ... Int32.max - 1 - } - - var flags: Int32 = 0 - var topMsgId: Int32? - if let threadId = peerInput.requestThreadId { - flags |= (1 << 1) - topMsgId = threadId.id + let rangeStartId = start.id + let rangeEndId = min(end.id, Int32.max - 1) + if rangeStartId <= rangeEndId { + minMaxRange = rangeStartId ... rangeEndId + } else { + minMaxRange = rangeStartId ... rangeStartId + assertionFailure() + } + } else { + offsetId = start.id == Int32.max ? start.id : (start.id + 1) + addOffset = 0 + maxId = start.id == Int32.max ? start.id : (start.id + 1) + minId = end.id + + let rangeStartId = end.id + let rangeEndId = min(start.id, Int32.max - 1) + if rangeStartId <= rangeEndId { + minMaxRange = rangeStartId ... rangeEndId + } else { + minMaxRange = rangeStartId ... rangeStartId + assertionFailure() + } } + case let .aroundId(id): + offsetId = id.id + addOffset = Int32(-selectedLimit / 2) + maxId = Int32.max + minId = 1 - request = source.request(Api.functions.messages.getUnreadMentions(flags: flags, peer: inputPeer, topMsgId: topMsgId, offsetId: offsetId, addOffset: addOffset, limit: Int32(selectedLimit), maxId: maxId, minId: minId)) - } else if tag == .unseenReaction { - let offsetId: Int32 - let addOffset: Int32 - let selectedLimit = count - let maxId: Int32 - let minId: Int32 - - switch direction { - case let .range(start, end): - if start.id <= end.id { - offsetId = start.id <= 1 ? 1 : (start.id - 1) - addOffset = Int32(-selectedLimit) - maxId = end.id - minId = start.id - 1 - - let rangeStartId = start.id - let rangeEndId = min(end.id, Int32.max - 1) - if rangeStartId <= rangeEndId { - minMaxRange = rangeStartId ... rangeEndId - } else { - minMaxRange = rangeStartId ... rangeStartId - assertionFailure() - } - } else { - offsetId = start.id == Int32.max ? start.id : (start.id + 1) - addOffset = 0 - maxId = start.id == Int32.max ? start.id : (start.id + 1) - minId = end.id - - let rangeStartId = end.id - let rangeEndId = min(start.id, Int32.max - 1) - if rangeStartId <= rangeEndId { - minMaxRange = rangeStartId ... rangeEndId - } else { - minMaxRange = rangeStartId ... rangeStartId - assertionFailure() - } - } - case let .aroundId(id): - offsetId = id.id - addOffset = Int32(-selectedLimit / 2) - maxId = Int32.max - minId = 1 + minMaxRange = 1 ... (Int32.max - 1) + } + + request = source.request(Api.functions.messages.getSavedHistory(peer: inputSubPeer, offsetId: offsetId, offsetDate: 0, addOffset: addOffset, limit: Int32(selectedLimit), maxId: maxId, minId: minId, hash: hash)) + } else { + let offsetId: Int32 + let addOffset: Int32 + let selectedLimit = count + let maxId: Int32 + let minId: Int32 + + switch direction { + case let .range(start, end): + if start.id <= end.id { + offsetId = start.id <= 1 ? 1 : (start.id - 1) + addOffset = Int32(-selectedLimit) + maxId = end.id + minId = start.id - 1 + + let rangeStartId = start.id + let rangeEndId = min(end.id, Int32.max - 1) + if rangeStartId <= rangeEndId { + minMaxRange = rangeStartId ... rangeEndId + } else { + minMaxRange = rangeStartId ... rangeStartId + assertionFailure() + } + } else { + offsetId = start.id == Int32.max ? start.id : (start.id + 1) + addOffset = 0 + maxId = start.id == Int32.max ? start.id : (start.id + 1) + minId = end.id == 1 ? 0 : end.id - minMaxRange = 1 ... Int32.max - 1 + let rangeStartId = end.id + let rangeEndId = min(start.id, Int32.max - 1) + if rangeStartId <= rangeEndId { + minMaxRange = rangeStartId ... rangeEndId + } else { + minMaxRange = rangeStartId ... rangeStartId + assertionFailure() + } } - - var flags: Int32 = 0 - var topMsgId: Int32? - if let threadId = peerInput.requestThreadId { - flags |= (1 << 0) - topMsgId = threadId.id + case let .aroundId(id): + offsetId = id.id + addOffset = Int32(-selectedLimit / 2) + maxId = Int32.max + minId = 1 + minMaxRange = 1 ... Int32.max - 1 + } + + request = source.request(Api.functions.messages.getHistory(peer: inputPeer, offsetId: offsetId, offsetDate: 0, addOffset: addOffset, limit: Int32(selectedLimit), maxId: maxId, minId: minId, hash: 0)) + } + case let .tag(tag): + assert(tag.containsSingleElement) + if tag == .unseenPersonalMessage { + let offsetId: Int32 + let addOffset: Int32 + let selectedLimit = count + let maxId: Int32 + let minId: Int32 + + switch direction { + case let .range(start, end): + if start.id <= end.id { + offsetId = start.id <= 1 ? 1 : (start.id - 1) + addOffset = Int32(-selectedLimit) + maxId = end.id + minId = start.id - 1 + + let rangeStartId = start.id + let rangeEndId = min(end.id, Int32.max - 1) + if rangeStartId <= rangeEndId { + minMaxRange = rangeStartId ... rangeEndId + } else { + minMaxRange = rangeStartId ... rangeStartId + assertionFailure() + } + } else { + offsetId = start.id == Int32.max ? start.id : (start.id + 1) + addOffset = 0 + maxId = start.id == Int32.max ? start.id : (start.id + 1) + minId = end.id + + let rangeStartId = end.id + let rangeEndId = min(start.id, Int32.max - 1) + if rangeStartId <= rangeEndId { + minMaxRange = rangeStartId ... rangeEndId + } else { + minMaxRange = rangeStartId ... rangeStartId + assertionFailure() + } } + case let .aroundId(id): + offsetId = id.id + addOffset = Int32(-selectedLimit / 2) + maxId = Int32.max + minId = 1 - request = source.request(Api.functions.messages.getUnreadReactions(flags: flags, peer: inputPeer, topMsgId: topMsgId, offsetId: offsetId, addOffset: addOffset, limit: Int32(selectedLimit), maxId: maxId, minId: minId)) - } else if tag == .liveLocation { - let selectedLimit = count - - switch direction { - case .aroundId, .range: - implicitelyFillHole = true + minMaxRange = 1 ... Int32.max - 1 + } + + var flags: Int32 = 0 + var topMsgId: Int32? + if let threadId = peerInput.requestThreadId(accountPeerId: accountPeerId) { + flags |= (1 << 1) + topMsgId = Int32(clamping: threadId) + } + + request = source.request(Api.functions.messages.getUnreadMentions(flags: flags, peer: inputPeer, topMsgId: topMsgId, offsetId: offsetId, addOffset: addOffset, limit: Int32(selectedLimit), maxId: maxId, minId: minId)) + } else if tag == .unseenReaction { + let offsetId: Int32 + let addOffset: Int32 + let selectedLimit = count + let maxId: Int32 + let minId: Int32 + + switch direction { + case let .range(start, end): + if start.id <= end.id { + offsetId = start.id <= 1 ? 1 : (start.id - 1) + addOffset = Int32(-selectedLimit) + maxId = end.id + minId = start.id - 1 + + let rangeStartId = start.id + let rangeEndId = min(end.id, Int32.max - 1) + if rangeStartId <= rangeEndId { + minMaxRange = rangeStartId ... rangeEndId + } else { + minMaxRange = rangeStartId ... rangeStartId + assertionFailure() + } + } else { + offsetId = start.id == Int32.max ? start.id : (start.id + 1) + addOffset = 0 + maxId = start.id == Int32.max ? start.id : (start.id + 1) + minId = end.id + + let rangeStartId = end.id + let rangeEndId = min(start.id, Int32.max - 1) + if rangeStartId <= rangeEndId { + minMaxRange = rangeStartId ... rangeEndId + } else { + minMaxRange = rangeStartId ... rangeStartId + assertionFailure() + } } - minMaxRange = 1 ... (Int32.max - 1) - request = source.request(Api.functions.messages.getRecentLocations(peer: inputPeer, limit: Int32(selectedLimit), hash: 0)) - } else if let filter = messageFilterForTagMask(tag) { - let offsetId: Int32 - let addOffset: Int32 - let selectedLimit = count - let maxId: Int32 - let minId: Int32 + case let .aroundId(id): + offsetId = id.id + addOffset = Int32(-selectedLimit / 2) + maxId = Int32.max + minId = 1 - switch direction { - case let .range(start, end): - if start.id <= end.id { - offsetId = start.id <= 1 ? 1 : (start.id - 1) - addOffset = Int32(-selectedLimit) - maxId = end.id - minId = start.id - 1 - - let rangeStartId = start.id - let rangeEndId = min(end.id, Int32.max - 1) - if rangeStartId <= rangeEndId { - minMaxRange = rangeStartId ... rangeEndId - } else { - minMaxRange = rangeStartId ... rangeStartId - assertionFailure() - } - } else { - offsetId = start.id == Int32.max ? start.id : (start.id + 1) - addOffset = 0 - maxId = start.id == Int32.max ? start.id : (start.id + 1) - minId = end.id - - let rangeStartId = end.id - let rangeEndId = min(start.id, Int32.max - 1) - if rangeStartId <= rangeEndId { - minMaxRange = rangeStartId ... rangeEndId - } else { - minMaxRange = rangeStartId ... rangeStartId - assertionFailure() - } - } - case let .aroundId(id): - offsetId = id.id - addOffset = Int32(-selectedLimit / 2) - maxId = Int32.max - minId = 1 + minMaxRange = 1 ... Int32.max - 1 + } + + var flags: Int32 = 0 + var topMsgId: Int32? + if let threadId = peerInput.requestThreadId(accountPeerId: accountPeerId) { + flags |= (1 << 0) + topMsgId = Int32(clamping: threadId) + } + + request = source.request(Api.functions.messages.getUnreadReactions(flags: flags, peer: inputPeer, topMsgId: topMsgId, offsetId: offsetId, addOffset: addOffset, limit: Int32(selectedLimit), maxId: maxId, minId: minId)) + } else if tag == .liveLocation { + let selectedLimit = count + + switch direction { + case .aroundId, .range: + implicitelyFillHole = true + } + minMaxRange = 1 ... (Int32.max - 1) + request = source.request(Api.functions.messages.getRecentLocations(peer: inputPeer, limit: Int32(selectedLimit), hash: 0)) + } else if let filter = messageFilterForTagMask(tag) { + let offsetId: Int32 + let addOffset: Int32 + let selectedLimit = count + let maxId: Int32 + let minId: Int32 + + switch direction { + case let .range(start, end): + if start.id <= end.id { + offsetId = start.id <= 1 ? 1 : (start.id - 1) + addOffset = Int32(-selectedLimit) + maxId = end.id + minId = start.id - 1 - minMaxRange = 1 ... (Int32.max - 1) + let rangeStartId = start.id + let rangeEndId = min(end.id, Int32.max - 1) + if rangeStartId <= rangeEndId { + minMaxRange = rangeStartId ... rangeEndId + } else { + minMaxRange = rangeStartId ... rangeStartId + assertionFailure() + } + } else { + offsetId = start.id == Int32.max ? start.id : (start.id + 1) + addOffset = 0 + maxId = start.id == Int32.max ? start.id : (start.id + 1) + minId = end.id + + let rangeStartId = end.id + let rangeEndId = min(start.id, Int32.max - 1) + if rangeStartId <= rangeEndId { + minMaxRange = rangeStartId ... rangeEndId + } else { + minMaxRange = rangeStartId ... rangeStartId + assertionFailure() + } } + case let .aroundId(id): + offsetId = id.id + addOffset = Int32(-selectedLimit / 2) + maxId = Int32.max + minId = 1 - var flags: Int32 = 0 - var topMsgId: Int32? - if let threadId = peerInput.requestThreadId { - flags |= (1 << 1) - topMsgId = threadId.id + minMaxRange = 1 ... (Int32.max - 1) + } + + var flags: Int32 = 0 + var topMsgId: Int32? + if let threadId = peerInput.requestThreadId(accountPeerId: accountPeerId) { + flags |= (1 << 1) + topMsgId = Int32(clamping: threadId) + } + + var savedPeerId: Api.InputPeer? + if let subPeerId = peerInput.requestSubPeerId(accountPeerId: accountPeerId), let subPeer = subPeer, subPeer.id == subPeerId { + if let inputPeer = apiInputPeer(subPeer) { + flags |= 1 << 2 + savedPeerId = inputPeer } - - request = source.request(Api.functions.messages.search(flags: flags, peer: inputPeer, q: "", fromId: nil, topMsgId: topMsgId, filter: filter, minDate: 0, maxDate: 0, offsetId: offsetId, addOffset: addOffset, limit: Int32(selectedLimit), maxId: maxId, minId: minId, hash: 0)) - } else { - assertionFailure() - minMaxRange = 1 ... 1 - request = .never() } + + request = source.request(Api.functions.messages.search(flags: flags, peer: inputPeer, q: "", fromId: nil, savedPeerId: savedPeerId, topMsgId: topMsgId, filter: filter, minDate: 0, maxDate: 0, offsetId: offsetId, addOffset: addOffset, limit: Int32(selectedLimit), maxId: maxId, minId: minId, hash: 0)) + } else { + assertionFailure() + minMaxRange = 1 ... 1 + request = .never() + } } return request @@ -784,7 +854,7 @@ func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryH } print("fetchMessageHistoryHole for \(peerInput) space \(space) done") - if peerInput.requestThreadId != nil, case .everywhere = space, case .aroundId = direction { + if peerInput.requestThreadId(accountPeerId: accountPeerId) != nil, case .everywhere = space, case .aroundId = direction { assert(true) } @@ -797,7 +867,7 @@ func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryH case let .aroundId(aroundId): filledRange = min(aroundId.id, messageRange.lowerBound) ... max(aroundId.id, messageRange.upperBound) strictFilledIndices = IndexSet(integersIn: Int(min(aroundId.id, messageRange.lowerBound)) ... Int(max(aroundId.id, messageRange.upperBound))) - if peerInput.requestThreadId != nil { + if peerInput.requestThreadId(accountPeerId: accountPeerId) != nil { if ids.count <= count / 2 - 1 { filledRange = minMaxRange } @@ -973,7 +1043,7 @@ func fetchCallListHole(network: Network, postbox: Postbox, accountPeerId: PeerId offset = single((holeIndex.timestamp, min(holeIndex.id.id, Int32.max - 1) + 1, Api.InputPeer.inputPeerEmpty), NoError.self) return offset |> mapToSignal { (timestamp, id, peer) -> Signal in - let searchResult = network.request(Api.functions.messages.search(flags: 0, peer: .inputPeerEmpty, q: "", fromId: nil, topMsgId: nil, filter: .inputMessagesFilterPhoneCalls(flags: 0), minDate: 0, maxDate: holeIndex.timestamp, offsetId: 0, addOffset: 0, limit: limit, maxId: holeIndex.id.id, minId: 0, hash: 0)) + let searchResult = network.request(Api.functions.messages.search(flags: 0, peer: .inputPeerEmpty, q: "", fromId: nil, savedPeerId: nil, topMsgId: nil, filter: .inputMessagesFilterPhoneCalls(flags: 0), minDate: 0, maxDate: holeIndex.timestamp, offsetId: 0, addOffset: 0, limit: limit, maxId: holeIndex.id.id, minId: 0, hash: 0)) |> retryRequest |> mapToSignal { result -> Signal in let messages: [Api.Message] diff --git a/submodules/TelegramCore/Sources/State/ManagedAutoremoveMessageOperations.swift b/submodules/TelegramCore/Sources/State/ManagedAutoremoveMessageOperations.swift index 9071cf2f79c..b480b40be64 100644 --- a/submodules/TelegramCore/Sources/State/ManagedAutoremoveMessageOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedAutoremoveMessageOperations.swift @@ -94,8 +94,14 @@ func managedAutoremoveMessageOperations(network: Network, postbox: Postbox, isRe for i in 0 ..< updatedMedia.count { if let _ = updatedMedia[i] as? TelegramMediaImage { updatedMedia[i] = TelegramMediaExpiredContent(data: .image) - } else if let _ = updatedMedia[i] as? TelegramMediaFile { - updatedMedia[i] = TelegramMediaExpiredContent(data: .file) + } else if let file = updatedMedia[i] as? TelegramMediaFile { + if file.isInstantVideo { + updatedMedia[i] = TelegramMediaExpiredContent(data: .videoMessage) + } else if file.isVoice { + updatedMedia[i] = TelegramMediaExpiredContent(data: .voiceMessage) + } else { + updatedMedia[i] = TelegramMediaExpiredContent(data: .file) + } } } var updatedAttributes = currentMessage.attributes diff --git a/submodules/TelegramCore/Sources/State/ManagedCloudChatRemoveMessagesOperations.swift b/submodules/TelegramCore/Sources/State/ManagedCloudChatRemoveMessagesOperations.swift index 8e7963bf1e0..1e4572414b7 100644 --- a/submodules/TelegramCore/Sources/State/ManagedCloudChatRemoveMessagesOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedCloudChatRemoveMessagesOperations.swift @@ -387,7 +387,50 @@ private func requestClearHistory(postbox: Postbox, network: Network, stateManage private func _internal_clearHistory(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, peer: Peer, operation: CloudChatClearHistoryOperation) -> Signal { if peer.id.namespace == Namespaces.Peer.CloudGroup || peer.id.namespace == Namespaces.Peer.CloudUser { if let inputPeer = apiInputPeer(peer) { - return requestClearHistory(postbox: postbox, network: network, stateManager: stateManager, inputPeer: inputPeer, maxId: operation.topMessageId.id, justClear: true, minTimestamp: operation.minTimestamp, maxTimestamp: operation.maxTimestamp, type: operation.type) + if peer.id == stateManager.accountPeerId, let threadId = operation.threadId { + guard let inputSubPeer = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer) else { + return .complete() + } + + var flags: Int32 = 0 + var updatedMaxId = operation.topMessageId.id + if operation.minTimestamp != nil { + flags |= 1 << 2 + updatedMaxId = 0 + } + if operation.maxTimestamp != nil { + flags |= 1 << 3 + updatedMaxId = 0 + } + let signal = network.request(Api.functions.messages.deleteSavedHistory(flags: flags, peer: inputSubPeer, maxId: updatedMaxId, minDate: operation.minTimestamp, maxDate: operation.maxTimestamp)) + |> map { result -> Api.messages.AffectedHistory? in + return result + } + |> `catch` { _ -> Signal in + return .fail(true) + } + |> mapToSignal { result -> Signal in + if let result = result { + switch result { + case let .affectedHistory(pts, ptsCount, offset): + stateManager.addUpdateGroups([.updatePts(pts: pts, ptsCount: ptsCount)]) + if offset == 0 { + return .fail(true) + } else { + return .complete() + } + } + } else { + return .fail(true) + } + } + return (signal |> restart) + |> `catch` { _ -> Signal in + return .complete() + } + } else { + return requestClearHistory(postbox: postbox, network: network, stateManager: stateManager, inputPeer: inputPeer, maxId: operation.topMessageId.id, justClear: true, minTimestamp: operation.minTimestamp, maxTimestamp: operation.maxTimestamp, type: operation.type) + } } else { return .complete() } diff --git a/submodules/TelegramCore/Sources/State/ManagedLocalInputActivities.swift b/submodules/TelegramCore/Sources/State/ManagedLocalInputActivities.swift index 8b8a6809a03..c2fa13e3c84 100644 --- a/submodules/TelegramCore/Sources/State/ManagedLocalInputActivities.swift +++ b/submodules/TelegramCore/Sources/State/ManagedLocalInputActivities.swift @@ -177,11 +177,11 @@ private func requestActivity(postbox: Postbox, network: Network, accountPeerId: if let inputPeer = apiInputPeer(peer) { var flags: Int32 = 0 - let topMessageId = threadId.flatMap { makeThreadIdMessageId(peerId: peerId, threadId: $0) } + let topMessageId = threadId.flatMap { Int32(clamping: $0) } if topMessageId != nil { flags |= 1 << 0 } - return network.request(Api.functions.messages.setTyping(flags: flags, peer: inputPeer, topMsgId: topMessageId?.id, action: actionFromActivity(activity))) + return network.request(Api.functions.messages.setTyping(flags: flags, peer: inputPeer, topMsgId: topMessageId, action: actionFromActivity(activity))) |> `catch` { _ -> Signal in return .single(.boolFalse) } diff --git a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift index 7bc4fd6b4d9..508a965546c 100644 --- a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift +++ b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift @@ -262,6 +262,36 @@ func managedFeaturedStatusEmoji(postbox: Postbox, network: Network) -> Signal then(.complete() |> suspendAwareDelay(3.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart } +func managedFeaturedChannelStatusEmoji(postbox: Postbox, network: Network) -> Signal { + let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudFeaturedChannelStatusEmoji, extractItemId: { RecentMediaItemId($0).mediaId.id }, reverseHashOrder: false, forceFetch: false, fetch: { hash in + return network.request(Api.functions.account.getChannelDefaultEmojiStatuses(hash: hash)) + |> retryRequest + |> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in + switch result { + case .emojiStatusesNotModified: + return .single(nil) + case let .emojiStatuses(_, statuses): + let parsedStatuses = statuses.compactMap(PeerEmojiStatus.init(apiStatus:)) + + return _internal_resolveInlineStickers(postbox: postbox, network: network, fileIds: parsedStatuses.map(\.fileId)) + |> map { files -> [OrderedItemListEntry] in + var items: [OrderedItemListEntry] = [] + for status in parsedStatuses { + guard let file = files[status.fileId] else { + continue + } + if let entry = CodableEntry(RecentMediaItem(file)) { + items.append(OrderedItemListEntry(id: RecentMediaItemId(file.fileId).rawValue, contents: entry)) + } + } + return items + } + } + } + }) + return (poll |> then(.complete() |> suspendAwareDelay(3.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart +} + func managedProfilePhotoEmoji(postbox: Postbox, network: Network) -> Signal { let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudFeaturedProfilePhotoEmoji, extractItemId: { RecentMediaItemId($0).mediaId.id }, reverseHashOrder: false, forceFetch: false, fetch: { hash in return network.request(Api.functions.account.getDefaultProfilePhotoEmojis(hash: hash)) @@ -346,6 +376,34 @@ func managedBackgroundIconEmoji(postbox: Postbox, network: Network) -> Signal then(.complete() |> suspendAwareDelay(3.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart } +func managedDisabledChannelStatusIconEmoji(postbox: Postbox, network: Network) -> Signal { + let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudDisabledChannelStatusEmoji, extractItemId: { RecentMediaItemId($0).mediaId.id }, reverseHashOrder: false, forceFetch: false, fetch: { hash in + return network.request(Api.functions.account.getChannelRestrictedStatusEmojis(hash: hash)) + |> retryRequest + |> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in + switch result { + case .emojiListNotModified: + return .single(nil) + case let .emojiList(_, documentIds): + return _internal_resolveInlineStickers(postbox: postbox, network: network, fileIds: documentIds) + |> map { files -> [OrderedItemListEntry] in + var items: [OrderedItemListEntry] = [] + for fileId in documentIds { + guard let file = files[fileId] else { + continue + } + if let entry = CodableEntry(RecentMediaItem(file)) { + items.append(OrderedItemListEntry(id: RecentMediaItemId(file.fileId).rawValue, contents: entry)) + } + } + return items + } + } + } + }) + return (poll |> then(.complete() |> suspendAwareDelay(3.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart +} + func managedRecentReactions(postbox: Postbox, network: Network) -> Signal { let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudRecentReactions, extractItemId: { rawId in switch RecentReactionItemId(rawId).id { diff --git a/submodules/TelegramCore/Sources/State/ManagedSynchronizePinnedChatsOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSynchronizePinnedChatsOperations.swift index 4dea62753c5..5db9adc5e38 100644 --- a/submodules/TelegramCore/Sources/State/ManagedSynchronizePinnedChatsOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedSynchronizePinnedChatsOperations.swift @@ -4,7 +4,6 @@ import SwiftSignalKit import TelegramApi import MtProtoKit - private final class ManagedSynchronizePinnedChatsOperationsHelper { var operationDisposables: [Int32: Disposable] = [:] @@ -49,10 +48,10 @@ private final class ManagedSynchronizePinnedChatsOperationsHelper { } } -private func withTakenOperation(postbox: Postbox, peerId: PeerId, tagLocalIndex: Int32, _ f: @escaping (Transaction, PeerMergedOperationLogEntry?) -> Signal) -> Signal { +private func withTakenOperation(postbox: Postbox, tag: PeerOperationLogTag, peerId: PeerId, tagLocalIndex: Int32, _ f: @escaping (Transaction, PeerMergedOperationLogEntry?) -> Signal) -> Signal { return postbox.transaction { transaction -> Signal in var result: PeerMergedOperationLogEntry? - transaction.operationLogUpdateEntry(peerId: peerId, tag: OperationLogTags.SynchronizePinnedChats, tagLocalIndex: tagLocalIndex, { entry in + transaction.operationLogUpdateEntry(peerId: peerId, tag: tag, tagLocalIndex: tagLocalIndex, { entry in if let entry = entry, let _ = entry.mergedIndex, entry.contents is SynchronizePinnedChatsOperation { result = entry.mergedEntry! return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none) @@ -65,11 +64,11 @@ private func withTakenOperation(postbox: Postbox, peerId: PeerId, tagLocalIndex: } |> switchToLatest } -func managedSynchronizePinnedChatsOperations(postbox: Postbox, network: Network, accountPeerId: PeerId, stateManager: AccountStateManager) -> Signal { +func managedSynchronizePinnedChatsOperations(postbox: Postbox, network: Network, accountPeerId: PeerId, stateManager: AccountStateManager, tag: PeerOperationLogTag) -> Signal { return Signal { _ in let helper = Atomic(value: ManagedSynchronizePinnedChatsOperationsHelper()) - let disposable = postbox.mergedOperationLogView(tag: OperationLogTags.SynchronizePinnedChats, limit: 10).start(next: { view in + let disposable = postbox.mergedOperationLogView(tag: tag, limit: 10).start(next: { view in let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) in return helper.update(view.entries) } @@ -79,10 +78,14 @@ func managedSynchronizePinnedChatsOperations(postbox: Postbox, network: Network, } for (entry, disposable) in beginOperations { - let signal = withTakenOperation(postbox: postbox, peerId: entry.peerId, tagLocalIndex: entry.tagLocalIndex, { transaction, entry -> Signal in + let signal = withTakenOperation(postbox: postbox, tag: tag, peerId: entry.peerId, tagLocalIndex: entry.tagLocalIndex, { transaction, entry -> Signal in if let entry = entry { if let operation = entry.contents as? SynchronizePinnedChatsOperation { - return synchronizePinnedChats(transaction: transaction, postbox: postbox, network: network, accountPeerId: accountPeerId, stateManager: stateManager, groupId: PeerGroupId(rawValue: Int32(entry.peerId.id._internalGetInt64Value())), operation: operation) + if tag == OperationLogTags.SynchronizePinnedChats { + return synchronizePinnedChats(transaction: transaction, postbox: postbox, network: network, accountPeerId: accountPeerId, stateManager: stateManager, groupId: PeerGroupId(rawValue: Int32(entry.peerId.id._internalGetInt64Value())), operation: operation) + } else if tag == OperationLogTags.SynchronizePinnedSavedChats { + return synchronizePinnedSavedChats(transaction: transaction, postbox: postbox, network: network, accountPeerId: accountPeerId, stateManager: stateManager, operation: operation) + } } else { assertionFailure() } @@ -90,7 +93,7 @@ func managedSynchronizePinnedChatsOperations(postbox: Postbox, network: Network, return .complete() }) |> then(postbox.transaction { transaction -> Void in - let _ = transaction.operationLogRemoveEntry(peerId: entry.peerId, tag: OperationLogTags.SynchronizePinnedChats, tagLocalIndex: entry.tagLocalIndex) + let _ = transaction.operationLogRemoveEntry(peerId: entry.peerId, tag: tag, tagLocalIndex: entry.tagLocalIndex) }) disposable.set((signal |> delay(2.0, queue: Queue.concurrentDefaultQueue())).start()) @@ -279,3 +282,65 @@ private func synchronizePinnedChats(transaction: Transaction, postbox: Postbox, |> switchToLatest } } + +private func synchronizePinnedSavedChats(transaction: Transaction, postbox: Postbox, network: Network, accountPeerId: PeerId, stateManager: AccountStateManager, operation: SynchronizePinnedChatsOperation) -> Signal { + return network.request(Api.functions.messages.getPinnedSavedDialogs()) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { dialogs -> Signal in + guard let dialogs = dialogs else { + return .never() + } + + let _ = dialogs + + /*return postbox.transaction { transaction -> Signal in + var storeMessages: [StoreMessage] = [] + var remoteItemIds: [PeerId] = [] + + let parsedPeers: AccumulatedPeers + + switch dialogs { + case .savedDialogs(let dialogs, let messages, let chats, let users), .savedDialogs(_, let dialogs, let messages, let chats, let users): + parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users) + + loop: for dialog in dialogs { + switch dialog { + case let .savedDialog(_, peer, _): + remoteItemIds.append(peer.peerId) + } + } + + for message in messages { + var peerIsForum = false + if let peerId = message.peerId, let peer = parsedPeers.get(peerId), peer.isForum { + peerIsForum = true + } + if let storeMessage = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peerIsForum) { + storeMessages.append(storeMessage) + } + } + case .savedDialogsNotModified: + parsedPeers = AccumulatedPeers(transaction: transaction, chats: [], users: []) + } + + let resultingItemIds: [PeerId] = remoteItemIds + + return postbox.transaction { transaction -> Signal in + updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) + + transaction.setPeerPinnedThreads(peerId: accountPeerId, threadIds: resultingItemIds.map { $0.toInt64() }) + + let _ = transaction.addMessages(storeMessages, location: .UpperHistoryBlock) + + return .complete() + } + |> switchToLatest + } + |> switchToLatest*/ + + return .complete() + } +} diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index 1db7e2bbec4..d02ad2476c4 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 169 + return 170 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/State/SynchronizeSavedStickersOperation.swift b/submodules/TelegramCore/Sources/State/SynchronizeSavedStickersOperation.swift index 30dbb8d72dc..b6788804a48 100644 --- a/submodules/TelegramCore/Sources/State/SynchronizeSavedStickersOperation.swift +++ b/submodules/TelegramCore/Sources/State/SynchronizeSavedStickersOperation.swift @@ -61,7 +61,7 @@ public func addSavedSticker(postbox: Postbox, network: Network, file: TelegramMe if !found { fetchReference = packReference } - case .animatedEmoji, .animatedEmojiAnimations, .dice, .premiumGifts, .emojiGenericAnimations, .iconStatusEmoji, .iconTopicEmoji: + case .animatedEmoji, .animatedEmojiAnimations, .dice, .premiumGifts, .emojiGenericAnimations, .iconStatusEmoji, .iconChannelStatusEmoji, .iconTopicEmoji: break } if let fetchReference = fetchReference { diff --git a/submodules/TelegramCore/Sources/State/UpdateMessageService.swift b/submodules/TelegramCore/Sources/State/UpdateMessageService.swift index ea5a7ddb52c..6d559c42e3a 100644 --- a/submodules/TelegramCore/Sources/State/UpdateMessageService.swift +++ b/submodules/TelegramCore/Sources/State/UpdateMessageService.swift @@ -58,7 +58,7 @@ class UpdateMessageService: NSObject, MTMessageService { self.putNext(groups) } case let .updateShortChatMessage(flags, id, fromId, chatId, message, pts, ptsCount, date, fwdFrom, viaBotId, replyHeader, entities, ttlPeriod): - let generatedMessage = Api.Message.message(flags: flags, id: id, fromId: .peerUser(userId: fromId), peerId: Api.Peer.peerChat(chatId: chatId), fwdFrom: fwdFrom, viaBotId: viaBotId, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod) + let generatedMessage = Api.Message.message(flags: flags, id: id, fromId: .peerUser(userId: fromId), peerId: Api.Peer.peerChat(chatId: chatId), savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod) let update = Api.Update.updateNewMessage(message: generatedMessage, pts: pts, ptsCount: ptsCount) let groups = groupUpdates([update], users: [], chats: [], date: date, seqRange: nil) if groups.count != 0 { @@ -74,7 +74,7 @@ class UpdateMessageService: NSObject, MTMessageService { let generatedPeerId = Api.Peer.peerUser(userId: userId) - let generatedMessage = Api.Message.message(flags: flags, id: id, fromId: generatedFromId, peerId: generatedPeerId, fwdFrom: fwdFrom, viaBotId: viaBotId, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod) + let generatedMessage = Api.Message.message(flags: flags, id: id, fromId: generatedFromId, peerId: generatedPeerId, savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod) let update = Api.Update.updateNewMessage(message: generatedMessage, pts: pts, ptsCount: ptsCount) let groups = groupUpdates([update], users: [], chats: [], date: date, seqRange: nil) if groups.count != 0 { diff --git a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift index 534baeba6b7..b1eedff4e56 100644 --- a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift +++ b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift @@ -104,7 +104,7 @@ extension Api.MessageMedia { extension Api.Message { var rawId: Int32 { switch self { - case let .message(_, id, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, id, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): return id case let .messageEmpty(_, id, _): return id @@ -115,7 +115,7 @@ extension Api.Message { func id(namespace: MessageId.Namespace = Namespaces.Message.Cloud) -> MessageId? { switch self { - case let .message(_, id, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, id, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): let peerId: PeerId = messagePeerId.peerId return MessageId(peerId: peerId, namespace: namespace, id: id) case let .messageEmpty(_, id, peerId): @@ -132,7 +132,7 @@ extension Api.Message { var peerId: PeerId? { switch self { - case let .message(_, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): let peerId: PeerId = messagePeerId.peerId return peerId case let .messageEmpty(_, _, peerId): @@ -145,7 +145,7 @@ extension Api.Message { var timestamp: Int32? { switch self { - case let .message(_, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, _, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _): return date case let .messageService(_, _, _, _, _, date, _, _): return date @@ -156,7 +156,7 @@ extension Api.Message { var preCachedResources: [(MediaResource, Data)]? { switch self { - case let .message(_, _, _, _, _, _, _, _, _, media, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, _, _, _, _, _, _, _, _, _, media, _, _, _, _, _, _, _, _, _, _, _): return media?.preCachedResources default: return nil @@ -165,7 +165,7 @@ extension Api.Message { var preCachedStories: [StoryId: Api.StoryItem]? { switch self { - case let .message(_, _, _, _, _, _, _, _, _, media, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, _, _, _, _, _, _, _, _, _, media, _, _, _, _, _, _, _, _, _, _, _): return media?.preCachedStories default: return nil diff --git a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift index d833c3e2a9a..060f3d50f6d 100644 --- a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift +++ b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift @@ -2,34 +2,36 @@ import Postbox import SwiftSignalKit public struct UserLimitsConfiguration: Equatable { - public let maxPinnedChatCount: Int32 - public let maxArchivedPinnedChatCount: Int32 - public let maxChannelsCount: Int32 - public let maxPublicLinksCount: Int32 - public let maxSavedGifCount: Int32 - public let maxFavedStickerCount: Int32 - public let maxFoldersCount: Int32 - public let maxFolderChatsCount: Int32 - public let maxCaptionLength: Int32 - public let maxUploadFileParts: Int32 - public let maxAboutLength: Int32 - public let maxAnimatedEmojisInText: Int32 - public let maxReactionsPerMessage: Int32 - public let maxSharedFolderInviteLinks: Int32 - public let maxSharedFolderJoin: Int32 - public let maxStoryCaptionLength: Int32 - public let maxExpiringStoriesCount: Int32 - public let maxStoriesWeeklyCount: Int32 - public let maxStoriesMonthlyCount: Int32 - public let maxStoriesSuggestedReactions: Int32 - public let maxGiveawayChannelsCount: Int32 - public let maxGiveawayCountriesCount: Int32 - public let maxGiveawayPeriodSeconds: Int32 - public let maxChannelRecommendationsCount: Int32 + public var maxPinnedChatCount: Int32 + public var maxPinnedSavedChatCount: Int32 + public var maxArchivedPinnedChatCount: Int32 + public var maxChannelsCount: Int32 + public var maxPublicLinksCount: Int32 + public var maxSavedGifCount: Int32 + public var maxFavedStickerCount: Int32 + public var maxFoldersCount: Int32 + public var maxFolderChatsCount: Int32 + public var maxCaptionLength: Int32 + public var maxUploadFileParts: Int32 + public var maxAboutLength: Int32 + public var maxAnimatedEmojisInText: Int32 + public var maxReactionsPerMessage: Int32 + public var maxSharedFolderInviteLinks: Int32 + public var maxSharedFolderJoin: Int32 + public var maxStoryCaptionLength: Int32 + public var maxExpiringStoriesCount: Int32 + public var maxStoriesWeeklyCount: Int32 + public var maxStoriesMonthlyCount: Int32 + public var maxStoriesSuggestedReactions: Int32 + public var maxGiveawayChannelsCount: Int32 + public var maxGiveawayCountriesCount: Int32 + public var maxGiveawayPeriodSeconds: Int32 + public var maxChannelRecommendationsCount: Int32 public static var defaultValue: UserLimitsConfiguration { return UserLimitsConfiguration( maxPinnedChatCount: 5, + maxPinnedSavedChatCount: 5, maxArchivedPinnedChatCount: 100, maxChannelsCount: 500, maxPublicLinksCount: 10, @@ -58,6 +60,7 @@ public struct UserLimitsConfiguration: Equatable { public init( maxPinnedChatCount: Int32, + maxPinnedSavedChatCount: Int32, maxArchivedPinnedChatCount: Int32, maxChannelsCount: Int32, maxPublicLinksCount: Int32, @@ -83,6 +86,7 @@ public struct UserLimitsConfiguration: Equatable { maxChannelRecommendationsCount: Int32 ) { self.maxPinnedChatCount = maxPinnedChatCount + self.maxPinnedSavedChatCount = maxPinnedSavedChatCount self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount self.maxChannelsCount = maxChannelsCount self.maxPublicLinksCount = maxPublicLinksCount @@ -112,7 +116,10 @@ public struct UserLimitsConfiguration: Equatable { extension UserLimitsConfiguration { init(appConfiguration: AppConfiguration, isPremium: Bool) { let keySuffix = isPremium ? "_premium" : "_default" - let defaultValue = UserLimitsConfiguration.defaultValue + var defaultValue = UserLimitsConfiguration.defaultValue + if isPremium { + defaultValue.maxPinnedSavedChatCount = 100 + } func getValue(_ key: String, orElse defaultValue: Int32) -> Int32 { if let value = appConfiguration.data?[key + keySuffix] as? Double { @@ -131,6 +138,7 @@ extension UserLimitsConfiguration { } self.maxPinnedChatCount = getValue("dialogs_pinned_limit", orElse: defaultValue.maxPinnedChatCount) + self.maxPinnedSavedChatCount = getValue("saved_pinned_limit", orElse: defaultValue.maxPinnedSavedChatCount) self.maxArchivedPinnedChatCount = getValue("dialogs_folder_pinned_limit", orElse: defaultValue.maxArchivedPinnedChatCount) self.maxChannelsCount = getValue("channels_limit", orElse: defaultValue.maxChannelsCount) self.maxPublicLinksCount = getValue("channels_public_limit", orElse: defaultValue.maxPublicLinksCount) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index 0f83ef3cd64..140799e0b84 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -51,6 +51,7 @@ public struct Namespaces { public static let CloudEmojiGenericAnimations: Int32 = 9 public static let CloudIconStatusEmoji: Int32 = 10 public static let CloudIconTopicEmoji: Int32 = 11 + public static let CloudIconChannelStatusEmoji: Int32 = 12 } public struct OrderedItemList { @@ -81,6 +82,8 @@ public struct Namespaces { public static let CloudFeaturedGroupPhotoEmoji: Int32 = 24 public static let NewSessionReviews: Int32 = 25 public static let CloudFeaturedBackgroundIconEmoji: Int32 = 26 + public static let CloudFeaturedChannelStatusEmoji: Int32 = 27 + public static let CloudDisabledChannelStatusEmoji: Int32 = 28 } public struct CachedItemCollection { @@ -195,6 +198,7 @@ public struct OperationLogTags { public static let SynchronizeAutosaveItems = PeerOperationLogTag(value: 23) public static let SynchronizeViewStories = PeerOperationLogTag(value: 24) public static let SynchronizePeerStories = PeerOperationLogTag(value: 25) + public static let SynchronizePinnedSavedChats = PeerOperationLogTag(value: 26) } public struct LegacyPeerSummaryCounterTags: OptionSet, Sequence, Hashable { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMessageAttribute.swift index b5918615dd1..a0cad0ee134 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMessageAttribute.swift @@ -109,7 +109,7 @@ public class QuotedReplyMessageAttribute: MessageAttribute { extension QuotedReplyMessageAttribute { convenience init(apiHeader: Api.MessageFwdHeader, quote: EngineMessageReplyQuote?, isQuote: Bool) { switch apiHeader { - case let .messageFwdHeader(_, fromId, fromName, _, _, _, _, _, _): + case let .messageFwdHeader(_, fromId, fromName, _, _, _, _, _, _, _, _, _): self.init(peerId: fromId?.peerId, authorName: fromName, quote: quote, isQuote: isQuote) } } @@ -138,12 +138,3 @@ public class ReplyStoryAttribute: MessageAttribute { encoder.encode(self.storyId, forKey: "i") } } - -public extension Message { - var effectiveReplyThreadMessageId: MessageId? { - if let threadId = self.threadId { - return makeThreadIdMessageId(peerId: self.id.peerId, threadId: threadId) - } - return nil - } -} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_SourceReferenceMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_SourceReferenceMessageAttribute.swift index 5865f83a211..5b7d48e0967 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_SourceReferenceMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_SourceReferenceMessageAttribute.swift @@ -3,17 +3,20 @@ import Postbox public class SourceReferenceMessageAttribute: MessageAttribute { public let messageId: MessageId + public let associatedMessageIds: [MessageId] = [] public let associatedPeerIds: [PeerId] public init(messageId: MessageId) { self.messageId = messageId + self.associatedPeerIds = [messageId.peerId] } required public init(decoder: PostboxDecoder) { let namespaceAndId: Int64 = decoder.decodeInt64ForKey("i", orElse: 0) self.messageId = MessageId(peerId: PeerId(decoder.decodeInt64ForKey("p", orElse: 0)), namespace: Int32(namespaceAndId & 0xffffffff), id: Int32((namespaceAndId >> 32) & 0xffffffff)) + self.associatedPeerIds = [self.messageId.peerId] } @@ -24,4 +27,57 @@ public class SourceReferenceMessageAttribute: MessageAttribute { } } - +public class SourceAuthorInfoMessageAttribute: MessageAttribute { + public let originalAuthor: PeerId? + public let originalAuthorName: String? + public let orignalDate: Int32? + public let originalOutgoing: Bool + + public let associatedMessageIds: [MessageId] = [] + public let associatedPeerIds: [PeerId] + + public init(originalAuthor: PeerId?, originalAuthorName: String?, orignalDate: Int32?, originalOutgoing: Bool) { + self.originalAuthor = originalAuthor + self.originalAuthorName = originalAuthorName + self.orignalDate = orignalDate + self.originalOutgoing = originalOutgoing + + if let originalAuthor = self.originalAuthor { + self.associatedPeerIds = [originalAuthor] + } else { + self.associatedPeerIds = [] + } + } + + required public init(decoder: PostboxDecoder) { + self.originalAuthor = decoder.decodeOptionalInt64ForKey("oa").flatMap(PeerId.init) + self.originalAuthorName = decoder.decodeOptionalStringForKey("oan") + self.orignalDate = decoder.decodeOptionalInt32ForKey("od") + self.originalOutgoing = decoder.decodeBoolForKey("oout", orElse: false) + + if let originalAuthor = self.originalAuthor { + self.associatedPeerIds = [originalAuthor] + } else { + self.associatedPeerIds = [] + } + } + + public func encode(_ encoder: PostboxEncoder) { + if let originalAuthor = self.originalAuthor { + encoder.encodeInt64(originalAuthor.toInt64(), forKey: "oa") + } else { + encoder.encodeNil(forKey: "oa") + } + if let originalAuthorName = self.originalAuthorName { + encoder.encodeString(originalAuthorName, forKey: "oan") + } else { + encoder.encodeNil(forKey: "oan") + } + if let orignalDate = self.orignalDate { + encoder.encodeInt32(orignalDate, forKey: "od") + } else { + encoder.encodeNil(forKey: "od") + } + encoder.encodeBool(self.originalOutgoing, forKey: "oout") + } +} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_StandaloneAccountTransaction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_StandaloneAccountTransaction.swift index 566c9ca0bdf..f939085aa80 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_StandaloneAccountTransaction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_StandaloneAccountTransaction.swift @@ -190,6 +190,13 @@ public let telegramPostboxSeedConfiguration: SeedConfiguration = { } } return false + }, + automaticThreadIndexInfo: { peerId, _ in + if peerId.namespace == Namespaces.Peer.CloudUser { + return StoredMessageHistoryThreadInfo(data: CodableEntry(data: Data()), summary: StoredMessageHistoryThreadInfo.Summary(totalUnreadCount: 0, mutedUntil: nil)) + } else { + return nil + } } ) }() diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_SynchronizePinnedChatsOperation.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_SynchronizePinnedChatsOperation.swift index 24dd5195fe6..d312d46d36c 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_SynchronizePinnedChatsOperation.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_SynchronizePinnedChatsOperation.swift @@ -61,3 +61,21 @@ public func addSynchronizePinnedChatsOperation(transaction: Transaction, groupId } transaction.operationLogAddEntry(peerId: PeerId(namespace: PeerId.Namespace._internalFromInt32Value(0), id: PeerId.Id._internalFromInt64Value(Int64(rawId))), tag: OperationLogTags.SynchronizePinnedChats, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: operationContents) } + +public func addSynchronizePinnedSavedChatsOperation(transaction: Transaction, accountPeerId: PeerId) { + var previousItemIds = transaction.getPeerPinnedThreads(peerId: accountPeerId).map { PinnedItemId.peer(PeerId($0)) } + var updateLocalIndex: Int32? + + transaction.operationLogEnumerateEntries(peerId: PeerId(namespace: PeerId.Namespace._internalFromInt32Value(0), id: PeerId.Id._internalFromInt64Value(0)), tag: OperationLogTags.SynchronizePinnedSavedChats, { entry in + updateLocalIndex = entry.tagLocalIndex + if let contents = entry.contents as? SynchronizePinnedChatsOperation { + previousItemIds = contents.previousItemIds + } + return false + }) + let operationContents = SynchronizePinnedChatsOperation(previousItemIds: previousItemIds) + if let updateLocalIndex = updateLocalIndex { + let _ = transaction.operationLogRemoveEntry(peerId: PeerId(namespace: PeerId.Namespace._internalFromInt32Value(0), id: PeerId.Id._internalFromInt64Value(0)), tag: OperationLogTags.SynchronizePinnedSavedChats, tagLocalIndex: updateLocalIndex) + } + transaction.operationLogAddEntry(peerId: PeerId(namespace: PeerId.Namespace._internalFromInt32Value(0), id: PeerId.Id._internalFromInt64Value(0)), tag: OperationLogTags.SynchronizePinnedSavedChats, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: operationContents) +} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaExpiredContent.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaExpiredContent.swift index cfa0f73ca67..da0dc939cb6 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaExpiredContent.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaExpiredContent.swift @@ -4,6 +4,8 @@ import Postbox public enum TelegramMediaExpiredContentData: Int32 { case image case file + case voiceMessage + case videoMessage } public final class TelegramMediaExpiredContent: Media, Equatable { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift index 0dbfbf7aa36..1ae8b35fd7d 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift @@ -23,24 +23,27 @@ public enum StickerPackReference: PostboxCoding, Hashable, Equatable, Codable { case emojiGenericAnimations case iconStatusEmoji case iconTopicEmoji + case iconChannelStatusEmoji public init(decoder: PostboxDecoder) { switch decoder.decodeInt32ForKey("r", orElse: 0) { - case 0: - self = .id(id: decoder.decodeInt64ForKey("i", orElse: 0), accessHash: decoder.decodeInt64ForKey("h", orElse: 0)) - case 1: - self = .name(decoder.decodeStringForKey("n", orElse: "")) - case 2: - self = .animatedEmoji - case 3: - self = .dice(decoder.decodeStringForKey("e", orElse: "🎲")) - case 4: - self = .animatedEmojiAnimations - case 5: - self = .premiumGifts - default: - self = .name("") - assertionFailure() + case 0: + self = .id(id: decoder.decodeInt64ForKey("i", orElse: 0), accessHash: decoder.decodeInt64ForKey("h", orElse: 0)) + case 1: + self = .name(decoder.decodeStringForKey("n", orElse: "")) + case 2: + self = .animatedEmoji + case 3: + self = .dice(decoder.decodeStringForKey("e", orElse: "🎲")) + case 4: + self = .animatedEmojiAnimations + case 5: + self = .premiumGifts + case 6: + self = .iconChannelStatusEmoji + default: + self = .name("") + assertionFailure() } } @@ -61,6 +64,8 @@ public enum StickerPackReference: PostboxCoding, Hashable, Equatable, Codable { self = .animatedEmojiAnimations case 5: self = .premiumGifts + case 6: + self = .iconChannelStatusEmoji default: self = .name("") assertionFailure() @@ -85,7 +90,7 @@ public enum StickerPackReference: PostboxCoding, Hashable, Equatable, Codable { encoder.encodeInt32(4, forKey: "r") case .premiumGifts: encoder.encodeInt32(5, forKey: "r") - case .emojiGenericAnimations, .iconStatusEmoji, .iconTopicEmoji: + case .emojiGenericAnimations, .iconStatusEmoji, .iconTopicEmoji, .iconChannelStatusEmoji: preconditionFailure() } } @@ -110,7 +115,7 @@ public enum StickerPackReference: PostboxCoding, Hashable, Equatable, Codable { try container.encode(4 as Int32, forKey: "r") case .premiumGifts: try container.encode(5 as Int32, forKey: "r") - case .emojiGenericAnimations, .iconStatusEmoji, .iconTopicEmoji: + case .emojiGenericAnimations, .iconStatusEmoji, .iconTopicEmoji, .iconChannelStatusEmoji: preconditionFailure() } } @@ -171,6 +176,12 @@ public enum StickerPackReference: PostboxCoding, Hashable, Equatable, Codable { } else { return false } + case .iconChannelStatusEmoji: + if case .iconChannelStatusEmoji = rhs { + return true + } else { + return false + } } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift index 03845767f65..8107e653cd6 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift @@ -37,6 +37,7 @@ public enum EngineConfiguration { public struct UserLimits: Equatable { public let maxPinnedChatCount: Int32 + public let maxPinnedSavedChatCount: Int32 public let maxArchivedPinnedChatCount: Int32 public let maxChannelsCount: Int32 public let maxPublicLinksCount: Int32 @@ -67,6 +68,7 @@ public enum EngineConfiguration { public init( maxPinnedChatCount: Int32, + maxPinnedSavedChatCount: Int32, maxArchivedPinnedChatCount: Int32, maxChannelsCount: Int32, maxPublicLinksCount: Int32, @@ -92,6 +94,7 @@ public enum EngineConfiguration { maxChannelRecommendationsCount: Int32 ) { self.maxPinnedChatCount = maxPinnedChatCount + self.maxPinnedSavedChatCount = maxPinnedSavedChatCount self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount self.maxChannelsCount = maxChannelsCount self.maxPublicLinksCount = maxPublicLinksCount @@ -153,6 +156,7 @@ public extension EngineConfiguration.UserLimits { init(_ userLimitsConfiguration: UserLimitsConfiguration) { self.init( maxPinnedChatCount: userLimitsConfiguration.maxPinnedChatCount, + maxPinnedSavedChatCount: userLimitsConfiguration.maxPinnedSavedChatCount, maxArchivedPinnedChatCount: userLimitsConfiguration.maxArchivedPinnedChatCount, maxChannelsCount: userLimitsConfiguration.maxChannelsCount, maxPublicLinksCount: userLimitsConfiguration.maxPublicLinksCount, diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index fbe82f18c49..39b69aeb036 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -1153,5 +1153,6 @@ public extension TelegramEngine.EngineData.Item { } } } + } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift index 4c8c2638ee8..fd85bd5d4b1 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift @@ -22,7 +22,7 @@ func addMessageMediaResourceIdsToRemove(message: Message, resourceIds: inout [Me } } -public func _internal_deleteMessages(transaction: Transaction, mediaBox: MediaBox, ids: [MessageId], deleteMedia: Bool = true, manualAddMessageThreadStatsDifference: ((MessageId, Int, Int) -> Void)? = nil) { +public func _internal_deleteMessages(transaction: Transaction, mediaBox: MediaBox, ids: [MessageId], deleteMedia: Bool = true, manualAddMessageThreadStatsDifference: ((MessageThreadKey, Int, Int) -> Void)? = nil) { var resourceIds: [MediaResourceId] = [] if deleteMedia { for id in ids { @@ -40,12 +40,12 @@ public func _internal_deleteMessages(transaction: Transaction, mediaBox: MediaBo if id.peerId.namespace == Namespaces.Peer.CloudChannel && id.namespace == Namespaces.Message.Cloud { if let message = transaction.getMessage(id) { if let threadId = message.threadId { - let messageThreadId = makeThreadIdMessageId(peerId: message.id.peerId, threadId: threadId) + let messageThreadKey = MessageThreadKey(peerId: message.id.peerId, threadId: threadId) if id.peerId.namespace == Namespaces.Peer.CloudChannel { if let manualAddMessageThreadStatsDifference = manualAddMessageThreadStatsDifference { - manualAddMessageThreadStatsDifference(messageThreadId, 0, 1) + manualAddMessageThreadStatsDifference(messageThreadKey, 0, 1) } else { - updateMessageThreadStats(transaction: transaction, threadMessageId: messageThreadId, removedCount: 1, addedMessagePeers: []) + updateMessageThreadStats(transaction: transaction, threadKey: messageThreadKey, removedCount: 1, addedMessagePeers: []) } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessagesInteractively.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessagesInteractively.swift index 18a38af631b..12e0fa696a7 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessagesInteractively.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessagesInteractively.swift @@ -148,7 +148,18 @@ func _internal_clearHistoryInteractively(postbox: Postbox, peerId: PeerId, threa } if let topIndex = topIndex { if peerId.namespace == Namespaces.Peer.CloudUser { - let _ = transaction.addMessages([StoreMessage(id: topIndex.id, globallyUniqueId: nil, groupingKey: nil, threadId: nil, timestamp: topIndex.timestamp, flags: StoreMessageFlags(), tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: nil, text: "", attributes: [], media: [TelegramMediaAction(action: .historyCleared)])], location: .Random) + var addEmptyMessage = false + if threadId == nil { + addEmptyMessage = true + } else { + if transaction.getTopPeerMessageId(peerId: peerId, namespace: Namespaces.Message.Cloud) == nil { + addEmptyMessage = true + } + } + + if addEmptyMessage { + let _ = transaction.addMessages([StoreMessage(id: topIndex.id, globallyUniqueId: nil, groupingKey: nil, threadId: nil, timestamp: topIndex.timestamp, flags: StoreMessageFlags(), tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: nil, text: "", attributes: [], media: [TelegramMediaAction(action: .historyCleared)])], location: .Random) + } } else { updatePeerChatInclusionWithMinTimestamp(transaction: transaction, id: peerId, minTimestamp: topIndex.timestamp, forceRootGroupIfNotExists: false) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/MarkMessageContentAsConsumedInteractively.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/MarkMessageContentAsConsumedInteractively.swift index 76d2aacf959..b181922619e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/MarkMessageContentAsConsumedInteractively.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/MarkMessageContentAsConsumedInteractively.swift @@ -198,8 +198,14 @@ func markMessageContentAsConsumedRemotely(transaction: Transaction, messageId: M for i in 0 ..< updatedMedia.count { if let _ = updatedMedia[i] as? TelegramMediaImage { updatedMedia[i] = TelegramMediaExpiredContent(data: .image) - } else if let _ = updatedMedia[i] as? TelegramMediaFile { - updatedMedia[i] = TelegramMediaExpiredContent(data: .file) + } else if let file = updatedMedia[i] as? TelegramMediaFile { + if file.isInstantVideo { + updatedMedia[i] = TelegramMediaExpiredContent(data: .videoMessage) + } else if file.isVoice { + updatedMedia[i] = TelegramMediaExpiredContent(data: .voiceMessage) + } else { + updatedMedia[i] = TelegramMediaExpiredContent(data: .file) + } } } } @@ -216,8 +222,14 @@ func markMessageContentAsConsumedRemotely(transaction: Transaction, messageId: M if attribute.timeout == viewOnceTimeout || timestamp >= countdownBeginTime + attribute.timeout { if let _ = updatedMedia[i] as? TelegramMediaImage { updatedMedia[i] = TelegramMediaExpiredContent(data: .image) - } else if let _ = updatedMedia[i] as? TelegramMediaFile { - updatedMedia[i] = TelegramMediaExpiredContent(data: .file) + } else if let file = updatedMedia[i] as? TelegramMediaFile { + if file.isInstantVideo { + updatedMedia[i] = TelegramMediaExpiredContent(data: .videoMessage) + } else if file.isVoice { + updatedMedia[i] = TelegramMediaExpiredContent(data: .voiceMessage) + } else { + updatedMedia[i] = TelegramMediaExpiredContent(data: .file) + } } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift index a55459cb39d..0695e05c0c6 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift @@ -17,7 +17,8 @@ private struct DiscussionMessage { private class ReplyThreadHistoryContextImpl { private let queue: Queue private let account: Account - private let messageId: MessageId + private let peerId: PeerId + private let threadId: Int64 private var currentHole: (MessageHistoryHolesViewEntry, Disposable)? @@ -68,7 +69,10 @@ private class ReplyThreadHistoryContextImpl { init(queue: Queue, account: Account, data: ChatReplyThreadMessage) { self.queue = queue self.account = account - self.messageId = data.messageId + self.peerId = data.peerId + self.threadId = data.threadId + + let referencedMessageId = MessageId(peerId: data.peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: data.threadId)) self.maxReadOutgoingMessageIdValue = data.maxReadOutgoingMessageId self.maxReadOutgoingMessageId.set(.single(self.maxReadOutgoingMessageIdValue)) @@ -79,7 +83,7 @@ private class ReplyThreadHistoryContextImpl { self.unreadCount.set(.single(self.unreadCountValue)) self.initialStateDisposable = (account.postbox.transaction { transaction -> State in - var indices = transaction.getThreadIndexHoles(peerId: data.messageId.peerId, threadId: makeMessageThreadId(data.messageId), namespace: Namespaces.Message.Cloud) + var indices = transaction.getThreadIndexHoles(peerId: data.peerId, threadId: data.threadId, namespace: Namespaces.Message.Cloud) indices.subtract(data.initialFilledHoles) /*let isParticipant = transaction.getPeerChatListIndex(data.messageId.peerId) != nil @@ -96,7 +100,7 @@ private class ReplyThreadHistoryContextImpl { indices.removeAll() } - return State(messageId: data.messageId, holeIndices: [Namespaces.Message.Cloud: indices], maxReadIncomingMessageId: data.maxReadIncomingMessageId, maxReadOutgoingMessageId: data.maxReadOutgoingMessageId) + return State(messageId: referencedMessageId, holeIndices: [Namespaces.Message.Cloud: indices], maxReadIncomingMessageId: data.maxReadIncomingMessageId, maxReadOutgoingMessageId: data.maxReadOutgoingMessageId) } |> deliverOn(self.queue)).start(next: { [weak self] state in guard let strongSelf = self else { @@ -106,7 +110,7 @@ private class ReplyThreadHistoryContextImpl { strongSelf.state.set(.single(state)) }) - let threadId = makeMessageThreadId(messageId) + let threadId = self.threadId self.holesDisposable = (account.postbox.messageHistoryHolesView() |> map { view -> MessageHistoryHolesViewEntry? in @@ -133,15 +137,15 @@ private class ReplyThreadHistoryContextImpl { guard let strongSelf = self else { return } - if let value = outgoing[data.messageId] { - strongSelf.maxReadOutgoingMessageIdValue = MessageId(peerId: data.messageId.peerId, namespace: Namespaces.Message.Cloud, id: value) + if let value = outgoing[referencedMessageId] { + strongSelf.maxReadOutgoingMessageIdValue = MessageId(peerId: data.peerId, namespace: Namespaces.Message.Cloud, id: value) } }) let accountPeerId = account.peerId let updateInitialState: Signal = account.postbox.transaction { transaction -> Peer? in - return transaction.getPeer(data.messageId.peerId) + return transaction.getPeer(data.peerId) } |> castError(FetchChannelReplyThreadMessageError.self) |> mapToSignal { peer -> Signal in @@ -151,8 +155,8 @@ private class ReplyThreadHistoryContextImpl { guard let inputPeer = apiInputPeer(peer) else { return .fail(.generic) } - - return account.network.request(Api.functions.messages.getDiscussionMessage(peer: inputPeer, msgId: data.messageId.id)) + + return account.network.request(Api.functions.messages.getDiscussionMessage(peer: inputPeer, msgId: Int32(clamping: data.threadId))) |> mapError { _ -> FetchChannelReplyThreadMessageError in return .generic } @@ -308,9 +312,10 @@ private class ReplyThreadHistoryContextImpl { } func applyMaxReadIndex(messageIndex: MessageIndex) { - let messageId = self.messageId + let peerId = self.peerId + let threadId = self.threadId - if messageIndex.id.namespace != messageId.namespace { + if messageIndex.id.namespace != Namespaces.Message.Cloud { return } @@ -326,16 +331,16 @@ private class ReplyThreadHistoryContextImpl { let account = self.account let _ = (self.account.postbox.transaction { transaction -> (Api.InputPeer?, MessageId?, Int?) in - if var data = transaction.getMessageHistoryThreadInfo(peerId: messageId.peerId, threadId: Int64(messageId.id))?.data.get(MessageHistoryThreadData.self) { + if var data = transaction.getMessageHistoryThreadInfo(peerId: peerId, threadId: threadId)?.data.get(MessageHistoryThreadData.self) { if messageIndex.id.id >= data.maxIncomingReadId { - if let count = transaction.getThreadMessageCount(peerId: messageId.peerId, threadId: Int64(messageId.id), namespace: messageId.namespace, fromIdExclusive: data.maxIncomingReadId, toIndex: messageIndex) { + if let count = transaction.getThreadMessageCount(peerId: peerId, threadId: threadId, namespace: Namespaces.Message.Cloud, fromIdExclusive: data.maxIncomingReadId, toIndex: messageIndex) { data.incomingUnreadCount = max(0, data.incomingUnreadCount - Int32(count)) data.maxIncomingReadId = messageIndex.id.id } - if let topMessageIndex = transaction.getMessageHistoryThreadTopMessage(peerId: messageId.peerId, threadId: Int64(messageId.id), namespaces: Set([Namespaces.Message.Cloud])) { + if let topMessageIndex = transaction.getMessageHistoryThreadTopMessage(peerId: peerId, threadId: threadId, namespaces: Set([Namespaces.Message.Cloud])) { if messageIndex.id.id >= topMessageIndex.id.id { - let containingHole = transaction.getThreadIndexHole(peerId: messageId.peerId, threadId: Int64(messageId.id), namespace: topMessageIndex.id.namespace, containing: topMessageIndex.id.id) + let containingHole = transaction.getThreadIndexHole(peerId: peerId, threadId: threadId, namespace: topMessageIndex.id.namespace, containing: topMessageIndex.id.id) if let _ = containingHole[.everywhere] { } else { data.incomingUnreadCount = 0 @@ -346,12 +351,13 @@ private class ReplyThreadHistoryContextImpl { data.maxKnownMessageId = max(data.maxKnownMessageId, messageIndex.id.id) if let entry = StoredMessageHistoryThreadInfo(data) { - transaction.setMessageHistoryThreadInfo(peerId: messageId.peerId, threadId: Int64(messageId.id), info: entry) + transaction.setMessageHistoryThreadInfo(peerId: peerId, threadId: threadId, info: entry) } } } - if let message = transaction.getMessage(messageId) { + let referencedMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)) + if let message = transaction.getMessage(referencedMessageId) { for attribute in message.attributes { if let attribute = attribute as? SourceReferenceMessageAttribute { if let sourceMessage = transaction.getMessage(attribute.messageId) { @@ -388,8 +394,8 @@ private class ReplyThreadHistoryContextImpl { } let inputPeer = transaction.getPeer(messageIndex.id.peerId).flatMap(apiInputPeer) - let readCount = transaction.getThreadMessageCount(peerId: messageId.peerId, threadId: makeMessageThreadId(messageId), namespace: messageId.namespace, fromIdExclusive: fromIdExclusive, toIndex: toIndex) - let topMessageId = transaction.getMessagesWithThreadId(peerId: messageId.peerId, namespace: messageId.namespace, threadId: makeMessageThreadId(messageId), from: MessageIndex.upperBound(peerId: messageId.peerId, namespace: messageId.namespace), includeFrom: false, to: MessageIndex.lowerBound(peerId: messageId.peerId, namespace: messageId.namespace), limit: 1).first?.id + let readCount = transaction.getThreadMessageCount(peerId: peerId, threadId: threadId, namespace: Namespaces.Message.Cloud, fromIdExclusive: fromIdExclusive, toIndex: toIndex) + let topMessageId = transaction.getMessagesWithThreadId(peerId: peerId, namespace: Namespaces.Message.Cloud, threadId: threadId, from: MessageIndex.upperBound(peerId: peerId, namespace: Namespaces.Message.Cloud), includeFrom: false, to: MessageIndex.lowerBound(peerId: peerId, namespace: Namespaces.Message.Cloud), limit: 1).first?.id return (inputPeer, topMessageId, readCount) } @@ -434,17 +440,17 @@ private class ReplyThreadHistoryContextImpl { } } - var signal = strongSelf.account.network.request(Api.functions.messages.readDiscussion(peer: inputPeer, msgId: messageId.id, readMaxId: messageIndex.id.id)) + var signal = strongSelf.account.network.request(Api.functions.messages.readDiscussion(peer: inputPeer, msgId: Int32(clamping: threadId), readMaxId: messageIndex.id.id)) |> `catch` { _ -> Signal in return .single(.boolFalse) } |> ignoreValues if revalidate { - let validateSignal = strongSelf.account.network.request(Api.functions.messages.getDiscussionMessage(peer: inputPeer, msgId: messageId.id)) + let validateSignal = strongSelf.account.network.request(Api.functions.messages.getDiscussionMessage(peer: inputPeer, msgId: Int32(clamping: threadId))) |> map { result -> (MessageId?, Int) in switch result { case let .discussionMessage(_, _, _, readInboxMaxId, _, unreadCount, _, _): - return (readInboxMaxId.flatMap({ MessageId(peerId: messageId.peerId, namespace: messageId.namespace, id: $0) }), Int(unreadCount)) + return (readInboxMaxId.flatMap({ MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) }), Int(unreadCount)) } } |> `catch` { _ -> Signal<(MessageId?, Int)?, NoError> in @@ -494,7 +500,7 @@ public class ReplyThreadHistoryContext { self.impl.with { impl in let stateDisposable = impl.state.get().start(next: { state in subscriber.putNext(MessageHistoryViewExternalInput( - content: .thread(peerId: state.messageId.peerId, id: makeMessageThreadId(state.messageId), holes: state.holeIndices), + content: .thread(peerId: state.messageId.peerId, id: Int64(state.messageId.id), holes: state.holeIndices), maxReadIncomingMessageId: state.maxReadIncomingMessageId, maxReadOutgoingMessageId: state.maxReadOutgoingMessageId )) @@ -554,7 +560,7 @@ public struct ChatReplyThreadMessage: Equatable { case lowerBoundMessage(MessageIndex) } - public var messageId: MessageId + public var peerId: PeerId public var threadId: Int64 public var channelMessageId: MessageId? public var isChannelPost: Bool @@ -567,8 +573,16 @@ public struct ChatReplyThreadMessage: Equatable { public var initialAnchor: Anchor public var isNotAvailable: Bool - public init(messageId: MessageId, threadId: Int64, channelMessageId: MessageId?, isChannelPost: Bool, isForumPost: Bool, maxMessage: MessageId?, maxReadIncomingMessageId: MessageId?, maxReadOutgoingMessageId: MessageId?, unreadCount: Int, initialFilledHoles: IndexSet, initialAnchor: Anchor, isNotAvailable: Bool) { - self.messageId = messageId + public var effectiveMessageId: MessageId? { + if self.peerId.namespace == Namespaces.Peer.CloudChannel { + return MessageId(peerId: self.peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: self.threadId)) + } else { + return nil + } + } + + public init(peerId: PeerId, threadId: Int64, channelMessageId: MessageId?, isChannelPost: Bool, isForumPost: Bool, maxMessage: MessageId?, maxReadIncomingMessageId: MessageId?, maxReadOutgoingMessageId: MessageId?, unreadCount: Int, initialFilledHoles: IndexSet, initialAnchor: Anchor, isNotAvailable: Bool) { + self.peerId = peerId self.threadId = threadId self.channelMessageId = channelMessageId self.isChannelPost = isChannelPost @@ -584,7 +598,7 @@ public struct ChatReplyThreadMessage: Equatable { public var normalized: ChatReplyThreadMessage { if self.isForumPost { - return ChatReplyThreadMessage(messageId: self.messageId, threadId: self.threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false) + return ChatReplyThreadMessage(peerId: self.peerId, threadId: self.threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false) } else { return self } @@ -760,32 +774,6 @@ func _internal_fetchChannelReplyThreadMessage(account: Account, messageId: Messa |> mapToSignal { threadData -> Signal<(FetchMessageHistoryHoleThreadInput, PeerId, MessageId?, Anchor, MessageId?), FetchChannelReplyThreadMessageError> in if let _ = threadData, !"".isEmpty { return .fail(.generic) - /*return account.postbox.transaction { transaction -> (FetchMessageHistoryHoleThreadInput, PeerId, MessageId?, Anchor, MessageId?) in - var threadInput: FetchMessageHistoryHoleThreadInput = .threadFromChannel(channelMessageId: messageId) - var threadMessageId: MessageId? - transaction.scanMessageAttributes(peerId: messageId.peerId, namespace: Namespaces.Message.Cloud, limit: 1000, { id, attributes in - for attribute in attributes { - if let attribute = attribute as? SourceReferenceMessageAttribute { - if attribute.messageId == messageId { - threadMessageId = id - threadInput = .direct(peerId: id.peerId, threadId: makeMessageThreadId(id)) - return false - } - } - } - return true - }) - let anchor: Anchor - if let atMessageId = atMessageId { - anchor = .message(atMessageId) - } else if let maxReadIncomingMessageId = replyInfo.maxReadIncomingMessageId { - anchor = .message(maxReadIncomingMessageId) - } else { - anchor = .lowerBound - } - return (threadInput, replyInfo.commentsPeerId, threadMessageId, anchor, replyInfo.maxMessageId) - } - |> castError(FetchChannelReplyThreadMessageError.self)*/ } else { return discussionMessage.get() |> take(1) @@ -805,7 +793,7 @@ func _internal_fetchChannelReplyThreadMessage(account: Account, messageId: Messa } else { anchor = .lowerBound } - return .single((.direct(peerId: commentsPeerId, threadId: makeMessageThreadId(topMessageId)), commentsPeerId, discussionMessage.messageId, anchor, discussionMessage.maxMessage)) + return .single((.direct(peerId: commentsPeerId, threadId: Int64(topMessageId.id)), commentsPeerId, discussionMessage.messageId, anchor, discussionMessage.maxMessage)) } } } @@ -820,7 +808,7 @@ func _internal_fetchChannelReplyThreadMessage(account: Account, messageId: Messa } return account.postbox.transaction { transaction -> Signal<(FetchMessageHistoryHoleResult?, ChatReplyThreadMessage.Anchor), FetchChannelReplyThreadMessageError> in if let threadMessageId = threadMessageId { - var holes = transaction.getThreadIndexHoles(peerId: threadMessageId.peerId, threadId: makeMessageThreadId(threadMessageId), namespace: Namespaces.Message.Cloud) + var holes = transaction.getThreadIndexHoles(peerId: threadMessageId.peerId, threadId: Int64(threadMessageId.id), namespace: Namespaces.Message.Cloud) holes.remove(integersIn: Int(maxMessageId.id + 1) ..< Int(Int32.max)) let isParticipant = transaction.getPeerChatListIndex(commentsPeerId) != nil @@ -843,7 +831,7 @@ func _internal_fetchChannelReplyThreadMessage(account: Account, messageId: Messa input: .external(MessageHistoryViewExternalInput( content: .thread( peerId: commentsPeerId, - id: makeMessageThreadId(threadMessageId), + id: Int64(threadMessageId.id), holes: [ Namespaces.Message.Cloud: holes ] @@ -932,12 +920,12 @@ func _internal_fetchChannelReplyThreadMessage(account: Account, messageId: Messa return account.postbox.transaction { transaction -> Signal in if let initialFilledHoles = initialFilledHoles { for range in initialFilledHoles.strictRemovedIndices.rangeView { - transaction.removeHole(peerId: discussionMessage.messageId.peerId, threadId: makeMessageThreadId(discussionMessage.messageId), namespace: Namespaces.Message.Cloud, space: .everywhere, range: Int32(range.lowerBound) ... Int32(range.upperBound)) + transaction.removeHole(peerId: discussionMessage.messageId.peerId, threadId: Int64(discussionMessage.messageId.id), namespace: Namespaces.Message.Cloud, space: .everywhere, range: Int32(range.lowerBound) ... Int32(range.upperBound)) } } return .single(ChatReplyThreadMessage( - messageId: discussionMessage.messageId, + peerId: discussionMessage.messageId.peerId, threadId: Int64(discussionMessage.messageId.id), channelMessageId: discussionMessage.channelMessageId, isChannelPost: discussionMessage.isChannelPost, diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift index 2793b3d89c0..7b8ad489619 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift @@ -8,7 +8,7 @@ import MtProtoKit public enum SearchMessagesLocation: Equatable { case general(tags: MessageTags?, minDate: Int32?, maxDate: Int32?) case group(groupId: PeerGroupId, tags: MessageTags?, minDate: Int32?, maxDate: Int32?) - case peer(peerId: PeerId, fromId: PeerId?, tags: MessageTags?, topMsgId: MessageId?, minDate: Int32?, maxDate: Int32?) + case peer(peerId: PeerId, fromId: PeerId?, tags: MessageTags?, threadId: Int64?, minDate: Int32?, maxDate: Int32?) case sentMedia(tags: MessageTags?) } @@ -139,7 +139,7 @@ private func mergedState(transaction: Transaction, seedConfiguration: SeedConfig for attribute in renderedMessage.attributes { if let attribute = attribute as? ReplyMessageAttribute { if let threadMessageId = attribute.threadMessageId { - let threadId = makeMessageThreadId(threadMessageId) + let threadId = Int64(threadMessageId.id) if let data = transaction.getMessageHistoryThreadInfo(peerId: peerId, threadId: threadId)?.data.get(MessageHistoryThreadData.self) { threadInfo[renderedMessage.id] = data break @@ -224,7 +224,7 @@ private func mergedResult(_ state: SearchMessagesState) -> SearchMessagesResult func _internal_searchMessages(account: Account, location: SearchMessagesLocation, query: String, state: SearchMessagesState?, limit: Int32 = 100) -> Signal<(SearchMessagesResult, SearchMessagesState), NoError> { let remoteSearchResult: Signal<(Api.messages.Messages?, Api.messages.Messages?), NoError> switch location { - case let .peer(peerId, fromId, tags, topMsgId, minDate, maxDate): + case let .peer(peerId, fromId, tags, threadId, minDate, maxDate): if peerId.namespace == Namespaces.Peer.SecretChat { return account.postbox.transaction { transaction -> (SearchMessagesResult, SearchMessagesState) in var readStates: [PeerId: CombinedPeerReadState] = [:] @@ -238,7 +238,7 @@ func _internal_searchMessages(account: Account, location: SearchMessagesLocation for attribute in message.attributes { if let attribute = attribute as? ReplyMessageAttribute { if let threadMessageId = attribute.threadMessageId { - let threadId = makeMessageThreadId(threadMessageId) + let threadId = Int64(threadMessageId.id) if let data = transaction.getMessageHistoryThreadInfo(peerId: peerId, threadId: threadId)?.data.get(MessageHistoryThreadData.self) { threadInfo[message.id] = data break @@ -253,7 +253,7 @@ func _internal_searchMessages(account: Account, location: SearchMessagesLocation } let filter: Api.MessagesFilter = tags.flatMap { messageFilterForTagMask($0) } ?? .inputMessagesFilterEmpty - remoteSearchResult = account.postbox.transaction { transaction -> (peer: Peer, additionalPeer: Peer?, from: Peer?)? in + remoteSearchResult = account.postbox.transaction { transaction -> (peer: Peer, additionalPeer: Peer?, from: Peer?, subPeer: Peer?)? in guard let peer = transaction.getPeer(peerId) else { return nil } @@ -261,10 +261,12 @@ func _internal_searchMessages(account: Account, location: SearchMessagesLocation if let _ = peer as? TelegramChannel, let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData, let migrationReference = cachedData.migrationReference { additionalPeer = transaction.getPeer(migrationReference.maxMessageId.peerId) } - if let fromId = fromId { - return (peer: peer, additionalPeer: additionalPeer, from: transaction.getPeer(fromId)) + var subPeer: Peer? + if peerId == account.peerId, let threadId = threadId { + subPeer = transaction.getPeer(PeerId(threadId)) } - return (peer: peer, additionalPeer: additionalPeer, from: nil) + + return (peer: peer, additionalPeer: additionalPeer, from: fromId.flatMap(transaction.getPeer), subPeer) } |> mapToSignal { values -> Signal<(Api.messages.Messages?, Api.messages.Messages?), NoError> in guard let values = values else { @@ -282,8 +284,18 @@ func _internal_searchMessages(account: Account, location: SearchMessagesLocation flags |= (1 << 0) } } - if let _ = topMsgId { + var inputSavedPeer: Api.InputPeer? = nil + if let subPeer = values.subPeer { + if let inputPeer = apiInputPeer(subPeer) { + inputSavedPeer = inputPeer + flags |= (1 << 2) + } + } + var topMsgId: Int32? + if peerId == account.peerId { + } else if let threadId = threadId { flags |= (1 << 1) + topMsgId = Int32(clamping: threadId) } let peerMessages: Signal if let completed = state?.main.completed, completed { @@ -294,7 +306,7 @@ func _internal_searchMessages(account: Account, location: SearchMessagesLocation if peer.id.namespace == Namespaces.Peer.CloudChannel && query.isEmpty && fromId == nil && tags == nil && minDate == nil && maxDate == nil { signal = account.network.request(Api.functions.messages.getHistory(peer: inputPeer, offsetId: lowerBound?.id.id ?? 0, offsetDate: 0, addOffset: 0, limit: limit, maxId: Int32.max - 1, minId: 0, hash: 0)) } else { - signal = account.network.request(Api.functions.messages.search(flags: flags, peer: inputPeer, q: query, fromId: fromInputPeer, topMsgId: topMsgId?.id, filter: filter, minDate: minDate ?? 0, maxDate: maxDate ?? (Int32.max - 1), offsetId: lowerBound?.id.id ?? 0, addOffset: 0, limit: limit, maxId: Int32.max - 1, minId: 0, hash: 0)) + signal = account.network.request(Api.functions.messages.search(flags: flags, peer: inputPeer, q: query, fromId: fromInputPeer, savedPeerId: inputSavedPeer, topMsgId: topMsgId, filter: filter, minDate: minDate ?? 0, maxDate: maxDate ?? (Int32.max - 1), offsetId: lowerBound?.id.id ?? 0, addOffset: 0, limit: limit, maxId: Int32.max - 1, minId: 0, hash: 0)) } peerMessages = signal |> map(Optional.init) @@ -310,7 +322,7 @@ func _internal_searchMessages(account: Account, location: SearchMessagesLocation additionalPeerMessages = .single(nil) } else if mainCompleted || !hasAdditional { let lowerBound = state?.additional?.messages.last.flatMap({ $0.index }) - additionalPeerMessages = account.network.request(Api.functions.messages.search(flags: flags, peer: inputPeer, q: query, fromId: fromInputPeer, topMsgId: topMsgId?.id, filter: filter, minDate: minDate ?? 0, maxDate: maxDate ?? (Int32.max - 1), offsetId: lowerBound?.id.id ?? 0, addOffset: 0, limit: limit, maxId: Int32.max - 1, minId: 0, hash: 0)) + additionalPeerMessages = account.network.request(Api.functions.messages.search(flags: flags, peer: inputPeer, q: query, fromId: fromInputPeer, savedPeerId: inputSavedPeer, topMsgId: topMsgId, filter: filter, minDate: minDate ?? 0, maxDate: maxDate ?? (Int32.max - 1), offsetId: lowerBound?.id.id ?? 0, addOffset: 0, limit: limit, maxId: Int32.max - 1, minId: 0, hash: 0)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) @@ -397,7 +409,7 @@ func _internal_searchMessages(account: Account, location: SearchMessagesLocation for attribute in message.attributes { if let attribute = attribute as? ReplyMessageAttribute { if let threadMessageId = attribute.threadMessageId { - let threadId = makeMessageThreadId(threadMessageId) + let threadId = Int64(threadMessageId.id) if let data = transaction.getMessageHistoryThreadInfo(peerId: message.id.peerId, threadId: threadId)?.data.get(MessageHistoryThreadData.self) { threadInfo[message.id] = data break @@ -603,10 +615,11 @@ func _internal_searchMessageIdByTimestamp(account: Account, peerId: PeerId, thre return .single(transaction.findClosestMessageIdByTimestamp(peerId: peerId, timestamp: timestamp)) } else if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { if let threadId = threadId { - let primaryIndex = account.network.request(Api.functions.messages.getReplies(peer: inputPeer, msgId: makeThreadIdMessageId(peerId: peerId, threadId: threadId).id, offsetId: 0, offsetDate: timestamp, addOffset: -1, limit: 1, maxId: 0, minId: 0, hash: 0)) - |> map { result -> MessageIndex? in - let messages: [Api.Message] - switch result { + if peerId.namespace == Namespaces.Peer.CloudChannel { + let primaryIndex = account.network.request(Api.functions.messages.getReplies(peer: inputPeer, msgId: Int32(clamping: threadId), offsetId: 0, offsetDate: timestamp, addOffset: -1, limit: 1, maxId: 0, minId: 0, hash: 0)) + |> map { result -> MessageIndex? in + let messages: [Api.Message] + switch result { case let .messages(apiMessages, _, _): messages = apiMessages case let .channelMessages(_, _, _, _, apiMessages, _, _, _): @@ -615,21 +628,55 @@ func _internal_searchMessageIdByTimestamp(account: Account, peerId: PeerId, thre messages = apiMessages case .messagesNotModified: messages = [] + } + for message in messages { + if let message = StoreMessage(apiMessage: message, accountPeerId: account.peerId, peerIsForum: peer.isForum) { + return message.index + } + } + return nil } - for message in messages { - if let message = StoreMessage(apiMessage: message, accountPeerId: account.peerId, peerIsForum: peer.isForum) { - return message.index + |> `catch` { _ -> Signal in + return .single(nil) + } + return primaryIndex + |> map { primaryIndex -> MessageId? in + return primaryIndex?.id + } + } else if peerId == account.peerId { + guard let subPeer = transaction.getPeer(PeerId(threadId)), let inputSubPeer = apiInputPeer(subPeer) else { + return .single(nil) + } + let primaryIndex = account.network.request(Api.functions.messages.getSavedHistory(peer: inputSubPeer, offsetId: 0, offsetDate: timestamp, addOffset: -1, limit: 1, maxId: 0, minId: 0, hash: 0)) + |> map { result -> MessageIndex? in + let messages: [Api.Message] + switch result { + case let .messages(apiMessages, _, _): + messages = apiMessages + case let .channelMessages(_, _, _, _, apiMessages, _, _, _): + messages = apiMessages + case let .messagesSlice(_, _, _, _, apiMessages, _, _): + messages = apiMessages + case .messagesNotModified: + messages = [] + } + for message in messages { + if let message = StoreMessage(apiMessage: message, accountPeerId: account.peerId, peerIsForum: peer.isForum) { + return message.index + } } + return nil } - return nil - } - |> `catch` { _ -> Signal in + |> `catch` { _ -> Signal in + return .single(nil) + } + return primaryIndex + |> map { primaryIndex -> MessageId? in + return primaryIndex?.id + } + } else { return .single(nil) } - return primaryIndex - |> map { primaryIndex -> MessageId? in - return primaryIndex?.id - } } else { var secondaryIndex: Signal = .single(nil) if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData, let migrationReference = cachedData.migrationReference, let secondaryPeer = transaction.getPeer(migrationReference.maxMessageId.peerId), let inputSecondaryPeer = apiInputPeer(secondaryPeer) { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift index 0ddbc9f3515..b62f85bb8b7 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift @@ -105,14 +105,15 @@ public final class SparseMessageList { self.sparseItemsDisposable = (self.account.postbox.transaction { transaction -> Api.InputPeer? in return transaction.getPeer(peerId).flatMap(apiInputPeer) } - |> mapToSignal { inputPeer -> Signal in + |> mapToSignal { inputPeer -> Signal in guard let inputPeer = inputPeer else { return .single(SparseItems(items: [])) } guard let messageFilter = messageFilterForTagMask(messageTag) else { return .single(SparseItems(items: [])) } - return account.network.request(Api.functions.messages.getSearchResultsPositions(peer: inputPeer, filter: messageFilter, offsetId: 0, limit: 1000)) + //TODO:api + return account.network.request(Api.functions.messages.getSearchResultsPositions(flags: 0, peer: inputPeer, savedPeerId: nil, filter: messageFilter, offsetId: 0, limit: 1000)) |> map { result -> SparseItems in switch result { case let .searchResultsPositions(totalCount, positions): @@ -817,7 +818,8 @@ public final class SparseMessageCalendar { guard let messageFilter = messageFilterForTagMask(messageTag) else { return .single(LoadResult(messagesByDay: [:], nextOffset: nil, minMessageId: nil, minTimestamp: nil)) } - return self.account.network.request(Api.functions.messages.getSearchResultsCalendar(peer: inputPeer, filter: messageFilter, offsetId: nextRequestOffset, offsetDate: 0)) + //TODO:api + return self.account.network.request(Api.functions.messages.getSearchResultsCalendar(flags: 0, peer: inputPeer, savedPeerId: nil, filter: messageFilter, offsetId: nextRequestOffset, offsetDate: 0)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index ca0889f58b4..576323ba11f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -124,6 +124,14 @@ public extension TelegramEngine { } public func deleteMessagesInteractively(messageIds: [MessageId], type: InteractiveMessagesDeletionType, deleteAllInGroup: Bool = false) -> Signal { + self.account.stateManager.messagesRemovedContext.addIsMessagesDeletedInteractively(ids: messageIds.map { id -> DeletedMessageId in + if id.namespace == Namespaces.Message.Cloud && (id.peerId.namespace == Namespaces.Peer.CloudUser || id.peerId.namespace == Namespaces.Peer.CloudGroup) { + return .global(id.id) + } else { + return .messageId(id) + } + }) + return _internal_deleteMessagesInteractively(account: self.account, messageIds: messageIds, type: type, deleteAllInGroup: deleteAllInGroup) } @@ -421,10 +429,16 @@ public extension TelegramEngine { public func refreshMessageTagStats(peerId: EnginePeer.Id, threadId: Int64?, tags: [EngineMessage.Tags]) -> Signal { let account = self.account - return self.account.postbox.transaction { transaction -> Api.InputPeer? in - return transaction.getPeer(peerId).flatMap(apiInputPeer) + return self.account.postbox.transaction { transaction -> (Api.InputPeer?, Api.InputPeer?) in + var inputSavedPeer: Api.InputPeer? + if let threadId = threadId { + if peerId == account.peerId { + inputSavedPeer = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer) + } + } + return (transaction.getPeer(peerId).flatMap(apiInputPeer), inputSavedPeer) } - |> mapToSignal { inputPeer -> Signal in + |> mapToSignal { inputPeer, inputSavedPeer -> Signal in guard let inputPeer = inputPeer else { return .complete() } @@ -438,11 +452,17 @@ public extension TelegramEngine { var flags: Int32 = 0 var topMsgId: Int32? if let threadId = threadId { - flags |= (1 << 1) - topMsgId = Int32(clamping: threadId) + if peerId == account.peerId { + if inputSavedPeer != nil { + flags |= (1 << 2) + } + } else { + flags |= (1 << 1) + topMsgId = Int32(clamping: threadId) + } } - signals.append(self.account.network.request(Api.functions.messages.search(flags: flags, peer: inputPeer, q: "", fromId: nil, topMsgId: topMsgId, filter: filter, minDate: 0, maxDate: 0, offsetId: 0, addOffset: 0, limit: 1, maxId: 0, minId: 0, hash: 0)) + signals.append(self.account.network.request(Api.functions.messages.search(flags: flags, peer: inputPeer, q: "", fromId: nil, savedPeerId: inputSavedPeer, topMsgId: topMsgId, filter: filter, minDate: 0, maxDate: 0, offsetId: 0, addOffset: 0, limit: 1, maxId: 0, minId: 0, hash: 0)) |> map { result -> (count: Int32?, topId: Int32?) in switch result { case let .messagesSlice(_, count, _, _, messages, _, _): @@ -465,7 +485,18 @@ public extension TelegramEngine { for i in 0 ..< tags.count { let (count, maxId) = counts[i] if let count = count { - transaction.replaceMessageTagSummary(peerId: peerId, threadId: threadId, tagMask: tags[i], namespace: Namespaces.Message.Cloud, count: count, maxId: maxId ?? 1) + if count == 0, peerId == account.peerId, let threadId { + var localCount = 0 + var maxId: Int32 = 1 + transaction.scanMessages(peerId: peerId, threadId: threadId, namespace: Namespaces.Message.Cloud, tag: tags[i], { message in + localCount += 1 + maxId = max(maxId, message.id.id) + return true + }) + transaction.replaceMessageTagSummary(peerId: peerId, threadId: threadId, tagMask: tags[i], namespace: Namespaces.Message.Cloud, count: Int32(localCount), maxId: maxId) + } else { + transaction.replaceMessageTagSummary(peerId: peerId, threadId: threadId, tagMask: tags[i], namespace: Namespaces.Message.Cloud, count: count, maxId: maxId ?? 1) + } } } } @@ -1292,5 +1323,42 @@ public extension TelegramEngine { public func getStory(peerId: EnginePeer.Id, id: Int32) -> Signal { return _internal_getStoryById(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, peerId: peerId, id: id) } + + public func synchronouslyIsMessageDeletedInteractively(ids: [EngineMessage.Id]) -> [EngineMessage.Id] { + return self.account.stateManager.synchronouslyIsMessageDeletedInteractively(ids: ids) + } + + public func savedMessagesPeerListHead() -> Signal { + return self.account.postbox.combinedView(keys: [.savedMessagesIndex(peerId: self.account.peerId)]) + |> map { views -> EnginePeer.Id? in + //TODO:api optimize + guard let view = views.views[.savedMessagesIndex(peerId: self.account.peerId)] as? MessageHistorySavedMessagesIndexView else { + return nil + } + if view.isLoading { + return nil + } else { + return view.items.first?.peer?.id + } + } + } + + public func savedMessagesPeersStats() -> Signal { + return self.account.postbox.combinedView(keys: [.savedMessagesStats(peerId: self.account.peerId)]) + |> map { views -> Int? in + guard let view = views.views[.savedMessagesStats(peerId: self.account.peerId)] as? MessageHistorySavedMessagesStatsView else { + return nil + } + if view.isLoading { + return nil + } else { + return view.count + } + } + } + + public func searchLocalSavedMessagesPeers(query: String, indexNameMapping: [EnginePeer.Id: [PeerIndexNameRepresentation]]) -> Signal<[EnginePeer], NoError> { + return _internal_searchLocalSavedMessagesPeers(account: self.account, query: query, indexNameMapping: indexNameMapping) + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/RequestUserPhotos.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RequestUserPhotos.swift index 06eb921e8a2..e96e1ee6ba4 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/RequestUserPhotos.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RequestUserPhotos.swift @@ -68,7 +68,7 @@ func _internal_requestPeerPhotos(accountPeerId: PeerId, postbox: Postbox, networ } } } else if let peer = peer, let inputPeer = apiInputPeer(peer) { - return network.request(Api.functions.messages.search(flags: 0, peer: inputPeer, q: "", fromId: nil, topMsgId: nil, filter: .inputMessagesFilterChatPhotos, minDate: 0, maxDate: 0, offsetId: 0, addOffset: 0, limit: 1000, maxId: 0, minId: 0, hash: 0)) + return network.request(Api.functions.messages.search(flags: 0, peer: inputPeer, q: "", fromId: nil, savedPeerId: nil, topMsgId: nil, filter: .inputMessagesFilterChatPhotos, minDate: 0, maxDate: 0, offsetId: 0, addOffset: 0, limit: 1000, maxId: 0, minId: 0, hash: 0)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift index ccdb358a1fa..74f69e52fa0 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift @@ -83,3 +83,8 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo return processedSearchResult } +func _internal_searchLocalSavedMessagesPeers(account: Account, query: String, indexNameMapping: [EnginePeer.Id: [PeerIndexNameRepresentation]]) -> Signal<[EnginePeer], NoError> { + return account.postbox.transaction { transaction -> [EnginePeer] in + return transaction.searchSubPeers(peerId: account.peerId, query: query, indexNameMapping: indexNameMapping).map(EnginePeer.init) + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 1d684e538c5..a54c40408e4 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -1063,13 +1063,23 @@ public extension TelegramEngine { public func toggleForumChannelTopicPinned(id: EnginePeer.Id, threadId: Int64) -> Signal { return self.account.postbox.transaction { transaction -> ([Int64], Int) in - var limit = 5 - let appConfiguration: AppConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration)?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue - if let data = appConfiguration.data, let value = data["topics_pinned_limit"] as? Double { - limit = Int(value) + if id == self.account.peerId { + var limit = 5 + let appConfiguration: AppConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration)?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue + if let data = appConfiguration.data, let value = data["saved_pinned_limit"] as? Double { + limit = Int(value) + } + + return (transaction.getPeerPinnedThreads(peerId: id), limit) + } else { + var limit = 5 + let appConfiguration: AppConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration)?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue + if let data = appConfiguration.data, let value = data["topics_pinned_limit"] as? Double { + limit = Int(value) + } + + return (transaction.getPeerPinnedThreads(peerId: id), limit) } - - return (transaction.getPeerPinnedThreads(peerId: id), limit) } |> castError(SetForumChannelTopicPinnedError.self) |> mapToSignal { threadIds, limit -> Signal in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/CachedStickerPack.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/CachedStickerPack.swift index f304be11ea8..8e556c97fd5 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/CachedStickerPack.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/CachedStickerPack.swift @@ -36,6 +36,9 @@ func cacheStickerPack(transaction: Transaction, info: StickerPackCollectionInfo, case .iconStatusEmoji: namespace = Namespaces.ItemCollection.CloudIconStatusEmoji id = 0 + case .iconChannelStatusEmoji: + namespace = Namespaces.ItemCollection.CloudIconChannelStatusEmoji + id = 0 case .iconTopicEmoji: namespace = Namespaces.ItemCollection.CloudIconTopicEmoji id = 0 @@ -186,6 +189,20 @@ func _internal_cachedStickerPack(postbox: Postbox, network: Network, reference: } else { return (.fetching, true, nil) } + case .iconChannelStatusEmoji: + let namespace = Namespaces.ItemCollection.CloudIconChannelStatusEmoji + let id: ItemCollectionId.Id = 0 + if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id))))?.get(CachedStickerPack.self), let info = cached.info { + previousHash = cached.hash + let current: CachedStickerPackResult = .result(info, cached.items, false) + if cached.hash != info.hash { + return (current, true, previousHash) + } else { + return (current, false, previousHash) + } + } else { + return (.fetching, true, nil) + } case .iconTopicEmoji: let namespace = Namespaces.ItemCollection.CloudIconTopicEmoji let id: ItemCollectionId.Id = 0 @@ -333,6 +350,18 @@ func cachedStickerPack(transaction: Transaction, reference: StickerPackReference if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id))))?.get(CachedStickerPack.self), let info = cached.info { return (info, cached.items, false) } + case .iconChannelStatusEmoji: + let namespace = Namespaces.ItemCollection.CloudIconChannelStatusEmoji + let id: ItemCollectionId.Id = 0 + if let currentInfo = transaction.getItemCollectionInfo(collectionId: ItemCollectionId(namespace: namespace, id: id)) as? StickerPackCollectionInfo { + let items = transaction.getItemCollectionItems(collectionId: ItemCollectionId(namespace: namespace, id: id)) + if !items.isEmpty { + return (currentInfo, items.compactMap { $0 as? StickerPackItem }, true) + } + } + if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id))))?.get(CachedStickerPack.self), let info = cached.info { + return (info, cached.items, false) + } case .iconTopicEmoji: let namespace = Namespaces.ItemCollection.CloudIconTopicEmoji let id: ItemCollectionId.Id = 0 diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/LoadedStickerPack.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/LoadedStickerPack.swift index 7633401323a..b89e44c713b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/LoadedStickerPack.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/LoadedStickerPack.swift @@ -26,6 +26,8 @@ extension StickerPackReference { return .inputStickerSetEmojiGenericAnimations case .iconStatusEmoji: return .inputStickerSetEmojiDefaultStatuses + case .iconChannelStatusEmoji: + return .inputStickerSetEmojiChannelDefaultStatuses case .iconTopicEmoji: return .inputStickerSetEmojiDefaultTopicIcons } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerSetInstallation.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerSetInstallation.swift index 9653113edf9..657fb8c1987 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerSetInstallation.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerSetInstallation.swift @@ -52,6 +52,9 @@ func _internal_requestStickerSet(postbox: Postbox, network: Network, reference: case .iconStatusEmoji: collectionId = nil input = .inputStickerSetEmojiDefaultStatuses + case .iconChannelStatusEmoji: + collectionId = nil + input = .inputStickerSetEmojiChannelDefaultStatuses case .iconTopicEmoji: collectionId = nil input = .inputStickerSetEmojiDefaultTopicIcons diff --git a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift index 371dcbb67a5..84a3f55f968 100644 --- a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift @@ -114,7 +114,21 @@ public extension Message { return nil } + var sourceAuthorInfo: SourceAuthorInfoMessageAttribute? { + for attribute in self.attributes { + if let attribute = attribute as? SourceAuthorInfoMessageAttribute { + return attribute + } + } + return nil + } + var effectiveAuthor: Peer? { + if let sourceAuthorInfo = self.sourceAuthorInfo { + if let sourceAuthorId = sourceAuthorInfo.originalAuthor, let peer = self.peers[sourceAuthorId] { + return peer + } + } if let forwardInfo = self.forwardInfo, let sourceReference = self.sourceReference, forwardInfo.author?.id == sourceReference.messageId.peerId { if let peer = self.peers[sourceReference.messageId.peerId] { return peer @@ -282,6 +296,18 @@ func locallyRenderedMessage(message: StoreMessage, peers: AccumulatedPeers, asso public extension Message { func effectivelyIncoming(_ accountPeerId: PeerId) -> Bool { if self.id.peerId == accountPeerId { + if let sourceAuthorInfo = self.sourceAuthorInfo { + if sourceAuthorInfo.originalOutgoing { + return false + } else if let originalAuthor = sourceAuthorInfo.originalAuthor, originalAuthor == accountPeerId { + return false + } + } else if let forwardInfo = self.forwardInfo { + if let author = forwardInfo.author, author.id == accountPeerId { + return false + } + } + if self.forwardInfo != nil { return true } else { diff --git a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift index ca97ebc3a74..17157865f16 100644 --- a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift @@ -441,4 +441,13 @@ public extension PeerId { } return false } + + var isAnonymousSavedMessages: Bool { + if self.namespace == Namespaces.Peer.CloudUser { + if self.id._internalGetInt64Value() == 2666000 { + return true + } + } + return false + } } diff --git a/submodules/TelegramCore/Sources/Utils/UpdateMessageMedia.swift b/submodules/TelegramCore/Sources/Utils/UpdateMessageMedia.swift index a5d8c84011a..a8e92b70325 100644 --- a/submodules/TelegramCore/Sources/Utils/UpdateMessageMedia.swift +++ b/submodules/TelegramCore/Sources/Utils/UpdateMessageMedia.swift @@ -34,12 +34,12 @@ struct ReplyThreadUserMessage { var isOutgoing: Bool } -func updateMessageThreadStats(transaction: Transaction, threadMessageId: MessageId, removedCount: Int, addedMessagePeers: [ReplyThreadUserMessage]) { - updateMessageThreadStatsInternal(transaction: transaction, threadMessageId: threadMessageId, removedCount: removedCount, addedMessagePeers: addedMessagePeers, allowChannel: false) +func updateMessageThreadStats(transaction: Transaction, threadKey: MessageThreadKey, removedCount: Int, addedMessagePeers: [ReplyThreadUserMessage]) { + updateMessageThreadStatsInternal(transaction: transaction, threadKey: threadKey, removedCount: removedCount, addedMessagePeers: addedMessagePeers, allowChannel: false) } -private func updateMessageThreadStatsInternal(transaction: Transaction, threadMessageId: MessageId, removedCount: Int, addedMessagePeers: [ReplyThreadUserMessage], allowChannel: Bool) { - guard let channel = transaction.getPeer(threadMessageId.peerId) as? TelegramChannel else { +private func updateMessageThreadStatsInternal(transaction: Transaction, threadKey: MessageThreadKey, removedCount: Int, addedMessagePeers: [ReplyThreadUserMessage], allowChannel: Bool) { + guard let channel = transaction.getPeer(threadKey.peerId) as? TelegramChannel else { return } var isGroup = true @@ -75,7 +75,7 @@ private func updateMessageThreadStatsInternal(transaction: Transaction, threadMe return current } - transaction.updateMessage(threadMessageId, update: { currentMessage in + transaction.updateMessage(MessageId(peerId: threadKey.peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadKey.threadId)), update: { currentMessage in var attributes = currentMessage.attributes loop: for j in 0 ..< attributes.count { if let attribute = attributes[j] as? ReplyThreadMessageAttribute { @@ -117,6 +117,6 @@ private func updateMessageThreadStatsInternal(transaction: Transaction, threadMe }) if let channelThreadMessageId = channelThreadMessageId { - updateMessageThreadStatsInternal(transaction: transaction, threadMessageId: channelThreadMessageId, removedCount: removedCount, addedMessagePeers: addedMessagePeers, allowChannel: true) + updateMessageThreadStatsInternal(transaction: transaction, threadKey: MessageThreadKey(peerId: channelThreadMessageId.peerId, threadId: Int64(channelThreadMessageId.id)), removedCount: removedCount, addedMessagePeers: addedMessagePeers, allowChannel: true) } } diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index 96bb4d1af12..f450a51749c 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -188,6 +188,10 @@ private enum ApplicationSpecificGlobalNotice: Int32 { case dismissedPremiumWallpapersBadge = 54 case dismissedPremiumColorsBadge = 55 case multipleReactionsSuggestion = 56 + case savedMessagesChatsSuggestion = 57 + case voiceMessagesPlayOnceSuggestion = 58 + case incomingVoiceMessagePlayOnceTip = 59 + case outgoingVoiceMessagePlayOnceTip = 60 var key: ValueBoxKey { let v = ValueBoxKey(length: 4) @@ -453,18 +457,38 @@ private struct ApplicationSpecificNoticeKeys { static func dismissedPremiumAppIconsBadge() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.dismissedPremiumAppIconsBadge.key) } + static func replyQuoteTextSelectionTip() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.replyQuoteTextSelectionTip.key) } + static func dismissedPremiumWallpapersBadge() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.dismissedPremiumWallpapersBadge.key) } + static func dismissedPremiumColorsBadge() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.dismissedPremiumColorsBadge.key) } + static func multipleReactionsSuggestion() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.multipleReactionsSuggestion.key) } + + static func savedMessagesChatsSuggestion() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.savedMessagesChatsSuggestion.key) + } + + static func voiceMessagesPlayOnceSuggestion() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.voiceMessagesPlayOnceSuggestion.key) + } + + static func incomingVoiceMessagePlayOnceTip() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.incomingVoiceMessagePlayOnceTip.key) + } + + static func outgoingVoiceMessagePlayOnceTip() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.outgoingVoiceMessagePlayOnceTip.key) + } } public struct ApplicationSpecificNotice { @@ -1852,4 +1876,112 @@ public struct ApplicationSpecificNotice { return Int(previousValue) } } + + public static func getSavedMessagesChatsSuggestion(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Int32 in + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.savedMessagesChatsSuggestion())?.get(ApplicationSpecificCounterNotice.self) { + return value.value + } else { + return 0 + } + } + } + + public static func incrementSavedMessagesChatsSuggestion(accountManager: AccountManager, count: Int = 1) -> Signal { + return accountManager.transaction { transaction -> Int in + var currentValue: Int32 = 0 + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.savedMessagesChatsSuggestion())?.get(ApplicationSpecificCounterNotice.self) { + currentValue = value.value + } + let previousValue = currentValue + currentValue += Int32(count) + + if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) { + transaction.setNotice(ApplicationSpecificNoticeKeys.savedMessagesChatsSuggestion(), entry) + } + + return Int(previousValue) + } + } + + public static func getVoiceMessagesPlayOnceSuggestion(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Int32 in + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.voiceMessagesPlayOnceSuggestion())?.get(ApplicationSpecificCounterNotice.self) { + return value.value + } else { + return 0 + } + } + } + + public static func incrementVoiceMessagesPlayOnceSuggestion(accountManager: AccountManager, count: Int = 1) -> Signal { + return accountManager.transaction { transaction -> Int in + var currentValue: Int32 = 0 + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.voiceMessagesPlayOnceSuggestion())?.get(ApplicationSpecificCounterNotice.self) { + currentValue = value.value + } + let previousValue = currentValue + currentValue += Int32(count) + + if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) { + transaction.setNotice(ApplicationSpecificNoticeKeys.voiceMessagesPlayOnceSuggestion(), entry) + } + + return Int(previousValue) + } + } + + public static func getIncomingVoiceMessagePlayOnceTip(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Int32 in + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.incomingVoiceMessagePlayOnceTip())?.get(ApplicationSpecificCounterNotice.self) { + return value.value + } else { + return 0 + } + } + } + + public static func incrementIncomingVoiceMessagePlayOnceTip(accountManager: AccountManager, count: Int = 1) -> Signal { + return accountManager.transaction { transaction -> Int in + var currentValue: Int32 = 0 + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.incomingVoiceMessagePlayOnceTip())?.get(ApplicationSpecificCounterNotice.self) { + currentValue = value.value + } + let previousValue = currentValue + currentValue += Int32(count) + + if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) { + transaction.setNotice(ApplicationSpecificNoticeKeys.incomingVoiceMessagePlayOnceTip(), entry) + } + + return Int(previousValue) + } + } + + public static func getOutgoingVoiceMessagePlayOnceTip(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Int32 in + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.outgoingVoiceMessagePlayOnceTip())?.get(ApplicationSpecificCounterNotice.self) { + return value.value + } else { + return 0 + } + } + } + + public static func incrementOutgoingVoiceMessagePlayOnceTip(accountManager: AccountManager, count: Int = 1) -> Signal { + return accountManager.transaction { transaction -> Int in + var currentValue: Int32 = 0 + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.outgoingVoiceMessagePlayOnceTip())?.get(ApplicationSpecificCounterNotice.self) { + currentValue = value.value + } + let previousValue = currentValue + currentValue += Int32(count) + + if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) { + transaction.setNotice(ApplicationSpecificNoticeKeys.outgoingVoiceMessagePlayOnceTip(), entry) + } + + return Int(previousValue) + } + } } diff --git a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift index b5fcd56959f..3f4587e2070 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift @@ -714,7 +714,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio shareButtonFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0x748391, alpha: 0.45)), shareButtonStrokeColor: PresentationThemeVariableColor(withWallpaper: .clear, withoutWallpaper: .clear), shareButtonForegroundColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: UIColor(rgb: 0xffffff)), - mediaOverlayControlColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x000000, alpha: 0.6), foregroundColor: UIColor(rgb: 0xffffff)), + mediaOverlayControlColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x000000, alpha: 0.45), foregroundColor: UIColor(rgb: 0xffffff)), selectionControlColors: PresentationThemeFillStrokeForeground(fillColor: defaultDayAccentColor, strokeColor: UIColor(rgb: 0xc7c7cc), foregroundColor: UIColor(rgb: 0xffffff)), deliveryFailedColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xff3b30), foregroundColor: UIColor(rgb: 0xffffff)), mediaHighlightOverlayColor: UIColor(white: 1.0, alpha: 0.6), @@ -857,7 +857,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio shareButtonFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0xffffff, alpha: 0.8)), shareButtonStrokeColor: PresentationThemeVariableColor(withWallpaper: .clear, withoutWallpaper: UIColor(rgb: 0xe5e5ea)), shareButtonForegroundColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: defaultDayAccentColor), - mediaOverlayControlColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x000000, alpha: 0.6), foregroundColor: UIColor(rgb: 0xffffff)), + mediaOverlayControlColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x000000, alpha: 0.45), foregroundColor: UIColor(rgb: 0xffffff)), selectionControlColors: PresentationThemeFillStrokeForeground(fillColor: defaultDayAccentColor, strokeColor: UIColor(rgb: 0xc7c7cc), foregroundColor: UIColor(rgb: 0xffffff)), deliveryFailedColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xff3b30), foregroundColor: UIColor(rgb: 0xffffff)), mediaHighlightOverlayColor: UIColor(rgb: 0xffffff, alpha: 0.6), diff --git a/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift b/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift index b8527bc98cb..da4a919a0ee 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift @@ -185,6 +185,8 @@ public final class PrincipalThemeEssentialGraphics { public let radialIndicatorFileIconIncoming: UIImage public let radialIndicatorFileIconOutgoing: UIImage + public let radialIndicatorViewOnceIcon: UIImage + public let incomingBubbleGradientImage: UIImage? public let outgoingBubbleGradientImage: UIImage? @@ -370,6 +372,8 @@ public final class PrincipalThemeEssentialGraphics { self.radialIndicatorFileIconIncoming = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocument"), color: .black)! self.radialIndicatorFileIconOutgoing = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocument"), color: .black)! + + self.radialIndicatorViewOnceIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/ViewOnce"), color: .black)! } else { self.chatMessageBackgroundIncomingMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .none, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) self.chatMessageBackgroundIncomingExtractedMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .extracted, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) @@ -489,6 +493,8 @@ public final class PrincipalThemeEssentialGraphics { self.radialIndicatorFileIconIncoming = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocument"), color: .black)! self.radialIndicatorFileIconOutgoing = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocument"), color: .black)! + + self.radialIndicatorViewOnceIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/ViewOnce"), color: .black)! } let chatDateSize: CGFloat = 20.0 diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index ea072932d0a..4c9a4623852 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -213,6 +213,7 @@ public enum PresentationResourceKey: Int32 { case chatInputSearchPanelMembersImage case chatHistoryNavigationButtonImage + case chatHistoryNavigationUpButtonImage case chatHistoryMentionsButtonImage case chatHistoryReactionsButtonImage case chatHistoryNavigationButtonBadgeImage diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift index c87bbce9c10..1ead3d9cee2 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift @@ -606,6 +606,28 @@ public struct PresentationResourcesChat { }) } + public static func chatHistoryNavigationUpButtonImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatHistoryNavigationUpButtonImage.rawValue, { theme in + return generateImage(CGSize(width: 38.0, height: 38.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setLineWidth(0.5) + context.setStrokeColor(theme.chat.historyNavigation.strokeColor.cgColor) + context.strokeEllipse(in: CGRect(origin: CGPoint(x: 0.25, y: 0.25), size: CGSize(width: size.width - 0.5, height: size.height - 0.5))) + context.setStrokeColor(theme.chat.historyNavigation.foregroundColor.cgColor) + context.setLineWidth(1.5) + + context.translateBy(x: size.width * 0.5, y: size.height * 0.5) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width * 0.5, y: -size.height * 0.5) + let position = CGPoint(x: 9.0 - 0.5, y: 24.0) + context.move(to: CGPoint(x: position.x + 1.0, y: position.y - 1.0)) + context.addLine(to: CGPoint(x: position.x + 10.0, y: position.y - 10.0)) + context.addLine(to: CGPoint(x: position.x + 19.0, y: position.y - 1.0)) + context.strokePath() + }) + }) + } + public static func chatHistoryMentionsButtonImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatHistoryMentionsButtonImage.rawValue, { theme in return generateImage(CGSize(width: 38.0, height: 38.0), contextGenerator: { size, context in diff --git a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift index f372aa491ee..7b445965c6e 100644 --- a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift +++ b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift @@ -21,6 +21,8 @@ public enum MessageContentKindKey { case liveLocation case expiredImage case expiredVideo + case expiredVoiceMessage + case expiredVideoMessage case poll case restricted case dice @@ -44,6 +46,8 @@ public enum MessageContentKind: Equatable { case liveLocation case expiredImage case expiredVideo + case expiredVoiceMessage + case expiredVideoMessage case poll(String) case restricted(String) case dice(String) @@ -137,6 +141,18 @@ public enum MessageContentKind: Equatable { } else { return false } + case .expiredVoiceMessage: + if case .expiredVoiceMessage = other { + return true + } else { + return false + } + case .expiredVideoMessage: + if case .expiredVideoMessage = other { + return true + } else { + return false + } case .poll: if case .poll = other { return true @@ -206,6 +222,10 @@ public enum MessageContentKind: Equatable { return .expiredImage case .expiredVideo: return .expiredVideo + case .expiredVoiceMessage: + return .expiredVoiceMessage + case .expiredVideoMessage: + return .expiredVideoMessage case .poll: return .poll case .restricted: @@ -278,7 +298,7 @@ public func mediaContentKind(_ media: EngineMedia, message: EngineMessage? = nil switch expiredMedia.data { case .image: return .expiredImage - case .file: + case .file, .videoMessage, .voiceMessage: return .expiredVideo } case .image: @@ -405,6 +425,10 @@ public func stringForMediaKind(_ kind: MessageContentKind, strings: Presentation return (NSAttributedString(string: strings.Message_ImageExpired), true) case .expiredVideo: return (NSAttributedString(string: strings.Message_VideoExpired), true) + case .expiredVoiceMessage: + return (NSAttributedString(string: strings.Message_VoiceMessageExpired), true) + case .expiredVideoMessage: + return (NSAttributedString(string: strings.Message_VideoMessageExpired), true) case let .poll(text): return (NSAttributedString(string: "📊 \(text)"), false) case let .restricted(text): diff --git a/submodules/TelegramStringFormatting/Sources/PeerDisplayName.swift b/submodules/TelegramStringFormatting/Sources/PeerDisplayName.swift index a7603c3d657..21c26624cf0 100644 --- a/submodules/TelegramStringFormatting/Sources/PeerDisplayName.swift +++ b/submodules/TelegramStringFormatting/Sources/PeerDisplayName.swift @@ -25,6 +25,8 @@ public func stringForFullAuthorName(message: EngineMessage, strings: Presentatio } else { if message.id.peerId == accountPeerId { authorString = [strings.DialogList_SavedMessages] + } else if message.id.peerId.isAnonymousSavedMessages { + authorString = [strings.ChatList_AuthorHidden] } else if message.flags.contains(.Incoming) { authorString = [peer.displayTitle(strings: strings, displayOrder: nameDisplayOrder)] } else { diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 7248c286e59..ed1e6f4ede8 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -965,6 +965,10 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributedString = NSAttributedString(string: strings.Message_ImageExpired, font: titleFont, textColor: primaryTextColor) case .file: attributedString = NSAttributedString(string: strings.Message_VideoExpired, font: titleFont, textColor: primaryTextColor) + case .videoMessage: + attributedString = NSAttributedString(string: strings.Message_VideoMessageExpired, font: titleFont, textColor: primaryTextColor) + case .voiceMessage: + attributedString = NSAttributedString(string: strings.Message_VoiceMessageExpired, font: titleFont, textColor: primaryTextColor) } } else if let _ = media as? TelegramMediaStory { let compactPeerName = message.peers[message.id.peerId].flatMap(EnginePeer.init)?.compactDisplayTitle ?? "" diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index b8ff54a41fb..61f41048625 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -448,6 +448,7 @@ swift_library( "//submodules/TelegramUI/Components/SavedMessages/SavedMessagesScreen", "//submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen", "//submodules/TelegramUI/Components/Settings/WallpaperGridScreen", + "//submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/AudioWaveformComponent/Sources/AudioWaveformComponent.swift b/submodules/TelegramUI/Components/AudioWaveformComponent/Sources/AudioWaveformComponent.swift index 73468673bdc..2eb94f5e3f2 100644 --- a/submodules/TelegramUI/Components/AudioWaveformComponent/Sources/AudioWaveformComponent.swift +++ b/submodules/TelegramUI/Components/AudioWaveformComponent/Sources/AudioWaveformComponent.swift @@ -19,6 +19,7 @@ public final class AudioWaveformComponent: Component { public let samples: Data public let peak: Int32 public let status: Signal + public let isViewOnceMessage: Bool public let seek: ((Double) -> Void)? public let updateIsSeeking: ((Bool) -> Void)? @@ -30,6 +31,7 @@ public final class AudioWaveformComponent: Component { samples: Data, peak: Int32, status: Signal, + isViewOnceMessage: Bool, seek: ((Double) -> Void)?, updateIsSeeking: ((Bool) -> Void)? ) { @@ -40,6 +42,7 @@ public final class AudioWaveformComponent: Component { self.samples = samples self.peak = peak self.status = status + self.isViewOnceMessage = isViewOnceMessage self.seek = seek self.updateIsSeeking = updateIsSeeking } @@ -63,6 +66,9 @@ public final class AudioWaveformComponent: Component { if lhs.peak != rhs.peak { return false } + if lhs.isViewOnceMessage != rhs.isViewOnceMessage { + return false + } return true } @@ -204,6 +210,10 @@ public final class AudioWaveformComponent: Component { private var statusDisposable: Disposable? private var playbackStatusAnimator: ConstantDisplayLinkAnimator? + private var sparksView: SparksView? + private var progress: CGFloat = 0.0 + private var lastHeight: CGFloat = 0.0 + private var revealProgress: CGFloat = 1.0 private var animator: DisplayLinkAnimator? @@ -391,6 +401,21 @@ public final class AudioWaveformComponent: Component { }) } + if component.isViewOnceMessage { + let sparksView: SparksView + if let current = self.sparksView { + sparksView = current + } else { + sparksView = SparksView() + self.addSubview(sparksView) + self.sparksView = sparksView + } + sparksView.frame = CGRect(origin: .zero, size: size).insetBy(dx: -10.0, dy: -15.0) + } else if let sparksView = self.sparksView { + self.sparksView = nil + sparksView.removeFromSuperview() + } + return size } @@ -408,12 +433,25 @@ public final class AudioWaveformComponent: Component { if needsAnimation != (self.playbackStatusAnimator != nil) { if needsAnimation { self.playbackStatusAnimator = ConstantDisplayLinkAnimator(update: { [weak self] in + if let self, let component = self.component, let sparksView = self.sparksView { + sparksView.update(position: CGPoint(x: 10.0 + (sparksView.bounds.width - 20.0) * self.progress, y: sparksView.bounds.height / 2.0 + 8.0), sampleHeight: self.lastHeight, color: component.foregroundColor) + } self?.setNeedsDisplay() }) self.playbackStatusAnimator?.isPaused = false + + if let sparksView = self.sparksView { + sparksView.alpha = 1.0 + sparksView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } } else { self.playbackStatusAnimator?.invalidate() self.playbackStatusAnimator = nil + + if let sparksView = self.sparksView { + sparksView.alpha = 0.0 + sparksView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } } } } @@ -445,7 +483,7 @@ public final class AudioWaveformComponent: Component { timestampAndDuration = nil } - let playbackProgress: CGFloat + var playbackProgress: CGFloat if let (timestamp, duration) = timestampAndDuration { if let scrubbingTimestampValue = self.scrubbingTimestampValue { var progress = CGFloat(scrubbingTimestampValue / duration) @@ -474,6 +512,10 @@ public final class AudioWaveformComponent: Component { } else { playbackProgress = 0.0 } + if component.isViewOnceMessage { + playbackProgress = 1.0 - playbackProgress + } + self.progress = playbackProgress let sampleWidth: CGFloat = 2.0 let halfSampleWidth: CGFloat = 1.0 @@ -533,6 +575,7 @@ public final class AudioWaveformComponent: Component { let commonRevealFraction = listViewAnimationCurveSystem(self.revealProgress) + var lastHeight: CGFloat = 0.0 for i in 0 ..< numSamples { let offset = CGFloat(i) * (sampleWidth + distance) let peakSample = adjustedSamples[i] @@ -555,6 +598,7 @@ public final class AudioWaveformComponent: Component { let colorMixFraction: CGFloat if startFraction < playbackProgress { colorMixFraction = max(0.0, min(1.0, (playbackProgress - startFraction) / (nextStartFraction - startFraction))) + lastHeight = sampleHeight } else { colorMixFraction = 0.0 } @@ -571,7 +615,11 @@ public final class AudioWaveformComponent: Component { } if component.backgroundColor.alpha > 0.0 { - context.setFillColor(component.backgroundColor.mixedWith(component.foregroundColor, alpha: colorMixFraction).cgColor) + var backgroundColor = component.backgroundColor + if component.isViewOnceMessage { + backgroundColor = component.foregroundColor.withMultipliedAlpha(0.0) + } + context.setFillColor(backgroundColor.mixedWith(component.foregroundColor, alpha: colorMixFraction).cgColor) } else { context.setFillColor(component.foregroundColor.cgColor) } @@ -592,6 +640,8 @@ public final class AudioWaveformComponent: Component { context.fill(adjustedRect) } } + + self.lastHeight = lastHeight } } } @@ -604,3 +654,105 @@ public final class AudioWaveformComponent: Component { return view.update(component: self, availableSize: availableSize, transition: transition) } } + +private struct ContentParticle { + var position: CGPoint + var direction: CGPoint + var velocity: CGFloat + var alpha: CGFloat + var lifetime: Double + var beginTime: Double + + init(position: CGPoint, direction: CGPoint, velocity: CGFloat, alpha: CGFloat, lifetime: Double, beginTime: Double) { + self.position = position + self.direction = direction + self.velocity = velocity + self.alpha = alpha + self.lifetime = lifetime + self.beginTime = beginTime + } +} + +private class SparksView: UIView { + private var particles: [ContentParticle] = [] + private var color: UIColor = .black + + override init(frame: CGRect) { + super.init(frame: frame) + + self.backgroundColor = nil + self.isOpaque = false + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var presentationSampleHeight: CGFloat = 0.0 + private var sampleHeight: CGFloat = 0.0 + + func update(position: CGPoint, sampleHeight: CGFloat, color: UIColor) { + self.color = color + + self.sampleHeight = sampleHeight + self.presentationSampleHeight = self.presentationSampleHeight * 0.9 + self.sampleHeight * 0.1 + + let v = CGPoint(x: 1.0, y: 0.0) + let c = CGPoint(x: position.x - 4.0, y: position.y + 1.0 - self.presentationSampleHeight * CGFloat(arc4random_uniform(100)) / 100.0) + + let timestamp = CACurrentMediaTime() + + let dt: CGFloat = 1.0 / 60.0 + var removeIndices: [Int] = [] + for i in 0 ..< self.particles.count { + let currentTime = timestamp - self.particles[i].beginTime + if currentTime > self.particles[i].lifetime { + removeIndices.append(i) + } else { + let input: CGFloat = CGFloat(currentTime / self.particles[i].lifetime) + let decelerated: CGFloat = (1.0 - (1.0 - input) * (1.0 - input)) + self.particles[i].alpha = 1.0 - decelerated + + var p = self.particles[i].position + let d = self.particles[i].direction + let v = self.particles[i].velocity + p = CGPoint(x: p.x + d.x * v * dt, y: p.y + d.y * v * dt) + self.particles[i].position = p + } + } + + for i in removeIndices.reversed() { + self.particles.remove(at: i) + } + + let newParticleCount = 3 + for _ in 0 ..< newParticleCount { + let degrees: CGFloat = CGFloat(arc4random_uniform(100)) - 65.0 + let angle: CGFloat = degrees * CGFloat.pi / 180.0 + + let direction = CGPoint(x: v.x * cos(angle) - v.y * sin(angle), y: v.x * sin(angle) + v.y * cos(angle)) + let velocity = (80.0 + (CGFloat(arc4random()) / CGFloat(UINT32_MAX)) * 4.0) * 0.5 + + let lifetime = Double(0.65 + CGFloat(arc4random_uniform(100)) * 0.01) + + let particle = ContentParticle(position: c, direction: direction, velocity: velocity, alpha: 1.0, lifetime: lifetime, beginTime: timestamp) + self.particles.append(particle) + } + + self.setNeedsDisplay() + } + + override public func draw(_ rect: CGRect) { + guard let context = UIGraphicsGetCurrentContext() else { + return + } + + context.setFillColor(self.color.cgColor) + + for particle in self.particles { + let size: CGFloat = 1.4 + context.setAlpha(particle.alpha * 1.0) + context.fillEllipse(in: CGRect(origin: CGPoint(x: particle.position.x - size / 2.0, y: particle.position.y - size / 2.0), size: CGSize(width: size, height: size))) + } + } +} diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift index 5cc53a71849..fbeb7452429 100644 --- a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift @@ -1045,8 +1045,9 @@ final class AvatarEditorScreenComponent: Component { state.updated(transition: .easeInOut(duration: 0.2)) } }, - openColorPicker: { [weak state] in - if let state { + openColorPicker: { [weak self, weak state] in + if let self, let state { + self.endEditing(true) state.editingColor = true state.previousColor = state.selectedBackground state.previousCustomColor = state.customColor diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/BUILD b/submodules/TelegramUI/Components/Calls/CallScreen/BUILD index ac35b7c0f23..6773bd07e81 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/BUILD +++ b/submodules/TelegramUI/Components/Calls/CallScreen/BUILD @@ -68,6 +68,7 @@ swift_library( "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/AppBundle", "//submodules/UIKitRuntimeUtils", + "//submodules/TelegramPresentationData", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/CallScreenAssets.xcassets/Call/Snow.imageset/Contents.json b/submodules/TelegramUI/Components/Calls/CallScreen/CallScreenAssets.xcassets/Call/Snow.imageset/Contents.json new file mode 100644 index 00000000000..de2d610148c --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/CallScreenAssets.xcassets/Call/Snow.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Snow.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/CallScreenAssets.xcassets/Call/Snow.imageset/Snow.png b/submodules/TelegramUI/Components/Calls/CallScreen/CallScreenAssets.xcassets/Call/Snow.imageset/Snow.png new file mode 100644 index 00000000000..2dddf3fa21b Binary files /dev/null and b/submodules/TelegramUI/Components/Calls/CallScreen/CallScreenAssets.xcassets/Call/Snow.imageset/Snow.png differ diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/BackButtonView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/BackButtonView.swift index 0fed60cf04a..696d0a5727f 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/BackButtonView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/BackButtonView.swift @@ -3,34 +3,39 @@ import UIKit import Display final class BackButtonView: HighlightableButton { + private struct Params: Equatable { + var text: String + + init(text: String) { + self.text = text + } + } + + private struct Layout: Equatable { + var params: Params + var size: CGSize + + init(params: Params, size: CGSize) { + self.params = params + self.size = size + } + } + private let iconView: UIImageView private let textView: TextView - let size: CGSize + private var currentLayout: Layout? var pressAction: (() -> Void)? - init(text: String) { + override init(frame: CGRect) { self.iconView = UIImageView(image: NavigationBar.backArrowImage(color: .white)) self.iconView.isUserInteractionEnabled = false self.textView = TextView() self.textView.isUserInteractionEnabled = false - let spacing: CGFloat = 8.0 - - var iconSize: CGSize = self.iconView.image?.size ?? CGSize(width: 2.0, height: 2.0) - let iconScaleFactor: CGFloat = 0.9 - iconSize.width = floor(iconSize.width * iconScaleFactor) - iconSize.height = floor(iconSize.height * iconScaleFactor) - - let textSize = self.textView.update(string: text, fontSize: 17.0, fontWeight: UIFont.Weight.regular.rawValue, color: .white, constrainedWidth: 100.0, transition: .immediate) - self.size = CGSize(width: iconSize.width + spacing + textSize.width, height: textSize.height) - - self.iconView.frame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((self.size.height - iconSize.height) * 0.5)), size: iconSize) - self.textView.frame = CGRect(origin: CGPoint(x: iconSize.width + spacing, y: floorToScreenPixels((self.size.height - textSize.height) * 0.5)), size: textSize) - - super.init(frame: CGRect()) + super.init(frame: frame) self.addSubview(self.iconView) self.addSubview(self.textView) @@ -53,4 +58,31 @@ final class BackButtonView: HighlightableButton { return nil } } + + func update(text: String) -> CGSize { + let params = Params(text: text) + if let currentLayout = self.currentLayout, currentLayout.params == params { + return currentLayout.size + } + let size = self.update(params: params) + self.currentLayout = Layout(params: params, size: size) + return size + } + + private func update(params: Params) -> CGSize { + let spacing: CGFloat = 8.0 + + var iconSize: CGSize = self.iconView.image?.size ?? CGSize(width: 2.0, height: 2.0) + let iconScaleFactor: CGFloat = 0.9 + iconSize.width = floor(iconSize.width * iconScaleFactor) + iconSize.height = floor(iconSize.height * iconScaleFactor) + + let textSize = self.textView.update(string: params.text, fontSize: 17.0, fontWeight: UIFont.Weight.regular.rawValue, color: .white, constrainedWidth: 100.0, transition: .immediate) + let size = CGSize(width: iconSize.width + spacing + textSize.width, height: textSize.height) + + self.iconView.frame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - iconSize.height) * 0.5)), size: iconSize) + self.textView.frame = CGRect(origin: CGPoint(x: iconSize.width + spacing, y: floorToScreenPixels((size.height - textSize.height) * 0.5)), size: textSize) + + return size + } } diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift index de46ab41e9e..73c36b48cec 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift @@ -3,6 +3,7 @@ import UIKit import Display import ComponentFlow import AppBundle +import TelegramPresentationData final class ButtonGroupView: OverlayMaskContainerView { final class Button { @@ -15,7 +16,7 @@ final class ButtonGroupView: OverlayMaskContainerView { case end } - case speaker(isActive: Bool) + case speaker(audioOutput: PrivateCallScreen.State.AudioOutput) case flipCamera case video(isActive: Bool) case microphone(isMuted: Bool) @@ -85,7 +86,7 @@ final class ButtonGroupView: OverlayMaskContainerView { return result } - func update(size: CGSize, insets: UIEdgeInsets, minWidth: CGFloat, controlsHidden: Bool, displayClose: Bool, buttons: [Button], notices: [Notice], transition: Transition) -> CGFloat { + func update(size: CGSize, insets: UIEdgeInsets, minWidth: CGFloat, controlsHidden: Bool, displayClose: Bool, strings: PresentationStrings, buttons: [Button], notices: [Notice], transition: Transition) -> CGFloat { self.buttons = buttons let buttonSize: CGFloat = 56.0 @@ -190,7 +191,7 @@ final class ButtonGroupView: OverlayMaskContainerView { } } let closeButtonSize = CGSize(width: minWidth, height: buttonSize) - closeButtonView.update(text: "Close", size: closeButtonSize, transition: closeButtonTransition) + closeButtonView.update(text: strings.Common_Close, size: closeButtonSize, transition: closeButtonTransition) closeButtonTransition.setFrame(view: closeButtonView, frame: CGRect(origin: CGPoint(x: floor((size.width - closeButtonSize.width) * 0.5), y: buttonY), size: closeButtonSize)) if animateIn && !transition.animation.isImmediate { @@ -215,24 +216,51 @@ final class ButtonGroupView: OverlayMaskContainerView { let isActive: Bool var isDestructive: Bool = false switch button.content { - case let .speaker(isActiveValue): - title = "speaker" - image = UIImage(bundleImageName: "Call/Speaker") - isActive = isActiveValue + case let .speaker(audioOutput): + switch audioOutput { + case .internalSpeaker, .speaker: + title = strings.Call_Speaker + default: + title = strings.Call_Audio + } + + switch audioOutput { + case .internalSpeaker: + image = UIImage(bundleImageName: "Call/Speaker") + isActive = false + case .speaker: + image = UIImage(bundleImageName: "Call/Speaker") + isActive = true + case .airpods: + image = UIImage(bundleImageName: "Call/CallAirpodsButton") + isActive = true + case .airpodsPro: + image = UIImage(bundleImageName: "Call/CallAirpodsProButton") + isActive = true + case .airpodsMax: + image = UIImage(bundleImageName: "Call/CallAirpodsMaxButton") + isActive = true + case .headphones: + image = UIImage(bundleImageName: "Call/CallHeadphonesButton") + isActive = true + case .bluetooth: + image = UIImage(bundleImageName: "Call/CallBluetoothButton") + isActive = true + } case .flipCamera: - title = "flip" + title = strings.Call_Flip image = UIImage(bundleImageName: "Call/Flip") isActive = false case let .video(isActiveValue): - title = "video" + title = strings.Call_Video image = UIImage(bundleImageName: "Call/Video") isActive = isActiveValue case let .microphone(isActiveValue): - title = "mute" + title = strings.Call_Mute image = UIImage(bundleImageName: "Call/Mute") isActive = isActiveValue case .end: - title = "end" + title = strings.Call_End image = UIImage(bundleImageName: "Call/End") isActive = false isDestructive = true diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/StatusView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/StatusView.swift index f2949a4f91b..30f287c191f 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/StatusView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/StatusView.swift @@ -2,6 +2,7 @@ import Foundation import UIKit import Display import ComponentFlow +import TelegramPresentationData private func addRoundedRectPath(context: CGContext, rect: CGRect, radius: CGFloat) { context.saveGState() @@ -154,13 +155,28 @@ private final class SignalStrengthView: UIView { final class StatusView: UIView { private struct LayoutState: Equatable { + var strings: PresentationStrings var state: State var size: CGSize - init(state: State, size: CGSize) { + init(strings: PresentationStrings, state: State, size: CGSize) { + self.strings = strings self.state = state self.size = size } + + static func ==(lhs: LayoutState, rhs: LayoutState) -> Bool { + if lhs.strings !== rhs.strings { + return false + } + if lhs.state != rhs.state { + return false + } + if lhs.size != rhs.size { + return false + } + return true + } } enum WaitingState { @@ -241,12 +257,12 @@ final class StatusView: UIView { self.activeDurationTimer?.invalidate() } - func update(state: State, transition: Transition) -> CGSize { - if let layoutState = self.layoutState, layoutState.state == state { + func update(strings: PresentationStrings, state: State, transition: Transition) -> CGSize { + if let layoutState = self.layoutState, layoutState.strings === strings, layoutState.state == state { return layoutState.size } - let size = self.updateInternal(state: state, transition: transition) - self.layoutState = LayoutState(state: state, size: size) + let size = self.updateInternal(strings: strings, state: state, transition: transition) + self.layoutState = LayoutState(strings: strings, state: state, size: size) self.updateActiveDurationTimer() @@ -268,7 +284,7 @@ final class StatusView: UIView { self.activeDurationTimer = nil if let layoutState = self.layoutState { - let size = self.updateInternal(state: layoutState.state, transition: .immediate) + let size = self.updateInternal(strings: layoutState.strings, state: layoutState.state, transition: .immediate) if layoutState.size != size { self.layoutState = nil self.requestLayout?() @@ -286,7 +302,7 @@ final class StatusView: UIView { } } - private func updateInternal(state: State, transition: Transition) -> CGSize { + private func updateInternal(strings: PresentationStrings, state: State, transition: Transition) -> CGSize { let textString: String var needsDots = false var monospacedDigits = false @@ -297,13 +313,13 @@ final class StatusView: UIView { switch waitingState { case .requesting: - textString = "Requesting" + textString = strings.Call_WaitingStatusRequesting case .ringing: - textString = "Ringing" + textString = strings.Call_WaitingStatusRinging case .connecting: - textString = "Connecting" + textString = strings.Call_WaitingStatusConnecting case .reconnecting: - textString = "Reconnecting" + textString = strings.Call_WaitingStatusReconnecting } case let .active(activeState): monospacedDigits = true diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift index db793d3e58c..aad8c7679c6 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift @@ -7,6 +7,7 @@ import MetalEngine import ComponentFlow import SwiftSignalKit import UIKitRuntimeUtils +import TelegramPresentationData public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictureControllerDelegate { public struct State: Equatable { @@ -60,8 +61,14 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu public enum AudioOutput: Equatable { case internalSpeaker case speaker + case headphones + case airpods + case airpodsPro + case airpodsMax + case bluetooth } + public var strings: PresentationStrings public var lifecycleState: LifecycleState public var name: String public var shortName: String @@ -72,8 +79,10 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu public var localVideo: VideoSource? public var remoteVideo: VideoSource? public var isRemoteBatteryLow: Bool + public var displaySnowEffect: Bool public init( + strings: PresentationStrings, lifecycleState: LifecycleState, name: String, shortName: String, @@ -83,8 +92,10 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu isRemoteAudioMuted: Bool, localVideo: VideoSource?, remoteVideo: VideoSource?, - isRemoteBatteryLow: Bool + isRemoteBatteryLow: Bool, + displaySnowEffect: Bool = false ) { + self.strings = strings self.lifecycleState = lifecycleState self.name = name self.shortName = shortName @@ -95,9 +106,13 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu self.localVideo = localVideo self.remoteVideo = remoteVideo self.isRemoteBatteryLow = isRemoteBatteryLow + self.displaySnowEffect = displaySnowEffect } public static func ==(lhs: State, rhs: State) -> Bool { + if lhs.strings !== rhs.strings { + return false + } if lhs.lifecycleState != rhs.lifecycleState { return false } @@ -128,6 +143,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu if lhs.isRemoteBatteryLow != rhs.isRemoteBatteryLow { return false } + if lhs.displaySnowEffect != rhs.displaySnowEffect { + return false + } return true } } @@ -213,6 +231,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu private var pipVideoCallViewController: UIViewController? private var pipController: AVPictureInPictureController? + private var snowEffectView: SnowEffectView? + public override init(frame: CGRect) { self.overlayContentsView = UIView() self.overlayContentsView.isUserInteractionEnabled = false @@ -235,7 +255,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu self.titleView = TextView() self.statusView = StatusView() - self.backButtonView = BackButtonView(text: "Back") + self.backButtonView = BackButtonView(frame: CGRect()) self.pipView = PrivateCallPictureInPictureView(frame: CGRect(origin: CGPoint(), size: CGSize())) @@ -354,6 +374,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu if !self.isUpdating { let wereControlsHidden = self.areControlsHidden self.areControlsHidden = true + self.displayEmojiTooltip = false self.update(transition: .immediate) if !wereControlsHidden { @@ -417,6 +438,24 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu if self.activeRemoteVideoSource != nil || self.activeLocalVideoSource != nil { self.areControlsHidden = !self.areControlsHidden update = true + + if self.areControlsHidden { + self.displayEmojiTooltip = false + self.hideControlsTimer?.invalidate() + self.hideControlsTimer = nil + } else { + self.hideControlsTimer?.invalidate() + self.hideControlsTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false, block: { [weak self] _ in + guard let self else { + return + } + if !self.areControlsHidden { + self.areControlsHidden = true + self.displayEmojiTooltip = false + self.update(transition: .spring(duration: 0.4)) + } + }) + } } if update { @@ -515,7 +554,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu if let previousParams = self.params, case .active = params.state.lifecycleState { switch previousParams.state.lifecycleState { case .requesting, .ringing, .connecting, .reconnecting: - if self.hideEmojiTooltipTimer == nil { + if self.hideEmojiTooltipTimer == nil && !self.areControlsHidden { self.displayEmojiTooltip = true self.hideEmojiTooltipTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false, block: { [weak self] _ in @@ -592,6 +631,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu } if !self.areControlsHidden { self.areControlsHidden = true + self.displayEmojiTooltip = false self.update(transition: .spring(duration: 0.4)) } }) @@ -704,7 +744,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu self.flipCameraAction?() }), at: 0) } else { - buttons.insert(ButtonGroupView.Button(content: .speaker(isActive: params.state.audioOutput != .internalSpeaker), isEnabled: !isTerminated, action: { [weak self] in + buttons.insert(ButtonGroupView.Button(content: .speaker(audioOutput: params.state.audioOutput), isEnabled: !isTerminated, action: { [weak self] in guard let self else { return } @@ -715,16 +755,16 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu var notices: [ButtonGroupView.Notice] = [] if !isTerminated { if params.state.isLocalAudioMuted { - notices.append(ButtonGroupView.Notice(id: AnyHashable(0 as Int), icon: "Call/CallToastMicrophone", text: "Your microphone is turned off")) + notices.append(ButtonGroupView.Notice(id: AnyHashable(0 as Int), icon: "Call/CallToastMicrophone", text: params.state.strings.Call_YourMicrophoneOff)) } if params.state.isRemoteAudioMuted { - notices.append(ButtonGroupView.Notice(id: AnyHashable(1 as Int), icon: "Call/CallToastMicrophone", text: "\(params.state.shortName)'s microphone is turned off")) + notices.append(ButtonGroupView.Notice(id: AnyHashable(1 as Int), icon: "Call/CallToastMicrophone", text: params.state.strings.Call_MicrophoneOff(params.state.shortName).string)) } if params.state.remoteVideo != nil && params.state.localVideo == nil { - notices.append(ButtonGroupView.Notice(id: AnyHashable(2 as Int), icon: "Call/CallToastCamera", text: "Your camera is turned off")) + notices.append(ButtonGroupView.Notice(id: AnyHashable(2 as Int), icon: "Call/CallToastCamera", text: params.state.strings.Call_YourCameraOff)) } if params.state.isRemoteBatteryLow { - notices.append(ButtonGroupView.Notice(id: AnyHashable(3 as Int), icon: "Call/CallToastBattery", text: "\(params.state.shortName)'s battery is low")) + notices.append(ButtonGroupView.Notice(id: AnyHashable(3 as Int), icon: "Call/CallToastBattery", text: params.state.strings.Call_BatteryLow(params.state.shortName).string)) } } @@ -734,7 +774,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu }*/ let displayClose = false - let contentBottomInset = self.buttonGroupView.update(size: params.size, insets: params.insets, minWidth: wideContentWidth, controlsHidden: currentAreControlsHidden, displayClose: displayClose, buttons: buttons, notices: notices, transition: transition) + let contentBottomInset = self.buttonGroupView.update(size: params.size, insets: params.insets, minWidth: wideContentWidth, controlsHidden: currentAreControlsHidden, displayClose: displayClose, strings: params.state.strings, buttons: buttons, notices: notices, transition: transition) var expandedEmojiKeyRect: CGRect? if self.isEmojiKeyExpanded { @@ -752,7 +792,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu alphaTransition = genericAlphaTransition } - emojiExpandedInfoView = EmojiExpandedInfoView(title: "This call is end-to-end encrypted", text: "If the emoji on \(params.state.shortName)'s screen are the same, this call is 100% secure.") + emojiExpandedInfoView = EmojiExpandedInfoView(title: params.state.strings.Call_EncryptedAlertTitle, text: params.state.strings.Call_EncryptedAlertText(params.state.shortName).string) self.emojiExpandedInfoView = emojiExpandedInfoView emojiExpandedInfoView.alpha = 0.0 Transition.immediate.setScale(view: emojiExpandedInfoView, scale: 0.5) @@ -799,13 +839,15 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu } } + let backButtonSize = self.backButtonView.update(text: params.state.strings.Common_Back) + let backButtonY: CGFloat if currentAreControlsHidden { - backButtonY = -self.backButtonView.size.height - 12.0 + backButtonY = -backButtonSize.height - 12.0 } else { backButtonY = params.insets.top + 12.0 } - let backButtonFrame = CGRect(origin: CGPoint(x: params.insets.left + 10.0, y: backButtonY), size: self.backButtonView.size) + let backButtonFrame = CGRect(origin: CGPoint(x: params.insets.left + 10.0, y: backButtonY), size: backButtonSize) transition.setFrame(view: self.backButtonView, frame: backButtonFrame) transition.setAlpha(view: self.backButtonView, alpha: currentAreControlsHidden ? 0.0 : 1.0) @@ -889,7 +931,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu emojiTooltipView = current } else { emojiTooltipTransition = emojiTooltipTransition.withAnimation(.none) - emojiTooltipView = EmojiTooltipView(text: "Encryption key of this call") + emojiTooltipView = EmojiTooltipView(text: params.state.strings.Call_EncryptionKeyTooltip) animateIn = true self.emojiTooltipView = emojiTooltipView self.addSubview(emojiTooltipView) @@ -1157,23 +1199,34 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu transition.setPosition(layer: self.blobLayer, position: CGPoint(x: blobFrame.width * 0.5, y: blobFrame.height * 0.5)) transition.setBounds(layer: self.blobLayer, bounds: CGRect(origin: CGPoint(), size: blobFrame.size)) + let displayAudioLevelBlob: Bool let titleString: String switch params.state.lifecycleState { case let .terminated(terminatedState): + displayAudioLevelBlob = false + self.titleView.contentMode = .center switch terminatedState.reason { case .busy: - titleString = "Line Busy" + titleString = params.state.strings.Call_StatusBusy case .declined: - titleString = "Call Declined" + titleString = params.state.strings.Call_StatusDeclined case .failed: - titleString = "Call Failed" + titleString = params.state.strings.Call_StatusFailed case .hangUp: - titleString = "Call Ended" + titleString = params.state.strings.Call_StatusEnded case .missed: - titleString = "Call Missed" + titleString = params.state.strings.Call_StatusMissed } + default: + displayAudioLevelBlob = !params.state.isRemoteAudioMuted + + self.titleView.contentMode = .scaleToFill + titleString = params.state.name + } + + if !displayAudioLevelBlob { genericAlphaTransition.setScale(layer: self.blobLayer, scale: 0.3) genericAlphaTransition.setAlpha(layer: self.blobLayer, alpha: 0.0) self.canAnimateAudioLevel = false @@ -1181,11 +1234,12 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu self.currentAvatarAudioScale = 1.0 transition.setScale(layer: self.avatarTransformLayer, scale: 1.0) transition.setScale(layer: self.blobTransformLayer, scale: 1.0) - default: - self.titleView.contentMode = .scaleToFill - titleString = params.state.name + } else { genericAlphaTransition.setAlpha(layer: self.blobLayer, alpha: (expandedEmojiKeyOverlapsAvatar && !havePrimaryVideo) ? 0.0 : 1.0) transition.setScale(layer: self.blobLayer, scale: expandedEmojiKeyOverlapsAvatar ? 0.001 : 1.0) + if !havePrimaryVideo { + self.canAnimateAudioLevel = true + } } let titleSize = self.titleView.update( @@ -1244,7 +1298,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu } } - let statusSize = self.statusView.update(state: statusState, transition: .immediate) + let statusSize = self.statusView.update(strings: params.state.strings, state: statusState, transition: .immediate) let titleY: CGFloat if currentAreControlsHidden { @@ -1321,5 +1375,75 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu }) } } + + /*if params.state.displaySnowEffect { + let snowEffectView: SnowEffectView + if let current = self.snowEffectView { + snowEffectView = current + } else { + snowEffectView = SnowEffectView(frame: CGRect()) + self.snowEffectView = snowEffectView + self.maskContents.addSubview(snowEffectView) + } + transition.setFrame(view: snowEffectView, frame: CGRect(origin: CGPoint(), size: params.size)) + snowEffectView.update(size: params.size) + } else { + if let snowEffectView = self.snowEffectView { + self.snowEffectView = nil + snowEffectView.removeFromSuperview() + } + }*/ + } +} + +final class SnowEffectView: UIView { + private let particlesLayer: CAEmitterLayer + + override init(frame: CGRect) { + let particlesLayer = CAEmitterLayer() + self.particlesLayer = particlesLayer + self.particlesLayer.backgroundColor = nil + self.particlesLayer.isOpaque = false + + particlesLayer.emitterShape = .circle + particlesLayer.emitterMode = .surface + particlesLayer.renderMode = .oldestLast + + let image1 = UIImage(named: "Call/Snow")?.cgImage + + let cell1 = CAEmitterCell() + cell1.contents = image1 + cell1.name = "Snow" + cell1.birthRate = 92.0 + cell1.lifetime = 20.0 + cell1.velocity = 59.0 + cell1.velocityRange = -15.0 + cell1.xAcceleration = 5.0 + cell1.yAcceleration = 40.0 + cell1.emissionRange = 90.0 * (.pi / 180.0) + cell1.spin = -28.6 * (.pi / 180.0) + cell1.spinRange = 57.2 * (.pi / 180.0) + cell1.scale = 0.06 + cell1.scaleRange = 0.3 + cell1.color = UIColor(red: 255.0/255.0, green: 255.0/255.0, blue: 255.0/255.0, alpha: 1.0).cgColor + + particlesLayer.emitterCells = [cell1] + + super.init(frame: frame) + + self.layer.addSublayer(particlesLayer) + self.clipsToBounds = true + self.backgroundColor = nil + self.isOpaque = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(size: CGSize) { + self.particlesLayer.frame = CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height) + self.particlesLayer.emitterSize = CGSize(width: size.width * 3.0, height: size.height * 2.0) + self.particlesLayer.emitterPosition = CGPoint(x: size.width * 0.5, y: -325.0) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift index b787d4ce5d0..a3e9810c3b4 100644 --- a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift @@ -59,7 +59,7 @@ private func titleAndColorForAction(_ action: SubscriberAction, theme: Presentat } private func actionForPeer(context: AccountContext, peer: Peer, interfaceState: ChatPresentationInterfaceState, isJoining: Bool, isMuted: Bool) -> SubscriberAction? { - if case let .replyThread(message) = interfaceState.chatLocation, message.messageId.peerId == context.account.peerId { + if case let .replyThread(message) = interfaceState.chatLocation, message.peerId == context.account.peerId { if let peer = interfaceState.savedMessagesTopicPeer { if case let .channel(channel) = peer { if case .broadcast = channel.info { diff --git a/submodules/TelegramUI/Components/Chat/ChatHistorySearchContainerNode/Sources/ChatHistorySearchContainerNode.swift b/submodules/TelegramUI/Components/Chat/ChatHistorySearchContainerNode/Sources/ChatHistorySearchContainerNode.swift index 2dd91738882..a6f74c60e9e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatHistorySearchContainerNode/Sources/ChatHistorySearchContainerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatHistorySearchContainerNode/Sources/ChatHistorySearchContainerNode.swift @@ -196,7 +196,7 @@ public final class ChatHistorySearchContainerNode: SearchDisplayControllerConten if let strongSelf = self { let signal: Signal<([ChatHistorySearchEntry], [MessageId: Message])?, NoError> if let query = query, !query.isEmpty { - let foundRemoteMessages: Signal<[Message], NoError> = context.engine.messages.searchMessages(location: .peer(peerId: peerId, fromId: nil, tags: tagMask, topMsgId: threadId.flatMap { makeThreadIdMessageId(peerId: peerId, threadId: $0) }, minDate: nil, maxDate: nil), query: query, state: nil) + let foundRemoteMessages: Signal<[Message], NoError> = context.engine.messages.searchMessages(location: .peer(peerId: peerId, fromId: nil, tags: tagMask, threadId: threadId, minDate: nil, maxDate: nil), query: query, state: nil) |> map { $0.0.messages } |> delay(0.2, queue: Queue.concurrentDefaultQueue()) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index b442a2b113c..f70e9ade846 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -141,7 +141,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { private var wasPending: Bool = false private var didChangeFromPendingToSent: Bool = false - required public init() { + required public init(rotated: Bool) { self.contextSourceNode = ContextExtractedContentContainingNode() self.containerNode = ContextControllerSourceNode() self.imageNode = TransformImageNode() @@ -156,7 +156,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { self.textNode.textNode.displaysAsynchronously = false self.textNode.textNode.isUserInteractionEnabled = false - super.init(layerBacked: false) + super.init(rotated: rotated) self.containerNode.shouldBegin = { [weak self] location in guard let strongSelf = self else { @@ -313,7 +313,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { return false } - if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.isChannelPost, replyThreadMessage.messageId.peerId != item.content.firstMessage.id.peerId { + if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.isChannelPost, replyThreadMessage.peerId != item.content.firstMessage.id.peerId { return false } @@ -826,8 +826,8 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { hasAvatar = true } case let .replyThread(replyThreadMessage): - if replyThreadMessage.messageId.peerId != item.context.account.peerId { - if replyThreadMessage.messageId.peerId.isGroupOrChannel && item.message.author != nil { + if replyThreadMessage.peerId != item.context.account.peerId { + if replyThreadMessage.peerId.isGroupOrChannel && item.message.author != nil { var isBroadcastChannel = false if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { isBroadcastChannel = true @@ -1102,7 +1102,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } if let replyAttribute = attribute as? ReplyMessageAttribute { - if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == replyAttribute.messageId { + if case let .replyThread(replyThreadMessage) = item.chatLocation, Int32(clamping: replyThreadMessage.threadId) == replyAttribute.messageId.id { } else { replyMessage = item.message.associatedMessages[replyAttribute.messageId] } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift index f006a5ca4d4..98ce2ab888d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift @@ -183,7 +183,17 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { } let messageTheme = incoming ? presentationData.theme.theme.chat.message.incoming : presentationData.theme.theme.chat.message.outgoing - let author = message.author + + var author = message.effectiveAuthor + + if let forwardInfo = message.forwardInfo { + if let peer = forwardInfo.author { + author = peer + } else if let authorSignature = forwardInfo.authorSignature { + author = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) + } + } + let nameColors = author?.nameColor.flatMap { context.peerNameColors.get($0, dark: presentationData.theme.theme.overallDarkAppearance) } let mainColor: UIColor diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 6238d518bdf..3f81f97375d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -660,7 +660,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } } - required public init() { + required public init(rotated: Bool) { self.mainContextSourceNode = ContextExtractedContentContainingNode() self.mainContainerNode = ContextControllerSourceNode() self.backgroundWallpaperNode = ChatMessageBubbleBackdrop() @@ -678,7 +678,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI //self.debugNode = ASDisplayNode() //self.debugNode.backgroundColor = .blue - super.init(layerBacked: false) + super.init(rotated: rotated) //self.addSubnode(self.debugNode) @@ -1245,7 +1245,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } } - if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.isChannelPost, replyThreadMessage.messageId.peerId != item.content.firstMessage.id.peerId { + if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.isChannelPost, replyThreadMessage.peerId != item.content.firstMessage.id.peerId { return false } @@ -1363,6 +1363,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI break } } + let sourceAuthorInfo = item.content.firstMessage.sourceAuthorInfo var isCrosspostFromChannel = false if let _ = sourceReference { @@ -1389,10 +1390,17 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI displayAuthorInfo = false } else if item.message.id.peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) { if let forwardInfo = item.content.firstMessage.forwardInfo { - ignoreForward = true effectiveAuthor = forwardInfo.author - if effectiveAuthor == nil, let authorSignature = forwardInfo.authorSignature { - effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) + + if let sourceAuthorInfo, let originalAuthorId = sourceAuthorInfo.originalAuthor, let peer = item.message.peers[originalAuthorId] { + effectiveAuthor = peer + } else if let sourceAuthorInfo, let originalAuthorName = sourceAuthorInfo.originalAuthorName { + effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(originalAuthorName.persistentHashValue % 32))), accessHash: nil, firstName: originalAuthorName, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) + } else { + ignoreForward = true + if effectiveAuthor == nil, let authorSignature = forwardInfo.authorSignature { + effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) + } } } displayAuthorInfo = !mergedTop.merged && incoming && effectiveAuthor != nil @@ -1717,7 +1725,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if case .admin = authorRank { } else if case .owner = authorRank { } else if authorRank == nil { - enableAutoRank = true + if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.peerId == item.context.account.peerId { + } else { + enableAutoRank = true + } } if enableAutoRank { if let topicAuthorId = item.associatedData.topicAuthorId, topicAuthorId == message.author?.id { @@ -1744,7 +1755,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI inlineBotNameString = attribute.title } } else if let attribute = attribute as? ReplyMessageAttribute { - if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == attribute.messageId { + if case let .replyThread(replyThreadMessage) = item.chatLocation, Int32(clamping: replyThreadMessage.threadId) == attribute.messageId.id { } else { replyMessage = firstMessage.associatedMessages[attribute.messageId] } @@ -4076,7 +4087,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } var isCurrentlyPlayingMedia = false - if item.associatedData.currentlyPlayingMessageId == item.message.index { + if item.associatedData.currentlyPlayingMessageId == item.message.index, let file = item.message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile, file.isInstantVideo { isCurrentlyPlayingMedia = true } @@ -4420,7 +4431,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if let item = self.item { for attribute in item.message.attributes { if let attribute = attribute as? ReplyMessageAttribute { - if let threadId = item.message.threadId, makeThreadIdMessageId(peerId: item.message.id.peerId, threadId: threadId) == attribute.messageId, let quotedReply = item.message.attributes.first(where: { $0 is QuotedReplyMessageAttribute }) as? QuotedReplyMessageAttribute { + if let threadId = item.message.threadId, Int32(clamping: threadId) == attribute.messageId.id, let quotedReply = item.message.attributes.first(where: { $0 is QuotedReplyMessageAttribute }) as? QuotedReplyMessageAttribute { return .action(InternalBubbleTapAction.Action({ [weak self, weak replyInfoNode] in guard let self, let item = self.item, let replyInfoNode else { return diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift index e1de4b5dc99..d400478e3c2 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift @@ -71,22 +71,28 @@ public func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Mess } } - let timestamp: Int32 + var timestamp: Int32 if let scheduleTime = message.scheduleTime { timestamp = scheduleTime } else { timestamp = message.timestamp } - var dateText = stringForMessageTimestamp(timestamp: timestamp, dateTimeFormat: dateTimeFormat) - if timestamp == scheduleWhenOnlineTimestamp { - dateText = " " - } var displayFullDate = false if case .full = format, timestamp > 100000 { displayFullDate = true - } else if let _ = message.forwardInfo, message.id.peerId == accountPeerId { + } else if let forwardInfo = message.forwardInfo, message.id.peerId == accountPeerId { displayFullDate = true + timestamp = forwardInfo.date + } + + if let sourceAuthorInfo = message.sourceAuthorInfo, let orignalDate = sourceAuthorInfo.orignalDate { + timestamp = orignalDate + } + + var dateText = stringForMessageTimestamp(timestamp: timestamp, dateTimeFormat: dateTimeFormat) + if timestamp == scheduleWhenOnlineTimestamp { + dateText = " " } if displayFullDate { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift index c6fdb43aaa2..9c67e031c4a 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift @@ -298,7 +298,8 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode, var trimSubscriptionCount = false if let prizeDescription { additionalPrizeSeparatorString = NSAttributedString(string: item.presentationData.strings.Chat_Giveaway_Message_With, font: textFont, textColor: secondaryTextColor) - additionalPrizeTextString = parseMarkdownIntoAttributedString("**\(giveaway.quantity)** \(prizeDescription)", attributes: MarkdownAttributes( + let quantityString = item.presentationData.strings.Chat_Giveaway_Message_CustomPrizeQuantity(giveaway.quantity) + additionalPrizeTextString = parseMarkdownIntoAttributedString("**\(quantityString)** \(prizeDescription)", attributes: MarkdownAttributes( body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: textColor), @@ -311,6 +312,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode, var subscriptionsString = item.presentationData.strings.Chat_Giveaway_Message_Subscriptions(giveaway.quantity) if trimSubscriptionCount { + subscriptionsString = item.presentationData.strings.Chat_Giveaway_Message_WithSubscriptions(giveaway.quantity) subscriptionsString = subscriptionsString.replacingOccurrences(of: "**\(giveaway.quantity)** ", with: "") } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift index e0e3ebe07f2..62303af69c8 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift @@ -89,13 +89,13 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureReco fileprivate var wasPlaying = false - required public init() { + required public init(rotated: Bool) { self.contextSourceNode = ContextExtractedContentContainingNode() self.containerNode = ContextControllerSourceNode() self.interactiveVideoNode = ChatMessageInteractiveInstantVideoNode() self.messageAccessibilityArea = AccessibilityAreaNode() - super.init(layerBacked: false) + super.init(rotated: rotated) self.interactiveVideoNode.shouldOpen = { [weak self] in if let strongSelf = self { @@ -221,7 +221,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureReco return false } - if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.isChannelPost, replyThreadMessage.messageId.peerId != item.content.firstMessage.id.peerId { + if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.isChannelPost, replyThreadMessage.peerId != item.content.firstMessage.id.peerId { return false } @@ -495,7 +495,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureReco } if let replyAttribute = attribute as? ReplyMessageAttribute { - if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == replyAttribute.messageId { + if case let .replyThread(replyThreadMessage) = item.chatLocation, Int32(clamping: replyThreadMessage.threadId) == replyAttribute.messageId.id { } else { replyMessage = item.message.associatedMessages[replyAttribute.messageId] } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/BUILD index bfe7061fab3..6a69b7642a1 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/BUILD @@ -50,6 +50,7 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode", "//submodules/TelegramUI/Components/Chat/ChatHistoryEntry", "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/AnimatedCountLabelNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift index 6693dbfe762..7d79b1e00a5 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift @@ -1,5 +1,6 @@ // MARK: Nicegram Imports import FeatPremiumUI +import FeatSpeechToText import NGData import NGStrings import NGTelegramIntegration @@ -41,6 +42,7 @@ import ChatMessageDateAndStatusNode import ChatHistoryEntry import ChatMessageItemCommon import TelegramStringFormatting +import AnimatedCountLabelNode private struct FetchControls { let fetch: (Bool) -> Void @@ -128,6 +130,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { private let descriptionMeasuringNode: TextNode public let fetchingTextNode: ImmediateTextNode public let fetchingCompactTextNode: ImmediateTextNode + private let countNode: ImmediateAnimatedCountLabelNode public var waveformView: ComponentHostView? @@ -202,6 +205,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { private var progressFrame: CGRect? private var streamingCacheStatusFrame: CGRect? private var fileIconImage: UIImage? + private var viewOnceIconImage: UIImage? public var audioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState = .collapsed public var forcedAudioTranscriptionText: TranscribedText? @@ -226,6 +230,9 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { self.descriptionNode.displaysAsynchronously = false self.descriptionNode.isUserInteractionEnabled = false + self.countNode = ImmediateAnimatedCountLabelNode() + self.countNode.alwaysOneDirection = true + self.descriptionMeasuringNode = TextNode() self.fetchingTextNode = ImmediateTextNode() @@ -427,35 +434,49 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { self.requestUpdateLayout(true) // MARK: Nicegram Speech2Text - let isTelegramPremium = arguments.associatedData.isPremium - let shouldUseNicegramTranscribe = !isTelegramPremium && isNicegramPremium + let shouldUseNicegramTranscribe: Bool + if #available(iOS 13.0, *) { + let resolveProviderTypeUseCase = SpeechToTextContainer.shared.resolveProviderTypeUseCase() + + let isTelegramPremium = arguments.associatedData.isPremium + let useOpenAi = resolveProviderTypeUseCase() == .openAi + + shouldUseNicegramTranscribe = !isTelegramPremium || useOpenAi + } else { + shouldUseNicegramTranscribe = false + } + if shouldUseNicegramTranscribe { - let appLocale = presentationData.strings.baseLanguageCode if let mediaFile = message.media.compactMap({ $0 as? TelegramMediaFile }).first(where: { $0.isVoice }) { - let processor = TgVoiceToTextProcessor(mediaBox: context.account.postbox.mediaBox, additionalLanguageCodes: [appLocale]) - processor.recognize(mediaFile: mediaFile) { [weak self] result in - switch result { - case let .success(text): - message.updateAudioTranscriptionAttribute(text: text, error: nil, context: context) - case let .failure(error): - message.updateAudioTranscriptionAttribute(text: "", error: error, context: context) + + if #available(iOS 13.0, *) { + Task { @MainActor in + let manager = TgSpeechToTextManager( + mediaBox: context.account.postbox.mediaBox + ) - let errorDescription: String? - switch error { - case .needPremium: - errorDescription = nil - case .lowAccuracy: - errorDescription = l("Messages.SpeechToText.LowAccuracyError", appLocale) - case .underlying(let error): - errorDescription = error.localizedDescription - } - if let errorDescription = errorDescription { - let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) - let c = getIAPErrorController(context: context, errorDescription, presentationData) - self?.arguments?.controllerInteraction.presentGlobalOverlayController(c, nil) + let result = await manager.convertSpeechToText( + mediaFile: mediaFile + ) + + switch result { + case .success(let text): + message.updateAudioTranscriptionAttribute(text: text, error: nil, context: context) + case .needsPremium: + PremiumUITgHelper.routeToPremium() + case .error(let error): + message.updateAudioTranscriptionAttribute(text: "", error: error, context: context) + + let c = getIAPErrorController( + context: context, + error.localizedDescription, + context.sharedContext.currentPresentationData.with({ $0 }) + ) + self.arguments?.controllerInteraction.presentGlobalOverlayController(c, nil) } + + self.audioTranscriptionState = .expanded } - self?.audioTranscriptionState = .expanded } } } @@ -518,6 +539,59 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { strongSelf.transcribeDisposable = nil }) } else { + // MARK: Nicegram Speech2Text + if #available(iOS 13.0, *) { + Task { @MainActor [weak self] in + try await Task.sleep(seconds: 4) + + guard let self else { return } + + let preferencesRepository = SpeechToTextContainer.shared.preferencesRepository() + guard await !preferencesRepository.sawNicegramPremiumTooltip() else { + return + } + + guard let message = self.message, + transcribedText(message: message) == nil else { + return + } + + guard !isPremium() else { + return + } + + let tooltipController = UndoOverlayController( + presentationData: presentationData, + content: .universal( + animation: "Transcribe", + scale: 0.06, + colors: [:], + title: nil, + text: l("SpeechToText.Toast"), + customUndoText: nil, + timeout: nil + ), + elevatedLayout: false, + animateInAsReplacement: false, + blurred: true, + action: { action in + if case .info = action { + PremiumUITgHelper.routeToPremium() + } + + return true + } + ) + + self.arguments?.controllerInteraction.presentControllerInCurrent(tooltipController, nil) + + await preferencesRepository.set( + sawNicegramPremiumTooltip: true + ) + } + } + // + self.transcribeDisposable = (context.engine.messages.transcribeAudio(messageId: message.id) |> deliverOnMainQueue).startStrict(next: { [weak self] result in guard let strongSelf = self else { @@ -793,6 +867,8 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { let (titleLayout, titleApply) = titleAsyncLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: hasThumbnail ? 2 : 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (descriptionLayout, descriptionApply) = descriptionAsyncLayout(TextNodeLayoutArguments(attributedString: descriptionString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let isViewOnceMessage = isVoice && arguments.message.minAutoremoveOrClearTimeout == viewOnceTimeout + let fileSizeString: String if let _ = arguments.file.size { fileSizeString = "000.0 MB" @@ -807,10 +883,9 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { var updatedAudioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState? var displayTranscribe = false - if arguments.message.id.peerId.namespace != Namespaces.Peer.SecretChat && !arguments.presentationData.isPreview { + if arguments.message.id.peerId.namespace != Namespaces.Peer.SecretChat && !isViewOnceMessage && !arguments.presentationData.isPreview { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: arguments.context.currentAppConfiguration.with { $0 }) - // MARK: Nicegram Speech2Text, nicegram premium check - if arguments.associatedData.isPremium || isPremium() { + if arguments.associatedData.isPremium { displayTranscribe = true } else if premiumConfiguration.audioTransciptionTrialCount > 0 { if arguments.incoming { @@ -825,6 +900,19 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { displayTranscribe = true } } + + // MARK: Nicegram Speech2Text + if #available(iOS 13.0, *) { + let isNicegramPrmeium = isPremium() + + let getSpeechToTextConfigUseCase = SpeechToTextContainer.shared.getSpeechToTextConfigUseCase() + let alwaysShowButton = getSpeechToTextConfigUseCase().alwaysShowButton + + if isNicegramPrmeium || alwaysShowButton { + displayTranscribe = true + } + } + // } let transcribedText = forcedAudioTranscriptionText ?? transcribedText(message: arguments.message) @@ -1027,12 +1115,14 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { minLayoutWidth = max(minLayoutWidth, textLayout.size.width + horizontalInset) let fileIconImage: UIImage? + var viewOnceIconImage: UIImage? if hasThumbnail { fileIconImage = nil } else { let principalGraphics = PresentationResourcesChat.principalGraphics(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper, bubbleCorners: arguments.presentationData.chatBubbleCorners) fileIconImage = arguments.incoming ? principalGraphics.radialIndicatorFileIconIncoming : principalGraphics.radialIndicatorFileIconOutgoing + viewOnceIconImage = principalGraphics.radialIndicatorViewOnceIcon } return (minLayoutWidth, { boundingWidth in @@ -1112,7 +1202,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { strongSelf.titleNode.frame = titleFrame strongSelf.descriptionNode.frame = descriptionFrame strongSelf.descriptionMeasuringNode.frame = CGRect(origin: CGPoint(), size: descriptionMeasuringLayout.size) - + if let updatedAudioTranscriptionState = updatedAudioTranscriptionState { strongSelf.audioTranscriptionState = updatedAudioTranscriptionState } @@ -1352,6 +1442,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { samples: audioWaveform?.samples ?? Data(), peak: audioWaveform?.peak ?? 0, status: strongSelf.playbackStatus.get(), + isViewOnceMessage: isViewOnceMessage, seek: { timestamp in if let strongSelf = self, let context = strongSelf.context, let message = strongSelf.message, let type = peerMessageMediaPlayerType(EngineMessage(message)) { context.sharedContext.mediaManager.playlistControl(.seek(timestamp), type: type) @@ -1494,7 +1585,8 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { strongSelf.progressFrame = progressFrame strongSelf.streamingCacheStatusFrame = streamingCacheStatusFrame strongSelf.fileIconImage = fileIconImage - + strongSelf.viewOnceIconImage = viewOnceIconImage + if let updatedFetchControls = updatedFetchControls { let _ = strongSelf.fetchControls.swap(updatedFetchControls) if arguments.automaticDownload { @@ -1610,6 +1702,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { } } } + let isViewOnceMessage = isVoice && message.minAutoremoveOrClearTimeout == viewOnceTimeout var state: SemanticStatusNodeState var streamingState: SemanticStatusNodeState = .none @@ -1618,6 +1711,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { var downloadingStrings: (String, String, UIFont)? + var playbackState: (position: Double, duration: Double, generationTimestamp: Double) = (0.0, 0.0, 0.0) if !isAudio { var fetchStatus: MediaResourceStatus? if let actualFetchStatus = self.actualFetchStatus, message.forwardInfo != nil { @@ -1641,75 +1735,87 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { } } else if isVoice { if let playerStatus = self.playerStatus { - var playerPosition: Int32? - var playerDuration: Int32 = 0 + var playerPosition: Double? + var playerDuration: Double = 0.0 if !playerStatus.generationTimestamp.isZero, case .playing = playerStatus.status { - playerPosition = Int32(playerStatus.timestamp + (CACurrentMediaTime() - playerStatus.generationTimestamp)) + playerPosition = playerStatus.timestamp + (CACurrentMediaTime() - playerStatus.generationTimestamp) } else { - playerPosition = Int32(playerStatus.timestamp) + playerPosition = playerStatus.timestamp } - playerDuration = Int32(playerStatus.duration) + playerDuration = playerStatus.duration + + let effectiveDuration = playerDuration > 0 ? playerDuration : Double(audioDuration ?? 0) - let durationString = stringForDuration(playerDuration > 0 ? playerDuration : (audioDuration ?? 0), position: playerPosition) + let durationString = stringForDuration(Int32(effectiveDuration), position: playerPosition.flatMap { Int32($0) }) let durationFont = Font.regular(floor(presentationData.fontSize.baseDisplaySize * 11.0 / 17.0)) downloadingStrings = (durationString, durationString, durationFont) + + playbackState = (playerStatus.timestamp, playerDuration, playerStatus.generationTimestamp) } } switch resourceStatus.mediaStatus { - case var .fetchStatus(fetchStatus): - if self.message?.forwardInfo != nil { - fetchStatus = resourceStatus.fetchStatus - } - (self.waveformView?.componentView as? AudioWaveformComponent.View)?.enableScrubbing = false - //self.waveformScrubbingNode?.enableScrubbing = false + case var .fetchStatus(fetchStatus): + if self.message?.forwardInfo != nil { + fetchStatus = resourceStatus.fetchStatus + } + (self.waveformView?.componentView as? AudioWaveformComponent.View)?.enableScrubbing = false - switch fetchStatus { - case let .Fetching(_, progress): - let adjustedProgress = max(progress, 0.027) - var wasCheck = false - if let statusNode = self.statusNode, case .check = statusNode.state { - wasCheck = true - } - - if isAudio && !isVoice && !isSending { - state = .play - } else { - if message.groupingKey != nil, adjustedProgress.isEqual(to: 1.0), (message.flags.contains(.Unsent) || wasCheck) { - state = .check(appearance: nil) - } else { - state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: nil) - } - } - case .Local: - if isAudio { - state = .play - } else if let fileIconImage = self.fileIconImage { - state = .customIcon(fileIconImage) - } else { - state = .none - } - case .Remote, .Paused: - if isAudio && !isVoice { - state = .play - } else { - state = .download - } + switch fetchStatus { + case let .Fetching(_, progress): + let adjustedProgress = max(progress, 0.027) + var wasCheck = false + if let statusNode = self.statusNode, case .check = statusNode.state { + wasCheck = true + } + + if isAudio && !isVoice && !isSending { + state = .play + } else { + if message.groupingKey != nil, adjustedProgress.isEqual(to: 1.0), (message.flags.contains(.Unsent) || wasCheck) { + state = .check(appearance: nil) + } else { + state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: nil) + } } - case let .playbackStatus(playbackStatus): - (self.waveformView?.componentView as? AudioWaveformComponent.View)?.enableScrubbing = true - //self.waveformScrubbingNode?.enableScrubbing = true + case .Local: + if isAudio { + state = .play + } else if let fileIconImage = self.fileIconImage { + state = .customIcon(fileIconImage) + } else { + state = .none + } + case .Remote, .Paused: + if isAudio && !isVoice { + state = .play + } else { + state = .download + } + } + case let .playbackStatus(playbackStatus): + (self.waveformView?.componentView as? AudioWaveformComponent.View)?.enableScrubbing = !isViewOnceMessage + if isViewOnceMessage && playbackStatus == .playing { + state = .secretTimeout(position: playbackState.position, duration: playbackState.duration, generationTimestamp: playbackState.generationTimestamp, appearance: .init(inset: 1.0 + UIScreenPixel, lineWidth: 2.0 - UIScreenPixel)) + if incoming { + self.consumableContentNode.isHidden = true + } + } else { switch playbackStatus { - case .playing: - state = .pause - case .paused: - state = .play + case .playing: + state = .pause + case .paused: + state = .play } + } } - if isAudio && !isVoice && !isSending && state != .pause { - switch resourceStatus.fetchStatus { + if isViewOnceMessage, let viewOnceIconImage = self.viewOnceIconImage, state == .play { + streamingState = .customIcon(viewOnceIconImage) + } else { + if isAudio && !isVoice && !isSending && state != .pause { + switch resourceStatus.fetchStatus { case let .Fetching(_, progress): let adjustedProgress = max(progress, 0.027) streamingState = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: .init(inset: 1.0, lineWidth: 2.0)) @@ -1717,9 +1823,10 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { streamingState = .none case .Remote, .Paused: streamingState = .download + } + } else { + streamingState = .none } - } else { - streamingState = .none } if isSending { @@ -1783,7 +1890,13 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { } let effectsEnabled = self.context?.sharedContext.energyUsageSettings.fullTranslucency ?? true - if case .pause = state, isVoice, self.playbackAudioLevelNode == nil, effectsEnabled { + var showBlobs = false + if case .pause = state { + showBlobs = true + } else if case .secretTimeout = state { + showBlobs = true + } + if showBlobs, isVoice, self.playbackAudioLevelNode == nil, effectsEnabled { let blobFrame = progressFrame.insetBy(dx: -12.0, dy: -12.0) let playbackAudioLevelNode = VoiceBlobNode( maxLevel: 0.3, @@ -1813,6 +1926,11 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { self.streamingStatusNode = streamingStatusNode streamingStatusNode.frame = streamingCacheStatusFrame self.addSubnode(streamingStatusNode) + + if isViewOnceMessage { + streamingStatusNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.linear.rawValue) + streamingStatusNode.layer.animateAlpha(from: 0.1, to: 1.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.linear.rawValue) + } } else if let streamingStatusNode = self.streamingStatusNode { streamingStatusNode.backgroundNodeColor = backgroundNodeColor } @@ -1831,10 +1949,9 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { } }) - switch state { - case .pause: + if showBlobs { self.playbackAudioLevelNode?.startAnimating() - default: + } else { self.playbackAudioLevelNode?.stopAnimating() } } @@ -1842,7 +1959,10 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { if let streamingStatusNode = self.streamingStatusNode { if streamingState == .none { self.streamingStatusNode = nil - streamingStatusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak streamingStatusNode] _ in + if isViewOnceMessage { + streamingStatusNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false) + } + streamingStatusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak streamingStatusNode] _ in if streamingState == .none { streamingStatusNode?.removeFromSupernode() } @@ -1863,9 +1983,30 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { statusNode.setCutout(cutoutFrame, animated: true) } + var displayingCountdown = false if let (expandedString, compactString, font) = downloadingStrings { self.fetchingTextNode.attributedText = NSAttributedString(string: expandedString, font: font, textColor: messageTheme.fileDurationColor) self.fetchingCompactTextNode.attributedText = NSAttributedString(string: compactString, font: font, textColor: messageTheme.fileDurationColor) + + if isViewOnceMessage { + var segments: [AnimatedCountLabelNode.Segment] = [] + var textCount = 0 + for char in expandedString { + if let intValue = Int(String(char)) { + segments.append(.number(intValue, NSAttributedString(string: String(char), font: font, textColor: messageTheme.fileDurationColor))) + } else { + segments.append(.text(textCount, NSAttributedString(string: String(char), font: font, textColor: messageTheme.fileDurationColor))) + textCount += 1 + } + } + if self.countNode.supernode == nil { + self.addSubnode(self.countNode) + } + self.countNode.segments = segments + displayingCountdown = true + } else if self.countNode.supernode != nil { + self.countNode.removeFromSupernode() + } } else { self.fetchingTextNode.attributedText = nil self.fetchingCompactTextNode.attributedText = nil @@ -1874,24 +2015,32 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { let maxFetchingStatusWidth = max(self.titleNode.frame.width, self.descriptionMeasuringNode.frame.width) + 2.0 let fetchingInfo = self.fetchingTextNode.updateLayoutInfo(CGSize(width: maxFetchingStatusWidth, height: CGFloat.greatestFiniteMagnitude)) let fetchingCompactSize = self.fetchingCompactTextNode.updateLayout(CGSize(width: maxFetchingStatusWidth, height: CGFloat.greatestFiniteMagnitude)) + let countSize = self.countNode.updateLayout(size: CGSize(width: maxFetchingStatusWidth, height: CGFloat.greatestFiniteMagnitude), animated: true) - if downloadingStrings != nil { + if displayingCountdown { + self.fetchingTextNode.isHidden = true + self.fetchingCompactTextNode.isHidden = true self.descriptionNode.isHidden = true - if fetchingInfo.truncated { - self.fetchingTextNode.isHidden = true - self.fetchingCompactTextNode.isHidden = false + } else { + if downloadingStrings != nil { + self.descriptionNode.isHidden = true + if fetchingInfo.truncated { + self.fetchingTextNode.isHidden = true + self.fetchingCompactTextNode.isHidden = false + } else { + self.fetchingTextNode.isHidden = false + self.fetchingCompactTextNode.isHidden = true + } } else { - self.fetchingTextNode.isHidden = false + self.descriptionNode.isHidden = false + self.fetchingTextNode.isHidden = true self.fetchingCompactTextNode.isHidden = true } - } else { - self.descriptionNode.isHidden = false - self.fetchingTextNode.isHidden = true - self.fetchingCompactTextNode.isHidden = true } self.fetchingTextNode.frame = CGRect(origin: self.descriptionNode.frame.origin, size: fetchingInfo.size) self.fetchingCompactTextNode.frame = CGRect(origin: self.descriptionNode.frame.origin, size: fetchingCompactSize) + self.countNode.frame = CGRect(origin: self.descriptionNode.frame.origin, size: countSize) } public typealias Apply = (Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> ChatMessageInteractiveFileNode diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/BUILD index 6e073b1b223..2e541b8c8d9 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/BUILD @@ -30,6 +30,7 @@ swift_library( "//submodules/TelegramNotices", "//submodules/Markdown", "//submodules/TextFormat", + "//submodules/InvisibleInkDustNode", "//submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode", "//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode", "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift index 46440364f07..355eab8cf14 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -29,6 +29,7 @@ import ChatInstantVideoMessageDurationNode import ChatControllerInteraction import WallpaperBackgroundNode import TelegramStringFormatting +import InvisibleInkDustNode public struct ChatMessageInstantVideoItemLayoutResult { public let contentSize: CGSize @@ -89,6 +90,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { public var audioTranscriptionButton: ComponentHostView? + private var dustNode: MediaDustNode? private var statusNode: RadialStatusNode? private var disappearingStatusNode: RadialStatusNode? private var playbackStatusNode: InstantVideoRadialStatusNode? @@ -278,8 +280,13 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { updatedMuteIconImage = PresentationResourcesChat.chatInstantMessageMuteIconImage(item.presentationData.theme.theme) } + let isViewOnceMessage = item.message.minAutoremoveOrClearTimeout == viewOnceTimeout + let theme = item.presentationData.theme - let isSecretMedia = item.message.containsSecretMedia + var isSecretMedia = item.message.containsSecretMedia + if isViewOnceMessage { + isSecretMedia = true + } if isSecretMedia { secretVideoPlaceholderBackgroundImage = PresentationResourcesChat.chatInstantVideoBackgroundImage(theme.theme, wallpaper: !theme.wallpaper.isEmpty) } @@ -363,7 +370,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } if let replyAttribute = attribute as? ReplyMessageAttribute { - if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == replyAttribute.messageId { + if case let .replyThread(replyThreadMessage) = item.chatLocation, Int32(clamping: replyThreadMessage.threadId) == replyAttribute.messageId.id { } else { replyMessage = item.message.associatedMessages[replyAttribute.messageId] } @@ -376,7 +383,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } if replyMessage != nil || replyForward != nil || replyStory != nil { - if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == replyMessage?.id { + if case let .replyThread(replyThreadMessage) = item.chatLocation, Int32(clamping: replyThreadMessage.threadId) == replyMessage?.id.id { } else { replyInfoApply = makeReplyInfoLayout(ChatMessageReplyInfoNode.Arguments( presentationData: item.presentationData, @@ -805,9 +812,9 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } })) } - + var displayTranscribe = false - if item.message.id.peerId.namespace != Namespaces.Peer.SecretChat && statusDisplayType == .free && !item.presentationData.isPreview { + if item.message.id.peerId.namespace != Namespaces.Peer.SecretChat && statusDisplayType == .free && !isViewOnceMessage && !item.presentationData.isPreview { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: item.context.currentAppConfiguration.with { $0 }) if item.associatedData.isPremium { displayTranscribe = true @@ -944,12 +951,15 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { animation.animator.updateFrame(layer: strongSelf.secretVideoPlaceholderBackground.layer, frame: displayVideoFrame, completion: nil) let placeholderFrame = videoFrame.insetBy(dx: 2.0, dy: 2.0) - strongSelf.secretVideoPlaceholder.bounds = CGRect(origin: CGPoint(), size: videoFrame.size) + strongSelf.secretVideoPlaceholder.bounds = CGRect(origin: CGPoint(), size: placeholderFrame.size) animation.animator.updateScale(layer: strongSelf.secretVideoPlaceholder.layer, scale: imageScale, completion: nil) animation.animator.updatePosition(layer: strongSelf.secretVideoPlaceholder.layer, position: displayVideoFrame.center, completion: nil) + let placeholderSide = floor(placeholderFrame.size.width / 2.0) * 2.0 + let placeholderSize = CGSize(width: placeholderSide, height: placeholderSide) + let makeSecretPlaceholderLayout = strongSelf.secretVideoPlaceholder.asyncLayout() - let arguments = TransformImageArguments(corners: ImageCorners(radius: placeholderFrame.size.width / 2.0), imageSize: placeholderFrame.size, boundingSize: placeholderFrame.size, intrinsicInsets: UIEdgeInsets()) + let arguments = TransformImageArguments(corners: ImageCorners(radius: placeholderSize.width / 2.0), imageSize: placeholderSize, boundingSize: placeholderSize, intrinsicInsets: UIEdgeInsets()) let applySecretPlaceholder = makeSecretPlaceholderLayout(arguments) applySecretPlaceholder() @@ -1142,7 +1152,10 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } let messageTheme = item.presentationData.theme.theme.chat.message + let isViewOnceMessage = item.message.minAutoremoveOrClearTimeout == viewOnceTimeout + let isSecretMedia = item.message.containsSecretMedia + var secretBeginTimeAndTimeout: (Double, Double)? if isSecretMedia { if let attribute = item.message.autoclearAttribute { @@ -1195,6 +1208,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { self.infoBackgroundNode.layer.animateScale(from: 1.0, to: 0.4, duration: 0.15) } } + self.infoBackgroundNode.isHidden = isViewOnceMessage var isBuffering: Bool? if let message = self.item?.message, let media = self.media, isMediaStreamable(message: message, media: media) && (self.automaticDownload ?? false) { @@ -1277,7 +1291,9 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { state = .progress(color: messageTheme.mediaOverlayControlColors.foregroundColor, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true, animateRotation: true) } case .Local: - if isSecretMedia { + if isViewOnceMessage { + state = .play(messageTheme.mediaOverlayControlColors.foregroundColor) + } else if isSecretMedia { if let (beginTime, timeout) = secretBeginTimeAndTimeout { state = .secretTimeout(color: messageTheme.mediaOverlayControlColors.foregroundColor, icon: .flame, beginTime: beginTime, timeout: timeout, sparks: true) } else { @@ -1319,7 +1335,8 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { if let current = self.playbackStatusNode { playbackStatusNode = current } else { - playbackStatusNode = InstantVideoRadialStatusNode(color: UIColor(white: 1.0, alpha: 0.6), hasSeek: true) + playbackStatusNode = InstantVideoRadialStatusNode(color: UIColor(white: 1.0, alpha: 0.6), hasSeek: !isViewOnceMessage, sparks: isViewOnceMessage) + playbackStatusNode.isUserInteractionEnabled = !isViewOnceMessage playbackStatusNode.seekTo = { [weak self] position, play in guard let strongSelf = self else { return @@ -1347,6 +1364,11 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { self.videoNode?.isHidden = false self.secretVideoPlaceholderBackground.isHidden = true self.secretVideoPlaceholder.isHidden = true + + if let dustNode = self.dustNode { + self.dustNode = nil + dustNode.removeFromSupernode() + } } else { if let playbackStatusNode = self.playbackStatusNode { self.playbackStatusNode = nil @@ -1354,9 +1376,24 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } self.durationNode?.status = .single(nil) - self.videoNode?.isHidden = isSecretMedia - self.secretVideoPlaceholderBackground.isHidden = !isSecretMedia - self.secretVideoPlaceholder.isHidden = !isSecretMedia && !item.presentationData.isPreview + self.videoNode?.isHidden = isSecretMedia || isViewOnceMessage + self.secretVideoPlaceholderBackground.isHidden = !isSecretMedia && !isViewOnceMessage + self.secretVideoPlaceholder.isHidden = !isSecretMedia && !isViewOnceMessage && !item.presentationData.isPreview + + if isViewOnceMessage { + let dustNode: MediaDustNode + if let current = self.dustNode { + dustNode = current + } else { + dustNode = MediaDustNode(enableAnimations: item.controllerInteraction.enableFullTranslucency) + dustNode.clipsToBounds = true + self.insertSubnode(dustNode, belowSubnode: self.dateAndStatusNode) + self.dustNode = dustNode + } + dustNode.cornerRadius = videoFrame.width / 2.0 + dustNode.frame = videoFrame + dustNode.update(size: videoFrame.size, color: .white, transition: .immediate) + } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index 006fd84bf54..7f13261cbcb 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -434,7 +434,8 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr private var internallyVisible = true private func updateVisibility() { - let visibility = self.visibility && self.internallyVisible + let isPreview = self.themeAndStrings?.3 ?? false + let visibility = self.visibility && self.internallyVisible && !isPreview if let videoNode = self.videoNode { if visibility { @@ -1258,7 +1259,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr uploading = true } - if file.isVideo && !file.isVideoSticker && !isSecretMedia && automaticPlayback && !isStory && !uploading && !presentationData.isPreview { + if file.isVideo && !file.isVideoSticker && !isSecretMedia && automaticPlayback && !isStory && !uploading { updateVideoFile = file if hasCurrentVideoNode { if let currentFile = currentMedia as? TelegramMediaFile { @@ -1458,6 +1459,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr if let statusApply = statusApply { let dateAndStatusFrame = CGRect(origin: CGPoint(x: cleanImageFrame.width - layoutConstants.image.statusInsets.right - statusSize.width, y: cleanImageFrame.height - layoutConstants.image.statusInsets.bottom - statusSize.height), size: statusSize) if strongSelf.dateAndStatusNode.supernode == nil { + strongSelf.dateAndStatusNode.view.tag = 0xFACE strongSelf.pinchContainerNode.contentNode.addSubnode(strongSelf.dateAndStatusNode) statusApply(.None) strongSelf.dateAndStatusNode.frame = dateAndStatusFrame @@ -1513,6 +1515,11 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr strongSelf.videoContent = videoContent strongSelf.videoNode = videoNode + if presentationData.isPreview { + videoNode.isHidden = true + strongSelf.pinchContainerNode.contentNode.insertSubnode(videoNode, aboveSubnode: strongSelf.imageNode) + } + updatedVideoNodeReadySignal = videoNode.ready updatedPlayerStatusSignal = videoNode.status |> mapToSignal { status -> Signal in @@ -1563,7 +1570,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr videoNode.updateLayout(size: arguments.drawingSize, transition: .immediate) videoNode.frame = CGRect(origin: CGPoint(), size: imageFrame.size) - if strongSelf.visibility && strongSelf.internallyVisible { + if strongSelf.visibility && strongSelf.internallyVisible && !presentationData.isPreview { if !videoNode.canAttachContent { videoNode.canAttachContent = true if videoNode.hasAttachedContext { @@ -2152,6 +2159,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr if var badgeContent = badgeContent { if self.badgeNode == nil { let badgeNode = ChatMessageInteractiveMediaBadge() + badgeNode.view.tag = 0xFACE if isPreview { badgeNode.durationNode.displaysAsynchronously = false } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift index e2d88372936..ac098658ff3 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift @@ -122,7 +122,7 @@ public protocol ChatMessageItem: ListViewItem { var sending: Bool { get } var failed: Bool { get } - func mergedWithItems(top: ListViewItem?, bottom: ListViewItem?) -> (top: ChatMessageMerge, bottom: ChatMessageMerge, dateAtBottom: Bool) + func mergedWithItems(top: ListViewItem?, bottom: ListViewItem?, isRotated: Bool) -> (top: ChatMessageMerge, bottom: ChatMessageMerge, dateAtBottom: Bool) } public func hasCommentButton(item: ChatMessageItem) -> Bool { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift index 23f849fb396..b2ef550c96a 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift @@ -234,7 +234,7 @@ public func isPollEffectivelyClosed(message: Message, poll: TelegramMediaPoll) - public extension ChatReplyThreadMessage { var effectiveTopId: MessageId { - return self.channelMessageId ?? self.messageId + return self.channelMessageId ?? MessageId(peerId: self.peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: self.threadId)) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift index ae698993b69..63ae52c794d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift @@ -47,9 +47,13 @@ public final class ChatMessageDateHeader: ListViewItemHeader { self.action = action self.roundedTimestamp = dateHeaderTimestampId(timestamp: timestamp) self.id = ListViewItemNode.HeaderId(space: 0, id: Int64(self.roundedTimestamp)) + + let isRotated = controllerInteraction?.chatIsRotated ?? true + + self.stickDirection = isRotated ? .bottom : .top } - public let stickDirection: ListViewItemHeaderStickDirection = .bottom + public let stickDirection: ListViewItemHeaderStickDirection public let stickOverInsets: Bool = true public let height: CGFloat = 34.0 @@ -191,9 +195,13 @@ public final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { } self.text = text - super.init(layerBacked: false, dynamicBounce: true, isRotated: true, seeThrough: false) + let isRotated = controllerInteraction?.chatIsRotated ?? true - self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + super.init(layerBacked: false, dynamicBounce: true, isRotated: isRotated, seeThrough: false) + + if isRotated { + self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + } let graphics = PresentationResourcesChat.principalGraphics(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, bubbleCorners: presentationData.chatBubbleCorners) @@ -398,9 +406,13 @@ public final class ChatMessageAvatarHeader: ListViewItemHeader { self.controllerInteraction = controllerInteraction self.id = ListViewItemNode.HeaderId(space: 1, id: Id(peerId: peerId, timestampId: dateHeaderTimestampId(timestamp: timestamp))) self.storyStats = storyStats + + let isRotated = controllerInteraction?.chatIsRotated ?? true + + self.stickDirection = isRotated ? .top : .bottom } - public let stickDirection: ListViewItemHeaderStickDirection = .top + public let stickDirection: ListViewItemHeaderStickDirection public let stickOverInsets: Bool = false public let height: CGFloat = 38.0 @@ -484,9 +496,13 @@ public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode, Chat self.avatarNode = AvatarNode(font: avatarFont) self.avatarNode.contentNode.displaysAsynchronously = !presentationData.isPreview - super.init(layerBacked: false, dynamicBounce: true, isRotated: true, seeThrough: false) + let isRotated = controllerInteraction?.chatIsRotated ?? true + + super.init(layerBacked: false, dynamicBounce: true, isRotated: isRotated, seeThrough: false) - self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + if isRotated { + self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + } self.addSubnode(self.containerNode) self.containerNode.addSubnode(self.avatarNode) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift index f64cfb2dff5..809bc40416b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift @@ -56,12 +56,24 @@ private func messagesShouldBeMerged(accountPeerId: PeerId, _ lhs: Message, _ rhs break } } + let lhsSourceAuthorInfo = lhs.sourceAuthorInfo + if let sourceAuthorInfo = lhsSourceAuthorInfo { + if let originalAuthor = sourceAuthorInfo.originalAuthor { + lhsEffectiveAuthor = lhs.peers[originalAuthor] + } + } for attribute in rhs.attributes { if let attribute = attribute as? SourceReferenceMessageAttribute { rhsEffectiveAuthor = rhs.peers[attribute.messageId.peerId] break } } + let rhsSourceAuthorInfo = rhs.sourceAuthorInfo + if let sourceAuthorInfo = rhsSourceAuthorInfo { + if let originalAuthor = sourceAuthorInfo.originalAuthor { + rhsEffectiveAuthor = rhs.peers[originalAuthor] + } + } var sameThread = true if let lhsPeer = lhs.peers[lhs.id.peerId], let rhsPeer = rhs.peers[rhs.id.peerId], arePeersEqual(lhsPeer, rhsPeer), let channel = lhsPeer as? TelegramChannel, channel.flags.contains(.isForum), lhs.threadId != rhs.threadId { @@ -73,6 +85,16 @@ private func messagesShouldBeMerged(accountPeerId: PeerId, _ lhs: Message, _ rhs sameAuthor = true } + if let lhsSourceAuthorInfo, let rhsSourceAuthorInfo { + if lhsSourceAuthorInfo.originalAuthor != rhsSourceAuthorInfo.originalAuthor { + sameAuthor = false + } else if lhsSourceAuthorInfo.originalAuthorName != rhsSourceAuthorInfo.originalAuthorName { + sameAuthor = false + } + } else if (lhsSourceAuthorInfo == nil) != (rhsSourceAuthorInfo == nil) { + sameAuthor = false + } + var lhsEffectiveTimestamp = lhs.timestamp var rhsEffectiveTimestamp = rhs.timestamp @@ -241,7 +263,7 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible self.associatedData = associatedData self.controllerInteraction = controllerInteraction self.content = content - self.disableDate = disableDate + self.disableDate = disableDate || !controllerInteraction.chatIsRotated self.additionalContent = additionalContent // MARK: Nicegram self.wantTrButton = wantTrButton @@ -264,6 +286,13 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) } } + if let sourceAuthorInfo = content.firstMessage.sourceAuthorInfo { + if let originalAuthor = sourceAuthorInfo.originalAuthor, let peer = content.firstMessage.peers[originalAuthor] { + effectiveAuthor = peer + } else if let authorSignature = sourceAuthorInfo.originalAuthorName { + effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) + } + } displayAuthorInfo = incoming && effectiveAuthor != nil } else { effectiveAuthor = content.firstMessage.author @@ -347,6 +376,9 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible if case .messageOptions = associatedData.subject { headers = [] } + if !controllerInteraction.chatIsRotated { + headers = [] + } if let avatarHeader = self.avatarHeader { headers.append(avatarHeader) } @@ -428,14 +460,14 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible } let configure = { - let node = (viewClassName as! ChatMessageItemView.Type).init() + let node = (viewClassName as! ChatMessageItemView.Type).init(rotated: self.controllerInteraction.chatIsRotated) // MARK: Nicegram node.wantTrButton = self.wantTrButton // node.setupItem(self, synchronousLoad: synchronousLoads) let nodeLayout = node.asyncLayout() - let (top, bottom, dateAtBottom) = self.mergedWithItems(top: previousItem, bottom: nextItem) + let (top, bottom, dateAtBottom) = self.mergedWithItems(top: previousItem, bottom: nextItem, isRotated: self.controllerInteraction.chatIsRotated) var disableDate = self.disableDate if let subject = self.associatedData.subject, case let .messageOptions(_, _, info) = subject { @@ -471,7 +503,15 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible } } - public func mergedWithItems(top: ListViewItem?, bottom: ListViewItem?) -> (top: ChatMessageMerge, bottom: ChatMessageMerge, dateAtBottom: Bool) { + public func mergedWithItems(top: ListViewItem?, bottom: ListViewItem?, isRotated: Bool) -> (top: ChatMessageMerge, bottom: ChatMessageMerge, dateAtBottom: Bool) { + var top = top + var bottom = bottom + if !isRotated { + let previousTop = top + top = bottom + bottom = previousTop + } + var mergedTop: ChatMessageMerge = .none var mergedBottom: ChatMessageMerge = .none var dateAtBottom = false @@ -511,8 +551,10 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible let nodeLayout = nodeValue.asyncLayout() + let isRotated = self.controllerInteraction.chatIsRotated + async { - let (top, bottom, dateAtBottom) = self.mergedWithItems(top: previousItem, bottom: nextItem) + let (top, bottom, dateAtBottom) = self.mergedWithItems(top: previousItem, bottom: nextItem, isRotated: isRotated) var disableDate = self.disableDate if let subject = self.associatedData.subject, case let .messageOptions(_, _, info) = subject { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift index 0ba0e07ddf7..c4356309f2e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift @@ -657,13 +657,11 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol { public var wantTrButton: [(Bool, [String])] = [(false, [])] // - public required convenience init() { - self.init(layerBacked: false) - } - - public init(layerBacked: Bool) { - super.init(layerBacked: layerBacked, dynamicBounce: true, rotated: true) - self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + public required init(rotated: Bool) { + super.init(layerBacked: false, dynamicBounce: true, rotated: rotated) + if rotated { + self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + } } required public init?(coder aDecoder: NSCoder) { @@ -688,7 +686,7 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol { override open func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = item as? ChatMessageItem { let doLayout = self.asyncLayout() - let merged = item.mergedWithItems(top: previousItem, bottom: nextItem) + let merged = item.mergedWithItems(top: previousItem, bottom: nextItem, isRotated: item.controllerInteraction.chatIsRotated) let (layout, apply) = doLayout(item, params, merged.top, merged.bottom, merged.dateAtBottom) self.contentSize = layout.contentSize self.insets = layout.insets diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/BUILD new file mode 100644 index 00000000000..0b956ee90fe --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/BUILD @@ -0,0 +1,38 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageNotificationItem", + module_name = "ChatMessageNotificationItem", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/AvatarNode", + "//submodules/AccountContext", + "//submodules/LocalizedPeerData", + "//submodules/StickerResources", + "//submodules/PhotoResources", + "//submodules/TelegramStringFormatting", + "//submodules/TextFormat", + "//submodules/InvisibleInkDustNode", + "//submodules/TelegramUI/Components/TextNodeWithEntities", + "//submodules/TelegramUI/Components/AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/ChatCallNotificationItem.swift b/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/ChatCallNotificationItem.swift new file mode 100644 index 00000000000..087a18ddaa2 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/ChatCallNotificationItem.swift @@ -0,0 +1,233 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import SwiftSignalKit +import TelegramPresentationData +import TelegramUIPreferences +import AvatarNode +import AccountContext +import LocalizedPeerData +import StickerResources +import PhotoResources +import TelegramStringFormatting +import TextFormat +import InvisibleInkDustNode +import TextNodeWithEntities +import AnimationCache +import MultiAnimationRenderer +import ComponentFlow +import MultilineTextComponent +import BundleIconComponent +import PlainButtonComponent + +public final class ChatCallNotificationItem: NotificationItem { + public let context: AccountContext + public let strings: PresentationStrings + public let nameDisplayOrder: PresentationPersonNameOrder + public let peer: EnginePeer + public let isVideo: Bool + public let action: (Bool) -> Void + + public var groupingKey: AnyHashable? { + return nil + } + + public init(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, peer: EnginePeer, isVideo: Bool, action: @escaping (Bool) -> Void) { + self.context = context + self.strings = strings + self.nameDisplayOrder = nameDisplayOrder + self.peer = peer + self.isVideo = isVideo + self.action = action + } + + public func node(compact: Bool) -> NotificationItemNode { + let node = ChatCallNotificationItemNode() + node.setupItem(self, compact: compact) + return node + } + + public func tapped(_ take: @escaping () -> (ASDisplayNode?, () -> Void)) { + } + + public func canBeExpanded() -> Bool { + return false + } + + public func expand(_ take: @escaping () -> (ASDisplayNode?, () -> Void)) { + } +} + +private let compactAvatarFont = avatarPlaceholderFont(size: 20.0) +private let avatarFont = avatarPlaceholderFont(size: 24.0) + +final class ChatCallNotificationItemNode: NotificationItemNode { + private var item: ChatCallNotificationItem? + + private let avatarNode: AvatarNode + private let title = ComponentView() + private let text = ComponentView() + private let answerButton = ComponentView() + private let declineButton = ComponentView() + + private var compact: Bool? + private var validLayout: CGFloat? + + override init() { + self.avatarNode = AvatarNode(font: avatarFont) + + super.init() + + self.acceptsTouches = true + + self.addSubnode(self.avatarNode) + } + + func setupItem(_ item: ChatCallNotificationItem, compact: Bool) { + self.item = item + + self.compact = compact + if compact { + self.avatarNode.font = compactAvatarFont + } + let presentationData = item.context.sharedContext.currentPresentationData.with { $0 } + + self.avatarNode.setPeer(context: item.context, theme: presentationData.theme, peer: item.peer, overrideImage: nil, emptyColor: presentationData.theme.list.mediaPlaceholderColor) + + if let width = self.validLayout { + let _ = self.updateLayout(width: width, transition: .immediate) + } + } + + override public func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + self.validLayout = width + + let panelHeight: CGFloat = 66.0 + + guard let item = self.item else { + return panelHeight + } + + let presentationData = item.context.sharedContext.currentPresentationData.with { $0 } + + let leftInset: CGFloat = 14.0 + let rightInset: CGFloat = 14.0 + let avatarSize: CGFloat = 38.0 + let avatarTextSpacing: CGFloat = 10.0 + let buttonSpacing: CGFloat = 14.0 + let titleTextSpacing: CGFloat = 0.0 + + let maxTextWidth: CGFloat = width - leftInset - avatarTextSpacing - rightInset - avatarSize * 2.0 - buttonSpacing - avatarTextSpacing + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: item.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.semibold(16.0), textColor: presentationData.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: maxTextWidth, height: 100.0) + ) + + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: item.isVideo ? presentationData.strings.Notification_VideoCallIncoming : presentationData.strings.Notification_CallIncoming, font: Font.regular(13.0), textColor: presentationData.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: maxTextWidth, height: 100.0) + ) + + let titleTextHeight = titleSize.height + titleTextSpacing + textSize.height + let titleTextY = floor((panelHeight - titleTextHeight) * 0.5) + let titleFrame = CGRect(origin: CGPoint(x: leftInset + avatarSize + avatarTextSpacing, y: titleTextY), size: titleSize) + let textFrame = CGRect(origin: CGPoint(x: leftInset + avatarSize + avatarTextSpacing, y: titleTextY + titleSize.height + titleTextSpacing), size: textSize) + + if let titleView = self.title.view { + if titleView.superview == nil { + self.view.addSubview(titleView) + } + titleView.frame = titleFrame + } + + if let textView = self.text.view { + if textView.superview == nil { + self.view.addSubview(textView) + } + textView.frame = textFrame + } + + transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: leftInset, y: (panelHeight - avatarSize) / 2.0), size: CGSize(width: avatarSize, height: avatarSize))) + + let answerButtonSize = self.answerButton.update( + transition: .immediate, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(ZStack([ + AnyComponentWithIdentity(id: 1, component: AnyComponent(Circle( + fillColor: UIColor(rgb: 0x34C759), + size: CGSize(width: avatarSize, height: avatarSize) + ))), + AnyComponentWithIdentity(id: 2, component: AnyComponent(BundleIconComponent( + name: "Call/CallNotificationAnswerIcon", + tintColor: .white + ))) + ])), + effectAlignment: .center, + minSize: CGSize(width: avatarSize, height: avatarSize), + action: { [weak self] in + guard let self, let item = self.item else { + return + } + item.action(true) + } + )), + environment: {}, + containerSize: CGSize(width: avatarSize, height: avatarSize) + ) + let declineButtonSize = self.declineButton.update( + transition: .immediate, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(ZStack([ + AnyComponentWithIdentity(id: 1, component: AnyComponent(Circle( + fillColor: UIColor(rgb: 0xFF3B30), + size: CGSize(width: avatarSize, height: avatarSize) + ))), + AnyComponentWithIdentity(id: 2, component: AnyComponent(BundleIconComponent( + name: "Call/CallNotificationDeclineIcon", + tintColor: .white + ))) + ])), + effectAlignment: .center, + minSize: CGSize(width: avatarSize, height: avatarSize), + action: { [weak self] in + guard let self, let item = self.item else { + return + } + item.action(false) + } + )), + environment: {}, + containerSize: CGSize(width: avatarSize, height: avatarSize) + ) + + let declineButtonFrame = CGRect(origin: CGPoint(x: width - rightInset - avatarSize - buttonSpacing - declineButtonSize.width, y: floor((panelHeight - declineButtonSize.height) * 0.5)), size: declineButtonSize) + if let declineButtonView = self.declineButton.view { + if declineButtonView.superview == nil { + self.view.addSubview(declineButtonView) + } + declineButtonView.frame = declineButtonFrame + } + + let answerButtonFrame = CGRect(origin: CGPoint(x: declineButtonFrame.maxX + buttonSpacing, y: floor((panelHeight - answerButtonSize.height) * 0.5)), size: answerButtonSize) + if let answerButtonView = self.answerButton.view { + if answerButtonView.superview == nil { + self.view.addSubview(answerButtonView) + } + answerButtonView.frame = answerButtonFrame + } + + return panelHeight + } +} diff --git a/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift b/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/ChatMessageNotificationItem.swift similarity index 97% rename from submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/ChatMessageNotificationItem.swift index 6b8b5f050eb..1d59686554f 100644 --- a/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/ChatMessageNotificationItem.swift @@ -20,14 +20,14 @@ import AnimationCache import MultiAnimationRenderer public final class ChatMessageNotificationItem: NotificationItem { - let context: AccountContext - let strings: PresentationStrings - let dateTimeFormat: PresentationDateTimeFormat - let nameDisplayOrder: PresentationPersonNameOrder - let messages: [Message] - let threadData: MessageHistoryThreadData? - let tapAction: () -> Bool - let expandAction: (@escaping () -> (ASDisplayNode?, () -> Void)) -> Void + public let context: AccountContext + public let strings: PresentationStrings + public let dateTimeFormat: PresentationDateTimeFormat + public let nameDisplayOrder: PresentationPersonNameOrder + public let messages: [Message] + public let threadData: MessageHistoryThreadData? + public let tapAction: () -> Bool + public let expandAction: (@escaping () -> (ASDisplayNode?, () -> Void)) -> Void public var groupingKey: AnyHashable? { return messages.first?.id.peerId @@ -380,7 +380,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { } } - override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + override public func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { self.validLayout = width let compact = self.compact ?? false diff --git a/submodules/TelegramUI/Sources/NotificationItem.swift b/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/NotificationItem.swift similarity index 75% rename from submodules/TelegramUI/Sources/NotificationItem.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/NotificationItem.swift index 397c7f4faa9..03f0542f3c0 100644 --- a/submodules/TelegramUI/Sources/NotificationItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/NotificationItem.swift @@ -13,7 +13,9 @@ public protocol NotificationItem { } public class NotificationItemNode: ASDisplayNode { - func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + public func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { return 32.0 } + + public var acceptsTouches: Bool = false } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift index 6991eff691c..2a69aa8db64 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift @@ -203,7 +203,15 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { var dashSecondaryColor: UIColor? var dashTertiaryColor: UIColor? - let author = arguments.message?.effectiveAuthor + var author = arguments.message?.effectiveAuthor + + if let forwardInfo = arguments.message?.forwardInfo { + if let peer = forwardInfo.author { + author = peer + } else if let authorSignature = forwardInfo.authorSignature { + author = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) + } + } let colors = author?.nameColor.flatMap { arguments.context.peerNameColors.get($0, dark: arguments.presentationData.theme.theme.overallDarkAppearance) } authorNameColor = colors?.main diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index d279d61542b..f9d6593249f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -95,7 +95,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { } } - required public init() { + required public init(rotated: Bool) { self.contextSourceNode = ContextExtractedContentContainingNode() self.containerNode = ContextControllerSourceNode() self.imageNode = TransformImageNode() @@ -104,7 +104,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { self.dateAndStatusNode = ChatMessageDateAndStatusNode() self.messageAccessibilityArea = AccessibilityAreaNode() - super.init(layerBacked: false) + super.init(rotated: rotated) var firstTime = true self.imageNode.imageUpdated = { [weak self] image in @@ -253,7 +253,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { return false } - if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.isChannelPost, replyThreadMessage.messageId.peerId != item.content.firstMessage.id.peerId { + if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.isChannelPost, replyThreadMessage.peerId != item.content.firstMessage.id.peerId { return false } @@ -466,8 +466,8 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { hasAvatar = true } case let .replyThread(replyThreadMessage): - if replyThreadMessage.messageId.peerId != item.context.account.peerId { - if replyThreadMessage.messageId.peerId.isGroupOrChannel && item.message.author != nil { + if replyThreadMessage.peerId != item.context.account.peerId { + if replyThreadMessage.peerId.isGroupOrChannel && item.message.author != nil { var isBroadcastChannel = false if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { isBroadcastChannel = true @@ -689,7 +689,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { if let replyAttribute = attribute as? ReplyMessageAttribute { - if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == replyAttribute.messageId { + if case let .replyThread(replyThreadMessage) = item.chatLocation, Int32(clamping: replyThreadMessage.threadId) == replyAttribute.messageId.id { } else { replyMessage = item.message.associatedMessages[replyAttribute.messageId] } diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift index bbe2be8fd04..bd1b7388399 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift @@ -105,7 +105,7 @@ public final class ChatRecentActionsController: TelegramBaseController { }, stopMediaRecording: { }, lockMediaRecording: { }, deleteRecordedMedia: { - }, sendRecordedMedia: { _ in + }, sendRecordedMedia: { _, _ in }, displayRestrictedInfo: { _, _ in }, displayVideoUnmuteTip: { _ in }, switchMediaRecordingMode: { diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index ed49c75dfb9..7e6df9da573 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -1090,6 +1090,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { break case .premiumGiftCode: break + case .premiumMultiGift: + break } } })) diff --git a/submodules/TelegramUI/Components/Chat/InstantVideoRadialStatusNode/Sources/InstantVideoRadialStatusNode.swift b/submodules/TelegramUI/Components/Chat/InstantVideoRadialStatusNode/Sources/InstantVideoRadialStatusNode.swift index 2d8a84006c6..bd7e982c5de 100644 --- a/submodules/TelegramUI/Components/Chat/InstantVideoRadialStatusNode/Sources/InstantVideoRadialStatusNode.swift +++ b/submodules/TelegramUI/Components/Chat/InstantVideoRadialStatusNode/Sources/InstantVideoRadialStatusNode.swift @@ -7,6 +7,24 @@ import UniversalMediaPlayer import LegacyComponents import UIKitRuntimeUtils +private struct ContentParticle { + var position: CGPoint + var direction: CGPoint + var velocity: CGFloat + var alpha: CGFloat + var lifetime: Double + var beginTime: Double + + init(position: CGPoint, direction: CGPoint, velocity: CGFloat, alpha: CGFloat, lifetime: Double, beginTime: Double) { + self.position = position + self.direction = direction + self.velocity = velocity + self.alpha = alpha + self.lifetime = lifetime + self.beginTime = beginTime + } +} + private final class InstantVideoRadialStatusNodeParameters: NSObject { let color: UIColor let progress: CGFloat @@ -14,14 +32,18 @@ private final class InstantVideoRadialStatusNodeParameters: NSObject { let playProgress: CGFloat let blinkProgress: CGFloat let hasSeek: Bool + let sparks: Bool + let particles: [ContentParticle] - init(color: UIColor, progress: CGFloat, dimProgress: CGFloat, playProgress: CGFloat, blinkProgress: CGFloat, hasSeek: Bool) { + init(color: UIColor, progress: CGFloat, dimProgress: CGFloat, playProgress: CGFloat, blinkProgress: CGFloat, hasSeek: Bool, sparks: Bool, particles: [ContentParticle]) { self.color = color self.progress = progress self.dimProgress = dimProgress self.playProgress = playProgress self.blinkProgress = blinkProgress self.hasSeek = hasSeek + self.sparks = sparks + self.particles = particles } } @@ -43,8 +65,12 @@ private extension CGPoint { public final class InstantVideoRadialStatusNode: ASDisplayNode, UIGestureRecognizerDelegate { private let color: UIColor private let hasSeek: Bool - private let hapticFeedback = HapticFeedback() + private let sparks: Bool + private let hapticFeedback = HapticFeedback() + + private var particles: [ContentParticle] = [] + private var effectiveProgress: CGFloat = 0.0 { didSet { self.setNeedsDisplay() @@ -85,6 +111,8 @@ public final class InstantVideoRadialStatusNode: ASDisplayNode, UIGestureRecogni } } + private var animator: ConstantDisplayLinkAnimator? + private var statusDisposable: Disposable? private var statusValuePromise = Promise() @@ -111,9 +139,10 @@ public final class InstantVideoRadialStatusNode: ASDisplayNode, UIGestureRecogni public var seekTo: ((Double, Bool) -> Void)? - public init(color: UIColor, hasSeek: Bool) { + public init(color: UIColor, hasSeek: Bool, sparks: Bool = false) { self.color = color self.hasSeek = hasSeek + self.sparks = sparks super.init() @@ -127,6 +156,13 @@ public final class InstantVideoRadialStatusNode: ASDisplayNode, UIGestureRecogni }) self.view.disablesInteractiveTransitionGestureRecognizer = true + + if sparks { + self.animator = ConstantDisplayLinkAnimator(update: { [weak self] in + self?.updateSparks() + }) + self.animator?.isPaused = false + } } deinit { @@ -165,6 +201,60 @@ public final class InstantVideoRadialStatusNode: ASDisplayNode, UIGestureRecogni } } + private func updateSparks() { +// let bounds = self.bounds + +// let lineWidth: CGFloat = 4.0 +// let center = CGPoint(x: bounds.midX, y: bounds.midY) +// let radius: CGFloat = (bounds.size.width - lineWidth - 4.0 * 2.0) * 0.5 + + let endAngle: CGFloat = -CGFloat.pi / 2.0 + 2.0 * CGFloat.pi * self.effectiveProgress + + let v = CGPoint(x: sin(endAngle), y: -cos(endAngle)) +// let c = CGPoint(x: -v.y * radius + center.x, y: v.x * radius + center.y) + + let timestamp = CACurrentMediaTime() + + let dt: CGFloat = 1.0 / 60.0 + var removeIndices: [Int] = [] + for i in 0 ..< self.particles.count { + let currentTime = timestamp - self.particles[i].beginTime + if currentTime > self.particles[i].lifetime { + removeIndices.append(i) + } else { + let input: CGFloat = CGFloat(currentTime / self.particles[i].lifetime) + let decelerated: CGFloat = (1.0 - (1.0 - input) * (1.0 - input)) + self.particles[i].alpha = 1.0 - decelerated + + var p = self.particles[i].position + let d = self.particles[i].direction + let v = self.particles[i].velocity + p = CGPoint(x: p.x + d.x * v * dt, y: p.y + d.y * v * dt) + self.particles[i].position = p + } + } + + for i in removeIndices.reversed() { + self.particles.remove(at: i) + } + + let newParticleCount = 1 + for _ in 0 ..< newParticleCount { + let degrees: CGFloat = CGFloat(arc4random_uniform(140)) - 70.0 + let angle: CGFloat = degrees * CGFloat.pi / 180.0 + + let direction = CGPoint(x: v.x * cos(angle) - v.y * sin(angle), y: v.x * sin(angle) + v.y * cos(angle)) + let velocity = (25.0 + (CGFloat(arc4random()) / CGFloat(UINT32_MAX)) * 4.0) * 0.5 + + let lifetime = Double(0.25 + CGFloat(arc4random_uniform(100)) * 0.01) + + let particle = ContentParticle(position: .zero, direction: direction, velocity: velocity, alpha: 0.8, lifetime: lifetime, beginTime: timestamp) + self.particles.append(particle) + } + + self.setNeedsDisplay() + } + @objc private func tapGesture(_ gestureRecognizer: UITapGestureRecognizer) { let center = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0) let location = gestureRecognizer.location(in: self.view) @@ -259,7 +349,7 @@ public final class InstantVideoRadialStatusNode: ASDisplayNode, UIGestureRecogni } override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { - return InstantVideoRadialStatusNodeParameters(color: self.color, progress: self.effectiveProgress, dimProgress: self.effectiveDimProgress, playProgress: self.effectivePlayProgress, blinkProgress: self.effectiveBlinkProgress, hasSeek: self.hasSeek) + return InstantVideoRadialStatusNodeParameters(color: self.color, progress: self.effectiveProgress, dimProgress: self.effectiveDimProgress, playProgress: self.effectivePlayProgress, blinkProgress: self.effectiveBlinkProgress, hasSeek: self.hasSeek, sparks: self.sparks, particles: self.particles) } @objc public override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { @@ -296,8 +386,15 @@ public final class InstantVideoRadialStatusNode: ASDisplayNode, UIGestureRecogni context.setBlendMode(.normal) var progress = parameters.progress - let startAngle = -CGFloat.pi / 2.0 - let endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle + let startAngle: CGFloat + let endAngle: CGFloat + if parameters.sparks { + endAngle = -CGFloat.pi / 2.0 + startAngle = CGFloat(progress) * 2.0 * CGFloat.pi + endAngle + } else { + startAngle = -CGFloat.pi / 2.0 + endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle + } progress = min(1.0, progress) @@ -363,6 +460,16 @@ public final class InstantVideoRadialStatusNode: ASDisplayNode, UIGestureRecogni context.setFillColor(UIColor.white.cgColor) context.fillEllipse(in: handleFrame) } + + let v = CGPoint(x: sin(startAngle), y: -cos(startAngle)) + let c = CGPoint(x: -v.y * pathDiameter * 0.5 + bounds.midX, y: v.x * pathDiameter * 0.5 + bounds.midY) + + context.setFillColor(parameters.color.cgColor) + for particle in parameters.particles { + let size: CGFloat = 1.3 + context.setAlpha(particle.alpha) + context.fillEllipse(in: CGRect(origin: CGPoint(x: c.x + particle.position.x - size / 2.0, y: c.y + particle.position.y - size / 2.0), size: CGSize(width: size, height: size))) + } } } diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index b8c212f020c..f8b682d32d1 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -263,6 +263,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public var playNextOutgoingGift: Bool = false public var recommendedChannelsOpenUp: Bool = false public var enableFullTranslucency: Bool = true + public var chatIsRotated: Bool = true public init( // MARK: Nicegram Translate diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index ccead170727..d5b1abb2d4c 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -2440,7 +2440,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, - mode: .standard(previewing: false), + mode: .standard(.default), chatLocation: .peer(id: self.context.account.peerId), subject: nil, peerNearbyData: nil, diff --git a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift index d2f3799ef66..26a2ac8bc4d 100644 --- a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift @@ -247,6 +247,8 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { segments = [.text(0, NSAttributedString(string: customTitle, font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))] } else if peerView.peerId == self.context.account.peerId { segments = [.text(0, NSAttributedString(string: self.strings.Conversation_SavedMessages, font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))] + } else if peerView.peerId.isAnonymousSavedMessages { + segments = [.text(0, NSAttributedString(string: self.strings.ChatList_AuthorHidden, font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))] } else { if !peerView.isContact, let user = peer as? TelegramUser, !user.flags.contains(.isSupport), user.botInfo == nil, let phone = user.phone, !phone.isEmpty { segments = [.text(0, NSAttributedString(string: formatPhoneNumber(context: self.context, number: phone), font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))] diff --git a/submodules/TelegramUI/Components/DustEffect/Sources/DustEffectLayer.swift b/submodules/TelegramUI/Components/DustEffect/Sources/DustEffectLayer.swift index fec29202b3e..1dc7209e00d 100644 --- a/submodules/TelegramUI/Components/DustEffect/Sources/DustEffectLayer.swift +++ b/submodules/TelegramUI/Components/DustEffect/Sources/DustEffectLayer.swift @@ -4,6 +4,40 @@ import Display import MetalEngine import MetalKit +#if DEBUG +import os +#endif + +#if DEBUG +class Signposter { + func emitEvent(_ string: StaticString) { + } +} + +@available(iOS 15.0, *) +final class SignposterImpl: Signposter { + private let signposter = OSSignposter() + private let signpostId: OSSignpostID + + override init() { + self.signpostId = self.signposter.makeSignpostID() + } + + override func emitEvent(_ string: StaticString) { + self.signposter.emitEvent("Fetch complete.", id: self.signpostId) + } +} + +let signposter: Signposter = { + if #available(iOS 15.0, *) { + return SignposterImpl() + } else { + return Signposter() + } +}() + +#endif + private final class BundleMarker: NSObject { } @@ -230,6 +264,13 @@ public final class DustEffectLayer: MetalEngineSubjectLayer, MetalEngineSubject let lastTimeStep = self.lastTimeStep self.lastTimeStep = 0.0 + #if DEBUG + if lastTimeStep * 1000.0 >= 20.0 { + print("Animation Lag: \(lastTimeStep * 1000.0) ms") + signposter.emitEvent("AnimationLag") + } + #endif + let _ = context.compute(state: DustComputeState.self, commands: { [weak self] commandBuffer, state in guard let self else { return diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index ffd8d3fb038..28589ba77a4 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -7251,9 +7251,10 @@ public final class EmojiPagerContentComponent: Component { } |> take(1) } else if case .channelStatus = subject { - orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudFeaturedStatusEmoji) + orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudFeaturedChannelStatusEmoji) + orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudDisabledChannelStatusEmoji) - iconStatusEmoji = context.engine.stickers.loadedStickerPack(reference: .iconStatusEmoji, forceActualized: false) + iconStatusEmoji = context.engine.stickers.loadedStickerPack(reference: .iconChannelStatusEmoji, forceActualized: false) |> map { result -> [TelegramMediaFile] in switch result { case let .result(_, items, _): @@ -7298,7 +7299,7 @@ public final class EmojiPagerContentComponent: Component { } else if case .status = subject { searchCategories = context.engine.stickers.emojiSearchCategories(kind: .status) } else if case .channelStatus = subject { - searchCategories = context.engine.stickers.emojiSearchCategories(kind: .status) + searchCategories = .single(nil) } else if [.profilePhoto, .groupPhoto].contains(subject) { searchCategories = context.engine.stickers.emojiSearchCategories(kind: .avatar) } else { @@ -7436,6 +7437,8 @@ public final class EmojiPagerContentComponent: Component { var recentEmoji: OrderedItemListView? var featuredStatusEmoji: OrderedItemListView? + var featuredChannelStatusEmoji: OrderedItemListView? + var disabledChannelStatusEmoji: OrderedItemListView? var recentStatusEmoji: OrderedItemListView? var topReactions: OrderedItemListView? var recentReactions: OrderedItemListView? @@ -7446,6 +7449,10 @@ public final class EmojiPagerContentComponent: Component { recentEmoji = orderedView } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudFeaturedStatusEmoji { featuredStatusEmoji = orderedView + } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudFeaturedChannelStatusEmoji { + featuredChannelStatusEmoji = orderedView + } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudDisabledChannelStatusEmoji { + disabledChannelStatusEmoji = orderedView } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudRecentStatusEmoji { recentStatusEmoji = orderedView } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudRecentReactions { @@ -7700,7 +7707,17 @@ public final class EmojiPagerContentComponent: Component { var existingIds = Set() - for file in iconStatusEmoji.prefix(7) { + if let disabledChannelStatusEmoji { + for item in disabledChannelStatusEmoji.items { + guard let item = item.contents.get(RecentMediaItem.self) else { + continue + } + let file = item.media + existingIds.insert(file.fileId) + } + } + + for file in iconStatusEmoji { if existingIds.contains(file.fileId) { continue } @@ -7740,58 +7757,8 @@ public final class EmojiPagerContentComponent: Component { } } - if let recentStatusEmoji = recentStatusEmoji { - for item in recentStatusEmoji.items { - guard let item = item.contents.get(RecentMediaItem.self) else { - continue - } - - let file = item.media - if existingIds.contains(file.fileId) { - continue - } - existingIds.insert(file.fileId) - - var tintMode: Item.TintMode = .none - if file.isCustomTemplateEmoji { - tintMode = .accent - } - for attribute in file.attributes { - if case let .CustomEmoji(_, _, _, packReference) = attribute { - switch packReference { - case let .id(id, _): - if id == 773947703670341676 || id == 2964141614563343 { - tintMode = .accent - } - default: - break - } - } - } - - let resultItem: EmojiPagerContentComponent.Item - - let animationData = EntityKeyboardAnimationData(file: file) - resultItem = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: file, - subgroupId: nil, - icon: .none, - tintMode: tintMode - ) - - if let groupIndex = itemGroupIndexById[groupId] { - if itemGroups[groupIndex].items.count >= (5 + 8) * 8 { - break - } - - itemGroups[groupIndex].items.append(resultItem) - } - } - } - if let featuredStatusEmoji = featuredStatusEmoji { - for item in featuredStatusEmoji.items { + if let featuredChannelStatusEmoji { + for item in featuredChannelStatusEmoji.items { guard let item = item.contents.get(RecentMediaItem.self) else { continue } @@ -7832,9 +7799,9 @@ public final class EmojiPagerContentComponent: Component { ) if let groupIndex = itemGroupIndexById[groupId] { - if itemGroups[groupIndex].items.count >= (5 + 8) * 8 { + /*if itemGroups[groupIndex].items.count >= (5 + 8) * 8 { break - } + }*/ itemGroups[groupIndex].items.append(resultItem) } @@ -8296,6 +8263,13 @@ public final class EmojiPagerContentComponent: Component { if !hasPremium { maybeAppendUnicodeEmoji() } + + var itemCollectionMapping: [ItemCollectionId: StickerPackCollectionInfo] = [:] + for (id, info, _) in view.collectionInfos { + if let info = info as? StickerPackCollectionInfo { + itemCollectionMapping[id] = info + } + } var skippedCollectionIds = Set() if areCustomEmojiEnabled { @@ -8316,6 +8290,15 @@ public final class EmojiPagerContentComponent: Component { continue } + if case .channelStatus = subject { + guard let collection = itemCollectionMapping[entry.index.collectionId] else { + continue + } + if !collection.flags.contains(.isAvailableAsChannelStatus) { + continue + } + } + var isTemplate = false var tintMode: Item.TintMode = .none if item.file.isCustomTemplateEmoji { @@ -8402,6 +8385,12 @@ public final class EmojiPagerContentComponent: Component { continue } + if case .channelStatus = subject { + if !featuredEmojiPack.info.flags.contains(.isAvailableAsChannelStatus) { + continue + } + } + for item in featuredEmojiPack.topItems { var tintMode: Item.TintMode = .none if item.file.isCustomTemplateEmoji { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift index 72910fd8db8..aeef94f3fa0 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift @@ -134,7 +134,7 @@ public enum CodableDrawingEntity: Equatable { reaction: reaction, flags: flags ) - } else if case let .message(messageIds, _, _) = entity.content, let messageId = messageIds.first { + } else if case let .message(messageIds, _, _, _, _) = entity.content, let messageId = messageIds.first { return .channelMessage(coordinates: coordinates, messageId: messageId) } else { return nil diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift index 3f7b805d3b1..3081218bc0a 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift @@ -33,7 +33,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { case animatedImage(Data, UIImage) case video(TelegramMediaFile) case dualVideoReference(Bool) - case message([MessageId], TelegramMediaFile?, CGSize) + case message([MessageId], CGSize, TelegramMediaFile?, CGRect?, CGFloat?) public static func == (lhs: Content, rhs: Content) -> Bool { switch lhs { @@ -67,9 +67,9 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { } else { return false } - case let .message(messageIds, innerFile, size): - if case .message(messageIds, innerFile, size) = rhs { - return true + case let .message(lhsMessageIds, lhsSize, lhsFile, lhsMediaFrame, lhsCornerRadius): + if case let .message(rhsMessageIds, rhsSize, rhsFile, rhsMediaFrame, rhsCornerRadius) = rhs { + return lhsMessageIds == rhsMessageIds && lhsSize == rhsSize && lhsFile?.fileId == rhsFile?.fileId && lhsMediaFrame == rhsMediaFrame && lhsCornerRadius == rhsCornerRadius } else { return false } @@ -89,14 +89,19 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { case dualVideo case isAdditionalVideo case messageIds - case explicitSize + case messageFile + case messageSize + case messageMediaRect + case messageMediaCornerRadius case referenceDrawingSize case position case scale case rotation case mirrored case isExplicitlyStatic + case canCutOut case renderImage + case renderSubEntities } public var uuid: UUID @@ -116,12 +121,15 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { public var rotation: CGFloat public var mirrored: Bool + public var canCutOut = false + public var isExplicitlyStatic: Bool public var color: DrawingColor = DrawingColor.clear public var lineWidth: CGFloat = 0.0 public var secondaryRenderImage: UIImage? + public var overlayRenderImage: UIImage? public var center: CGPoint { return self.position @@ -146,7 +154,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { dimensions = file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0) case .dualVideoReference: dimensions = CGSize(width: 512.0, height: 512.0) - case let .message(_, _, size): + case let .message(_, size, _, _, _): dimensions = size } @@ -217,8 +225,11 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { let container = try decoder.container(keyedBy: CodingKeys.self) self.uuid = try container.decode(UUID.self, forKey: .uuid) if let messageIds = try container.decodeIfPresent([MessageId].self, forKey: .messageIds) { - let size = try container.decodeIfPresent(CGSize.self, forKey: .explicitSize) ?? .zero - self.content = .message(messageIds, nil, size) + let size = try container.decodeIfPresent(CGSize.self, forKey: .messageSize) ?? .zero + let file = try container.decodeIfPresent(TelegramMediaFile.self, forKey: .messageFile) + let mediaRect = try container.decodeIfPresent(CGRect.self, forKey: .messageMediaRect) + let mediaCornerRadius = try container.decodeIfPresent(CGFloat.self, forKey: .messageMediaCornerRadius) + self.content = .message(messageIds, size, file, mediaRect, mediaCornerRadius) } else if let _ = try container.decodeIfPresent(Bool.self, forKey: .dualVideo) { let isAdditional = try container.decodeIfPresent(Bool.self, forKey: .isAdditionalVideo) ?? false self.content = .dualVideoReference(isAdditional) @@ -260,9 +271,14 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { self.mirrored = try container.decode(Bool.self, forKey: .mirrored) self.isExplicitlyStatic = try container.decodeIfPresent(Bool.self, forKey: .isExplicitlyStatic) ?? false + self.canCutOut = try container.decodeIfPresent(Bool.self, forKey: .canCutOut) ?? false + if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .renderImage) { self.renderImage = UIImage(data: renderImageData) } + if let renderSubEntities = try? container.decodeIfPresent([CodableDrawingEntity].self, forKey: .renderSubEntities) { + self.renderSubEntities = renderSubEntities.compactMap { $0.entity as? DrawingStickerEntity } + } } public func encode(to encoder: Encoder) throws { @@ -313,10 +329,12 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { case let .dualVideoReference(isAdditional): try container.encode(true, forKey: .dualVideo) try container.encode(isAdditional, forKey: .isAdditionalVideo) - case let .message(messageIds, innerFile, size): + case let .message(messageIds, size, file, mediaRect, mediaCornerRadius): try container.encode(messageIds, forKey: .messageIds) - let _ = innerFile - try container.encode(size, forKey: .explicitSize) + try container.encode(size, forKey: .messageSize) + try container.encodeIfPresent(file, forKey: .messageFile) + try container.encodeIfPresent(mediaRect, forKey: .messageMediaRect) + try container.encodeIfPresent(mediaCornerRadius, forKey: .messageMediaCornerRadius) } try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) try container.encode(self.position, forKey: .position) @@ -325,9 +343,15 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { try container.encode(self.mirrored, forKey: .mirrored) try container.encode(self.isExplicitlyStatic, forKey: .isExplicitlyStatic) + try container.encode(self.canCutOut, forKey: .canCutOut) + if let renderImage, let data = renderImage.pngData() { try container.encode(data, forKey: .renderImage) } + if let renderSubEntities = self.renderSubEntities { + let codableEntities: [CodableDrawingEntity] = renderSubEntities.compactMap { CodableDrawingEntity(entity: $0) } + try container.encode(codableEntities, forKey: .renderSubEntities) + } } public func duplicate(copy: Bool) -> DrawingEntity { @@ -341,6 +365,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { newEntity.rotation = self.rotation newEntity.mirrored = self.mirrored newEntity.isExplicitlyStatic = self.isExplicitlyStatic + newEntity.canCutOut = self.canCutOut return newEntity } @@ -372,6 +397,9 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { if self.isExplicitlyStatic != other.isExplicitlyStatic { return false } + if self.canCutOut != other.canCutOut { + return false + } return true } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift index c39d2d363c3..cd0ba03359b 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift @@ -85,15 +85,17 @@ public final class DrawingMessageRenderer { private let context: AccountContext private let messages: [Message] private let isNight: Bool + private let isOverlay: Bool private let messagesContainerNode: ASDisplayNode private var avatarHeaderNode: ListViewItemHeaderNode? private var messageNodes: [ListViewItemNode]? - init(context: AccountContext, messages: [Message], isNight: Bool = false) { + init(context: AccountContext, messages: [Message], isNight: Bool = false, isOverlay: Bool = false) { self.context = context self.messages = messages self.isNight = isNight + self.isOverlay = isOverlay self.messagesContainerNode = ASDisplayNode() self.messagesContainerNode.clipsToBounds = true @@ -104,25 +106,8 @@ public final class DrawingMessageRenderer { self.addSubnode(self.messagesContainerNode) } - public func render(completion: @escaping (CGSize, UIImage?) -> Void) { - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - let defaultPresentationData = defaultPresentationData() - - var mockPresentationData = PresentationData( - strings: presentationData.strings, - theme: defaultPresentationTheme, - autoNightModeTriggered: false, - chatWallpaper: presentationData.chatWallpaper, - chatFontSize: defaultPresentationData.chatFontSize, - chatBubbleCorners: defaultPresentationData.chatBubbleCorners, - listsFontSize: defaultPresentationData.listsFontSize, - dateTimeFormat: presentationData.dateTimeFormat, - nameDisplayOrder: presentationData.nameDisplayOrder, - nameSortOrder: presentationData.nameSortOrder, - reduceMotion: false, - largeEmoji: true - ) - + public func render(presentationData: PresentationData, completion: @escaping (CGSize, UIImage?, CGRect?) -> Void) { + var mockPresentationData = presentationData if self.isNight { let darkTheme = defaultDarkColorPresentationTheme mockPresentationData = mockPresentationData.withUpdated(theme: darkTheme).withUpdated(chatWallpaper: darkTheme.chat.defaultWallpaper) @@ -132,8 +117,48 @@ public final class DrawingMessageRenderer { let size = self.updateMessagesLayout(layout: layout, presentationData: mockPresentationData) Queue.mainQueue().after(0.05, { + var mediaRect: CGRect? + if let messageNode = self.messageNodes?.first { + if self.isOverlay { + func hideNonOverlayViews(_ view: UIView) -> Bool { + var hasResult = false + for view in view.subviews { + if view.tag == 0xFACE { + hasResult = true + } else { + if hideNonOverlayViews(view) { + hasResult = true + } else { + view.isHidden = true + } + } + } + return hasResult + } + let _ = hideNonOverlayViews(messageNode.view) + } else if !self.isNight { + func findMediaView(_ view: UIView) -> UIView? { + for view in view.subviews { + if let _ = view.asyncdisplaykit_node as? UniversalVideoNode { + return view + } else { + if let result = findMediaView(view) { + return result + } + } + } + return nil + } + + if let mediaView = findMediaView(messageNode.view) { + var rect = mediaView.convert(mediaView.bounds, to: self.messagesContainerNode.view) + rect.origin.y = self.messagesContainerNode.frame.height - rect.maxY + mediaRect = rect + } + } + } self.generate(size: size) { image in - completion(size, image) + completion(size, image, mediaRect) } }) } @@ -162,7 +187,7 @@ public final class DrawingMessageRenderer { let avatarHeaderItem = self.context.sharedContext.makeChatMessageAvatarHeaderItem(context: self.context, timestamp: self.messages.first?.timestamp ?? 0, peer: self.messages.first!.peers[self.messages.first!.author!.id]!, message: self.messages.first!, theme: theme, strings: presentationData.strings, wallpaper: presentationData.chatWallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: presentationData.chatBubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder) - let items: [ListViewItem] = [self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: self.messages, theme: theme, strings: presentationData.strings, wallpaper: presentationData.theme.chat.defaultWallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: presentationData.chatBubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: nil, availableReactions: nil, accountPeer: nil, isCentered: false)] + let items: [ListViewItem] = [self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: self.messages, theme: theme, strings: presentationData.strings, wallpaper: presentationData.theme.chat.defaultWallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: presentationData.chatBubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: nil, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true)] let inset: CGFloat = 16.0 let leftInset: CGFloat = 37.0 @@ -259,11 +284,25 @@ public final class DrawingMessageRenderer { } } + public struct Result { + public struct MediaFrame { + public let rect: CGRect + public let cornerRadius: CGFloat + } + + public let size: CGSize + public let dayImage: UIImage + public let nightImage: UIImage + public let overlayImage: UIImage + public let mediaFrame: MediaFrame? + } + private let context: AccountContext private let messages: [Message] private let dayContainerNode: ContainerNode private let nightContainerNode: ContainerNode + private let overlayContainerNode: ContainerNode public init(context: AccountContext, messages: [Message]) { self.context = context @@ -271,27 +310,58 @@ public final class DrawingMessageRenderer { self.dayContainerNode = ContainerNode(context: context, messages: messages) self.nightContainerNode = ContainerNode(context: context, messages: messages, isNight: true) + self.overlayContainerNode = ContainerNode(context: context, messages: messages, isOverlay: true) } - public func render(completion: @escaping (CGSize, UIImage?, UIImage?) -> Void) { + public func render(completion: @escaping (Result) -> Void) { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let defaultPresentationData = defaultPresentationData() + + let mockPresentationData = PresentationData( + strings: presentationData.strings, + theme: defaultPresentationTheme, + autoNightModeTriggered: false, + chatWallpaper: presentationData.chatWallpaper, + chatFontSize: defaultPresentationData.chatFontSize, + chatBubbleCorners: defaultPresentationData.chatBubbleCorners, + listsFontSize: defaultPresentationData.listsFontSize, + dateTimeFormat: presentationData.dateTimeFormat, + nameDisplayOrder: presentationData.nameDisplayOrder, + nameSortOrder: presentationData.nameSortOrder, + reduceMotion: false, + largeEmoji: true + ) + var finalSize: CGSize = .zero var dayImage: UIImage? var nightImage: UIImage? + var overlayImage: UIImage? + var mediaRect: CGRect? let completeIfReady = { - if let dayImage, let nightImage { - completion(finalSize, dayImage, nightImage) + if let dayImage, let nightImage, let overlayImage { + var cornerRadius: CGFloat = defaultPresentationData.chatBubbleCorners.mainRadius + if let mediaRect, mediaRect.width == mediaRect.height, mediaRect.width == 240.0 { + cornerRadius = mediaRect.width / 2.0 + } else if let rect = mediaRect { + mediaRect = CGRect(x: rect.minX + 4.0, y: rect.minY, width: rect.width - 6.0, height: rect.height - 1.0) + } + completion(Result(size: finalSize, dayImage: dayImage, nightImage: nightImage, overlayImage: overlayImage, mediaFrame: mediaRect.flatMap { Result.MediaFrame(rect: $0, cornerRadius: cornerRadius) })) } } - self.dayContainerNode.render { size, image in + self.dayContainerNode.render(presentationData: mockPresentationData) { size, image, rect in finalSize = size dayImage = image + mediaRect = rect completeIfReady() } - self.nightContainerNode.render { size, image in - finalSize = size + self.nightContainerNode.render(presentationData: mockPresentationData) { size, image, _ in nightImage = image completeIfReady() } + self.overlayContainerNode.render(presentationData: mockPresentationData) { size, image, _ in + overlayImage = image + completeIfReady() + } } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index de2553c9d9a..899204a09ea 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -74,6 +74,16 @@ public final class MediaEditor { public let top: UIColor public let bottom: UIColor + public init(colors: [UIColor]) { + if colors.count == 2 || colors.count == 1 { + self.top = colors.first! + self.bottom = colors.last! + } else { + self.top = .black + self.bottom = .black + } + } + public init(top: UIColor, bottom: UIColor) { self.top = top self.bottom = bottom @@ -110,7 +120,11 @@ public final class MediaEditor { private let clock = CMClockGetHostTimeClock() - private var player: AVPlayer? + private var player: AVPlayer? { + didSet { + + } + } private var playerAudioMix: AVMutableAudioMix? private var additionalPlayer: AVPlayer? @@ -146,11 +160,6 @@ public final class MediaEditor { private var textureSourceDisposable: Disposable? private let gradientColorsPromise = Promise() - private var gradientColorsValue: GradientColors? { - didSet { - self.gradientColorsPromise.set(.single(self.gradientColorsValue)) - } - } public var gradientColors: Signal { return self.gradientColorsPromise.get() } @@ -468,103 +477,53 @@ public final class MediaEditor { return } + let context = self.context + let clock = self.clock if let device = renderTarget.mtlDevice, CVMetalTextureCacheCreate(nil, nil, device, nil, &self.textureCache) != kCVReturnSuccess { print("error") } - - let context = self.context - let clock = self.clock - let textureSource: Signal<(UIImage?, UIImage?, AVPlayer?, AVPlayer?, GradientColors), NoError> - switch subject { - case let .image(image, _): - let colors = mediaEditorGetGradientColors(from: image) - textureSource = .single((image, nil, nil, nil, colors)) - case let .draft(draft): - if draft.isVideo { - textureSource = Signal { subscriber in - let url = URL(fileURLWithPath: draft.fullPath(engine: context.engine)) - let asset = AVURLAsset(url: url) - - let playerItem = AVPlayerItem(asset: asset) - let player = AVPlayer(playerItem: playerItem) - if #available(iOS 15.0, *) { - player.sourceClock = clock - } else { - player.masterClock = clock - } - player.automaticallyWaitsToMinimizeStalling = false - - if let gradientColors = draft.values.gradientColors { - let colors = GradientColors(top: gradientColors.first!, bottom: gradientColors.last!) - subscriber.putNext((nil, nil, player, nil, colors)) - subscriber.putCompletion() - - return EmptyDisposable - } else { - let imageGenerator = AVAssetImageGenerator(asset: asset) - imageGenerator.appliesPreferredTrackTransform = true - imageGenerator.maximumSize = CGSize(width: 72, height: 128) - imageGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: CMTime(seconds: 0, preferredTimescale: CMTimeScale(30.0)))]) { _, image, _, _, _ in - let colors: GradientColors = image.flatMap({ mediaEditorGetGradientColors(from: UIImage(cgImage: $0)) }) ?? GradientColors(top: .black, bottom: .black) - subscriber.putNext((nil, nil, player, nil, colors)) - subscriber.putCompletion() - } - return ActionDisposable { - imageGenerator.cancelAllCGImageGeneration() - } - } - } + + struct TextureSourceResult { + let image: UIImage? + let nightImage: UIImage? + let player: AVPlayer? + let playerIsReference: Bool + let gradientColors: GradientColors + + init(image: UIImage? = nil, nightImage: UIImage? = nil, player: AVPlayer? = nil, playerIsReference: Bool = false, gradientColors: GradientColors) { + self.image = image + self.nightImage = nightImage + self.player = player + self.playerIsReference = playerIsReference + self.gradientColors = gradientColors + } + } + + func makePlayer(asset: AVAsset) -> AVPlayer { + let player = AVPlayer(playerItem: AVPlayerItem(asset: asset)) + if #available(iOS 15.0, *) { + player.sourceClock = clock } else { - guard let image = UIImage(contentsOfFile: draft.fullPath(engine: context.engine)) else { - return - } - let colors: GradientColors - if let gradientColors = draft.values.gradientColors { - colors = GradientColors(top: gradientColors.first!, bottom: gradientColors.last!) - } else { - colors = mediaEditorGetGradientColors(from: image) - } - textureSource = .single((image, nil, nil, nil, colors)) + player.masterClock = clock } - case let .video(path, transitionImage, mirror, _, _, _): - let _ = mirror - textureSource = Signal { subscriber in - let asset = AVURLAsset(url: URL(fileURLWithPath: path)) - let player = AVPlayer(playerItem: AVPlayerItem(asset: asset)) - if #available(iOS 15.0, *) { - player.sourceClock = clock - } else { - player.masterClock = clock - } - player.automaticallyWaitsToMinimizeStalling = false - -// var additionalPlayer: AVPlayer? -// if let additionalPath { -// let additionalAsset = AVURLAsset(url: URL(fileURLWithPath: additionalPath)) -// additionalPlayer = AVPlayer(playerItem: AVPlayerItem(asset: additionalAsset)) -// if #available(iOS 15.0, *) { -// additionalPlayer?.sourceClock = clock -// } else { -// additionalPlayer?.masterClock = clock -// } -// additionalPlayer?.automaticallyWaitsToMinimizeStalling = false -// } - - if let transitionImage { - let colors = mediaEditorGetGradientColors(from: transitionImage) - //TODO pass mirror - subscriber.putNext((nil, nil, player, nil, colors)) + player.automaticallyWaitsToMinimizeStalling = false + return player + } + + func textureSourceResult(for asset: AVAsset, gradientColors: GradientColors? = nil) -> Signal { + return Signal { subscriber in + let player = makePlayer(asset: asset) + if let gradientColors { + subscriber.putNext(TextureSourceResult(player: player, gradientColors: gradientColors)) subscriber.putCompletion() - return EmptyDisposable } else { let imageGenerator = AVAssetImageGenerator(asset: asset) imageGenerator.appliesPreferredTrackTransform = true imageGenerator.maximumSize = CGSize(width: 72, height: 128) imageGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: CMTime(seconds: 0, preferredTimescale: CMTimeScale(30.0)))]) { _, image, _, _, _ in - let colors: GradientColors = image.flatMap({ mediaEditorGetGradientColors(from: UIImage(cgImage: $0)) }) ?? GradientColors(top: .black, bottom: .black) - //TODO pass mirror - subscriber.putNext((nil, nil, player, nil, colors)) + let gradientColors: GradientColors = image.flatMap({ mediaEditorGetGradientColors(from: UIImage(cgImage: $0)) }) ?? GradientColors(top: .black, bottom: .black) + subscriber.putNext(TextureSourceResult(player: player, gradientColors: gradientColors)) subscriber.putCompletion() } return ActionDisposable { @@ -572,47 +531,55 @@ public final class MediaEditor { } } } + } + + let textureSource: Signal + switch subject { + case let .image(image, _): + textureSource = .single( + TextureSourceResult( + image: image, + gradientColors: mediaEditorGetGradientColors(from: image) + ) + ) + case let .draft(draft): + let gradientColors = draft.values.gradientColors.flatMap { GradientColors(colors: $0) } + let fullPath = draft.fullPath(engine: context.engine) + if draft.isVideo { + let url = URL(fileURLWithPath: fullPath) + let asset = AVURLAsset(url: url) + textureSource = textureSourceResult(for: asset, gradientColors: gradientColors) + } else { + guard let image = UIImage(contentsOfFile: fullPath) else { + return + } + textureSource = .single( + TextureSourceResult( + image: image, + gradientColors: gradientColors ?? mediaEditorGetGradientColors(from: image) + ) + ) + } + case let .video(path, _, mirror, _, _, _): + //TODO: pass mirror + let _ = mirror + let asset = AVURLAsset(url: URL(fileURLWithPath: path)) + textureSource = textureSourceResult(for: asset) case let .asset(asset): textureSource = Signal { subscriber in - if asset.mediaType == .video { - let options = PHImageRequestOptions() - options.deliveryMode = .fastFormat - options.isNetworkAccessAllowed = true - let requestId = PHImageManager.default().requestImage(for: asset, targetSize: CGSize(width: 128.0, height: 128.0), contentMode: .aspectFit, options: options, resultHandler: { image, info in - if let image { - if let info { - if let cancelled = info[PHImageCancelledKey] as? Bool, cancelled { - return - } - } - let colors = mediaEditorGetGradientColors(from: image) - PHImageManager.default().requestAVAsset(forVideo: asset, options: nil, resultHandler: { asset, _, _ in - if let asset { - let playerItem = AVPlayerItem(asset: asset) - let player = AVPlayer(playerItem: playerItem) - player.automaticallyWaitsToMinimizeStalling = false - - #if targetEnvironment(simulator) - let additionalPlayerItem = AVPlayerItem(asset: asset) - let additionalPlayer = AVPlayer(playerItem: additionalPlayerItem) - additionalPlayer.automaticallyWaitsToMinimizeStalling = false - subscriber.putNext((nil, nil, player, additionalPlayer, colors)) - #else - subscriber.putNext((nil, nil, player, nil, colors)) - #endif - subscriber.putCompletion() - } - }) - } - }) - return ActionDisposable { - PHImageManager.default().cancelImageRequest(requestId) - } - } else { - let options = PHImageRequestOptions() - options.deliveryMode = .highQualityFormat - options.isNetworkAccessAllowed = true - let requestId = PHImageManager.default().requestImage(for: asset, targetSize: CGSize(width: 1920.0, height: 1920.0), contentMode: .aspectFit, options: options, resultHandler: { image, info in + let isVideo = asset.mediaType == .video + + let targetSize = isVideo ? CGSize(width: 128.0, height: 128.0) : CGSize(width: 1920.0, height: 1920.0) + let options = PHImageRequestOptions() + options.deliveryMode = isVideo ? .fastFormat : .highQualityFormat + options.isNetworkAccessAllowed = true + + let requestId = PHImageManager.default().requestImage( + for: asset, + targetSize: targetSize, + contentMode: .aspectFit, + options: options, + resultHandler: { image, info in if let image { var degraded = false if let info { @@ -623,29 +590,63 @@ public final class MediaEditor { degraded = true } } - if !degraded { - let colors = mediaEditorGetGradientColors(from: image) - subscriber.putNext((image, nil, nil, nil, colors)) - subscriber.putCompletion() + if isVideo { + PHImageManager.default().requestAVAsset(forVideo: asset, options: nil, resultHandler: { asset, _, _ in + if let asset { + let player = makePlayer(asset: asset) + subscriber.putNext( + TextureSourceResult( + player: player, + gradientColors: mediaEditorGetGradientColors(from: image) + ) + ) + subscriber.putCompletion() + } + }) + } else { + if !degraded { + subscriber.putNext( + TextureSourceResult( + image: image, + gradientColors: mediaEditorGetGradientColors(from: image) + ) + ) + subscriber.putCompletion() + } } } - }) - return ActionDisposable { - PHImageManager.default().cancelImageRequest(requestId) } + ) + return ActionDisposable { + PHImageManager.default().cancelImageRequest(requestId) } } case let .message(messageId): - textureSource = getChatWallpaperImage(context: self.context, messageId: messageId) - |> map { _, image, nightImage in - return (image, nightImage, nil, nil, GradientColors(top: .black, bottom: .black)) + textureSource = self.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: messageId)) + |> mapToSignal { message in + var player: AVPlayer? + if let message, !"".isEmpty { + if let maybeFile = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile, maybeFile.isVideo, let path = self.context.account.postbox.mediaBox.completedResourcePath(maybeFile.resource, pathExtension: "mp4") { + let asset = AVURLAsset(url: URL(fileURLWithPath: path)) + player = makePlayer(asset: asset) + } + } + return getChatWallpaperImage(context: self.context, messageId: messageId) + |> map { _, image, nightImage in + return TextureSourceResult( + image: image, + nightImage: nightImage, + player: player, + playerIsReference: true, + gradientColors: GradientColors(top: .black, bottom: .black) + ) + } } } self.textureSourceDisposable = (textureSource - |> deliverOnMainQueue).start(next: { [weak self] sourceAndColors in + |> deliverOnMainQueue).start(next: { [weak self] textureSourceResult in if let self { - let (image, nightImage, player, additionalPlayer, colors) = sourceAndColors self.renderer.onNextRender = { [weak self] in self?.onFirstDisplay() } @@ -653,25 +654,22 @@ public final class MediaEditor { let textureSource = UniversalTextureSource(renderTarget: renderTarget) if case .message = self.self.subject { - if let image { - self.wallpapers = (image, nightImage ?? image) + if let image = textureSourceResult.image { + self.wallpapers = (image, textureSourceResult.nightImage ?? image) } } - self.player = player + self.player = textureSourceResult.player self.playerPromise.set(.single(player)) - - self.additionalPlayer = additionalPlayer - self.additionalPlayerPromise.set(.single(additionalPlayer)) - if let image { - if self.values.nightTheme, let nightImage { + if let image = textureSourceResult.image { + if self.values.nightTheme, let nightImage = textureSourceResult.nightImage { textureSource.setMainInput(.image(nightImage)) } else { textureSource.setMainInput(.image(image)) } } - if let player, let playerItem = player.currentItem { + if let player, let playerItem = player.currentItem, !textureSourceResult.playerIsReference { textureSource.setMainInput(.video(playerItem)) } if let additionalPlayer, let playerItem = additionalPlayer.currentItem { @@ -679,13 +677,12 @@ public final class MediaEditor { } self.renderer.textureSource = textureSource - self.gradientColorsValue = colors - self.setGradientColors(colors.array) + self.setGradientColors(textureSourceResult.gradientColors) - if player == nil { + if let _ = textureSourceResult.player { self.updateRenderChain() // let _ = image - self.maybeGeneratePersonSegmentation(image) +// self.maybeGeneratePersonSegmentation(image) } if let _ = self.values.audioTrack { @@ -697,7 +694,7 @@ public final class MediaEditor { player.isMuted = self.values.videoIsMuted if let trimRange = self.values.videoTrimRange { player.currentItem?.forwardPlaybackEndTime = CMTime(seconds: trimRange.upperBound, preferredTimescale: CMTimeScale(1000)) - additionalPlayer?.currentItem?.forwardPlaybackEndTime = CMTime(seconds: trimRange.upperBound, preferredTimescale: CMTimeScale(1000)) +// additionalPlayer?.currentItem?.forwardPlaybackEndTime = CMTime(seconds: trimRange.upperBound, preferredTimescale: CMTimeScale(1000)) } if let initialSeekPosition = self.initialSeekPosition { @@ -711,7 +708,7 @@ public final class MediaEditor { Queue.mainQueue().justDispatch { let startPlayback = { player.playImmediately(atRate: 1.0) - additionalPlayer?.playImmediately(atRate: 1.0) +// additionalPlayer?.playImmediately(atRate: 1.0) self.audioPlayer?.playImmediately(atRate: 1.0) self.onPlaybackAction(.play) self.volumeFadeIn = player.fadeVolume(from: 0.0, to: 1.0, duration: 0.4) @@ -1616,9 +1613,10 @@ public final class MediaEditor { } } - public func setGradientColors(_ gradientColors: [UIColor]) { + public func setGradientColors(_ gradientColors: GradientColors) { + self.gradientColorsPromise.set(.single(gradientColors)) self.updateValues(mode: .skipRendering) { values in - return values.withUpdatedGradientColors(gradientColors: gradientColors) + return values.withUpdatedGradientColors(gradientColors: gradientColors.array) } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift index 73ac5d0cfd6..eb276b8a087 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift @@ -126,7 +126,7 @@ final class MediaEditorComposer { } var previousAdditionalInput: Input? - func process(main: Input, additional: Input?, pool: CVPixelBufferPool?, completion: @escaping (CVPixelBuffer?) -> Void) { + func process(main: Input, additional: Input?, timestamp: CMTime, pool: CVPixelBufferPool?, completion: @escaping (CVPixelBuffer?) -> Void) { guard let pool, let ciContext = self.ciContext else { completion(nil) return @@ -147,10 +147,8 @@ final class MediaEditorComposer { var pixelBuffer: CVPixelBuffer? CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pool, &pixelBuffer) - if let pixelBuffer { - let time = main.timestamp - - makeEditorImageFrameComposition(context: ciContext, inputImage: ciImage, drawingImage: self.drawingImage, dimensions: self.dimensions, outputDimensions: self.outputDimensions, values: self.values, entities: self.entities, time: time, completion: { compositedImage in + if let pixelBuffer { + makeEditorImageFrameComposition(context: ciContext, inputImage: ciImage, drawingImage: self.drawingImage, dimensions: self.dimensions, outputDimensions: self.outputDimensions, values: self.values, entities: self.entities, time: timestamp, completion: { compositedImage in if var compositedImage { let scale = self.outputDimensions.width / compositedImage.extent.width compositedImage = compositedImage.samplingLinear().transformed(by: CGAffineTransform(scaleX: scale, y: scale)) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift index 14718adb060..ab1c4569b55 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift @@ -81,7 +81,14 @@ func composerEntitiesForDrawingEntity(postbox: Postbox, textScale: CGFloat, enti return [] case .message: if let renderImage = entity.renderImage, let image = CIImage(image: renderImage, options: [.colorSpace: colorSpace]) { - return [MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: false)] + var entities: [MediaEditorComposerEntity] = [] + entities.append(MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: false)) + if let renderSubEntities = entity.renderSubEntities { + for subEntity in renderSubEntities { + entities.append(contentsOf: composerEntitiesForDrawingEntity(postbox: postbox, textScale: textScale, entity: subEntity, colorSpace: colorSpace)) + } + } + return entities } else { return [] } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift index 232a4815384..3ac4dd34f33 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift @@ -797,6 +797,7 @@ public final class MediaEditorVideoExport { composer.process( main: mainInput!, additional: additionalInput, + timestamp: timestamp, pool: writer.pixelBufferPool, completion: { pixelBuffer in if let pixelBuffer { @@ -805,10 +806,6 @@ public final class MediaEditorVideoExport { appendFailed = true } } else { -// if !writer.appendVideoBuffer(sampleBuffer) { -// writer.markVideoAsFinished() -// appendFailed = true -// } appendFailed = true } self.semaphore.signal() @@ -882,9 +879,6 @@ public final class MediaEditorVideoExport { self.internalStatus = .exporting - if let timeRange = self.reader?.timeRange { - print("reader timerange: \(timeRange)") - } writer.startSession(atSourceTime: self.reader?.timeRange.start ?? .zero) var videoCompleted = false diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index e9a09fad9f1..c8ed2ae3050 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -1015,7 +1015,7 @@ final class MediaEditorScreenComponent: Component { fontSize: presentationData.chatFontSize, bubbleCorners: presentationData.chatBubbleCorners, accountPeerId: component.context.account.peerId, - mode: .standard(previewing: false), + mode: .standard(.default), chatLocation: .peer(id: component.context.account.peerId), subject: nil, peerNearbyData: nil, @@ -2277,7 +2277,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate var effectiveSubject = subject if case let .draft(draft, _ ) = subject { for entity in draft.values.entities { - if case let .sticker(sticker) = entity, case let .message(ids, _, _) = sticker.content { + if case let .sticker(sticker) = entity, case let .message(ids, _, _, _, _) = sticker.content { effectiveSubject = .message(ids) break } @@ -2351,7 +2351,6 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let entityView = self.entitiesView.getView(for: mediaEntity.uuid) as? DrawingMediaEntityView { self.entitiesView.sendSubviewToBack(entityView) -// entityView.previewView = self.previewView entityView.updated = { [weak self, weak mediaEntity] in if let self, let mediaEntity { let rotationDelta = mediaEntity.rotation - initialRotation @@ -2445,10 +2444,16 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } - let maybeFile = messages.first?.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile + var messageFile: TelegramMediaFile? + if let maybeFile = messages.first?.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile, maybeFile.isVideo, let _ = self.context.account.postbox.mediaBox.completedResourcePath(maybeFile.resource, pathExtension: nil) { + messageFile = maybeFile + } + if "".isEmpty { + messageFile = nil + } let renderer = DrawingMessageRenderer(context: self.context, messages: messages) - renderer.render(completion: { size, dayImage, nightImage in + renderer.render(completion: { result in if case .draft = subject, let existingEntityView = self.entitiesView.getView(where: { entityView in if let stickerEntityView = entityView as? DrawingStickerEntityView, case .message = (stickerEntityView.entity as! DrawingStickerEntity).content { return true @@ -2458,17 +2463,19 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate }) as? DrawingStickerEntityView { existingEntityView.isNightTheme = isNightTheme let messageEntity = existingEntityView.entity as! DrawingStickerEntity - messageEntity.renderImage = dayImage - messageEntity.secondaryRenderImage = nightImage + messageEntity.renderImage = result.dayImage + messageEntity.secondaryRenderImage = result.nightImage + messageEntity.overlayRenderImage = result.overlayImage existingEntityView.update(animated: false) } else { - let messageEntity = DrawingStickerEntity(content: .message(messageIds, maybeFile?.isVideo == true ? maybeFile : nil, size)) - messageEntity.renderImage = dayImage - messageEntity.secondaryRenderImage = nightImage + let messageEntity = DrawingStickerEntity(content: .message(messageIds, result.size, messageFile, result.mediaFrame?.rect, result.mediaFrame?.cornerRadius)) + messageEntity.renderImage = result.dayImage + messageEntity.secondaryRenderImage = result.nightImage + messageEntity.overlayRenderImage = result.overlayImage messageEntity.referenceDrawingSize = storyDimensions - messageEntity.position = CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0) + messageEntity.position = CGPoint(x: storyDimensions.width / 2.0 - 54.0, y: storyDimensions.height / 2.0) - let fraction = max(size.width, size.height) / 353.0 + let fraction = max(result.size.width, result.size.height) / 353.0 messageEntity.scale = min(6.0, 3.3 * fraction) if let entityView = self.entitiesView.add(messageEntity, announce: false) as? DrawingStickerEntityView { @@ -3365,7 +3372,17 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let completeWithImage: (UIImage) -> Void = { [weak self] image in let updatedImage = roundedImageWithTransparentCorners(image: image, cornerRadius: floor(image.size.width * 0.03))! - self?.interaction?.insertEntity(DrawingStickerEntity(content: .image(updatedImage, .rectangle)), scale: 2.5) + let entity = DrawingStickerEntity(content: .image(updatedImage, .rectangle)) + entity.canCutOut = false + + let _ = (cutoutStickerImage(from: image) + |> deliverOnMainQueue).start(next: { [weak entity] result in + if result != nil, let entity { + entity.canCutOut = true + } + }) + + self?.interaction?.insertEntity(entity, scale: 2.5) } if let asset = result as? PHAsset { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaScrubberComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaScrubberComponent.swift index 037d7fa5441..9c4132866eb 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaScrubberComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaScrubberComponent.swift @@ -1046,6 +1046,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega samples: samples, peak: peak, status: .complete(), + isViewOnceMessage: false, seek: nil, updateIsSeeking: nil ) diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MediaPreviewPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MediaPreviewPanelComponent.swift index dbd47142322..69f49b4767f 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MediaPreviewPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MediaPreviewPanelComponent.swift @@ -288,6 +288,7 @@ public final class MediaPreviewPanelComponent: Component { ) } }, + isViewOnceMessage: false, seek: { [weak self] timestamp in guard let self, let mediaPlayer = self.mediaPlayer else { return @@ -318,6 +319,7 @@ public final class MediaPreviewPanelComponent: Component { samples: component.mediaPreview.waveform.samples, peak: component.mediaPreview.waveform.peak, status: .complete(), + isViewOnceMessage: false, seek: nil, updateIsSeeking: nil )), diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/BUILD index cfd1866200a..586c694ff34 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/BUILD @@ -22,6 +22,8 @@ swift_library( "//submodules/AppBundle", "//submodules/ChatListUI", "//submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode", + "//submodules/DeleteChatPeerActionSheetItem", + "//submodules/UndoUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift index 7fac23a272e..ad0b9b11d13 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift @@ -14,6 +14,8 @@ import TelegramUIPreferences import AppBundle import PeerInfoPaneNode import ChatListUI +import DeleteChatPeerActionSheetItem +import UndoUI public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate, UIGestureRecognizerDelegate { private let context: AccountContext @@ -44,11 +46,17 @@ public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, UI private var presentationDataDisposable: Disposable? private let chatListNode: ChatListNode + + private var emptyShimmerEffectNode: ChatListShimmerNode? + private var shimmerNodeOffset: CGFloat = 0.0 + private var floatingHeaderOffset: CGFloat? public init(context: AccountContext, navigationController: @escaping () -> NavigationController?) { self.context = context self.navigationController = navigationController - self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.presentationData = presentationData + let strings = presentationData.strings self.chatListNode = ChatListNode( context: self.context, @@ -86,6 +94,15 @@ public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, UI self.ready.set(self.chatListNode.ready) + self.statusPromise.set(self.context.engine.messages.savedMessagesPeersStats() + |> map { count in + if let count { + return PeerInfoStatusData(text: strings.Notifications_Exceptions(Int32(count)), isActivity: false, key: .savedMessagesChats) + } else { + return PeerInfoStatusData(text: strings.Channel_NotificationLoading.lowercased(), isActivity: false, key: .savedMessagesChats) + } + }) + self.chatListNode.peerSelected = { [weak self] peer, _, _, _, _ in guard let self, let navigationController = self.navigationController() else { return @@ -94,10 +111,7 @@ public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, UI navigationController: navigationController, context: self.context, chatLocation: .replyThread(ChatReplyThreadMessage( - messageId: makeThreadIdMessageId( - peerId: self.context.account.peerId, - threadId: peer.id.toInt64() - ), + peerId: self.context.account.peerId, threadId: peer.id.toInt64(), channelMessageId: nil, isChannelPost: false, @@ -115,6 +129,159 @@ public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, UI )) self.chatListNode.clearHighlightAnimated(true) } + + self.chatListNode.isEmptyUpdated = { [weak self] isEmptyState, _, transition in + guard let self else { + return + } + var needsShimmerNode = false + let shimmerNodeOffset: CGFloat = 0.0 + + switch isEmptyState { + case let .empty(isLoadingValue, _): + if isLoadingValue { + needsShimmerNode = true + } + case .notEmpty: + break + } + + if needsShimmerNode { + self.shimmerNodeOffset = shimmerNodeOffset + if self.emptyShimmerEffectNode == nil { + let emptyShimmerEffectNode = ChatListShimmerNode() + self.emptyShimmerEffectNode = emptyShimmerEffectNode + self.insertSubnode(emptyShimmerEffectNode, belowSubnode: self.chatListNode) + if let currentParams = self.currentParams, let offset = self.floatingHeaderOffset { + self.layoutEmptyShimmerEffectNode(node: emptyShimmerEffectNode, size: currentParams.size, insets: UIEdgeInsets(top: currentParams.topInset, left: currentParams.sideInset, bottom: currentParams.bottomInset, right: currentParams.sideInset), verticalOffset: offset + self.shimmerNodeOffset, transition: .immediate) + } + } + } else if let emptyShimmerEffectNode = self.emptyShimmerEffectNode { + self.emptyShimmerEffectNode = nil + let emptyNodeTransition = transition.isAnimated ? transition : .animated(duration: 0.3, curve: .easeInOut) + emptyNodeTransition.updateAlpha(node: emptyShimmerEffectNode, alpha: 0.0, completion: { [weak emptyShimmerEffectNode] _ in + emptyShimmerEffectNode?.removeFromSupernode() + }) + self.chatListNode.alpha = 0.0 + emptyNodeTransition.updateAlpha(node: self.chatListNode, alpha: 1.0) + } + } + + self.chatListNode.updateFloatingHeaderOffset = { [weak self] offset, transition in + guard let self else { + return + } + self.floatingHeaderOffset = offset + if let currentParams = self.currentParams, let emptyShimmerEffectNode = self.emptyShimmerEffectNode { + self.layoutEmptyShimmerEffectNode(node: emptyShimmerEffectNode, size: currentParams.size, insets: UIEdgeInsets(top: currentParams.topInset, left: currentParams.sideInset, bottom: currentParams.bottomInset, right: currentParams.sideInset), verticalOffset: offset + self.shimmerNodeOffset, transition: transition) + } + } + + self.chatListNode.push = { [weak self] c in + guard let self else { + return + } + self.parentController?.push(c) + } + + self.chatListNode.present = { [weak self] c in + guard let self else { + return + } + self.parentController?.present(c, in: .window(.root)) + } + + self.chatListNode.deletePeerChat = { [weak self] peerId, _ in + guard let self else { + return + } + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + + self.view.window?.endEditing(true) + + let actionSheet = ActionSheetController(presentationData: self.presentationData) + var items: [ActionSheetItem] = [] + items.append(DeleteChatPeerActionSheetItem(context: self.context, peer: peer, chatPeer: peer, action: .deleteSavedPeer, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, balancedLayout: true)) + items.append(ActionSheetButtonItem(title: self.presentationData.strings.Common_Delete, color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + + guard let self else { + return + } + + self.chatListNode.updateState({ state in + var state = state + state.pendingRemovalItemIds.insert(ChatListNodeState.ItemId(peerId: peer.id, threadId: nil)) + return state + }) + self.parentController?.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitActionAndReplacementAnimation() + } + return true + }) + + self.parentController?.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: self.presentationData.strings.SavedMessages_SubChatDeleted, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in + guard let self else { + return false + } + if value == .commit { + let _ = self.context.engine.messages.clearHistoryInteractively(peerId: self.context.account.peerId, threadId: peer.id.toInt64(), type: .forLocalPeer).startStandalone(completed: { [weak self] in + guard let self else { + return + } + self.chatListNode.updateState({ state in + var state = state + state.pendingRemovalItemIds.remove(ChatListNodeState.ItemId(peerId: peer.id, threadId: nil)) + return state + }) + }) + return true + } else if value == .undo { + self.chatListNode.updateState({ state in + var state = state + state.pendingRemovalItemIds.remove(ChatListNodeState.ItemId(peerId: peer.id, threadId: nil)) + return state + }) + return true + } + return false + }), in: .current) + })) + + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) + ]) + self.parentController?.present(actionSheet, in: .window(.root)) + }) + } + + self.chatListNode.activateChatPreview = { [weak self] item, _, node, gesture, location in + guard let self, let parentController = self.parentController else { + gesture?.cancel() + return + } + + if case let .peer(peerData) = item.content { + let threadId = peerData.peer.peerId.toInt64() + let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .replyThread(message: ChatReplyThreadMessage( + peerId: self.context.account.peerId, threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: false, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false + )), subject: nil, botStart: nil, mode: .standard(.previewing)) + chatController.canReadHistory.set(false) + let source: ContextContentSource = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: parentController.navigationController as? NavigationController)) + + let contextController = ContextController(presentationData: self.presentationData, source: source, items: savedMessagesPeerMenuItems(context: self.context, threadId: threadId, parentController: parentController) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + parentController.presentInGlobalOverlay(contextController) + } + } } deinit { @@ -125,6 +292,8 @@ public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, UI } public func scrollToTop() -> Bool { + self.chatListNode.scrollToPosition(.top(adjustForTempInset: false)) + return false } @@ -164,6 +333,11 @@ public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, UI return true } + private func layoutEmptyShimmerEffectNode(node: ChatListShimmerNode, size: CGSize, insets: UIEdgeInsets, verticalOffset: CGFloat, transition: ContainedViewLayoutTransition) { + node.update(context: self.context, animationCache: self.context.animationCache, animationRenderer: self.context.animationRenderer, size: size, isInlineMode: false, presentationData: self.presentationData, transition: .immediate) + transition.updateFrameAdditive(node: node, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: size)) + } + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { if gestureRecognizer.state != .failed, let otherGestureRecognizer = otherGestureRecognizer as? UIPanGestureRecognizer { let _ = otherGestureRecognizer @@ -176,7 +350,7 @@ public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, UI public func updateSelectedMessages(animated: Bool) { } - public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { self.currentParams = (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) transition.updateFrame(node: self.chatListNode, frame: CGRect(origin: CGPoint(), size: size)) @@ -204,3 +378,32 @@ public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, UI return result } } + +private final class ContextControllerContentSourceImpl: ContextControllerContentSource { + let controller: ViewController + weak var sourceNode: ASDisplayNode? + + let navigationController: NavigationController? + + let passthroughTouches: Bool = true + + init(controller: ViewController, sourceNode: ASDisplayNode?, navigationController: NavigationController?) { + self.controller = controller + self.sourceNode = sourceNode + self.navigationController = navigationController + } + + func transitionInfo() -> ContextControllerTakeControllerInfo? { + let sourceNode = self.sourceNode + return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in + if let sourceNode = sourceNode { + return (sourceNode.view, sourceNode.bounds) + } else { + return nil + } + }) + } + + func animatedIn() { + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatPaneNode/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatPaneNode/BUILD new file mode 100644 index 00000000000..6ff7241b519 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatPaneNode/BUILD @@ -0,0 +1,27 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "PeerInfoChatPaneNode", + module_name = "PeerInfoChatPaneNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/AsyncDisplayKit", + "//submodules/TelegramCore", + "//submodules/Postbox", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/TelegramStringFormatting", + "//submodules/ComponentFlow", + "//submodules/AppBundle", + "//submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatPaneNode/Sources/PeerInfoChatPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatPaneNode/Sources/PeerInfoChatPaneNode.swift new file mode 100644 index 00000000000..89d0a2a7f09 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatPaneNode/Sources/PeerInfoChatPaneNode.swift @@ -0,0 +1,160 @@ +import AsyncDisplayKit +import Display +import TelegramCore +import SwiftSignalKit +import Postbox +import TelegramPresentationData +import AccountContext +import TelegramStringFormatting +import ComponentFlow +import TelegramUIPreferences +import AppBundle +import PeerInfoPaneNode + +public final class PeerInfoChatPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate, UIGestureRecognizerDelegate { + private let context: AccountContext + private let peerId: EnginePeer.Id + private let navigationController: () -> NavigationController? + + private let chatController: ChatController + + public weak var parentController: ViewController? { + didSet { + if self.parentController !== oldValue { + if let parentController = self.parentController { + self.chatController.willMove(toParent: parentController) + parentController.addChild(self.chatController) + self.chatController.didMove(toParent: parentController) + } else { + self.chatController.willMove(toParent: nil) + self.chatController.removeFromParent() + self.chatController.didMove(toParent: nil) + } + } + } + } + + private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData)? + + private let ready = Promise() + private var didSetReady: Bool = false + public var isReady: Signal { + return self.ready.get() + } + + private let statusPromise = Promise(nil) + public var status: Signal { + self.statusPromise.get() + } + + public var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)? + public var tabBarOffset: CGFloat { + return 0.0 + } + + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + + public init(context: AccountContext, peerId: EnginePeer.Id, navigationController: @escaping () -> NavigationController?) { + self.context = context + self.peerId = peerId + self.navigationController = navigationController + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + + self.chatController = context.sharedContext.makeChatController(context: context, chatLocation: .replyThread(message: ChatReplyThreadMessage(peerId: context.account.peerId, threadId: peerId.toInt64(), channelMessageId: nil, isChannelPost: false, isForumPost: false, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)), subject: nil, botStart: nil, mode: .standard(.embedded(invertDirection: true))) + + super.init() + + self.presentationDataDisposable = (self.context.sharedContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + guard let self else { + return + } + self.presentationData = presentationData + }) + + self.ready.set(self.chatController.ready.get()) + + self.addSubnode(self.chatController.displayNode) + self.chatController.displayNode.clipsToBounds = true + } + + deinit { + self.presentationDataDisposable?.dispose() + } + + public func ensureMessageIsVisible(id: MessageId) { + } + + public func scrollToTop() -> Bool { + return self.chatController.performScrollToTop() + } + + public func hitTestResultForScrolling() -> UIView? { + return nil + } + + public func brieflyDisableTouchActions() { + } + + public func findLoadedMessage(id: MessageId) -> Message? { + return nil + } + + public func updateHiddenMedia() { + } + + public func transferVelocity(_ velocity: CGFloat) { + if velocity > 0.0 { + self.chatController.transferScrollingVelocity(velocity) + } + } + + public func cancelPreviewGestures() { + } + + public func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + return nil + } + + public func addToTransitionSurface(view: UIView) { + } + + override public func didLoad() { + super.didLoad() + } + + + override public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer.state != .failed, let otherGestureRecognizer = otherGestureRecognizer as? UIPanGestureRecognizer { + let _ = otherGestureRecognizer + return true + } else { + return false + } + } + + public func updateSelectedMessages(animated: Bool) { + } + + public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + self.currentParams = (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) + let chatFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: size.width, height: size.height - topInset)) + + let combinedBottomInset = bottomInset + transition.updateFrame(node: self.chatController.displayNode, frame: chatFrame) + self.chatController.updateIsScrollingLockedAtTop(isScrollingLockedAtTop: isScrollingLockedAtTop) + self.chatController.containerLayoutUpdated(ContainerViewLayout(size: chatFrame.size, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 4.0, left: sideInset, bottom: combinedBottomInset, right: sideInset), safeInsets: UIEdgeInsets(top: 4.0, left: sideInset, bottom: combinedBottomInset, right: sideInset), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition) + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let result = super.hitTest(point, with: event) else { + return nil + } + return result + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift index 406286f904a..8e79a998755 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift @@ -17,6 +17,7 @@ public enum PeerInfoPaneKey: Int32 { case groupsInCommon case recommended case savedMessagesChats + case savedMessages } public struct PeerInfoStatusData: Equatable { @@ -44,7 +45,7 @@ public protocol PeerInfoPaneNode: ASDisplayNode { var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)? { get set } var tabBarOffset: CGFloat { get } - func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) + func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) func scrollToTop() -> Bool func transferVelocity(_ velocity: CGFloat) func cancelPreviewGestures() diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD index 5a22b6a0917..fd690648f86 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD @@ -144,6 +144,7 @@ swift_library( "//submodules/SolidRoundedButtonNode", "//submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode", "//submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode", + "//submodules/TelegramUI/Components/PeerInfo/PeerInfoChatPaneNode", "//submodules/MediaPickerUI", "//submodules/AttachmentUI", "//submodules/TelegramUI/Components/Settings/BoostLevelIconComponent", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift index 80932776520..4eceee485ea 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift @@ -19,6 +19,9 @@ enum PeerInfoScreenLabeledValueTextBehavior: Equatable { } enum PeerInfoScreenLabeledValueIcon { + // MARK: Nicegram TranslateBio + case nicegram(UIImage?) + // case qrCode } @@ -367,6 +370,10 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { if let icon = item.icon { let iconImage: UIImage? switch icon { + // MARK: Nicegram TranslateBio + case let .nicegram(image): + iconImage = image + // case .qrCode: iconImage = UIImage(bundleImageName: "Settings/QrIcon") } @@ -509,7 +516,13 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { let iconButtonFrame = CGRect(x: width - safeInsets.right - height, y: 0.0, width: height, height: height) transition.updateFrame(node: self.iconButtonNode, frame: iconButtonFrame) if let iconSize = self.iconNode.image?.size { - transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: width - safeInsets.right - sideInset - iconSize.width + 5.0, y: floorToScreenPixels((height - iconSize.height) / 2.0)), size: iconSize)) + // MARK: Nicegram TranslateBio, iconFrame variable + let iconFrame = CGRect(origin: CGPoint(x: width - safeInsets.right - sideInset - iconSize.width + 5.0, y: floorToScreenPixels((height - iconSize.height) / 2.0)), size: iconSize) + transition.updateFrame(node: self.iconNode, frame: iconFrame) + + // MARK: Nicegram TranslateBio, identical frames for iconNode and iconButtonNode + transition.updateFrame(node: self.iconButtonNode, frame: iconFrame) + // } if additionalTextSize.height > 0.0 { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGifPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGifPaneNode.swift index 9788c296063..340d6800aca 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGifPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGifPaneNode.swift @@ -786,7 +786,7 @@ final class PeerInfoGifPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDe return self._itemInteraction! } - private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData)? + private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData)? private let ready = Promise() private var didSetReady: Bool = false @@ -959,8 +959,8 @@ final class PeerInfoGifPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDe let wasFirstHistoryView = self.isFirstHistoryView self.isFirstHistoryView = false - if let (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) = self.currentParams { - self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: wasFirstHistoryView, transition: .immediate) + if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) = self.currentParams { + self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: wasFirstHistoryView, transition: .immediate) if !self.didSetReady { self.didSetReady = true self.ready.set(.single(true)) @@ -1066,9 +1066,9 @@ final class PeerInfoGifPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDe } } - func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { let previousParams = self.currentParams - self.currentParams = (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) + self.currentParams = (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: size.width, height: size.height - topInset))) @@ -1110,7 +1110,7 @@ final class PeerInfoGifPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDe private var previousDidScrollTimestamp: Double = 0.0 func scrollViewDidScroll(_ scrollView: UIScrollView) { - if let (size, _, sideInset, bottomInset, visibleHeight, _, _, presentationData) = self.currentParams { + if let (size, _, sideInset, bottomInset, _, visibleHeight, _, _, presentationData) = self.currentParams { self.updateVisibleItems(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, theme: presentationData.theme, strings: presentationData.strings, synchronousLoad: false) if scrollView.contentOffset.y >= scrollView.contentSize.height - scrollView.bounds.height * 2.0, let currentView = self.currentView, currentView.earlierId != nil { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGroupsInCommonPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGroupsInCommonPaneNode.swift index 7ce1f983973..1ffa847f015 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGroupsInCommonPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGroupsInCommonPaneNode.swift @@ -155,7 +155,7 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode { } } - func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { let isFirstLayout = self.currentParams == nil self.currentParams = (size, isScrollingLockedAtTop, presentationData) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoListPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoListPaneNode.swift index 7aa0d6f3e6f..0cddeb82765 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoListPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoListPaneNode.swift @@ -31,7 +31,7 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { private let listNode: ChatHistoryListNode - private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData)? + private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData)? private let ready = Promise() private var didSetReady: Bool = false @@ -146,8 +146,8 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { strongSelf.playlistLocation = nil } - if let (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) = strongSelf.currentParams { - strongSelf.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: true, transition: .animated(duration: 0.4, curve: .spring)) + if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) = strongSelf.currentParams { + strongSelf.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: true, transition: .animated(duration: 0.4, curve: .spring)) } } }) @@ -200,8 +200,8 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { } } - func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { - self.currentParams = (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) + func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + self.currentParams = (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) var topPanelHeight: CGFloat = 0.0 if let (item, previousItem, nextItem, order, type, _) = self.playlistStateAndType { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift index 28750cf74c8..712656f0e09 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift @@ -238,7 +238,7 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode { } } - func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { let isFirstLayout = self.currentParams == nil self.currentParams = (size, isScrollingLockedAtTop) self.presentationDataPromise.set(.single(presentationData)) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedChannelsPane.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedChannelsPane.swift index 3720a1935c3..089c3bea7f8 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedChannelsPane.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedChannelsPane.swift @@ -195,7 +195,7 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode } } - func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { let isFirstLayout = self.currentParams == nil self.currentParams = (size, sideInset, bottomInset, isScrollingLockedAtTop, presentationData) self.presentationDataPromise.set(.single(presentationData)) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index 658bf70aca8..1e360459d6e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -290,7 +290,7 @@ private enum PeerInfoScreenInputData: Equatable { public func hasAvailablePeerInfoMediaPanes(context: AccountContext, peerId: PeerId) -> Signal { let chatLocationContextHolder = Atomic(value: nil) - return peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: .peer(id: peerId), chatLocationContextHolder: chatLocationContextHolder) + let mediaPanes = peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: .peer(id: peerId), chatLocationContextHolder: chatLocationContextHolder) |> map { panes -> Bool in if let panes { return !panes.isEmpty @@ -298,6 +298,22 @@ public func hasAvailablePeerInfoMediaPanes(context: AccountContext, peerId: Peer return false } } + + let hasSavedMessagesChats: Signal + if peerId == context.account.peerId { + hasSavedMessagesChats = context.engine.messages.savedMessagesPeerListHead() + |> map { headPeerId -> Bool in + return headPeerId != nil + } + |> distinctUntilChanged + } else { + hasSavedMessagesChats = .single(false) + } + + return combineLatest(queue: .mainQueue(), [mediaPanes, hasSavedMessagesChats]) + |> map { values in + return values.contains(true) + } } private func peerInfoAvailableMediaPanes(context: AccountContext, peerId: PeerId, chatLocation: ChatLocation, chatLocationContextHolder: Atomic) -> Signal<[PeerInfoPaneKey]?, NoError> { @@ -809,6 +825,29 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen savedMessagesPeer = .single(nil) } + let hasSavedMessages: Signal + let hasSavedMessagesChats: Signal + if case .peer = chatLocation { + hasSavedMessages = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.MessageCount(peerId: context.account.peerId, threadId: peerId.toInt64(), tag: MessageTags())) + |> map { count -> Bool in + if let count, count != 0 { + return true + } else { + return false + } + } + |> distinctUntilChanged + + hasSavedMessagesChats = context.engine.messages.savedMessagesPeerListHead() + |> map { headPeerId -> Bool in + return headPeerId != nil + } + |> distinctUntilChanged + } else { + hasSavedMessages = .single(false) + hasSavedMessagesChats = .single(false) + } + return combineLatest( context.account.viewTracker.peerView(peerId, updateData: true), peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder), @@ -817,9 +856,11 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen status, hasStories, accountIsPremium, - savedMessagesPeer + savedMessagesPeer, + hasSavedMessagesChats, + hasSavedMessages ) - |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, accountIsPremium, savedMessagesPeer -> PeerInfoScreenData in + |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages -> PeerInfoScreenData in var availablePanes = availablePanes if let hasStories { @@ -833,9 +874,22 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen } } - /*if peerId == context.account.peerId, case .peer = chatLocation { - availablePanes?.insert(.savedMessagesChats, at: 0) - }*/ + if case .peer = chatLocation { + if peerId == context.account.peerId { + if hasSavedMessagesChats { + availablePanes?.insert(.savedMessagesChats, at: 0) + } + } else if hasSavedMessages && hasSavedMessagesChats { + if var availablePanesValue = availablePanes { + if let index = availablePanesValue.firstIndex(of: .media) { + availablePanesValue.insert(.savedMessages, at: index + 1) + } else { + availablePanesValue.insert(.savedMessages, at: 0) + } + availablePanes = availablePanesValue + } + } + } } else { availablePanes = nil } @@ -904,6 +958,28 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen } |> distinctUntilChanged + let hasSavedMessages: Signal + let hasSavedMessagesChats: Signal + if case .peer = chatLocation { + hasSavedMessages = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.MessageCount(peerId: context.account.peerId, threadId: peerId.toInt64(), tag: MessageTags())) + |> map { count -> Bool in + if let count, count != 0 { + return true + } else { + return false + } + } + |> distinctUntilChanged + hasSavedMessagesChats = context.engine.messages.savedMessagesPeerListHead() + |> map { headPeerId -> Bool in + return headPeerId != nil + } + |> distinctUntilChanged + } else { + hasSavedMessages = .single(false) + hasSavedMessagesChats = .single(false) + } + return combineLatest( context.account.viewTracker.peerView(peerId, updateData: true), peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder), @@ -915,9 +991,11 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen requestsStatePromise.get(), hasStories, accountIsPremium, - context.engine.peers.recommendedChannels(peerId: peerId) + context.engine.peers.recommendedChannels(peerId: peerId), + hasSavedMessages, + hasSavedMessagesChats ) - |> map { peerView, availablePanes, globalNotificationSettings, status, currentInvitationsContext, invitations, currentRequestsContext, requests, hasStories, accountIsPremium, recommendedChannels -> PeerInfoScreenData in + |> map { peerView, availablePanes, globalNotificationSettings, status, currentInvitationsContext, invitations, currentRequestsContext, requests, hasStories, accountIsPremium, recommendedChannels, hasSavedMessages, hasSavedMessagesChats -> PeerInfoScreenData in var availablePanes = availablePanes if let hasStories { if hasStories { @@ -926,6 +1004,17 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen if let recommendedChannels, !recommendedChannels.channels.isEmpty { availablePanes?.append(.recommended) } + + if case .peer = chatLocation { + if hasSavedMessages, hasSavedMessagesChats, var availablePanesValue = availablePanes { + if let index = availablePanesValue.firstIndex(of: .media) { + availablePanesValue.insert(.savedMessages, at: index + 1) + } else { + availablePanesValue.insert(.savedMessages, at: 0) + } + availablePanes = availablePanesValue + } + } } else { availablePanes = nil } @@ -1103,6 +1192,28 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen } |> distinctUntilChanged + let hasSavedMessages: Signal + let hasSavedMessagesChats: Signal + if case .peer = chatLocation { + hasSavedMessages = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.MessageCount(peerId: context.account.peerId, threadId: peerId.toInt64(), tag: MessageTags())) + |> map { count -> Bool in + if let count, count != 0 { + return true + } else { + return false + } + } + |> distinctUntilChanged + hasSavedMessagesChats = context.engine.messages.savedMessagesPeerListHead() + |> map { headPeerId -> Bool in + return headPeerId != nil + } + |> distinctUntilChanged + } else { + hasSavedMessages = .single(false) + hasSavedMessagesChats = .single(false) + } + return combineLatest(queue: .mainQueue(), context.account.viewTracker.peerView(groupId, updateData: true), peerInfoAvailableMediaPanes(context: context, peerId: groupId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder), @@ -1115,9 +1226,11 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen requestsStatePromise.get(), threadData, context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]), - accountIsPremium + accountIsPremium, + hasSavedMessages, + hasSavedMessagesChats ) - |> mapToSignal { peerView, availablePanes, globalNotificationSettings, status, membersData, currentInvitationsContext, invitations, currentRequestsContext, requests, threadData, preferencesView, accountIsPremium -> Signal in + |> mapToSignal { peerView, availablePanes, globalNotificationSettings, status, membersData, currentInvitationsContext, invitations, currentRequestsContext, requests, threadData, preferencesView, accountIsPremium, hasSavedMessages, hasSavedMessagesChats -> Signal in var discussionPeer: Peer? if case let .known(maybeLinkedDiscussionPeerId) = (peerView.cachedData as? CachedChannelData)?.linkedDiscussionPeerId, let linkedDiscussionPeerId = maybeLinkedDiscussionPeerId, let peer = peerView.peers[linkedDiscussionPeerId] { discussionPeer = peer @@ -1132,6 +1245,17 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen } } + if case .peer = chatLocation { + if hasSavedMessages, hasSavedMessagesChats, var availablePanesValue = availablePanes { + if let index = availablePanesValue.firstIndex(of: .media) { + availablePanesValue.insert(.savedMessages, at: index + 1) + } else { + availablePanesValue.insert(.savedMessages, at: 0) + } + availablePanes = availablePanesValue + } + } + var canManageInvitations = false if let group = peerViewMainPeer(peerView) as? TelegramGroup { let previousValue = wasUpgradedGroup.swap(group.migrationReference != nil) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index 7a04230755a..d7eec72277d 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -453,7 +453,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { private var currentPanelStatusData: PeerInfoStatusData? func update(width: CGFloat, containerHeight: CGFloat, containerInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, isModalOverlay: Bool, isMediaOnly: Bool, contentOffset: CGFloat, paneContainerY: CGFloat, presentationData: PresentationData, peer: Peer?, cachedData: CachedPeerData?, threadData: MessageHistoryThreadData?, peerNotificationSettings: TelegramPeerNotificationSettings?, threadNotificationSettings: TelegramPeerNotificationSettings?, globalNotificationSettings: EngineGlobalNotificationSettings?, statusData: PeerInfoStatusData?, panelStatusData: (PeerInfoStatusData?, PeerInfoStatusData?, CGFloat?), isSecretChat: Bool, isContact: Bool, isSettings: Bool, state: PeerInfoState, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition, additive: Bool, animateHeader: Bool) -> CGFloat { var threadData = threadData - if case let .replyThread(replyThreadMessage) = self.chatLocation, replyThreadMessage.messageId.peerId == self.context.account.peerId { + if case let .replyThread(replyThreadMessage) = self.chatLocation, replyThreadMessage.peerId == self.context.account.peerId { threadData = nil } @@ -971,6 +971,8 @@ final class PeerInfoHeaderNode: ASDisplayNode { var title: String if peer.id == self.context.account.peerId && !self.isSettings { title = presentationData.strings.Conversation_SavedMessages + } else if peer.id.isAnonymousSavedMessages { + title = presentationData.strings.ChatList_AuthorHidden } else if let threadData = threadData { title = threadData.info.title } else { @@ -1618,7 +1620,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { if self.isSettings { expandablePart += 20.0 } else { - if case let .replyThread(replyThreadMessage) = self.chatLocation, replyThreadMessage.messageId.peerId == self.context.account.peerId { + if case let .replyThread(replyThreadMessage) = self.chatLocation, replyThreadMessage.peerId == self.context.account.peerId { expandablePart = 0.0 } else if peer?.id == self.context.account.peerId { expandablePart = 0.0 diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift index 198fc3cfba2..c95a1b375d8 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift @@ -12,26 +12,27 @@ import ChatControllerInteraction import PeerInfoVisualMediaPaneNode import PeerInfoPaneNode import PeerInfoChatListPaneNode +import PeerInfoChatPaneNode final class PeerInfoPaneWrapper { let key: PeerInfoPaneKey let node: PeerInfoPaneNode var isAnimatingOut: Bool = false - private var appliedParams: (CGSize, CGFloat, CGFloat, CGFloat, CGFloat, Bool, CGFloat, PresentationData)? + private var appliedParams: (CGSize, CGFloat, CGFloat, CGFloat, DeviceMetrics, CGFloat, Bool, CGFloat, PresentationData)? init(key: PeerInfoPaneKey, node: PeerInfoPaneNode) { self.key = key self.node = node } - func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { - if let (currentSize, currentTopInset, currentSideInset, currentBottomInset, _, currentIsScrollingLockedAtTop, currentExpandProgress, currentPresentationData) = self.appliedParams { - if currentSize == size && currentTopInset == topInset, currentSideInset == sideInset && currentBottomInset == bottomInset, currentIsScrollingLockedAtTop == isScrollingLockedAtTop && currentExpandProgress == expandProgress && currentPresentationData === presentationData { + func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + if let (currentSize, currentTopInset, currentSideInset, currentBottomInset, _, currentVisibleHeight, currentIsScrollingLockedAtTop, currentExpandProgress, currentPresentationData) = self.appliedParams { + if currentSize == size && currentTopInset == topInset, currentSideInset == sideInset && currentBottomInset == bottomInset && currentVisibleHeight == visibleHeight && currentIsScrollingLockedAtTop == isScrollingLockedAtTop && currentExpandProgress == expandProgress && currentPresentationData === presentationData { return } } - self.appliedParams = (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) - self.node.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: synchronous, transition: transition) + self.appliedParams = (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) + self.node.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: synchronous, transition: transition) } } @@ -422,6 +423,8 @@ private final class PeerInfoPendingPane { paneNode = PeerInfoRecommendedChannelsPaneNode(context: context, peerId: peerId, chatControllerInteraction: chatControllerInteraction, openPeerContextAction: openPeerContextAction) case .savedMessagesChats: paneNode = PeerInfoChatListPaneNode(context: context, navigationController: chatControllerInteraction.navigationController) + case .savedMessages: + paneNode = PeerInfoChatPaneNode(context: context, peerId: peerId, navigationController: chatControllerInteraction.navigationController) } paneNode.parentController = parentController self.pane = PeerInfoPaneWrapper(key: key, node: paneNode) @@ -456,7 +459,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat let isReady = Promise() var didSetIsReady = false - private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, expansionFraction: CGFloat, presentationData: PresentationData, data: PeerInfoScreenData?)? + private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, expansionFraction: CGFloat, presentationData: PresentationData, data: PeerInfoScreenData?)? private(set) var currentPaneKey: PeerInfoPaneKey? var pendingSwitchToPaneKey: PeerInfoPaneKey? @@ -548,8 +551,8 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat if strongSelf.currentPanes[key] != nil { strongSelf.currentPaneKey = key - if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams { - strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .animated(duration: 0.4, curve: .spring)) + if let (size, sideInset, bottomInset, deviceMetrics, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams { + strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .animated(duration: 0.4, curve: .spring)) strongSelf.currentPaneUpdated?(true) @@ -561,8 +564,8 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat strongSelf.pendingSwitchToPaneKey = key strongSelf.expandOnSwitch = true - if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams { - strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .animated(duration: 0.4, curve: .spring)) + if let (size, sideInset, bottomInset, deviceMetrics, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams { + strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .animated(duration: 0.4, curve: .spring)) } } } @@ -578,6 +581,12 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat if strongSelf.tabsContainerNode.bounds.contains(strongSelf.view.convert(point, to: strongSelf.tabsContainerNode.view)) { return [] } + if case .savedMessagesChats = currentPaneKey { + if index == 0 { + return .leftCenter + } + return [.leftCenter, .rightCenter] + } if index == 0 { return .left } @@ -621,7 +630,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat cancelContextGestures(view: self.view) case .changed: - if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = self.currentParams, let availablePanes = data?.availablePanes, availablePanes.count > 1, let currentPaneKey = self.currentPaneKey, let currentIndex = availablePanes.firstIndex(of: currentPaneKey) { + if let (size, sideInset, bottomInset, deviceMetrics, visibleHeight, expansionFraction, presentationData, data) = self.currentParams, let availablePanes = data?.availablePanes, availablePanes.count > 1, let currentPaneKey = self.currentPaneKey, let currentIndex = availablePanes.firstIndex(of: currentPaneKey) { let translation = recognizer.translation(in: self.view) var transitionFraction = translation.x / size.width if currentIndex <= 0 { @@ -636,11 +645,11 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat // print(transitionFraction) self.paneTransitionPromise.set(transitionFraction) - self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .immediate) + self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .immediate) self.currentPaneUpdated?(false) } case .cancelled, .ended: - if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = self.currentParams, let availablePanes = data?.availablePanes, availablePanes.count > 1, let currentPaneKey = self.currentPaneKey, let currentIndex = availablePanes.firstIndex(of: currentPaneKey) { + if let (size, sideInset, bottomInset, deviceMetrics, visibleHeight, expansionFraction, presentationData, data) = self.currentParams, let availablePanes = data?.availablePanes, availablePanes.count > 1, let currentPaneKey = self.currentPaneKey, let currentIndex = availablePanes.firstIndex(of: currentPaneKey) { let translation = recognizer.translation(in: self.view) let velocity = recognizer.velocity(in: self.view) var directionIsToRight: Bool? @@ -664,7 +673,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat } } self.transitionFraction = 0.0 - self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .animated(duration: 0.35, curve: .spring)) + self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .animated(duration: 0.35, curve: .spring)) self.currentPaneUpdated?(false) self.currentPaneStatusPromise.set(self.currentPane?.node.status ?? .single(nil)) @@ -703,7 +712,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat } } - func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, expansionFraction: CGFloat, presentationData: PresentationData, data: PeerInfoScreenData?, transition: ContainedViewLayoutTransition) { + func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, expansionFraction: CGFloat, presentationData: PresentationData, data: PeerInfoScreenData?, transition: ContainedViewLayoutTransition) { let previousAvailablePanes = self.currentAvailablePanes let availablePanes = data?.availablePanes ?? [] self.currentAvailablePanes = data?.availablePanes @@ -747,7 +756,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat currentIndex = nil } - self.currentParams = (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) + self.currentParams = (size, sideInset, bottomInset, deviceMetrics, visibleHeight, expansionFraction, presentationData, data) transition.updateAlpha(node: self.coveringBackgroundNode, alpha: expansionFraction) @@ -817,12 +826,12 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat guard let strongSelf = self else { return } - if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams { + if let (size, sideInset, bottomInset, deviceMetrics, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams { var transition: ContainedViewLayoutTransition = .immediate if strongSelf.pendingSwitchToPaneKey == key && strongSelf.currentPaneKey != nil { transition = .animated(duration: 0.4, curve: .spring) } - strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: transition) + strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: transition) } } if leftScope { @@ -845,14 +854,14 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat ) self.pendingPanes[key] = pane pane.pane.node.frame = paneFrame - pane.pane.update(size: paneFrame.size, topInset: tabsHeight, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expansionFraction, presentationData: presentationData, synchronous: true, transition: .immediate) + pane.pane.update(size: paneFrame.size, topInset: tabsHeight, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expansionFraction, presentationData: presentationData, synchronous: true, transition: .immediate) let paneNode = pane.pane.node pane.pane.node.tabBarOffsetUpdated = { [weak self, weak paneNode] transition in guard let strongSelf = self, let paneNode = paneNode, let currentPane = strongSelf.currentPane, paneNode === currentPane.node else { return } - if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams { - strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: transition) + if let (size, sideInset, bottomInset, deviceMetrics, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams { + strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: transition) } } leftScope = true @@ -861,7 +870,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat for (key, pane) in self.pendingPanes { pane.pane.node.frame = paneFrame - pane.pane.update(size: paneFrame.size, topInset: tabsHeight, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expansionFraction, presentationData: presentationData, synchronous: self.currentPaneKey == nil, transition: .immediate) + pane.pane.update(size: paneFrame.size, topInset: tabsHeight, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expansionFraction, presentationData: presentationData, synchronous: self.currentPaneKey == nil, transition: .immediate) if pane.isReady { self.pendingPanes.removeValue(forKey: key) @@ -922,7 +931,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat return } pane.isAnimatingOut = false - if let (_, _, _, _, _, _, data) = strongSelf.currentParams { + if let (_, _, _, _, _, _, _, data) = strongSelf.currentParams { if let availablePanes = data?.availablePanes, let currentPaneKey = strongSelf.currentPaneKey, let currentIndex = availablePanes.firstIndex(of: currentPaneKey), let paneIndex = availablePanes.firstIndex(of: key), abs(paneIndex - currentIndex) <= 1 { } else { if let pane = strongSelf.currentPanes.removeValue(forKey: key) { @@ -953,7 +962,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat paneCompletion() }) } - pane.update(size: paneFrame.size, topInset: tabsHeight, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expansionFraction, presentationData: presentationData, synchronous: paneWasAdded, transition: paneTransition) + pane.update(size: paneFrame.size, topInset: tabsHeight, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expansionFraction, presentationData: presentationData, synchronous: paneWasAdded, transition: paneTransition) } } @@ -1002,6 +1011,8 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat title = presentationData.strings.PeerInfo_PaneRecommended case .savedMessagesChats: title = presentationData.strings.DialogList_TabTitle + case .savedMessages: + title = presentationData.strings.PeerInfo_SavedMessagesTabTitle } return PeerInfoPaneSpecifier(key: key, title: title) }, selectedPane: self.currentPaneKey, transitionFraction: self.transitionFraction, transition: transition) @@ -1009,7 +1020,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat for (_, pane) in self.pendingPanes { let paneTransition: ContainedViewLayoutTransition = .immediate paneTransition.updateFrame(node: pane.pane.node, frame: paneFrame) - pane.pane.update(size: paneFrame.size, topInset: tabsHeight, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expansionFraction, presentationData: presentationData, synchronous: true, transition: paneTransition) + pane.pane.update(size: paneFrame.size, topInset: tabsHeight, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expansionFraction, presentationData: presentationData, synchronous: true, transition: paneTransition) } var removeKeys: [PeerInfoPaneKey] = [] diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 24db27103f0..d88fdf965d7 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -362,7 +362,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { }, stopMediaRecording: { }, lockMediaRecording: { }, deleteRecordedMedia: { - }, sendRecordedMedia: { _ in + }, sendRecordedMedia: { _, _ in }, displayRestrictedInfo: { _, _ in }, displayVideoUnmuteTip: { _ in }, switchMediaRecordingMode: { @@ -443,7 +443,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { self.backgroundNode.updateColor(color: presentationData.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) self.separatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor - let interfaceState = ChatPresentationInterfaceState(chatWallpaper: .color(0), theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, limitsConfiguration: .defaultValue, fontSize: .regular, bubbleCorners: PresentationChatBubbleCorners(mainRadius: 16.0, auxiliaryRadius: 8.0, mergeBubbleCorners: true), accountPeerId: self.context.account.peerId, mode: .standard(previewing: false), chatLocation: .peer(id: self.peerId), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil) + let interfaceState = ChatPresentationInterfaceState(chatWallpaper: .color(0), theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, limitsConfiguration: .defaultValue, fontSize: .regular, bubbleCorners: PresentationChatBubbleCorners(mainRadius: 16.0, auxiliaryRadius: 8.0, mergeBubbleCorners: true), accountPeerId: self.context.account.peerId, mode: .standard(.default), chatLocation: .peer(id: self.peerId), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil) let panelHeight = self.selectionPanel.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: layout.intrinsicInsets.bottom, additionalSideInsets: UIEdgeInsets(), maxHeight: 0.0, isSecondary: false, transition: transition, interfaceState: interfaceState, metrics: layout.metrics, isMediaInputExpanded: false) transition.updateFrame(node: self.selectionPanel, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: panelHeight))) @@ -535,6 +535,9 @@ private enum TopicsLimitedReason { } private final class PeerInfoInteraction { + // MARK: Nicegram TranslateBio + let translate: (String) -> Void + // let getPeerRegDate: (Int64, Int64) -> Void let openChat: () -> Void let openUsername: (String) -> Void @@ -591,6 +594,9 @@ private final class PeerInfoInteraction { let openEditing: () -> Void init( + // MARK: Nicegram TranslateBio + translate: @escaping (String) -> Void, + // getPeerRegDate: @escaping (Int64, Int64) -> Void, openUsername: @escaping (String) -> Void, openPhone: @escaping (String, ASDisplayNode, ContextGesture?) -> Void, @@ -646,6 +652,9 @@ private final class PeerInfoInteraction { openBotApp: @escaping (AttachMenuBot) -> Void, openEditing: @escaping () -> Void ) { + // MARK: Nicegram TranslateBio + self.translate = translate + // self.getPeerRegDate = getPeerRegDate self.openUsername = openUsername self.openPhone = openPhone @@ -1157,6 +1166,13 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese var isUser = false let lang = presentationData.strings.baseLanguageCode + // MARK: Nicegram TranslateBio + let bioIcon = PeerInfoScreenLabeledValueIcon.nicegram( + UIImage(bundleImageName: "NGTranslateIcon") + ) + let bioIconAction = interaction.translate + // + enum Section: Int, CaseIterable { case nicegram case groupLocation @@ -1246,7 +1262,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese interaction.requestLayout(false) })) } else if let about = cachedData.about, !about.isEmpty { - items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Profile_BotInfo, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.isPremium ? enabledPublicBioEntities : enabledPrivateBioEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: { + // MARK: Nicegram TranslateBio, add icon and iconAction + items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Profile_BotInfo, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.isPremium ? enabledPublicBioEntities : enabledPrivateBioEntities), icon: bioIcon, action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, iconAction: { bioIconAction(about) }, requestLayout: { interaction.requestLayout(false) })) } @@ -1452,7 +1469,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese if case .group = channel.info { enabledEntities = enabledPrivateBioEntities } - items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: presentationData.strings.Channel_Info_Description, text: aboutText, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: { + // MARK: Nicegram TranslateBio, add icon and iconAction + items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: presentationData.strings.Channel_Info_Description, text: aboutText, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledEntities), icon: bioIcon, action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, iconAction: { bioIconAction(aboutText) }, requestLayout: { interaction.requestLayout(true) })) } @@ -1505,7 +1523,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } if let aboutText = aboutText { - items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.Channel_Info_Description, text: aboutText, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledPrivateBioEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: { + // MARK: Nicegram TranslateBio, add icon and iconAction + items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.Channel_Info_Description, text: aboutText, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledPrivateBioEntities), icon: bioIcon, action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, iconAction: { bioIconAction(aboutText) }, requestLayout: { interaction.requestLayout(true) })) } @@ -1809,13 +1828,28 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL let colorImage = generateSettingsMenuPeerColorsLabelIcon(colors: colors) var boostIcon: UIImage? - var additionalBadge: String? if let approximateBoostLevel = channel.approximateBoostLevel, approximateBoostLevel < 1 { boostIcon = generateDisclosureActionBoostLevelBadgeImage(text: presentationData.strings.Channel_Info_BoostLevelPlusBadge("1").string) } else { - additionalBadge = presentationData.strings.Settings_New + let labelText = NSAttributedString(string: presentationData.strings.Settings_New, font: Font.medium(11.0), textColor: presentationData.theme.list.itemCheckColors.foregroundColor) + let labelBounds = labelText.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: [.usesLineFragmentOrigin], context: nil) + let labelSize = CGSize(width: ceil(labelBounds.width), height: ceil(labelBounds.height)) + let badgeSize = CGSize(width: labelSize.width + 8.0, height: labelSize.height + 2.0 + 1.0) + boostIcon = generateImage(badgeSize, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + let rect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height - UIScreenPixel * 2.0)) + + context.addPath(UIBezierPath(roundedRect: rect, cornerRadius: 5.0).cgPath) + context.setFillColor(presentationData.theme.list.itemCheckColors.fillColor.cgColor) + context.fillPath() + + UIGraphicsPushContext(context) + labelText.draw(at: CGPoint(x: 4.0, y: 1.0 + UIScreenPixel)) + UIGraphicsPopContext() + }) } - items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerColor, label: .image(colorImage, colorImage.size), additionalBadgeLabel: additionalBadge, additionalBadgeIcon: boostIcon, text: presentationData.strings.Channel_Info_AppearanceItem, icon: UIImage(bundleImageName: "Chat/Info/NameColorIcon"), action: { + items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerColor, label: .image(colorImage, colorImage.size), additionalBadgeIcon: boostIcon, text: presentationData.strings.Channel_Info_AppearanceItem, icon: UIImage(bundleImageName: "Chat/Info/NameColorIcon"), action: { interaction.editingOpenNameColorSetup() })) } @@ -2444,6 +2478,23 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.paneContainerNode.parentController = controller self._interaction = PeerInfoInteraction( + // MARK: Nicegram TranslateBio + translate: { [weak self] bio in + guard let self else { return } + + let (_, fromLanguage) = canTranslateText(context: context, text: bio, showTranslate: true, showTranslateIfTopical: false, ignoredLanguages: []) + + let controller = TranslateScreen(context: context, text: bio, canCopy: true, fromLanguage: fromLanguage, ignoredLanguages: []) + controller.pushController = { [weak self] c in + (self?.controller?.navigationController as? NavigationController)?._keepModalDismissProgress = true + self?.controller?.push(c) + } + controller.presentController = { [weak self] c in + self?.controller?.present(c, in: .window(.root)) + } + self.controller?.present(controller, in: .window(.root)) + }, + // getPeerRegDate: { [weak self] peerId, ownerId in self?.getPeerRegDate(peerId: peerId, ownerId: ownerId) }, @@ -2713,7 +2764,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if message.isCopyProtected() { - } else if message.id.peerId.namespace != Namespaces.Peer.SecretChat { + } else if message.id.peerId.namespace != Namespaces.Peer.SecretChat && message.minAutoremoveOrClearTimeout == nil { items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_ContextMenuForward, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { c, _ in c.dismiss(completion: { if let strongSelf = self { @@ -3184,7 +3235,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return } let presentationData = strongSelf.presentationData - let chatController = strongSelf.context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(previewing: true)) + let chatController = strongSelf.context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.previewing)) chatController.canReadHistory.set(false) let items: [ContextMenuItem] if recommended { @@ -5036,6 +5087,32 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } + // MARK: Nicegram ConfirmCall + private func requestCallConfirmation( + onCofirm: @escaping () -> Void + ) { + let alert = textAlertController( + context: self.context, + updatedPresentationData: self.controller?.updatedPresentationData, + title: nil, + text: l("ConfirmCall.Desc"), + actions: [ + TextAlertAction( + type: .genericAction, + title: self.presentationData.strings.Common_Cancel, + action: {} + ), + TextAlertAction( + type: .defaultAction, + title: self.presentationData.strings.PeerInfo_ButtonCall, + action: onCofirm + ) + ] + ) + self.controller?.present(alert, in: .window(.root)) + } + // + private func performButtonAction(key: PeerInfoHeaderButtonKey, gesture: ContextGesture?) { guard let controller = self.controller else { return @@ -5069,11 +5146,20 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }) } case .call: - self.requestCall(isVideo: false) + // MARK: Nicegram ConfirmCall, wrap in requestCallConfirmation + requestCallConfirmation { [weak self] in + self?.requestCall(isVideo: false) + } case .videoCall: - self.requestCall(isVideo: true) + // MARK: Nicegram ConfirmCall, wrap in requestCallConfirmation + requestCallConfirmation { [weak self] in + self?.requestCall(isVideo: true) + } case .voiceChat: - self.requestCall(isVideo: false, gesture: gesture) + // MARK: Nicegram ConfirmCall, wrap in requestCallConfirmation + requestCallConfirmation { [weak self] in + self?.requestCall(isVideo: false, gesture: gesture) + } case .mute: var displayCustomNotificationSettings = false @@ -5582,7 +5668,21 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } - strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in + if savedMessages, let self, action == .info { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.controller?.navigationController as? NavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) + }) + } + return false + }), in: .current) }) } } @@ -5614,7 +5714,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } if !foundController { - let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: strongSelf.peerId), subject: nil, botStart: nil, mode: .standard(previewing: false)) + let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: strongSelf.peerId), subject: nil, botStart: nil, mode: .standard(.default)) chatController.hintPlayNextOutgoingGift() controllers.append(chatController) } @@ -6139,7 +6239,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro private func openChatWithMessageSearch() { if let navigationController = (self.controller?.navigationController as? NavigationController) { if case let .replyThread(currentMessage) = self.chatLocation, let current = navigationController.viewControllers.first(where: { controller in - if let controller = controller as? ChatController, case let .replyThread(message) = controller.chatLocation, message.messageId == currentMessage.messageId { + if let controller = controller as? ChatController, case let .replyThread(message) = controller.chatLocation, message.peerId == currentMessage.peerId, message.threadId == currentMessage.threadId { return true } return false @@ -6464,7 +6564,21 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } - strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in + if savedMessages, let self, action == .info { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.controller?.navigationController as? NavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) + }) + } + return false + }), in: .current) }) } shareController.actionCompleted = { [weak self] in @@ -7331,7 +7445,21 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } - strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in + if savedMessages, let self, action == .info { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.controller?.navigationController as? NavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) + }) + } + return false + }), in: .current) }) } shareController.actionCompleted = { [weak self] in @@ -8896,9 +9024,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro case .language: push(LocalizationListController(context: self.context)) case .premium: - self.controller?.push(PremiumIntroScreen(context: self.context, modal: false, source: .settings)) + let controller = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .settings, forceDark: false, dismissed: nil) + self.controller?.push(controller) case .premiumGift: - let controller = self.context.sharedContext.makePremiumGiftController(context: self.context) + let controller = self.context.sharedContext.makePremiumGiftController(context: self.context, source: .settings) self.controller?.push(controller) case .stickers: if let settings = self.data?.globalSettings { @@ -8930,7 +9059,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } self.supportPeerDisposable.set((supportPeer.get() |> take(1) |> deliverOnMainQueue).startStrict(next: { [weak self] peerId in if let strongSelf = self, let peerId = peerId { - push(strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(previewing: false))) + push(strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(.default))) } })) })]), in: .window(.root)) @@ -9341,7 +9470,21 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } - strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in + if savedMessages, let self, action == .info { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.controller?.navigationController as? NavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) + }) + } + return false + }), in: .current) } peerSelectionController.peerSelected = { [weak self, weak peerSelectionController] peer, threadId in let peerId = peer.id @@ -9353,7 +9496,21 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: true, text: messageIds.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: true, text: messageIds.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many), elevatedLayout: false, animateInAsReplacement: true, action: { action in + if let self, action == .info { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.controller?.navigationController as? NavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) + }) + } + return false + }), in: .current) strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone, nil, nil) @@ -9417,7 +9574,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro proceed(chatController) }) } else { - let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: .none, botStart: nil, mode: .standard(previewing: false)) + let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: .none, botStart: nil, mode: .standard(.default)) proceed(chatController) } } @@ -9476,6 +9633,84 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }), cancel: { [weak self] in self?.deactivateSearch() }) + } else if let currentPaneKey = self.paneContainerNode.currentPaneKey, case .savedMessagesChats = currentPaneKey { + let contentNode = ChatListSearchContainerNode(context: self.context, animationCache: self.context.animationCache, animationRenderer: self.context.animationRenderer, filter: [.removeSearchHeader], requestPeerType: nil, location: .savedMessagesChats, displaySearchFilters: false, hasDownloads: false, initialFilter: .chats, openPeer: { [weak self] peer, _, _, _ in + guard let self else { + return + } + guard let navigationController = self.controller?.navigationController as? NavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams( + navigationController: navigationController, + context: self.context, + chatLocation: .replyThread(ChatReplyThreadMessage( + peerId: self.context.account.peerId, + threadId: peer.id.toInt64(), + channelMessageId: nil, + isChannelPost: false, + isForumPost: false, + maxMessage: nil, + maxReadIncomingMessageId: nil, + maxReadOutgoingMessageId: nil, + unreadCount: 0, + initialFilledHoles: IndexSet(), + initialAnchor: .automatic, + isNotAvailable: false + )), + subject: nil, + keepStack: .always + )) + }, openDisabledPeer: { _, _ in + }, openRecentPeerOptions: { _ in + }, openMessage: { [weak self] peer, threadId, messageId, deactivateOnAction in + guard let self else { + return + } + guard let navigationController = self.controller?.navigationController as? NavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams( + navigationController: navigationController, + context: self.context, + chatLocation: .replyThread(ChatReplyThreadMessage( + peerId: self.context.account.peerId, + threadId: peer.id.toInt64(), + channelMessageId: nil, + isChannelPost: false, + isForumPost: false, + maxMessage: nil, + maxReadIncomingMessageId: nil, + maxReadOutgoingMessageId: nil, + unreadCount: 0, + initialFilledHoles: IndexSet(), + initialAnchor: .automatic, + isNotAvailable: false + )), + subject: nil, + keepStack: .always + )) + }, addContact: { _ in + }, peerContextAction: nil, present: { [weak self] c, a in + guard let self else { + return + } + self.controller?.present(c, in: .window(.root), with: a) + }, presentInGlobalOverlay: { [weak self] c, a in + guard let self else { + return + } + self.controller?.presentInGlobalOverlay(c, with: a) + }, navigationController: self.controller?.navigationController as? NavigationController, parentController: { [weak self] in + guard let self else { + return nil + } + return self.controller + }) + + self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, mode: .list, placeholder: self.presentationData.strings.Common_Search, hasBackground: true, hasSeparator: true, contentNode: contentNode, cancel: { [weak self] in + self?.deactivateSearch() + }) } else { var tagMask: MessageTags = .file if let currentPaneKey = self.paneContainerNode.currentPaneKey { @@ -9795,7 +10030,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro )) }))) - let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: strongSelf.peerId), subject: .message(id: .id(index.id), highlight: nil, timecode: nil), botStart: nil, mode: .standard(previewing: true)) + let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: strongSelf.peerId), subject: .message(id: .id(index.id), highlight: nil, timecode: nil), botStart: nil, mode: .standard(.previewing)) chatController.canReadHistory.set(false) let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, sourceRect: sourceRect, passthroughTouches: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) strongSelf.controller?.presentInGlobalOverlay(contextController) @@ -10309,7 +10544,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } let navigationBarHeight: CGFloat = !self.isSettings && layout.isModalOverlay ? 56.0 : 44.0 - self.paneContainerNode.update(size: self.paneContainerNode.bounds.size, sideInset: layout.safeInsets.left, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: effectiveAreaExpansionFraction, presentationData: self.presentationData, data: self.data, transition: transition) + self.paneContainerNode.update(size: self.paneContainerNode.bounds.size, sideInset: layout.safeInsets.left, bottomInset: bottomInset, deviceMetrics: layout.deviceMetrics, visibleHeight: visibleHeight, expansionFraction: effectiveAreaExpansionFraction, presentationData: self.presentationData, data: self.data, transition: transition) transition.updateFrame(node: self.headerNode.navigationButtonContainer, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left, y: layout.statusBarHeight ?? 0.0), size: CGSize(width: layout.size.width - layout.safeInsets.left * 2.0, height: navigationBarHeight))) @@ -10334,7 +10569,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if self.state.selectedMessageIds == nil { if let currentPaneKey = self.paneContainerNode.currentPaneKey { switch currentPaneKey { - case .files, .music, .links, .members: + case .files, .music, .links, .members, .savedMessagesChats: rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .search, isForExpandedView: true)) case .media: rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .more, isForExpandedView: true)) @@ -11168,7 +11403,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc let navigateChatLocation: NavigateToChatControllerParams.Location if let threadId = item.threadId { navigateChatLocation = .replyThread(ChatReplyThreadMessage( - messageId: MessageId(peerId: item.peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false + peerId: item.peerId, threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false )) } else { navigateChatLocation = .peer(itemPeer) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift index b1b19c96058..cb020bae7e4 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift @@ -482,6 +482,7 @@ final class PeerInfoStoryGridScreenComponent: Component { topInset: environment.navigationHeight, sideInset: environment.safeInsets.left, bottomInset: bottomInset, + deviceMetrics: environment.deviceMetrics, visibleHeight: availableSize.height, isScrollingLockedAtTop: false, expandProgress: 1.0, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 79821d32741..7491954a5df 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -950,7 +950,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr public private(set) var isSelectionModeActive: Bool - private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData)? + private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData)? private let ready = Promise() private var didSetReady: Bool = false @@ -1730,12 +1730,12 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr private func updateHistory(items: SparseItemGrid.Items, synchronous: Bool, reloadAtTop: Bool) { self.items = items - if let (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) = self.currentParams { + if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) = self.currentParams { var gridSnapshot: UIView? if reloadAtTop { gridSnapshot = self.itemGrid.view.snapshotView(afterScreenUpdates: false) } - self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: false, transition: .immediate) + self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: false, transition: .immediate) self.updateSelectedItems(animated: false) if let gridSnapshot = gridSnapshot { self.view.addSubview(gridSnapshot) @@ -2006,8 +2006,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } } - public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { - self.currentParams = (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) + public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + self.currentParams = (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) transition.updateFrame(node: self.contextGestureContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift index b068285ea57..eb6d94202c1 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift @@ -1101,7 +1101,7 @@ public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, return self._itemInteraction! } - private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData)? + private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData)? private let ready = Promise() private var didSetReady: Bool = false @@ -1605,7 +1605,7 @@ public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, self.presentationDataDisposable = (self.context.sharedContext.presentationData |> deliverOnMainQueue).start(next: { [weak self] presentationData in - guard let strongSelf = self, let (size, topInset, sideInset, bottomInset, _, _, _, _) = strongSelf.currentParams else { + guard let strongSelf = self, let (size, topInset, sideInset, bottomInset, _, _, _, _, _) = strongSelf.currentParams else { return } strongSelf.itemGridBinding.updatePresentationData(presentationData: presentationData) @@ -1742,12 +1742,12 @@ public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, private func updateHistory(items: SparseItemGrid.Items, synchronous: Bool, reloadAtTop: Bool) { self.items = items - if let (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) = self.currentParams { + if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) = self.currentParams { var gridSnapshot: UIView? if reloadAtTop { gridSnapshot = self.itemGrid.view.snapshotView(afterScreenUpdates: false) } - self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: false, transition: .immediate) + self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: false, transition: .immediate) if let gridSnapshot = gridSnapshot { self.view.addSubview(gridSnapshot) gridSnapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak gridSnapshot] _ in @@ -2036,7 +2036,7 @@ public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, switch self.contentType { case .files, .music, .voiceAndVideoMessages: self.itemGrid.forEachVisibleItem { item in - guard let itemView = item.view as? ItemView, let (size, topInset, sideInset, bottomInset, _, _, _, _) = self.currentParams else { + guard let itemView = item.view as? ItemView, let (size, topInset, sideInset, bottomInset, _, _, _, _, _) = self.currentParams else { return } if let item = itemView.item { @@ -2093,8 +2093,8 @@ public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, } } - public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { - self.currentParams = (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) + public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + self.currentParams = (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) transition.updateFrame(node: self.contextGestureContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index 78772f9c8a6..1765d9efc12 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -124,7 +124,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.animationCache = context.animationCache self.animationRenderer = context.animationRenderer - self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(previewing: false), chatLocation: .peer(id: PeerId(0)), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil) + self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(.default), chatLocation: .peer(id: PeerId(0)), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil) self.presentationInterfaceState = self.presentationInterfaceState.updatedInterfaceState { $0.withUpdatedForwardMessageIds(forwardedMessageIds) } self.presentationInterfaceStatePromise.set(self.presentationInterfaceState) @@ -373,7 +373,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { chatLocation: .peer(id: strongSelf.context.account.peerId), subject: .messageOptions(peerIds: peerIds, ids: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [], info: .forward(ChatControllerSubject.MessageOptionsInfo.Forward(options: forwardOptions))), botStart: nil, - mode: .standard(previewing: true) + mode: .standard(.previewing) ) chatController.canReadHistory.set(false) @@ -594,7 +594,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { }, stopMediaRecording: { }, lockMediaRecording: { }, deleteRecordedMedia: { - }, sendRecordedMedia: { _ in + }, sendRecordedMedia: { _, _ in }, displayRestrictedInfo: { _, _ in }, displayVideoUnmuteTip: { _ in }, switchMediaRecordingMode: { diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift index ef33770dfc7..c7b18fc0384 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift @@ -333,6 +333,13 @@ final class ChannelAppearanceScreenComponent: Component { } if !resolvedState.changes.isEmpty { + if let premiumConfiguration = self.premiumConfiguration, let requiredBoostSubject = self.requiredBoostSubject{ + let requiredLevel = requiredBoostSubject.requiredLevel(context: component.context, configuration: premiumConfiguration) + if let boostLevel = self.boostLevel, requiredLevel > boostLevel { + return true + } + } + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.Channel_Appearance_UnsavedChangesAlertTitle, text: presentationData.strings.Channel_Appearance_UnsavedChangesAlertText, actions: [ TextAlertAction(type: .genericAction, title: presentationData.strings.Channel_Appearance_UnsavedChangesAlertDiscard, action: { [weak self] in diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift index 31335793745..90a0f7db35b 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift @@ -236,7 +236,7 @@ final class PeerNameColorChatPreviewItemNode: ListViewItemNode { } let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: messageItem.outgoing ? [] : [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[authorPeerId], text: messageItem.text, attributes: messageItem.reply != nil ? [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false)] : [], media: media, peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false)) + items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true)) } var nodes: [ListViewItemNode] = [] diff --git a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift index 603c48c9ac1..cac1f69208c 100644 --- a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift +++ b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift @@ -292,7 +292,7 @@ class ReactionChatPreviewItemNode: ListViewItemNode { attributes.append(ReactionsMessageAttribute(canViewList: false, reactions: [MessageReaction(value: reaction, count: 1, chosenOrder: 0)], recentPeers: recentPeers)) } - let messageItem = item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: chatPeerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[userPeerId], text: messageText, attributes: attributes, media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])], theme: item.theme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: item.availableReactions, accountPeer: item.accountPeer, isCentered: true) + let messageItem = item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: chatPeerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[userPeerId], text: messageText, attributes: attributes, media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])], theme: item.theme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: item.availableReactions, accountPeer: item.accountPeer, isCentered: true, isPreview: true) var node: ListViewItemNode? if let current = currentNode { diff --git a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift index a23fef5c55b..52018357b51 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift @@ -1087,7 +1087,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate return state }, animated: true) }, clickThroughMessage: { - }, backgroundNode: self.backgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false) + }, backgroundNode: self.backgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true) return item } diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift index 84ecd5cac6d..2a423e6c830 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift @@ -1622,19 +1622,19 @@ final class WallpaperGalleryItemNode: GalleryItemNode { if !bottomMessageText.isEmpty { let message1 = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: bottomMessageText, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: theme, strings: self.presentationData.strings, wallpaper: currentWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.nativeNode, availableReactions: nil, accountPeer: nil, isCentered: false)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: theme, strings: self.presentationData.strings, wallpaper: currentWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.nativeNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true)) } let message2 = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: topMessageText, attributes: messageAttributes, media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: theme, strings: self.presentationData.strings, wallpaper: currentWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.nativeNode, availableReactions: nil, accountPeer: nil, isCentered: false)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: theme, strings: self.presentationData.strings, wallpaper: currentWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.nativeNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true)) if let serviceMessageText { let attributedText = convertMarkdownToAttributes(NSAttributedString(string: serviceMessageText)) let entities = generateChatInputTextEntities(attributedText) let message3 = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66002, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [TelegramMediaAction(action: .customText(text: attributedText.string, entities: entities, additionalAttributes: nil))], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message3], theme: theme, strings: self.presentationData.strings, wallpaper: currentWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.nativeNode, availableReactions: nil, accountPeer: nil, isCentered: false)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message3], theme: theme, strings: self.presentationData.strings, wallpaper: currentWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.nativeNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true)) } let params = ListViewItemLayoutParams(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, availableHeight: layout.size.height) diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift index 28cd361f2fe..3e2661e6f35 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift @@ -2564,7 +2564,7 @@ final class StorageUsageScreenComponent: Component { var chatLocation: NavigateToChatControllerParams.Location = .peer(peer) if case let .channel(channel) = peer, channel.flags.contains(.isForum), let threadId = message.threadId { - chatLocation = .replyThread(ChatReplyThreadMessage(messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)) + chatLocation = .replyThread(ChatReplyThreadMessage(peerId: peer.id, threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)) } component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams( @@ -2667,7 +2667,7 @@ final class StorageUsageScreenComponent: Component { var chatLocation: NavigateToChatControllerParams.Location = .peer(peer) if case let .channel(channel) = peer, channel.flags.contains(.isForum), let threadId = message.threadId { - chatLocation = .replyThread(ChatReplyThreadMessage(messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)) + chatLocation = .replyThread(ChatReplyThreadMessage(peerId: peer.id, threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)) } component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams( diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index ad342faa281..6468c504e22 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -996,7 +996,7 @@ public final class StoryItemSetContainerComponent: Component { } if let selectedMediaArea { - self.sendMessageContext.activateMediaArea(view: self, mediaArea: selectedMediaArea) + self.sendMessageContext.activateMediaArea(view: self, mediaArea: selectedMediaArea, position: point) } else { var direction: NavigationDirection? if point.x < itemLayout.containerSize.width * 0.25 { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index fc56f247b9e..8824f085646 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -259,7 +259,7 @@ final class StoryItemSetContainerSendMessage { fontSize: presentationData.chatFontSize, bubbleCorners: presentationData.chatBubbleCorners, accountPeerId: context.account.peerId, - mode: .standard(previewing: false), + mode: .standard(.default), chatLocation: .peer(id: context.account.peerId), subject: nil, peerNearbyData: nil, @@ -1041,13 +1041,11 @@ final class StoryItemSetContainerSendMessage { immediateExternalShare: false, forceTheme: defaultDarkColorPresentationTheme ) - if !component.slice.peer.isService { - shareController.shareStory = { [weak view] in - guard let view else { - return - } - view.openStoryEditing(repost: true) + shareController.shareStory = { [weak view] in + guard let view else { + return } + view.openStoryEditing(repost: true) } shareController.completed = { [weak view] peerIds in guard let view, let component = view.component else { @@ -1092,13 +1090,28 @@ final class StoryItemSetContainerSendMessage { } if let controller = component.controller() { + let context = component.context let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } controller.present(UndoOverlayController( presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: false, - action: { _ in return false } + action: { [weak controller] action in + if savedMessages, action == .info { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { peer in + guard let controller, let peer else { + return + } + guard let navigationController = controller.navigationController as? NavigationController else { + return + } + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer))) + }) + } + return false + } ), in: .current) } }) @@ -3266,7 +3279,7 @@ final class StoryItemSetContainerSendMessage { } private var selectedMediaArea: MediaArea? - func activateMediaArea(view: StoryItemSetContainerComponent.View, mediaArea: MediaArea, immediate: Bool = false) { + func activateMediaArea(view: StoryItemSetContainerComponent.View, mediaArea: MediaArea, position: CGPoint? = nil, immediate: Bool = false) { guard let component = view.component, let controller = component.controller() else { return } @@ -3276,6 +3289,8 @@ final class StoryItemSetContainerSendMessage { let context = component.context + var useGesturePosition = false + var actions: [ContextMenuAction] = [] switch mediaArea { case let .venue(_, venue): @@ -3312,6 +3327,7 @@ final class StoryItemSetContainerSendMessage { action() })) case let .channelMessage(_, messageId): + useGesturePosition = true let action = { [weak self, weak view, weak controller] in let _ = ((context.engine.messages.getMessagesLoadIfNecessary([messageId], strategy: .cloud(skipLocal: true)) |> mapToSignal { result -> Signal in @@ -3371,6 +3387,10 @@ final class StoryItemSetContainerSendMessage { var frame = CGRect(x: mediaArea.coordinates.x / 100.0 * referenceSize.width - size.width / 2.0, y: mediaArea.coordinates.y / 100.0 * referenceSize.height - size.height / 2.0, width: size.width, height: size.height) frame = view.controlsContainerView.convert(frame, to: nil) + if useGesturePosition, let position { + frame = CGRect(origin: position.offsetBy(dx: 0.0, dy: 44.0), size: .zero) + } + let node = controller.displayNode let menuController = makeContextMenuController(actions: actions, blurred: true) menuController.centerHorizontally = true diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift index ddbb3b83ee5..4693f3baf5d 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift @@ -584,6 +584,9 @@ final class StoryItemSetViewListComponent: Component { guard let self, let component = self.component else { return } + guard peer.id != component.context.account.peerId else { + return + } if let messageId { component.openMessage(peer, messageId) } else if let storyItem, let sourceView { @@ -592,7 +595,7 @@ final class StoryItemSetViewListComponent: Component { component.openPeer(peer) } }, - contextAction: { peer, view, gesture in + contextAction: component.peerId.isGroupOrChannel || item.peer.id == component.context.account.peerId ? nil : { peer, view, gesture in component.peerContextAction(peer, view, gesture) }, openStories: { [weak self] peer, avatarNode in @@ -954,7 +957,7 @@ final class StoryItemSetViewListComponent: Component { } var premiumFooterSize: CGSize? - if self.configuration.listMode == .everyone, let viewListState = self.viewListState, viewListState.loadMoreToken == nil, !viewListState.items.isEmpty, let views = component.storyItem.views, views.seenCount > viewListState.totalCount, component.storyItem.expirationTimestamp <= Int32(Date().timeIntervalSince1970) { + if self.configuration.listMode == .everyone, let viewListState = self.viewListState, viewListState.loadMoreToken == nil, !viewListState.items.isEmpty, let views = component.storyItem.views, views.seenCount > viewListState.totalCount, component.storyItem.expirationTimestamp <= Int32(Date().timeIntervalSince1970), !component.peerId.isGroupOrChannel { let premiumFooterText: ComponentView if let current = self.premiumFooterText { premiumFooterText = current diff --git a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift index 1b1b481aa49..39d580dace1 100644 --- a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift @@ -400,7 +400,7 @@ public final class StoryFooterPanelComponent: Component { var displayViewLists = false if case let .channel(channel) = component.peer, channel.flags.contains(.isCreator) || component.canViewChannelStats { - displayViewLists = reactionCount != 0 || forwardCount != 0 + displayViewLists = true } else { displayViewLists = viewCount != 0 && !component.isChannel } diff --git a/submodules/TelegramUI/Images.xcassets/Avatar/AnonymousSenderIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Avatar/AnonymousSenderIcon.imageset/Contents.json new file mode 100644 index 00000000000..80053edc1fa --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Avatar/AnonymousSenderIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "large_hidden 1.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Avatar/AnonymousSenderIcon.imageset/large_hidden 1.pdf b/submodules/TelegramUI/Images.xcassets/Avatar/AnonymousSenderIcon.imageset/large_hidden 1.pdf new file mode 100644 index 00000000000..933e557a6b0 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Avatar/AnonymousSenderIcon.imageset/large_hidden 1.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallNotificationAnswerIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/CallNotificationAnswerIcon.imageset/Contents.json new file mode 100644 index 00000000000..0ae3b213236 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Call/CallNotificationAnswerIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "phone.fill.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallNotificationAnswerIcon.imageset/phone.fill.svg b/submodules/TelegramUI/Images.xcassets/Call/CallNotificationAnswerIcon.imageset/phone.fill.svg new file mode 100644 index 00000000000..ccfed91796c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Call/CallNotificationAnswerIcon.imageset/phone.fill.svg @@ -0,0 +1,3 @@ + + + diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallNotificationDeclineIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/CallNotificationDeclineIcon.imageset/Contents.json new file mode 100644 index 00000000000..47bbf25854a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Call/CallNotificationDeclineIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "phone.down.fill.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallNotificationDeclineIcon.imageset/phone.down.fill.svg b/submodules/TelegramUI/Images.xcassets/Call/CallNotificationDeclineIcon.imageset/phone.down.fill.svg new file mode 100644 index 00000000000..4b1eb771db8 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Call/CallNotificationDeclineIcon.imageset/phone.down.fill.svg @@ -0,0 +1,3 @@ + + + diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/ViewOnce.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/ViewOnce.imageset/Contents.json new file mode 100644 index 00000000000..1e4d392fee8 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/ViewOnce.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "viewonceonplay_20 (2).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/ViewOnce.imageset/viewonceonplay_20 (2).pdf b/submodules/TelegramUI/Images.xcassets/Chat/Message/ViewOnce.imageset/viewonceonplay_20 (2).pdf new file mode 100644 index 00000000000..55d2817d04f Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Message/ViewOnce.imageset/viewonceonplay_20 (2).pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnceEnabled.imageset/1filled.pdf b/submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnceEnabled.imageset/1filled.pdf new file mode 100644 index 00000000000..19ddecd42f3 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnceEnabled.imageset/1filled.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnceEnabled.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnceEnabled.imageset/Contents.json new file mode 100644 index 00000000000..bf64e7bd8f7 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnceEnabled.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "1filled.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Resources/Animations/anim_flame_1.tgs b/submodules/TelegramUI/Resources/Animations/anim_flame_1.tgs new file mode 100644 index 00000000000..835556dd0b3 Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/anim_flame_1.tgs differ diff --git a/submodules/TelegramUI/Resources/Animations/anim_flame_2.tgs b/submodules/TelegramUI/Resources/Animations/anim_flame_2.tgs new file mode 100644 index 00000000000..01661129813 Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/anim_flame_2.tgs differ diff --git a/submodules/TelegramUI/Sources/AccountContext.swift b/submodules/TelegramUI/Sources/AccountContext.swift index 5441f218b8f..a7dce56b2ec 100644 --- a/submodules/TelegramUI/Sources/AccountContext.swift +++ b/submodules/TelegramUI/Sources/AccountContext.swift @@ -477,11 +477,11 @@ public final class AccountContextImpl: AccountContext { case let .peer(peerId): return .peer(peerId: peerId, threadId: nil) case let .replyThread(data): - if data.isForumPost { - return .peer(peerId: data.messageId.peerId, threadId: Int64(data.messageId.id)) + if data.isForumPost || data.peerId.namespace != Namespaces.Peer.CloudChannel { + return .peer(peerId: data.peerId, threadId: data.threadId) } else { let context = chatLocationContext(holder: contextHolder, account: self.account, data: data) - return .thread(peerId: data.messageId.peerId, threadId: makeMessageThreadId(data.messageId), data: context.state) + return .thread(peerId: data.peerId, threadId: data.threadId, data: context.state) } case .feed: preconditionFailure() @@ -494,7 +494,7 @@ public final class AccountContextImpl: AccountContext { return .single(nil) case let .replyThread(data): if data.isForumPost, let peerId = location.peerId { - let viewKey: PostboxViewKey = .messageHistoryThreadInfo(peerId: data.messageId.peerId, threadId: Int64(data.messageId.id)) + let viewKey: PostboxViewKey = .messageHistoryThreadInfo(peerId: data.peerId, threadId: data.threadId) return self.account.postbox.combinedView(keys: [viewKey]) |> map { views -> MessageId? in if let threadInfo = views.views[viewKey] as? MessageHistoryThreadInfoView, let data = threadInfo.info?.data.get(MessageHistoryThreadData.self) { @@ -503,9 +503,11 @@ public final class AccountContextImpl: AccountContext { return nil } } - } else { + } else if data.peerId.namespace == Namespaces.Peer.CloudChannel { let context = chatLocationContext(holder: contextHolder, account: self.account, data: data) return context.maxReadOutgoingMessageId + } else { + return .single(nil) } case .feed: return .single(nil) @@ -530,7 +532,7 @@ public final class AccountContextImpl: AccountContext { } case let .replyThread(data): if data.isForumPost { - let viewKey: PostboxViewKey = .messageHistoryThreadInfo(peerId: data.messageId.peerId, threadId: Int64(data.messageId.id)) + let viewKey: PostboxViewKey = .messageHistoryThreadInfo(peerId: data.peerId, threadId: data.threadId) return self.account.postbox.combinedView(keys: [viewKey]) |> map { views -> Int in if let threadInfo = views.views[viewKey] as? MessageHistoryThreadInfoView, let data = threadInfo.info?.data.get(MessageHistoryThreadData.self) { @@ -539,6 +541,8 @@ public final class AccountContextImpl: AccountContext { return 0 } } + } else if data.peerId.namespace != Namespaces.Peer.CloudChannel { + return .single(0) } else { let context = chatLocationContext(holder: contextHolder, account: self.account, data: data) return context.unreadCount @@ -722,7 +726,7 @@ private final class ChatLocationReplyContextHolderImpl: ChatLocationContextHolde let context: ReplyThreadHistoryContext init(account: Account, data: ChatReplyThreadMessage) { - self.context = ReplyThreadHistoryContext(account: account, peerId: data.messageId.peerId, data: data) + self.context = ReplyThreadHistoryContext(account: account, peerId: data.peerId, data: data) } } diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 82a50b5fda9..fcef84238fd 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -908,9 +908,9 @@ private class UserInterfaceStyleObserverWindow: UIWindow { } icons.append(PresentationAppIcon(name: "Premium", imageName: "Premium", isPremium: true)) - icons.append(PresentationAppIcon(name: "PremiumBlack", imageName: "PremiumBlack", isPremium: true)) icons.append(PresentationAppIcon(name: "PremiumTurbo", imageName: "PremiumTurbo", isPremium: true)) - + icons.append(PresentationAppIcon(name: "PremiumBlack", imageName: "PremiumBlack", isPremium: true)) + return icons } else { return [] @@ -1019,7 +1019,7 @@ private class UserInterfaceStyleObserverWindow: UIWindow { } var setPresentationCall: ((PresentationCall?) -> Void)? - let sharedContext = SharedAccountContextImpl(mainWindow: self.mainWindow, sharedContainerPath: legacyBasePath, basePath: rootPath, encryptionParameters: encryptionParameters, accountManager: accountManager, appLockContext: appLockContext, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings, networkArguments: networkArguments, hasInAppPurchases: buildConfig.isAppStoreBuild && buildConfig.apiId == 1, rootPath: rootPath, legacyBasePath: legacyBasePath, apsNotificationToken: self.notificationTokenPromise.get() |> map(Optional.init), voipNotificationToken: self.voipTokenPromise.get() |> map(Optional.init), firebaseSecretStream: self.firebaseSecretStream.get(), setNotificationCall: { call in + let sharedContext = SharedAccountContextImpl(mainWindow: self.mainWindow, sharedContainerPath: legacyBasePath, basePath: rootPath, encryptionParameters: encryptionParameters, accountManager: accountManager, appLockContext: appLockContext, notificationController: nil, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings, networkArguments: networkArguments, hasInAppPurchases: buildConfig.isAppStoreBuild && buildConfig.apiId == 1, rootPath: rootPath, legacyBasePath: legacyBasePath, apsNotificationToken: self.notificationTokenPromise.get() |> map(Optional.init), voipNotificationToken: self.voipTokenPromise.get() |> map(Optional.init), firebaseSecretStream: self.firebaseSecretStream.get(), setNotificationCall: { call in setPresentationCall?(call) }, navigateToChat: { accountId, peerId, messageId in self.openChatWhenReady(accountId: accountId, peerId: peerId, threadId: nil, messageId: messageId, storyId: nil) @@ -1454,6 +1454,7 @@ private class UserInterfaceStyleObserverWindow: UIWindow { return true }) self.mainWindow.topLevelOverlayControllers = [context.sharedApplicationContext.overlayMediaController, context.notificationController] + (context.context.sharedContext as? SharedAccountContextImpl)?.notificationController = context.notificationController var authorizeNotifications = true if #available(iOS 10.0, *) { authorizeNotifications = false diff --git a/submodules/TelegramUI/Sources/ApplicationContext.swift b/submodules/TelegramUI/Sources/ApplicationContext.swift index 3426f8ca31d..5a1a4b96664 100644 --- a/submodules/TelegramUI/Sources/ApplicationContext.swift +++ b/submodules/TelegramUI/Sources/ApplicationContext.swift @@ -28,6 +28,7 @@ import TelegramCallsUI import AuthorizationUI import ChatListUI import StoryContainerScreen +import ChatMessageNotificationItem final class UnauthorizedApplicationContext { let sharedContext: SharedAccountContextImpl @@ -313,7 +314,7 @@ final class AuthorizedApplicationContext { let chatLocation: NavigateToChatControllerParams.Location if let _ = threadData, let threadId = firstMessage.threadId { chatLocation = .replyThread(ChatReplyThreadMessage( - messageId: MessageId(peerId: firstMessage.id.peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false + peerId: firstMessage.id.peerId, threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false ).normalized) } else { guard let peer = firstMessage.peers[firstMessage.id.peerId] else { @@ -905,7 +906,7 @@ final class AuthorizedApplicationContext { let chatLocation: NavigateToChatControllerParams.Location if let threadId = threadId { chatLocation = .replyThread(ChatReplyThreadMessage( - messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false + peerId: peerId, threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false )) } else { chatLocation = .peer(peer) diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift index f33d0ca9d4e..09c69f5f42f 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift @@ -10,6 +10,7 @@ import ChatControllerInteraction import OverlayStatusController import TelegramPresentationData import PresentationDataUtils +import UndoUI extension ChatControllerImpl { func navigateToMessage( @@ -18,9 +19,9 @@ extension ChatControllerImpl { params: NavigateToMessageParams ) { var id = id - if case let .replyThread(message) = self.chatLocation { + if case let .replyThread(message) = self.chatLocation, let effectiveMessageId = message.effectiveMessageId { if let channelMessageId = message.channelMessageId, id == channelMessageId { - id = message.messageId + id = effectiveMessageId } } @@ -130,11 +131,13 @@ extension ChatControllerImpl { let navigateToLocation: NavigateToChatControllerParams.Location if let message = messages.first, let threadId = message.threadId, let channel = message.peers[message.id.peerId] as? TelegramChannel, channel.flags.contains(.isForum) { - navigateToLocation = .replyThread(ChatReplyThreadMessage(messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)) + navigateToLocation = .replyThread(ChatReplyThreadMessage(peerId: peer.id, threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)) } else { navigateToLocation = .peer(peer) } self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: navigateToLocation, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), keepStack: .always)) + + completion?() }) } else if case let .peer(peerId) = self.chatLocation, let messageId = messageLocation.messageId, (messageId.peerId != peerId && !forceInCurrentChat) || (isScheduledMessages && messageId.id != 0 && !Namespaces.Message.allScheduled.contains(messageId.namespace)) { let _ = (self.context.engine.data.get( @@ -147,8 +150,13 @@ extension ChatControllerImpl { } if let navigationController = self.effectiveNavigationController { var chatLocation: NavigateToChatControllerParams.Location = .peer(peer) - if case let .channel(channel) = peer, channel.flags.contains(.isForum), let message = message, let threadId = message.threadId { - chatLocation = .replyThread(ChatReplyThreadMessage(messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)) + var displayMessageNotFoundToast = false + if case let .channel(channel) = peer, channel.flags.contains(.isForum) { + if let message = message, let threadId = message.threadId { + chatLocation = .replyThread(ChatReplyThreadMessage(peerId: peer.id, threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)) + } else { + displayMessageNotFoundToast = true + } } var quote: ChatControllerSubject.MessageHighlight.Quote? @@ -156,8 +164,18 @@ extension ChatControllerImpl { quote = params.quote.flatMap { quote in ChatControllerSubject.MessageHighlight.Quote(string: quote.string, offset: quote.offset) } } - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: chatLocation, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: quote), timecode: nil), keepStack: .always)) + let context = self.context + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: chatLocation, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: quote), timecode: nil), keepStack: .always, chatListCompletion: { chatListController in + if displayMessageNotFoundToast { + let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) + chatListController.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Conversation_MessageDoesntExist, timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in + return true + }), in: .current) + } + })) } + + completion?() }) } else if forceInCurrentChat { if let _ = fromId, let fromIndex = fromIndex, rememberInStack { @@ -221,7 +239,7 @@ extension ChatControllerImpl { let searchLocation: ChatHistoryInitialSearchLocation switch messageLocation { case let .id(id, _): - if case let .replyThread(message) = self.chatLocation, id == message.messageId { + if case let .replyThread(message) = self.chatLocation, id == message.effectiveMessageId { searchLocation = .index(.absoluteLowerBound()) } else { searchLocation = .id(id) @@ -308,17 +326,19 @@ extension ChatControllerImpl { |> deliverOnMainQueue).startStrict(next: { [weak self] index in if let strongSelf = self, let index = index.0 { strongSelf.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: index, animated: animated, quote: quote, scrollPosition: scrollPosition) - completion?() } else if index.1 { if !progressStarted { progressStarted = true progressDisposable.set(progressSignal.start()) } + } else if let strongSelf = self { + strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.presentationData.strings.Conversation_MessageDoesntExist, timeout: nil, customUndoText: nil)) } }, completed: { [weak self] in if let strongSelf = self { strongSelf.loadingMessage.set(.single(nil)) } + completion?() })) cancelImpl = { [weak self] in if let strongSelf = self { @@ -387,7 +407,7 @@ extension ChatControllerImpl { quote = params.quote.flatMap { quote in ChatControllerSubject.MessageHighlight.Quote(string: quote.string, offset: quote.offset) } } - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), subject: messageLocation.messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: quote), timecode: nil) })) + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), subject: messageLocation.messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: quote), timecode: nil) }, keepStack: .always)) } }) completion?() diff --git a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift index c82be4bbb9a..12bfa12d891 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift @@ -100,7 +100,7 @@ private func chatForwardOptions(selfController: ChatControllerImpl, sourceNode: } |> distinctUntilChanged - let chatController = selfController.context.sharedContext.makeChatController(context: selfController.context, chatLocation: .peer(id: peerId), subject: .messageOptions(peerIds: [peerId], ids: selfController.presentationInterfaceState.interfaceState.forwardMessageIds ?? [], info: .forward(ChatControllerSubject.MessageOptionsInfo.Forward(options: forwardOptions))), botStart: nil, mode: .standard(previewing: true)) + let chatController = selfController.context.sharedContext.makeChatController(context: selfController.context, chatLocation: .peer(id: peerId), subject: .messageOptions(peerIds: [peerId], ids: selfController.presentationInterfaceState.interfaceState.forwardMessageIds ?? [], info: .forward(ChatControllerSubject.MessageOptionsInfo.Forward(options: forwardOptions))), botStart: nil, mode: .standard(.previewing)) chatController.canReadHistory.set(false) let messageIds = selfController.presentationInterfaceState.interfaceState.forwardMessageIds ?? [] @@ -417,7 +417,9 @@ private func generateChatReplyOptionItems(selfController: ChatControllerImpl, ch if message.id.peerId.namespace == Namespaces.Peer.SecretChat { canReplyInAnotherChat = false } - + if message.minAutoremoveOrClearTimeout == viewOnceTimeout { + canReplyInAnotherChat = false + } } if canReplyInAnotherChat { @@ -508,7 +510,7 @@ private func chatReplyOptions(selfController: ChatControllerImpl, sourceNode: AS } |> distinctUntilChanged) - guard let chatController = selfController.context.sharedContext.makeChatController(context: selfController.context, chatLocation: .peer(id: peerId), subject: .messageOptions(peerIds: [replySubject.messageId.peerId], ids: [replySubject.messageId], info: .reply(ChatControllerSubject.MessageOptionsInfo.Reply(quote: replyQuote, selectionState: selectionState))), botStart: nil, mode: .standard(previewing: true)) as? ChatControllerImpl else { + guard let chatController = selfController.context.sharedContext.makeChatController(context: selfController.context, chatLocation: .peer(id: peerId), subject: .messageOptions(peerIds: [replySubject.messageId.peerId], ids: [replySubject.messageId], info: .reply(ChatControllerSubject.MessageOptionsInfo.Reply(quote: replyQuote, selectionState: selectionState))), botStart: nil, mode: .standard(.previewing)) as? ChatControllerImpl else { return nil } chatController.canReadHistory.set(false) @@ -736,7 +738,7 @@ private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASD } |> distinctUntilChanged - guard let chatController = selfController.context.sharedContext.makeChatController(context: selfController.context, chatLocation: .peer(id: peerId), subject: .messageOptions(peerIds: [peerId], ids: selfController.presentationInterfaceState.interfaceState.forwardMessageIds ?? [], info: .link(ChatControllerSubject.MessageOptionsInfo.Link(options: linkOptions))), botStart: nil, mode: .standard(previewing: true)) as? ChatControllerImpl else { + guard let chatController = selfController.context.sharedContext.makeChatController(context: selfController.context, chatLocation: .peer(id: peerId), subject: .messageOptions(peerIds: [peerId], ids: selfController.presentationInterfaceState.interfaceState.forwardMessageIds ?? [], info: .link(ChatControllerSubject.MessageOptionsInfo.Link(options: linkOptions))), botStart: nil, mode: .standard(.previewing)) as? ChatControllerImpl else { return nil } chatController.canReadHistory.set(false) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index d507ff95ddd..5e04bc3392a 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -335,6 +335,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var preloadNextChatPeerId: PeerId? let preloadNextChatPeerIdDisposable = MetaDisposable() + var preloadSavedMessagesChatsDisposable: Disposable? + let botCallbackAlertMessage = Promise(nil) var botCallbackAlertMessageDisposable: Disposable? @@ -579,7 +581,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var performTextSelectionAction: ((Message?, Bool, NSAttributedString, TextSelectionAction) -> Void)? var performOpenURL: ((Message?, String, Promise?) -> Void)? - public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, botAppStart: ChatControllerInitialBotAppStart? = nil, mode: ChatControllerPresentationMode = .standard(previewing: false), peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [ChatNavigationStackItem] = []) { + public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, botAppStart: ChatControllerInitialBotAppStart? = nil, mode: ChatControllerPresentationMode = .standard(.default), peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [ChatNavigationStackItem] = []) { let _ = ChatControllerCount.modify { value in return value + 1 } @@ -597,7 +599,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var useSharedAnimationPhase = false switch mode { - case .standard(false): + case .standard(.default): useSharedAnimationPhase = true default: break @@ -622,13 +624,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G locationBroadcastPanelSource = .none groupCallPanelSource = .none let promise = Promise() - promise.set(context.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.Message(id: replyThreadMessage.messageId)) - |> map { message -> Message? in - guard let message = message else { - return nil - } - return message._asMessage() - }) + 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 .feed: locationBroadcastPanelSource = .none @@ -653,10 +659,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let navigationBarPresentationData: NavigationBarPresentationData? switch mode { - case .inline: - navigationBarPresentationData = nil - default: - navigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData, hideBackground: self.context.sharedContext.immediateExperimentalUISettings.playerEmbedding ? true : false, hideBadge: false) + case .inline, .standard(.embedded): + navigationBarPresentationData = nil + default: + navigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData, hideBackground: self.context.sharedContext.immediateExperimentalUISettings.playerEmbedding ? true : false, hideBadge: false) } self.moreBarButton = MoreHeaderButton(color: self.presentationData.theme.rootController.navigationBar.buttonColor) @@ -752,6 +758,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if !displayVoiceMessageDiscardAlert() { return false } + + if (file.isVoice || file.isInstantVideo) && message.minAutoremoveOrClearTimeout == viewOnceTimeout { + strongSelf.openViewOnceMediaMessage(message) + return false + } } } if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia { @@ -1061,11 +1072,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let openChatLocation = strongSelf.chatLocation - /*if case let .replyThread(replyThreadMessage) = openChatLocation { - if message.threadId != makeMessageThreadId(replyThreadMessage.messageId) { - openChatLocation = .peer(id: message.id.peerId) - } - }*/ return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: openChatLocation, chatLocationContextHolder: strongSelf.chatLocationContextHolder, message: message, standalone: false, reverseMessageGalleryOrder: false, mode: mode, navigationController: strongSelf.effectiveNavigationController, dismissInput: { self?.chatDisplayNode.dismissInput() @@ -2275,6 +2281,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let concealed = urlData.concealed let message = urlData.message let progress = urlData.progress + let forceExternal = urlData.external ?? false var skipConcealedAlert = false if let author = message?.author, author.isVerified { @@ -2288,7 +2295,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let performOpenURL = strongSelf.performOpenURL { performOpenURL(message, url, progress) } else { - strongSelf.openUrl(url, concealed: concealed, skipConcealedAlert: skipConcealedAlert, message: message, allowInlineWebpageResolution: urlData.allowInlineWebpageResolution, progress: progress) + strongSelf.openUrl(url, concealed: concealed, forceExternal: forceExternal, skipConcealedAlert: skipConcealedAlert, message: message, allowInlineWebpageResolution: urlData.allowInlineWebpageResolution, progress: progress) } } }, shareCurrentLocation: { [weak self] in @@ -2536,7 +2543,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in + if savedMessages, let self, action == .info { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.navigationController as? NavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) + }) + } + return false + }), in: .current) }) } strongSelf.chatDisplayNode.dismissInput() @@ -2827,12 +2848,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if isCopyLink, let channel = channel as? TelegramChannel { - var threadMessageId: MessageId? + var threadId: Int64? if case let .replyThread(replyThreadMessage) = chatPresentationInterfaceState.chatLocation { - threadMessageId = replyThreadMessage.messageId + threadId = replyThreadMessage.threadId } - let _ = (context.engine.messages.exportMessageLink(peerId: messageId.peerId, messageId: messageId, isThread: threadMessageId != nil) + let _ = (context.engine.messages.exportMessageLink(peerId: messageId.peerId, messageId: messageId, isThread: threadId != nil) |> map { result -> String? in return result } @@ -3018,7 +3039,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } if let strongSelf = self { - if case let .replyThread(replyThreadMessage) = strongSelf.chatLocation, replyThreadMessage.messageId == message.id { + if case let .replyThread(replyThreadMessage) = strongSelf.chatLocation, replyThreadMessage.effectiveMessageId == message.id { + return .none + } + if case let .replyThread(replyThreadMessage) = strongSelf.chatLocation, replyThreadMessage.peerId == strongSelf.context.account.peerId { return .none } if case .peer = strongSelf.chatLocation, let channel = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.flags.contains(.isForum) { @@ -3027,7 +3051,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - if canReplyInChat(strongSelf.presentationInterfaceState) { + if canReplyInChat(strongSelf.presentationInterfaceState, accountPeerId: strongSelf.context.account.peerId) { return .reply } else if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info { } @@ -3050,7 +3074,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.navigateToMessage(from: nil, to: .index(MessageIndex(id: MessageId(peerId: peerId, namespace: 0, id: 0), timestamp: timestamp - Int32(NSTimeZone.local.secondsFromGMT()))), scrollPosition: .bottom(0.0), rememberInStack: false, animated: true, completion: nil) } case let .replyThread(replyThreadMessage): - let peerId = replyThreadMessage.messageId.peerId + let peerId = replyThreadMessage.peerId strongSelf.navigateToMessage(from: nil, to: .index(MessageIndex(id: MessageId(peerId: peerId, namespace: 0, id: 0), timestamp: timestamp - Int32(NSTimeZone.local.secondsFromGMT()))), scrollPosition: .bottom(0.0), rememberInStack: false, forceInCurrentChat: true, animated: true, completion: nil) case .feed: break @@ -4188,7 +4212,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case let .join(_, joinHash): self.controllerInteraction?.openJoinLink(joinHash) case let .webPage(_, url): - self.controllerInteraction?.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: false, external: false)) + self.controllerInteraction?.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: false, external: true)) case let .botApp(peerId, botApp, startParam): let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in @@ -4621,7 +4645,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(previewing: true)) + let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.previewing)) chatController.canReadHistory.set(false) var items: [ContextMenuItem] = [ @@ -5297,6 +5321,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G imageOverride = .savedMessagesIcon } else if peer.id.isReplies { imageOverride = .repliesIcon + } else if peer.id.isAnonymousSavedMessages { + imageOverride = .anonymousSavedMessagesIcon } else if peer.isDeleted { imageOverride = .deletedIcon } else { @@ -5656,7 +5682,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }) - if case .standard(previewing: false) = mode, let channel = renderedPeer?.chatMainPeer as? TelegramChannel, case .broadcast = channel.info { + 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 { @@ -5728,6 +5754,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } })) + + 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)) @@ -5737,7 +5767,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .peer: replyThreadType = .replies case let .replyThread(replyThreadMessage): - if replyThreadMessage.messageId.peerId == context.account.peerId { + if replyThreadMessage.peerId == context.account.peerId { replyThreadId = replyThreadMessage.threadId replyThreadType = .replies } else { @@ -5783,8 +5813,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let savedMessagesPeerId: PeerId? - if case let .replyThread(replyThreadMessage) = chatLocation, replyThreadMessage.messageId.peerId == context.account.peerId { - savedMessagesPeerId = PeerId(makeMessageThreadId(replyThreadMessage.messageId)) + if case let .replyThread(replyThreadMessage) = chatLocation, replyThreadMessage.peerId == context.account.peerId { + savedMessagesPeerId = PeerId(replyThreadMessage.threadId) } else { savedMessagesPeerId = nil } @@ -5921,7 +5951,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let mappedPeerData = ChatTitleContent.PeerData( peerId: savedMessagesPeerId, peer: savedMessagesPeer?.peer?._asPeer(), - isContact: false, + isContact: true, notificationSettings: nil, peerPresences: [:], cachedData: nil @@ -5930,6 +5960,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.peerView = peerView + let imageOverride: AvatarNodeImageOverride? + if strongSelf.context.account.peerId == savedMessagesPeerId { + imageOverride = .savedMessagesIcon + } else if savedMessagesPeerId.isReplies { + imageOverride = .repliesIcon + } else if savedMessagesPeerId.isAnonymousSavedMessages { + imageOverride = .anonymousSavedMessagesIcon + } else if let peer = savedMessagesPeer?.peer, peer.isDeleted { + imageOverride = .deletedIcon + } else { + imageOverride = nil + } + if strongSelf.isNodeLoaded { strongSelf.chatDisplayNode.overlayTitle = strongSelf.overlayTitle } @@ -5941,7 +5984,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }.updatedSavedMessagesTopicPeer(savedMessagesPeer?.peer) }) - (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.setPeer(context: strongSelf.context, theme: strongSelf.presentationData.theme, peer: savedMessagesPeer?.peer) + (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 { @@ -6334,7 +6377,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case let .peer(peerId): activitySpace = PeerActivitySpace(peerId: peerId, category: .global) case let .replyThread(replyThreadMessage): - activitySpace = PeerActivitySpace(peerId: replyThreadMessage.messageId.peerId, category: .thread(makeMessageThreadId(replyThreadMessage.messageId))) + activitySpace = PeerActivitySpace(peerId: replyThreadMessage.peerId, category: .thread(replyThreadMessage.threadId)) case .feed: activitySpace = nil } @@ -6652,7 +6695,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) self.networkStateDisposable = (context.account.networkState |> deliverOnMainQueue).startStrict(next: { [weak self] state in - if let strongSelf = self, case .standard(previewing: false) = strongSelf.presentationInterfaceState.mode { + if let strongSelf = self, case .standard(.default) = strongSelf.presentationInterfaceState.mode { strongSelf.chatTitleView?.networkState = state } }) @@ -6760,6 +6803,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.automaticMediaDownloadSettingsDisposable?.dispose() self.stickerSettingsDisposable?.dispose() self.searchQuerySuggestionState?.1.dispose() + self.preloadSavedMessagesChatsDisposable?.dispose() } deallocate() } @@ -6779,9 +6823,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G func themeAndStringsUpdated() { self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) switch self.presentationInterfaceState.mode { - case .standard: - self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style - self.deferScreenEdgeGestures = [] + case let .standard(standardMode): + switch standardMode { + case .embedded: + self.statusBar.statusBarStyle = .Ignore + default: + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style + self.deferScreenEdgeGestures = [] + } case .overlay: self.statusBar.statusBarStyle = .Hide self.deferScreenEdgeGestures = [.top] @@ -6900,7 +6949,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let chatLocation: ChatLocation if let threadId { - chatLocation = .replyThread(message: ChatReplyThreadMessage(messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)) + chatLocation = .replyThread(message: ChatReplyThreadMessage(peerId: peerId, threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)) } else { chatLocation = .peer(id: peerId) } @@ -7197,6 +7246,41 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.updateChatPresentationInterfaceState(interactive: false, { $0.updatedHasPlentyOfMessages(hasPlentyOfMessages) }) } } + if case .peer(self.context.account.peerId) = self.chatLocation { + var didDisplayTooltip = false + if "".isEmpty { + didDisplayTooltip = true + } + self.chatDisplayNode.historyNode.hasLotsOfMessagesUpdated = { [weak self] hasLotsOfMessages in + guard let self, hasLotsOfMessages else { + return + } + if didDisplayTooltip { + return + } + didDisplayTooltip = true + + let _ = (ApplicationSpecificNotice.getSavedMessagesChatsSuggestion(accountManager: self.context.sharedContext.accountManager) + |> deliverOnMainQueue).startStandalone(next: { [weak self] counter in + guard let self else { + return + } + if counter >= 3 { + return + } + guard let navigationBar = self.navigationBar else { + return + } + + let tooltipScreen = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: self.presentationData.strings.Chat_SavedMessagesChatsTooltip), location: .point(navigationBar.frame, .top), displayDuration: .manual, shouldDismissOnTouch: { point, _ in + return .ignore + }) + self.present(tooltipScreen, in: .current) + + let _ = ApplicationSpecificNotice.incrementSavedMessagesChatsSuggestion(accountManager: self.context.sharedContext.accountManager).startStandalone() + }) + } + } self.chatDisplayNode.historyNode.addContentOffset = { [weak self] offset, itemNode in guard let strongSelf = self else { @@ -7208,12 +7292,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var closeOnEmpty = false if case .pinnedMessages = self.presentationInterfaceState.subject { closeOnEmpty = true - } else if case let .replyThread(replyThreadMessage) = self.chatLocation, replyThreadMessage.messageId.peerId == self.context.account.peerId { + } else if case let .replyThread(replyThreadMessage) = self.chatLocation, replyThreadMessage.peerId == self.context.account.peerId { closeOnEmpty = true } if closeOnEmpty { - self.chatDisplayNode.historyNode.setLoadStateUpdated({ [weak self] state, _ in + self.chatDisplayNode.historyNode.addSetLoadStateUpdated({ [weak self] state, _ in guard let strongSelf = self else { return } @@ -7567,7 +7651,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G threadData = .single(nil) } - if case .standard(previewing: true) = self.presentationInterfaceState.mode { + 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) @@ -9540,8 +9624,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.lockMediaRecorder() }, deleteRecordedMedia: { [weak self] in self?.deleteMediaRecording() - }, sendRecordedMedia: { [weak self] silentPosting in - self?.sendMediaRecording(silentPosting: silentPosting) + }, sendRecordedMedia: { [weak self] silentPosting, viewOnce in + self?.sendMediaRecording(silentPosting: silentPosting, viewOnce: viewOnce) }, displayRestrictedInfo: { [weak self] subject, displayType in guard let strongSelf = self else { return @@ -10693,7 +10777,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G chatLocation = .peer(id: peerId) } - let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: chatLocation, subject: .pinnedMessages(id: pinnedMessage.message.id), botStart: nil, mode: .standard(previewing: true)) + let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: chatLocation, subject: .pinnedMessages(id: pinnedMessage.message.id), botStart: nil, mode: .standard(.previewing)) chatController.canReadHistory.set(false) strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() @@ -11084,7 +11168,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.chatUnreadMentionCountDisposable = (self.context.account.viewTracker.unseenPersonalMessagesAndReactionCount(peerId: peerId, threadId: nil) |> deliverOnMainQueue).startStrict(next: { [weak self] mentionCount, reactionCount in if let strongSelf = self { - if case let .standard(previewing) = strongSelf.presentationInterfaceState.mode, previewing { + if case .standard(.previewing) = strongSelf.presentationInterfaceState.mode { strongSelf.chatDisplayNode.navigateButtons.mentionCount = 0 strongSelf.chatDisplayNode.navigateButtons.reactionsCount = 0 } else { @@ -11099,7 +11183,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else if let peerId = self.chatLocation.peerId, let threadId = self.chatLocation.threadId { self.chatUnreadMentionCountDisposable = (self.context.account.viewTracker.unseenPersonalMessagesAndReactionCount(peerId: peerId, threadId: threadId) |> deliverOnMainQueue).startStrict(next: { [weak self] mentionCount, reactionCount in if let strongSelf = self { - if case let .standard(previewing) = strongSelf.presentationInterfaceState.mode, previewing { + if case .standard(.previewing) = strongSelf.presentationInterfaceState.mode { strongSelf.chatDisplayNode.navigateButtons.mentionCount = 0 strongSelf.chatDisplayNode.navigateButtons.reactionsCount = 0 } else { @@ -11121,7 +11205,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case let .peer(peerId): activitySpace = PeerActivitySpace(peerId: peerId, category: .global) case let .replyThread(replyThreadMessage): - activitySpace = PeerActivitySpace(peerId: replyThreadMessage.messageId.peerId, category: .thread(makeMessageThreadId(replyThreadMessage.messageId))) + activitySpace = PeerActivitySpace(peerId: replyThreadMessage.peerId, category: .thread(replyThreadMessage.threadId)) case .feed: activitySpace = nil } @@ -11437,7 +11521,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if case let .replyThread(message) = self.chatLocation, message.isForumPost { if self.keepMessageCountersSyncrhonizedDisposable == nil { - self.keepMessageCountersSyncrhonizedDisposable = self.context.engine.messages.keepMessageCountersSyncrhonized(peerId: message.messageId.peerId, threadId: Int64(message.messageId.id)).startStrict() + self.keepMessageCountersSyncrhonizedDisposable = self.context.engine.messages.keepMessageCountersSyncrhonized(peerId: message.peerId, threadId: message.threadId).startStrict() } } @@ -11561,7 +11645,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - if case .standard(false) = self.presentationInterfaceState.mode, self.raiseToListen == nil { + if case .standard(.default) = self.presentationInterfaceState.mode, self.raiseToListen == nil { self.raiseToListen = RaiseToListenManager(shouldActivate: { [weak self] in if let strongSelf = self, strongSelf.isNodeLoaded && strongSelf.canReadHistoryValue, strongSelf.presentationInterfaceState.interfaceState.editMessage == nil, strongSelf.playlistStateAndType == nil { if !strongSelf.context.sharedContext.currentMediaInputSettings.with({ $0.enableRaiseToSpeak }) { @@ -11579,7 +11663,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if !strongSelf.traceVisibility() { return false } - + if strongSelf.currentContextController != nil { + return false + } if !isTopmostChatController(strongSelf) { return false } @@ -11677,6 +11763,23 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return false } }) + } else if peerId.namespace == Namespaces.Peer.CloudUser { + self.screenCaptureManager = ScreenCaptureDetectionManager(check: { [weak self] in + guard let self else { + return false + } + + let _ = (self.context.sharedContext.mediaManager.globalMediaPlayerState + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak self] playlistStateAndType in + if let self, let (_, playbackState, _) = playlistStateAndType, case let .state(state) = playbackState { + if let source = state.item.playbackData?.source, case let .telegramFile(_, _, isViewOnce) = source, isViewOnce { + self.context.sharedContext.mediaManager.setPlaylist(nil, type: .voice, control: .playback(.pause)) + } + } + }) + return true + }) } } @@ -12024,8 +12127,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case let .peer(peerIdValue): peerId = peerIdValue case let .replyThread(replyThreadMessage): - peerId = replyThreadMessage.messageId.peerId - threadId = makeMessageThreadId(replyThreadMessage.messageId) + peerId = replyThreadMessage.peerId + threadId = replyThreadMessage.threadId case .feed: return } @@ -12097,6 +12200,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return nil } + public func updateIsScrollingLockedAtTop(isScrollingLockedAtTop: Bool) { + self.chatDisplayNode.isScrollingLockedAtTop = isScrollingLockedAtTop + } + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { self.suspendNavigationBarLayout = true super.containerLayoutUpdated(layout, transition: transition) @@ -12574,7 +12681,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } })) case .replyThread: - if let peer = self.presentationInterfaceState.renderedPeer?.peer, case let .replyThread(replyThreadMessage) = self.chatLocation, replyThreadMessage.messageId.peerId == self.context.account.peerId { + if let peer = self.presentationInterfaceState.renderedPeer?.peer, case let .replyThread(replyThreadMessage) = self.chatLocation, replyThreadMessage.peerId == self.context.account.peerId { if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: peer, mode: .forumTopic(thread: replyThreadMessage), avatarInitiallyExpanded: false, fromChat: true, requestsContext: nil) { self.effectiveNavigationController?.pushViewController(infoController) } @@ -15130,7 +15237,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .peer: break case let .replyThread(replyThreadMessage): - defaultReplyMessageSubject = EngineMessageReplySubject(messageId: replyThreadMessage.messageId, quote: nil) + if let effectiveMessageId = replyThreadMessage.effectiveMessageId { + defaultReplyMessageSubject = EngineMessageReplySubject(messageId: effectiveMessageId, quote: nil) + } case .feed: break } @@ -15550,9 +15659,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject let correlationId = Int64.random(in: 0 ..< Int64.max) + let updatedMessage = message .withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel) .withUpdatedCorrelationId(correlationId) +// .withUpdatedAttributes({ attributes in +// var attributes = attributes +//#if DEBUG +// attributes.append(AutoremoveTimeoutMessageAttribute(timeout: viewOnceTimeout, countdownBeginTime: nil)) +//#endif +// return attributes +// }) var usedCorrelationId = false @@ -15645,10 +15762,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }) strongSelf.recorderFeedback = nil + strongSelf.updateDownButtonVisibility() } } }) - case .send: + case let .send(viewOnce): self.chatDisplayNode.updateRecordedMediaDeleted(false) let _ = (audioRecorderValue.takenRecordedData() |> deliverOnMainQueue).startStandalone(next: { [weak self] data in @@ -15690,7 +15808,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }, usedCorrelationId ? correlationId : nil) - strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.compressedData.count), attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)])), threadId: strongSelf.chatLocation.threadId, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])]) + var attributes: [MessageAttribute] = [] + if viewOnce { + attributes.append(AutoremoveTimeoutMessageAttribute(timeout: viewOnceTimeout, countdownBeginTime: nil)) + } + + strongSelf.sendMessages([.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.compressedData.count), attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)])), threadId: strongSelf.chatLocation.threadId, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])]) strongSelf.recorderFeedback?.tap() strongSelf.recorderFeedback = nil @@ -15757,9 +15880,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedRecordedMediaPreview(nil) }) + self.updateDownButtonVisibility() } - func sendMediaRecording(silentPosting: Bool? = nil, scheduleTime: Int32? = nil) { + func sendMediaRecording(silentPosting: Bool? = nil, scheduleTime: Int32? = nil, viewOnce: Bool = false) { self.chatDisplayNode.updateRecordedMediaDeleted(false) if let recordedMediaPreview = self.presentationInterfaceState.recordedMediaPreview { @@ -15784,10 +15908,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedRecordedMediaPreview(nil).updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) + + strongSelf.updateDownButtonVisibility() } }, nil) - let messages: [EnqueueMessage] = [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: recordedMediaPreview.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(recordedMediaPreview.fileSize), attributes: [.Audio(isVoice: true, duration: Int(recordedMediaPreview.duration), title: nil, performer: nil, waveform: waveformBuffer)])), threadId: self.chatLocation.threadId, replyToMessageId: self.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] + var attributes: [MessageAttribute] = [] + if viewOnce { + attributes.append(AutoremoveTimeoutMessageAttribute(timeout: viewOnceTimeout, countdownBeginTime: nil)) + } + + let messages: [EnqueueMessage] = [.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: recordedMediaPreview.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(recordedMediaPreview.fileSize), attributes: [.Audio(isVoice: true, duration: Int(recordedMediaPreview.duration), title: nil, performer: nil, waveform: waveformBuffer)])), threadId: self.chatLocation.threadId, replyToMessageId: self.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] let transformedMessages: [EnqueueMessage] if let silentPosting = silentPosting { @@ -15833,22 +15964,22 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } return nil } - var searchTopMsgId: MessageId? + var threadId: Int64? switch self.chatLocation { case .peer: break case let .replyThread(replyThreadMessage): - searchTopMsgId = replyThreadMessage.messageId + threadId = replyThreadMessage.threadId case .feed: break } switch search.domain { case .everything: - derivedSearchState = ChatSearchState(query: search.query, location: .peer(peerId: peerId, fromId: nil, tags: nil, topMsgId: searchTopMsgId, minDate: nil, maxDate: nil), loadMoreState: loadMoreStateFromResultsState(search.resultsState)) + derivedSearchState = ChatSearchState(query: search.query, location: .peer(peerId: peerId, fromId: nil, tags: nil, threadId: threadId, minDate: nil, maxDate: nil), loadMoreState: loadMoreStateFromResultsState(search.resultsState)) case .members: derivedSearchState = nil case let .member(peer): - derivedSearchState = ChatSearchState(query: search.query, location: .peer(peerId: peerId, fromId: peer.id, tags: nil, topMsgId: searchTopMsgId, minDate: nil, maxDate: nil), loadMoreState: loadMoreStateFromResultsState(search.resultsState)) + derivedSearchState = ChatSearchState(query: search.query, location: .peer(peerId: peerId, fromId: peer.id, tags: nil, threadId: threadId, minDate: nil, maxDate: nil), loadMoreState: loadMoreStateFromResultsState(search.resultsState)) } } @@ -16105,7 +16236,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } func updateDownButtonVisibility() { - let recordingMediaMessage = self.audioRecorderValue != nil || self.videoRecorderValue != nil + let recordingMediaMessage = self.audioRecorderValue != nil || self.videoRecorderValue != nil || self.presentationInterfaceState.recordedMediaPreview != nil self.chatDisplayNode.navigateButtons.displayDownButton = self.shouldDisplayDownButton && !recordingMediaMessage // MARK: Nicegram AiChat updateAiOverlayVisibility() @@ -16166,8 +16297,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G peerId = peerIdValue threadId = nil case let .replyThread(replyThreadMessage): - peerId = replyThreadMessage.messageId.peerId - threadId = makeMessageThreadId(replyThreadMessage.messageId) + peerId = replyThreadMessage.peerId + threadId = replyThreadMessage.threadId case .feed: return } @@ -16203,8 +16334,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G peerId = peerIdValue threadId = nil case let .replyThread(replyThreadMessage): - peerId = replyThreadMessage.messageId.peerId - threadId = makeMessageThreadId(replyThreadMessage.messageId) + peerId = replyThreadMessage.peerId + threadId = replyThreadMessage.threadId case .feed: return } @@ -16237,7 +16368,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }))) } - let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: .message(id: .timestamp(timestamp), highlight: nil, timecode: nil), botStart: nil, mode: .standard(previewing: true)) + let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: .message(id: .timestamp(timestamp), highlight: nil, timecode: nil), botStart: nil, mode: .standard(.previewing)) chatController.canReadHistory.set(false) strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() @@ -16599,7 +16730,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in + if savedMessages, let self, action == .info { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.navigationController as? NavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) + }) + } + return false + }), in: .current) } switch mode { @@ -17639,11 +17784,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } f(.default) } else { - c.dismiss(completion: { - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { - commit() + if "".isEmpty { + f(.dismissWithoutContent) + commit() + } else { + c.dismiss(completion: { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { + commit() + }) }) - }) + } } } }))) @@ -17657,7 +17807,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if options.contains(.deleteLocally) { var localOptionText = self.presentationData.strings.Conversation_DeleteMessagesForMe - if case .scheduledMessages = self.presentationInterfaceState.subject { + if self.chatLocation.peerId == self.context.account.peerId { + localOptionText = self.presentationData.strings.Chat_ConfirmationRemoveFromSavedMessages + } else if case .scheduledMessages = self.presentationInterfaceState.subject { localOptionText = messageIds.count > 1 ? self.presentationData.strings.ScheduledMessages_DeleteMany : self.presentationData.strings.ScheduledMessages_Delete } else { if options.contains(.unsendPersonal) { @@ -17674,14 +17826,23 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - c.dismiss(completion: { [weak strongSelf] in - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { - guard let strongSelf else { - return - } - let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).startStandalone() + let commit: () -> Void = { + guard let strongSelf = self else { + return + } + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).startStandalone() + } + + if "".isEmpty { + f(.dismissWithoutContent) + commit() + } else { + c.dismiss(completion: { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { + commit() + }) }) - }) + } } }))) items.append(ActionSheetButtonItem(title: localOptionText, color: .destructive, action: { [weak self, weak actionSheet] in @@ -18184,7 +18345,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G ) ) - if canReplyInChat(self.presentationInterfaceState) { + if canReplyInChat(self.presentationInterfaceState, accountPeerId: self.context.account.peerId) { inputShortcuts.append( KeyShortcut( input: UIKeyCommand.inputUpArrow, @@ -19093,6 +19254,61 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } + func openViewOnceMediaMessage(_ message: Message) { + if self.screenCaptureManager?.isRecordingActive == true { + let controller = textAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, title: nil, text: self.presentationData.strings.Chat_PlayOnceMesasge_DisableScreenCapture, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { + })]) + self.present(controller, in: .window(.root)) + return + } + + let isIncoming = message.effectivelyIncoming(self.context.account.peerId) + + var presentImpl: ((ViewController) -> Void)? + let configuration = ContextController.Configuration( + sources: [ + ContextController.Source( + id: 0, + title: "", + source: .extracted(ChatViewOnceMessageContextExtractedContentSource( + context: self.context, + presentationData: self.presentationData, + chatNode: self.chatDisplayNode, + backgroundNode: self.chatBackgroundNode, + engine: self.context.engine, + message: message, + present: { c in + presentImpl?(c) + } + )), + items: .single(ContextController.Items(content: .list([]))), + closeActionTitle: isIncoming ? self.presentationData.strings.Chat_PlayOnceMesasgeCloseAndDelete : self.presentationData.strings.Chat_PlayOnceMesasgeClose, + closeAction: { [weak self] in + if let self { + self.context.sharedContext.mediaManager.setPlaylist(nil, type: .voice, control: .playback(.pause)) + } + } + ) + ], initialId: 0 + ) + + let contextController = ContextController(presentationData: self.presentationData, configuration: configuration) + contextController.getOverlayViews = { [weak self] in + guard let self else { + return [] + } + return [self.chatDisplayNode.navigateButtons.view] + } + self.currentContextController = contextController + self.presentInGlobalOverlay(contextController) + + presentImpl = { [weak contextController] c in + contextController?.present(c, in: .current) + } + + let _ = self.context.sharedContext.openChatMessage(OpenChatMessageParams(context: self.context, chatLocation: nil, chatLocationContextHolder: nil, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: nil, dismissInput: { }, present: { _, _ in }, transitionNode: { _, _, _ in return nil }, addToTransitionSurface: { _ in }, openUrl: { _ in }, openPeer: { _, _ in }, callPeer: { _, _ in }, enqueueMessage: { _ in }, sendSticker: nil, sendEmoji: nil, setupTemporaryHiddenMedia: { _, _, _ in }, chatAvatarHiddenMedia: { _, _ in }, playlistLocation: .singleMessage(message.id))) + } + func openStorySharing(messages: [Message]) { let context = self.context let subject: Signal = .single(.message(messages.map { $0.id })) @@ -19158,6 +19374,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G ) self.push(controller) } + + public func transferScrollingVelocity(_ velocity: CGFloat) { + self.chatDisplayNode.historyNode.transferVelocity(velocity) + } + + public func performScrollToTop() -> Bool { + let offset = self.chatDisplayNode.historyNode.visibleContentOffset() + switch offset { + case let .known(value) where value <= CGFloat.ulpOfOne: + return false + default: + self.chatDisplayNode.historyNode.scrollToEndOfHistory() + return true + } + } } final class ChatContextControllerContentSourceImpl: ContextControllerContentSource { diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 072d16ee0f6..31977a9b6d3 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -150,13 +150,15 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let loadingNode: ChatLoadingNode private(set) var loadingPlaceholderNode: ChatLoadingPlaceholderNode? + var isScrollingLockedAtTop: Bool = false + private var emptyNode: ChatEmptyNode? private(set) var emptyType: ChatHistoryNodeLoadState.EmptyType? private var didDisplayEmptyGreeting = false private var validEmptyNodeLayout: (CGSize, UIEdgeInsets)? var restrictedNode: ChatRecentActionsEmptyNode? - private var validLayout: (ContainerViewLayout, CGFloat)? + private(set) var validLayout: (ContainerViewLayout, CGFloat)? private var visibleAreaInset = UIEdgeInsets() private var searchNavigationNode: ChatSearchNavigationContentNode? @@ -330,7 +332,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private var isLoadingValue: Bool = false private var isLoadingEarlier: Bool = false private func updateIsLoading(isLoading: Bool, earlier: Bool, animated: Bool) { - let useLoadingPlaceholder = self.chatLocation.peerId?.namespace != Namespaces.Peer.CloudUser + var useLoadingPlaceholder = self.chatLocation.peerId?.namespace != Namespaces.Peer.CloudUser + if case let .replyThread(message) = self.chatLocation, message.peerId == self.context.account.peerId { + useLoadingPlaceholder = true + } let updated = isLoading != self.isLoadingValue || (isLoading && earlier && !self.isLoadingEarlier) @@ -647,15 +652,23 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } else { source = .default } + + var historyNodeRotated = true + switch chatPresentationInterfaceState.mode { + case let .standard(standardMode): + if case .embedded(true) = standardMode { + historyNodeRotated = false + } + default: + break + } + + self.controllerInteraction.chatIsRotated = historyNodeRotated var getMessageTransitionNode: (() -> ChatMessageTransitionNodeImpl?)? - self.historyNode = ChatHistoryListNodeImpl(context: context, updatedPresentationData: controller?.updatedPresentationData ?? (context.sharedContext.currentPresentationData.with({ $0 }), context.sharedContext.presentationData), chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, tagMask: nil, source: source, subject: subject, controllerInteraction: controllerInteraction, selectedMessages: self.selectedMessagesPromise.get(), messageTransitionNode: { + self.historyNode = ChatHistoryListNodeImpl(context: context, updatedPresentationData: controller?.updatedPresentationData ?? (context.sharedContext.currentPresentationData.with({ $0 }), context.sharedContext.presentationData), chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, tagMask: nil, source: source, subject: subject, controllerInteraction: controllerInteraction, selectedMessages: self.selectedMessagesPromise.get(), rotated: historyNodeRotated, messageTransitionNode: { return getMessageTransitionNode?() }) - self.historyNode.rotated = true - - //self.historyScrollingArea = SparseDiscreteScrollingArea() - //self.historyNode.historyScrollingArea = self.historyScrollingArea self.historyNodeContainer = HistoryNodeContainer(isSecret: chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat) @@ -693,7 +706,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.inputPanelBottomBackgroundSeparatorNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputMediaPanel.panelSeparatorColor self.inputPanelBottomBackgroundSeparatorNode.isLayerBacked = true - self.navigateButtons = ChatHistoryNavigationButtons(theme: self.chatPresentationInterfaceState.theme, dateTimeFormat: self.chatPresentationInterfaceState.dateTimeFormat, backgroundNode: self.backgroundNode) + self.navigateButtons = ChatHistoryNavigationButtons(theme: self.chatPresentationInterfaceState.theme, dateTimeFormat: self.chatPresentationInterfaceState.dateTimeFormat, backgroundNode: self.backgroundNode, isChatRotated: historyNodeRotated) self.navigateButtons.accessibilityElementsHidden = true super.init() @@ -1475,7 +1488,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } let previewing: Bool - if case .standard(true) = self.chatPresentationInterfaceState.mode { + if case .standard(.previewing) = self.chatPresentationInterfaceState.mode { previewing = true } else { previewing = false @@ -1499,9 +1512,12 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { insets = layout.insets(options: [.input]) } - if case .overlay = self.chatPresentationInterfaceState.mode { + switch self.chatPresentationInterfaceState.mode { + case .standard(.embedded): + break + case .overlay: insets.top = 44.0 - } else { + default: insets.top += navigationBarHeight } @@ -1534,6 +1550,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { inputPanelNode.removeFromSupernode() inputPanelNode.prevInputPanelNode = prevInputPanelNode inputPanelNode.addSubnode(prevInputPanelNode) + + prevInputPanelNode.viewForOverlayContent?.removeFromSuperview() } else { dismissedInputPanelNode = self.inputPanelNode } @@ -1543,10 +1561,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if inputPanelNode.supernode !== self { immediatelyLayoutInputPanelAndAnimateAppearance = true self.inputPanelClippingNode.insertSubnode(inputPanelNode, aboveSubnode: self.inputPanelBackgroundNode) - - if let viewForOverlayContent = inputPanelNode.viewForOverlayContent { - self.inputPanelOverlayNode.view.addSubview(viewForOverlayContent) - } + } + if let viewForOverlayContent = inputPanelNode.viewForOverlayContent, viewForOverlayContent.superview == nil { + self.inputPanelOverlayNode.view.addSubview(viewForOverlayContent) } } else { let inputPanelHeight = inputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: layout.intrinsicInsets.bottom, additionalSideInsets: layout.additionalInsets, maxHeight: layout.size.height - insets.top - inputPanelBottomInset - 120.0, isSecondary: false, transition: transition, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics, isMediaInputExpanded: self.inputPanelContainerNode.expansionFraction == 1.0) @@ -1984,6 +2001,12 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } + if !self.historyNode.rotated { + let current = listInsets + listInsets.top = current.bottom + listInsets.bottom = current.top + } + var displayTopDimNode = false let ensureTopInsetForOverlayHighlightedItems: CGFloat? = nil var expandTopDimNode = false @@ -2050,6 +2073,17 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { strongSelf.notifyTransitionCompletionListeners(transition: transition) } }) + if self.isScrollingLockedAtTop { + switch self.historyNode.visibleContentOffset() { + case let .known(value) where value <= CGFloat.ulpOfOne: + break + case .none: + break + default: + self.historyNode.scrollToEndOfHistory() + } + } + self.historyNode.scrollEnabled = !self.isScrollingLockedAtTop let navigateButtonsSize = self.navigateButtons.updateLayout(transition: transition) var navigateButtonsFrame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.right - navigateButtonsSize.width - 6.0, y: layout.size.height - containerInsets.bottom - inputPanelsHeight - navigateButtonsSize.height - 6.0), size: navigateButtonsSize) @@ -2073,6 +2107,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { apparentNavigateButtonsFrame.origin.y -= 16.0 } + if !self.historyNode.rotated { + apparentNavigateButtonsFrame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.right - navigateButtonsSize.width - 6.0, y: 6.0), size: navigateButtonsSize) + } + var isInputExpansionEnabled = false if case .media = self.chatPresentationInterfaceState.inputMode { isInputExpansionEnabled = true @@ -2126,6 +2164,11 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { inputPanelUpdateTransition = .immediate } + if case .standard(.embedded) = self.chatPresentationInterfaceState.mode { + self.inputPanelBackgroundNode.isHidden = true + self.inputPanelBackgroundSeparatorNode.isHidden = true + self.inputPanelBottomBackgroundSeparatorNode.isHidden = true + } self.inputPanelBackgroundNode.update(size: CGSize(width: intrinsicInputPanelBackgroundNodeSize.width, height: intrinsicInputPanelBackgroundNodeSize.height + inputPanelBackgroundExtension), transition: inputPanelUpdateTransition, beginWithCurrentState: true) self.inputPanelBottomBackgroundSeparatorBaseOffset = intrinsicInputPanelBackgroundNodeSize.height inputPanelUpdateTransition.updateFrame(node: self.inputPanelBottomBackgroundSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: intrinsicInputPanelBackgroundNodeSize.height + inputPanelBackgroundExtension), size: CGSize(width: intrinsicInputPanelBackgroundNodeSize.width, height: UIScreenPixel)), beginWithCurrentState: true) @@ -3276,7 +3319,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { switch self.chatPresentationInterfaceState.mode { - case .standard(previewing: true): + case .standard(.previewing): if let subject = self.controller?.subject, case let .messageOptions(_, _, info) = subject, case .reply = info { if let controller = self.controller { if let result = controller.presentationContext.hitTest(view: self.view, point: point, with: event) { diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift index d9eadc9668c..6edcc48582a 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift @@ -21,6 +21,7 @@ func chatHistoryEntriesForView( selectedMessages: Set?, presentationData: ChatPresentationData, historyAppearsCleared: Bool, + skipViewOnceMedia: Bool, pendingUnpinnedAllMessages: Bool, pendingRemovedMessages: Set, associatedData: ChatMessageItemAssociatedData, @@ -152,6 +153,10 @@ func chatHistoryEntriesForView( } } + if skipViewOnceMedia, message.minAutoremoveOrClearTimeout != nil { + continue loop + } + var contentTypeHint: ChatMessageEntryContentType = .generic for media in message.media { diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index fff1dc8c19b..1980e0e7481 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -402,7 +402,7 @@ private func extractAssociatedData( } } } else if case let .replyThread(message) = chatLocation, message.isForumPost { - automaticDownloadPeerId = message.messageId.peerId + automaticDownloadPeerId = message.peerId } return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes) @@ -639,10 +639,14 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto public private(set) var loadState: ChatHistoryNodeLoadState? private var loadStateUpdated: ((ChatHistoryNodeLoadState, Bool) -> Void)? + private var additionalLoadStateUpdated: [(ChatHistoryNodeLoadState, Bool) -> Void] = [] public private(set) var hasPlentyOfMessages: Bool = false public var hasPlentyOfMessagesUpdated: ((Bool) -> Void)? + public private(set) var hasLotsOfMessages: Bool = false + public var hasLotsOfMessagesUpdated: ((Bool) -> Void)? + private var loadedMessagesFromCachedDataDisposable: Disposable? public var wantTrButton: [(Bool, [String])] @@ -707,7 +711,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto private var allowDustEffect: Bool = true private var dustEffectLayer: DustEffectLayer? - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal), chatLocation: ChatLocation, chatLocationContextHolder: Atomic, tagMask: MessageTags?, source: ChatHistoryListSource, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteraction, selectedMessages: Signal?, NoError>, mode: ChatHistoryListMode = .bubbles, messageTransitionNode: @escaping () -> ChatMessageTransitionNodeImpl?) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal), chatLocation: ChatLocation, chatLocationContextHolder: Atomic, tagMask: MessageTags?, source: ChatHistoryListSource, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteraction, selectedMessages: Signal?, NoError>, mode: ChatHistoryListMode = .bubbles, rotated: Bool = false, messageTransitionNode: @escaping () -> ChatMessageTransitionNodeImpl?) { // MARK: Nicegram self.wantTrButton = usetrButton() // @@ -765,6 +769,11 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto nextClientId += 1 super.init() + + self.rotated = rotated + if rotated { + self.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0) + } self.clipsToBounds = false @@ -836,12 +845,6 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } self.preloadPages = false - switch self.mode { - case .bubbles: - self.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0) - case .list: - break - } self.beginChatHistoryTransitions( selectedMessages: selectedMessages, @@ -1173,11 +1176,11 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto additionalData.append(.totalUnreadState) } if case let .replyThread(replyThreadMessage) = self.chatLocation { - additionalData.append(.cachedPeerData(replyThreadMessage.messageId.peerId)) - additionalData.append(.peerNotificationSettings(replyThreadMessage.messageId.peerId)) - if replyThreadMessage.messageId.peerId.namespace == Namespaces.Peer.CloudChannel { - additionalData.append(.cacheEntry(cachedChannelAdminRanksEntryId(peerId: replyThreadMessage.messageId.peerId))) - additionalData.append(.peer(replyThreadMessage.messageId.peerId)) + additionalData.append(.cachedPeerData(replyThreadMessage.peerId)) + additionalData.append(.peerNotificationSettings(replyThreadMessage.peerId)) + if replyThreadMessage.peerId.namespace == Namespaces.Peer.CloudChannel { + additionalData.append(.cacheEntry(cachedChannelAdminRanksEntryId(peerId: replyThreadMessage.peerId))) + additionalData.append(.peer(replyThreadMessage.peerId)) } additionalData.append(.message(replyThreadMessage.effectiveTopId)) @@ -1535,6 +1538,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto if strongSelf.loadState != loadState { strongSelf.loadState = loadState strongSelf.loadStateUpdated?(loadState, false) + for f in strongSelf.additionalLoadStateUpdated { + f(loadState, false) + } } let historyState: ChatHistoryNodeHistoryState = .loading @@ -1610,6 +1616,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto selectedMessages: selectedMessages, presentationData: chatPresentationData, historyAppearsCleared: historyAppearsCleared, + skipViewOnceMedia: mode != .bubbles, pendingUnpinnedAllMessages: pendingUnpinnedAllMessages, pendingRemovedMessages: pendingRemovedMessages, associatedData: associatedData, @@ -1976,6 +1983,10 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto public func setLoadStateUpdated(_ f: @escaping (ChatHistoryNodeLoadState, Bool) -> Void) { self.loadStateUpdated = f } + + public func addSetLoadStateUpdated(_ f: @escaping (ChatHistoryNodeLoadState, Bool) -> Void) { + self.additionalLoadStateUpdated.append(f) + } private func maybeUpdateOverscrollAction(offset: CGFloat?) { if self.freezeOverscrollControl { @@ -2994,6 +3005,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto if self.loadState != loadState { self.loadState = loadState self.loadStateUpdated?(loadState, transition.options.contains(.AnimateInsertion)) + for f in self.additionalLoadStateUpdated { + f(loadState, transition.options.contains(.AnimateInsertion)) + } } let isEmpty = transition.historyView.originalView.entries.isEmpty || loadState == .empty(.botInfo) @@ -3028,6 +3042,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } } let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent()) + var maybeRemovedInteractivelyMessageIds: [(UInt32, EngineMessage.Id)] = [] for entry in previousHistoryView.filteredEntries { switch entry { case let .MessageEntry(message, _, _, _, _, _): @@ -3038,7 +3053,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto expiredMessageStableIds.insert(message.stableId) } } else { - //expiredMessageStableIds.insert(message.stableId) + maybeRemovedInteractivelyMessageIds.append((message.stableId, message.id)) } } case let .MessageGroupEntry(_, messages, _): @@ -3056,13 +3071,28 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto expiredMessageStableIds.insert(message.stableId) } } else { - //expiredMessageStableIds.insert(message.stableId) + maybeRemovedInteractivelyMessageIds.append((message.stableId, message.id)) } } default: break } } + + var testIds: [MessageId] = [] + if !maybeRemovedInteractivelyMessageIds.isEmpty { + for (_, id) in maybeRemovedInteractivelyMessageIds { + testIds.append(id) + } + } + for id in self.context.engine.messages.synchronouslyIsMessageDeletedInteractively(ids: testIds) { + inner: for (stableId, listId) in maybeRemovedInteractivelyMessageIds { + if listId == id { + expiredMessageStableIds.insert(stableId) + break inner + } + } + } } self.currentDeleteAnimationCorrelationIds.formUnion(expiredMessageStableIds) @@ -3093,7 +3123,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto dustEffectLayer.bounds = CGRect(origin: CGPoint(), size: self.bounds.size) self.dustEffectLayer = dustEffectLayer dustEffectLayer.zPosition = 10.0 - dustEffectLayer.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0) + if self.rotated { + dustEffectLayer.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0) + } self.layer.addSublayer(dustEffectLayer) dustEffectLayer.becameEmpty = { [weak self] in guard let self else { @@ -3303,17 +3335,26 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } } strongSelf.loadState = loadState - strongSelf.loadStateUpdated?(loadState, animated || transition.animateIn || animateIn) + let isAnimated = animated || transition.animateIn || animateIn + strongSelf.loadStateUpdated?(loadState, isAnimated) + for f in strongSelf.additionalLoadStateUpdated { + f(loadState, isAnimated) + } } var hasPlentyOfMessages = false + var hasLotsOfMessages = false if let historyView = strongSelf.historyView { if historyView.originalView.holeEarlier || historyView.originalView.holeLater { hasPlentyOfMessages = true + hasLotsOfMessages = true } else if !historyView.originalView.holeEarlier && !historyView.originalView.holeLater { if historyView.filteredEntries.count >= 10 { hasPlentyOfMessages = true } + if historyView.filteredEntries.count >= 40 { + hasLotsOfMessages = true + } } } @@ -3321,6 +3362,10 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto strongSelf.hasPlentyOfMessages = hasPlentyOfMessages strongSelf.hasPlentyOfMessagesUpdated?(hasPlentyOfMessages) } + if strongSelf.hasLotsOfMessages != hasLotsOfMessages { + strongSelf.hasLotsOfMessages = hasLotsOfMessages + strongSelf.hasLotsOfMessagesUpdated?(hasLotsOfMessages) + } if let _ = visibleRange.loadedRange { if let visible = visibleRange.visibleRange { @@ -4182,7 +4227,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto overscrollView.frame = overscrollView.convert(overscrollView.bounds, to: self.view) snapshotView.addSubview(overscrollView) - overscrollView.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + if self.rotated { + overscrollView.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + } } return SnapshotState( @@ -4207,13 +4254,17 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto let snapshotParentView = UIView() snapshotParentView.addSubview(snapshotState.snapshotView) - snapshotParentView.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0) + if self.rotated { + snapshotParentView.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0) + } snapshotParentView.frame = self.view.frame snapshotState.snapshotView.frame = snapshotParentView.bounds snapshotState.snapshotView.clipsToBounds = true - snapshotState.snapshotView.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + if self.rotated { + snapshotState.snapshotView.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + } self.view.superview?.insertSubview(snapshotParentView, belowSubview: self.view) diff --git a/submodules/TelegramUI/Sources/ChatHistoryNavigationButtonNode.swift b/submodules/TelegramUI/Sources/ChatHistoryNavigationButtonNode.swift index 0c87bc53660..7dc96eea3ee 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryNavigationButtonNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryNavigationButtonNode.swift @@ -10,6 +10,7 @@ private let badgeFont = Font.with(size: 13.0, traits: [.monospacedNumbers]) enum ChatHistoryNavigationButtonType { case down + case up case mentions case reactions } @@ -60,6 +61,8 @@ class ChatHistoryNavigationButtonNode: ContextControllerSourceNode { switch type { case .down: self.imageNode.image = PresentationResourcesChat.chatHistoryNavigationButtonImage(theme) + case .up: + self.imageNode.image = PresentationResourcesChat.chatHistoryNavigationUpButtonImage(theme) case .mentions: self.imageNode.image = PresentationResourcesChat.chatHistoryMentionsButtonImage(theme) case .reactions: @@ -113,6 +116,8 @@ class ChatHistoryNavigationButtonNode: ContextControllerSourceNode { switch self.type { case .down: self.imageNode.image = PresentationResourcesChat.chatHistoryNavigationButtonImage(theme) + case .up: + self.imageNode.image = PresentationResourcesChat.chatHistoryNavigationUpButtonImage(theme) case .mentions: self.imageNode.image = PresentationResourcesChat.chatHistoryMentionsButtonImage(theme) case .reactions: diff --git a/submodules/TelegramUI/Sources/ChatHistoryNavigationButtons.swift b/submodules/TelegramUI/Sources/ChatHistoryNavigationButtons.swift index b582b5627ef..085418897a0 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryNavigationButtons.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryNavigationButtons.swift @@ -11,6 +11,7 @@ import WallpaperBackgroundNode final class ChatHistoryNavigationButtons: ASDisplayNode { private var theme: PresentationTheme private var dateTimeFormat: PresentationDateTimeFormat + private let isChatRotated: Bool let reactionsButton: ChatHistoryNavigationButtonNode let mentionsButton: ChatHistoryNavigationButtonNode @@ -71,7 +72,8 @@ final class ChatHistoryNavigationButtons: ASDisplayNode { } } - init(theme: PresentationTheme, dateTimeFormat: PresentationDateTimeFormat, backgroundNode: WallpaperBackgroundNode) { + init(theme: PresentationTheme, dateTimeFormat: PresentationDateTimeFormat, backgroundNode: WallpaperBackgroundNode, isChatRotated: Bool) { + self.isChatRotated = isChatRotated self.theme = theme self.dateTimeFormat = dateTimeFormat @@ -83,7 +85,7 @@ final class ChatHistoryNavigationButtons: ASDisplayNode { self.reactionsButton.alpha = 0.0 self.reactionsButton.isHidden = true - self.downButton = ChatHistoryNavigationButtonNode(theme: theme, backgroundNode: backgroundNode, type: .down) + self.downButton = ChatHistoryNavigationButtonNode(theme: theme, backgroundNode: backgroundNode, type: isChatRotated ? .down : .up) self.downButton.alpha = 0.0 self.downButton.isHidden = true @@ -190,11 +192,15 @@ final class ChatHistoryNavigationButtons: ASDisplayNode { transition.updateTransformScale(node: self.reactionsButton, scale: 0.2) } - transition.updatePosition(node: self.downButton, position: CGRect(origin: CGPoint(x: 0.0, y: completeSize.height - buttonSize.height), size: buttonSize).center) - - transition.updatePosition(node: self.mentionsButton, position: CGRect(origin: CGPoint(x: 0.0, y: completeSize.height - buttonSize.height - mentionsOffset), size: buttonSize).center) - - transition.updatePosition(node: self.reactionsButton, position: CGRect(origin: CGPoint(x: 0.0, y: completeSize.height - buttonSize.height - mentionsOffset - reactionsOffset), size: buttonSize).center) + if self.isChatRotated { + transition.updatePosition(node: self.downButton, position: CGRect(origin: CGPoint(x: 0.0, y: completeSize.height - buttonSize.height), size: buttonSize).center) + transition.updatePosition(node: self.mentionsButton, position: CGRect(origin: CGPoint(x: 0.0, y: completeSize.height - buttonSize.height - mentionsOffset), size: buttonSize).center) + transition.updatePosition(node: self.reactionsButton, position: CGRect(origin: CGPoint(x: 0.0, y: completeSize.height - buttonSize.height - mentionsOffset - reactionsOffset), size: buttonSize).center) + } else { + transition.updatePosition(node: self.downButton, position: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: buttonSize).center) + transition.updatePosition(node: self.mentionsButton, position: CGRect(origin: CGPoint(x: 0.0, y: mentionsOffset), size: buttonSize).center) + transition.updatePosition(node: self.reactionsButton, position: CGRect(origin: CGPoint(x: 0.0, y: mentionsOffset + reactionsOffset), size: buttonSize).center) + } if let (rect, containerSize) = self.absoluteRect { self.update(rect: rect, within: containerSize, transition: transition) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift index eac1111677f..fe7d3204546 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift @@ -10,7 +10,7 @@ import ForwardAccessoryPanelNode import ReplyAccessoryPanelNode func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: AccessoryPanelNode?, chatControllerInteraction: ChatControllerInteraction?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> AccessoryPanelNode? { - if case .standard(previewing: true) = chatPresentationInterfaceState.mode { + if case .standard(.previewing) = chatPresentationInterfaceState.mode { return nil } if let _ = chatPresentationInterfaceState.interfaceState.selectionState { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 4473825f084..8f99a2c0a52 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -267,7 +267,7 @@ private func canViewReadStats(message: Message, participantCount: Int?, isMessag return true } -func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceState) -> Bool { +func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, accountPeerId: PeerId) -> Bool { guard let peer = chatPresentationInterfaceState.renderedPeer?.peer else { return false } @@ -285,9 +285,14 @@ func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceS switch chatPresentationInterfaceState.mode { case .inline: return false + case .standard(.embedded): + return false default: break } + if case let .replyThread(replyThreadMessage) = chatPresentationInterfaceState.chatLocation, replyThreadMessage.peerId == accountPeerId { + return false + } if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) { if let threadData = chatPresentationInterfaceState.threadData { @@ -370,7 +375,7 @@ func messageMediaEditingOptions(message: Message) -> MessageMediaEditingOptions case .Sticker: return [] case .Animated: - return [] + break case let .Video(_, _, flags, _): if flags.contains(.instantRoundVideo) { return [] @@ -442,6 +447,11 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState return .single(ContextController.Items(content: .list([]))) } + var isEmbeddedMode = false + if case .standard(.embedded) = chatPresentationInterfaceState.mode { + isEmbeddedMode = true + } + var hasExpandedAudioTranscription = false if let messageNode = messageNode as? ChatMessageBubbleItemNode { hasExpandedAudioTranscription = messageNode.hasExpandedAudioTranscription() @@ -527,7 +537,6 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState actions.append(.separator) if chatPresentationInterfaceState.copyProtectionEnabled { - } else { actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuCopy, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.actionSheet.primaryTextColor) @@ -623,7 +632,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } } - var canReply = canReplyInChat(chatPresentationInterfaceState) + var canReply = canReplyInChat(chatPresentationInterfaceState, accountPeerId: context.account.peerId) var canPin = false let canSelect = !isAction @@ -736,7 +745,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState let readCounters: Signal if case let .replyThread(threadMessage) = chatPresentationInterfaceState.chatLocation, threadMessage.isForumPost { - readCounters = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.ThreadData(id: threadMessage.messageId.peerId, threadId: Int64(threadMessage.messageId.id))) + readCounters = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.ThreadData(id: threadMessage.peerId, threadId: threadMessage.threadId)) |> map { threadData -> Bool in guard let threadData else { return false @@ -787,7 +796,20 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState loggingSettings = LoggingSettings.defaultSettings } - return (MessageContextMenuData(starStatus: stickerSaveStatus, canReply: canReply, canPin: canPin, canEdit: canEdit, canSelect: canSelect, resourceStatus: resourceStatus, messageActions: messageActions), updatingMessageMedia, infoSummaryData, appConfig, isMessageRead, messageViewsPrivacyTips, availableReactions, translationSettings, loggingSettings, notificationSoundList, accountPeer) + return (MessageContextMenuData( + starStatus: stickerSaveStatus, + canReply: canReply && !isEmbeddedMode, + canPin: canPin && !isEmbeddedMode, + canEdit: canEdit && !isEmbeddedMode, + canSelect: canSelect && !isEmbeddedMode, + resourceStatus: resourceStatus, + messageActions: isEmbeddedMode ? ChatAvailableMessageActions( + options: [], + banAuthor: nil, + disableDelete: true, + isCopyProtected: messageActions.isCopyProtected + ) : messageActions + ), updatingMessageMedia, infoSummaryData, appConfig, isMessageRead, messageViewsPrivacyTips, availableReactions, translationSettings, loggingSettings, notificationSoundList, accountPeer) } return dataSignal @@ -905,7 +927,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState actions.insert(.separator, at: 1) } - if !hasRateTranscription { + if !hasRateTranscription && message.minAutoremoveOrClearTimeout == nil { for media in message.media { if let file = media as? TelegramMediaFile, let size = file.size, size < 1 * 1024 * 1024, let duration = file.duration, duration < 60, (["audio/mpeg", "audio/mp3", "audio/mpeg3", "audio/ogg"] as [String]).contains(file.mimeType.lowercased()) { let fileName = file.fileName ?? "Tone" @@ -1319,7 +1341,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } else { for attribute in messages[0].attributes { if let attribute = attribute as? ReplyThreadMessageAttribute, attribute.count > 0 { - threadId = makeMessageThreadId(messages[0].id) + threadId = Int64(messages[0].id.id) threadMessageCount = Int(attribute.count) } } @@ -1327,7 +1349,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } else { for attribute in messages[0].attributes { if let attribute = attribute as? ReplyThreadMessageAttribute, attribute.count > 0 { - threadId = makeMessageThreadId(messages[0].id) + threadId = Int64(messages[0].id.id) threadMessageCount = Int(attribute.count) } } @@ -1475,7 +1497,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState }, action: { _, f in var threadMessageId: MessageId? if case let .replyThread(replyThreadMessage) = chatPresentationInterfaceState.chatLocation { - threadMessageId = replyThreadMessage.messageId + threadMessageId = replyThreadMessage.effectiveMessageId } let _ = (context.engine.messages.exportMessageLink(peerId: message.id.peerId, messageId: message.id, isThread: threadMessageId != nil) |> map { result -> String? in @@ -1926,7 +1948,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } // - // MARK: - Nicegram Speech2Text + // MARK: Nicegram Speech2Text if !isSecretChat, let mediaFile = message.media.compactMap({ $0 as? TelegramMediaFile }).first(where: { $0.isVoice }) { @@ -1945,22 +1967,26 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState return generateTintedImage(image: UIImage(bundleImageName: "NicegramN"), color: theme.actionSheet.primaryTextColor) } action: { controller, f in if mode == "do" { - let processor = TgVoiceToTextProcessor(mediaBox: context.account.postbox.mediaBox, additionalLanguageCodes: [locale]) - message.setSpeechToTextLoading(context: context) - processor.recognize(mediaFile: mediaFile) { result in - switch result { - case .success(let translation): - message.setSpeechToTextTranslation(translation, context: context) - case .failure(let error): - message.removeSpeechToTextMeta(context: context) + if #available(iOS 13.0, *) { + Task { @MainActor in + let manager = TgSpeechToTextManager(mediaBox: context.account.postbox.mediaBox) + + message.setSpeechToTextLoading(context: context) + + let result = await manager.convertSpeechToText( + mediaFile: mediaFile + ) - switch error { - case .needPremium: + switch result { + case .success(let translation): + message.setSpeechToTextTranslation(translation, context: context) + case .needsPremium: + message.removeSpeechToTextMeta(context: context) + PremiumUITgHelper.routeToPremium() - case .lowAccuracy: - let c = getIAPErrorController(context: context, l("Messages.SpeechToText.LowAccuracyError", locale), presentationData) - controllerInteraction.presentGlobalOverlayController(c, nil) - case .underlying(_): + case .error(let error): + message.removeSpeechToTextMeta(context: context) + let c = getIAPErrorController(context: context, error.localizedDescription, presentationData) controllerInteraction.presentGlobalOverlayController(c, nil) } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift index 5d9a9bae6d8..06764a40f93 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift @@ -12,6 +12,10 @@ import ChatChannelSubscriberInputPanelNode import ChatMessageSelectionInputPanelNode func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: ChatInputPanelNode?, currentSecondaryPanel: ChatInputPanelNode?, textInputPanelNode: ChatTextInputPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> (primary: ChatInputPanelNode?, secondary: ChatInputPanelNode?) { + if case .standard(.embedded) = chatPresentationInterfaceState.mode { + return (nil, nil) + } + if let renderedPeer = chatPresentationInterfaceState.renderedPeer, renderedPeer.peer?.restrictionText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) != nil { if isAllowedChat(peer: renderedPeer.peer, contentSettings: context.currentContentSettings.with { $0 }) { } else { @@ -126,14 +130,25 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } } - if case let .replyThread(message) = chatPresentationInterfaceState.chatLocation, message.messageId.peerId == context.account.peerId { - if let currentPanel = (currentPanel as? ChatChannelSubscriberInputPanelNode) ?? (currentSecondaryPanel as? ChatChannelSubscriberInputPanelNode) { - return (currentPanel, nil) + if case let .replyThread(message) = chatPresentationInterfaceState.chatLocation, message.peerId == context.account.peerId { + if EnginePeer.Id(message.threadId).isAnonymousSavedMessages { + if let currentPanel = (currentPanel as? ChatRestrictedInputPanelNode) ?? (currentSecondaryPanel as? ChatRestrictedInputPanelNode) { + return (currentPanel, nil) + } else { + let panel = ChatRestrictedInputPanelNode() + panel.context = context + panel.interfaceInteraction = interfaceInteraction + return (panel, nil) + } } else { - let panel = ChatChannelSubscriberInputPanelNode() - panel.interfaceInteraction = interfaceInteraction - panel.context = context - return (panel, nil) + if let currentPanel = (currentPanel as? ChatChannelSubscriberInputPanelNode) ?? (currentSecondaryPanel as? ChatChannelSubscriberInputPanelNode) { + return (currentPanel, nil) + } else { + let panel = ChatChannelSubscriberInputPanelNode() + panel.interfaceInteraction = interfaceInteraction + panel.context = context + return (panel, nil) + } } } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift index 90475e41d26..8cf582a26de 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift @@ -71,7 +71,7 @@ func rightNavigationButtonForChatInterfaceState(context: AccountContext, present } } - if case let .replyThread(message) = presentationInterfaceState.chatLocation, message.messageId.peerId == context.account.peerId { + if case let .replyThread(message) = presentationInterfaceState.chatLocation, message.peerId == context.account.peerId { return chatInfoNavigationButton } @@ -142,7 +142,7 @@ func rightNavigationButtonForChatInterfaceState(context: AccountContext, present return chatInfoNavigationButton } - if case .standard(true) = presentationInterfaceState.mode { + if case .standard(.previewing) = presentationInterfaceState.mode { return chatInfoNavigationButton } else if let peer = presentationInterfaceState.renderedPeer?.peer { if presentationInterfaceState.accountPeerId == peer.id { diff --git a/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift b/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift index 67be7252337..61fa9d19138 100644 --- a/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift +++ b/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift @@ -1,11 +1,18 @@ import Foundation import UIKit +import AsyncDisplayKit import Display import ContextUI import Postbox import TelegramCore import SwiftSignalKit import ChatMessageItemView +import AccountContext +import WallpaperBackgroundNode +import TelegramPresentationData +import DustEffect +import TooltipUI +import TelegramNotices final class ChatMessageContextLocationContentSource: ContextLocationContentSource { private let controller: ViewController @@ -25,6 +32,7 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou let keepInPlace: Bool = false let ignoreContentTouches: Bool = false let blurBackground: Bool = true + let centerVertically: Bool private weak var chatNode: ChatControllerNode? private let engine: TelegramEngine @@ -47,11 +55,12 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou |> distinctUntilChanged } - init(chatNode: ChatControllerNode, engine: TelegramEngine, message: Message, selectAll: Bool) { + init(chatNode: ChatControllerNode, engine: TelegramEngine, message: Message, selectAll: Bool, centerVertically: Bool = false) { self.chatNode = chatNode self.engine = engine self.message = message self.selectAll = selectAll + self.centerVertically = centerVertically } func takeView() -> ContextControllerTakeViewInfo? { @@ -95,6 +104,259 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou } } +final class ChatViewOnceMessageContextExtractedContentSource: ContextExtractedContentSource { + let keepInPlace: Bool = false + let ignoreContentTouches: Bool = false + let blurBackground: Bool = true + let centerVertically: Bool = true + + private let context: AccountContext + private let presentationData: PresentationData + private weak var chatNode: ChatControllerNode? + private weak var backgroundNode: WallpaperBackgroundNode? + private let engine: TelegramEngine + private let message: Message + private let present: (ViewController) -> Void + + private var messageNodeCopy: ChatMessageItemView? + private weak var tooltipController: TooltipScreen? + + private let idleTimerExtensionDisposable = MetaDisposable() + + var shouldBeDismissed: Signal { + return self.context.sharedContext.mediaManager.globalMediaPlayerState + |> filter { playlistStateAndType in + if let (_, state, _) = playlistStateAndType, case .state = state { + return true + } else { + return false + } + } + |> take(1) + |> map { _ in + return false + } + |> then( + self.context.sharedContext.mediaManager.globalMediaPlayerState + |> filter { playlistStateAndType in + return playlistStateAndType == nil + } + |> take(1) + |> map { _ in + return true + } + ) + } + + init(context: AccountContext, presentationData: PresentationData, chatNode: ChatControllerNode, backgroundNode: WallpaperBackgroundNode, engine: TelegramEngine, message: Message, present: @escaping (ViewController) -> Void) { + self.context = context + self.presentationData = presentationData + self.chatNode = chatNode + self.backgroundNode = backgroundNode + self.engine = engine + self.message = message + self.present = present + } + + deinit { + self.idleTimerExtensionDisposable.dispose() + } + + func takeView() -> ContextControllerTakeViewInfo? { + guard let chatNode = self.chatNode, let backgroundNode = self.backgroundNode, let validLayout = chatNode.validLayout?.0 else { + return nil + } + + self.idleTimerExtensionDisposable.set(self.context.sharedContext.applicationBindings.pushIdleTimerExtension()) + + var result: ContextControllerTakeViewInfo? + var sourceNode: ContextExtractedContentContainingNode? + var sourceRect: CGRect = .zero + chatNode.historyNode.forEachItemNode { itemNode in + guard let itemNode = itemNode as? ChatMessageItemView else { + return + } + guard let item = itemNode.item else { + return + } + if item.content.contains(where: { $0.0.stableId == self.message.stableId }), let contentNode = itemNode.getMessageContextSourceNode(stableId: self.message.stableId) { + sourceNode = contentNode + sourceRect = itemNode.frame + } + } + + let isIncoming = self.message.effectivelyIncoming(self.context.account.peerId) + let isVideo = (self.message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile)?.isInstantVideo ?? false + + var tooltipSourceRect: CGRect = .zero + + if let sourceNode { + var bubbleWidth: CGFloat = 0.0 + + if (isIncoming || "".isEmpty) && !isVideo { + let messageItem = self.context.sharedContext.makeChatMessagePreviewItem( + context: self.context, + messages: [self.message], + theme: self.presentationData.theme, + strings: self.presentationData.strings, + wallpaper: self.presentationData.chatWallpaper, + fontSize: self.presentationData.chatFontSize, + chatBubbleCorners: self.presentationData.chatBubbleCorners, + dateTimeFormat: self.presentationData.dateTimeFormat, + nameOrder: self.presentationData.nameDisplayOrder, + forcedResourceStatus: nil, + tapMessage: nil, + clickThroughMessage: nil, + backgroundNode: backgroundNode, + availableReactions: nil, + accountPeer: nil, + isCentered: false, + isPreview: false + ) + + let params = ListViewItemLayoutParams(width: chatNode.historyNode.frame.width, leftInset: validLayout.safeInsets.left, rightInset: validLayout.safeInsets.right, availableHeight: chatNode.historyNode.frame.height, isStandalone: false) + var node: ListViewItemNode? + + messageItem.nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: nil, nextItem: nil, completion: { messageNode, apply in + node = messageNode + apply().1(ListViewItemApply(isOnScreen: true)) + }) + + if let messageNode = node as? ChatMessageItemView, let copyContentNode = messageNode.getMessageContextSourceNode(stableId: self.message.stableId) { + messageNode.frame.origin.y = chatNode.frame.height - sourceRect.origin.y - sourceRect.size.height + chatNode.addSubnode(messageNode) + result = ContextControllerTakeViewInfo(containingItem: .node(copyContentNode), contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil)) + + bubbleWidth = copyContentNode.contentNode.subnodes?.first?.frame.width ?? messageNode.frame.width + } + + self.messageNodeCopy = node as? ChatMessageItemView + } else { + result = ContextControllerTakeViewInfo(containingItem: .node(sourceNode), contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil)) + } + + tooltipSourceRect = CGRect(x: isIncoming ? 22.0 : chatNode.frame.width - bubbleWidth + 10.0, y: floorToScreenPixels((chatNode.frame.height - 75.0) / 2.0) - 43.0, width: 44.0, height: 44.0) + } + + if !isVideo { + let displayTooltip = { [weak self] in + guard let self else { + return + } + let absoluteFrame = tooltipSourceRect + let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.maxY), size: CGSize()) + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + var tooltipText: String? + if isIncoming { + tooltipText = presentationData.strings.Chat_PlayOnceVoiceMessageTooltip + } else if let peer = self.message.peers[self.message.id.peerId] { + let peerName = EnginePeer(peer).compactDisplayTitle + tooltipText = presentationData.strings.Chat_PlayOnceVoiceMessageYourTooltip(peerName).string + } + + if let tooltipText { + let tooltipController = TooltipScreen( + account: self.context.account, + sharedContext: self.context.sharedContext, + text: .markdown(text: tooltipText), + balancedTextLayout: true, + constrainWidth: 240.0, + style: .customBlur(UIColor(rgb: 0x18181a), 0.0), + arrowStyle: .small, + icon: nil, + location: .point(location, .bottom), + displayDuration: .custom(3.0), + inset: 8.0, + cornerRadius: 11.0, + shouldDismissOnTouch: { _, _ in + return .ignore + } + ) + self.tooltipController = tooltipController + self.present(tooltipController) + } + } + + if isIncoming { + let _ = (ApplicationSpecificNotice.getIncomingVoiceMessagePlayOnceTip(accountManager: self.context.sharedContext.accountManager) + |> deliverOnMainQueue).startStandalone(next: { [weak self] counter in + guard let self else { + return + } + if counter >= 2 { + return + } + Queue.mainQueue().after(0.3) { + displayTooltip() + } + let _ = ApplicationSpecificNotice.incrementIncomingVoiceMessagePlayOnceTip(accountManager: self.context.sharedContext.accountManager).startStandalone() + }) + } else { + let _ = (ApplicationSpecificNotice.getOutgoingVoiceMessagePlayOnceTip(accountManager: self.context.sharedContext.accountManager) + |> deliverOnMainQueue).startStandalone(next: { [weak self] counter in + guard let self else { + return + } + if counter >= 2 { + return + } + Queue.mainQueue().after(0.3) { + displayTooltip() + } + let _ = ApplicationSpecificNotice.incrementOutgoingVoiceMessagePlayOnceTip(accountManager: self.context.sharedContext.accountManager).startStandalone() + }) + } + } + return result + } + + private var dustEffectLayer: DustEffectLayer? + func putBack() -> ContextControllerPutBackViewInfo? { + guard let chatNode = self.chatNode else { + return nil + } + + self.idleTimerExtensionDisposable.set(nil) + + if let tooltipController = self.tooltipController { + tooltipController.dismiss() + } + + if let messageNodeCopy = self.messageNodeCopy, let sourceView = messageNodeCopy.supernode?.view, let contentNode = messageNodeCopy.getMessageContextSourceNode(stableId: nil)?.contentNode, let parentNode = contentNode.supernode?.supernode?.supernode { + let dustEffectLayer = DustEffectLayer() + dustEffectLayer.position = sourceView.bounds.center + dustEffectLayer.bounds = CGRect(origin: CGPoint(), size: sourceView.bounds.size) + dustEffectLayer.zPosition = 10.0 + parentNode.layer.addSublayer(dustEffectLayer) + + guard let (image, subFrame) = messageNodeCopy.makeContentSnapshot() else { + return nil + } + var itemFrame = subFrame //messageNodeCopy.layer.convert(subFrame, to: dustEffectLayer) + itemFrame.origin.y = floorToScreenPixels((sourceView.frame.height - subFrame.height) / 2.0) + dustEffectLayer.addItem(frame: itemFrame, image: image) + messageNodeCopy.removeFromSupernode() + contentNode.removeFromSupernode() + return nil + } else { + var result: ContextControllerPutBackViewInfo? + chatNode.historyNode.forEachItemNode { itemNode in + guard let itemNode = itemNode as? ChatMessageItemView else { + return + } + guard let item = itemNode.item else { + return + } + if item.content.contains(where: { $0.0.stableId == self.message.stableId }) { + result = ContextControllerPutBackViewInfo(contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil)) + } + } + return result + } + } +} + final class ChatMessageReactionContextExtractedContentSource: ContextExtractedContentSource { let keepInPlace: Bool = false let ignoreContentTouches: Bool = true diff --git a/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift index 9ee9c0168be..3cf0e0d099e 100644 --- a/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift @@ -15,10 +15,48 @@ import ChatPresentationInterfaceState import ChatSendButtonRadialStatusNode import AudioWaveformNode import ChatInputPanelNode +import TooltipUI +import TelegramNotices extension AudioWaveformNode: CustomMediaPlayerScrubbingForegroundNode { } +final class ChatRecordingPreviewViewForOverlayContent: UIView, ChatInputPanelViewForOverlayContent { + let ignoreHit: (UIView, CGPoint) -> Bool + + init(ignoreHit: @escaping (UIView, CGPoint) -> Bool) { + self.ignoreHit = ignoreHit + + super.init(frame: CGRect()) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + func maybeDismissContent(point: CGPoint) { + for subview in self.subviews.reversed() { + if let _ = subview.hitTest(self.convert(point, to: subview), with: nil) { + return + } + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + for subview in self.subviews.reversed() { + if let result = subview.hitTest(self.convert(point, to: subview), with: event) { + return result + } + } + + if event == nil || self.ignoreHit(self, point) { + return nil + } + + return nil + } +} + final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { let deleteButton: HighlightableButtonNode let binNode: AnimationNode @@ -29,6 +67,9 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { private let waveformButton: ASButtonNode let waveformBackgroundNode: ASImageNode + private var viewOnce = false + let viewOnceButton: ChatRecordingViewOnceButtonNode + private let waveformNode: AudioWaveformNode private let waveformForegroundNode: AudioWaveformNode let waveformScubberNode: MediaPlayerScrubbingNode @@ -63,6 +104,8 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { self.sendButton.displaysAsynchronously = false self.sendButton.setImage(PresentationResourcesChat.chatInputPanelSendButtonImage(theme), for: []) + self.viewOnceButton = ChatRecordingViewOnceButtonNode() + self.waveformBackgroundNode = ASImageNode() self.waveformBackgroundNode.isLayerBacked = true self.waveformBackgroundNode.displaysAsynchronously = false @@ -92,6 +135,21 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { super.init() + self.viewForOverlayContent = ChatRecordingPreviewViewForOverlayContent( + ignoreHit: { [weak self] view, point in + guard let strongSelf = self else { + return false + } + if strongSelf.view.hitTest(view.convert(point, to: strongSelf.view), with: nil) != nil { + return true + } + if view.convert(point, to: strongSelf.view).y > strongSelf.view.bounds.maxY { + return true + } + return false + } + ) + self.addSubnode(self.deleteButton) self.deleteButton.addSubnode(self.binNode) self.addSubnode(self.waveformBackgroundNode) @@ -111,9 +169,10 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { } } } - + self.deleteButton.addTarget(self, action: #selector(self.deletePressed), forControlEvents: [.touchUpInside]) self.sendButton.addTarget(self, action: #selector(self.sendPressed), forControlEvents: [.touchUpInside]) + self.viewOnceButton.addTarget(self, action: #selector(self.viewOncePressed), forControlEvents: [.touchUpInside]) self.waveformButton.addTarget(self, action: #selector(self.waveformPressed), forControlEvents: .touchUpInside) } @@ -135,9 +194,38 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { } strongSelf.interfaceInteraction?.displaySendMessageOptions(strongSelf.sendButton, gesture) } + + if let viewForOverlayContent = self.viewForOverlayContent { + viewForOverlayContent.addSubnode(self.viewOnceButton) + } + } + + private func maybePresentViewOnceTooltip() { + guard let context = self.context else { + return + } + let _ = (ApplicationSpecificNotice.getVoiceMessagesPlayOnceSuggestion(accountManager: context.sharedContext.accountManager) + |> deliverOnMainQueue).startStandalone(next: { [weak self] counter in + guard let self, let interfaceState = self.presentationInterfaceState else { + return + } + if counter >= 3 { + return + } + + Queue.mainQueue().after(0.3) { + self.displayViewOnceTooltip(text: interfaceState.strings.Chat_TapToPlayVoiceMessageOnceTooltip, hasIcon: true) + } + + let _ = ApplicationSpecificNotice.incrementVoiceMessagesPlayOnceSuggestion(accountManager: context.sharedContext.accountManager).startStandalone() + }) } override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { + var isFirstTime = false + if self.presentationInterfaceState == nil { + isFirstTime = true + } if self.presentationInterfaceState != interfaceState { var updateWaveform = false if self.presentationInterfaceState?.recordedMediaPreview != interfaceState.recordedMediaPreview { @@ -148,8 +236,9 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { self.sendButton.accessibilityLabel = interfaceState.strings.VoiceOver_MessageContextSend self.waveformButton.accessibilityLabel = interfaceState.strings.VoiceOver_Chat_RecordPreviewVoiceMessage } - self.presentationInterfaceState = interfaceState + self.presentationInterfaceState = interfaceState + if let recordedMediaPreview = interfaceState.recordedMediaPreview, updateWaveform { self.waveformNode.setup(color: interfaceState.theme.chat.inputPanel.actionControlForegroundColor.withAlphaComponent(0.5), gravity: .center, waveform: recordedMediaPreview.waveform) self.waveformForegroundNode.setup(color: interfaceState.theme.chat.inputPanel.actionControlForegroundColor, gravity: .center, waveform: recordedMediaPreview.waveform) @@ -182,12 +271,20 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { } } - let panelHeight = defaultHeight(metrics: metrics) + if isFirstTime, !self.viewOnceButton.isHidden { + self.maybePresentViewOnceTooltip() + } + let panelHeight = defaultHeight(metrics: metrics) + transition.updateFrame(node: self.deleteButton, frame: CGRect(origin: CGPoint(x: leftInset + 2.0 - UIScreenPixel, y: 1), size: CGSize(width: 40.0, height: 40))) transition.updateFrame(node: self.sendButton, frame: CGRect(origin: CGPoint(x: width - rightInset - 43.0 - UIScreenPixel, y: 2 - UIScreenPixel), size: CGSize(width: 44.0, height: 44))) self.binNode.frame = self.deleteButton.bounds + let viewOnceSize = self.viewOnceButton.update(theme: interfaceState.theme) + let viewOnceButtonFrame = CGRect(origin: CGPoint(x: width - rightInset - 44.0 - UIScreenPixel, y: -64.0), size: viewOnceSize) + transition.updateFrame(node: self.viewOnceButton, frame: viewOnceButtonFrame) + var isScheduledMessages = false if case .scheduledMessages = interfaceState.subject { isScheduledMessages = true @@ -229,6 +326,17 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { if let prevTextInputPanelNode = self.prevInputPanelNode as? ChatTextInputPanelNode { self.prevInputPanelNode = nil + self.viewOnceButton.isHidden = prevTextInputPanelNode.viewOnceButton.isHidden + self.viewOnce = prevTextInputPanelNode.viewOnce + self.viewOnceButton.update(isSelected: self.viewOnce, animated: false) + + prevTextInputPanelNode.viewOnceButton.isHidden = true + prevTextInputPanelNode.viewOnce = false + self.viewOnceButton.layer.animatePosition(from: prevTextInputPanelNode.viewOnceButton.position, to: self.viewOnceButton.position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in + prevTextInputPanelNode.viewOnceButton.isHidden = false + prevTextInputPanelNode.viewOnceButton.update(isSelected: false, animated: false) + }) + if let audioRecordingDotNode = prevTextInputPanelNode.audioRecordingDotNode { let startAlpha = CGFloat(audioRecordingDotNode.layer.presentation()?.opacity ?? 1.0) audioRecordingDotNode.layer.removeAllAnimations() @@ -283,12 +391,63 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { } @objc func deletePressed() { + self.tooltipController?.dismiss() + self.mediaPlayer?.pause() self.interfaceInteraction?.deleteRecordedMedia() } @objc func sendPressed() { - self.interfaceInteraction?.sendRecordedMedia(false) + self.tooltipController?.dismiss() + + self.interfaceInteraction?.sendRecordedMedia(false, self.viewOnce) + } + + private weak var tooltipController: TooltipScreen? + @objc private func viewOncePressed() { + guard let context = self.context, let interfaceState = self.presentationInterfaceState else { + return + } + self.viewOnce = !self.viewOnce + + self.viewOnceButton.update(isSelected: self.viewOnce, animated: true) + + self.tooltipController?.dismiss() + if self.viewOnce { + self.displayViewOnceTooltip(text: interfaceState.strings.Chat_PlayVoiceMessageOnceTooltip, hasIcon: true) + + let _ = ApplicationSpecificNotice.incrementVoiceMessagesPlayOnceSuggestion(accountManager: context.sharedContext.accountManager, count: 3).startStandalone() + } + } + + private func displayViewOnceTooltip(text: String, hasIcon: Bool) { + guard let context = self.context, let parentController = self.interfaceInteraction?.chatController() else { + return + } + + let absoluteFrame = self.viewOnceButton.view.convert(self.viewOnceButton.bounds, to: parentController.view) + let location = CGRect(origin: CGPoint(x: absoluteFrame.midX - 20.0, y: absoluteFrame.midY), size: CGSize()) + + let tooltipController = TooltipScreen( + account: context.account, + sharedContext: context.sharedContext, + text: .markdown(text: text), + balancedTextLayout: true, + constrainWidth: 240.0, + style: .customBlur(UIColor(rgb: 0x18181a), 0.0), + arrowStyle: .small, + icon: hasIcon ? .animation(name: "anim_autoremove_on", delay: 0.1, tintColor: nil) : nil, + location: .point(location, .right), + displayDuration: .default, + inset: 8.0, + cornerRadius: 8.0, + shouldDismissOnTouch: { _, _ in + return .ignore + } + ) + self.tooltipController = tooltipController + + parentController.present(tooltipController, in: .window(.root)) } @objc func waveformPressed() { @@ -353,3 +512,94 @@ private final class PlayPauseIconNode: ManagedAnimationNode { } } } + + +final class ChatRecordingViewOnceButtonNode: HighlightTrackingButtonNode { + private let backgroundNode: ASImageNode + private let iconNode: ASImageNode + + private var theme: PresentationTheme? + + override init(pointerStyle: PointerStyle? = nil) { + self.backgroundNode = ASImageNode() + self.backgroundNode.isUserInteractionEnabled = false + + self.iconNode = ASImageNode() + self.iconNode.isUserInteractionEnabled = false + + super.init(pointerStyle: pointerStyle) + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.iconNode) + + self.highligthedChanged = { [weak self] highlighted in + if let self, self.bounds.width > 0.0 { + let topScale: CGFloat = (self.bounds.width - 8.0) / self.bounds.width + let maxScale: CGFloat = (self.bounds.width + 2.0) / self.bounds.width + + if highlighted { + self.layer.removeAnimation(forKey: "sublayerTransform") + let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) + transition.updateTransformScale(node: self, scale: topScale) + } else { + let transition = ContainedViewLayoutTransition.immediate + transition.updateTransformScale(node: self, scale: 1.0) + + self.layer.animateScale(from: topScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in + guard let self else { + return + } + + self.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue) + }) + } + } + } + } + + private var innerIsSelected = false + func update(isSelected: Bool, animated: Bool = false) { + guard let theme = self.theme else { + return + } + + let updated = self.iconNode.image == nil || self.innerIsSelected != isSelected + self.innerIsSelected = isSelected + + if animated, updated && self.iconNode.image != nil, let snapshot = self.iconNode.view.snapshotContentTree() { + self.view.addSubview(snapshot) + snapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + snapshot.removeFromSuperview() + }) + + self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + if updated { + self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: self.innerIsSelected ? "Media Gallery/ViewOnceEnabled" : "Media Gallery/ViewOnce"), color: theme.chat.inputPanel.panelControlAccentColor) + } + } + + func update(theme: PresentationTheme) -> CGSize { + let size = CGSize(width: 44.0, height: 44.0) + let innerSize = CGSize(width: 40.0, height: 40.0) + + if self.theme !== theme { + self.theme = theme + + self.backgroundNode.image = generateFilledCircleImage(diameter: innerSize.width, color: theme.rootController.navigationBar.opaqueBackgroundColor, strokeColor: theme.chat.inputPanel.panelSeparatorColor, strokeWidth: 0.5, backgroundColor: nil) + self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: self.innerIsSelected ? "Media Gallery/ViewOnceEnabled" : "Media Gallery/ViewOnce"), color: theme.chat.inputPanel.panelControlAccentColor) + } + + if let backgroundImage = self.backgroundNode.image { + let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(size.width / 2.0 - backgroundImage.size.width / 2.0), y: floorToScreenPixels(size.height / 2.0 - backgroundImage.size.height / 2.0)), size: backgroundImage.size) + self.backgroundNode.frame = backgroundFrame + } + + if let iconImage = self.iconNode.image { + let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(size.width / 2.0 - iconImage.size.width / 2.0), y: floorToScreenPixels(size.height / 2.0 - iconImage.size.height / 2.0)), size: iconImage.size) + self.iconNode.frame = iconFrame + } + return size + } +} diff --git a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift index 3bc3af51e12..75efeff8ef6 100644 --- a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift @@ -46,7 +46,9 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode { var iconImage: UIImage? - if let threadData = interfaceState.threadData, threadData.isClosed { + if case let .replyThread(message) = interfaceState.chatLocation, message.peerId == self.context?.account.peerId { + self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Chat_PanelStatusAuthorHidden, font: Font.regular(13.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor) + } else if let threadData = interfaceState.threadData, threadData.isClosed { iconImage = PresentationResourcesChat.chatPanelLockIcon(interfaceState.theme) self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Chat_PanelTopicClosedText, font: Font.regular(15.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor) } else if let channel = interfaceState.renderedPeer?.peer as? TelegramChannel, channel.flags.contains(.isForum), case .peer = interfaceState.chatLocation { diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index 5b124d6689d..a99bd19f415 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -251,7 +251,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe switch item.content { case let .peer(peerData): if let message = peerData.messages.first { - let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerData.peer.peerId), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), botStart: nil, mode: .standard(previewing: true)) + let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerData.peer.peerId), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), botStart: nil, mode: .standard(.previewing)) chatController.canReadHistory.set(false) let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: .single(ContextController.Items(content: .list([]))), gesture: gesture) presentInGlobalOverlay(contextController) diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 85c5680d23e..42f947d0260 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -566,6 +566,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch var audioRecordingCancelIndicator: ChatTextInputAudioRecordingCancelIndicator? var animatingBinNode: AnimationNode? + var viewOnce = false + let viewOnceButton: ChatRecordingViewOnceButtonNode + private var accessoryItemButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButtonNode)] = [] private var validLayout: (CGFloat, CGFloat, CGFloat, CGFloat, UIEdgeInsets, CGFloat, LayoutMetrics, Bool, Bool)? @@ -860,6 +863,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.counterTextNode = ImmediateTextNode() self.counterTextNode.textAlignment = .center + self.viewOnceButton = ChatRecordingViewOnceButtonNode() + super.init() self.viewForOverlayContent = ChatTextViewForOverlayContent( @@ -996,13 +1001,15 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch if let strongSelf = self, let interfaceState = strongSelf.presentationInterfaceState, let interfaceInteraction = strongSelf.interfaceInteraction { if let _ = interfaceState.inputTextPanelState.mediaRecordingState { if sendMedia { - interfaceInteraction.finishMediaRecording(.send) + interfaceInteraction.finishMediaRecording(.send(viewOnce: strongSelf.viewOnce)) } else { interfaceInteraction.finishMediaRecording(.dismiss) } } else { interfaceInteraction.finishMediaRecording(.dismiss) } + strongSelf.viewOnce = false + strongSelf.tooltipController?.dismiss() } } self.actionButtons.micButton.offsetRecordingControls = { [weak self] in @@ -1022,6 +1029,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.actionButtons.micButton.stopRecording = { [weak self] in if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { interfaceInteraction.stopMediaRecording() + + strongSelf.tooltipController?.dismiss() } } self.actionButtons.micButton.updateLocked = { [weak self] _ in @@ -1109,6 +1118,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch return EmojiTextAttachmentView(context: context, userLocation: .other, emoji: emoji, file: emoji.file, cache: presentationContext.animationCache, renderer: presentationContext.animationRenderer, placeholderColor: presentationInterfaceState.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12), pointSize: CGSize(width: pointSize, height: pointSize)) } } + + self.viewOnceButton.addTarget(self, action: #selector(self.viewOncePressed), forControlEvents: [.touchUpInside]) } required init?(coder aDecoder: NSCoder) { @@ -1122,6 +1133,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.currentEmojiSuggestion?.disposable.dispose() } + override func didLoad() { + super.didLoad() + + if let viewForOverlayContent = self.viewForOverlayContent { + viewForOverlayContent.addSubnode(self.viewOnceButton) + } + } + func loadTextInputNodeIfNeeded() { if self.textInputNode == nil { self.loadTextInputNode() @@ -2051,7 +2070,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch animateCancelSlideIn = transition.isAnimated && mediaRecordingState != nil audioRecordingCancelIndicator = ChatTextInputAudioRecordingCancelIndicator(theme: interfaceState.theme, strings: interfaceState.strings, cancel: { [weak self] in + self?.viewOnce = false self?.interfaceInteraction?.finishMediaRecording(.dismiss) + self?.tooltipController?.dismiss() }) self.audioRecordingCancelIndicator = audioRecordingCancelIndicator self.clippingNode.insertSubnode(audioRecordingCancelIndicator, at: 0) @@ -2322,7 +2343,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch mediaRecordingAccessibilityArea.accessibilityTraits = [.button, .startsMediaSession] self.mediaRecordingAccessibilityArea = mediaRecordingAccessibilityArea mediaRecordingAccessibilityArea.activate = { [weak self] in - self?.interfaceInteraction?.finishMediaRecording(.send) + if let self { + self.interfaceInteraction?.finishMediaRecording(.send(viewOnce: self.viewOnce)) + } return true } self.clippingNode.insertSubnode(mediaRecordingAccessibilityArea, aboveSubnode: self.actionButtons) @@ -2560,6 +2583,16 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch if let prevPreviewInputPanelNode = self.prevInputPanelNode as? ChatRecordingPreviewInputPanelNode { self.prevInputPanelNode = nil + if prevPreviewInputPanelNode.viewOnceButton.alpha > 0.0 { + if let snapshotView = prevPreviewInputPanelNode.viewOnceButton.view.snapshotContentTree() { + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + snapshotView.removeFromSuperview() + }) + snapshotView.layer.animateScale(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.viewForOverlayContent?.addSubview(snapshotView) + } + } + prevPreviewInputPanelNode.gestureRecognizer?.isEnabled = false prevPreviewInputPanelNode.isUserInteractionEnabled = false @@ -2636,6 +2669,27 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } + + let viewOnceSize = self.viewOnceButton.update(theme: interfaceState.theme) + let viewOnceButtonFrame = CGRect(origin: CGPoint(x: width - rightInset - 44.0 - UIScreenPixel, y: -152.0), size: viewOnceSize) + self.viewOnceButton.bounds = CGRect(origin: .zero, size: viewOnceButtonFrame.size) + transition.updatePosition(node: self.viewOnceButton, position: viewOnceButtonFrame.center) + + var viewOnceIsVisible = false + if let recordingState = interfaceState.inputTextPanelState.mediaRecordingState, case let .audio(_, isLocked) = recordingState, isLocked { + viewOnceIsVisible = true + } + if self.viewOnceButton.alpha.isZero && viewOnceIsVisible { + self.viewOnceButton.update(isSelected: self.viewOnce, animated: false) + } + transition.updateAlpha(node: self.viewOnceButton, alpha: viewOnceIsVisible ? 1.0 : 0.0) + transition.updateTransformScale(node: self.viewOnceButton, scale: viewOnceIsVisible ? 1.0 : 0.01) + if let user = interfaceState.renderedPeer?.peer as? TelegramUser, user.id != interfaceState.accountPeerId && user.botInfo == nil { + self.viewOnceButton.isHidden = false + } else { + self.viewOnceButton.isHidden = true + } + var clippingDelta: CGFloat = 0.0 if case let .media(_, _, focused) = interfaceState.inputMode, focused { clippingDelta = -panelHeight @@ -2646,6 +2700,50 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch return panelHeight } + @objc private func viewOncePressed() { + guard let interfaceState = self.presentationInterfaceState else { + return + } + self.viewOnce = !self.viewOnce + + self.viewOnceButton.update(isSelected: self.viewOnce, animated: true) + + self.tooltipController?.dismiss() + if self.viewOnce { + self.displayViewOnceTooltip(text: interfaceState.strings.Chat_PlayVoiceMessageOnceTooltip) + } + } + + private func displayViewOnceTooltip(text: String) { + guard let context = self.context, let parentController = self.interfaceInteraction?.chatController() else { + return + } + + let absoluteFrame = self.viewOnceButton.view.convert(self.viewOnceButton.bounds, to: parentController.view) + let location = CGRect(origin: CGPoint(x: absoluteFrame.midX - 20.0, y: absoluteFrame.midY), size: CGSize()) + + let tooltipController = TooltipScreen( + account: context.account, + sharedContext: context.sharedContext, + text: .plain(text: text), + balancedTextLayout: true, + constrainWidth: 240.0, + style: .customBlur(UIColor(rgb: 0x18181a), 0.0), + arrowStyle: .small, + icon: .animation(name: "anim_autoremove_on", delay: 0.1, tintColor: nil), + location: .point(location, .right), + displayDuration: .default, + inset: 8.0, + cornerRadius: 8.0, + shouldDismissOnTouch: { _, _ in + return .ignore + } + ) + self.tooltipController = tooltipController + + parentController.present(tooltipController, in: .window(.root)) + } + override func canHandleTransition(from prevInputPanelNode: ChatInputPanelNode?) -> Bool { return prevInputPanelNode is ChatRecordingPreviewInputPanelNode } diff --git a/submodules/TelegramUI/Sources/MakeTempAccountContext.swift b/submodules/TelegramUI/Sources/MakeTempAccountContext.swift index 0fc439dd797..89f69e5ddba 100644 --- a/submodules/TelegramUI/Sources/MakeTempAccountContext.swift +++ b/submodules/TelegramUI/Sources/MakeTempAccountContext.swift @@ -27,6 +27,7 @@ public func makeTempContext( encryptionParameters: encryptionParameters, accountManager: accountManager, appLockContext: appLockContext, + notificationController: nil, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings, networkArguments: networkArguments, diff --git a/submodules/TelegramUI/Sources/ManagedAudioRecorder.swift b/submodules/TelegramUI/Sources/ManagedAudioRecorder.swift index d832f4dea84..3aa85a5df04 100644 --- a/submodules/TelegramUI/Sources/ManagedAudioRecorder.swift +++ b/submodules/TelegramUI/Sources/ManagedAudioRecorder.swift @@ -168,8 +168,6 @@ final class ManagedAudioRecorderContext { private var micLevelPeakCount: Int = 0 private var audioLevelPeakUpdate: Double = 0.0 - fileprivate var isPaused = false - private var recordingStateUpdateTimestamp: Double? private var hasAudioSession = false diff --git a/submodules/TelegramUI/Sources/NavigateToChatController.swift b/submodules/TelegramUI/Sources/NavigateToChatController.swift index f181e3c929e..cddcdac8277 100644 --- a/submodules/TelegramUI/Sources/NavigateToChatController.swift +++ b/submodules/TelegramUI/Sources/NavigateToChatController.swift @@ -20,6 +20,7 @@ import MediaEditorScreen import ChatControllerInteraction import SavedMessagesScreen import WallpaperGalleryScreen +import ChatMessageNotificationItem public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParams) { if case let .peer(peer) = params.chatLocation { @@ -75,23 +76,23 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam let controller = ChatListControllerImpl(context: params.context, location: .forum(peerId: peer.id), controlsHistoryPreload: false, enableDebugActions: false) let activateMessageSearch = params.activateMessageSearch + let chatListCompletion = params.chatListCompletion params.navigationController.pushViewController(controller, completion: { [weak controller] in - guard let controller, let activateMessageSearch else { + guard let controller else { return } - controller.activateSearch(query: activateMessageSearch.1) + if let activateMessageSearch { + controller.activateSearch(query: activateMessageSearch.1) + } + + if let chatListCompletion { + chatListCompletion(controller) + } }) return } - /*if case let .peer(peer) = params.chatLocation, peer.id == params.context.account.peerId { - let savedMessagesScreen = SavedMessagesScreen(context: params.context) - params.navigationController.pushViewController(savedMessagesScreen, completion: { - }) - return - }*/ - var found = false var isFirst = true if params.useExisting { @@ -268,7 +269,7 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam return true } case let .replyThread(replyThreadMessage): - if message.id.peerId == replyThreadMessage.messageId.peerId { + if message.id.peerId == replyThreadMessage.peerId { return true } } diff --git a/submodules/TelegramUI/Sources/NotificationContainerController.swift b/submodules/TelegramUI/Sources/NotificationContainerController.swift index 607f7fc2a83..d8cce2c1888 100644 --- a/submodules/TelegramUI/Sources/NotificationContainerController.swift +++ b/submodules/TelegramUI/Sources/NotificationContainerController.swift @@ -6,6 +6,7 @@ import TelegramCore import SwiftSignalKit import TelegramPresentationData import AccountContext +import ChatMessageNotificationItem public final class NotificationContainerController: ViewController { private var controllerNode: NotificationContainerControllerNode { @@ -97,6 +98,10 @@ public final class NotificationContainerController: ViewController { self.controllerNode.enqueue(item) } + public func setBlocking(_ item: NotificationItem?) { + self.controllerNode.setBlocking(item) + } + public func removeItems(_ f: (NotificationItem) -> Bool) { self.controllerNode.removeItems(f) } diff --git a/submodules/TelegramUI/Sources/NotificationContainerControllerNode.swift b/submodules/TelegramUI/Sources/NotificationContainerControllerNode.swift index d2eebdf6351..5870ae84269 100644 --- a/submodules/TelegramUI/Sources/NotificationContainerControllerNode.swift +++ b/submodules/TelegramUI/Sources/NotificationContainerControllerNode.swift @@ -4,6 +4,7 @@ import Display import AsyncDisplayKit import SwiftSignalKit import TelegramPresentationData +import ChatMessageNotificationItem private final class NotificationContainerControllerNodeView: UITracingLayerView { var hitTestImpl: ((CGPoint, UIEvent?) -> UIView?)? @@ -16,6 +17,7 @@ private final class NotificationContainerControllerNodeView: UITracingLayerView final class NotificationContainerControllerNode: ASDisplayNode { private var validLayout: ContainerViewLayout? private var topItemAndNode: (NotificationItem, NotificationItemContainerNode)? + private var blockingItemAndNode: (NotificationItem, NotificationItemContainerNode)? var displayingItemsUpdated: ((Bool) -> Void)? @@ -49,6 +51,9 @@ final class NotificationContainerControllerNode: ASDisplayNode { } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let (_, blockingItemNode) = self.blockingItemAndNode { + return blockingItemNode.hitTest(point, with: event) + } if let (_, topItemNode) = self.topItemAndNode { return topItemNode.hitTest(point, with: event) } @@ -77,6 +82,10 @@ final class NotificationContainerControllerNode: ASDisplayNode { } func enqueue(_ item: NotificationItem) { + if self.blockingItemAndNode != nil { + return + } + if let (_, topItemNode) = self.topItemAndNode { topItemNode.animateOut(completion: { [weak topItemNode] in topItemNode?.removeFromSupernode() @@ -89,9 +98,8 @@ final class NotificationContainerControllerNode: ASDisplayNode { } let itemNode = item.node(compact: useCompactLayout) - let containerNode = NotificationItemContainerNode(theme: self.presentationData.theme) + let containerNode = NotificationItemContainerNode(theme: self.presentationData.theme, contentNode: itemNode) containerNode.item = item - containerNode.contentNode = itemNode containerNode.dismissed = { [weak self] item in if let strongSelf = self { if let (topItem, topItemNode) = strongSelf.topItemAndNode, topItem.groupingKey != nil && topItem.groupingKey == item.groupingKey { @@ -120,7 +128,12 @@ final class NotificationContainerControllerNode: ASDisplayNode { } } self.topItemAndNode = (item, containerNode) - self.addSubnode(containerNode) + + if let blockingItemAndNode = self.blockingItemAndNode { + self.insertSubnode(containerNode, belowSubnode: blockingItemAndNode.1) + } else { + self.addSubnode(containerNode) + } if let validLayout = self.validLayout { containerNode.updateLayout(layout: validLayout, transition: .immediate) @@ -133,6 +146,70 @@ final class NotificationContainerControllerNode: ASDisplayNode { self.resetTimeoutTimer() } + func setBlocking(_ item: NotificationItem?) { + if let (_, blockingItemNode) = self.blockingItemAndNode { + blockingItemNode.animateOut(completion: { [weak blockingItemNode] in + blockingItemNode?.removeFromSupernode() + }) + self.blockingItemAndNode = nil + } + + if let item = item { + if let (_, topItemNode) = self.topItemAndNode { + topItemNode.animateOut(completion: { [weak topItemNode] in + topItemNode?.removeFromSupernode() + }) + } + self.topItemAndNode = nil + + var useCompactLayout = false + if let validLayout = self.validLayout { + useCompactLayout = min(validLayout.size.width, validLayout.size.height) < 375.0 + } + + let itemNode = item.node(compact: useCompactLayout) + let containerNode = NotificationItemContainerNode(theme: self.presentationData.theme, contentNode: itemNode) + containerNode.item = item + containerNode.dismissed = { [weak self] item in + if let strongSelf = self { + if let (topItem, topItemNode) = strongSelf.topItemAndNode, topItem.groupingKey != nil && topItem.groupingKey == item.groupingKey { + topItemNode.removeFromSupernode() + strongSelf.topItemAndNode = nil + + if let strongSelf = self, strongSelf.topItemAndNode == nil { + strongSelf.displayingItemsUpdated?(false) + } + } + } + } + containerNode.cancelTimeout = { [weak self] item in + if let strongSelf = self { + if let (topItem, _) = strongSelf.topItemAndNode, topItem.groupingKey != nil && topItem.groupingKey == item.groupingKey { + strongSelf.timeoutTimer?.invalidate() + strongSelf.timeoutTimer = nil + } + } + } + containerNode.resumeTimeout = { [weak self] item in + if let strongSelf = self { + if let (topItem, _) = strongSelf.topItemAndNode, topItem.groupingKey != nil && topItem.groupingKey == item.groupingKey { + strongSelf.resetTimeoutTimer() + } + } + } + self.blockingItemAndNode = (item, containerNode) + self.addSubnode(containerNode) + + if let validLayout = self.validLayout { + containerNode.updateLayout(layout: validLayout, transition: .immediate) + containerNode.frame = CGRect(origin: CGPoint(), size: validLayout.size) + containerNode.animateIn() + } + + self.displayingItemsUpdated?(true) + } + } + func removeItems(_ f: (NotificationItem) -> Bool) { if let (topItem, topItemNode) = self.topItemAndNode { if f(topItem) { diff --git a/submodules/TelegramUI/Sources/NotificationContentContext.swift b/submodules/TelegramUI/Sources/NotificationContentContext.swift index 50e34eb2afa..3d7b08a1d80 100644 --- a/submodules/TelegramUI/Sources/NotificationContentContext.swift +++ b/submodules/TelegramUI/Sources/NotificationContentContext.swift @@ -142,7 +142,7 @@ public final class NotificationViewControllerImpl { return nil }) // MARK: Nicegram DB Changes, openDoubleBottomFlow added - sharedAccountContext = SharedAccountContextImpl(mainWindow: nil, sharedContainerPath: self.initializationData.appGroupPath, basePath: rootPath, encryptionParameters: ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: self.initializationData.encryptionParameters.0)!, salt: ValueBoxEncryptionParameters.Salt(data: self.initializationData.encryptionParameters.1)!), accountManager: accountManager, appLockContext: appLockContext, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings!, networkArguments: NetworkInitializationArguments(apiId: self.initializationData.apiId, apiHash: self.initializationData.apiHash, languagesCategory: self.initializationData.languagesCategory, appVersion: self.initializationData.appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(self.initializationData.bundleData), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider(), deviceModelName: nil, useBetaFeatures: self.initializationData.useBetaFeatures, isICloudEnabled: false), hasInAppPurchases: false, rootPath: rootPath, legacyBasePath: nil, apsNotificationToken: .never(), voipNotificationToken: .never(), firebaseSecretStream: .never(), setNotificationCall: { _ in }, navigateToChat: { _, _, _ in }, openDoubleBottomFlow: { _ in }, appDelegate: nil) + sharedAccountContext = SharedAccountContextImpl(mainWindow: nil, sharedContainerPath: self.initializationData.appGroupPath, basePath: rootPath, encryptionParameters: ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: self.initializationData.encryptionParameters.0)!, salt: ValueBoxEncryptionParameters.Salt(data: self.initializationData.encryptionParameters.1)!), accountManager: accountManager, appLockContext: appLockContext, notificationController: nil, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings!, networkArguments: NetworkInitializationArguments(apiId: self.initializationData.apiId, apiHash: self.initializationData.apiHash, languagesCategory: self.initializationData.languagesCategory, appVersion: self.initializationData.appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(self.initializationData.bundleData), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider(), deviceModelName: nil, useBetaFeatures: self.initializationData.useBetaFeatures, isICloudEnabled: false), hasInAppPurchases: false, rootPath: rootPath, legacyBasePath: nil, apsNotificationToken: .never(), voipNotificationToken: .never(), firebaseSecretStream: .never(), setNotificationCall: { _ in }, navigateToChat: { _, _, _ in }, openDoubleBottomFlow: { _ in }, appDelegate: nil) presentationDataPromise.set(sharedAccountContext!.presentationData) } diff --git a/submodules/TelegramUI/Sources/NotificationItemContainerNode.swift b/submodules/TelegramUI/Sources/NotificationItemContainerNode.swift index 22e32ffe500..aff92e020c4 100644 --- a/submodules/TelegramUI/Sources/NotificationItemContainerNode.swift +++ b/submodules/TelegramUI/Sources/NotificationItemContainerNode.swift @@ -3,6 +3,7 @@ import UIKit import AsyncDisplayKit import Display import TelegramPresentationData +import ChatMessageNotificationItem final class NotificationItemContainerNode: ASDisplayNode { private let backgroundNode: ASImageNode @@ -45,7 +46,9 @@ final class NotificationItemContainerNode: ASDisplayNode { var cancelledTimeout = false - init(theme: PresentationTheme) { + init(theme: PresentationTheme, contentNode: NotificationItemNode?) { + self.contentNode = contentNode + self.backgroundNode = ASImageNode() self.backgroundNode.displayWithoutProcessing = true self.backgroundNode.displaysAsynchronously = false @@ -54,16 +57,21 @@ final class NotificationItemContainerNode: ASDisplayNode { super.init() self.addSubnode(self.backgroundNode) + if let contentNode { + self.addSubnode(contentNode) + } } override func didLoad() { super.didLoad() - self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) - let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) - panRecognizer.delaysTouchesBegan = false - panRecognizer.cancelsTouchesInView = false - self.view.addGestureRecognizer(panRecognizer) + if let contentNode = self.contentNode, !contentNode.acceptsTouches { + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + panRecognizer.delaysTouchesBegan = false + panRecognizer.cancelsTouchesInView = false + self.view.addGestureRecognizer(panRecognizer) + } } func animateIn() { @@ -113,6 +121,11 @@ final class NotificationItemContainerNode: ASDisplayNode { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let contentNode = self.contentNode, contentNode.frame.contains(point) { + if contentNode.acceptsTouches { + if let result = contentNode.view.hitTest(self.view.convert(point, to: contentNode.view), with: event) { + return result + } + } return self.view } return nil diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index d7ba72c9457..364f5b61039 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -206,10 +206,10 @@ func openResolvedUrlImpl( case let .channelMessage(peer, messageId, timecode): openPeer(EnginePeer(peer), .chat(textInputState: nil, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: timecode), peekData: nil)) case let .replyThreadMessage(replyThreadMessage, messageId): - if let navigationController = navigationController { + if let navigationController = navigationController, let effectiveMessageId = replyThreadMessage.effectiveMessageId { let _ = ChatControllerImpl.openMessageReplies(context: context, navigationController: navigationController, present: { c, a in present(c, a) - }, messageId: replyThreadMessage.messageId, isChannelPost: replyThreadMessage.isChannelPost, atMessage: messageId, displayModalProgress: true).startStandalone() + }, messageId: effectiveMessageId, isChannelPost: replyThreadMessage.isChannelPost, atMessage: messageId, displayModalProgress: true).startStandalone() } case let .replyThread(messageId): if let navigationController = navigationController { @@ -581,7 +581,13 @@ func openResolvedUrlImpl( } case let .premiumOffer(reference): dismissInput() - let controller = PremiumIntroScreen(context: context, source: .deeplink(reference)) + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .deeplink(reference), forceDark: false, dismissed: nil) + if let navigationController = navigationController { + navigationController.pushViewController(controller, animated: true) + } + case let .premiumMultiGift(reference): + dismissInput() + let controller = context.sharedContext.makePremiumGiftController(context: context, source: .deeplink(reference)) if let navigationController = navigationController { navigationController.pushViewController(controller, animated: true) } diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index 60aa7e3d298..ce084b15e9f 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -880,6 +880,20 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur } } handleResolvedUrl(.premiumOffer(reference: reference)) + } else if parsedUrl.host == "premium_multigift" { + var reference: String? + if let components = URLComponents(string: "/?" + query) { + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "ref" { + reference = value + } + } + } + } + } + handleResolvedUrl(.premiumMultiGift(reference: reference)) } else if parsedUrl.host == "addlist" { if let components = URLComponents(string: "/?" + query) { var slug: String? diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift index 5682f9d0236..0ef19eeee87 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift @@ -104,7 +104,21 @@ final class OverlayAudioPlayerControllerImpl: ViewController, OverlayAudioPlayer } } - strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in + if savedMessages, let self, action == .info { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.navigationController as? NavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) + }) + } + return false + }), in: .current) } }) } diff --git a/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift b/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift index dcb25facb60..412a55eab2d 100644 --- a/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift +++ b/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift @@ -461,7 +461,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { var canShare = true if let (_, valueOrLoading, _) = value, case let .state(value) = valueOrLoading, let source = value.item.playbackData?.source { switch source { - case let .telegramFile(fileReference, isCopyProtected): + case let .telegramFile(fileReference, isCopyProtected, _): canShare = !isCopyProtected strongSelf.currentFileReference = fileReference if let size = fileReference.media.size { diff --git a/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift b/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift index 0dc57685256..2dc30772ef3 100644 --- a/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift +++ b/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift @@ -63,7 +63,7 @@ final class MessageMediaPlaylistItem: SharedMediaPlaylistItem { lazy var playbackData: SharedMediaPlaybackData? = { if let file = extractFileMedia(self.message) { let fileReference = FileMediaReference.message(message: MessageReference(self.message), media: file) - let source = SharedMediaPlaybackDataSource.telegramFile(reference: fileReference, isCopyProtected: self.message.isCopyProtected()) + let source = SharedMediaPlaybackDataSource.telegramFile(reference: fileReference, isCopyProtected: self.message.isCopyProtected(), isViewOnce: self.message.minAutoremoveOrClearTimeout == viewOnceTimeout) for attribute in file.attributes { switch attribute { case let .Audio(isVoice, _, _, _, _): @@ -173,19 +173,27 @@ private func aroundMessagesFromMessages(_ messages: [Message], centralIndex: Mes } private func aroundMessagesFromView(view: MessageHistoryView, centralIndex: MessageIndex) -> [Message] { - guard let index = view.entries.firstIndex(where: { $0.index.id == centralIndex.id }) else { + let filteredEntries = view.entries.filter { entry in + if entry.message.minAutoremoveOrClearTimeout == viewOnceTimeout { + return false + } else { + return true + } + } + + guard let index = filteredEntries.firstIndex(where: { $0.index.id == centralIndex.id }) else { return [] } var result: [Message] = [] if index != 0 { for i in (0 ..< index).reversed() { - result.append(view.entries[i].message) + result.append(filteredEntries[i].message) break } } - if index != view.entries.count - 1 { - for i in index + 1 ..< view.entries.count { - result.append(view.entries[i].message) + if index != filteredEntries.count - 1 { + for i in index + 1 ..< filteredEntries.count { + result.append(filteredEntries[i].message) break } } @@ -234,7 +242,15 @@ private func navigatedMessageFromMessages(_ messages: [Message], anchorIndex: Me private func navigatedMessageFromView(_ view: MessageHistoryView, anchorIndex: MessageIndex, position: NavigatedMessageFromViewPosition, reversed: Bool) -> (message: Message, around: [Message], exact: Bool)? { var index = 0 - for entry in view.entries { + let filteredEntries = view.entries.filter { entry in + if entry.message.minAutoremoveOrClearTimeout == viewOnceTimeout { + return false + } else { + return true + } + } + + for entry in filteredEntries { if entry.index.id == anchorIndex.id { let currentGroupKey = entry.message.groupingKey @@ -243,63 +259,63 @@ private func navigatedMessageFromView(_ view: MessageHistoryView, anchorIndex: M return (entry.message, aroundMessagesFromView(view: view, centralIndex: entry.index), true) case .later: if !reversed, let currentGroupKey { - if index - 1 > 0, view.entries[index - 1].message.groupingKey == currentGroupKey { - let message = view.entries[index - 1].message - return (message, aroundMessagesFromView(view: view, centralIndex: view.entries[index - 1].index), true) + if index - 1 > 0, filteredEntries[index - 1].message.groupingKey == currentGroupKey { + let message = filteredEntries[index - 1].message + return (message, aroundMessagesFromView(view: view, centralIndex: filteredEntries[index - 1].index), true) } else { - for i in index ..< view.entries.count { - if view.entries[i].message.groupingKey != currentGroupKey { - let message = view.entries[i].message - return (message, aroundMessagesFromView(view: view, centralIndex: view.entries[i].index), true) + for i in index ..< filteredEntries.count { + if filteredEntries[i].message.groupingKey != currentGroupKey { + let message = filteredEntries[i].message + return (message, aroundMessagesFromView(view: view, centralIndex: filteredEntries[i].index), true) } } } - } else if index + 1 < view.entries.count { - let message = view.entries[index + 1].message - return (message, aroundMessagesFromView(view: view, centralIndex: view.entries[index + 1].index), true) + } else if index + 1 < filteredEntries.count { + let message = filteredEntries[index + 1].message + return (message, aroundMessagesFromView(view: view, centralIndex: filteredEntries[index + 1].index), true) } else { return nil } case .earlier: if !reversed, let currentGroupKey { - if index + 1 < view.entries.count, view.entries[index + 1].message.groupingKey == currentGroupKey { - let message = view.entries[index + 1].message - return (message, aroundMessagesFromView(view: view, centralIndex: view.entries[index + 1].index), true) + if index + 1 < filteredEntries.count, filteredEntries[index + 1].message.groupingKey == currentGroupKey { + let message = filteredEntries[index + 1].message + return (message, aroundMessagesFromView(view: view, centralIndex: filteredEntries[index + 1].index), true) } else { var nextGroupingKey: Int64? for i in (0 ..< index).reversed() { if let nextGroupingKey { - if view.entries[i].message.groupingKey != nextGroupingKey { - let message = view.entries[i + 1].message - return (message, aroundMessagesFromView(view: view, centralIndex: view.entries[i + 1].index), true) + if filteredEntries[i].message.groupingKey != nextGroupingKey { + let message = filteredEntries[i + 1].message + return (message, aroundMessagesFromView(view: view, centralIndex: filteredEntries[i + 1].index), true) } else if i == 0 { - let message = view.entries[i].message - return (message, aroundMessagesFromView(view: view, centralIndex: view.entries[i].index), true) + let message = filteredEntries[i].message + return (message, aroundMessagesFromView(view: view, centralIndex: filteredEntries[i].index), true) } - } else if view.entries[i].message.groupingKey != currentGroupKey { - if let groupingKey = view.entries[i].message.groupingKey { + } else if filteredEntries[i].message.groupingKey != currentGroupKey { + if let groupingKey = filteredEntries[i].message.groupingKey { nextGroupingKey = groupingKey } else { - let message = view.entries[i].message - return (message, aroundMessagesFromView(view: view, centralIndex: view.entries[i].index), true) + let message = filteredEntries[i].message + return (message, aroundMessagesFromView(view: view, centralIndex: filteredEntries[i].index), true) } } } } } else if index != 0 { - let message = view.entries[index - 1].message + let message = filteredEntries[index - 1].message if !reversed, let nextGroupingKey = message.groupingKey { for i in (0 ..< index).reversed() { - if view.entries[i].message.groupingKey != nextGroupingKey { - let message = view.entries[i + 1].message - return (message, aroundMessagesFromView(view: view, centralIndex: view.entries[i + 1].index), true) + if filteredEntries[i].message.groupingKey != nextGroupingKey { + let message = filteredEntries[i + 1].message + return (message, aroundMessagesFromView(view: view, centralIndex: filteredEntries[i + 1].index), true) } else if i == 0 { - let message = view.entries[i].message - return (message, aroundMessagesFromView(view: view, centralIndex: view.entries[i].index), true) + let message = filteredEntries[i].message + return (message, aroundMessagesFromView(view: view, centralIndex: filteredEntries[i].index), true) } } } - return (message, aroundMessagesFromView(view: view, centralIndex: view.entries[index - 1].index), true) + return (message, aroundMessagesFromView(view: view, centralIndex: filteredEntries[index - 1].index), true) } else { return nil } @@ -307,14 +323,14 @@ private func navigatedMessageFromView(_ view: MessageHistoryView, anchorIndex: M } index += 1 } - if !view.entries.isEmpty { + if !filteredEntries.isEmpty { switch position { case .later, .exact: - let message = view.entries[view.entries.count - 1].message - return (message, aroundMessagesFromView(view: view, centralIndex: view.entries[view.entries.count - 1].index), false) + let message = filteredEntries[filteredEntries.count - 1].message + return (message, aroundMessagesFromView(view: view, centralIndex: filteredEntries[filteredEntries.count - 1].index), false) case .earlier: - let message = view.entries[0].message - return (message, aroundMessagesFromView(view: view, centralIndex: view.entries[0].index), false) + let message = filteredEntries[0].message + return (message, aroundMessagesFromView(view: view, centralIndex: filteredEntries[0].index), false) } } else { return nil diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index fbb7cf19cb6..ee3b60a111b 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -51,6 +51,7 @@ import ChatRecentActionsController import PeerInfoScreen import ChatQrCodeScreen import UndoUI +import ChatMessageNotificationItem import NGCore import NGData @@ -91,6 +92,15 @@ public final class SharedAccountContextImpl: SharedAccountContext { public let basePath: String public let accountManager: AccountManager public let appLockContext: AppLockContext + public var notificationController: NotificationContainerController? { + didSet { + if self.notificationController !== oldValue { + if let oldValue { + oldValue.setBlocking(nil) + } + } + } + } private let navigateToChatImpl: (AccountRecordId, PeerId, MessageId?) -> Void @@ -141,6 +151,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { public let hasOngoingCall = ValuePromise(false) private let callState = Promise(nil) private var awaitingCallConnectionDisposable: Disposable? + private var callPeerDisposable: Disposable? private var groupCallController: VoiceChatController? public var currentGroupCallController: ViewController? { @@ -232,7 +243,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { private let energyUsageAutomaticDisposable = MetaDisposable() // MARK: Nicegram DB Changes, openDoubleBottomFlow added - init(mainWindow: Window1?, sharedContainerPath: String, basePath: String, encryptionParameters: ValueBoxEncryptionParameters, accountManager: AccountManager, appLockContext: AppLockContext, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, hasInAppPurchases: Bool, rootPath: String, legacyBasePath: String?, apsNotificationToken: Signal, voipNotificationToken: Signal, firebaseSecretStream: Signal<[String: String], NoError>, setNotificationCall: @escaping (PresentationCall?) -> Void, navigateToChat: @escaping (AccountRecordId, PeerId, MessageId?) -> Void, displayUpgradeProgress: @escaping (Float?) -> Void = { _ in }, openDoubleBottomFlow: @escaping (AccountContext) -> Void = { _ in }, appDelegate: AppDelegate?) { + init(mainWindow: Window1?, sharedContainerPath: String, basePath: String, encryptionParameters: ValueBoxEncryptionParameters, accountManager: AccountManager, appLockContext: AppLockContext, notificationController: NotificationContainerController?, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, hasInAppPurchases: Bool, rootPath: String, legacyBasePath: String?, apsNotificationToken: Signal, voipNotificationToken: Signal, firebaseSecretStream: Signal<[String: String], NoError>, setNotificationCall: @escaping (PresentationCall?) -> Void, navigateToChat: @escaping (AccountRecordId, PeerId, MessageId?) -> Void, displayUpgradeProgress: @escaping (Float?) -> Void = { _ in }, openDoubleBottomFlow: @escaping (AccountContext) -> Void = { _ in }, appDelegate: AppDelegate?) { assert(Queue.mainQueue().isCurrent()) precondition(!testHasInstance) @@ -247,6 +258,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { self.navigateToChatImpl = navigateToChat self.displayUpgradeProgress = displayUpgradeProgress self.appLockContext = appLockContext + self.notificationController = notificationController self.hasInAppPurchases = hasInAppPurchases self.accountManager.mediaBox.fetchCachedResourceRepresentation = { (resource, representation) -> Signal in @@ -797,18 +809,52 @@ public final class SharedAccountContextImpl: SharedAccountContext { self.callController = nil self.hasOngoingCall.set(false) + self.notificationController?.setBlocking(nil) + + self.callPeerDisposable?.dispose() + self.callPeerDisposable = nil + if let call { self.callState.set(call.state |> map(Optional.init)) self.hasOngoingCall.set(true) setNotificationCall(call) - if !call.isOutgoing && call.isIntegratedWithCallKit { + if call.isOutgoing { + self.presentControllerWithCurrentCall() + } else { + if !call.isIntegratedWithCallKit { + self.callPeerDisposable?.dispose() + self.callPeerDisposable = (call.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: call.peerId)) + |> deliverOnMainQueue).startStrict(next: { [weak self, weak call] peer in + guard let self, let call, let peer else { + return + } + if self.call !== call { + return + } + + let presentationData = self.currentPresentationData.with { $0 } + self.notificationController?.setBlocking(ChatCallNotificationItem(context: call.context, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, peer: peer, isVideo: call.isVideo, action: { [weak call] answerAction in + guard let call else { + return + } + if answerAction { + call.answer() + } else { + call.rejectBusy() + } + })) + }) + } + self.awaitingCallConnectionDisposable = (call.state |> filter { state in switch state.state { case .ringing: return false + case .terminating, .terminated: + return false default: return true } @@ -818,10 +864,12 @@ public final class SharedAccountContextImpl: SharedAccountContext { guard let self else { return } + self.notificationController?.setBlocking(nil) self.presentControllerWithCurrentCall() + + self.callPeerDisposable?.dispose() + self.callPeerDisposable = nil }) - } else{ - self.presentControllerWithCurrentCall() } } else { self.callState.set(.single(nil)) @@ -891,6 +939,29 @@ public final class SharedAccountContextImpl: SharedAccountContext { let callSignal: Signal = .single(nil) |> then( callManager.currentCallSignal + |> deliverOnMainQueue + |> mapToSignal { call -> Signal in + guard let call else { + return .single(nil) + } + return call.state + |> map { [weak call] state -> PresentationCall? in + guard let call else { + return nil + } + switch state.state { + case .ringing: + return nil + case .terminating, .terminated: + return nil + default: + return call + } + } + } + |> distinctUntilChanged(isEqual: { lhs, rhs in + return lhs === rhs + }) ) let groupCallSignal: Signal = .single(nil) |> then( @@ -1072,6 +1143,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { self.groupCallDisposable?.dispose() self.callStateDisposable?.dispose() self.awaitingCallConnectionDisposable?.dispose() + self.callPeerDisposable?.dispose() // MARK: Nicegram DB Changes self.activeAccountsSettingsDisposable?.dispose() self.applicationInForegroundDisposable?.dispose() @@ -1736,7 +1808,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { return presentAddMembersImpl(context: context, updatedPresentationData: updatedPresentationData, parentController: parentController, groupPeer: groupPeer, selectAddMemberDisposable: selectAddMemberDisposable, addMemberDisposable: addMemberDisposable) } - public func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: (() -> Void)? = nil, backgroundNode: ASDisplayNode?, availableReactions: AvailableReactions?, accountPeer: Peer?, isCentered: Bool) -> ListViewItem { + public func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: (() -> Void)? = nil, backgroundNode: ASDisplayNode?, availableReactions: AvailableReactions?, accountPeer: Peer?, isCentered: Bool, isPreview: Bool) -> ListViewItem { let controllerInteraction: ChatControllerInteraction controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in @@ -1831,7 +1903,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { chatLocation = .peer(id: messages.first!.id.peerId) } - return ChatMessageItemImpl(presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: theme, wallpaper: wallpaper), fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameOrder, disableAnimations: false, largeEmoji: false, chatBubbleCorners: chatBubbleCorners, animatedEmojiScale: 1.0, isPreview: true), context: context, chatLocation: chatLocation, associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .contact, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: false, subject: nil, contactsPeerIds: Set(), animatedEmojiStickers: [:], forcedResourceStatus: forcedResourceStatus, availableReactions: availableReactions, defaultReaction: nil, isPremium: false, accountPeer: accountPeer.flatMap(EnginePeer.init), forceInlineReactions: true), controllerInteraction: controllerInteraction, content: content, disableDate: true, additionalContent: nil) + return ChatMessageItemImpl(presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: theme, wallpaper: wallpaper), fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameOrder, disableAnimations: false, largeEmoji: false, chatBubbleCorners: chatBubbleCorners, animatedEmojiScale: 1.0, isPreview: isPreview), context: context, chatLocation: chatLocation, associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .contact, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: false, subject: nil, contactsPeerIds: Set(), animatedEmojiStickers: [:], forcedResourceStatus: forcedResourceStatus, availableReactions: availableReactions, defaultReaction: nil, isPremium: false, accountPeer: accountPeer.flatMap(EnginePeer.init), forceInlineReactions: true), controllerInteraction: controllerInteraction, content: content, disableDate: true, additionalContent: nil) } public func makeChatMessageDateHeaderItem(context: AccountContext, timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader { @@ -1938,10 +2010,12 @@ public final class SharedAccountContextImpl: SharedAccountContext { } public func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController { + var modal = true let mappedSource: PremiumSource switch source { case .settings: mappedSource = .settings + modal = false case .stickers: mappedSource = .stickers case .reactions: @@ -2005,7 +2079,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { case .wallpapers: mappedSource = .wallpapers } - let controller = PremiumIntroScreen(context: context, source: mappedSource, forceDark: forceDark) + let controller = PremiumIntroScreen(context: context, modal: modal, source: mappedSource, forceDark: forceDark) controller.wasDismissed = dismissed return controller } @@ -2082,16 +2156,16 @@ public final class SharedAccountContextImpl: SharedAccountContext { return PremiumLimitScreen(context: context, subject: mappedSubject, count: count, forceDark: forceDark, cancel: cancel, action: action) } - public func makePremiumGiftController(context: AccountContext) -> ViewController { + public func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource) -> ViewController { let options = Promise<[PremiumGiftCodeOption]>() options.set(context.engine.payments.premiumGiftCodeOptions(peerId: nil)) - + let presentationData = context.sharedContext.currentPresentationData.with { $0 } let limit: Int32 = 10 var reachedLimitImpl: ((Int32) -> Void)? let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .premiumGifting, options: [], isPeerEnabled: { peer in - if case let .user(user) = peer, user.botInfo == nil { + if case let .user(user) = peer, user.botInfo == nil && !peer.isService && !user.flags.contains(.isSupport) { return true } else { return false @@ -2123,11 +2197,14 @@ public final class SharedAccountContextImpl: SharedAccountContext { } }) } + guard !peerIds.isEmpty else { + return + } let mappedOptions = options.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) } var pushImpl: ((ViewController) -> Void)? var filterImpl: (() -> Void)? - let giftController = PremiumGiftScreen(context: context, peerIds: peerIds, options: mappedOptions, source: .settings, pushController: { c in + let giftController = PremiumGiftScreen(context: context, peerIds: peerIds, options: mappedOptions, source: source, pushController: { c in pushImpl?(c) }, completion: { filterImpl?() diff --git a/submodules/TelegramUI/Sources/SharedMediaPlayer.swift b/submodules/TelegramUI/Sources/SharedMediaPlayer.swift index 77ee1a4af18..a5961e28768 100644 --- a/submodules/TelegramUI/Sources/SharedMediaPlayer.swift +++ b/submodules/TelegramUI/Sources/SharedMediaPlayer.swift @@ -229,13 +229,13 @@ final class SharedMediaPlayer { switch playbackData.type { case .voice, .music: switch playbackData.source { - case let .telegramFile(fileReference, _): + case let .telegramFile(fileReference, _, _): strongSelf.playbackItem = .audio(MediaPlayer(audioSessionManager: strongSelf.audioSession, postbox: strongSelf.account.postbox, userLocation: .other, userContentType: .audio, resourceReference: fileReference.resourceReference(fileReference.media.resource), streamable: playbackData.type == .music ? .conservative : .none, video: false, preferSoftwareDecoding: false, enableSound: true, baseRate: rateValue, fetchAutomatically: true, playAndRecord: controlPlaybackWithProximity, isAudioVideoMessage: playbackData.type == .voice)) } case .instantVideo: if let mediaManager = strongSelf.mediaManager, let item = item as? MessageMediaPlaylistItem { switch playbackData.source { - case let .telegramFile(fileReference, _): + case let .telegramFile(fileReference, _, _): let videoNode = OverlayInstantVideoNode(postbox: strongSelf.account.postbox, audioSession: strongSelf.audioSession, manager: mediaManager.universalVideoManager, content: NativeVideoContent(id: .message(item.message.stableId, fileReference.media.fileId), userLocation: .peer(item.message.id.peerId), fileReference: fileReference, enableSound: false, baseRate: rateValue, isAudioVideoMessage: true, captureProtected: item.message.isCopyProtected(), storeAfterDownload: nil), close: { [weak mediaManager] in mediaManager?.setPlaylist(nil, type: .voice, control: .playback(.pause)) }) @@ -493,7 +493,7 @@ final class SharedMediaPlayer { let fetchedCurrentSignal: Signal let fetchedNextSignal: Signal switch current { - case let .telegramFile(file, _): + case let .telegramFile(file, _, _): fetchedCurrentSignal = self.account.postbox.mediaBox.resourceData(file.media.resource) |> mapToSignal { data -> Signal in if data.complete { @@ -506,7 +506,7 @@ final class SharedMediaPlayer { |> ignoreValues } switch next { - case let .telegramFile(file, _): + case let .telegramFile(file, _, _): fetchedNextSignal = fetchedMediaResource(mediaBox: self.account.postbox.mediaBox, userLocation: .other, userContentType: .audio, reference: file.resourceReference(file.media.resource)) |> ignoreValues |> `catch` { _ -> Signal in diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 5a5a2228a90..b378dabadce 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -52,7 +52,7 @@ private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceh init(context: AccountContext) { self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: context.account.peerId, mode: .standard(previewing: false), chatLocation: .peer(id: context.account.peerId), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil) + self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: context.account.peerId, mode: .standard(.default), chatLocation: .peer(id: context.account.peerId), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil) self.wallpaperBackgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: true, useSharedAnimationPhase: true) self.emptyNode = ChatEmptyNode(context: context, interaction: nil) @@ -65,7 +65,7 @@ private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceh func updatePresentationData(_ presentationData: PresentationData) { self.presentationData = presentationData - self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.presentationInterfaceState.limitsConfiguration, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.presentationInterfaceState.accountPeerId, mode: .standard(previewing: false), chatLocation: self.presentationInterfaceState.chatLocation, subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil) + self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.presentationInterfaceState.limitsConfiguration, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.presentationInterfaceState.accountPeerId, mode: .standard(.default), chatLocation: self.presentationInterfaceState.chatLocation, subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil) self.wallpaperBackgroundNode.update(wallpaper: presentationData.chatWallpaper, animated: false) } @@ -176,20 +176,6 @@ public final class TelegramRootController: NavigationController, TelegramRootCon backgroundColor: .secondarySystemBackground, content: { AssistantPopover() } ) - }, - showGrumTooltip: { [weak self] in - self?.showNicegramTooltip( - backgroundColor: .grumBg, - content: { - GrumPopover( - openAssistant: { - AssistantUITgHelper.routeToAssistant( - source: .generic - ) - } - ) - } - ) } ) } diff --git a/submodules/TelegramUI/Sources/TextLinkHandling.swift b/submodules/TelegramUI/Sources/TextLinkHandling.swift index e925d46e541..4d862fbebdf 100644 --- a/submodules/TelegramUI/Sources/TextLinkHandling.swift +++ b/submodules/TelegramUI/Sources/TextLinkHandling.swift @@ -72,10 +72,10 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: EnginePeer.Id?, n context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(EnginePeer(peer)), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: timecode))) } case let .replyThreadMessage(replyThreadMessage, messageId): - if let navigationController = controller.navigationController as? NavigationController { + if let navigationController = controller.navigationController as? NavigationController, let effectiveMessageId = replyThreadMessage.effectiveMessageId { let _ = ChatControllerImpl.openMessageReplies(context: context, navigationController: navigationController, present: { [weak controller] c, a in controller?.present(c, in: .window(.root), with: a) - }, messageId: replyThreadMessage.messageId, isChannelPost: replyThreadMessage.isChannelPost, atMessage: messageId, displayModalProgress: true).start() + }, messageId: effectiveMessageId, isChannelPost: replyThreadMessage.isChannelPost, atMessage: messageId, displayModalProgress: true).start() } case let .replyThread(messageId): if let navigationController = controller.navigationController as? NavigationController { diff --git a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift index b935dc4a6eb..2cb7f8f0908 100644 --- a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift +++ b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift @@ -53,7 +53,7 @@ public struct ExperimentalUISettings: Codable, Equatable { public var storiesExperiment: Bool public var storiesJpegExperiment: Bool public var crashOnMemoryPressure: Bool - public var unidirectionalSwipeToReply: Bool + public var dustEffect: Bool public var callV2: Bool public var alternativeStoryMedia: Bool public var allowWebViewInspection: Bool @@ -89,7 +89,7 @@ public struct ExperimentalUISettings: Codable, Equatable { storiesExperiment: false, storiesJpegExperiment: false, crashOnMemoryPressure: false, - unidirectionalSwipeToReply: false, + dustEffect: false, callV2: false, alternativeStoryMedia: false, allowWebViewInspection: false @@ -125,7 +125,7 @@ public struct ExperimentalUISettings: Codable, Equatable { storiesExperiment: Bool, storiesJpegExperiment: Bool, crashOnMemoryPressure: Bool, - unidirectionalSwipeToReply: Bool, + dustEffect: Bool, callV2: Bool, alternativeStoryMedia: Bool, allowWebViewInspection: Bool @@ -158,7 +158,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.storiesExperiment = storiesExperiment self.storiesJpegExperiment = storiesJpegExperiment self.crashOnMemoryPressure = crashOnMemoryPressure - self.unidirectionalSwipeToReply = unidirectionalSwipeToReply + self.dustEffect = dustEffect self.callV2 = callV2 self.alternativeStoryMedia = alternativeStoryMedia self.allowWebViewInspection = allowWebViewInspection @@ -195,7 +195,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.storiesExperiment = try container.decodeIfPresent(Bool.self, forKey: "storiesExperiment") ?? false self.storiesJpegExperiment = try container.decodeIfPresent(Bool.self, forKey: "storiesJpegExperiment") ?? false self.crashOnMemoryPressure = try container.decodeIfPresent(Bool.self, forKey: "crashOnMemoryPressure") ?? false - self.unidirectionalSwipeToReply = try container.decodeIfPresent(Bool.self, forKey: "unidirectionalSwipeToReply") ?? false + self.dustEffect = try container.decodeIfPresent(Bool.self, forKey: "dustEffect") ?? false self.callV2 = try container.decodeIfPresent(Bool.self, forKey: "callV2") ?? false self.alternativeStoryMedia = try container.decodeIfPresent(Bool.self, forKey: "alternativeStoryMedia") ?? false self.allowWebViewInspection = try container.decodeIfPresent(Bool.self, forKey: "allowWebViewInspection") ?? false @@ -232,7 +232,7 @@ public struct ExperimentalUISettings: Codable, Equatable { try container.encode(self.storiesExperiment, forKey: "storiesExperiment") try container.encode(self.storiesJpegExperiment, forKey: "storiesJpegExperiment") try container.encode(self.crashOnMemoryPressure, forKey: "crashOnMemoryPressure") - try container.encode(self.unidirectionalSwipeToReply, forKey: "unidirectionalSwipeToReply") + try container.encode(self.dustEffect, forKey: "dustEffect") try container.encode(self.callV2, forKey: "callV2") try container.encode(self.alternativeStoryMedia, forKey: "alternativeStoryMedia") try container.encode(self.allowWebViewInspection, forKey: "allowWebViewInspection") diff --git a/submodules/TelegramVoip/Sources/GroupCallContext.swift b/submodules/TelegramVoip/Sources/GroupCallContext.swift index 1860d6caeee..f8fa7d5abfd 100644 --- a/submodules/TelegramVoip/Sources/GroupCallContext.swift +++ b/submodules/TelegramVoip/Sources/GroupCallContext.swift @@ -25,6 +25,17 @@ final class ContextQueueImpl: NSObject, OngoingCallThreadLocalContextQueueWebrtc func isCurrent() -> Bool { return self.queue.isCurrent() } + + func scheduleBlock(_ f: @escaping () -> Void, after timeout: Double) -> GroupCallDisposable { + let timer = SwiftSignalKit.Timer(timeout: timeout, repeat: false, completion: { + f() + }, queue: self.queue) + timer.start() + + return GroupCallDisposable(block: { + timer.invalidate() + }) + } } enum BroadcastPartSubject { diff --git a/submodules/TelegramVoip/Sources/OngoingCallContext.swift b/submodules/TelegramVoip/Sources/OngoingCallContext.swift index 1c1270e8c99..5c2aba5e1b5 100644 --- a/submodules/TelegramVoip/Sources/OngoingCallContext.swift +++ b/submodules/TelegramVoip/Sources/OngoingCallContext.swift @@ -213,7 +213,7 @@ public struct OngoingCallContextState: Equatable { public let remoteBatteryLevel: RemoteBatteryLevel } -private final class OngoingCallThreadLocalContextQueueImpl: NSObject, OngoingCallThreadLocalContextQueue, OngoingCallThreadLocalContextQueueWebrtc /*, OngoingCallThreadLocalContextQueueWebrtcCustom*/ { +private final class OngoingCallThreadLocalContextQueueImpl: NSObject, OngoingCallThreadLocalContextQueue, OngoingCallThreadLocalContextQueueWebrtc { private let queue: Queue init(queue: Queue) { @@ -235,6 +235,17 @@ private final class OngoingCallThreadLocalContextQueueImpl: NSObject, OngoingCal func isCurrent() -> Bool { return self.queue.isCurrent() } + + func scheduleBlock(_ f: @escaping () -> Void, after timeout: Double) -> GroupCallDisposable { + let timer = SwiftSignalKit.Timer(timeout: timeout, repeat: false, completion: { + f() + }, queue: self.queue) + timer.start() + + return GroupCallDisposable(block: { + timer.invalidate() + }) + } } private func ongoingNetworkTypeForType(_ type: NetworkType) -> OngoingCallNetworkType { diff --git a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h index ae8a5336752..e6e9833f1a6 100644 --- a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h +++ b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h @@ -98,11 +98,20 @@ typedef NS_ENUM(int32_t, OngoingCallDataSavingWebrtc) { OngoingCallDataSavingAlways }; +@interface GroupCallDisposable : NSObject + +- (instancetype _Nonnull)initWithBlock:(dispatch_block_t _Nonnull)block; +- (void)dispose; + +@end + @protocol OngoingCallThreadLocalContextQueueWebrtc - (void)dispatch:(void (^ _Nonnull)())f; - (bool)isCurrent; +- (GroupCallDisposable * _Nonnull)scheduleBlock:(void (^ _Nonnull)())f after:(double)timeout; + @end @interface VoipProxyServerWebrtc : NSObject @@ -133,13 +142,6 @@ typedef NS_ENUM(int32_t, OngoingCallDataSavingWebrtc) { #endif @end -@interface GroupCallDisposable : NSObject - -- (instancetype _Nonnull)initWithBlock:(dispatch_block_t _Nonnull)block; -- (void)dispose; - -@end - @protocol CallVideoFrameBuffer @end diff --git a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm index 06a79241885..f0e739f150b 100644 --- a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm +++ b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm @@ -925,7 +925,11 @@ @interface OngoingCallThreadLocalContextWebrtc () { std::unique_ptr _tgVoip; bool _didStop; + OngoingCallStateWebrtc _pendingState; OngoingCallStateWebrtc _state; + bool _didPushStateOnce; + GroupCallDisposable *_pushStateDisposable; + OngoingCallVideoStateWebrtc _videoState; bool _connectedOnce; OngoingCallRemoteBatteryLevelWebrtc _remoteBatteryLevel; @@ -1356,6 +1360,7 @@ - (instancetype _Nonnull)initWithVersion:(NSString * _Nonnull)version queue:(id< .directConnectionChannel = directConnectionChannel }); _state = OngoingCallStateInitializing; + _pendingState = OngoingCallStateInitializing; _signalBars = 4; } return self; @@ -1374,6 +1379,8 @@ - (void)dealloc { _currentAudioDeviceModuleThread = nullptr; } + [_pushStateDisposable dispose]; + if (_tgVoip != NULL) { [self stop:nil]; } @@ -1469,6 +1476,18 @@ - (NSData * _Nonnull)getDerivedState { } } +- (void)pushPendingState { + _didPushStateOnce = true; + + if (_state != _pendingState) { + _state = _pendingState; + + if (_stateChanged) { + _stateChanged(_state, _videoState, _remoteVideoState, _remoteAudioState, _remoteBatteryLevel, _remotePreferredAspectRatio); + } + } +} + - (void)controllerStateChanged:(tgcalls::State)state { OngoingCallStateWebrtc callState = OngoingCallStateInitializing; switch (state) { @@ -1485,11 +1504,32 @@ - (void)controllerStateChanged:(tgcalls::State)state { break; } - if (_state != callState) { - _state = callState; + if (_pendingState != callState) { + _pendingState = callState; - if (_stateChanged) { - _stateChanged(_state, _videoState, _remoteVideoState, _remoteAudioState, _remoteBatteryLevel, _remotePreferredAspectRatio); + [_pushStateDisposable dispose]; + _pushStateDisposable = nil; + + bool maybeDelayPush = false; + if (!_didPushStateOnce) { + maybeDelayPush = false; + } else if (callState == OngoingCallStateReconnecting) { + maybeDelayPush = true; + } else { + maybeDelayPush = false; + } + + if (!maybeDelayPush) { + [self pushPendingState]; + } else { + __weak OngoingCallThreadLocalContextWebrtc *weakSelf = self; + _pushStateDisposable = [_queue scheduleBlock:^{ + __strong OngoingCallThreadLocalContextWebrtc *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + [strongSelf pushPendingState]; + } after:1.0]; } } } diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index 6b73742cdc1..564c632f936 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit 6b73742cdc140c46a1ab1b8e3390354a9738e429 +Subproject commit 564c632f9368409870631d3cef75a7fc4070d45b diff --git a/submodules/TooltipUI/Sources/TooltipScreen.swift b/submodules/TooltipUI/Sources/TooltipScreen.swift index 40c98470381..f8d4f5234df 100644 --- a/submodules/TooltipUI/Sources/TooltipScreen.swift +++ b/submodules/TooltipUI/Sources/TooltipScreen.swift @@ -810,6 +810,9 @@ private final class TooltipScreenNode: ViewControllerTracingNode { if let _ = self.openActiveTextItem, let textComponentView = self.textView.view, let result = textComponentView.hitTest(self.view.convert(point, to: textComponentView), with: event) { return result } + if let closeButtonNode = self.closeButtonNode, let result = closeButtonNode.hitTest(self.view.convert(point, to: closeButtonNode.view), with: event) { + return result + } var eventIsPresses = false if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m index 6e3f53029d0..8d74325e8fc 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m @@ -161,6 +161,18 @@ + (UIWindow * _Nullable)remoteKeyboardWindowForScreen:(UIScreen * _Nullable)scre @end +@interface UIFocusSystem (Telegram) + +@end + +@implementation UIFocusSystem (Telegram) + +- (void)_65087dc8_updateFocusIfNeeded { + //TODO:Re-enable +} + +@end + @implementation UIViewController (Navigation) + (void)load @@ -183,6 +195,8 @@ + (void)load } else if (@available(iOS 15.0, *)) { [RuntimeUtils swizzleInstanceMethodOfClass:[CADisplayLink class] currentSelector:@selector(setPreferredFrameRateRange:) newSelector:@selector(_65087dc8_setPreferredFrameRateRange:)]; } + + [RuntimeUtils swizzleInstanceMethodOfClass:[UIFocusSystem class] currentSelector:@selector(updateFocusIfNeeded) newSelector:@selector(_65087dc8_updateFocusIfNeeded)]; }); } diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 865ca15d614..509cf77fe8d 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -639,10 +639,19 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.animatedStickerNode = nil let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) - let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let bold: MarkdownAttributeSet + if savedMessages { + bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: presentationData.theme.list.itemAccentColor.withMultiplied(hue: 0.933, saturation: 0.61, brightness: 1.0), additionalAttributes: ["URL": ""]) + } else { + bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + } let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .natural) self.textNode.attributedText = attributedText self.textNode.maximumNumberOfLines = 2 + + if savedMessages { + isUserInteractionEnabled = true + } displayUndo = false self.originalRemainingSeconds = 3 diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index b6179adffd6..361bed480da 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -719,7 +719,7 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) return .progress case let .result(info): if let _ = info { - return .result(.replyThreadMessage(replyThreadMessage: ChatReplyThreadMessage(messageId: MessageId(peerId: channel.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false), messageId: messageId)) + return .result(.replyThreadMessage(replyThreadMessage: ChatReplyThreadMessage(peerId: channel.id, threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false), messageId: messageId)) } else { return .result(.peer(peer._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil))) } @@ -744,7 +744,7 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) return .progress case let .result(info): if let _ = info { - return .result(.replyThreadMessage(replyThreadMessage: ChatReplyThreadMessage(messageId: MessageId(peerId: channel.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: replyThreadMessageId.id)), threadId: Int64(replyThreadMessageId.id), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false), messageId: MessageId(peerId: channel.id, namespace: Namespaces.Message.Cloud, id: replyId))) + return .result(.replyThreadMessage(replyThreadMessage: ChatReplyThreadMessage(peerId: channel.id, threadId: Int64(replyThreadMessageId.id), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false), messageId: MessageId(peerId: channel.id, namespace: Namespaces.Message.Cloud, id: replyId))) } else { return .result(.peer(peer._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil))) } @@ -760,7 +760,7 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) guard let result = result else { return .result(.channelMessage(peer: peer._asPeer(), messageId: replyThreadMessageId, timecode: nil)) } - return .result(.replyThreadMessage(replyThreadMessage: result, messageId: MessageId(peerId: result.messageId.peerId, namespace: Namespaces.Message.Cloud, id: replyId))) + return .result(.replyThreadMessage(replyThreadMessage: result, messageId: MessageId(peerId: result.peerId, namespace: Namespaces.Message.Cloud, id: replyId))) }) } case let .voiceChat(invite): @@ -825,7 +825,7 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) return .progress case let .result(info): if let _ = info { - return .result(.replyThreadMessage(replyThreadMessage: ChatReplyThreadMessage(messageId: MessageId(peerId: channel.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), threadId: Int64(threadId), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false), messageId: messageId)) + return .result(.replyThreadMessage(replyThreadMessage: ChatReplyThreadMessage(peerId: channel.id, threadId: Int64(threadId), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false), messageId: messageId)) } else { return .result(.peer(peer?._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil))) } @@ -849,7 +849,7 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) return .progress case let .result(info): if let _ = info { - return .result(.replyThreadMessage(replyThreadMessage: ChatReplyThreadMessage(messageId: MessageId(peerId: channel.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false), messageId: messageId)) + return .result(.replyThreadMessage(replyThreadMessage: ChatReplyThreadMessage(peerId: channel.id, threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false), messageId: messageId)) } else { return .result(.peer(peer?._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil))) } diff --git a/submodules/WebUI/Sources/WebAppWebView.swift b/submodules/WebUI/Sources/WebAppWebView.swift index ee016c6db03..169383d6f7d 100644 --- a/submodules/WebUI/Sources/WebAppWebView.swift +++ b/submodules/WebUI/Sources/WebAppWebView.swift @@ -94,18 +94,29 @@ final class WebAppWebView: WKWebView { init(account: Account) { let configuration = WKWebViewConfiguration() - - let uuid: UUID - - if let current = UserDefaults.standard.object(forKey: "TelegramWebStoreUUID_\(account.id.int64)") as? String { - uuid = UUID(uuidString: current)! - } else { - uuid = UUID() - UserDefaults.standard.set(uuid.uuidString, forKey: "TelegramWebStoreUUID_\(account.id.int64)") - } if #available(iOS 17.0, *) { - configuration.websiteDataStore = WKWebsiteDataStore(forIdentifier: uuid) + var uuid: UUID? + if let current = UserDefaults.standard.object(forKey: "TelegramWebStoreUUID_\(account.id.int64)") as? String { + uuid = UUID(uuidString: current)! + } else { + let mainAccountId: Int64 + if let current = UserDefaults.standard.object(forKey: "TelegramWebStoreMainAccountId") as? Int64 { + mainAccountId = current + } else { + mainAccountId = account.id.int64 + UserDefaults.standard.set(mainAccountId, forKey: "TelegramWebStoreMainAccountId") + } + + if account.id.int64 != mainAccountId { + uuid = UUID() + UserDefaults.standard.set(uuid!.uuidString, forKey: "TelegramWebStoreUUID_\(account.id.int64)") + } + } + + if let uuid { + configuration.websiteDataStore = WKWebsiteDataStore(forIdentifier: uuid) + } } let contentController = WKUserContentController() diff --git a/swift_deps.bzl b/swift_deps.bzl index 37e90e6d77a..e4eff91d2ef 100644 --- a/swift_deps.bzl +++ b/swift_deps.bzl @@ -36,7 +36,7 @@ def swift_dependencies(): # branch: develop swift_package( name = "swiftpkg_nicegram_assistant_ios", - commit = "b0d993e6fcb6e48734d8361c26bd401752ac5513", + commit = "2cf7fd3fa9abeaee81fd2b970681ab3ae83043f4", dependencies_index = "@//:swift_deps_index.json", remote = "git@bitbucket.org:mobyrix/nicegram-assistant-ios.git", ) @@ -52,7 +52,7 @@ def swift_dependencies(): # version: 5.15.5 swift_package( name = "swiftpkg_sdwebimage", - commit = "e278c13e46e8d20c895c221e922c6ac6b72aaca9", + commit = "fd010e54231331fc19338f81c6d072cd9ace2825", dependencies_index = "@//:swift_deps_index.json", remote = "https://github.com/SDWebImage/SDWebImage.git", ) @@ -60,7 +60,7 @@ def swift_dependencies(): # version: 5.6.0 swift_package( name = "swiftpkg_snapkit", - commit = "f222cbdf325885926566172f6f5f06af95473158", + commit = "e74fe2a978d1216c3602b129447c7301573cc2d8", dependencies_index = "@//:swift_deps_index.json", remote = "https://github.com/SnapKit/SnapKit.git", ) diff --git a/swift_deps_index.json b/swift_deps_index.json index db541e8613c..7f0970d93a9 100644 --- a/swift_deps_index.json +++ b/swift_deps_index.json @@ -187,6 +187,16 @@ "nicegram-assistant" ] }, + { + "name": "FeatSpeechToText", + "c99name": "FeatSpeechToText", + "src_type": "swift", + "label": "@swiftpkg_nicegram_assistant_ios//:Sources_FeatSpeechToText", + "package_identity": "nicegram-assistant-ios", + "product_memberships": [ + "nicegram-assistant" + ] + }, { "name": "FeatTasks", "c99name": "FeatTasks", @@ -659,6 +669,7 @@ "@swiftpkg_nicegram_assistant_ios//:Sources_FeatImagesHubUI", "@swiftpkg_nicegram_assistant_ios//:Sources_FeatPinnedChats", "@swiftpkg_nicegram_assistant_ios//:Sources_FeatPremiumUI", + "@swiftpkg_nicegram_assistant_ios//:Sources_FeatSpeechToText", "@swiftpkg_nicegram_assistant_ios//:Sources_NGAiChatUI", "@swiftpkg_nicegram_assistant_ios//:Sources_NGAssistantUI", "@swiftpkg_nicegram_assistant_ios//:Sources_NGCardUI", @@ -827,7 +838,7 @@ "name": "swiftpkg_nicegram_assistant_ios", "identity": "nicegram-assistant-ios", "remote": { - "commit": "b0d993e6fcb6e48734d8361c26bd401752ac5513", + "commit": "2cf7fd3fa9abeaee81fd2b970681ab3ae83043f4", "remote": "git@bitbucket.org:mobyrix/nicegram-assistant-ios.git", "branch": "develop" } @@ -845,18 +856,18 @@ "name": "swiftpkg_sdwebimage", "identity": "sdwebimage", "remote": { - "commit": "e278c13e46e8d20c895c221e922c6ac6b72aaca9", + "commit": "fd010e54231331fc19338f81c6d072cd9ace2825", "remote": "https://github.com/SDWebImage/SDWebImage.git", - "version": "5.18.7" + "version": "5.18.8" } }, { "name": "swiftpkg_snapkit", "identity": "snapkit", "remote": { - "commit": "f222cbdf325885926566172f6f5f06af95473158", + "commit": "e74fe2a978d1216c3602b129447c7301573cc2d8", "remote": "https://github.com/SnapKit/SnapKit.git", - "version": "5.6.0" + "version": "5.7.0" } }, { diff --git a/versions.json b/versions.json index ca3424e1ccd..c97cbc50d3a 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,6 @@ { - "app": "1.5.0", + "app": "1.5.1", "bazel": "6.4.0", - "xcode": "15.0.1" + "xcode": "15.0.1", + "macos": "13.0" }