@blocksdiy/react-common 1.70.2 → 1.71.0

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 (57) hide show
  1. package/dist/comments/anchor.d.ts +74 -0
  2. package/dist/comments/anchor.d.ts.map +1 -0
  3. package/dist/comments/anchor.js +279 -0
  4. package/dist/comments/api.d.ts +38 -0
  5. package/dist/comments/api.d.ts.map +1 -0
  6. package/dist/comments/api.js +62 -0
  7. package/dist/comments/comment-agents.d.ts +28 -0
  8. package/dist/comments/comment-agents.d.ts.map +1 -0
  9. package/dist/comments/comment-agents.js +32 -0
  10. package/dist/comments/comment-mode.d.ts +97 -0
  11. package/dist/comments/comment-mode.d.ts.map +1 -0
  12. package/dist/comments/comment-mode.js +159 -0
  13. package/dist/comments/composer.d.ts +133 -0
  14. package/dist/comments/composer.d.ts.map +1 -0
  15. package/dist/comments/composer.js +273 -0
  16. package/dist/comments/index.d.ts +13 -0
  17. package/dist/comments/index.d.ts.map +1 -0
  18. package/dist/comments/index.js +17 -0
  19. package/dist/comments/layout.d.ts +25 -0
  20. package/dist/comments/layout.d.ts.map +1 -0
  21. package/dist/comments/layout.js +83 -0
  22. package/dist/comments/mentions.d.ts +32 -0
  23. package/dist/comments/mentions.d.ts.map +1 -0
  24. package/dist/comments/mentions.js +30 -0
  25. package/dist/comments/page-pick.d.ts +76 -0
  26. package/dist/comments/page-pick.d.ts.map +1 -0
  27. package/dist/comments/page-pick.js +267 -0
  28. package/dist/comments/pick-message.d.ts +11 -0
  29. package/dist/comments/pick-message.d.ts.map +1 -0
  30. package/dist/comments/pick-message.js +33 -0
  31. package/dist/comments/pins.d.ts +72 -0
  32. package/dist/comments/pins.d.ts.map +1 -0
  33. package/dist/comments/pins.js +125 -0
  34. package/dist/comments/text-pick.d.ts +59 -0
  35. package/dist/comments/text-pick.d.ts.map +1 -0
  36. package/dist/comments/text-pick.js +165 -0
  37. package/dist/comments/threads-store.d.ts +119 -0
  38. package/dist/comments/threads-store.d.ts.map +1 -0
  39. package/dist/comments/threads-store.js +286 -0
  40. package/dist/comments/types.d.ts +232 -0
  41. package/dist/comments/types.d.ts.map +1 -0
  42. package/dist/comments/types.js +109 -0
  43. package/dist/hooks/agent-conversation-context.d.ts +12 -1
  44. package/dist/hooks/agent-conversation-context.d.ts.map +1 -1
  45. package/dist/hooks/agent-conversation-context.js +12 -5
  46. package/dist/hooks/agent-thread-live-update.d.ts +5 -2
  47. package/dist/hooks/agent-thread-live-update.d.ts.map +1 -1
  48. package/dist/hooks/agent-thread-live-update.js +14 -0
  49. package/dist/hooks/use-agent-conversation.d.ts +8 -0
  50. package/dist/hooks/use-agent-conversation.d.ts.map +1 -1
  51. package/dist/hooks/use-agent-conversation.js +10 -0
  52. package/dist/hooks/use-agent-threads.d.ts +1 -0
  53. package/dist/hooks/use-agent-threads.d.ts.map +1 -1
  54. package/dist/hooks/use-agent-threads.js +4 -1
  55. package/dist/iframe-communication/types.d.ts +24 -0
  56. package/dist/iframe-communication/types.d.ts.map +1 -1
  57. package/package.json +5 -2
