Merge commit '81f6181fbfa2da0d95862134b47a0a8795745b83'

This commit is contained in:
Ali 2020-01-08 19:27:33 +04:00
commit 444d29b23f
26 changed files with 1001 additions and 87 deletions

View File

@ -246,7 +246,7 @@ public enum ChatControllerSubject: Equatable {
public enum ChatControllerPresentationMode: Equatable { public enum ChatControllerPresentationMode: Equatable {
case standard(previewing: Bool) case standard(previewing: Bool)
case overlay case overlay(NavigationController?)
case inline(NavigationController?) case inline(NavigationController?)
} }

View File

@ -44,19 +44,19 @@ private func getCoveringViewSnaphot(window: Window1) -> UIImage? {
context.clear(CGRect(origin: CGPoint(), size: size)) context.clear(CGRect(origin: CGPoint(), size: size))
context.scaleBy(x: scale, y: scale) context.scaleBy(x: scale, y: scale)
UIGraphicsPushContext(context) UIGraphicsPushContext(context)
window.forEachViewController { controller in window.forEachViewController({ controller in
if let controller = controller as? PasscodeEntryController { if let controller = controller as? PasscodeEntryController {
controller.displayNode.alpha = 0.0 controller.displayNode.alpha = 0.0
} }
return true return true
} })
window.hostView.containerView.drawHierarchy(in: CGRect(origin: CGPoint(), size: unscaledSize), afterScreenUpdates: false) 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 { if let controller = controller as? PasscodeEntryController {
controller.displayNode.alpha = 1.0 controller.displayNode.alpha = 1.0
} }
return true return true
} })
UIGraphicsPopContext() UIGraphicsPopContext()
}).flatMap(applyScreenshotEffectToImage) }).flatMap(applyScreenshotEffectToImage)
} }

View File

@ -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",
],
)

View File

@ -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
}

View File

@ -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) 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 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 strongSelf.didShowProxyUnavailableTooltipController = true
let tooltipController = TooltipController(content: .text(strongSelf.presentationData.strings.Proxy_TooltipUnavailable), baseFontSize: strongSelf.presentationData.listsFontSize.baseDisplaySize, timeout: 60.0, dismissByTapOutside: true) let tooltipController = TooltipController(content: .text(strongSelf.presentationData.strings.Proxy_TooltipUnavailable), baseFontSize: strongSelf.presentationData.listsFontSize.baseDisplaySize, timeout: 60.0, dismissByTapOutside: true)
strongSelf.proxyUnavailableTooltipController = tooltipController strongSelf.proxyUnavailableTooltipController = tooltipController

View File

