diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index c28ea26dda..02667eb150 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -8159,3 +8159,6 @@ Sorry for the inconvenience."; "ChatList.EmptyTopicsTitle" = "No topics here yet"; "ChatList.EmptyTopicsCreate" = "Create New Topic"; "ChatList.EmptyTopicsShowAsMessages" = "Show as Messages"; + +"Message.AudioTranscription.SubscribeToPremium" = "Subscribe to **Telegram Premium** to convert voice to text."; +"Message.AudioTranscription.SubscribeToPremiumAction" = "More"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 7d59817fbb..2adaa73533 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -792,6 +792,7 @@ public enum PremiumIntroSource { case deeplink(String?) case profile(PeerId) case emojiStatus(PeerId, Int64, TelegramMediaFile?, LoadedStickerPack?) + case voiceToText } #if ENABLE_WALLET diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 32dc9402b9..b2df2dc604 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -30,9 +30,10 @@ public final class ChatMessageItemAssociatedData: Equatable { public let defaultReaction: MessageReaction.Reaction? public let isPremium: Bool public let forceInlineReactions: Bool + public let alwaysDisplayTranscribeButton: Bool public let accountPeer: EnginePeer? - public init(automaticDownloadPeerType: MediaAutoDownloadPeerType, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, isRecentActions: Bool = false, subject: ChatControllerSubject? = nil, contactsPeerIds: Set = Set(), channelDiscussionGroup: ChannelDiscussionGroupStatus = .unknown, animatedEmojiStickers: [String: [StickerPackItem]] = [:], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]] = [:], forcedResourceStatus: FileMediaResourceStatus? = nil, currentlyPlayingMessageId: EngineMessage.Index? = nil, isCopyProtectionEnabled: Bool = false, availableReactions: AvailableReactions?, defaultReaction: MessageReaction.Reaction?, isPremium: Bool, accountPeer: EnginePeer?, forceInlineReactions: Bool = false) { + public init(automaticDownloadPeerType: MediaAutoDownloadPeerType, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, isRecentActions: Bool = false, subject: ChatControllerSubject? = nil, contactsPeerIds: Set = Set(), channelDiscussionGroup: ChannelDiscussionGroupStatus = .unknown, animatedEmojiStickers: [String: [StickerPackItem]] = [:], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]] = [:], forcedResourceStatus: FileMediaResourceStatus? = nil, currentlyPlayingMessageId: EngineMessage.Index? = nil, isCopyProtectionEnabled: Bool = false, availableReactions: AvailableReactions?, defaultReaction: MessageReaction.Reaction?, isPremium: Bool, accountPeer: EnginePeer?, forceInlineReactions: Bool = false, alwaysDisplayTranscribeButton: Bool = false) { self.automaticDownloadPeerType = automaticDownloadPeerType self.automaticDownloadNetworkType = automaticDownloadNetworkType self.isRecentActions = isRecentActions @@ -49,6 +50,7 @@ public final class ChatMessageItemAssociatedData: Equatable { self.isPremium = isPremium self.accountPeer = accountPeer self.forceInlineReactions = forceInlineReactions + self.alwaysDisplayTranscribeButton = alwaysDisplayTranscribeButton } public static func == (lhs: ChatMessageItemAssociatedData, rhs: ChatMessageItemAssociatedData) -> Bool { @@ -97,6 +99,9 @@ public final class ChatMessageItemAssociatedData: Equatable { if lhs.forceInlineReactions != rhs.forceInlineReactions { return false } + if lhs.alwaysDisplayTranscribeButton != rhs.alwaysDisplayTranscribeButton { + return false + } return true } } diff --git a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift index b69f9668c2..6bb01fc170 100644 --- a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift @@ -1265,7 +1265,7 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa case .privateChannel: let invite = (view.cachedData as? CachedGroupData)?.exportedInvitation entries.append(.privateLinkHeader(presentationData.theme, presentationData.strings.InviteLink_InviteLink.uppercased())) - entries.append(.privateLink(presentationData.theme, invite, importers?.importers.prefix(3).compactMap { $0.peer.peer.flatMap(EnginePeer.init) } ?? [], importers?.count ?? 0, !isInitialSetup)) + entries.append(.privateLink(presentationData.theme, invite, importers?.importers.prefix(3).compactMap { $0.peer.peer.flatMap(EnginePeer.init) } ?? [], importers?.count ?? 0, !isInitialSetup)) entries.append(.privateLinkInfo(presentationData.theme, presentationData.strings.Group_Username_CreatePrivateLinkHelp)) switch mode { case .initialSetup, .revokeNames: @@ -1413,6 +1413,9 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta let revokeAddressNameDisposable = MetaDisposable() actionsDisposable.add(revokeAddressNameDisposable) + let deactivateAllAddressNamesDisposable = MetaDisposable() + actionsDisposable.add(deactivateAllAddressNamesDisposable) + let revokeLinkDisposable = MetaDisposable() actionsDisposable.add(revokeLinkDisposable) @@ -1433,17 +1436,20 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta queue: Queue.mainQueue(), adminedPublicChannels.get() |> filter { $0 != nil } |> take(1), context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)), + context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)), context.engine.data.get( TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false), TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true) ) - ).start(next: { peers, accountPeer, data in + ).start(next: { peers, accountPeer, peer, data in let (limits, premiumLimits) = data let isPremium = accountPeer?.isPremium ?? false + let hasAdditionalUsernames = (peer?._asPeer().usernames.firstIndex(where: { !$0.flags.contains(.isEditable) }) ?? nil) != nil + if let peers = peers { let count = Int32(peers.count) - if count < limits.maxPublicLinksCount || (count < premiumLimits.maxPublicLinksCount && isPremium) { + if count < limits.maxPublicLinksCount || (count < premiumLimits.maxPublicLinksCount && isPremium) || hasAdditionalUsernames { updateState { state in return state.withUpdatedSelectedType(type) } @@ -1820,6 +1826,10 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta return state } + if let selectedType = state.selectedType, case .privateChannel = selectedType, peer.usernames.firstIndex(where: { $0.isActive && !$0.flags.contains(.isEditable) }) != nil { + deactivateAllAddressNamesDisposable.set(context.engine.peers.deactivateAllAddressNames(peerId: peerId).start()) + } + if let updatedCopyProtection = state.forwardingEnabled { toggleCopyProtectionDisposable.set(context.engine.peers.toggleMessageCopyProtection(peerId: peerId, enabled: !updatedCopyProtection).start()) } diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 907e9d6b15..7e1cf67650 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -153,6 +153,12 @@ public enum PremiumSource: Equatable { } else { return false } + case .voiceToText: + if case .voiceToText = rhs { + return true + } else { + return false + } } } @@ -177,6 +183,7 @@ public enum PremiumSource: Equatable { case emojiStatus(PeerId, Int64, TelegramMediaFile?, LoadedStickerPack?) case gift(from: PeerId, to: PeerId, duration: Int32) case giftTerms + case voiceToText var identifier: String? { switch self { @@ -216,6 +223,8 @@ public enum PremiumSource: Equatable { return "profile__\(id.id._internalGetInt64Value())" case .emojiStatus: return "emoji_status" + case .voiceToText: + return "voice_to_text" case .gift, .giftTerms: return nil case let .deeplink(reference): diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift index 2fb8ac007f..0bb6585333 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift @@ -185,7 +185,7 @@ func _internal_deactivateAllAddressNames(account: Account, peerId: EnginePeer.Id } |> mapToSignal { result -> Signal in return account.postbox.transaction { transaction -> Signal in - if case .boolTrue = result, let peer = transaction.getPeer(account.peerId) as? TelegramChannel { + if case .boolTrue = result, let peer = transaction.getPeer(peerId) as? TelegramChannel { var updatedNames: [TelegramPeerUsername] = [] for username in peer.usernames { var updatedFlags = username.flags diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index 33fc816bfa..7ccc7097a8 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -164,6 +164,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 { case sharedMediaFastScrollingTooltip = 30 case forcedPasswordSetup = 31 case emojiTooltip = 32 + case audioTranscriptionSuggestion = 33 var key: ValueBoxKey { let v = ValueBoxKey(length: 4) @@ -344,6 +345,10 @@ private struct ApplicationSpecificNoticeKeys { static func emojiTooltip() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.emojiTooltip.key) } + + static func audioTranscriptionSuggestion() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.audioTranscriptionSuggestion.key) + } } public struct ApplicationSpecificNotice { @@ -1140,6 +1145,36 @@ public struct ApplicationSpecificNotice { return engine.notices.set(id: ApplicationSpecificNoticeKeys.forcedPasswordSetup(), item: item) } + public static func audioTranscriptionSuggestionKey() -> NoticeEntryKey { + return ApplicationSpecificNoticeKeys.audioTranscriptionSuggestion() + } + + public static func getAudioTranscriptionSuggestion(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Int32 in + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.audioTranscriptionSuggestion())?.get(ApplicationSpecificCounterNotice.self) { + return value.value + } else { + return 0 + } + } + } + + public static func incrementAudioTranscriptionSuggestion(accountManager: AccountManager, count: Int = 1) -> Signal { + return accountManager.transaction { transaction -> Int in + var currentValue: Int32 = 0 + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.audioTranscriptionSuggestion())?.get(ApplicationSpecificCounterNotice.self) { + currentValue = value.value + } + let previousValue = currentValue + currentValue += Int32(count) + + if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) { + transaction.setNotice(ApplicationSpecificNoticeKeys.audioTranscriptionSuggestion(), entry) + } + + return Int(previousValue) + } + } public static func reset(accountManager: AccountManager) -> Signal { return accountManager.transaction { transaction -> Void in } diff --git a/submodules/TelegramUI/Resources/Animations/anim_voiceToText.json b/submodules/TelegramUI/Resources/Animations/anim_voiceToText.json new file mode 100644 index 0000000000..f1130e622d --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/anim_voiceToText.json @@ -0,0 +1 @@ +{"v":"5.9.6","fr":60,"ip":0,"op":120,"w":512,"h":512,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Artboard Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":8,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":9,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":10,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":11,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":12,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":13,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":14,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":15,"s":[0]},{"i":{"x":[0.833],"y":[0.479]},"o":{"x":[0.167],"y":[0]},"t":16,"s":[0]},{"i":{"x":[0.833],"y":[0.715]},"o":{"x":[0.167],"y":[0.083]},"t":17,"s":[0]},{"i":{"x":[0.833],"y":[0.865]},"o":{"x":[0.167],"y":[0.118]},"t":18,"s":[6.247]},{"i":{"x":[0.833],"y":[0.742]},"o":{"x":[0.167],"y":[0.219]},"t":19,"s":[21.333]},{"i":{"x":[0.833],"y":[0.839]},"o":{"x":[0.167],"y":[0.123]},"t":20,"s":[30.627]},{"i":{"x":[0.833],"y":[0.88]},"o":{"x":[0.167],"y":[0.172]},"t":21,"s":[50.16]},{"i":{"x":[0.833],"y":[0.781]},"o":{"x":[0.167],"y":[0.273]},"t":22,"s":[68.438]},{"i":{"x":[0.833],"y":[0.86]},"o":{"x":[0.167],"y":[0.134]},"t":23,"s":[76.448]},{"i":{"x":[0.833],"y":[0.889]},"o":{"x":[0.167],"y":[0.207]},"t":24,"s":[89.501]},{"i":{"x":[0.833],"y":[0.828]},"o":{"x":[0.167],"y":[0.339]},"t":25,"s":[98.316]},{"i":{"x":[0.833],"y":[0.911]},"o":{"x":[0.167],"y":[0.161]},"t":26,"s":[101.192]},{"i":{"x":[0.833],"y":[1.198]},"o":{"x":[0.167],"y":[1.343]},"t":27,"s":[104.264]},{"i":{"x":[0.833],"y":[0.627]},"o":{"x":[0.167],"y":[0.059]},"t":28,"s":[104.467]},{"i":{"x":[0.833],"y":[0.809]},"o":{"x":[0.167],"y":[0.107]},"t":29,"s":[103.781]},{"i":{"x":[0.833],"y":[0.873]},"o":{"x":[0.167],"y":[0.148]},"t":30,"s":[101.394]},{"i":{"x":[0.833],"y":[0.76]},"o":{"x":[0.167],"y":[0.244]},"t":31,"s":[98.311]},{"i":{"x":[0.833],"y":[0.847]},"o":{"x":[0.167],"y":[0.128]},"t":32,"s":[96.713]},{"i":{"x":[0.833],"y":[0.883]},"o":{"x":[0.167],"y":[0.184]},"t":33,"s":[93.707]},{"i":{"x":[0.833],"y":[0.793]},"o":{"x":[0.167],"y":[0.291]},"t":34,"s":[91.209]},{"i":{"x":[0.833],"y":[0.87]},"o":{"x":[0.167],"y":[0.14]},"t":35,"s":[90.206]},{"i":{"x":[0.833],"y":[0.897]},"o":{"x":[0.167],"y":[0.233]},"t":36,"s":[88.722]},{"i":{"x":[0.833],"y":[0.891]},"o":{"x":[0.167],"y":[0.428]},"t":37,"s":[87.893]},{"i":{"x":[0.833],"y":[1.284]},"o":{"x":[0.167],"y":[0.359]},"t":38,"s":[87.692]},{"i":{"x":[0.833],"y":[0.851]},"o":{"x":[0.167],"y":[0.064]},"t":39,"s":[87.632]},{"i":{"x":[0.833],"y":[0.72]},"o":{"x":[0.167],"y":[0.189]},"t":40,"s":[87.899]},{"i":{"x":[0.833],"y":[0.831]},"o":{"x":[0.167],"y":[0.119]},"t":41,"s":[88.111]},{"i":{"x":[0.833],"y":[0.878]},"o":{"x":[0.167],"y":[0.164]},"t":42,"s":[88.609]},{"i":{"x":[0.833],"y":[0.773]},"o":{"x":[0.167],"y":[0.262]},"t":43,"s":[89.124]},{"i":{"x":[0.833],"y":[0.855]},"o":{"x":[0.167],"y":[0.132]},"t":44,"s":[89.364]},{"i":{"x":[0.833],"y":[0.887]},"o":{"x":[0.167],"y":[0.196]},"t":45,"s":[89.778]},{"i":{"x":[0.833],"y":[0.81]},"o":{"x":[0.167],"y":[0.314]},"t":46,"s":[90.085]},{"i":{"x":[0.833],"y":[0.887]},"o":{"x":[0.167],"y":[0.148]},"t":47,"s":[90.195]},{"i":{"x":[0.833],"y":[0.919]},"o":{"x":[0.167],"y":[0.314]},"t":48,"s":[90.337]},{"i":{"x":[0.833],"y":[-1.909]},"o":{"x":[0.167],"y":[-3.478]},"t":49,"s":[90.389]},{"i":{"x":[0.833],"y":[0.768]},"o":{"x":[0.167],"y":[0.086]},"t":50,"s":[90.387]},{"i":{"x":[0.833],"y":[0.869]},"o":{"x":[0.167],"y":[0.13]},"t":51,"s":[90.347]},{"i":{"x":[0.833],"y":[0.748]},"o":{"x":[0.167],"y":[0.228]},"t":52,"s":[90.274]},{"i":{"x":[0.833],"y":[0.842]},"o":{"x":[0.167],"y":[0.125]},"t":53,"s":[90.233]},{"i":{"x":[0.833],"y":[0.881]},"o":{"x":[0.167],"y":[0.176]},"t":54,"s":[90.148]},{"i":{"x":[0.833],"y":[0.785]},"o":{"x":[0.167],"y":[0.279]},"t":55,"s":[90.073]},{"i":{"x":[0.833],"y":[0.863]},"o":{"x":[0.167],"y":[0.136]},"t":56,"s":[90.04]},{"i":{"x":[0.833],"y":[0.891]},"o":{"x":[0.167],"y":[0.213]},"t":57,"s":[89.989]},{"i":{"x":[0.833],"y":[0.84]},"o":{"x":[0.167],"y":[0.356]},"t":58,"s":[89.956]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.174]},"t":59,"s":[89.946]},{"t":60,"s":[89.937]}],"ix":10},"p":{"a":0,"k":[256,256,0],"ix":2,"l":2},"a":{"a":0,"k":[150,150,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[-0.154,-0.154,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.633,0.633,0]},"t":8,"s":[136.531,136.531,100]},{"i":{"x":[0.833,0.833,0.833],"y":[-0.395,-0.395,1]},"o":{"x":[0.167,0.167,0.167],"y":[1.154,1.154,0]},"t":9,"s":[136.531,136.531,100]},{"i":{"x":[0.833,0.833,0.833],"y":[-0.828,-0.828,1]},"o":{"x":[0.167,0.167,0.167],"y":[1.395,1.395,0]},"t":10,"s":[136.531,136.531,100]},{"i":{"x":[0.833,0.833,0.833],"y":[-1.604,-1.604,1]},"o":{"x":[0.167,0.167,0.167],"y":[1.828,1.828,0]},"t":11,"s":[136.531,136.531,100]},{"i":{"x":[0.833,0.833,0.833],"y":[-1.571,-1.571,1]},"o":{"x":[0.167,0.167,0.167],"y":[2.604,2.604,0]},"t":12,"s":[136.531,136.531,100]},{"i":{"x":[0.833,0.833,0.833],"y":[-0.785,-0.785,1]},"o":{"x":[0.167,0.167,0.167],"y":[2.571,2.571,0]},"t":13,"s":[136.531,136.531,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.708,0.708,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.362,0.362,0]},"t":14,"s":[136.531,136.531,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.885,0.885,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.134,0.134,0]},"t":15,"s":[141.464,141.464,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.809,0.809,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.306,0.306,0]},"t":16,"s":[152.189,152.189,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.891,0.891,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.148,0.148,0]},"t":17,"s":[156.208,156.208,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.939,0.939,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.357,0.357,0]},"t":18,"s":[161.42,161.42,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.357,0.357,1]},"o":{"x":[0.167,0.167,0.167],"y":[-0.228,-0.228,0]},"t":19,"s":[163.007,163.007,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.776,0.776,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.096,0.096,0]},"t":20,"s":[162.582,162.582,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.867,0.867,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.133,0.133,0]},"t":21,"s":[159.727,159.727,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.739,0.739,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.223,0.223,0]},"t":22,"s":[154.892,154.892,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.835,0.835,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.122,0.122,0]},"t":23,"s":[152.008,152.008,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.878,0.878,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.168,0.168,0]},"t":24,"s":[145.843,145.843,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.774,0.774,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.265,0.265,0]},"t":25,"s":[139.793,139.793,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.857,0.857,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.132,0.132,0]},"t":26,"s":[137.015,137.015,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.888,0.888,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.199,0.199,0]},"t":27,"s":[132.26,132.26,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.827,0.827,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.327,0.327,0]},"t":28,"s":[128.827,128.827,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.92,0.92,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.161,0.161,0]},"t":29,"s":[127.655,127.655,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.264,0.264,1]},"o":{"x":[0.167,0.167,0.167],"y":[-1.748,-1.748,0]},"t":30,"s":[126.391,126.391,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.629,0.629,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.094,0.094,0]},"t":31,"s":[126.449,126.449,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.802,0.802,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.107,0.107,0]},"t":32,"s":[126.899,126.899,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.871,0.871,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.144,0.144,0]},"t":33,"s":[128.454,128.454,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.748,0.748,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.234,0.234,0]},"t":34,"s":[130.597,130.597,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.84,0.84,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.124,0.124,0]},"t":35,"s":[131.782,131.782,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.88,0.88,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.174,0.174,0]},"t":36,"s":[134.184,134.184,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.781,0.781,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.273,0.273,0]},"t":37,"s":[136.404,136.404,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.862,0.862,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.135,0.135,0]},"t":38,"s":[137.378,137.378,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.892,0.892,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.211,0.211,0]},"t":39,"s":[138.962,138.962,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.859,0.859,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.366,0.366,0]},"t":40,"s":[139.998,139.998,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1.018,1.018,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.205,0.205,0]},"t":41,"s":[140.303,140.303,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.829,0.829,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.015,0.015,0]},"t":42,"s":[140.514,140.514,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.688,0.688,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.163,0.163,0]},"t":43,"s":[140.257,140.257,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.816,0.816,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.114,0.114,0]},"t":44,"s":[139.988,139.988,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.873,0.873,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.152,0.152,0]},"t":45,"s":[139.248,139.248,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.756,0.756,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.243,0.243,0]},"t":46,"s":[138.35,138.35,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.844,0.844,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.126,0.126,0]},"t":47,"s":[137.882,137.882,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.882,0.882,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.179,0.179,0]},"t":48,"s":[136.976,136.976,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.789,0.789,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.283,0.283,0]},"t":49,"s":[136.186,136.186,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.869,0.869,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.138,0.138,0]},"t":50,"s":[135.857,135.857,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.898,0.898,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.23,0.23,0]},"t":51,"s":[135.352,135.352,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.942,0.942,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.463,0.463,0]},"t":52,"s":[135.066,135.066,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.124,0.124,1]},"o":{"x":[0.167,0.167,0.167],"y":[-0.196,-0.196,0]},"t":53,"s":[135.003,135.003,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.854,0.854,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.092,0.092,0]},"t":54,"s":[135.022,135.022,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.714,0.714,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.194,0.194,0]},"t":55,"s":[135.2,135.2,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.824,0.824,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.118,0.118,0]},"t":56,"s":[135.334,135.334,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.875,0.875,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.159,0.159,0]},"t":57,"s":[135.66,135.66,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.762,0.762,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.251,0.251,0]},"t":58,"s":[136.021,136.021,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.128,0.128,0]},"t":59,"s":[136.2,136.2,100]},{"t":60,"s":[136.531,136.531,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":19,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[150,80],[150,150]],"c":false}]},{"t":44,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[170,50],[170,120]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":19,"s":[{"i":[[0,0],[0,0],[0,-10],[0,0]],"o":[[0,0],[0,10],[0,0],[0,0]],"v":[[150,80],[150,100],[150,130],[150,150]],"c":false}]},{"t":44,"s":[{"i":[[0,0],[0,0],[-9.267,-4.333],[0,0]],"o":[[0,0],[-9.267,4.333],[0,0],[0,0]],"v":[[200,35],[94.267,75.667],[94.267,94.333],[200,135]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":19,"s":[100]},{"t":33,"s":[20]}],"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":19,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[150,270],[150,230]],"c":false}]},{"t":44,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[150,270],[150,185]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":19,"s":[{"i":[[0,0],[-44.183,0],[0,44.183]],"o":[[0,44.183],[44.183,0],[0,0]],"v":[[-80,-40],[0,40],[80,-40]],"c":false}]},{"t":36,"s":[{"i":[[0,0],[0,0],[-10,-10]],"o":[[10,-10],[0,0],[0,0]],"v":[[-50,30],[0,-15],[50,30]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[150,190],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":123,"st":0,"ct":1,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 61596d917d..5d1010308e 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -20,6 +20,7 @@ import ChatListUI import ComponentFlow import ReactionSelectionNode import ChatPresentationInterfaceState +import TelegramNotices extension ChatReplyThreadMessage { var effectiveTopId: MessageId { @@ -314,7 +315,7 @@ private final class ChatHistoryTransactionOpaqueState { } } -private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHistoryView, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, animatedEmojiStickers: [String: [StickerPackItem]], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]], subject: ChatControllerSubject?, currentlyPlayingMessageId: MessageIndex?, isCopyProtectionEnabled: Bool, availableReactions: AvailableReactions?, defaultReaction: MessageReaction.Reaction?, isPremium: Bool, accountPeer: EnginePeer?) -> ChatMessageItemAssociatedData { +private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHistoryView, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, animatedEmojiStickers: [String: [StickerPackItem]], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]], subject: ChatControllerSubject?, currentlyPlayingMessageId: MessageIndex?, isCopyProtectionEnabled: Bool, availableReactions: AvailableReactions?, defaultReaction: MessageReaction.Reaction?, isPremium: Bool, alwaysDisplayTranscribeButton: Bool, accountPeer: EnginePeer?) -> ChatMessageItemAssociatedData { var automaticMediaDownloadPeerType: MediaAutoDownloadPeerType = .channel var contactsPeerIds: Set = Set() var channelDiscussionGroup: ChatMessageItemAssociatedData.ChannelDiscussionGroupStatus = .unknown @@ -363,7 +364,7 @@ private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHist } } - return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, 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) + return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, 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) } private extension ChatHistoryLocationInput { @@ -1032,26 +1033,37 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } |> distinctUntilChanged + let suggestAudioTranscription = context.engine.data.get(TelegramEngine.EngineData.Item.Notices.Notice(key: ApplicationSpecificNotice.audioTranscriptionSuggestionKey())) + |> map { entry -> Int32 in + return entry?.get(ApplicationSpecificCounterNotice.self)?.value ?? 0 + } + + let promises = combineLatest( + self.historyAppearsClearedPromise.get(), + self.pendingUnpinnedAllMessagesPromise.get(), + self.pendingRemovedMessagesPromise.get(), + self.currentlyPlayingMessageIdPromise.get(), + self.scrollToMessageIdPromise.get() + ) + let historyViewTransitionDisposable = combineLatest(queue: messageViewQueue, historyViewUpdate, self.chatPresentationDataPromise.get(), selectedMessages, updatingMedia, automaticDownloadNetworkType, - self.historyAppearsClearedPromise.get(), - self.pendingUnpinnedAllMessagesPromise.get(), - self.pendingRemovedMessagesPromise.get(), animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, - self.currentlyPlayingMessageIdPromise.get(), - self.scrollToMessageIdPromise.get(), adMessages, availableReactions, defaultReaction, - accountPeer - ).start(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, currentlyPlayingMessageIdAndType, scrollToMessageId, adMessages, availableReactions, defaultReaction, accountPeer in + accountPeer, + suggestAudioTranscription, + promises + ).start(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, adMessages, availableReactions, defaultReaction, accountPeer, suggestAudioTranscription, promises in + let (historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, currentlyPlayingMessageIdAndType, scrollToMessageId) = promises let currentlyPlayingMessageId = currentlyPlayingMessageIdAndType?.0 func applyHole() { @@ -1185,7 +1197,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { isPremium = true } - let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer) + let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, defaultReaction: defaultReaction, isPremium: isPremium, alwaysDisplayTranscribeButton: suggestAudioTranscription < 2, accountPeer: accountPeer) let filteredEntries = chatHistoryEntriesForView( location: chatLocation, diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift index 5d3e366341..1ae2cce842 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift @@ -26,6 +26,8 @@ import ConvertOpusToAAC import LocalAudioTranscription import TextSelectionNode import AudioTranscriptionPendingIndicatorComponent +import UndoUI +import TelegramNotices private struct FetchControls { let fetch: (Bool) -> Void @@ -348,7 +350,21 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } private func transcribe() { - guard let context = self.context, let message = self.message, let presentationData = self.presentationData else { + guard let arguments = self.arguments, let context = self.context, let message = self.message, let presentationData = self.presentationData else { + return + } + + guard arguments.associatedData.isPremium else { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + 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), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { action in + if case .undo = action { + let introController = context.sharedContext.makePremiumIntroController(context: context, source: .settings) + arguments.controllerInteraction.navigationController()?.pushViewController(introController, animated: true) + + ApplicationSpecificNotice.incrementAudioTranscriptionSuggestion(accountManager: context.sharedContext.accountManager).start() + } + return false }) + arguments.controllerInteraction.presentControllerInCurrent(tipController, nil) return } @@ -550,7 +566,18 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { var isVoice = false var audioDuration: Int32 = 0 - let canTranscribe = arguments.associatedData.isPremium && arguments.message.id.peerId.namespace != Namespaces.Peer.SecretChat + let displayTranscribe: Bool + if arguments.message.id.peerId.namespace != Namespaces.Peer.SecretChat { + if arguments.associatedData.isPremium { + displayTranscribe = true + } else if arguments.associatedData.alwaysDisplayTranscribeButton { + displayTranscribe = true + } else { + displayTranscribe = false + } + } else { + displayTranscribe = false + } let messageTheme = arguments.incoming ? arguments.presentationData.theme.theme.chat.message.incoming : arguments.presentationData.theme.theme.chat.message.outgoing @@ -805,7 +832,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { let calcDuration = max(minVoiceLength, min(maxVoiceLength, CGFloat(audioDuration))) minLayoutWidth = minVoiceWidth + (maxVoiceWidth - minVoiceWidth) * (calcDuration - minVoiceLength) / (maxVoiceLength - minVoiceLength) - if canTranscribe { + if displayTranscribe { minLayoutWidth += 30.0 + 8.0 } minLayoutWidth = max(descriptionAndStatusWidth + 56, minLayoutWidth) @@ -1093,7 +1120,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { if isVoice { var scrubbingFrame = CGRect(origin: CGPoint(x: 57.0, y: 1.0), size: CGSize(width: boundingWidth - 60.0, height: 18.0)) - if canTranscribe { + if displayTranscribe { scrubbingFrame.size.width -= 30.0 + 4.0 } @@ -1152,7 +1179,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { animation.animator.updateFrame(layer: waveformView.layer, frame: scrubbingFrame, completion: nil) animation.animator.updateFrame(layer: waveformView.componentView!.layer, frame: CGRect(origin: CGPoint(), size: scrubbingFrame.size), completion: nil) - if canTranscribe { + if displayTranscribe { let audioTranscriptionButton: ComponentHostView if let current = strongSelf.audioTranscriptionButton { audioTranscriptionButton = current diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift index e52feba0ba..8b96cc9d98 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -15,6 +15,8 @@ import FileMediaResourceStatus import HierarchyTrackingLayer import ComponentFlow import AudioTranscriptionButtonComponent +import UndoUI +import TelegramNotices struct ChatMessageInstantVideoItemLayoutResult { let contentSize: CGSize @@ -584,8 +586,20 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { let incoming = item.message.effectivelyIncoming(item.context.account.peerId) - var canTranscribe = statusDisplayType == .free && item.associatedData.isPremium && item.message.id.peerId.namespace != Namespaces.Peer.SecretChat - if canTranscribe, let durationBlurColor = durationBlurColor { + var displayTranscribe: Bool + if item.message.id.peerId.namespace != Namespaces.Peer.SecretChat && statusDisplayType == .free { + if item.associatedData.isPremium { + displayTranscribe = true + } else if item.associatedData.alwaysDisplayTranscribeButton { + displayTranscribe = true + } else { + displayTranscribe = false + } + } else { + displayTranscribe = false + } + + if displayTranscribe, let durationBlurColor = durationBlurColor { let audioTranscriptionButton: ComponentHostView if let current = strongSelf.audioTranscriptionButton { audioTranscriptionButton = current @@ -627,7 +641,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { animation.animator.updateAlpha(layer: audioTranscriptionButton.layer, alpha: scaleProgress.isZero ? 1.0 : 0.0, completion: nil) if !scaleProgress.isZero { - canTranscribe = false + displayTranscribe = false } } else { if let audioTranscriptionButton = strongSelf.audioTranscriptionButton { @@ -644,7 +658,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { if let durationBackgroundNode = strongSelf.durationBackgroundNode, size.width > 1.0 { durationBackgroundNode.update(size: size, cornerRadius: size.height / 2.0, transition: .immediate) - if !incoming, let audioTranscriptionButton = strongSelf.audioTranscriptionButton, canTranscribe { + if !incoming, let audioTranscriptionButton = strongSelf.audioTranscriptionButton, displayTranscribe { durationFrame.origin.x = audioTranscriptionButton.frame.minX - 7.0 } animation.animator.updateFrame(layer: durationNode.layer, frame: durationFrame, completion: nil) @@ -662,14 +676,14 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { dateAndStatusOrigin = CGPoint(x: displayVideoFrame.minX - 4.0, y: displayVideoFrame.maxY + 2.0) } else { dateAndStatusOrigin = CGPoint(x: min(floorToScreenPixels(displayVideoFrame.midX) + 55.0 + 25.0 * scaleProgress, width - dateAndStatusSize.width - 4.0), y: displayVideoFrame.height - dateAndStatusSize.height) - if !incoming, let audioTranscriptionButton = strongSelf.audioTranscriptionButton, canTranscribe { + if !incoming, let audioTranscriptionButton = strongSelf.audioTranscriptionButton, displayTranscribe { dateAndStatusOrigin.x = audioTranscriptionButton.frame.maxX + 7.0 } } animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: CGRect(origin: dateAndStatusOrigin, size: dateAndStatusSize), completion: nil) case let .constrained(_, right): var dateAndStatusFrame = CGRect(origin: CGPoint(x: min(floorToScreenPixels(displayVideoFrame.midX) + 55.0 + 25.0 * scaleProgress, displayVideoFrame.maxX + right - dateAndStatusSize.width - 4.0), y: displayVideoFrame.maxY - dateAndStatusSize.height), size: dateAndStatusSize) - if incoming, let audioTranscriptionButton = strongSelf.audioTranscriptionButton, canTranscribe { + if incoming, let audioTranscriptionButton = strongSelf.audioTranscriptionButton, displayTranscribe { dateAndStatusFrame.origin.x = audioTranscriptionButton.frame.maxX + 7.0 } animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: dateAndStatusFrame, completion: nil) @@ -1164,7 +1178,21 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } private func transcribe() { - guard let context = self.item?.context, let message = self.item?.message, self.statusNode == nil else { + guard let item = self.item, self.statusNode == nil else { + return + } + + guard item.associatedData.isPremium else { + let presentationData = item.context.sharedContext.currentPresentationData.with { $0 } + 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), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { action in + if case .undo = action { + let introController = item.context.sharedContext.makePremiumIntroController(context: item.context, source: .settings) + item.controllerInteraction.navigationController()?.pushViewController(introController, animated: true) + + ApplicationSpecificNotice.incrementAudioTranscriptionSuggestion(accountManager: context.sharedContext.accountManager).start() + } + return false }) + item.controllerInteraction.presentControllerInCurrent(tipController, nil) return } @@ -1174,7 +1202,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { if case .expanded = self.audioTranscriptionState { shouldExpandNow = true } else { - if let result = transcribedText(message: message) { + if let result = transcribedText(message: item.message) { shouldExpandNow = true if case let .success(_, isPending) = result { @@ -1192,7 +1220,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { self.audioTranscriptionState = .inProgress self.requestUpdateLayout(true) - self.transcribeDisposable = (context.engine.messages.transcribeAudio(messageId: message.id) + self.transcribeDisposable = (item.context.engine.messages.transcribeAudio(messageId: item.message.id) |> deliverOnMainQueue).start(next: { [weak self] result in guard let strongSelf = self else { return diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 286b1f878f..973f2f5ff6 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1520,6 +1520,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { mappedSource = .profile(peerId) case let .emojiStatus(peerId, fileId, file, packTitle): mappedSource = .emojiStatus(peerId, fileId, file, packTitle) + case .voiceToText: + mappedSource = .voiceToText } return PremiumIntroScreen(context: context, source: mappedSource) }