diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 33d919d785..4f4f791b3f 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -805,15 +805,18 @@ public struct StoryCameraTransitionOut { public weak var destinationView: UIView? public let destinationRect: CGRect public let destinationCornerRadius: CGFloat + public let completion: (() -> Void)? public init( destinationView: UIView, destinationRect: CGRect, - destinationCornerRadius: CGFloat + destinationCornerRadius: CGFloat, + completion: (() -> Void)? = nil ) { self.destinationView = destinationView self.destinationRect = destinationRect self.destinationCornerRadius = destinationCornerRadius + self.completion = completion } } @@ -909,6 +912,12 @@ public struct ChatControllerParams { } } +public enum ChatOpenWebViewSource: Equatable { + case generic + case menu + case inline(bot: EnginePeer) +} + public protocol SharedAccountContext: AnyObject { var sharedContainerPath: String { get } var basePath: String { get } @@ -1075,6 +1084,7 @@ public protocol SharedAccountContext: AnyObject { func makeStarsAmountScreen(context: AccountContext, initialValue: Int64?, completion: @escaping (Int64) -> Void) -> ViewController func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController + func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool) func makeDebugSettingsController(context: AccountContext?) -> ViewController? diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 25abf72f66..6f38e5b00e 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -951,6 +951,7 @@ public protocol PeerInfoScreen: ViewController { func openBirthdaySetup() func toggleStorySelection(ids: [Int32], isSelected: Bool) + func togglePaneIsReordering(isReordering: Bool) func cancelItemSelection() } diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index 6cc0ed6c59..b20d77d6f0 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -141,387 +141,398 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch }))) items.append(.separator) } - } - } - - let isSavedMessages = peerId == context.account.peerId - - if !isSavedMessages, case let .user(peer) = peer, !peer.flags.contains(.isSupport), peer.botInfo == nil && !peer.isDeleted { - if !isContact { - items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_AddToContacts, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: theme.contextMenu.primaryColor) }, action: { _, f in - context.sharedContext.openAddPersonContact(context: context, peerId: peerId, pushController: { controller in - if let navigationController = chatListController?.navigationController as? NavigationController { - navigationController.pushViewController(controller) - } - }, present: { c, a in - if let chatListController = chatListController { - chatListController.present(c, in: .window(.root), with: a) - } + case .recentApps: + items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_RemoveFromRecents, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.contextMenu.destructiveColor) }, action: { _, f in + let _ = (context.engine.peers.removeRecentlyUsedApp(peerId: peerId) + |> deliverOnMainQueue).startStandalone(completed: { + f(.default) }) - f(.default) }))) items.append(.separator) } } - var isMuted = false - if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { - isMuted = true - } else if case .default = notificationSettings.muteState { - if case .user = peer { - isMuted = !globalNotificationSettings.privateChats.enabled - } else if case .legacyGroup = peer { - isMuted = !globalNotificationSettings.groupChats.enabled - } else if case let .channel(channel) = peer { - switch channel.info { - case .group: - isMuted = !globalNotificationSettings.groupChats.enabled - case .broadcast: - isMuted = !globalNotificationSettings.channels.enabled + if case .search(.recentApps) = source { + } else { + let isSavedMessages = peerId == context.account.peerId + + if !isSavedMessages, case let .user(peer) = peer, !peer.flags.contains(.isSupport), peer.botInfo == nil && !peer.isDeleted { + if !isContact { + items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_AddToContacts, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: theme.contextMenu.primaryColor) }, action: { _, f in + context.sharedContext.openAddPersonContact(context: context, peerId: peerId, pushController: { controller in + if let navigationController = chatListController?.navigationController as? NavigationController { + navigationController.pushViewController(controller) + } + }, present: { c, a in + if let chatListController = chatListController { + chatListController.present(c, in: .window(.root), with: a) + } + }) + f(.default) + }))) + items.append(.separator) } } - } - - var isUnread = false - if readCounters.isUnread { - isUnread = true - } - - var isForum = false - if case let .channel(channel) = peer, channel.flags.contains(.isForum) { - isForum = true - } - - var hasRemoveFromFolder = false - if case let .chatList(currentFilter) = source { - if let currentFilter = currentFilter, case let .filter(id, title, emoticon, data) = currentFilter { - items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_RemoveFromFolder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/RemoveFromFolder"), color: theme.contextMenu.primaryColor) }, action: { c, _ in - let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in - var filters = filters - for i in 0 ..< filters.count { - if filters[i].id == currentFilter.id { - var updatedData = data - let _ = updatedData.addExcludePeer(peerId: peer.id) - filters[i] = .filter(id: id, title: title, emoticon: emoticon, data: updatedData) - break + + var isMuted = false + if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { + isMuted = true + } else if case .default = notificationSettings.muteState { + if case .user = peer { + isMuted = !globalNotificationSettings.privateChats.enabled + } else if case .legacyGroup = peer { + isMuted = !globalNotificationSettings.groupChats.enabled + } else if case let .channel(channel) = peer { + switch channel.info { + case .group: + isMuted = !globalNotificationSettings.groupChats.enabled + case .broadcast: + isMuted = !globalNotificationSettings.channels.enabled + } + } + } + + var isUnread = false + if readCounters.isUnread { + isUnread = true + } + + var isForum = false + if case let .channel(channel) = peer, channel.flags.contains(.isForum) { + isForum = true + } + + var hasRemoveFromFolder = false + if case let .chatList(currentFilter) = source { + if let currentFilter = currentFilter, case let .filter(id, title, emoticon, data) = currentFilter { + items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_RemoveFromFolder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/RemoveFromFolder"), color: theme.contextMenu.primaryColor) }, action: { c, _ in + let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in + var filters = filters + for i in 0 ..< filters.count { + if filters[i].id == currentFilter.id { + var updatedData = data + let _ = updatedData.addExcludePeer(peerId: peer.id) + filters[i] = .filter(id: id, title: title, emoticon: emoticon, data: updatedData) + break + } + } + return filters + } + |> deliverOnMainQueue).startStandalone(completed: { + c?.dismiss(completion: { + chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatRemovedFromFolder(chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + return false + }), in: .current) + }) + }) + }))) + hasRemoveFromFolder = true + } + } + + if !hasRemoveFromFolder && peerGroup != nil { + var hasFolders = false + + for case let .filter(_, _, _, data) in filters { + let predicate = chatListFilterPredicate(filter: data, accountPeerId: context.account.peerId) + if predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: false) { + continue + } + + var data = data + if data.addIncludePeer(peerId: peer.id) { + hasFolders = true + break + } + } + + if hasFolders { + items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_AddToFolder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Folder"), color: theme.contextMenu.primaryColor) }, action: { c, _ in + var updatedItems: [ContextMenuItem] = [] + + for filter in filters { + if case let .filter(_, title, _, data) = filter { + let predicate = chatListFilterPredicate(filter: data, accountPeerId: context.account.peerId) + if predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: false) { + continue + } + + var data = data + if !data.addIncludePeer(peerId: peer.id) { + continue + } + + let filterType = chatListFilterType(data) + updatedItems.append(.action(ContextMenuActionItem(text: title, icon: { theme in + let imageName: String + switch filterType { + case .generic: + imageName = "Chat/Context Menu/List" + case .unmuted: + imageName = "Chat/Context Menu/Unmute" + case .unread: + imageName = "Chat/Context Menu/MarkAsUnread" + case .channels: + imageName = "Chat/Context Menu/Channels" + case .groups: + imageName = "Chat/Context Menu/Groups" + case .bots: + imageName = "Chat/Context Menu/Bots" + case .contacts: + imageName = "Chat/Context Menu/User" + case .nonContacts: + imageName = "Chat/Context Menu/UnknownUser" + } + return generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.contextMenu.primaryColor) + }, action: { c, f in + c?.dismiss(completion: { + let isPremium = limitsData.0?.isPremium ?? false + let (_, limits, premiumLimits) = limitsData + + let limit = limits.maxFolderChatsCount + let premiumLimit = premiumLimits.maxFolderChatsCount + + let count = data.includePeers.peers.count - 1 + if count >= premiumLimit { + let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: { + return true + }) + chatListController?.push(controller) + return + } else if count >= limit && !isPremium { + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: { + let controller = PremiumIntroScreen(context: context, source: .chatsPerFolder) + replaceImpl?(controller) + return true + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + chatListController?.push(controller) + return + } + + let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in + var filters = filters + for i in 0 ..< filters.count { + if filters[i].id == filter.id { + if case let .filter(id, title, emoticon, data) = filter { + var updatedData = data + let _ = updatedData.addIncludePeer(peerId: peer.id) + filters[i] = .filter(id: id, title: title, emoticon: emoticon, data: updatedData) + } + break + } + } + return filters + }).startStandalone() + + chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatAddedToFolder(chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + return false + }), in: .current) + }) + }))) } } - return filters - } - |> deliverOnMainQueue).startStandalone(completed: { - c?.dismiss(completion: { - chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatRemovedFromFolder(chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title), elevatedLayout: false, animateInAsReplacement: true, action: { _ in - return false - }), in: .current) - }) - }) + + updatedItems.append(.separator) + updatedItems.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) + }, iconPosition: .left, action: { c, _ in + c?.setItems(chatContextMenuItems(context: context, peerId: peerId, promoInfo: promoInfo, source: source, chatListController: chatListController, joined: joined) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) + }))) + + c?.setItems(.single(ContextController.Items(content: .list(updatedItems))), minHeight: nil, animated: true) + }))) + } + } + + if isUnread { + items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_MarkAsRead, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MarkAsRead"), color: theme.contextMenu.primaryColor) }, action: { _, f in + let _ = context.engine.messages.togglePeersUnreadMarkInteractively(peerIds: [peerId], setToValue: nil).startStandalone() + f(.default) + }))) + } else if !isForum { + items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_MarkAsUnread, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MarkAsUnread"), color: theme.contextMenu.primaryColor) }, action: { _, f in + let _ = context.engine.messages.togglePeersUnreadMarkInteractively(peerIds: [peerId], setToValue: nil).startStandalone() + f(.default) }))) - hasRemoveFromFolder = true } - } - - if !hasRemoveFromFolder && peerGroup != nil { - var hasFolders = false - - for case let .filter(_, _, _, data) in filters { - let predicate = chatListFilterPredicate(filter: data, accountPeerId: context.account.peerId) - if predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: false) { - continue - } - - var data = data - if data.addIncludePeer(peerId: peer.id) { - hasFolders = true - break - } - } - - if hasFolders { - items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_AddToFolder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Folder"), color: theme.contextMenu.primaryColor) }, action: { c, _ in - var updatedItems: [ContextMenuItem] = [] - - for filter in filters { - if case let .filter(_, title, _, data) = filter { - let predicate = chatListFilterPredicate(filter: data, accountPeerId: context.account.peerId) - if predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: false) { - continue + + let archiveEnabled = !isSavedMessages && peerId != PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(777000)) && peerId == context.account.peerId + if let group = peerGroup { + if archiveEnabled { + let isArchived = group == .archive + items.append(.action(ContextMenuActionItem(text: isArchived ? strings.ChatList_Context_Unarchive : strings.ChatList_Context_Archive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isArchived ? "Chat/Context Menu/Unarchive" : "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor) }, action: { _, f in + if isArchived { + let _ = (context.engine.peers.updatePeersGroupIdInteractively(peerIds: [peerId], groupId: .root) + |> deliverOnMainQueue).startStandalone(completed: { + f(.default) + }) + } else { + if let chatListController = chatListController { + chatListController.archiveChats(peerIds: [peerId]) + f(.default) + } else { + let _ = (context.engine.peers.updatePeersGroupIdInteractively(peerIds: [peerId], groupId: .archive) + |> deliverOnMainQueue).startStandalone(completed: { + f(.default) + }) } - - var data = data - if !data.addIncludePeer(peerId: peer.id) { - continue - } - - let filterType = chatListFilterType(data) - updatedItems.append(.action(ContextMenuActionItem(text: title, icon: { theme in - let imageName: String - switch filterType { - case .generic: - imageName = "Chat/Context Menu/List" - case .unmuted: - imageName = "Chat/Context Menu/Unmute" - case .unread: - imageName = "Chat/Context Menu/MarkAsUnread" - case .channels: - imageName = "Chat/Context Menu/Channels" - case .groups: - imageName = "Chat/Context Menu/Groups" - case .bots: - imageName = "Chat/Context Menu/Bots" - case .contacts: - imageName = "Chat/Context Menu/User" - case .nonContacts: - imageName = "Chat/Context Menu/UnknownUser" - } - return generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.contextMenu.primaryColor) - }, action: { c, f in - c?.dismiss(completion: { - let isPremium = limitsData.0?.isPremium ?? false - let (_, limits, premiumLimits) = limitsData - - let limit = limits.maxFolderChatsCount - let premiumLimit = premiumLimits.maxFolderChatsCount - - let count = data.includePeers.peers.count - 1 - if count >= premiumLimit { + } + }))) + } + + if isPinned || chatListFilter == nil || peerId.namespace != Namespaces.Peer.SecretChat { + items.append(.action(ContextMenuActionItem(text: isPinned ? strings.ChatList_Context_Unpin : strings.ChatList_Context_Pin, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isPinned ? "Chat/Context Menu/Unpin" : "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { c, f in + let _ = (context.engine.peers.toggleItemPinned(location: location, itemId: .peer(peerId)) + |> deliverOnMainQueue).startStandalone(next: { result in + switch result { + case .done: + f(.default) + case let .limitExceeded(count, _): + f(.default) + + let isPremium = limitsData.0?.isPremium ?? false + if isPremium { + if case .filter = location { let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: { return true }) chatListController?.push(controller) - return - } else if count >= limit && !isPremium { - var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: { - let controller = PremiumIntroScreen(context: context, source: .chatsPerFolder) - replaceImpl?(controller) + } else { + let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: { return true }) + chatListController?.push(controller) + } + } else { + if case .filter = location { + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: { + let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats) + replaceImpl?(premiumScreen) + return true + }) + chatListController?.push(controller) replaceImpl = { [weak controller] c in controller?.replace(with: c) } + } else { + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: { + let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats) + replaceImpl?(premiumScreen) + return true + }) chatListController?.push(controller) - return - } - - let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in - var filters = filters - for i in 0 ..< filters.count { - if filters[i].id == filter.id { - if case let .filter(id, title, emoticon, data) = filter { - var updatedData = data - let _ = updatedData.addIncludePeer(peerId: peer.id) - filters[i] = .filter(id: id, title: title, emoticon: emoticon, data: updatedData) - } - break - } + replaceImpl = { [weak controller] c in + controller?.replace(with: c) } - return filters - }).startStandalone() - - chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatAddedToFolder(chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title), elevatedLayout: false, animateInAsReplacement: true, action: { _ in - return false - }), in: .current) - }) - }))) + } + } + } + }) + }))) + } + + if !isSavedMessages { + var isMuted = false + if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { + isMuted = true + } else if case .default = notificationSettings.muteState { + if case .user = peer { + isMuted = !globalNotificationSettings.privateChats.enabled + } else if case .legacyGroup = peer { + isMuted = !globalNotificationSettings.groupChats.enabled + } else if case let .channel(channel) = peer { + switch channel.info { + case .group: + isMuted = !globalNotificationSettings.groupChats.enabled + case .broadcast: + isMuted = !globalNotificationSettings.channels.enabled + } } } - - updatedItems.append(.separator) - updatedItems.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Back, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) - }, iconPosition: .left, action: { c, _ in - c?.setItems(chatContextMenuItems(context: context, peerId: peerId, promoInfo: promoInfo, source: source, chatListController: chatListController, joined: joined) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) - }))) - - c?.setItems(.single(ContextController.Items(content: .list(updatedItems))), minHeight: nil, animated: true) - }))) - } - } - - if isUnread { - items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_MarkAsRead, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MarkAsRead"), color: theme.contextMenu.primaryColor) }, action: { _, f in - let _ = context.engine.messages.togglePeersUnreadMarkInteractively(peerIds: [peerId], setToValue: nil).startStandalone() - f(.default) - }))) - } else if !isForum { - items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_MarkAsUnread, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MarkAsUnread"), color: theme.contextMenu.primaryColor) }, action: { _, f in - let _ = context.engine.messages.togglePeersUnreadMarkInteractively(peerIds: [peerId], setToValue: nil).startStandalone() - f(.default) - }))) - } - - let archiveEnabled = !isSavedMessages && peerId != PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(777000)) && peerId == context.account.peerId - if let group = peerGroup { - if archiveEnabled { - let isArchived = group == .archive - items.append(.action(ContextMenuActionItem(text: isArchived ? strings.ChatList_Context_Unarchive : strings.ChatList_Context_Archive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isArchived ? "Chat/Context Menu/Unarchive" : "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor) }, action: { _, f in - if isArchived { - let _ = (context.engine.peers.updatePeersGroupIdInteractively(peerIds: [peerId], groupId: .root) - |> deliverOnMainQueue).startStandalone(completed: { + items.append(.action(ContextMenuActionItem(text: isMuted ? strings.ChatList_Context_Unmute : strings.ChatList_Context_Mute, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor) }, action: { _, f in + let _ = (context.engine.peers.togglePeerMuted(peerId: peerId, threadId: nil) + |> deliverOnMainQueue).startStandalone(completed: { f(.default) }) - } else { - if let chatListController = chatListController { - chatListController.archiveChats(peerIds: [peerId]) - f(.default) - } else { - let _ = (context.engine.peers.updatePeersGroupIdInteractively(peerIds: [peerId], groupId: .archive) - |> deliverOnMainQueue).startStandalone(completed: { - f(.default) - }) - } - } - }))) - } - - if isPinned || chatListFilter == nil || peerId.namespace != Namespaces.Peer.SecretChat { - items.append(.action(ContextMenuActionItem(text: isPinned ? strings.ChatList_Context_Unpin : strings.ChatList_Context_Pin, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isPinned ? "Chat/Context Menu/Unpin" : "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { c, f in - let _ = (context.engine.peers.toggleItemPinned(location: location, itemId: .peer(peerId)) - |> deliverOnMainQueue).startStandalone(next: { result in - switch result { - case .done: - f(.default) - case let .limitExceeded(count, _): - f(.default) - - let isPremium = limitsData.0?.isPremium ?? false - if isPremium { - if case .filter = location { - let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: { - return true - }) - chatListController?.push(controller) - } else { - let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: { - return true - }) - chatListController?.push(controller) - } - } else { - if case .filter = location { - var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: { - let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats) - replaceImpl?(premiumScreen) - return true - }) - chatListController?.push(controller) - replaceImpl = { [weak controller] c in - controller?.replace(with: c) - } - } else { - var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: { - let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats) - replaceImpl?(premiumScreen) - return true - }) - chatListController?.push(controller) - replaceImpl = { [weak controller] c in - controller?.replace(with: c) - } - } - } - } - }) - }))) - } - - if !isSavedMessages { - var isMuted = false - if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { - isMuted = true - } else if case .default = notificationSettings.muteState { - if case .user = peer { - isMuted = !globalNotificationSettings.privateChats.enabled - } else if case .legacyGroup = peer { - isMuted = !globalNotificationSettings.groupChats.enabled - } else if case let .channel(channel) = peer { - switch channel.info { - case .group: - isMuted = !globalNotificationSettings.groupChats.enabled - case .broadcast: - isMuted = !globalNotificationSettings.channels.enabled - } - } - } - items.append(.action(ContextMenuActionItem(text: isMuted ? strings.ChatList_Context_Unmute : strings.ChatList_Context_Mute, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor) }, action: { _, f in - let _ = (context.engine.peers.togglePeerMuted(peerId: peerId, threadId: nil) - |> deliverOnMainQueue).startStandalone(completed: { - f(.default) - }) - }))) - } - } else { - if case .search = source { - if case let .channel(peer) = peer { - let text: String - if case .broadcast = peer.info { - text = strings.ChatList_Context_JoinChannel - } else { - text = strings.ChatList_Context_JoinChat - } - items.append(.action(ContextMenuActionItem(text: text, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) }, action: { _, f in - var createSignal = context.peerChannelMemberCategoriesContextsManager.join(engine: context.engine, peerId: peerId, hash: nil) - var cancelImpl: (() -> Void)? - let progressSignal = Signal { subscriber in - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { - cancelImpl?() - })) - chatListController?.present(controller, in: .window(.root)) - return ActionDisposable { [weak controller] in - Queue.mainQueue().async() { - controller?.dismiss() - } - } - } - |> runOn(Queue.mainQueue()) - |> delay(0.15, queue: Queue.mainQueue()) - let progressDisposable = progressSignal.start() - - createSignal = createSignal - |> afterDisposed { - Queue.mainQueue().async { - progressDisposable.dispose() - } - } - let joinChannelDisposable = MetaDisposable() - cancelImpl = { - joinChannelDisposable.set(nil) - } - - joinChannelDisposable.set((createSignal - |> deliverOnMainQueue).start(next: { _ in - }, error: { _ in - if let chatListController = chatListController { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - chatListController.present(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - } - }, completed: { - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) - |> deliverOnMainQueue).startStandalone(next: { peer in - guard let peer = peer else { - return - } - if let navigationController = (chatListController?.navigationController as? NavigationController) { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer))) - } - }) - })) - f(.default) }))) } - } - } - - if case .chatList = source, peerGroup != nil { - items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { _, f in - if let chatListController = chatListController { - chatListController.deletePeerChat(peerId: peerId, joined: joined) + } else { + if case .search = source { + if case let .channel(peer) = peer { + let text: String + if case .broadcast = peer.info { + text = strings.ChatList_Context_JoinChannel + } else { + text = strings.ChatList_Context_JoinChat + } + items.append(.action(ContextMenuActionItem(text: text, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) }, action: { _, f in + var createSignal = context.peerChannelMemberCategoriesContextsManager.join(engine: context.engine, peerId: peerId, hash: nil) + var cancelImpl: (() -> Void)? + let progressSignal = Signal { subscriber in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + chatListController?.present(controller, in: .window(.root)) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.15, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + + createSignal = createSignal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + let joinChannelDisposable = MetaDisposable() + cancelImpl = { + joinChannelDisposable.set(nil) + } + + joinChannelDisposable.set((createSignal + |> deliverOnMainQueue).start(next: { _ in + }, error: { _ in + if let chatListController = chatListController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + chatListController.present(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + } + }, completed: { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> deliverOnMainQueue).startStandalone(next: { peer in + guard let peer = peer else { + return + } + if let navigationController = (chatListController?.navigationController as? NavigationController) { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer))) + } + }) + })) + f(.default) + }))) + } } - f(.default) - }))) + } + + if case .chatList = source, peerGroup != nil { + items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { _, f in + if let chatListController = chatListController { + chatListController.deletePeerChat(peerId: peerId, joined: joined) + } + f(.default) + }))) + } } if let item = items.last, case .separator = item { diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 3572d2f62a..4c55b16c03 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -173,7 +173,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo self.dimNode.backgroundColor = UIColor.black.withAlphaComponent(0.5) self.filterContainerNode = ChatListSearchFiltersContainerNode() - self.paneContainerNode = ChatListSearchPaneContainerNode(context: context, animationCache: animationCache, animationRenderer: animationRenderer, updatedPresentationData: updatedPresentationData, peersFilter: self.peersFilter, requestPeerType: self.requestPeerType, location: location, searchQuery: self.searchQuery.get(), searchOptions: self.searchOptions.get(), navigationController: navigationController) + self.paneContainerNode = ChatListSearchPaneContainerNode(context: context, animationCache: animationCache, animationRenderer: animationRenderer, updatedPresentationData: updatedPresentationData, peersFilter: self.peersFilter, requestPeerType: self.requestPeerType, location: location, searchQuery: self.searchQuery.get(), searchOptions: self.searchOptions.get(), navigationController: navigationController, parentController: parentController()) self.paneContainerNode.clipsToBounds = true super.init() diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 5596602d70..8e17694c38 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -289,7 +289,7 @@ private enum ChatListRecentEntry: Comparable, Identifiable { header: header, action: { _ in if let chatPeer = peer.peer.peers[peer.peer.peerId] { - peerSelected(EnginePeer(chatPeer), nil, section == .recommendedChannels) + peerSelected(EnginePeer(chatPeer), nil, section == .recommendedChannels || section == .popularApps) } }, disabledAction: { _ in @@ -298,10 +298,18 @@ private enum ChatListRecentEntry: Comparable, Identifiable { } }, deletePeer: deletePeer, - contextAction: (key == .channels || key == .apps) ? nil : peerContextAction.flatMap { peerContextAction in + contextAction: (key == .channels) ? nil : peerContextAction.flatMap { peerContextAction in return { node, gesture, location in if let chatPeer = peer.peer.peers[peer.peer.peerId] { - peerContextAction(EnginePeer(chatPeer), .recentSearch, node, gesture, location) + let source: ChatListSearchContextActionSource + + if key == .apps { + source = .recentApps + } else { + source = .recentSearch + } + + peerContextAction(EnginePeer(chatPeer), source, node, gesture, location) } else { gesture?.cancel() } @@ -1072,6 +1080,7 @@ private struct ChatListSearchMessagesContext { public enum ChatListSearchContextActionSource { case recentPeers case recentSearch + case recentApps case search(EngineMessage.Id?) } @@ -1249,6 +1258,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private let tagMask: EngineMessage.Tags? private let location: ChatListControllerLocation private let navigationController: NavigationController? + private weak var parentController: ViewController? private let recentListNode: ListView private let shimmerNode: ChatListSearchShimmerNode @@ -1321,7 +1331,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private var searchQueryDisposable: Disposable? private var searchOptionsDisposable: Disposable? - init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, interaction: ChatListSearchInteraction, key: ChatListSearchPaneKey, peersFilter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?, globalPeerSearchContext: GlobalPeerSearchContext?) { + init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, interaction: ChatListSearchInteraction, key: ChatListSearchPaneKey, peersFilter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?, parentController: ViewController?, globalPeerSearchContext: GlobalPeerSearchContext?) { self.context = context self.animationCache = animationCache self.animationRenderer = animationRenderer @@ -1329,6 +1339,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { self.key = key self.location = location self.navigationController = navigationController + self.parentController = parentController let globalPeerSearchContext = globalPeerSearchContext ?? GlobalPeerSearchContext() @@ -3531,22 +3542,32 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } } else if case .apps = key { if let navigationController = self.navigationController { - var customChatNavigationStack: [EnginePeer.Id]? if isRecommended { - if let recommendedChannelOrder = previousRecentItemsValue.with({ $0 })?.recommendedChannelOrder { - var customChatNavigationStackValue: [EnginePeer.Id] = [] - customChatNavigationStackValue.append(contentsOf: recommendedChannelOrder) - customChatNavigationStack = customChatNavigationStackValue + if let peerInfoScreen = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + navigationController.pushViewController(peerInfoScreen) } + } else if case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), let parentController = self.parentController { + self.context.sharedContext.openWebApp( + context: self.context, + parentController: parentController, + updatedPresentationData: nil, + peer: peer, + threadId: nil, + buttonText: "", + url: "", + simple: true, + source: .generic, + skipTermsOfService: true + ) + } else { + + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams( + navigationController: navigationController, + context: self.context, + chatLocation: .peer(peer), + keepStack: .always + )) } - - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams( - navigationController: navigationController, - context: self.context, - chatLocation: .peer(peer), - keepStack: .always, - customChatNavigationStack: customChatNavigationStack - )) } } else { interaction.openPeer(peer, nil, threadId, true) diff --git a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift index 8605ead568..e6ae53e812 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift @@ -126,6 +126,7 @@ private final class ChatListSearchPendingPane { updatedPresentationData: (initial: PresentationData, signal: Signal)?, interaction: ChatListSearchInteraction, navigationController: NavigationController?, + parentController: ViewController?, peersFilter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, @@ -135,7 +136,7 @@ private final class ChatListSearchPendingPane { key: ChatListSearchPaneKey, hasBecomeReady: @escaping (ChatListSearchPaneKey) -> Void ) { - let paneNode = ChatListSearchListPaneNode(context: context, animationCache: animationCache, animationRenderer: animationRenderer, updatedPresentationData: updatedPresentationData, interaction: interaction, key: key, peersFilter: (key == .chats || key == .topics) ? peersFilter : [], requestPeerType: requestPeerType, location: location, searchQuery: searchQuery, searchOptions: searchOptions, navigationController: navigationController, globalPeerSearchContext: globalPeerSearchContext) + let paneNode = ChatListSearchListPaneNode(context: context, animationCache: animationCache, animationRenderer: animationRenderer, updatedPresentationData: updatedPresentationData, interaction: interaction, key: key, peersFilter: (key == .chats || key == .topics) ? peersFilter : [], requestPeerType: requestPeerType, location: location, searchQuery: searchQuery, searchOptions: searchOptions, navigationController: navigationController, parentController: parentController, globalPeerSearchContext: globalPeerSearchContext) self.pane = ChatListSearchPaneWrapper(key: key, node: paneNode) self.disposable = (paneNode.isReady @@ -163,6 +164,7 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, ASGestureRecognizerD private let searchOptions: Signal private let globalPeerSearchContext: GlobalPeerSearchContext private let navigationController: NavigationController? + private weak var parentController: ViewController? var interaction: ChatListSearchInteraction? let isReady = Promise() @@ -193,7 +195,7 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, ASGestureRecognizerD private var currentAvailablePanes: [ChatListSearchPaneKey]? - init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peersFilter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?) { + init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peersFilter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?, parentController: ViewController?) { self.context = context self.animationCache = animationCache self.animationRenderer = animationRenderer @@ -204,6 +206,7 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, ASGestureRecognizerD self.searchQuery = searchQuery self.searchOptions = searchOptions self.navigationController = navigationController + self.parentController = parentController self.globalPeerSearchContext = GlobalPeerSearchContext() super.init() @@ -434,6 +437,7 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, ASGestureRecognizerD updatedPresentationData: self.updatedPresentationData, interaction: self.interaction!, navigationController: self.navigationController, + parentController: self.parentController, peersFilter: self.peersFilter, requestPeerType: self.requestPeerType, location: self.location, diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift index 4ba52d7ff2..6f4f9066ee 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift @@ -61,12 +61,6 @@ public enum ChatTranslationDisplayType { case translated } -public enum ChatOpenWebViewSource: Equatable { - case generic - case menu - case inline(bot: EnginePeer) -} - public final class ChatPanelInterfaceInteraction { public let setupReplyMessage: (MessageId?, @escaping (ContainedViewLayoutTransition, @escaping () -> Void) -> Void) -> Void public let setupEditMessage: (MessageId?, @escaping (ContainedViewLayoutTransition) -> Void) -> Void diff --git a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift index 4d02db8394..33ef3ccc3a 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift @@ -842,10 +842,12 @@ private func selectivePrivacySettingsControllerEntries(presentationData: Present } if case .phoneNumber = kind, state.setting == .nobody { - entries.append(.phoneDiscoveryHeader(presentationData.theme, presentationData.strings.PrivacyPhoneNumberSettings_DiscoveryHeader)) - entries.append(.phoneDiscoveryEverybody(presentationData.theme, presentationData.strings.PrivacySettings_LastSeenEverybody, state.phoneDiscoveryEnabled != false)) - entries.append(.phoneDiscoveryMyContacts(presentationData.theme, presentationData.strings.PrivacySettings_LastSeenContacts, state.phoneDiscoveryEnabled == false)) - entries.append(.phoneDiscoveryInfo(presentationData.theme, state.phoneDiscoveryEnabled != false ? presentationData.strings.PrivacyPhoneNumberSettings_CustomPublicLink("+\(phoneNumber)").string : presentationData.strings.PrivacyPhoneNumberSettings_CustomDisabledHelp, phoneLink)) + if state.phoneDiscoveryEnabled == false { + entries.append(.phoneDiscoveryHeader(presentationData.theme, presentationData.strings.PrivacyPhoneNumberSettings_DiscoveryHeader)) + entries.append(.phoneDiscoveryEverybody(presentationData.theme, presentationData.strings.PrivacySettings_LastSeenEverybody, state.phoneDiscoveryEnabled != false)) + entries.append(.phoneDiscoveryMyContacts(presentationData.theme, presentationData.strings.PrivacySettings_LastSeenContacts, state.phoneDiscoveryEnabled == false)) + entries.append(.phoneDiscoveryInfo(presentationData.theme, state.phoneDiscoveryEnabled != false ? presentationData.strings.PrivacyPhoneNumberSettings_CustomPublicLink("+\(phoneNumber)").string : presentationData.strings.PrivacyPhoneNumberSettings_CustomDisabledHelp, phoneLink)) + } } if case .voiceMessages = kind, !isPremium { diff --git a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift index 8e0a317db8..01f9576c49 100644 --- a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift +++ b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift @@ -38,6 +38,7 @@ public protocol SparseItemGridBinding: AnyObject { func unbindLayer(layer: SparseItemGridLayer) func scrollerTextForTag(tag: Int32) -> String? func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal + func reorderIfPossible(item: SparseItemGrid.Item, toIndex: Int) func onTap(item: SparseItemGrid.Item, itemLayer: CALayer, point: CGPoint) func onTagTap() func didScroll() @@ -376,6 +377,48 @@ public final class SparseItemGrid: ASDisplayNode { } } } + + var position: CGPoint { + get { + return self.displayLayer.position + } set(value) { + if let layer = self.layer { + layer.position = value + } else if let view = self.view { + view.center = value + } else { + preconditionFailure() + } + } + } + + var bounds: CGRect { + get { + return self.displayLayer.bounds + } set(value) { + if let layer = self.layer { + layer.bounds = value + } else if let view = self.view { + view.bounds = value + } else { + preconditionFailure() + } + } + } + + var transform: CATransform3D { + get { + return self.displayLayer.transform + } set(value) { + if let layer = self.layer { + layer.transform = value + } else if let view = self.view { + view.layer.transform = value + } else { + preconditionFailure() + } + } + } var needsShimmer: Bool { if let layer = self.layer { @@ -485,6 +528,8 @@ public final class SparseItemGrid: ASDisplayNode { var items: Items? var visibleItems: [AnyHashable: VisibleItem] = [:] var visiblePlaceholders: [SparseItemGridShimmerLayer] = [] + + private var reorderingItem: (id: AnyHashable, initialPosition: CGPoint, position: CGPoint)? private var scrollingArea: SparseItemGridScrollingArea? private var currentScrollingTag: Int32? @@ -492,6 +537,8 @@ public final class SparseItemGrid: ASDisplayNode { private var ignoreScrolling: Bool = false private var isFastScrolling: Bool = false + + private var isReordering: Bool = false private var previousScrollOffset: CGFloat = 0.0 var coveringInsetOffset: CGFloat = 0.0 @@ -532,16 +579,68 @@ public final class SparseItemGrid: ASDisplayNode { self.view.addSubview(self.scrollView) } - func update(containerLayout: ContainerLayout, items: Items, restoreScrollPosition: (y: CGFloat, index: Int)?, synchronous: SparseItemGrid.Synchronous) { + func update(containerLayout: ContainerLayout, items: Items, restoreScrollPosition: (y: CGFloat, index: Int)?, synchronous: SparseItemGrid.Synchronous, transition: ComponentTransition) { if self.layout?.containerLayout != containerLayout || self.items !== items { self.layout = Layout(containerLayout: containerLayout, zoomLevel: self.zoomLevel, itemCount: items.count) self.items = items - self.updateVisibleItems(resetScrolling: true, synchronous: synchronous, restoreScrollPosition: restoreScrollPosition) + self.updateVisibleItems(resetScrolling: true, synchronous: synchronous, restoreScrollPosition: restoreScrollPosition, transition: transition) self.snapCoveringInsetOffset(animated: false) } } + + func setReordering(isReordering: Bool) { + if self.isReordering != isReordering { + self.isReordering = isReordering + + self.updateVisibleItems(resetScrolling: true, synchronous: .semi, restoreScrollPosition: nil, transition: .spring(duration: 0.4)) + } + } + + func setReorderingItem(item: SparseItemGridDisplayItem?) { + var mappedItem: (AnyHashable, VisibleItem)? + if let item, let itemLayer = item.layer { + for (id, visibleItem) in self.visibleItems { + if visibleItem.layer === itemLayer { + mappedItem = (id, visibleItem) + break + } + } + } + + if self.reorderingItem?.id != mappedItem?.0 { + if let (id, visibleItem) = mappedItem, let itemLayer = visibleItem.layer { + self.scrollView.layer.addSublayer(itemLayer) + self.reorderingItem = (id, itemLayer.position, itemLayer.position) + } else { + self.reorderingItem = nil + } + self.updateVisibleItems(resetScrolling: true, synchronous: .semi, restoreScrollPosition: nil, transition: .spring(duration: 0.4)) + } + } + + func moveReorderingItem(distance: CGPoint) { + if let (id, initialPosition, _) = self.reorderingItem { + let targetPosition = CGPoint(x: initialPosition.x + distance.x, y: initialPosition.y + distance.y) + self.reorderingItem = (id, initialPosition, targetPosition) + self.updateVisibleItems(resetScrolling: true, synchronous: .semi, restoreScrollPosition: nil, transition: .immediate) + + if let items = self.items, let visibleReorderingItem = self.visibleItems[id] { + for (visibleId, visibleItem) in self.visibleItems { + if visibleItem === visibleReorderingItem { + continue + } + if visibleItem.frame.contains(targetPosition) { + if let item = items.items.first(where: { $0.id == id }), let targetItem = items.items.first(where: { $0.id == visibleId }) { + items.itemBinding.reorderIfPossible(item: item, toIndex: targetItem.index) + } + break + } + } + } + } + } @objc func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { self.items?.itemBinding.didScroll() @@ -554,7 +653,7 @@ public final class SparseItemGrid: ASDisplayNode { @objc func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { - self.updateVisibleItems(resetScrolling: false, synchronous: .full, restoreScrollPosition: nil) + self.updateVisibleItems(resetScrolling: false, synchronous: .full, restoreScrollPosition: nil, transition: .immediate) if let layout = self.layout, let _ = self.items { let offset = scrollView.contentOffset.y @@ -916,10 +1015,10 @@ public final class SparseItemGrid: ASDisplayNode { } func updateShimmerColors() { - self.updateVisibleItems(resetScrolling: false, synchronous: .none, restoreScrollPosition: nil) + self.updateVisibleItems(resetScrolling: false, synchronous: .none, restoreScrollPosition: nil, transition: .immediate) } - private func updateVisibleItems(resetScrolling: Bool, synchronous: SparseItemGrid.Synchronous, restoreScrollPosition: (y: CGFloat, index: Int)?) { + private func updateVisibleItems(resetScrolling: Bool, synchronous: SparseItemGrid.Synchronous, restoreScrollPosition: (y: CGFloat, index: Int)?, transition: ComponentTransition) { guard let layout = self.layout, let items = self.items else { return } @@ -980,23 +1079,32 @@ public final class SparseItemGrid: ASDisplayNode { let visibleRange = layout.visibleItemRange(for: visibleBounds, count: items.count) if visibleRange.maxIndex >= visibleRange.minIndex { - for index in visibleRange.minIndex ... visibleRange.maxIndex { + let processItemAtIndex: (Int) -> Void = { index in if let item = items.item(at: index) { - let itemFrame = layout.frame(at: index) + var itemFrame = layout.frame(at: index) let itemLayer: VisibleItem + var isNewlyAdded = false if let current = self.visibleItems[item.id] { itemLayer = current updateLayers.append((itemLayer, index)) } else { + isNewlyAdded = true itemLayer = VisibleItem(layer: items.itemBinding.createLayer(item: item), view: items.itemBinding.createView()) + + itemLayer.layer?.masksToBounds = true + self.visibleItems[item.id] = itemLayer bindItems.append(item) bindLayers.append(itemLayer) if let layer = itemLayer.layer { - self.scrollView.layer.addSublayer(layer) + if let reorderingItem = self.reorderingItem, let visibleReorderingItem = self.visibleItems[reorderingItem.id] { + self.scrollView.layer.insertSublayer(layer, below: visibleReorderingItem.layer) + } else { + self.scrollView.layer.addSublayer(layer) + } } else if let view = itemLayer.view { self.scrollView.addSubview(view) } @@ -1038,9 +1146,55 @@ public final class SparseItemGrid: ASDisplayNode { validIds.insert(item.id) - itemLayer.frame = itemFrame - if let blurLayer = itemLayer.blurLayer { - blurLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: itemFrame.minY), size: CGSize(width: layout.containerLayout.size.width, height: itemFrame.height)) + var itemScale: CGFloat + let itemCornerRadius: CGFloat + if self.isReordering { + itemScale = (itemFrame.height - 6.0 * 2.0) / itemFrame.height + itemCornerRadius = 10.0 + } else { + itemScale = 1.0 + itemCornerRadius = 0.0 + } + + let itemAlpha: CGFloat + if let reorderingItem = self.reorderingItem, item.id == reorderingItem.id { + itemAlpha = 0.8 + itemScale = 0.9 + itemFrame = itemFrame.size.centered(around: reorderingItem.position) + } else { + itemAlpha = 1.0 + } + + if transition.animation.isImmediate || isNewlyAdded { + itemLayer.position = itemFrame.center + itemLayer.bounds = CGRect(origin: CGPoint(), size: itemFrame.size) + itemLayer.transform = CATransform3DMakeScale(itemScale, itemScale, 1.0) + itemLayer.layer?.cornerRadius = itemCornerRadius + itemLayer.layer?.opacity = Float(itemAlpha) + if let blurLayer = itemLayer.blurLayer { + blurLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: itemFrame.minY), size: CGSize(width: layout.containerLayout.size.width, height: itemFrame.height)) + } + } else { + if let itemLayerValue = itemLayer.layer { + transition.setPosition(layer: itemLayerValue, position: itemFrame.center) + transition.setBounds(layer: itemLayerValue, bounds: CGRect(origin: CGPoint(), size: itemFrame.size)) + transition.setTransform(layer: itemLayerValue, transform: CATransform3DMakeScale(itemScale, itemScale, 1.0)) + transition.setCornerRadius(layer: itemLayerValue, cornerRadius: itemCornerRadius) + transition.setAlpha(layer: itemLayerValue, alpha: itemAlpha) + + if let blurLayer = itemLayer.blurLayer { + blurLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: itemFrame.minY), size: CGSize(width: layout.containerLayout.size.width, height: itemFrame.height)) + } + } else { + itemLayer.position = itemFrame.center + itemLayer.bounds = CGRect(origin: CGPoint(), size: itemFrame.size) + itemLayer.transform = CATransform3DMakeScale(itemScale, itemScale, 1.0) + itemLayer.layer?.cornerRadius = itemCornerRadius + itemLayer.layer?.opacity = Float(itemAlpha) + if let blurLayer = itemLayer.blurLayer { + blurLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: itemFrame.minY), size: CGSize(width: layout.containerLayout.size.width, height: itemFrame.height)) + } + } } } else { let placeholderLayer: SparseItemGridShimmerLayer @@ -1058,6 +1212,22 @@ public final class SparseItemGrid: ASDisplayNode { usedPlaceholderCount += 1 } } + for index in visibleRange.minIndex ... visibleRange.maxIndex { + processItemAtIndex(index) + } + if let reorderingItem = self.reorderingItem, let items = self.items { + var reorderingItemIndex: Int? + for item in items.items { + if item.id == reorderingItem.id { + reorderingItemIndex = item.index + break + } + } + + if let reorderingItemIndex, !(visibleRange.minIndex ... visibleRange.maxIndex).contains(reorderingItemIndex) { + processItemAtIndex(reorderingItemIndex) + } + } } if !bindItems.isEmpty { @@ -1068,9 +1238,20 @@ public final class SparseItemGrid: ASDisplayNode { let item = item as! VisibleItem let contentItem = items.item(at: index) if let layer = item.layer { - layer.update(size: layer.frame.size, insets: layout.containerLayout.insets, displayItem: item, binding: items.itemBinding, item: contentItem) + layer.update(size: layer.bounds.size, insets: layout.containerLayout.insets, displayItem: item, binding: items.itemBinding, item: contentItem) + + if self.isReordering { + if layer.animation(forKey: "shaking_position") == nil { + startShaking(layer: layer) + } + } else { + if layer.animation(forKey: "shaking_position") != nil { + layer.removeAnimation(forKey: "shaking_position") + layer.removeAnimation(forKey: "shaking_rotation") + } + } } else if let view = item.view { - view.update(size: view.layer.frame.size, insets: layout.containerLayout.insets) + view.update(size: view.layer.bounds.size, insets: layout.containerLayout.insets) } } @@ -1418,6 +1599,9 @@ public final class SparseItemGrid: ASDisplayNode { private var tapRecognizer: UITapGestureRecognizer? private var pinchRecognizer: UIPinchGestureRecognizer? + + private var isReordering: Bool = false + private var reorderRecognizer: ReorderGestureRecognizer? private var theme: PresentationTheme private var containerLayout: ContainerLayout? @@ -1492,6 +1676,41 @@ public final class SparseItemGrid: ASDisplayNode { let pinchRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.pinchGesture(_:))) self.pinchRecognizer = pinchRecognizer self.view.addGestureRecognizer(pinchRecognizer) + + let reorderRecognizer = ReorderGestureRecognizer( + shouldBegin: { [weak self] point in + guard let self, let item = self.item(at: point) else { + return (allowed: false, requiresLongPress: false, item: nil) + } + + return (allowed: true, requiresLongPress: false, item: item) + }, + willBegin: { point in + }, + began: { [weak self] item in + guard let self, let currentViewport = self.currentViewport else { + return + } + currentViewport.setReorderingItem(item: item) + }, + ended: { [weak self] in + guard let self, let currentViewport = self.currentViewport else { + return + } + currentViewport.setReorderingItem(item: nil) + }, + moved: { [weak self] distance in + guard let self, let currentViewport = self.currentViewport else { + return + } + currentViewport.moveReorderingItem(distance: distance) + }, + isActiveUpdated: { _ in + } + ) + self.reorderRecognizer = reorderRecognizer + self.view.addGestureRecognizer(reorderRecognizer) + reorderRecognizer.isEnabled = false self.addSubnode(self.scrollingArea) self.scrollingArea.openCurrentDate = { [weak self] in @@ -1584,7 +1803,7 @@ public final class SparseItemGrid: ASDisplayNode { }) nextViewport.frame = CGRect(origin: CGPoint(), size: containerLayout.size) - nextViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: restoreScrollPosition, synchronous: .semi) + nextViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: restoreScrollPosition, synchronous: .semi, transition: .immediate) self.currentViewportTransition?.removeFromSupernode() @@ -1638,7 +1857,7 @@ public final class SparseItemGrid: ASDisplayNode { }) nextViewport.frame = CGRect(origin: CGPoint(), size: containerLayout.size) - nextViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: restoreScrollPosition, synchronous: .semi) + nextViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: restoreScrollPosition, synchronous: .semi, transition: .immediate) let currentViewportTransition = ViewportTransition(interactiveState: interactiveState, layout: containerLayout, anchorItemIndex: anchorItemIndex, transitionAnchorPoint: anchorLocation, from: previousViewport, to: nextViewport, coveringOffsetUpdated: { [weak self] transition in self?.transitionCoveringOffsetUpdated(transition: transition) @@ -1679,7 +1898,7 @@ public final class SparseItemGrid: ASDisplayNode { strongSelf.scrollingArea.frame = CGRect(origin: CGPoint(), size: containerLayout.size) currentViewport.setScrollingArea(scrollingArea: strongSelf.scrollingArea) currentViewport.frame = CGRect(origin: CGPoint(), size: containerLayout.size) - currentViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: nil, synchronous: .semi) + currentViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: nil, synchronous: .semi, transition: .immediate) } strongSelf.currentViewportTransition = nil @@ -1691,7 +1910,7 @@ public final class SparseItemGrid: ASDisplayNode { } } - public func update(size: CGSize, insets: UIEdgeInsets, useSideInsets: Bool, scrollIndicatorInsets: UIEdgeInsets, lockScrollingAtTop: Bool, fixedItemHeight: CGFloat?, fixedItemAspect: CGFloat?, items: Items, theme: PresentationTheme, synchronous: SparseItemGrid.Synchronous) { + public func update(size: CGSize, insets: UIEdgeInsets, useSideInsets: Bool, scrollIndicatorInsets: UIEdgeInsets, lockScrollingAtTop: Bool, fixedItemHeight: CGFloat?, fixedItemAspect: CGFloat?, items: Items, theme: PresentationTheme, synchronous: SparseItemGrid.Synchronous, transition: ComponentTransition = .immediate) { self.theme = theme var headerInset: CGFloat = 0.0 @@ -1737,9 +1956,15 @@ public final class SparseItemGrid: ASDisplayNode { self.items = items self.scrollingArea.isHidden = lockScrollingAtTop - self.tapRecognizer?.isEnabled = fixedItemHeight == nil - self.pinchRecognizer?.isEnabled = fixedItemHeight == nil - + if self.isReordering { + self.tapRecognizer?.isEnabled = false + self.pinchRecognizer?.isEnabled = false + self.reorderRecognizer?.isEnabled = true + } else { + self.tapRecognizer?.isEnabled = fixedItemHeight == nil + self.pinchRecognizer?.isEnabled = fixedItemHeight == nil + self.reorderRecognizer?.isEnabled = false + } if self.currentViewport == nil { let currentViewport = Viewport(theme: self.theme, zoomLevel: self.initialZoomLevel ?? ZoomLevel(rawValue: 3), maybeLoadHoleAnchor: { [weak self] holeAnchor, location in @@ -1762,7 +1987,7 @@ public final class SparseItemGrid: ASDisplayNode { } else if let currentViewport = self.currentViewport { self.scrollingArea.frame = CGRect(origin: CGPoint(), size: size) currentViewport.frame = CGRect(origin: CGPoint(), size: size) - currentViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: nil, synchronous: synchronous) + currentViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: nil, synchronous: synchronous, transition: transition) } } @@ -1841,7 +2066,7 @@ public final class SparseItemGrid: ASDisplayNode { self.scrollingArea.frame = CGRect(origin: CGPoint(), size: containerLayout.size) currentViewport.frame = CGRect(origin: CGPoint(), size: containerLayout.size) - currentViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: restoreScrollPosition, synchronous: .semi) + currentViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: restoreScrollPosition, synchronous: .semi, transition: .immediate) let currentViewportTransition = ViewportTransition(interactiveState: nil, layout: containerLayout, anchorItemIndex: anchorItemIndex, transitionAnchorPoint: anchorLocation, from: previousViewport, to: currentViewport, coveringOffsetUpdated: { [weak self] transition in self?.transitionCoveringOffsetUpdated(transition: transition) @@ -1861,7 +2086,7 @@ public final class SparseItemGrid: ASDisplayNode { strongSelf.insertSubnode(currentViewport, belowSubnode: strongSelf.scrollingArea) strongSelf.scrollingArea.frame = CGRect(origin: CGPoint(), size: containerLayout.size) currentViewport.frame = CGRect(origin: CGPoint(), size: containerLayout.size) - currentViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: nil, synchronous: .semi) + currentViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: nil, synchronous: .semi, transition: .immediate) } strongSelf.currentViewport?.setScrollingArea(scrollingArea: strongSelf.scrollingArea) @@ -1874,6 +2099,24 @@ public final class SparseItemGrid: ASDisplayNode { } } } + + public func setReordering(isReordering: Bool) { + self.isReordering = isReordering + + if let currentViewport = self.currentViewport { + currentViewport.setReordering(isReordering: isReordering) + } + + if self.isReordering { + self.tapRecognizer?.isEnabled = false + self.pinchRecognizer?.isEnabled = false + self.reorderRecognizer?.isEnabled = true + } else { + self.tapRecognizer?.isEnabled = self.containerLayout?.fixedItemHeight == nil + self.pinchRecognizer?.isEnabled = self.containerLayout?.fixedItemHeight == nil + self.reorderRecognizer?.isEnabled = false + } + } private func coveringOffsetUpdated(viewport: Viewport, transition: ContainedViewLayoutTransition) { guard let items = self.items else { @@ -2050,3 +2293,244 @@ public final class SparseItemGrid: ASDisplayNode { } } } + +private func startShaking(layer: CALayer) { + func degreesToRadians(_ x: CGFloat) -> CGFloat { + return .pi * x / 180.0 + } + + let duration: Double = 0.4 + let displacement: CGFloat = 1.0 + let degreesRotation: CGFloat = 2.0 + + let negativeDisplacement = -1.0 * displacement + let position = CAKeyframeAnimation.init(keyPath: "position") + position.beginTime = 0.8 + position.duration = duration + position.values = [ + NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)), + NSValue(cgPoint: CGPoint(x: 0, y: 0)), + NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: 0)), + NSValue(cgPoint: CGPoint(x: 0, y: negativeDisplacement)), + NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)) + ] + position.calculationMode = .linear + position.isRemovedOnCompletion = false + position.repeatCount = Float.greatestFiniteMagnitude + position.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100)) + position.isAdditive = true + + let transform = CAKeyframeAnimation.init(keyPath: "transform") + transform.beginTime = 2.6 + transform.duration = 0.3 + transform.valueFunction = CAValueFunction(name: CAValueFunctionName.rotateZ) + transform.values = [ + degreesToRadians(-1.0 * degreesRotation), + degreesToRadians(degreesRotation), + degreesToRadians(-1.0 * degreesRotation) + ] + transform.calculationMode = .linear + transform.isRemovedOnCompletion = false + transform.repeatCount = Float.greatestFiniteMagnitude + transform.isAdditive = true + transform.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100)) + + layer.add(position, forKey: "shaking_position") + layer.add(transform, forKey: "shaking_rotation") +} + +private final class ReorderGestureRecognizer: UIGestureRecognizer { + private let shouldBegin: (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, item: SparseItemGridDisplayItem?) + private let willBegin: (CGPoint) -> Void + private let began: (SparseItemGridDisplayItem) -> Void + private let ended: () -> Void + private let moved: (CGPoint) -> Void + private let isActiveUpdated: (Bool) -> Void + + private var initialLocation: CGPoint? + private var longTapTimer: SwiftSignalKit.Timer? + private var longPressTimer: SwiftSignalKit.Timer? + + private var itemView: SparseItemGridDisplayItem? + + public init(shouldBegin: @escaping (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, item: SparseItemGridDisplayItem?), willBegin: @escaping (CGPoint) -> Void, began: @escaping (SparseItemGridDisplayItem) -> Void, ended: @escaping () -> Void, moved: @escaping (CGPoint) -> Void, isActiveUpdated: @escaping (Bool) -> Void) { + self.shouldBegin = shouldBegin + self.willBegin = willBegin + self.began = began + self.ended = ended + self.moved = moved + self.isActiveUpdated = isActiveUpdated + + super.init(target: nil, action: nil) + } + + deinit { + self.longTapTimer?.invalidate() + self.longPressTimer?.invalidate() + } + + private func startLongTapTimer() { + self.longTapTimer?.invalidate() + let longTapTimer = SwiftSignalKit.Timer(timeout: 0.25, repeat: false, completion: { [weak self] in + self?.longTapTimerFired() + }, queue: Queue.mainQueue()) + self.longTapTimer = longTapTimer + longTapTimer.start() + } + + private func stopLongTapTimer() { + self.itemView = nil + self.longTapTimer?.invalidate() + self.longTapTimer = nil + } + + private func startLongPressTimer() { + self.longPressTimer?.invalidate() + let longPressTimer = SwiftSignalKit.Timer(timeout: 0.6, repeat: false, completion: { [weak self] in + self?.longPressTimerFired() + }, queue: Queue.mainQueue()) + self.longPressTimer = longPressTimer + longPressTimer.start() + } + + private func stopLongPressTimer() { + self.itemView = nil + self.longPressTimer?.invalidate() + self.longPressTimer = nil + } + + override public func reset() { + super.reset() + + self.itemView = nil + self.stopLongTapTimer() + self.stopLongPressTimer() + self.initialLocation = nil + + self.isActiveUpdated(false) + } + + private func longTapTimerFired() { + guard let location = self.initialLocation else { + return + } + + self.longTapTimer?.invalidate() + self.longTapTimer = nil + + self.willBegin(location) + } + + private func longPressTimerFired() { + guard let _ = self.initialLocation else { + return + } + + self.isActiveUpdated(true) + self.state = .began + self.longPressTimer?.invalidate() + self.longPressTimer = nil + self.longTapTimer?.invalidate() + self.longTapTimer = nil + if let itemView = self.itemView { + self.began(itemView) + } + self.isActiveUpdated(true) + } + + override public func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + if self.numberOfTouches > 1 { + self.isActiveUpdated(false) + self.state = .failed + self.ended() + return + } + + if self.state == .possible { + if let location = touches.first?.location(in: self.view) { + let (allowed, requiresLongPress, itemView) = self.shouldBegin(location) + if allowed { + self.isActiveUpdated(true) + + self.itemView = itemView + self.initialLocation = location + if requiresLongPress { + self.startLongTapTimer() + self.startLongPressTimer() + } else { + self.state = .began + if let itemView = self.itemView { + self.began(itemView) + } + } + } else { + self.isActiveUpdated(false) + self.state = .failed + } + } else { + self.isActiveUpdated(false) + self.state = .failed + } + } + } + + override public func touchesEnded(_ touches: Set, with event: UIEvent) { + super.touchesEnded(touches, with: event) + + self.initialLocation = nil + + self.stopLongTapTimer() + if self.longPressTimer != nil { + self.stopLongPressTimer() + self.isActiveUpdated(false) + self.state = .failed + } + if self.state == .began || self.state == .changed { + self.isActiveUpdated(false) + self.ended() + self.state = .failed + } + } + + override public func touchesCancelled(_ touches: Set, with event: UIEvent) { + super.touchesCancelled(touches, with: event) + + self.initialLocation = nil + + self.stopLongTapTimer() + if self.longPressTimer != nil { + self.isActiveUpdated(false) + self.stopLongPressTimer() + self.state = .failed + } + if self.state == .began || self.state == .changed { + self.isActiveUpdated(false) + self.ended() + self.state = .failed + } + } + + override public func touchesMoved(_ touches: Set, with event: UIEvent) { + super.touchesMoved(touches, with: event) + + if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) { + self.state = .changed + let offset = CGPoint(x: location.x - initialLocation.x, y: location.y - initialLocation.y) + self.moved(offset) + } else if let touch = touches.first, let initialTapLocation = self.initialLocation, self.longPressTimer != nil { + let touchLocation = touch.location(in: self.view) + let dX = touchLocation.x - initialTapLocation.x + let dY = touchLocation.y - initialTapLocation.y + + if dX * dX + dY * dY > 3.0 * 3.0 { + self.stopLongTapTimer() + self.stopLongPressTimer() + self.initialLocation = nil + self.isActiveUpdated(false) + self.state = .failed + } + } + } +} diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index e48473d94e..ed28e4f1ad 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -1081,7 +1081,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1886646706] = { return Api.UrlAuthResult.parse_urlAuthResultAccepted($0) } dict[-1445536993] = { return Api.UrlAuthResult.parse_urlAuthResultDefault($0) } dict[-1831650802] = { return Api.UrlAuthResult.parse_urlAuthResultRequest($0) } - dict[1340722400] = { return Api.User.parse_user($0) } + dict[-2093920310] = { return Api.User.parse_user($0) } dict[-742634630] = { return Api.User.parse_userEmpty($0) } dict[-862357728] = { return Api.UserFull.parse_userFull($0) } dict[-2100168954] = { return Api.UserProfilePhoto.parse_userProfilePhoto($0) } @@ -1399,7 +1399,7 @@ public extension Api { return parser(reader) } else { - telegramApiLog("Type constructor \(String(signature, radix: 16, uppercase: false)) not found") + telegramApiLog("Type constructor \(String(UInt32(bitPattern: signature), radix: 16, uppercase: false)) not found") return nil } } diff --git a/submodules/TelegramApi/Sources/Api26.swift b/submodules/TelegramApi/Sources/Api26.swift index 4d3405e880..07aeb17b3b 100644 --- a/submodules/TelegramApi/Sources/Api26.swift +++ b/submodules/TelegramApi/Sources/Api26.swift @@ -452,14 +452,14 @@ public extension Api { } public extension Api { enum User: TypeConstructorDescription { - case user(flags: Int32, flags2: Int32, id: Int64, accessHash: Int64?, firstName: String?, lastName: String?, username: String?, phone: String?, photo: Api.UserProfilePhoto?, status: Api.UserStatus?, botInfoVersion: Int32?, restrictionReason: [Api.RestrictionReason]?, botInlinePlaceholder: String?, langCode: String?, emojiStatus: Api.EmojiStatus?, usernames: [Api.Username]?, storiesMaxId: Int32?, color: Api.PeerColor?, profileColor: Api.PeerColor?, botDailyUsers: Int32?) + case user(flags: Int32, flags2: Int32, id: Int64, accessHash: Int64?, firstName: String?, lastName: String?, username: String?, phone: String?, photo: Api.UserProfilePhoto?, status: Api.UserStatus?, botInfoVersion: Int32?, restrictionReason: [Api.RestrictionReason]?, botInlinePlaceholder: String?, langCode: String?, emojiStatus: Api.EmojiStatus?, usernames: [Api.Username]?, storiesMaxId: Int32?, color: Api.PeerColor?, profileColor: Api.PeerColor?, botActiveUsers: Int32?) case userEmpty(id: Int64) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .user(let flags, let flags2, let id, let accessHash, let firstName, let lastName, let username, let phone, let photo, let status, let botInfoVersion, let restrictionReason, let botInlinePlaceholder, let langCode, let emojiStatus, let usernames, let storiesMaxId, let color, let profileColor, let botDailyUsers): + case .user(let flags, let flags2, let id, let accessHash, let firstName, let lastName, let username, let phone, let photo, let status, let botInfoVersion, let restrictionReason, let botInlinePlaceholder, let langCode, let emojiStatus, let usernames, let storiesMaxId, let color, let profileColor, let botActiveUsers): if boxed { - buffer.appendInt32(1340722400) + buffer.appendInt32(-2093920310) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags2, buffer: buffer, boxed: false) @@ -488,7 +488,7 @@ public extension Api { if Int(flags2) & Int(1 << 5) != 0 {serializeInt32(storiesMaxId!, buffer: buffer, boxed: false)} if Int(flags2) & Int(1 << 8) != 0 {color!.serialize(buffer, true)} if Int(flags2) & Int(1 << 9) != 0 {profileColor!.serialize(buffer, true)} - if Int(flags2) & Int(1 << 12) != 0 {serializeInt32(botDailyUsers!, buffer: buffer, boxed: false)} + if Int(flags2) & Int(1 << 12) != 0 {serializeInt32(botActiveUsers!, buffer: buffer, boxed: false)} break case .userEmpty(let id): if boxed { @@ -501,8 +501,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .user(let flags, let flags2, let id, let accessHash, let firstName, let lastName, let username, let phone, let photo, let status, let botInfoVersion, let restrictionReason, let botInlinePlaceholder, let langCode, let emojiStatus, let usernames, let storiesMaxId, let color, let profileColor, let botDailyUsers): - return ("user", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("firstName", firstName as Any), ("lastName", lastName as Any), ("username", username as Any), ("phone", phone as Any), ("photo", photo as Any), ("status", status as Any), ("botInfoVersion", botInfoVersion as Any), ("restrictionReason", restrictionReason as Any), ("botInlinePlaceholder", botInlinePlaceholder as Any), ("langCode", langCode as Any), ("emojiStatus", emojiStatus as Any), ("usernames", usernames as Any), ("storiesMaxId", storiesMaxId as Any), ("color", color as Any), ("profileColor", profileColor as Any), ("botDailyUsers", botDailyUsers as Any)]) + case .user(let flags, let flags2, let id, let accessHash, let firstName, let lastName, let username, let phone, let photo, let status, let botInfoVersion, let restrictionReason, let botInlinePlaceholder, let langCode, let emojiStatus, let usernames, let storiesMaxId, let color, let profileColor, let botActiveUsers): + return ("user", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("firstName", firstName as Any), ("lastName", lastName as Any), ("username", username as Any), ("phone", phone as Any), ("photo", photo as Any), ("status", status as Any), ("botInfoVersion", botInfoVersion as Any), ("restrictionReason", restrictionReason as Any), ("botInlinePlaceholder", botInlinePlaceholder as Any), ("langCode", langCode as Any), ("emojiStatus", emojiStatus as Any), ("usernames", usernames as Any), ("storiesMaxId", storiesMaxId as Any), ("color", color as Any), ("profileColor", profileColor as Any), ("botActiveUsers", botActiveUsers as Any)]) case .userEmpty(let id): return ("userEmpty", [("id", id as Any)]) } @@ -584,7 +584,7 @@ public extension Api { let _c19 = (Int(_2!) & Int(1 << 9) == 0) || _19 != nil let _c20 = (Int(_2!) & Int(1 << 12) == 0) || _20 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 { - return Api.User.user(flags: _1!, flags2: _2!, id: _3!, accessHash: _4, firstName: _5, lastName: _6, username: _7, phone: _8, photo: _9, status: _10, botInfoVersion: _11, restrictionReason: _12, botInlinePlaceholder: _13, langCode: _14, emojiStatus: _15, usernames: _16, storiesMaxId: _17, color: _18, profileColor: _19, botDailyUsers: _20) + return Api.User.user(flags: _1!, flags2: _2!, id: _3!, accessHash: _4, firstName: _5, lastName: _6, username: _7, phone: _8, photo: _9, status: _10, botInfoVersion: _11, restrictionReason: _12, botInlinePlaceholder: _13, langCode: _14, emojiStatus: _15, usernames: _16, storiesMaxId: _17, color: _18, profileColor: _19, botActiveUsers: _20) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api36.swift b/submodules/TelegramApi/Sources/Api36.swift index 3670a82adc..8ddd53d008 100644 --- a/submodules/TelegramApi/Sources/Api36.swift +++ b/submodules/TelegramApi/Sources/Api36.swift @@ -7448,6 +7448,26 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func requestMainWebView(flags: Int32, peer: Api.InputPeer, bot: Api.InputUser, startParam: String?, themeParams: Api.DataJSON?, platform: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-908059013) + serializeInt32(flags, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + bot.serialize(buffer, true) + if Int(flags) & Int(1 << 1) != 0 {serializeString(startParam!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 0) != 0 {themeParams!.serialize(buffer, true)} + serializeString(platform, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.requestMainWebView", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("bot", String(describing: bot)), ("startParam", String(describing: startParam)), ("themeParams", String(describing: themeParams)), ("platform", String(describing: platform))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.WebViewResult? in + let reader = BufferReader(buffer) + var result: Api.WebViewResult? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.WebViewResult + } + return result + }) + } +} public extension Api.functions.messages { static func requestSimpleWebView(flags: Int32, bot: Api.InputUser, url: String?, startParam: String?, themeParams: Api.DataJSON?, platform: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift index b93cc8e6aa..23ea3fd5e4 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift @@ -99,6 +99,9 @@ extension TelegramUser { if (flags2 & (1 << 11)) != 0 { botFlags.insert(.isBusiness) } + if (flags2 & (1 << 13)) != 0 { + botFlags.insert(.hasWebApp) + } botInfo = BotUserInfo(flags: botFlags, inlinePlaceholder: botInlinePlaceholder) } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift index afa7b85300..d5dc9bf17e 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift @@ -38,6 +38,7 @@ public struct BotUserInfoFlags: OptionSet { public static let canBeAddedToAttachMenu = BotUserInfoFlags(rawValue: (1 << 4)) public static let canEdit = BotUserInfoFlags(rawValue: (1 << 5)) public static let isBusiness = BotUserInfoFlags(rawValue: (1 << 6)) + public static let hasWebApp = BotUserInfoFlags(rawValue: (1 << 7)) } public struct BotUserInfo: PostboxCoding, Equatable { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift index cdbcd868d8..622ddeac15 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift @@ -60,6 +60,50 @@ func _internal_requestSimpleWebView(postbox: Postbox, network: Network, botId: P |> switchToLatest } +func _internal_requestMainWebView(postbox: Postbox, network: Network, botId: PeerId, source: RequestSimpleWebViewSource, themeParams: [String: Any]?) -> Signal { + var serializedThemeParams: Api.DataJSON? + if let themeParams = themeParams, let data = try? JSONSerialization.data(withJSONObject: themeParams, options: []), let dataString = String(data: data, encoding: .utf8) { + serializedThemeParams = .dataJSON(data: dataString) + } + return postbox.transaction { transaction -> Signal in + guard let bot = transaction.getPeer(botId), let inputUser = apiInputUser(bot) else { + return .fail(.generic) + } + guard let peer = transaction.getPeer(botId), let inputPeer = apiInputPeer(peer) else { + return .fail(.generic) + } + + var flags: Int32 = 0 + if let _ = serializedThemeParams { + flags |= (1 << 0) + } + switch source { + case .inline: + flags |= (1 << 1) + case .settings: + flags |= (1 << 2) + default: + break + } + return network.request(Api.functions.messages.requestMainWebView(flags: flags, peer: inputPeer, bot: inputUser, startParam: nil, themeParams: serializedThemeParams, platform: botWebViewPlatform)) + |> mapError { _ -> RequestWebViewError in + return .generic + } + |> mapToSignal { result -> Signal in + switch result { + case let .webViewResultUrl(flags, queryId, url): + var resultFlags: RequestWebViewResult.Flags = [] + if (flags & (1 << 1)) != 0 { + resultFlags.insert(.fullSize) + } + return .single(RequestWebViewResult(flags: resultFlags, queryId: queryId, url: url, keepAliveSignal: nil)) + } + } + } + |> castError(RequestWebViewError.self) + |> switchToLatest +} + public enum KeepWebViewError { case generic } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 11663bbd71..9840057937 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -1290,7 +1290,7 @@ func _internal_uploadBotPreviewImpl( } let passFetchProgress = media is TelegramMediaFile - let (contentSignal, _) = uploadedStoryContent(postbox: postbox, network: network, media: media, mediaReference: nil, embeddedStickers: embeddedStickers, accountPeerId: accountPeerId, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, auxiliaryMethods: auxiliaryMethods, passFetchProgress: passFetchProgress) + let (contentSignal, originalMedia) = uploadedStoryContent(postbox: postbox, network: network, media: media, mediaReference: nil, embeddedStickers: embeddedStickers, accountPeerId: accountPeerId, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, auxiliaryMethods: auxiliaryMethods, passFetchProgress: passFetchProgress) return contentSignal |> mapToSignal { result -> Signal in switch result { @@ -1319,6 +1319,8 @@ func _internal_uploadBotPreviewImpl( } if let resultMediaValue = textMediaAndExpirationTimerFromApiMedia(resultMedia, toPeerId).media { + applyMediaResourceChanges(from: originalMedia, to: resultMediaValue, postbox: postbox, force: originalMedia is TelegramMediaFile && resultMediaValue is TelegramMediaFile) + transaction.updatePeerCachedData(peerIds: Set([toPeerId]), update: { _, current in guard var current = current as? CachedUserData else { return current @@ -1330,7 +1332,7 @@ func _internal_uploadBotPreviewImpl( if let index = media.firstIndex(where: { $0.id == resultMediaValue.id }) { media.remove(at: index) } - media.append(resultMediaValue) + media.insert(resultMediaValue, at: 0) let botPreview = CachedUserData.BotPreview(media: media) current = current.withUpdatedBotPreview(botPreview) return current diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index fa33bd18a0..99b21d5b95 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -2087,13 +2087,15 @@ public final class BotPreviewStoryListContext: StoryListContext { private var isLoadingMore: Bool = false private var requestDisposable: Disposable? - private var updatesDisposable: Disposable? + private let reorderDisposable = MetaDisposable() private var completionCallbacksByToken: [AnyHashable: [() -> Void]] = [:] private var nextId: Int32 = 1 + private var pendingIdMapping: [Int32: Int32] = [:] private var idMapping: [MediaId: Int32] = [:] + private var reverseIdMapping: [Int32: MediaId] = [:] init(queue: Queue, account: Account, engine: TelegramEngine, peerId: EnginePeer.Id) { self.queue = queue @@ -2107,17 +2109,68 @@ public final class BotPreviewStoryListContext: StoryListContext { self.stateValue = State(peerReference: nil, items: [], pinnedIds: Set(), totalCount: 0, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: false, allEntityFiles: [:], isLoading: false) - self.requestDisposable = (engine.data.subscribe( - TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), - TelegramEngine.EngineData.Item.Peer.BotPreview(id: peerId) + let localStateKey: PostboxViewKey = .storiesState(key: .local) + + self.requestDisposable = (combineLatest(queue: queue, + engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), + TelegramEngine.EngineData.Item.Peer.BotPreview(id: peerId) + ), + account.postbox.combinedView(keys: [localStateKey]) ) - |> deliverOnMainQueue).start(next: { [weak self] peer, botPreview in + |> deliverOn(self.queue)).start(next: { [weak self] peerAndBotPreview, combinedView in guard let self else { return } + let (peer, botPreview) = peerAndBotPreview + var items: [State.Item] = [] + if let stateView = combinedView.views[localStateKey] as? StoryStatesView, let localState = stateView.value?.get(Stories.LocalState.self) { + for item in localState.items.reversed() { + let mappedId: Int32 + if let current = self.pendingIdMapping[item.stableId] { + mappedId = current + } else { + mappedId = self.nextId + self.nextId += 1 + self.pendingIdMapping[item.stableId] = mappedId + } + if case .botPreview(peerId) = item.target { + items.append(State.Item( + id: StoryId(peerId: peerId, id: mappedId), + storyItem: EngineStoryItem( + id: mappedId, + timestamp: 0, + expirationTimestamp: Int32.max, + media: EngineMedia(item.media), + alternativeMedia: nil, + mediaAreas: [], + text: "", + entities: [], + views: nil, + privacy: nil, + isPinned: false, + isExpired: false, + isPublic: false, + isPending: true, + isCloseFriends: false, + isContacts: false, + isSelectedContacts: false, + isForwardingDisabled: false, + isEdited: false, + isMy: false, + myReaction: nil, + forwardInfo: nil, + author: nil + ), + peer: nil + )) + } + } + } + if let botPreview { for media in botPreview.media { guard let mediaId = media.id else { @@ -2131,6 +2184,7 @@ public final class BotPreviewStoryListContext: StoryListContext { id = self.nextId self.nextId += 1 self.idMapping[mediaId] = id + self.reverseIdMapping[id] = mediaId } items.append(State.Item( @@ -2181,10 +2235,77 @@ public final class BotPreviewStoryListContext: StoryListContext { deinit { self.requestDisposable?.dispose() + self.updatesDisposable?.dispose() + self.reorderDisposable.dispose() } func loadMore(completion: (() -> Void)?) { } + + func reorderItems(ids: [StoryId]) { + let peerId = self.peerId + let idMapping = self.idMapping + let reverseIdMapping = self.reverseIdMapping + + let _ = (self.account.postbox.transaction({ transaction -> (Api.InputUser?, [Api.InputMedia]) in + let inputUser = transaction.getPeer(peerId).flatMap(apiInputUser) + + var inputMedia: [Api.InputMedia] = [] + transaction.updatePeerCachedData(peerIds: Set([self.peerId]), update: { _, current in + guard var current = current as? CachedUserData else { + return current + } + guard let currentBotPreview = current.botPreview else { + return current + } + + var media: [Media] = [] + media = [] + + var seenIds = Set() + for id in ids { + guard let mediaId = reverseIdMapping[id.id] else { + continue + } + if let index = currentBotPreview.media.firstIndex(where: { $0.id == mediaId }) { + seenIds.insert(id.id) + media.append(currentBotPreview.media[index]) + } + } + + for item in currentBotPreview.media { + guard let id = item.id, let storyId = idMapping[id] else { + continue + } + if !seenIds.contains(storyId) { + media.append(item) + } + } + + for item in media { + if let image = item as? TelegramMediaImage, let resource = image.representations.last?.resource as? CloudPhotoSizeMediaResource { + inputMedia.append(.inputMediaPhoto(flags: 0, id: .inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil)) + inputMedia.append(Api.InputMedia.inputMediaPhoto(flags: 0, id: Api.InputPhoto.inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil)) + } else if let file = item as? TelegramMediaFile, let resource = file.resource as? CloudDocumentMediaResource { + inputMedia.append(.inputMediaDocument(flags: 0, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: nil)) + } + } + + let botPreview = CachedUserData.BotPreview(media: media) + current = current.withUpdatedBotPreview(botPreview) + return current + }) + + return (inputUser, inputMedia) + }) + |> deliverOn(self.queue)).startStandalone(next: { [weak self] inputUser, inputMedia in + guard let self, let inputUser else { + return + } + let signal = self.account.network.request(Api.functions.bots.reorderPreviewMedias(bot: inputUser, order: inputMedia)) + self.reorderDisposable.set(signal.startStrict()) + }) + } } public var state: Signal { @@ -2206,7 +2327,13 @@ public final class BotPreviewStoryListContext: StoryListContext { public func loadMore(completion: (() -> Void)? = nil) { self.impl.with { impl in - impl.loadMore(completion : completion) + impl.loadMore(completion: completion) + } + } + + public func reorderItems(ids: [StoryId]) { + self.impl.with { impl in + impl.reorderItems(ids: ids) } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 7b24d405ac..3343528dd1 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -574,6 +574,10 @@ public extension TelegramEngine { return _internal_requestSimpleWebView(postbox: self.account.postbox, network: self.account.network, botId: botId, url: url, source: source, themeParams: themeParams) } + public func requestMainWebView(botId: PeerId, source: RequestSimpleWebViewSource, themeParams: [String: Any]?) -> Signal { + return _internal_requestMainWebView(postbox: self.account.postbox, network: self.account.network, botId: botId, source: source, themeParams: themeParams) + } + public func requestAppWebView(peerId: PeerId, appReference: BotAppReference, payload: String?, themeParams: [String: Any]?, compact: Bool, allowWrite: Bool) -> Signal { return _internal_requestAppWebView(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, peerId: peerId, appReference: appReference, payload: payload, themeParams: themeParams, compact: compact, allowWrite: allowWrite) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentPeers.swift index ff6c0b438f..b81478c4b7 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentPeers.swift @@ -306,3 +306,26 @@ public func _internal_managedUpdatedRecentApps(accountPeerId: PeerId, postbox: P return updateOnce } } + +func _internal_removeRecentlyUsedApp(account: Account, peerId: PeerId) -> Signal { + return account.postbox.transaction { transaction -> Signal in + if let entry = transaction.retrieveItemCacheEntry(id: cachedRecentAppsEntryId()), let recentPeers = entry.get(CachedRecentPeers.self) { + let updatedRecentPeers = CachedRecentPeers(enabled: recentPeers.enabled, ids: recentPeers.ids.filter({ $0 != peerId })) + if let updatedEntry = CodableEntry(updatedRecentPeers) { + transaction.putItemCacheEntry(id: cachedRecentAppsEntryId(), entry: updatedEntry) + } + } + + if let peer = transaction.getPeer(peerId), let apiPeer = apiInputPeer(peer) { + return account.network.request(Api.functions.contacts.resetTopPeerRating(category: .topPeerCategoryBotsApp, peer: apiPeer)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> mapToSignal { _ -> Signal in + return .complete() + } + } else { + return .complete() + } + } |> switchToLatest +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index cf5e915035..948d724634 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -633,6 +633,10 @@ public extension TelegramEngine { public func removeRecentlyUsedInlineBot(peerId: PeerId) -> Signal { return _internal_removeRecentlyUsedInlineBot(account: self.account, peerId: peerId) } + + public func removeRecentlyUsedApp(peerId: PeerId) -> Signal { + return _internal_removeRecentlyUsedApp(account: self.account, peerId: peerId) + } public func uploadedPeerPhoto(resource: MediaResource) -> Signal { return _internal_uploadedPeerPhoto(postbox: self.account.postbox, network: self.account.network, resource: resource) diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 19262fd22a..3d2f70b061 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -457,6 +457,7 @@ swift_library( "//submodules/TelegramUI/Components/PeerManagement/OldChannelsController", "//submodules/TelegramUI/Components/Chat/ChatSendStarsScreen", "//submodules/TelegramUI/Components/MinimizedContainer", + "//submodules/TelegramUI/Components/SpaceWarpView", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 6baabab4ba..c3bc3a2eb4 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -1396,15 +1396,18 @@ public class CameraScreen: ViewController { public weak var destinationView: UIView? public let destinationRect: CGRect public let destinationCornerRadius: CGFloat + public let completion: (() -> Void)? public init( destinationView: UIView, destinationRect: CGRect, - destinationCornerRadius: CGFloat + destinationCornerRadius: CGFloat, + completion: (() -> Void)? = nil ) { self.destinationView = destinationView self.destinationRect = destinationRect self.destinationCornerRadius = destinationCornerRadius + self.completion = completion } } @@ -2182,9 +2185,12 @@ public class CameraScreen: ViewController { let destinationLocalFrame = destinationView.convert(transitionOut.destinationRect, to: self.view) let targetScale = destinationLocalFrame.width / self.previewContainerView.frame.width + let transitionOutCompletion = transitionOut.completion + if case .story = controller.mode { self.previewContainerView.layer.animatePosition(from: self.previewContainerView.center, to: destinationLocalFrame.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in completion() + transitionOutCompletion?() }) self.previewContainerView.layer.animateScale(from: 1.0, to: targetScale, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) @@ -2208,6 +2214,7 @@ public class CameraScreen: ViewController { self.mainPreviewAnimationWrapperView.center = destinationInnerFrame.center self.mainPreviewAnimationWrapperView.layer.animatePosition(from: initialCenter, to: self.mainPreviewAnimationWrapperView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in completion() + transitionOutCompletion?() }) var targetBounds = self.mainPreviewView.bounds diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index cfde1b0530..baf6aeba3e 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -2443,15 +2443,18 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate public weak var destinationView: UIView? public let destinationRect: CGRect public let destinationCornerRadius: CGFloat + public let completion: (() -> Void)? public init( destinationView: UIView, destinationRect: CGRect, - destinationCornerRadius: CGFloat + destinationCornerRadius: CGFloat, + completion: (() -> Void)? = nil ) { self.destinationView = destinationView self.destinationRect = destinationRect self.destinationCornerRadius = destinationCornerRadius + self.completion = completion } } @@ -3785,6 +3788,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let transitionOut = controller.transitionOut(finished, isNew), let destinationView = transitionOut.destinationView { var destinationTransitionView: UIView? var destinationTransitionRect: CGRect = .zero + let transitionOutCompletion = transitionOut.completion if !finished { if let transitionIn = controller.transitionIn, case let .gallery(galleryTransitionIn) = transitionIn, let sourceImage = galleryTransitionIn.sourceImage, isNew != true { let sourceSuperView = galleryTransitionIn.sourceView?.superview?.superview @@ -3867,6 +3871,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate destinationView.isHidden = false destinationSnapshotView?.removeFromSuperview() completion() + transitionOutCompletion?() }) self.previewContainerView.layer.animateScale(from: 1.0, to: destinationScale, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) self.previewContainerView.layer.animateBounds(from: self.previewContainerView.bounds, to: CGRect(origin: CGPoint(x: 0.0, y: (self.previewContainerView.bounds.height - self.previewContainerView.bounds.width * destinationAspectRatio) / 2.0), size: CGSize(width: self.previewContainerView.bounds.width, height: self.previewContainerView.bounds.width * destinationAspectRatio)), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift index da78f4618e..01056e97dc 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift @@ -673,7 +673,11 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { } if let button = item.button { - height += 3.0 + if textSize.height > 0.0 { + height += 3.0 + } else { + height -= 7.0 + } let actionButton: ComponentView if let current = self.actionButton { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index faaa189d25..ca0ea8fa43 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -30,6 +30,7 @@ final class PeerInfoState { let isEditing: Bool let selectedMessageIds: Set? let selectedStoryIds: Set? + let paneIsReordering: Bool let updatingAvatar: PeerInfoUpdatingAvatar? let updatingBio: String? let avatarUploadProgress: AvatarUploadProgress? @@ -42,6 +43,7 @@ final class PeerInfoState { isEditing: Bool, selectedMessageIds: Set?, selectedStoryIds: Set?, + paneIsReordering: Bool, updatingAvatar: PeerInfoUpdatingAvatar?, updatingBio: String?, avatarUploadProgress: AvatarUploadProgress?, @@ -53,6 +55,7 @@ final class PeerInfoState { self.isEditing = isEditing self.selectedMessageIds = selectedMessageIds self.selectedStoryIds = selectedStoryIds + self.paneIsReordering = paneIsReordering self.updatingAvatar = updatingAvatar self.updatingBio = updatingBio self.avatarUploadProgress = avatarUploadProgress @@ -67,6 +70,7 @@ final class PeerInfoState { isEditing: isEditing, selectedMessageIds: self.selectedMessageIds, selectedStoryIds: self.selectedStoryIds, + paneIsReordering: self.paneIsReordering, updatingAvatar: self.updatingAvatar, updatingBio: self.updatingBio, avatarUploadProgress: self.avatarUploadProgress, @@ -82,6 +86,7 @@ final class PeerInfoState { isEditing: self.isEditing, selectedMessageIds: selectedMessageIds, selectedStoryIds: self.selectedStoryIds, + paneIsReordering: self.paneIsReordering, updatingAvatar: self.updatingAvatar, updatingBio: self.updatingBio, avatarUploadProgress: self.avatarUploadProgress, @@ -97,6 +102,23 @@ final class PeerInfoState { isEditing: self.isEditing, selectedMessageIds: self.selectedMessageIds, selectedStoryIds: selectedStoryIds, + paneIsReordering: self.paneIsReordering, + updatingAvatar: self.updatingAvatar, + updatingBio: self.updatingBio, + avatarUploadProgress: self.avatarUploadProgress, + highlightedButton: self.highlightedButton, + isEditingBirthDate: self.isEditingBirthDate, + updatingBirthDate: self.updatingBirthDate, + personalChannels: self.personalChannels + ) + } + + func withPaneIsReordering(_ paneIsReordering: Bool) -> PeerInfoState { + return PeerInfoState( + isEditing: self.isEditing, + selectedMessageIds: self.selectedMessageIds, + selectedStoryIds: self.selectedStoryIds, + paneIsReordering: paneIsReordering, updatingAvatar: self.updatingAvatar, updatingBio: self.updatingBio, avatarUploadProgress: self.avatarUploadProgress, @@ -112,6 +134,7 @@ final class PeerInfoState { isEditing: self.isEditing, selectedMessageIds: self.selectedMessageIds, selectedStoryIds: self.selectedStoryIds, + paneIsReordering: self.paneIsReordering, updatingAvatar: updatingAvatar, updatingBio: self.updatingBio, avatarUploadProgress: self.avatarUploadProgress, @@ -127,6 +150,7 @@ final class PeerInfoState { isEditing: self.isEditing, selectedMessageIds: self.selectedMessageIds, selectedStoryIds: self.selectedStoryIds, + paneIsReordering: self.paneIsReordering, updatingAvatar: self.updatingAvatar, updatingBio: updatingBio, avatarUploadProgress: self.avatarUploadProgress, @@ -142,6 +166,7 @@ final class PeerInfoState { isEditing: self.isEditing, selectedMessageIds: self.selectedMessageIds, selectedStoryIds: self.selectedStoryIds, + paneIsReordering: self.paneIsReordering, updatingAvatar: self.updatingAvatar, updatingBio: self.updatingBio, avatarUploadProgress: avatarUploadProgress, @@ -157,6 +182,7 @@ final class PeerInfoState { isEditing: self.isEditing, selectedMessageIds: self.selectedMessageIds, selectedStoryIds: self.selectedStoryIds, + paneIsReordering: self.paneIsReordering, updatingAvatar: self.updatingAvatar, updatingBio: self.updatingBio, avatarUploadProgress: self.avatarUploadProgress, @@ -172,6 +198,7 @@ final class PeerInfoState { isEditing: self.isEditing, selectedMessageIds: self.selectedMessageIds, selectedStoryIds: self.selectedStoryIds, + paneIsReordering: self.paneIsReordering, updatingAvatar: self.updatingAvatar, updatingBio: self.updatingBio, avatarUploadProgress: self.avatarUploadProgress, @@ -187,6 +214,7 @@ final class PeerInfoState { isEditing: self.isEditing, selectedMessageIds: self.selectedMessageIds, selectedStoryIds: self.selectedStoryIds, + paneIsReordering: self.paneIsReordering, updatingAvatar: self.updatingAvatar, updatingBio: self.updatingBio, avatarUploadProgress: self.avatarUploadProgress, @@ -202,6 +230,7 @@ final class PeerInfoState { isEditing: self.isEditing, selectedMessageIds: self.selectedMessageIds, selectedStoryIds: self.selectedStoryIds, + paneIsReordering: self.paneIsReordering, updatingAvatar: self.updatingAvatar, updatingBio: self.updatingBio, avatarUploadProgress: self.avatarUploadProgress, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift index 378306bbfc..91606b4938 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift @@ -810,7 +810,20 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat } for (_, pane) in self.pendingPanes { if let paneNode = pane.pane.node as? PeerInfoStoryPaneNode { - paneNode.updateSelectedStories(selectedStoryIds: selectedStoryIds, animated: animated) + paneNode.updateSelectedStories(selectedStoryIds: selectedStoryIds, animated: false) + } + } + } + + func updatePaneIsReordering(isReordering: Bool, animated: Bool) { + for (_, pane) in self.currentPanes { + if let paneNode = pane.node as? PeerInfoStoryPaneNode { + paneNode.updateIsReordering(isReordering: isReordering, animated: animated) + } + } + for (_, pane) in self.pendingPanes { + if let paneNode = pane.pane.node as? PeerInfoStoryPaneNode { + paneNode.updateIsReordering(isReordering: isReordering, animated: false) } } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 92698737c5..9a3c72e377 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -1336,6 +1336,16 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese })) } + var hasAbout = false + if let about = cachedData.about, !about.isEmpty { + hasAbout = true + } + + var hasWebApp = false + if let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp) { + hasWebApp = true + } + if user.isFake { items[currentPeerInfoSection]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: "", text: user.botInfo != nil ? presentationData.strings.UserInfo_FakeBotWarning : presentationData.strings.UserInfo_FakeUserWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.botInfo != nil ? enabledPrivateBioEntities : []), action: nil, requestLayout: { interaction.requestLayout(false) @@ -1344,28 +1354,36 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese items[currentPeerInfoSection]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Profile_BotInfo, text: user.botInfo != nil ? presentationData.strings.UserInfo_ScamBotWarning : presentationData.strings.UserInfo_ScamUserWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.botInfo != nil ? enabledPrivateBioEntities : []), action: nil, requestLayout: { interaction.requestLayout(false) })) - } else if let about = cachedData.about, !about.isEmpty { + } else if hasAbout || hasWebApp { var actionButton: PeerInfoScreenLabeledValueItem.Button? - if let menuButton = cachedData.botInfo?.menuButton, case let .webView(text, url) = menuButton { + if hasWebApp { //TODO:localize actionButton = PeerInfoScreenLabeledValueItem.Button(title: "Open App", action: { guard let parentController = interaction.getController() else { return } - openWebApp( - parentController: parentController, + context.sharedContext.openWebApp( context: context, + parentController: parentController, + updatedPresentationData: nil, peer: .user(user), - buttonText: text, - url: url, - simple: false, - source: .menu + threadId: nil, + buttonText: "", + url: "", + simple: true, + source: .generic, + skipTermsOfService: true ) }) } - items[currentPeerInfoSection]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Profile_BotInfo, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.isPremium ? enabledPublicBioEntities : enabledPrivateBioEntities), action: isMyProfile ? { node, _ in + var label: String = "" + if let about = cachedData.about, !about.isEmpty { + label = user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Profile_BotInfo + } + + items[currentPeerInfoSection]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: label, text: cachedData.about ?? "", textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.isPremium ? enabledPublicBioEntities : enabledPrivateBioEntities), action: isMyProfile ? { node, _ in bioContextAction(node, nil, nil) } : nil, linkItemAction: bioLinkAction, button: actionButton, contextAction: bioContextAction, requestLayout: { interaction.requestLayout(false) @@ -2569,6 +2587,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro isEditing: false, selectedMessageIds: nil, selectedStoryIds: nil, + paneIsReordering: false, updatingAvatar: nil, updatingBio: nil, avatarUploadProgress: nil, @@ -4220,13 +4239,14 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro strongSelf.chatInterfaceInteraction.selectionState = strongSelf.state.selectedMessageIds.flatMap { ChatInterfaceSelectionState(selectedIds: $0) } strongSelf.paneContainerNode.updateSelectedMessageIds(strongSelf.state.selectedMessageIds, animated: true) case .selectionDone: - strongSelf.state = strongSelf.state.withSelectedMessageIds(nil).withSelectedStoryIds(nil) + strongSelf.state = strongSelf.state.withSelectedMessageIds(nil).withSelectedStoryIds(nil).withPaneIsReordering(false) if let (layout, navigationHeight) = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring), additive: false) } strongSelf.chatInterfaceInteraction.selectionState = strongSelf.state.selectedMessageIds.flatMap { ChatInterfaceSelectionState(selectedIds: $0) } strongSelf.paneContainerNode.updateSelectedMessageIds(strongSelf.state.selectedMessageIds, animated: true) strongSelf.paneContainerNode.updateSelectedStoryIds(strongSelf.state.selectedStoryIds, animated: true) + strongSelf.paneContainerNode.updatePaneIsReordering(isReordering: strongSelf.state.paneIsReordering, animated: true) case .search, .searchWithTags, .standaloneSearch: strongSelf.activateSearch() case .more: @@ -9886,16 +9906,31 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return nil } - if !self.headerNode.isAvatarExpanded { - let transitionView = self.headerNode.avatarListNode.avatarContainerNode.avatarNode.contentNode.view - return StoryCameraTransitionOut( - destinationView: transitionView, - destinationRect: transitionView.bounds, - destinationCornerRadius: transitionView.bounds.height * 0.5 - ) + if let data = self.data, let user = data.peer as? TelegramUser, let _ = user.botInfo { + if let pane = self.paneContainerNode.currentPane?.node as? PeerInfoStoryPaneNode, let transitionView = pane.extractPendingStoryTransitionView() { + return StoryCameraTransitionOut( + destinationView: transitionView, + destinationRect: transitionView.bounds, + destinationCornerRadius: 0.0, + completion: { [weak transitionView] in + transitionView?.removeFromSuperview() + } + ) + } + + return nil + } else { + if !self.headerNode.isAvatarExpanded { + let transitionView = self.headerNode.avatarListNode.avatarContainerNode.avatarNode.contentNode.view + return StoryCameraTransitionOut( + destinationView: transitionView, + destinationRect: transitionView.bounds, + destinationCornerRadius: transitionView.bounds.height * 0.5 + ) + } + + return nil } - - return nil } } @@ -10822,18 +10857,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro items.append(.action(ContextMenuActionItem(text: "Reorder", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, a in - if ignoreNextActions { - return - } - ignoreNextActions = true - a(.default) - - let _ = self - }))) - - items.append(.action(ContextMenuActionItem(text: "Select", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) }, action: { [weak pane] _, a in if ignoreNextActions { return @@ -10842,7 +10865,21 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro a(.default) if let pane { - pane.setIsSelectionModeActive(true) + pane.beginReordering() + } + }))) + + items.append(.action(ContextMenuActionItem(text: "Select", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + if ignoreNextActions { + return + } + ignoreNextActions = true + a(.default) + + if let self { + self.toggleStorySelection(ids: [], isSelected: true) } }))) @@ -11251,7 +11288,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.headerNode.customNavigationContentNode = self.paneContainerNode.currentPane?.node.navigationContentNode var isScrollEnabled = !self.isMediaOnly && self.headerNode.customNavigationContentNode == nil - if self.state.selectedStoryIds != nil { + if self.state.selectedStoryIds != nil || self.state.paneIsReordering { isScrollEnabled = false } if self.scrollNode.view.isScrollEnabled != isScrollEnabled { @@ -11686,7 +11723,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } var disableTabSwitching = false - if self.state.selectedStoryIds != nil { + if self.state.selectedStoryIds != nil || self.state.paneIsReordering { disableTabSwitching = true } @@ -11717,7 +11754,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro rightNavigationButtons.insert(PeerInfoHeaderNavigationButtonSpec(key: .postStory, isForExpandedView: false), at: 0) } - if self.state.selectedMessageIds == nil && self.state.selectedStoryIds == nil { + if self.state.selectedMessageIds == nil && self.state.selectedStoryIds == nil && !self.state.paneIsReordering { if let currentPaneKey = self.paneContainerNode.currentPaneKey { switch currentPaneKey { case .files, .music, .links, .members: @@ -12082,6 +12119,17 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.paneContainerNode.updateSelectedStoryIds(self.state.selectedStoryIds, animated: true) } + func togglePaneIsReordering(isReordering: Bool) { + self.expandTabs(animated: true) + + self.state = self.state.withPaneIsReordering(true) + + if let (layout, navigationHeight) = self.validLayout { + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring), additive: false) + } + self.paneContainerNode.updatePaneIsReordering(isReordering: self.state.paneIsReordering, animated: true) + } + func cancelItemSelection() { self.headerNode.navigationButtonContainer.performAction?(.selectionDone, nil, nil) } @@ -12927,6 +12975,10 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc self.controllerNode.toggleStorySelection(ids: ids, isSelected: isSelected) } + public func togglePaneIsReordering(isReordering: Bool) { + self.controllerNode.togglePaneIsReordering(isReordering: isReordering) + } + public func cancelItemSelection() { self.controllerNode.cancelItemSelection() } @@ -13898,7 +13950,7 @@ private final class HeaderContextReferenceContentSource: ContextReferenceContent } } -private func openWebApp(parentController: ViewController, context: AccountContext, peer: EnginePeer, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource) { +/*private func openWebApp(parentController: ViewController, context: AccountContext, peer: EnginePeer, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource) { let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) let botName: String @@ -14085,4 +14137,4 @@ private func openWebApp(parentController: ViewController, context: AccountContex parentController.present(controller, in: .window(.root)) } }) -} +}*/ diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 04ea7426a0..64f4f15f36 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -553,6 +553,10 @@ private final class ItemLayer: CALayer, SparseItemGridLayer { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + override init(layer: Any) { + super.init(layer: layer) + } deinit { self.disposable?.dispose() @@ -1104,6 +1108,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding { var onBeginFastScrollingImpl: (() -> Void)? var getShimmerColorsImpl: (() -> SparseItemGrid.ShimmerColors)? var updateShimmerLayersImpl: ((SparseItemGridDisplayItem) -> Void)? + var reorderIfPossibleImpl: ((SparseItemGrid.Item, Int) -> Void)? var revealedSpoilerMessageIds = Set() @@ -1356,6 +1361,12 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding { return .never() } } + + func reorderIfPossible(item: SparseItemGrid.Item, toIndex: Int) { + if let reorderIfPossibleImpl = self.reorderIfPossibleImpl { + reorderIfPossibleImpl(item, toIndex) + } + } func onTap(item: SparseItemGrid.Item, itemLayer: CALayer, point: CGPoint) { guard let item = item as? VisualMediaItem else { @@ -1556,6 +1567,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr private let directMediaImageCache: DirectMediaImageCache private var items: SparseItemGrid.Items? private var pinnedIds: Set = Set() + private var reorderedIds: [StoryId]? private var itemCount: Int? private var didUpdateItemsOnce: Bool = false @@ -1576,6 +1588,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr return self.selectedIdsPromise.get() } + private var isReordering: Bool = false + public var selectedItems: [Int32: EngineStoryItem] { var result: [Int32: EngineStoryItem] = [:] for id in self.selectedIds { @@ -1621,7 +1635,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr public var tabBarOffset: CGFloat { return self.itemGrid.coveringInsetOffset } - + + private var currentListState: StoryListContext.State? private var listDisposable: Disposable? private var hiddenMediaDisposable: Disposable? private let updateDisposable = MetaDisposable() @@ -1868,6 +1883,12 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr return nil } ) + storyContainerScreen.performReorderAction = { [weak self] in + guard let self else { + return + } + self.beginReordering() + } self.hiddenMediaDisposable?.dispose() self.hiddenMediaDisposable = (storyContainerScreen.focusedItem @@ -1913,6 +1934,13 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } strongSelf.openCurrentDate?() } + + self.itemGridBinding.reorderIfPossibleImpl = { [weak self] item, toIndex in + guard let self else { + return + } + self.reorderIfPossible(item: item, toIndex: toIndex) + } self.itemGridBinding.didScrollImpl = { [weak self] in guard let strongSelf = self else { @@ -2477,6 +2505,30 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr }))) } + if canManage, case .botPreview = self.scope { + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Reorder", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in + c?.dismiss(completion: { + guard let self else { + return + } + + self.beginReordering() + }) + }))) + + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Edit Preview", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in + c?.dismiss(completion: { + guard let self else { + return + } + + let _ = self + }) + }))) + } + if !item.isForwardingDisabled, case .everyone = item.privacy?.base { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.StoryList_ItemAction_Forward, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in c?.dismiss(completion: { @@ -2621,7 +2673,6 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.listDisposable?.dispose() self.listDisposable = nil - let context = self.context self.listDisposable = (state |> deliverOn(queue)).startStrict(next: { [weak self] state in guard let self else { @@ -2664,76 +2715,104 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr paneKey = .stories } self.statusPromise.set(.single(PeerInfoStatusData(text: title, isActivity: false, key: paneKey))) - - let timezoneOffset = Int32(TimeZone.current.secondsFromGMT()) - - var mappedItems: [SparseItemGrid.Item] = [] - var mappedHoles: [SparseItemGrid.HoleAnchor] = [] - var totalCount: Int = 0 - for item in state.items { - var peerReference: PeerReference? - if let value = state.peerReference { - peerReference = value - } else if let peer = item.peer { - peerReference = PeerReference(peer._asPeer()) - } - guard let peerReference else { - continue - } - - mappedItems.append(VisualMediaItem( - index: mappedItems.count, - peer: peerReference, - storyId: item.id, - story: item.storyItem, - authorPeer: item.peer, - isPinned: state.pinnedIds.contains(item.storyItem.id), - localMonthTimestamp: Month(localTimestamp: item.storyItem.timestamp + timezoneOffset).packedValue - )) - } - if mappedItems.count < state.totalCount, let lastItem = state.items.last, let _ = state.loadMoreToken { - mappedHoles.append(VisualMediaHoleAnchor(index: mappedItems.count, storyId: StoryId(peerId: context.account.peerId, id: Int32.max), localMonthTimestamp: Month(localTimestamp: lastItem.storyItem.timestamp + timezoneOffset).packedValue)) - } - totalCount = state.totalCount - totalCount = max(mappedItems.count, totalCount) - - if totalCount == 0 && state.loadMoreToken != nil && !state.isCached { - totalCount = 100 - } Queue.mainQueue().async { [weak self] in - guard let strongSelf = self else { + guard let self else { return } - var headerText: String? - if case let .peer(peerId, _, isArchived) = strongSelf.scope { - if isArchived && !mappedItems.isEmpty && peerId == strongSelf.context.account.peerId { - headerText = strongSelf.presentationData.strings.StoryList_ArchiveDescription - } - } - - let items = SparseItemGrid.Items( - items: mappedItems, - holeAnchors: mappedHoles, - count: totalCount, - itemBinding: strongSelf.itemGridBinding, - headerText: headerText, - snapTopInset: false - ) + self.currentListState = state - strongSelf.itemCount = state.totalCount - - let currentSynchronous = synchronous && firstTime - let currentReloadAtTop = reloadAtTop && firstTime + self.updateItemsFromState(state: state, firstTime: firstTime, reloadAtTop: reloadAtTop, synchronous: synchronous, animated: false) firstTime = false - strongSelf.updateHistory(items: items, pinnedIds: state.pinnedIds, synchronous: currentSynchronous, reloadAtTop: currentReloadAtTop) - strongSelf.isRequestingView = false + self.isRequestingView = false } }) } - private func updateHistory(items: SparseItemGrid.Items, pinnedIds: Set, synchronous: Bool, reloadAtTop: Bool) { + private func updateItemsFromState(state: StoryListContext.State, firstTime: Bool, reloadAtTop: Bool, synchronous: Bool, animated: Bool) { + let timezoneOffset = Int32(TimeZone.current.secondsFromGMT()) + + var mappedItems: [SparseItemGrid.Item] = [] + var mappedHoles: [SparseItemGrid.HoleAnchor] = [] + var totalCount: Int = 0 + + var stateItems = state.items + if let reorderedIds = self.reorderedIds { + var fixedStateItems: [StoryListContext.State.Item] = [] + + var seenIds = Set() + for id in reorderedIds { + if let index = stateItems.firstIndex(where: { $0.id == id }) { + seenIds.insert(id) + fixedStateItems.append(stateItems[index]) + } + } + + for item in stateItems { + if !seenIds.contains(item.id) { + fixedStateItems.append(item) + } + } + stateItems = fixedStateItems + self.reorderedIds = fixedStateItems.map(\.id) + } + + for item in stateItems { + var peerReference: PeerReference? + if let value = state.peerReference { + peerReference = value + } else if let peer = item.peer { + peerReference = PeerReference(peer._asPeer()) + } + guard let peerReference else { + continue + } + + mappedItems.append(VisualMediaItem( + index: mappedItems.count, + peer: peerReference, + storyId: item.id, + story: item.storyItem, + authorPeer: item.peer, + isPinned: state.pinnedIds.contains(item.storyItem.id), + localMonthTimestamp: Month(localTimestamp: item.storyItem.timestamp + timezoneOffset).packedValue + )) + } + if mappedItems.count < state.totalCount, let lastItem = state.items.last, let _ = state.loadMoreToken { + mappedHoles.append(VisualMediaHoleAnchor(index: mappedItems.count, storyId: StoryId(peerId: context.account.peerId, id: Int32.max), localMonthTimestamp: Month(localTimestamp: lastItem.storyItem.timestamp + timezoneOffset).packedValue)) + } + totalCount = state.totalCount + totalCount = max(mappedItems.count, totalCount) + + if totalCount == 0 && state.loadMoreToken != nil && !state.isCached { + totalCount = 100 + } + + var headerText: String? + if case let .peer(peerId, _, isArchived) = self.scope { + if isArchived && !mappedItems.isEmpty && peerId == self.context.account.peerId { + headerText = self.presentationData.strings.StoryList_ArchiveDescription + } + } + + let items = SparseItemGrid.Items( + items: mappedItems, + holeAnchors: mappedHoles, + count: totalCount, + itemBinding: self.itemGridBinding, + headerText: headerText, + snapTopInset: false + ) + + self.itemCount = state.totalCount + + let currentSynchronous = synchronous && firstTime + let currentReloadAtTop = reloadAtTop && firstTime + self.updateHistory(items: items, pinnedIds: state.pinnedIds, synchronous: currentSynchronous, reloadAtTop: currentReloadAtTop, animated: animated) + } + + private func updateHistory(items: SparseItemGrid.Items, pinnedIds: Set, synchronous: Bool, reloadAtTop: Bool, animated: Bool) { var transition: ContainedViewLayoutTransition = .immediate if case .location = self.scope, let previousItems = self.items, previousItems.items.count == 0, previousItems.count != 0, items.items.count == 0, items.count == 0 { transition = .animated(duration: 0.3, curve: .spring) @@ -2747,7 +2826,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr if reloadAtTop { gridSnapshot = self.itemGrid.view.snapshotView(afterScreenUpdates: false) } - self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: false, transition: transition) + self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: false, transition: transition, animateGridItems: animated) self.updateSelectedItems(animated: false) if let gridSnapshot = gridSnapshot { self.view.addSubview(gridSnapshot) @@ -2765,6 +2844,40 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } } + private func reorderIfPossible(item: SparseItemGrid.Item, toIndex: Int) { + if case .botPreview = self.scope, let items = self.items, let item = item as? VisualMediaItem { + guard let toItem = items.items.first(where: { $0.index == toIndex }) as? VisualMediaItem else { + return + } + if item.story.isPending || toItem.story.isPending { + return + } + + var ids = items.items.compactMap { item -> StoryId? in + return (item as? VisualMediaItem)?.storyId + } + + if let fromIndex = ids.firstIndex(of: item.storyId) { + if fromIndex < toIndex { + ids.insert(item.storyId, at: toIndex + 1) + ids.remove(at: fromIndex) + } else if fromIndex > toIndex { + ids.remove(at: fromIndex) + ids.insert(item.storyId, at: toIndex) + } + } + if self.reorderedIds != ids { + self.reorderedIds = ids + + HapticFeedback().tap() + + if let currentListState = self.currentListState { + self.updateItemsFromState(state: currentListState, firstTime: false, reloadAtTop: false, synchronous: false, animated: true) + } + } + } + } + public func scrollToTop() -> Bool { return self.itemGrid.scrollToTop() } @@ -2849,6 +2962,46 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr return nil*/ } + public func extractPendingStoryTransitionView() -> UIView? { + guard let items = self.items else { + return nil + } + guard let visualItem = items.items.last(where: { item in + guard let item = item as? VisualMediaItem else { + return false + } + if item.story.isPending { + return true + } + return false + }) else { + return nil + } + guard let item = self.itemGrid.item(at: visualItem.index) else { + return nil + } + + guard let itemLayer = item.layer as? ItemLayer else { + return nil + } + guard let story = itemLayer.item?.story else { + return nil + } + let rect = self.itemGrid.frameForItem(layer: itemLayer) + + let tempSourceNode = TempExtractedItemNode( + item: story, + itemLayer: itemLayer + ) + tempSourceNode.frame = rect + tempSourceNode.update(size: rect.size) + + self.tempContextContentItemNode = tempSourceNode + self.view.addSubview(tempSourceNode.view) + + return tempSourceNode.view + } + public func addToTransitionSurface(view: UIView) { self.itemGrid.addToTransitionSurface(view: view) } @@ -2982,7 +3135,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } private func updateSelectedItems(animated: Bool) { - self.contextGestureContainerNode.isGestureEnabled = self.isProfileEmbedded && self.itemInteraction.selectedIds == nil + self.contextGestureContainerNode.isGestureEnabled = self.isProfileEmbedded && self.itemInteraction.selectedIds == nil && !self.isReordering self.itemGrid.forEachVisibleItem { item in guard let itemLayer = item.layer as? ItemLayer, let item = itemLayer.item else { @@ -3304,6 +3457,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: synchronous, transition: transition, animateGridItems: false) + } + + private func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition, animateGridItems: Bool) { self.currentParams = (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) var gridTopInset = topInset @@ -3697,7 +3854,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr let fixedItemAspect: CGFloat? = 0.81 self.itemGrid.pinchEnabled = items.count > 2 - self.itemGrid.update(size: size, insets: UIEdgeInsets(top: gridTopInset, left: sideInset, bottom: bottomInset, right: sideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none) + self.itemGrid.update(size: size, insets: UIEdgeInsets(top: gridTopInset, left: sideInset, bottom: bottomInset, right: sideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none, transition: animateGridItems ? .spring(duration: 0.35) : .immediate) } } @@ -3788,12 +3945,45 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } var maxCount = 10 - if let data = self.context.currentAppConfiguration.with({ $0 }).data, let value = data["appConfig.bot_preview_medias_max"] as? Double { + if let data = self.context.currentAppConfiguration.with({ $0 }).data, let value = data["bot_preview_medias_max"] as? Double { maxCount = Int(value) } return items.count < maxCount } + + public func beginReordering() { + if let parentController = self.parentController as? PeerInfoScreen { + parentController.togglePaneIsReordering(isReordering: true) + } else { + self.updateIsReordering(isReordering: true, animated: true) + } + } + + public func endReordering() { + if let parentController = self.parentController as? PeerInfoScreen { + parentController.togglePaneIsReordering(isReordering: false) + } else { + self.updateIsReordering(isReordering: false, animated: true) + } + } + + public func updateIsReordering(isReordering: Bool, animated: Bool) { + if self.isReordering != isReordering { + self.isReordering = isReordering + + self.contextGestureContainerNode.isGestureEnabled = self.isProfileEmbedded && self.itemInteraction.selectedIds == nil && !self.isReordering + + self.itemGrid.setReordering(isReordering: isReordering) + + if !isReordering, let reorderedIds = self.reorderedIds { + self.reorderedIds = nil + if case .botPreview = self.scope, let listSource = self.listSource as? BotPreviewStoryListContext { + listSource.reorderItems(ids: reorderedIds) + } + } + } + } } private class MediaListSelectionRecognizer: UIPanGestureRecognizer { @@ -4033,7 +4223,6 @@ private final class BottomActionsPanelComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: BottomActionsPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift index 8d31667d61..f43cb26bca 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift @@ -331,6 +331,10 @@ private final class GenericItemLayer: CALayer, ItemLayer { self.contentsGravity = .resize } + + override init(layer: Any) { + super.init(layer: layer) + } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") @@ -995,6 +999,9 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme return .never() } } + + func reorderIfPossible(item: SparseItemGrid.Item, toIndex: Int) { + } func onTap(item: SparseItemGrid.Item, itemLayer: CALayer, point: CGPoint) { guard let item = item as? VisualMediaItem else { diff --git a/submodules/TelegramUI/Components/SpaceWarpView/BUILD b/submodules/TelegramUI/Components/SpaceWarpView/BUILD new file mode 100644 index 0000000000..44c7e34a6d --- /dev/null +++ b/submodules/TelegramUI/Components/SpaceWarpView/BUILD @@ -0,0 +1,19 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SpaceWarpView", + module_name = "SpaceWarpView", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/AsyncDisplayKit", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift b/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift new file mode 100644 index 0000000000..7e4ec16830 --- /dev/null +++ b/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift @@ -0,0 +1,8 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit + +open class SpaceWarpView: UIView { + +} diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 8aebaa5df7..8e2347ed53 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -1669,6 +1669,18 @@ private final class StoryContainerScreenComponent: Component { } component.content.markAsSeen(id: id) }, + reorder: { [weak self] in + guard let self, let environment = self.environment else { + return + } + var performReorderAction: (() -> Void)? + if let controller = environment.controller() as? StoryContainerScreen { + performReorderAction = controller.performReorderAction + } + environment.controller()?.dismiss(completion: { + performReorderAction?() + }) + }, controller: { [weak self] in return self?.environment?.controller() }, @@ -2027,6 +2039,7 @@ public class StoryContainerScreen: ViewControllerComponentContainer { } public var customBackAction: (() -> Void)? + public var performReorderAction: (() -> Void)? public init( context: AccountContext, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 5c49acf56c..f7060b88da 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -118,6 +118,7 @@ public final class StoryItemSetContainerComponent: Component { public let navigate: (NavigationDirection) -> Void public let delete: () -> Void public let markAsSeen: (StoryId) -> Void + public let reorder: () -> Void public let controller: () -> ViewController? public let toggleAmbientMode: () -> Void public let keyboardInputData: Signal @@ -154,6 +155,7 @@ public final class StoryItemSetContainerComponent: Component { navigate: @escaping (NavigationDirection) -> Void, delete: @escaping () -> Void, markAsSeen: @escaping (StoryId) -> Void, + reorder: @escaping () -> Void, controller: @escaping () -> ViewController?, toggleAmbientMode: @escaping () -> Void, keyboardInputData: Signal, @@ -189,6 +191,7 @@ public final class StoryItemSetContainerComponent: Component { self.navigate = navigate self.delete = delete self.markAsSeen = markAsSeen + self.reorder = reorder self.controller = controller self.toggleAmbientMode = toggleAmbientMode self.keyboardInputData = keyboardInputData @@ -5678,6 +5681,14 @@ public final class StoryItemSetContainerComponent: Component { component.presentController(actionSheet, nil) } + private func performReorderAction() { + guard let component = self.component else { + return + } + + component.reorder() + } + private func performLikeAction() { guard let component = self.component else { return @@ -6566,7 +6577,7 @@ public final class StoryItemSetContainerComponent: Component { return } - let _ = component + component.reorder() }))) items.append(.action(ContextMenuActionItem(text: "Edit Preview", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift index 0dba24ec14..11b5b619e6 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift @@ -12,38 +12,39 @@ import TelegramNotices import PresentationDataUtils import UndoUI import UrlHandling +import TelegramPresentationData -public extension ChatControllerImpl { - func openWebApp(buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource) { - guard let peerId = self.chatLocation.peerId, let peer = self.presentationInterfaceState.renderedPeer?.peer else { - return - } - let context = self.context - - self.chatDisplayNode.dismissInput() - - let botName: String - let botAddress: String - let botVerified: Bool - if case let .inline(bot) = source { - botName = bot.compactDisplayTitle - botAddress = bot.addressName ?? "" - botVerified = bot.isVerified - } else { - botName = EnginePeer(peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) - botAddress = peer.addressName ?? "" - botVerified = peer.isVerified - } - - if source == .generic { - self.updateChatPresentationInterfaceState(animated: true, interactive: true, { +func openWebAppImpl(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool) { + let presentationData: PresentationData + if let parentController = parentController as? ChatControllerImpl { + presentationData = parentController.presentationData + } else { + presentationData = context.sharedContext.currentPresentationData.with({ $0 }) + } + + let botName: String + let botAddress: String + let botVerified: Bool + if case let .inline(bot) = source { + botName = bot.compactDisplayTitle + botAddress = bot.addressName ?? "" + botVerified = bot.isVerified + } else { + botName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + botAddress = peer.addressName ?? "" + botVerified = peer.isVerified + } + + if source == .generic { + if let parentController = parentController as? ChatControllerImpl { + parentController.updateChatPresentationInterfaceState(animated: true, interactive: true, { return $0.updatedTitlePanelContext { if !$0.contains(where: { switch $0 { - case .requestInProgress: - return true - default: - return false + case .requestInProgress: + return true + default: + return false } }) { var updatedContexts = $0 @@ -54,202 +55,263 @@ public extension ChatControllerImpl { } }) } - - 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 + } + + let updateProgress = { [weak parentController] in + Queue.mainQueue().async { + if let parentController = parentController as? ChatControllerImpl { + parentController.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedTitlePanelContext { + if let index = $0.firstIndex(where: { + switch $0 { + case .requestInProgress: + return true + default: + return false } - return $0 + }) { + var updatedContexts = $0 + updatedContexts.remove(at: index) + return updatedContexts } - }) - } + return $0 + } + }) } } - - - let openWebView = { - if source == .menu { - self.updateChatPresentationInterfaceState(interactive: false) { state in + } + + + let openWebView = { [weak parentController] in + guard let parentController else { + return + } + if source == .menu { + if let parentController = parentController as? ChatControllerImpl { + parentController.updateChatPresentationInterfaceState(interactive: false) { state in return state.updatedForceInputCommandsHidden(true) } - - if let navigationController = self.navigationController as? NavigationController, let minimizedContainer = navigationController.minimizedContainer { - for controller in minimizedContainer.controllers { - if let controller = controller as? AttachmentController, let mainController = controller.mainController as? WebAppController, mainController.botId == peerId && mainController.source == .menu { - navigationController.maximizeViewController(controller, animated: true) - return - } + } + + if let navigationController = parentController.navigationController as? NavigationController, let minimizedContainer = navigationController.minimizedContainer { + for controller in minimizedContainer.controllers { + if let controller = controller as? AttachmentController, let mainController = controller.mainController as? WebAppController, mainController.botId == peer.id && mainController.source == .menu { + navigationController.maximizeViewController(controller, animated: true) + return } } - - var fullSize = false - if isTelegramMeLink(url), let internalUrl = parseFullInternalUrl(sharedContext: self.context.sharedContext, url: url), case .peer(_, .appStart) = internalUrl { - fullSize = !url.contains("?mode=compact") - } + } + + var fullSize = false + if isTelegramMeLink(url), let internalUrl = parseFullInternalUrl(sharedContext: context.sharedContext, url: url), case .peer(_, .appStart) = internalUrl { + fullSize = !url.contains("?mode=compact") + } - var presentImpl: ((ViewController, Any?) -> Void)? - let params = WebAppParameters(source: .menu, peerId: peerId, botId: peerId, botName: botName, botVerified: botVerified, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: fullSize) - let controller = standaloneWebAppController(context: self.context, updatedPresentationData: self.updatedPresentationData, params: params, threadId: self.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in - ChatControllerImpl.botOpenUrl(context: context, peerId: peerId, controller: self, url: url, concealed: concealed, present: { c, a in - presentImpl?(c, a) - }, commit: commit) - }, requestSwitchInline: { [weak self] query, chatTypes, completion in - ChatControllerImpl.botRequestSwitchInline(context: context, controller: self, peerId: peerId, botAddress: botAddress, query: query, chatTypes: chatTypes, completion: completion) - }, getInputContainerNode: { [weak self] in - if let strongSelf = self, let layout = strongSelf.validLayout, case .compact = layout.metrics.widthClass { - return (strongSelf.chatDisplayNode.getWindowInputAccessoryHeight(), strongSelf.chatDisplayNode.inputPanelContainerNode, { - return strongSelf.chatDisplayNode.textInputPanelNode?.makeAttachmentMenuTransition(accessoryPanelNode: nil) - }) - } else { - return nil - } - }, completion: { [weak self] in - self?.chatDisplayNode.historyNode.scrollToEndOfHistory() - }, willDismiss: { [weak self] in - self?.interfaceInteraction?.updateShowWebView { _ in + var presentImpl: ((ViewController, Any?) -> Void)? + let params = WebAppParameters(source: .menu, peerId: peer.id, botId: peer.id, botName: botName, botVerified: botVerified, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: fullSize) + let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, commit in + ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, present: { c, a in + presentImpl?(c, a) + }, commit: commit) + }, requestSwitchInline: { [weak parentController] query, chatTypes, completion in + ChatControllerImpl.botRequestSwitchInline(context: context, controller: parentController as? ChatControllerImpl, peerId: peer.id, botAddress: botAddress, query: query, chatTypes: chatTypes, completion: completion) + }, getInputContainerNode: { [weak parentController] in + if let parentController = parentController as? ChatControllerImpl, let layout = parentController.validLayout, case .compact = layout.metrics.widthClass { + return (parentController.chatDisplayNode.getWindowInputAccessoryHeight(), parentController.chatDisplayNode.inputPanelContainerNode, { + return parentController.chatDisplayNode.textInputPanelNode?.makeAttachmentMenuTransition(accessoryPanelNode: nil) + }) + } else { + return nil + } + }, completion: { [weak parentController] in + if let parentController = parentController as? ChatControllerImpl { + parentController.chatDisplayNode.historyNode.scrollToEndOfHistory() + } + }, willDismiss: { [weak parentController] in + if let parentController = parentController as? ChatControllerImpl { + parentController.interfaceInteraction?.updateShowWebView { _ in return false } - }, didDismiss: { [weak self] in - if let strongSelf = self { - let isFocused = strongSelf.chatDisplayNode.textInputPanelNode?.isFocused ?? false - strongSelf.chatDisplayNode.insertSubnode(strongSelf.chatDisplayNode.inputPanelContainerNode, aboveSubnode: strongSelf.chatDisplayNode.inputContextPanelContainer) - if isFocused { - strongSelf.chatDisplayNode.textInputPanelNode?.ensureFocused() - } - - strongSelf.updateChatPresentationInterfaceState(interactive: false) { state in - return state.updatedForceInputCommandsHidden(false) - } + } + }, didDismiss: { [weak parentController] in + if let parentController = parentController as? ChatControllerImpl { + let isFocused = parentController.chatDisplayNode.textInputPanelNode?.isFocused ?? false + parentController.chatDisplayNode.insertSubnode(parentController.chatDisplayNode.inputPanelContainerNode, aboveSubnode: parentController.chatDisplayNode.inputContextPanelContainer) + if isFocused { + parentController.chatDisplayNode.textInputPanelNode?.ensureFocused() + } + + parentController.updateChatPresentationInterfaceState(interactive: false) { state in + return state.updatedForceInputCommandsHidden(false) + } + } + }, getNavigationController: { [weak parentController] in + if let parentController = parentController as? ChatControllerImpl { + return parentController.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController + } else { + return parentController?.navigationController as? NavigationController + } + }) + controller.navigationPresentation = .flatModal + parentController.push(controller) + + presentImpl = { [weak controller] c, a in + controller?.present(c, in: .window(.root), with: a) + } + } else if simple { + var isInline = false + var botId = peer.id + var botName = botName + var botAddress = "" + var botVerified = false + if case let .inline(bot) = source { + isInline = true + botId = bot.id + botName = bot.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + botAddress = bot.addressName ?? "" + botVerified = bot.isVerified + } + + let messageActionCallbackDisposable: MetaDisposable + if let parentController = parentController as? ChatControllerImpl { + messageActionCallbackDisposable = parentController.messageActionCallbackDisposable + } else { + messageActionCallbackDisposable = MetaDisposable() + } + + let webViewSignal: Signal + if url.isEmpty { + webViewSignal = context.engine.messages.requestMainWebView(botId: botId, source: isInline ? .inline : .generic, themeParams: generateWebAppThemeParams(presentationData.theme)) + } else { + webViewSignal = context.engine.messages.requestSimpleWebView(botId: botId, url: url, source: isInline ? .inline : .generic, themeParams: generateWebAppThemeParams(presentationData.theme)) + } + + messageActionCallbackDisposable.set(((webViewSignal + |> afterDisposed { + updateProgress() + }) + |> deliverOnMainQueue).start(next: { [weak parentController] result in + guard let parentController else { + return + } + var presentImpl: ((ViewController, Any?) -> Void)? + let params = WebAppParameters(source: isInline ? .inline : .simple, peerId: peer.id, botId: botId, botName: botName, botVerified: botVerified, url: result.url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize)) + let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, commit in + ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, present: { c, a in + presentImpl?(c, a) + }, commit: commit) + }, requestSwitchInline: { [weak parentController] query, chatTypes, completion in + ChatControllerImpl.botRequestSwitchInline(context: context, controller: parentController as? ChatControllerImpl, peerId: peer.id, botAddress: botAddress, query: query, chatTypes: chatTypes, completion: completion) + }, getNavigationController: { [weak parentController] in + if let parentController = parentController as? ChatControllerImpl { + return parentController.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController + } else { + return parentController?.navigationController as? NavigationController } - }, getNavigationController: { [weak self] in - return self?.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController }) controller.navigationPresentation = .flatModal - self.push(controller) + if let parentController = parentController as? ChatControllerImpl { + parentController.currentWebAppController = controller + } + parentController.push(controller) presentImpl = { [weak controller] c, a in controller?.present(c, in: .window(.root), with: a) } - } else if simple { - var isInline = false - var botId = peerId - var botName = botName - var botAddress = "" - var botVerified = false - if case let .inline(bot) = source { - isInline = true - botId = bot.id - botName = bot.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) - botAddress = bot.addressName ?? "" - botVerified = bot.isVerified + }, error: { [weak parentController] error in + if let parentController { + parentController.present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { + })]), in: .window(.root)) } - - self.messageActionCallbackDisposable.set(((self.context.engine.messages.requestSimpleWebView(botId: botId, url: url, source: isInline ? .inline : .generic, themeParams: generateWebAppThemeParams(self.presentationData.theme)) - |> afterDisposed { - updateProgress() - }) - |> deliverOnMainQueue).startStrict(next: { [weak self] result in - guard let strongSelf = self else { - return - } - var presentImpl: ((ViewController, Any?) -> Void)? - let context = strongSelf.context - let params = WebAppParameters(source: isInline ? .inline : .simple, peerId: peerId, botId: botId, botName: botName, botVerified: botVerified, url: result.url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize)) - let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in - ChatControllerImpl.botOpenUrl(context: context, peerId: peerId, controller: self, url: url, concealed: concealed, present: { c, a in - presentImpl?(c, a) - }, commit: commit) - }, requestSwitchInline: { [weak self] query, chatTypes, completion in - ChatControllerImpl.botRequestSwitchInline(context: context, controller: self, peerId: peerId, botAddress: botAddress, query: query, chatTypes: chatTypes, completion: completion) - }, getNavigationController: { [weak self] in - return self?.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController - }) - controller.navigationPresentation = .flatModal - strongSelf.currentWebAppController = controller - strongSelf.push(controller) - - presentImpl = { [weak controller] c, a in - controller?.present(c, in: .window(.root), with: a) - } - }, 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)) - } - })) + })) + } else { + let messageActionCallbackDisposable: MetaDisposable + if let parentController = parentController as? ChatControllerImpl { + messageActionCallbackDisposable = parentController.messageActionCallbackDisposable } else { - self.messageActionCallbackDisposable.set(((self.context.engine.messages.requestWebView(peerId: peerId, botId: peerId, url: !url.isEmpty ? url : nil, payload: nil, themeParams: generateWebAppThemeParams(self.presentationData.theme), fromMenu: false, replyToMessageId: nil, threadId: self.chatLocation.threadId) - |> afterDisposed { - updateProgress() - }) - |> deliverOnMainQueue).startStrict(next: { [weak self] result in - guard let strongSelf = self else { - return - } - var presentImpl: ((ViewController, Any?) -> Void)? - let context = strongSelf.context - let params = WebAppParameters(source: .button, peerId: peerId, botId: peerId, botName: botName, botVerified: botVerified, url: result.url, queryId: result.queryId, payload: nil, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, forceHasSettings: false, fullSize: result.flags.contains(.fullSize)) - let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in - ChatControllerImpl.botOpenUrl(context: context, peerId: peerId, controller: self, url: url, concealed: concealed, present: { c, a in - presentImpl?(c, a) - }, commit: commit) - }, completion: { [weak self] in - self?.chatDisplayNode.historyNode.scrollToEndOfHistory() - }, getNavigationController: { [weak self] in - return self?.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController - }) - controller.navigationPresentation = .flatModal - strongSelf.currentWebAppController = controller - strongSelf.push(controller) - - presentImpl = { [weak controller] c, a in - controller?.present(c, in: .window(.root), with: a) - } - }, 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)) - } - })) + messageActionCallbackDisposable = MetaDisposable() } + + messageActionCallbackDisposable.set(((context.engine.messages.requestWebView(peerId: peer.id, botId: peer.id, url: !url.isEmpty ? url : nil, payload: nil, themeParams: generateWebAppThemeParams(presentationData.theme), fromMenu: false, replyToMessageId: nil, threadId: threadId) + |> afterDisposed { + updateProgress() + }) + |> deliverOnMainQueue).startStrict(next: { [weak parentController] result in + guard let parentController else { + return + } + var presentImpl: ((ViewController, Any?) -> Void)? + let params = WebAppParameters(source: .button, peerId: peer.id, botId: peer.id, botName: botName, botVerified: botVerified, url: result.url, queryId: result.queryId, payload: nil, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, forceHasSettings: false, fullSize: result.flags.contains(.fullSize)) + let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, commit in + ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, present: { c, a in + presentImpl?(c, a) + }, commit: commit) + }, completion: { [weak parentController] in + if let parentController = parentController as? ChatControllerImpl { + parentController.chatDisplayNode.historyNode.scrollToEndOfHistory() + } + }, getNavigationController: { [weak parentController] in + if let parentController = parentController as? ChatControllerImpl { + return parentController.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController + } else { + return parentController?.navigationController as? NavigationController + } + }) + controller.navigationPresentation = .flatModal + if let parentController = parentController as? ChatControllerImpl { + parentController.currentWebAppController = controller + } + parentController.push(controller) + + presentImpl = { [weak controller] c, a in + controller?.present(c, in: .window(.root), with: a) + } + }, error: { [weak parentController] error in + if let parentController { + parentController.present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { + })]), in: .window(.root)) + } + })) } - - var botPeer = EnginePeer(peer) + } + + if skipTermsOfService { + openWebView() + } else { + var botPeer = peer if case let .inline(bot) = source { botPeer = bot } - let _ = (ApplicationSpecificNotice.getBotGameNotice(accountManager: self.context.sharedContext.accountManager, peerId: botPeer.id) - |> deliverOnMainQueue).startStandalone(next: { [weak self] value in - guard let strongSelf = self else { + let _ = (ApplicationSpecificNotice.getBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id) + |> deliverOnMainQueue).startStandalone(next: { [weak parentController] value in + guard let parentController else { return } - + if value { openWebView() } else { - let controller = webAppLaunchConfirmationController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: botPeer, completion: { _ in - let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: strongSelf.context.sharedContext.accountManager, peerId: botPeer.id).startStandalone() + let controller = webAppLaunchConfirmationController(context: context, updatedPresentationData: updatedPresentationData, peer: botPeer, completion: { _ in + let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).startStandalone() openWebView() }, showMore: nil) - strongSelf.present(controller, in: .window(.root)) + parentController.present(controller, in: .window(.root)) } }) } +} + +public extension ChatControllerImpl { + func openWebApp(buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource) { + guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { + return + } + self.chatDisplayNode.dismissInput() + + self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), threadId: self.chatLocation.threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: false) + } - private static func botRequestSwitchInline(context: AccountContext, controller: ChatControllerImpl?, peerId: EnginePeer.Id, botAddress: String, query: String, chatTypes: [ReplyMarkupButtonRequestPeerType]?, completion: @escaping () -> Void) -> Void { + static func botRequestSwitchInline(context: AccountContext, controller: ChatControllerImpl?, peerId: EnginePeer.Id, botAddress: String, query: String, chatTypes: [ReplyMarkupButtonRequestPeerType]?, completion: @escaping () -> Void) -> Void { let activateSwitchInline = { var chatController: ChatControllerImpl? if let current = controller { @@ -311,7 +373,7 @@ public extension ChatControllerImpl { }) } - private static func botOpenUrl(context: AccountContext, peerId: EnginePeer.Id, controller: ChatControllerImpl?, url: String, concealed: Bool, present: @escaping (ViewController, Any?) -> Void, commit: @escaping () -> Void = {}) { + static func botOpenUrl(context: AccountContext, peerId: EnginePeer.Id, controller: ChatControllerImpl?, url: String, concealed: Bool, present: @escaping (ViewController, Any?) -> Void, commit: @escaping () -> Void = {}) { if let controller { controller.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) } else { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 54f2d73f79..d1f3cd3e2d 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2679,6 +2679,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { public func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController { return StarsTransactionScreen(context: context, subject: .gift(message), action: {}) } + + public func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool) { + openWebAppImpl(context: context, parentController: parentController, updatedPresentationData: updatedPresentationData, peer: peer, threadId: threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: skipTermsOfService) + } } private func peerInfoControllerImpl(context: AccountContext, updatedPresentationData: (PresentationData, Signal)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, requestsContext: PeerInvitationImportersContext? = nil) -> ViewController? { diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 4979df19a1..610a6fbd86 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -338,7 +338,8 @@ public final class TelegramRootController: NavigationController, TelegramRootCon return CameraScreen.TransitionOut( destinationView: destinationView, destinationRect: transitionOut.destinationRect, - destinationCornerRadius: transitionOut.destinationCornerRadius + destinationCornerRadius: transitionOut.destinationCornerRadius, + completion: transitionOut.completion ) } else { return nil @@ -408,13 +409,15 @@ public final class TelegramRootController: NavigationController, TelegramRootCon return MediaEditorScreen.TransitionOut( destinationView: destinationView, destinationRect: transitionOut.destinationRect, - destinationCornerRadius: transitionOut.destinationCornerRadius + destinationCornerRadius: transitionOut.destinationCornerRadius, + completion: transitionOut.completion ) } else if !finished, let resultTransition, let (destinationView, destinationRect) = resultTransition.transitionOut(isNew) { return MediaEditorScreen.TransitionOut( destinationView: destinationView, destinationRect: destinationRect, - destinationCornerRadius: 0.0 + destinationCornerRadius: 0.0, + completion: nil ) } else { return nil