Refactor toolbar

This commit is contained in:
Kylmakalle 2025-01-20 22:50:18 +02:00
parent 3a6690f0ef
commit a7816937b0
7 changed files with 280 additions and 245 deletions

View File

@ -0,0 +1,17 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "SGInputToolbar",
module_name = "SGInputToolbar",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,147 @@
import SwiftUI
import Foundation
// MARK: Swiftgram
@available(iOS 13.0, *)
public struct ChatToolbarView: View {
var onQuote: () -> Void
var onSpoiler: () -> Void
var onBold: () -> Void
var onItalic: () -> Void
var onMonospace: () -> Void
var onLink: () -> Void
var onStrikethrough: () -> Void
var onUnderline: () -> Void
var onCode: () -> Void
var onNewLine: () -> Void
@Binding private var showNewLine: Bool
var onClearFormatting: () -> Void
public init(
onQuote: @escaping () -> Void,
onSpoiler: @escaping () -> Void,
onBold: @escaping () -> Void,
onItalic: @escaping () -> Void,
onMonospace: @escaping () -> Void,
onLink: @escaping () -> Void,
onStrikethrough: @escaping () -> Void,
onUnderline: @escaping () -> Void,
onCode: @escaping () -> Void,
onNewLine: @escaping () -> Void,
showNewLine: Binding<Bool>,
onClearFormatting: @escaping () -> Void
) {
self.onQuote = onQuote
self.onSpoiler = onSpoiler
self.onBold = onBold
self.onItalic = onItalic
self.onMonospace = onMonospace
self.onLink = onLink
self.onStrikethrough = onStrikethrough
self.onUnderline = onUnderline
self.onCode = onCode
self.onNewLine = onNewLine
self._showNewLine = showNewLine
self.onClearFormatting = onClearFormatting
}
public func setShowNewLine(_ value: Bool) {
self.showNewLine = value
}
public var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
if showNewLine {
Button(action: onNewLine) {
Image(systemName: "return")
}
.buttonStyle(ToolbarButtonStyle())
}
Button(action: onClearFormatting) {
Image(systemName: "pencil.slash")
}
.buttonStyle(ToolbarButtonStyle())
Spacer()
// Quote Button
Button(action: onQuote) {
Image(systemName: "text.quote")
}
.buttonStyle(ToolbarButtonStyle())
// Spoiler Button
Button(action: onSpoiler) {
Image(systemName: "eye.slash")
}
.buttonStyle(ToolbarButtonStyle())
// Bold Button
Button(action: onBold) {
Image(systemName: "bold")
}
.buttonStyle(ToolbarButtonStyle())
// Italic Button
Button(action: onItalic) {
Image(systemName: "italic")
}
.buttonStyle(ToolbarButtonStyle())
// Monospace Button
Button(action: onMonospace) {
if #available(iOS 16.4, *) {
Text("M").monospaced()
} else {
Text("M")
}
}
.buttonStyle(ToolbarButtonStyle())
// Link Button
Button(action: onLink) {
Image(systemName: "link")
}
.buttonStyle(ToolbarButtonStyle())
// Underline Button
Button(action: onUnderline) {
Image(systemName: "underline")
}
.buttonStyle(ToolbarButtonStyle())
// Strikethrough Button
Button(action: onStrikethrough) {
Image(systemName: "strikethrough")
}
.buttonStyle(ToolbarButtonStyle())
// Code Button
Button(action: onCode) {
Image(systemName: "chevron.left.forwardslash.chevron.right")
}
.buttonStyle(ToolbarButtonStyle())
}
.padding(.horizontal, 8)
.padding(.vertical, 8)
}
.background(Color(UIColor.clear))
}
}
@available(iOS 13.0, *)
struct ToolbarButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(.system(size: 17))
.frame(width: 36, height: 36, alignment: .center)
.background(Color(UIColor.tertiarySystemBackground))
.cornerRadius(8)
// TODO(swiftgram): Does not work for fast taps (like mine)
.opacity(configuration.isPressed ? 0.4 : 1.0)
}
}

View File

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

View File

