@apollohg/react-native-rich-text-editor 2.0.0 → 2.0.2

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.
Files changed (180) hide show
  1. package/README.md +8 -116
  2. package/THIRD_PARTY_NOTICES.md +1 -1
  3. package/android/build.gradle +3 -0
  4. package/android/src/main/java/com/apollohg/editor/EditorEditTextImages.kt +25 -0
  5. package/android/src/main/java/com/apollohg/editor/EditorV2RenderValidation.kt +1 -0
  6. package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +101 -33
  7. package/android/src/main/java/com/apollohg/editor/RenderMediaSpans.kt +15 -3
  8. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +14 -0
  9. package/android/src/main/java/com/apollohg/editor/viewer/AndroidProseLayoutEngine.kt +1 -1
  10. package/app.plugin.js +42 -39
  11. package/dist/AtomHost.js +18 -11
  12. package/dist/EditorAddon.js +4 -2
  13. package/dist/EditorAddonNormalization.js +17 -9
  14. package/dist/EditorMentionThemeNormalization.js +6 -3
  15. package/dist/EditorMentionThemeValidation.js +28 -11
  16. package/dist/EditorStyleSheetNormalization.js +85 -38
  17. package/dist/EditorStyleSheetTypes.d.ts +1 -1
  18. package/dist/EditorTheme.d.ts +4 -0
  19. package/dist/EditorTheme.js +5 -3
  20. package/dist/EditorToolbarComponent.d.ts +1 -1
  21. package/dist/EditorToolbarComponent.js +1 -1
  22. package/dist/EditorToolbarRegistry.js +5 -5
  23. package/dist/EditorToolbarTypes.d.ts +12 -12
  24. package/dist/EditorToolbarVisuals.js +1 -1
  25. package/dist/ExternalTextComposition.js +89 -43
  26. package/dist/ImageLoadingPolicy.d.ts +1 -1
  27. package/dist/NativeEditorBoundaryError.d.ts +7 -7
  28. package/dist/NativeEditorBoundaryError.js +9 -5
  29. package/dist/NativeEditorCollaborationTransport.d.ts +7 -7
  30. package/dist/NativeEditorCollaborationTransport.js +28 -15
  31. package/dist/NativeEditorCreateConfig.js +11 -6
  32. package/dist/NativeEditorCreateJson.js +51 -25
  33. package/dist/NativeEditorCreateValidation.js +11 -6
  34. package/dist/NativeEditorDocumentBridge.d.ts +1 -1
  35. package/dist/NativeEditorDocumentBridge.js +32 -18
  36. package/dist/NativeEditorDocumentHandle.d.ts +4 -4
  37. package/dist/NativeEditorDocumentHandle.js +1 -1
  38. package/dist/NativeEditorLocalAwareness.js +25 -13
  39. package/dist/NativeEditorNativeModule.d.ts +8 -1
  40. package/dist/NativeEditorNativeModule.js +7 -5
  41. package/dist/NativeEditorRenderNormalization.js +66 -25
  42. package/dist/NativeEditorResultNormalization.js +53 -29
  43. package/dist/NativeEditorTypes.d.ts +10 -10
  44. package/dist/NativeEditorV2Decimal.js +6 -3
  45. package/dist/NativeProseViewer.js +11 -3
  46. package/dist/ResourceLimits.d.ts +2 -2
  47. package/dist/ResourceLimits.js +7 -2
  48. package/dist/RichTextEditor.js +1 -1
  49. package/dist/RichTextEditorNativeTypes.d.ts +1 -1
  50. package/dist/RichTextEditorNativeView.d.ts +1 -3
  51. package/dist/RichTextEditorSerialization.js +24 -14
  52. package/dist/RichTextEditorTypes.d.ts +1 -1
  53. package/dist/YjsCollaboration.d.ts +7 -7
  54. package/dist/YjsCollaboration.js +42 -26
  55. package/dist/addons.d.ts +4 -4
  56. package/dist/addons.js +3 -3
  57. package/dist/atomInstances.js +6 -3
  58. package/dist/atomUpdates.js +7 -4
  59. package/dist/atoms.js +20 -12
  60. package/dist/attributeValidation.js +55 -24
  61. package/dist/contentExpression.js +80 -42
  62. package/dist/listMarkers.d.ts +1 -1
  63. package/dist/listMarkers.js +1 -1
  64. package/dist/schemaDefinition.d.ts +1 -1
  65. package/dist/schemaDefinition.js +23 -14
  66. package/dist/schemaDocument.js +1 -1
  67. package/dist/schemaDocumentConstruction.js +25 -14
  68. package/dist/schemaNormalization.js +51 -29
  69. package/dist/schemaPresets.d.ts +1 -1
  70. package/dist/schemaPresets.js +9 -4
  71. package/dist/schemaResolution.js +46 -25
  72. package/dist/specs/PreparedProseViewerNativeComponent.d.ts +4 -0
  73. package/dist/useEditorToolbarInteractions.d.ts +2 -2
  74. package/dist/useEditorToolbarInteractions.js +45 -23
  75. package/dist/useEditorToolbarItems.d.ts +1 -1
  76. package/dist/useEditorToolbarItems.js +4 -3
  77. package/dist/useEditorToolbarPresentation.d.ts +3 -3
  78. package/dist/useEditorToolbarPresentation.js +4 -4
  79. package/dist/useEditorToolbarState.d.ts +1 -1
  80. package/dist/useEditorToolbarState.js +1 -1
  81. package/dist/useFocusPreservingFrames.js +11 -8
  82. package/dist/useNativeEditor.d.ts +1 -1
  83. package/dist/useNativeEditor.js +21 -11
  84. package/dist/useRichTextEditorCommands.d.ts +2 -2
  85. package/dist/useRichTextEditorCommands.js +59 -29
  86. package/dist/useRichTextEditorEvents.d.ts +3 -3
  87. package/dist/useRichTextEditorEvents.js +75 -32
  88. package/dist/useRichTextEditorMentions.d.ts +4 -4
  89. package/dist/useRichTextEditorMentions.js +48 -29
  90. package/dist/useRichTextEditorPresentation.d.ts +4 -4
  91. package/dist/useRichTextEditorPresentation.js +25 -23
  92. package/dist/useRichTextEditorState.d.ts +3 -3
  93. package/dist/useRichTextEditorState.js +20 -15
  94. package/dist/useRichTextEditorUpdates.d.ts +1 -1
  95. package/dist/useRichTextEditorUpdates.js +99 -41
  96. package/dist/useViewerAtoms.js +63 -36
  97. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  98. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  99. package/ios/EditorInteractionOverlays.swift +114 -122
  100. package/ios/EditorLayoutManager.swift +13 -3
  101. package/ios/EditorStyleSheet.swift +12 -1
  102. package/ios/EditorTextView+Images.swift +32 -10
  103. package/ios/EditorTextView.swift +1 -1
  104. package/ios/EditorV2Adapter+RenderValidation.swift +2 -2
  105. package/ios/NativeToolbar+ButtonStyle.swift +17 -5
  106. package/ios/NativeToolbar+Diagnostics.swift +15 -0
  107. package/ios/RenderBridge+Atoms.swift +0 -1
  108. package/ios/RenderBridge+Attachments.swift +0 -29
  109. package/ios/RichTextEditorView.swift +4 -12
  110. package/ios/Viewer/CoreTextProseLayoutEngine+AttributedText.swift +3 -2
  111. package/package.json +11 -3
  112. package/react-native.config.js +1 -1
  113. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  114. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  115. package/rust/android/x86/libeditor_core.so +0 -0
  116. package/rust/android/x86_64/libeditor_core.so +0 -0
  117. package/src/AtomHost.tsx +58 -28
  118. package/src/EditorAddon.ts +18 -8
  119. package/src/EditorAddonNormalization.ts +40 -11
  120. package/src/EditorMentionThemeNormalization.ts +15 -4
  121. package/src/EditorMentionThemeValidation.ts +51 -14
  122. package/src/EditorStyleSheet.ts +1 -0
  123. package/src/EditorStyleSheetNormalization.ts +195 -64
  124. package/src/EditorStyleSheetTypes.ts +1 -1
  125. package/src/EditorTheme.ts +13 -5
  126. package/src/EditorToolbarComponent.tsx +2 -1
  127. package/src/EditorToolbarRegistry.tsx +23 -13
  128. package/src/EditorToolbarTypes.tsx +12 -12
  129. package/src/EditorToolbarVisuals.tsx +6 -3
  130. package/src/EditorUpdateRevision.ts +1 -0
  131. package/src/ExternalTextComposition.ts +259 -49
  132. package/src/ImageLoadingPolicy.ts +3 -1
  133. package/src/NativeEditorBoundaryError.ts +38 -11
  134. package/src/NativeEditorCollaborationTransport.ts +108 -27
  135. package/src/NativeEditorCreateConfig.ts +92 -19
  136. package/src/NativeEditorCreateJson.ts +163 -26
  137. package/src/NativeEditorCreateValidation.ts +33 -12
  138. package/src/NativeEditorDocumentBridge.ts +111 -19
  139. package/src/NativeEditorDocumentHandle.ts +9 -4
  140. package/src/NativeEditorLocalAwareness.ts +70 -15
  141. package/src/NativeEditorNativeModule.ts +13 -9
  142. package/src/NativeEditorRenderNormalization.ts +128 -29
  143. package/src/NativeEditorResultNormalization.ts +143 -32
  144. package/src/NativeEditorTypes.ts +12 -11
  145. package/src/NativeEditorV2Decimal.ts +12 -3
  146. package/src/NativeProseViewer.tsx +46 -16
  147. package/src/ResourceLimits.ts +15 -8
  148. package/src/RichTextEditor.tsx +3 -1
  149. package/src/RichTextEditorNativeTypes.tsx +1 -1
  150. package/src/RichTextEditorNativeView.tsx +2 -8
  151. package/src/RichTextEditorSerialization.tsx +70 -24
  152. package/src/RichTextEditorTypes.tsx +1 -1
  153. package/src/YjsCollaboration.ts +143 -47
  154. package/src/addons.ts +12 -8
  155. package/src/atomInstances.ts +25 -5
  156. package/src/atomUpdates.ts +23 -6
  157. package/src/atoms.ts +85 -18
  158. package/src/attributeValidation.ts +97 -33
  159. package/src/contentExpression.ts +218 -63
  160. package/src/listMarkers.ts +1 -1
  161. package/src/schemaDefinition.ts +82 -20
  162. package/src/schemaDocument.ts +15 -1
  163. package/src/schemaDocumentConstruction.ts +80 -16
  164. package/src/schemaNormalization.ts +155 -41
  165. package/src/schemaPresets.ts +37 -31
  166. package/src/schemaResolution.ts +126 -27
  167. package/src/specs/PreparedProseViewerNativeComponent.ts +4 -2
  168. package/src/useEditorToolbarInteractions.tsx +85 -36
  169. package/src/useEditorToolbarItems.tsx +50 -22
  170. package/src/useEditorToolbarPresentation.tsx +81 -58
  171. package/src/useEditorToolbarState.tsx +8 -6
  172. package/src/useFocusPreservingFrames.ts +43 -15
  173. package/src/useNativeEditor.ts +111 -51
  174. package/src/useRichTextEditorCommands.tsx +127 -54
  175. package/src/useRichTextEditorEvents.tsx +144 -42
  176. package/src/useRichTextEditorMentions.tsx +118 -37
  177. package/src/useRichTextEditorPresentation.tsx +128 -114
  178. package/src/useRichTextEditorState.tsx +76 -40
  179. package/src/useRichTextEditorUpdates.tsx +207 -56
  180. package/src/useViewerAtoms.tsx +162 -66
