mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-06 17:00:13 +00:00
iPad trackpad and key shortcuts improvements
This commit is contained in:
parent
6c4a730b42
commit
5b8961d02a
@ -7942,3 +7942,13 @@ Sorry for the inconvenience.";
|
|||||||
|
|
||||||
"StickerPacks.DeleteEmojiPacksConfirmation_1" = "Delete 1 Emoji Pack";
|
"StickerPacks.DeleteEmojiPacksConfirmation_1" = "Delete 1 Emoji Pack";
|
||||||
"StickerPacks.DeleteEmojiPacksConfirmation_any" = "Delete %@ Emoji Packs";
|
"StickerPacks.DeleteEmojiPacksConfirmation_any" = "Delete %@ Emoji Packs";
|
||||||
|
|
||||||
|
"KeyCommand.LockWithPasscode" = "Lock with Passcode";
|
||||||
|
"KeyCommand.Play" = "Play Video";
|
||||||
|
"KeyCommand.Pause" = "Pause Video";
|
||||||
|
"KeyCommand.SeekBackward" = "Seek Backward";
|
||||||
|
"KeyCommand.SeekForward" = "Seek Forward";
|
||||||
|
"KeyCommand.Share" = "Share";
|
||||||
|
"KeyCommand.SwitchToPIP" = "Switch to Picture-in-Picture";
|
||||||
|
"KeyCommand.EnterFullscreen" = "Enter Fullscreen";
|
||||||
|
"KeyCommand.ExitFullscreen" = "Exit Fullscreen";
|
||||||
|
|||||||
@ -25,6 +25,8 @@ final class AttachmentTextInputActionButtonsNode: ASDisplayNode {
|
|||||||
self.gestureRecognizer?.isEnabled = self.sendButtonLongPressEnabled
|
self.gestureRecognizer?.isEnabled = self.sendButtonLongPressEnabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var sendButtonPointerInteraction: PointerInteraction?
|
||||||
|
|
||||||
private var validLayout: CGSize?
|
private var validLayout: CGSize?
|
||||||
|
|
||||||
@ -39,7 +41,7 @@ final class AttachmentTextInputActionButtonsNode: ASDisplayNode {
|
|||||||
self.backgroundNode = ASDisplayNode()
|
self.backgroundNode = ASDisplayNode()
|
||||||
self.backgroundNode.backgroundColor = theme.chat.inputPanel.actionControlFillColor
|
self.backgroundNode.backgroundColor = theme.chat.inputPanel.actionControlFillColor
|
||||||
self.backgroundNode.clipsToBounds = true
|
self.backgroundNode.clipsToBounds = true
|
||||||
self.sendButton = HighlightTrackingButtonNode(pointerStyle: .lift)
|
self.sendButton = HighlightTrackingButtonNode(pointerStyle: nil)
|
||||||
|
|
||||||
self.textNode = ImmediateTextNode()
|
self.textNode = ImmediateTextNode()
|
||||||
self.textNode.attributedText = NSAttributedString(string: self.strings.MediaPicker_Send, font: Font.semibold(17.0), textColor: theme.chat.inputPanel.actionControlForegroundColor)
|
self.textNode.attributedText = NSAttributedString(string: self.strings.MediaPicker_Send, font: Font.semibold(17.0), textColor: theme.chat.inputPanel.actionControlForegroundColor)
|
||||||
@ -90,6 +92,8 @@ final class AttachmentTextInputActionButtonsNode: ASDisplayNode {
|
|||||||
strongSelf.sendButtonLongPressed?(strongSelf.sendContainerNode, recognizer)
|
strongSelf.sendButtonLongPressed?(strongSelf.sendContainerNode, recognizer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.sendButtonPointerInteraction = PointerInteraction(view: self.sendButton.view, customInteractionView: self.backgroundNode.view, style: .lift)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateTheme(theme: PresentationTheme, wallpaper: TelegramWallpaper) {
|
func updateTheme(theme: PresentationTheme, wallpaper: TelegramWallpaper) {
|
||||||
|
|||||||
@ -2424,10 +2424,30 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
strongSelf.composePressed()
|
strongSelf.composePressed()
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
KeyShortcut(title: strings.KeyCommand_LockWithPasscode, input: "L", modifiers: [.command], action: { [weak self] in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.context.sharedContext.appLockContext.lock()
|
||||||
|
}
|
||||||
|
}),
|
||||||
KeyShortcut(title: strings.KeyCommand_Find, input: "\t", modifiers: [], action: toggleSearch),
|
KeyShortcut(title: strings.KeyCommand_Find, input: "\t", modifiers: [], action: toggleSearch),
|
||||||
KeyShortcut(input: UIKeyCommand.inputEscape, modifiers: [], action: toggleSearch)
|
KeyShortcut(input: UIKeyCommand.inputEscape, modifiers: [], action: toggleSearch)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
let openTab: (Int) -> Void = { [weak self] index in
|
||||||
|
if let strongSelf = self {
|
||||||
|
let filters = strongSelf.chatListDisplayNode.containerNode.availableFilters
|
||||||
|
if index > filters.count - 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch filters[index] {
|
||||||
|
case .all:
|
||||||
|
strongSelf.selectTab(id: .all)
|
||||||
|
case let .filter(filter):
|
||||||
|
strongSelf.selectTab(id: .filter(filter.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let openChat: (Int) -> Void = { [weak self] index in
|
let openChat: (Int) -> Void = { [weak self] index in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
if index == 0 {
|
if index == 0 {
|
||||||
@ -2438,13 +2458,23 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let chatShortcuts: [KeyShortcut] = (0 ... 9).map { index in
|
let folderShortcuts: [KeyShortcut] = (0 ... 9).map { index in
|
||||||
return KeyShortcut(input: "\(index)", modifiers: [.command], action: {
|
return KeyShortcut(input: "\(index)", modifiers: [.command], action: {
|
||||||
|
if index == 0 {
|
||||||
|
openChat(0)
|
||||||
|
} else {
|
||||||
|
openTab(index - 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let chatShortcuts: [KeyShortcut] = (0 ... 9).map { index in
|
||||||
|
return KeyShortcut(input: "\(index)", modifiers: [.command, .alternate], action: {
|
||||||
openChat(index)
|
openChat(index)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return inputShortcuts + chatShortcuts
|
return inputShortcuts + folderShortcuts + chatShortcuts
|
||||||
}
|
}
|
||||||
|
|
||||||
override public func toolbarActionSelected(action: ToolbarActionOption) {
|
override public func toolbarActionSelected(action: ToolbarActionOption) {
|
||||||
|
|||||||
@ -88,6 +88,8 @@ private final class ItemNode: ASDisplayNode {
|
|||||||
|
|
||||||
private var theme: PresentationTheme?
|
private var theme: PresentationTheme?
|
||||||
|
|
||||||
|
private var pointerInteraction: PointerInteraction?
|
||||||
|
|
||||||
init(pressed: @escaping (Bool) -> Void, requestedDeletion: @escaping () -> Void, contextGesture: @escaping (ContextExtractedContentContainingNode, ContextGesture, Bool) -> Void) {
|
init(pressed: @escaping (Bool) -> Void, requestedDeletion: @escaping () -> Void, contextGesture: @escaping (ContextExtractedContentContainingNode, ContextGesture, Bool) -> Void) {
|
||||||
self.pressed = pressed
|
self.pressed = pressed
|
||||||
self.requestedDeletion = requestedDeletion
|
self.requestedDeletion = requestedDeletion
|
||||||
@ -189,6 +191,12 @@ private final class ItemNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func didLoad() {
|
||||||
|
super.didLoad()
|
||||||
|
|
||||||
|
self.pointerInteraction = PointerInteraction(view: self.containerNode.view, customInteractionView: nil, style: .insetRectangle(-10.0, 4.0))
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func buttonPressed() {
|
@objc private func buttonPressed() {
|
||||||
self.pressed(self.isDisabled)
|
self.pressed(self.isDisabled)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2106,7 +2106,7 @@ public final class ChatListNode: ListView {
|
|||||||
|> take(1)
|
|> take(1)
|
||||||
|> deliverOnMainQueue).start(next: { update in
|
|> deliverOnMainQueue).start(next: { update in
|
||||||
let entries = update.view.entries
|
let entries = update.view.entries
|
||||||
if entries.count > index, case let .MessageEntry(index, _, _, _, _, renderedPeer, _, _, _, _) = entries[10 - index - 1] {
|
if entries.count > index, case let .MessageEntry(index, _, _, _, _, renderedPeer, _, _, _, _) = entries[9 - index - 1] {
|
||||||
let location: ChatListNodeLocation = .scroll(index: index, sourceIndex: .absoluteLowerBound, scrollPosition: .center(.top), animated: true, filter: filter)
|
let location: ChatListNodeLocation = .scroll(index: index, sourceIndex: .absoluteLowerBound, scrollPosition: .center(.top), animated: true, filter: filter)
|
||||||
self.setChatListLocation(location)
|
self.setChatListLocation(location)
|
||||||
self.peerSelected?(EnginePeer(renderedPeer.peer!), false, false, nil)
|
self.peerSelected?(EnginePeer(renderedPeer.peer!), false, false, nil)
|
||||||
|
|||||||
@ -405,11 +405,10 @@ public class ContactsController: ViewController {
|
|||||||
let controller = strongSelf.context.sharedContext.makePeersNearbyController(context: strongSelf.context)
|
let controller = strongSelf.context.sharedContext.makePeersNearbyController(context: strongSelf.context)
|
||||||
controller.navigationPresentation = .master
|
controller.navigationPresentation = .master
|
||||||
if let navigationController = strongSelf.context.sharedContext.mainWindow?.viewController as? NavigationController {
|
if let navigationController = strongSelf.context.sharedContext.mainWindow?.viewController as? NavigationController {
|
||||||
navigationController.pushViewController(controller, animated: true, completion: { [weak self] in
|
var controllers = navigationController.viewControllers.filter { !($0 is PermissionController) }
|
||||||
if let strongSelf = self {
|
controllers.append(controller)
|
||||||
strongSelf.contactsNode.contactListNode.listNode.clearHighlightAnimated(true)
|
navigationController.setViewControllers(controllers, animated: true)
|
||||||
}
|
strongSelf.contactsNode.contactListNode.listNode.clearHighlightAnimated(true)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2205,6 +2205,18 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
|||||||
|
|
||||||
return self.dismissNode.view
|
return self.dismissNode.view
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileprivate func performHighlightedAction() {
|
||||||
|
self.presentationNode?.highlightGestureFinished(performAction: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func decreaseHighlightedIndex() {
|
||||||
|
self.presentationNode?.decreaseHighlightedIndex()
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func increaseHighlightedIndex() {
|
||||||
|
self.presentationNode?.increaseHighlightedIndex()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class ContextControllerLocationViewInfo {
|
public final class ContextControllerLocationViewInfo {
|
||||||
@ -2350,7 +2362,7 @@ public protocol ContextControllerItemsContent: AnyObject {
|
|||||||
) -> ContextControllerItemsNode
|
) -> ContextControllerItemsNode
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class ContextController: ViewController, StandalonePresentableController, ContextControllerProtocol {
|
public final class ContextController: ViewController, StandalonePresentableController, ContextControllerProtocol, KeyShortcutResponder {
|
||||||
public struct Items {
|
public struct Items {
|
||||||
public enum Content {
|
public enum Content {
|
||||||
case list([ContextMenuItem])
|
case list([ContextMenuItem])
|
||||||
@ -2644,4 +2656,44 @@ public final class ContextController: ViewController, StandalonePresentableContr
|
|||||||
public func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) {
|
public func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) {
|
||||||
self.controllerNode.addRelativeContentOffset(offset, transition: transition)
|
self.controllerNode.addRelativeContentOffset(offset, transition: transition)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var keyShortcuts: [KeyShortcut] {
|
||||||
|
return [
|
||||||
|
KeyShortcut(
|
||||||
|
input: UIKeyCommand.inputEscape,
|
||||||
|
modifiers: [],
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.dismissWithoutContent()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
KeyShortcut(
|
||||||
|
input: "W",
|
||||||
|
modifiers: [.command],
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.dismissWithoutContent()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
KeyShortcut(
|
||||||
|
input: "\r",
|
||||||
|
modifiers: [],
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.controllerNode.performHighlightedAction()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
KeyShortcut(
|
||||||
|
input: UIKeyCommand.inputUpArrow,
|
||||||
|
modifiers: [],
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.controllerNode.decreaseHighlightedIndex()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
KeyShortcut(
|
||||||
|
input: UIKeyCommand.inputDownArrow,
|
||||||
|
modifiers: [],
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.controllerNode.increaseHighlightedIndex()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,9 @@ public protocol ContextControllerActionsStackItemNode: ASDisplayNode {
|
|||||||
|
|
||||||
func highlightGestureMoved(location: CGPoint)
|
func highlightGestureMoved(location: CGPoint)
|
||||||
func highlightGestureFinished(performAction: Bool)
|
func highlightGestureFinished(performAction: Bool)
|
||||||
|
|
||||||
|
func decreaseHighlightedIndex()
|
||||||
|
func increaseHighlightedIndex()
|
||||||
}
|
}
|
||||||
|
|
||||||
public protocol ContextControllerActionsStackItem: AnyObject {
|
public protocol ContextControllerActionsStackItem: AnyObject {
|
||||||
@ -585,6 +588,34 @@ final class ContextControllerActionsListStackItem: ContextControllerActionsStack
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func decreaseHighlightedIndex() {
|
||||||
|
let previousHighlightedItemNode: Item? = self.highlightedItemNode
|
||||||
|
if let highlightedItemNode = self.highlightedItemNode, let index = self.itemNodes.firstIndex(where: { $0 === highlightedItemNode }) {
|
||||||
|
self.highlightedItemNode = self.itemNodes[max(0, index - 1)]
|
||||||
|
} else {
|
||||||
|
self.highlightedItemNode = self.itemNodes.first
|
||||||
|
}
|
||||||
|
|
||||||
|
if previousHighlightedItemNode !== self.highlightedItemNode {
|
||||||
|
previousHighlightedItemNode?.node.updateIsHighlighted(isHighlighted: false)
|
||||||
|
self.highlightedItemNode?.node.updateIsHighlighted(isHighlighted: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func increaseHighlightedIndex() {
|
||||||
|
let previousHighlightedItemNode: Item? = self.highlightedItemNode
|
||||||
|
if let highlightedItemNode = self.highlightedItemNode, let index = self.itemNodes.firstIndex(where: { $0 === highlightedItemNode }) {
|
||||||
|
self.highlightedItemNode = self.itemNodes[min(self.itemNodes.count - 1, index + 1)]
|
||||||
|
} else {
|
||||||
|
self.highlightedItemNode = self.itemNodes.first
|
||||||
|
}
|
||||||
|
|
||||||
|
if previousHighlightedItemNode !== self.highlightedItemNode {
|
||||||
|
previousHighlightedItemNode?.node.updateIsHighlighted(isHighlighted: false)
|
||||||
|
self.highlightedItemNode?.node.updateIsHighlighted(isHighlighted: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private let items: [ContextMenuItem]
|
private let items: [ContextMenuItem]
|
||||||
@ -663,6 +694,12 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta
|
|||||||
|
|
||||||
func highlightGestureFinished(performAction: Bool) {
|
func highlightGestureFinished(performAction: Bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func decreaseHighlightedIndex() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func increaseHighlightedIndex() {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private let content: ContextControllerItemsContent
|
private let content: ContextControllerItemsContent
|
||||||
@ -925,6 +962,14 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
self.node.highlightGestureFinished(performAction: performAction)
|
self.node.highlightGestureFinished(performAction: performAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func decreaseHighlightedIndex() {
|
||||||
|
self.node.decreaseHighlightedIndex()
|
||||||
|
}
|
||||||
|
|
||||||
|
func increaseHighlightedIndex() {
|
||||||
|
self.node.increaseHighlightedIndex()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private let getController: () -> ContextControllerProtocol?
|
private let getController: () -> ContextControllerProtocol?
|
||||||
@ -1229,6 +1274,18 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func decreaseHighlightedIndex() {
|
||||||
|
if let topItemContainer = self.itemContainers.last {
|
||||||
|
topItemContainer.decreaseHighlightedIndex()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func increaseHighlightedIndex() {
|
||||||
|
if let topItemContainer = self.itemContainers.last {
|
||||||
|
topItemContainer.increaseHighlightedIndex()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func updatePanSelection(isEnabled: Bool) {
|
func updatePanSelection(isEnabled: Bool) {
|
||||||
if let selectionPanGesture = self.selectionPanGesture {
|
if let selectionPanGesture = self.selectionPanGesture {
|
||||||
selectionPanGesture.isEnabled = isEnabled
|
selectionPanGesture.isEnabled = isEnabled
|
||||||
|
|||||||
@ -315,6 +315,14 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func decreaseHighlightedIndex() {
|
||||||
|
self.actionsStackNode.decreaseHighlightedIndex()
|
||||||
|
}
|
||||||
|
|
||||||
|
func increaseHighlightedIndex() {
|
||||||
|
self.actionsStackNode.increaseHighlightedIndex()
|
||||||
|
}
|
||||||
|
|
||||||
func replaceItems(items: ContextController.Items, animated: Bool) {
|
func replaceItems(items: ContextController.Items, animated: Bool) {
|
||||||
self.actionsStackNode.replace(item: makeContextControllerActionsStackItem(items: items), animated: animated)
|
self.actionsStackNode.replace(item: makeContextControllerActionsStackItem(items: items), animated: animated)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,5 +31,8 @@ protocol ContextControllerPresentationNode: ASDisplayNode {
|
|||||||
func highlightGestureMoved(location: CGPoint, hover: Bool)
|
func highlightGestureMoved(location: CGPoint, hover: Bool)
|
||||||
func highlightGestureFinished(performAction: Bool)
|
func highlightGestureFinished(performAction: Bool)
|
||||||
|
|
||||||
|
func decreaseHighlightedIndex()
|
||||||
|
func increaseHighlightedIndex()
|
||||||
|
|
||||||
func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition)
|
func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -87,13 +87,7 @@ public class ActionSheetButtonNode: ActionSheetItemNode {
|
|||||||
|
|
||||||
self.button.highligthedChanged = { [weak self] highlighted in
|
self.button.highligthedChanged = { [weak self] highlighted in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
if highlighted {
|
strongSelf.setHighlighted(highlighted, animated: true)
|
||||||
strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemHighlightedBackgroundColor
|
|
||||||
} else {
|
|
||||||
UIView.animate(withDuration: 0.3, animations: {
|
|
||||||
strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemBackgroundColor
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,16 +98,35 @@ public class ActionSheetButtonNode: ActionSheetItemNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func setHighlighted(_ highlighted: Bool, animated: Bool) {
|
||||||
|
self.highlightedUpdated(highlighted)
|
||||||
|
if highlighted {
|
||||||
|
self.backgroundNode.backgroundColor = self.theme.itemHighlightedBackgroundColor
|
||||||
|
} else {
|
||||||
|
if animated {
|
||||||
|
UIView.animate(withDuration: 0.3, animations: {
|
||||||
|
self.backgroundNode.backgroundColor = self.theme.itemBackgroundColor
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
self.backgroundNode.backgroundColor = self.theme.itemBackgroundColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func performAction() {
|
||||||
|
self.buttonPressed()
|
||||||
|
}
|
||||||
|
|
||||||
public override func didLoad() {
|
public override func didLoad() {
|
||||||
super.didLoad()
|
super.didLoad()
|
||||||
|
|
||||||
self.pointerInteraction = PointerInteraction(node: self, style: .hover, willEnter: { [weak self] in
|
self.pointerInteraction = PointerInteraction(node: self, style: .hover, willEnter: { [weak self] in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemHighlightedBackgroundColor
|
strongSelf.setHighlighted(true, animated: false)
|
||||||
}
|
}
|
||||||
}, willExit: { [weak self] in
|
}, willExit: { [weak self] in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemBackgroundColor
|
strongSelf.setHighlighted(false, animated: false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,7 @@ public protocol ActionSheetGroupOverlayNode: ASDisplayNode {
|
|||||||
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition)
|
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition)
|
||||||
}
|
}
|
||||||
|
|
||||||
open class ActionSheetController: ViewController, PresentableController, StandalonePresentableController {
|
open class ActionSheetController: ViewController, PresentableController, StandalonePresentableController, KeyShortcutResponder {
|
||||||
private var actionSheetNode: ActionSheetControllerNode {
|
private var actionSheetNode: ActionSheetControllerNode {
|
||||||
return self.displayNode as! ActionSheetControllerNode
|
return self.displayNode as! ActionSheetControllerNode
|
||||||
}
|
}
|
||||||
@ -94,4 +94,44 @@ open class ActionSheetController: ViewController, PresentableController, Standal
|
|||||||
self.actionSheetNode.setItemGroupOverlayNode(groupIndex: groupIndex, node: node)
|
self.actionSheetNode.setItemGroupOverlayNode(groupIndex: groupIndex, node: node)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var keyShortcuts: [KeyShortcut] {
|
||||||
|
return [
|
||||||
|
KeyShortcut(
|
||||||
|
input: UIKeyCommand.inputEscape,
|
||||||
|
modifiers: [],
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.dismissAnimated()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
KeyShortcut(
|
||||||
|
input: "W",
|
||||||
|
modifiers: [.command],
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.dismissAnimated()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
KeyShortcut(
|
||||||
|
input: "\r",
|
||||||
|
modifiers: [],
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.actionSheetNode.performHighlightedAction()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
KeyShortcut(
|
||||||
|
input: UIKeyCommand.inputUpArrow,
|
||||||
|
modifiers: [],
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.actionSheetNode.decreaseHighlightedIndex()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
KeyShortcut(
|
||||||
|
input: UIKeyCommand.inputDownArrow,
|
||||||
|
modifiers: [],
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.actionSheetNode.increaseHighlightedIndex()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -88,6 +88,18 @@ final class ActionSheetControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func performHighlightedAction() {
|
||||||
|
self.itemGroupsContainerNode.performHighlightedAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decreaseHighlightedIndex() {
|
||||||
|
self.itemGroupsContainerNode.decreaseHighlightedIndex()
|
||||||
|
}
|
||||||
|
|
||||||
|
func increaseHighlightedIndex() {
|
||||||
|
self.itemGroupsContainerNode.increaseHighlightedIndex()
|
||||||
|
}
|
||||||
|
|
||||||
func updateTheme() {
|
func updateTheme() {
|
||||||
self.leftDimView.backgroundColor = self.theme.dimColor
|
self.leftDimView.backgroundColor = self.theme.dimColor
|
||||||
self.rightDimView.backgroundColor = self.theme.dimColor
|
self.rightDimView.backgroundColor = self.theme.dimColor
|
||||||
|
|||||||
@ -13,7 +13,7 @@ final class ActionSheetItemGroupNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
private let backgroundEffectView: UIVisualEffectView
|
private let backgroundEffectView: UIVisualEffectView
|
||||||
private let scrollNode: ASScrollNode
|
private let scrollNode: ASScrollNode
|
||||||
|
|
||||||
private var itemNodes: [ActionSheetItemNode] = []
|
var itemNodes: [ActionSheetItemNode] = []
|
||||||
private var leadingVisibleNodeCount: CGFloat = 100.0
|
private var leadingVisibleNodeCount: CGFloat = 100.0
|
||||||
|
|
||||||
private var validLayout: CGSize?
|
private var validLayout: CGSize?
|
||||||
|
|||||||
@ -13,6 +13,8 @@ final class ActionSheetItemGroupsContainerNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var highlightedItemIndex: Int? = nil
|
||||||
|
|
||||||
private var groups: [ActionSheetItemGroup] = []
|
private var groups: [ActionSheetItemGroup] = []
|
||||||
var groupNodes: [ActionSheetItemGroupNode] = []
|
var groupNodes: [ActionSheetItemGroupNode] = []
|
||||||
|
|
||||||
@ -26,6 +28,66 @@ final class ActionSheetItemGroupsContainerNode: ASDisplayNode {
|
|||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setHighlightedItemIndex(_ index: Int?, update: Bool = false) {
|
||||||
|
self.highlightedItemIndex = index
|
||||||
|
|
||||||
|
if update {
|
||||||
|
var groupIndex = 0
|
||||||
|
var i = 0
|
||||||
|
for _ in self.groups {
|
||||||
|
for itemNode in self.groupNodes[groupIndex].itemNodes {
|
||||||
|
if i == index {
|
||||||
|
itemNode.setHighlighted(true, animated: false)
|
||||||
|
} else {
|
||||||
|
itemNode.setHighlighted(false, animated: false)
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
groupIndex += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decreaseHighlightedIndex() {
|
||||||
|
let currentHighlightedIndex = self.highlightedItemIndex ?? 0
|
||||||
|
|
||||||
|
self.setHighlightedItemIndex(max(0, currentHighlightedIndex - 1), update: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func increaseHighlightedIndex() {
|
||||||
|
let currentHighlightedIndex = self.highlightedItemIndex ?? -1
|
||||||
|
|
||||||
|
var groupIndex = 0
|
||||||
|
var maxAvailabledIndex = 0
|
||||||
|
for _ in self.groups {
|
||||||
|
for _ in self.groupNodes[groupIndex].itemNodes {
|
||||||
|
maxAvailabledIndex += 1
|
||||||
|
}
|
||||||
|
groupIndex += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
self.setHighlightedItemIndex(min(maxAvailabledIndex - 1, currentHighlightedIndex + 1), update: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func performHighlightedAction() {
|
||||||
|
guard let highlightedItemIndex = self.highlightedItemIndex else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var i = 0
|
||||||
|
var groupIndex = 0
|
||||||
|
for _ in self.groups {
|
||||||
|
for itemNode in self.groupNodes[groupIndex].itemNodes {
|
||||||
|
if i == highlightedItemIndex {
|
||||||
|
itemNode.performAction()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
groupIndex += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func setGroups(_ groups: [ActionSheetItemGroup]) {
|
func setGroups(_ groups: [ActionSheetItemGroup]) {
|
||||||
self.groups = groups
|
self.groups = groups
|
||||||
|
|
||||||
@ -34,6 +96,7 @@ final class ActionSheetItemGroupsContainerNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
self.groupNodes.removeAll()
|
self.groupNodes.removeAll()
|
||||||
|
|
||||||
|
var i = 0
|
||||||
for group in groups {
|
for group in groups {
|
||||||
let groupNode = ActionSheetItemGroupNode(theme: self.theme)
|
let groupNode = ActionSheetItemGroupNode(theme: self.theme)
|
||||||
let itemNodes = group.items.map({ $0.node(theme: self.theme) })
|
let itemNodes = group.items.map({ $0.node(theme: self.theme) })
|
||||||
@ -42,6 +105,13 @@ final class ActionSheetItemGroupsContainerNode: ASDisplayNode {
|
|||||||
node.requestLayout = { [weak self] in
|
node.requestLayout = { [weak self] in
|
||||||
self?.requestLayout?()
|
self?.requestLayout?()
|
||||||
}
|
}
|
||||||
|
let index = i
|
||||||
|
node.highlightedUpdated = { [weak self] highlighted in
|
||||||
|
if highlighted {
|
||||||
|
self?.highlightedItemIndex = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
}
|
}
|
||||||
groupNode.updateItemNodes(itemNodes, leadingVisibleNodeCount: group.leadingVisibleNodeCount ?? 1000.0)
|
groupNode.updateItemNodes(itemNodes, leadingVisibleNodeCount: group.leadingVisibleNodeCount ?? 1000.0)
|
||||||
self.groupNodes.append(groupNode)
|
self.groupNodes.append(groupNode)
|
||||||
|
|||||||
@ -13,6 +13,8 @@ open class ActionSheetItemNode: ASDisplayNode {
|
|||||||
|
|
||||||
private var validSize: CGSize?
|
private var validSize: CGSize?
|
||||||
|
|
||||||
|
var highlightedUpdated: (Bool) -> Void = { _ in }
|
||||||
|
|
||||||
public init(theme: ActionSheetControllerTheme) {
|
public init(theme: ActionSheetControllerTheme) {
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
|
|
||||||
@ -34,6 +36,14 @@ open class ActionSheetItemNode: ASDisplayNode {
|
|||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setHighlighted(_ highlighted: Bool, animated: Bool) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func performAction() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public func updateInternalLayout(_ calculatedSize: CGSize, constrainedSize: CGSize) {
|
public func updateInternalLayout(_ calculatedSize: CGSize, constrainedSize: CGSize) {
|
||||||
self.validSize = constrainedSize
|
self.validSize = constrainedSize
|
||||||
|
|
||||||
|
|||||||
@ -18,4 +18,16 @@ open class AlertContentNode: ASDisplayNode {
|
|||||||
open func updateTheme(_ theme: AlertControllerTheme) {
|
open func updateTheme(_ theme: AlertControllerTheme) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open func performHighlightedAction() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
open func decreaseHighlightedIndex() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
open func increaseHighlightedIndex() {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,7 +71,7 @@ public final class AlertControllerTheme: Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open class AlertController: ViewController, StandalonePresentableController {
|
open class AlertController: ViewController, StandalonePresentableController, KeyShortcutResponder {
|
||||||
private var controllerNode: AlertControllerNode {
|
private var controllerNode: AlertControllerNode {
|
||||||
return self.displayNode as! AlertControllerNode
|
return self.displayNode as! AlertControllerNode
|
||||||
}
|
}
|
||||||
@ -155,4 +155,44 @@ open class AlertController: ViewController, StandalonePresentableController {
|
|||||||
self?.dismiss()
|
self?.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var keyShortcuts: [KeyShortcut] {
|
||||||
|
return [
|
||||||
|
KeyShortcut(
|
||||||
|
input: UIKeyCommand.inputEscape,
|
||||||
|
modifiers: [],
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.dismissAnimated()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
KeyShortcut(
|
||||||
|
input: "W",
|
||||||
|
modifiers: [.command],
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.dismissAnimated()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
KeyShortcut(
|
||||||
|
input: "\r",
|
||||||
|
modifiers: [],
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.controllerNode.performHighlightedAction()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
KeyShortcut(
|
||||||
|
input: UIKeyCommand.inputUpArrow,
|
||||||
|
modifiers: [],
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.controllerNode.decreaseHighlightedIndex()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
KeyShortcut(
|
||||||
|
input: UIKeyCommand.inputDownArrow,
|
||||||
|
modifiers: [],
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.controllerNode.increaseHighlightedIndex()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -83,6 +83,18 @@ final class AlertControllerNode: ASDisplayNode {
|
|||||||
self.rightDimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimmingNodeTapGesture(_:))))
|
self.rightDimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimmingNodeTapGesture(_:))))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func performHighlightedAction() {
|
||||||
|
self.contentNode.performHighlightedAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decreaseHighlightedIndex() {
|
||||||
|
self.contentNode.decreaseHighlightedIndex()
|
||||||
|
}
|
||||||
|
|
||||||
|
func increaseHighlightedIndex() {
|
||||||
|
self.contentNode.increaseHighlightedIndex()
|
||||||
|
}
|
||||||
|
|
||||||
func updateTheme(_ theme: AlertControllerTheme) {
|
func updateTheme(_ theme: AlertControllerTheme) {
|
||||||
if let effectView = self.effectNode.view as? UIVisualEffectView {
|
if let effectView = self.effectNode.view as? UIVisualEffectView {
|
||||||
effectView.effect = UIBlurEffect(style: theme.backgroundType == .light ? .light : .dark)
|
effectView.effect = UIBlurEffect(style: theme.backgroundType == .light ? .light : .dark)
|
||||||
|
|||||||
@ -32,7 +32,7 @@ open class HighlightTrackingButtonNode: ASButtonNode {
|
|||||||
public var highligthedChanged: (Bool) -> Void = { _ in }
|
public var highligthedChanged: (Bool) -> Void = { _ in }
|
||||||
|
|
||||||
private let pointerStyle: PointerStyle?
|
private let pointerStyle: PointerStyle?
|
||||||
private var pointerInteraction: PointerInteraction?
|
public var pointerInteraction: PointerInteraction?
|
||||||
|
|
||||||
public init(pointerStyle: PointerStyle? = nil) {
|
public init(pointerStyle: PointerStyle? = nil) {
|
||||||
self.pointerStyle = pointerStyle
|
self.pointerStyle = pointerStyle
|
||||||
|
|||||||
@ -31,15 +31,11 @@ extension UIKeyModifierFlags: Hashable {
|
|||||||
|
|
||||||
extension KeyShortcut {
|
extension KeyShortcut {
|
||||||
var uiKeyCommand: UIKeyCommand {
|
var uiKeyCommand: UIKeyCommand {
|
||||||
if #available(iOSApplicationExtension 9.0, iOS 9.0, *), !self.title.isEmpty {
|
let command = UIKeyCommand(input: self.input, modifierFlags: self.modifiers, action: #selector(KeyShortcutsController.handleKeyCommand(_:)), discoverabilityTitle: self.title)
|
||||||
let command = UIKeyCommand(input: self.input, modifierFlags: self.modifiers, action: #selector(KeyShortcutsController.handleKeyCommand(_:)), discoverabilityTitle: self.title)
|
if #available(iOS 15.0, *), ["\t", UIKeyCommand.inputUpArrow, UIKeyCommand.inputDownArrow, UIKeyCommand.inputLeftArrow, UIKeyCommand.inputRightArrow].contains(command.input) && self.modifiers.isEmpty {
|
||||||
if #available(iOS 15.0, *), ["\t", UIKeyCommand.inputUpArrow].contains(command.input) {
|
command.wantsPriorityOverSystemBehavior = true
|
||||||
command.wantsPriorityOverSystemBehavior = true
|
|
||||||
}
|
|
||||||
return command
|
|
||||||
} else {
|
|
||||||
return UIKeyCommand(input: self.input, modifierFlags: self.modifiers, action: #selector(KeyShortcutsController.handleKeyCommand(_:)))
|
|
||||||
}
|
}
|
||||||
|
return command
|
||||||
}
|
}
|
||||||
|
|
||||||
func isEqual(to command: UIKeyCommand) -> Bool {
|
func isEqual(to command: UIKeyCommand) -> Bool {
|
||||||
|
|||||||
@ -117,6 +117,8 @@ public final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelega
|
|||||||
var statusBarStyle: StatusBarStyle = .Ignore
|
var statusBarStyle: StatusBarStyle = .Ignore
|
||||||
var statusBarStyleUpdated: ((ContainedViewLayoutTransition) -> Void)?
|
var statusBarStyleUpdated: ((ContainedViewLayoutTransition) -> Void)?
|
||||||
|
|
||||||
|
private var panRecognizer: InteractiveTransitionGestureRecognizer?
|
||||||
|
|
||||||
public init(controllerRemoved: @escaping (ViewController) -> Void) {
|
public init(controllerRemoved: @escaping (ViewController) -> Void) {
|
||||||
self.controllerRemoved = controllerRemoved
|
self.controllerRemoved = controllerRemoved
|
||||||
|
|
||||||
@ -132,9 +134,13 @@ public final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelega
|
|||||||
}
|
}
|
||||||
return .right
|
return .right
|
||||||
})
|
})
|
||||||
|
if #available(iOS 13.4, *) {
|
||||||
|
panRecognizer.allowedScrollTypesMask = .continuous
|
||||||
|
}
|
||||||
panRecognizer.delegate = self
|
panRecognizer.delegate = self
|
||||||
panRecognizer.delaysTouchesBegan = false
|
panRecognizer.delaysTouchesBegan = false
|
||||||
panRecognizer.cancelsTouchesInView = true
|
panRecognizer.cancelsTouchesInView = true
|
||||||
|
self.panRecognizer = panRecognizer
|
||||||
self.view.addGestureRecognizer(panRecognizer)
|
self.view.addGestureRecognizer(panRecognizer)
|
||||||
|
|
||||||
/*self.view.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in
|
/*self.view.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in
|
||||||
@ -155,6 +161,24 @@ public final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelega
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
|
if gestureRecognizer == self.panRecognizer, let gestureRecognizer = self.panRecognizer, gestureRecognizer.numberOfTouches == 0 {
|
||||||
|
let translation = gestureRecognizer.velocity(in: gestureRecognizer.view)
|
||||||
|
if abs(translation.y) > 4.0 && abs(translation.y) > abs(translation.x) * 2.5 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if translation.x < 4.0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if self.controllers.count == 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@ -97,6 +97,9 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes
|
|||||||
}
|
}
|
||||||
return .right
|
return .right
|
||||||
})
|
})
|
||||||
|
if #available(iOS 13.4, *) {
|
||||||
|
panRecognizer.allowedScrollTypesMask = .continuous
|
||||||
|
}
|
||||||
self.panRecognizer = panRecognizer
|
self.panRecognizer = panRecognizer
|
||||||
if let layout = self.validLayout {
|
if let layout = self.validLayout {
|
||||||
switch layout.metrics.widthClass {
|
switch layout.metrics.widthClass {
|
||||||
@ -115,6 +118,24 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
|
if gestureRecognizer == self.panRecognizer, let gestureRecognizer = self.panRecognizer, gestureRecognizer.numberOfTouches == 0 {
|
||||||
|
let translation = gestureRecognizer.velocity(in: gestureRecognizer.view)
|
||||||
|
if abs(translation.y) > 4.0 && abs(translation.y) > abs(translation.x) * 2.5 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if translation.x < 4.0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if self.isDismissed {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@ -99,6 +99,7 @@ private final class NavigationButtonItemNode: ImmediateTextNode {
|
|||||||
self.addSubnode(node)
|
self.addSubnode(node)
|
||||||
self.invalidateCalculatedLayout()
|
self.invalidateCalculatedLayout()
|
||||||
self.setNeedsLayout()
|
self.setNeedsLayout()
|
||||||
|
self.updatePointerInteraction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -210,7 +211,7 @@ private final class NavigationButtonItemNode: ImmediateTextNode {
|
|||||||
if self.node != nil {
|
if self.node != nil {
|
||||||
pointerStyle = .lift
|
pointerStyle = .lift
|
||||||
} else {
|
} else {
|
||||||
pointerStyle = .default
|
pointerStyle = .insetRectangle(-8.0, 2.0)
|
||||||
}
|
}
|
||||||
self.pointerInteraction = PointerInteraction(node: self, style: pointerStyle)
|
self.pointerInteraction = PointerInteraction(node: self, style: pointerStyle)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,9 @@ import AsyncDisplayKit
|
|||||||
|
|
||||||
public enum PointerStyle {
|
public enum PointerStyle {
|
||||||
case `default`
|
case `default`
|
||||||
|
case insetRectangle(CGFloat, CGFloat)
|
||||||
case rectangle(CGSize)
|
case rectangle(CGSize)
|
||||||
case circle
|
case circle(CGFloat?)
|
||||||
case caret
|
case caret
|
||||||
case lift
|
case lift
|
||||||
case hover
|
case hover
|
||||||
@ -12,7 +13,8 @@ public enum PointerStyle {
|
|||||||
|
|
||||||
@available(iOSApplicationExtension 13.4, iOS 13.4, *)
|
@available(iOSApplicationExtension 13.4, iOS 13.4, *)
|
||||||
private final class PointerInteractionImpl: NSObject, UIPointerInteractionDelegate {
|
private final class PointerInteractionImpl: NSObject, UIPointerInteractionDelegate {
|
||||||
weak var pointerInteraction: UIPointerInteraction?
|
private weak var pointerInteraction: UIPointerInteraction?
|
||||||
|
private weak var customInteractionView: UIView?
|
||||||
|
|
||||||
private let style: PointerStyle
|
private let style: PointerStyle
|
||||||
|
|
||||||
@ -33,7 +35,9 @@ private final class PointerInteractionImpl: NSObject, UIPointerInteractionDelega
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setup(view: UIView) {
|
func setup(view: UIView, customInteractionView: UIView?) {
|
||||||
|
self.customInteractionView = customInteractionView
|
||||||
|
|
||||||
let pointerInteraction = UIPointerInteraction(delegate: self)
|
let pointerInteraction = UIPointerInteraction(delegate: self)
|
||||||
view.addInteraction(pointerInteraction)
|
view.addInteraction(pointerInteraction)
|
||||||
self.pointerInteraction = pointerInteraction
|
self.pointerInteraction = pointerInteraction
|
||||||
@ -41,7 +45,10 @@ private final class PointerInteractionImpl: NSObject, UIPointerInteractionDelega
|
|||||||
|
|
||||||
func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? {
|
func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? {
|
||||||
var pointerStyle: UIPointerStyle? = nil
|
var pointerStyle: UIPointerStyle? = nil
|
||||||
if let interactionView = interaction.view {
|
|
||||||
|
let interactionView = self.customInteractionView ?? interaction.view
|
||||||
|
|
||||||
|
if let interactionView = interactionView {
|
||||||
let targetedPreview = UITargetedPreview(view: interactionView)
|
let targetedPreview = UITargetedPreview(view: interactionView)
|
||||||
switch self.style {
|
switch self.style {
|
||||||
case .default:
|
case .default:
|
||||||
@ -50,11 +57,15 @@ private final class PointerInteractionImpl: NSObject, UIPointerInteractionDelega
|
|||||||
let minHeight: CGFloat = 40.0
|
let minHeight: CGFloat = 40.0
|
||||||
let size: CGSize = CGSize(width: targetedPreview.size.width + horizontalPadding * 2.0, height: max(minHeight, targetedPreview.size.height + verticalPadding * 2.0))
|
let size: CGSize = CGSize(width: targetedPreview.size.width + horizontalPadding * 2.0, height: max(minHeight, targetedPreview.size.height + verticalPadding * 2.0))
|
||||||
pointerStyle = UIPointerStyle(effect: .highlight(targetedPreview), shape: .roundedRect(CGRect(origin: CGPoint(x: targetedPreview.view.center.x - size.width / 2.0, y: targetedPreview.view.center.y - size.height / 2.0), size: size), radius: UIPointerShape.defaultCornerRadius))
|
pointerStyle = UIPointerStyle(effect: .highlight(targetedPreview), shape: .roundedRect(CGRect(origin: CGPoint(x: targetedPreview.view.center.x - size.width / 2.0, y: targetedPreview.view.center.y - size.height / 2.0), size: size), radius: UIPointerShape.defaultCornerRadius))
|
||||||
|
case let .insetRectangle(x, y):
|
||||||
|
let insetSize = CGSize(width: targetedPreview.size.width - x * 2.0, height: targetedPreview.size.height - y * 2.0)
|
||||||
|
pointerStyle = UIPointerStyle(effect: .highlight(targetedPreview), shape: .roundedRect(CGRect(origin: CGPoint(x: targetedPreview.view.center.x - insetSize.width / 2.0, y: targetedPreview.view.center.y - insetSize.height / 2.0), size: insetSize), radius: UIPointerShape.defaultCornerRadius))
|
||||||
case let .rectangle(size):
|
case let .rectangle(size):
|
||||||
pointerStyle = UIPointerStyle(effect: .highlight(targetedPreview), shape: .roundedRect(CGRect(origin: CGPoint(x: targetedPreview.view.center.x - size.width / 2.0, y: targetedPreview.view.center.y - size.height / 2.0), size: size), radius: UIPointerShape.defaultCornerRadius))
|
pointerStyle = UIPointerStyle(effect: .highlight(targetedPreview), shape: .roundedRect(CGRect(origin: CGPoint(x: targetedPreview.view.center.x - size.width / 2.0, y: targetedPreview.view.center.y - size.height / 2.0), size: size), radius: UIPointerShape.defaultCornerRadius))
|
||||||
case .circle:
|
case let .circle(diameter):
|
||||||
let maxSide = max(targetedPreview.size.width, targetedPreview.size.height)
|
let maxSide = max(targetedPreview.size.width, targetedPreview.size.height)
|
||||||
pointerStyle = UIPointerStyle(effect: .highlight(targetedPreview), shape: .path(UIBezierPath(ovalIn: CGRect(origin: CGPoint(), size: CGSize(width: maxSide, height: maxSide)))))
|
let finalDiameter = diameter ?? maxSide
|
||||||
|
pointerStyle = UIPointerStyle(effect: .highlight(targetedPreview), shape: .path(UIBezierPath(ovalIn: CGRect(origin: CGPoint(x: floorToScreenPixels(targetedPreview.view.center.x - finalDiameter / 2.0), y: floorToScreenPixels(targetedPreview.view.center.y - finalDiameter / 2.0)), size: CGSize(width: finalDiameter, height: finalDiameter)))))
|
||||||
case .caret:
|
case .caret:
|
||||||
pointerStyle = UIPointerStyle(shape: .verticalBeam(length: 24.0), constrainedAxes: .vertical)
|
pointerStyle = UIPointerStyle(shape: .verticalBeam(length: 24.0), constrainedAxes: .vertical)
|
||||||
case .lift:
|
case .lift:
|
||||||
@ -106,13 +117,13 @@ public final class PointerInteraction {
|
|||||||
self.init(view: node.view, style: style, willEnter: willEnter, willExit: willExit)
|
self.init(view: node.view, style: style, willEnter: willEnter, willExit: willExit)
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(view: UIView, style: PointerStyle = .default, willEnter: @escaping () -> Void = {}, willExit: @escaping () -> Void = {}) {
|
public init(view: UIView, customInteractionView: UIView? = nil, style: PointerStyle = .default, willEnter: @escaping () -> Void = {}, willExit: @escaping () -> Void = {}) {
|
||||||
self.style = style
|
self.style = style
|
||||||
self.willEnter = willEnter
|
self.willEnter = willEnter
|
||||||
self.willExit = willExit
|
self.willExit = willExit
|
||||||
if #available(iOSApplicationExtension 13.4, iOS 13.4, *) {
|
if #available(iOSApplicationExtension 13.4, iOS 13.4, *) {
|
||||||
self.withImpl { impl in
|
self.withImpl { impl in
|
||||||
impl.setup(view: view)
|
impl.setup(view: view, customInteractionView: customInteractionView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,8 +29,8 @@ public final class TextAlertContentActionNode: HighlightableButtonNode {
|
|||||||
|
|
||||||
private let backgroundNode: ASDisplayNode
|
private let backgroundNode: ASDisplayNode
|
||||||
|
|
||||||
private var pointerInteraction: PointerInteraction?
|
var highlightedUpdated: (Bool) -> Void = { _ in }
|
||||||
|
|
||||||
public init(theme: AlertControllerTheme, action: TextAlertAction) {
|
public init(theme: AlertControllerTheme, action: TextAlertAction) {
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.action = action
|
self.action = action
|
||||||
@ -45,16 +45,7 @@ public final class TextAlertContentActionNode: HighlightableButtonNode {
|
|||||||
|
|
||||||
self.highligthedChanged = { [weak self] value in
|
self.highligthedChanged = { [weak self] value in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
if value {
|
strongSelf.setHighlighted(value, animated: true)
|
||||||
if strongSelf.backgroundNode.supernode == nil {
|
|
||||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
|
||||||
}
|
|
||||||
strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity")
|
|
||||||
strongSelf.backgroundNode.alpha = 1.0
|
|
||||||
} else if !strongSelf.backgroundNode.alpha.isZero {
|
|
||||||
strongSelf.backgroundNode.alpha = 0.0
|
|
||||||
strongSelf.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,15 +59,38 @@ public final class TextAlertContentActionNode: HighlightableButtonNode {
|
|||||||
|
|
||||||
self.pointerInteraction = PointerInteraction(node: self, style: .hover, willEnter: { [weak self] in
|
self.pointerInteraction = PointerInteraction(node: self, style: .hover, willEnter: { [weak self] in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.backgroundNode.alpha = 0.25
|
strongSelf.setHighlighted(true, animated: false)
|
||||||
}
|
}
|
||||||
}, willExit: { [weak self] in
|
}, willExit: { [weak self] in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.backgroundNode.alpha = 1.0
|
strongSelf.setHighlighted(false, animated: false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func performAction() {
|
||||||
|
if self.actionEnabled {
|
||||||
|
self.action.action()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setHighlighted(_ highlighted: Bool, animated: Bool) {
|
||||||
|
self.highlightedUpdated(highlighted)
|
||||||
|
if highlighted {
|
||||||
|
if self.backgroundNode.supernode == nil {
|
||||||
|
self.insertSubnode(self.backgroundNode, at: 0)
|
||||||
|
}
|
||||||
|
self.backgroundNode.alpha = 1.0
|
||||||
|
} else {
|
||||||
|
if animated {
|
||||||
|
UIView.animate(withDuration: 0.3, animations: {
|
||||||
|
self.backgroundNode.alpha = 0.0
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
self.backgroundNode.alpha = 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
public var actionEnabled: Bool = true {
|
public var actionEnabled: Bool = true {
|
||||||
didSet {
|
didSet {
|
||||||
self.isUserInteractionEnabled = self.actionEnabled
|
self.isUserInteractionEnabled = self.actionEnabled
|
||||||
@ -142,6 +156,8 @@ public final class TextAlertContentNode: AlertContentNode {
|
|||||||
return self._dismissOnOutsideTap
|
return self._dismissOnOutsideTap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var highlightedItemIndex: Int? = nil
|
||||||
|
|
||||||
public var textAttributeAction: (NSAttributedString.Key, (Any) -> Void)? {
|
public var textAttributeAction: (NSAttributedString.Key, (Any) -> Void)? {
|
||||||
didSet {
|
didSet {
|
||||||
if let (attribute, textAttributeAction) = self.textAttributeAction {
|
if let (attribute, textAttributeAction) = self.textAttributeAction {
|
||||||
@ -224,8 +240,17 @@ public final class TextAlertContentNode: AlertContentNode {
|
|||||||
|
|
||||||
self.addSubnode(self.actionNodesSeparator)
|
self.addSubnode(self.actionNodesSeparator)
|
||||||
|
|
||||||
|
var i = 0
|
||||||
for actionNode in self.actionNodes {
|
for actionNode in self.actionNodes {
|
||||||
self.addSubnode(actionNode)
|
self.addSubnode(actionNode)
|
||||||
|
|
||||||
|
let index = i
|
||||||
|
actionNode.highlightedUpdated = { [weak self] highlighted in
|
||||||
|
if highlighted {
|
||||||
|
self?.highlightedItemIndex = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
for separatorNode in self.actionVerticalSeparators {
|
for separatorNode in self.actionVerticalSeparators {
|
||||||
@ -233,6 +258,49 @@ public final class TextAlertContentNode: AlertContentNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setHighlightedItemIndex(_ index: Int?, update: Bool = false) {
|
||||||
|
self.highlightedItemIndex = index
|
||||||
|
|
||||||
|
if update {
|
||||||
|
var i = 0
|
||||||
|
for actionNode in self.actionNodes {
|
||||||
|
if i == index {
|
||||||
|
actionNode.setHighlighted(true, animated: false)
|
||||||
|
} else {
|
||||||
|
actionNode.setHighlighted(false, animated: false)
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func decreaseHighlightedIndex() {
|
||||||
|
let currentHighlightedIndex = self.highlightedItemIndex ?? 0
|
||||||
|
|
||||||
|
self.setHighlightedItemIndex(max(0, currentHighlightedIndex - 1), update: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func increaseHighlightedIndex() {
|
||||||
|
let currentHighlightedIndex = self.highlightedItemIndex ?? -1
|
||||||
|
|
||||||
|
self.setHighlightedItemIndex(min(self.actionNodes.count - 1, currentHighlightedIndex + 1), update: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func performHighlightedAction() {
|
||||||
|
guard let highlightedItemIndex = self.highlightedItemIndex else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var i = 0
|
||||||
|
for itemNode in self.actionNodes {
|
||||||
|
if i == highlightedItemIndex {
|
||||||
|
itemNode.performAction()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override public func updateTheme(_ theme: AlertControllerTheme) {
|
override public func updateTheme(_ theme: AlertControllerTheme) {
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
|
|
||||||
|
|||||||
@ -52,13 +52,13 @@ public final class ToolbarNode: ASDisplayNode {
|
|||||||
|
|
||||||
self.leftTitle = ImmediateTextNode()
|
self.leftTitle = ImmediateTextNode()
|
||||||
self.leftTitle.displaysAsynchronously = false
|
self.leftTitle.displaysAsynchronously = false
|
||||||
self.leftButton = HighlightTrackingButtonNode()
|
self.leftButton = HighlightTrackingButtonNode(pointerStyle: .insetRectangle(2.0, 2.0))
|
||||||
self.rightTitle = ImmediateTextNode()
|
self.rightTitle = ImmediateTextNode()
|
||||||
self.rightTitle.displaysAsynchronously = false
|
self.rightTitle.displaysAsynchronously = false
|
||||||
self.rightButton = HighlightTrackingButtonNode()
|
self.rightButton = HighlightTrackingButtonNode(pointerStyle: .insetRectangle(2.0, 2.0))
|
||||||
self.middleTitle = ImmediateTextNode()
|
self.middleTitle = ImmediateTextNode()
|
||||||
self.middleTitle.displaysAsynchronously = false
|
self.middleTitle.displaysAsynchronously = false
|
||||||
self.middleButton = HighlightTrackingButtonNode()
|
self.middleButton = HighlightTrackingButtonNode(pointerStyle: .insetRectangle(2.0, 2.0))
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
|
|||||||
@ -329,7 +329,7 @@ public struct GalleryConfiguration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GalleryController: ViewController, StandalonePresentableController {
|
public class GalleryController: ViewController, StandalonePresentableController, KeyShortcutResponder {
|
||||||
public static let darkNavigationTheme = NavigationBarTheme(buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: UIColor(white: 0.0, alpha: 0.6), enableBackgroundBlur: false, separatorColor: UIColor(white: 0.0, alpha: 0.8), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear)
|
public static let darkNavigationTheme = NavigationBarTheme(buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: UIColor(white: 0.0, alpha: 0.6), enableBackgroundBlur: false, separatorColor: UIColor(white: 0.0, alpha: 0.8), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear)
|
||||||
public static let lightNavigationTheme = NavigationBarTheme(buttonColor: UIColor(rgb: 0x007aff), disabledButtonColor: UIColor(rgb: 0xd0d0d0), primaryTextColor: .black, backgroundColor: UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0), enableBackgroundBlur: false, separatorColor: UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear)
|
public static let lightNavigationTheme = NavigationBarTheme(buttonColor: UIColor(rgb: 0x007aff), disabledButtonColor: UIColor(rgb: 0xd0d0d0), primaryTextColor: .black, backgroundColor: UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0), enableBackgroundBlur: false, separatorColor: UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear)
|
||||||
|
|
||||||
@ -1384,4 +1384,67 @@ public class GalleryController: ViewController, StandalonePresentableController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var keyShortcuts: [KeyShortcut] {
|
||||||
|
var keyShortcuts: [KeyShortcut] = []
|
||||||
|
keyShortcuts.append(
|
||||||
|
KeyShortcut(
|
||||||
|
title: "",
|
||||||
|
input: UIKeyCommand.inputUpArrow,
|
||||||
|
modifiers: [.command],
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.dismiss(forceAway: false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
keyShortcuts.append(
|
||||||
|
KeyShortcut(
|
||||||
|
title: "",
|
||||||
|
input: "W",
|
||||||
|
modifiers: [.command],
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.dismiss(forceAway: false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
keyShortcuts.append(
|
||||||
|
KeyShortcut(
|
||||||
|
title: self.galleryNode.areControlsHidden ? self.presentationData.strings.KeyCommand_ExitFullscreen : self.presentationData.strings.KeyCommand_EnterFullscreen,
|
||||||
|
input: "F",
|
||||||
|
modifiers: [.control, .command],
|
||||||
|
action: { [weak self] in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.galleryNode.setControlsHidden(!strongSelf.galleryNode.areControlsHidden, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if self.galleryNode.pager.items.count > 1 {
|
||||||
|
if self.galleryNode.pager.canGoToPreviousItem() {
|
||||||
|
keyShortcuts.append(
|
||||||
|
KeyShortcut(
|
||||||
|
input: UIKeyCommand.inputLeftArrow,
|
||||||
|
modifiers: [],
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.galleryNode.pager.goToPreviousItem()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if self.galleryNode.pager.canGoToNextItem() {
|
||||||
|
keyShortcuts.append(
|
||||||
|
KeyShortcut(
|
||||||
|
input: UIKeyCommand.inputRightArrow,
|
||||||
|
modifiers: [],
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.galleryNode.pager.goToNextItem()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let itemNodeShortcuts = self.galleryNode.pager.centralItemNode()?.keyShortcuts ?? []
|
||||||
|
keyShortcuts.append(contentsOf: itemNodeShortcuts)
|
||||||
|
return keyShortcuts
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -103,4 +103,8 @@ open class GalleryItemNode: ASDisplayNode {
|
|||||||
open func contentSize() -> CGSize? {
|
open func contentSize() -> CGSize? {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open var keyShortcuts: [KeyShortcut] {
|
||||||
|
return []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -457,7 +457,7 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate, UIGest
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func canGoToPreviousItem() -> Bool {
|
func canGoToPreviousItem() -> Bool {
|
||||||
if self.disableTapNavigation {
|
if self.disableTapNavigation {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -468,7 +468,7 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate, UIGest
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func canGoToNextItem() -> Bool {
|
func canGoToNextItem() -> Bool {
|
||||||
if self.disableTapNavigation {
|
if self.disableTapNavigation {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -479,13 +479,13 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate, UIGest
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func goToPreviousItem() {
|
func goToPreviousItem() {
|
||||||
if let index = self.centralItemIndex, index > 0 {
|
if let index = self.centralItemIndex, index > 0 {
|
||||||
self.transaction(GalleryPagerTransaction(deleteItems: [], insertItems: [], updateItems: [], focusOnItem: index - 1, synchronous: false))
|
self.transaction(GalleryPagerTransaction(deleteItems: [], insertItems: [], updateItems: [], focusOnItem: index - 1, synchronous: false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func goToNextItem() {
|
func goToNextItem() {
|
||||||
if let index = self.centralItemIndex, index < self.items.count - 1 {
|
if let index = self.centralItemIndex, index < self.items.count - 1 {
|
||||||
self.transaction(GalleryPagerTransaction(deleteItems: [], insertItems: [], updateItems: [], focusOnItem: index + 1, synchronous: false))
|
self.transaction(GalleryPagerTransaction(deleteItems: [], insertItems: [], updateItems: [], focusOnItem: index + 1, synchronous: false))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -192,6 +192,7 @@ class ChatImageGalleryItem: GalleryItem {
|
|||||||
final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
private var message: Message?
|
private var message: Message?
|
||||||
|
private let presentationData: PresentationData
|
||||||
|
|
||||||
private let imageNode: TransformImageNode
|
private let imageNode: TransformImageNode
|
||||||
private var recognizedContentNode: RecognizedContentContainer?
|
private var recognizedContentNode: RecognizedContentContainer?
|
||||||
@ -220,6 +221,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
|
|
||||||
init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) {
|
init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) {
|
||||||
self.context = context
|
self.context = context
|
||||||
|
self.presentationData = presentationData
|
||||||
|
|
||||||
self.imageNode = TransformImageNode()
|
self.imageNode = TransformImageNode()
|
||||||
self.imageNode.contentAnimations = .subsequentUpdates
|
self.imageNode.contentAnimations = .subsequentUpdates
|
||||||
@ -798,6 +800,60 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
|
|
||||||
self.recognitionOverlayContentNode.isHidden = true
|
self.recognitionOverlayContentNode.isHidden = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func canDelete() -> Bool {
|
||||||
|
guard let message = self.message else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var canDelete = false
|
||||||
|
if let peer = message.peers[message.id.peerId] {
|
||||||
|
if peer is TelegramUser || peer is TelegramSecretChat {
|
||||||
|
canDelete = true
|
||||||
|
} else if let _ = peer as? TelegramGroup {
|
||||||
|
canDelete = true
|
||||||
|
} else if let channel = peer as? TelegramChannel {
|
||||||
|
if message.flags.contains(.Incoming) {
|
||||||
|
canDelete = channel.hasPermission(.deleteAllMessages)
|
||||||
|
} else {
|
||||||
|
canDelete = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
canDelete = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
canDelete = false
|
||||||
|
}
|
||||||
|
return canDelete
|
||||||
|
}
|
||||||
|
|
||||||
|
override var keyShortcuts: [KeyShortcut] {
|
||||||
|
let strings = self.presentationData.strings
|
||||||
|
|
||||||
|
var keyShortcuts: [KeyShortcut] = []
|
||||||
|
keyShortcuts.append(
|
||||||
|
KeyShortcut(
|
||||||
|
title: strings.KeyCommand_Share,
|
||||||
|
input: "S",
|
||||||
|
modifiers: [.command],
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.footerContentNode.actionButtonPressed()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if self.canDelete() {
|
||||||
|
keyShortcuts.append(
|
||||||
|
KeyShortcut(
|
||||||
|
input: "\u{8}",
|
||||||
|
modifiers: [],
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.footerContentNode.deleteButtonPressed()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return keyShortcuts
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*private func tileRectForImage(_ mappedImage: CGImage, rect: CGRect) -> CGRect {
|
/*private func tileRectForImage(_ mappedImage: CGImage, rect: CGRect) -> CGRect {
|
||||||
|
|||||||
@ -775,6 +775,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
private var scrubbingFrames = false
|
private var scrubbingFrames = false
|
||||||
private var scrubbingFrameDisposable: Disposable?
|
private var scrubbingFrameDisposable: Disposable?
|
||||||
|
|
||||||
|
private var isPlaying = false
|
||||||
private let isPlayingPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
|
private let isPlayingPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||||||
private let isInteractingPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
|
private let isInteractingPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||||||
private let controlsVisiblePromise = ValuePromise<Bool>(true, ignoreRepeated: true)
|
private let controlsVisiblePromise = ValuePromise<Bool>(true, ignoreRepeated: true)
|
||||||
@ -1066,6 +1067,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
}
|
}
|
||||||
self.footerContentNode.scrubberView = scrubberView
|
self.footerContentNode.scrubberView = scrubberView
|
||||||
|
|
||||||
|
self.isPlaying = false
|
||||||
self.isPlayingPromise.set(false)
|
self.isPlayingPromise.set(false)
|
||||||
|
|
||||||
if item.hideControls {
|
if item.hideControls {
|
||||||
@ -1346,6 +1348,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
}
|
}
|
||||||
} else if strongSelf.actionAtEnd == .stop {
|
} else if strongSelf.actionAtEnd == .stop {
|
||||||
strongSelf.isPlayingPromise.set(false)
|
strongSelf.isPlayingPromise.set(false)
|
||||||
|
strongSelf.isPlaying = false
|
||||||
if strongSelf.isCentral == true {
|
if strongSelf.isCentral == true {
|
||||||
strongSelf.updateControlsVisibility(true)
|
strongSelf.updateControlsVisibility(true)
|
||||||
}
|
}
|
||||||
@ -1358,8 +1361,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
|
|
||||||
if !disablePlayerControls && strongSelf.isCentral == true && isPlaying {
|
if !disablePlayerControls && strongSelf.isCentral == true && isPlaying {
|
||||||
strongSelf.isPlayingPromise.set(true)
|
strongSelf.isPlayingPromise.set(true)
|
||||||
|
strongSelf.isPlaying = true
|
||||||
} else if !isPlaying {
|
} else if !isPlaying {
|
||||||
strongSelf.isPlayingPromise.set(false)
|
strongSelf.isPlayingPromise.set(false)
|
||||||
|
strongSelf.isPlaying = false
|
||||||
}
|
}
|
||||||
|
|
||||||
var fetching = false
|
var fetching = false
|
||||||
@ -1471,6 +1476,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
|
|
||||||
if strongSelf.actionAtEnd == .stop && strongSelf.isCentral == true {
|
if strongSelf.actionAtEnd == .stop && strongSelf.isCentral == true {
|
||||||
strongSelf.isPlayingPromise.set(false)
|
strongSelf.isPlayingPromise.set(false)
|
||||||
|
strongSelf.isPlaying = false
|
||||||
strongSelf.updateControlsVisibility(true)
|
strongSelf.updateControlsVisibility(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1591,6 +1597,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.isPlayingPromise.set(false)
|
self.isPlayingPromise.set(false)
|
||||||
|
self.isPlaying = false
|
||||||
|
|
||||||
self.dismissOnOrientationChange = false
|
self.dismissOnOrientationChange = false
|
||||||
if videoNode.ownsContentNode {
|
if videoNode.ownsContentNode {
|
||||||
@ -2710,6 +2717,78 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
|
|
||||||
self.playbackRatePromise.set(self.playbackRate ?? 1.0)
|
self.playbackRatePromise.set(self.playbackRate ?? 1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override var keyShortcuts: [KeyShortcut] {
|
||||||
|
let strings = self.presentationData.strings
|
||||||
|
|
||||||
|
var keyShortcuts: [KeyShortcut] = []
|
||||||
|
keyShortcuts.append(
|
||||||
|
KeyShortcut(
|
||||||
|
title: self.isPlaying ? strings.KeyCommand_Pause : strings.KeyCommand_Play,
|
||||||
|
input: " ",
|
||||||
|
modifiers: [],
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.footerContentNode.playbackControl?()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
keyShortcuts.append(
|
||||||
|
KeyShortcut(
|
||||||
|
title: strings.KeyCommand_SeekBackward,
|
||||||
|
input: UIKeyCommand.inputLeftArrow,
|
||||||
|
modifiers: [.shift],
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.footerContentNode.seekBackward?(5)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
keyShortcuts.append(
|
||||||
|
KeyShortcut(
|
||||||
|
title: strings.KeyCommand_SeekForward,
|
||||||
|
input: UIKeyCommand.inputRightArrow,
|
||||||
|
modifiers: [.shift],
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.footerContentNode.seekForward?(5)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
keyShortcuts.append(
|
||||||
|
KeyShortcut(
|
||||||
|
title: strings.KeyCommand_Share,
|
||||||
|
input: "S",
|
||||||
|
modifiers: [.command],
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.footerContentNode.actionButtonPressed()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if self.hasPictureInPicture {
|
||||||
|
keyShortcuts.append(
|
||||||
|
KeyShortcut(
|
||||||
|
title: strings.KeyCommand_SwitchToPIP,
|
||||||
|
input: "P",
|
||||||
|
modifiers: [.command],
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.pictureInPictureButtonPressed()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if self.canDelete() {
|
||||||
|
keyShortcuts.append(
|
||||||
|
KeyShortcut(
|
||||||
|
input: "\u{8}",
|
||||||
|
modifiers: [],
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.footerContentNode.deleteButtonPressed()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return keyShortcuts
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class HeaderContextReferenceContentSource: ContextReferenceContentSource {
|
private final class HeaderContextReferenceContentSource: ContextReferenceContentSource {
|
||||||
|
|||||||
@ -197,11 +197,18 @@ class PremiumStarComponent: Component {
|
|||||||
|
|
||||||
self.previousInteractionTimestamp = CACurrentMediaTime()
|
self.previousInteractionTimestamp = CACurrentMediaTime()
|
||||||
|
|
||||||
|
let keys = [
|
||||||
|
"rotate",
|
||||||
|
"tapRotate"
|
||||||
|
]
|
||||||
if #available(iOS 11.0, *) {
|
if #available(iOS 11.0, *) {
|
||||||
node.removeAnimation(forKey: "rotate", blendOutDuration: 0.1)
|
for key in keys {
|
||||||
node.removeAnimation(forKey: "tapRotate", blendOutDuration: 0.1)
|
node.removeAnimation(forKey: key, blendOutDuration: 0.1)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
node.removeAllAnimations()
|
for key in keys {
|
||||||
|
node.removeAnimation(forKey: key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch gesture.state {
|
switch gesture.state {
|
||||||
@ -337,6 +344,7 @@ class PremiumStarComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func onReady() {
|
private func onReady() {
|
||||||
|
self.setupScaleAnimation()
|
||||||
self.setupGradientAnimation()
|
self.setupGradientAnimation()
|
||||||
self.setupShineAnimation()
|
self.setupShineAnimation()
|
||||||
|
|
||||||
@ -355,6 +363,22 @@ class PremiumStarComponent: Component {
|
|||||||
self.timer?.start()
|
self.timer?.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func setupScaleAnimation() {
|
||||||
|
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let animation = CABasicAnimation(keyPath: "scale")
|
||||||
|
animation.duration = 2.0
|
||||||
|
animation.fromValue = NSValue(scnVector3: SCNVector3(x: 0.1, y: 0.1, z: 0.1))
|
||||||
|
animation.toValue = NSValue(scnVector3: SCNVector3(x: 0.115, y: 0.115, z: 0.115))
|
||||||
|
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
||||||
|
animation.autoreverses = true
|
||||||
|
animation.repeatCount = .infinity
|
||||||
|
|
||||||
|
node.addAnimation(animation, forKey: "scale")
|
||||||
|
}
|
||||||
|
|
||||||
private func setupGradientAnimation() {
|
private func setupGradientAnimation() {
|
||||||
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
|
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
|
||||||
return
|
return
|
||||||
|
|||||||
@ -125,6 +125,8 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg
|
|||||||
if let layout = self.validLayout {
|
if let layout = self.validLayout {
|
||||||
let _ = self.updateLayout(layout, transition: .immediate)
|
let _ = self.updateLayout(layout, transition: .immediate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.updatePointerInteraction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,6 +142,8 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg
|
|||||||
if let layout = self.validLayout {
|
if let layout = self.validLayout {
|
||||||
let _ = self.updateLayout(layout, transition: .immediate)
|
let _ = self.updateLayout(layout, transition: .immediate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.updatePointerInteraction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,6 +155,8 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg
|
|||||||
if let layout = self.validLayout {
|
if let layout = self.validLayout {
|
||||||
let _ = self.updateLayout(layout, transition: .animated(duration: 0.2, curve: .easeInOut))
|
let _ = self.updateLayout(layout, transition: .animated(duration: 0.2, curve: .easeInOut))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.updatePointerInteraction()
|
||||||
}
|
}
|
||||||
|
|
||||||
public var selectedIndexChanged: (Int) -> Void = { _ in }
|
public var selectedIndexChanged: (Int) -> Void = { _ in }
|
||||||
@ -212,6 +218,18 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg
|
|||||||
self.gestureRecognizer = gestureRecognizer
|
self.gestureRecognizer = gestureRecognizer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func updatePointerInteraction() {
|
||||||
|
for i in 0 ..< self.itemNodes.count {
|
||||||
|
let itemNode = self.itemNodes[i]
|
||||||
|
|
||||||
|
if i == self.selectedIndex {
|
||||||
|
itemNode.pointerInteraction = PointerInteraction(view: itemNode.view, customInteractionView: self.selectionNode.view, style: .lift)
|
||||||
|
} else {
|
||||||
|
itemNode.pointerInteraction = PointerInteraction(view: itemNode.view, style: .insetRectangle(2.0, 2.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func setupButtons() {
|
private func setupButtons() {
|
||||||
for i in 0 ..< self.itemNodes.count {
|
for i in 0 ..< self.itemNodes.count {
|
||||||
let itemNode = self.itemNodes[i]
|
let itemNode = self.itemNodes[i]
|
||||||
@ -233,6 +251,8 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.updatePointerInteraction()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateButtonsHighlights(highlightedIndex: Int?, gestureSelectedIndex: Int?) {
|
private func updateButtonsHighlights(highlightedIndex: Int?, gestureSelectedIndex: Int?) {
|
||||||
@ -379,6 +399,8 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg
|
|||||||
if let layout = strongSelf.validLayout {
|
if let layout = strongSelf.validLayout {
|
||||||
let _ = strongSelf.updateLayout(layout, transition: .animated(duration: 0.2, curve: .slide))
|
let _ = strongSelf.updateLayout(layout, transition: .animated(duration: 0.2, curve: .slide))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
strongSelf.updatePointerInteraction()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -415,6 +437,8 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg
|
|||||||
if commit {
|
if commit {
|
||||||
strongSelf._selectedIndex = gestureSelectedIndex
|
strongSelf._selectedIndex = gestureSelectedIndex
|
||||||
strongSelf.selectedIndexChanged(gestureSelectedIndex)
|
strongSelf.selectedIndexChanged(gestureSelectedIndex)
|
||||||
|
|
||||||
|
strongSelf.updatePointerInteraction()
|
||||||
} else {
|
} else {
|
||||||
if let layout = strongSelf.validLayout {
|
if let layout = strongSelf.validLayout {
|
||||||
let _ = strongSelf.updateLayout(layout, transition: .animated(duration: 0.2, curve: .slide))
|
let _ = strongSelf.updateLayout(layout, transition: .animated(duration: 0.2, curve: .slide))
|
||||||
|
|||||||
@ -1325,8 +1325,9 @@ public final class SparseItemGrid: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(theme: PresentationTheme) {
|
public init(theme: PresentationTheme, initialZoomLevel: ZoomLevel? = nil) {
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
|
self.initialZoomLevel = initialZoomLevel
|
||||||
|
|
||||||
self.scrollingArea = SparseItemGridScrollingArea()
|
self.scrollingArea = SparseItemGridScrollingArea()
|
||||||
|
|
||||||
|
|||||||
@ -226,6 +226,11 @@ final class AuthorizedApplicationContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
strongSelf.mainWindow.forEachViewController(f)
|
strongSelf.mainWindow.forEachViewController(f)
|
||||||
|
if let globalOverlayController = strongSelf.rootController.globalOverlayControllers.last {
|
||||||
|
if !f(globalOverlayController) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15841,6 +15841,27 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
KeyShortcut(input: "U", modifiers: [.command], action: { [weak self] in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
|
||||||
|
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.underline), inputMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
KeyShortcut(input: "X", modifiers: [.command, .shift], action: { [weak self] in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
|
||||||
|
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.strikethrough), inputMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
KeyShortcut(input: "P", modifiers: [.command, .shift], action: { [weak self] in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
|
||||||
|
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.spoiler), inputMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
KeyShortcut(input: "K", modifiers: [.command], action: { [weak self] in
|
KeyShortcut(input: "K", modifiers: [.command], action: { [weak self] in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.interfaceInteraction?.openLinkEditing()
|
strongSelf.interfaceInteraction?.openLinkEditing()
|
||||||
@ -15912,11 +15933,107 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
inputShortcuts = []
|
inputShortcuts = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inputShortcuts.append(
|
||||||
|
KeyShortcut(
|
||||||
|
input: "W",
|
||||||
|
modifiers: [.command],
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.dismiss(animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if canReplyInChat(self.presentationInterfaceState) {
|
||||||
|
inputShortcuts.append(
|
||||||
|
KeyShortcut(
|
||||||
|
input: UIKeyCommand.inputUpArrow,
|
||||||
|
modifiers: [.alternate, .command],
|
||||||
|
action: { [weak self] in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId {
|
||||||
|
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(before: replyMessageId) {
|
||||||
|
strongSelf.updateChatPresentationInterfaceState(interactive: true, { state in
|
||||||
|
var updatedState = state.updatedInterfaceState({ state in
|
||||||
|
return state.withUpdatedReplyMessageId(message.id)
|
||||||
|
})
|
||||||
|
if updatedState.inputMode == .none {
|
||||||
|
updatedState = updatedState.updatedInputMode({ _ in .text })
|
||||||
|
}
|
||||||
|
return updatedState
|
||||||
|
})
|
||||||
|
|
||||||
|
strongSelf.navigateToMessage(messageLocation: .id(message.id, nil), animated: true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
strongSelf.scrollToEndOfHistory()
|
||||||
|
Queue.mainQueue().after(0.1, {
|
||||||
|
let lastMessage = strongSelf.chatDisplayNode.historyNode.latestMessageInCurrentHistoryView()
|
||||||
|
strongSelf.updateChatPresentationInterfaceState(interactive: true, { state in
|
||||||
|
var updatedState = state.updatedInterfaceState({ state in
|
||||||
|
return state.withUpdatedReplyMessageId(lastMessage?.id)
|
||||||
|
})
|
||||||
|
if updatedState.inputMode == .none {
|
||||||
|
updatedState = updatedState.updatedInputMode({ _ in .text })
|
||||||
|
}
|
||||||
|
return updatedState
|
||||||
|
})
|
||||||
|
|
||||||
|
if let lastMessage = lastMessage {
|
||||||
|
strongSelf.navigateToMessage(messageLocation: .id(lastMessage.id, nil), animated: true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
inputShortcuts.append(
|
||||||
|
KeyShortcut(
|
||||||
|
input: UIKeyCommand.inputDownArrow,
|
||||||
|
modifiers: [.alternate, .command],
|
||||||
|
action: { [weak self] in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId {
|
||||||
|
let lastMessage = strongSelf.chatDisplayNode.historyNode.latestMessageInCurrentHistoryView()
|
||||||
|
var updatedReplyMessageId: MessageId?
|
||||||
|
if replyMessageId == lastMessage?.id {
|
||||||
|
updatedReplyMessageId = nil
|
||||||
|
} else if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(after: replyMessageId) {
|
||||||
|
updatedReplyMessageId = message.id
|
||||||
|
}
|
||||||
|
|
||||||
|
strongSelf.updateChatPresentationInterfaceState(interactive: true, { state in
|
||||||
|
var updatedState = state.updatedInterfaceState({ state in
|
||||||
|
return state.withUpdatedReplyMessageId(updatedReplyMessageId)
|
||||||
|
})
|
||||||
|
if updatedState.inputMode == .none {
|
||||||
|
updatedState = updatedState.updatedInputMode({ _ in .text })
|
||||||
|
} else if updatedReplyMessageId == nil {
|
||||||
|
updatedState = updatedState.updatedInputMode({ _ in .none })
|
||||||
|
}
|
||||||
|
return updatedState
|
||||||
|
})
|
||||||
|
|
||||||
|
if let updatedReplyMessageId = updatedReplyMessageId {
|
||||||
|
strongSelf.navigateToMessage(messageLocation: .id(updatedReplyMessageId, nil), animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
var canEdit = false
|
var canEdit = false
|
||||||
if self.presentationInterfaceState.interfaceState.effectiveInputState.inputText.length == 0 && self.presentationInterfaceState.interfaceState.editMessage == nil {
|
if self.presentationInterfaceState.interfaceState.effectiveInputState.inputText.length == 0 && self.presentationInterfaceState.interfaceState.editMessage == nil {
|
||||||
canEdit = true
|
canEdit = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if canEdit, let message = self.chatDisplayNode.historyNode.firstMessageForEditInCurrentHistoryView() {
|
if canEdit, let message = self.chatDisplayNode.historyNode.firstMessageForEditInCurrentHistoryView() {
|
||||||
inputShortcuts.append(KeyShortcut(input: UIKeyCommand.inputUpArrow, action: { [weak self] in
|
inputShortcuts.append(KeyShortcut(input: UIKeyCommand.inputUpArrow, action: { [weak self] in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
|
|||||||
@ -88,6 +88,23 @@ enum ChatHistoryEntry: Identifiable, Comparable {
|
|||||||
return MessageIndex.absoluteLowerBound()
|
return MessageIndex.absoluteLowerBound()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var firstIndex: MessageIndex {
|
||||||
|
switch self {
|
||||||
|
case let .MessageEntry(message, _, _, _, _, _):
|
||||||
|
return message.index
|
||||||
|
case let .MessageGroupEntry(_, messages, _):
|
||||||
|
return messages[0].0.index
|
||||||
|
case let .UnreadEntry(index, _):
|
||||||
|
return index
|
||||||
|
case let .ReplyCountEntry(index, _, _, _):
|
||||||
|
return index
|
||||||
|
case .ChatInfoEntry:
|
||||||
|
return MessageIndex.absoluteLowerBound()
|
||||||
|
case .SearchEntry:
|
||||||
|
return MessageIndex.absoluteLowerBound()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static func ==(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool {
|
static func ==(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool {
|
||||||
switch lhs {
|
switch lhs {
|
||||||
|
|||||||
@ -2195,6 +2195,34 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func messageInCurrentHistoryView(after messageId: MessageId) -> Message? {
|
||||||
|
if let historyView = self.historyView {
|
||||||
|
if let index = historyView.filteredEntries.firstIndex(where: { $0.firstIndex.id == messageId }), index < historyView.filteredEntries.count - 1 {
|
||||||
|
let nextEntry = historyView.filteredEntries[index + 1]
|
||||||
|
if case let .MessageEntry(message, _, _, _, _, _) = nextEntry {
|
||||||
|
return message
|
||||||
|
} else if case let .MessageGroupEntry(_, messages, _) = nextEntry, let firstMessage = messages.first {
|
||||||
|
return firstMessage.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public func messageInCurrentHistoryView(before messageId: MessageId) -> Message? {
|
||||||
|
if let historyView = self.historyView {
|
||||||
|
if let index = historyView.filteredEntries.firstIndex(where: { $0.firstIndex.id == messageId }), index > 0 {
|
||||||
|
let nextEntry = historyView.filteredEntries[index - 1]
|
||||||
|
if case let .MessageEntry(message, _, _, _, _, _) = nextEntry {
|
||||||
|
return message
|
||||||
|
} else if case let .MessageGroupEntry(_, messages, _) = nextEntry, let firstMessage = messages.first {
|
||||||
|
return firstMessage.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
public func messageInCurrentHistoryView(_ id: MessageId) -> Message? {
|
public func messageInCurrentHistoryView(_ id: MessageId) -> Message? {
|
||||||
if let historyView = self.historyView {
|
if let historyView = self.historyView {
|
||||||
for entry in historyView.filteredEntries {
|
for entry in historyView.filteredEntries {
|
||||||
|
|||||||
@ -56,21 +56,21 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
|
|||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.peerMedia = peerMedia
|
self.peerMedia = peerMedia
|
||||||
|
|
||||||
self.deleteButton = HighlightableButtonNode(pointerStyle: .default)
|
self.deleteButton = HighlightableButtonNode(pointerStyle: .rectangle(CGSize(width: 56.0, height: 40.0)))
|
||||||
self.deleteButton.isEnabled = false
|
self.deleteButton.isEnabled = false
|
||||||
self.deleteButton.isAccessibilityElement = true
|
self.deleteButton.isAccessibilityElement = true
|
||||||
self.deleteButton.accessibilityLabel = strings.VoiceOver_MessageContextDelete
|
self.deleteButton.accessibilityLabel = strings.VoiceOver_MessageContextDelete
|
||||||
|
|
||||||
self.reportButton = HighlightableButtonNode(pointerStyle: .default)
|
self.reportButton = HighlightableButtonNode(pointerStyle: .rectangle(CGSize(width: 56.0, height: 40.0)))
|
||||||
self.reportButton.isEnabled = false
|
self.reportButton.isEnabled = false
|
||||||
self.reportButton.isAccessibilityElement = true
|
self.reportButton.isAccessibilityElement = true
|
||||||
self.reportButton.accessibilityLabel = strings.VoiceOver_MessageContextReport
|
self.reportButton.accessibilityLabel = strings.VoiceOver_MessageContextReport
|
||||||
|
|
||||||
self.forwardButton = HighlightableButtonNode(pointerStyle: .default)
|
self.forwardButton = HighlightableButtonNode(pointerStyle: .rectangle(CGSize(width: 56.0, height: 40.0)))
|
||||||
self.forwardButton.isAccessibilityElement = true
|
self.forwardButton.isAccessibilityElement = true
|
||||||
self.forwardButton.accessibilityLabel = strings.VoiceOver_MessageContextForward
|
self.forwardButton.accessibilityLabel = strings.VoiceOver_MessageContextForward
|
||||||
|
|
||||||
self.shareButton = HighlightableButtonNode(pointerStyle: .default)
|
self.shareButton = HighlightableButtonNode(pointerStyle: .rectangle(CGSize(width: 56.0, height: 40.0)))
|
||||||
self.shareButton.isAccessibilityElement = true
|
self.shareButton.isAccessibilityElement = true
|
||||||
self.shareButton.accessibilityLabel = strings.VoiceOver_MessageContextShare
|
self.shareButton.accessibilityLabel = strings.VoiceOver_MessageContextShare
|
||||||
|
|
||||||
|
|||||||
@ -50,12 +50,12 @@ final class ChatSearchInputPanelNode: ChatInputPanelNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init(theme: PresentationTheme) {
|
init(theme: PresentationTheme) {
|
||||||
self.upButton = HighlightableButtonNode(pointerStyle: .default)
|
self.upButton = HighlightableButtonNode(pointerStyle: .circle(36.0))
|
||||||
self.upButton.isEnabled = false
|
self.upButton.isEnabled = false
|
||||||
self.downButton = HighlightableButtonNode(pointerStyle: .default)
|
self.downButton = HighlightableButtonNode(pointerStyle: .circle(36.0))
|
||||||
self.downButton.isEnabled = false
|
self.downButton.isEnabled = false
|
||||||
self.calendarButton = HighlightableButtonNode()
|
self.calendarButton = HighlightableButtonNode()
|
||||||
self.membersButton = HighlightableButtonNode(pointerStyle: .default)
|
self.membersButton = HighlightableButtonNode(pointerStyle: .circle(36.0))
|
||||||
self.measureResultsLabel = TextNode()
|
self.measureResultsLabel = TextNode()
|
||||||
self.measureResultsLabel.displaysAsynchronously = false
|
self.measureResultsLabel.displaysAsynchronously = false
|
||||||
self.resultsButton = HighlightableButtonNode(pointerStyle: .default)
|
self.resultsButton = HighlightableButtonNode(pointerStyle: .default)
|
||||||
|
|||||||
@ -31,7 +31,8 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var micButtonPointerInteraction: PointerInteraction?
|
private var micButtonPointerInteraction: PointerInteraction?
|
||||||
|
private var sendButtonPointerInteraction: PointerInteraction?
|
||||||
|
|
||||||
private var validLayout: CGSize?
|
private var validLayout: CGSize?
|
||||||
|
|
||||||
@ -50,9 +51,9 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode {
|
|||||||
self.backgroundNode.backgroundColor = theme.chat.inputPanel.actionControlFillColor
|
self.backgroundNode.backgroundColor = theme.chat.inputPanel.actionControlFillColor
|
||||||
self.backgroundNode.clipsToBounds = true
|
self.backgroundNode.clipsToBounds = true
|
||||||
self.backdropNode = ChatMessageBubbleBackdrop()
|
self.backdropNode = ChatMessageBubbleBackdrop()
|
||||||
self.sendButton = HighlightTrackingButtonNode(pointerStyle: .lift)
|
self.sendButton = HighlightTrackingButtonNode(pointerStyle: nil)
|
||||||
|
|
||||||
self.expandMediaInputButton = HighlightableButtonNode(pointerStyle: .default)
|
self.expandMediaInputButton = HighlightableButtonNode(pointerStyle: .circle(36.0))
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
@ -107,7 +108,8 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.micButtonPointerInteraction = PointerInteraction(view: self.micButton, style: .circle)
|
self.micButtonPointerInteraction = PointerInteraction(view: self.micButton, style: .circle(36.0))
|
||||||
|
self.sendButtonPointerInteraction = PointerInteraction(view: self.sendButton.view, customInteractionView: self.backgroundNode.view, style: .lift)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateTheme(theme: PresentationTheme, wallpaper: TelegramWallpaper) {
|
func updateTheme(theme: PresentationTheme, wallpaper: TelegramWallpaper) {
|
||||||
|
|||||||
@ -51,7 +51,7 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode {
|
|||||||
|
|
||||||
self.width = AccessoryItemIconButtonNode.calculateWidth(item: item, image: image, text: text, strings: strings)
|
self.width = AccessoryItemIconButtonNode.calculateWidth(item: item, image: image, text: text, strings: strings)
|
||||||
|
|
||||||
super.init()
|
super.init(pointerStyle: .circle(30.0))
|
||||||
|
|
||||||
self.isAccessibilityElement = true
|
self.isAccessibilityElement = true
|
||||||
self.accessibilityTraits = [.button]
|
self.accessibilityTraits = [.button]
|
||||||
@ -754,7 +754,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
|||||||
self.sendAsAvatarContainerNode.animateScale = false
|
self.sendAsAvatarContainerNode.animateScale = false
|
||||||
self.sendAsAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 16.0))
|
self.sendAsAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 16.0))
|
||||||
|
|
||||||
self.attachmentButton = HighlightableButtonNode(pointerStyle: .circle)
|
self.attachmentButton = HighlightableButtonNode(pointerStyle: .circle(36.0))
|
||||||
self.attachmentButton.accessibilityLabel = presentationInterfaceState.strings.VoiceOver_AttachMedia
|
self.attachmentButton.accessibilityLabel = presentationInterfaceState.strings.VoiceOver_AttachMedia
|
||||||
self.attachmentButton.accessibilityTraits = [.button]
|
self.attachmentButton.accessibilityTraits = [.button]
|
||||||
self.attachmentButton.isAccessibilityElement = true
|
self.attachmentButton.isAccessibilityElement = true
|
||||||
|
|||||||
@ -61,7 +61,7 @@ final class IconButtonNode: HighlightTrackingButtonNode {
|
|||||||
self.iconNode.displaysAsynchronously = false
|
self.iconNode.displaysAsynchronously = false
|
||||||
self.iconNode.displayWithoutProcessing = true
|
self.iconNode.displayWithoutProcessing = true
|
||||||
|
|
||||||
super.init(pointerStyle: .circle)
|
super.init(pointerStyle: .circle(nil))
|
||||||
|
|
||||||
self.addSubnode(self.iconNode)
|
self.addSubnode(self.iconNode)
|
||||||
|
|
||||||
|
|||||||
@ -1054,7 +1054,7 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode {
|
|||||||
self.iconNode.displaysAsynchronously = false
|
self.iconNode.displaysAsynchronously = false
|
||||||
self.iconNode.displayWithoutProcessing = true
|
self.iconNode.displayWithoutProcessing = true
|
||||||
|
|
||||||
super.init(pointerStyle: .default)
|
super.init(pointerStyle: .insetRectangle(-8.0, 2.0))
|
||||||
|
|
||||||
self.isAccessibilityElement = true
|
self.isAccessibilityElement = true
|
||||||
self.accessibilityTraits = .button
|
self.accessibilityTraits = .button
|
||||||
|
|||||||
@ -7780,7 +7780,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen {
|
public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortcutResponder {
|
||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
fileprivate let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
|
fileprivate let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
|
||||||
private let peerId: PeerId
|
private let peerId: PeerId
|
||||||
@ -8278,6 +8278,37 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen {
|
|||||||
let controller = ContextController(account: primary.0.account, presentationData: self.presentationData, source: .extracted(SettingsTabBarContextExtractedContentSource(controller: self, sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture)
|
let controller = ContextController(account: primary.0.account, presentationData: self.presentationData, source: .extracted(SettingsTabBarContextExtractedContentSource(controller: self, sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture)
|
||||||
self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller)
|
self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var keyShortcuts: [KeyShortcut] {
|
||||||
|
if self.isSettings {
|
||||||
|
return [
|
||||||
|
KeyShortcut(
|
||||||
|
input: "0",
|
||||||
|
modifiers: [.command],
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.controllerNode.openSettings(section: .savedMessages)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
KeyShortcut(
|
||||||
|
input: "W",
|
||||||
|
modifiers: [.command],
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.dismiss(animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
KeyShortcut(
|
||||||
|
input: UIKeyCommand.inputEscape,
|
||||||
|
modifiers: [],
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.dismiss(animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class SettingsTabBarContextExtractedContentSource: ContextExtractedContentSource {
|
private final class SettingsTabBarContextExtractedContentSource: ContextExtractedContentSource {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user