@ -1222,10 +1222,12 @@ public class Window1 {
return hidden return hidden
} }
public func forEachViewController(_ f: (ContainableController) -> Bool) { public func forEachViewController(_ f: (ContainableController) -> Bool, excludeNavigationSubControllers: Bool = false) {
if let navigationController = self._rootController as? NavigationController { if let navigationController = self._rootController as? NavigationController {
for case let controller as ContainableController in navigationController.viewControllers { if !excludeNavigationSubControllers {
!f(controller) for case let controller as ContainableController in navigationController.viewControllers {
!f(controller)
}
} }
if let controller = navigationController.topOverlayController { if let controller = navigationController.topOverlayController {
!f(controller) !f(controller)

View File

@ -48,6 +48,13 @@ private final class LegacyComponentsAccessCheckerImpl: NSObject, LegacyComponent
} }
public func checkPhotoAuthorizationStatus(for intent: TGPhotoAccessIntent, alertDismissCompletion: (() -> Void)!) -> Bool { 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 return true
} }

View File

@ -548,7 +548,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode {
if foundVenues == nil && !state.searchingVenuesAround { if foundVenues == nil && !state.searchingVenuesAround {
displayingPlacesButton = true displayingPlacesButton = true
} else if let previousLocation = foundVenuesLocation { } 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 { if currentLocation.distance(from: previousLocation) > 300 {
displayingPlacesButton = true displayingPlacesButton = true
} }

View File

@ -551,7 +551,7 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll
context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) 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 var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers
themeSpecificChatWallpapers[themeReference.index] = nil 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) 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 var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers
themeSpecificChatWallpapers[themeReference.index] = nil themeSpecificChatWallpapers[themeReference.index] = nil

View File

@ -260,7 +260,7 @@ final class ThemeAccentColorController: ViewController {
if case let .result(resultTheme) = next { if case let .result(resultTheme) = next {
let _ = applyTheme(accountManager: context.sharedContext.accountManager, account: context.account, theme: resultTheme).start() let _ = applyTheme(accountManager: context.sharedContext.accountManager, account: context.account, theme: resultTheme).start()
return updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in 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 updatedTheme = current.theme
var updatedAutomaticThemeSwitchSetting = current.automaticThemeSwitchSetting var updatedAutomaticThemeSwitchSetting = current.automaticThemeSwitchSetting
@ -289,7 +289,7 @@ final class ThemeAccentColorController: ViewController {
if case let .result(resultTheme) = next { if case let .result(resultTheme) = next {
let _ = applyTheme(accountManager: context.sharedContext.accountManager, account: context.account, theme: resultTheme).start() let _ = applyTheme(accountManager: context.sharedContext.accountManager, account: context.account, theme: resultTheme).start()
return updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in 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 updatedTheme = current.theme
var updatedAutomaticThemeSwitchSetting = current.automaticThemeSwitchSetting var updatedAutomaticThemeSwitchSetting = current.automaticThemeSwitchSetting

View File

@ -550,7 +550,7 @@ public func themeAutoNightSettingsController(context: AccountContext) -> ViewCon
|> mapToSignal { resolvedWallpaper -> Signal<Void, NoError> in |> mapToSignal { resolvedWallpaper -> Signal<Void, NoError> in
var updatedTheme = theme var updatedTheme = theme
if case let .cloud(info) = 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 updateSettings { settings in
@ -572,7 +572,7 @@ public func themeAutoNightSettingsController(context: AccountContext) -> ViewCon
let settings = (sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings] as? PresentationThemeSettings) ?? PresentationThemeSettings.defaultSettings let settings = (sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings] as? PresentationThemeSettings) ?? PresentationThemeSettings.defaultSettings
let defaultThemes: [PresentationThemeReference] = [.builtin(.night), .builtin(.nightAccent)] 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 var availableThemes = defaultThemes
availableThemes.append(contentsOf: cloudThemes) availableThemes.append(contentsOf: cloudThemes)

View File

@ -234,9 +234,9 @@ public final class ThemePreviewController: ViewController {
|> mapToSignal { theme, wallpaper -> Signal<PresentationThemeReference?, NoError> in |> mapToSignal { theme, wallpaper -> Signal<PresentationThemeReference?, NoError> in
if let theme = theme { if let theme = theme {
if case let .file(file) = wallpaper, file.id != 0 { 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 { } 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 { } else {
return .complete() return .complete()
@ -313,7 +313,7 @@ public final class ThemePreviewController: ViewController {
} }
if case let .result(theme) = result, let file = theme.file { if case let .result(theme) = result, let file = theme.file {
context.sharedContext.accountManager.mediaBox.moveResourceData(from: info.resource.id, to: file.resource.id) 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 { } else {
return .complete() return .complete()
} }
@ -332,7 +332,7 @@ public final class ThemePreviewController: ViewController {
} }
if case let .result(updatedTheme) = result, let file = updatedTheme.file { if case let .result(updatedTheme) = result, let file = updatedTheme.file {
context.sharedContext.accountManager.mediaBox.moveResourceData(from: info.resource.id, to: file.resource.id) 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 { } else {
return .complete() return .complete()
} }

View File

@ -729,7 +729,8 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
let previousThemeIndex = themes.prefix(upTo: currentThemeIndex).reversed().firstIndex(where: { $0.file != nil }) let previousThemeIndex = themes.prefix(upTo: currentThemeIndex).reversed().firstIndex(where: { $0.file != nil })
let newTheme: PresentationThemeReference let newTheme: PresentationThemeReference
if let previousThemeIndex = previousThemeIndex { 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 { } else {
newTheme = .builtin(.nightAccent) 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 previousThemeIndex = themes.prefix(upTo: currentThemeIndex).reversed().firstIndex(where: { $0.file != nil })
let newTheme: PresentationThemeReference let newTheme: PresentationThemeReference
if let previousThemeIndex = previousThemeIndex { 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 { } else {
if settings.baseTheme == .night { if settings.baseTheme == .night {
selectAccentColorImpl?(PresentationThemeAccentColor(baseColor: .blue)) selectAccentColorImpl?(PresentationThemeAccentColor(baseColor: .blue))
@ -1014,7 +1016,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
} }
defaultThemes.append(contentsOf: [.builtin(.night), .builtin(.nightAccent)]) 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 var availableThemes = defaultThemes
if defaultThemes.first(where: { $0.index == themeReference.index }) == nil && cloudThemes.first(where: { $0.index == themeReference.index }) == nil { 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 baseThemeIndex: Int64?
var updatedThemeBaseIndex: Int64? var updatedThemeBaseIndex: Int64?
if case let .cloud(info) = 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))
if let settings = info.theme.settings { if let settings = info.theme.settings {
baseThemeIndex = PresentationThemeReference.builtin(PresentationBuiltinThemeReference(baseTheme: settings.baseTheme)).index baseThemeIndex = PresentationThemeReference.builtin(PresentationBuiltinThemeReference(baseTheme: settings.baseTheme)).index
updatedThemeBaseIndex = baseThemeIndex updatedThemeBaseIndex = baseThemeIndex

View File

@ -24,6 +24,7 @@ static_library(
"//submodules/ActivityIndicator:ActivityIndicator", "//submodules/ActivityIndicator:ActivityIndicator",
"//submodules/AnimatedStickerNode:AnimatedStickerNode", "//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/ArchivedStickerPacksNotice:ArchivedStickerPacksNotice",
], ],
frameworks = [ frameworks = [
"$SDKROOT/System/Library/Frameworks/Foundation.framework", "$SDKROOT/System/Library/Frameworks/Foundation.framework",

View File

@ -231,3 +231,20 @@ public func recentlyUsedInlineBots(postbox: Postbox) -> Signal<[(Peer, Double)],
} }
} }
public func removeRecentlyUsedInlineBot(account: Account, peerId: PeerId) -> Signal<Void, NoError> {
return account.postbox.transaction { transaction -> Signal<Void, NoError> 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<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
} else {
return .complete()
}
} |> switchToLatest
}

View File

@ -816,12 +816,12 @@ final class SharedApplicationContext {
return return
} }
var exists = false var exists = false
strongSelf.mainWindow.forEachViewController { controller in strongSelf.mainWindow.forEachViewController({ controller in
if controller is ThemeSettingsCrossfadeController || controller is ThemeSettingsController { if controller is ThemeSettingsCrossfadeController || controller is ThemeSettingsController {
exists = true exists = true
} }
return true return true
} })
if !exists { if !exists {
strongSelf.mainWindow.present(ThemeSettingsCrossfadeController(), on: .root) 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 { if let controller = controller as? UndoOverlayController {
controller.dismissWithCommitAction() controller.dismissWithCommitAction()
} }
return true return true
} })
} }
func applicationDidEnterBackground(_ application: UIApplication) { func applicationDidEnterBackground(_ application: UIApplication) {

View File

@ -333,7 +333,7 @@ final class AuthorizedApplicationContext {
return false return false
} }
return true return true
}) }, excludeNavigationSubControllers: true)
if foundOverlay { if foundOverlay {
return true return true
@ -362,7 +362,7 @@ final class AuthorizedApplicationContext {
return false return false
}, expandAction: { expandData in }, expandAction: { expandData in
if let strongSelf = self { 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.navigation_setNavigationController(strongSelf.rootController)
chatController.presentationArguments = ChatControllerOverlayPresentationData(expandData: expandData()) chatController.presentationArguments = ChatControllerOverlayPresentationData(expandData: expandData())
//strongSelf.rootController.pushViewController(chatController) //strongSelf.rootController.pushViewController(chatController)

View File

@ -8274,6 +8274,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return navigationController return navigationController
} else if case let .inline(navigationController) = self.presentationInterfaceState.mode { } else if case let .inline(navigationController) = self.presentationInterfaceState.mode {
return navigationController return navigationController
} else if case let .overlay(navigationController) = self.presentationInterfaceState.mode {
return navigationController
} else { } else {
return nil return nil
} }

View File

@ -496,7 +496,14 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
self.navigationBar?.isHidden = true self.navigationBar?.isHidden = true
} }
if self.overlayNavigationBar == nil { 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() self?.dismissAsOverlay()
}) })
overlayNavigationBar.peerView = self.peerView overlayNavigationBar.peerView = self.peerView

View File

@ -14,6 +14,7 @@ final class ChatOverlayNavigationBar: ASDisplayNode {
private let theme: PresentationTheme private let theme: PresentationTheme
private let strings: PresentationStrings private let strings: PresentationStrings
private let nameDisplayOrder: PresentationPersonNameOrder private let nameDisplayOrder: PresentationPersonNameOrder
private let tapped: () -> Void
private let close: () -> Void private let close: () -> Void
private let separatorNode: ASDisplayNode 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.theme = theme
self.strings = strings self.strings = strings
self.nameDisplayOrder = nameDisplayOrder self.nameDisplayOrder = nameDisplayOrder
self.tapped = tapped
self.close = close self.close = close
self.separatorNode = ASDisplayNode() self.separatorNode = ASDisplayNode()
@ -83,6 +85,13 @@ final class ChatOverlayNavigationBar: ASDisplayNode {
self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside]) 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) { 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))) 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() 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)) 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)) let closeButtonSize = CGSize(width: size.height, height: size.height)
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)) 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() self.close()
} }
} }

