querysub 0.152.0 → 0.154.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 (65) hide show
  1. package/package.json +6 -6
  2. package/src/-b-authorities/cloudflareHelpers.ts +11 -2
  3. package/src/3-path-functions/PathFunctionRunner.ts +168 -97
  4. package/src/3-path-functions/PathFunctionRunnerMain.ts +8 -2
  5. package/src/3-path-functions/pathFunctionLoader.ts +11 -6
  6. package/src/3-path-functions/syncSchema.ts +10 -1
  7. package/src/4-deploy/edgeBootstrap.ts +10 -1
  8. package/src/4-querysub/Querysub.ts +77 -3
  9. package/src/4-querysub/QuerysubController.ts +22 -2
  10. package/src/4-querysub/permissions.ts +33 -2
  11. package/src/4-querysub/querysubPrediction.ts +52 -18
  12. package/src/archiveapps/archiveGCEntry.tsx +38 -0
  13. package/src/archiveapps/archiveJoinEntry.ts +121 -0
  14. package/src/archiveapps/archiveMergeEntry.tsx +47 -0
  15. package/src/archiveapps/compressTest.tsx +59 -0
  16. package/src/archiveapps/lockTest.ts +127 -0
  17. package/src/config.ts +5 -0
  18. package/src/diagnostics/managementPages.tsx +55 -0
  19. package/src/diagnostics/misc-pages/ArchiveInspect.tsx +325 -0
  20. package/src/diagnostics/misc-pages/ArchiveViewer.tsx +781 -0
  21. package/src/diagnostics/misc-pages/ArchiveViewerTable.tsx +156 -0
  22. package/src/diagnostics/misc-pages/ArchiveViewerTree.tsx +573 -0
  23. package/src/diagnostics/misc-pages/ComponentSyncStats.tsx +129 -0
  24. package/src/diagnostics/misc-pages/LocalWatchViewer.tsx +431 -0
  25. package/src/diagnostics/misc-pages/RequireAuditPage.tsx +218 -0
  26. package/src/diagnostics/misc-pages/SnapshotViewer.tsx +206 -0
  27. package/src/diagnostics/misc-pages/TimeRangeView.tsx +648 -0
  28. package/src/diagnostics/misc-pages/archiveViewerFilter.tsx +221 -0
  29. package/src/diagnostics/misc-pages/archiveViewerShared.tsx +76 -0
  30. package/src/email/postmark.tsx +40 -0
  31. package/src/email/sendgrid.tsx +44 -0
  32. package/src/functional/UndoWatch.tsx +133 -0
  33. package/src/functional/diff.ts +858 -0
  34. package/src/functional/promiseCache.ts +67 -0
  35. package/src/functional/random.ts +9 -0
  36. package/src/functional/runCommand.ts +42 -0
  37. package/src/functional/runOnce.ts +7 -0
  38. package/src/functional/stats.ts +61 -0
  39. package/src/functional/throttleRerender.tsx +80 -0
  40. package/src/library-components/AspectSizedComponent.tsx +88 -0
  41. package/src/library-components/Histogram.tsx +338 -0
  42. package/src/library-components/InlinePopup.tsx +67 -0
  43. package/src/library-components/Notifications.tsx +153 -0
  44. package/src/library-components/RenderIfVisible.tsx +80 -0
  45. package/src/library-components/SimpleNotification.tsx +133 -0
  46. package/src/library-components/TabbedUI.tsx +39 -0
  47. package/src/library-components/animateAnyElement.tsx +65 -0
  48. package/src/library-components/errorNotifications.tsx +81 -0
  49. package/src/library-components/placeholder.ts +18 -0
  50. package/src/misc/format2.ts +48 -0
  51. package/src/misc.ts +33 -0
  52. package/src/misc2.ts +5 -0
  53. package/src/server.ts +2 -1
  54. package/src/storage/diskCache.ts +227 -0
  55. package/src/storage/diskCache2.ts +122 -0
  56. package/src/storage/fileSystemPointer.ts +72 -0
  57. package/src/user-implementation/LoginPage.tsx +78 -0
  58. package/src/user-implementation/RequireAuditPage.tsx +219 -0
  59. package/src/user-implementation/SecurityPage.tsx +212 -0
  60. package/src/user-implementation/UserPage.tsx +320 -0
  61. package/src/user-implementation/addSuperUser.ts +21 -0
  62. package/src/user-implementation/canSeeSource.ts +41 -0
  63. package/src/user-implementation/loginEmail.tsx +159 -0
  64. package/src/user-implementation/setEmailKey.ts +20 -0
  65. package/src/user-implementation/userData.ts +974 -0