@ -63,6 +63,8 @@ public enum ChatTranslationDisplayType {
public final class ChatPanelInterfaceInteraction {
public let setupReplyMessage: (MessageId?, @escaping (ContainedViewLayoutTransition, @escaping () -> Void) -> Void) -> Void
public let sgSelectLastWordIfIdle: () -> Void
public let sgSetNewLine: () -> Void
public let setupEditMessage: (MessageId?, @escaping (ContainedViewLayoutTransition) -> Void) -> Void
public let beginMessageSelection: ([MessageId], @escaping (ContainedViewLayoutTransition) -> Void) -> Void
public let cancelMessageSelection: (ContainedViewLayoutTransition) -> Void
@ -405,6 +407,93 @@ public final class ChatPanelInterfaceInteraction {
self.chatController = chatController
self.statuses = statuses
// MARK: Swiftgram
self.sgSelectLastWordIfIdle = {
updateTextInputStateAndMode { current, inputMode in
// No changes to current selection
if !current.selectionRange.isEmpty {
return (current, inputMode)
}
let inputText = (current.inputText.mutableCopy() as? NSMutableAttributedString) ?? NSMutableAttributedString()
// If text is empty or cursor is at the start, return current state
guard inputText.length > 0, current.selectionRange.lowerBound > 0 else {
return (current, inputMode)
}
let plainText = inputText.string
let nsString = plainText as NSString
// Create character set for word boundaries
let wordBoundaries = CharacterSet.whitespacesAndNewlines
// Start from cursor position instead of end of text
var endIndex = current.selectionRange.lowerBound - 1
// Find last non-whitespace character before cursor
while endIndex >= 0 &&
(nsString.substring(with: NSRange(location: endIndex, length: 1)) as NSString)
.rangeOfCharacter(from: wordBoundaries).location != NSNotFound {
endIndex -= 1
}
// If we only had whitespace before cursor, return current state
guard endIndex >= 0 else {
return (current, inputMode)
}
// Find start of the current word by looking backwards for whitespace
var startIndex = endIndex
while startIndex > 0 {
let char = nsString.substring(with: NSRange(location: startIndex - 1, length: 1))
if (char as NSString).rangeOfCharacter(from: wordBoundaries).location != NSNotFound {
break
}
startIndex -= 1
}
// Create range for the word at cursor
let wordLength = endIndex - startIndex + 1
let wordRange = NSRange(location: startIndex, length: wordLength)
// Create new selection range
let newSelectionRange = wordRange.location ..< (wordRange.location + wordLength)
return (ChatTextInputState(inputText: inputText, selectionRange: newSelectionRange), inputMode)
}
}
self.sgSetNewLine = {
updateTextInputStateAndMode { current, inputMode in
let inputText = (current.inputText.mutableCopy() as? NSMutableAttributedString) ?? NSMutableAttributedString()
// Check if there's selected text
let hasSelection = current.selectionRange.count > 0
if hasSelection {
// Move selected text to new line
let selectedText = inputText.attributedSubstring(from: NSRange(current.selectionRange))
let newLineAttr = NSAttributedString(string: "\n")
// Insert newline and selected text
inputText.replaceCharacters(in: NSRange(current.selectionRange), with: newLineAttr)
inputText.insert(selectedText, at: current.selectionRange.lowerBound + 1)
// Update selection range to end of moved text
let newPosition = current.selectionRange.lowerBound + 1 + selectedText.length
return (ChatTextInputState(inputText: inputText, selectionRange: newPosition ..< newPosition), inputMode)
} else {
// Simple newline insertion at current position
let attributedString = NSAttributedString(string: "\n")
inputText.replaceCharacters(in: NSRange(current.selectionRange), with: attributedString)
// Update cursor position
let newPosition = current.selectionRange.lowerBound + attributedString.length
return (ChatTextInputState(inputText: inputText, selectionRange: newPosition ..< newPosition), inputMode)
}
}
}
}
public convenience init(

View File

@ -16,7 +16,8 @@ sgdeps = [
"//Swiftgram/SGActionRequestHandlerSanitizer:SGActionRequestHandlerSanitizer",
"//Swiftgram/Wrap:Wrap",
"//Swiftgram/SGDeviceToken:SGDeviceToken",
"//Swiftgram/SGDebugUI:SGDebugUI"
"//Swiftgram/SGDebugUI:SGDebugUI",
"//Swiftgram/SGInputToolbar:SGInputToolbar",
# "//Swiftgram/SGContentAnalysis:SGContentAnalysis"
]

View File

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

View File

@ -2,6 +2,7 @@
import TelegramUIPreferences
import SGSimpleSettings
import SwiftUI
import SGInputToolbar
import Foundation
import UIKit
@ -5063,62 +5064,6 @@ private final class BoostSlowModeButton: HighlightTrackingButtonNode {
// MARK: Swiftgram
extension ChatTextInputPanelNode {
func selectLastWordIfIdle() {
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
// No changes to current selection
if !current.selectionRange.isEmpty {
return (current, inputMode)
}
let inputText = (current.inputText.mutableCopy() as? NSMutableAttributedString) ?? NSMutableAttributedString()
// If text is empty or cursor is at the start, return current state
guard inputText.length > 0, current.selectionRange.lowerBound > 0 else {
return (current, inputMode)
}
let plainText = inputText.string
let nsString = plainText as NSString
// Create character set for word boundaries
let wordBoundaries = CharacterSet.whitespacesAndNewlines
// Start from cursor position instead of end of text
var endIndex = current.selectionRange.lowerBound - 1
// Find last non-whitespace character before cursor
while endIndex >= 0 &&
(nsString.substring(with: NSRange(location: endIndex, length: 1)) as NSString)
.rangeOfCharacter(from: wordBoundaries).location != NSNotFound {
endIndex -= 1
}
// If we only had whitespace before cursor, return current state
guard endIndex >= 0 else {
return (current, inputMode)
}
// Find start of the current word by looking backwards for whitespace
var startIndex = endIndex
while startIndex > 0 {
let char = nsString.substring(with: NSRange(location: startIndex - 1, length: 1))
if (char as NSString).rangeOfCharacter(from: wordBoundaries).location != NSNotFound {
break
}
startIndex -= 1
}
// Create range for the word at cursor
let wordLength = endIndex - startIndex + 1
let wordRange = NSRange(location: startIndex, length: wordLength)
// Create new selection range
let newSelectionRange = wordRange.location ..< (wordRange.location + wordLength)
return (ChatTextInputState(inputText: inputText, selectionRange: newSelectionRange), inputMode)
}
}
func initToolbarIfNeeded() {
guard #available(iOS 13.0, *) else { return }
guard SGSimpleSettings.shared.inputToolbar else { return }
@ -5127,87 +5072,59 @@ extension ChatTextInputPanelNode {
let toolbarView = ChatToolbarView(
onQuote: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.selectLastWordIfIdle()
strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle()
strongSelf.formatAttributesQuote(strongSelf)
},
onSpoiler: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.selectLastWordIfIdle()
strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle()
strongSelf.formatAttributesSpoiler(strongSelf)
},
onBold: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.selectLastWordIfIdle()
strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle()
strongSelf.formatAttributesBold(strongSelf)
},
onItalic: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.selectLastWordIfIdle()
strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle()
strongSelf.formatAttributesItalic(strongSelf)
},
onMonospace: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.selectLastWordIfIdle()
strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle()
strongSelf.formatAttributesMonospace(strongSelf)
},
onLink: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.selectLastWordIfIdle()
strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle()
strongSelf.formatAttributesLink(self!)
},
onStrikethrough: { [weak self]
in guard let strongSelf = self else { return }
strongSelf.selectLastWordIfIdle()
strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle()
strongSelf.formatAttributesStrikethrough(strongSelf)
},
onUnderline: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.selectLastWordIfIdle()
strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle()
strongSelf.formatAttributesUnderline(strongSelf)
},
onCode: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.selectLastWordIfIdle()
strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle()
strongSelf.formatAttributesCodeBlock(strongSelf)
},
onNewLine: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
let inputText = (current.inputText.mutableCopy() as? NSMutableAttributedString) ?? NSMutableAttributedString()
// Check if there's selected text
let hasSelection = current.selectionRange.count > 0
if hasSelection {
// Move selected text to new line
let selectedText = inputText.attributedSubstring(from: NSRange(current.selectionRange))
let newLineAttr = NSAttributedString(string: "\n")
// Insert newline and selected text
inputText.replaceCharacters(in: NSRange(current.selectionRange), with: newLineAttr)
inputText.insert(selectedText, at: current.selectionRange.lowerBound + 1)
// Update selection range to end of moved text
let newPosition = current.selectionRange.lowerBound + 1 + selectedText.length
return (ChatTextInputState(inputText: inputText, selectionRange: newPosition ..< newPosition), inputMode)
} else {
// Simple newline insertion at current position
let attributedString = NSAttributedString(string: "\n")
inputText.replaceCharacters(in: NSRange(current.selectionRange), with: attributedString)
// Update cursor position
let newPosition = current.selectionRange.lowerBound + attributedString.length
return (ChatTextInputState(inputText: inputText, selectionRange: newPosition ..< newPosition), inputMode)
}
}
strongSelf.interfaceInteraction?.sgSetNewLine()
},
// TODO(swiftgram): Binding
showNewLine: .constant(true), //.constant(self.sendWithReturnKey)
onClearFormatting: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
return (chatTextInputAddFormattingAttribute(forceRemoveAll: true,current, attribute: ChatTextInputAttributes.allAttributes[0], value: nil), inputMode)
return (chatTextInputAddFormattingAttribute(forceRemoveAll: true, current, attribute: ChatTextInputAttributes.allAttributes[0], value: nil), inputMode)
}
}
)
@ -5216,7 +5133,7 @@ extension ChatTextInputPanelNode {
let toolbarNode = ASDisplayNode { toolbarHostingController.view }
self.toolbarNode = toolbarNode
// assigning toolbarHostingController bugs responsivness and overrides layout
// self.toolbarHostingController = toolbarHostingController
// self.toolbarHostingController = toolbarHostingController
// Disable "Swipe to go back" gesture when touching scrollview
self.view.interactiveTransitionGestureRecognizerTest = { [weak self] point in
@ -5248,150 +5165,4 @@ extension ChatTextInputPanelNode {
}
return toolbarHeight + toolbarSpacing
}
}
// MARK: Swiftgram
@available(iOS 13.0, *)
struct ChatToolbarView: View {
var onQuote: () -> Void
var onSpoiler: () -> Void
var onBold: () -> Void
var onItalic: () -> Void
var onMonospace: () -> Void
var onLink: () -> Void
var onStrikethrough: () -> Void
var onUnderline: () -> Void
var onCode: () -> Void
var onNewLine: () -> Void
@Binding private var showNewLine: Bool
var onClearFormatting: () -> Void
public init(
onQuote: @escaping () -> Void,
onSpoiler: @escaping () -> Void,
onBold: @escaping () -> Void,
onItalic: @escaping () -> Void,
onMonospace: @escaping () -> Void,
onLink: @escaping () -> Void,
onStrikethrough: @escaping () -> Void,
onUnderline: @escaping () -> Void,
onCode: @escaping () -> Void,
onNewLine: @escaping () -> Void,
showNewLine: Binding<Bool>,
onClearFormatting: @escaping () -> Void
) {
self.onQuote = onQuote
self.onSpoiler = onSpoiler
self.onBold = onBold
self.onItalic = onItalic
self.onMonospace = onMonospace
self.onLink = onLink
self.onStrikethrough = onStrikethrough
self.onUnderline = onUnderline
self.onCode = onCode
self.onNewLine = onNewLine
self._showNewLine = showNewLine
self.onClearFormatting = onClearFormatting
}
public func setShowNewLine(_ value: Bool) {
self.showNewLine = value
}
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
if showNewLine {
Button(action: onNewLine) {
Image(systemName: "return")
}
.buttonStyle(ToolbarButtonStyle())
}
Button(action: onClearFormatting) {
Image(systemName: "pencil.slash")
}
.buttonStyle(ToolbarButtonStyle())
Spacer()
// Quote Button
Button(action: onQuote) {
Image(systemName: "text.quote")
}
.buttonStyle(ToolbarButtonStyle())
// Spoiler Button
Button(action: onSpoiler) {
Image(systemName: "eye.slash")
}
.buttonStyle(ToolbarButtonStyle())
// Bold Button
Button(action: onBold) {
Image(systemName: "bold")
}
.buttonStyle(ToolbarButtonStyle())
// Italic Button
Button(action: onItalic) {
Image(systemName: "italic")
}
.buttonStyle(ToolbarButtonStyle())
// Monospace Button
Button(action: onMonospace) {
if #available(iOS 16.4, *) {
Text("M").monospaced()
} else {
Text("M")
}
}
.buttonStyle(ToolbarButtonStyle())
// Link Button
Button(action: onLink) {
Image(systemName: "link")
}
.buttonStyle(ToolbarButtonStyle())
// Underline Button
Button(action: onUnderline) {
Image(systemName: "underline")
}
.buttonStyle(ToolbarButtonStyle())
// Strikethrough Button
Button(action: onStrikethrough) {
Image(systemName: "strikethrough")
}
.buttonStyle(ToolbarButtonStyle())
// Code Button
Button(action: onCode) {
Image(systemName: "chevron.left.forwardslash.chevron.right")
}
.buttonStyle(ToolbarButtonStyle())
}
.padding(.horizontal, 8)
.padding(.vertical, 8)
}
.background(Color(UIColor.clear))
}
}
@available(iOS 13.0, *)
struct ToolbarButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(.system(size: 17))
.frame(width: 36, height: 36, alignment: .center)
.background(Color(UIColor.tertiarySystemBackground))
.cornerRadius(8)
// TODO(swiftgram): Does not work for fast taps (like mine)
.opacity(configuration.isPressed ? 0.4 : 1.0)
}
}