View File

@ -10,6 +10,7 @@ import TelegramUIPreferences
import MergeLists import MergeLists
import AccountContext import AccountContext
import AccountContext import AccountContext
import ItemListUI
private struct HashtagChatInputContextPanelEntryStableId: Hashable { private struct HashtagChatInputContextPanelEntryStableId: Hashable {
let text: String let text: String
@ -19,25 +20,26 @@ private struct HashtagChatInputContextPanelEntry: Comparable, Identifiable {
let index: Int let index: Int
let theme: PresentationTheme let theme: PresentationTheme
let text: String let text: String
let revealed: Bool
var stableId: HashtagChatInputContextPanelEntryStableId { var stableId: HashtagChatInputContextPanelEntryStableId {
return HashtagChatInputContextPanelEntryStableId(text: self.text) return HashtagChatInputContextPanelEntryStableId(text: self.text)
} }
func withUpdatedTheme(_ theme: PresentationTheme) -> HashtagChatInputContextPanelEntry { 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 { 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 { static func <(lhs: HashtagChatInputContextPanelEntry, rhs: HashtagChatInputContextPanelEntry) -> Bool {
return lhs.index < rhs.index return lhs.index < rhs.index
} }
func item(account: Account, fontSize: PresentationFontSize, hashtagSelected: @escaping (String) -> Void) -> ListViewItem { func item(account: Account, presentationData: PresentationData, setHashtagRevealed: @escaping (String?) -> Void, hashtagSelected: @escaping (String) -> Void, removeRequested: @escaping (String) -> Void) -> ListViewItem {
return HashtagChatInputPanelItem(theme: self.theme, fontSize: fontSize, text: self.text, hashtagSelected: hashtagSelected) 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] 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 (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } 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 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, fontSize: fontSize, hashtagSelected: hashtagSelected), 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) return HashtagChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates)
} }
@ -61,6 +63,9 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode {
private let listView: ListView private let listView: ListView
private var currentEntries: [HashtagChatInputContextPanelEntry]? private var currentEntries: [HashtagChatInputContextPanelEntry]?
private var currentResults: [String] = []
private var revealedHashtag: String?
private var enqueuedTransitions: [(HashtagChatInputContextPanelTransition, Bool)] = [] private var enqueuedTransitions: [(HashtagChatInputContextPanelTransition, Bool)] = []
private var validLayout: (CGSize, CGFloat, CGFloat, CGFloat)? private var validLayout: (CGSize, CGFloat, CGFloat, CGFloat)?
@ -81,11 +86,13 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode {
} }
func updateResults(_ results: [String]) { func updateResults(_ results: [String]) {
self.currentResults = results
var entries: [HashtagChatInputContextPanelEntry] = [] var entries: [HashtagChatInputContextPanelEntry] = []
var index = 0 var index = 0
var stableIds = Set<HashtagChatInputContextPanelEntryStableId>() var stableIds = Set<HashtagChatInputContextPanelEntryStableId>()
for text in results { 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) { if stableIds.contains(entry.stableId) {
continue continue
} }
@ -98,7 +105,12 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode {
private func prepareTransition(from: [HashtagChatInputContextPanelEntry]? , to: [HashtagChatInputContextPanelEntry]) { private func prepareTransition(from: [HashtagChatInputContextPanelEntry]? , to: [HashtagChatInputContextPanelEntry]) {
let firstTime = from == nil 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 { if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction {
interfaceInteraction.updateTextInputStateAndMode { textInputState, inputMode in interfaceInteraction.updateTextInputStateAndMode { textInputState, inputMode in
var hashtagQueryRange: NSRange? var hashtagQueryRange: NSRange?
@ -123,6 +135,10 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode {
return (textInputState, inputMode) 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.currentEntries = to
self.enqueueTransition(transition, firstTime: firstTime) self.enqueueTransition(transition, firstTime: firstTime)

View File

@ -8,20 +8,25 @@ import SwiftSignalKit
import Postbox import Postbox
import TelegramPresentationData import TelegramPresentationData
import TelegramUIPreferences import TelegramUIPreferences
import ItemListUI
final class HashtagChatInputPanelItem: ListViewItem { final class HashtagChatInputPanelItem: ListViewItem {
fileprivate let theme: PresentationTheme fileprivate let presentationData: ItemListPresentationData
fileprivate let fontSize: PresentationFontSize
fileprivate let text: String fileprivate let text: String
fileprivate let revealed: Bool
fileprivate let setHashtagRevealed: (String?) -> Void
private let hashtagSelected: (String) -> Void private let hashtagSelected: (String) -> Void
fileprivate let removeRequested: (String) -> Void
let selectable: Bool = true let selectable: Bool = true
public init(theme: PresentationTheme, fontSize: PresentationFontSize, text: String, hashtagSelected: @escaping (String) -> Void) { public init(presentationData: ItemListPresentationData, text: String, revealed: Bool, setHashtagRevealed: @escaping (String?) -> Void, hashtagSelected: @escaping (String) -> Void, removeRequested: @escaping (String) -> Void) {
self.theme = theme self.presentationData = presentationData
self.fontSize = fontSize
self.text = text self.text = text
self.revealed = revealed
self.setHashtagRevealed = setHashtagRevealed
self.hashtagSelected = hashtagSelected 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<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) { public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
@ -72,7 +77,11 @@ final class HashtagChatInputPanelItem: ListViewItem {
} }
func selected(listView: ListView) { 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 topSeparatorNode: ASDisplayNode
private let separatorNode: ASDisplayNode private let separatorNode: ASDisplayNode
private let highlightedBackgroundNode: 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() { init() {
self.textNode = TextNode() self.textNode = TextNode()
@ -102,6 +122,15 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode {
self.addSubnode(self.textNode) 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?) { override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
if let item = item as? HashtagChatInputPanelItem { if let item = item as? HashtagChatInputPanelItem {
let doLayout = self.asyncLayout() 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) { func asyncLayout() -> (_ item: HashtagChatInputPanelItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
let makeTextLayout = TextNode.asyncLayout(self.textNode) let makeTextLayout = TextNode.asyncLayout(self.textNode)
return { [weak self] item, params, mergedTop, mergedBottom in 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 baseWidth = params.width - params.leftInset - params.rightInset
let leftInset: CGFloat = 15.0 + params.leftInset let leftInset: CGFloat = 15.0 + params.leftInset
let rightInset: CGFloat = 10.0 + params.rightInset 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()) let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: HashtagChatInputPanelItemNode.itemHeight), insets: UIEdgeInsets())
return (nodeLayout, { _ in return (nodeLayout, { animation in
if let strongSelf = self { if let strongSelf = self {
strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor strongSelf.item = item
strongSelf.topSeparatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor strongSelf.validLayout = (nodeLayout.contentSize, params.leftInset, params.rightInset)
strongSelf.backgroundColor = item.theme.list.plainBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor 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() 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.topSeparatorNode.isHidden = mergedTop
strongSelf.separatorNode.isHidden = !mergedBottom 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.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.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) { override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated) super.setHighlighted(highlighted, at: point, animated: animated)
if let revealNode = self.revealNode, self.revealOffset != 0 {
return
}
if highlighted { if highlighted {
self.highlightedBackgroundNode.alpha = 1.0 self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil { 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)
}
} }

View File

@ -11,25 +11,27 @@ import MergeLists
import TextFormat import TextFormat
import AccountContext import AccountContext
import LocalizedPeerData import LocalizedPeerData
import ItemListUI
private struct MentionChatInputContextPanelEntry: Comparable, Identifiable { private struct MentionChatInputContextPanelEntry: Comparable, Identifiable {
let index: Int let index: Int
let peer: Peer let peer: Peer
let revealed: Bool
var stableId: Int64 { var stableId: Int64 {
return self.peer.id.toInt64() return self.peer.id.toInt64()
} }
static func ==(lhs: MentionChatInputContextPanelEntry, rhs: MentionChatInputContextPanelEntry) -> Bool { 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 { static func <(lhs: MentionChatInputContextPanelEntry, rhs: MentionChatInputContextPanelEntry) -> Bool {
return lhs.index < rhs.index return lhs.index < rhs.index
} }
func item(context: AccountContext, theme: PresentationTheme, fontSize: PresentationFontSize, inverted: Bool, peerSelected: @escaping (Peer) -> Void) -> ListViewItem { 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, theme: theme, fontSize: fontSize, inverted: inverted, peer: self.peer, peerSelected: peerSelected) 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] 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 (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } 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 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, 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, presentationData: presentationData, inverted: inverted, setPeerIdRevealed: setPeerIdRevealed, peerSelected: peerSelected, removeRequested: removeRequested), directionHint: nil) }
return CommandChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates) return CommandChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates)
} }
@ -60,6 +62,8 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode {
private let listView: ListView private let listView: ListView
private var currentEntries: [MentionChatInputContextPanelEntry]? private var currentEntries: [MentionChatInputContextPanelEntry]?
private var revealedPeerId: PeerId?
private var enqueuedTransitions: [(CommandChatInputContextPanelTransition, Bool)] = [] private var enqueuedTransitions: [(CommandChatInputContextPanelTransition, Bool)] = []
private var validLayout: (CGSize, CGFloat, CGFloat, CGFloat)? private var validLayout: (CGSize, CGFloat, CGFloat, CGFloat)?
@ -95,7 +99,7 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode {
continue continue
} }
peerIdSet.insert(peerId) peerIdSet.insert(peerId)
entries.append(MentionChatInputContextPanelEntry(index: index, peer: peer)) entries.append(MentionChatInputContextPanelEntry(index: index, peer: peer, revealed: revealedPeerId == peer.id))
index += 1 index += 1
} }
self.updateToEntries(entries: entries, forceUpdate: false) self.updateToEntries(entries: entries, forceUpdate: false)
@ -103,7 +107,10 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode {
private func updateToEntries(entries: [MentionChatInputContextPanelEntry], forceUpdate: Bool) { private func updateToEntries(entries: [MentionChatInputContextPanelEntry], forceUpdate: Bool) {
let firstTime = self.currentEntries == nil 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 { if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction {
switch strongSelf.mode { switch strongSelf.mode {
case .input: case .input:
@ -147,6 +154,10 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode {
interfaceInteraction.beginMessageSearch(.member(peer), "") 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.currentEntries = entries
self.enqueueTransition(transition, firstTime: firstTime) self.enqueueTransition(transition, firstTime: firstTime)

View File

@ -10,24 +10,29 @@ import TelegramPresentationData
import TelegramUIPreferences import TelegramUIPreferences
import AvatarNode import AvatarNode
import AccountContext import AccountContext
import ItemListUI
final class MentionChatInputPanelItem: ListViewItem { final class MentionChatInputPanelItem: ListViewItem {
fileprivate let context: AccountContext fileprivate let context: AccountContext
fileprivate let theme: PresentationTheme fileprivate let presentationData: ItemListPresentationData
fileprivate let fontSize: PresentationFontSize fileprivate let revealed: Bool
fileprivate let inverted: Bool fileprivate let inverted: Bool
fileprivate let peer: Peer fileprivate let peer: Peer
private let peerSelected: (Peer) -> Void private let peerSelected: (Peer) -> Void
fileprivate let setPeerIdRevealed: (PeerId?) -> Void
fileprivate let removeRequested: (PeerId) -> Void
let selectable: Bool = true 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.context = context
self.theme = theme self.presentationData = presentationData
self.fontSize = fontSize
self.inverted = inverted self.inverted = inverted
self.peer = peer self.peer = peer
self.revealed = revealed
self.setPeerIdRevealed = setPeerIdRevealed
self.peerSelected = peerSelected 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<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) { public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
@ -86,15 +91,24 @@ private let avatarFont = avatarPlaceholderFont(size: 16.0)
final class MentionChatInputPanelItemNode: ListViewItemNode { final class MentionChatInputPanelItemNode: ListViewItemNode {
static let itemHeight: CGFloat = 42.0 static let itemHeight: CGFloat = 42.0
private var item: MentionChatInputPanelItem?
private let avatarNode: AvatarNode private let avatarNode: AvatarNode
private let textNode: TextNode private let textNode: TextNode
private let topSeparatorNode: ASDisplayNode private let topSeparatorNode: ASDisplayNode
private let separatorNode: ASDisplayNode private let separatorNode: ASDisplayNode
private let highlightedBackgroundNode: 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() { init() {
self.avatarNode = AvatarNode(font: avatarFont) self.avatarNode = AvatarNode(font: avatarFont)
self.textNode = TextNode() self.textNode = TextNode()
@ -117,6 +131,15 @@ final class MentionChatInputPanelItemNode: ListViewItemNode {
self.addSubnode(self.textNode) 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?) { override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
if let item = item as? MentionChatInputPanelItem { if let item = item as? MentionChatInputPanelItem {
let doLayout = self.asyncLayout() let doLayout = self.asyncLayout()
@ -134,8 +157,8 @@ final class MentionChatInputPanelItemNode: ListViewItemNode {
let previousItem = self.item let previousItem = self.item
return { [weak self] item, params, mergedTop, mergedBottom in return { [weak self] item, params, mergedTop, mergedBottom in
let primaryFont = Font.medium(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.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 leftInset: CGFloat = 55.0 + params.leftInset
let rightInset: CGFloat = 10.0 + params.rightInset let rightInset: CGFloat = 10.0 + params.rightInset
@ -146,19 +169,23 @@ final class MentionChatInputPanelItemNode: ListViewItemNode {
} }
let string = NSMutableAttributedString() 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 { 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 (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()) let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: HashtagChatInputPanelItemNode.itemHeight), insets: UIEdgeInsets())
return (nodeLayout, { _ in return (nodeLayout, { animation in
if let strongSelf = self { if let strongSelf = self {
strongSelf.item = item strongSelf.item = item
strongSelf.validLayout = (nodeLayout.contentSize, params.leftInset, params.rightInset)
let revealOffset = strongSelf.revealOffset
if let updatedInverted = updatedInverted { if let updatedInverted = updatedInverted {
if updatedInverted { if updatedInverted {
strongSelf.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0) 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.separatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor
strongSelf.topSeparatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor strongSelf.topSeparatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor
strongSelf.backgroundColor = item.theme.list.plainBackgroundColor strongSelf.backgroundColor = item.presentationData.theme.list.plainBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor 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() 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.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)) 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) { override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated) super.setHighlighted(highlighted, at: point, animated: animated)
if let revealNode = self.revealNode, self.revealOffset != 0 {
return
}
if highlighted { if highlighted {
self.highlightedBackgroundNode.alpha = 1.0 self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil { 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)
}
} }

View File

@ -111,10 +111,10 @@ final class ThemeUpdateManagerImpl: ThemeUpdateManager {
return .complete() return .complete()
} }
accountManager.mediaBox.storeResourceData(file.file.resource.id, data: fullSizeData, synchronous: true) 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 { } 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))
} }
} }
} }

View File

@ -97,15 +97,18 @@ public struct PresentationLocalTheme: PostboxCoding, Equatable {
public struct PresentationCloudTheme: PostboxCoding, Equatable { public struct PresentationCloudTheme: PostboxCoding, Equatable {
public let theme: TelegramTheme public let theme: TelegramTheme
public let resolvedWallpaper: TelegramWallpaper? 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.theme = theme
self.resolvedWallpaper = resolvedWallpaper self.resolvedWallpaper = resolvedWallpaper
self.creatorAccountId = creatorAccountId
} }
public init(decoder: PostboxDecoder) { public init(decoder: PostboxDecoder) {
self.theme = decoder.decodeObjectForKey("theme", decoder: { TelegramTheme(decoder: $0) }) as! TelegramTheme self.theme = decoder.decodeObjectForKey("theme", decoder: { TelegramTheme(decoder: $0) }) as! TelegramTheme
self.resolvedWallpaper = decoder.decodeObjectForKey("wallpaper", decoder: { TelegramWallpaper(decoder: $0) }) as? TelegramWallpaper 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) { public func encode(_ encoder: PostboxEncoder) {
@ -115,6 +118,11 @@ public struct PresentationCloudTheme: PostboxCoding, Equatable {
} else { } else {
encoder.encodeNil(forKey: "wallpaper") 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 { public static func ==(lhs: PresentationCloudTheme, rhs: PresentationCloudTheme) -> Bool {