mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-15 18:59:54 +00:00
Proper toolbar
This commit is contained in:
parent
6553d1f34f
commit
4adcbe99ad
@ -843,6 +843,7 @@ private enum SGDebugActions: String {
|
||||
private enum SGDebugToggles: String {
|
||||
case forceImmediateShareSheet
|
||||
case legacyNotificationsFix
|
||||
case inputToolbar
|
||||
}
|
||||
|
||||
|
||||
@ -860,6 +861,9 @@ private func SGDebugControllerEntries(presentationData: PresentationData) -> [SG
|
||||
if SGSimpleSettings.shared.b {
|
||||
entries.append(.disclosure(id: id.count, section: .base, link: .sessionBackupManager, text: "Session Backup"))
|
||||
entries.append(.disclosure(id: id.count, section: .base, link: .messageFilter, text: "Message Filter"))
|
||||
if #available(iOS 13.0, *) {
|
||||
entries.append(.toggle(id: id.count, section: .base, settingName: .inputToolbar, value: SGSimpleSettings.shared.inputToolbar, text: "Message Formatting Toolbar", enabled: true))
|
||||
}
|
||||
}
|
||||
entries.append(.action(id: id.count, section: .base, actionType: .clearRegDateCache, text: "Clear Regdate cache", kind: .generic))
|
||||
entries.append(.toggle(id: id.count, section: .base, settingName: .forceImmediateShareSheet, value: SGSimpleSettings.shared.forceSystemSharing, text: "Force System Share Sheet", enabled: true))
|
||||
@ -884,6 +888,8 @@ public func sgDebugController(context: AccountContext) -> ViewController {
|
||||
SGSimpleSettings.shared.forceSystemSharing = value
|
||||
case .legacyNotificationsFix:
|
||||
SGSimpleSettings.shared.legacyNotificationsFix = value
|
||||
case .inputToolbar:
|
||||
SGSimpleSettings.shared.inputToolbar = value
|
||||
}
|
||||
}, openDisclosureLink: { link in
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
@ -34,7 +34,8 @@ public class SGSimpleSettings {
|
||||
{ let _ = self.disableSendAsButton },
|
||||
{ let _ = self.disableSnapDeletionEffect },
|
||||
{ let _ = self.startTelescopeWithRearCam },
|
||||
{ let _ = self.hideRecordingButton }
|
||||
{ let _ = self.hideRecordingButton },
|
||||
{ let _ = self.inputToolbar }
|
||||
]
|
||||
|
||||
tasks.forEach { task in
|
||||
@ -109,6 +110,7 @@ public class SGSimpleSettings {
|
||||
case videoPIPSwipeDirection
|
||||
case legacyNotificationsFix
|
||||
case messageFilterKeywords
|
||||
case inputToolbar
|
||||
}
|
||||
|
||||
public enum DownloadSpeedBoostValues: String, CaseIterable {
|
||||
@ -207,7 +209,8 @@ public class SGSimpleSettings {
|
||||
Keys.confirmCalls.rawValue: true,
|
||||
Keys.videoPIPSwipeDirection.rawValue: VideoPIPSwipeDirection.up.rawValue,
|
||||
Keys.legacyNotificationsFix.rawValue: false,
|
||||
Keys.messageFilterKeywords.rawValue: []
|
||||
Keys.messageFilterKeywords.rawValue: [],
|
||||
Keys.inputToolbar.rawValue: false
|
||||
]
|
||||
|
||||
@UserDefault(key: Keys.hidePhoneInSettings.rawValue)
|
||||
@ -393,6 +396,9 @@ public class SGSimpleSettings {
|
||||
|
||||
@UserDefault(key: Keys.messageFilterKeywords.rawValue)
|
||||
public var messageFilterKeywords: [String]
|
||||
|
||||
@UserDefault(key: Keys.inputToolbar.rawValue)
|
||||
public var inputToolbar: Bool
|
||||
}
|
||||
|
||||
extension SGSimpleSettings {
|
||||
|
||||
@ -3,15 +3,15 @@ import TextFormat
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
|
||||
public func chatTextInputAddFormattingAttribute(_ state: ChatTextInputState, attribute: NSAttributedString.Key, value: Any?) -> ChatTextInputState {
|
||||
public func chatTextInputAddFormattingAttribute(forceRemoveAll: Bool = false, _ state: ChatTextInputState, attribute: NSAttributedString.Key, value: Any?) -> ChatTextInputState {
|
||||
if !state.selectionRange.isEmpty {
|
||||
let nsRange = NSRange(location: state.selectionRange.lowerBound, length: state.selectionRange.count)
|
||||
var addAttribute = true
|
||||
var attributesToRemove: [NSAttributedString.Key] = []
|
||||
state.inputText.enumerateAttributes(in: nsRange, options: .longestEffectiveRangeNotRequired) { attributes, range, _ in
|
||||
for (key, _) in attributes {
|
||||
if key == attribute {
|
||||
if nsRange == range {
|
||||
if key == attribute || forceRemoveAll {
|
||||
if nsRange == range || forceRemoveAll {
|
||||
addAttribute = false
|
||||
attributesToRemove.append(key)
|
||||
}
|
||||
|
||||
@ -929,11 +929,20 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
|
||||
textInputNode.textView.returnKeyType = strongSelf.sendWithReturnKey ? .send : .default
|
||||
textInputNode.textView.reloadInputViews()
|
||||
}
|
||||
if #available(iOS 13.0, *), let toolbar = strongSelf.toolbarHostingController as? UIHostingController<ChatToolbarView> {
|
||||
toolbar.rootView.setShowNewLine(value)
|
||||
}
|
||||
}
|
||||
})
|
||||
self.initToolbar()
|
||||
|
||||
self.addSubnode(self.clippingNode)
|
||||
|
||||
// MARK: Swiftgram
|
||||
if #available(iOS 13.0, *), let toolbarHostingController = self.toolbarHostingController as? UIHostingController<ChatToolbarView> {
|
||||
self.view.addSubview(toolbarHostingController.view)
|
||||
}
|
||||
|
||||
self.sendAsAvatarContainerNode.activated = { [weak self] gesture, _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
@ -1212,16 +1221,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
|
||||
textInputNode.textView.returnKeyType = self.sendWithReturnKey ? .send : .default
|
||||
self.textInputNode = textInputNode
|
||||
|
||||
#if DEBUG
|
||||
if #available(iOS 13.0, *) {
|
||||
let toolbarView = ChatToolbarView()
|
||||
let toolbarHostingController = UIHostingController(rootView: toolbarView)
|
||||
toolbarHostingController.view.frame = CGRect(x: 0, y: 0, width: textInputNode.frame.width, height: 88/*44*/)
|
||||
toolbarHostingController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
self.toolbarHostingController = toolbarHostingController
|
||||
self.textInputNode?.textView.inputAccessoryView = toolbarHostingController.view
|
||||
}
|
||||
#endif
|
||||
|
||||
var accessoryButtonsWidth: CGFloat = 0.0
|
||||
var firstButton = true
|
||||
@ -2128,7 +2127,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
|
||||
if buttonTitleUpdated && !transition.isAnimated {
|
||||
transition = .animated(duration: 0.3, curve: .easeInOut)
|
||||
}
|
||||
|
||||
// MARK: Swiftgram
|
||||
let originalLeftInset = leftInset
|
||||
var leftInset = leftInset
|
||||
|
||||
var textInputBackgroundWidthOffset: CGFloat = 0.0
|
||||
@ -2938,7 +2938,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
|
||||
self.viewOnceButton.isHidden = true
|
||||
}
|
||||
|
||||
return panelHeight
|
||||
// MARK: Swiftgram
|
||||
var toolbarOffset: CGFloat = 0.0
|
||||
if !displayBotStartButton {
|
||||
toolbarOffset = layoutToolbar(transition: transition, panelHeight: panelHeight, width: width, leftInset: originalLeftInset, rightInset: rightInset)
|
||||
}
|
||||
|
||||
return panelHeight + toolbarOffset
|
||||
}
|
||||
|
||||
@objc private func slowModeButtonPressed() {
|
||||
@ -5057,35 +5063,321 @@ 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, return current state
|
||||
guard inputText.length > 0 else {
|
||||
return (current, inputMode)
|
||||
}
|
||||
|
||||
let plainText = inputText.string
|
||||
let nsString = plainText as NSString
|
||||
|
||||
// Create character set for word boundaries (spaces and newlines)
|
||||
let wordBoundaries = CharacterSet.whitespacesAndNewlines
|
||||
|
||||
// Find last non-whitespace character
|
||||
var endIndex = nsString.length - 1
|
||||
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, return current state
|
||||
guard endIndex >= 0 else {
|
||||
return (current, inputMode)
|
||||
}
|
||||
|
||||
// Find start of the last 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 last word
|
||||
let wordLength = endIndex - startIndex + 1
|
||||
let lastWordRange = NSRange(location: startIndex, length: wordLength)
|
||||
|
||||
// Create new selection range
|
||||
let newSelectionRange = lastWordRange.location ..< (lastWordRange.location + lastWordRange.length)
|
||||
|
||||
return (ChatTextInputState(inputText: inputText, selectionRange: newSelectionRange), inputMode)
|
||||
}
|
||||
}
|
||||
|
||||
func initToolbar() {
|
||||
guard #available(iOS 13.0, *) else { return }
|
||||
guard SGSimpleSettings.shared.inputToolbar else { return }
|
||||
guard SGSimpleSettings.shared.b else { return }
|
||||
let toolbarView = ChatToolbarView(
|
||||
onQuote: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.selectLastWordIfIdle()
|
||||
strongSelf.formatAttributesQuote(strongSelf)
|
||||
},
|
||||
onSpoiler: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.selectLastWordIfIdle()
|
||||
strongSelf.formatAttributesSpoiler(strongSelf)
|
||||
},
|
||||
onBold: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.selectLastWordIfIdle()
|
||||
strongSelf.formatAttributesBold(strongSelf)
|
||||
},
|
||||
onItalic: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.selectLastWordIfIdle()
|
||||
strongSelf.formatAttributesItalic(strongSelf)
|
||||
},
|
||||
onMonospace: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.selectLastWordIfIdle()
|
||||
strongSelf.formatAttributesMonospace(strongSelf)
|
||||
},
|
||||
onLink: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.selectLastWordIfIdle()
|
||||
strongSelf.formatAttributesLink(self!)
|
||||
},
|
||||
onStrikethrough: { [weak self]
|
||||
in guard let strongSelf = self else { return }
|
||||
strongSelf.selectLastWordIfIdle()
|
||||
strongSelf.formatAttributesStrikethrough(strongSelf)
|
||||
},
|
||||
onUnderline: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.selectLastWordIfIdle()
|
||||
strongSelf.formatAttributesUnderline(strongSelf)
|
||||
},
|
||||
onCode: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.selectLastWordIfIdle()
|
||||
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)
|
||||
}
|
||||
}
|
||||
},
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
)
|
||||
let toolbarHostingController = UIHostingController(rootView: toolbarView/*, ignoreSafeArea: true*/)
|
||||
self.toolbarHostingController = toolbarHostingController
|
||||
toolbarHostingController.view.backgroundColor = .clear
|
||||
|
||||
// Disable "Swipe to go back" gesture when touching scrollview
|
||||
self.view.interactiveTransitionGestureRecognizerTest = { [weak self] point in
|
||||
if let self, let _ = (self.toolbarHostingController as? UIHostingController<ChatToolbarView>)?.view.hitTest(point, with: nil) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func layoutToolbar(transition: ContainedViewLayoutTransition, panelHeight: CGFloat, width: CGFloat, leftInset: CGFloat, rightInset: CGFloat) -> CGFloat {
|
||||
var toolbarHeight: CGFloat = 0.0
|
||||
var toolbarSpacing: CGFloat = 0.0
|
||||
if #available(iOS 13.0, *) {
|
||||
if let toolbarHostingController = self.toolbarHostingController as? UIHostingController<ChatToolbarView> {
|
||||
toolbarHeight = 44.0
|
||||
toolbarSpacing = 1.0
|
||||
transition.updateFrame(view: toolbarHostingController.view, frame: CGRect(origin: CGPoint(x: leftInset, y: panelHeight + toolbarSpacing), size: CGSize(width: width - rightInset - leftInset, height: toolbarHeight)))
|
||||
}
|
||||
}
|
||||
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 {
|
||||
HStack {
|
||||
Button(action: {
|
||||
// Action 1
|
||||
}) {
|
||||
Image(systemName: "photo")
|
||||
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)
|
||||
|
||||
Button(action: {
|
||||
// Action 2
|
||||
}) {
|
||||
Image(systemName: "camera")
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
// Action 3
|
||||
}) {
|
||||
Image(systemName: "keyboard")
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
// .background(Color(UIColor.systemBackground))
|
||||
.background(Color(UIColor.red))
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user