diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index 52de061600..de0ef04311 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -97,7 +97,8 @@ public class AttachmentController: ViewController { private let context: AccountContext private let updatedPresentationData: (initial: PresentationData, signal: Signal)? private let chatLocation: ChatLocation - private var buttons: [AttachmentButtonType] + private let buttons: [AttachmentButtonType] + private let initialButton: AttachmentButtonType public var mediaPickerContext: AttachmentMediaPickerContext? { get { @@ -268,7 +269,20 @@ public class AttachmentController: ViewController { self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) - let _ = self.switchToController(.gallery) + if let controller = self.controller { + let _ = self.switchToController(controller.initialButton) + if case let .app(botId, _, _) = controller.initialButton { + if let index = controller.buttons.firstIndex(where: { + if case let .app(otherBotId, _, _) = $0, otherBotId == botId { + return true + } else { + return false + } + }) { + self.panel.updateSelectedIndex(index) + } + } + } } private func updateSelectionCount(_ count: Int) { @@ -293,25 +307,6 @@ public class AttachmentController: ViewController { } } - func switchTo(_ type: AttachmentButtonType) { - guard let buttons = self.controller?.buttons else { - return - } - if case let .app(botId, _, _) = type { - let index = buttons.firstIndex(where: { - if case let .app(otherBotId, _, _) = $0, otherBotId == botId { - return true - } else { - return false - } - }) - if let index = index { - self.panel.updateSelectedIndex(index) - let _ = self.switchToController(buttons[index], animated: false) - } - } - } - func switchToController(_ type: AttachmentButtonType, animated: Bool = true) -> Bool { guard self.currentType != type else { if self.animating { @@ -583,13 +578,12 @@ public class AttachmentController: ViewController { completion(nil, nil) } - private var buttonsDisposable: Disposable? - - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, chatLocation: ChatLocation, buttons: Signal<[AttachmentButtonType], NoError>) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, chatLocation: ChatLocation, buttons: [AttachmentButtonType], initialButton: AttachmentButtonType = .gallery) { self.context = context self.updatedPresentationData = updatedPresentationData self.chatLocation = chatLocation - self.buttons = [] + self.buttons = buttons + self.initialButton = initialButton super.init(navigationBarPresentationData: nil) @@ -602,21 +596,6 @@ public class AttachmentController: ViewController { strongSelf.node.scrollToTop() } } - - self.buttonsDisposable = (buttons - |> deliverOnMainQueue).start(next: { [weak self] buttons in - if let strongSelf = self { - let previousButtons = strongSelf.buttons - strongSelf.buttons = buttons - if let layout = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout, transition: !previousButtons.isEmpty ? .animated(duration: 0.2, curve: .easeInOut) : .immediate) - } - } - }) - } - - deinit { - self.buttonsDisposable?.dispose() } public required init(coder aDecoder: NSCoder) { @@ -632,10 +611,6 @@ public class AttachmentController: ViewController { self.displayNodeDidLoad() } - public func switchTo(_ type: AttachmentButtonType) { - (self.displayNode as! Node).switchTo(type) - } - public func _dismiss() { super.dismiss(animated: false, completion: {}) } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 614b8dd7e2..17b6b20448 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -10497,6 +10497,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } + let context = self.context + let inputIsActive = self.presentationInterfaceState.inputMode == .text self.chatDisplayNode.dismissInput() @@ -10535,254 +10537,220 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G isScheduledMessages = true } - var switchToBotImpl: ((AttachmentButtonType) -> Void)? - var switchToBotId = botId - let buttons: Signal<[AttachmentButtonType], NoError> + let buttons: Signal<([AttachmentButtonType], AttachmentButtonType?), NoError> if let _ = peer as? TelegramUser, !isScheduledMessages { - buttons = .single(availableButtons) - |> then( - self.context.engine.messages.attachMenuBots() - |> map { attachMenuBots in - var buttons = availableButtons - for bot in attachMenuBots.reversed() { - let peerTitle = EnginePeer(bot.peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - buttons.insert(.app(bot.peer.id, peerTitle, bot.icon), at: 1) - } - return buttons + buttons = self.context.engine.messages.attachMenuBots() + |> map { attachMenuBots in + var buttons = availableButtons + var initialButton: AttachmentButtonType? + if botId == nil { + initialButton = .gallery } - ) |> afterNext { buttons in - if let botId = switchToBotId, let button = buttons.first(where: { - if case let .app(otherBotId, _,_) = $0, botId == otherBotId { - return true - } else { - return false + for bot in attachMenuBots.reversed() { + let peerTitle = EnginePeer(bot.peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + let button: AttachmentButtonType = .app(bot.peer.id, peerTitle, bot.icon) + buttons.insert(button, at: 1) + + if initialButton == nil && bot.peer.id == botId { + initialButton = button } - }) { - Queue.mainQueue().justDispatch { - switchToBotImpl?(button) - } - switchToBotId = nil } + return (buttons, initialButton) } } else { - buttons = .single(availableButtons) + buttons = .single((availableButtons, .gallery)) } - let inputText = self.presentationInterfaceState.interfaceState.effectiveInputState.inputText - - let currentMediaController = Atomic(value: nil) - let currentFilesController = Atomic(value: nil) - let currentLocationController = Atomic(value: nil) - - let attachmentController = AttachmentController(context: self.context, updatedPresentationData: self.updatedPresentationData, chatLocation: self.chatLocation, buttons: buttons) - switchToBotImpl = { [weak attachmentController] button in - attachmentController?.switchTo(button) - } - attachmentController.requestController = { [weak self, weak attachmentController] type, completion in + let _ = (buttons + |> deliverOnMainQueue).start(next: { [weak self] buttons, initialButton in guard let strongSelf = self else { return } - switch type { - case .gallery: - strongSelf.controllerNavigationDisposable.set(nil) - let existingController = currentMediaController.with { $0 } - if let controller = existingController { - completion(controller, controller.mediaPickerContext) - controller.prepareForReuse() - return - } - strongSelf.presentMediaPicker(bannedSendMedia: bannedSendMedia, present: { controller, mediaPickerContext in - let _ = currentMediaController.swap(controller) - if !inputText.string.isEmpty { - mediaPickerContext?.setCaption(inputText) - } - completion(controller, mediaPickerContext) - }, updateMediaPickerContext: { [weak attachmentController] mediaPickerContext in - attachmentController?.mediaPickerContext = mediaPickerContext - }, completion: { [weak self] signals, silentPosting, scheduleTime, getAnimatedTransitionSource, completion in - if !inputText.string.isEmpty { - self?.clearInputText() - } - self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion) - }) - case .file: - strongSelf.controllerNavigationDisposable.set(nil) - let existingController = currentFilesController.with { $0 } - if let controller = existingController { - completion(controller, nil) - controller.prepareForReuse() - return - } - let controller = attachmentFileController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, bannedSendMedia: bannedSendMedia, presentGallery: { [weak self, weak attachmentController] in - attachmentController?.dismiss(animated: true) - self?.presentFileGallery() - }, presentFiles: { [weak self, weak attachmentController] in - attachmentController?.dismiss(animated: true) - self?.presentICloudFileGallery() - }, send: { [weak self] mediaReference in - guard let strongSelf = self, let peerId = strongSelf.chatLocation.peerId else { - return - } - let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: mediaReference, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil) - let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: strongSelf.transformEnqueueMessages([message])) - |> deliverOnMainQueue).start(next: { [weak self] _ in - if let strongSelf = self, strongSelf.presentationInterfaceState.subject != .scheduledMessages { - strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() - } - }) - }) - let _ = currentFilesController.swap(controller) - completion(controller, nil) - case .location: - strongSelf.controllerNavigationDisposable.set(nil) - let existingController = currentLocationController.with { $0 } - if let controller = existingController { - completion(controller, nil) - controller.prepareForReuse() - return - } - let selfPeerId: PeerId - if let peer = peer as? TelegramChannel, case .broadcast = peer.info { - selfPeerId = peer.id - } else if let peer = peer as? TelegramChannel, case .group = peer.info, peer.hasPermission(.canBeAnonymous) { - selfPeerId = peer.id - } else { - selfPeerId = strongSelf.context.account.peerId - } - let _ = (strongSelf.context.account.postbox.transaction { transaction -> Peer? in - return transaction.getPeer(selfPeerId) - } - |> deliverOnMainQueue).start(next: { [weak self] selfPeer in - guard let strongSelf = self, let selfPeer = selfPeer else { - return - } - let hasLiveLocation = peer.id.namespace != Namespaces.Peer.SecretChat && peer.id != strongSelf.context.account.peerId && strongSelf.presentationInterfaceState.subject != .scheduledMessages - let controller = LocationPickerController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, mode: .share(peer: peer, selfPeer: selfPeer, hasLiveLocation: hasLiveLocation), completion: { [weak self] location, _ in - guard let strongSelf = self else { + + guard let initialButton = initialButton else { + if let botId = botId { + let _ = (strongSelf.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: botId) + ) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let strongSelf = self, let peer = peer else { return } - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId - let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: location), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil) - strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } - }) + let _ = (strongSelf.context.engine.messages.requestWebView(peerId: peer.id, botId: botId, url: nil, themeParams: nil, replyToMessageId: nil) + |> deliverOnMainQueue).start(next: { [weak self] result in + if let strongSelf = self, case let .requestConfirmation(botIcon) = result { + if case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.canBeAddedToAttachMenu) { + let controller = addWebAppToAttachmentController(context: context, peerName: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), peerIcon: botIcon, completion: { + let _ = context.engine.messages.addBotToAttachMenu(peerId: botId).start() + + Queue.mainQueue().after(1.0, { + strongSelf.presentAttachmentBot(botId: botId) + }) + }) + strongSelf.present(controller, in: .window(.root)) + } else { + strongSelf.present(textAlertController(context: context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + } } - }, nil) - strongSelf.sendMessages([message]) + }) }) - completion(controller, nil) - - let _ = currentLocationController.swap(controller) - }) - case .contact: - let contactsController = ContactSelectionControllerImpl(ContactSelectionControllerParams(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: { $0.Contacts_Title }, displayDeviceContacts: true, multipleSelection: true)) - contactsController.presentScheduleTimePicker = { [weak self] completion in - if let strongSelf = self { - strongSelf.presentScheduleTimePicker(completion: completion) - } } - contactsController.navigationPresentation = .modal - completion(contactsController, contactsController.mediaPickerContext) - strongSelf.controllerNavigationDisposable.set((contactsController.result - |> deliverOnMainQueue).start(next: { [weak self] peers in - if let strongSelf = self, let (peers, _, silent, scheduleTime, text) = peers { - var textEnqueueMessage: EnqueueMessage? - if let text = text, text.length > 0 { - var attributes: [MessageAttribute] = [] - let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text)) - if !entities.isEmpty { - attributes.append(TextEntitiesMessageAttribute(entities: entities)) - } - textEnqueueMessage = .message(text: text.string, attributes: attributes, mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil) + return + } + + let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText + + let currentMediaController = Atomic(value: nil) + let currentFilesController = Atomic(value: nil) + let currentLocationController = Atomic(value: nil) + + let attachmentController = AttachmentController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: strongSelf.chatLocation, buttons: buttons, initialButton: initialButton) + attachmentController.requestController = { [weak self, weak attachmentController] type, completion in + guard let strongSelf = self else { + return + } + switch type { + case .gallery: + strongSelf.controllerNavigationDisposable.set(nil) + let existingController = currentMediaController.with { $0 } + if let controller = existingController { + completion(controller, controller.mediaPickerContext) + controller.prepareForReuse() + return + } + strongSelf.presentMediaPicker(bannedSendMedia: bannedSendMedia, present: { controller, mediaPickerContext in + let _ = currentMediaController.swap(controller) + if !inputText.string.isEmpty { + mediaPickerContext?.setCaption(inputText) } - if peers.count > 1 { - var enqueueMessages: [EnqueueMessage] = [] - if let textEnqueueMessage = textEnqueueMessage { - enqueueMessages.append(textEnqueueMessage) + completion(controller, mediaPickerContext) + }, updateMediaPickerContext: { [weak attachmentController] mediaPickerContext in + attachmentController?.mediaPickerContext = mediaPickerContext + }, completion: { [weak self] signals, silentPosting, scheduleTime, getAnimatedTransitionSource, completion in + if !inputText.string.isEmpty { + self?.clearInputText() + } + self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion) + }) + case .file: + strongSelf.controllerNavigationDisposable.set(nil) + let existingController = currentFilesController.with { $0 } + if let controller = existingController { + completion(controller, nil) + controller.prepareForReuse() + return + } + let controller = attachmentFileController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, bannedSendMedia: bannedSendMedia, presentGallery: { [weak self, weak attachmentController] in + attachmentController?.dismiss(animated: true) + self?.presentFileGallery() + }, presentFiles: { [weak self, weak attachmentController] in + attachmentController?.dismiss(animated: true) + self?.presentICloudFileGallery() + }, send: { [weak self] mediaReference in + guard let strongSelf = self, let peerId = strongSelf.chatLocation.peerId else { + return + } + let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: mediaReference, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil) + let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: strongSelf.transformEnqueueMessages([message])) + |> deliverOnMainQueue).start(next: { [weak self] _ in + if let strongSelf = self, strongSelf.presentationInterfaceState.subject != .scheduledMessages { + strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() } - for peer in peers { - var media: TelegramMediaContact? - switch peer { - case let .peer(contact, _, _): - guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else { - continue - } - let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") - - let phone = contactData.basicData.phoneNumbers[0].value - media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: contact.id, vCardData: nil) - case let .deviceContact(_, basicData): - guard !basicData.phoneNumbers.isEmpty else { - continue - } - let contactData = DeviceContactExtendedData(basicData: basicData, middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") - - let phone = contactData.basicData.phoneNumbers[0].value - media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: nil, vCardData: nil) + }) + }) + let _ = currentFilesController.swap(controller) + completion(controller, nil) + case .location: + strongSelf.controllerNavigationDisposable.set(nil) + let existingController = currentLocationController.with { $0 } + if let controller = existingController { + completion(controller, nil) + controller.prepareForReuse() + return + } + let selfPeerId: PeerId + if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + selfPeerId = peer.id + } else if let peer = peer as? TelegramChannel, case .group = peer.info, peer.hasPermission(.canBeAnonymous) { + selfPeerId = peer.id + } else { + selfPeerId = strongSelf.context.account.peerId + } + let _ = (strongSelf.context.account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(selfPeerId) + } + |> deliverOnMainQueue).start(next: { [weak self] selfPeer in + guard let strongSelf = self, let selfPeer = selfPeer else { + return + } + let hasLiveLocation = peer.id.namespace != Namespaces.Peer.SecretChat && peer.id != strongSelf.context.account.peerId && strongSelf.presentationInterfaceState.subject != .scheduledMessages + let controller = LocationPickerController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, mode: .share(peer: peer, selfPeer: selfPeer, hasLiveLocation: hasLiveLocation), completion: { [weak self] location, _ in + guard let strongSelf = self else { + return + } + let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: location), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil) + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + }) } + }, nil) + strongSelf.sendMessages([message]) + }) + completion(controller, nil) + + let _ = currentLocationController.swap(controller) + }) + case .contact: + let contactsController = ContactSelectionControllerImpl(ContactSelectionControllerParams(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: { $0.Contacts_Title }, displayDeviceContacts: true, multipleSelection: true)) + contactsController.presentScheduleTimePicker = { [weak self] completion in + if let strongSelf = self { + strongSelf.presentScheduleTimePicker(completion: completion) + } + } + contactsController.navigationPresentation = .modal + completion(contactsController, contactsController.mediaPickerContext) + strongSelf.controllerNavigationDisposable.set((contactsController.result + |> deliverOnMainQueue).start(next: { [weak self] peers in + if let strongSelf = self, let (peers, _, silent, scheduleTime, text) = peers { + var textEnqueueMessage: EnqueueMessage? + if let text = text, text.length > 0 { + var attributes: [MessageAttribute] = [] + let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text)) + if !entities.isEmpty { + attributes.append(TextEntitiesMessageAttribute(entities: entities)) + } + textEnqueueMessage = .message(text: text.string, attributes: attributes, mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil) + } + if peers.count > 1 { + var enqueueMessages: [EnqueueMessage] = [] + if let textEnqueueMessage = textEnqueueMessage { + enqueueMessages.append(textEnqueueMessage) + } + for peer in peers { + var media: TelegramMediaContact? + switch peer { + case let .peer(contact, _, _): + guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else { + continue + } + let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") + + let phone = contactData.basicData.phoneNumbers[0].value + media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: contact.id, vCardData: nil) + case let .deviceContact(_, basicData): + guard !basicData.phoneNumbers.isEmpty else { + continue + } + let contactData = DeviceContactExtendedData(basicData: basicData, middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") + + let phone = contactData.basicData.phoneNumbers[0].value + media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: nil, vCardData: nil) + } - if let media = media { - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId - strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } - }) - } - }, nil) - let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil) - enqueueMessages.append(message) - } - } - strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)) - } else if let peer = peers.first { - let dataSignal: Signal<(Peer?, DeviceContactExtendedData?), NoError> - switch peer { - case let .peer(contact, _, _): - guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else { - return - } - let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") - let context = strongSelf.context - dataSignal = (strongSelf.context.sharedContext.contactDataManager?.basicData() ?? .single([:])) - |> take(1) - |> mapToSignal { basicData -> Signal<(Peer?, DeviceContactExtendedData?), NoError> in - var stableId: String? - let queryPhoneNumber = formatPhoneNumber(phoneNumber) - outer: for (id, data) in basicData { - for phoneNumber in data.phoneNumbers { - if formatPhoneNumber(phoneNumber.value) == queryPhoneNumber { - stableId = id - break outer - } - } - } - - if let stableId = stableId { - return (context.sharedContext.contactDataManager?.extendedData(stableId: stableId) ?? .single(nil)) - |> take(1) - |> map { extendedData -> (Peer?, DeviceContactExtendedData?) in - return (contact, extendedData) - } - } else { - return .single((contact, contactData)) - } - } - case let .deviceContact(id, _): - dataSignal = (strongSelf.context.sharedContext.contactDataManager?.extendedData(stableId: id) ?? .single(nil)) - |> take(1) - |> map { extendedData -> (Peer?, DeviceContactExtendedData?) in - return (nil, extendedData) - } - } - strongSelf.controllerNavigationDisposable.set((dataSignal - |> deliverOnMainQueue).start(next: { peerAndContactData in - if let strongSelf = self, let contactData = peerAndContactData.1, contactData.basicData.phoneNumbers.count != 0 { - if contactData.isPrimitive { - let phone = contactData.basicData.phoneNumbers[0].value - let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peerAndContactData.0?.id, vCardData: nil) + if let media = media { let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { @@ -10791,71 +10759,131 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } }, nil) - - var enqueueMessages: [EnqueueMessage] = [] - if let textEnqueueMessage = textEnqueueMessage { - enqueueMessages.append(textEnqueueMessage) - } - enqueueMessages.append(.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil)) - strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)) - } else { - let contactController = strongSelf.context.sharedContext.makeDeviceContactInfoController(context: strongSelf.context, subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in - guard let strongSelf = self, !contactData.basicData.phoneNumbers.isEmpty else { - return - } - let phone = contactData.basicData.phoneNumbers[0].value - if let vCardData = contactData.serializedVCard() { - let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peer?.id, vCardData: vCardData) - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId - strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } - }) - } - }, nil) - - var enqueueMessages: [EnqueueMessage] = [] - if let textEnqueueMessage = textEnqueueMessage { - enqueueMessages.append(textEnqueueMessage) - } - enqueueMessages.append(.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil)) - strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)) - } - }), completed: nil, cancelled: nil) - strongSelf.effectiveNavigationController?.pushViewController(contactController) + let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil) + enqueueMessages.append(message) } } - })) + strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)) + } else if let peer = peers.first { + let dataSignal: Signal<(Peer?, DeviceContactExtendedData?), NoError> + switch peer { + case let .peer(contact, _, _): + guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else { + return + } + let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") + let context = strongSelf.context + dataSignal = (strongSelf.context.sharedContext.contactDataManager?.basicData() ?? .single([:])) + |> take(1) + |> mapToSignal { basicData -> Signal<(Peer?, DeviceContactExtendedData?), NoError> in + var stableId: String? + let queryPhoneNumber = formatPhoneNumber(phoneNumber) + outer: for (id, data) in basicData { + for phoneNumber in data.phoneNumbers { + if formatPhoneNumber(phoneNumber.value) == queryPhoneNumber { + stableId = id + break outer + } + } + } + + if let stableId = stableId { + return (context.sharedContext.contactDataManager?.extendedData(stableId: stableId) ?? .single(nil)) + |> take(1) + |> map { extendedData -> (Peer?, DeviceContactExtendedData?) in + return (contact, extendedData) + } + } else { + return .single((contact, contactData)) + } + } + case let .deviceContact(id, _): + dataSignal = (strongSelf.context.sharedContext.contactDataManager?.extendedData(stableId: id) ?? .single(nil)) + |> take(1) + |> map { extendedData -> (Peer?, DeviceContactExtendedData?) in + return (nil, extendedData) + } + } + strongSelf.controllerNavigationDisposable.set((dataSignal + |> deliverOnMainQueue).start(next: { peerAndContactData in + if let strongSelf = self, let contactData = peerAndContactData.1, contactData.basicData.phoneNumbers.count != 0 { + if contactData.isPrimitive { + let phone = contactData.basicData.phoneNumbers[0].value + let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peerAndContactData.0?.id, vCardData: nil) + let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + }) + } + }, nil) + + var enqueueMessages: [EnqueueMessage] = [] + if let textEnqueueMessage = textEnqueueMessage { + enqueueMessages.append(textEnqueueMessage) + } + enqueueMessages.append(.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil)) + strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)) + } else { + let contactController = strongSelf.context.sharedContext.makeDeviceContactInfoController(context: strongSelf.context, subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in + guard let strongSelf = self, !contactData.basicData.phoneNumbers.isEmpty else { + return + } + let phone = contactData.basicData.phoneNumbers[0].value + if let vCardData = contactData.serializedVCard() { + let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peer?.id, vCardData: vCardData) + let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + }) + } + }, nil) + + var enqueueMessages: [EnqueueMessage] = [] + if let textEnqueueMessage = textEnqueueMessage { + enqueueMessages.append(textEnqueueMessage) + } + enqueueMessages.append(.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil)) + strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)) + } + }), completed: nil, cancelled: nil) + strongSelf.effectiveNavigationController?.pushViewController(contactController) + } + } + })) + } } + })) + case .poll: + let controller = strongSelf.configurePollCreation() + completion(controller, nil) + strongSelf.controllerNavigationDisposable.set(nil) + case let .app(botId, botName, botIcon): + let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: peer.id, botId: botId, botName: botName, url: nil, queryId: nil, buttonText: nil, keepAliveSignal: nil, replyToMessageId: replyMessageId, iconFile: botIcon) + controller.getNavigationController = { [weak self] in + return self?.effectiveNavigationController } - })) - case .poll: - let controller = strongSelf.configurePollCreation() - completion(controller, nil) - strongSelf.controllerNavigationDisposable.set(nil) - case let .app(botId, botName, botIcon): - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId - let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: peer.id, botId: botId, botName: botName, url: nil, queryId: nil, buttonText: nil, keepAliveSignal: nil, replyToMessageId: replyMessageId, iconFile: botIcon) - controller.getNavigationController = { [weak self] in - return self?.effectiveNavigationController + completion(controller, nil) + strongSelf.controllerNavigationDisposable.set(nil) } - completion(controller, nil) - strongSelf.controllerNavigationDisposable.set(nil) } - } - let present = { - self.present(attachmentController, in: .window(.root)) - self.attachmentController = attachmentController - } - - if inputIsActive { - Queue.mainQueue().after(0.15, { + let present = { + strongSelf.present(attachmentController, in: .window(.root)) + strongSelf.attachmentController = attachmentController + } + + if inputIsActive { + Queue.mainQueue().after(0.15, { + present() + }) + } else { present() - }) - } else { - present() - } + } + }) } private func oldPresentAttachmentMenu(editMediaOptions: MessageMediaEditingOptions?, editMediaReference: AnyMediaReference?) { diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift index 14b6960db0..1b2dd9f8b8 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift @@ -1269,7 +1269,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } badgeContent = .text(inset: 0.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: string) } - var animated: Bool = animated + var animated = animated if let updatingMedia = attributes.updatingMedia, case .update = updatingMedia.media { state = .progress(color: messageTheme.mediaOverlayControlColors.foregroundColor, lineWidth: nil, value: CGFloat(updatingMedia.progress), cancelEnabled: true, animateRotation: true) } else if var fetchStatus = self.fetchStatus { @@ -1497,6 +1497,14 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio self.statusNode = nil removeStatusNode = true } + + var animated = animated + if case .download = statusNode.state, case .progress = state { + animated = true + } else if case .progress = statusNode.state, case .download = state { + animated = true + } + statusNode.transitionToState(state, animated: animated, completion: { [weak statusNode] in if removeStatusNode { statusNode?.removeFromSupernode() diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 73ef493e7d..2cc3abb37d 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -554,7 +554,11 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur let _ = (context.engine.messages.attachMenuBots() |> deliverOnMainQueue).start(next: { attachMenuBots in if let _ = attachMenuBots.firstIndex(where: { $0.peer.id == peerId }) { - presentError(presentationData.strings.WebApp_AddToAttachmentAlreadyAddedError) + if let navigationController = navigationController, case let .chat(chatPeerId, _) = urlContext { + let _ = context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(id: chatPeerId), attachBotId: peerId, useExisting: true)) + } else { + presentError(presentationData.strings.WebApp_AddToAttachmentAlreadyAddedError) + } } else { let _ = (context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) @@ -581,6 +585,8 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur presentError(presentationData.strings.Login_UnknownError) } } + }, error: { _ in + presentError(presentationData.strings.Login_UnknownError) }) }) } diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index 6a779cd231..7b1fc0164e 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -710,7 +710,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur if let path = parsedUrl.pathComponents.last { var section: ResolvedUrlSettingsSection? switch path { - case "theme": + case "themes": section = .theme case "devices": section = .devices diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 5ff97959bb..b4a1b0deee 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -45,6 +45,68 @@ public func generateWebAppThemeParams(_ presentationTheme: PresentationTheme) -> ] } +private final class LoadingProgressNode: ASDisplayNode { + var color: UIColor { + didSet { + self.foregroundNode.backgroundColor = self.color + } + } + + private let foregroundNode: ASDisplayNode + + init(color: UIColor) { + self.color = color + + self.foregroundNode = ASDisplayNode() + self.foregroundNode.backgroundColor = color + + super.init() + + self.addSubnode(self.foregroundNode) + } + + private var _progress: CGFloat = 0.0 + func updateProgress(_ progress: CGFloat, animated: Bool = false) { + if self._progress == progress && animated { + return + } + + var animated = animated + if (progress < self._progress && animated) { + animated = false + } + + let size = self.bounds.size + + self._progress = progress + + let transition: ContainedViewLayoutTransition + if animated && progress > 0.0 { + transition = .animated(duration: 0.7, curve: .spring) + } else { + transition = .immediate + } + + let alpaTransition: ContainedViewLayoutTransition + if animated { + alpaTransition = .animated(duration: 0.3, curve: .easeInOut) + } else { + alpaTransition = .immediate + } + + transition.updateFrame(node: self.foregroundNode, frame: CGRect(x: -2.0, y: 0.0, width: (size.width + 4.0) * progress, height: size.height)) + + let alpha: CGFloat = progress < 0.001 || progress > 0.999 ? 0.0 : 1.0 + alpaTransition.updateAlpha(node: self.foregroundNode, alpha: alpha) + } + + override func layout() { + super.layout() + + self.foregroundNode.cornerRadius = self.frame.height / 2.0 + } +} + public final class WebAppController: ViewController, AttachmentContainable { public var requestAttachmentMenuExpansion: () -> Void = { } public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } @@ -59,6 +121,8 @@ public final class WebAppController: ViewController, AttachmentContainable { private var placeholderIcon: UIImage? private var placeholderNode: ShimmerEffectNode? + private let loadingProgressNode: LoadingProgressNode + private let context: AccountContext var presentationData: PresentationData private let present: (ViewController, Any?) -> Void @@ -73,6 +137,8 @@ public final class WebAppController: ViewController, AttachmentContainable { self.presentationData = controller.presentationData self.present = present + self.loadingProgressNode = LoadingProgressNode(color: presentationData.theme.rootController.tabBar.selectedIconColor) + super.init() if self.presentationData.theme.list.plainBackgroundColor.rgb == 0x000000 { @@ -133,12 +199,17 @@ public final class WebAppController: ViewController, AttachmentContainable { } webView.allowsBackForwardNavigationGestures = false webView.scrollView.delegate = self + webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: [], context: nil) self.webView = webView let placeholderNode = ShimmerEffectNode() self.addSubnode(placeholderNode) self.placeholderNode = placeholderNode + if controller.buttonText == nil { + self.addSubnode(self.loadingProgressNode) + } + if let iconFile = controller.iconFile { let _ = freeMediaFileInteractiveFetched(account: self.context.account, fileReference: .standalone(media: iconFile)).start() self.iconDisposable = (svgIconImageFile(account: self.context.account, fileReference: .standalone(media: iconFile)) @@ -156,22 +227,24 @@ public final class WebAppController: ViewController, AttachmentContainable { }) } - if let url = controller.url, let queryId = controller.queryId, let keepAliveSignal = controller.keepAliveSignal { - self.queryId = queryId + if let url = controller.url { + self.queryId = controller.queryId if let parsedUrl = URL(string: url) { self.webView?.load(URLRequest(url: parsedUrl)) } - self.keepAliveDisposable = (keepAliveSignal - |> deliverOnMainQueue).start(error: { [weak self] _ in - if let strongSelf = self { - strongSelf.controller?.dismiss() - } - }, completed: { [weak self] in - if let strongSelf = self { - strongSelf.controller?.dismiss() - } - }) + if let keepAliveSignal = controller.keepAliveSignal { + self.keepAliveDisposable = (keepAliveSignal + |> deliverOnMainQueue).start(error: { [weak self] _ in + if let strongSelf = self { + strongSelf.controller?.dismiss() + } + }, completed: { [weak self] in + if let strongSelf = self { + strongSelf.controller?.dismiss() + } + }) + } } else { let _ = (context.engine.messages.requestWebView(peerId: controller.peerId, botId: controller.botId, url: controller.url, themeParams: generateWebAppThemeParams(presentationData.theme), replyToMessageId: controller.replyToMessageId) |> deliverOnMainQueue).start(next: { [weak self] result in @@ -205,6 +278,8 @@ public final class WebAppController: ViewController, AttachmentContainable { deinit { self.iconDisposable?.dispose() self.keepAliveDisposable?.dispose() + + self.webView?.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress)) } override func didLoad() { @@ -272,14 +347,23 @@ public final class WebAppController: ViewController, AttachmentContainable { let height: CGFloat if case .compact = layout.metrics.widthClass { - height = layout.size.height - attachmentDefaultTopInset(layout: layout) - 56.0 + height = layout.size.height - attachmentDefaultTopInset(layout: layout) - layout.intrinsicInsets.bottom - 14.0 } else { - height = layout.size.height - 56.0 + height = layout.size.height - layout.intrinsicInsets.bottom } let placeholderFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - iconSize.width) / 2.0), y: floorToScreenPixels((height - iconSize.height) / 2.0)), size: iconSize) transition.updateFrame(node: placeholderNode, frame: placeholderFrame) placeholderNode.updateAbsoluteRect(placeholderFrame, within: layout.size) + + let loadingProgressHeight: CGFloat = 2.0 + transition.updateFrame(node: self.loadingProgressNode, frame: CGRect(origin: CGPoint(x: 0.0, y: height - loadingProgressHeight), size: CGSize(width: layout.size.width, height: loadingProgressHeight))) + } + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if keyPath == "estimatedProgress", let webView = self.webView { + self.loadingProgressNode.updateProgress(webView.estimatedProgress, animated: true) } }