Refactor toolbar to be a part of MessageInputPanel

This commit is contained in:
Kylmakalle 2025-01-26 14:41:10 +02:00
parent 7194cdc6d2
commit 9e1acf72dd
5 changed files with 166 additions and 126 deletions

View File

@ -19,6 +19,7 @@ public struct ChatToolbarView: View {
@Binding private var showNewLine: Bool @Binding private var showNewLine: Bool
var onClearFormatting: () -> Void var onClearFormatting: () -> Void
var preferredColorScheme: ColorScheme?
public init( public init(
onQuote: @escaping () -> Void, onQuote: @escaping () -> Void,
@ -32,7 +33,8 @@ public struct ChatToolbarView: View {
onCode: @escaping () -> Void, onCode: @escaping () -> Void,
onNewLine: @escaping () -> Void, onNewLine: @escaping () -> Void,
showNewLine: Binding<Bool>, showNewLine: Binding<Bool>,
onClearFormatting: @escaping () -> Void onClearFormatting: @escaping () -> Void,
preferredColorScheme: ColorScheme? = nil
) { ) {
self.onQuote = onQuote self.onQuote = onQuote
self.onSpoiler = onSpoiler self.onSpoiler = onSpoiler
@ -46,6 +48,7 @@ public struct ChatToolbarView: View {
self.onNewLine = onNewLine self.onNewLine = onNewLine
self._showNewLine = showNewLine self._showNewLine = showNewLine
self.onClearFormatting = onClearFormatting self.onClearFormatting = onClearFormatting
self.preferredColorScheme = preferredColorScheme // TODO(swiftgram): Does not work for buttons :(
} }
public func setShowNewLine(_ value: Bool) { public func setShowNewLine(_ value: Bool) {
@ -59,36 +62,42 @@ public struct ChatToolbarView: View {
Button(action: onNewLine) { Button(action: onNewLine) {
Image(systemName: "return") Image(systemName: "return")
} }
.buttonStyle(ToolbarButtonStyle()) .buttonStyle(ToolbarButtonStyle(preferredColorScheme: preferredColorScheme))
.preferredColorScheme(preferredColorScheme)
} }
Button(action: onClearFormatting) { Button(action: onClearFormatting) {
Image(systemName: "pencil.slash") Image(systemName: "pencil.slash")
} }
.buttonStyle(ToolbarButtonStyle()) .buttonStyle(ToolbarButtonStyle(preferredColorScheme: preferredColorScheme))
.preferredColorScheme(preferredColorScheme)
Spacer() Spacer()
// Quote Button // Quote Button
Button(action: onQuote) { Button(action: onQuote) {
Image(systemName: "text.quote") Image(systemName: "text.quote")
} }
.buttonStyle(ToolbarButtonStyle()) .buttonStyle(ToolbarButtonStyle(preferredColorScheme: preferredColorScheme))
.preferredColorScheme(preferredColorScheme)
// Spoiler Button // Spoiler Button
Button(action: onSpoiler) { Button(action: onSpoiler) {
Image(systemName: "eye.slash") Image(systemName: "eye.slash")
} }
.buttonStyle(ToolbarButtonStyle()) .buttonStyle(ToolbarButtonStyle(preferredColorScheme: preferredColorScheme))
.preferredColorScheme(preferredColorScheme)
// Bold Button // Bold Button
Button(action: onBold) { Button(action: onBold) {
Image(systemName: "bold") Image(systemName: "bold")
} }
.buttonStyle(ToolbarButtonStyle()) .buttonStyle(ToolbarButtonStyle(preferredColorScheme: preferredColorScheme))
.preferredColorScheme(preferredColorScheme)
// Italic Button // Italic Button
Button(action: onItalic) { Button(action: onItalic) {
Image(systemName: "italic") Image(systemName: "italic")
} }
.buttonStyle(ToolbarButtonStyle()) .buttonStyle(ToolbarButtonStyle(preferredColorScheme: preferredColorScheme))
.preferredColorScheme(preferredColorScheme)
// Monospace Button // Monospace Button
Button(action: onMonospace) { Button(action: onMonospace) {
@ -98,43 +107,52 @@ public struct ChatToolbarView: View {
Text("M") Text("M")
} }
} }
.buttonStyle(ToolbarButtonStyle()) .buttonStyle(ToolbarButtonStyle(preferredColorScheme: preferredColorScheme))
.preferredColorScheme(preferredColorScheme)
// Link Button // Link Button
Button(action: onLink) { Button(action: onLink) {
Image(systemName: "link") Image(systemName: "link")
} }
.buttonStyle(ToolbarButtonStyle()) .buttonStyle(ToolbarButtonStyle(preferredColorScheme: preferredColorScheme))
.preferredColorScheme(preferredColorScheme)
// Underline Button // Underline Button
Button(action: onUnderline) { Button(action: onUnderline) {
Image(systemName: "underline") Image(systemName: "underline")
} }
.buttonStyle(ToolbarButtonStyle()) .buttonStyle(ToolbarButtonStyle(preferredColorScheme: preferredColorScheme))
.preferredColorScheme(preferredColorScheme)
// Strikethrough Button // Strikethrough Button
Button(action: onStrikethrough) { Button(action: onStrikethrough) {
Image(systemName: "strikethrough") Image(systemName: "strikethrough")
} }
.buttonStyle(ToolbarButtonStyle()) .buttonStyle(ToolbarButtonStyle(preferredColorScheme: preferredColorScheme))
.preferredColorScheme(preferredColorScheme)
// Code Button // Code Button
Button(action: onCode) { Button(action: onCode) {
Image(systemName: "chevron.left.forwardslash.chevron.right") Image(systemName: "chevron.left.forwardslash.chevron.right")
} }
.buttonStyle(ToolbarButtonStyle()) .buttonStyle(ToolbarButtonStyle(preferredColorScheme: preferredColorScheme))
.preferredColorScheme(preferredColorScheme)
} }
.padding(.horizontal, 8) .padding(.horizontal, 8)
.padding(.vertical, 8) .padding(.vertical, 8)
} }
.background(Color(UIColor.clear)) .background(Color(UIColor.clear))
.preferredColorScheme(preferredColorScheme)
} }
} }
@available(iOS 13.0, *) @available(iOS 13.0, *)
struct ToolbarButtonStyle: ButtonStyle { struct ToolbarButtonStyle: ButtonStyle {
var preferredColorScheme: ColorScheme? = nil
func makeBody(configuration: Configuration) -> some View { func makeBody(configuration: Configuration) -> some View {
configuration.label configuration.label
.font(.system(size: 17)) .font(.system(size: 17))
@ -143,5 +161,6 @@ struct ToolbarButtonStyle: ButtonStyle {
.cornerRadius(8) .cornerRadius(8)
// TODO(swiftgram): Does not work for fast taps (like mine) // TODO(swiftgram): Does not work for fast taps (like mine)
.opacity(configuration.isPressed ? 0.4 : 1.0) .opacity(configuration.isPressed ? 0.4 : 1.0)
.preferredColorScheme(preferredColorScheme)
} }
} }

View File

@ -1,9 +1,5 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
sgDeps = [
"//Swiftgram/SGSimpleSettings:SGSimpleSettings",
"//Swiftgram/SGInputToolbar:SGInputToolbar"
]
swift_library( swift_library(
name = "LegacyMessageInputPanel", name = "LegacyMessageInputPanel",
@ -14,7 +10,7 @@ swift_library(
copts = [ copts = [
"-warnings-as-errors", "-warnings-as-errors",
], ],
deps = sgDeps + [ deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display", "//submodules/Display",

View File

@ -1,8 +1,3 @@
// MARK: Swiftgram
import SwiftUI
import SGInputToolbar
import SGSimpleSettings
import Foundation import Foundation
import UIKit import UIKit
import AsyncDisplayKit import AsyncDisplayKit
@ -43,9 +38,6 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
private let hapticFeedback = HapticFeedback() private let hapticFeedback = HapticFeedback()
// MARK: Swiftgram
private var toolbarNode: ASDisplayNode?
private var inputView: LegacyMessageInputPanelInputView? private var inputView: LegacyMessageInputPanelInputView?
private var isEmojiKeyboardActive = false private var isEmojiKeyboardActive = false
@ -84,9 +76,6 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
self.update(transition: transition.containedViewLayoutTransition) self.update(transition: transition.containedViewLayoutTransition)
} }
} }
// MARK: Swiftgram
self.initToolbarIfNeeded()
} }
public func updateLayoutSize(_ size: CGSize, keyboardHeight: CGFloat, sideInset: CGFloat, animated: Bool) -> CGFloat { public func updateLayoutSize(_ size: CGSize, keyboardHeight: CGFloat, sideInset: CGFloat, animated: Bool) -> CGFloat {
@ -215,7 +204,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
} }
self.inputPanel.parentState = self.state self.inputPanel.parentState = self.state
var inputPanelSize = self.inputPanel.update( let inputPanelSize = self.inputPanel.update(
transition: ComponentTransition(transition), transition: ComponentTransition(transition),
component: AnyComponent( component: AnyComponent(
MessageInputPanelComponent( MessageInputPanelComponent(
@ -306,12 +295,6 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
environment: {}, environment: {},
containerSize: CGSize(width: width, height: maxInputPanelHeight) containerSize: CGSize(width: width, height: maxInputPanelHeight)
) )
// MARK: Swiftgram
var toolbarOffset: CGFloat = 0.0
toolbarOffset = self.layoutToolbar(transition: transition, panelHeight: inputPanelSize.height - 8.0, width: width, leftInset: leftInset, rightInset: rightInset)
inputPanelSize.height += toolbarOffset
if let view = self.inputPanel.view { if let view = self.inputPanel.view {
if view.superview == nil { if view.superview == nil {
self.view.addSubview(view) self.view.addSubview(view)
@ -556,9 +539,6 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event) let result = super.hitTest(point, with: event)
if let toolbarNode = self.toolbarNode, let toolbarResult = toolbarNode.hitTest(self.view.convert(point, to: toolbarNode.view), with: event) {
return toolbarResult
}
if let view = self.inputPanel.view, let panelResult = view.hitTest(self.view.convert(point, to: view), with: event) { if let view = self.inputPanel.view, let panelResult = view.hitTest(self.view.convert(point, to: view), with: event) {
return panelResult return panelResult
} }
@ -583,90 +563,3 @@ private final class HeaderContextReferenceContentSource: ContextReferenceContent
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: self.position) return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: self.position)
} }
} }
extension LegacyMessageInputPanelNode {
func initToolbarIfNeeded() {
guard #available(iOS 13.0, *) else { return }
guard SGSimpleSettings.shared.inputToolbar else { return }
guard SGSimpleSettings.shared.b else { return }
guard self.toolbarNode == nil else { return }
let notificationName = Notification.Name("sgToolbarAction")
let toolbarView = ChatToolbarView(
onQuote: { [weak self] in
guard let _ = self else { return }
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "quote"])
},
onSpoiler: { [weak self] in
guard let _ = self else { return }
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "spoiler"])
},
onBold: { [weak self] in
guard let _ = self else { return }
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "bold"])
},
onItalic: { [weak self] in
guard let _ = self else { return }
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "italic"])
},
onMonospace: { [weak self] in
guard let _ = self else { return }
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "monospace"])
},
onLink: { [weak self] in
guard let _ = self else { return }
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "link"])
},
onStrikethrough: { [weak self]
in guard let _ = self else { return }
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "strikethrough"])
},
onUnderline: { [weak self] in
guard let _ = self else { return }
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "underline"])
},
onCode: { [weak self] in
guard let _ = self else { return }
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "code"])
},
onNewLine: { [weak self] in
guard let _ = self else { return }
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "newline"])
},
// TODO(swiftgram): Binding
showNewLine: .constant(true), //.constant(self.sendWithReturnKey)
onClearFormatting: { [weak self] in
guard let _ = self else { return }
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "clearFormatting"])
}
)
let toolbarHostingController = UIHostingController(rootView: toolbarView)
toolbarHostingController.view.backgroundColor = .clear
let toolbarNode = ASDisplayNode { toolbarHostingController.view }
self.toolbarNode = toolbarNode
// assigning toolbarHostingController bugs responsivness and overrides layout
// self.toolbarHostingController = toolbarHostingController
// Disable "Swipe to go back" gesture when touching scrollview
self.view.interactiveTransitionGestureRecognizerTest = { [weak self] point in
if let self, let _ = self.toolbarNode?.view.hitTest(point, with: nil) {
return false
}
return true
}
self.addSubnode(toolbarNode)
}
func layoutToolbar(transition: ContainedViewLayoutTransition, panelHeight: CGFloat, width: CGFloat, leftInset: CGFloat, rightInset: CGFloat) -> CGFloat {
// TODO(swiftgram): Do not show if locked formatting
var toolbarHeight: CGFloat = 0.0
var toolbarSpacing: CGFloat = 0.0
if let toolbarNode = self.toolbarNode {
toolbarHeight = 44.0
toolbarSpacing = 1.0
transition.updateFrame(node: toolbarNode, frame: CGRect(origin: CGPoint(x: leftInset, y: panelHeight + toolbarSpacing), size: CGSize(width: width - rightInset - leftInset, height: toolbarHeight)))
}
return toolbarHeight + toolbarSpacing
}
}