@@ -15,8 +15,9 @@ function isRecord(value) {
15
15
  }
16
16
  function parsePositions(json, definitions, snapshot) {
17
17
  const values = JSON.parse(json);
18
- if (!Array.isArray(values))
18
+ if (!Array.isArray(values)) {
19
19
  throw new Error('Atom positions must be an array.');
20
+ }
20
21
  const seen = new Set();
21
22
  const identities = new Set();
22
23
  return values.map((value) => {
@@ -27,24 +28,27 @@ function parsePositions(json, definitions, snapshot) {
27
28
  Number(value.docPos) < 0 ||
28
29
  Number(value.docPos) > 0xffffffff ||
29
30
  typeof value.attrsJson !== 'string' ||
30
- !['x', 'y', 'width', 'height'].every((key) => typeof value[key] === 'number' && Number.isFinite(value[key])) ||
31
+ !['x', 'y', 'width', 'height'].every(key => typeof value[key] === 'number' && Number.isFinite(value[key])) ||
31
32
  Number(value.width) <= 0 ||
32
33
  Number(value.height) < 0 ||
33
34
  seen.has(Number(value.docPos))) {
34
35
  throw new Error('Invalid prepared atom position.');
35
36
  }
36
37
  const attrs = JSON.parse(value.attrsJson);
37
- if (!isRecord(attrs))
38
+ if (!isRecord(attrs)) {
38
39
  throw new Error('Atom attributes must be a JSON object.');
40
+ }
39
41
  const idAttribute = definitions.get(value.nodeType)?.idAttribute;
40
42
  const atomId = idAttribute == null ? undefined : attrs[idAttribute];
41
- if (idAttribute != null && (typeof atomId !== 'string' || atomId.length === 0))
43
+ if (idAttribute != null && (typeof atomId !== 'string' || atomId.length === 0)) {
42
44
  throw new Error('Atom identity must be a non-empty string.');
45
+ }
43
46
  const key = typeof atomId === 'string'
44
47
  ? JSON.stringify([value.nodeType, atomId])
45
- : `${snapshot}:${value.docPos}:${value.nodeType}`;
46
- if (identities.has(key))
48
+ : `${snapshot}:${String(value.docPos)}:${value.nodeType}`;
49
+ if (identities.has(key)) {
47
50
  throw new Error('Duplicate atom identity.');
51
+ }
48
52
  identities.add(key);
49
53
  seen.add(Number(value.docPos));
50
54
  return { ...value, attrs, key, atomId };
@@ -52,18 +56,20 @@ function parsePositions(json, definitions, snapshot) {
52
56
  }
53
57
  function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = true, viewport, onUpdateAtomAttrs, onError, }) {
54
58
  const [width, setWidth] = (0, react_1.useState)(null);
55
- const snapshot = (0, react_1.useMemo)(() => String(++nextGeneration), [identity]);
59
+ const snapshot = (0, react_1.useMemo)(() => ({ identity, value: String(++nextGeneration) }), [identity]).value;
56
60
  const serializedAtoms = (0, atoms_1.serializeEditorAtoms)(atoms);
57
- const identifiers = JSON.stringify((atoms ?? []).map((atom) => [atom.name, atom.idAttribute]));
58
- const generation = (0, react_1.useMemo)(() => String(++nextGeneration), [identity, identifiers, serializedAtoms, width]);
59
- const definitions = (0, react_1.useMemo)(() => new Map((atoms ?? []).map((atom) => [atom.name, atom])), [atoms]);
61
+ const identifiers = JSON.stringify((atoms ?? []).map(atom => [atom.name, atom.idAttribute]));
62
+ const generation = (0, react_1.useMemo)(() => ({
63
+ identity, identifiers, serializedAtoms, width, value: String(++nextGeneration),
64
+ }), [identity, identifiers, serializedAtoms, width]).value;
65
+ const definitions = (0, react_1.useMemo)(() => new Map((atoms ?? []).map(atom => [atom.name, atom])), [atoms]);
60
66
  const enabled = definitions.size > 0;
61
67
  const [measurementState, setMeasurementState] = (0, react_1.useState)({
62
68
  generation,
63
69
  revision: 0,
64
70
  values: {},
65
71
  });
66
- const measurements = measurementState.generation === generation ? measurementState.values : {};
72
+ const measurements = (0, react_1.useMemo)(() => measurementState.generation === generation ? measurementState.values : {}, [measurementState, generation]);
67
73
  const revision = String(measurementState.generation === generation ? measurementState.revision : 0);
68
74
  const [layout, setLayout] = (0, react_1.useState)(null);
69
75
  const pendingLayout = layout?.generation !== generation;
@@ -72,8 +78,9 @@ function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = tr
72
78
  const mountedMeasurements = (0, react_1.useRef)(new Map());
73
79
  (0, react_1.useLayoutEffect)(() => {
74
80
  for (const [key, measured] of mountedMeasurements.current) {
75
- if (!definitions.has(measured.nodeType))
81
+ if (!definitions.has(measured.nodeType)) {
76
82
  mountedMeasurements.current.delete(key);
83
+ }
77
84
  }
78
85
  }, [definitions]);
79
86
  const mounted = (0, react_1.useRef)(false);
@@ -119,7 +126,12 @@ function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = tr
119
126
  measurements,
120
127
  },
121
128
  })
