diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 9e21f14875..da9d89bc63 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -246,7 +246,7 @@ public enum ChatControllerSubject: Equatable { public enum ChatControllerPresentationMode: Equatable { case standard(previewing: Bool) - case overlay + case overlay(NavigationController?) case inline(NavigationController?) } diff --git a/submodules/AppLock/Sources/AppLock.swift b/submodules/AppLock/Sources/AppLock.swift index 8e8ecda201..fdb2016360 100644 --- a/submodules/AppLock/Sources/AppLock.swift +++ b/submodules/AppLock/Sources/AppLock.swift @@ -44,19 +44,19 @@ private func getCoveringViewSnaphot(window: Window1) -> UIImage? { context.clear(CGRect(origin: CGPoint(), size: size)) context.scaleBy(x: scale, y: scale) UIGraphicsPushContext(context) - window.forEachViewController { controller in + window.forEachViewController({ controller in if let controller = controller as? PasscodeEntryController { controller.displayNode.alpha = 0.0 } return true - } + }) window.hostView.containerView.drawHierarchy(in: CGRect(origin: CGPoint(), size: unscaledSize), afterScreenUpdates: false) - window.forEachViewController { controller in + window.forEachViewController({ controller in if let controller = controller as? PasscodeEntryController { controller.displayNode.alpha = 1.0 } return true - } + }) UIGraphicsPopContext() }).flatMap(applyScreenshotEffectToImage) } diff --git a/submodules/ArchivedStickerPacksNotice/BUCK b/submodules/ArchivedStickerPacksNotice/BUCK new file mode 100644 index 0000000000..d544ffbad2 --- /dev/null +++ b/submodules/ArchivedStickerPacksNotice/BUCK @@ -0,0 +1,29 @@ +load("//Config:buck_rule_macros.bzl", "static_library") + +static_library( + name = "ArchivedStickerPacksNotice", + srcs = glob([ + "Sources/**/*.swift", + ]), + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit#shared", + "//submodules/AsyncDisplayKit:AsyncDisplayKit#shared", + "//submodules/Display:Display#shared", + "//submodules/Postbox:Postbox#shared", + "//submodules/TelegramCore:TelegramCore#shared", + "//submodules/SyncCore:SyncCore#shared", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/AccountContext:AccountContext", + "//submodules/TelegramUIPreferences:TelegramUIPreferences", + "//submodules/StickerResources:StickerResources", + "//submodules/AlertUI:AlertUI", + "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/MergeLists:MergeLists", + "//submodules/ItemListUI:ItemListUI", + "//submodules/ItemListStickerPackItem:ItemListStickerPackItem", + ], + frameworks = [ + "$SDKROOT/System/Library/Frameworks/Foundation.framework", + "$SDKROOT/System/Library/Frameworks/UIKit.framework", + ], +) diff --git a/submodules/ArchivedStickerPacksNotice/Sources/ArchivedStickerPacksNoticeController.swift b/submodules/ArchivedStickerPacksNotice/Sources/ArchivedStickerPacksNoticeController.swift new file mode 100644 index 0000000000..4cc7acdcb2 --- /dev/null +++ b/submodules/ArchivedStickerPacksNotice/Sources/ArchivedStickerPacksNoticeController.swift @@ -0,0 +1,316 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import SyncCore +import TelegramPresentationData +import ActivityIndicator +import AccountContext +import AlertUI +import PresentationDataUtils +import MergeLists +import ItemListUI +import ItemListStickerPackItem + +private struct ArchivedStickersNoticeEntry: Comparable, Identifiable { + let index: Int + let info: StickerPackCollectionInfo + let topItem: StickerPackItem? + let count: String + + var stableId: ItemCollectionId { + return info.id + } + + static func ==(lhs: ArchivedStickersNoticeEntry, rhs: ArchivedStickersNoticeEntry) -> Bool { + return lhs.index == rhs.index && lhs.info.id == rhs.info.id && lhs.count == rhs.count + } + + static func <(lhs: ArchivedStickersNoticeEntry, rhs: ArchivedStickersNoticeEntry) -> Bool { + return lhs.index < rhs.index + } + + func item(account: Account, presentationData: PresentationData) -> ListViewItem { + return ItemListStickerPackItem(presentationData: ItemListPresentationData(presentationData), account: account, packInfo: info, itemCount: self.count, topItem: topItem, unread: false, control: .none, editing: ItemListStickerPackItemEditing(editable: false, editing: false, revealed: false, reorderable: false), enabled: true, playAnimatedStickers: true, sectionId: 0, action: { + }, setPackIdWithRevealedOptions: { current, previous in + }, addPack: { + }, removePack: { + }) + } +} + +private struct ArchivedStickersNoticeTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] +} + +private func preparedTransition(from fromEntries: [ArchivedStickersNoticeEntry], to toEntries: [ArchivedStickersNoticeEntry], account: Account, presentationData: PresentationData) -> ArchivedStickersNoticeTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, presentationData: presentationData), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, presentationData: presentationData), directionHint: nil) } + + return ArchivedStickersNoticeTransition(deletions: deletions, insertions: insertions, updates: updates) +} + + +private final class ArchivedStickersNoticeAlertContentNode: AlertContentNode { + private let presentationData: PresentationData + private let archivedStickerPacks: [(StickerPackCollectionInfo, StickerPackItem?)] + + private let textNode: ASTextNode + private let listView: ListView + + private var enqueuedTransitions: [ArchivedStickersNoticeTransition] = [] + + private let actionNodesSeparator: ASDisplayNode + private let actionNodes: [TextAlertContentActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + private let disposable = MetaDisposable() + + private var validLayout: CGSize? + + override var dismissOnOutsideTap: Bool { + return self.isUserInteractionEnabled + } + + init(theme: AlertControllerTheme, account: Account, presentationData: PresentationData, archivedStickerPacks: [(StickerPackCollectionInfo, StickerPackItem?)], actions: [TextAlertAction]) { + self.presentationData = presentationData + self.archivedStickerPacks = archivedStickerPacks + + self.textNode = ASTextNode() + self.textNode.maximumNumberOfLines = 4 + + self.listView = ListView() + self.listView.isOpaque = false + + self.actionNodesSeparator = ASDisplayNode() + self.actionNodesSeparator.isLayerBacked = true + + self.actionNodes = actions.map { action -> TextAlertContentActionNode in + return TextAlertContentActionNode(theme: theme, action: action) + } + + var actionVerticalSeparators: [ASDisplayNode] = [] + if actions.count > 1 { + for _ in 0 ..< actions.count - 1 { + let separatorNode = ASDisplayNode() + separatorNode.isLayerBacked = true + actionVerticalSeparators.append(separatorNode) + } + } + self.actionVerticalSeparators = actionVerticalSeparators + + super.init() + + self.addSubnode(self.textNode) + self.addSubnode(self.listView) + + self.addSubnode(self.actionNodesSeparator) + + for actionNode in self.actionNodes { + self.addSubnode(actionNode) + } + self.actionNodes.last?.actionEnabled = false + + for separatorNode in self.actionVerticalSeparators { + self.addSubnode(separatorNode) + } + + self.updateTheme(theme) + + var index: Int = 0 + var entries: [ArchivedStickersNoticeEntry] = [] + for pack in archivedStickerPacks { + entries.append(ArchivedStickersNoticeEntry(index: index, info: pack.0, topItem: pack.1, count: presentationData.strings.StickerPack_StickerCount(pack.0.count))) + index += 1 + } + + let transition = preparedTransition(from: [], to: entries, account: account, presentationData: presentationData) + self.enqueueTransition(transition) + } + + deinit { + self.disposable.dispose() + } + + private func enqueueTransition(_ transition: ArchivedStickersNoticeTransition) { + self.enqueuedTransitions.append(transition) + + if let _ = self.validLayout { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + private func dequeueTransition() { + guard let layout = self.validLayout, let transition = self.enqueuedTransitions.first else { + return + } + self.enqueuedTransitions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + + self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in + }) + } + + override func updateTheme(_ theme: AlertControllerTheme) { + self.textNode.attributedText = NSAttributedString(string: self.presentationData.strings.ArchivedPacksAlert_Title, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) + + self.actionNodesSeparator.backgroundColor = theme.separatorColor + for actionNode in self.actionNodes { + actionNode.updateTheme(theme) + } + for separatorNode in self.actionVerticalSeparators { + separatorNode.backgroundColor = theme.separatorColor + } + + if let size = self.validLayout { + _ = self.updateLayout(size: size, transition: .immediate) + } + } + + override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + var size = size + size.width = min(size.width, 270.0) + let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude) + + let hadValidLayout = self.validLayout != nil + + self.validLayout = size + + var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) + + let textSize = self.textNode.measure(measureSize) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)) + origin.y += textSize.height + 16.0 + + let actionButtonHeight: CGFloat = 44.0 + var minActionsWidth: CGFloat = 0.0 + let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) + let actionTitleInsets: CGFloat = 8.0 + + var effectiveActionLayout = TextAlertContentActionLayout.horizontal + for actionNode in self.actionNodes { + let actionTitleSize = actionNode.titleNode.measure(CGSize(width: maxActionWidth, height: actionButtonHeight)) + if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { + effectiveActionLayout = .vertical + } + switch effectiveActionLayout { + case .horizontal: + minActionsWidth += actionTitleSize.width + actionTitleInsets + case .vertical: + minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) + } + } + + let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0) + + var contentWidth = max(textSize.width, minActionsWidth) + contentWidth = max(contentWidth, 234.0) + + var actionsHeight: CGFloat = 0.0 + switch effectiveActionLayout { + case .horizontal: + actionsHeight = actionButtonHeight + case .vertical: + actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) + } + + let resultWidth = contentWidth + insets.left + insets.right + + let listHeight: CGFloat = CGFloat(min(3, self.archivedStickerPacks.count)) * 56.0 + + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: resultWidth, height: listHeight), insets: UIEdgeInsets(top: -35.0, left: 0.0, bottom: 0.0, right: 0.0), headerInsets: UIEdgeInsets(), scrollIndicatorInsets: UIEdgeInsets(), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + transition.updateFrame(node: self.listView, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: listHeight)) + + let resultSize = CGSize(width: resultWidth, height: textSize.height + actionsHeight + listHeight + 10.0 + insets.top + insets.bottom) + + transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + + var actionOffset: CGFloat = 0.0 + let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) + var separatorIndex = -1 + var nodeIndex = 0 + for actionNode in self.actionNodes { + if separatorIndex >= 0 { + let separatorNode = self.actionVerticalSeparators[separatorIndex] + switch effectiveActionLayout { + case .horizontal: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) + case .vertical: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + } + } + separatorIndex += 1 + + let currentActionWidth: CGFloat + switch effectiveActionLayout { + case .horizontal: + if nodeIndex == self.actionNodes.count - 1 { + currentActionWidth = resultSize.width - actionOffset + } else { + currentActionWidth = actionWidth + } + case .vertical: + currentActionWidth = resultSize.width + } + + let actionNodeFrame: CGRect + switch effectiveActionLayout { + case .horizontal: + actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += currentActionWidth + case .vertical: + actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += actionButtonHeight + } + + transition.updateFrame(node: actionNode, frame: actionNodeFrame) + + nodeIndex += 1 + } + + if !hadValidLayout { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + + return resultSize + } +} + +public func archivedStickerPacksNoticeController(context: AccountContext, archivedStickerPacks: [(StickerPackCollectionInfo, StickerPackItem?)]) -> ViewController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + var dismissImpl: (() -> Void)? + + let disposable = MetaDisposable() + + let contentNode = ArchivedStickersNoticeAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), account: context.account, presentationData: presentationData, archivedStickerPacks: archivedStickerPacks, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { + dismissImpl?() + })]) + + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) + let presentationDataDisposable = context.sharedContext.presentationData.start(next: { [weak controller, weak contentNode] presentationData in + controller?.theme = AlertControllerTheme(presentationData: presentationData) + }) + controller.dismissed = { + presentationDataDisposable.dispose() + disposable.dispose() + } + dismissImpl = { [weak controller, weak contentNode] in + controller?.dismissAnimated() + } + return controller +} diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index f1854fdfec..f2049711c5 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -306,7 +306,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, strongSelf.titleView.title = NetworkStatusTitle(text: defaultTitle, activity: false, hasProxy: isRoot && hasProxy, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked) } if case .root = groupId, checkProxy { - if strongSelf.proxyUnavailableTooltipController == nil && !strongSelf.didShowProxyUnavailableTooltipController && strongSelf.isNodeLoaded && strongSelf.displayNode.view.window != nil { + if strongSelf.proxyUnavailableTooltipController == nil && !strongSelf.didShowProxyUnavailableTooltipController && strongSelf.isNodeLoaded && strongSelf.displayNode.view.window != nil && strongSelf.navigationController?.topViewController === self { strongSelf.didShowProxyUnavailableTooltipController = true let tooltipController = TooltipController(content: .text(strongSelf.presentationData.strings.Proxy_TooltipUnavailable), baseFontSize: strongSelf.presentationData.listsFontSize.baseDisplaySize, timeout: 60.0, dismissByTapOutside: true) strongSelf.proxyUnavailableTooltipController = tooltipController diff --git a/submodules/Display/Display/WindowContent.swift b/submodules/Display/Display/WindowContent.swift index 868b94b6eb..19514e85ee 100644 --- a/submodules/Display/Display/WindowContent.swift +++ b/submodules/Display/Display/WindowContent.swift @@ -1222,10 +1222,12 @@ public class Window1 { return hidden } - public func forEachViewController(_ f: (ContainableController) -> Bool) { + public func forEachViewController(_ f: (ContainableController) -> Bool, excludeNavigationSubControllers: Bool = false) { if let navigationController = self._rootController as? NavigationController { - for case let controller as ContainableController in navigationController.viewControllers { - !f(controller) + if !excludeNavigationSubControllers { + for case let controller as ContainableController in navigationController.viewControllers { + !f(controller) + } } if let controller = navigationController.topOverlayController { !f(controller) diff --git a/submodules/LegacyUI/Sources/TelegramInitializeLegacyComponents.swift b/submodules/LegacyUI/Sources/TelegramInitializeLegacyComponents.swift index 32df01e865..b77631410c 100644 --- a/submodules/LegacyUI/Sources/TelegramInitializeLegacyComponents.swift +++ b/submodules/LegacyUI/Sources/TelegramInitializeLegacyComponents.swift @@ -48,6 +48,13 @@ private final class LegacyComponentsAccessCheckerImpl: NSObject, LegacyComponent } public func checkPhotoAuthorizationStatus(for intent: TGPhotoAccessIntent, alertDismissCompletion: (() -> Void)!) -> Bool { + if let context = self.context { + DeviceAccess.authorizeAccess(to: .mediaLibrary(.send), presentationData: context.sharedContext.currentPresentationData.with { $0 }, present: context.sharedContext.presentGlobalController, openSettings: context.sharedContext.applicationBindings.openSettings, { value in + if !value { + alertDismissCompletion?() + } + }) + } return true } diff --git a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift index f13772ef3b..64a1d42783 100644 --- a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift +++ b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift @@ -548,7 +548,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode { if foundVenues == nil && !state.searchingVenuesAround { displayingPlacesButton = true } else if let previousLocation = foundVenuesLocation { - let currentLocation = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude) + let currentLocation = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude) if currentLocation.distance(from: previousLocation) > 300 { displayingPlacesButton = true } diff --git a/submodules/SettingsUI/Sources/Themes/EditThemeController.swift b/submodules/SettingsUI/Sources/Themes/EditThemeController.swift index d0933c5a1c..b918e65d1a 100644 --- a/submodules/SettingsUI/Sources/Themes/EditThemeController.swift +++ b/submodules/SettingsUI/Sources/Themes/EditThemeController.swift @@ -551,7 +551,7 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) } - let themeReference: PresentationThemeReference = .cloud(PresentationCloudTheme(theme: resultTheme, resolvedWallpaper: resolvedWallpaper)) + let themeReference: PresentationThemeReference = .cloud(PresentationCloudTheme(theme: resultTheme, resolvedWallpaper: resolvedWallpaper, creatorAccountId: context.account.id)) var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers themeSpecificChatWallpapers[themeReference.index] = nil @@ -585,7 +585,7 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) } - let themeReference: PresentationThemeReference = .cloud(PresentationCloudTheme(theme: resultTheme, resolvedWallpaper: resolvedWallpaper)) + let themeReference: PresentationThemeReference = .cloud(PresentationCloudTheme(theme: resultTheme, resolvedWallpaper: resolvedWallpaper, creatorAccountId: context.account.id)) var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers themeSpecificChatWallpapers[themeReference.index] = nil diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorController.swift b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorController.swift index 11f2677bcc..af8fd96d9f 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorController.swift @@ -260,7 +260,7 @@ final class ThemeAccentColorController: ViewController { if case let .result(resultTheme) = next { let _ = applyTheme(accountManager: context.sharedContext.accountManager, account: context.account, theme: resultTheme).start() return updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in - let themeReference: PresentationThemeReference = .cloud(PresentationCloudTheme(theme: resultTheme, resolvedWallpaper: wallpaper)) + let themeReference: PresentationThemeReference = .cloud(PresentationCloudTheme(theme: resultTheme, resolvedWallpaper: wallpaper, creatorAccountId: context.account.id)) var updatedTheme = current.theme var updatedAutomaticThemeSwitchSetting = current.automaticThemeSwitchSetting @@ -289,7 +289,7 @@ final class ThemeAccentColorController: ViewController { if case let .result(resultTheme) = next { let _ = applyTheme(accountManager: context.sharedContext.accountManager, account: context.account, theme: resultTheme).start() return updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in - let themeReference: PresentationThemeReference = .cloud(PresentationCloudTheme(theme: resultTheme, resolvedWallpaper: wallpaper)) + let themeReference: PresentationThemeReference = .cloud(PresentationCloudTheme(theme: resultTheme, resolvedWallpaper: wallpaper, creatorAccountId: context.account.id)) var updatedTheme = current.theme var updatedAutomaticThemeSwitchSetting = current.automaticThemeSwitchSetting diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift index 34b81ce429..a95320fa48 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift @@ -550,7 +550,7 @@ public func themeAutoNightSettingsController(context: AccountContext) -> ViewCon |> mapToSignal { resolvedWallpaper -> Signal in var updatedTheme = theme if case let .cloud(info) = theme { - updatedTheme = .cloud(PresentationCloudTheme(theme: info.theme, resolvedWallpaper: resolvedWallpaper)) + updatedTheme = .cloud(PresentationCloudTheme(theme: info.theme, resolvedWallpaper: resolvedWallpaper, creatorAccountId: info.theme.isCreator ? context.account.id : nil)) } updateSettings { settings in @@ -572,7 +572,7 @@ public func themeAutoNightSettingsController(context: AccountContext) -> ViewCon let settings = (sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings] as? PresentationThemeSettings) ?? PresentationThemeSettings.defaultSettings let defaultThemes: [PresentationThemeReference] = [.builtin(.night), .builtin(.nightAccent)] - let cloudThemes: [PresentationThemeReference] = cloudThemes.map { .cloud(PresentationCloudTheme(theme: $0, resolvedWallpaper: nil)) } + let cloudThemes: [PresentationThemeReference] = cloudThemes.map { .cloud(PresentationCloudTheme(theme: $0, resolvedWallpaper: nil, creatorAccountId: $0.isCreator ? context.account.id : nil)) } var availableThemes = defaultThemes availableThemes.append(contentsOf: cloudThemes) diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift index 6fd9305c14..6ea9fd97cd 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift @@ -234,9 +234,9 @@ public final class ThemePreviewController: ViewController { |> mapToSignal { theme, wallpaper -> Signal in if let theme = theme { if case let .file(file) = wallpaper, file.id != 0 { - return .single(.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: wallpaper))) + return .single(.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: wallpaper, creatorAccountId: theme.isCreator ? context.account.id : nil))) } else { - return .single(.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: nil))) + return .single(.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: nil, creatorAccountId: theme.isCreator ? context.account.id : nil))) } } else { return .complete() @@ -313,7 +313,7 @@ public final class ThemePreviewController: ViewController { } if case let .result(theme) = result, let file = theme.file { context.sharedContext.accountManager.mediaBox.moveResourceData(from: info.resource.id, to: file.resource.id) - return .single((.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: resolvedWallpaper)), true)) + return .single((.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: resolvedWallpaper, creatorAccountId: theme.isCreator ? context.account.id : nil)), true)) } else { return .complete() } @@ -332,7 +332,7 @@ public final class ThemePreviewController: ViewController { } if case let .result(updatedTheme) = result, let file = updatedTheme.file { context.sharedContext.accountManager.mediaBox.moveResourceData(from: info.resource.id, to: file.resource.id) - return .single((.cloud(PresentationCloudTheme(theme: updatedTheme, resolvedWallpaper: resolvedWallpaper)), true)) + return .single((.cloud(PresentationCloudTheme(theme: updatedTheme, resolvedWallpaper: resolvedWallpaper, creatorAccountId: updatedTheme.isCreator ? context.account.id : nil)), true)) } else { return .complete() } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift index c47addabc2..d45c21762c 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift @@ -729,7 +729,8 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The let previousThemeIndex = themes.prefix(upTo: currentThemeIndex).reversed().firstIndex(where: { $0.file != nil }) let newTheme: PresentationThemeReference if let previousThemeIndex = previousThemeIndex { - newTheme = .cloud(PresentationCloudTheme(theme: themes[themes.index(before: previousThemeIndex.base)], resolvedWallpaper: nil)) + let theme = themes[themes.index(before: previousThemeIndex.base)] + newTheme = .cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: nil, creatorAccountId: theme.isCreator ? context.account.id : nil)) } else { newTheme = .builtin(.nightAccent) } @@ -953,7 +954,8 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The let previousThemeIndex = themes.prefix(upTo: currentThemeIndex).reversed().firstIndex(where: { $0.file != nil }) let newTheme: PresentationThemeReference if let previousThemeIndex = previousThemeIndex { - selectThemeImpl?(.cloud(PresentationCloudTheme(theme: themes[themes.index(before: previousThemeIndex.base)], resolvedWallpaper: nil))) + let theme = themes[themes.index(before: previousThemeIndex.base)] + selectThemeImpl?(.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: nil, creatorAccountId: theme.isCreator ? context.account.id : nil))) } else { if settings.baseTheme == .night { selectAccentColorImpl?(PresentationThemeAccentColor(baseColor: .blue)) @@ -1014,7 +1016,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The } defaultThemes.append(contentsOf: [.builtin(.night), .builtin(.nightAccent)]) - let cloudThemes: [PresentationThemeReference] = cloudThemes.map { .cloud(PresentationCloudTheme(theme: $0, resolvedWallpaper: nil)) }.filter { !removedThemeIndexes.contains($0.index) } + let cloudThemes: [PresentationThemeReference] = cloudThemes.map { .cloud(PresentationCloudTheme(theme: $0, resolvedWallpaper: nil, creatorAccountId: $0.isCreator ? context.account.id : nil)) }.filter { !removedThemeIndexes.contains($0.index) } var availableThemes = defaultThemes if defaultThemes.first(where: { $0.index == themeReference.index }) == nil && cloudThemes.first(where: { $0.index == themeReference.index }) == nil { @@ -1158,7 +1160,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The var baseThemeIndex: Int64? var updatedThemeBaseIndex: Int64? if case let .cloud(info) = theme { - updatedTheme = .cloud(PresentationCloudTheme(theme: info.theme, resolvedWallpaper: resolvedWallpaper)) + updatedTheme = .cloud(PresentationCloudTheme(theme: info.theme, resolvedWallpaper: resolvedWallpaper, creatorAccountId: info.theme.isCreator ? context.account.id : nil)) if let settings = info.theme.settings { baseThemeIndex = PresentationThemeReference.builtin(PresentationBuiltinThemeReference(baseTheme: settings.baseTheme)).index updatedThemeBaseIndex = baseThemeIndex diff --git a/submodules/StickerPackPreviewUI/BUCK b/submodules/StickerPackPreviewUI/BUCK index d9156bceac..4b0f121d0a 100644 --- a/submodules/StickerPackPreviewUI/BUCK +++ b/submodules/StickerPackPreviewUI/BUCK @@ -24,6 +24,7 @@ static_library( "//submodules/ActivityIndicator:ActivityIndicator", "//submodules/AnimatedStickerNode:AnimatedStickerNode", "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", + "//submodules/ArchivedStickerPacksNotice:ArchivedStickerPacksNotice", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/TelegramCore/Sources/RecentPeers.swift b/submodules/TelegramCore/Sources/RecentPeers.swift index eb44d97c28..fd4d853cb6 100644 --- a/submodules/TelegramCore/Sources/RecentPeers.swift +++ b/submodules/TelegramCore/Sources/RecentPeers.swift @@ -231,3 +231,20 @@ public func recentlyUsedInlineBots(postbox: Postbox) -> Signal<[(Peer, Double)], } } +public func removeRecentlyUsedInlineBot(account: Account, peerId: PeerId) -> Signal { + return account.postbox.transaction { transaction -> Signal in + transaction.removeOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudRecentInlineBots, itemId: RecentPeerItemId(peerId).rawValue) + + if let peer = transaction.getPeer(peerId), let apiPeer = apiInputPeer(peer) { + return account.network.request(Api.functions.contacts.resetTopPeerRating(category: .topPeerCategoryBotsInline, peer: apiPeer)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> mapToSignal { _ -> Signal in + return .complete() + } + } else { + return .complete() + } + } |> switchToLatest +} diff --git a/submodules/TelegramUI/TelegramUI/AppDelegate.swift b/submodules/TelegramUI/TelegramUI/AppDelegate.swift index d40a654516..5ef9a5f5f1 100644 --- a/submodules/TelegramUI/TelegramUI/AppDelegate.swift +++ b/submodules/TelegramUI/TelegramUI/AppDelegate.swift @@ -816,12 +816,12 @@ final class SharedApplicationContext { return } var exists = false - strongSelf.mainWindow.forEachViewController { controller in + strongSelf.mainWindow.forEachViewController({ controller in if controller is ThemeSettingsCrossfadeController || controller is ThemeSettingsController { exists = true } return true - } + }) if !exists { strongSelf.mainWindow.present(ThemeSettingsCrossfadeController(), on: .root) @@ -1425,12 +1425,12 @@ final class SharedApplicationContext { } } } - self.mainWindow.forEachViewController { controller in + self.mainWindow.forEachViewController({ controller in if let controller = controller as? UndoOverlayController { controller.dismissWithCommitAction() } return true - } + }) } func applicationDidEnterBackground(_ application: UIApplication) { diff --git a/submodules/TelegramUI/TelegramUI/ApplicationContext.swift b/submodules/TelegramUI/TelegramUI/ApplicationContext.swift index f8aaeef786..5754ef44da 100644 --- a/submodules/TelegramUI/TelegramUI/ApplicationContext.swift +++ b/submodules/TelegramUI/TelegramUI/ApplicationContext.swift @@ -333,7 +333,7 @@ final class AuthorizedApplicationContext { return false } return true - }) + }, excludeNavigationSubControllers: true) if foundOverlay { return true @@ -362,7 +362,7 @@ final class AuthorizedApplicationContext { return false }, expandAction: { expandData in if let strongSelf = self { - let chatController = ChatControllerImpl(context: strongSelf.context, chatLocation: .peer(firstMessage.id.peerId), mode: .overlay) + let chatController = ChatControllerImpl(context: strongSelf.context, chatLocation: .peer(firstMessage.id.peerId), mode: .overlay(strongSelf.rootController)) //chatController.navigation_setNavigationController(strongSelf.rootController) chatController.presentationArguments = ChatControllerOverlayPresentationData(expandData: expandData()) //strongSelf.rootController.pushViewController(chatController) diff --git a/submodules/TelegramUI/TelegramUI/ChatController.swift b/submodules/TelegramUI/TelegramUI/ChatController.swift index c677325613..ed59ac9d2a 100644 --- a/submodules/TelegramUI/TelegramUI/ChatController.swift +++ b/submodules/TelegramUI/TelegramUI/ChatController.swift @@ -8274,6 +8274,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return navigationController } else if case let .inline(navigationController) = self.presentationInterfaceState.mode { return navigationController + } else if case let .overlay(navigationController) = self.presentationInterfaceState.mode { + return navigationController } else { return nil } diff --git a/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift b/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift index 959684b84a..df12564141 100644 --- a/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift @@ -496,7 +496,14 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.navigationBar?.isHidden = true } if self.overlayNavigationBar == nil { - let overlayNavigationBar = ChatOverlayNavigationBar(theme: self.chatPresentationInterfaceState.theme, strings: self.chatPresentationInterfaceState.strings, nameDisplayOrder: self.chatPresentationInterfaceState.nameDisplayOrder, close: { [weak self] in + let overlayNavigationBar = ChatOverlayNavigationBar(theme: self.chatPresentationInterfaceState.theme, strings: self.chatPresentationInterfaceState.strings, nameDisplayOrder: self.chatPresentationInterfaceState.nameDisplayOrder, tapped: { [weak self] in + if let strongSelf = self { + strongSelf.dismissAsOverlay() + if case let .peer(id) = strongSelf.chatPresentationInterfaceState.chatLocation { + strongSelf.interfaceInteraction?.navigateToChat(id) + } + } + }, close: { [weak self] in self?.dismissAsOverlay() }) overlayNavigationBar.peerView = self.peerView diff --git a/submodules/TelegramUI/TelegramUI/ChatOverlayNavigationBar.swift b/submodules/TelegramUI/TelegramUI/ChatOverlayNavigationBar.swift index a19a48aa66..720135e850 100644 --- a/submodules/TelegramUI/TelegramUI/ChatOverlayNavigationBar.swift +++ b/submodules/TelegramUI/TelegramUI/ChatOverlayNavigationBar.swift @@ -14,6 +14,7 @@ final class ChatOverlayNavigationBar: ASDisplayNode { private let theme: PresentationTheme private let strings: PresentationStrings private let nameDisplayOrder: PresentationPersonNameOrder + private let tapped: () -> Void private let close: () -> Void private let separatorNode: ASDisplayNode @@ -40,10 +41,11 @@ final class ChatOverlayNavigationBar: ASDisplayNode { } } - init(theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, close: @escaping () -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, tapped: @escaping () -> Void, close: @escaping () -> Void) { self.theme = theme self.strings = strings self.nameDisplayOrder = nameDisplayOrder + self.tapped = tapped self.close = close self.separatorNode = ASDisplayNode() @@ -83,6 +85,13 @@ final class ChatOverlayNavigationBar: ASDisplayNode { self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside]) } + override func didLoad() { + super.didLoad() + + let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap)) + self.view.addGestureRecognizer(gestureRecognizer) + } + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) @@ -93,11 +102,15 @@ final class ChatOverlayNavigationBar: ASDisplayNode { let _ = titleApply() transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: sideInset, y: floor((size.height - titleLayout.size.height) / 2.0)), size: titleLayout.size)) - let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) - transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: size.width - sideInset - closeButtonSize.width - 6.0, y: floor((size.height - closeButtonSize.height) / 2.0)), size: closeButtonSize)) + let closeButtonSize = CGSize(width: size.height, height: size.height) + transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: size.width - sideInset - closeButtonSize.width + 10.0, y: 0.0), size: closeButtonSize)) } - @objc func closePressed() { + @objc private func handleTap() { + self.tapped() + } + + @objc private func closePressed() { self.close() } } diff --git a/submodules/TelegramUI/TelegramUI/HashtagChatInputContextPanelNode.swift b/submodules/TelegramUI/TelegramUI/HashtagChatInputContextPanelNode.swift index 4105db2245..ba6669116c 100644 --- a/submodules/TelegramUI/TelegramUI/HashtagChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/HashtagChatInputContextPanelNode.swift @@ -10,6 +10,7 @@ import TelegramUIPreferences import MergeLists import AccountContext import AccountContext +import ItemListUI private struct HashtagChatInputContextPanelEntryStableId: Hashable { let text: String @@ -19,25 +20,26 @@ private struct HashtagChatInputContextPanelEntry: Comparable, Identifiable { let index: Int let theme: PresentationTheme let text: String + let revealed: Bool var stableId: HashtagChatInputContextPanelEntryStableId { return HashtagChatInputContextPanelEntryStableId(text: self.text) } func withUpdatedTheme(_ theme: PresentationTheme) -> HashtagChatInputContextPanelEntry { - return HashtagChatInputContextPanelEntry(index: self.index, theme: theme, text: self.text) + return HashtagChatInputContextPanelEntry(index: self.index, theme: theme, text: self.text, revealed: self.revealed) } static func ==(lhs: HashtagChatInputContextPanelEntry, rhs: HashtagChatInputContextPanelEntry) -> Bool { - return lhs.index == rhs.index && lhs.text == rhs.text && lhs.theme === rhs.theme + return lhs.index == rhs.index && lhs.text == rhs.text && lhs.theme === rhs.theme && lhs.revealed == rhs.revealed } static func <(lhs: HashtagChatInputContextPanelEntry, rhs: HashtagChatInputContextPanelEntry) -> Bool { return lhs.index < rhs.index } - func item(account: Account, fontSize: PresentationFontSize, hashtagSelected: @escaping (String) -> Void) -> ListViewItem { - return HashtagChatInputPanelItem(theme: self.theme, fontSize: fontSize, text: self.text, hashtagSelected: hashtagSelected) + func item(account: Account, presentationData: PresentationData, setHashtagRevealed: @escaping (String?) -> Void, hashtagSelected: @escaping (String) -> Void, removeRequested: @escaping (String) -> Void) -> ListViewItem { + return HashtagChatInputPanelItem(presentationData: ItemListPresentationData(presentationData), text: self.text, revealed: self.revealed, setHashtagRevealed: setHashtagRevealed, hashtagSelected: hashtagSelected, removeRequested: removeRequested) } } @@ -47,12 +49,12 @@ private struct HashtagChatInputContextPanelTransition { let updates: [ListViewUpdateItem] } -private func preparedTransition(from fromEntries: [HashtagChatInputContextPanelEntry], to toEntries: [HashtagChatInputContextPanelEntry], account: Account, fontSize: PresentationFontSize, hashtagSelected: @escaping (String) -> Void) -> HashtagChatInputContextPanelTransition { +private func preparedTransition(from fromEntries: [HashtagChatInputContextPanelEntry], to toEntries: [HashtagChatInputContextPanelEntry], account: Account, presentationData: PresentationData, setHashtagRevealed: @escaping (String?) -> Void, hashtagSelected: @escaping (String) -> Void, removeRequested: @escaping (String) -> Void) -> HashtagChatInputContextPanelTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, fontSize: fontSize, hashtagSelected: hashtagSelected), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, fontSize: fontSize, hashtagSelected: hashtagSelected), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, presentationData: presentationData, setHashtagRevealed: setHashtagRevealed, hashtagSelected: hashtagSelected, removeRequested: removeRequested), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, presentationData: presentationData, setHashtagRevealed: setHashtagRevealed, hashtagSelected: hashtagSelected, removeRequested: removeRequested), directionHint: nil) } return HashtagChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates) } @@ -61,6 +63,9 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { private let listView: ListView private var currentEntries: [HashtagChatInputContextPanelEntry]? + private var currentResults: [String] = [] + private var revealedHashtag: String? + private var enqueuedTransitions: [(HashtagChatInputContextPanelTransition, Bool)] = [] private var validLayout: (CGSize, CGFloat, CGFloat, CGFloat)? @@ -81,11 +86,13 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { } func updateResults(_ results: [String]) { + self.currentResults = results + var entries: [HashtagChatInputContextPanelEntry] = [] var index = 0 var stableIds = Set() for text in results { - let entry = HashtagChatInputContextPanelEntry(index: index, theme: self.theme, text: text) + let entry = HashtagChatInputContextPanelEntry(index: index, theme: self.theme, text: text, revealed: text == self.revealedHashtag) if stableIds.contains(entry.stableId) { continue } @@ -98,7 +105,12 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { private func prepareTransition(from: [HashtagChatInputContextPanelEntry]? , to: [HashtagChatInputContextPanelEntry]) { let firstTime = from == nil - let transition = preparedTransition(from: from ?? [], to: to, account: self.context.account, fontSize: self.fontSize, hashtagSelected: { [weak self] text in + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let transition = preparedTransition(from: from ?? [], to: to, account: self.context.account, presentationData: presentationData, setHashtagRevealed: { [weak self] text in + if let strongSelf = self { + strongSelf.revealedHashtag = text + } + }, hashtagSelected: { [weak self] text in if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { interfaceInteraction.updateTextInputStateAndMode { textInputState, inputMode in var hashtagQueryRange: NSRange? @@ -123,6 +135,10 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { return (textInputState, inputMode) } } + }, removeRequested: { [weak self] text in + if let strongSelf = self { + let _ = removeRecentlyUsedHashtag(postbox: strongSelf.context.account.postbox, string: text).start() + } }) self.currentEntries = to self.enqueueTransition(transition, firstTime: firstTime) diff --git a/submodules/TelegramUI/TelegramUI/HashtagChatInputPanelItem.swift b/submodules/TelegramUI/TelegramUI/HashtagChatInputPanelItem.swift index 57247e8a23..1f8a32d4a7 100644 --- a/submodules/TelegramUI/TelegramUI/HashtagChatInputPanelItem.swift +++ b/submodules/TelegramUI/TelegramUI/HashtagChatInputPanelItem.swift @@ -8,20 +8,25 @@ import SwiftSignalKit import Postbox import TelegramPresentationData import TelegramUIPreferences +import ItemListUI final class HashtagChatInputPanelItem: ListViewItem { - fileprivate let theme: PresentationTheme - fileprivate let fontSize: PresentationFontSize + fileprivate let presentationData: ItemListPresentationData fileprivate let text: String + fileprivate let revealed: Bool + fileprivate let setHashtagRevealed: (String?) -> Void private let hashtagSelected: (String) -> Void + fileprivate let removeRequested: (String) -> Void let selectable: Bool = true - public init(theme: PresentationTheme, fontSize: PresentationFontSize, text: String, hashtagSelected: @escaping (String) -> Void) { - self.theme = theme - self.fontSize = fontSize + public init(presentationData: ItemListPresentationData, text: String, revealed: Bool, setHashtagRevealed: @escaping (String?) -> Void, hashtagSelected: @escaping (String) -> Void, removeRequested: @escaping (String) -> Void) { + self.presentationData = presentationData self.text = text + self.revealed = revealed + self.setHashtagRevealed = setHashtagRevealed self.hashtagSelected = hashtagSelected + self.removeRequested = removeRequested } public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -72,7 +77,11 @@ final class HashtagChatInputPanelItem: ListViewItem { } func selected(listView: ListView) { - self.hashtagSelected(self.text) + if self.revealed { + self.setHashtagRevealed(nil) + } else { + self.hashtagSelected(self.text) + } } } @@ -82,6 +91,17 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode { private let topSeparatorNode: ASDisplayNode private let separatorNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode + + private var revealNode: ItemListRevealOptionsNode? + private var revealOptions: [ItemListRevealOption] = [] + private var initialRevealOffset: CGFloat = 0.0 + public private(set) var revealOffset: CGFloat = 0.0 + private var recognizer: ItemListRevealOptionsGestureRecognizer? + private var hapticFeedback: HapticFeedback? + + private var item: HashtagChatInputPanelItem? + + private var validLayout: (CGSize, CGFloat, CGFloat)? init() { self.textNode = TextNode() @@ -102,6 +122,15 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode { self.addSubnode(self.textNode) } + override func didLoad() { + super.didLoad() + + let recognizer = ItemListRevealOptionsGestureRecognizer(target: self, action: #selector(self.revealGesture(_:))) + self.recognizer = recognizer + recognizer.allowAnyDirection = false + self.view.addGestureRecognizer(recognizer) + } + override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = item as? HashtagChatInputPanelItem { let doLayout = self.asyncLayout() @@ -116,26 +145,31 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode { func asyncLayout() -> (_ item: HashtagChatInputPanelItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let makeTextLayout = TextNode.asyncLayout(self.textNode) return { [weak self] item, params, mergedTop, mergedBottom in - let textFont = Font.medium(floor(item.fontSize.baseDisplaySize * 14.0 / 17.0)) + let textFont = Font.medium(floor(item.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0)) let baseWidth = params.width - params.leftInset - params.rightInset let leftInset: CGFloat = 15.0 + params.leftInset let rightInset: CGFloat = 10.0 + params.rightInset - let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "#\(item.text)", font: textFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: baseWidth, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "#\(item.text)", font: textFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: baseWidth, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: HashtagChatInputPanelItemNode.itemHeight), insets: UIEdgeInsets()) - return (nodeLayout, { _ in + return (nodeLayout, { animation in if let strongSelf = self { - strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor - strongSelf.topSeparatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor - strongSelf.backgroundColor = item.theme.list.plainBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + strongSelf.item = item + strongSelf.validLayout = (nodeLayout.contentSize, params.leftInset, params.rightInset) + + let revealOffset = strongSelf.revealOffset + + strongSelf.separatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor + strongSelf.topSeparatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor + strongSelf.backgroundColor = item.presentationData.theme.list.plainBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor let _ = textApply() - strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((nodeLayout.contentSize.height - textLayout.size.height) / 2.0)), size: textLayout.size) + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: revealOffset + leftInset, y: floor((nodeLayout.contentSize.height - textLayout.size.height) / 2.0)), size: textLayout.size) strongSelf.topSeparatorNode.isHidden = mergedTop strongSelf.separatorNode.isHidden = !mergedBottom @@ -144,14 +178,27 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode { strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: params.width - leftInset, height: UIScreenPixel)) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: nodeLayout.size.height + UIScreenPixel)) + + strongSelf.setRevealOptions([ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)]) + strongSelf.setRevealOptionsOpened(item.revealed, animated: animation.isAnimated) } }) } } + func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { + if let (size, leftInset, rightInset) = self.validLayout { + transition.updateFrameAdditive(node: self.textNode, frame: CGRect(origin: CGPoint(x: min(offset, 0.0) + 15.0 + leftInset, y: self.textNode.frame.minY), size: self.textNode.frame.size)) + } + } + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { super.setHighlighted(highlighted, at: point, animated: animated) + if let revealNode = self.revealNode, self.revealOffset != 0 { + return + } + if highlighted { self.highlightedBackgroundNode.alpha = 1.0 if self.highlightedBackgroundNode.supernode == nil { @@ -174,4 +221,199 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode { } } } + + func setRevealOptions(_ options: [ItemListRevealOption]) { + if self.revealOptions == options { + return + } + let previousOptions = self.revealOptions + let wasEmpty = self.revealOptions.isEmpty + self.revealOptions = options + let isEmpty = options.isEmpty + if options.isEmpty { + if let _ = self.revealNode { + self.recognizer?.becomeCancelled() + self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring)) + } + } + if wasEmpty != isEmpty { + self.recognizer?.isEnabled = !isEmpty + } + } + + private func setRevealOptionsOpened(_ value: Bool, animated: Bool) { + if value != !self.revealOffset.isZero { + if !self.revealOffset.isZero { + self.recognizer?.becomeCancelled() + } + let transition: ContainedViewLayoutTransition + if animated { + transition = .animated(duration: 0.3, curve: .spring) + } else { + transition = .immediate + } + if value { + if self.revealNode == nil { + self.setupAndAddRevealNode() + if let revealNode = self.revealNode, revealNode.isNodeLoaded, let _ = self.validLayout { + revealNode.layout() + let revealSize = revealNode.bounds.size + self.updateRevealOffsetInternal(offset: -revealSize.width, transition: transition) + } + } + } else if !self.revealOffset.isZero { + self.updateRevealOffsetInternal(offset: 0.0, transition: transition) + } + } + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if let recognizer = self.recognizer, otherGestureRecognizer == recognizer { + return true + } else { + return false + } + } + + @objc func revealGesture(_ recognizer: ItemListRevealOptionsGestureRecognizer) { + guard let (size, _, _) = self.validLayout else { + return + } + switch recognizer.state { + case .began: + if let revealNode = self.revealNode { + let revealSize = revealNode.bounds.size + let location = recognizer.location(in: self.view) + if location.x > size.width - revealSize.width { + recognizer.becomeCancelled() + } else { + self.initialRevealOffset = self.revealOffset + } + } else { + if self.revealOptions.isEmpty { + recognizer.becomeCancelled() + } + self.initialRevealOffset = self.revealOffset + } + case .changed: + var translation = recognizer.translation(in: self.view) + translation.x += self.initialRevealOffset + if self.revealNode == nil && translation.x.isLess(than: 0.0) { + self.setupAndAddRevealNode() + self.revealOptionsInteractivelyOpened() + } + self.updateRevealOffsetInternal(offset: translation.x, transition: .immediate) + if self.revealNode == nil { + self.revealOptionsInteractivelyClosed() + } + case .ended, .cancelled: + guard let recognizer = self.recognizer else { + break + } + + if let revealNode = self.revealNode { + let velocity = recognizer.velocity(in: self.view) + let revealSize = revealNode.bounds.size + var reveal = false + if abs(velocity.x) < 100.0 { + if self.initialRevealOffset.isZero && self.revealOffset < 0.0 { + reveal = true + } else if self.revealOffset < -revealSize.width { + reveal = true + } else { + reveal = false + } + } else { + if velocity.x < 0.0 { + reveal = true + } else { + reveal = false + } + } + self.updateRevealOffsetInternal(offset: reveal ? -revealSize.width : 0.0, transition: .animated(duration: 0.3, curve: .spring)) + if !reveal { + self.revealOptionsInteractivelyClosed() + } + } + default: + break + } + } + + private func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) { + guard let item = self.item else { + return + } + item.removeRequested(item.text) + } + + private func setupAndAddRevealNode() { + if !self.revealOptions.isEmpty { + let revealNode = ItemListRevealOptionsNode(optionSelected: { [weak self] option in + self?.revealOptionSelected(option, animated: false) + }, tapticAction: { [weak self] in + self?.hapticImpact() + }) + revealNode.setOptions(self.revealOptions, isLeft: false) + self.revealNode = revealNode + + if let (size, _, rightInset) = self.validLayout { + var revealSize = revealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: size.height)) + revealSize.width += rightInset + + revealNode.frame = CGRect(origin: CGPoint(x: size.width + max(self.revealOffset, -revealSize.width), y: 0.0), size: revealSize) + revealNode.updateRevealOffset(offset: 0.0, sideInset: -rightInset, transition: .immediate) + } + + self.addSubnode(revealNode) + } + } + + private func updateRevealOffsetInternal(offset: CGFloat, transition: ContainedViewLayoutTransition) { + self.revealOffset = offset + guard let (size, leftInset, rightInset) = self.validLayout else { + return + } + + if let revealNode = self.revealNode { + let revealSize = revealNode.bounds.size + + let revealFrame = CGRect(origin: CGPoint(x: size.width + max(self.revealOffset, -revealSize.width), y: 0.0), size: revealSize) + let revealNodeOffset = -max(self.revealOffset, -revealSize.width) + revealNode.updateRevealOffset(offset: revealNodeOffset, sideInset: -rightInset, transition: transition) + + if CGFloat(0.0).isLessThanOrEqualTo(offset) { + self.revealNode = nil + transition.updateFrame(node: revealNode, frame: revealFrame, completion: { [weak revealNode] _ in + revealNode?.removeFromSupernode() + }) + } else { + transition.updateFrame(node: revealNode, frame: revealFrame) + } + } + self.updateRevealOffset(offset: offset, transition: transition) + } + + func revealOptionsInteractivelyOpened() { + if let item = self.item { + item.setHashtagRevealed(item.text) + } + } + + func revealOptionsInteractivelyClosed() { + if let item = self.item { + item.setHashtagRevealed(nil) + } + } + + private func hapticImpact() { + if self.hapticFeedback == nil { + self.hapticFeedback = HapticFeedback() + } + self.hapticFeedback?.impact(.medium) + } } diff --git a/submodules/TelegramUI/TelegramUI/MentionChatInputContextPanelNode.swift b/submodules/TelegramUI/TelegramUI/MentionChatInputContextPanelNode.swift index 6dc770a61c..4f2c0ab346 100644 --- a/submodules/TelegramUI/TelegramUI/MentionChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/MentionChatInputContextPanelNode.swift @@ -11,25 +11,27 @@ import MergeLists import TextFormat import AccountContext import LocalizedPeerData +import ItemListUI private struct MentionChatInputContextPanelEntry: Comparable, Identifiable { let index: Int let peer: Peer + let revealed: Bool var stableId: Int64 { return self.peer.id.toInt64() } static func ==(lhs: MentionChatInputContextPanelEntry, rhs: MentionChatInputContextPanelEntry) -> Bool { - return lhs.index == rhs.index && lhs.peer.isEqual(rhs.peer) + return lhs.index == rhs.index && lhs.peer.isEqual(rhs.peer) && lhs.revealed == rhs.revealed } static func <(lhs: MentionChatInputContextPanelEntry, rhs: MentionChatInputContextPanelEntry) -> Bool { return lhs.index < rhs.index } - func item(context: AccountContext, theme: PresentationTheme, fontSize: PresentationFontSize, inverted: Bool, peerSelected: @escaping (Peer) -> Void) -> ListViewItem { - return MentionChatInputPanelItem(context: context, theme: theme, fontSize: fontSize, inverted: inverted, peer: self.peer, peerSelected: peerSelected) + func item(context: AccountContext, presentationData: PresentationData, inverted: Bool, setPeerIdRevealed: @escaping (PeerId?) -> Void, peerSelected: @escaping (Peer) -> Void, removeRequested: @escaping (PeerId) -> Void) -> ListViewItem { + return MentionChatInputPanelItem(context: context, presentationData: ItemListPresentationData(presentationData), inverted: inverted, peer: self.peer, revealed: self.revealed, setPeerIdRevealed: setPeerIdRevealed, peerSelected: peerSelected, removeRequested: removeRequested) } } @@ -39,12 +41,12 @@ private struct CommandChatInputContextPanelTransition { let updates: [ListViewUpdateItem] } -private func preparedTransition(from fromEntries: [MentionChatInputContextPanelEntry], to toEntries: [MentionChatInputContextPanelEntry], context: AccountContext, theme: PresentationTheme, fontSize: PresentationFontSize, inverted: Bool, forceUpdate: Bool, peerSelected: @escaping (Peer) -> Void) -> CommandChatInputContextPanelTransition { +private func preparedTransition(from fromEntries: [MentionChatInputContextPanelEntry], to toEntries: [MentionChatInputContextPanelEntry], context: AccountContext, presentationData: PresentationData, inverted: Bool, forceUpdate: Bool, setPeerIdRevealed: @escaping (PeerId?) -> Void, peerSelected: @escaping (Peer) -> Void, removeRequested: @escaping (PeerId) -> Void) -> CommandChatInputContextPanelTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, theme: theme, fontSize: fontSize, inverted: inverted, peerSelected: peerSelected), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, theme: theme, fontSize: fontSize, inverted: inverted, peerSelected: peerSelected), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, inverted: inverted, setPeerIdRevealed: setPeerIdRevealed, peerSelected: peerSelected, removeRequested: removeRequested), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, inverted: inverted, setPeerIdRevealed: setPeerIdRevealed, peerSelected: peerSelected, removeRequested: removeRequested), directionHint: nil) } return CommandChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates) } @@ -60,6 +62,8 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { private let listView: ListView private var currentEntries: [MentionChatInputContextPanelEntry]? + private var revealedPeerId: PeerId? + private var enqueuedTransitions: [(CommandChatInputContextPanelTransition, Bool)] = [] private var validLayout: (CGSize, CGFloat, CGFloat, CGFloat)? @@ -95,7 +99,7 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { continue } peerIdSet.insert(peerId) - entries.append(MentionChatInputContextPanelEntry(index: index, peer: peer)) + entries.append(MentionChatInputContextPanelEntry(index: index, peer: peer, revealed: revealedPeerId == peer.id)) index += 1 } self.updateToEntries(entries: entries, forceUpdate: false) @@ -103,7 +107,10 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { private func updateToEntries(entries: [MentionChatInputContextPanelEntry], forceUpdate: Bool) { let firstTime = self.currentEntries == nil - let transition = preparedTransition(from: self.currentEntries ?? [], to: entries, context: self.context, theme: self.theme, fontSize: self.fontSize, inverted: self.mode == .search, forceUpdate: forceUpdate, peerSelected: { [weak self] peer in + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let transition = preparedTransition(from: self.currentEntries ?? [], to: entries, context: self.context, presentationData: presentationData, inverted: self.mode == .search, forceUpdate: forceUpdate, setPeerIdRevealed: { [weak self] peerId in + + }, peerSelected: { [weak self] peer in if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { switch strongSelf.mode { case .input: @@ -147,6 +154,10 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { interfaceInteraction.beginMessageSearch(.member(peer), "") } } + }, removeRequested: { [weak self] peerId in + if let strongSelf = self { + let _ = removeRecentlyUsedInlineBot(account: strongSelf.context.account, peerId: peerId).start() + } }) self.currentEntries = entries self.enqueueTransition(transition, firstTime: firstTime) diff --git a/submodules/TelegramUI/TelegramUI/MentionChatInputPanelItem.swift b/submodules/TelegramUI/TelegramUI/MentionChatInputPanelItem.swift index a97d6708fe..d4d2c83949 100644 --- a/submodules/TelegramUI/TelegramUI/MentionChatInputPanelItem.swift +++ b/submodules/TelegramUI/TelegramUI/MentionChatInputPanelItem.swift @@ -10,24 +10,29 @@ import TelegramPresentationData import TelegramUIPreferences import AvatarNode import AccountContext +import ItemListUI final class MentionChatInputPanelItem: ListViewItem { fileprivate let context: AccountContext - fileprivate let theme: PresentationTheme - fileprivate let fontSize: PresentationFontSize + fileprivate let presentationData: ItemListPresentationData + fileprivate let revealed: Bool fileprivate let inverted: Bool fileprivate let peer: Peer private let peerSelected: (Peer) -> Void + fileprivate let setPeerIdRevealed: (PeerId?) -> Void + fileprivate let removeRequested: (PeerId) -> Void let selectable: Bool = true - public init(context: AccountContext, theme: PresentationTheme, fontSize: PresentationFontSize, inverted: Bool, peer: Peer, peerSelected: @escaping (Peer) -> Void) { + public init(context: AccountContext, presentationData: ItemListPresentationData, inverted: Bool, peer: Peer, revealed: Bool, setPeerIdRevealed: @escaping (PeerId?) -> Void, peerSelected: @escaping (Peer) -> Void, removeRequested: @escaping (PeerId) -> Void) { self.context = context - self.theme = theme - self.fontSize = fontSize + self.presentationData = presentationData self.inverted = inverted self.peer = peer + self.revealed = revealed + self.setPeerIdRevealed = setPeerIdRevealed self.peerSelected = peerSelected + self.removeRequested = removeRequested } public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -86,15 +91,24 @@ private let avatarFont = avatarPlaceholderFont(size: 16.0) final class MentionChatInputPanelItemNode: ListViewItemNode { static let itemHeight: CGFloat = 42.0 - - private var item: MentionChatInputPanelItem? - + private let avatarNode: AvatarNode private let textNode: TextNode private let topSeparatorNode: ASDisplayNode private let separatorNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode + private var revealNode: ItemListRevealOptionsNode? + private var revealOptions: [ItemListRevealOption] = [] + private var initialRevealOffset: CGFloat = 0.0 + public private(set) var revealOffset: CGFloat = 0.0 + private var recognizer: ItemListRevealOptionsGestureRecognizer? + private var hapticFeedback: HapticFeedback? + + private var item: MentionChatInputPanelItem? + + private var validLayout: (CGSize, CGFloat, CGFloat)? + init() { self.avatarNode = AvatarNode(font: avatarFont) self.textNode = TextNode() @@ -117,6 +131,15 @@ final class MentionChatInputPanelItemNode: ListViewItemNode { self.addSubnode(self.textNode) } + override func didLoad() { + super.didLoad() + + let recognizer = ItemListRevealOptionsGestureRecognizer(target: self, action: #selector(self.revealGesture(_:))) + self.recognizer = recognizer + recognizer.allowAnyDirection = false + self.view.addGestureRecognizer(recognizer) + } + override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = item as? MentionChatInputPanelItem { let doLayout = self.asyncLayout() @@ -134,8 +157,8 @@ final class MentionChatInputPanelItemNode: ListViewItemNode { let previousItem = self.item return { [weak self] item, params, mergedTop, mergedBottom in - let primaryFont = Font.medium(floor(item.fontSize.baseDisplaySize * 14.0 / 17.0)) - let secondaryFont = Font.regular(floor(item.fontSize.baseDisplaySize * 14.0 / 17.0)) + let primaryFont = Font.medium(floor(item.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0)) + let secondaryFont = Font.regular(floor(item.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0)) let leftInset: CGFloat = 55.0 + params.leftInset let rightInset: CGFloat = 10.0 + params.rightInset @@ -146,19 +169,23 @@ final class MentionChatInputPanelItemNode: ListViewItemNode { } let string = NSMutableAttributedString() - string.append(NSAttributedString(string: item.peer.debugDisplayTitle, font: primaryFont, textColor: item.theme.list.itemPrimaryTextColor)) + string.append(NSAttributedString(string: item.peer.debugDisplayTitle, font: primaryFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)) if let addressName = item.peer.addressName, !addressName.isEmpty { - string.append(NSAttributedString(string: " @\(addressName)", font: secondaryFont, textColor: item.theme.list.itemSecondaryTextColor)) + string.append(NSAttributedString(string: " @\(addressName)", font: secondaryFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)) } let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: HashtagChatInputPanelItemNode.itemHeight), insets: UIEdgeInsets()) - return (nodeLayout, { _ in + return (nodeLayout, { animation in if let strongSelf = self { strongSelf.item = item + strongSelf.validLayout = (nodeLayout.contentSize, params.leftInset, params.rightInset) + + let revealOffset = strongSelf.revealOffset + if let updatedInverted = updatedInverted { if updatedInverted { strongSelf.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0) @@ -167,12 +194,12 @@ final class MentionChatInputPanelItemNode: ListViewItemNode { } } - strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor - strongSelf.topSeparatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor - strongSelf.backgroundColor = item.theme.list.plainBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + strongSelf.separatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor + strongSelf.topSeparatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor + strongSelf.backgroundColor = item.presentationData.theme.list.plainBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor - strongSelf.avatarNode.setPeer(context: item.context, theme: item.theme, peer: item.peer, emptyColor: item.theme.list.mediaPlaceholderColor) + strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: item.peer, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor) let _ = textApply() @@ -186,14 +213,33 @@ final class MentionChatInputPanelItemNode: ListViewItemNode { strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: !item.inverted ? (nodeLayout.contentSize.height - UIScreenPixel) : 0.0), size: CGSize(width: params.width - leftInset, height: UIScreenPixel)) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: nodeLayout.size.height + UIScreenPixel)) + + if let peer = item.peer as? TelegramUser, let _ = peer.botInfo { + strongSelf.setRevealOptions([ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)]) + strongSelf.setRevealOptionsOpened(item.revealed, animated: animation.isAnimated) + } else { + strongSelf.setRevealOptions([]) + strongSelf.setRevealOptionsOpened(false, animated: animation.isAnimated) + } } }) } } + func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { + if let (size, leftInset, rightInset) = self.validLayout { + transition.updateFrameAdditive(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: min(offset, 0.0) + 12.0 + leftInset, y: self.avatarNode.frame.minY), size: self.avatarNode.frame.size)) + transition.updateFrameAdditive(node: self.textNode, frame: CGRect(origin: CGPoint(x: min(offset, 0.0) + 55.0 + leftInset, y: self.textNode.frame.minY), size: self.textNode.frame.size)) + } + } + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { super.setHighlighted(highlighted, at: point, animated: animated) + if let revealNode = self.revealNode, self.revealOffset != 0 { + return + } + if highlighted { self.highlightedBackgroundNode.alpha = 1.0 if self.highlightedBackgroundNode.supernode == nil { @@ -216,4 +262,199 @@ final class MentionChatInputPanelItemNode: ListViewItemNode { } } } + + func setRevealOptions(_ options: [ItemListRevealOption]) { + if self.revealOptions == options { + return + } + let previousOptions = self.revealOptions + let wasEmpty = self.revealOptions.isEmpty + self.revealOptions = options + let isEmpty = options.isEmpty + if options.isEmpty { + if let _ = self.revealNode { + self.recognizer?.becomeCancelled() + self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring)) + } + } + if wasEmpty != isEmpty { + self.recognizer?.isEnabled = !isEmpty + } + } + + private func setRevealOptionsOpened(_ value: Bool, animated: Bool) { + if value != !self.revealOffset.isZero { + if !self.revealOffset.isZero { + self.recognizer?.becomeCancelled() + } + let transition: ContainedViewLayoutTransition + if animated { + transition = .animated(duration: 0.3, curve: .spring) + } else { + transition = .immediate + } + if value { + if self.revealNode == nil { + self.setupAndAddRevealNode() + if let revealNode = self.revealNode, revealNode.isNodeLoaded, let _ = self.validLayout { + revealNode.layout() + let revealSize = revealNode.bounds.size + self.updateRevealOffsetInternal(offset: -revealSize.width, transition: transition) + } + } + } else if !self.revealOffset.isZero { + self.updateRevealOffsetInternal(offset: 0.0, transition: transition) + } + } + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if let recognizer = self.recognizer, otherGestureRecognizer == recognizer { + return true + } else { + return false + } + } + + @objc func revealGesture(_ recognizer: ItemListRevealOptionsGestureRecognizer) { + guard let (size, _, _) = self.validLayout else { + return + } + switch recognizer.state { + case .began: + if let revealNode = self.revealNode { + let revealSize = revealNode.bounds.size + let location = recognizer.location(in: self.view) + if location.x > size.width - revealSize.width { + recognizer.becomeCancelled() + } else { + self.initialRevealOffset = self.revealOffset + } + } else { + if self.revealOptions.isEmpty { + recognizer.becomeCancelled() + } + self.initialRevealOffset = self.revealOffset + } + case .changed: + var translation = recognizer.translation(in: self.view) + translation.x += self.initialRevealOffset + if self.revealNode == nil && translation.x.isLess(than: 0.0) { + self.setupAndAddRevealNode() + self.revealOptionsInteractivelyOpened() + } + self.updateRevealOffsetInternal(offset: translation.x, transition: .immediate) + if self.revealNode == nil { + self.revealOptionsInteractivelyClosed() + } + case .ended, .cancelled: + guard let recognizer = self.recognizer else { + break + } + + if let revealNode = self.revealNode { + let velocity = recognizer.velocity(in: self.view) + let revealSize = revealNode.bounds.size + var reveal = false + if abs(velocity.x) < 100.0 { + if self.initialRevealOffset.isZero && self.revealOffset < 0.0 { + reveal = true + } else if self.revealOffset < -revealSize.width { + reveal = true + } else { + reveal = false + } + } else { + if velocity.x < 0.0 { + reveal = true + } else { + reveal = false + } + } + self.updateRevealOffsetInternal(offset: reveal ? -revealSize.width : 0.0, transition: .animated(duration: 0.3, curve: .spring)) + if !reveal { + self.revealOptionsInteractivelyClosed() + } + } + default: + break + } + } + + private func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) { + guard let item = self.item else { + return + } + item.removeRequested(item.peer.id) + } + + private func setupAndAddRevealNode() { + if !self.revealOptions.isEmpty { + let revealNode = ItemListRevealOptionsNode(optionSelected: { [weak self] option in + self?.revealOptionSelected(option, animated: false) + }, tapticAction: { [weak self] in + self?.hapticImpact() + }) + revealNode.setOptions(self.revealOptions, isLeft: false) + self.revealNode = revealNode + + if let (size, _, rightInset) = self.validLayout { + var revealSize = revealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: size.height)) + revealSize.width += rightInset + + revealNode.frame = CGRect(origin: CGPoint(x: size.width + max(self.revealOffset, -revealSize.width), y: 0.0), size: revealSize) + revealNode.updateRevealOffset(offset: 0.0, sideInset: -rightInset, transition: .immediate) + } + + self.addSubnode(revealNode) + } + } + + private func updateRevealOffsetInternal(offset: CGFloat, transition: ContainedViewLayoutTransition) { + self.revealOffset = offset + guard let (size, leftInset, rightInset) = self.validLayout else { + return + } + + if let revealNode = self.revealNode { + let revealSize = revealNode.bounds.size + + let revealFrame = CGRect(origin: CGPoint(x: size.width + max(self.revealOffset, -revealSize.width), y: 0.0), size: revealSize) + let revealNodeOffset = -max(self.revealOffset, -revealSize.width) + revealNode.updateRevealOffset(offset: revealNodeOffset, sideInset: -rightInset, transition: transition) + + if CGFloat(0.0).isLessThanOrEqualTo(offset) { + self.revealNode = nil + transition.updateFrame(node: revealNode, frame: revealFrame, completion: { [weak revealNode] _ in + revealNode?.removeFromSupernode() + }) + } else { + transition.updateFrame(node: revealNode, frame: revealFrame) + } + } + self.updateRevealOffset(offset: offset, transition: transition) + } + + func revealOptionsInteractivelyOpened() { + if let item = self.item { + item.setPeerIdRevealed(item.peer.id) + } + } + + func revealOptionsInteractivelyClosed() { + if let item = self.item { + item.setPeerIdRevealed(nil) + } + } + + private func hapticImpact() { + if self.hapticFeedback == nil { + self.hapticFeedback = HapticFeedback() + } + self.hapticFeedback?.impact(.medium) + } } diff --git a/submodules/TelegramUI/TelegramUI/ThemeUpdateManager.swift b/submodules/TelegramUI/TelegramUI/ThemeUpdateManager.swift index b1a4e73adb..3051fa73ed 100644 --- a/submodules/TelegramUI/TelegramUI/ThemeUpdateManager.swift +++ b/submodules/TelegramUI/TelegramUI/ThemeUpdateManager.swift @@ -111,10 +111,10 @@ final class ThemeUpdateManagerImpl: ThemeUpdateManager { return .complete() } accountManager.mediaBox.storeResourceData(file.file.resource.id, data: fullSizeData, synchronous: true) - return .single((.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: wallpaper)), presentationTheme)) + return .single((.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: wallpaper, creatorAccountId: theme.isCreator ? account.id : nil)), presentationTheme)) } } else { - return .single((.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: nil)), presentationTheme)) + return .single((.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: nil, creatorAccountId: theme.isCreator ? account.id : nil)), presentationTheme)) } } } diff --git a/submodules/TelegramUIPreferences/Sources/PresentationThemeSettings.swift b/submodules/TelegramUIPreferences/Sources/PresentationThemeSettings.swift index 4a5b29ef8f..f78a460341 100644 --- a/submodules/TelegramUIPreferences/Sources/PresentationThemeSettings.swift +++ b/submodules/TelegramUIPreferences/Sources/PresentationThemeSettings.swift @@ -97,15 +97,18 @@ public struct PresentationLocalTheme: PostboxCoding, Equatable { public struct PresentationCloudTheme: PostboxCoding, Equatable { public let theme: TelegramTheme public let resolvedWallpaper: TelegramWallpaper? + public let creatorAccountId: AccountRecordId? - public init(theme: TelegramTheme, resolvedWallpaper: TelegramWallpaper?) { + public init(theme: TelegramTheme, resolvedWallpaper: TelegramWallpaper?, creatorAccountId: AccountRecordId?) { self.theme = theme self.resolvedWallpaper = resolvedWallpaper + self.creatorAccountId = creatorAccountId } public init(decoder: PostboxDecoder) { self.theme = decoder.decodeObjectForKey("theme", decoder: { TelegramTheme(decoder: $0) }) as! TelegramTheme self.resolvedWallpaper = decoder.decodeObjectForKey("wallpaper", decoder: { TelegramWallpaper(decoder: $0) }) as? TelegramWallpaper + self.creatorAccountId = decoder.decodeOptionalInt64ForKey("account").flatMap { AccountRecordId(rawValue: $0) } } public func encode(_ encoder: PostboxEncoder) { @@ -115,6 +118,11 @@ public struct PresentationCloudTheme: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "wallpaper") } + if let accountId = self.creatorAccountId { + encoder.encodeInt64(accountId.int64, forKey: "account") + } else { + encoder.encodeNil(forKey: "account") + } } public static func ==(lhs: PresentationCloudTheme, rhs: PresentationCloudTheme) -> Bool {