damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

SelectableText.swift (8678B)


      1 //
      2 //  SelectableText.swift
      3 //  damus
      4 //
      5 //  Created by Oleg Abalonski on 2/16/23.
      6 //
      7 
      8 import UIKit
      9 import SwiftUI
     10 
     11 struct SelectableText: View {
     12     let damus_state: DamusState
     13     let event: NostrEvent?
     14     let attributedString: AttributedString
     15     let textAlignment: NSTextAlignment
     16     @State private var selectedTextActionState: SelectedTextActionState = .hide
     17     @State private var selectedTextHeight: CGFloat = .zero
     18     @State private var selectedTextWidth: CGFloat = .zero
     19 
     20     let size: EventViewKind
     21 
     22     init(damus_state: DamusState, event: NostrEvent?, attributedString: AttributedString, textAlignment: NSTextAlignment? = nil, size: EventViewKind) {
     23         self.damus_state = damus_state
     24         self.event = event
     25         self.attributedString = attributedString
     26         self.textAlignment = textAlignment ?? NSTextAlignment.natural
     27         self.size = size
     28     }
     29     
     30     var body: some View {
     31         GeometryReader { geo in
     32             TextViewRepresentable(
     33                 attributedString: attributedString,
     34                 textColor: UIColor.label,
     35                 font: eventviewsize_to_uifont(size),
     36                 fixedWidth: selectedTextWidth,
     37                 textAlignment: self.textAlignment,
     38                 enableHighlighting: self.enableHighlighting(),
     39                 postHighlight: { selectedText in
     40                     self.selectedTextActionState = .show_highlight_post_view(highlighted_text: selectedText)
     41                 },
     42                 muteWord: { selectedText in
     43                     self.selectedTextActionState = .show_mute_word_view(highlighted_text: selectedText)
     44                 },
     45                 height: $selectedTextHeight
     46             )
     47             .padding([.leading, .trailing], -1.0)
     48             .onAppear {
     49                 if geo.size.width == .zero {
     50                     self.selectedTextHeight = 1000.0
     51                 } else {
     52                     self.selectedTextWidth = geo.size.width
     53                 }
     54             }
     55             .onChange(of: geo.size) { newSize in
     56                 self.selectedTextWidth = newSize.width
     57             }
     58         }
     59         .sheet(isPresented: Binding(get: {
     60             return self.selectedTextActionState.should_show_highlight_post_view()
     61         }, set: { newValue in
     62             self.selectedTextActionState = newValue ? .show_highlight_post_view(highlighted_text: self.selectedTextActionState.highlighted_text() ?? "") : .hide
     63         })) {
     64             if let event, case .show_highlight_post_view(let highlighted_text) = self.selectedTextActionState {
     65                 PostView(
     66                     action: .highlighting(.init(selected_text: highlighted_text, source: .event(event.id))),
     67                     damus_state: damus_state
     68                 )
     69                 .presentationDragIndicator(.visible)
     70                 .presentationDetents([.height(selectedTextHeight + 450), .medium, .large])
     71             }
     72         }
     73         .sheet(isPresented: Binding(get: {
     74             return self.selectedTextActionState.should_show_mute_word_view()
     75         }, set: { newValue in
     76             self.selectedTextActionState = newValue ? .show_mute_word_view(highlighted_text: self.selectedTextActionState.highlighted_text() ?? "") : .hide
     77         })) {
     78             if case .show_mute_word_view(let highlighted_text) = selectedTextActionState {
     79                 AddMuteItemView(state: damus_state, new_text: .constant(highlighted_text))
     80                     .presentationDragIndicator(.visible)
     81                     .presentationDetents([.height(300), .medium, .large])
     82             }
     83         }
     84         .frame(height: selectedTextHeight)
     85     }
     86     
     87     func enableHighlighting() -> Bool {
     88         self.event != nil
     89     }
     90     
     91     enum SelectedTextActionState {
     92         case hide
     93         case show_highlight_post_view(highlighted_text: String)
     94         case show_mute_word_view(highlighted_text: String)
     95         
     96         func should_show_highlight_post_view() -> Bool {
     97             guard case .show_highlight_post_view(let highlighted_text) = self else { return false }
     98             return true
     99         }
    100         
    101         func should_show_mute_word_view() -> Bool {
    102             guard case .show_mute_word_view(let highlighted_text) = self else { return false }
    103             return true
    104         }
    105         
    106         func highlighted_text() -> String? {
    107             switch self {
    108                 case .hide:
    109                     return nil
    110                 case .show_mute_word_view(highlighted_text: let highlighted_text):
    111                     return highlighted_text
    112                 case .show_highlight_post_view(highlighted_text: let highlighted_text):
    113                     return highlighted_text
    114             }
    115         }
    116     }
    117 }
    118 
    119 fileprivate class TextView: UITextView {
    120     var postHighlight: (String) -> Void
    121     var muteWord: (String) -> Void
    122 
    123     init(frame: CGRect, textContainer: NSTextContainer?, postHighlight: @escaping (String) -> Void, muteWord: @escaping (String) -> Void) {
    124         self.postHighlight = postHighlight
    125         self.muteWord = muteWord
    126         super.init(frame: frame, textContainer: textContainer)
    127     }
    128 
    129     required init?(coder: NSCoder) {
    130             fatalError("init(coder:) has not been implemented")
    131         }
    132 
    133     override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
    134         if action == #selector(highlightText(_:)) {
    135             return true
    136         }
    137         
    138         if action == #selector(muteText(_:)) {
    139             return true
    140         }
    141         
    142         return super.canPerformAction(action, withSender: sender)
    143     }
    144     
    145     func getSelectedText() -> String? {
    146         guard let selectedRange = self.selectedTextRange else { return nil }
    147         return self.text(in: selectedRange)
    148     }
    149 
    150     @objc public func highlightText(_ sender: Any?) {
    151         guard let selectedText = self.getSelectedText() else { return }
    152         self.postHighlight(selectedText)
    153     }
    154     
    155     @objc public func muteText(_ sender: Any?) {
    156         guard let selectedText = self.getSelectedText() else { return }
    157         self.muteWord(selectedText)
    158     }
    159 
    160 }
    161 
    162 fileprivate struct TextViewRepresentable: UIViewRepresentable {
    163 
    164     let attributedString: AttributedString
    165     let textColor: UIColor
    166     let font: UIFont
    167     let fixedWidth: CGFloat
    168     let textAlignment: NSTextAlignment
    169     let enableHighlighting: Bool
    170     let postHighlight: (String) -> Void
    171     let muteWord: (String) -> Void
    172     @Binding var height: CGFloat
    173 
    174     func makeUIView(context: UIViewRepresentableContext<Self>) -> TextView {
    175         let view = TextView(frame: .zero, textContainer: nil, postHighlight: postHighlight, muteWord: muteWord)
    176         view.isEditable = false
    177         view.dataDetectorTypes = .all
    178         view.isSelectable = true
    179         view.backgroundColor = .clear
    180         view.textContainer.lineFragmentPadding = 0
    181         view.textContainerInset = .zero
    182         view.textContainerInset.left = 1.0
    183         view.textContainerInset.right = 1.0
    184         view.textAlignment = textAlignment
    185 
    186         let menuController = UIMenuController.shared
    187         let highlightItem = UIMenuItem(title: "Highlight", action: #selector(view.highlightText(_:)))
    188         let muteItem = UIMenuItem(title: "Mute", action: #selector(view.muteText(_:)))
    189         menuController.menuItems = self.enableHighlighting ? [highlightItem, muteItem] : []
    190 
    191         return view
    192     }
    193 
    194     func updateUIView(_ uiView: TextView, context: UIViewRepresentableContext<Self>) {
    195         let mutableAttributedString = createNSAttributedString()
    196         uiView.attributedText = mutableAttributedString
    197         uiView.textAlignment = self.textAlignment
    198 
    199         let newHeight = mutableAttributedString.height(containerWidth: fixedWidth)
    200 
    201         DispatchQueue.main.async {
    202             height = newHeight
    203         }
    204     }
    205 
    206     func createNSAttributedString() -> NSMutableAttributedString {
    207         let mutableAttributedString = NSMutableAttributedString(attributedString)
    208         let myAttribute = [
    209             NSAttributedString.Key.font: font,
    210             NSAttributedString.Key.foregroundColor: textColor
    211         ]
    212 
    213         mutableAttributedString.addAttributes(
    214             myAttribute,
    215             range: NSRange.init(location: 0, length: mutableAttributedString.length)
    216         )
    217 
    218         return mutableAttributedString
    219     }
    220 }
    221 
    222 fileprivate extension NSAttributedString {
    223 
    224     func height(containerWidth: CGFloat) -> CGFloat {
    225 
    226         let rect = self.boundingRect(
    227             with: CGSize.init(width: containerWidth, height: CGFloat.greatestFiniteMagnitude),
    228             options: [.usesLineFragmentOrigin, .usesFontLeading],
    229             context: nil
    230         )
    231 
    232         return ceil(rect.size.height)
    233     }
    234 }