@apollohg/react-native-prose-editor 0.5.16 → 0.5.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +2440 -275
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +783 -64
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +1767 -81
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +209 -87
- package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +27 -0
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +58 -9
- package/dist/NativeEditorBridge.d.ts +34 -1
- package/dist/NativeEditorBridge.js +243 -83
- package/dist/NativeRichTextEditor.js +998 -137
- package/dist/addons.d.ts +7 -0
- package/ios/EditorCore.xcframework/Info.plist +5 -5
- package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
- package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
- package/ios/NativeEditorExpoView.swift +830 -17
- package/ios/NativeEditorModule.swift +304 -108
- package/ios/PositionBridge.swift +24 -1
- package/ios/RichTextEditorView.swift +912 -89
- package/package.json +2 -1
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
|
@@ -1,6 +1,113 @@
|
|
|
1
1
|
import ExpoModulesCore
|
|
2
2
|
import UIKit
|
|
3
3
|
|
|
4
|
+
private final class WeakNativeEditorExpoView {
|
|
5
|
+
weak var view: NativeEditorExpoView?
|
|
6
|
+
|
|
7
|
+
init(_ view: NativeEditorExpoView) {
|
|
8
|
+
self.view = view
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
final class NativeEditorViewRegistry {
|
|
13
|
+
static let shared = NativeEditorViewRegistry()
|
|
14
|
+
|
|
15
|
+
private var viewsByEditorId: [UInt64: WeakNativeEditorExpoView] = [:]
|
|
16
|
+
private var destroyedEditorIds: Set<UInt64> = []
|
|
17
|
+
|
|
18
|
+
private init() {}
|
|
19
|
+
|
|
20
|
+
func markEditorCreated(editorId: UInt64) {
|
|
21
|
+
guard editorId != 0 else { return }
|
|
22
|
+
performOnMain {
|
|
23
|
+
destroyedEditorIds.remove(editorId)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func isDestroyed(editorId: UInt64) -> Bool {
|
|
28
|
+
guard editorId != 0 else { return false }
|
|
29
|
+
return performOnMain {
|
|
30
|
+
destroyedEditorIds.contains(editorId)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@discardableResult
|
|
35
|
+
func register(editorId: UInt64, view: NativeEditorExpoView) -> Bool {
|
|
36
|
+
guard editorId != 0 else { return false }
|
|
37
|
+
return performOnMain {
|
|
38
|
+
guard !destroyedEditorIds.contains(editorId) else { return false }
|
|
39
|
+
viewsByEditorId[editorId] = WeakNativeEditorExpoView(view)
|
|
40
|
+
return true
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func unregister(editorId: UInt64, view: NativeEditorExpoView) {
|
|
45
|
+
guard editorId != 0 else { return }
|
|
46
|
+
performOnMain {
|
|
47
|
+
guard viewsByEditorId[editorId]?.view === view else { return }
|
|
48
|
+
viewsByEditorId.removeValue(forKey: editorId)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
func invalidateDestroyedEditor(editorId: UInt64) {
|
|
53
|
+
guard editorId != 0 else { return }
|
|
54
|
+
performOnMain {
|
|
55
|
+
destroyedEditorIds.insert(editorId)
|
|
56
|
+
guard let view = viewsByEditorId.removeValue(forKey: editorId)?.view else {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
view.handleEditorDestroyed(editorId)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
func prepareForCommandJSON(editorId: UInt64) -> String {
|
|
64
|
+
let prepare = { () -> String in
|
|
65
|
+
if self.destroyedEditorIds.contains(editorId) {
|
|
66
|
+
return Self.commandPreparationJSON(ready: false, blockedReason: "destroyed")
|
|
67
|
+
}
|
|
68
|
+
guard let view = self.viewsByEditorId[editorId]?.view else {
|
|
69
|
+
self.viewsByEditorId.removeValue(forKey: editorId)
|
|
70
|
+
return Self.commandPreparationJSON(ready: true)
|
|
71
|
+
}
|
|
72
|
+
return view.prepareForEditorCommandJSON()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return performOnMain(prepare)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
static func commandPreparationJSON(
|
|
79
|
+
ready: Bool,
|
|
80
|
+
updateJSON: String? = nil,
|
|
81
|
+
blockedReason: String? = nil
|
|
82
|
+
) -> String {
|
|
83
|
+
var payload: [String: Any] = ["ready": ready]
|
|
84
|
+
if let updateJSON {
|
|
85
|
+
payload["updateJSON"] = updateJSON
|
|
86
|
+
}
|
|
87
|
+
if let blockedReason {
|
|
88
|
+
payload["blockedReason"] = blockedReason
|
|
89
|
+
}
|
|
90
|
+
guard let data = try? JSONSerialization.data(withJSONObject: payload),
|
|
91
|
+
let json = String(data: data, encoding: .utf8)
|
|
92
|
+
else {
|
|
93
|
+
if let blockedReason {
|
|
94
|
+
return ready
|
|
95
|
+
? "{\"ready\":true,\"blockedReason\":\"\(blockedReason)\"}"
|
|
96
|
+
: "{\"ready\":false,\"blockedReason\":\"\(blockedReason)\"}"
|
|
97
|
+
}
|
|
98
|
+
return ready ? "{\"ready\":true}" : "{\"ready\":false}"
|
|
99
|
+
}
|
|
100
|
+
return json
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private func performOnMain<T>(_ work: () -> T) -> T {
|
|
104
|
+
if Thread.isMainThread {
|
|
105
|
+
return work()
|
|
106
|
+
}
|
|
107
|
+
return DispatchQueue.main.sync(execute: work)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
4
111
|
private struct NativeToolbarState {
|
|
5
112
|
let marks: [String: Bool]
|
|
6
113
|
let nodes: [String: Bool]
|
|
@@ -1638,6 +1745,7 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1638
1745
|
private var addons = NativeEditorAddons(mentions: nil)
|
|
1639
1746
|
private var mentionQueryState: MentionQueryState?
|
|
1640
1747
|
private var lastMentionEventJSON: String?
|
|
1748
|
+
private var desiredThemeJSON: String?
|
|
1641
1749
|
private var lastThemeJSON: String?
|
|
1642
1750
|
private var lastAddonsJSON: String?
|
|
1643
1751
|
private var lastRemoteSelectionsJSON: String?
|
|
@@ -1646,6 +1754,29 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1646
1754
|
private var pendingEditorUpdateJSON: String?
|
|
1647
1755
|
private var pendingEditorUpdateRevision = 0
|
|
1648
1756
|
private var appliedEditorUpdateRevision = 0
|
|
1757
|
+
private var pendingEditorUpdateRetryScheduled = false
|
|
1758
|
+
private var pendingEditorUpdateRetryEditorId: UInt64?
|
|
1759
|
+
private var pendingEditorUpdateRetryGeneration: UInt64 = 0
|
|
1760
|
+
private var pendingViewCommandUpdateJSON: String?
|
|
1761
|
+
private var pendingViewCommandUpdateEditorId: UInt64?
|
|
1762
|
+
private var pendingViewCommandUpdateRetryScheduled = false
|
|
1763
|
+
private var pendingViewCommandUpdateRetryGeneration: UInt64 = 0
|
|
1764
|
+
private var pendingEditableRetryValue: Bool?
|
|
1765
|
+
private var pendingEditableRetryEditorId: UInt64?
|
|
1766
|
+
private var pendingEditableRetryScheduled = false
|
|
1767
|
+
private var pendingEditableRetryGeneration: UInt64 = 0
|
|
1768
|
+
private var pendingThemeRetryJSON: String?
|
|
1769
|
+
private var pendingThemeRetryEditorId: UInt64?
|
|
1770
|
+
private var pendingThemeRetryScheduled = false
|
|
1771
|
+
private var pendingThemeRetryGeneration: UInt64 = 0
|
|
1772
|
+
private var pendingAccessoryRetryActions: [PendingAccessoryRetryAction] = []
|
|
1773
|
+
private var invalidatedAccessoryRetryActions = Set<PendingAccessoryRetryAction>()
|
|
1774
|
+
private var pendingAccessoryRetryEditorId: UInt64?
|
|
1775
|
+
private var pendingAccessoryRetryScheduled = false
|
|
1776
|
+
private var pendingAccessoryRetryGeneration: UInt64 = 0
|
|
1777
|
+
private var pendingMentionSuggestionRetry: PendingMentionSuggestionRetry?
|
|
1778
|
+
private var pendingMentionSuggestionRetryScheduled = false
|
|
1779
|
+
private var pendingMentionSuggestionRetryGeneration: UInt64 = 0
|
|
1649
1780
|
private lazy var outsideTapGestureRecognizer: UITapGestureRecognizer = {
|
|
1650
1781
|
let recognizer = UITapGestureRecognizer(
|
|
1651
1782
|
target: self,
|
|
@@ -1672,6 +1803,31 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1672
1803
|
let onAddonEvent = EventDispatcher()
|
|
1673
1804
|
private var lastEmittedContentHeight: CGFloat = 0
|
|
1674
1805
|
private var cachedAutoGrowContentHeight: CGFloat = 0
|
|
1806
|
+
private var lastAddonEventJSONForTestingValue: String?
|
|
1807
|
+
|
|
1808
|
+
private enum PendingAccessoryRetryAction: Hashable {
|
|
1809
|
+
case reloadInputViews
|
|
1810
|
+
case refreshMentionQuery
|
|
1811
|
+
case clearMentionQueryState
|
|
1812
|
+
case updateAccessoryToolbarVisibility
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
private struct PendingMentionSuggestionRetry {
|
|
1816
|
+
let suggestionKey: String
|
|
1817
|
+
let editorId: UInt64
|
|
1818
|
+
let trigger: String
|
|
1819
|
+
let query: String
|
|
1820
|
+
let anchor: UInt32
|
|
1821
|
+
let head: UInt32
|
|
1822
|
+
let documentVersion: Int?
|
|
1823
|
+
let textSnapshot: String
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
private struct MentionRetryTextDiff {
|
|
1827
|
+
let start: Int
|
|
1828
|
+
let oldEnd: Int
|
|
1829
|
+
let newEnd: Int
|
|
1830
|
+
}
|
|
1675
1831
|
|
|
1676
1832
|
// MARK: - Initialization
|
|
1677
1833
|
|
|
@@ -1705,6 +1861,7 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1705
1861
|
}
|
|
1706
1862
|
|
|
1707
1863
|
deinit {
|
|
1864
|
+
NativeEditorViewRegistry.shared.unregister(editorId: richTextView.editorId, view: self)
|
|
1708
1865
|
NotificationCenter.default.removeObserver(self)
|
|
1709
1866
|
}
|
|
1710
1867
|
|
|
@@ -1743,8 +1900,65 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1743
1900
|
|
|
1744
1901
|
// MARK: - Editor Binding
|
|
1745
1902
|
|
|
1903
|
+
func handleEditorDestroyed(_ editorId: UInt64) {
|
|
1904
|
+
guard editorId != 0 else { return }
|
|
1905
|
+
guard richTextView.editorId == editorId || richTextView.textView.editorId == editorId else {
|
|
1906
|
+
NativeEditorViewRegistry.shared.unregister(editorId: editorId, view: self)
|
|
1907
|
+
return
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
NativeEditorViewRegistry.shared.unregister(editorId: editorId, view: self)
|
|
1911
|
+
clearPendingEditorUpdateRetries()
|
|
1912
|
+
clearPendingViewCommandUpdateRetry()
|
|
1913
|
+
clearPendingEditableRetry()
|
|
1914
|
+
clearPendingThemeRetry()
|
|
1915
|
+
clearPendingAccessoryRetry()
|
|
1916
|
+
clearPendingMentionSuggestionRetry()
|
|
1917
|
+
lastMentionEventJSON = nil
|
|
1918
|
+
richTextView.textView.resignFirstResponder()
|
|
1919
|
+
richTextView.editorId = 0
|
|
1920
|
+
mentionQueryState = nil
|
|
1921
|
+
_ = accessoryToolbar.setMentionSuggestions([])
|
|
1922
|
+
toolbarState = .empty
|
|
1923
|
+
accessoryToolbar.apply(state: .empty)
|
|
1924
|
+
uninstallOutsideTapRecognizer()
|
|
1925
|
+
refreshSystemAssistantToolbarIfNeeded()
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1746
1928
|
func setEditorId(_ id: UInt64) {
|
|
1929
|
+
let previousEditorId = richTextView.editorId
|
|
1930
|
+
if id != 0 && NativeEditorViewRegistry.shared.isDestroyed(editorId: id) {
|
|
1931
|
+
if previousEditorId == id {
|
|
1932
|
+
handleEditorDestroyed(id)
|
|
1933
|
+
} else {
|
|
1934
|
+
setEditorId(0)
|
|
1935
|
+
}
|
|
1936
|
+
return
|
|
1937
|
+
}
|
|
1938
|
+
guard previousEditorId != id else {
|
|
1939
|
+
if id != 0 {
|
|
1940
|
+
if !NativeEditorViewRegistry.shared.register(editorId: id, view: self) {
|
|
1941
|
+
handleEditorDestroyed(id)
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
return
|
|
1945
|
+
}
|
|
1946
|
+
if previousEditorId != id {
|
|
1947
|
+
NativeEditorViewRegistry.shared.unregister(editorId: previousEditorId, view: self)
|
|
1948
|
+
clearPendingEditorUpdateRetries()
|
|
1949
|
+
clearPendingViewCommandUpdateRetry()
|
|
1950
|
+
clearPendingEditableRetry()
|
|
1951
|
+
clearPendingThemeRetry()
|
|
1952
|
+
clearPendingAccessoryRetry()
|
|
1953
|
+
clearPendingMentionSuggestionRetry()
|
|
1954
|
+
}
|
|
1747
1955
|
richTextView.editorId = id
|
|
1956
|
+
if id != 0 {
|
|
1957
|
+
guard NativeEditorViewRegistry.shared.register(editorId: id, view: self) else {
|
|
1958
|
+
handleEditorDestroyed(id)
|
|
1959
|
+
return
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1748
1962
|
if id != 0 {
|
|
1749
1963
|
let stateJSON = editorGetCurrentState(id: id)
|
|
1750
1964
|
if let state = NativeToolbarState(updateJSON: stateJSON) {
|
|
@@ -1758,25 +1972,225 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1758
1972
|
toolbarState = .empty
|
|
1759
1973
|
accessoryToolbar.apply(state: .empty)
|
|
1760
1974
|
}
|
|
1975
|
+
if desiredThemeJSON != lastThemeJSON {
|
|
1976
|
+
setThemeJson(desiredThemeJSON)
|
|
1977
|
+
}
|
|
1761
1978
|
refreshSystemAssistantToolbarIfNeeded()
|
|
1762
1979
|
refreshMentionQuery()
|
|
1763
1980
|
}
|
|
1764
1981
|
|
|
1765
1982
|
func setThemeJson(_ themeJson: String?) {
|
|
1766
|
-
|
|
1767
|
-
lastThemeJSON
|
|
1983
|
+
desiredThemeJSON = themeJson
|
|
1984
|
+
guard lastThemeJSON != themeJson else {
|
|
1985
|
+
clearPendingThemeRetry()
|
|
1986
|
+
return
|
|
1987
|
+
}
|
|
1768
1988
|
let theme = EditorTheme.from(json: themeJson)
|
|
1769
|
-
richTextView.applyTheme(theme)
|
|
1989
|
+
guard richTextView.applyTheme(theme) else {
|
|
1990
|
+
scheduleThemeRetry(themeJson)
|
|
1991
|
+
return
|
|
1992
|
+
}
|
|
1993
|
+
lastThemeJSON = themeJson
|
|
1994
|
+
clearPendingThemeRetry()
|
|
1770
1995
|
accessoryToolbar.apply(theme: theme?.toolbar)
|
|
1771
1996
|
accessoryToolbar.apply(mentionTheme: theme?.mentions ?? addons.mentions?.theme)
|
|
1772
1997
|
refreshSystemAssistantToolbarIfNeeded()
|
|
1773
1998
|
if richTextView.textView.isFirstResponder,
|
|
1774
1999
|
(richTextView.textView.inputAccessoryView === accessoryToolbar || shouldUseSystemAssistantToolbar)
|
|
1775
2000
|
{
|
|
1776
|
-
|
|
2001
|
+
reloadInputViewsAfterPreparingOrRetry()
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
private func clearPendingEditorUpdateRetries() {
|
|
2006
|
+
pendingEditorUpdateJSON = nil
|
|
2007
|
+
pendingEditorUpdateRevision = 0
|
|
2008
|
+
appliedEditorUpdateRevision = 0
|
|
2009
|
+
pendingEditorUpdateRetryScheduled = false
|
|
2010
|
+
pendingEditorUpdateRetryEditorId = nil
|
|
2011
|
+
pendingEditorUpdateRetryGeneration &+= 1
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
private func clearPendingViewCommandUpdateRetry() {
|
|
2015
|
+
pendingViewCommandUpdateJSON = nil
|
|
2016
|
+
pendingViewCommandUpdateEditorId = nil
|
|
2017
|
+
pendingViewCommandUpdateRetryScheduled = false
|
|
2018
|
+
pendingViewCommandUpdateRetryGeneration &+= 1
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
private func clearPendingEditableRetry() {
|
|
2022
|
+
pendingEditableRetryValue = nil
|
|
2023
|
+
pendingEditableRetryEditorId = nil
|
|
2024
|
+
pendingEditableRetryScheduled = false
|
|
2025
|
+
pendingEditableRetryGeneration &+= 1
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
private func clearPendingThemeRetry() {
|
|
2029
|
+
pendingThemeRetryJSON = nil
|
|
2030
|
+
pendingThemeRetryEditorId = nil
|
|
2031
|
+
pendingThemeRetryScheduled = false
|
|
2032
|
+
pendingThemeRetryGeneration &+= 1
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
private func clearPendingAccessoryRetry() {
|
|
2036
|
+
pendingAccessoryRetryActions = []
|
|
2037
|
+
invalidatedAccessoryRetryActions.removeAll()
|
|
2038
|
+
pendingAccessoryRetryEditorId = nil
|
|
2039
|
+
pendingAccessoryRetryScheduled = false
|
|
2040
|
+
pendingAccessoryRetryGeneration &+= 1
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
private func clearPendingMentionSuggestionRetry() {
|
|
2044
|
+
pendingMentionSuggestionRetry = nil
|
|
2045
|
+
pendingMentionSuggestionRetryScheduled = false
|
|
2046
|
+
pendingMentionSuggestionRetryGeneration &+= 1
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
private func scheduleThemeRetry(_ themeJson: String?) {
|
|
2050
|
+
pendingThemeRetryJSON = themeJson
|
|
2051
|
+
pendingThemeRetryEditorId = richTextView.editorId
|
|
2052
|
+
guard !pendingThemeRetryScheduled else { return }
|
|
2053
|
+
pendingThemeRetryScheduled = true
|
|
2054
|
+
pendingThemeRetryGeneration &+= 1
|
|
2055
|
+
let retryGeneration = pendingThemeRetryGeneration
|
|
2056
|
+
DispatchQueue.main.async { [weak self] in
|
|
2057
|
+
guard let self else { return }
|
|
2058
|
+
guard retryGeneration == self.pendingThemeRetryGeneration else { return }
|
|
2059
|
+
let retryJSON = self.pendingThemeRetryJSON
|
|
2060
|
+
self.pendingThemeRetryJSON = nil
|
|
2061
|
+
let retryEditorId = self.pendingThemeRetryEditorId
|
|
2062
|
+
self.pendingThemeRetryEditorId = nil
|
|
2063
|
+
self.pendingThemeRetryScheduled = false
|
|
2064
|
+
guard retryEditorId == self.richTextView.editorId else {
|
|
2065
|
+
self.clearPendingThemeRetry()
|
|
2066
|
+
return
|
|
2067
|
+
}
|
|
2068
|
+
guard retryJSON == self.desiredThemeJSON else {
|
|
2069
|
+
self.clearPendingThemeRetry()
|
|
2070
|
+
return
|
|
2071
|
+
}
|
|
2072
|
+
self.setThemeJson(retryJSON)
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
private func prepareForInputAccessoryMutationOrRetry(_ action: PendingAccessoryRetryAction) -> Bool {
|
|
2077
|
+
guard richTextView.editorId != 0, richTextView.textView.isFirstResponder else {
|
|
2078
|
+
return true
|
|
2079
|
+
}
|
|
2080
|
+
guard richTextView.textView.prepareForExternalEditorUpdate() else {
|
|
2081
|
+
scheduleAccessoryRetry(action)
|
|
2082
|
+
return false
|
|
2083
|
+
}
|
|
2084
|
+
return true
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
private func reloadInputViewsAfterPreparingOrRetry() {
|
|
2088
|
+
guard prepareForInputAccessoryMutationOrRetry(.reloadInputViews) else { return }
|
|
2089
|
+
richTextView.textView.reloadInputViews()
|
|
2090
|
+
markAccessoryMutationSucceeded(.reloadInputViews)
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
private func scheduleAccessoryRetry(_ action: PendingAccessoryRetryAction) {
|
|
2094
|
+
invalidatedAccessoryRetryActions.remove(action)
|
|
2095
|
+
pendingAccessoryRetryActions.removeAll { $0 == action }
|
|
2096
|
+
pendingAccessoryRetryActions.append(action)
|
|
2097
|
+
pendingAccessoryRetryEditorId = richTextView.editorId
|
|
2098
|
+
guard !pendingAccessoryRetryScheduled else { return }
|
|
2099
|
+
pendingAccessoryRetryScheduled = true
|
|
2100
|
+
pendingAccessoryRetryGeneration &+= 1
|
|
2101
|
+
let retryGeneration = pendingAccessoryRetryGeneration
|
|
2102
|
+
DispatchQueue.main.async { [weak self] in
|
|
2103
|
+
guard let self else { return }
|
|
2104
|
+
guard retryGeneration == self.pendingAccessoryRetryGeneration else { return }
|
|
2105
|
+
guard self.pendingAccessoryRetryEditorId == self.richTextView.editorId else {
|
|
2106
|
+
self.clearPendingAccessoryRetry()
|
|
2107
|
+
return
|
|
2108
|
+
}
|
|
2109
|
+
let actions = self.pendingAccessoryRetryActions
|
|
2110
|
+
self.pendingAccessoryRetryActions = []
|
|
2111
|
+
self.pendingAccessoryRetryEditorId = nil
|
|
2112
|
+
self.pendingAccessoryRetryScheduled = false
|
|
2113
|
+
for index in actions.indices {
|
|
2114
|
+
let action = actions[index]
|
|
2115
|
+
guard retryGeneration == self.pendingAccessoryRetryGeneration else { return }
|
|
2116
|
+
guard !self.invalidatedAccessoryRetryActions.contains(action) else {
|
|
2117
|
+
self.invalidatedAccessoryRetryActions.remove(action)
|
|
2118
|
+
continue
|
|
2119
|
+
}
|
|
2120
|
+
let generationBeforeAction = self.pendingAccessoryRetryGeneration
|
|
2121
|
+
self.performAccessoryRetryAction(action)
|
|
2122
|
+
guard self.pendingAccessoryRetryGeneration == generationBeforeAction else {
|
|
2123
|
+
let remainingIndex = actions.index(after: index)
|
|
2124
|
+
if remainingIndex < actions.endIndex {
|
|
2125
|
+
self.requeueUnprocessedAccessoryRetryActions(actions[remainingIndex...])
|
|
2126
|
+
}
|
|
2127
|
+
return
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
self.invalidatedAccessoryRetryActions.subtract(actions)
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
private func requeueUnprocessedAccessoryRetryActions(
|
|
2135
|
+
_ actions: ArraySlice<PendingAccessoryRetryAction>
|
|
2136
|
+
) {
|
|
2137
|
+
for action in actions {
|
|
2138
|
+
guard !invalidatedAccessoryRetryActions.contains(action) else {
|
|
2139
|
+
invalidatedAccessoryRetryActions.remove(action)
|
|
2140
|
+
continue
|
|
2141
|
+
}
|
|
2142
|
+
pendingAccessoryRetryActions.removeAll { $0 == action }
|
|
2143
|
+
pendingAccessoryRetryActions.append(action)
|
|
2144
|
+
}
|
|
2145
|
+
if !pendingAccessoryRetryActions.isEmpty {
|
|
2146
|
+
pendingAccessoryRetryEditorId = richTextView.editorId
|
|
1777
2147
|
}
|
|
1778
2148
|
}
|
|
1779
2149
|
|
|
2150
|
+
private func performAccessoryRetryAction(_ action: PendingAccessoryRetryAction) {
|
|
2151
|
+
switch action {
|
|
2152
|
+
case .reloadInputViews:
|
|
2153
|
+
reloadInputViewsAfterPreparingOrRetry()
|
|
2154
|
+
case .refreshMentionQuery:
|
|
2155
|
+
refreshMentionQuery()
|
|
2156
|
+
case .clearMentionQueryState:
|
|
2157
|
+
clearMentionQueryStateAndHidePopover()
|
|
2158
|
+
case .updateAccessoryToolbarVisibility:
|
|
2159
|
+
updateAccessoryToolbarVisibility()
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
private func markAccessoryMutationSucceeded(_ action: PendingAccessoryRetryAction) {
|
|
2164
|
+
var invalidated: Set<PendingAccessoryRetryAction> = [action]
|
|
2165
|
+
switch action {
|
|
2166
|
+
case .refreshMentionQuery:
|
|
2167
|
+
invalidated.insert(.clearMentionQueryState)
|
|
2168
|
+
case .clearMentionQueryState:
|
|
2169
|
+
if !hasActiveMentionQueryForCurrentAddons() {
|
|
2170
|
+
invalidated.insert(.refreshMentionQuery)
|
|
2171
|
+
}
|
|
2172
|
+
case .reloadInputViews, .updateAccessoryToolbarVisibility:
|
|
2173
|
+
break
|
|
2174
|
+
}
|
|
2175
|
+
invalidatePendingAccessoryRetries(invalidated)
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
private func invalidatePendingAccessoryRetries(_ actions: Set<PendingAccessoryRetryAction>) {
|
|
2179
|
+
guard !actions.isEmpty else { return }
|
|
2180
|
+
invalidatedAccessoryRetryActions.formUnion(actions)
|
|
2181
|
+
pendingAccessoryRetryActions.removeAll { actions.contains($0) }
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
private func hasActiveMentionQueryForCurrentAddons() -> Bool {
|
|
2185
|
+
guard richTextView.editorId != 0,
|
|
2186
|
+
richTextView.textView.isFirstResponder,
|
|
2187
|
+
let mentions = addons.mentions
|
|
2188
|
+
else {
|
|
2189
|
+
return false
|
|
2190
|
+
}
|
|
2191
|
+
return currentMentionQueryState(trigger: mentions.trigger) != nil
|
|
2192
|
+
}
|
|
2193
|
+
|
|
1780
2194
|
func setAddonsJson(_ addonsJson: String?) {
|
|
1781
2195
|
guard lastAddonsJSON != addonsJson else { return }
|
|
1782
2196
|
lastAddonsJSON = addonsJson
|
|
@@ -1792,10 +2206,46 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1792
2206
|
}
|
|
1793
2207
|
|
|
1794
2208
|
func setEditable(_ editable: Bool) {
|
|
2209
|
+
if !editable,
|
|
2210
|
+
richTextView.textView.isEditable,
|
|
2211
|
+
richTextView.editorId != 0,
|
|
2212
|
+
!richTextView.textView.prepareForExternalEditorUpdate()
|
|
2213
|
+
{
|
|
2214
|
+
scheduleEditableRetry(editable)
|
|
2215
|
+
return
|
|
2216
|
+
}
|
|
2217
|
+
pendingEditableRetryValue = nil
|
|
2218
|
+
pendingEditableRetryEditorId = nil
|
|
2219
|
+
pendingEditableRetryScheduled = false
|
|
1795
2220
|
richTextView.textView.isEditable = editable
|
|
1796
2221
|
updateAccessoryToolbarVisibility()
|
|
1797
2222
|
}
|
|
1798
2223
|
|
|
2224
|
+
private func scheduleEditableRetry(_ editable: Bool) {
|
|
2225
|
+
pendingEditableRetryValue = editable
|
|
2226
|
+
pendingEditableRetryEditorId = richTextView.editorId
|
|
2227
|
+
guard !pendingEditableRetryScheduled else { return }
|
|
2228
|
+
pendingEditableRetryScheduled = true
|
|
2229
|
+
pendingEditableRetryGeneration &+= 1
|
|
2230
|
+
let retryGeneration = pendingEditableRetryGeneration
|
|
2231
|
+
DispatchQueue.main.async { [weak self] in
|
|
2232
|
+
guard let self else { return }
|
|
2233
|
+
guard retryGeneration == self.pendingEditableRetryGeneration else { return }
|
|
2234
|
+
guard let pendingEditable = self.pendingEditableRetryValue else {
|
|
2235
|
+
self.pendingEditableRetryScheduled = false
|
|
2236
|
+
return
|
|
2237
|
+
}
|
|
2238
|
+
guard self.pendingEditableRetryEditorId == self.richTextView.editorId else {
|
|
2239
|
+
self.clearPendingEditableRetry()
|
|
2240
|
+
return
|
|
2241
|
+
}
|
|
2242
|
+
self.pendingEditableRetryValue = nil
|
|
2243
|
+
self.pendingEditableRetryEditorId = nil
|
|
2244
|
+
self.pendingEditableRetryScheduled = false
|
|
2245
|
+
self.setEditable(pendingEditable)
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
|
|
1799
2249
|
func setAutoFocus(_ autoFocus: Bool) {
|
|
1800
2250
|
guard autoFocus, !didApplyAutoFocus else { return }
|
|
1801
2251
|
didApplyAutoFocus = true
|
|
@@ -1908,18 +2358,96 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1908
2358
|
guard pendingEditorUpdateRevision != 0 else { return }
|
|
1909
2359
|
guard pendingEditorUpdateRevision != appliedEditorUpdateRevision else { return }
|
|
1910
2360
|
guard let updateJSON = pendingEditorUpdateJSON else { return }
|
|
2361
|
+
guard applyEditorUpdate(updateJSON) else {
|
|
2362
|
+
schedulePendingEditorUpdateRetry()
|
|
2363
|
+
return
|
|
2364
|
+
}
|
|
1911
2365
|
appliedEditorUpdateRevision = pendingEditorUpdateRevision
|
|
1912
|
-
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
private func schedulePendingEditorUpdateRetry() {
|
|
2369
|
+
guard !pendingEditorUpdateRetryScheduled else { return }
|
|
2370
|
+
pendingEditorUpdateRetryEditorId = richTextView.editorId
|
|
2371
|
+
pendingEditorUpdateRetryScheduled = true
|
|
2372
|
+
pendingEditorUpdateRetryGeneration &+= 1
|
|
2373
|
+
let retryGeneration = pendingEditorUpdateRetryGeneration
|
|
2374
|
+
DispatchQueue.main.async { [weak self] in
|
|
2375
|
+
guard let self else { return }
|
|
2376
|
+
guard retryGeneration == self.pendingEditorUpdateRetryGeneration else {
|
|
2377
|
+
return
|
|
2378
|
+
}
|
|
2379
|
+
guard self.pendingEditorUpdateRetryEditorId == self.richTextView.editorId else {
|
|
2380
|
+
self.pendingEditorUpdateRetryScheduled = false
|
|
2381
|
+
self.clearPendingEditorUpdateRetries()
|
|
2382
|
+
return
|
|
2383
|
+
}
|
|
2384
|
+
self.pendingEditorUpdateRetryScheduled = false
|
|
2385
|
+
self.pendingEditorUpdateRetryEditorId = nil
|
|
2386
|
+
self.applyPendingEditorUpdateIfNeeded()
|
|
2387
|
+
}
|
|
1913
2388
|
}
|
|
1914
2389
|
|
|
1915
2390
|
// MARK: - View Commands
|
|
1916
2391
|
|
|
1917
2392
|
/// Apply an editor update from JS. Sets the echo-suppression flag so the
|
|
1918
2393
|
/// resulting delegate callback is NOT re-dispatched back to JS.
|
|
1919
|
-
|
|
2394
|
+
@discardableResult
|
|
2395
|
+
func applyEditorUpdate(_ updateJson: String) -> Bool {
|
|
2396
|
+
guard richTextView.textView.prepareForExternalEditorUpdate() else {
|
|
2397
|
+
scheduleViewCommandUpdateRetry(updateJson)
|
|
2398
|
+
return false
|
|
2399
|
+
}
|
|
1920
2400
|
isApplyingJSUpdate = true
|
|
2401
|
+
defer { isApplyingJSUpdate = false }
|
|
1921
2402
|
richTextView.textView.applyUpdateJSON(updateJson)
|
|
1922
|
-
|
|
2403
|
+
return true
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
private func scheduleViewCommandUpdateRetry(_ updateJson: String) {
|
|
2407
|
+
pendingViewCommandUpdateJSON = updateJson
|
|
2408
|
+
pendingViewCommandUpdateEditorId = richTextView.editorId
|
|
2409
|
+
guard !pendingViewCommandUpdateRetryScheduled else { return }
|
|
2410
|
+
pendingViewCommandUpdateRetryScheduled = true
|
|
2411
|
+
pendingViewCommandUpdateRetryGeneration &+= 1
|
|
2412
|
+
let retryGeneration = pendingViewCommandUpdateRetryGeneration
|
|
2413
|
+
DispatchQueue.main.async { [weak self] in
|
|
2414
|
+
guard let self else { return }
|
|
2415
|
+
guard retryGeneration == self.pendingViewCommandUpdateRetryGeneration else {
|
|
2416
|
+
return
|
|
2417
|
+
}
|
|
2418
|
+
guard self.pendingViewCommandUpdateJSON != nil else {
|
|
2419
|
+
self.pendingViewCommandUpdateRetryScheduled = false
|
|
2420
|
+
return
|
|
2421
|
+
}
|
|
2422
|
+
guard self.pendingViewCommandUpdateEditorId == self.richTextView.editorId else {
|
|
2423
|
+
self.pendingViewCommandUpdateJSON = nil
|
|
2424
|
+
self.pendingViewCommandUpdateEditorId = nil
|
|
2425
|
+
self.pendingViewCommandUpdateRetryScheduled = false
|
|
2426
|
+
return
|
|
2427
|
+
}
|
|
2428
|
+
guard self.richTextView.editorId != 0 else {
|
|
2429
|
+
self.pendingViewCommandUpdateJSON = nil
|
|
2430
|
+
self.pendingViewCommandUpdateEditorId = nil
|
|
2431
|
+
self.pendingViewCommandUpdateRetryScheduled = false
|
|
2432
|
+
return
|
|
2433
|
+
}
|
|
2434
|
+
self.pendingViewCommandUpdateJSON = nil
|
|
2435
|
+
self.pendingViewCommandUpdateEditorId = nil
|
|
2436
|
+
self.pendingViewCommandUpdateRetryScheduled = false
|
|
2437
|
+
let updateJSON = editorGetCurrentState(id: self.richTextView.editorId)
|
|
2438
|
+
_ = self.applyEditorUpdate(updateJSON)
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
func prepareForEditorCommandJSON() -> String {
|
|
2443
|
+
isApplyingJSUpdate = true
|
|
2444
|
+
defer { isApplyingJSUpdate = false }
|
|
2445
|
+
let preparation = richTextView.textView.prepareForExternalEditorCommand()
|
|
2446
|
+
return NativeEditorViewRegistry.commandPreparationJSON(
|
|
2447
|
+
ready: preparation.ready,
|
|
2448
|
+
updateJSON: preparation.updateJSON,
|
|
2449
|
+
blockedReason: preparation.blockedReason
|
|
2450
|
+
)
|
|
1923
2451
|
}
|
|
1924
2452
|
|
|
1925
2453
|
// MARK: - Focus Commands
|
|
@@ -2111,6 +2639,7 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
2111
2639
|
clearMentionQueryStateAndHidePopover()
|
|
2112
2640
|
return
|
|
2113
2641
|
}
|
|
2642
|
+
guard prepareForInputAccessoryMutationOrRetry(.refreshMentionQuery) else { return }
|
|
2114
2643
|
|
|
2115
2644
|
guard let queryState = currentMentionQueryState(trigger: mentions.trigger) else {
|
|
2116
2645
|
emitMentionQueryChange(query: "", trigger: mentions.trigger, anchor: 0, head: 0, isActive: false)
|
|
@@ -2129,6 +2658,7 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
2129
2658
|
{
|
|
2130
2659
|
richTextView.textView.reloadInputViews()
|
|
2131
2660
|
}
|
|
2661
|
+
markAccessoryMutationSucceeded(.refreshMentionQuery)
|
|
2132
2662
|
emitMentionQueryChange(
|
|
2133
2663
|
query: queryState.query,
|
|
2134
2664
|
trigger: queryState.trigger,
|
|
@@ -2139,6 +2669,7 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
2139
2669
|
}
|
|
2140
2670
|
|
|
2141
2671
|
private func clearMentionQueryStateAndHidePopover() {
|
|
2672
|
+
guard prepareForInputAccessoryMutationOrRetry(.clearMentionQueryState) else { return }
|
|
2142
2673
|
mentionQueryState = nil
|
|
2143
2674
|
let didChangeToolbarHeight = accessoryToolbar.setMentionSuggestions([])
|
|
2144
2675
|
refreshSystemAssistantToolbarIfNeeded()
|
|
@@ -2148,6 +2679,7 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
2148
2679
|
{
|
|
2149
2680
|
richTextView.textView.reloadInputViews()
|
|
2150
2681
|
}
|
|
2682
|
+
markAccessoryMutationSucceeded(.clearMentionQueryState)
|
|
2151
2683
|
}
|
|
2152
2684
|
|
|
2153
2685
|
private func emitMentionQueryChange(
|
|
@@ -2174,7 +2706,7 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
2174
2706
|
}
|
|
2175
2707
|
guard json != lastMentionEventJSON else { return }
|
|
2176
2708
|
lastMentionEventJSON = json
|
|
2177
|
-
|
|
2709
|
+
dispatchAddonEvent(json)
|
|
2178
2710
|
}
|
|
2179
2711
|
|
|
2180
2712
|
private func resolvedMentionAttrs(
|
|
@@ -2207,16 +2739,17 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
2207
2739
|
else {
|
|
2208
2740
|
return
|
|
2209
2741
|
}
|
|
2210
|
-
|
|
2742
|
+
dispatchAddonEvent(json)
|
|
2211
2743
|
}
|
|
2212
2744
|
|
|
2213
2745
|
private func emitMentionSelectRequest(
|
|
2214
2746
|
trigger: String,
|
|
2215
2747
|
suggestion: NativeMentionSuggestion,
|
|
2216
2748
|
attrs: [String: Any],
|
|
2217
|
-
range: MentionQueryState
|
|
2749
|
+
range: MentionQueryState,
|
|
2750
|
+
preflightUpdateJSON: String? = nil
|
|
2218
2751
|
) {
|
|
2219
|
-
|
|
2752
|
+
var payload: [String: Any] = [
|
|
2220
2753
|
"type": "mentionsSelectRequest",
|
|
2221
2754
|
"trigger": trigger,
|
|
2222
2755
|
"suggestionKey": suggestion.key,
|
|
@@ -2226,14 +2759,41 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
2226
2759
|
"head": Int(range.head),
|
|
2227
2760
|
],
|
|
2228
2761
|
]
|
|
2762
|
+
if let preflightUpdateJSON {
|
|
2763
|
+
payload["updateJson"] = preflightUpdateJSON
|
|
2764
|
+
}
|
|
2765
|
+
if let documentVersion = documentVersion(fromUpdateJSON: preflightUpdateJSON) {
|
|
2766
|
+
payload["documentVersion"] = documentVersion
|
|
2767
|
+
}
|
|
2229
2768
|
guard let data = try? JSONSerialization.data(withJSONObject: payload),
|
|
2230
2769
|
let json = String(data: data, encoding: .utf8)
|
|
2231
2770
|
else {
|
|
2232
2771
|
return
|
|
2233
2772
|
}
|
|
2773
|
+
dispatchAddonEvent(json)
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
private func dispatchAddonEvent(_ json: String) {
|
|
2777
|
+
lastAddonEventJSONForTestingValue = json
|
|
2234
2778
|
onAddonEvent(["eventJson": json])
|
|
2235
2779
|
}
|
|
2236
2780
|
|
|
2781
|
+
private func documentVersion(fromUpdateJSON updateJSON: String?) -> Int? {
|
|
2782
|
+
guard let updateJSON,
|
|
2783
|
+
let data = updateJSON.data(using: .utf8),
|
|
2784
|
+
let raw = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
2785
|
+
else {
|
|
2786
|
+
return nil
|
|
2787
|
+
}
|
|
2788
|
+
if let version = raw["documentVersion"] as? Int {
|
|
2789
|
+
return version
|
|
2790
|
+
}
|
|
2791
|
+
if let number = raw["documentVersion"] as? NSNumber {
|
|
2792
|
+
return number.intValue
|
|
2793
|
+
}
|
|
2794
|
+
return nil
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2237
2797
|
private func filteredMentionSuggestions(
|
|
2238
2798
|
for queryState: MentionQueryState,
|
|
2239
2799
|
config: NativeMentionsAddonConfig
|
|
@@ -2325,20 +2885,90 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
2325
2885
|
return false
|
|
2326
2886
|
}
|
|
2327
2887
|
|
|
2328
|
-
private func insertMentionSuggestion(
|
|
2888
|
+
private func insertMentionSuggestion(
|
|
2889
|
+
_ suggestion: NativeMentionSuggestion
|
|
2890
|
+
) {
|
|
2891
|
+
insertMentionSuggestion(suggestionKey: suggestion.key)
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
private func insertMentionSuggestion(
|
|
2895
|
+
retryScope: PendingMentionSuggestionRetry
|
|
2896
|
+
) {
|
|
2897
|
+
insertMentionSuggestion(
|
|
2898
|
+
suggestionKey: retryScope.suggestionKey,
|
|
2899
|
+
retryScope: retryScope
|
|
2900
|
+
)
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
private func insertMentionSuggestion(
|
|
2904
|
+
suggestionKey: String,
|
|
2905
|
+
retryScope: PendingMentionSuggestionRetry? = nil
|
|
2906
|
+
) {
|
|
2329
2907
|
guard let mentions = addons.mentions,
|
|
2330
|
-
|
|
2908
|
+
mentionQueryState != nil
|
|
2331
2909
|
else {
|
|
2332
2910
|
return
|
|
2333
2911
|
}
|
|
2912
|
+
if let retryScope,
|
|
2913
|
+
!isMentionSuggestionRetryScopeCurrent(retryScope)
|
|
2914
|
+
{
|
|
2915
|
+
return
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
let scopedQueryState = currentMentionQueryState(trigger: mentions.trigger) ?? mentionQueryState
|
|
2919
|
+
guard let scopedQueryState else {
|
|
2920
|
+
clearMentionQueryStateAndHidePopover()
|
|
2921
|
+
return
|
|
2922
|
+
}
|
|
2923
|
+
let preparation = richTextView.textView.prepareForExternalEditorCommand()
|
|
2924
|
+
guard preparation.ready else {
|
|
2925
|
+
scheduleMentionSuggestionRetry(
|
|
2926
|
+
PendingMentionSuggestionRetry(
|
|
2927
|
+
suggestionKey: suggestionKey,
|
|
2928
|
+
editorId: richTextView.editorId,
|
|
2929
|
+
trigger: mentions.trigger,
|
|
2930
|
+
query: scopedQueryState.query,
|
|
2931
|
+
anchor: scopedQueryState.anchor,
|
|
2932
|
+
head: scopedQueryState.head,
|
|
2933
|
+
documentVersion: currentDocumentVersion(),
|
|
2934
|
+
textSnapshot: richTextView.textView.text ?? ""
|
|
2935
|
+
)
|
|
2936
|
+
)
|
|
2937
|
+
return
|
|
2938
|
+
}
|
|
2939
|
+
let queryState = currentMentionQueryState(trigger: mentions.trigger)
|
|
2940
|
+
?? (richTextView.textView.isFirstResponder ? nil : mentionQueryState)
|
|
2941
|
+
guard let queryState else {
|
|
2942
|
+
clearMentionQueryStateAndHidePopover()
|
|
2943
|
+
return
|
|
2944
|
+
}
|
|
2945
|
+
if let retryScope,
|
|
2946
|
+
!doesMentionQueryState(
|
|
2947
|
+
queryState,
|
|
2948
|
+
match: retryScope,
|
|
2949
|
+
acceptingPreflightDocumentVersion: documentVersion(fromUpdateJSON: preparation.updateJSON),
|
|
2950
|
+
currentText: richTextView.textView.text ?? ""
|
|
2951
|
+
)
|
|
2952
|
+
{
|
|
2953
|
+
return
|
|
2954
|
+
}
|
|
2955
|
+
guard let currentSuggestion = filteredMentionSuggestions(
|
|
2956
|
+
for: queryState,
|
|
2957
|
+
config: mentions
|
|
2958
|
+
).first(where: { $0.key == suggestionKey }) else {
|
|
2959
|
+
clearMentionQueryStateAndHidePopover()
|
|
2960
|
+
return
|
|
2961
|
+
}
|
|
2962
|
+
mentionQueryState = queryState
|
|
2334
2963
|
|
|
2335
|
-
let attrs = resolvedMentionAttrs(trigger: mentions.trigger, suggestion:
|
|
2964
|
+
let attrs = resolvedMentionAttrs(trigger: mentions.trigger, suggestion: currentSuggestion)
|
|
2336
2965
|
if mentions.resolveSelectionAttrs || mentions.resolveTheme {
|
|
2337
2966
|
emitMentionSelectRequest(
|
|
2338
2967
|
trigger: mentions.trigger,
|
|
2339
|
-
suggestion:
|
|
2968
|
+
suggestion: currentSuggestion,
|
|
2340
2969
|
attrs: attrs,
|
|
2341
|
-
range: queryState
|
|
2970
|
+
range: queryState,
|
|
2971
|
+
preflightUpdateJSON: preparation.updateJSON
|
|
2342
2972
|
)
|
|
2343
2973
|
lastMentionEventJSON = nil
|
|
2344
2974
|
clearMentionQueryStateAndHidePopover()
|
|
@@ -2364,11 +2994,184 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
2364
2994
|
json: json
|
|
2365
2995
|
)
|
|
2366
2996
|
richTextView.textView.applyUpdateJSON(updateJSON)
|
|
2367
|
-
emitMentionSelect(trigger: mentions.trigger, suggestion:
|
|
2997
|
+
emitMentionSelect(trigger: mentions.trigger, suggestion: currentSuggestion, attrs: attrs)
|
|
2368
2998
|
lastMentionEventJSON = nil
|
|
2369
2999
|
clearMentionQueryStateAndHidePopover()
|
|
2370
3000
|
}
|
|
2371
3001
|
|
|
3002
|
+
private func scheduleMentionSuggestionRetry(_ retry: PendingMentionSuggestionRetry) {
|
|
3003
|
+
pendingMentionSuggestionRetry = retry
|
|
3004
|
+
guard !pendingMentionSuggestionRetryScheduled else { return }
|
|
3005
|
+
pendingMentionSuggestionRetryScheduled = true
|
|
3006
|
+
pendingMentionSuggestionRetryGeneration &+= 1
|
|
3007
|
+
let retryGeneration = pendingMentionSuggestionRetryGeneration
|
|
3008
|
+
DispatchQueue.main.async { [weak self] in
|
|
3009
|
+
guard let self else { return }
|
|
3010
|
+
guard retryGeneration == self.pendingMentionSuggestionRetryGeneration else { return }
|
|
3011
|
+
guard let retry = self.pendingMentionSuggestionRetry else {
|
|
3012
|
+
self.pendingMentionSuggestionRetryScheduled = false
|
|
3013
|
+
return
|
|
3014
|
+
}
|
|
3015
|
+
guard retry.editorId == self.richTextView.editorId else {
|
|
3016
|
+
self.clearPendingMentionSuggestionRetry()
|
|
3017
|
+
return
|
|
3018
|
+
}
|
|
3019
|
+
self.pendingMentionSuggestionRetry = nil
|
|
3020
|
+
self.pendingMentionSuggestionRetryScheduled = false
|
|
3021
|
+
self.insertMentionSuggestion(retryScope: retry)
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
private func isMentionSuggestionRetryScopeCurrent(
|
|
3026
|
+
_ retry: PendingMentionSuggestionRetry
|
|
3027
|
+
) -> Bool {
|
|
3028
|
+
guard retry.editorId == richTextView.editorId,
|
|
3029
|
+
addons.mentions?.trigger == retry.trigger
|
|
3030
|
+
else {
|
|
3031
|
+
return false
|
|
3032
|
+
}
|
|
3033
|
+
let queryState = currentMentionQueryState(trigger: retry.trigger) ?? mentionQueryState
|
|
3034
|
+
guard let queryState else { return false }
|
|
3035
|
+
guard doesMentionQueryStateMatchRetryIdentity(queryState, match: retry) else {
|
|
3036
|
+
return false
|
|
3037
|
+
}
|
|
3038
|
+
return isMentionSuggestionRetryDocumentVersionCurrent(retry)
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
private func doesMentionQueryState(
|
|
3042
|
+
_ queryState: MentionQueryState,
|
|
3043
|
+
match retry: PendingMentionSuggestionRetry,
|
|
3044
|
+
acceptingPreflightDocumentVersion preflightDocumentVersion: Int? = nil,
|
|
3045
|
+
currentText: String? = nil
|
|
3046
|
+
) -> Bool {
|
|
3047
|
+
guard doesMentionQueryStateMatchRetryIdentity(queryState, match: retry) else {
|
|
3048
|
+
return false
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
let currentVersion = currentDocumentVersion()
|
|
3052
|
+
var acceptedPreflightVersionChange = false
|
|
3053
|
+
if let retryVersion = retry.documentVersion,
|
|
3054
|
+
let currentVersion,
|
|
3055
|
+
currentVersion != retryVersion
|
|
3056
|
+
{
|
|
3057
|
+
guard let preflightDocumentVersion,
|
|
3058
|
+
currentVersion == preflightDocumentVersion
|
|
3059
|
+
else {
|
|
3060
|
+
return false
|
|
3061
|
+
}
|
|
3062
|
+
acceptedPreflightVersionChange = true
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3065
|
+
if queryState.anchor == retry.anchor && queryState.head == retry.head {
|
|
3066
|
+
return true
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
guard acceptedPreflightVersionChange else {
|
|
3070
|
+
return false
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
guard let currentText,
|
|
3074
|
+
let diff = mentionRetryTextDiff(
|
|
3075
|
+
from: retry.textSnapshot,
|
|
3076
|
+
to: currentText
|
|
3077
|
+
),
|
|
3078
|
+
let mappedRange = mappedMentionRetryRange(retry, through: diff)
|
|
3079
|
+
else {
|
|
3080
|
+
return false
|
|
3081
|
+
}
|
|
3082
|
+
|
|
3083
|
+
return queryState.anchor == mappedRange.anchor && queryState.head == mappedRange.head
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
private func doesMentionQueryStateMatchRetryIdentity(
|
|
3087
|
+
_ queryState: MentionQueryState,
|
|
3088
|
+
match retry: PendingMentionSuggestionRetry
|
|
3089
|
+
) -> Bool {
|
|
3090
|
+
queryState.trigger == retry.trigger && queryState.query == retry.query
|
|
3091
|
+
}
|
|
3092
|
+
|
|
3093
|
+
private func isMentionSuggestionRetryDocumentVersionCurrent(
|
|
3094
|
+
_ retry: PendingMentionSuggestionRetry
|
|
3095
|
+
) -> Bool {
|
|
3096
|
+
let currentVersion = currentDocumentVersion()
|
|
3097
|
+
if let retryVersion = retry.documentVersion,
|
|
3098
|
+
let currentVersion,
|
|
3099
|
+
currentVersion != retryVersion
|
|
3100
|
+
{
|
|
3101
|
+
return false
|
|
3102
|
+
}
|
|
3103
|
+
return true
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
private func mentionRetryTextDiff(
|
|
3107
|
+
from oldText: String,
|
|
3108
|
+
to newText: String
|
|
3109
|
+
) -> MentionRetryTextDiff? {
|
|
3110
|
+
let oldScalars = Array(oldText.unicodeScalars)
|
|
3111
|
+
let newScalars = Array(newText.unicodeScalars)
|
|
3112
|
+
let sharedLength = min(oldScalars.count, newScalars.count)
|
|
3113
|
+
|
|
3114
|
+
var prefix = 0
|
|
3115
|
+
while prefix < sharedLength,
|
|
3116
|
+
oldScalars[prefix] == newScalars[prefix]
|
|
3117
|
+
{
|
|
3118
|
+
prefix += 1
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
var oldEnd = oldScalars.count
|
|
3122
|
+
var newEnd = newScalars.count
|
|
3123
|
+
while oldEnd > prefix,
|
|
3124
|
+
newEnd > prefix,
|
|
3125
|
+
oldScalars[oldEnd - 1] == newScalars[newEnd - 1]
|
|
3126
|
+
{
|
|
3127
|
+
oldEnd -= 1
|
|
3128
|
+
newEnd -= 1
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3131
|
+
guard prefix != oldEnd || prefix != newEnd else {
|
|
3132
|
+
return nil
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
return MentionRetryTextDiff(
|
|
3136
|
+
start: prefix,
|
|
3137
|
+
oldEnd: oldEnd,
|
|
3138
|
+
newEnd: newEnd
|
|
3139
|
+
)
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
private func mappedMentionRetryRange(
|
|
3143
|
+
_ retry: PendingMentionSuggestionRetry,
|
|
3144
|
+
through diff: MentionRetryTextDiff
|
|
3145
|
+
) -> (anchor: UInt32, head: UInt32)? {
|
|
3146
|
+
let anchor = Int(retry.anchor)
|
|
3147
|
+
let head = Int(retry.head)
|
|
3148
|
+
guard anchor <= head else { return nil }
|
|
3149
|
+
|
|
3150
|
+
if head <= diff.start {
|
|
3151
|
+
return (retry.anchor, retry.head)
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
if anchor >= diff.oldEnd {
|
|
3155
|
+
let delta = diff.newEnd - diff.oldEnd
|
|
3156
|
+
let mappedAnchor = anchor + delta
|
|
3157
|
+
let mappedHead = head + delta
|
|
3158
|
+
guard mappedAnchor >= 0,
|
|
3159
|
+
mappedHead >= mappedAnchor,
|
|
3160
|
+
mappedHead <= Int(UInt32.max)
|
|
3161
|
+
else {
|
|
3162
|
+
return nil
|
|
3163
|
+
}
|
|
3164
|
+
return (UInt32(mappedAnchor), UInt32(mappedHead))
|
|
3165
|
+
}
|
|
3166
|
+
|
|
3167
|
+
return nil
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
private func currentDocumentVersion() -> Int? {
|
|
3171
|
+
guard richTextView.editorId != 0 else { return nil }
|
|
3172
|
+
return documentVersion(fromUpdateJSON: editorGetCurrentState(id: richTextView.editorId))
|
|
3173
|
+
}
|
|
3174
|
+
|
|
2372
3175
|
func setMentionQueryStateForTesting(_ state: MentionQueryState?) {
|
|
2373
3176
|
mentionQueryState = state
|
|
2374
3177
|
}
|
|
@@ -2381,6 +3184,14 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
2381
3184
|
accessoryToolbar.setMentionSuggestions(suggestions)
|
|
2382
3185
|
}
|
|
2383
3186
|
|
|
3187
|
+
func isShowingMentionSuggestionsForTesting() -> Bool {
|
|
3188
|
+
accessoryToolbar.isShowingMentionSuggestions
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
func lastAddonEventJSONForTesting() -> String? {
|
|
3192
|
+
lastAddonEventJSONForTestingValue
|
|
3193
|
+
}
|
|
3194
|
+
|
|
2384
3195
|
func triggerMentionSuggestionTapForTesting(at index: Int) {
|
|
2385
3196
|
accessoryToolbar.triggerMentionSuggestionTapForTesting(at: index)
|
|
2386
3197
|
}
|
|
@@ -2398,6 +3209,7 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
2398
3209
|
}
|
|
2399
3210
|
|
|
2400
3211
|
private func updateAccessoryToolbarVisibility() {
|
|
3212
|
+
guard prepareForInputAccessoryMutationOrRetry(.updateAccessoryToolbarVisibility) else { return }
|
|
2401
3213
|
refreshSystemAssistantToolbarIfNeeded()
|
|
2402
3214
|
let nextAccessoryView: UIView?
|
|
2403
3215
|
if showsToolbar &&
|
|
@@ -2417,6 +3229,7 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
2417
3229
|
richTextView.textView.reloadInputViews()
|
|
2418
3230
|
}
|
|
2419
3231
|
}
|
|
3232
|
+
markAccessoryMutationSucceeded(.updateAccessoryToolbarVisibility)
|
|
2420
3233
|
}
|
|
2421
3234
|
|
|
2422
3235
|
private var shouldUseSystemAssistantToolbar: Bool {
|