diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 89de56c420..3949543938 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -523,6 +523,23 @@ private final class CachedChatListSearchResult { } } +private final class CachedCustomTextEntities { + let text: String + let textEntities: [MessageTextEntity] + + init(text: String, textEntities: [MessageTextEntity]) { + self.text = text + self.textEntities = textEntities + } + + func matches(text: String) -> Bool { + if self.text != text { + return false + } + return true + } +} + private let playIconImage = UIImage(bundleImageName: "Chat List/MiniThumbnailPlay")?.precomposed() private final class ChatListMediaPreviewNode: ASDisplayNode { @@ -611,6 +628,8 @@ private final class ChatListMediaPreviewNode: ASDisplayNode { } } +private let loginCodeRegex = try? NSRegularExpression(pattern: "[\\d\\-]{5,7}", options: []) + class ChatListItemNode: ItemListRevealOptionsItemNode { final class TopicItemNode: ASDisplayNode { let topicTitleNode: TextNode @@ -924,6 +943,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { private var cachedChatListText: (String, String)? private var cachedChatListSearchResult: CachedChatListSearchResult? + private var cachedCustomTextEntities: CachedCustomTextEntities? var layoutParams: (ChatListItem, first: Bool, last: Bool, firstWithHeader: Bool, nextIsPinned: Bool, ListViewItemLayoutParams, countersSize: CGFloat)? @@ -1492,6 +1512,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let currentItem = self.layoutParams?.0 let currentChatListText = self.cachedChatListText let currentChatListSearchResult = self.cachedChatListSearchResult + let currentCustomTextEntities = self.cachedCustomTextEntities return { item, params, first, last, firstWithHeader, nextIsPinned in let titleFont = Font.medium(floor(item.presentationData.fontSize.itemListBaseFontSize * 16.0 / 17.0)) @@ -1769,6 +1790,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var chatListText: (String, String)? var chatListSearchResult: CachedChatListSearchResult? + var customTextEntities: CachedCustomTextEntities? let contentImageSide: CGFloat = max(10.0, min(20.0, floor(item.presentationData.fontSize.baseDisplaySize * 18.0 / 17.0))) let contentImageSize = CGSize(width: contentImageSide, height: contentImageSide) @@ -1841,7 +1863,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { authorAttributedString = NSAttributedString(string: peerText, font: textFont, textColor: theme.authorNameColor) } - let entities = (message._asMessage().textEntitiesAttribute?.entities ?? []).filter { entity in + var entities = (message._asMessage().textEntitiesAttribute?.entities ?? []).filter { entity in switch entity.type { case .Spoiler, .CustomEmoji: return true @@ -1851,6 +1873,23 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { return false } } + + if message.id.peerId.namespace == Namespaces.Peer.CloudUser && message.id.peerId.id._internalGetInt64Value() == 777000 { + if let cached = currentCustomTextEntities, cached.matches(text: message.text) { + customTextEntities = cached + } else if let matches = loginCodeRegex?.matches(in: message.text, options: [], range: NSMakeRange(0, (message.text as NSString).length)) { + var entities: [MessageTextEntity] = [] + if let first = matches.first { + entities.append(MessageTextEntity(range: first.range.location ..< first.range.location + first.range.length, type: .Spoiler)) + } + customTextEntities = CachedCustomTextEntities(text: message.text, textEntities: entities) + } + } + + if let customTextEntities, !customTextEntities.textEntities.isEmpty { + entities.append(contentsOf: customTextEntities.textEntities) + } + let messageString: NSAttributedString if !message.text.isEmpty && entities.count > 0 { var messageText = message.text @@ -2560,6 +2599,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.currentItemHeight = itemHeight strongSelf.cachedChatListText = chatListText strongSelf.cachedChatListSearchResult = chatListSearchResult + strongSelf.cachedCustomTextEntities = customTextEntities strongSelf.onlineIsVoiceChat = onlineIsVoiceChat var animateOnline = animateOnline diff --git a/submodules/CheckNode/Sources/CheckNode.swift b/submodules/CheckNode/Sources/CheckNode.swift index f8aaab7cf0..607158d5c1 100644 --- a/submodules/CheckNode/Sources/CheckNode.swift +++ b/submodules/CheckNode/Sources/CheckNode.swift @@ -476,9 +476,19 @@ public class CheckLayer: CALayer { context.strokePath() case let .counter(number): - let text = NSAttributedString(string: "\(number)", font: Font.with(size: 16.0, design: .round, weight: .regular, traits: []), textColor: parameters.theme.strokeColor.withMultipliedAlpha(parameters.animationProgress)) + let fontSize: CGFloat + let string = "\(number)" + switch string.count { + case 1: + fontSize = 16.0 + case 2: + fontSize = 15.0 + default: + fontSize = 13.0 + } + let text = NSAttributedString(string: string, font: Font.with(size: fontSize, design: .round, weight: .medium, traits: []), textColor: parameters.theme.strokeColor.withMultipliedAlpha(parameters.animationProgress)) let textRect = text.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: [.usesLineFragmentOrigin], context: nil) - text.draw(at: CGPoint(x: UIScreenPixel + textRect.minX + floor((size.width - textRect.width) * 0.5), y: textRect.minY + floor((size.height - textRect.height) * 0.5))) + text.draw(at: CGPoint(x: textRect.minX + floorToScreenPixels((size.width - textRect.width) * 0.5), y: textRect.minY + floorToScreenPixels((size.height - textRect.height) * 0.5))) } } } diff --git a/submodules/LocalMediaResources/Sources/FetchPhotoLibraryImageResource.swift b/submodules/LocalMediaResources/Sources/FetchPhotoLibraryImageResource.swift index e623472588..f2b7aa5631 100644 --- a/submodules/LocalMediaResources/Sources/FetchPhotoLibraryImageResource.swift +++ b/submodules/LocalMediaResources/Sources/FetchPhotoLibraryImageResource.swift @@ -82,76 +82,74 @@ extension UIImage.Orientation { } } +private let fetchPhotoWorkers = ThreadPool(threadCount: 3, threadPriority: 0.2) + public func fetchPhotoLibraryResource(localIdentifier: String) -> Signal { return Signal { subscriber in + let queue = ThreadPoolQueue(threadPool: fetchPhotoWorkers) + let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [localIdentifier], options: nil) let requestId = Atomic(value: RequestId()) if fetchResult.count != 0 { let asset = fetchResult.object(at: 0) let option = PHImageRequestOptions() - option.deliveryMode = .opportunistic + option.deliveryMode = .highQualityFormat option.isNetworkAccessAllowed = true option.isSynchronous = false - let madeProgress = Atomic(value: false) - option.progressHandler = { progress, error, _, _ in - if !madeProgress.swap(true) { - //subscriber.putNext(.reset) - } - } + let size = CGSize(width: 1280.0, height: 1280.0) - let startTime = CACurrentMediaTime() - - let requestIdValue = PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .aspectFit, options: option, resultHandler: { (image, info) -> Void in - Queue.concurrentDefaultQueue().async { - requestId.with { current -> Void in - if !current.invalidated { - current.id = nil - current.invalidated = true + queue.addTask(ThreadPoolTask({ _ in + let startTime = CACurrentMediaTime() + + let semaphore = DispatchSemaphore(value: 0) + let requestIdValue = PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .aspectFit, options: option, resultHandler: { (image, info) -> Void in + Queue.concurrentDefaultQueue().async { + requestId.with { current -> Void in + if !current.invalidated { + current.id = nil + current.invalidated = true + } } - } - if let image = image { - if let info = info, let degraded = info[PHImageResultIsDegradedKey], (degraded as AnyObject).boolValue!{ - if !madeProgress.swap(true) { - //subscriber.putNext(.reset) + if let image = image { + if let info = info, let degraded = info[PHImageResultIsDegradedKey], (degraded as AnyObject).boolValue!{ + + } else { +#if DEBUG + print("load completion \((CACurrentMediaTime() - startTime) * 1000.0) ms") +#endif + + let scale = min(1.0, min(size.width / max(1.0, image.size.width), size.height / max(1.0, image.size.height))) + let scaledSize = CGSize(width: floor(image.size.width * scale), height: floor(image.size.height * scale)) + let scaledImage = resizedImage(image, for: scaledSize) + +#if DEBUG + print("scaled completion \((CACurrentMediaTime() - startTime) * 1000.0) ms") +#endif + + if let scaledImage = scaledImage, let data = compressImageToJPEG(scaledImage, quality: 0.6) { +#if DEBUG + print("compression completion \((CACurrentMediaTime() - startTime) * 1000.0) ms") +#endif + subscriber.putNext(.dataPart(resourceOffset: 0, data: data, range: 0 ..< Int64(data.count), complete: true)) + subscriber.putCompletion() + } else { + subscriber.putCompletion() + } + semaphore.signal() } } else { - #if DEBUG - print("load completion \((CACurrentMediaTime() - startTime) * 1000.0) ms") - #endif - - _ = madeProgress.swap(true) - - let scale = min(1.0, min(size.width / max(1.0, image.size.width), size.height / max(1.0, image.size.height))) - let scaledSize = CGSize(width: floor(image.size.width * scale), height: floor(image.size.height * scale)) - let scaledImage = resizedImage(image, for: scaledSize) - - #if DEBUG - print("scaled completion \((CACurrentMediaTime() - startTime) * 1000.0) ms") - #endif - - if let scaledImage = scaledImage, let data = compressImageToJPEG(scaledImage, quality: 0.6) { - #if DEBUG - print("compression completion \((CACurrentMediaTime() - startTime) * 1000.0) ms") - #endif - subscriber.putNext(.dataPart(resourceOffset: 0, data: data, range: 0 ..< Int64(data.count), complete: true)) - subscriber.putCompletion() - } else { - subscriber.putCompletion() - } - } - } else { - if !madeProgress.swap(true) { - //subscriber.putNext(.reset) + semaphore.signal() } } + }) + requestId.with { current -> Void in + if !current.invalidated { + current.id = requestIdValue + } } - }) - requestId.with { current -> Void in - if !current.invalidated { - current.id = requestIdValue - } - } + semaphore.wait() + })) } else { subscriber.putNext(.reset) } diff --git a/submodules/MediaPickerUI/Sources/FetchAssets.swift b/submodules/MediaPickerUI/Sources/FetchAssets.swift index cacc356fcb..1d99afd22f 100644 --- a/submodules/MediaPickerUI/Sources/FetchAssets.swift +++ b/submodules/MediaPickerUI/Sources/FetchAssets.swift @@ -25,6 +25,7 @@ func assetImage(asset: PHAsset, targetSize: CGSize, exact: Bool, deliveryMode: P options.resizeMode = .exact } options.isSynchronous = synchronous + options.isNetworkAccessAllowed = true let token = imageManager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: options) { (image, info) in var degraded = false diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 88c3d66f36..4bb1cadaf9 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -1452,13 +1452,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } } - selectionState.setItem(item, selected: value) + let success = selectionState.setItem(item, selected: value) if showUndo { self.showSelectionUndo(item: item) } - return true + return success } else { return false } diff --git a/submodules/QrCodeUI/Sources/QrCodeScreen.swift b/submodules/QrCodeUI/Sources/QrCodeScreen.swift index 08dff93dac..c0853f568e 100644 --- a/submodules/QrCodeUI/Sources/QrCodeScreen.swift +++ b/submodules/QrCodeUI/Sources/QrCodeScreen.swift @@ -47,7 +47,7 @@ public final class QrCodeScreen: ViewController { case let .invite(invite, _): return invite.link ?? "" case let .chatFolder(slug): - return "https://t.me/list/\(slug)" + return slug } } diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index ce0960de10..1c7581b5ea 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -95,6 +95,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, for media in message.media { if let action = media as? TelegramMediaAction { let authorName = message.author?.displayTitle(strings: strings, displayOrder: nameDisplayOrder) ?? "" + let compactAuthorName = message.author?.compactDisplayTitle ?? "" var isChannel = false if message.id.peerId.namespace == Namespaces.Peer.CloudChannel, let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { @@ -695,7 +696,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributedString = NSAttributedString(string: strings.Notification_YouDisabledTheme, font: titleFont, textColor: primaryTextColor) } else { let attributePeerIds: [(Int, EnginePeer.Id?)] = [(0, message.author?.id)] - let resultTitleString = strings.Notification_DisabledTheme(authorName) + let resultTitleString = strings.Notification_DisabledTheme(compactAuthorName) attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: attributePeerIds)) } } else { @@ -704,7 +705,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, } else if message.author?.id == accountPeerId { attributedString = NSAttributedString(string: strings.Notification_YouChangedTheme(emoji).string, font: titleFont, textColor: primaryTextColor) } else { - let resultTitleString = strings.Notification_ChangedTheme(authorName, emoji) + let resultTitleString = strings.Notification_ChangedTheme(compactAuthorName, emoji) attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) } } @@ -717,7 +718,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, } else { var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)]) attributes[1] = boldAttributes - attributedString = addAttributesToStringWithRanges(strings.Notification_PremiumGift_Sent(authorName, price)._tuple, body: bodyAttributes, argumentAttributes: attributes) + attributedString = addAttributesToStringWithRanges(strings.Notification_PremiumGift_Sent(compactAuthorName, price)._tuple, body: bodyAttributes, argumentAttributes: attributes) } case let .topicCreated(title, iconColor, iconFileId): if forForumOverview { @@ -877,14 +878,14 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, if message.author?.id == accountPeerId { attributedString = NSAttributedString(string: strings.Notification_YouChangedWallpaper, font: titleFont, textColor: primaryTextColor) } else { - let resultTitleString = strings.Notification_ChangedWallpaper(authorName) + let resultTitleString = strings.Notification_ChangedWallpaper(compactAuthorName) attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) } case .setSameChatWallpaper: if message.author?.id == accountPeerId { attributedString = NSAttributedString(string: strings.Notification_YouChangedToSameWallpaper, font: titleFont, textColor: primaryTextColor) } else { - let resultTitleString = strings.Notification_ChangedToSameWallpaper(authorName) + let resultTitleString = strings.Notification_ChangedToSameWallpaper(compactAuthorName) attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) } case .unknown: diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index dc0b4d56cc..5f35e2b080 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -705,10 +705,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } isLocation = true } - if let file = media as? TelegramMediaFile, file.isInstantVideo { - if strongSelf.chatDisplayNode.isInputViewFocused { - strongSelf.returnInputViewFocus = true - strongSelf.chatDisplayNode.dismissInput() + if let file = media as? TelegramMediaFile { + if file.isInstantVideo { + if strongSelf.chatDisplayNode.isInputViewFocused { + strongSelf.returnInputViewFocus = true + strongSelf.chatDisplayNode.dismissInput() + } + } + if file.isMusic || file.isVoice || file.isInstantVideo { + if !displayVoiceMessageDiscardAlert() { + return false + } } } if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia { @@ -4294,7 +4301,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - let _ = (ApplicationSpecificNotice.getBotGameNotice(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peer.id) + var botPeer = EnginePeer(peer) + if case let .inline(bot) = source { + botPeer = bot + } + let _ = (ApplicationSpecificNotice.getBotGameNotice(accountManager: strongSelf.context.sharedContext.accountManager, peerId: botPeer.id) |> deliverOnMainQueue).start(next: { value in guard let strongSelf = self else { return @@ -4303,8 +4314,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if value { openWebView() } else { - let controller = webAppLaunchConfirmationController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: EnginePeer(peer), commit: { - let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peer.id).start() + let controller = webAppLaunchConfirmationController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: botPeer, commit: { + let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: strongSelf.context.sharedContext.accountManager, peerId: botPeer.id).start() openWebView() }, showMore: nil) strongSelf.present(controller, in: .window(.root)) @@ -12857,97 +12868,114 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.presentAttachmentMenu(subject: .bot(id: botId, payload: payload, justInstalled: justInstalled)) } - public func presentBotApp(botApp: BotApp, botPeer: EnginePeer, payload: String?) { + public func presentBotApp(botApp: BotApp, botPeer: EnginePeer, payload: String?, concealed: Bool = false) { guard let peerId = self.chatLocation.peerId else { return } self.attachmentController?.dismiss(animated: true, completion: nil) - self.updateChatPresentationInterfaceState(animated: true, interactive: true, { - return $0.updatedTitlePanelContext { - if !$0.contains(where: { - switch $0 { + let openBotApp = { [weak self] allowWrite in + guard let strongSelf = self else { + return + } + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedTitlePanelContext { + if !$0.contains(where: { + switch $0 { case .requestInProgress: return true default: return false + } + }) { + var updatedContexts = $0 + updatedContexts.append(.requestInProgress) + return updatedContexts.sorted() } - }) { - var updatedContexts = $0 - updatedContexts.append(.requestInProgress) - return updatedContexts.sorted() + return $0 } - return $0 - } - }) - - let updateProgress = { [weak self] in - Queue.mainQueue().async { - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - return $0.updatedTitlePanelContext { - if let index = $0.firstIndex(where: { - switch $0 { + }) + + let updateProgress = { [weak self] in + Queue.mainQueue().async { + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedTitlePanelContext { + if let index = $0.firstIndex(where: { + switch $0 { case .requestInProgress: return true default: return false + } + }) { + var updatedContexts = $0 + updatedContexts.remove(at: index) + return updatedContexts } - }) { - var updatedContexts = $0 - updatedContexts.remove(at: index) - return updatedContexts + return $0 } - return $0 - } - }) - } - } - } - - let botAddress = botPeer.addressName ?? "" - - self.messageActionCallbackDisposable.set(((self.context.engine.messages.requestAppWebView(peerId: peerId, appReference: .id(id: botApp.id, accessHash: botApp.accessHash), payload: payload, themeParams: generateWebAppThemeParams(self.presentationData.theme), allowWrite: false) - |> afterDisposed { - updateProgress() - }) - |> deliverOnMainQueue).start(next: { [weak self] url in - guard let strongSelf = self else { - return - } - let params = WebAppParameters(peerId: peerId, botId: peerId, botName: botApp.title, url: url, queryId: 0, payload: payload, buttonText: "", keepAliveSignal: nil, fromMenu: false, fromAttachMenu: false, isInline: false, isSimple: false) - let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url in - self?.openUrl(url, concealed: true, forceExternal: true) - }, requestSwitchInline: { [weak self] query, chatTypes, completion in - if let strongSelf = self { - if let chatTypes { - let controller = strongSelf.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: strongSelf.context, filter: [.excludeRecent, .doNotSearchMessages], requestPeerType: chatTypes, hasContactSelector: false, hasCreation: false)) - controller.peerSelected = { [weak self, weak controller] peer, _ in - if let strongSelf = self { - completion() - controller?.dismiss() - strongSelf.controllerInteraction?.activateSwitchInline(peer.id, "@\(botAddress) \(query)", nil) - } - } - strongSelf.push(controller) - } else { - strongSelf.controllerInteraction?.activateSwitchInline(peerId, "@\(botAddress) \(query)", nil) + }) } } - }, completion: { [weak self] in - self?.chatDisplayNode.historyNode.scrollToEndOfHistory() - }, getNavigationController: { [weak self] in - return self?.effectiveNavigationController - }) - controller.navigationPresentation = .flatModal - strongSelf.currentWebAppController = controller - strongSelf.push(controller) - }, error: { [weak self] error in - if let strongSelf = self { - strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { - })]), in: .window(.root)) } - })) + + let botAddress = botPeer.addressName ?? "" + strongSelf.messageActionCallbackDisposable.set(((strongSelf.context.engine.messages.requestAppWebView(peerId: peerId, appReference: .id(id: botApp.id, accessHash: botApp.accessHash), payload: payload, themeParams: generateWebAppThemeParams(strongSelf.presentationData.theme), allowWrite: allowWrite) + |> afterDisposed { + updateProgress() + }) + |> deliverOnMainQueue).start(next: { [weak self] url in + guard let strongSelf = self else { + return + } + let params = WebAppParameters(peerId: peerId, botId: peerId, botName: botApp.title, url: url, queryId: 0, payload: payload, buttonText: "", keepAliveSignal: nil, fromMenu: false, fromAttachMenu: false, isInline: false, isSimple: false) + let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url in + self?.openUrl(url, concealed: true, forceExternal: true) + }, requestSwitchInline: { [weak self] query, chatTypes, completion in + if let strongSelf = self { + if let chatTypes { + let controller = strongSelf.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: strongSelf.context, filter: [.excludeRecent, .doNotSearchMessages], requestPeerType: chatTypes, hasContactSelector: false, hasCreation: false)) + controller.peerSelected = { [weak self, weak controller] peer, _ in + if let strongSelf = self { + completion() + controller?.dismiss() + strongSelf.controllerInteraction?.activateSwitchInline(peer.id, "@\(botAddress) \(query)", nil) + } + } + strongSelf.push(controller) + } else { + strongSelf.controllerInteraction?.activateSwitchInline(peerId, "@\(botAddress) \(query)", nil) + } + } + }, completion: { [weak self] in + self?.chatDisplayNode.historyNode.scrollToEndOfHistory() + }, getNavigationController: { [weak self] in + return self?.effectiveNavigationController + }) + controller.navigationPresentation = .flatModal + strongSelf.currentWebAppController = controller + strongSelf.push(controller) + }, error: { [weak self] error in + if let strongSelf = self { + strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { + })]), in: .window(.root)) + } + })) + } + + if concealed || botApp.flags.contains(.notActivated) { + let controller = webAppLaunchConfirmationController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: botPeer, commit: { + openBotApp(false) + }, showMore: { [weak self] in + if let strongSelf = self { + strongSelf.openResolved(result: .peer(botPeer._asPeer(), .info), sourceMessageId: nil) + } + }) + self.present(controller, in: .window(.root)) + } else { + openBotApp(false) + } } private func presentAttachmentPremiumGift() { @@ -17329,9 +17357,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) case let .withBotStartPayload(startPayload): if case .peer(peerId.id) = strongSelf.chatLocation { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - $0.updatedBotStartPayload(startPayload.payload) - }) + strongSelf.startBot(startPayload.payload) } else if let navigationController = strongSelf.effectiveNavigationController { strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peerId), botStart: startPayload, keepStack: .always)) } @@ -17343,23 +17369,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId.id)) |> deliverOnMainQueue).start(next: { [weak self] peer in if let strongSelf = self, let peer { - let openBotApp = { [weak self] in - if let strongSelf = self { - strongSelf.presentBotApp(botApp: botAppStart.botApp, botPeer: peerId, payload: botAppStart.payload) - } - } - if concealed { - let controller = webAppLaunchConfirmationController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: peer, commit: { - openBotApp() - }, showMore: { [weak self] in - if let strongSelf = self { - strongSelf.openResolved(result: .peer(peer._asPeer(), .info), sourceMessageId: nil) - } - }) - strongSelf.present(controller, in: .window(.root)) - } else { - openBotApp() - } + strongSelf.presentBotApp(botApp: botAppStart.botApp, botPeer: peer, payload: botAppStart.payload, concealed: concealed) } }) default: diff --git a/submodules/TelegramUI/Sources/ChatLoadingNode.swift b/submodules/TelegramUI/Sources/ChatLoadingNode.swift index bb015b4a8e..422f70e82a 100644 --- a/submodules/TelegramUI/Sources/ChatLoadingNode.swift +++ b/submodules/TelegramUI/Sources/ChatLoadingNode.swift @@ -1,6 +1,7 @@ import Foundation import UIKit import AsyncDisplayKit +import SwiftSignalKit import Display import TelegramCore import TelegramPresentationData @@ -207,7 +208,11 @@ final class ChatLoadingPlaceholderNode: ASDisplayNode { self.borderNode.view.mask = self.borderMaskNode.view if self.context.sharedContext.energyUsageSettings.fullTranslucency { - self.backgroundNode?.updateIsLooping(true) + Queue.mainQueue().after(0.3) { + if !self.didAnimateOut { + self.backgroundNode?.updateIsLooping(true) + } + } } } @@ -279,11 +284,12 @@ final class ChatLoadingPlaceholderNode: ASDisplayNode { self.borderMaskNode.bounds = self.borderMaskNode.bounds.offsetBy(dx: 0.0, dy: inset) } + private var didAnimateOut = false func animateOut(_ historyNode: ChatHistoryNode, completion: @escaping () -> Void = {}) { guard let listNode = historyNode as? ListView, let (size, _, _) = self.validLayout else { return } - + self.didAnimateOut = true self.backgroundNode?.updateIsLooping(false) let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .spring) diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index e92874376b..86b0d1a3fb 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -1473,7 +1473,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { transition.animatePosition(layer: self.startButton.layer, from: CGPoint(x: 0.0, y: 80.0), to: CGPoint(), additive: true) } if let context = self.context { - let absoluteFrame = self.startButton.view.convert(self.startButton.bounds, to: nil) + let parentFrame = self.view.convert(self.bounds, to: nil) + let absoluteFrame = self.startButton.view.convert(self.startButton.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0) let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 1.0), size: CGSize()) if let tooltipController = self.tooltipController { @@ -1494,7 +1495,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { delay = 0.35 } Queue.mainQueue().after(delay, { - let absoluteFrame = self.startButton.view.convert(self.startButton.bounds, to: nil) + let parentFrame = self.view.convert(self.bounds, to: nil) + let absoluteFrame = self.startButton.view.convert(self.startButton.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0) let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 1.0), size: CGSize()) controller.location = .point(location, .bottom) self.interfaceInteraction?.presentControllerInCurrent(controller, nil) diff --git a/submodules/TelegramUI/Sources/FetchVideoMediaResource.swift b/submodules/TelegramUI/Sources/FetchVideoMediaResource.swift index 6f35c73e81..13b0a19b55 100644 --- a/submodules/TelegramUI/Sources/FetchVideoMediaResource.swift +++ b/submodules/TelegramUI/Sources/FetchVideoMediaResource.swift @@ -207,198 +207,65 @@ private final class FetchVideoLibraryMediaResourceContext { private let throttlingContext = FetchVideoLibraryMediaResourceContext() public func fetchVideoLibraryMediaResource(account: Account, resource: VideoLibraryMediaResource) -> Signal { - return account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) - |> take(1) - |> map { view in - return view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? .defaultValue - } - |> castError(MediaResourceDataFetchError.self) - |> mapToSignal { appConfiguration -> Signal in - let signal = Signal { subscriber in - subscriber.putNext(.reset) - let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [resource.localIdentifier], options: nil) - var requestId: PHImageRequestID? - let disposable = MetaDisposable() - if fetchResult.count != 0 { - let asset = fetchResult.object(at: 0) - let option = PHVideoRequestOptions() - option.isNetworkAccessAllowed = true - option.deliveryMode = .highQualityFormat + let signal = Signal { subscriber in + subscriber.putNext(.reset) + let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [resource.localIdentifier], options: nil) + var requestId: PHImageRequestID? + let disposable = MetaDisposable() + if fetchResult.count != 0 { + let asset = fetchResult.object(at: 0) + let option = PHVideoRequestOptions() + option.isNetworkAccessAllowed = true + option.deliveryMode = .highQualityFormat + + let alreadyReceivedAsset = Atomic(value: false) + requestId = PHImageManager.default().requestAVAsset(forVideo: asset, options: option, resultHandler: { avAsset, _, _ in + if avAsset == nil { + return + } - let alreadyReceivedAsset = Atomic(value: false) - requestId = PHImageManager.default().requestAVAsset(forVideo: asset, options: option, resultHandler: { avAsset, _, _ in - if avAsset == nil { - return - } - - if alreadyReceivedAsset.swap(true) { - return - } - - var adjustments: TGVideoEditAdjustments? - switch resource.conversion { - case .passthrough: - if let asset = avAsset as? AVURLAsset { - var value = stat() - if stat(asset.url.path, &value) == 0 { - subscriber.putNext(.copyLocalItem(AVURLAssetCopyItem(url: asset.url))) - subscriber.putCompletion() - } else { - subscriber.putError(.generic) - } - return - } else { - adjustments = nil - } - case let .compress(adjustmentsValue): - if let adjustmentsValue = adjustmentsValue { - if let dict = NSKeyedUnarchiver.unarchiveObject(with: adjustmentsValue.data.makeData()) as? [AnyHashable : Any] { - adjustments = TGVideoEditAdjustments(dictionary: dict) - } - } - } - let updatedSize = Atomic(value: 0) - let entityRenderer: LegacyPaintEntityRenderer? = adjustments.flatMap { adjustments in - if let paintingData = adjustments.paintingData, paintingData.hasAnimation { - return LegacyPaintEntityRenderer(account: account, adjustments: adjustments) - } else { - return nil - } - } - let tempFile = EngineTempBox.shared.tempFile(fileName: "video.mp4") - let signal = TGMediaVideoConverter.convert(avAsset, adjustments: adjustments, path: tempFile.path, watcher: VideoConversionWatcher(update: { path, size in - var value = stat() - if stat(path, &value) == 0 { - if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) { - var range: Range? - let _ = updatedSize.modify { updatedSize in - range = updatedSize ..< value.st_size - return value.st_size - } - //print("size = \(Int(value.st_size)), range: \(range!)") - subscriber.putNext(.dataPart(resourceOffset: range!.lowerBound, data: data, range: range!, complete: false)) - } - } - }), entityRenderer: entityRenderer)! - let signalDisposable = signal.start(next: { next in - if let result = next as? TGMediaVideoConversionResult { + if alreadyReceivedAsset.swap(true) { + return + } + + var adjustments: TGVideoEditAdjustments? + switch resource.conversion { + case .passthrough: + if let asset = avAsset as? AVURLAsset { var value = stat() - if stat(result.fileURL.path, &value) == 0 { - if let data = try? Data(contentsOf: result.fileURL, options: [.mappedRead]) { - var range: Range? - let _ = updatedSize.modify { updatedSize in - range = updatedSize ..< value.st_size - return value.st_size - } - //print("finish size = \(Int(value.st_size)), range: \(range!)") - subscriber.putNext(.dataPart(resourceOffset: range!.lowerBound, data: data, range: range!, complete: false)) - subscriber.putNext(.replaceHeader(data: data, range: 0 ..< 1024)) - subscriber.putNext(.dataPart(resourceOffset: Int64(data.count), data: Data(), range: 0 ..< 0, complete: true)) - } + if stat(asset.url.path, &value) == 0 { + subscriber.putNext(.copyLocalItem(AVURLAssetCopyItem(url: asset.url))) + subscriber.putCompletion() } else { subscriber.putError(.generic) } - subscriber.putCompletion() - - EngineTempBox.shared.dispose(tempFile) - } - }, error: { _ in - subscriber.putError(.generic) - }, completed: nil) - disposable.set(ActionDisposable { - signalDisposable?.dispose() - }) - }) - } - - return ActionDisposable { - if let requestId = requestId { - PHImageManager.default().cancelImageRequest(requestId) - } - disposable.dispose() - } - } - return throttlingContext.wrap(priority: .default, signal: signal) - } -} - -func fetchLocalFileVideoMediaResource(account: Account, resource: LocalFileVideoMediaResource) -> Signal { - return account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) - |> take(1) - |> map { view in - return view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? .defaultValue - } - |> castError(MediaResourceDataFetchError.self) - |> mapToSignal { appConfiguration -> Signal in - let signal = Signal { subscriber in - subscriber.putNext(.reset) - - var filteredPath = resource.path - if filteredPath.hasPrefix("file://") { - filteredPath = String(filteredPath[filteredPath.index(filteredPath.startIndex, offsetBy: "file://".count)]) - } - - let avAsset = AVURLAsset(url: URL(fileURLWithPath: filteredPath)) - var adjustments: TGVideoEditAdjustments? - if let videoAdjustments = resource.adjustments { - if let dict = NSKeyedUnarchiver.unarchiveObject(with: videoAdjustments.data.makeData()) as? [AnyHashable : Any] { - adjustments = TGVideoEditAdjustments(dictionary: dict) - } - } - let tempFile = EngineTempBox.shared.tempFile(fileName: "video.mp4") - let updatedSize = Atomic(value: 0) - let entityRenderer: LegacyPaintEntityRenderer? = adjustments.flatMap { adjustments in - if let paintingData = adjustments.paintingData, paintingData.hasAnimation { - return LegacyPaintEntityRenderer(account: account, adjustments: adjustments) - } else { - return nil - } - } - let signal: SSignal - if filteredPath.contains(".jpg"), let entityRenderer = entityRenderer { - if let data = try? Data(contentsOf: URL(fileURLWithPath: filteredPath), options: [.mappedRead]), let image = UIImage(data: data) { - let durationSignal: SSignal = SSignal(generator: { subscriber in - let disposable = (entityRenderer.duration()).start(next: { duration in - subscriber.putNext(duration) - subscriber.putCompletion() - }) - - return SBlockDisposable(block: { - disposable.dispose() - }) - }) - - signal = durationSignal.map(toSignal: { duration -> SSignal in - if let duration = duration as? Double { - return TGMediaVideoConverter.renderUIImage(image, duration: duration, adjustments: adjustments, path: tempFile.path, watcher: VideoConversionWatcher(update: { path, size in - var value = stat() - if stat(path, &value) == 0 { - if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) { - var range: Range? - let _ = updatedSize.modify { updatedSize in - range = updatedSize ..< value.st_size - return value.st_size - } - //print("size = \(Int(value.st_size)), range: \(range!)") - subscriber.putNext(.dataPart(resourceOffset: range!.lowerBound, data: data, range: range!, complete: false)) - } - } - }), entityRenderer: entityRenderer)! + return } else { - return SSignal.single(nil) + adjustments = nil + } + case let .compress(adjustmentsValue): + if let adjustmentsValue = adjustmentsValue { + if let dict = NSKeyedUnarchiver.unarchiveObject(with: adjustmentsValue.data.makeData()) as? [AnyHashable : Any] { + adjustments = TGVideoEditAdjustments(dictionary: dict) + } } - }) - } else { - signal = SSignal.single(nil) } - } else { - signal = TGMediaVideoConverter.convert(avAsset, adjustments: adjustments, path: tempFile.path, watcher: VideoConversionWatcher(update: { path, size in + let updatedSize = Atomic(value: 0) + let entityRenderer: LegacyPaintEntityRenderer? = adjustments.flatMap { adjustments in + if let paintingData = adjustments.paintingData, paintingData.hasAnimation { + return LegacyPaintEntityRenderer(account: account, adjustments: adjustments) + } else { + return nil + } + } + let tempFile = EngineTempBox.shared.tempFile(fileName: "video.mp4") + let signal = TGMediaVideoConverter.convert(avAsset, adjustments: adjustments, path: tempFile.path, watcher: VideoConversionWatcher(update: { path, size in var value = stat() if stat(path, &value) == 0 { if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) { var range: Range? let _ = updatedSize.modify { updatedSize in - range = updatedSize ..< Int64(value.st_size) + range = updatedSize ..< value.st_size return value.st_size } //print("size = \(Int(value.st_size)), range: \(range!)") @@ -406,12 +273,130 @@ func fetchLocalFileVideoMediaResource(account: Account, resource: LocalFileVideo } } }), entityRenderer: entityRenderer)! + let signalDisposable = signal.start(next: { next in + if let result = next as? TGMediaVideoConversionResult { + var value = stat() + if stat(result.fileURL.path, &value) == 0 { + if let data = try? Data(contentsOf: result.fileURL, options: [.mappedRead]) { + var range: Range? + let _ = updatedSize.modify { updatedSize in + range = updatedSize ..< value.st_size + return value.st_size + } + //print("finish size = \(Int(value.st_size)), range: \(range!)") + subscriber.putNext(.dataPart(resourceOffset: range!.lowerBound, data: data, range: range!, complete: false)) + subscriber.putNext(.replaceHeader(data: data, range: 0 ..< 1024)) + subscriber.putNext(.dataPart(resourceOffset: Int64(data.count), data: Data(), range: 0 ..< 0, complete: true)) + } + } else { + subscriber.putError(.generic) + } + subscriber.putCompletion() + + EngineTempBox.shared.dispose(tempFile) + } + }, error: { _ in + subscriber.putError(.generic) + }, completed: nil) + disposable.set(ActionDisposable { + signalDisposable?.dispose() + }) + }) + } + + return ActionDisposable { + if let requestId = requestId { + PHImageManager.default().cancelImageRequest(requestId) } + disposable.dispose() + } + } + return throttlingContext.wrap(priority: .default, signal: signal) +} + +func fetchLocalFileVideoMediaResource(account: Account, resource: LocalFileVideoMediaResource) -> Signal { + let signal = Signal { subscriber in + subscriber.putNext(.reset) + + var filteredPath = resource.path + if filteredPath.hasPrefix("file://") { + filteredPath = String(filteredPath[filteredPath.index(filteredPath.startIndex, offsetBy: "file://".count)]) + } + + let avAsset = AVURLAsset(url: URL(fileURLWithPath: filteredPath)) + var adjustments: TGVideoEditAdjustments? + if let videoAdjustments = resource.adjustments { + if let dict = NSKeyedUnarchiver.unarchiveObject(with: videoAdjustments.data.makeData()) as? [AnyHashable : Any] { + adjustments = TGVideoEditAdjustments(dictionary: dict) + } + } + let tempFile = EngineTempBox.shared.tempFile(fileName: "video.mp4") + let updatedSize = Atomic(value: 0) + let entityRenderer: LegacyPaintEntityRenderer? = adjustments.flatMap { adjustments in + if let paintingData = adjustments.paintingData, paintingData.hasAnimation { + return LegacyPaintEntityRenderer(account: account, adjustments: adjustments) + } else { + return nil + } + } + let signal: SSignal + if filteredPath.contains(".jpg"), let entityRenderer = entityRenderer { + if let data = try? Data(contentsOf: URL(fileURLWithPath: filteredPath), options: [.mappedRead]), let image = UIImage(data: data) { + let durationSignal: SSignal = SSignal(generator: { subscriber in + let disposable = (entityRenderer.duration()).start(next: { duration in + subscriber.putNext(duration) + subscriber.putCompletion() + }) + + return SBlockDisposable(block: { + disposable.dispose() + }) + }) - let signalDisposable = signal.start(next: { next in - if let result = next as? TGMediaVideoConversionResult { - var value = stat() - if stat(result.fileURL.path, &value) == 0 { + signal = durationSignal.map(toSignal: { duration -> SSignal in + if let duration = duration as? Double { + return TGMediaVideoConverter.renderUIImage(image, duration: duration, adjustments: adjustments, path: tempFile.path, watcher: VideoConversionWatcher(update: { path, size in + var value = stat() + if stat(path, &value) == 0 { + if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) { + var range: Range? + let _ = updatedSize.modify { updatedSize in + range = updatedSize ..< value.st_size + return value.st_size + } + //print("size = \(Int(value.st_size)), range: \(range!)") + subscriber.putNext(.dataPart(resourceOffset: range!.lowerBound, data: data, range: range!, complete: false)) + } + } + }), entityRenderer: entityRenderer)! + } else { + return SSignal.single(nil) + } + }) + } else { + signal = SSignal.single(nil) + } + } else { + signal = TGMediaVideoConverter.convert(avAsset, adjustments: adjustments, path: tempFile.path, watcher: VideoConversionWatcher(update: { path, size in + var value = stat() + if stat(path, &value) == 0 { + if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) { + var range: Range? + let _ = updatedSize.modify { updatedSize in + range = updatedSize ..< Int64(value.st_size) + return value.st_size + } + //print("size = \(Int(value.st_size)), range: \(range!)") + subscriber.putNext(.dataPart(resourceOffset: range!.lowerBound, data: data, range: range!, complete: false)) + } + } + }), entityRenderer: entityRenderer)! + } + + let signalDisposable = signal.start(next: { next in + if let result = next as? TGMediaVideoConversionResult { + var value = stat() + if stat(result.fileURL.path, &value) == 0 { // if config.remuxToFMp4 { // let tempFile = TempBox.shared.tempFile(fileName: "video.mp4") // if FFMpegRemuxer.remux(result.fileURL.path, to: tempFile.path) { @@ -424,35 +409,34 @@ func fetchLocalFileVideoMediaResource(account: Account, resource: LocalFileVideo // } else { // subscriber.putNext(.moveLocalFile(path: result.fileURL.path)) // } - if let data = try? Data(contentsOf: result.fileURL, options: [.mappedRead]) { - var range: Range? - let _ = updatedSize.modify { updatedSize in - range = updatedSize ..< value.st_size - return value.st_size - } - //print("finish size = \(Int(value.st_size)), range: \(range!)") - subscriber.putNext(.dataPart(resourceOffset: range!.lowerBound, data: data, range: range!, complete: false)) - subscriber.putNext(.replaceHeader(data: data, range: 0 ..< 1024)) - subscriber.putNext(.dataPart(resourceOffset: 0, data: Data(), range: 0 ..< 0, complete: true)) - - EngineTempBox.shared.dispose(tempFile) + if let data = try? Data(contentsOf: result.fileURL, options: [.mappedRead]) { + var range: Range? + let _ = updatedSize.modify { updatedSize in + range = updatedSize ..< value.st_size + return value.st_size } + //print("finish size = \(Int(value.st_size)), range: \(range!)") + subscriber.putNext(.dataPart(resourceOffset: range!.lowerBound, data: data, range: range!, complete: false)) + subscriber.putNext(.replaceHeader(data: data, range: 0 ..< 1024)) + subscriber.putNext(.dataPart(resourceOffset: 0, data: Data(), range: 0 ..< 0, complete: true)) + + EngineTempBox.shared.dispose(tempFile) } - subscriber.putCompletion() } - }, error: { _ in - }, completed: nil) - - let disposable = ActionDisposable { - signalDisposable?.dispose() - } - - return ActionDisposable { - disposable.dispose() + subscriber.putCompletion() } + }, error: { _ in + }, completed: nil) + + let disposable = ActionDisposable { + signalDisposable?.dispose() + } + + return ActionDisposable { + disposable.dispose() } - return throttlingContext.wrap(priority: .default, signal: signal) } + return throttlingContext.wrap(priority: .default, signal: signal) } public func fetchVideoLibraryMediaResourceHash(resource: VideoLibraryMediaResource) -> Signal { diff --git a/submodules/TelegramUI/Sources/NavigateToChatController.swift b/submodules/TelegramUI/Sources/NavigateToChatController.swift index 4776b1c213..b12dd7d16d 100644 --- a/submodules/TelegramUI/Sources/NavigateToChatController.swift +++ b/submodules/TelegramUI/Sources/NavigateToChatController.swift @@ -137,6 +137,10 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam } } else { controller = ChatControllerImpl(context: params.context, chatLocation: params.chatLocation.asChatLocation, chatLocationContextHolder: params.chatLocationContextHolder, subject: params.subject, botStart: params.botStart, attachBotStart: params.attachBotStart, botAppStart: params.botAppStart, peekData: params.peekData, peerNearbyData: params.peerNearbyData, chatListFilter: params.chatListFilter, chatNavigationStack: params.chatNavigationStack) + + if let botAppStart = params.botAppStart, case let .peer(peer) = params.chatLocation { + controller.presentBotApp(botApp: botAppStart.botApp, botPeer: peer, payload: botAppStart.payload) + } } controller.purposefulAction = params.purposefulAction if let search = params.activateMessageSearch { diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index 894951b5c6..d63fe297f4 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -219,6 +219,11 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur if let navigationController = navigationController { context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), attachBotStart: attachBotStart)) } + case let .withBotApp(botAppStart): + context.sharedContext.applicationBindings.dismissNativeController() + if let navigationController = navigationController { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), botAppStart: botAppStart)) + } default: break } @@ -738,7 +743,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur } else if let domain = domain { var result = "https://t.me/\(domain)" if let appName { - result += "\(appName)" + result += "/\(appName)" } if let startApp { result += "?startapp=\(startApp)" diff --git a/submodules/TranslateUI/Sources/ChatTranslation.swift b/submodules/TranslateUI/Sources/ChatTranslation.swift index 6fd7639622..07abfa4cab 100644 --- a/submodules/TranslateUI/Sources/ChatTranslation.swift +++ b/submodules/TranslateUI/Sources/ChatTranslation.swift @@ -206,10 +206,7 @@ public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id) var fromLangs: [String: Int] = [:] var count = 0 for message in messages { - if let _ = URL(string: message.text) { - continue - } - if message.text.count >= 10 { + if message.effectivelyIncoming(context.account.peerId), message.text.count >= 10 { var text = String(message.text.prefix(256)) if var entities = message.textEntitiesAttribute?.entities.filter({ [.Pre, .Code, .Url, .Email, .Mention, .Hashtag, .BotCommand].contains($0.type) }) { entities = entities.sorted(by: { $0.range.lowerBound > $1.range.lowerBound })