@mushi-mushi/web 1.10.0 → 1.12.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.
- package/README.md +28 -0
- package/dist/index.cjs +588 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.js +589 -6
- package/dist/index.js.map +1 -1
- package/dist/test-utils.cjs +1 -1
- package/dist/test-utils.js +1 -1
- package/package.json +6 -3
package/dist/index.cjs
CHANGED
|
@@ -2,7 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
var core = require('@mushi-mushi/core');
|
|
4
4
|
|
|
5
|
-
// src/
|
|
5
|
+
// src/capture/compress-screenshot.ts
|
|
6
|
+
async function compressScreenshotDataUrl(dataUrl, maxBytes = core.MAX_SCREENSHOT_DATA_URL_BYTES) {
|
|
7
|
+
if (!dataUrl.startsWith("data:image/")) return dataUrl;
|
|
8
|
+
if (estimateDataUrlBytes(dataUrl) <= maxBytes) return dataUrl;
|
|
9
|
+
if (typeof document === "undefined") return null;
|
|
10
|
+
const img = new Image();
|
|
11
|
+
await new Promise((resolve, reject) => {
|
|
12
|
+
img.onload = () => resolve();
|
|
13
|
+
img.onerror = () => reject(new Error("screenshot_decode_failed"));
|
|
14
|
+
img.src = dataUrl;
|
|
15
|
+
});
|
|
16
|
+
const qualities = [0.65, 0.5, 0.35, 0.25];
|
|
17
|
+
const scales = [1, 0.85, 0.7, 0.55, 0.4];
|
|
18
|
+
for (const scale of scales) {
|
|
19
|
+
const canvas = document.createElement("canvas");
|
|
20
|
+
const w = Math.max(1, Math.round(img.naturalWidth * scale));
|
|
21
|
+
const h = Math.max(1, Math.round(img.naturalHeight * scale));
|
|
22
|
+
canvas.width = w;
|
|
23
|
+
canvas.height = h;
|
|
24
|
+
const ctx = canvas.getContext("2d");
|
|
25
|
+
if (!ctx) continue;
|
|
26
|
+
ctx.drawImage(img, 0, 0, w, h);
|
|
27
|
+
for (const q of qualities) {
|
|
28
|
+
const candidate = canvas.toDataURL("image/jpeg", q);
|
|
29
|
+
if (estimateDataUrlBytes(candidate) <= maxBytes) return candidate;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
function estimateDataUrlBytes(dataUrl) {
|
|
35
|
+
const comma = dataUrl.indexOf(",");
|
|
36
|
+
if (comma < 0) return dataUrl.length;
|
|
37
|
+
const b64 = dataUrl.slice(comma + 1);
|
|
38
|
+
return Math.ceil(b64.length * 3 / 4);
|
|
39
|
+
}
|
|
6
40
|
|
|
7
41
|
// src/i18n/en.ts
|
|
8
42
|
var en = {
|
|
@@ -1005,6 +1039,19 @@ function getWidgetStyles(theme) {
|
|
|
1005
1039
|
outline: 2px solid ${widgetAccent};
|
|
1006
1040
|
outline-offset: 2px;
|
|
1007
1041
|
}
|
|
1042
|
+
.mushi-annotate-host {
|
|
1043
|
+
flex-basis: 100%;
|
|
1044
|
+
margin-top: 8px;
|
|
1045
|
+
}
|
|
1046
|
+
.mushi-annotate-host:empty {
|
|
1047
|
+
margin-top: 0;
|
|
1048
|
+
}
|
|
1049
|
+
.mushi-annotate-toolbar {
|
|
1050
|
+
display: flex;
|
|
1051
|
+
flex-wrap: wrap;
|
|
1052
|
+
gap: 6px;
|
|
1053
|
+
margin-bottom: 8px;
|
|
1054
|
+
}
|
|
1008
1055
|
@keyframes mushi-spin {
|
|
1009
1056
|
to { transform: rotate(360deg); }
|
|
1010
1057
|
}
|
|
@@ -1834,7 +1881,11 @@ function reporterStatusLabel(status) {
|
|
|
1834
1881
|
case "fixed":
|
|
1835
1882
|
case "resolved":
|
|
1836
1883
|
case "completed":
|
|
1837
|
-
return "Fixed";
|
|
1884
|
+
return "Fixed \u2014 confirm?";
|
|
1885
|
+
case "verified":
|
|
1886
|
+
return "Verified";
|
|
1887
|
+
case "reopened":
|
|
1888
|
+
return "Reopened";
|
|
1838
1889
|
case "dismissed":
|
|
1839
1890
|
return "Closed";
|
|
1840
1891
|
default:
|
|
@@ -1859,6 +1910,10 @@ function reporterStatusTone(status) {
|
|
|
1859
1910
|
case "resolved":
|
|
1860
1911
|
case "completed":
|
|
1861
1912
|
return "fixed";
|
|
1913
|
+
case "verified":
|
|
1914
|
+
return "fixed";
|
|
1915
|
+
case "reopened":
|
|
1916
|
+
return "fixing";
|
|
1862
1917
|
case "dismissed":
|
|
1863
1918
|
return "closed";
|
|
1864
1919
|
default:
|
|
@@ -1978,6 +2033,7 @@ var MushiWidget = class _MushiWidget {
|
|
|
1978
2033
|
nudgeTimer = null;
|
|
1979
2034
|
sdkFreshness = null;
|
|
1980
2035
|
reporterReports = [];
|
|
2036
|
+
featureBoard = [];
|
|
1981
2037
|
reporterComments = [];
|
|
1982
2038
|
selectedReportId = null;
|
|
1983
2039
|
reporterLoading = false;
|
|
@@ -2697,6 +2753,8 @@ var MushiWidget = class _MushiWidget {
|
|
|
2697
2753
|
return this.renderReportDetailStep();
|
|
2698
2754
|
case "leaderboard":
|
|
2699
2755
|
return this.renderLeaderboardStep();
|
|
2756
|
+
case "roadmap":
|
|
2757
|
+
return this.renderRoadmapStep();
|
|
2700
2758
|
}
|
|
2701
2759
|
}
|
|
2702
2760
|
renderOutdatedBanner() {
|
|
@@ -2784,6 +2842,15 @@ var MushiWidget = class _MushiWidget {
|
|
|
2784
2842
|
<span class="mushi-option-arrow" aria-hidden="true">\u2192</span>
|
|
2785
2843
|
</button>
|
|
2786
2844
|
${this.renderFeatureRequestEntry()}
|
|
2845
|
+
${this.callbacks.onFeatureBoardRequest ? `
|
|
2846
|
+
<button type="button" class="mushi-option-btn" data-action="roadmap">
|
|
2847
|
+
<span class="mushi-option-icon" aria-hidden="true">\u{1F5F3}\uFE0F</span>
|
|
2848
|
+
<div class="mushi-option-text">
|
|
2849
|
+
<span class="mushi-option-label">Community ideas</span>
|
|
2850
|
+
<span class="mushi-option-desc">Vote on features and see what shipped</span>
|
|
2851
|
+
</div>
|
|
2852
|
+
<span class="mushi-option-arrow" aria-hidden="true">\u2192</span>
|
|
2853
|
+
</button>` : ""}
|
|
2787
2854
|
${categories}
|
|
2788
2855
|
${this.rewardsState ? this.renderRewardsNudge() : ""}
|
|
2789
2856
|
</div>
|
|
@@ -2894,6 +2961,38 @@ var MushiWidget = class _MushiWidget {
|
|
|
2894
2961
|
</div>
|
|
2895
2962
|
`;
|
|
2896
2963
|
}
|
|
2964
|
+
renderRoadmapStep() {
|
|
2965
|
+
const rows = this.featureBoard.map((ticket) => {
|
|
2966
|
+
const id = String(ticket.id ?? "");
|
|
2967
|
+
const subject = escapeHtml(String(ticket.subject ?? "Untitled idea"));
|
|
2968
|
+
const votes = Number(ticket.vote_count ?? 0);
|
|
2969
|
+
const shipped = Boolean(ticket.shipped_at);
|
|
2970
|
+
const voted = Boolean(ticket.my_vote);
|
|
2971
|
+
const status = shipped ? "Shipped" : String(ticket.status_label ?? ticket.status ?? "open");
|
|
2972
|
+
return `
|
|
2973
|
+
<div class="mushi-report-row mushi-roadmap-row">
|
|
2974
|
+
<div class="mushi-report-main">
|
|
2975
|
+
<span class="mushi-report-title">${subject}</span>
|
|
2976
|
+
<span class="mushi-report-meta">
|
|
2977
|
+
<span class="mushi-report-status">${escapeHtml(status)}</span>
|
|
2978
|
+
<span class="mushi-report-when">${votes} vote${votes === 1 ? "" : "s"}</span>
|
|
2979
|
+
</span>
|
|
2980
|
+
</div>
|
|
2981
|
+
${this.callbacks.onFeatureBoardVote ? `
|
|
2982
|
+
<button type="button" class="mushi-vote-btn" data-vote-id="${escapeHtml(id)}" aria-pressed="${voted}">
|
|
2983
|
+
${voted ? "Voted" : "Vote"}
|
|
2984
|
+
</button>` : ""}
|
|
2985
|
+
</div>`;
|
|
2986
|
+
}).join("");
|
|
2987
|
+
return `
|
|
2988
|
+
${this.renderHeader({ title: "Community ideas", showBack: true, eyebrow: "Mushi \xB7 Roadmap" })}
|
|
2989
|
+
<div class="mushi-body">
|
|
2990
|
+
${this.reporterLoading ? '<p class="mushi-muted">Loading ideas\u2026</p>' : ""}
|
|
2991
|
+
${this.reporterError ? `<p class="mushi-error-inline">${escapeHtml(this.reporterError)}</p>` : ""}
|
|
2992
|
+
${rows || (!this.reporterLoading ? '<p class="mushi-muted">No community ideas yet. Be the first to suggest one.</p>' : "")}
|
|
2993
|
+
</div>
|
|
2994
|
+
`;
|
|
2995
|
+
}
|
|
2897
2996
|
renderLeaderboardStep() {
|
|
2898
2997
|
const myRank = this.rewardsState && this.leaderboardEntries ? this.leaderboardEntries.findIndex((e) => e.display_name === "You") + 1 : 0;
|
|
2899
2998
|
const rows = (this.leaderboardEntries ?? []).map((e, i) => `
|
|
@@ -2939,6 +3038,12 @@ var MushiWidget = class _MushiWidget {
|
|
|
2939
3038
|
<div class="mushi-thread">
|
|
2940
3039
|
${this.reporterLoading ? '<p class="mushi-muted">Loading thread\u2026</p>' : comments || '<p class="mushi-muted">No developer replies yet.</p>'}
|
|
2941
3040
|
</div>
|
|
3041
|
+
${["fixed", "resolved", "verified"].includes(status) ? `
|
|
3042
|
+
<div class="mushi-verify-actions" role="group" aria-label="Fix verification">
|
|
3043
|
+
<button type="button" class="mushi-intent-btn" data-action="reporter-confirms">Yes, fixed for me</button>
|
|
3044
|
+
<button type="button" class="mushi-intent-btn" data-action="reporter-not-fixed">Not fixed yet</button>
|
|
3045
|
+
</div>
|
|
3046
|
+
` : ""}
|
|
2942
3047
|
<textarea class="mushi-textarea" data-role="reporter-reply" rows="3" placeholder="Reply to the developer\u2026"></textarea>
|
|
2943
3048
|
<button type="button" class="mushi-submit" data-action="reporter-reply">
|
|
2944
3049
|
<span>Reply</span><span class="mushi-submit-arrow" aria-hidden="true">\u2192</span>
|
|
@@ -3018,6 +3123,7 @@ var MushiWidget = class _MushiWidget {
|
|
|
3018
3123
|
${escapeHtml(screenshotLabel)}
|
|
3019
3124
|
</button>
|
|
3020
3125
|
${this.screenshotAttached && this.allowScreenshotRemove ? '<button type="button" class="mushi-attach-btn danger" data-action="remove-screenshot" aria-label="Remove screenshot">\u2715 Remove</button>' : ""}
|
|
3126
|
+
${this.screenshotAttached ? '<button type="button" class="mushi-attach-btn" data-action="annotate-screenshot" aria-label="Mark up screenshot">\u270F Mark up</button><div class="mushi-annotate-host" data-role="annotate-host"></div>' : ""}
|
|
3021
3127
|
<button type="button" class="${elementClass}"
|
|
3022
3128
|
data-action="element"
|
|
3023
3129
|
${this.elementCapturing ? "disabled" : ""}
|
|
@@ -3243,12 +3349,23 @@ var MushiWidget = class _MushiWidget {
|
|
|
3243
3349
|
this.selectedReportId = null;
|
|
3244
3350
|
} else if (this.step === "leaderboard") {
|
|
3245
3351
|
this.step = "reports";
|
|
3352
|
+
} else if (this.step === "roadmap") {
|
|
3353
|
+
this.step = "category";
|
|
3246
3354
|
}
|
|
3247
3355
|
this.render();
|
|
3248
3356
|
});
|
|
3249
3357
|
panel.querySelector('[data-action="reports"]')?.addEventListener("click", () => {
|
|
3250
3358
|
void this.loadReporterReports();
|
|
3251
3359
|
});
|
|
3360
|
+
panel.querySelector('[data-action="roadmap"]')?.addEventListener("click", () => {
|
|
3361
|
+
void this.loadFeatureBoard();
|
|
3362
|
+
});
|
|
3363
|
+
panel.querySelectorAll("[data-vote-id]").forEach((btn) => {
|
|
3364
|
+
btn.addEventListener("click", () => {
|
|
3365
|
+
const requestId = btn.dataset.voteId;
|
|
3366
|
+
if (requestId) void this.voteFeatureBoard(requestId);
|
|
3367
|
+
});
|
|
3368
|
+
});
|
|
3252
3369
|
panel.querySelector('[data-action="feature-request"]')?.addEventListener("click", () => {
|
|
3253
3370
|
this.selectedCategory = "other";
|
|
3254
3371
|
this.selectedIntent = FEATURE_REQUEST_INTENT;
|
|
@@ -3270,6 +3387,12 @@ var MushiWidget = class _MushiWidget {
|
|
|
3270
3387
|
panel.querySelector('[data-action="reporter-reply"]')?.addEventListener("click", () => {
|
|
3271
3388
|
void this.submitReporterReply(panel);
|
|
3272
3389
|
});
|
|
3390
|
+
panel.querySelector('[data-action="reporter-confirms"]')?.addEventListener("click", () => {
|
|
3391
|
+
void this.submitReporterFeedback("confirms");
|
|
3392
|
+
});
|
|
3393
|
+
panel.querySelector('[data-action="reporter-not-fixed"]')?.addEventListener("click", () => {
|
|
3394
|
+
void this.submitReporterReopen();
|
|
3395
|
+
});
|
|
3273
3396
|
panel.querySelector('[data-action="copy-report-id"]')?.addEventListener("click", (e) => {
|
|
3274
3397
|
const btn = e.currentTarget;
|
|
3275
3398
|
const id = btn.dataset.copyId;
|
|
@@ -3335,6 +3458,12 @@ var MushiWidget = class _MushiWidget {
|
|
|
3335
3458
|
panel.querySelector('[data-action="remove-screenshot"]')?.addEventListener("click", () => {
|
|
3336
3459
|
this.callbacks.onScreenshotRemove?.();
|
|
3337
3460
|
});
|
|
3461
|
+
panel.querySelector('[data-action="annotate-screenshot"]')?.addEventListener("click", () => {
|
|
3462
|
+
const host = panel.querySelector('[data-role="annotate-host"]');
|
|
3463
|
+
if (host && this.callbacks.onScreenshotAnnotateRequest) {
|
|
3464
|
+
void this.callbacks.onScreenshotAnnotateRequest(host);
|
|
3465
|
+
}
|
|
3466
|
+
});
|
|
3338
3467
|
panel.querySelector('[data-action="element"]')?.addEventListener("click", () => {
|
|
3339
3468
|
this.callbacks.onElementSelectorRequest?.();
|
|
3340
3469
|
});
|
|
@@ -3422,6 +3551,53 @@ var MushiWidget = class _MushiWidget {
|
|
|
3422
3551
|
unreadCount() {
|
|
3423
3552
|
return this.reporterReports.reduce((sum, report) => sum + (report.unread_count ?? 0), 0);
|
|
3424
3553
|
}
|
|
3554
|
+
async loadFeatureBoard() {
|
|
3555
|
+
this.step = "roadmap";
|
|
3556
|
+
this.reporterLoading = true;
|
|
3557
|
+
this.reporterError = null;
|
|
3558
|
+
this.render();
|
|
3559
|
+
try {
|
|
3560
|
+
this.featureBoard = await this.callbacks.onFeatureBoardRequest?.() ?? [];
|
|
3561
|
+
} catch (err) {
|
|
3562
|
+
this.reporterError = err instanceof Error ? err.message : "Could not load community ideas.";
|
|
3563
|
+
} finally {
|
|
3564
|
+
this.reporterLoading = false;
|
|
3565
|
+
this.render();
|
|
3566
|
+
}
|
|
3567
|
+
}
|
|
3568
|
+
async voteFeatureBoard(requestId) {
|
|
3569
|
+
if (!this.callbacks.onFeatureBoardVote || this.reporterLoading) return;
|
|
3570
|
+
this.reporterLoading = true;
|
|
3571
|
+
this.render();
|
|
3572
|
+
try {
|
|
3573
|
+
await this.callbacks.onFeatureBoardVote(requestId);
|
|
3574
|
+
this.featureBoard = await this.callbacks.onFeatureBoardRequest?.() ?? this.featureBoard;
|
|
3575
|
+
} catch (err) {
|
|
3576
|
+
this.reporterError = err instanceof Error ? err.message : "Could not update vote.";
|
|
3577
|
+
} finally {
|
|
3578
|
+
this.reporterLoading = false;
|
|
3579
|
+
this.render();
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
async submitReporterReopen() {
|
|
3583
|
+
const reportId = this.selectedReportId;
|
|
3584
|
+
if (!reportId || this.reporterLoading) return;
|
|
3585
|
+
this.reporterLoading = true;
|
|
3586
|
+
this.render();
|
|
3587
|
+
try {
|
|
3588
|
+
if (this.callbacks.onReporterReopen) {
|
|
3589
|
+
await this.callbacks.onReporterReopen(reportId, "Not fixed for me");
|
|
3590
|
+
} else {
|
|
3591
|
+
await this.callbacks.onReporterFeedback?.(reportId, "not_fixed", "Not fixed for me");
|
|
3592
|
+
}
|
|
3593
|
+
await this.loadReporterReports();
|
|
3594
|
+
} catch (err) {
|
|
3595
|
+
this.reporterError = err instanceof Error ? err.message : "Could not reopen report.";
|
|
3596
|
+
} finally {
|
|
3597
|
+
this.reporterLoading = false;
|
|
3598
|
+
this.render();
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3425
3601
|
async loadReporterReports() {
|
|
3426
3602
|
this.step = "reports";
|
|
3427
3603
|
this.reporterLoading = true;
|
|
@@ -3451,6 +3627,21 @@ var MushiWidget = class _MushiWidget {
|
|
|
3451
3627
|
this.render();
|
|
3452
3628
|
}
|
|
3453
3629
|
}
|
|
3630
|
+
async submitReporterFeedback(signal) {
|
|
3631
|
+
const reportId = this.selectedReportId;
|
|
3632
|
+
if (!reportId || this.reporterLoading) return;
|
|
3633
|
+
this.reporterLoading = true;
|
|
3634
|
+
this.render();
|
|
3635
|
+
try {
|
|
3636
|
+
await this.callbacks.onReporterFeedback?.(reportId, signal);
|
|
3637
|
+
await this.loadReporterReports();
|
|
3638
|
+
if (reportId) await this.loadReporterComments(reportId);
|
|
3639
|
+
} catch (err) {
|
|
3640
|
+
this.reporterError = err instanceof Error ? err.message : "Could not send feedback.";
|
|
3641
|
+
this.reporterLoading = false;
|
|
3642
|
+
this.render();
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
3454
3645
|
async submitReporterReply(panel) {
|
|
3455
3646
|
const reportId = this.selectedReportId;
|
|
3456
3647
|
const textarea = panel.querySelector('[data-role="reporter-reply"]');
|
|
@@ -4821,6 +5012,256 @@ function createDiscoveryCapture(opts) {
|
|
|
4821
5012
|
};
|
|
4822
5013
|
}
|
|
4823
5014
|
|
|
5015
|
+
// src/capture/replay.ts
|
|
5016
|
+
var DEFAULT_MAX_MS = 3e4;
|
|
5017
|
+
var MAX_EVENTS = 400;
|
|
5018
|
+
var RRWEB_META = 4;
|
|
5019
|
+
var RRWEB_FULL_SNAPSHOT = 2;
|
|
5020
|
+
function trimReplayBuffer(events, maxMs, maxEvents) {
|
|
5021
|
+
const ts = (e) => e.timestamp;
|
|
5022
|
+
const isFullSnapshot = (e) => e.type === RRWEB_FULL_SNAPSHOT;
|
|
5023
|
+
const isMeta = (e) => e.type === RRWEB_META;
|
|
5024
|
+
const cutoff = Date.now() - maxMs;
|
|
5025
|
+
let baseIndex = 0;
|
|
5026
|
+
for (let i = 0; i < events.length; i++) {
|
|
5027
|
+
const t = ts(events[i]);
|
|
5028
|
+
if (typeof t === "number" && t >= cutoff) break;
|
|
5029
|
+
if (isFullSnapshot(events[i])) baseIndex = isMeta(events[i - 1]) ? i - 1 : i;
|
|
5030
|
+
}
|
|
5031
|
+
if (events.length - baseIndex > maxEvents) {
|
|
5032
|
+
for (let i = baseIndex + 1; i < events.length; i++) {
|
|
5033
|
+
if (isFullSnapshot(events[i]) && events.length - i <= maxEvents) {
|
|
5034
|
+
baseIndex = isMeta(events[i - 1]) ? i - 1 : i;
|
|
5035
|
+
break;
|
|
5036
|
+
}
|
|
5037
|
+
}
|
|
5038
|
+
}
|
|
5039
|
+
if (baseIndex > 0) events.splice(0, baseIndex);
|
|
5040
|
+
}
|
|
5041
|
+
function createLiteReplay(maxMs) {
|
|
5042
|
+
const events = [];
|
|
5043
|
+
let active = false;
|
|
5044
|
+
const onClick = (ev) => {
|
|
5045
|
+
if (!active) return;
|
|
5046
|
+
const target = ev.target instanceof Element ? ev.target : null;
|
|
5047
|
+
const tag = target?.tagName?.toLowerCase() ?? "unknown";
|
|
5048
|
+
const testId = target?.closest("[data-testid]")?.getAttribute("data-testid") ?? void 0;
|
|
5049
|
+
events.push({ type: "lite_click", timestamp: Date.now(), data: { tag, testId } });
|
|
5050
|
+
if (events.length > MAX_EVENTS) events.shift();
|
|
5051
|
+
};
|
|
5052
|
+
const stop = () => {
|
|
5053
|
+
active = false;
|
|
5054
|
+
document.removeEventListener("click", onClick, true);
|
|
5055
|
+
};
|
|
5056
|
+
return {
|
|
5057
|
+
start() {
|
|
5058
|
+
if (active) return;
|
|
5059
|
+
active = true;
|
|
5060
|
+
document.addEventListener("click", onClick, true);
|
|
5061
|
+
},
|
|
5062
|
+
stop,
|
|
5063
|
+
flush() {
|
|
5064
|
+
const cutoff = Date.now() - maxMs;
|
|
5065
|
+
return events.filter((e) => e.timestamp >= cutoff);
|
|
5066
|
+
},
|
|
5067
|
+
destroy() {
|
|
5068
|
+
stop();
|
|
5069
|
+
events.length = 0;
|
|
5070
|
+
}
|
|
5071
|
+
};
|
|
5072
|
+
}
|
|
5073
|
+
var rrwebModule = null;
|
|
5074
|
+
async function loadRrweb() {
|
|
5075
|
+
if (rrwebModule) return rrwebModule;
|
|
5076
|
+
try {
|
|
5077
|
+
const specifier = "rrweb";
|
|
5078
|
+
rrwebModule = await import(
|
|
5079
|
+
/* @vite-ignore */
|
|
5080
|
+
specifier
|
|
5081
|
+
);
|
|
5082
|
+
return rrwebModule;
|
|
5083
|
+
} catch {
|
|
5084
|
+
return null;
|
|
5085
|
+
}
|
|
5086
|
+
}
|
|
5087
|
+
async function createReplayCapture(opts) {
|
|
5088
|
+
const maxMs = opts.maxDurationMs ?? DEFAULT_MAX_MS;
|
|
5089
|
+
const rrweb = await loadRrweb();
|
|
5090
|
+
if (!rrweb?.record) {
|
|
5091
|
+
return createLiteReplay(maxMs);
|
|
5092
|
+
}
|
|
5093
|
+
const record = rrweb.record;
|
|
5094
|
+
const events = [];
|
|
5095
|
+
let stopFn = null;
|
|
5096
|
+
let recording = false;
|
|
5097
|
+
const maskSelectors = ['input[type="password"]', ...opts.redactSelectors ?? []];
|
|
5098
|
+
const stop = () => {
|
|
5099
|
+
stopFn?.();
|
|
5100
|
+
stopFn = null;
|
|
5101
|
+
recording = false;
|
|
5102
|
+
};
|
|
5103
|
+
return {
|
|
5104
|
+
start() {
|
|
5105
|
+
if (recording) return;
|
|
5106
|
+
recording = true;
|
|
5107
|
+
stopFn = record({
|
|
5108
|
+
emit(event) {
|
|
5109
|
+
events.push(event);
|
|
5110
|
+
trimReplayBuffer(events, maxMs, MAX_EVENTS);
|
|
5111
|
+
},
|
|
5112
|
+
maskAllInputs: true,
|
|
5113
|
+
// Mask rendered DOM text too — without this, rrweb records every
|
|
5114
|
+
// visible label (emails, names) as plaintext. Hosts that need richer
|
|
5115
|
+
// capture can opt out via their own rrweb integration.
|
|
5116
|
+
maskAllText: true,
|
|
5117
|
+
maskTextSelector: maskSelectors.join(","),
|
|
5118
|
+
// Re-emit a full snapshot roughly once per retained window so trimming
|
|
5119
|
+
// never leaves incrementals without a base snapshot.
|
|
5120
|
+
checkoutEveryNms: maxMs,
|
|
5121
|
+
sampling: { mousemove: false, mouseInteraction: true, scroll: 150, media: 800 }
|
|
5122
|
+
}) ?? null;
|
|
5123
|
+
},
|
|
5124
|
+
stop,
|
|
5125
|
+
flush() {
|
|
5126
|
+
trimReplayBuffer(events, maxMs, MAX_EVENTS);
|
|
5127
|
+
return [...events];
|
|
5128
|
+
},
|
|
5129
|
+
destroy() {
|
|
5130
|
+
stop();
|
|
5131
|
+
events.length = 0;
|
|
5132
|
+
}
|
|
5133
|
+
};
|
|
5134
|
+
}
|
|
5135
|
+
|
|
5136
|
+
// src/capture/screenshot-annotation.ts
|
|
5137
|
+
function createScreenshotAnnotation(imageDataUrl, container) {
|
|
5138
|
+
return new Promise((resolve, reject) => {
|
|
5139
|
+
const img = new Image();
|
|
5140
|
+
img.onload = () => {
|
|
5141
|
+
const wrap = document.createElement("div");
|
|
5142
|
+
wrap.style.position = "relative";
|
|
5143
|
+
wrap.style.display = "inline-block";
|
|
5144
|
+
wrap.style.maxWidth = "100%";
|
|
5145
|
+
const base = document.createElement("canvas");
|
|
5146
|
+
const overlay = document.createElement("canvas");
|
|
5147
|
+
const scale = Math.min(1, 720 / img.width);
|
|
5148
|
+
const w = Math.round(img.width * scale);
|
|
5149
|
+
const h = Math.round(img.height * scale);
|
|
5150
|
+
for (const c of [base, overlay]) {
|
|
5151
|
+
c.width = w;
|
|
5152
|
+
c.height = h;
|
|
5153
|
+
c.style.width = "100%";
|
|
5154
|
+
c.style.height = "auto";
|
|
5155
|
+
c.style.display = "block";
|
|
5156
|
+
}
|
|
5157
|
+
overlay.style.position = "absolute";
|
|
5158
|
+
overlay.style.left = "0";
|
|
5159
|
+
overlay.style.top = "0";
|
|
5160
|
+
overlay.style.cursor = "crosshair";
|
|
5161
|
+
const bctx = base.getContext("2d");
|
|
5162
|
+
const octx = overlay.getContext("2d");
|
|
5163
|
+
if (!bctx || !octx) {
|
|
5164
|
+
reject(new Error("Canvas not supported"));
|
|
5165
|
+
return;
|
|
5166
|
+
}
|
|
5167
|
+
bctx.drawImage(img, 0, 0, w, h);
|
|
5168
|
+
wrap.appendChild(base);
|
|
5169
|
+
wrap.appendChild(overlay);
|
|
5170
|
+
container.appendChild(wrap);
|
|
5171
|
+
let tool = "highlight";
|
|
5172
|
+
let drawing = false;
|
|
5173
|
+
let start = null;
|
|
5174
|
+
const strokes = [];
|
|
5175
|
+
const toLocal = (ev) => {
|
|
5176
|
+
const rect = overlay.getBoundingClientRect();
|
|
5177
|
+
const touch = "touches" in ev ? ev.touches[0] ?? ev.changedTouches[0] : null;
|
|
5178
|
+
const clientX = touch ? touch.clientX : ev.clientX;
|
|
5179
|
+
const clientY = touch ? touch.clientY : ev.clientY;
|
|
5180
|
+
return {
|
|
5181
|
+
x: (clientX - rect.left) / rect.width * w,
|
|
5182
|
+
y: (clientY - rect.top) / rect.height * h
|
|
5183
|
+
};
|
|
5184
|
+
};
|
|
5185
|
+
const redraw = () => {
|
|
5186
|
+
octx.clearRect(0, 0, w, h);
|
|
5187
|
+
for (const s of strokes) {
|
|
5188
|
+
if (s.tool === "highlight" && s.points.length >= 2) {
|
|
5189
|
+
const [a, b] = s.points;
|
|
5190
|
+
octx.fillStyle = "rgba(255, 230, 0, 0.35)";
|
|
5191
|
+
octx.fillRect(
|
|
5192
|
+
Math.min(a.x, b.x),
|
|
5193
|
+
Math.min(a.y, b.y),
|
|
5194
|
+
Math.abs(b.x - a.x),
|
|
5195
|
+
Math.abs(b.y - a.y)
|
|
5196
|
+
);
|
|
5197
|
+
} else if (s.tool === "arrow" && s.points.length >= 2) {
|
|
5198
|
+
const [a, b] = s.points;
|
|
5199
|
+
octx.strokeStyle = "#ef4444";
|
|
5200
|
+
octx.lineWidth = 3;
|
|
5201
|
+
octx.beginPath();
|
|
5202
|
+
octx.moveTo(a.x, a.y);
|
|
5203
|
+
octx.lineTo(b.x, b.y);
|
|
5204
|
+
octx.stroke();
|
|
5205
|
+
}
|
|
5206
|
+
}
|
|
5207
|
+
};
|
|
5208
|
+
const onDown = (ev) => {
|
|
5209
|
+
drawing = true;
|
|
5210
|
+
start = toLocal(ev);
|
|
5211
|
+
};
|
|
5212
|
+
const onUp = (ev) => {
|
|
5213
|
+
if (!drawing || !start) return;
|
|
5214
|
+
drawing = false;
|
|
5215
|
+
const end = toLocal(ev);
|
|
5216
|
+
if (tool === "blur") {
|
|
5217
|
+
const x = Math.min(start.x, end.x);
|
|
5218
|
+
const y = Math.min(start.y, end.y);
|
|
5219
|
+
const bw = Math.abs(end.x - start.x);
|
|
5220
|
+
const bh = Math.abs(end.y - start.y);
|
|
5221
|
+
if (bw >= 1 && bh >= 1) {
|
|
5222
|
+
bctx.filter = "blur(8px)";
|
|
5223
|
+
bctx.drawImage(base, x, y, bw, bh, x, y, bw, bh);
|
|
5224
|
+
bctx.filter = "none";
|
|
5225
|
+
}
|
|
5226
|
+
} else {
|
|
5227
|
+
strokes.push({ tool, points: [start, end] });
|
|
5228
|
+
}
|
|
5229
|
+
start = null;
|
|
5230
|
+
redraw();
|
|
5231
|
+
};
|
|
5232
|
+
overlay.addEventListener("mousedown", onDown);
|
|
5233
|
+
overlay.addEventListener("mouseup", onUp);
|
|
5234
|
+
overlay.addEventListener("touchstart", onDown, { passive: true });
|
|
5235
|
+
overlay.addEventListener("touchend", onUp);
|
|
5236
|
+
resolve({
|
|
5237
|
+
canvas: base,
|
|
5238
|
+
setTool(t) {
|
|
5239
|
+
tool = t;
|
|
5240
|
+
},
|
|
5241
|
+
getDataUrl() {
|
|
5242
|
+
const out = document.createElement("canvas");
|
|
5243
|
+
out.width = w;
|
|
5244
|
+
out.height = h;
|
|
5245
|
+
const ctx = out.getContext("2d");
|
|
5246
|
+
if (!ctx) return imageDataUrl;
|
|
5247
|
+
ctx.drawImage(base, 0, 0);
|
|
5248
|
+
ctx.drawImage(overlay, 0, 0);
|
|
5249
|
+
return out.toDataURL("image/jpeg", 0.85);
|
|
5250
|
+
},
|
|
5251
|
+
destroy() {
|
|
5252
|
+
overlay.removeEventListener("mousedown", onDown);
|
|
5253
|
+
overlay.removeEventListener("mouseup", onUp);
|
|
5254
|
+
overlay.removeEventListener("touchstart", onDown);
|
|
5255
|
+
overlay.removeEventListener("touchend", onUp);
|
|
5256
|
+
wrap.remove();
|
|
5257
|
+
}
|
|
5258
|
+
});
|
|
5259
|
+
};
|
|
5260
|
+
img.onerror = () => reject(new Error("Failed to load screenshot"));
|
|
5261
|
+
img.src = imageDataUrl;
|
|
5262
|
+
});
|
|
5263
|
+
}
|
|
5264
|
+
|
|
4824
5265
|
// src/sentry.ts
|
|
4825
5266
|
function getSentryGlobal() {
|
|
4826
5267
|
try {
|
|
@@ -5304,7 +5745,7 @@ function createProactiveManager(config = {}) {
|
|
|
5304
5745
|
|
|
5305
5746
|
// src/version.ts
|
|
5306
5747
|
var MUSHI_SDK_PACKAGE = "@mushi-mushi/web";
|
|
5307
|
-
var MUSHI_SDK_VERSION = "1.
|
|
5748
|
+
var MUSHI_SDK_VERSION = "1.12.0" ;
|
|
5308
5749
|
|
|
5309
5750
|
// src/mushi.ts
|
|
5310
5751
|
var instance = null;
|
|
@@ -5383,6 +5824,8 @@ function createInstance(config) {
|
|
|
5383
5824
|
let elementSelector = null;
|
|
5384
5825
|
let discoveryCap = null;
|
|
5385
5826
|
const timelineCap = createTimelineCapture();
|
|
5827
|
+
let replayCap = null;
|
|
5828
|
+
let replayGeneration = 0;
|
|
5386
5829
|
let widget;
|
|
5387
5830
|
function syncCaptureModules() {
|
|
5388
5831
|
if (activeConfig.capture?.console !== false) {
|
|
@@ -5466,6 +5909,25 @@ function createInstance(config) {
|
|
|
5466
5909
|
discoveryCap?.destroy();
|
|
5467
5910
|
discoveryCap = null;
|
|
5468
5911
|
}
|
|
5912
|
+
const replayMode = activeConfig.capture?.replay ?? "off";
|
|
5913
|
+
if (replayMode === "rrweb" || replayMode === "lite") {
|
|
5914
|
+
const generation = ++replayGeneration;
|
|
5915
|
+
void createReplayCapture({
|
|
5916
|
+
redactSelectors: activeConfig.privacy?.redactSelectors
|
|
5917
|
+
}).then((cap) => {
|
|
5918
|
+
if (generation !== replayGeneration) {
|
|
5919
|
+
cap.destroy();
|
|
5920
|
+
return;
|
|
5921
|
+
}
|
|
5922
|
+
replayCap?.destroy();
|
|
5923
|
+
replayCap = cap;
|
|
5924
|
+
replayCap.start();
|
|
5925
|
+
});
|
|
5926
|
+
} else {
|
|
5927
|
+
replayGeneration++;
|
|
5928
|
+
replayCap?.destroy();
|
|
5929
|
+
replayCap = null;
|
|
5930
|
+
}
|
|
5469
5931
|
}
|
|
5470
5932
|
const listeners = /* @__PURE__ */ new Map();
|
|
5471
5933
|
function emit(type, data) {
|
|
@@ -5527,6 +5989,7 @@ function createInstance(config) {
|
|
|
5527
5989
|
onOpen: () => {
|
|
5528
5990
|
log.debug("Widget opened");
|
|
5529
5991
|
void autoCaptureScreenshot("open");
|
|
5992
|
+
replayCap?.start();
|
|
5530
5993
|
emit("widget:opened");
|
|
5531
5994
|
},
|
|
5532
5995
|
onClose: () => {
|
|
@@ -5551,6 +6014,55 @@ function createInstance(config) {
|
|
|
5551
6014
|
pendingScreenshot = null;
|
|
5552
6015
|
widget.setScreenshotAttached(false);
|
|
5553
6016
|
},
|
|
6017
|
+
onScreenshotAnnotateRequest: async (container) => {
|
|
6018
|
+
if (!pendingScreenshot) return;
|
|
6019
|
+
if (container.childElementCount > 0) return;
|
|
6020
|
+
let session;
|
|
6021
|
+
try {
|
|
6022
|
+
session = await createScreenshotAnnotation(pendingScreenshot, container);
|
|
6023
|
+
} catch (err) {
|
|
6024
|
+
log.warn("Screenshot annotation failed", {
|
|
6025
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6026
|
+
});
|
|
6027
|
+
return;
|
|
6028
|
+
}
|
|
6029
|
+
const toolbar = document.createElement("div");
|
|
6030
|
+
toolbar.className = "mushi-annotate-toolbar";
|
|
6031
|
+
const finish = (commit) => {
|
|
6032
|
+
{
|
|
6033
|
+
pendingScreenshot = session.getDataUrl();
|
|
6034
|
+
widget.setScreenshotAttached(true);
|
|
6035
|
+
}
|
|
6036
|
+
session.destroy();
|
|
6037
|
+
toolbar.remove();
|
|
6038
|
+
};
|
|
6039
|
+
const tools = [
|
|
6040
|
+
{ id: "highlight", label: "\u270F\uFE0F Highlight" },
|
|
6041
|
+
{ id: "blur", label: "\u{1F512} Blur" },
|
|
6042
|
+
{ id: "arrow", label: "\u2197\uFE0F Arrow" }
|
|
6043
|
+
];
|
|
6044
|
+
for (const toolDef of tools) {
|
|
6045
|
+
const btn = document.createElement("button");
|
|
6046
|
+
btn.type = "button";
|
|
6047
|
+
btn.className = "mushi-attach-btn";
|
|
6048
|
+
btn.dataset.tool = toolDef.id;
|
|
6049
|
+
btn.textContent = toolDef.label;
|
|
6050
|
+
btn.addEventListener("click", () => {
|
|
6051
|
+
session.setTool(toolDef.id);
|
|
6052
|
+
toolbar.querySelectorAll("button[data-tool]").forEach((b) => b.classList.remove("active"));
|
|
6053
|
+
btn.classList.add("active");
|
|
6054
|
+
});
|
|
6055
|
+
toolbar.appendChild(btn);
|
|
6056
|
+
}
|
|
6057
|
+
toolbar.querySelector('button[data-tool="highlight"]')?.classList.add("active");
|
|
6058
|
+
const doneBtn = document.createElement("button");
|
|
6059
|
+
doneBtn.type = "button";
|
|
6060
|
+
doneBtn.className = "mushi-attach-btn";
|
|
6061
|
+
doneBtn.textContent = "\u2713 Done";
|
|
6062
|
+
doneBtn.addEventListener("click", () => finish());
|
|
6063
|
+
toolbar.appendChild(doneBtn);
|
|
6064
|
+
container.insertBefore(toolbar, container.firstChild);
|
|
6065
|
+
},
|
|
5554
6066
|
onElementSelectorRequest: async () => {
|
|
5555
6067
|
if (!elementSelector || activeConfig.capture?.elementSelector === false) return;
|
|
5556
6068
|
log.debug("Element selector activated");
|
|
@@ -5583,6 +6095,26 @@ function createInstance(config) {
|
|
|
5583
6095
|
const result = await apiClient2.replyToReporterReport(reportId, core.getReporterToken(), body);
|
|
5584
6096
|
if (!result.ok) throw new Error(result.error?.message ?? "Could not send reply");
|
|
5585
6097
|
},
|
|
6098
|
+
async onReporterFeedback(reportId, signal, note) {
|
|
6099
|
+
const result = await apiClient2.replyToReporterReport(reportId, core.getReporterToken(), note ?? "", signal);
|
|
6100
|
+
if (!result.ok) throw new Error(result.error?.message ?? "Could not send feedback");
|
|
6101
|
+
return result.data?.feedback ?? null;
|
|
6102
|
+
},
|
|
6103
|
+
async onReporterReopen(reportId, note) {
|
|
6104
|
+
const result = await apiClient2.reopenReporterReport(reportId, core.getReporterToken(), note);
|
|
6105
|
+
if (!result.ok) throw new Error(result.error?.message ?? "Could not reopen report");
|
|
6106
|
+
return result.data?.outcome ?? null;
|
|
6107
|
+
},
|
|
6108
|
+
async onFeatureBoardRequest() {
|
|
6109
|
+
const result = await apiClient2.listReporterFeatureBoard(core.getReporterToken());
|
|
6110
|
+
if (!result.ok) throw new Error(result.error?.message ?? "Could not load community ideas");
|
|
6111
|
+
return result.data?.tickets ?? [];
|
|
6112
|
+
},
|
|
6113
|
+
async onFeatureBoardVote(requestId) {
|
|
6114
|
+
const result = await apiClient2.voteReporterFeatureBoard(requestId, core.getReporterToken());
|
|
6115
|
+
if (!result.ok) throw new Error(result.error?.message ?? "Could not vote");
|
|
6116
|
+
return result.data ?? { voted: true, action: "added" };
|
|
6117
|
+
},
|
|
5586
6118
|
onLeaderboardOpen() {
|
|
5587
6119
|
widget.setLeaderboard(null, true);
|
|
5588
6120
|
void fetchLeaderboard(10).then((entries) => {
|
|
@@ -5760,6 +6292,10 @@ function createInstance(config) {
|
|
|
5760
6292
|
...sentryCtx.breadcrumbs ? { breadcrumbs: scrubBreadcrumbsForWire(sentryCtx.breadcrumbs) } : {},
|
|
5761
6293
|
...sentryCtx.tags ? { tags: scrubTagsForWire(sentryCtx.tags) } : {}
|
|
5762
6294
|
} : void 0;
|
|
6295
|
+
const screenshotForWire = pendingScreenshot ? await compressScreenshotDataUrl(pendingScreenshot).catch(() => null) : null;
|
|
6296
|
+
if (pendingScreenshot && !screenshotForWire) {
|
|
6297
|
+
log.warn("Screenshot dropped \u2014 could not compress under wire budget");
|
|
6298
|
+
}
|
|
5763
6299
|
const report = {
|
|
5764
6300
|
id: core.newUuid(),
|
|
5765
6301
|
projectId: config.projectId,
|
|
@@ -5771,8 +6307,9 @@ function createInstance(config) {
|
|
|
5771
6307
|
networkLogs,
|
|
5772
6308
|
performanceMetrics: activeConfig.capture?.performance === false ? void 0 : perfCap?.getMetrics(),
|
|
5773
6309
|
timeline: timelineCap.getEntries({ consoleLogs, networkLogs }),
|
|
5774
|
-
screenshotDataUrl:
|
|
6310
|
+
screenshotDataUrl: screenshotForWire ?? void 0,
|
|
5775
6311
|
selectedElement: pendingElement ?? void 0,
|
|
6312
|
+
...replayCap ? { replayEvents: replayCap.flush() } : {},
|
|
5776
6313
|
metadata: {
|
|
5777
6314
|
...customMetadata,
|
|
5778
6315
|
...userInfo ? { user: userInfo } : {},
|
|
@@ -5846,7 +6383,23 @@ function createInstance(config) {
|
|
|
5846
6383
|
emit("report:queued", { reportId: finalReport.id });
|
|
5847
6384
|
return { reportId: null, queuedOffline: true };
|
|
5848
6385
|
}
|
|
5849
|
-
|
|
6386
|
+
let result = await apiClient2.submitReport(finalReport);
|
|
6387
|
+
if (!result.ok && result.error?.code === "PAYLOAD_TOO_LARGE") {
|
|
6388
|
+
if (Array.isArray(finalReport.replayEvents) && finalReport.replayEvents.length > 0) {
|
|
6389
|
+
log.warn("Report too large \u2014 dropping replay buffer and retrying", {
|
|
6390
|
+
reportId: finalReport.id
|
|
6391
|
+
});
|
|
6392
|
+
finalReport = { ...finalReport, replayEvents: void 0 };
|
|
6393
|
+
result = await apiClient2.submitReport(finalReport);
|
|
6394
|
+
}
|
|
6395
|
+
if (!result.ok && result.error?.code === "PAYLOAD_TOO_LARGE" && finalReport.screenshotDataUrl) {
|
|
6396
|
+
log.warn("Report still too large \u2014 dropping screenshot and retrying", {
|
|
6397
|
+
reportId: finalReport.id
|
|
6398
|
+
});
|
|
6399
|
+
finalReport = { ...finalReport, screenshotDataUrl: void 0 };
|
|
6400
|
+
result = await apiClient2.submitReport(finalReport);
|
|
6401
|
+
}
|
|
6402
|
+
}
|
|
5850
6403
|
if (result.ok) {
|
|
5851
6404
|
log.info("Report sent", { reportId: result.data?.reportId });
|
|
5852
6405
|
emit("report:sent", { reportId: result.data?.reportId });
|
|
@@ -5869,6 +6422,17 @@ function createInstance(config) {
|
|
|
5869
6422
|
}
|
|
5870
6423
|
} catch {
|
|
5871
6424
|
}
|
|
6425
|
+
} else if (result.error?.code === "PAYLOAD_TOO_LARGE" || result.error?.code === "SERIALIZE_FAILED") {
|
|
6426
|
+
log.warn("Report exceeds size limit after degradation \u2014 dropping", {
|
|
6427
|
+
reportId: finalReport.id,
|
|
6428
|
+
error: result.error
|
|
6429
|
+
});
|
|
6430
|
+
emit("report:failed", { reportId: finalReport.id, error: result.error });
|
|
6431
|
+
breadcrumbs.add({
|
|
6432
|
+
category: "lifecycle",
|
|
6433
|
+
level: "error",
|
|
6434
|
+
message: `Mushi report dropped \u2014 payload too large (${finalReport.id})`
|
|
6435
|
+
});
|
|
5872
6436
|
} else {
|
|
5873
6437
|
log.warn("Report failed, queuing for retry", { reportId: finalReport.id, error: result.error });
|
|
5874
6438
|
await offlineQueue.enqueue(finalReport);
|
|
@@ -5954,6 +6518,8 @@ function createInstance(config) {
|
|
|
5954
6518
|
timelineCap.destroy();
|
|
5955
6519
|
discoveryCap?.destroy();
|
|
5956
6520
|
discoveryCap = null;
|
|
6521
|
+
replayCap?.destroy();
|
|
6522
|
+
replayCap = null;
|
|
5957
6523
|
offlineQueue.stopAutoSync();
|
|
5958
6524
|
detachAutoBreadcrumbs?.();
|
|
5959
6525
|
detachAutoBreadcrumbs = null;
|
|
@@ -6159,6 +6725,19 @@ function createInstance(config) {
|
|
|
6159
6725
|
if (!result.ok) return null;
|
|
6160
6726
|
return result.data?.comment ?? null;
|
|
6161
6727
|
},
|
|
6728
|
+
async submitFeedbackSignal(reportId, signal, note) {
|
|
6729
|
+
const result = await apiClient2.replyToReporterReport(reportId, core.getReporterToken(), note ?? "", signal);
|
|
6730
|
+
if (!result.ok) return null;
|
|
6731
|
+
return result.data?.feedback ?? null;
|
|
6732
|
+
},
|
|
6733
|
+
async reopenReport(reportId, note) {
|
|
6734
|
+
const result = await apiClient2.reopenReporterReport(reportId, core.getReporterToken(), note);
|
|
6735
|
+
if (!result.ok) return null;
|
|
6736
|
+
return result.data?.outcome ?? null;
|
|
6737
|
+
},
|
|
6738
|
+
openMyReports() {
|
|
6739
|
+
widget.recorderOpenMyReports();
|
|
6740
|
+
},
|
|
6162
6741
|
async getHallOfFame(limit = 20) {
|
|
6163
6742
|
const result = await apiClient2.getHallOfFame(limit);
|
|
6164
6743
|
if (!result.ok) return [];
|
|
@@ -6485,6 +7064,10 @@ function createNoopInstance() {
|
|
|
6485
7064
|
listMyReports: async () => [],
|
|
6486
7065
|
listMyComments: async () => [],
|
|
6487
7066
|
replyToReport: async () => null,
|
|
7067
|
+
submitFeedbackSignal: async () => null,
|
|
7068
|
+
reopenReport: async () => null,
|
|
7069
|
+
openMyReports: () => {
|
|
7070
|
+
},
|
|
6488
7071
|
getHallOfFame: async () => []
|
|
6489
7072
|
};
|
|
6490
7073
|
}
|