@@ -0,0 +1,125 @@
1
+ import { useMemo } from "react";
2
+ import { checkText, findAnchorItemElement, resolveAnchor } from "./anchor.js";
3
+ import { clipRectsOf, toRect, usePageLayoutTick, visiblePart } from "./layout.js";
4
+ /**
5
+ * Where each thread's pin goes on the page as it is now. Never stored: worked out from the anchor
6
+ * every time the page moves, so a pin follows its element across screen sizes, re-sorted lists and
7
+ * scrolling.
8
+ */
9
+ /** How far a pin keeps from the viewport's edges. */
10
+ export const PIN_EDGE_MARGIN = 12;
11
+ /**
12
+ * The pin's point, in viewport coordinates.
13
+ * - A quote: the end of its last line.
14
+ * - An element: where it was clicked, scaled to the element's box now; else its top-right corner.
15
+ */
16
+ export const pinPoint = (target) => {
17
+ if (target.kind === "quote") {
18
+ return { x: target.lastLine.right, y: (target.lastLine.top + target.lastLine.bottom) / 2 };
19
+ }
20
+ const { rect, point } = target;
21
+ if (!point) {
22
+ return { x: rect.right, y: rect.top };
23
+ }
24
+ return {
25
+ x: rect.left + point.x * (rect.right - rect.left),
26
+ y: rect.top + point.y * (rect.bottom - rect.top),
27
+ };
28
+ };
29
+ /** `point` kept `margin` inside the viewport. */
30
+ export const keepInViewport = (point, viewport, margin = PIN_EDGE_MARGIN) => ({
31
+ x: Math.min(Math.max(point.x, margin), Math.max(margin, viewport.width - margin)),
32
+ y: Math.min(Math.max(point.y, margin), Math.max(margin, viewport.height - margin)),
33
+ });
34
+ /** Pins on the same element become one group, at the first one's point (items in input order). */
35
+ export const groupPins = (pins) => {
36
+ const groups = new Map();
37
+ for (const pin of pins) {
38
+ const group = groups.get(pin.key);
39
+ if (group) {
40
+ group.items.push(pin.item);
41
+ }
42
+ else {
43
+ groups.set(pin.key, { key: pin.key, point: pin.point, items: [pin.item] });
44
+ }
45
+ }
46
+ return [...groups.values()];
47
+ };
48
+ const HIDDEN = { point: null, groupKey: null };
49
+ /**
50
+ * The comments' own layer (pins, threads) marks its root with this attribute: what it draws over an
51
+ * element doesn't hide the element. Other ignored UI (the agent chat's panel) does.
52
+ */
53
+ export const COMMENTS_LAYER_ATTRIBUTE = "data-blocks-comments-layer";
54
+ /** Whether the element can be seen at `visible`'s centre, or only the comments' own layer is over it. */
55
+ const isUncovered = (element, visible) => {
56
+ const hit = document.elementFromPoint((visible.left + visible.right) / 2, (visible.top + visible.bottom) / 2);
57
+ return !hit || element.contains(hit) || hit.closest(`[${COMMENTS_LAYER_ATTRIBUTE}]`) !== null;
58
+ };
59
+ const elementKeys = new WeakMap();
60
+ let nextElementKey = 0;
61
+ const keyOf = (element) => {
62
+ let key = elementKeys.get(element);
63
+ if (!key) {
64
+ key = `el-${++nextElementKey}`;
65
+ elementKeys.set(element, key);
66
+ }
67
+ return key;
68
+ };
69
+ /** Lays out one anchor whose element is known. */
70
+ export const layoutAnchor = (anchor, resolved) => {
71
+ const { element, state, range, currentText } = resolved;
72
+ if (!element) {
73
+ return { state, element: null, currentText, ...HIDDEN };
74
+ }
75
+ const box = toRect(element.getBoundingClientRect());
76
+ const visible = visiblePart(box, clipRectsOf(element));
77
+ if (!visible || !isUncovered(element, visible)) {
78
+ return { state, element, currentText, ...HIDDEN };
79
+ }
80
+ const lines = range ? Array.from(range.getClientRects()).filter((rect) => rect.width > 0 || rect.height > 0) : [];
81
+ const lastLine = lines.at(-1);
82
+ const point = lastLine
83
+ ? pinPoint({ kind: "quote", lastLine: toRect(lastLine) })
84
+ : pinPoint({ kind: "element", rect: box, point: anchor.point });
85
+ return {
86
+ state,
87
+ element,
88
+ currentText,
89
+ point: keepInViewport(point, { width: window.innerWidth, height: window.innerHeight }),
90
+ groupKey: keyOf(element),
91
+ };
92
+ };
93
+ /** The element each anchor was last found at: an anchor's object stays the same while its thread is unchanged. */
94
+ const foundByAnchor = new WeakMap();
95
+ /** Finds an anchor again, re-checking only its text while its element is still in the page and still in the same row. */
96
+ const resolveCached = (anchor) => {
97
+ const known = foundByAnchor.get(anchor);
98
+ const stillThere = known?.isConnected && (!anchor.item || findAnchorItemElement(anchor)?.contains(known) === true);
99
+ const resolved = stillThere ? checkText(anchor, known) : resolveAnchor(anchor);
100
+ if (resolved.element) {
101
+ foundByAnchor.set(anchor, resolved.element);
102
+ }
103
+ else {
104
+ foundByAnchor.delete(anchor);
105
+ }
106
+ return resolved;
107
+ };
108
+ /** Where each thread is on the page now, re-measured at most once per frame while `isActive`. */
109
+ export const useCommentPins = (anchors, isActive) => {
110
+ // Not for the React Compiler (app-render compiles this module): the memo's input is the page's
111
+ // layout, read through `tick`, which the compiler would drop from the deps as unused.
112
+ "use no memo";
113
+ const tick = usePageLayoutTick(isActive);
114
+ return useMemo(() => {
115
+ void tick;
116
+ const layouts = new Map();
117
+ if (!isActive || typeof document === "undefined") {
118
+ return layouts;
119
+ }
120
+ for (const { id, anchor } of anchors) {
121
+ layouts.set(id, layoutAnchor(anchor, resolveCached(anchor)));
122
+ }
123
+ return layouts;
124
+ }, [anchors, isActive, tick]);
125
+ };
@@ -0,0 +1,59 @@
1
+ import { type Rect } from "./layout.js";
2
+ /**
3
+ * Pointing at part of a text, as the picker does over text: the caret position under the pointer,
4
+ * the word around it, and the range a drag covers — clamped to the row it started in, so a quote
5
+ * never spans two records. The geometry is the browser's; what can be computed without it is pure
6
+ * (word bounds, hit tests, ordering) and tested on its own.
7
+ */
8
+ /** A position in a text node: before the character at `offset`. */
9
+ export interface TextPoint {
10
+ node: Text;
11
+ offset: number;
12
+ }
13
+ /** A position between nodes or characters, as a Range's boundary takes it. */
14
+ export interface Boundary {
15
+ node: Node;
16
+ offset: number;
17
+ }
18
+ /** How far the pointer moves from a press on text before it is a drag, not a click. */
19
+ export declare const DRAG_THRESHOLD_PX = 4;
20
+ /**
21
+ * The word around `offset` in `text` — the one it is in, else the one ending right before it —
22
+ * as `[start, end)`; null over spaces and punctuation. Words in any script (`Intl.Segmenter`), a
23
+ * whitespace split where it is missing.
24
+ */
25
+ export declare const wordBoundsAt: (text: string, offset: number, locale?: string) => {
26
+ start: number;
27
+ end: number;
28
+ } | null;
29
+ /** Whether `(x, y)` is on one of `rects`, with `slop` px around each. */
30
+ export declare const isPointInRects: (x: number, y: number, rects: readonly Rect[], slop?: number) => boolean;
31
+ /** Whether `a` comes after `b` in the document. */
32
+ export declare const isAfter: (a: Boundary, b: Boundary) => boolean;
33
+ /** The range between two positions, in document order. */
34
+ export declare const rangeBetween: (doc: Document, a: Boundary, b: Boundary) => Range;
35
+ /**
36
+ * What a drag that starts at `node` may cover: the record it is in (the nearest `itemAnchorProps`
37
+ * element), else the element a click there would pick. A quote never leaves it.
38
+ */
39
+ export declare const dragRootOf: (node: Node) => Element | null;
40
+ /** `point` kept inside `root`: a position past its end is its end, one before its start is its start. */
41
+ export declare const clampToRoot: (root: Element, point: Boundary) => Boundary;
42
+ /** The caret position the browser puts under `(x, y)`, when it is in a text node. */
43
+ export declare const caretFromPoint: (doc: Document, x: number, y: number) => TextPoint | null;
44
+ /**
45
+ * The text position under the pointer, only when the pointer is ON the text: the browser answers
46
+ * with the nearest text from anywhere in an element, so the characters beside the caret decide
47
+ * (a space between words counts, so the cursor doesn't flicker along a line). Null over empty
48
+ * space, in ignored UI or an editable field.
49
+ */
50
+ export declare const textPointAt: (doc: Document, x: number, y: number) => TextPoint | null;
51
+ /** The word under a text position, as a range: what the hover tints. */
52
+ export declare const wordRangeAt: (doc: Document, point: TextPoint) => Range | null;
53
+ /** Where the caret bar goes for a text position: the edge of the character beside it. */
54
+ export declare const caretRectAt: (doc: Document, point: TextPoint) => {
55
+ x: number;
56
+ top: number;
57
+ bottom: number;
58
+ } | null;
59
+ //# sourceMappingURL=text-pick.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-pick.d.ts","sourceRoot":"","sources":["../../src/comments/text-pick.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,IAAI,EAAU,MAAM,UAAU,CAAC;AAE7C;;;;;GAKG;AAEH,mEAAmE;AACnE,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,8EAA8E;AAC9E,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,uFAAuF;AACvF,eAAO,MAAM,iBAAiB,IAAI,CAAC;AAgBnC;;;;GAIG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,EAAE,QAAQ,MAAM,EAAE,SAAS,MAAM,KAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAoC7G,CAAC;AAEF,yEAAyE;AACzE,eAAO,MAAM,cAAc,GAAI,GAAG,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,SAAS,IAAI,EAAE,EAAE,aAAQ,KAAG,OAGrF,CAAC;AAEJ,mDAAmD;AACnD,eAAO,MAAM,OAAO,GAAI,GAAG,QAAQ,EAAE,GAAG,QAAQ,KAAG,OAMlD,CAAC;AAEF,0DAA0D;AAC1D,eAAO,MAAM,YAAY,GAAI,KAAK,QAAQ,EAAE,GAAG,QAAQ,EAAE,GAAG,QAAQ,KAAG,KAMtE,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,UAAU,GAAI,MAAM,IAAI,KAAG,OAAO,GAAG,IAGjD,CAAC;AAEF,yGAAyG;AACzG,eAAO,MAAM,WAAW,GAAI,MAAM,OAAO,EAAE,OAAO,QAAQ,KAAG,QAM5D,CAAC;AAOF,qFAAqF;AACrF,eAAO,MAAM,cAAc,GAAI,KAAK,QAAQ,EAAE,GAAG,MAAM,EAAE,GAAG,MAAM,KAAG,SAAS,GAAG,IAchF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,QAAQ,EAAE,GAAG,MAAM,EAAE,GAAG,MAAM,KAAG,SAAS,GAAG,IAgB7E,CAAC;AAEF,wEAAwE;AACxE,eAAO,MAAM,WAAW,GAAI,KAAK,QAAQ,EAAE,OAAO,SAAS,KAAG,KAAK,GAAG,IASrE,CAAC;AAEF,yFAAyF;AACzF,eAAO,MAAM,WAAW,GAAI,KAAK,QAAQ,EAAE,OAAO,SAAS,KAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAmB1G,CAAC"}
@@ -0,0 +1,165 @@
1
+ import { isPickIgnored, ITEM_ATTRIBUTE, pickTarget } from "./anchor.js";
2
+ import { toRect } from "./layout.js";
3
+ /** How far the pointer moves from a press on text before it is a drag, not a click. */
4
+ export const DRAG_THRESHOLD_PX = 4;
5
+ const segmenters = new Map();
6
+ const wordSegmenter = (locale) => {
7
+ if (typeof Intl === "undefined" || typeof Intl.Segmenter !== "function") {
8
+ return null;
9
+ }
10
+ const key = locale ?? "";
11
+ let segmenter = segmenters.get(key);
12
+ if (!segmenter) {
13
+ segmenter = new Intl.Segmenter(locale, { granularity: "word" });
14
+ segmenters.set(key, segmenter);
15
+ }
16
+ return segmenter;
17
+ };
18
+ /**
19
+ * The word around `offset` in `text` — the one it is in, else the one ending right before it —
20
+ * as `[start, end)`; null over spaces and punctuation. Words in any script (`Intl.Segmenter`), a
21
+ * whitespace split where it is missing.
22
+ */
23
+ export const wordBoundsAt = (text, offset, locale) => {
24
+ if (!text) {
25
+ return null;
26
+ }
27
+ const at = Math.max(0, Math.min(offset, text.length));
28
+ const segmenter = wordSegmenter(locale);
29
+ if (segmenter) {
30
+ let before = null;
31
+ for (const segment of segmenter.segment(text)) {
32
+ const start = segment.index;
33
+ const end = start + segment.segment.length;
34
+ if (!segment.isWordLike) {
35
+ continue;
36
+ }
37
+ if (at >= start && at < end) {
38
+ return { start, end };
39
+ }
40
+ if (end === at) {
41
+ before = { start, end };
42
+ }
43
+ }
44
+ return before;
45
+ }
46
+ const isWordChar = (char) => !!char && !/[\s.,;:!?()[\]{}"'«»“”‘’…-]/.test(char);
47
+ let start = isWordChar(text[at]) ? at : isWordChar(text[at - 1]) ? at - 1 : -1;
48
+ if (start < 0) {
49
+ return null;
50
+ }
51
+ let end = start;
52
+ while (start > 0 && isWordChar(text[start - 1])) {
53
+ start -= 1;
54
+ }
55
+ while (end < text.length && isWordChar(text[end])) {
56
+ end += 1;
57
+ }
58
+ return { start, end };
59
+ };
60
+ /** Whether `(x, y)` is on one of `rects`, with `slop` px around each. */
61
+ export const isPointInRects = (x, y, rects, slop = 1) => rects.some((rect) => x >= rect.left - slop && x <= rect.right + slop && y >= rect.top - slop && y <= rect.bottom + slop);
62
+ /** Whether `a` comes after `b` in the document. */
63
+ export const isAfter = (a, b) => {
64
+ const doc = b.node.ownerDocument ?? b.node;
65
+ const at = doc.createRange();
66
+ at.setStart(b.node, b.offset);
67
+ at.collapse(true);
68
+ return at.comparePoint(a.node, a.offset) > 0;
69
+ };
70
+ /** The range between two positions, in document order. */
71
+ export const rangeBetween = (doc, a, b) => {
72
+ const [start, end] = isAfter(a, b) ? [b, a] : [a, b];
73
+ const range = doc.createRange();
74
+ range.setStart(start.node, start.offset);
75
+ range.setEnd(end.node, end.offset);
76
+ return range;
77
+ };
78
+ /**
79
+ * What a drag that starts at `node` may cover: the record it is in (the nearest `itemAnchorProps`
80
+ * element), else the element a click there would pick. A quote never leaves it.
81
+ */
82
+ export const dragRootOf = (node) => {
83
+ const element = node instanceof Element ? node : node.parentElement;
84
+ return element?.closest(`[${ITEM_ATTRIBUTE}]`) ?? pickTarget(node);
85
+ };
86
+ /** `point` kept inside `root`: a position past its end is its end, one before its start is its start. */
87
+ export const clampToRoot = (root, point) => {
88
+ if (root.contains(point.node)) {
89
+ return point;
90
+ }
91
+ const end = { node: root, offset: root.childNodes.length };
92
+ return isAfter(point, end) ? end : { node: root, offset: 0 };
93
+ };
94
+ /** The caret position the browser puts under `(x, y)`, when it is in a text node. */
95
+ export const caretFromPoint = (doc, x, y) => {
96
+ const caretDoc = doc;
97
+ let node = null;
98
+ let offset = 0;
99
+ if (typeof caretDoc.caretPositionFromPoint === "function") {
100
+ const position = caretDoc.caretPositionFromPoint(x, y);
101
+ node = position?.offsetNode ?? null;
102
+ offset = position?.offset ?? 0;
103
+ }
104
+ else if (typeof caretDoc.caretRangeFromPoint === "function") {
105
+ const range = caretDoc.caretRangeFromPoint(x, y);
106
+ node = range?.startContainer ?? null;
107
+ offset = range?.startOffset ?? 0;
108
+ }
109
+ return node && node.nodeType === Node.TEXT_NODE ? { node: node, offset } : null;
110
+ };
111
+ /**
112
+ * The text position under the pointer, only when the pointer is ON the text: the browser answers
113
+ * with the nearest text from anywhere in an element, so the characters beside the caret decide
114
+ * (a space between words counts, so the cursor doesn't flicker along a line). Null over empty
115
+ * space, in ignored UI or an editable field.
116
+ */
117
+ export const textPointAt = (doc, x, y) => {
118
+ const point = caretFromPoint(doc, x, y);
119
+ if (!point || isPickIgnored(point.node)) {
120
+ return null;
121
+ }
122
+ if (point.node.parentElement?.closest("input, textarea, [contenteditable]")) {
123
+ return null;
124
+ }
125
+ const length = point.node.data.length;
126
+ if (length === 0) {
127
+ return null;
128
+ }
129
+ const range = doc.createRange();
130
+ range.setStart(point.node, Math.max(0, Math.min(point.offset, length) - 1));
131
+ range.setEnd(point.node, Math.min(length, point.offset + 1));
132
+ return isPointInRects(x, y, Array.from(range.getClientRects(), toRect)) ? point : null;
133
+ };
134
+ /** The word under a text position, as a range: what the hover tints. */
135
+ export const wordRangeAt = (doc, point) => {
136
+ const word = wordBoundsAt(point.node.data, point.offset, doc.documentElement.lang || undefined);
137
+ if (!word) {
138
+ return null;
139
+ }
140
+ const range = doc.createRange();
141
+ range.setStart(point.node, word.start);
142
+ range.setEnd(point.node, word.end);
143
+ return range;
144
+ };
145
+ /** Where the caret bar goes for a text position: the edge of the character beside it. */
146
+ export const caretRectAt = (doc, point) => {
147
+ const range = doc.createRange();
148
+ const length = point.node.data.length;
149
+ if (length === 0) {
150
+ return null;
151
+ }
152
+ const after = point.offset < length;
153
+ range.setStart(point.node, after ? point.offset : point.offset - 1);
154
+ range.setEnd(point.node, after ? point.offset + 1 : point.offset);
155
+ const rect = range.getClientRects()[0];
156
+ if (!rect) {
157
+ return null;
158
+ }
159
+ const isRtl = point.node.parentElement
160
+ ? doc.defaultView?.getComputedStyle(point.node.parentElement).direction === "rtl"
161
+ : false;
162
+ // Before a character: its leading edge; after the last one: its trailing edge.
163
+ const x = after !== isRtl ? rect.left : rect.right;
164
+ return { x, top: rect.top, bottom: rect.bottom };
165
+ };
@@ -0,0 +1,119 @@
1
+ import { type CommentsApi } from "./api.js";
2
+ import { type CommentAnchor, type CommentsChangedPayload, type CommentThreadDto, type CommentThreadSummaryDto, type MentionableUserDto } from "./types.js";
3
+ /**
4
+ * The comment threads of one page of an app, kept current: the page's threads, the open thread's
5
+ * comments, and the people who can be mentioned. Every change another tab makes, and every step of
6
+ * an agent's reply (this tab's mention included), arrives as an ids-only `COMMENTS_CHANGED` ping,
7
+ * and the store refetches that thread by id, which applies who may see it (a 404 removes it).
8
+ * Writes return the thread as the server saved it; either way a thread is replaced by id, and an
9
+ * answer that crossed a newer one is read again rather than set the thread back.
10
+ */
11
+ /** A page with more threads than this shows the newest ones. */
12
+ export declare const MAX_THREADS_PER_PAGE = 500;
13
+ /** Pings arriving together are refetched together. */
14
+ export declare const LIVE_BATCH_MS = 150;
15
+ /**
16
+ * Where opening a thread stands while its comments aren't in: `loading`, `not_found` (it was
17
+ * deleted, or the reader may not see it: a 404) or `failed` (a request that may work again).
18
+ */
19
+ export type CommentThreadLoadState = "loading" | "not_found" | "failed";
20
+ export interface CommentThreadsSnapshot {
21
+ status: "idle" | "loading" | "ready" | "error";
22
+ /** Newest activity first. */
23
+ threads: CommentThreadSummaryDto[];
24
+ /** The threads whose comments were loaded, by id. */
25
+ details: ReadonlyMap<number, CommentThreadDto>;
26
+ /** Threads asked for whose comments aren't in (none once they are), by id. */
27
+ threadLoads: ReadonlyMap<number, CommentThreadLoadState>;
28
+ mentionableUsers: MentionableUserDto[];
29
+ }
30
+ /** Subscribes to the app channel's comment pings; returns the unsubscribe. */
31
+ export type SubscribeToCommentPings = (onPing: (payload: CommentsChangedPayload) => void) => () => void;
32
+ export interface CommentThreadsStoreOptions {
33
+ api: CommentsApi;
34
+ appId: string;
35
+ subscribe: SubscribeToCommentPings;
36
+ setTimer?: (callback: () => void, ms: number) => ReturnType<typeof setTimeout>;
37
+ clearTimer?: (id: ReturnType<typeof setTimeout>) => void;
38
+ }
39
+ export declare class CommentThreadsStore {
40
+ private readonly options;
41
+ private snapshot;
42
+ private readonly listeners;
43
+ private routePath;
44
+ /** Bumps on every route change: an answer for an older route is dropped. */
45
+ private generation;
46
+ private stopPings;
47
+ private pendingIds;
48
+ private batchTimer;
49
+ /** Answers taken in so far; `takenAt` holds the count as of each thread's latest one. */
50
+ private taken;
51
+ private readonly takenAt;
52
+ private readonly setTimer;
53
+ private readonly clearTimer;
54
+ constructor(options: CommentThreadsStoreOptions);
55
+ getSnapshot: () => CommentThreadsSnapshot;
56
+ subscribe: (listener: () => void) => (() => void);
57
+ /** Shows the threads of `routePath` (null: none, and stop listening). */
58
+ setRoute(routePath: string | null): Promise<void>;
59
+ /**
60
+ * Loads a thread's comments (when it is opened). Until they are in, `threadLoads` says where it
61
+ * stands; a thread already loaded stays shown while it is read again.
62
+ */
63
+ loadThread(threadId: number): Promise<CommentThreadDto | null>;
64
+ createThread(anchor: CommentAnchor, body: string): Promise<CommentThreadDto>;
65
+ addComment(threadId: number, body: string): Promise<CommentThreadDto>;
66
+ updateComment(commentId: number, body: string): Promise<CommentThreadDto>;
67
+ /**
68
+ * Deletes one of the reader's comments. Null when it was the thread's last (no person's or agent's
69
+ * left): the thread is gone, and goes from the store as a 404 takes it (`not_found`).
70
+ */
71
+ deleteComment(commentId: number): Promise<CommentThreadDto | null>;
72
+ setResolved(threadId: number, resolved: boolean): Promise<CommentThreadDto>;
73
+ dispose(): void;
74
+ private loadAll;
75
+ private onPing;
76
+ private cancelBatch;
77
+ /** Whether the thread took an answer after a request sent at `sentAt` (a count of answers taken). */
78
+ private isCrossed;
79
+ /**
80
+ * Reads a thread and takes in the answer, by the rule above (`mayRetry` false: this is the read
81
+ * again, taken as it is). A 404 removes the thread. Returns the thread as the store has it now.
82
+ */
83
+ private refetch;
84
+ /**
85
+ * Sends a write and takes in its answer, by the rule above. Should reading the thread again fail,
86
+ * the write's answer is taken all the same, unless the thread is gone: the person's own change
87
+ * must show, and their tab gets no ping for it.
88
+ */
89
+ private write;
90
+ /** Takes in a thread as the server returned it. A thread of another page is kept out of the list. */
91
+ private apply;
92
+ private setLoad;
93
+ private remove;
94
+ private set;
95
+ }
96
+ /** Subscribes through the SDK's websocket connection (the app channel the app already joined). */
97
+ export declare const subscribeWithWebsockets: (service: {
98
+ listen(event: string, callback: (data: unknown) => Promise<void>, options?: {
99
+ filter?: Record<string, unknown>;
100
+ }): void;
101
+ unlisten(event: string, callback: (data: unknown) => Promise<void>): void;
102
+ }) => ((appId: string) => SubscribeToCommentPings);
103
+ /** The app's comment pings, on the websocket connection the SDK opened for the app. */
104
+ export declare const commentPingsFor: (appId: string) => SubscribeToCommentPings;
105
+ /**
106
+ * The threads of the current page while `isActive` (comment mode on): nothing is loaded and nothing
107
+ * listened to otherwise.
108
+ */
109
+ export declare const useCommentThreads: ({ api, appId, subscribe, routePath, isActive, }: {
110
+ api: CommentsApi;
111
+ appId: string;
112
+ subscribe: SubscribeToCommentPings;
113
+ routePath: string;
114
+ isActive: boolean;
115
+ }) => {
116
+ store: CommentThreadsStore;
117
+ snapshot: CommentThreadsSnapshot;
118
+ };
119
+ //# sourceMappingURL=threads-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"threads-store.d.ts","sourceRoot":"","sources":["../../src/comments/threads-store.ts"],"names":[],"mappings":"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"}