122
- : themeJson, [enabled, themeJson, serializedAtoms, generation, revision, measurements]);
129
+ : themeJson, [enabled,
130
+ themeJson,
131
+ serializedAtoms,
132
+ generation,
133
+ revision,
134
+ measurements]);
123
135
  const onAtomLayout = (0, react_1.useCallback)((event) => {
124
136
  const value = event.nativeEvent;
125
137
  const latest = current.current;
@@ -128,8 +140,9 @@ function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = tr
128
140
  value.revision !== latest.revision ||
129
141
  !Number.isFinite(value.layoutWidth) ||
130
142
  value.layoutWidth <= 0 ||
131
- (latest.width != null && Math.abs(latest.width - value.layoutWidth) > 1))
143
+ (latest.width != null && Math.abs(latest.width - value.layoutWidth) > 1)) {
132
144
  return;
145
+ }
133
146
  try {
134
147
  const next = parsePositions(value.atomsJson, latest.definitions, latest.snapshot);
135
148
  const retainedMeasurements = {};
@@ -143,18 +156,19 @@ function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = tr
143
156
  }
144
157
  }
145
158
  for (const key of mountedMeasurements.current.keys()) {
146
- if (!retainedKeys.has(key))
159
+ if (!retainedKeys.has(key)) {
147
160
  mountedMeasurements.current.delete(key);
161
+ }
148
162
  }