@@ -0,0 +1,338 @@
1
+ import preact from "preact"; import { qreact } from "../4-dom/qreact";
2
+ import { formatNumber } from "socket-function/src/formatting/format";
3
+ import { css } from "typesafecss";
4
+ import { URLParam } from "../library-components/URLParam";
5
+ import { Anchor } from "../library-components/ATag";
6
+ import { performDrag2 } from "../library-components/drag";
7
+ import { atomic, atomicObjectSymbol, atomicObjectWrite } from "../2-proxy/PathValueProxyWatcher";
8
+ import { getProxyPath } from "../2-proxy/pathValueProxy";
9
+ import { authorityStorage } from "../0-path-value-core/pathValueCore";
10
+ import { addEpsilons } from "../bits";
11
+ import { Querysub } from "../4-querysub/QuerysubController";
12
+ import { MeasuredDiv } from "../library-components/MeasuredDiv";
13
+
14
+ export type ColumnDesc = {
15
+ title?: string;
16
+ format?: (value: number, context: { maxAbsoluteValue: number; long?: boolean }) => preact.ComponentChild;
17
+ link?: (range: { xStart: number; xEnd: number; }) => { param: URLParam; value: unknown; }[];
18
+ };
19
+ export type XYValue = { x: number; y: number };
20
+ export class Histogram extends qreact.Component<{
21
+ title?: string;
22
+ className?: string;
23
+ values: XYValue[];
24
+ xColumn?: ColumnDesc;
25
+ yColumn?: ColumnDesc;
26
+ noXAxisRounding?: boolean;
27
+
28
+ barWidth?: number;
29
+ barGap?: number;
30
+ yLabelCount?: number;
31
+ pixelsPerLabel?: number;
32
+ }> {
33
+ state = {
34
+ width: window.innerWidth,
35
+ height: 500,
36
+ yAxisWidth: 100,
37
+ dragging: undefined as {
38
+ xStart: number;
39
+ xEnd: number;
40
+ } | undefined,
41
+ };
42
+
43
+ render() {
44
+ let { className, values, xColumn, yColumn, noXAxisRounding } = this.props;
45
+ if (values.length === 0) return <div class={this.props.className + css.center}>No data</div>;
46
+ let width = Math.max(1, this.state.width);
47
+ let yAxisWidth = this.state.yAxisWidth;
48
+ const barSpacing = this.props.barGap ?? 2;
49
+ // In reality the width will be less, as some space will be taken up by the axis labels
50
+ let barWidth = this.props.barWidth ?? 20;
51
+ let barCount = Math.ceil((width - yAxisWidth) / (barWidth + barSpacing));
52
+ const pixelsPerLabel = this.props.pixelsPerLabel ?? 100;
53
+ let barsPerLabel = Math.ceil(pixelsPerLabel / (barWidth + barSpacing));
54
+ // The bars have to align with our labels, otherwise they can't be equally spaced,
55
+ // and that is the only spacing mode flex supports!
56
+ let barCountDelta = (barCount % barsPerLabel);
57
+ barCount -= barCountDelta;
58
+ barCount = Math.max(barsPerLabel, barCount);
59
+ // Recalculate the width
60
+ barWidth = (width - yAxisWidth) / barCount - barSpacing;
61
+ const yLabelCount = this.props.yLabelCount ?? 10;
62
+
63
+ // Values test code
64
+ // values = [];
65
+ // let rand = getSeededRandom(7);
66
+ // for (let i = 0; i < 1000; i++) {
67
+ // values.push({ x: i + 500, y: rand() * 1000 });
68
+ // }
69
+ // xColumn = { title: "X" };
70
+ // yColumn = { title: "Y" };
71
+
72
+ let minX = Math.min(...values.map((v) => v.x));
73
+ let maxX = Math.max(...values.map((v) => v.x));
74
+ if (maxX === minX) {
75
+ maxX = minX + 1;
76
+ }
77
+ // We often use maxX as the end, instead of the max, so... make it a bit bigger, so that works.
78
+ maxX = addEpsilons(maxX, 1);
79
+ let xIncrement = Math.ceil((maxX - minX) / barCount);
80
+ /** Get a nice number, https://stackoverflow.com/a/16363437/1117119, https://gist.github.com/igodorogea/4f42a95ea31414c3a755a8b202676dfd
81
+ * - Modified to handle negative numbers
82
+ * - Always returns a value >= num, unless num is < 0, then returns a number <= num.
83
+ */
84
+ function getNiceNumber(num: number): number {
85
+ if (num < 0) return -getNiceNumber(-num);
86
+ let zeros = Math.floor(Math.log10(num));
87
+ if (zeros === Number.NEGATIVE_INFINITY) return 1;
88
+ let fraction = num / 10 ** zeros;
89
+ if (fraction <= 1) fraction = 1;
90
+ else if (fraction <= 2) fraction = 2;
91
+ else if (fraction <= 5) fraction = 5;
92
+ else fraction = 10;
93
+ return fraction * 10 ** zeros;
94
+ }
95
+ if (!noXAxisRounding) {
96
+ xIncrement = getNiceNumber(xIncrement);
97
+ }
98
+
99
+ // Round to xIncrement
100
+ minX = Math.floor(minX / xIncrement) * xIncrement;
101
+ maxX = Math.ceil(maxX / xIncrement) * xIncrement;
102
+
103
+ // xIncrement MIGHT not cover the range we need (as we extended the range).
104
+ // So just add bars in increments of barsParLabel. This shrinks the bar pixel width, but... it should be fine.
105
+ while (barCount * xIncrement <= (maxX - minX)) {
106
+ barCount += barsPerLabel;
107
+ }
108
+ if (noXAxisRounding) {
109
+ xIncrement = (maxX - minX) / barCount;
110
+ }
111
+
112
+ let xBuckets: {
113
+ xStart: number;
114
+ xEnd: number;
115
+ sum: number;
116
+ }[] = [];
117
+ for (let i = 0; i < barCount; i++) {
118
+ xBuckets.push({
119
+ xStart: minX + i * xIncrement,
120
+ xEnd: minX + (i + 1) * xIncrement,
121
+ sum: 0,
122
+ });
123
+ }
124
+ function getBucketIndex(xValue: number) {
125
+ return Math.floor((xValue - minX) / xIncrement);
126
+ }
127
+ for (let value of values) {
128
+ let bucketIndex = getBucketIndex(value.x);
129
+ xBuckets[bucketIndex].sum += value.y;
130
+ }
131
+
132
+ let minY = Math.min(...xBuckets.map((v) => v.sum));
133
+ let maxY = Math.max(...xBuckets.map((v) => v.sum));
134
+ maxY = getNiceNumber(maxY);
135
+ // Always try to start at zero
136
+ if (minY / (maxY - minY) < 0.35) {
137
+ minY = 0;
138
+ }
139
+
140
+ let yIncrement = getNiceNumber((maxY - minY) / yLabelCount);
141
+ let yLabelValues: number[] = [];
142
+ for (let i = 0; i < yLabelCount; i++) {
143
+ yLabelValues.push(minY + i * yIncrement);
144
+ }
145
+ yLabelValues.reverse();
146
+
147
+
148
+ let maxAbsoluteY = Math.max(Math.abs(minY), Math.abs(maxY));
149
+ let maxAbsoluteX = Math.max(Math.abs(minX), Math.abs(maxX));
150
+
151
+ function formatX(value: number) {
152
+ return xColumn?.format?.(value, { maxAbsoluteValue: maxAbsoluteX }) ?? formatNumber(value);
153
+ }
154
+ function formatXLong(value: number) {
155
+ return xColumn?.format?.(value, { maxAbsoluteValue: maxAbsoluteX, long: true }) ?? formatNumber(value);
156
+ }
157
+ function formatY(value: number) {
158
+ return yColumn?.format?.(value, { maxAbsoluteValue: maxAbsoluteY }) ?? formatNumber(value);
159
+ }
160
+ function formatYLong(value: number) {
161
+ return yColumn?.format?.(value, { maxAbsoluteValue: maxAbsoluteY, long: true }) ?? formatNumber(value);
162
+ }
163
+
164
+ // NOTE: Y Axis labels are less accurately positioned (especially if some have wrapping text, and others don't)
165
+ // but... it should be fine. The x axis labels are the ones that really need to be accurate, because the users will
166
+ // be looking at specific bars.
167
+ let yAxisLabels = yLabelValues.map((yValue, i) => {
168
+ i = yLabelCount - i - 1;
169
+ let frac = 1 - i / (yLabelCount - 1);
170
+ let pos = frac === 0 ? "0%" as const : `calc(${frac * 100}% - 2px)` as const;
171
+ return (
172
+ <div class={
173
+ css.maxWidth("30vw").height("100%").hbox0.relative
174
+ }>
175
+ <div class={css.relative.top(`${80 * (frac - 0.5)}%`)}>
176
+ {formatY(yValue)}
177
+ </div>
178
+ {/* <div class={css.fillHeight.relative.marginLeft(4)}>
179
+ <div class={css.relative.pos(0, pos).size(4, 1).hsla(0, 0, 7, 0.4).display("block").marginTop(1)} />
180
+ </div> */}
181
+ </div>
182
+ );
183
+ });
184
+ let xAxisLabels: preact.ComponentChild[] = [];
185
+ for (let i = 0; i < barCount; i += barsPerLabel) {
186
+ let firstBucket = xBuckets[i];
187
+ let lastBucket = xBuckets[i + barsPerLabel - 1];
188
+ let middleXValue = (firstBucket.xStart + lastBucket.xEnd) / 2;
189
+ if (i === 0) {
190
+ middleXValue = firstBucket.xStart;
191
+ } else if (i === barCount - barsPerLabel) {
192
+ middleXValue = lastBucket.xEnd;
193
+ }
194
+ let link = xColumn?.link?.({ xStart: firstBucket.xStart, xEnd: lastBucket.xEnd }) || [];
195
+ xAxisLabels.push(
196
+ <div class={
197
+ css.relative
198
+ .width("100%")
199
+ .vbox0
200
+ .alignCenter
201
+ .offsetx(`calc(${barWidth * 0.5}px - 50%)`)
202
+ }>
203
+ {/* NOTE: Our ticks are off again, so... just don't show them. */}
204
+ {/* <div class={css.offsetx("-50%").size(1, 4).hsla(0, 0, 7, 0.4).display("block").marginTop(1)} /> */}
205
+ <Anchor
206
+ class={
207
+ css
208
+ .pad2(10, 0)
209
+ .boxSizing("content-box")
210
+ .textAlign("center")
211
+ }
212
+ values={link}
213
+ >
214
+ {formatX(middleXValue)}
215
+ </Anchor>
216
+ </div>
217
+ );
218
+ }
219
+
220
+ let totalY = xBuckets.reduce((sum, v) => sum + v.sum, 0);
221
+ const renderBucket = (bucket: { xStart: number; xEnd: number; sum: number }) => {
222
+ let barEnd = (bucket.sum - minY) / (maxY - minY);
223
+ let barStart = 0;
224
+ // If we have a value, but it's very small, increase it, so the bar is visible
225
+ if (0 < barEnd && barEnd < 0.01) {
226
+ // We want it to be 1 pixel high. Our bar height is less than this.state.height,
227
+ // but... as long as this is > 0.5 pixels, it should be visible.
228
+ barEnd = 1 / this.state.height;
229
+ }
230
+ if (minY < 0) {
231
+ barStart = (0 - minY) / (maxY - minY);
232
+ }
233
+ if (barEnd < barStart) {
234
+ [barStart, barEnd] = [barEnd, barStart];
235
+ }
236
+ let link = xColumn?.link?.({ xStart: bucket.xStart, xEnd: bucket.xEnd }) || [];
237
+ let drag = atomic(this.state.dragging);
238
+ let center = (bucket.xStart + bucket.xEnd) / 2;
239
+ let draggedOver = drag && drag.xStart <= center && center <= drag.xEnd;
240
+ return (
241
+ <Anchor
242
+ class={
243
+ css
244
+ .size(barWidth, "100%")
245
+ .relative
246
+ .background(`hsla(0, 0%, 100%, 0.2)`, "hover")
247
+ .userSelect("none").filter("brightness(1.1)", "hover")
248
+ .fillWidth
249
+ .flexShrink(100000000)
250
+ + (draggedOver && css.background("hsla(0, 0%, 100%, 0.4)"))
251
+ }
252
+ title={`${formatYLong(bucket.sum)} at ${formatXLong(bucket.xStart)} to ${formatXLong(bucket.xEnd)}`}
253
+ values={link}
254
+ onMouseDown={(e) => {
255
+ e.preventDefault();
256
+ const self = this;
257
+ self.state.dragging = atomicObjectWrite({ xStart: bucket.xStart, xEnd: bucket.xEnd });
258
+ performDrag2({
259
+ e,
260
+ slop: 5,
261
+ onFinally(apply) {
262
+ let dragState = atomic(self.state.dragging);
263
+ self.state.dragging = undefined;
264
+ if (dragState && apply) {
265
+ let xStart = dragState.xStart;
266
+ let xEnd = dragState.xEnd;
267
+ let link = xColumn?.link?.({ xStart, xEnd }) || [];
268
+ for (let value of link) {
269
+ value.param.value = value.value;
270
+ }
271
+ }
272
+ }
273
+ });
274
+ }}
275
+ onMouseOver={e => {
276
+ let dragging = atomic(this.state.dragging);
277
+ if (dragging) {
278
+ let xStart = Math.min(dragging.xStart, bucket.xStart);
279
+ let xEnd = Math.max(dragging.xEnd, bucket.xEnd);
280
+ this.state.dragging = atomicObjectWrite({ xStart, xEnd });
281
+ }
282
+ }}
283
+ >
284
+ <div class={
285
+ css
286
+ .absolute
287
+ .size("100%", `${(barEnd - barStart) * 100}%`)
288
+ .pos(0, `${(1 - barEnd) * 100}%`)
289
+ .hsl(240, 50, 75).vbox0.justifyContent("flex-end")
290
+ }>
291
+ </div>
292
+ </Anchor>
293
+ );
294
+ };
295
+ return (
296
+ <MeasuredDiv
297
+ onNewSize={(width, height, elem) => {
298
+ this.state.width = width;
299
+ this.state.height = height;
300
+ }}
301
+ class={
302
+ (className || css.size("100%", "100%"))
303
+ + css.hsl(75, 50, 75)
304
+ // NOTE: We might need scroll bars if the axis labels are too big (too much text).
305
+ + css.overflowAuto.vbox0
306
+ }
307
+ >
308
+ <div class={css.alignSelf("center").margins2(10, 4).fontSize(16)}>
309
+ {this.props.title}
310
+ {this.props.title && " ("}
311
+ {
312
+ yColumn && yColumn && `${formatY(totalY)} ${yColumn.title} vs ${xColumn?.title}`
313
+ || yColumn?.title && `${formatY(totalY)} ${yColumn.title}`
314
+ || xColumn?.title
315
+ }
316
+ {this.props.title && ")"}
317
+ </div>
318
+ <div class={
319
+ css
320
+ .display("grid").gridTemplateColumns("auto 1fr").gridTemplateRows("1fr auto")
321
+ .fillBoth
322
+ .relative
323
+ }>
324
+ <div className={css.vbox0.justifyContent("space-between").flexShrink0.fillHeight.alignEnd.margins2(4, 0)}>
325
+ {yAxisLabels}
326
+ </div>
327
+ <div class={css.hbox(barSpacing).fillBoth.overflowHidden.flexShrink(10000000)}>
328
+ {xBuckets.map(renderBucket)}
329
+ </div>
330
+ <div />
331
+ <div class={css.hbox(barSpacing).alignItems("start").margins2(0, 2)}>
332
+ {xAxisLabels}
333
+ </div>
334
+ </div>
335
+ </MeasuredDiv>
336
+ );
337
+ }
338
+ }
@@ -0,0 +1,67 @@
1
+ import preact from "preact";
2
+ import { Querysub } from "../4-querysub/QuerysubController";
3
+ import { qreact } from "../4-dom/qreact";
4
+ import { Button } from "../library-components/Button";
5
+ import { Icon } from "../library-components/icons";
6
+ import { createURLSync } from "./URLParam";
7
+
8
+ const popupURL = createURLSync<string>("popup", "");
9
+
10
+
11
+ export class InlinePopup extends qreact.Component<{
12
+ type: string;
13
+ margin?: number;
14
+ }> {
15
+ render() {
16
+ if (popupURL.value !== this.props.type) return undefined;
17
+ let margin = this.props.margin ?? 100;
18
+ return (
19
+ <div
20
+ style={{
21
+ position: "fixed",
22
+ left: 0, top: 0,
23
+ width: "100vw", height: "100vh",
24
+ zIndex: 1000,
25
+ }}
26
+ onClick={e => qreact.isTarget(e) && (popupURL.value = "")}
27
+ >
28
+ <div style={{
29
+ margin,
30
+ position: "relative",
31
+ zIndex: 1,
32
+ }}>
33
+ <Button
34
+ hotkeys={["Escape"]}
35
+ showHotkeys
36
+ onClick={() => popupURL.value = ""}
37
+ style={{
38
+ position: "absolute",
39
+ right: 0, top: -10,
40
+ transform: "translate(0%, -100%)",
41
+ border: "1px solid hsl(0, 0%, 7%)",
42
+ }}
43
+ >
44
+ {Icon.close()}
45
+ </Button>
46
+ <div style={{
47
+ maxHeight: `calc(100vh - ${margin * 2}px)`,
48
+ overflow: "auto",
49
+ }}>
50
+ {this.props.children}
51
+ </div>
52
+ </div>
53
+ <div
54
+ className="button"
55
+ style={{
56
+ position: "absolute",
57
+ left: 0, top: 0,
58
+ width: "100%", height: "100%",
59
+ background: "hsl(0, 0%, 50%, 0.5)",
60
+ zIndex: 0,
61
+ }}
62
+ onClick={e => popupURL.value = ""}
63
+ />
64
+ </div>
65
+ );
66
+ }
67
+ }
@@ -0,0 +1,153 @@
1
+ import preact from "preact";
2
+ import { showModal } from "../5-diagnostics/Modal";
3
+ import { lazy } from "socket-function/src/caching";
4
+ import { Querysub } from "../4-querysub/Querysub";
5
+ import { nextId, sort } from "socket-function/src/misc";
6
+ import { atomicObjectWrite } from "../2-proxy/PathValueProxyWatcher";
7
+ import { css } from "../4-dom/css";
8
+ import { qreact } from "../4-dom/qreact";
9
+ import { yellow } from "socket-function/src/formatting/logColors";
10
+
11
+ export type NotifyPos = "topLeft" | "topRight" | "bottomLeft" | "bottomRight";
12
+
13
+ let nextSeqNum = 1;
14
+ interface NotifySchema {
15
+ id?: string;
16
+ seqNum: number;
17
+ notification: preact.ComponentChild;
18
+ pos: NotifyPos;
19
+ }
20
+ let notifySchema = Querysub.createLocalSchema<{
21
+ notifications: {
22
+ [seqNum: number]: NotifySchema;
23
+ };
24
+ }>("notifications");
25
+
26
+ class Notifications extends qreact.Component {
27
+ render() {
28
+ let notes = Object.values(notifySchema().notifications);
29
+ let byPos = new Map<NotifyPos, NotifySchema[]>();
30
+ for (let note of notes) {
31
+ let pos = note.pos;
32
+ if (!byPos.has(pos)) byPos.set(pos, []);
33
+ byPos.get(pos)!.push(note);
34
+ }
35
+ return <>
36
+ {Array.from(byPos.entries()).map(([pos, notes]) => {
37
+ sort(notes, note => note.seqNum);
38
+ let style: preact.JSX.CSSProperties = {};
39
+ if (pos === "topLeft") {
40
+ style.top = 0;
41
+ style.left = 0;
42
+ }
43
+ if (pos === "topRight") {
44
+ style.top = 0;
45
+ style.right = 0;
46
+ }
47
+ if (pos === "bottomLeft") {
48
+ style.bottom = 0;
49
+ style.left = 0;
50
+ }
51
+ if (pos === "bottomRight") {
52
+ style.bottom = 0;
53
+ style.right = 0;
54
+ }
55
+ let usedIds = new Set<string>();
56
+ notes = notes.filter(note => {
57
+ if (!note.id) return true;
58
+ if (usedIds.has(note.id)) return false;
59
+ usedIds.add(note.id);
60
+ return true;
61
+ });
62
+ return <div
63
+ class={
64
+ css.position("fixed").vbox(10).pad(20)
65
+ .zIndex(1000000)
66
+ }
67
+ style={style}
68
+ >
69
+ {notes.slice(0, 10).map(note =>
70
+ <CloseWrapper key={note.seqNum} seqNum={note.seqNum}>
71
+ {note.notification}
72
+ </CloseWrapper>
73
+ )}
74
+ {notes.length > 10 && <div class={css.hsl(0, 0, 50).pad2(4, 2)}>
75
+ <div>And {notes.length - 10} more notifications</div>
76
+ </div>}
77
+ </div>;
78
+ })}
79
+ </>;
80
+ }
81
+ }
82
+ class CloseWrapper extends qreact.Component<{ seqNum: number }> {
83
+ render() {
84
+ return this.props.children;
85
+ }
86
+ }
87
+ let ensureNotifications = lazy(() => {
88
+ showModal({ content: <Notifications /> });
89
+ });
90
+
91
+ /** Allows capturing the current state so the notification can be closed later on */
92
+ export function createCloseCurrentNotification() {
93
+ const ancestor = qreact.getAncestor(CloseWrapper);
94
+ if (!ancestor) {
95
+ console.warn(yellow(`Cannot close current Notification as we are not presently inside of a Notification.`));
96
+ return;
97
+ }
98
+ let seqNum = ancestor.data().props.seqNum as number;
99
+ return () => {
100
+ Querysub.commit(() => {
101
+ // Close all, as we are getting too much notify spam
102
+ for (let key in notifySchema().notifications) {
103
+ delete notifySchema().notifications[key];
104
+ }
105
+ // delete notifySchema().notifications[seqNum];
106
+ });
107
+ };
108
+ }
109
+
110
+ export function closeCurrentNotification() {
111
+ const ancestor = qreact.getAncestor(CloseWrapper);
112
+ if (!ancestor) {
113
+ console.warn(yellow(`Cannot close current Notification as we are not presently inside of a Notification.`));
114
+ return;
115
+ }
116
+ Querysub.commit(() => {
117
+ // Close all, as we are getting too much notify spam
118
+ for (let key in notifySchema().notifications) {
119
+ delete notifySchema().notifications[key];
120
+ }
121
+ //delete notifySchema().notifications[ancestor.data().props.seqNum as number];
122
+ });
123
+ }
124
+
125
+ export function showNotification(config: {
126
+ notification: preact.ComponentChild;
127
+ pos: NotifyPos;
128
+ timeout?: number;
129
+ id?: string;
130
+ }): {
131
+ close: () => void;
132
+ } {
133
+ let { notification, pos, timeout, id } = config;
134
+ ensureNotifications();
135
+ let seqNum = nextSeqNum++;
136
+ Querysub.commit(() => {
137
+ notifySchema().notifications[seqNum] = atomicObjectWrite({
138
+ notification,
139
+ pos,
140
+ seqNum,
141
+ id,
142
+ });
143
+ });
144
+ function close() {
145
+ Querysub.commit(() => {
146
+ delete notifySchema().notifications[seqNum];
147
+ });
148
+ }
149
+ if (timeout) {
150
+ setTimeout(close, timeout);
151
+ }
152
+ return { close };
153
+ }
@@ -0,0 +1,80 @@
1
+
2
+ import preact from "preact";
3
+ import { VirtualDOM, qreact } from "../4-dom/qreact";
4
+ import { Querysub } from "../4-querysub/Querysub";
5
+ import { cacheLimited, cacheWeak, lazy } from "socket-function/src/caching";
6
+ import { nextId, throttleFunction } from "socket-function/src/misc";
7
+
8
+ let data = Querysub.createLocalSchema<{
9
+ visibleIds: {
10
+ [key: string]: boolean;
11
+ }
12
+ }>("RenderIfVisible");
13
+
14
+ let getIntersectionObserver = cacheWeak((root: HTMLElement) => {
15
+ return new IntersectionObserver(entries => {
16
+ let visibleCount = 0;
17
+ Querysub.localCommit(() => {
18
+ for (let entry of entries) {
19
+ let id = entry.target.getAttribute("data-intersection-id")!;
20
+ let isVisible = !!entry.isIntersecting;
21
+ data().visibleIds[id] = isVisible;
22
+ if (isVisible) {
23
+ visibleCount++;
24
+ }
25
+ }
26
+ });
27
+ console.log("Visible count", visibleCount);
28
+ }, {
29
+ root,
30
+ rootMargin: "2000px",
31
+ // TODO: Scrolling breaks this slightly. We need to add a fixed position
32
+ // div inside of the scroll div, and use that as the root. Maybe?
33
+ // - See https://stackoverflow.com/questions/58622664/intersectionobserver-how-rootmargin-work/58625634#58625634
34
+ // - Basically... we can't know the position of the images outside of the scrolled div,
35
+ // because they are cutoff by the scrolling, which hides their true position
36
+ // from the IntersectionObserver. Or something...
37
+ });
38
+ });
39
+
40
+ /** If not visible, wipes out the children. We still need to render something,
41
+ * otherwise we can't know if it would be visible or not!
42
+ * The size of the element shouldn't depend on it's children, otherwise it will move
43
+ * around, and possibly get an infinite loop of re-renders.
44
+ */
45
+ export class RenderIfVisible extends qreact.Component<preact.JSX.HTMLAttributes<HTMLDivElement> & {
46
+ uniqueKey: string;
47
+ overrideType?: string;
48
+ root?: () => HTMLElement | undefined | null;
49
+ replacement?: VirtualDOM;
50
+ forceShow?: boolean;
51
+ }> {
52
+ id = nextId();
53
+ lastElement: HTMLElement | null = null;
54
+ observer() {
55
+ return getIntersectionObserver(this.props.root?.() ?? document.body);
56
+ }
57
+ componentWillUnmount(): void {
58
+ if (this.lastElement) {
59
+ this.observer().unobserve(this.lastElement);
60
+ }
61
+ }
62
+ render() {
63
+ let props = { ...this.props };
64
+ if (!this.props.forceShow && !data().visibleIds[this.id]) {
65
+ props.children = undefined;
66
+ }
67
+ return qreact.createElement(this.props.overrideType ?? "div", {
68
+ key: this.props.uniqueKey,
69
+ ...props,
70
+ "data-intersection-id": this.id,
71
+ ref2: (elem: HTMLElement) => {
72
+ if (this.lastElement) {
73
+ this.observer().unobserve(this.lastElement);
74
+ }
75
+ this.lastElement = elem;
76
+ this.observer().observe(elem);
77
+ }
78
+ } as any);
79
+ }
80
+ }