@retor/react-native 0.4.4 → 0.4.6

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/README.md CHANGED
@@ -111,24 +111,43 @@ function MyLineCard({ line }: { line: RetorLine }) {
111
111
 
112
112
  ## Notes
113
113
 
114
- `<AddNoteSheet>` triggers when you call `useAddNote().open(tagId?)` — for example from a "+" button on the active tag in your `LineTagList`. It collects text + private/public, then calls `onNoteSubmit` on the parent `<Viewer>`. **Persistence is your responsibility** — store the note however you like, then re-pass updated notes back to Retor via `<Notes>`.
114
+ `<AddNoteSheet>` triggers when you call `useAddNote().open(tagId?)`. It collects text + private/public, then either:
115
+ - calls `onNoteSubmit` on the parent `<Viewer>` (if set) — **persistence is your responsibility**
116
+ - or falls back to persisting via Retor's own backend (Convex) when no `onNoteSubmit` is provided
117
+
118
+ Re-pass saved notes back to the 3D scene via `<Notes>`.
119
+
120
+ ### Note fields for proper rendering
121
+
122
+ When passing notes via `<Notes>`, each note should include:
123
+
124
+ | Field | Required | Purpose |
125
+ |-------|----------|---------|
126
+ | `_id` | yes | Unique identifier |
127
+ | `name` | yes | The note text (displayed in the tag list and 3D scene) |
128
+ | `position` | yes | `{ x, y, z }` — where the note sits on the line |
129
+ | `objectId` | yes | Set to the `lineId` so the note associates with the correct line |
130
+ | `progress` | yes | `0..1` position along the line (from the submit payload) — used for sort order and scroll-to |
131
+ | `avatarUrl` | recommended | Profile image URL — renders as a circular avatar in the tag pill and list item |
132
+ | `authorName` | recommended | Display name — used as initial-letter fallback when no `avatarUrl` |
133
+ | `tagType` | recommended | Set to `"icon"` for the standard note pill appearance |
134
+ | `userId` | for deletion | The note author's user ID — compared against `Viewer.userId` to show the delete button |
135
+
136
+ ### Creating + deleting notes
115
137
 
116
138
  ```tsx
117
139
  import { useState } from "react";
118
- import { Viewer, Hud, ProjectSheet, LineDetailSheet, AddNoteSheet, Notes, useAddNote, type RetorTag } from "@retor/react-native";
119
-
120
- function AddButton() {
121
- const { open } = useAddNote();
122
- return <Pressable onPress={() => open()}><Text>+</Text></Pressable>;
123
- }
140
+ import { Viewer, Hud, ProjectSheet, LineDetailSheet, AddNoteSheet, Notes, type RetorTag } from "@retor/react-native";
124
141
 
125
142
  export default function Scene() {
126
143
  const [notes, setNotes] = useState<RetorTag[]>([]);
144
+ const currentUserId = "user_abc"; // your auth system's user ID
127
145
 
128
146
  return (
129
147
  <Viewer
130
148
  projectId="abc123"
131
- onNoteSubmit={({ text, isPrivate, tagId, lineId, position }) => {
149
+ userId={currentUserId}
150
+ onNoteSubmit={({ text, isPrivate, lineId, position, progress }) => {
132
151
  if (!position) return;
133
152
  setNotes((prev) => [
134
153
  ...prev,
@@ -136,24 +155,24 @@ export default function Scene() {
136
155
  _id: `note-${Date.now()}`,
137
156
  name: text,
138
157
  position,
158
+ progress,
139
159
  objectId: lineId ?? undefined,
160
+ tagType: "icon",
161
+ avatarUrl: "https://example.com/avatar.jpg",
162
+ authorName: "Jane",
163
+ userId: currentUserId,
140
164
  },
141
165
  ]);
142
166
  }}
167
+ onNoteDelete={(noteId) => {
168
+ setNotes((prev) => prev.filter((n) => n._id !== noteId));
169
+ // also delete from your backend
170
+ }}
143
171
  >
144
172
  <Notes notes={notes} />
145
173
  <Hud>
146
174
  <ProjectSheet />
147
- <LineDetailSheet>
148
- <LineTagList>
149
- {(tag, isActive) => (
150
- <View style={{ flexDirection: "row", padding: 12 }}>
151
- <Text style={{ flex: 1, color: isActive ? "white" : "gray" }}>{tag.name}</Text>
152
- {isActive && <AddButton />}
153
- </View>
154
- )}
155
- </LineTagList>
156
- </LineDetailSheet>
175
+ <LineDetailSheet />
157
176
  <AddNoteSheet />
158
177
  </Hud>
159
178
  </Viewer>
@@ -161,6 +180,12 @@ export default function Scene() {
161
180
  }
162
181
  ```
163
182
 
183
+ When `onNoteSubmit` is **not** provided, the SDK sends the note to Retor's backend automatically (using the signed-in Clerk session inside the WebView). No `<Notes>` re-injection needed in that case.
184
+
185
+ When a user deletes a note, the SDK:
186
+ 1. Optimistically removes it from the local tag list
187
+ 2. Fires `onNoteDelete(noteId)` so you can delete from your backend
188
+
164
189
  ## Hooks
165
190
 
166
191
  All hooks read from the bridge context provided by the parent `<Viewer>`.
@@ -173,7 +198,7 @@ All hooks read from the bridge context provided by the parent `<Viewer>`.
173
198
  | `useLineProgress()` | `{ progress, closestTagId }` |
174
199
  | `useAutoplay()` | `{ isPlaying, toggle, play, pause }` |
175
200
  | `useAddNote()` | `{ isOpen, tagId, open, close, submit }` |
176
- | `useViewer()` | Imperative controls (`openLine`, `exitLine`, `scrollToTag`, ...) |
201
+ | `useViewer()` | Imperative controls (`openLine`, `exitLine`, `scrollToTag`, `scrollToProgress`, ...) |
177
202
 
178
203
  ## Imperative API
179
204
 
package/dist/index.d.mts CHANGED
@@ -23,6 +23,8 @@ interface RetorTag {
23
23
  avatarUrl?: string;
24
24
  /** Display name of the note's author. Used for the initial-letter fallback when `avatarUrl` is absent. */
25
25
  authorName?: string;
26
+ /** ID of the user who created this note. Compared against `Viewer.userId` to show the delete button. */
27
+ userId?: string;
26
28
  }
27
29
  interface RetorLine {
28
30
  _id: string;
@@ -34,6 +36,8 @@ interface RetorLine {
34
36
  scrollType?: "track" | "observe";
35
37
  closed?: boolean;
36
38
  notesSupported?: boolean;
39
+ /** User-defined key/value fields set in the line editor. */
40
+ metadata?: Record<string, string | number | boolean>;
37
41
  tags: RetorTag[];
38
42
  }
39
43
  interface RetorProject {
@@ -103,12 +107,16 @@ interface RetorBridgeContextValue {
103
107
  lineNotes: RetorTag[];
104
108
  /** Notes injected by the consumer via the <Notes> sentinel. */
105
109
  externalNotes: RetorTag[];
110
+ /** The current user's ID — notes with a matching `userId` show a delete button. */
111
+ userId: string | null;
106
112
  isAddNoteOpen: boolean;
107
113
  addNoteTagId: string | null;
108
114
  controls: ViewerHandle;
109
115
  openAddNote: (tagId?: string) => void;
110
116
  closeAddNote: () => void;
111
117
  submitNote: (text: string, isPrivate?: boolean) => void;
118
+ /** Optimistically remove a note and fire the consumer's onNoteDelete callback. */
119
+ deleteNote: (noteId: string) => void;
112
120
  }
113
121
  declare function useRetorBridge(): RetorBridgeContextValue;
114
122
  /** Returns the array of all lines in the current project. */
@@ -187,8 +195,12 @@ interface ViewerProps {
187
195
  id?: string;
188
196
  /** Base URL where Retor is hosted. Defaults to https://retor.app */
189
197
  baseUrl?: string;
198
+ /** The current user's ID. When set, notes with a matching `userId` field show a delete button. */
199
+ userId?: string;
190
200
  /** Called when a note is submitted via `<AddNoteSheet>`. Receives the text + position. */
191
201
  onNoteSubmit?: (note: NoteSubmitPayload) => void;
202
+ /** Called when the user deletes a note. The note is optimistically removed from the list. */
203
+ onNoteDelete?: (noteId: string) => void;
192
204
  onInit?: (data: InitPayload) => void;
193
205
  onLineOpen?: (data: {
194
206
  lineId: string;
package/dist/index.d.ts CHANGED
@@ -23,6 +23,8 @@ interface RetorTag {
23
23
  avatarUrl?: string;
24
24
  /** Display name of the note's author. Used for the initial-letter fallback when `avatarUrl` is absent. */
25
25
  authorName?: string;
26
+ /** ID of the user who created this note. Compared against `Viewer.userId` to show the delete button. */
27
+ userId?: string;
26
28
  }
27
29
  interface RetorLine {
28
30
  _id: string;
@@ -34,6 +36,8 @@ interface RetorLine {
34
36
  scrollType?: "track" | "observe";
35
37
  closed?: boolean;
36
38
  notesSupported?: boolean;
39
+ /** User-defined key/value fields set in the line editor. */
40
+ metadata?: Record<string, string | number | boolean>;
37
41
  tags: RetorTag[];
38
42
  }
39
43
  interface RetorProject {
@@ -103,12 +107,16 @@ interface RetorBridgeContextValue {
103
107
  lineNotes: RetorTag[];
104
108
  /** Notes injected by the consumer via the <Notes> sentinel. */
105
109
  externalNotes: RetorTag[];
110
+ /** The current user's ID — notes with a matching `userId` show a delete button. */
111
+ userId: string | null;
106
112
  isAddNoteOpen: boolean;
107
113
  addNoteTagId: string | null;
108
114
  controls: ViewerHandle;
109
115
  openAddNote: (tagId?: string) => void;
110
116
  closeAddNote: () => void;
111
117
  submitNote: (text: string, isPrivate?: boolean) => void;
118
+ /** Optimistically remove a note and fire the consumer's onNoteDelete callback. */
119
+ deleteNote: (noteId: string) => void;
112
120
  }
113
121
  declare function useRetorBridge(): RetorBridgeContextValue;
114
122
  /** Returns the array of all lines in the current project. */
@@ -187,8 +195,12 @@ interface ViewerProps {
187
195
  id?: string;
188
196
  /** Base URL where Retor is hosted. Defaults to https://retor.app */
189
197
  baseUrl?: string;
198
+ /** The current user's ID. When set, notes with a matching `userId` field show a delete button. */
199
+ userId?: string;
190
200
  /** Called when a note is submitted via `<AddNoteSheet>`. Receives the text + position. */
191
201
  onNoteSubmit?: (note: NoteSubmitPayload) => void;
202
+ /** Called when the user deletes a note. The note is optimistically removed from the list. */
203
+ onNoteDelete?: (noteId: string) => void;
192
204
  onInit?: (data: InitPayload) => void;
193
205
  onLineOpen?: (data: {
194
206
  lineId: string;
package/dist/index.js CHANGED
@@ -78,12 +78,14 @@ var fallback = {
78
78
  activeLine: null,
79
79
  lineNotes: [],
80
80
  externalNotes: [],
81
+ userId: null,
81
82
  isAddNoteOpen: false,
82
83
  addNoteTagId: null,
83
84
  controls: noopHandle,
84
85
  openAddNote: noop,
85
86
  closeAddNote: noop,
86
- submitNote: noop
87
+ submitNote: noop,
88
+ deleteNote: noop
87
89
  };
88
90
  function useRetorBridge() {
89
91
  return (0, import_react.useContext)(RetorBridgeContext) ?? fallback;
@@ -205,7 +207,7 @@ function extractNotes(children) {
205
207
  });
206
208
  return notes;
207
209
  }
208
- var Viewer = (0, import_react2.forwardRef)(function Viewer2({ projectId, id = "default", baseUrl = "https://retor.app", onNoteSubmit, onInit, onLineOpen, onLineClose, onLineProgress, onMessage, style, children }, ref) {
210
+ var Viewer = (0, import_react2.forwardRef)(function Viewer2({ projectId, id = "default", baseUrl = "https://retor.app", userId = null, onNoteSubmit, onNoteDelete, onInit, onLineOpen, onLineClose, onLineProgress, onMessage, style, children }, ref) {
209
211
  const webviewRef = (0, import_react2.useRef)(null);
210
212
  const notes = extractNotes(children);
211
213
  const readyRef = (0, import_react2.useRef)(false);
@@ -260,6 +262,7 @@ var Viewer = (0, import_react2.forwardRef)(function Viewer2({ projectId, id = "d
260
262
  const addNoteTagIdRef = (0, import_react2.useRef)(addNoteTagId);
261
263
  const activeLineIdRef = (0, import_react2.useRef)(activeLineId);
262
264
  const onNoteSubmitRef = (0, import_react2.useRef)(onNoteSubmit);
265
+ const onNoteDeleteRef = (0, import_react2.useRef)(onNoteDelete);
263
266
  const noteSnapshotRef = (0, import_react2.useRef)({ progress: 0, targetPosition: null, distanceFromStart: null });
264
267
  const progressRef = (0, import_react2.useRef)(progress);
265
268
  const targetPositionRef = (0, import_react2.useRef)(targetPosition);
@@ -276,6 +279,9 @@ var Viewer = (0, import_react2.forwardRef)(function Viewer2({ projectId, id = "d
276
279
  (0, import_react2.useEffect)(() => {
277
280
  onNoteSubmitRef.current = onNoteSubmit;
278
281
  }, [onNoteSubmit]);
282
+ (0, import_react2.useEffect)(() => {
283
+ onNoteDeleteRef.current = onNoteDelete;
284
+ }, [onNoteDelete]);
279
285
  (0, import_react2.useEffect)(() => {
280
286
  progressRef.current = progress;
281
287
  }, [progress]);
@@ -317,6 +323,10 @@ var Viewer = (0, import_react2.forwardRef)(function Viewer2({ projectId, id = "d
317
323
  }
318
324
  setIsAddNoteOpen(false);
319
325
  }, [send]);
326
+ const deleteNote = (0, import_react2.useCallback)((noteId) => {
327
+ setLineNotes((prev) => prev.filter((n) => n._id !== noteId));
328
+ onNoteDeleteRef.current?.(noteId);
329
+ }, []);
320
330
  const handleMessage = (0, import_react2.useCallback)(
321
331
  (event) => {
322
332
  let data = null;
@@ -391,14 +401,16 @@ var Viewer = (0, import_react2.forwardRef)(function Viewer2({ projectId, id = "d
391
401
  activeLine,
392
402
  lineNotes,
393
403
  externalNotes,
404
+ userId,
394
405
  isAddNoteOpen,
395
406
  addNoteTagId,
396
407
  controls,
397
408
  openAddNote,
398
409
  closeAddNote,
399
- submitNote
410
+ submitNote,
411
+ deleteNote
400
412
  }),
401
- [project, lines, activeLineId, activeLine, lineNotes, externalNotes, isAddNoteOpen, addNoteTagId, controls, openAddNote, closeAddNote, submitNote]
413
+ [project, lines, activeLineId, activeLine, lineNotes, externalNotes, userId, isAddNoteOpen, addNoteTagId, controls, openAddNote, closeAddNote, submitNote, deleteNote]
402
414
  );
403
415
  const progressCtx = (0, import_react2.useMemo)(
404
416
  () => ({ progress, closestTagId, targetPosition, distanceFromStart, isPlaying }),
@@ -655,7 +667,7 @@ function LineDetailSheet({ snapPoints = ["35%", "75%"], topInset, renderHeader,
655
667
  backgroundComponent: BlurBackground,
656
668
  topInset: effectiveTopInset
657
669
  },
658
- activeLine && (children ?? /* @__PURE__ */ import_react6.default.createElement(DefaultLineTagList, { listHeader: header }))
670
+ activeLine && (children ? /* @__PURE__ */ import_react6.default.createElement(import_bottom_sheet3.BottomSheetScrollView, { contentContainerStyle: styles3.customContent }, children) : /* @__PURE__ */ import_react6.default.createElement(DefaultLineTagList, { listHeader: header }))
659
671
  );
660
672
  }
661
673
  function DefaultHeader({
@@ -727,14 +739,22 @@ function LineTagList({ children, listHeader }) {
727
739
  contentContainerStyle: styles3.list
728
740
  },
729
741
  listHeader,
730
- tags.map((tag) => /* @__PURE__ */ import_react6.default.createElement(import_react_native5.View, { key: tag._id, onLayout: (e) => handleItemLayout(tag._id, e) }, children(tag, tag._id === closestTagId)))
742
+ tags.map((tag, i) => {
743
+ const isNote = !!tag._isNote;
744
+ const prevIsNote = i > 0 && !!tags[i - 1]._isNote;
745
+ const showSep = isNote && !prevIsNote && i > 0 || !isNote && i > 0 && prevIsNote;
746
+ return /* @__PURE__ */ import_react6.default.createElement(import_react6.Fragment, { key: tag._id }, showSep && /* @__PURE__ */ import_react6.default.createElement(import_react_native5.View, { style: styles3.separator }), /* @__PURE__ */ import_react6.default.createElement(import_react_native5.View, { onLayout: (e) => handleItemLayout(tag._id, e) }, children(tag, tag._id === closestTagId)));
747
+ })
731
748
  );
732
749
  }
733
750
  function DefaultLineTagList({ listHeader }) {
734
751
  return /* @__PURE__ */ import_react6.default.createElement(LineTagList, { listHeader }, (tag, isActive) => /* @__PURE__ */ import_react6.default.createElement(DefaultTagItem, { tag, isActive }));
735
752
  }
736
753
  function DefaultTagItem({ tag, isActive }) {
737
- const { controls, openAddNote } = useRetorBridge();
754
+ const { controls, openAddNote, userId, deleteNote } = useRetorBridge();
755
+ const isNote = !!tag._isNote;
756
+ const tagUserId = tag.userId;
757
+ const isOwn = !!userId && !!tagUserId && tagUserId === userId;
738
758
  const initial = tag.authorName?.trim().charAt(0).toUpperCase();
739
759
  const handlePress = () => {
740
760
  if (typeof tag.progress === "number") controls.scrollToProgress(tag.progress);
@@ -747,9 +767,17 @@ function DefaultTagItem({ tag, isActive }) {
747
767
  style: [styles3.tagItem, isActive && styles3.tagItemActive]
748
768
  },
749
769
  tag.avatarUrl ? /* @__PURE__ */ import_react6.default.createElement(import_react_native5.Image, { source: { uri: tag.avatarUrl }, style: styles3.tagAvatar }) : initial ? /* @__PURE__ */ import_react6.default.createElement(import_react_native5.View, { style: styles3.tagInitial }, /* @__PURE__ */ import_react6.default.createElement(import_react_native5.Text, { style: styles3.tagInitialText }, initial)) : null,
750
- /* @__PURE__ */ import_react6.default.createElement(import_react_native5.View, { style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ import_react6.default.createElement(import_react_native5.Text, { style: [styles3.tagText, isActive && styles3.tagTextActive], numberOfLines: 1 }, tag.name), isActive && tag.description && /* @__PURE__ */ import_react6.default.createElement(import_react_native5.Text, { style: styles3.tagDescription, numberOfLines: 2 }, tag.description)),
770
+ /* @__PURE__ */ import_react6.default.createElement(import_react_native5.View, { style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ import_react6.default.createElement(import_react_native5.Text, { style: [styles3.tagText, isActive && styles3.tagTextActive], numberOfLines: isNote ? 3 : 1 }, tag.name), isActive && tag.description && /* @__PURE__ */ import_react6.default.createElement(import_react_native5.Text, { style: styles3.tagDescription, numberOfLines: 2 }, tag.description)),
751
771
  tag.subtitle && /* @__PURE__ */ import_react6.default.createElement(import_react_native5.Text, { style: styles3.tagSubtitle, numberOfLines: 1 }, tag.subtitle),
752
- isActive && /* @__PURE__ */ import_react6.default.createElement(
772
+ isNote && isOwn && /* @__PURE__ */ import_react6.default.createElement(
773
+ import_react_native5.Pressable,
774
+ {
775
+ onPress: () => deleteNote(tag._id),
776
+ style: styles3.deleteBtn
777
+ },
778
+ /* @__PURE__ */ import_react6.default.createElement(import_lucide_react_native2.Trash2, { size: 12, color: "rgba(255,255,255,0.4)" })
779
+ ),
780
+ isActive && !isNote && /* @__PURE__ */ import_react6.default.createElement(
753
781
  import_react_native5.Pressable,
754
782
  {
755
783
  onPress: (e) => {
@@ -764,6 +792,7 @@ function DefaultTagItem({ tag, isActive }) {
764
792
  }
765
793
  var styles3 = import_react_native5.StyleSheet.create({
766
794
  handle: { backgroundColor: "rgba(255,255,255,0.3)" },
795
+ customContent: { paddingBottom: 120 },
767
796
  header: {
768
797
  flexDirection: "row",
769
798
  alignItems: "flex-start",
@@ -806,10 +835,18 @@ var styles3 = import_react_native5.StyleSheet.create({
806
835
  justifyContent: "center"
807
836
  },
808
837
  tagInitialText: { color: "white", fontSize: 11, fontWeight: "700" },
838
+ separator: { height: 1, backgroundColor: "rgba(255,255,255,0.1)", marginHorizontal: 20, marginVertical: 4 },
809
839
  tagText: { color: "rgba(255,255,255,0.6)", fontSize: 13 },
810
840
  tagTextActive: { color: "white", fontWeight: "600" },
811
841
  tagDescription: { color: "rgba(255,255,255,0.4)", fontSize: 11, marginTop: 2, lineHeight: 14 },
812
842
  tagSubtitle: { color: "rgba(255,255,255,0.4)", fontSize: 10 },
843
+ deleteBtn: {
844
+ width: 24,
845
+ height: 24,
846
+ borderRadius: 12,
847
+ alignItems: "center",
848
+ justifyContent: "center"
849
+ },
813
850
  plusBtn: {
814
851
  width: 24,
815
852
  height: 24,
package/dist/index.mjs CHANGED
@@ -25,12 +25,14 @@ var fallback = {
25
25
  activeLine: null,
26
26
  lineNotes: [],
27
27
  externalNotes: [],
28
+ userId: null,
28
29
  isAddNoteOpen: false,
29
30
  addNoteTagId: null,
30
31
  controls: noopHandle,
31
32
  openAddNote: noop,
32
33
  closeAddNote: noop,
33
- submitNote: noop
34
+ submitNote: noop,
35
+ deleteNote: noop
34
36
  };
35
37
  function useRetorBridge() {
36
38
  return useContext(RetorBridgeContext) ?? fallback;
@@ -161,7 +163,7 @@ function extractNotes(children) {
161
163
  });
162
164
  return notes;
163
165
  }
164
- var Viewer = forwardRef(function Viewer2({ projectId, id = "default", baseUrl = "https://retor.app", onNoteSubmit, onInit, onLineOpen, onLineClose, onLineProgress, onMessage, style, children }, ref) {
166
+ var Viewer = forwardRef(function Viewer2({ projectId, id = "default", baseUrl = "https://retor.app", userId = null, onNoteSubmit, onNoteDelete, onInit, onLineOpen, onLineClose, onLineProgress, onMessage, style, children }, ref) {
165
167
  const webviewRef = useRef(null);
166
168
  const notes = extractNotes(children);
167
169
  const readyRef = useRef(false);
@@ -216,6 +218,7 @@ var Viewer = forwardRef(function Viewer2({ projectId, id = "default", baseUrl =
216
218
  const addNoteTagIdRef = useRef(addNoteTagId);
217
219
  const activeLineIdRef = useRef(activeLineId);
218
220
  const onNoteSubmitRef = useRef(onNoteSubmit);
221
+ const onNoteDeleteRef = useRef(onNoteDelete);
219
222
  const noteSnapshotRef = useRef({ progress: 0, targetPosition: null, distanceFromStart: null });
220
223
  const progressRef = useRef(progress);
221
224
  const targetPositionRef = useRef(targetPosition);
@@ -232,6 +235,9 @@ var Viewer = forwardRef(function Viewer2({ projectId, id = "default", baseUrl =
232
235
  useEffect(() => {
233
236
  onNoteSubmitRef.current = onNoteSubmit;
234
237
  }, [onNoteSubmit]);
238
+ useEffect(() => {
239
+ onNoteDeleteRef.current = onNoteDelete;
240
+ }, [onNoteDelete]);
235
241
  useEffect(() => {
236
242
  progressRef.current = progress;
237
243
  }, [progress]);
@@ -273,6 +279,10 @@ var Viewer = forwardRef(function Viewer2({ projectId, id = "default", baseUrl =
273
279
  }
274
280
  setIsAddNoteOpen(false);
275
281
  }, [send]);
282
+ const deleteNote = useCallback((noteId) => {
283
+ setLineNotes((prev) => prev.filter((n) => n._id !== noteId));
284
+ onNoteDeleteRef.current?.(noteId);
285
+ }, []);
276
286
  const handleMessage = useCallback(
277
287
  (event) => {
278
288
  let data = null;
@@ -347,14 +357,16 @@ var Viewer = forwardRef(function Viewer2({ projectId, id = "default", baseUrl =
347
357
  activeLine,
348
358
  lineNotes,
349
359
  externalNotes,
360
+ userId,
350
361
  isAddNoteOpen,
351
362
  addNoteTagId,
352
363
  controls,
353
364
  openAddNote,
354
365
  closeAddNote,
355
- submitNote
366
+ submitNote,
367
+ deleteNote
356
368
  }),
357
- [project, lines, activeLineId, activeLine, lineNotes, externalNotes, isAddNoteOpen, addNoteTagId, controls, openAddNote, closeAddNote, submitNote]
369
+ [project, lines, activeLineId, activeLine, lineNotes, externalNotes, userId, isAddNoteOpen, addNoteTagId, controls, openAddNote, closeAddNote, submitNote, deleteNote]
358
370
  );
359
371
  const progressCtx = useMemo2(
360
372
  () => ({ progress, closestTagId, targetPosition, distanceFromStart, isPlaying }),
@@ -532,7 +544,7 @@ var styles2 = StyleSheet4.create({
532
544
  });
533
545
 
534
546
  // src/LineDetailSheet.tsx
535
- import React6, { useCallback as useCallback2, useEffect as useEffect3, useMemo as useMemo4, useRef as useRef3, useState as useState3 } from "react";
547
+ import React6, { Fragment, useCallback as useCallback2, useEffect as useEffect3, useMemo as useMemo4, useRef as useRef3, useState as useState3 } from "react";
536
548
  import { Image, Pressable as Pressable2, StyleSheet as StyleSheet5, Text as Text2, View as View5 } from "react-native";
537
549
  import {
538
550
  BottomSheetBackdrop as BottomSheetBackdrop2,
@@ -541,7 +553,7 @@ import {
541
553
  BottomSheetScrollView
542
554
  } from "@gorhom/bottom-sheet";
543
555
  import Svg, { Circle } from "react-native-svg";
544
- import { ArrowDown as ArrowDown2, ArrowUp as ArrowUp2, Pause, Play, Plus } from "lucide-react-native";
556
+ import { ArrowDown as ArrowDown2, ArrowUp as ArrowUp2, Pause, Play, Plus, Trash2 } from "lucide-react-native";
545
557
 
546
558
  // src/lineProgress.ts
547
559
  function mergeLineTagsByProgress(controls, notes) {
@@ -616,7 +628,7 @@ function LineDetailSheet({ snapPoints = ["35%", "75%"], topInset, renderHeader,
616
628
  backgroundComponent: BlurBackground,
617
629
  topInset: effectiveTopInset
618
630
  },
619
- activeLine && (children ?? /* @__PURE__ */ React6.createElement(DefaultLineTagList, { listHeader: header }))
631
+ activeLine && (children ? /* @__PURE__ */ React6.createElement(BottomSheetScrollView, { contentContainerStyle: styles3.customContent }, children) : /* @__PURE__ */ React6.createElement(DefaultLineTagList, { listHeader: header }))
620
632
  );
621
633
  }
622
634
  function DefaultHeader({
@@ -688,14 +700,22 @@ function LineTagList({ children, listHeader }) {
688
700
  contentContainerStyle: styles3.list
689
701
  },
690
702
  listHeader,
691
- tags.map((tag) => /* @__PURE__ */ React6.createElement(View5, { key: tag._id, onLayout: (e) => handleItemLayout(tag._id, e) }, children(tag, tag._id === closestTagId)))
703
+ tags.map((tag, i) => {
704
+ const isNote = !!tag._isNote;
705
+ const prevIsNote = i > 0 && !!tags[i - 1]._isNote;
706
+ const showSep = isNote && !prevIsNote && i > 0 || !isNote && i > 0 && prevIsNote;
707
+ return /* @__PURE__ */ React6.createElement(Fragment, { key: tag._id }, showSep && /* @__PURE__ */ React6.createElement(View5, { style: styles3.separator }), /* @__PURE__ */ React6.createElement(View5, { onLayout: (e) => handleItemLayout(tag._id, e) }, children(tag, tag._id === closestTagId)));
708
+ })
692
709
  );
693
710
  }
694
711
  function DefaultLineTagList({ listHeader }) {
695
712
  return /* @__PURE__ */ React6.createElement(LineTagList, { listHeader }, (tag, isActive) => /* @__PURE__ */ React6.createElement(DefaultTagItem, { tag, isActive }));
696
713
  }
697
714
  function DefaultTagItem({ tag, isActive }) {
698
- const { controls, openAddNote } = useRetorBridge();
715
+ const { controls, openAddNote, userId, deleteNote } = useRetorBridge();
716
+ const isNote = !!tag._isNote;
717
+ const tagUserId = tag.userId;
718
+ const isOwn = !!userId && !!tagUserId && tagUserId === userId;
699
719
  const initial = tag.authorName?.trim().charAt(0).toUpperCase();
700
720
  const handlePress = () => {
701
721
  if (typeof tag.progress === "number") controls.scrollToProgress(tag.progress);
@@ -708,9 +728,17 @@ function DefaultTagItem({ tag, isActive }) {
708
728
  style: [styles3.tagItem, isActive && styles3.tagItemActive]
709
729
  },
710
730
  tag.avatarUrl ? /* @__PURE__ */ React6.createElement(Image, { source: { uri: tag.avatarUrl }, style: styles3.tagAvatar }) : initial ? /* @__PURE__ */ React6.createElement(View5, { style: styles3.tagInitial }, /* @__PURE__ */ React6.createElement(Text2, { style: styles3.tagInitialText }, initial)) : null,
711
- /* @__PURE__ */ React6.createElement(View5, { style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ React6.createElement(Text2, { style: [styles3.tagText, isActive && styles3.tagTextActive], numberOfLines: 1 }, tag.name), isActive && tag.description && /* @__PURE__ */ React6.createElement(Text2, { style: styles3.tagDescription, numberOfLines: 2 }, tag.description)),
731
+ /* @__PURE__ */ React6.createElement(View5, { style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ React6.createElement(Text2, { style: [styles3.tagText, isActive && styles3.tagTextActive], numberOfLines: isNote ? 3 : 1 }, tag.name), isActive && tag.description && /* @__PURE__ */ React6.createElement(Text2, { style: styles3.tagDescription, numberOfLines: 2 }, tag.description)),
712
732
  tag.subtitle && /* @__PURE__ */ React6.createElement(Text2, { style: styles3.tagSubtitle, numberOfLines: 1 }, tag.subtitle),
713
- isActive && /* @__PURE__ */ React6.createElement(
733
+ isNote && isOwn && /* @__PURE__ */ React6.createElement(
734
+ Pressable2,
735
+ {
736
+ onPress: () => deleteNote(tag._id),
737
+ style: styles3.deleteBtn
738
+ },
739
+ /* @__PURE__ */ React6.createElement(Trash2, { size: 12, color: "rgba(255,255,255,0.4)" })
740
+ ),
741
+ isActive && !isNote && /* @__PURE__ */ React6.createElement(
714
742
  Pressable2,
715
743
  {
716
744
  onPress: (e) => {
@@ -725,6 +753,7 @@ function DefaultTagItem({ tag, isActive }) {
725
753
  }
726
754
  var styles3 = StyleSheet5.create({
727
755
  handle: { backgroundColor: "rgba(255,255,255,0.3)" },
756
+ customContent: { paddingBottom: 120 },
728
757
  header: {
729
758
  flexDirection: "row",
730
759
  alignItems: "flex-start",
@@ -767,10 +796,18 @@ var styles3 = StyleSheet5.create({
767
796
  justifyContent: "center"
768
797
  },
769
798
  tagInitialText: { color: "white", fontSize: 11, fontWeight: "700" },
799
+ separator: { height: 1, backgroundColor: "rgba(255,255,255,0.1)", marginHorizontal: 20, marginVertical: 4 },
770
800
  tagText: { color: "rgba(255,255,255,0.6)", fontSize: 13 },
771
801
  tagTextActive: { color: "white", fontWeight: "600" },
772
802
  tagDescription: { color: "rgba(255,255,255,0.4)", fontSize: 11, marginTop: 2, lineHeight: 14 },
773
803
  tagSubtitle: { color: "rgba(255,255,255,0.4)", fontSize: 10 },
804
+ deleteBtn: {
805
+ width: 24,
806
+ height: 24,
807
+ borderRadius: 12,
808
+ alignItems: "center",
809
+ justifyContent: "center"
810
+ },
774
811
  plusBtn: {
775
812
  width: 24,
776
813
  height: 24,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retor/react-native",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "React Native SDK for embedding Retor 3D experiences",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",