149
163
  // Unchanged mounted sizes do not emit another Yoga onLayout event.
150
- setMeasurementState((previous) => previous.generation === value.generation
164
+ setMeasurementState(previous => previous.generation === value.generation
151
165
  ? previous
152
166
  : {
153
167
  generation: value.generation,
154
168
  revision: Object.keys(retainedMeasurements).length > 0 ? 1 : 0,
155
169
  values: retainedMeasurements,
156
170
  });
157
- setLayout((previous) => previous?.generation === value.generation && previous.json === value.atomsJson
171
+ setLayout(previous => previous?.generation === value.generation && previous.json === value.atomsJson
158
172
  ? previous
159
173
  : { generation: value.generation, json: value.atomsJson, positions: next });
160
174
  }
@@ -171,8 +185,9 @@ function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = tr
171
185
  }, []);
172
186
  const onContainerLayout = (0, react_1.useCallback)((event) => {
173
187
  const next = event.nativeEvent.layout.width;
174
- if (Number.isFinite(next) && next > 0)
175
- setWidth((previous) => (previous === next ? previous : next));
188
+ if (Number.isFinite(next) && next > 0) {
189
+ setWidth(previous => (previous === next ? previous : next));
190
+ }
176
191
  }, []);
177
192
  const updateQueues = (0, react_1.useRef)(new Map());
178
193
  const acknowledged = (0, react_1.useRef)(new Map());
@@ -182,14 +197,16 @@ function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = tr
182
197
  const updateAttrs = (0, react_1.useCallback)((owner, atom, update) => {
183
198
  const execute = async () => {
184
199
  const latest = current.current;
185
- if (!mounted.current)
200
+ if (!mounted.current) {
186
201
  throw new atomInstances_1.AtomUpdateAttrsError('not-ready', 'The viewer is unmounted.');
202
+ }
187
203
  if (owner !== latest.generation ||
188
- !latest.positions.some((position) => position.key === atom.key && position.attrsJson === atom.attrsJson)) {
204
+ !latest.positions.some(position => position.key === atom.key && position.attrsJson === atom.attrsJson)) {
189
205
  throw new atomInstances_1.AtomUpdateAttrsError('stale-revision', 'The viewer content has changed.');
190
206
  }
191
- if (latest.readOnly || !latest.onUpdateAtomAttrs)
207
+ if (latest.readOnly || !latest.onUpdateAtomAttrs) {
192
208
  throw new atomInstances_1.AtomUpdateAttrsError('not-applicable', 'Viewer updates require readOnly={false} and onUpdateAtomAttrs.');
209
+ }
193
210
  const accepted = acknowledged.current.get(atom.key);
194
211
  const attrs = accepted?.generation === owner ? accepted.attrs : atom.attrs;
195
212
  const partial = (0, atomUpdates_1.resolveAtomAttrsUpdate)(attrs, update);
@@ -198,8 +215,9 @@ function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = tr
198
215
  (0, attributeValidation_1.validateAttributes)({ ...attrs, ...partial }, definition.attrs ?? {});
199
216
  if (definition.idAttribute &&
200
217
  Object.prototype.hasOwnProperty.call(partial, definition.idAttribute) &&
201
- partial[definition.idAttribute] !== attrs[definition.idAttribute])
218
+ partial[definition.idAttribute] !== attrs[definition.idAttribute]) {
202
219
  throw new Error('An atom identity cannot be changed.');
220
+ }
203
221
  }
204
222
  catch (error) {
205
223
  throw new atomInstances_1.AtomUpdateAttrsError('not-applicable', error instanceof Error ? error.message : String(error));
@@ -211,18 +229,21 @@ function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = tr
211
229
  attrs,
212
230
  partial,
213
231
  });
214
- if (mounted.current && current.current.generation === owner)
232
+ if (mounted.current && current.current.generation === owner) {
215
233
  acknowledged.current.set(atom.key, {
216
234
  generation: owner,
217
235
  attrs: { ...attrs, ...partial },
218
236
  });
237
+ }
219
238
  };
220
239
  const previous = updateQueues.current.get(atom.key);
221
- const result = previous ? previous.catch(() => { }).then(execute) : execute();
240
+ const result = previous ? previous.catch(() => {
241
+ }).then(execute) : execute();
222
242
  updateQueues.current.set(atom.key, result);
