From d2205e93a5dfc81e0be8cb92c8c7e44793a12802 Mon Sep 17 00:00:00 2001 From: Denis Shilovich Date: Tue, 26 Nov 2024 15:18:04 +0000 Subject: [PATCH] 1.9.0 (380) --- MODULE.bazel.lock | 25 +- Nicegram/NGData/Sources/NGSettings.swift | 26 +- Nicegram/NGSpeechToText/BUILD | 26 ++ .../Sources/ConvertSpeechToText.swift | 224 ++++++++++++ .../RecognitionLanguagesController.swift | 206 +++++++++++ .../Sources}/TelegramHelpers.swift | 0 .../Sources/TgSpeechToTextManager.swift | 71 ++++ Nicegram/NGTranslate/BUILD | 4 +- .../SpeechToText/TgSpeechToTextManager.swift | 44 --- .../NGUI/Sources/LanguageListController.swift | 45 ++- Nicegram/NGUI/Sources/PremiumController.swift | 51 +-- Package.resolved | 9 - Package.swift | 2 +- .../en.lproj/NiceLocalizable.strings | 7 + ng-env.txt | 2 +- ...rizationSequencePhoneEntryController.swift | 19 -- .../ItemListTextWithBackgroundItem.swift | 320 ++++++++++++++++++ .../Sources/PresentationCallManager.swift | 4 +- .../Chat/ChatMessageInteractiveFileNode/BUILD | 1 + .../ChatMessageInteractiveFileNode.swift | 148 ++++---- .../ChatInterfaceStateContextMenus.swift | 62 ++-- 21 files changed, 1030 insertions(+), 266 deletions(-) create mode 100644 Nicegram/NGSpeechToText/BUILD create mode 100644 Nicegram/NGSpeechToText/Sources/ConvertSpeechToText.swift create mode 100644 Nicegram/NGSpeechToText/Sources/RecognitionLanguagesController.swift rename Nicegram/{NGTranslate/Sources/SpeechToText => NGSpeechToText/Sources}/TelegramHelpers.swift (100%) create mode 100644 Nicegram/NGSpeechToText/Sources/TgSpeechToTextManager.swift delete mode 100644 Nicegram/NGTranslate/Sources/SpeechToText/TgSpeechToTextManager.swift create mode 100644 submodules/ItemListUI/Sources/Items/ItemListTextWithBackgroundItem.swift diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 10aeca8594d..9fa6d140de5 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -633,8 +633,8 @@ "bzlTransitiveDigest": "8/YWyYftd8THfVoADvrOmQLl45wUGfP2MVjLM5FFn50=", "usagesDigest": "voXBMcSNlo2fnK6JIvInIrncYhBKKG8nBeKvToaUA0Y=", "recordedFileInputs": { - "@@//Package.resolved": "770cf4948d2c3d7c9fa536ccac4b16daf8b6eb21a50c4d861392616f5eb80693", - "@@//Package.swift": "0d5b980a628dae3257cbf0bfaf02acbc46a99bb5a68616efa6b98fbd823657b9" + "@@//Package.resolved": "000bc5208a0bee29be3f946b417edc379239cf1b2c9902399fd19766783a74da", + "@@//Package.swift": "c27a48f2e0e1469592ea0083ce190cfeab21a0f3c960a29a3a0e7b05f5ba3f61" }, "recordedDirentsInputs": {}, "envVariables": {}, @@ -796,9 +796,9 @@ "ruleClassName": "swift_package", "attributes": { "bazel_package_name": "swiftpkg_factory", - "commit": "f350e0d71ba241b392f70519a67e769d5e3858d4", + "commit": "51660d788542bf33033a35f9a9660b7005f6e89e", "remote": "https://github.com/hmlongco/Factory.git", - "version": "2.4.1", + "version": "2.4.2", "init_submodules": false, "recursive_init_submodules": true, "patch_args": [ @@ -1115,22 +1115,11 @@ } }, "swiftpkg_nicegram_assistant_ios": { - "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", - "ruleClassName": "swift_package", + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:local_swift_package.bzl", + "ruleClassName": "local_swift_package", "attributes": { "bazel_package_name": "swiftpkg_nicegram_assistant_ios", - "commit": "5c433ef6aad5b180f91560826e45275d1f22e739", - "remote": "git@bitbucket.org:mobyrix/nicegram-assistant-ios.git", - "version": "", - "init_submodules": false, - "recursive_init_submodules": true, - "patch_args": [ - "-p0" - ], - "patch_cmds": [], - "patch_cmds_win": [], - "patch_tool": "", - "patches": [] + "path": "/Users/eugenefilipkov/Documents/mobyrix/nicegram-assistant-ios" } }, "swiftpkg_fetch_node_details_swift": { diff --git a/Nicegram/NGData/Sources/NGSettings.swift b/Nicegram/NGData/Sources/NGSettings.swift index a6f13d67a3b..1c08c40d5b3 100644 --- a/Nicegram/NGData/Sources/NGSettings.swift +++ b/Nicegram/NGData/Sources/NGSettings.swift @@ -57,6 +57,9 @@ public struct NGSettings { @NGStorage(key: "rememberFolderOnExit", defaultValue: false) public static var rememberFolderOnExit: Bool + + @NGStorage(key: "useOpenAI", defaultValue: false) + public static var useOpenAI: Bool @NGStorage(key: "lastFolder", defaultValue: -1) public static var lastFolder: Int32 @@ -124,6 +127,9 @@ public struct NGSettings { @NGStorage(key: "hideMentionNotification", defaultValue: false) public static var hideMentionNotification: Bool + + @NGStorage(key: "appleSpeechToTextLocale", defaultValue: [:]) + public static var appleSpeechToTextLocale: [Int64: Locale] } public struct NGWebSettings { @@ -167,14 +173,11 @@ public func isPremium() -> Bool { } public func usetrButton() -> [(Bool, [String])] { - if isPremium() { - var ignoredLangs = NGSettings.ignoreTranslate - if !NGSettings.useIgnoreLanguages { - ignoredLangs = [] - } - return [(NGSettings.oneTapTr, ignoredLangs)] + var ignoredLangs = NGSettings.ignoreTranslate + if !NGSettings.useIgnoreLanguages { + ignoredLangs = [] } - return [(false, [])] + return [(NGSettings.oneTapTr, ignoredLangs)] } public class SystemNGSettings { @@ -217,6 +220,15 @@ public class SystemNGSettings { UD.set(newValue, forKey: "inDoubleBottom") } } + + public var hideReactionsToYourMessages: Bool { + get { + return UD.bool(forKey: "hideReactionsToYourMessages") + } + set { + UD.set(newValue, forKey: "hideReactionsToYourMessages") + } + } } public var VarSystemNGSettings = SystemNGSettings() diff --git a/Nicegram/NGSpeechToText/BUILD b/Nicegram/NGSpeechToText/BUILD new file mode 100644 index 00000000000..64085899c8c --- /dev/null +++ b/Nicegram/NGSpeechToText/BUILD @@ -0,0 +1,26 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "NGSpeechToText", + module_name = "NGSpeechToText", + srcs = glob([ + "Sources/**/*.swift", + ]), + deps = [ + "//submodules/AccountContext:AccountContext", + "//submodules/Display:Display", + "//submodules/ItemListUI:ItemListUI", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/TelegramCore:TelegramCore", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/TranslateUI:TranslateUI", + "//submodules/Media/ConvertOpusToAAC", + "//Nicegram/NGUI:NGUI", + "@swiftpkg_nicegram_assistant_ios//:FeatPremiumUI", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Nicegram/NGSpeechToText/Sources/ConvertSpeechToText.swift b/Nicegram/NGSpeechToText/Sources/ConvertSpeechToText.swift new file mode 100644 index 00000000000..969917bb114 --- /dev/null +++ b/Nicegram/NGSpeechToText/Sources/ConvertSpeechToText.swift @@ -0,0 +1,224 @@ +import Foundation +import TelegramCore +import ChatControllerInteraction +import Postbox +import FeatPremiumUI +import AccountContext +import TelegramPresentationData +import NGData +import NGUI + +public enum SpeechToTextMessageSource { + case chat, contextMenu +} + +public func convertSpeechToText( + from source: SpeechToTextMessageSource = .chat, + languageStyle: RecognitionLanguagesControllerStyle = .normal, + context: AccountContext, + mediaFile: TelegramMediaFile, + message: Message?, + presentationData: PresentationData, + controllerInteraction: ChatControllerInteraction, + completion: (() -> Void)? = nil, + closeWithoutSelect: (() -> Void)? = nil +) { + var id: Int64? + if let peer = message?.peers.toDict().first?.value, + languageStyle == .normal { + switch EnginePeer(peer) { + case let .channel(channel): + id = channel.id.toInt64() + case let .legacyGroup(group): + id = group.id.toInt64() + case let .user(user): + id = user.id.toInt64() + default: + return + } + } + + if NGSettings.useOpenAI { + startConvertSpeechToTextTask( + from: source, + context: context, + mediaFile: mediaFile, + source: .openAI, + message: message, + presentationData: presentationData, + controllerInteraction: controllerInteraction, + completion: completion + ) + } else { + if let id, + let locale = NGSettings.appleSpeechToTextLocale[id] { + startConvertSpeechToTextTask( + from: source, + context: context, + mediaFile: mediaFile, + source: .apple(locale), + message: message, + presentationData: presentationData, + controllerInteraction: controllerInteraction, + completion: completion + ) + } else { + showLanguages( + with: context, + controllerInteraction: controllerInteraction, + style: languageStyle + ) { locale in + if let id { + var appleSpeechToTextLocale = NGSettings.appleSpeechToTextLocale + appleSpeechToTextLocale[id] = locale + NGSettings.appleSpeechToTextLocale = appleSpeechToTextLocale + } + _ = controllerInteraction.navigationController()?.popViewController(animated: true) + startConvertSpeechToTextTask( + from: source, + context: context, + mediaFile: mediaFile, + source: .apple(locale), + message: message, + presentationData: presentationData, + controllerInteraction: controllerInteraction, + completion: completion + ) + } selectWhisper: { + _ = controllerInteraction.navigationController()?.popViewController(animated: true) + + PremiumUITgHelper.routeToPremium( + source: .speechToText + ) + } closeWithoutSelect: { + closeWithoutSelect?() + } + } + } +} + +private func showLanguages( + with context: AccountContext, + controllerInteraction: ChatControllerInteraction, + style: RecognitionLanguagesControllerStyle = .normal, + selectLocale: @escaping (Locale) -> Void, + selectWhisper: @escaping () -> Void, + closeWithoutSelect: @escaping () -> Void +) { + let controller = recognitionLanguagesController( + context: context, + style: style, + selectLocale: selectLocale, + selectWhisper: selectWhisper, + closeWithoutSelect: closeWithoutSelect + ) + controller.navigationPresentation = .modal + + controllerInteraction.navigationController()?.pushViewController(controller, animated: true) +} + +private func startConvertSpeechToTextTask( + from messageSource: SpeechToTextMessageSource, + context: AccountContext, + mediaFile: TelegramMediaFile, + source: TgSpeechToTextManager.Source, + message: Message?, + presentationData: PresentationData, + controllerInteraction: ChatControllerInteraction, + completion: (() -> Void)? = nil +) { + Task { @MainActor in + let manager = TgSpeechToTextManager( + accountContext: context + ) + + if messageSource == .contextMenu { + message?.setSpeechToTextLoading(context: context) + } + + let result = await manager.convertSpeechToText( + mediaFile: mediaFile, + source: source + ) + + switch result { + case .success(let text): + switch messageSource { + case .chat: + message?.updateAudioTranscriptionAttribute(text: text, error: nil, context: context) + case .contextMenu: + message?.setSpeechToTextTranslation(text, context: context) + } + case .needsPremium: + PremiumUITgHelper.routeToPremium( + source: .speechToText + ) + case .error(let error): + switch error { + case .recognition(_): + if messageSource == .contextMenu { + message?.removeSpeechToTextMeta(context: context) + } + convertSpeechToText( + from: messageSource, + languageStyle: .whisper, + context: context, + mediaFile: mediaFile, + message: message, + presentationData: presentationData, + controllerInteraction: controllerInteraction + ) + case .notAvailable: + if messageSource == .contextMenu { + message?.removeSpeechToTextMeta(context: context) + } + let c = getIAPErrorController( + context: context, + "Speech to text recognizer not available.", + presentationData + ) + controllerInteraction.presentGlobalOverlayController(c, nil) + case .authorizationStatus: + if messageSource == .contextMenu { + message?.removeSpeechToTextMeta(context: context) + } + let c = getIAPErrorController( + context: context, + "Speech to text recognizer autorization status error.", + presentationData + ) + controllerInteraction.presentGlobalOverlayController(c, nil) + case let .api(error): + switch messageSource { + case .chat: + message?.updateAudioTranscriptionAttribute(text: "", error: error, context: context) + case .contextMenu: + message?.removeSpeechToTextMeta(context: context) + } + + let c = getIAPErrorController( + context: context, + error.localizedDescription, + presentationData + ) + controllerInteraction.presentGlobalOverlayController(c, nil) + case let .other(error): + switch messageSource { + case .chat: + message?.updateAudioTranscriptionAttribute(text: "", error: error, context: context) + case .contextMenu: + message?.removeSpeechToTextMeta(context: context) + } + + let c = getIAPErrorController( + context: context, + error.localizedDescription, + presentationData + ) + controllerInteraction.presentGlobalOverlayController(c, nil) + } + } + + completion?() + } +} diff --git a/Nicegram/NGSpeechToText/Sources/RecognitionLanguagesController.swift b/Nicegram/NGSpeechToText/Sources/RecognitionLanguagesController.swift new file mode 100644 index 00000000000..02795391f63 --- /dev/null +++ b/Nicegram/NGSpeechToText/Sources/RecognitionLanguagesController.swift @@ -0,0 +1,206 @@ +import UIKit +import Display +import ComponentFlow +import Speech +import AccountContext +import SwiftSignalKit +import ItemListUI +import TranslateUI +import TelegramPresentationData + +import NGStrings +import NGUI + +public enum RecognitionLanguagesControllerStyle { + case normal, whisper +} + +public func recognitionLanguagesController( + context: AccountContext, + selectedLocale: Locale? = nil, + style: RecognitionLanguagesControllerStyle = .normal, + selectLocale: @escaping (Locale) -> Void, + selectWhisper: @escaping () -> Void, + closeWithoutSelect: @escaping () -> Void +) -> ViewController { + let languages = SFSpeechRecognizer.supportedLocales() + .compactMap { locale -> LanguageInfo? in + guard let title = locale.localizedString(forIdentifier: locale.identifier) else { + return nil + } + let subtitle = locale.localizedString(forIdentifier: locale.identifier) ?? title + + return LanguageInfo(code: locale.identifier, title: title, subtitle: subtitle) + } + .sorted(by: { $0.title < $1.title }) + + let initialState = LanguageListControllerState( + languages: languages, + selectedLanguageCode: selectedLocale?.identifier + ) + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((LanguageListControllerState) -> LanguageListControllerState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + let arguments = LanguageListControllerArguments( + selectLanguage: { code in + updateState { state in + var state = state + state.selectedLanguageCode = code + return state + } + selectLocale(Locale(identifier: code)) + }, + selectWhisper: { + selectWhisper() + } + ) + + let signal = combineLatest(context.sharedContext.presentationData, statePromise.get()) + |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in + let controllerState = ItemListControllerState( + presentationData: ItemListPresentationData(presentationData), + title: .text(l("NicegramSpeechToText.Language.Title")), + leftNavigationButton: nil, + rightNavigationButton: nil, + backNavigationButton: .init(title: presentationData.strings.Common_Close) + ) + + let listState = ItemListNodeState( + presentationData: ItemListPresentationData(presentationData), + entries: entries(theme: presentationData.theme, state: state, style: style), + style: .blocks, + animateChanges: false + ) + + return (controllerState, (listState, arguments)) + } + + let controller = ItemListController(context: context, state: signal) + controller.willDisappear = { _ in + stateValue.with { state in + if state.selectedLanguageCode == nil { + closeWithoutSelect() + } + } + } + + return controller +} + +private func entries( + theme: PresentationTheme, + state: LanguageListControllerState, + style: RecognitionLanguagesControllerStyle +) -> [RecognitionLanguageEntry] { + var index: Int32 = 0 + var entries: [RecognitionLanguageEntry] + + if style == .whisper { + let error = RecognitionLanguageEntry.whisper( + index, + theme, + l("NicegramSpeechToText.Language.Whisper"), + false + ) + index += 1 + let header = RecognitionLanguageEntry.header(index, l("NicegramSpeechToText.Language.Choose").uppercased()) + entries = [error, header] + } else { + let header = RecognitionLanguageEntry.header(index, l("NicegramSpeechToText.Language.Choose").uppercased()) + entries = [header] + } + index += 1 + + let (languages, selectedCode) = (state.languages, state.selectedLanguageCode) + for lang in languages { + entries.append(.language(index, theme, lang, lang.code == selectedCode)) + index += 1 + } + + return entries +} + +private enum RecognitionLanguageSection: Int32 { + case languages + case whisper + case header +} + +private enum RecognitionLanguageEntry: ItemListNodeEntry { + case header(Int32, String) + case language(Int32, PresentationTheme, LanguageInfo, Bool) + case whisper(Int32, PresentationTheme, String, Bool) + + var section: ItemListSectionId { + switch self { + case .language: + return RecognitionLanguageSection.languages.rawValue + case .whisper: + return RecognitionLanguageSection.whisper.rawValue + case .header: + return RecognitionLanguageSection.languages.rawValue + } + } + + var stableId: Int32 { + switch self { + case let .language(index, _, _, _): return index + case let .whisper(index, _, _, _): return index + case let .header(index, _): return index + } + } + + static func <(lhs: Self, rhs: Self) -> Bool { + lhs.stableId < rhs.stableId + } + + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { + let arguments = arguments as! LanguageListControllerArguments + switch self { + case let .header(_, text): + return ItemListSectionHeaderItem( + presentationData: presentationData, + text: text, + sectionId: self.section + ) + case let .whisper(_, _, message, _): + return ItemListTextWithBackgroundItem( + presentationData: presentationData, + text: .markdown(message), + style: .blocks, + sectionId: self.section, + linkAction: { _ in + arguments.selectWhisper() + } + ) + case let .language(_, _, info, value): + return LocalizationListItem( + presentationData: presentationData, + id: info.code, + title: info.title, + subtitle: info.subtitle, + checked: value, + activity: false, + loading: false, + editing: LocalizationListItemEditing( + editable: false, + editing: false, + revealed: false, + reorderable: false + ), + sectionId: self.section, + alwaysPlain: false, + action: { + if !value { + arguments.selectLanguage(info.code) + } + }, + setItemWithRevealedOptions: { _, _ in }, + removeItem: { _ in } + ) + } + } +} diff --git a/Nicegram/NGTranslate/Sources/SpeechToText/TelegramHelpers.swift b/Nicegram/NGSpeechToText/Sources/TelegramHelpers.swift similarity index 100% rename from Nicegram/NGTranslate/Sources/SpeechToText/TelegramHelpers.swift rename to Nicegram/NGSpeechToText/Sources/TelegramHelpers.swift diff --git a/Nicegram/NGSpeechToText/Sources/TgSpeechToTextManager.swift b/Nicegram/NGSpeechToText/Sources/TgSpeechToTextManager.swift new file mode 100644 index 00000000000..939b6f400df --- /dev/null +++ b/Nicegram/NGSpeechToText/Sources/TgSpeechToTextManager.swift @@ -0,0 +1,71 @@ +import FeatSpeechToText +import Speech +import Foundation +import Postbox +import SwiftSignalKit +import TelegramCore +import ConvertOpusToAAC +import AccountContext + +@available(iOS 13.0, *) +public class TgSpeechToTextManager { + public enum Source { + case apple(Locale), openAI + } + + // MARK: - Dependencies + + private let convertSpeechToTextUseCase: ConvertSpeechToTextUseCase + private let mediaBox: MediaBox + private let accountContext: AccountContext + + // MARK: - Lifecycle + + public init( + accountContext: AccountContext + ) { + self.convertSpeechToTextUseCase = SpeechToTextContainer.shared.convertSpeechToTextUseCase() + self.mediaBox = accountContext.account.postbox.mediaBox + self.accountContext = accountContext + } +} + +@available(iOS 13.0, *) +public extension TgSpeechToTextManager { + func convertSpeechToText( + mediaFile: TelegramMediaFile, + source: Source + ) async -> ConvertSpeechToTextResult { + await withCheckedContinuation { continuation in + let _ = ( + mediaBox.resourceData(mediaFile.resource) + |> take(1) + |> mapToSignal { [weak self] data -> Signal in + guard let self, + case .apple = source else { return .single(data.path) } + + return convertOpusToAAC(sourcePath: data.path, allocateTempFile: { + return TempBox.shared.tempFile(fileName: "audio.m4a").path + }) + } + ).start { result in + guard let path = result else { return } + + let url = URL( + fileURLWithPath: path + ) + + Task { + switch source { + case .openAI: + let result = await self.convertSpeechToTextUseCase.openAISpeechToText(url: url) + continuation.resume(returning: result) + case let .apple(locale): + let result = await self.convertSpeechToTextUseCase.appleSpeechToText(url: url, locale: locale) + continuation.resume(returning: result) + } + } + } + } + } +} diff --git a/Nicegram/NGTranslate/BUILD b/Nicegram/NGTranslate/BUILD index c7d5fe85a8f..b445840116b 100644 --- a/Nicegram/NGTranslate/BUILD +++ b/Nicegram/NGTranslate/BUILD @@ -11,12 +11,12 @@ swift_library( "@swiftpkg_nicegram_assistant_ios//:NGCore", "//submodules/AccountContext:AccountContext", "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", - "//submodules/TelegramCore:TelegramCore", + "//submodules/TelegramCore:TelegramCore", "//Nicegram/NGLogging:NGLogging", "//Nicegram/NGData:NGData", "//Nicegram/NGModels:NGModels", "//Nicegram/NGStrings:NGStrings", - "//Nicegram/NGUtils:NGUtils", + "//Nicegram/NGUtils:NGUtils" ], visibility = [ "//visibility:public", diff --git a/Nicegram/NGTranslate/Sources/SpeechToText/TgSpeechToTextManager.swift b/Nicegram/NGTranslate/Sources/SpeechToText/TgSpeechToTextManager.swift deleted file mode 100644 index 9396e8e6c96..00000000000 --- a/Nicegram/NGTranslate/Sources/SpeechToText/TgSpeechToTextManager.swift +++ /dev/null @@ -1,44 +0,0 @@ -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/NGUI/Sources/LanguageListController.swift b/Nicegram/NGUI/Sources/LanguageListController.swift index abf8784bc68..72d68b69d4f 100644 --- a/Nicegram/NGUI/Sources/LanguageListController.swift +++ b/Nicegram/NGUI/Sources/LanguageListController.swift @@ -7,11 +7,16 @@ import SwiftSignalKit import TelegramPresentationData import TranslateUI -private final class LanguageListControllerArguments { - let selectLanguage: (String) -> Void +public final class LanguageListControllerArguments { + public let selectLanguage: (String) -> Void + public let selectWhisper: () -> Void - init(selectLanguage: @escaping (String) -> Void) { + public init( + selectLanguage: @escaping (String) -> Void, + selectWhisper: @escaping () -> Void + ) { self.selectLanguage = selectLanguage + self.selectWhisper = selectWhisper } } @@ -53,15 +58,26 @@ private enum LanguageListControllerEntry: ItemListNodeEntry { } } -private struct LanguageInfo: Hashable { - let code: String - let title: String - let subtitle: String +public struct LanguageInfo: Hashable { + public let code: String + public let title: String + public let subtitle: String + + public init(code: String, title: String, subtitle: String) { + self.code = code + self.title = title + self.subtitle = subtitle + } } -private struct LanguageListControllerState: Equatable { - var languages: [LanguageInfo] - var selectedLanguageCode: String? +public struct LanguageListControllerState: Equatable { + public var languages: [LanguageInfo] + public var selectedLanguageCode: String? + + public init(languages: [LanguageInfo], selectedLanguageCode: String?) { + self.languages = languages + self.selectedLanguageCode = selectedLanguageCode + } } private func languageListControllerEntries(theme: PresentationTheme, state: LanguageListControllerState) -> [LanguageListControllerEntry] { @@ -77,7 +93,11 @@ private func languageListControllerEntries(theme: PresentationTheme, state: Lang return entries } -public func languageListController(context: AccountContext, selectedLanguageCode: String?, selectLanguage: @escaping (String) -> Void) -> ViewController { +public func languageListController( + context: AccountContext, + selectedLanguageCode: String?, + selectLanguage: @escaping (String) -> Void +) -> ViewController { let primaryLanguageCodes = [selectedLanguageCode].compactMap{$0} + popularTranslationLanguages let supportedTranslationLanguageCodes = supportedTranslationLanguages @@ -103,7 +123,8 @@ public func languageListController(context: AccountContext, selectedLanguageCode return state } selectLanguage(code) - } + }, + selectWhisper: {} ) let signal = combineLatest(context.sharedContext.presentationData, statePromise.get()) diff --git a/Nicegram/NGUI/Sources/PremiumController.swift b/Nicegram/NGUI/Sources/PremiumController.swift index 65d10c93058..b6b626f41ff 100644 --- a/Nicegram/NGUI/Sources/PremiumController.swift +++ b/Nicegram/NGUI/Sources/PremiumController.swift @@ -60,6 +60,7 @@ private enum PremiumSettingsToggle { case syncPins case oneTapTr case rememberFilterOnExit + case useOpenAI } private enum PremiumControllerEntry: ItemListNodeEntry { @@ -84,8 +85,7 @@ private enum PremiumControllerEntry: ItemListNodeEntry { case testButton(PresentationTheme, String) case ignoretr(PresentationTheme, String) - case useOpenAi(Bool) - + case useOpenAI(PresentationTheme, String, Bool) case recordAllCalls(String, Bool) var section: ItemListSectionId { @@ -102,7 +102,7 @@ private enum PremiumControllerEntry: ItemListNodeEntry { return premiumControllerSection.other.rawValue case .testButton: return premiumControllerSection.test.rawValue - case .useOpenAi: + case .useOpenAI: return premiumControllerSection.speechToText.rawValue case .recordAllCalls: return premiumControllerSection.calls.rawValue @@ -137,7 +137,7 @@ private enum PremiumControllerEntry: ItemListNodeEntry { return 11000 case .ignoretr: return 12000 - case .useOpenAi: + case .useOpenAI: return 13000 case .recordAllCalls: return 14000 @@ -234,8 +234,8 @@ private enum PremiumControllerEntry: ItemListNodeEntry { } else { return false } - case let .useOpenAi(lhsValue): - if case let .useOpenAi(rhsValue) = rhs, lhsValue == rhsValue { + case let .useOpenAI(lhsValue): + if case let .useOpenAI(rhsValue) = rhs, lhsValue == rhsValue { return true } else { return false @@ -298,16 +298,9 @@ 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 .useOpenAI(_, text, value): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: true, sectionId: self.section, style: .blocks, updated: { value in + arguments.toggleSetting(value, .useOpenAI) }) case let .recordAllCalls(title, value): return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, enabled: true, sectionId: self.section, style: .blocks, updated: { value in @@ -321,7 +314,7 @@ private enum PremiumControllerEntry: ItemListNodeEntry { } -private func premiumControllerEntries(presentationData: PresentationData, useOpenAi: Bool) -> [PremiumControllerEntry] { +private func premiumControllerEntries(presentationData: PresentationData) -> [PremiumControllerEntry] { var entries: [PremiumControllerEntry] = [] let theme = presentationData.theme @@ -330,8 +323,7 @@ private func premiumControllerEntries(presentationData: PresentationData, useOpe entries.append(.onetaptr(theme, l("Premium.OnetapTranslate"), NGSettings.oneTapTr)) entries.append(.ignoretr(theme, l("Premium.IgnoreTranslate.Title"))) - entries.append(.useOpenAi(useOpenAi)) - + entries.append(.useOpenAI(theme, l("SpeechToText.UseOpenAi"), NGSettings.useOpenAI)) entries.append(.recordAllCalls(l("Premium.RecordAllCalls"), NGSettings.recordAllCalls)) #if DEBUG @@ -371,19 +363,6 @@ public func premiumController(context: AccountContext) -> ViewController { let updateState: ((SelectionState) -> SelectionState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } - - let useOpenAiSignal: Signal - if #available(iOS 13.0, *) { - let getPreferredProviderTypeUseCase = SpeechToTextContainer.shared.getPreferredProviderTypeUseCase() - - useOpenAiSignal = getPreferredProviderTypeUseCase - .publisher() - .map { $0 == .openAi } - .toSignal() - .skipError() - } else { - useOpenAiSignal = .single(false) - } let arguments = PremiumControllerArguments(toggleSetting: { value, setting in switch (setting) { @@ -391,6 +370,8 @@ public func premiumController(context: AccountContext) -> ViewController { NGSettings.oneTapTr = value case .rememberFilterOnExit: NGSettings.rememberFolderOnExit = value + case .useOpenAI: + NGSettings.useOpenAI = value default: break } @@ -490,10 +471,10 @@ public func premiumController(context: AccountContext) -> ViewController { - let signal = combineLatest(context.sharedContext.presentationData, statePromise.get(), useOpenAiSignal) - |> map { presentationData, state, useOpenAi -> (ItemListControllerState, (ItemListNodeState, Any)) in + let signal = combineLatest(context.sharedContext.presentationData, statePromise.get()) + |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in - let entries = premiumControllerEntries(presentationData: presentationData, useOpenAi: useOpenAi) + let entries = premiumControllerEntries(presentationData: presentationData) var _ = 0 var scrollToItem: ListViewScrollToItem? diff --git a/Package.resolved b/Package.resolved index 169ce653ead..abafe00f7f1 100644 --- a/Package.resolved +++ b/Package.resolved @@ -108,15 +108,6 @@ "revision" : "66716ce9c31198931c2275a0b69de2fdaa687e74" } }, - { - "identity" : "nicegram-assistant-ios", - "kind" : "remoteSourceControl", - "location" : "git@bitbucket.org:mobyrix/nicegram-assistant-ios.git", - "state" : { - "branch" : "feat/daily-login-limit", - "revision" : "3efed4eedb5cfd3a1a587b6ca4ce19a57b146df4" - } - }, { "identity" : "nicegram-wallet-ios", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 4addb6540f4..13bc1ceba39 100644 --- a/Package.swift +++ b/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "nicegram-package", dependencies: [ - .package(url: "git@bitbucket.org:mobyrix/nicegram-assistant-ios.git", branch: "feat/daily-login-limit"), + .package(url: "git@bitbucket.org:mobyrix/nicegram-assistant-ios.git", branch: "feat/NCG-6326"), .package(url: "git@bitbucket.org:mobyrix/nicegram-wallet-ios.git", branch: "develop") ] ) diff --git a/Telegram/Telegram-iOS/en.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/en.lproj/NiceLocalizable.strings index 2d93107d5b2..9e90d350d5a 100644 --- a/Telegram/Telegram-iOS/en.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/NiceLocalizable.strings @@ -155,6 +155,7 @@ "NicegramSettings.Unblock.Header" = "Unblock"; "NicegramSettings.Unblock.Button" = "Unblock Guide"; "NicegramSettings.Other.hideReactions" = "Hide Reactions"; +"NicegramSettings.Other.hideReactionsToYourMessages" = "Hide reactions to your messages"; "NicegramSettings.RoundVideos.DownloadVideos" = "Download videos to Gallery"; "Premium.OnetapTranslate.UseIgnoreLanguages" = "Ignore Languages"; "Premium.OnetapTranslate.UseIgnoreLanguages.Note" = "App detects language of each message on your device. It's a high-performance task which can affect your battery life."; @@ -225,3 +226,9 @@ "NicegramFeed.Title" = "Feed"; "NicegramFeed.Add" = "Add to Feed"; "NicegramFeed.Remove" = "Remove from Feed"; + +/*SpeechToText*/ +"NicegramSpeechToText.Language.Title" = "Speach-2-Text"; +"NicegramSpeechToText.Language.Whisper" = "⚠️ We couldn't detect the language of your voice message. Choose the right language manually or [use Whisper](https://my.nicegram.app/) to detect it automatically."; +"NicegramSpeechToText.Language.Choose" = "Choose a language for recognition"; +"NicegramSpeechToText.SelectLanguage" = "Select Language"; diff --git a/ng-env.txt b/ng-env.txt index 30d74d25844..c23f7a80886 100644 --- a/ng-env.txt +++ b/ng-env.txt @@ -1 +1 @@ -test \ No newline at end of file +prod \ No newline at end of file diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift index 8ad8300d757..80c71524a91 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift @@ -4,9 +4,6 @@ import FeatPhoneEntryBanner // MARK: Nicegram Auth import FeatAuth // -// MARK: Nicegram DailyLoginLimit -import CoreSwiftUI -// // MARK: Nicegram Onboarding import FeatOnboarding // @@ -249,10 +246,6 @@ public final class AuthorizationSequencePhoneEntryController: ViewController, MF } } - // MARK: Nicegram DailyLoginLimit - private var sawDailyLoginLimitPopup = false - // - override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) @@ -265,18 +258,6 @@ public final class AuthorizationSequencePhoneEntryController: ViewController, MF self.loginWithNumber?(AppReviewLogin.phone, self.controllerNode.syncContacts) } // - - // MARK: Nicegram DailyLoginLimit - if #available(iOS 15.0, *) { - if self.otherAccountPhoneNumbers.1.count >= 3, !self.sawDailyLoginLimitPopup { - self.sawDailyLoginLimitPopup = true - Task { - try? await Task.sleep(seconds: 0.5) - DailyLoginLimitPopupPresenter().present() - } - } - } - // } override public func viewWillDisappear(_ animated: Bool) { diff --git a/submodules/ItemListUI/Sources/Items/ItemListTextWithBackgroundItem.swift b/submodules/ItemListUI/Sources/Items/ItemListTextWithBackgroundItem.swift new file mode 100644 index 00000000000..dad8d307965 --- /dev/null +++ b/submodules/ItemListUI/Sources/Items/ItemListTextWithBackgroundItem.swift @@ -0,0 +1,320 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramPresentationData +import TextFormat +import Markdown + +public class ItemListTextWithBackgroundItem: InfoListItem, ItemListItem { + public let sectionId: ItemListSectionId + + public init(presentationData: ItemListPresentationData, text: InfoListItemText, style: ItemListStyle, sectionId: ItemListSectionId, linkAction: ((InfoListItemLinkAction) -> Void)? = nil) { + self.sectionId = sectionId + super.init(presentationData: presentationData, title: "", text: text, style: style, linkAction: linkAction, closeAction: nil) + } + + override public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = ItemListTextWithBackgroundNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + override public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? ItemListTextWithBackgroundNode { + let makeLayout = nodeValue.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } + } +} + +public class ItemListTextWithBackgroundNode: ListViewItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let maskNode: ASImageNode + + private let labelNode: TextNode + private let textNode: TextNode + private var linkHighlightingNode: LinkHighlightingNode? + + private let activateArea: AccessibilityAreaNode + + private var item: InfoListItem? + + public override var canBeSelected: Bool { + return false + } + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.backgroundColor = .white + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.maskNode = ASImageNode() + + self.labelNode = TextNode() + self.labelNode.isUserInteractionEnabled = false + + self.textNode = TextNode() + self.textNode.isUserInteractionEnabled = false + + self.activateArea = AccessibilityAreaNode() + self.activateArea.accessibilityTraits = .staticText + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.labelNode) + self.addSubnode(self.textNode) + self.addSubnode(self.activateArea) + } + + public override func didLoad() { + super.didLoad() + + let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) + recognizer.tapActionAtPoint = { [weak self] point in + if let strongSelf = self { + return .waitForSingleTap + } + return .fail + } + recognizer.highlight = { [weak self] point in + if let strongSelf = self { + strongSelf.updateTouchesAtPoint(point) + } + } + self.view.addGestureRecognizer(recognizer) + } + + func asyncLayout() -> (_ item: InfoListItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors?) -> (ListViewItemNodeLayout, () -> Void) { + let makeTextLayout = TextNode.asyncLayout(self.textNode) + + let currentItem = self.item + + return { item, params, neighbors in + let leftInset: CGFloat = 16.0 + params.leftInset + let rightInset: CGFloat = 16.0 + params.rightInset + + let smallerTextFont = Font.regular(item.presentationData.fontSize.itemListBaseLabelFontSize / 14.0 * 15.0) + let textFont = Font.regular(item.presentationData.fontSize.itemListBaseLabelFontSize / 14.0 * 16.0) + let textBoldFont = Font.semibold(item.presentationData.fontSize.itemListBaseLabelFontSize / 14.0 * 16.0) + + var updatedTheme: PresentationTheme? + + let badgeDiameter: CGFloat = item.isWarning ? 30.0 : 22.0 + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme + } + + let insets: UIEdgeInsets + if let neighbors = neighbors { + insets = itemListNeighborsGroupedInsets(neighbors, params) + } else { + insets = UIEdgeInsets() + } + let separatorHeight = UIScreenPixel + + let itemBackgroundColor: UIColor + let itemSeparatorColor: UIColor + + switch item.style { + case .plain: + itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor + case .blocks: + itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor + } + + let attributedText: NSAttributedString + switch item.text { + case let .plain(text): + attributedText = NSAttributedString(string: text, font: textFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) + case let .markdown(text): + attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: item.isWarning ? smallerTextFont : textFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), bold: MarkdownAttributeSet(font: textBoldFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), link: MarkdownAttributeSet(font: textFont, textColor: item.presentationData.theme.list.itemAccentColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + })) + } + + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let contentSize = CGSize(width: params.width, height: textLayout.size.height + 38.0) + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + + return (layout, { [weak self] in + if let strongSelf = self { + strongSelf.item = item + + strongSelf.accessibilityLabel = "\(item.title)\n\(attributedText.string)" + strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height)) + strongSelf.activateArea.accessibilityLabel = strongSelf.accessibilityLabel + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = itemBackgroundColor + } + + let _ = textApply() + + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + if strongSelf.maskNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.maskNode, at: 3) + } + + let hasCorners = itemListHasRoundedBlockLayout(params) + var hasTopCorners = false + var hasBottomCorners = false + if let neighbors = neighbors { + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + hasTopCorners = true + strongSelf.topStripeNode.isHidden = hasCorners || !item.hasDecorations + } + } + let bottomStripeInset: CGFloat + if let neighbors = neighbors { + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = leftInset + strongSelf.bottomStripeNode.isHidden = false + default: + bottomStripeInset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners || !item.hasDecorations + } + } else { + bottomStripeInset = leftInset + if !item.hasDecorations { + strongSelf.topStripeNode.isHidden = true + } + } + + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight)) + + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 15.0), size: textLayout.size) + } + }) + } + } + + public override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + public override func animateAdded(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + public override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } + + @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + switch recognizer.state { + case .ended: + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .tap: + let titleFrame = self.textNode.frame + if let item = self.item, titleFrame.contains(location) { + if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) { + if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + item.linkAction?(.tap(url)) + } + } + } + default: + break + } + } + default: + break + } + } + + private func updateTouchesAtPoint(_ point: CGPoint?) { + if let item = self.item { + var rects: [CGRect]? + if let point = point { + let textNodeFrame = self.textNode.frame + if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + let possibleNames: [String] = [ + TelegramTextAttributes.URL, + TelegramTextAttributes.PeerMention, + TelegramTextAttributes.PeerTextMention, + TelegramTextAttributes.BotCommand, + TelegramTextAttributes.Hashtag + ] + for name in possibleNames { + if let _ = attributes[NSAttributedString.Key(rawValue: name)] { + rects = self.textNode.attributeRects(name: name, at: index) + break + } + } + } + } + + if let rects = rects { + let linkHighlightingNode: LinkHighlightingNode + if let current = self.linkHighlightingNode { + linkHighlightingNode = current + } else { + linkHighlightingNode = LinkHighlightingNode(color: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.2)) + self.linkHighlightingNode = linkHighlightingNode + self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode) + } + linkHighlightingNode.frame = self.textNode.frame + linkHighlightingNode.updateRects(rects) + } else if let linkHighlightingNode = self.linkHighlightingNode { + self.linkHighlightingNode = nil + linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in + linkHighlightingNode?.removeFromSupernode() + }) + } + } + } +} diff --git a/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift b/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift index 787bd98691e..dad2fe1ea73 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift @@ -1040,8 +1040,8 @@ public final class PresentationCallManagerImpl: PresentationCallManager { previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, -// mimeType: "audio/ogg", - mimeType: "audio/wav", + mimeType: "audio/ogg", +// mimeType: "audio/wav", size: Int64(size), attributes: [ .Audio( diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/BUILD index 76e0baff227..f1f809c0168 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/BUILD @@ -5,6 +5,7 @@ NGDEPS = [ "//Nicegram/NGTelegramIntegration:NGTelegramIntegration", "//Nicegram/NGTranslate:NGTranslate", "//Nicegram/NGUI:NGUI", + "//Nicegram/NGSpeechToText:NGSpeechToText", "@swiftpkg_nicegram_assistant_ios//:FeatPremiumUI", ] diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift index bfbbd9a7c4e..6dd590065e3 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift @@ -6,6 +6,7 @@ import NGStrings import NGTelegramIntegration import NGTranslate import NGUI +import NGSpeechToText // import Foundation import UIKit @@ -358,12 +359,8 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { guard let arguments = self.arguments, let context = self.context, let message = self.message else { return } - - // MARK: Nicegram Speech2Text - let isNicegramPremium = isPremium() - // - - if !context.isPremium, case .inProgress = self.audioTranscriptionState { +// MARK: Nicegram NCG-6326 Apple Speech2Text, remove premium check !context.isPremium + if case .inProgress = self.audioTranscriptionState { return } @@ -371,7 +368,8 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: arguments.context.currentAppConfiguration.with { $0 }) let transcriptionText = self.forcedAudioTranscriptionText ?? transcribedText(message: message) - if transcriptionText == nil && !arguments.associatedData.alwaysDisplayTranscribeButton.providedByGroupBoost { +// MARK: Nicegram NCG-6326 Apple Speech2Text, added false to skip this condition + if transcriptionText == nil && !arguments.associatedData.alwaysDisplayTranscribeButton.providedByGroupBoost && false { if premiumConfiguration.audioTransciptionTrialCount > 0 { if !arguments.associatedData.isPremium { if self.presentAudioTranscriptionTooltip(finished: false) { @@ -379,40 +377,28 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { } } } else { - // MARK: Nicegram Speech2Text, nicegram premium check - guard arguments.associatedData.isPremium || isNicegramPremium else { - // MARK: Nicegram Speech2Text, routeToNicegramPremium - if isNicegram() { - PremiumUITgHelper.routeToPremium( - source: .speechToText - ) - return - } - // - - if self.hapticFeedback == nil { - self.hapticFeedback = HapticFeedback() - } - self.hapticFeedback?.impact(.medium) - - let tipController = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_voiceToText", scale: 0.065, colors: [:], title: nil, text: presentationData.strings.Message_AudioTranscription_SubscribeToPremium, customUndoText: presentationData.strings.Message_AudioTranscription_SubscribeToPremiumAction, timeout: nil), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { action in - if case .undo = action { - var replaceImpl: ((ViewController) -> Void)? - let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, forceDark: false, action: { - let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil) - replaceImpl?(controller) - }, dismissed: nil) - replaceImpl = { [weak controller] c in - controller?.replace(with: c) - } - arguments.controllerInteraction.navigationController()?.pushViewController(controller, animated: true) - - let _ = ApplicationSpecificNotice.incrementAudioTranscriptionSuggestion(accountManager: context.sharedContext.accountManager).startStandalone() - } - return false }) - arguments.controllerInteraction.presentControllerInCurrent(tipController, nil) - return + if self.hapticFeedback == nil { + self.hapticFeedback = HapticFeedback() } + self.hapticFeedback?.impact(.medium) + + let tipController = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_voiceToText", scale: 0.065, colors: [:], title: nil, text: presentationData.strings.Message_AudioTranscription_SubscribeToPremium, customUndoText: presentationData.strings.Message_AudioTranscription_SubscribeToPremiumAction, timeout: nil), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { action in + if case .undo = action { + var replaceImpl: ((ViewController) -> Void)? + let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, forceDark: false, action: { + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil) + replaceImpl?(controller) + }, dismissed: nil) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + arguments.controllerInteraction.navigationController()?.pushViewController(controller, animated: true) + + let _ = ApplicationSpecificNotice.incrementAudioTranscriptionSuggestion(accountManager: context.sharedContext.accountManager).startStandalone() + } + return false }) + arguments.controllerInteraction.presentControllerInCurrent(tipController, nil) + return } } @@ -441,53 +427,10 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { self.requestUpdateLayout(true) // MARK: Nicegram Speech2Text - 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 - } + let shouldUseNicegramTranscribe = true if shouldUseNicegramTranscribe { - if let mediaFile = message.media.compactMap({ $0 as? TelegramMediaFile }).first(where: { $0.isVoice }) { - - if #available(iOS 13.0, *) { - Task { @MainActor in - let manager = TgSpeechToTextManager( - mediaBox: context.account.postbox.mediaBox - ) - - 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( - source: .speechToText - ) - 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 - } - } - } + internalConvertSpeechToText() } // else if context.sharedContext.immediateExperimentalUISettings.localTranscription { @@ -923,12 +866,10 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { // MARK: Nicegram Speech2Text if #available(iOS 13.0, *) { - let isNicegramPrmeium = isPremium() - let getSpeechToTextConfigUseCase = SpeechToTextContainer.shared.getSpeechToTextConfigUseCase() let alwaysShowButton = getSpeechToTextConfigUseCase().alwaysShowButton - if isNicegramPrmeium || alwaysShowButton { + if alwaysShowButton { displayTranscribe = true } } @@ -2238,6 +2179,37 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { view.animateIn() } } + +// MARK: Nicegram NCG-6326 Apple Speech2Text + private func internalConvertSpeechToText( + with languageStyle: RecognitionLanguagesControllerStyle = .normal + ) { + guard let arguments = self.arguments, + let context = self.context, + let message = self.message, + let mediaFile = message.media.compactMap({ $0 as? TelegramMediaFile }).first(where: { $0.isVoice }) else { + return + } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let controllerInteraction = arguments.controllerInteraction + + convertSpeechToText( + languageStyle: languageStyle, + context: context, + mediaFile: mediaFile, + message: message, + presentationData: presentationData, + controllerInteraction: controllerInteraction + ) { [weak self] in + self?.audioTranscriptionState = .expanded + } closeWithoutSelect: { [weak self] in + Queue.mainQueue().async { + self?.audioTranscriptionState = .collapsed + self?.requestUpdateLayout(true) + } + } + } +// } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 0f2f1afab8c..d652c9eeb7a 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -38,6 +38,8 @@ import NGStrings import NGTranslate import NGUI import PeerInfoUI +import NGData +import NGSpeechToText // import TranslateUI import DebugSettingsUI @@ -1039,7 +1041,30 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState break } } - +// MARK: Nicegram NCG-6326 Apple Speech2Text + if let mediaFile = message.media.compactMap({ $0 as? TelegramMediaFile }).first(where: { $0.isVoice }) { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + didRateAudioTranscription = true + actions.append(.action(ContextMenuActionItem( + text: l("NicegramSpeechToText.SelectLanguage"), + icon: { theme in + nil + }, action: { _, f in + convertSpeechToText( + from: .chat, + languageStyle: .whisper, + context: context, + mediaFile: mediaFile, + message: message, + presentationData: presentationData, + controllerInteraction: controllerInteraction + ) + f(.dismissWithoutContent) + } + ))) + actions.append(.separator) + } +// var hasRateTranscription = false if hasExpandedAudioTranscription, let audioTranscription = audioTranscription, !didRateAudioTranscription { hasRateTranscription = true @@ -2134,33 +2159,14 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState icon: nicegramIcon, action: { controller, f in if mode == "do" { - 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 result { - case .success(let translation): - message.setSpeechToTextTranslation(translation, context: context) - case .needsPremium: - message.removeSpeechToTextMeta(context: context) - - PremiumUITgHelper.routeToPremium( - source: .speechToText - ) - case .error(let error): - message.removeSpeechToTextMeta(context: context) - - let c = getIAPErrorController(context: context, error.localizedDescription, presentationData) - controllerInteraction.presentGlobalOverlayController(c, nil) - } - } - } + convertSpeechToText( + from: .contextMenu, + context: context, + mediaFile: mediaFile, + message: message, + presentationData: presentationData, + controllerInteraction: controllerInteraction + ) } else { message.removeSpeechToTextMeta(context: context) }