1import ExpoModulesCore
2import UIKit
3
4enum T3ComposerClipboard {
5 static let fragmentType = "app.t3.context-fragment"
6
7 static func write(text: String, fragment: String) {
8 var items: [String: Any] = ["public.utf8-plain-text": text]
9 if let data = fragment.data(using: .utf8),
10 var payload = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
11 let records = payload["records"] as? [[String: Any]] {
12 var selected = records.filter { record in
13 guard let id = record["contextId"] as? String else { return false }
14 return text.contains("/\(id))")
15 }
16 let screenshots = Set(selected.compactMap { $0["screenshotContextId"] as? String })
17 selected.append(contentsOf: records.filter { screenshots.contains($0["contextId"] as? String ?? "") && !text.contains("/\($0["contextId"] as? String ?? ""))") })
18 payload["records"] = selected
19 if !selected.isEmpty, let encoded = try? JSONSerialization.data(withJSONObject: payload), let raw = String(data: encoded, encoding: .utf8) {
20 let attribute = raw.addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? ""
21 let escaped = text.replacingOccurrences(of: "&", with: "&amp;").replacingOccurrences(of: "<", with: "&lt;").replacingOccurrences(of: ">", with: "&gt;")
22 items[fragmentType] = encoded
23 items["public.html"] = Data("<pre data-t3-context-fragment=\"\(attribute)\">\(escaped)</pre>".utf8)
24 }
25 }
26 UIPasteboard.general.items = [items]
27 }
28
29 static func read() -> [String: String] {
30 let board = UIPasteboard.general
31 return [
32 "text": board.string ?? "",
33 "fragment": board.data(forPasteboardType: fragmentType).flatMap { String(data: $0, encoding: .utf8) } ?? "",
34 "html": board.data(forPasteboardType: "public.html").flatMap { String(data: $0, encoding: .utf8) } ?? "",
35 ]
36 }
37}
38
39// Expo Modules 2.0: the clipboard function is a @JS member; the view stays in
40// definition() until the 2.0 view API lands.
41@ExpoModule("T3ComposerEditor")
42public class T3ComposerEditorModule: Module {
43 // UIPasteboard is main-thread only, so the member is main-actor isolated.
44 @JS
45 @MainActor
46 func writeContextClipboard(text: String, fragment: String) async {
47 T3ComposerClipboard.write(text: text, fragment: fragment)
48 }
49
50 public func definition() -> ModuleDefinition {
51 View(T3ComposerEditorView.self) {
52 Prop("controlledDocumentJson") { (view: T3ComposerEditorView, documentJson: String) in
53 view.setControlledDocumentJson(documentJson)
54 }
55 Prop("themeJson") { (view: T3ComposerEditorView, themeJson: String) in
56 view.setThemeJson(themeJson)
57 }
58 Prop("clipboardFragment") { (view: T3ComposerEditorView, fragment: String) in
59 view.setClipboardFragment(fragment)
60 }
61 Prop("placeholder") { (view: T3ComposerEditorView, placeholder: String) in
62 view.setPlaceholder(placeholder)
63 }
64 Prop("fontFamily") { (view: T3ComposerEditorView, fontFamily: String) in
65 view.setFontFamily(fontFamily)
66 }
67 Prop("fontSize") { (view: T3ComposerEditorView, fontSize: Double) in
68 view.setFontSize(CGFloat(fontSize))
69 }
70 Prop("lineHeight") { (view: T3ComposerEditorView, lineHeight: Double) in
71 view.setLineHeight(CGFloat(lineHeight))
72 }
73 Prop("contentInsetVertical") { (view: T3ComposerEditorView, contentInsetVertical: Double) in
74 view.setContentInsetVertical(CGFloat(contentInsetVertical))
75 }
76 Prop("editable") { (view: T3ComposerEditorView, editable: Bool) in
77 view.setEditable(editable)
78 }
79 Prop("readOnly") { (view: T3ComposerEditorView, readOnly: Bool) in
80 view.setReadOnly(readOnly)
81 }
82 Prop("scrollEnabled") { (view: T3ComposerEditorView, scrollEnabled: Bool) in
83 view.setScrollEnabled(scrollEnabled)
84 }
85 Prop("autoFocus") { (view: T3ComposerEditorView, autoFocus: Bool) in
86 view.setAutoFocus(autoFocus)
87 }
88 Prop("autoCorrect") { (view: T3ComposerEditorView, autoCorrect: Bool) in
89 view.setAutoCorrect(autoCorrect)
90 }
91 Prop("spellCheck") { (view: T3ComposerEditorView, spellCheck: Bool) in
92 view.setSpellCheck(spellCheck)
93 }
94 Prop("submitTitle") { (view: T3ComposerEditorView, title: String) in
95 view.setSubmitTitle(title)
96 }
97 Prop("alternateSubmitTitle") { (view: T3ComposerEditorView, title: String) in
98 view.setAlternateSubmitTitle(title)
99 }
100 Prop("enterBehavior") { (view: T3ComposerEditorView, behavior: String) in
101 view.setEnterBehavior(behavior)
102 }
103 Prop("textPasteThresholdBytes") { (view: T3ComposerEditorView, threshold: Int) in
104 view.setTextPasteThresholdBytes(threshold)
105 }
106 Prop("maxInputChars") { (view: T3ComposerEditorView, maxInputChars: Int) in
107 view.setMaxInputChars(maxInputChars)
108 }
109
110 Events(
111 "onComposerChange",
112 "onComposerSelectionChange",
113 "onComposerFocus",
114 "onComposerBlur",
115 "onComposerSubmit",
116 "onComposerPasteImages",
117 "onComposerContextPress",
118 "onComposerPasteContext",
119 "onComposerPasteText",
120 "onComposerContentSizeChange"
121 )
122
123 AsyncFunction("focus") { (view: T3ComposerEditorView) in
124 view.focusEditor()
125 }
126 AsyncFunction("blur") { (view: T3ComposerEditorView) in
127 view.blurEditor()
128 }
129 AsyncFunction("setSelection") { (view: T3ComposerEditorView, start: Int, end: Int) in
130 view.setSelection(start: start, end: end)
131 }
132 }
133 }
134}