223
243
  const clean = () => {
224
- if (updateQueues.current.get(atom.key) === result)
244
+ if (updateQueues.current.get(atom.key) === result) {
225
245
  updateQueues.current.delete(atom.key);
246
+ }
226
247
  };
227
248
  void result.then(clean, clean);
228
249
  return result;
@@ -238,22 +259,25 @@ function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = tr
238
259
  !Number.isFinite(measured.height) ||
239
260
  measured.height < 0 ||
240
261
  Math.abs(measured.width - atom.width) > 1 ||
241
- !latest.renderedPositions.some((position) => position.docPos === atom.docPos &&
262
+ !latest.renderedPositions.some(position => position.docPos === atom.docPos &&
242
263
  position.nodeType === atom.nodeType &&
243
264
  position.width === atom.width &&
244
- position.attrsJson === atom.attrsJson))
265
+ position.attrsJson === atom.attrsJson)) {
245
266
  return;
267
+ }
246
268
  mountedMeasurements.current.set(atom.key, {
247
269
  nodeType: atom.nodeType,
248
270
  size: { width: atom.width, height: measured.height },
249
271
  });
250
- if (latest.pendingLayout)
272
+ if (latest.pendingLayout) {
251
273
  return;
252
- setMeasurementState((previous) => {
274
+ }
275
+ setMeasurementState(previous => {
253
276
  const values = previous.generation === owner ? previous.values : {};
254
277
  const existing = values[String(atom.docPos)];
255
- if (existing?.width === atom.width && existing.height === measured.height)
278
+ if (existing?.width === atom.width && existing.height === measured.height) {
256
279
  return previous;
280
+ }
257
281
  return {
258
282
  generation: owner,
259
283
  revision: previous.generation === owner ? previous.revision + 1 : 1,
@@ -264,25 +288,28 @@ function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = tr
264
288
  };
265
289
  });
266
290
  }, []);