View File

@ -1,5 +1,11 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
sgDeps = [
"//Swiftgram/SGSimpleSettings:SGSimpleSettings",
"//Swiftgram/SGInputToolbar:SGInputToolbar"
]
swift_library( swift_library(
name = "MessageInputPanelComponent", name = "MessageInputPanelComponent",
module_name = "MessageInputPanelComponent", module_name = "MessageInputPanelComponent",
@ -9,7 +15,7 @@ swift_library(
copts = [ copts = [
"-warnings-as-errors", "-warnings-as-errors",
], ],
deps = [ deps = sgDeps + [
"//submodules/Display", "//submodules/Display",
"//submodules/ComponentFlow", "//submodules/ComponentFlow",
"//submodules/AppBundle", "//submodules/AppBundle",

View File

@ -1,3 +1,8 @@
// MARK: Swiftgram
import class SwiftUI.UIHostingController
import SGSimpleSettings
import SGInputToolbar
import Foundation import Foundation
import UIKit import UIKit
import Display import Display
@ -467,6 +472,9 @@ public final class MessageInputPanelComponent: Component {
private let counter = ComponentView<Empty>() private let counter = ComponentView<Empty>()
private var header: ComponentView<Empty>? private var header: ComponentView<Empty>?
// MARK: Swiftgram
private var toolbarView: UIView?
private var disabledPlaceholder: ComponentView<Empty>? private var disabledPlaceholder: ComponentView<Empty>?
private var textClippingView = UIView() private var textClippingView = UIView()
private let textField = ComponentView<Empty>() private let textField = ComponentView<Empty>()
@ -563,6 +571,9 @@ public final class MessageInputPanelComponent: Component {
self.state?.updated() self.state?.updated()
} }
) )
// MARK: Swiftgram
self.initToolbarIfNeeded()
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -733,6 +744,11 @@ public final class MessageInputPanelComponent: Component {
return panelResult return panelResult
} }
// MARK: Swiftgram
if result == nil, let toolbarView = self.toolbarView, let toolbarResult = toolbarView.hitTest(self.convert(point, to: toolbarView), with: event) {
return toolbarResult
}
return result return result
} }
@ -2253,6 +2269,9 @@ public final class MessageInputPanelComponent: Component {
} }
} }
// MARK: Swiftgram
size = self.layoutToolbar(transition: transition, layoutFromTop: layoutFromTop, size: size, availableSize: availableSize, defaultInsets: defaultInsets, textFieldSize: textFieldSize, previousComponent: previousComponent)
return size return size
} }
} }
@ -2306,3 +2325,110 @@ final class ViewForOverlayContent: UIView {
return nil return nil
} }
} }
extension MessageInputPanelComponent.View {
func initToolbarIfNeeded() {
guard #available(iOS 13.0, *) else { return }
guard SGSimpleSettings.shared.inputToolbar else { return }
guard SGSimpleSettings.shared.b else { return }
guard self.toolbarView == nil else { return }
let notificationName = Notification.Name("sgToolbarAction")
let toolbar = ChatToolbarView(
onQuote: { [weak self] in
guard let _ = self else { return }
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "quote"])
},
onSpoiler: { [weak self] in
guard let _ = self else { return }
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "spoiler"])
},
onBold: { [weak self] in
guard let _ = self else { return }
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "bold"])
},
onItalic: { [weak self] in
guard let _ = self else { return }
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "italic"])
},
onMonospace: { [weak self] in
guard let _ = self else { return }
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "monospace"])
},
onLink: { [weak self] in
guard let _ = self else { return }
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "link"])
},
onStrikethrough: { [weak self]
in guard let _ = self else { return }
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "strikethrough"])
},
onUnderline: { [weak self] in
guard let _ = self else { return }
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "underline"])
},
onCode: { [weak self] in
guard let _ = self else { return }
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "code"])
},
onNewLine: { [weak self] in
guard let _ = self else { return }
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "newline"])
},
// TODO(swiftgram): Binding
showNewLine: .constant(true), //.constant(self.sendWithReturnKey)
onClearFormatting: { [weak self] in
guard let _ = self else { return }
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "clearFormatting"])
},
preferredColorScheme: .dark
)
let toolbarHostingController = UIHostingController(rootView: toolbar)
toolbarHostingController.view.backgroundColor = .clear
let toolbarView = toolbarHostingController.view
self.toolbarView = toolbarView
// assigning toolbarHostingController bugs responsivness and overrides layout
// self.toolbarHostingController = toolbarHostingController
// Disable "Swipe to go back" gesture when touching scrollview
self.interactiveTransitionGestureRecognizerTest = { [weak self] point in
if let self, let _ = self.toolbarView?.hitTest(point, with: nil) {
return false
}
return true
}
if let toolbarView = self.toolbarView {
self.addSubview(toolbarView)
}
}
func layoutToolbar(transition: ComponentTransition, layoutFromTop: Bool, size: CGSize, availableSize: CGSize, defaultInsets: UIEdgeInsets, textFieldSize: CGSize, previousComponent: MessageInputPanelComponent?) -> CGSize {
// TODO(swiftgram): Do not show if locked formatting
var transition = transition
if let previousComponent = previousComponent {
let previousLayoutFromTop = previousComponent.attachmentButtonMode == .captionDown
if previousLayoutFromTop != layoutFromTop {
// attachmentButtonMode changed
transition = .immediate
}
}
var size = size
if let toolbarView = self.toolbarView {
let toolbarHeight: CGFloat = 44.0
let toolbarSpacing: CGFloat = 1.0
let toolbarSize = CGSize(width: availableSize.width, height: toolbarHeight)
let hasFirstResponder = self.hasFirstResponder()
transition.setAlpha(view: toolbarView, alpha: hasFirstResponder ? 1.0 : 0.0)
if layoutFromTop {
transition.setFrame(view: toolbarView, frame: CGRect(origin: CGPoint(x: .zero, y: availableSize.height + toolbarSpacing), size: toolbarSize))
} else {
transition.setFrame(view: toolbarView, frame: CGRect(origin: CGPoint(x: .zero, y: textFieldSize.height + defaultInsets.top + toolbarSpacing), size: toolbarSize))
if hasFirstResponder {
size.height += toolbarHeight + toolbarSpacing
}
}
}
return size
}
}