267
- const children = renderedPositions.map((atom) => {
291
+ const children = renderedPositions.map(atom => {
268
292
  const Component = definitions.get(atom.nodeType)?.component;
269
- if (!Component)
293
+ if (!Component) {
270
294
  return null;
295
+ }
271
296
  return ((0, jsx_runtime_1.jsx)(react_native_1.View, { collapsable: false, pointerEvents: !interactive || pendingLayout ? 'none' : 'box-none', accessibilityElementsHidden: pendingLayout, importantForAccessibility: pendingLayout ? 'no-hide-descendants' : 'auto', style: {
272
297
  position: 'absolute',
273
298
  left: atom.x,
274
299
  top: atom.y,
275
300
  width: atom.width,
276
301
  opacity: pendingLayout ? 0 : 1,
277
- }, onLayout: (event) => measure(generation, atom, Component, event), children: (0, jsx_runtime_1.jsx)(AtomHost_1.AtomHost, { component: Component, width: atom.width, estimatedHeight: atom.height, visible: (0, AtomHost_1.atomIsVisible)(atom.y, atom.height, viewport), onMeasure: (event) => measure(generation, atom, Component, event), atomProps: {
302
+ }, onLayout: event => measure(generation, atom, Component, event), children: (0, jsx_runtime_1.jsx)(AtomHost_1.AtomHost, { component: Component, width: atom.width, estimatedHeight: atom.height, visible: (0, AtomHost_1.atomIsVisible)(atom.y, atom.height, viewport), onMeasure: event => measure(generation, atom, Component, event), atomProps: {
278
303
  attrs: atom.attrs,
279
304
  nodeType: atom.nodeType,
280
305
  selected: false,
281
306
  readOnly,
282
307
  interactive: interactive && !pendingLayout,
283
308
  isViewer: true,
284
- updateAttrs: (partial) => updateAttrs(layout.generation, atom, partial),
309
+ updateAttrs: partial => updateAttrs(layout.generation, atom, partial),
285
310
  } }) }, atom.key));
286
311
  });
287
- return { enabled, themeJson: configuredThemeJson, onAtomLayout, onContainerLayout, children };
312
+ return {
313
+ enabled, themeJson: configuredThemeJson, onAtomLayout, onContainerLayout, children,
314
+ };
288
315
  }
@@ -408,38 +408,29 @@ final class ImageResizeOverlayView: UIView {
408
408
 
409
409
  private struct DragState {
410
410
  let corner: Corner
411
- let originalRect: CGRect
411
+ let originalSize: CGSize
412
412
  let docPos: UInt32
413
413
  let maximumWidth: CGFloat
414
+ let editorId: UInt64
415
+ let attachment: BlockImageAttachment
416
+ let originalWidth: CGFloat?
417
+ let originalHeight: CGFloat?
418
+ var previewSize: CGSize
414
419
  }
415
420
 
416
421
  private weak var editorView: RichTextEditorView?
417
422
  private let selectionLayer = CAShapeLayer()
418
- private let previewBackdropView = UIView()
419
- private let previewImageView = UIImageView()
420
423
  private var handleViews: [Corner: ImageResizeHandleView] = [:]
421
424
  private var currentRect: CGRect?
422
425
  private var currentDocPos: UInt32?
423
426
  private var dragState: DragState?
424
427
  private let handleSize: CGFloat = 20
425
- private let minimumImageSize: CGFloat = 48
426
428
 
427
429
  override init(frame: CGRect) {
428
430
  super.init(frame: frame)
429
431
  backgroundColor = .clear
430
432
  clipsToBounds = true
431
433
 
432
- previewBackdropView.isUserInteractionEnabled = false
433
- previewBackdropView.isHidden = true
434
- previewBackdropView.layer.zPosition = 1
435
- addSubview(previewBackdropView)
436
-
437
- previewImageView.isUserInteractionEnabled = false
438
- previewImageView.isHidden = true
439
- previewImageView.contentMode = .scaleToFill
440
- previewImageView.layer.zPosition = 2
441
- addSubview(previewImageView)
442
-
443
434
  selectionLayer.strokeColor = UIColor.systemBlue.cgColor
444
435
  selectionLayer.fillColor = UIColor.clear.cgColor
445
436
  selectionLayer.lineWidth = 2
@@ -467,8 +458,8 @@ final class ImageResizeOverlayView: UIView {
467
458
  }
468
459
 
469
460
  func refresh() {
470
- if dragState != nil {
471
- return
461
+ if let dragState, !isCurrentDrag(dragState) {
462
+ finishPreviewResize(commit: false)
472
463
  }
473
464
 
474
465
  guard let editorView,
@@ -478,7 +469,6 @@ final class ImageResizeOverlayView: UIView {
478
469
  return
479
470
  }
480
471
 
481
- hidePreviewLayers()
482
472
  applyGeometry(rect: geometry.rect, docPos: geometry.docPos)
483
473
  }
484
474
 
@@ -488,15 +478,16 @@ final class ImageResizeOverlayView: UIView {
488
478
  }
489
479
 
490
480
  func simulatePreviewResizeForTesting(width: CGFloat, height: CGFloat) {
491
- guard beginPreviewResize(from: .bottomRight) else { return }
492
- let nextRect = CGRect(
493
- origin: dragState?.originalRect.origin ?? .zero,
494
- size: editorView?.clampedImageSize(
495
- CGSize(width: width, height: height),
496
- maximumWidth: dragState?.maximumWidth
497
- ) ?? CGSize(width: width, height: height)
498
- )
499
- updatePreviewRect(nextRect)
481
+ if dragState == nil, !beginPreviewResize(from: .bottomRight) { return }
482
+ let size = editorView?.clampedImageSize(
483
+ CGSize(width: width, height: height), maximumWidth: dragState?.maximumWidth
484
+ ) ?? CGSize(width: width, height: height)
485
+ updatePreviewSize(size)
486
+ }
487
+
488
+ func simulatePreviewDragForTesting(corner: Corner, translation: CGPoint) {
489
+ if dragState == nil, !beginPreviewResize(from: corner) { return }
490
+ updatePreview(translation: translation)
500
491
  }
501
492
 
502
493
  func commitPreviewResizeForTesting() {
@@ -511,10 +502,6 @@ final class ImageResizeOverlayView: UIView {
511
502
  !isHidden
512
503
  }
513
504
 
514
- var previewHasImageForTesting: Bool {
515
- !previewImageView.isHidden && previewImageView.image != nil
516
- }
517
-
518
505
  func interceptsPointForTesting(_ location: CGPoint) -> Bool {
519
506
  self.point(inside: location, with: nil)
520
507
  }
@@ -530,8 +517,7 @@ final class ImageResizeOverlayView: UIView {
530
517
  }
531
518
 
532
519
  private func hideOverlay() {
533
- hidePreviewLayers()
534
- dragState = nil
520
+ if dragState != nil { finishPreviewResize(commit: false) }
535
521
  currentRect = nil
536
522
  currentDocPos = nil
537
523
  selectionLayer.path = nil
@@ -539,64 +525,94 @@ final class ImageResizeOverlayView: UIView {
539
525
  }
540
526
 
541
527
  private func applyGeometry(rect: CGRect, docPos: UInt32) {
542
- let integralRect = rect.integral
543
- currentRect = integralRect
528
+ guard currentRect != rect || currentDocPos != docPos || isHidden else { return }
529
+ currentRect = rect
544
530
  currentDocPos = docPos
545
- selectionLayer.path = UIBezierPath(roundedRect: integralRect, cornerRadius: 8).cgPath
531
+ CATransaction.begin()
532
+ CATransaction.setDisableActions(true)
533
+ selectionLayer.path = UIBezierPath(roundedRect: rect, cornerRadius: 8).cgPath
546
534
  isHidden = false
547
- layoutHandleViews(for: integralRect)
535
+ layoutHandleViews(for: rect)
536
+ CATransaction.commit()
548
537
  }
549
538
 
550
- private func hidePreviewLayers() {
551
- previewBackdropView.isHidden = true
552
- previewImageView.isHidden = true
553
- previewImageView.image = nil
554
- }
555
-
556
- private func showPreview(docPos: UInt32, originalRect: CGRect) {
557
- previewBackdropView.backgroundColor = editorView?.imageResizePreviewBackgroundColor() ?? .systemBackground
558
- previewBackdropView.frame = originalRect
559
- previewBackdropView.isHidden = false
560
-
561
- previewImageView.image = editorView?.imagePreviewForResize(docPos: docPos)
562
- previewImageView.frame = originalRect
563
- previewImageView.isHidden = previewImageView.image == nil
539
+ private func isCurrentDrag(_ state: DragState) -> Bool {
540
+ guard let editorView, editorView.editorId == state.editorId,
541
+ editorView.textView.selectedImageSelectionState()?.docPos == state.docPos,
542
+ editorView.textView.blockImageAttachment(docPos: state.docPos)?.attachment === state.attachment
543
+ else { return false }
544
+ return true
564
545
  }
565
546
 
566
547
  @discardableResult
567
548
  private func beginPreviewResize(from corner: Corner) -> Bool {
568
- guard let currentRect, let currentDocPos else { return false }
569
- editorView?.setImageResizePreviewActive(true)
570
- let maximumWidth = editorView?.maximumImageWidthForResizeGesture() ?? currentRect.width
549
+ guard let editorView, let currentRect, let currentDocPos,
550
+ let image = editorView.textView.blockImageAttachment(docPos: currentDocPos)
551
+ else { return false }
552
+ editorView.setImageResizePreviewActive(true)
553
+ let inset = image.attachment.styleBox?.inset ?? .zero
554
+ let contentSize = CGSize(
555
+ width: max(1, currentRect.width - inset.left - inset.right),
556
+ height: max(1, currentRect.height - inset.top - inset.bottom)
557
+ )
571
558
  dragState = DragState(
572
559
  corner: corner,
573
- originalRect: currentRect,
560
+ originalSize: contentSize,
574
561
  docPos: currentDocPos,
575
- maximumWidth: maximumWidth
562
+ maximumWidth: max(48, editorView.maximumImageWidthForResizeGesture() - inset.left - inset.right),
563
+ editorId: editorView.editorId,
564
+ attachment: image.attachment,
565
+ originalWidth: image.attachment.preferredWidth,
566
+ originalHeight: image.attachment.preferredHeight,
567
+ previewSize: contentSize
576
568
  )
577
- showPreview(docPos: currentDocPos, originalRect: currentRect)
578
569
  return true
579
570
  }
580
571
 
581
- private func updatePreviewRect(_ rect: CGRect) {
582
- guard let currentDocPos else { return }
583
- applyGeometry(rect: rect, docPos: currentDocPos)
584
- previewImageView.frame = currentRect ?? rect.integral
572
+ private func updatePreviewSize(_ proposedSize: CGSize) {
573
+ guard var state = dragState, let editorView else { return }
574
+ guard isCurrentDrag(state) else {
575
+ finishPreviewResize(commit: false)
576
+ return
577
+ }
578
+ let scale = window?.screen.scale ?? 1
579
+ let size = proposedSize == state.originalSize ? proposedSize : CGSize(
580
+ width: (proposedSize.width * scale).rounded() / scale,
581
+ height: (proposedSize.height * scale).rounded() / scale
582
+ )
583
+ guard size != state.previewSize else { return }
584
+ state.previewSize = size
585
+ dragState = state
586
+ UIView.performWithoutAnimation {
587
+ editorView.textView.previewResizeImageAtDocPos(
588
+ state.docPos,
589
+ width: size.width,
590
+ height: size.height
591
+ )
592
+ if let geometry = editorView.selectedImageGeometry() {
593
+ applyGeometry(rect: geometry.rect, docPos: geometry.docPos)
594
+ }
595
+ }
585
596
  }
586
597
 
587
598
  private func finishPreviewResize(commit: Bool) {
588
- guard let dragState else { return }
589
- let finalSize = currentRect?.size ?? dragState.originalRect.size
590
- self.dragState = nil
591
- editorView?.setImageResizePreviewActive(false)
592
- if commit {
593
- editorView?.resizeImage(docPos: dragState.docPos, size: finalSize)
599
+ guard let state = dragState, let editorView else { return }
600
+ let shouldCommit = commit && isCurrentDrag(state)
601
+ dragState = nil
602
+ if shouldCommit, state.previewSize != state.originalSize {
603
+ editorView.resizeImage(docPos: state.docPos, size: state.previewSize)
594
604
  } else {
595
- hidePreviewLayers()
596
- }
597
- DispatchQueue.main.async { [weak self] in
598
- self?.refresh()
605
+ editorView.textView.restoreImageResizePreview(
606
+ state.attachment, width: state.originalWidth, height: state.originalHeight
607
+ )
599
608
  }
609
+ editorView.setImageResizePreviewActive(false)
610
+ refresh()
611
+ }
612
+
613
+ override func didMoveToWindow() {
614
+ super.didMoveToWindow()
615
+ if window == nil { finishPreviewResize(commit: false) }
600
616
  }
601
617
 
602
618
  private func layoutHandleViews(for rect: CGRect) {
@@ -624,50 +640,30 @@ final class ImageResizeOverlayView: UIView {
624
640
  }
625
641
  }
626
642
 
627
- private func anchorPoint(for corner: Corner, in rect: CGRect) -> CGPoint {
628
- switch corner {
629
- case .topLeft:
630
- return CGPoint(x: rect.maxX, y: rect.maxY)
631
- case .topRight:
632
- return CGPoint(x: rect.minX, y: rect.maxY)
633
- case .bottomLeft:
634
- return CGPoint(x: rect.maxX, y: rect.minY)
635
- case .bottomRight:
636
- return CGPoint(x: rect.minX, y: rect.minY)
637
- }
643
+ static func resizedSize(from size: CGSize, corner: Corner, translation: CGPoint) -> CGSize {
644
+ let dx = (corner == .topRight || corner == .bottomRight) ? translation.x : -translation.x
645
+ let dy = (corner == .bottomLeft || corner == .bottomRight) ? translation.y : -translation.y
646
+ let lengthSquared = max(1, size.width * size.width + size.height * size.height)
647
+ let minimumScale = 48 / max(1, min(size.width, size.height))
648
+ let scale = max(minimumScale, 1 + (dx * size.width + dy * size.height) / lengthSquared)
649
+ return CGSize(width: size.width * scale, height: size.height * scale)
638
650
  }
639
651
 
640
- private func resizedRect(
641
- from originalRect: CGRect,
642
- corner: Corner,
643
- translation: CGPoint,
644
- maximumWidth: CGFloat?
645
- ) -> CGRect {
646
- let aspectRatio = max(originalRect.width / max(originalRect.height, 1), 0.1)
647
- let signedDx = (corner == .topRight || corner == .bottomRight) ? translation.x : -translation.x
648
- let signedDy = (corner == .bottomLeft || corner == .bottomRight) ? translation.y : -translation.y
649
- let widthScale = (originalRect.width + signedDx) / max(originalRect.width, 1)
650
- let heightScale = (originalRect.height + signedDy) / max(originalRect.height, 1)
651
- let scale = max(minimumImageSize / max(originalRect.width, 1), widthScale, heightScale)
652
- let unclampedSize = CGSize(
653
- width: max(minimumImageSize, originalRect.width * scale),
654
- height: max(minimumImageSize / aspectRatio, (max(minimumImageSize, originalRect.width * scale) / aspectRatio))
655
- )
656
- let clampedSize = editorView?.clampedImageSize(unclampedSize, maximumWidth: maximumWidth) ?? unclampedSize
657
- let width = clampedSize.width
658
- let height = clampedSize.height
659
- let anchor = anchorPoint(for: corner, in: originalRect)
660
-
661
- switch corner {
662
- case .topLeft:
663
- return CGRect(x: anchor.x - width, y: anchor.y - height, width: width, height: height)
664
- case .topRight:
665
- return CGRect(x: anchor.x, y: anchor.y - height, width: width, height: height)
666
- case .bottomLeft:
667
- return CGRect(x: anchor.x - width, y: anchor.y, width: width, height: height)
668
- case .bottomRight:
669
- return CGRect(x: anchor.x, y: anchor.y, width: width, height: height)
652
+ private func updatePreview(translation: CGPoint) {
653
+ guard let dragState else { return }
654
+ if translation == .zero {
655
+ updatePreviewSize(dragState.originalSize)
656
+ return
670
657
  }
658
+ let size = Self.resizedSize(
659
+ from: dragState.originalSize,
660
+ corner: dragState.corner,
661
+ translation: translation
662
+ )
663
+ let clampedSize = editorView?.clampedImageSize(
664
+ size, maximumWidth: dragState.maximumWidth
665
+ ) ?? size
666
+ updatePreviewSize(clampedSize)
671
667
  }
672
668
 
673
669
  @objc
@@ -676,22 +672,18 @@ final class ImageResizeOverlayView: UIView {
676
672
 
677
673
  switch gesture.state {
678
674
  case .began:
679
- _ = beginPreviewResize(from: handleView.corner)
675
+ if beginPreviewResize(from: handleView.corner) {
676
+ updatePreview(translation: gesture.translation(in: window))
677
+ }
680
678
  case .changed:
681
- guard let dragState else { return }
682
- let nextRect = resizedRect(
683
- from: dragState.originalRect,
684
- corner: dragState.corner,
685
- translation: gesture.translation(in: self),
686
- maximumWidth: dragState.maximumWidth
687
- )
688
- updatePreviewRect(nextRect)
679
+ updatePreview(translation: gesture.translation(in: window))
689
680
  case .ended:
681
+ updatePreview(translation: gesture.translation(in: window))
690
682
  finishPreviewResize(commit: true)
691
683
  case .cancelled, .failed:
692
684
  finishPreviewResize(commit: false)
693
685
  default:
694
- finishPreviewResize(commit: false)
686
+ break
695
687
  }
696
688
  }
697
689
  }
@@ -54,7 +54,16 @@ final class EditorLayoutManager: NSLayoutManager, NSLayoutManagerDelegate {
54
54
  storage.enumerateAttribute(editorInlineLineHeightAttribute, in: characters) { value, _, _ in
55
55
  height = max(height, EditorTheme.cgFloat(value) ?? 0)
56
56
  }
57
+ var ascent = baselineOffset.pointee + (height - lineFragmentUsedRect.pointee.height) / 2
58
+ var descent = height - ascent
59
+ storage.enumerateAttribute(editorMentionBoxAttribute, in: characters) { value, _, _ in
60
+ guard let chip = value as? EditorMentionRenderedBox else { return }
61
+ ascent = max(ascent, chip.baselineOffset)
62
+ descent = max(descent, chip.size.height - chip.baselineOffset)
63
+ }
64
+ height = ascent + descent
57
65
  let extra = height - lineFragmentUsedRect.pointee.height
66
+ let baselineAdjustment = ascent - baselineOffset.pointee
58
67
  var leading: CGFloat = 0
59
68
  var overlap: CGFloat = 0
60
69
  if characters.location < storage.length,
@@ -76,7 +85,7 @@ final class EditorLayoutManager: NSLayoutManager, NSLayoutManagerDelegate {
76
85
  lineFragmentRect.pointee.size.height += extra + leading
77
86
  lineFragmentUsedRect.pointee.size.height = height
78
87
  lineFragmentUsedRect.pointee.origin.y += leading + overlap
79
- baselineOffset.pointee += extra / 2 + leading
88
+ baselineOffset.pointee += baselineAdjustment + leading
80
89
  return true
81
90
  }
82
91
 
@@ -226,10 +235,11 @@ final class EditorLayoutManager: NSLayoutManager, NSLayoutManagerDelegate {
226
235
  storage.enumerateAttribute(editorMentionBoxAttribute, in: visible) { value, range, _ in
227
236
  guard let box = value as? EditorMentionRenderedBox else { return }
228
237
  let glyph = self.glyphIndexForCharacter(at: range.location)
229
- let line = self.lineFragmentUsedRect(forGlyphAt: glyph, effectiveRange: nil)
230
238
  let location = self.location(forGlyphAt: glyph)
231
239
  let fragment = self.lineFragmentRect(forGlyphAt: glyph, effectiveRange: nil)
232
- let rect = CGRect(x: fragment.minX + location.x + origin.x, y: line.midY - box.size.height / 2 + origin.y, width: box.size.width, height: box.size.height)
240
+ let rect = CGRect(x: fragment.minX + location.x + origin.x,
241
+ y: fragment.minY + location.y + origin.y - box.baselineOffset,
242
+ width: box.size.width, height: box.size.height)
233
243
  box.box.draw(in: rect, context: context)
234
244
  let labelHeight = box.label?.size().height ?? 0
235
245
  box.label?.draw(at: CGPoint(x: rect.minX + box.padding.left,