@d3lm/pr-stats 0.2.21 → 0.2.23
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/dist/tui-app.mjs
CHANGED
|
@@ -136,10 +136,10 @@ import { createRoot } from "@opentui/react";
|
|
|
136
136
|
|
|
137
137
|
// src/tui/App.tsx
|
|
138
138
|
import { useKeyboard, useRenderer as useRenderer3, useTerminalDimensions as useTerminalDimensions3 } from "@opentui/react";
|
|
139
|
-
import { useEffect as useEffect8, useMemo as useMemo2, useReducer, useRef as
|
|
139
|
+
import { useEffect as useEffect8, useMemo as useMemo2, useReducer, useRef as useRef8, useState as useState6 } from "react";
|
|
140
140
|
|
|
141
|
-
// src/
|
|
142
|
-
import { readFileSync as readFileSync3
|
|
141
|
+
// src/mentions.ts
|
|
142
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
143
143
|
import { join as join3 } from "node:path";
|
|
144
144
|
|
|
145
145
|
// src/cache.ts
|
|
@@ -166,7 +166,7 @@ function cacheDir() {
|
|
|
166
166
|
function prKey(repo, number) {
|
|
167
167
|
return `${repo}#${number}`;
|
|
168
168
|
}
|
|
169
|
-
var CACHE_FILES = ["details", "sizes", "user", "snapshot"];
|
|
169
|
+
var CACHE_FILES = ["details", "sizes", "mentions", "user", "snapshot", "mention-baseline"];
|
|
170
170
|
function clearCache() {
|
|
171
171
|
if (!enabled) {
|
|
172
172
|
return false;
|
|
@@ -283,6 +283,9 @@ function percentile(sorted, percent) {
|
|
|
283
283
|
}
|
|
284
284
|
|
|
285
285
|
// src/snooze.ts
|
|
286
|
+
function snoozeMatches(snooze, target) {
|
|
287
|
+
return snooze.kind === target.kind && snooze.ref === target.ref;
|
|
288
|
+
}
|
|
286
289
|
var DEFAULT_SNOOZE_DURATION = "30m";
|
|
287
290
|
var MINUTE_MS = 6e4;
|
|
288
291
|
var HOUR_MS = 60 * MINUTE_MS;
|
|
@@ -315,20 +318,33 @@ function formatWakeTime(until, now = Date.now()) {
|
|
|
315
318
|
function snoozesFile() {
|
|
316
319
|
return join2(cacheDir(), "snoozes.json");
|
|
317
320
|
}
|
|
318
|
-
function
|
|
321
|
+
function parseStoredKey(key) {
|
|
322
|
+
for (const kind of ["review", "mention"]) {
|
|
323
|
+
if (key.startsWith(`${kind}:`)) {
|
|
324
|
+
return { kind, ref: key.slice(kind.length + 1) };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return { kind: "review", ref: key };
|
|
328
|
+
}
|
|
329
|
+
function reviveSnooze(key, stored) {
|
|
319
330
|
if (typeof stored !== "object" || stored === null) {
|
|
320
331
|
return null;
|
|
321
332
|
}
|
|
322
|
-
const { until, requestedAt } = stored;
|
|
323
|
-
|
|
333
|
+
const { until, at, requestedAt, ids } = stored;
|
|
334
|
+
const askedAt = at ?? requestedAt;
|
|
335
|
+
if (typeof until !== "string" || typeof askedAt !== "string") {
|
|
324
336
|
return null;
|
|
325
337
|
}
|
|
326
338
|
const untilMs = Date.parse(until);
|
|
327
|
-
const
|
|
328
|
-
if (Number.isNaN(untilMs) || Number.isNaN(
|
|
339
|
+
const atMs = Date.parse(askedAt);
|
|
340
|
+
if (Number.isNaN(untilMs) || Number.isNaN(atMs)) {
|
|
329
341
|
return null;
|
|
330
342
|
}
|
|
331
|
-
|
|
343
|
+
const snooze = { ...parseStoredKey(key), until: untilMs, at: atMs };
|
|
344
|
+
if (Array.isArray(ids)) {
|
|
345
|
+
snooze.ids = ids.filter((id) => typeof id === "string");
|
|
346
|
+
}
|
|
347
|
+
return snooze;
|
|
332
348
|
}
|
|
333
349
|
function readSnoozes() {
|
|
334
350
|
if (!cacheEnabled()) {
|
|
@@ -344,8 +360,8 @@ function readSnoozes() {
|
|
|
344
360
|
return [];
|
|
345
361
|
}
|
|
346
362
|
const snoozes2 = [];
|
|
347
|
-
for (const [
|
|
348
|
-
const snooze = reviveSnooze(
|
|
363
|
+
for (const [key, stored] of Object.entries(parsed)) {
|
|
364
|
+
const snooze = reviveSnooze(key, stored);
|
|
349
365
|
if (snooze !== null) {
|
|
350
366
|
snoozes2.push(snooze);
|
|
351
367
|
}
|
|
@@ -358,25 +374,25 @@ function writeSnoozes(snoozes2) {
|
|
|
358
374
|
}
|
|
359
375
|
const stored = {};
|
|
360
376
|
for (const snooze of snoozes2) {
|
|
361
|
-
stored[snooze.ref] = {
|
|
377
|
+
stored[`${snooze.kind}:${snooze.ref}`] = {
|
|
362
378
|
until: new Date(snooze.until).toISOString(),
|
|
363
|
-
|
|
379
|
+
at: new Date(snooze.at).toISOString(),
|
|
380
|
+
...snooze.ids === void 0 ? {} : { ids: [...snooze.ids] }
|
|
364
381
|
};
|
|
365
382
|
}
|
|
366
383
|
writeFileAtomic(snoozesFile(), `${JSON.stringify(stored, null, 2)}
|
|
367
384
|
`);
|
|
368
385
|
return true;
|
|
369
386
|
}
|
|
370
|
-
function
|
|
371
|
-
return snoozes2.find(
|
|
372
|
-
(snooze) => snooze.ref === ref && snooze.until > now && requestedAt.getTime() <= snooze.requestedAt
|
|
373
|
-
);
|
|
387
|
+
function findSnooze(snoozes2, target, now, covers) {
|
|
388
|
+
return snoozes2.find((snooze) => snoozeMatches(snooze, target) && snooze.until > now && covers(snooze));
|
|
374
389
|
}
|
|
375
|
-
function
|
|
390
|
+
function partitionSnoozed(kind, entries, snoozes2, now, covers) {
|
|
376
391
|
const awaiting = [];
|
|
377
392
|
const snoozed = [];
|
|
378
393
|
for (const entry of entries) {
|
|
379
|
-
const
|
|
394
|
+
const target = { kind, ref: prKey(entry.pr.repo, entry.pr.number) };
|
|
395
|
+
const snooze = findSnooze(snoozes2, target, now, (candidate) => covers(candidate, entry));
|
|
380
396
|
if (snooze === void 0) {
|
|
381
397
|
awaiting.push(entry);
|
|
382
398
|
} else {
|
|
@@ -386,6 +402,9 @@ function splitSnoozed(entries, snoozes2, now) {
|
|
|
386
402
|
snoozed.sort((a, b) => a.until - b.until);
|
|
387
403
|
return { awaiting, snoozed };
|
|
388
404
|
}
|
|
405
|
+
function splitSnoozed(entries, snoozes2, now) {
|
|
406
|
+
return partitionSnoozed("review", entries, snoozes2, now, (snooze, entry) => entry.requestedAt.getTime() <= snooze.at);
|
|
407
|
+
}
|
|
389
408
|
function nextWakeUp(snoozes2) {
|
|
390
409
|
if (snoozes2.length === 0) {
|
|
391
410
|
return null;
|
|
@@ -406,12 +425,181 @@ function wokenPrs(due, results) {
|
|
|
406
425
|
}
|
|
407
426
|
}
|
|
408
427
|
return due.flatMap((snooze) => {
|
|
428
|
+
if (snooze.kind !== "review") {
|
|
429
|
+
return [];
|
|
430
|
+
}
|
|
409
431
|
const entry = pending.get(snooze.ref);
|
|
410
|
-
return entry !== void 0 && entry.requestedAt <= snooze.
|
|
432
|
+
return entry !== void 0 && entry.requestedAt <= snooze.at ? [entry.pr] : [];
|
|
411
433
|
});
|
|
412
434
|
}
|
|
413
435
|
|
|
436
|
+
// src/mentions.ts
|
|
437
|
+
function emptyMentionReads() {
|
|
438
|
+
return { seed: null, reads: /* @__PURE__ */ new Map() };
|
|
439
|
+
}
|
|
440
|
+
function mentionIdsOf(entry) {
|
|
441
|
+
return [...(entry.mentions ?? []).map((mention) => mention.id), ...entry.earlier];
|
|
442
|
+
}
|
|
443
|
+
function mentionItems(entries) {
|
|
444
|
+
const items = [];
|
|
445
|
+
for (const entry of entries) {
|
|
446
|
+
if (entry.mentions === null || entry.mentions.length === 0) {
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
items.push({
|
|
450
|
+
pr: entry.pr,
|
|
451
|
+
mentionedAt: Math.max(...entry.mentions.map((mention) => mention.at.getTime())),
|
|
452
|
+
mentions: entry.mentions,
|
|
453
|
+
ids: mentionIdsOf(entry)
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
return items;
|
|
457
|
+
}
|
|
458
|
+
function markOf(item) {
|
|
459
|
+
return { at: item.mentionedAt, ids: item.ids };
|
|
460
|
+
}
|
|
461
|
+
function hasNewMention(item, mark) {
|
|
462
|
+
return item.mentions.some((mention) => mention.at.getTime() > mark.at && !mark.ids.includes(mention.id));
|
|
463
|
+
}
|
|
464
|
+
function isUnreadMention(reads, item) {
|
|
465
|
+
const mark = reads.reads.get(prKey(item.pr.repo, item.pr.number));
|
|
466
|
+
if (mark === null) {
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
if (mark !== void 0) {
|
|
470
|
+
return hasNewMention(item, mark);
|
|
471
|
+
}
|
|
472
|
+
return reads.seed !== null && hasNewMention(item, reads.seed);
|
|
473
|
+
}
|
|
474
|
+
function seedMentionReads(reads, seed) {
|
|
475
|
+
return reads.seed === null ? { ...reads, seed } : reads;
|
|
476
|
+
}
|
|
477
|
+
function unseedMentionReads(reads) {
|
|
478
|
+
return reads.seed === null ? reads : { ...reads, seed: null };
|
|
479
|
+
}
|
|
480
|
+
function markMentionRead(reads, ref, mark) {
|
|
481
|
+
return { ...reads, reads: new Map(reads.reads).set(ref, mark) };
|
|
482
|
+
}
|
|
483
|
+
function markMentionUnread(reads, ref) {
|
|
484
|
+
return { ...reads, reads: new Map(reads.reads).set(ref, null) };
|
|
485
|
+
}
|
|
486
|
+
function snoozeMark(snooze) {
|
|
487
|
+
return { at: snooze.at, ids: snooze.ids ?? [] };
|
|
488
|
+
}
|
|
489
|
+
function splitMentions(items, reads, snoozes2, now) {
|
|
490
|
+
const read = [];
|
|
491
|
+
const pending = [];
|
|
492
|
+
for (const item of items) {
|
|
493
|
+
(isUnreadMention(reads, item) ? pending : read).push(item);
|
|
494
|
+
}
|
|
495
|
+
const { awaiting, snoozed } = partitionSnoozed(
|
|
496
|
+
"mention",
|
|
497
|
+
pending,
|
|
498
|
+
snoozes2,
|
|
499
|
+
now,
|
|
500
|
+
(snooze, item) => !hasNewMention(item, snoozeMark(snooze))
|
|
501
|
+
);
|
|
502
|
+
return { unread: awaiting.toSorted(byNewestMention), snoozed, read: read.toSorted(byNewestMention) };
|
|
503
|
+
}
|
|
504
|
+
function byNewestMention(a, b) {
|
|
505
|
+
return b.mentionedAt - a.mentionedAt;
|
|
506
|
+
}
|
|
507
|
+
function wokenMentions(due, entries, reads) {
|
|
508
|
+
const items = /* @__PURE__ */ new Map();
|
|
509
|
+
for (const item of mentionItems(entries)) {
|
|
510
|
+
items.set(prKey(item.pr.repo, item.pr.number), item);
|
|
511
|
+
}
|
|
512
|
+
return due.flatMap((snooze) => {
|
|
513
|
+
if (snooze.kind !== "mention") {
|
|
514
|
+
return [];
|
|
515
|
+
}
|
|
516
|
+
const item = items.get(snooze.ref);
|
|
517
|
+
return item !== void 0 && !hasNewMention(item, snoozeMark(snooze)) && isUnreadMention(reads, item) ? [item.pr] : [];
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
function mentionReadsFile() {
|
|
521
|
+
return join3(cacheDir(), "mention-reads.json");
|
|
522
|
+
}
|
|
523
|
+
function reviveMark(stored) {
|
|
524
|
+
if (typeof stored !== "object" || stored === null || Array.isArray(stored)) {
|
|
525
|
+
return void 0;
|
|
526
|
+
}
|
|
527
|
+
const { at, ids } = stored;
|
|
528
|
+
if (typeof at !== "string" || !Array.isArray(ids)) {
|
|
529
|
+
return void 0;
|
|
530
|
+
}
|
|
531
|
+
const ms = Date.parse(at);
|
|
532
|
+
return Number.isNaN(ms) ? void 0 : { at: ms, ids: ids.filter((id) => typeof id === "string") };
|
|
533
|
+
}
|
|
534
|
+
function reviveReads(stored) {
|
|
535
|
+
if (typeof stored !== "object" || stored === null || Array.isArray(stored)) {
|
|
536
|
+
return void 0;
|
|
537
|
+
}
|
|
538
|
+
const { seed, reads: marks } = stored;
|
|
539
|
+
const reads = /* @__PURE__ */ new Map();
|
|
540
|
+
if (typeof marks === "object" && marks !== null && !Array.isArray(marks)) {
|
|
541
|
+
for (const [ref, mark] of Object.entries(marks)) {
|
|
542
|
+
if (mark === null) {
|
|
543
|
+
reads.set(ref, null);
|
|
544
|
+
} else {
|
|
545
|
+
const revived = reviveMark(mark);
|
|
546
|
+
if (revived !== void 0) {
|
|
547
|
+
reads.set(ref, revived);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return { seed: reviveMark(seed) ?? null, reads };
|
|
553
|
+
}
|
|
554
|
+
function readMentionReads() {
|
|
555
|
+
const all = /* @__PURE__ */ new Map();
|
|
556
|
+
if (!cacheEnabled()) {
|
|
557
|
+
return all;
|
|
558
|
+
}
|
|
559
|
+
let parsed;
|
|
560
|
+
try {
|
|
561
|
+
parsed = JSON.parse(readFileSync3(mentionReadsFile(), "utf8"));
|
|
562
|
+
} catch {
|
|
563
|
+
return all;
|
|
564
|
+
}
|
|
565
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
566
|
+
return all;
|
|
567
|
+
}
|
|
568
|
+
const { users } = parsed;
|
|
569
|
+
if (typeof users !== "object" || users === null || Array.isArray(users)) {
|
|
570
|
+
return all;
|
|
571
|
+
}
|
|
572
|
+
for (const [login, stored] of Object.entries(users)) {
|
|
573
|
+
const reads = reviveReads(stored);
|
|
574
|
+
if (reads !== void 0) {
|
|
575
|
+
all.set(login.toLowerCase(), reads);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return all;
|
|
579
|
+
}
|
|
580
|
+
function storeMark(mark) {
|
|
581
|
+
return { at: new Date(mark.at).toISOString(), ids: [...mark.ids] };
|
|
582
|
+
}
|
|
583
|
+
function writeMentionReads(all) {
|
|
584
|
+
if (!cacheEnabled()) {
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
const stored = { users: {} };
|
|
588
|
+
for (const [login, reads] of all) {
|
|
589
|
+
const marks = {};
|
|
590
|
+
for (const [ref, mark] of reads.reads) {
|
|
591
|
+
marks[ref] = mark === null ? null : storeMark(mark);
|
|
592
|
+
}
|
|
593
|
+
stored.users[login] = { seed: reads.seed === null ? null : storeMark(reads.seed), reads: marks };
|
|
594
|
+
}
|
|
595
|
+
writeFileAtomic(mentionReadsFile(), `${JSON.stringify(stored, null, 2)}
|
|
596
|
+
`);
|
|
597
|
+
return true;
|
|
598
|
+
}
|
|
599
|
+
|
|
414
600
|
// src/settings.ts
|
|
601
|
+
import { readFileSync as readFileSync4, rmSync as rmSync2 } from "node:fs";
|
|
602
|
+
import { join as join4 } from "node:path";
|
|
415
603
|
var NOTIFY_CHANNELS = ["auto", "terminal", "command", "bell"];
|
|
416
604
|
var DEFAULT_RELOAD_INTERVAL = "10m";
|
|
417
605
|
var MAX_RELOAD_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -431,7 +619,7 @@ function parseReloadInterval(input) {
|
|
|
431
619
|
return ms;
|
|
432
620
|
}
|
|
433
621
|
function settingsFile() {
|
|
434
|
-
return
|
|
622
|
+
return join4(cacheDir(), "settings.json");
|
|
435
623
|
}
|
|
436
624
|
var current = {};
|
|
437
625
|
function writeCurrent() {
|
|
@@ -449,7 +637,7 @@ function loadSettings() {
|
|
|
449
637
|
}
|
|
450
638
|
let text;
|
|
451
639
|
try {
|
|
452
|
-
text =
|
|
640
|
+
text = readFileSync4(settingsFile(), "utf8");
|
|
453
641
|
} catch {
|
|
454
642
|
return current;
|
|
455
643
|
}
|
|
@@ -480,6 +668,12 @@ function loadSettings() {
|
|
|
480
668
|
if (settings.notifications !== void 0 && typeof settings.notifications !== "boolean") {
|
|
481
669
|
throw new CliError(`"notifications" in ${settingsFile()} must be true or false`);
|
|
482
670
|
}
|
|
671
|
+
if (settings.trackMentions !== void 0 && typeof settings.trackMentions !== "boolean") {
|
|
672
|
+
throw new CliError(`"trackMentions" in ${settingsFile()} must be true or false`);
|
|
673
|
+
}
|
|
674
|
+
if (settings.notifyMentions !== void 0 && typeof settings.notifyMentions !== "boolean") {
|
|
675
|
+
throw new CliError(`"notifyMentions" in ${settingsFile()} must be true or false`);
|
|
676
|
+
}
|
|
483
677
|
if (settings.notifyChannel !== void 0 && !NOTIFY_CHANNELS.includes(settings.notifyChannel)) {
|
|
484
678
|
throw new CliError(`"notifyChannel" in ${settingsFile()} must be "auto", "terminal", "command", or "bell"`);
|
|
485
679
|
}
|
|
@@ -511,6 +705,14 @@ function saveNotifications(on) {
|
|
|
511
705
|
current = { ...current, notifications: on };
|
|
512
706
|
return writeCurrent();
|
|
513
707
|
}
|
|
708
|
+
function saveTrackMentions(on) {
|
|
709
|
+
current = { ...current, trackMentions: on };
|
|
710
|
+
return writeCurrent();
|
|
711
|
+
}
|
|
712
|
+
function saveNotifyMentions(on) {
|
|
713
|
+
current = { ...current, notifyMentions: on };
|
|
714
|
+
return writeCurrent();
|
|
715
|
+
}
|
|
514
716
|
function saveNotifyChannel(channel) {
|
|
515
717
|
current = { ...current, notifyChannel: channel };
|
|
516
718
|
return writeCurrent();
|
|
@@ -544,6 +746,124 @@ function applySettings(values, explicit) {
|
|
|
544
746
|
return settings;
|
|
545
747
|
}
|
|
546
748
|
|
|
749
|
+
// src/tui/state/browse.ts
|
|
750
|
+
var TABS = ["1 Awaiting you", "2 Your PRs", "3 Reviews", "4 PR size", "5 Comments"];
|
|
751
|
+
var PENDING_SUB_TABS = [
|
|
752
|
+
{ key: "pending", label: "Awaiting review" },
|
|
753
|
+
{ key: "reviewed", label: "Reviewed" },
|
|
754
|
+
{ key: "mentions", label: "Mentions" }
|
|
755
|
+
];
|
|
756
|
+
var AUTHORED_SUB_TABS = [
|
|
757
|
+
{ key: "open", label: "Open" },
|
|
758
|
+
{ key: "merged", label: "Merged & closed" }
|
|
759
|
+
];
|
|
760
|
+
function cycledPendingSubTab(current2, delta) {
|
|
761
|
+
const index = PENDING_SUB_TABS.findIndex((entry) => entry.key === current2);
|
|
762
|
+
return PENDING_SUB_TABS[(index + PENDING_SUB_TABS.length + delta) % PENDING_SUB_TABS.length];
|
|
763
|
+
}
|
|
764
|
+
var initialBrowseState = {
|
|
765
|
+
tab: 0,
|
|
766
|
+
pendingTab: "pending",
|
|
767
|
+
authoredTab: "open",
|
|
768
|
+
scopes: {
|
|
769
|
+
pending: { view: "list" },
|
|
770
|
+
reviewed: { view: "list" },
|
|
771
|
+
mentions: { view: "list" },
|
|
772
|
+
open: { view: "list" },
|
|
773
|
+
review: { view: "list" },
|
|
774
|
+
size: { view: "list" },
|
|
775
|
+
comment: { view: "list" },
|
|
776
|
+
merged: { view: "list" }
|
|
777
|
+
},
|
|
778
|
+
repoCursors: { pending: 0, reviewed: 0, mentions: 0, open: 0, review: 0, size: 0, comment: 0, merged: 0 },
|
|
779
|
+
rowCursors: { pending: 0, reviewed: 0, mentions: 0, open: 0 },
|
|
780
|
+
grouped: { pending: false, reviewed: false, mentions: false, open: false },
|
|
781
|
+
expanded: { review: false, size: false, comment: false, merged: false }
|
|
782
|
+
};
|
|
783
|
+
function activeQueueTab(state) {
|
|
784
|
+
if (state.tab === 0) {
|
|
785
|
+
return state.pendingTab;
|
|
786
|
+
}
|
|
787
|
+
if (state.tab === 1 && state.authoredTab === "open") {
|
|
788
|
+
return "open";
|
|
789
|
+
}
|
|
790
|
+
return null;
|
|
791
|
+
}
|
|
792
|
+
function dropVanishedRepo(scope, repos) {
|
|
793
|
+
if (scope.view === "detail" && scope.repo !== null && !repos.some((option) => option.repo === scope.repo)) {
|
|
794
|
+
return { view: "list" };
|
|
795
|
+
}
|
|
796
|
+
return scope;
|
|
797
|
+
}
|
|
798
|
+
function cycled(previous, delta, count2) {
|
|
799
|
+
return (Math.min(previous, count2 - 1) + count2 + delta) % count2;
|
|
800
|
+
}
|
|
801
|
+
function browseReducer(state, action) {
|
|
802
|
+
switch (action.type) {
|
|
803
|
+
case "tabSelected": {
|
|
804
|
+
return { ...state, tab: action.tab };
|
|
805
|
+
}
|
|
806
|
+
case "tabCycled": {
|
|
807
|
+
return { ...state, tab: (state.tab + TABS.length + action.delta) % TABS.length };
|
|
808
|
+
}
|
|
809
|
+
case "subTabCycled": {
|
|
810
|
+
if (state.tab === 0) {
|
|
811
|
+
return { ...state, pendingTab: cycledPendingSubTab(state.pendingTab, action.delta).key };
|
|
812
|
+
}
|
|
813
|
+
if (state.tab === 1) {
|
|
814
|
+
return { ...state, authoredTab: state.authoredTab === "open" ? "merged" : "open" };
|
|
815
|
+
}
|
|
816
|
+
return state;
|
|
817
|
+
}
|
|
818
|
+
case "repoCursorMoved": {
|
|
819
|
+
return {
|
|
820
|
+
...state,
|
|
821
|
+
repoCursors: {
|
|
822
|
+
...state.repoCursors,
|
|
823
|
+
[action.tab]: cycled(state.repoCursors[action.tab], action.delta, action.count)
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
case "rowCursorMoved": {
|
|
828
|
+
return {
|
|
829
|
+
...state,
|
|
830
|
+
rowCursors: {
|
|
831
|
+
...state.rowCursors,
|
|
832
|
+
[action.tab]: cycled(state.rowCursors[action.tab], action.delta, action.count)
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
case "repoOpened": {
|
|
837
|
+
return { ...state, scopes: { ...state.scopes, [action.tab]: { view: "detail", repo: action.repo } } };
|
|
838
|
+
}
|
|
839
|
+
case "pickerReturned": {
|
|
840
|
+
return { ...state, scopes: { ...state.scopes, [action.tab]: { view: "list" } } };
|
|
841
|
+
}
|
|
842
|
+
case "groupingToggled": {
|
|
843
|
+
return { ...state, grouped: { ...state.grouped, [action.tab]: !state.grouped[action.tab] } };
|
|
844
|
+
}
|
|
845
|
+
case "expandToggled": {
|
|
846
|
+
return { ...state, expanded: { ...state.expanded, [action.tab]: !state.expanded[action.tab] } };
|
|
847
|
+
}
|
|
848
|
+
case "dataLoaded": {
|
|
849
|
+
return {
|
|
850
|
+
...state,
|
|
851
|
+
scopes: {
|
|
852
|
+
pending: dropVanishedRepo(state.scopes.pending, action.repos.pending),
|
|
853
|
+
reviewed: dropVanishedRepo(state.scopes.reviewed, action.repos.reviewed),
|
|
854
|
+
mentions: dropVanishedRepo(state.scopes.mentions, action.repos.mentions),
|
|
855
|
+
open: dropVanishedRepo(state.scopes.open, action.repos.open),
|
|
856
|
+
review: dropVanishedRepo(state.scopes.review, action.repos.review),
|
|
857
|
+
size: dropVanishedRepo(state.scopes.size, action.repos.size),
|
|
858
|
+
comment: dropVanishedRepo(state.scopes.comment, action.repos.comment),
|
|
859
|
+
merged: dropVanishedRepo(state.scopes.merged, action.repos.merged)
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return state;
|
|
865
|
+
}
|
|
866
|
+
|
|
547
867
|
// src/tui/theme.ts
|
|
548
868
|
var BASE = {
|
|
549
869
|
bg: "#1e1e1e",
|
|
@@ -837,26 +1157,23 @@ function durationHours(start, end) {
|
|
|
837
1157
|
}
|
|
838
1158
|
|
|
839
1159
|
// src/compute.ts
|
|
840
|
-
function
|
|
841
|
-
const reviewed = [];
|
|
1160
|
+
function pendingRequests(results) {
|
|
842
1161
|
const pending = [];
|
|
843
1162
|
for (const result of results) {
|
|
844
|
-
if (result.kind === "
|
|
845
|
-
|
|
846
|
-
pr: result.pr,
|
|
847
|
-
requestedAt: result.requestedAt,
|
|
848
|
-
reviewedAt: result.reviewedAt,
|
|
849
|
-
hours: durationHours(result.requestedAt, result.reviewedAt),
|
|
850
|
-
verdict: result.verdict,
|
|
851
|
-
lines: result.lines
|
|
852
|
-
});
|
|
853
|
-
} else if (result.kind === "pending" && result.pr.state === "open") {
|
|
854
|
-
pending.push({ pr: result.pr, requestedAt: result.requestedAt, hours: durationHours(result.requestedAt, now) });
|
|
1163
|
+
if (result.kind === "pending" && result.pr.state === "open") {
|
|
1164
|
+
pending.push({ pr: result.pr, requestedAt: result.requestedAt });
|
|
855
1165
|
}
|
|
856
1166
|
}
|
|
857
|
-
pending.
|
|
858
|
-
|
|
859
|
-
|
|
1167
|
+
return pending.toSorted((a, b) => a.requestedAt.getTime() - b.requestedAt.getTime());
|
|
1168
|
+
}
|
|
1169
|
+
function latestReviews(results) {
|
|
1170
|
+
const pendingKeys = /* @__PURE__ */ new Set();
|
|
1171
|
+
for (const result of results) {
|
|
1172
|
+
if (result.kind === "pending" && result.pr.state === "open") {
|
|
1173
|
+
pendingKeys.add(`${result.pr.repo}#${result.pr.number}`);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
const latest = /* @__PURE__ */ new Map();
|
|
860
1177
|
for (const result of results) {
|
|
861
1178
|
if (result.kind !== "reviewed" && result.kind !== "unrequested" || result.pr.state !== "open") {
|
|
862
1179
|
continue;
|
|
@@ -865,14 +1182,33 @@ function computeReviewStats(results, { targetHours, now = /* @__PURE__ */ new Da
|
|
|
865
1182
|
if (pendingKeys.has(key)) {
|
|
866
1183
|
continue;
|
|
867
1184
|
}
|
|
868
|
-
const
|
|
869
|
-
if (
|
|
870
|
-
|
|
1185
|
+
const known = latest.get(key);
|
|
1186
|
+
if (known === void 0 || result.reviewedAt > known.reviewedAt) {
|
|
1187
|
+
latest.set(key, { pr: result.pr, reviewedAt: result.reviewedAt });
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
return [...latest.values()].toSorted((a, b) => a.reviewedAt.getTime() - b.reviewedAt.getTime());
|
|
1191
|
+
}
|
|
1192
|
+
function computeReviewStats(results, { targetHours, now = /* @__PURE__ */ new Date() } = {}) {
|
|
1193
|
+
const reviewed = [];
|
|
1194
|
+
for (const result of results) {
|
|
1195
|
+
if (result.kind === "reviewed") {
|
|
1196
|
+
reviewed.push({
|
|
1197
|
+
pr: result.pr,
|
|
1198
|
+
requestedAt: result.requestedAt,
|
|
1199
|
+
reviewedAt: result.reviewedAt,
|
|
1200
|
+
hours: durationHours(result.requestedAt, result.reviewedAt),
|
|
1201
|
+
verdict: result.verdict,
|
|
1202
|
+
lines: result.lines
|
|
1203
|
+
});
|
|
871
1204
|
}
|
|
872
1205
|
}
|
|
873
|
-
const
|
|
874
|
-
return {
|
|
875
|
-
})
|
|
1206
|
+
const pending = pendingRequests(results).map((entry) => {
|
|
1207
|
+
return { ...entry, hours: durationHours(entry.requestedAt, now) };
|
|
1208
|
+
});
|
|
1209
|
+
const reviewing = latestReviews(results).map((entry) => {
|
|
1210
|
+
return { ...entry, hours: durationHours(entry.reviewedAt, now) };
|
|
1211
|
+
});
|
|
876
1212
|
const expired = results.filter((result) => result.kind === "pending" && result.pr.state !== "open");
|
|
877
1213
|
const unrequested = results.filter((result) => result.kind === "unrequested");
|
|
878
1214
|
const allHours = reviewed.map((result) => result.hours);
|
|
@@ -1125,12 +1461,24 @@ function queueRowAt(view, cursor) {
|
|
|
1125
1461
|
return rows[Math.min(cursor, rows.length - 1)];
|
|
1126
1462
|
}
|
|
1127
1463
|
function snoozeActionOf(row) {
|
|
1128
|
-
if (row?.pending
|
|
1464
|
+
if (row?.pending !== void 0) {
|
|
1465
|
+
return row.pending.snoozed ? "unsnooze" : "snooze";
|
|
1466
|
+
}
|
|
1467
|
+
if (row?.mention === void 0 || row.mention.state === "read") {
|
|
1468
|
+
return null;
|
|
1469
|
+
}
|
|
1470
|
+
return row.mention.state === "snoozed" ? "unsnooze" : "snooze";
|
|
1471
|
+
}
|
|
1472
|
+
function mentionActionOf(row) {
|
|
1473
|
+
if (row?.mention === void 0) {
|
|
1129
1474
|
return null;
|
|
1130
1475
|
}
|
|
1131
|
-
return row.
|
|
1476
|
+
return row.mention.state === "read" ? "unread" : "read";
|
|
1132
1477
|
}
|
|
1133
|
-
function
|
|
1478
|
+
function unreadMentionRows(view) {
|
|
1479
|
+
return view === null ? [] : queueRows(view).filter((row) => row.mention?.state === "unread");
|
|
1480
|
+
}
|
|
1481
|
+
function groupedLists(entries, rowsOf) {
|
|
1134
1482
|
const groups = /* @__PURE__ */ new Map();
|
|
1135
1483
|
for (const entry of entries) {
|
|
1136
1484
|
const group = groups.get(entry.pr.repo) ?? [];
|
|
@@ -1138,40 +1486,108 @@ function groupedLists(entries, rowsOf2) {
|
|
|
1138
1486
|
groups.set(entry.pr.repo, group);
|
|
1139
1487
|
}
|
|
1140
1488
|
return [...groups.entries()].toSorted((a, b) => b[1].length - a[1].length || a[0].localeCompare(b[0])).map(([repo, group]) => {
|
|
1141
|
-
return { title: `${repo} (n=${group.length})`, rows:
|
|
1489
|
+
return { title: `${repo} (n=${group.length})`, rows: rowsOf(group) };
|
|
1142
1490
|
});
|
|
1143
1491
|
}
|
|
1144
|
-
function
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1492
|
+
function inScopeOf(entries, repo) {
|
|
1493
|
+
return repo === null ? entries : entries.filter((entry) => entry.pr.repo === repo);
|
|
1494
|
+
}
|
|
1495
|
+
function sectionOf(title, entries, rowsOf, split) {
|
|
1496
|
+
return split ? { title, rows: [], lists: groupedLists(entries, rowsOf) } : { title, rows: rowsOf(entries), lists: [] };
|
|
1497
|
+
}
|
|
1498
|
+
function unreadMentionRefs(raw, snoozes2, reads, now) {
|
|
1499
|
+
const { unread } = splitMentions(mentionItems(raw.mentions ?? []), reads, snoozes2, now);
|
|
1500
|
+
return new Set(unread.map((item) => prKey(item.pr.repo, item.pr.number)));
|
|
1501
|
+
}
|
|
1502
|
+
function queueAlerts(raw, snoozes2 = [], reads = emptyMentionReads(), now = Date.now()) {
|
|
1503
|
+
const { awaiting } = splitSnoozed(pendingRequests(raw.reviewResults), snoozes2, now);
|
|
1504
|
+
const { unread } = splitMentions(mentionItems(raw.mentions ?? []), reads, snoozes2, now);
|
|
1505
|
+
return { pending: awaiting.length > 0, mentions: unread.length > 0 };
|
|
1506
|
+
}
|
|
1507
|
+
function buildPendingReviewView(raw, repo = null, grouped = false, snoozes2 = [], reads = emptyMentionReads(), now = Date.now()) {
|
|
1508
|
+
const { awaiting, snoozed } = splitSnoozed(inScopeOf(pendingRequests(raw.reviewResults), repo), snoozes2, now);
|
|
1509
|
+
if (awaiting.length === 0 && snoozed.length === 0) {
|
|
1510
|
+
return { empty: "No PRs are awaiting your review.", sections: [] };
|
|
1151
1511
|
}
|
|
1152
1512
|
const split = repo === null && grouped;
|
|
1153
|
-
const
|
|
1154
|
-
const
|
|
1513
|
+
const mentioned = unreadMentionRefs(raw, snoozes2, reads, now);
|
|
1514
|
+
const awaitingRowsOf = (group) => waitRows(group, (entry) => entry.requestedAt, raw.fetchedAt).map((row, i) => {
|
|
1515
|
+
return {
|
|
1516
|
+
...row,
|
|
1517
|
+
pending: { requestedAt: group[i].requestedAt.getTime(), snoozed: false },
|
|
1518
|
+
...badgeOf(row, mentioned)
|
|
1519
|
+
};
|
|
1520
|
+
});
|
|
1521
|
+
const snoozedRowsOf = (group) => wakeRows(group, now).map((row, i) => {
|
|
1522
|
+
return {
|
|
1523
|
+
...row,
|
|
1524
|
+
pending: { requestedAt: group[i].requestedAt.getTime(), snoozed: true },
|
|
1525
|
+
...badgeOf(row, mentioned)
|
|
1526
|
+
};
|
|
1527
|
+
});
|
|
1155
1528
|
return {
|
|
1156
1529
|
empty: null,
|
|
1157
1530
|
sections: [
|
|
1158
|
-
...awaiting.length === 0 ? [] : [sectionOf(`Awaiting your review (n=${awaiting.length})`, awaiting,
|
|
1159
|
-
...snoozed.length === 0 ? [] : [sectionOf(`Snoozed (n=${snoozed.length})`, snoozed, snoozedRowsOf)]
|
|
1160
|
-
...reviewing.length === 0 ? [] : [sectionOf(`Reviewed (n=${reviewing.length})`, reviewing, rowsOf)]
|
|
1531
|
+
...awaiting.length === 0 ? [] : [sectionOf(`Awaiting your review (n=${awaiting.length})`, awaiting, awaitingRowsOf, split)],
|
|
1532
|
+
...snoozed.length === 0 ? [] : [sectionOf(`Snoozed (n=${snoozed.length})`, snoozed, snoozedRowsOf, split)]
|
|
1161
1533
|
]
|
|
1162
1534
|
};
|
|
1163
1535
|
}
|
|
1164
|
-
function
|
|
1165
|
-
|
|
1166
|
-
|
|
1536
|
+
function buildReviewedView(raw, repo = null, grouped = false, snoozes2 = [], reads = emptyMentionReads(), now = Date.now()) {
|
|
1537
|
+
const reviewing = inScopeOf(latestReviews(raw.reviewResults), repo);
|
|
1538
|
+
if (reviewing.length === 0) {
|
|
1539
|
+
return { empty: "No PRs you reviewed are still open.", sections: [] };
|
|
1540
|
+
}
|
|
1541
|
+
const mentioned = unreadMentionRefs(raw, snoozes2, reads, now);
|
|
1542
|
+
const reviewedRowsOf = (group) => waitRows(group, (entry) => entry.reviewedAt, raw.fetchedAt).map((row) => {
|
|
1543
|
+
return { ...row, ...badgeOf(row, mentioned) };
|
|
1167
1544
|
});
|
|
1545
|
+
return {
|
|
1546
|
+
empty: null,
|
|
1547
|
+
sections: [sectionOf(`Reviewed (n=${reviewing.length})`, reviewing, reviewedRowsOf, repo === null && grouped)]
|
|
1548
|
+
};
|
|
1168
1549
|
}
|
|
1169
|
-
function
|
|
1550
|
+
function buildMentionsView(raw, repo = null, grouped = false, snoozes2 = [], reads = emptyMentionReads(), now = Date.now()) {
|
|
1551
|
+
if (raw.mentions === null) {
|
|
1552
|
+
return {
|
|
1553
|
+
empty: "Mention tracking is off. Turn on Track mentions in the settings (S) to list the PRs that mention you.",
|
|
1554
|
+
sections: []
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
const { unread, snoozed, read } = splitMentions(inScopeOf(mentionItems(raw.mentions), repo), reads, snoozes2, now);
|
|
1558
|
+
if (unread.length === 0 && snoozed.length === 0 && read.length === 0) {
|
|
1559
|
+
return { empty: "No PR mentions you.", sections: [] };
|
|
1560
|
+
}
|
|
1561
|
+
const split = repo === null && grouped;
|
|
1562
|
+
const unreadRowsOf = (group) => mentionRows(group, "unread", raw.fetchedAt);
|
|
1563
|
+
const readRowsOf = (group) => mentionRows(group, "read", raw.fetchedAt);
|
|
1564
|
+
const snoozedRowsOf = (group) => wakeRows(group, now).map((row, i) => {
|
|
1565
|
+
return { ...row, mention: { mark: markOf(group[i]), state: "snoozed" } };
|
|
1566
|
+
});
|
|
1567
|
+
return {
|
|
1568
|
+
empty: null,
|
|
1569
|
+
sections: [
|
|
1570
|
+
...unread.length === 0 ? [] : [sectionOf(`Unread (n=${unread.length})`, unread, unreadRowsOf, split)],
|
|
1571
|
+
...snoozed.length === 0 ? [] : [sectionOf(`Snoozed (n=${snoozed.length})`, snoozed, snoozedRowsOf, split)],
|
|
1572
|
+
...read.length === 0 ? [] : [sectionOf(`Read (n=${read.length})`, read, readRowsOf, split)]
|
|
1573
|
+
]
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
function badgeOf(row, mentioned) {
|
|
1577
|
+
return mentioned.has(row.ref) ? { mentioned: true } : {};
|
|
1578
|
+
}
|
|
1579
|
+
function wakeRows(group, now) {
|
|
1580
|
+
return toPrRows(
|
|
1581
|
+
group,
|
|
1582
|
+
group.map((item) => `until ${formatWakeTime(item.until, now)}`)
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
function mentionRows(group, state, fetchedAt) {
|
|
1170
1586
|
return toPrRows(
|
|
1171
1587
|
group,
|
|
1172
|
-
group.map((
|
|
1588
|
+
group.map((item) => durationLead({ hours: durationHours(new Date(item.mentionedAt), fetchedAt) }))
|
|
1173
1589
|
).map((row, i) => {
|
|
1174
|
-
return { ...row,
|
|
1590
|
+
return { ...row, mention: { mark: markOf(group[i]), state } };
|
|
1175
1591
|
});
|
|
1176
1592
|
}
|
|
1177
1593
|
function buildOpenAuthoredView(raw, repo = null, grouped = false) {
|
|
@@ -1179,7 +1595,7 @@ function buildOpenAuthoredView(raw, repo = null, grouped = false) {
|
|
|
1179
1595
|
if (open.length === 0) {
|
|
1180
1596
|
return { empty: "No open authored PRs found.", sections: [] };
|
|
1181
1597
|
}
|
|
1182
|
-
const
|
|
1598
|
+
const rowsOf = (group) => {
|
|
1183
1599
|
const ages = group.map((entry) => durationLead({ hours: durationHours(entry.pr.createdAt, raw.fetchedAt) }));
|
|
1184
1600
|
const ageWidth = Math.max(...ages.map((age) => age.length));
|
|
1185
1601
|
return toPrRows(
|
|
@@ -1191,14 +1607,14 @@ function buildOpenAuthoredView(raw, repo = null, grouped = false) {
|
|
|
1191
1607
|
};
|
|
1192
1608
|
const title = `Your open authored PRs (n=${open.length})`;
|
|
1193
1609
|
if (repo === null && grouped) {
|
|
1194
|
-
return { empty: null, sections: [{ title, rows: [], lists: groupedLists(open,
|
|
1610
|
+
return { empty: null, sections: [{ title, rows: [], lists: groupedLists(open, rowsOf) }] };
|
|
1195
1611
|
}
|
|
1196
|
-
return { empty: null, sections: [{ title, rows:
|
|
1612
|
+
return { empty: null, sections: [{ title, rows: rowsOf(open), lists: [] }] };
|
|
1197
1613
|
}
|
|
1198
|
-
function
|
|
1614
|
+
function waitRows(group, startOf, fetchedAt) {
|
|
1199
1615
|
return toPrRows(
|
|
1200
1616
|
group,
|
|
1201
|
-
group.map((entry) => durationLead(entry))
|
|
1617
|
+
group.map((entry) => durationLead({ hours: durationHours(startOf(entry), fetchedAt) }))
|
|
1202
1618
|
);
|
|
1203
1619
|
}
|
|
1204
1620
|
|
|
@@ -1208,10 +1624,8 @@ function Footer({
|
|
|
1208
1624
|
width,
|
|
1209
1625
|
modal,
|
|
1210
1626
|
editing,
|
|
1211
|
-
|
|
1212
|
-
authoredTab,
|
|
1627
|
+
browse,
|
|
1213
1628
|
views,
|
|
1214
|
-
pendingCursor,
|
|
1215
1629
|
copyLinks: copyLinks2,
|
|
1216
1630
|
openError,
|
|
1217
1631
|
successNotice,
|
|
@@ -1220,10 +1634,7 @@ function Footer({
|
|
|
1220
1634
|
const notice = openError ?? successNotice ?? (stale ? "options changed \xB7 press r to reload" : "");
|
|
1221
1635
|
const check = openError === null && successNotice !== null;
|
|
1222
1636
|
const noticeWidth = notice === "" ? 0 : notice.length + (check ? 2 : 0) + 2;
|
|
1223
|
-
const hints = truncated(
|
|
1224
|
-
hintsFor(modal, editing, tab, authoredTab, views, pendingCursor, copyLinks2),
|
|
1225
|
-
width - 2 - noticeWidth
|
|
1226
|
-
);
|
|
1637
|
+
const hints = truncated(hintsFor(modal, editing, browse, views, copyLinks2), width - 2 - noticeWidth);
|
|
1227
1638
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1228
1639
|
/* @__PURE__ */ jsx("box", { height: 1, children: /* @__PURE__ */ jsx("text", { wrapMode: "none", fg: theme.border, children: "\u2500".repeat(width) }) }),
|
|
1229
1640
|
/* @__PURE__ */ jsxs(
|
|
@@ -1252,7 +1663,37 @@ function truncated(text, limit2) {
|
|
|
1252
1663
|
}
|
|
1253
1664
|
return limit2 <= 1 ? "" : `${text.slice(0, limit2 - 1).trimEnd()}\u2026`;
|
|
1254
1665
|
}
|
|
1255
|
-
function
|
|
1666
|
+
function queueViewsOf(key, views) {
|
|
1667
|
+
if (key === "pending") {
|
|
1668
|
+
return { view: views.pending, repos: views.pendingRepos, scope: views.pendingScope };
|
|
1669
|
+
}
|
|
1670
|
+
if (key === "reviewed") {
|
|
1671
|
+
return { view: views.reviewed, repos: views.reviewedRepos, scope: views.reviewedScope };
|
|
1672
|
+
}
|
|
1673
|
+
if (key === "mentions") {
|
|
1674
|
+
return { view: views.mentions, repos: views.mentionsRepos, scope: views.mentionsScope };
|
|
1675
|
+
}
|
|
1676
|
+
return { view: views.open, repos: views.openRepos, scope: views.openScope };
|
|
1677
|
+
}
|
|
1678
|
+
function subTabHint(browse) {
|
|
1679
|
+
if (browse.tab === 0) {
|
|
1680
|
+
return `t ${cycledPendingSubTab(browse.pendingTab, 1).label.toLowerCase()} \xB7 `;
|
|
1681
|
+
}
|
|
1682
|
+
if (browse.tab === 1) {
|
|
1683
|
+
return browse.authoredTab === "open" ? "t merged stats \xB7 " : "t open PRs \xB7 ";
|
|
1684
|
+
}
|
|
1685
|
+
return "";
|
|
1686
|
+
}
|
|
1687
|
+
function queueKeyHints(view, cursor) {
|
|
1688
|
+
const row = queueRowAt(view, cursor);
|
|
1689
|
+
const snoozeAction = snoozeActionOf(row);
|
|
1690
|
+
const mentionAction = mentionActionOf(row);
|
|
1691
|
+
const snooze = snoozeAction === null ? "" : `s ${snoozeAction} \xB7 `;
|
|
1692
|
+
const mark = mentionAction === null ? "" : `d mark ${mentionAction} \xB7 `;
|
|
1693
|
+
const markAll = unreadMentionRows(view).length > 0 ? "D read all \xB7 " : "";
|
|
1694
|
+
return `${snooze}${mark}${markAll}`;
|
|
1695
|
+
}
|
|
1696
|
+
function hintsFor(modal, editing, browse, views, copyLinks2) {
|
|
1256
1697
|
if (modal === "options") {
|
|
1257
1698
|
return editing ? "enter apply \xB7 esc cancel" : "\u2191/\u2193 select \xB7 enter edit \xB7 \u2190/\u2192 toggle \xB7 s save \xB7 esc close \xB7 q quit";
|
|
1258
1699
|
}
|
|
@@ -1265,20 +1706,20 @@ function hintsFor(modal, editing, tab, authoredTab, views, pendingCursor, copyLi
|
|
|
1265
1706
|
if (modal === "snooze") {
|
|
1266
1707
|
return "enter snooze \xB7 esc cancel";
|
|
1267
1708
|
}
|
|
1268
|
-
const
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1709
|
+
const { tab } = browse;
|
|
1710
|
+
const toggle = subTabHint(browse);
|
|
1711
|
+
const queue = activeQueueTab(browse);
|
|
1712
|
+
if (queue !== null) {
|
|
1713
|
+
const { view: view2, repos: repos2, scope: scope2 } = views === null ? { view: null, repos: [], scope: null } : queueViewsOf(queue, views);
|
|
1272
1714
|
if (scope2?.view === "list") {
|
|
1273
1715
|
return `\u2191/\u2193 select \xB7 enter open \xB7 ${toggle}\u2190/\u2192 tabs \xB7 o options \xB7 S settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
|
|
1274
1716
|
}
|
|
1275
|
-
const
|
|
1276
|
-
const snooze = snoozeAction === null ? "" : `s ${snoozeAction} \xB7 `;
|
|
1717
|
+
const queueKeys = queueKeyHints(view2, browse.rowCursors[queue]);
|
|
1277
1718
|
const action = copyLinks2 ? "enter copy link" : "enter open";
|
|
1278
1719
|
if (scope2 !== null && repos2.length > 0) {
|
|
1279
|
-
return scope2.repo === null ? `\u2191/\u2193 select \xB7 ${action} \xB7 ${
|
|
1720
|
+
return scope2.repo === null ? `\u2191/\u2193 select \xB7 ${action} \xB7 ${queueKeys}${toggle}g group by repo \xB7 esc back \xB7 o options \xB7 S settings \xB7 r reload \xB7 q quit` : `\u2191/\u2193 select \xB7 ${action} \xB7 ${queueKeys}${toggle}esc back \xB7 1-5 tabs \xB7 o options \xB7 S settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
|
|
1280
1721
|
}
|
|
1281
|
-
return `\u2191/\u2193 select \xB7 ${copyLinks2 ? "enter copy link" : "enter open in browser"} \xB7 ${
|
|
1722
|
+
return `\u2191/\u2193 select \xB7 ${copyLinks2 ? "enter copy link" : "enter open in browser"} \xB7 ${queueKeys}${toggle}\u2190/\u2192 tabs \xB7 o options \xB7 S settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
|
|
1282
1723
|
}
|
|
1283
1724
|
const scope = views === null ? null : tab === 1 ? views.mergedScope : tab === 2 ? views.reviewScope : tab === 3 ? views.sizeScope : views.commentScope;
|
|
1284
1725
|
const repos = views === null ? [] : tab === 1 ? views.mergedRepos : tab === 2 ? views.reviewRepos : tab === 3 ? views.sizeRepos : views.commentRepos;
|
|
@@ -2536,7 +2977,8 @@ function QueuePanel({
|
|
|
2536
2977
|
const renderRow = (row, index, indent) => {
|
|
2537
2978
|
const isSelected = index === cursor;
|
|
2538
2979
|
const bg = isSelected ? theme.selectedBg : void 0;
|
|
2539
|
-
const
|
|
2980
|
+
const parked = row.pending?.snoozed === true || row.mention?.state === "snoozed" || row.mention?.state === "read";
|
|
2981
|
+
const fg = parked ? theme.muted : theme.text;
|
|
2540
2982
|
const refStart = indent.length + 2 + row.lead.length + 2;
|
|
2541
2983
|
return /* @__PURE__ */ jsxs5(
|
|
2542
2984
|
"text",
|
|
@@ -2567,10 +3009,9 @@ function QueuePanel({
|
|
|
2567
3009
|
" "
|
|
2568
3010
|
] }),
|
|
2569
3011
|
onRefClick === null ? /* @__PURE__ */ jsx6("a", { href: row.url, fg: theme.accent, bg, children: row.ref }) : /* @__PURE__ */ jsx6("span", { fg: theme.accent, bg, children: row.ref }),
|
|
2570
|
-
/* @__PURE__ */
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
] })
|
|
3012
|
+
/* @__PURE__ */ jsx6("span", { fg, bg, children: " " }),
|
|
3013
|
+
row.mentioned === true && /* @__PURE__ */ jsx6("span", { fg: theme.accent, bg, children: "@ " }),
|
|
3014
|
+
/* @__PURE__ */ jsx6("span", { fg, bg, children: row.title })
|
|
2574
3015
|
]
|
|
2575
3016
|
},
|
|
2576
3017
|
row.url
|
|
@@ -2647,91 +3088,6 @@ function RepoList({
|
|
|
2647
3088
|
);
|
|
2648
3089
|
}
|
|
2649
3090
|
|
|
2650
|
-
// src/tui/state/browse.ts
|
|
2651
|
-
var TABS = ["1 Awaiting you", "2 Your PRs", "3 Reviews", "4 PR size", "5 Comments"];
|
|
2652
|
-
var initialBrowseState = {
|
|
2653
|
-
tab: 0,
|
|
2654
|
-
authoredTab: "open",
|
|
2655
|
-
scopes: {
|
|
2656
|
-
pending: { view: "list" },
|
|
2657
|
-
open: { view: "list" },
|
|
2658
|
-
review: { view: "list" },
|
|
2659
|
-
size: { view: "list" },
|
|
2660
|
-
comment: { view: "list" },
|
|
2661
|
-
merged: { view: "list" }
|
|
2662
|
-
},
|
|
2663
|
-
repoCursors: { pending: 0, open: 0, review: 0, size: 0, comment: 0, merged: 0 },
|
|
2664
|
-
rowCursors: { pending: 0, open: 0 },
|
|
2665
|
-
grouped: { pending: false, open: false },
|
|
2666
|
-
expanded: { review: false, size: false, comment: false, merged: false }
|
|
2667
|
-
};
|
|
2668
|
-
function dropVanishedRepo(scope, repos) {
|
|
2669
|
-
if (scope.view === "detail" && scope.repo !== null && !repos.some((option) => option.repo === scope.repo)) {
|
|
2670
|
-
return { view: "list" };
|
|
2671
|
-
}
|
|
2672
|
-
return scope;
|
|
2673
|
-
}
|
|
2674
|
-
function cycled(previous, delta, count2) {
|
|
2675
|
-
return (Math.min(previous, count2 - 1) + count2 + delta) % count2;
|
|
2676
|
-
}
|
|
2677
|
-
function browseReducer(state, action) {
|
|
2678
|
-
switch (action.type) {
|
|
2679
|
-
case "tabSelected": {
|
|
2680
|
-
return { ...state, tab: action.tab };
|
|
2681
|
-
}
|
|
2682
|
-
case "tabCycled": {
|
|
2683
|
-
return { ...state, tab: (state.tab + TABS.length + action.delta) % TABS.length };
|
|
2684
|
-
}
|
|
2685
|
-
case "subTabToggled": {
|
|
2686
|
-
return { ...state, authoredTab: state.authoredTab === "open" ? "merged" : "open" };
|
|
2687
|
-
}
|
|
2688
|
-
case "repoCursorMoved": {
|
|
2689
|
-
return {
|
|
2690
|
-
...state,
|
|
2691
|
-
repoCursors: {
|
|
2692
|
-
...state.repoCursors,
|
|
2693
|
-
[action.tab]: cycled(state.repoCursors[action.tab], action.delta, action.count)
|
|
2694
|
-
}
|
|
2695
|
-
};
|
|
2696
|
-
}
|
|
2697
|
-
case "rowCursorMoved": {
|
|
2698
|
-
return {
|
|
2699
|
-
...state,
|
|
2700
|
-
rowCursors: {
|
|
2701
|
-
...state.rowCursors,
|
|
2702
|
-
[action.tab]: cycled(state.rowCursors[action.tab], action.delta, action.count)
|
|
2703
|
-
}
|
|
2704
|
-
};
|
|
2705
|
-
}
|
|
2706
|
-
case "repoOpened": {
|
|
2707
|
-
return { ...state, scopes: { ...state.scopes, [action.tab]: { view: "detail", repo: action.repo } } };
|
|
2708
|
-
}
|
|
2709
|
-
case "pickerReturned": {
|
|
2710
|
-
return { ...state, scopes: { ...state.scopes, [action.tab]: { view: "list" } } };
|
|
2711
|
-
}
|
|
2712
|
-
case "groupingToggled": {
|
|
2713
|
-
return { ...state, grouped: { ...state.grouped, [action.tab]: !state.grouped[action.tab] } };
|
|
2714
|
-
}
|
|
2715
|
-
case "expandToggled": {
|
|
2716
|
-
return { ...state, expanded: { ...state.expanded, [action.tab]: !state.expanded[action.tab] } };
|
|
2717
|
-
}
|
|
2718
|
-
case "dataLoaded": {
|
|
2719
|
-
return {
|
|
2720
|
-
...state,
|
|
2721
|
-
scopes: {
|
|
2722
|
-
pending: dropVanishedRepo(state.scopes.pending, action.repos.pending),
|
|
2723
|
-
open: dropVanishedRepo(state.scopes.open, action.repos.open),
|
|
2724
|
-
review: dropVanishedRepo(state.scopes.review, action.repos.review),
|
|
2725
|
-
size: dropVanishedRepo(state.scopes.size, action.repos.size),
|
|
2726
|
-
comment: dropVanishedRepo(state.scopes.comment, action.repos.comment),
|
|
2727
|
-
merged: dropVanishedRepo(state.scopes.merged, action.repos.merged)
|
|
2728
|
-
}
|
|
2729
|
-
};
|
|
2730
|
-
}
|
|
2731
|
-
}
|
|
2732
|
-
return state;
|
|
2733
|
-
}
|
|
2734
|
-
|
|
2735
3091
|
// src/tui/components/TabBar.tsx
|
|
2736
3092
|
import { jsx as jsx8, jsxs as jsxs7 } from "@opentui/react/jsx-runtime";
|
|
2737
3093
|
function TabBar({ tab }) {
|
|
@@ -2746,23 +3102,22 @@ function TabBar({ tab }) {
|
|
|
2746
3102
|
label
|
|
2747
3103
|
)) });
|
|
2748
3104
|
}
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
3105
|
+
function SubTabBar({
|
|
3106
|
+
tabs,
|
|
3107
|
+
active,
|
|
3108
|
+
alerts = {}
|
|
3109
|
+
}) {
|
|
2754
3110
|
return /* @__PURE__ */ jsxs7("box", { flexDirection: "row", height: 1, paddingLeft: 1, marginBottom: 1, columnGap: 1, children: [
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
fg:
|
|
2760
|
-
|
|
2761
|
-
children:
|
|
2762
|
-
},
|
|
2763
|
-
|
|
2764
|
-
)
|
|
2765
|
-
/* @__PURE__ */ jsx8("text", { wrapMode: "none", fg: theme.dim, children: "t switches" })
|
|
3111
|
+
tabs.map(({ key, label }) => {
|
|
3112
|
+
const isActive = key === active;
|
|
3113
|
+
const bg = isActive ? theme.selectedBg : void 0;
|
|
3114
|
+
return /* @__PURE__ */ jsxs7("text", { wrapMode: "none", children: [
|
|
3115
|
+
/* @__PURE__ */ jsx8("span", { fg: isActive ? theme.accent : theme.muted, bg, children: " " }),
|
|
3116
|
+
alerts[key] === true && /* @__PURE__ */ jsx8("span", { fg: theme.accent, bg, children: "* " }),
|
|
3117
|
+
/* @__PURE__ */ jsx8("span", { fg: isActive ? theme.accent : theme.muted, bg, children: `${label} ` })
|
|
3118
|
+
] }, key);
|
|
3119
|
+
}),
|
|
3120
|
+
/* @__PURE__ */ jsx8("text", { wrapMode: "none", fg: theme.dim, children: "t/T switches" })
|
|
2766
3121
|
] });
|
|
2767
3122
|
}
|
|
2768
3123
|
|
|
@@ -2834,21 +3189,50 @@ function MainPanel({
|
|
|
2834
3189
|
load,
|
|
2835
3190
|
onRefClick
|
|
2836
3191
|
}) {
|
|
2837
|
-
return /* @__PURE__ */ jsx9("box", { flexGrow: 1, flexDirection: "column", marginTop: 1, children: views === null ? /* @__PURE__ */ jsx9(Placeholder, { error, loading, load }) : browse.tab === 0 ? /* @__PURE__ */
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
3192
|
+
return /* @__PURE__ */ jsx9("box", { flexGrow: 1, flexDirection: "column", marginTop: 1, children: views === null ? /* @__PURE__ */ jsx9(Placeholder, { error, loading, load }) : browse.tab === 0 ? /* @__PURE__ */ jsxs8("box", { flexGrow: 1, flexDirection: "column", children: [
|
|
3193
|
+
/* @__PURE__ */ jsx9(SubTabBar, { tabs: PENDING_SUB_TABS, active: browse.pendingTab, alerts: views.alerts }),
|
|
3194
|
+
browse.pendingTab === "pending" ? /* @__PURE__ */ jsx9(
|
|
3195
|
+
QueueTab,
|
|
3196
|
+
{
|
|
3197
|
+
prompt: "Select a repository and press enter to open its review queue.",
|
|
3198
|
+
repos: views.pendingRepos,
|
|
3199
|
+
scope: views.pendingScope,
|
|
3200
|
+
view: views.pending,
|
|
3201
|
+
repoCursor: browse.repoCursors.pending,
|
|
3202
|
+
rowCursor: browse.rowCursors.pending,
|
|
3203
|
+
grouped: browse.grouped.pending,
|
|
3204
|
+
warning,
|
|
3205
|
+
onRefClick
|
|
3206
|
+
}
|
|
3207
|
+
) : browse.pendingTab === "reviewed" ? /* @__PURE__ */ jsx9(
|
|
3208
|
+
QueueTab,
|
|
3209
|
+
{
|
|
3210
|
+
prompt: "Select a repository and press enter to list the open PRs you reviewed.",
|
|
3211
|
+
repos: views.reviewedRepos,
|
|
3212
|
+
scope: views.reviewedScope,
|
|
3213
|
+
view: views.reviewed,
|
|
3214
|
+
repoCursor: browse.repoCursors.reviewed,
|
|
3215
|
+
rowCursor: browse.rowCursors.reviewed,
|
|
3216
|
+
grouped: browse.grouped.reviewed,
|
|
3217
|
+
warning,
|
|
3218
|
+
onRefClick
|
|
3219
|
+
}
|
|
3220
|
+
) : /* @__PURE__ */ jsx9(
|
|
3221
|
+
QueueTab,
|
|
3222
|
+
{
|
|
3223
|
+
prompt: "Select a repository and press enter to open its mention inbox.",
|
|
3224
|
+
repos: views.mentionsRepos,
|
|
3225
|
+
scope: views.mentionsScope,
|
|
3226
|
+
view: views.mentions,
|
|
3227
|
+
repoCursor: browse.repoCursors.mentions,
|
|
3228
|
+
rowCursor: browse.rowCursors.mentions,
|
|
3229
|
+
grouped: browse.grouped.mentions,
|
|
3230
|
+
warning,
|
|
3231
|
+
onRefClick
|
|
3232
|
+
}
|
|
3233
|
+
)
|
|
3234
|
+
] }) : browse.tab === 1 ? /* @__PURE__ */ jsxs8("box", { flexGrow: 1, flexDirection: "column", children: [
|
|
3235
|
+
/* @__PURE__ */ jsx9(SubTabBar, { tabs: AUTHORED_SUB_TABS, active: browse.authoredTab }),
|
|
2852
3236
|
browse.authoredTab === "open" ? /* @__PURE__ */ jsx9(
|
|
2853
3237
|
QueueTab,
|
|
2854
3238
|
{
|
|
@@ -3405,23 +3789,24 @@ import { useRenderer as useRenderer2 } from "@opentui/react";
|
|
|
3405
3789
|
import { homedir as homedir2 } from "node:os";
|
|
3406
3790
|
|
|
3407
3791
|
// src/tui/data/export.ts
|
|
3408
|
-
import { join as
|
|
3792
|
+
import { join as join6 } from "node:path";
|
|
3409
3793
|
|
|
3410
3794
|
// src/github.ts
|
|
3411
3795
|
import { execFile } from "node:child_process";
|
|
3412
3796
|
import { createHash } from "node:crypto";
|
|
3413
3797
|
import { statSync } from "node:fs";
|
|
3414
|
-
import { join as
|
|
3798
|
+
import { join as join5, resolve } from "node:path";
|
|
3415
3799
|
import { promisify } from "node:util";
|
|
3416
3800
|
var execFileAsync = promisify(execFile);
|
|
3417
3801
|
var API_BASE = "https://api.github.com";
|
|
3802
|
+
var SEARCH_LIMIT = 1e3;
|
|
3418
3803
|
var token;
|
|
3419
3804
|
var ghBinary = "gh";
|
|
3420
3805
|
function resolveDebugBinary(input) {
|
|
3421
3806
|
const resolved = resolve(input);
|
|
3422
3807
|
const stats = statSync(resolved, { throwIfNoEntry: false });
|
|
3423
3808
|
if (stats?.isDirectory()) {
|
|
3424
|
-
const binary =
|
|
3809
|
+
const binary = join5(resolved, "gh");
|
|
3425
3810
|
if (!statSync(binary, { throwIfNoEntry: false })?.isFile()) {
|
|
3426
3811
|
throw new CliError(`--debug directory "${input}" does not contain a gh executable`);
|
|
3427
3812
|
}
|
|
@@ -3546,27 +3931,41 @@ async function resolveRepos(repos) {
|
|
|
3546
3931
|
}
|
|
3547
3932
|
return resolved;
|
|
3548
3933
|
}
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3934
|
+
function excludesOwnPrs(mode) {
|
|
3935
|
+
return mode === "requested" || mode === "reviewed";
|
|
3936
|
+
}
|
|
3937
|
+
function isMentionQuery(query) {
|
|
3938
|
+
return query === "mentioned" || query === "mentionedText";
|
|
3939
|
+
}
|
|
3940
|
+
function mentionTerm(user) {
|
|
3941
|
+
return `"@${user}"`;
|
|
3942
|
+
}
|
|
3943
|
+
async function searchPrsViaApi({ user, sinceIso, repos, includeDrafts, query }) {
|
|
3944
|
+
const terms = ["type:pr"];
|
|
3945
|
+
if (query === "mentioned") {
|
|
3946
|
+
terms.push(`mentions:${user}`, `updated:>=${sinceIso}`);
|
|
3947
|
+
} else if (query === "mentionedText") {
|
|
3948
|
+
terms.push(mentionTerm(user), `involves:${user}`, `updated:>=${sinceIso}`);
|
|
3949
|
+
} else {
|
|
3950
|
+
const qualifier = { requested: "review-requested", reviewed: "reviewed-by", authored: "author" }[query];
|
|
3951
|
+
terms.push(`${qualifier}:${user}`, `created:>=${sinceIso}`);
|
|
3952
|
+
}
|
|
3953
|
+
const mode = isMentionQuery(query) ? "mentioned" : query;
|
|
3954
|
+
if (excludesOwnPrs(mode)) {
|
|
3557
3955
|
terms.push(`-author:${user}`);
|
|
3558
3956
|
}
|
|
3559
|
-
if (!includeDrafts) {
|
|
3957
|
+
if (!includeDrafts && mode !== "mentioned") {
|
|
3560
3958
|
terms.push("draft:false");
|
|
3561
3959
|
}
|
|
3562
3960
|
for (const repo of repos) {
|
|
3563
3961
|
terms.push(`repo:${repo}`);
|
|
3564
3962
|
}
|
|
3565
|
-
const
|
|
3963
|
+
const encoded = encodeURIComponent(terms.join(" "));
|
|
3964
|
+
const sort = mode === "mentioned" ? "&sort=updated&order=desc" : "";
|
|
3566
3965
|
const items = [];
|
|
3567
|
-
for (let page = 1; page <=
|
|
3966
|
+
for (let page = 1; page <= SEARCH_LIMIT / 100; page++) {
|
|
3568
3967
|
const result = await api(
|
|
3569
|
-
`/search/issues?q=${
|
|
3968
|
+
`/search/issues?q=${encoded}${sort}&per_page=100&page=${page}`
|
|
3570
3969
|
);
|
|
3571
3970
|
items.push(...result.items);
|
|
3572
3971
|
if (result.items.length === 0 || items.length >= result.total_count) {
|
|
@@ -3580,39 +3979,60 @@ async function searchPrsViaApi({ user, sinceIso, repos, includeDrafts, mode }) {
|
|
|
3580
3979
|
title: item.title,
|
|
3581
3980
|
url: item.html_url,
|
|
3582
3981
|
createdAt: item.created_at,
|
|
3982
|
+
updatedAt: item.updated_at,
|
|
3583
3983
|
isDraft: item.draft,
|
|
3584
3984
|
state: item.state
|
|
3585
3985
|
};
|
|
3586
3986
|
});
|
|
3587
3987
|
}
|
|
3588
|
-
async function searchPrs({ user, sinceIso, repos, includeDrafts, mode }) {
|
|
3988
|
+
async function searchPrs({ user, sinceIso, repos, includeDrafts, mode }) {
|
|
3989
|
+
const queries = mode === "mentioned" ? ["mentioned", "mentionedText"] : [mode];
|
|
3990
|
+
const results = await Promise.all(
|
|
3991
|
+
queries.map((query) => {
|
|
3992
|
+
return searchOnce({ user, sinceIso, repos, includeDrafts, query });
|
|
3993
|
+
})
|
|
3994
|
+
);
|
|
3995
|
+
const capped = results.some((items) => items.length >= SEARCH_LIMIT);
|
|
3996
|
+
if (results.length === 1) {
|
|
3997
|
+
return { items: results[0], capped };
|
|
3998
|
+
}
|
|
3999
|
+
const byRef = /* @__PURE__ */ new Map();
|
|
4000
|
+
for (const item of results.flat()) {
|
|
4001
|
+
const ref = `${item.repository.nameWithOwner}#${item.number}`;
|
|
4002
|
+
const known = byRef.get(ref);
|
|
4003
|
+
if (known === void 0 || item.updatedAt > known.updatedAt) {
|
|
4004
|
+
byRef.set(ref, item);
|
|
4005
|
+
}
|
|
4006
|
+
}
|
|
4007
|
+
return { items: [...byRef.values()], capped };
|
|
4008
|
+
}
|
|
4009
|
+
async function searchOnce({ user, sinceIso, repos, includeDrafts, query }) {
|
|
3589
4010
|
if (token) {
|
|
3590
|
-
return searchPrsViaApi({ user, sinceIso, repos, includeDrafts,
|
|
4011
|
+
return searchPrsViaApi({ user, sinceIso, repos, includeDrafts, query });
|
|
3591
4012
|
}
|
|
3592
|
-
const
|
|
3593
|
-
|
|
3594
|
-
reviewed: "--reviewed-by",
|
|
3595
|
-
|
|
3596
|
-
|
|
4013
|
+
const mode = isMentionQuery(query) ? "mentioned" : query;
|
|
4014
|
+
const selection = query === "mentioned" ? ["--mentions", user, "--updated", `>=${sinceIso}`, "--sort", "updated", "--order", "desc"] : query === "mentionedText" ? [mentionTerm(user), "--involves", user, "--updated", `>=${sinceIso}`, "--sort", "updated", "--order", "desc"] : [
|
|
4015
|
+
{ requested: "--review-requested", reviewed: "--reviewed-by", authored: "--author" }[query],
|
|
4016
|
+
user,
|
|
4017
|
+
"--created",
|
|
4018
|
+
`>=${sinceIso}`
|
|
4019
|
+
];
|
|
3597
4020
|
const args = [
|
|
3598
4021
|
"search",
|
|
3599
4022
|
"prs",
|
|
3600
|
-
|
|
3601
|
-
user,
|
|
3602
|
-
"--created",
|
|
3603
|
-
`>=${sinceIso}`,
|
|
4023
|
+
...selection,
|
|
3604
4024
|
"--limit",
|
|
3605
|
-
|
|
4025
|
+
String(SEARCH_LIMIT),
|
|
3606
4026
|
"--json",
|
|
3607
|
-
"number,repository,title,url,createdAt,isDraft,state"
|
|
4027
|
+
"number,repository,title,url,createdAt,updatedAt,isDraft,state"
|
|
3608
4028
|
];
|
|
3609
|
-
if (!includeDrafts) {
|
|
4029
|
+
if (!includeDrafts && mode !== "mentioned") {
|
|
3610
4030
|
args.push("--draft=false");
|
|
3611
4031
|
}
|
|
3612
4032
|
for (const repo of repos) {
|
|
3613
4033
|
args.push("--repo", repo);
|
|
3614
4034
|
}
|
|
3615
|
-
if (mode
|
|
4035
|
+
if (excludesOwnPrs(mode)) {
|
|
3616
4036
|
args.push("--", `-author:${user}`);
|
|
3617
4037
|
}
|
|
3618
4038
|
return JSON.parse(await gh(args));
|
|
@@ -3680,6 +4100,125 @@ async function fetchPrSizes(prs) {
|
|
|
3680
4100
|
const data = await runGraphql(query);
|
|
3681
4101
|
return prs.map((pr, i) => data[`pr${i}`]?.pullRequest ?? null);
|
|
3682
4102
|
}
|
|
4103
|
+
var MENTION_TEXT_FRAGMENT = `
|
|
4104
|
+
fragment MentionText on Comment {
|
|
4105
|
+
id
|
|
4106
|
+
body
|
|
4107
|
+
createdAt
|
|
4108
|
+
publishedAt
|
|
4109
|
+
lastEditedAt
|
|
4110
|
+
author { login }
|
|
4111
|
+
}`;
|
|
4112
|
+
var PAGE_INFO = "pageInfo { hasNextPage endCursor }";
|
|
4113
|
+
var COMMENTS_PAGE = 100;
|
|
4114
|
+
var REVIEWS_PAGE = 50;
|
|
4115
|
+
var REVIEW_COMMENTS_PAGE = 20;
|
|
4116
|
+
function textPage(field, size, after) {
|
|
4117
|
+
const cursor = after === void 0 ? "" : `, after: ${JSON.stringify(after)}`;
|
|
4118
|
+
return `${field}(first: ${size}${cursor}) { ${PAGE_INFO} nodes { ...MentionText } }`;
|
|
4119
|
+
}
|
|
4120
|
+
function reviewsPage(after) {
|
|
4121
|
+
const cursor = after === void 0 ? "" : `, after: ${JSON.stringify(after)}`;
|
|
4122
|
+
return `reviews(first: ${REVIEWS_PAGE}${cursor}) {
|
|
4123
|
+
${PAGE_INFO}
|
|
4124
|
+
nodes {
|
|
4125
|
+
id
|
|
4126
|
+
body
|
|
4127
|
+
submittedAt
|
|
4128
|
+
lastEditedAt
|
|
4129
|
+
author { login }
|
|
4130
|
+
${textPage("comments", REVIEW_COMMENTS_PAGE)}
|
|
4131
|
+
}
|
|
4132
|
+
}`;
|
|
4133
|
+
}
|
|
4134
|
+
function pullRequestSelection(pr, selection) {
|
|
4135
|
+
const [owner, name] = pr.repo.split("/");
|
|
4136
|
+
return `repository(owner: ${JSON.stringify(owner)}, name: ${JSON.stringify(name)}) {
|
|
4137
|
+
pullRequest(number: ${pr.number}) { ${selection} }
|
|
4138
|
+
}`;
|
|
4139
|
+
}
|
|
4140
|
+
async function completeMentionPages(pr, raw) {
|
|
4141
|
+
for (; ; ) {
|
|
4142
|
+
const parts = [];
|
|
4143
|
+
if (raw.comments.pageInfo.hasNextPage) {
|
|
4144
|
+
const after = raw.comments.pageInfo.endCursor ?? void 0;
|
|
4145
|
+
parts.push(`comments: ${pullRequestSelection(pr, textPage("comments", COMMENTS_PAGE, after))}`);
|
|
4146
|
+
}
|
|
4147
|
+
if (raw.reviews.pageInfo.hasNextPage) {
|
|
4148
|
+
parts.push(`reviews: ${pullRequestSelection(pr, reviewsPage(raw.reviews.pageInfo.endCursor ?? void 0))}`);
|
|
4149
|
+
}
|
|
4150
|
+
const incompleteReviews = [];
|
|
4151
|
+
for (const [i, review] of raw.reviews.nodes.entries()) {
|
|
4152
|
+
if (review?.comments.pageInfo.hasNextPage) {
|
|
4153
|
+
const after = review.comments.pageInfo.endCursor ?? void 0;
|
|
4154
|
+
const page = textPage("comments", COMMENTS_PAGE, after);
|
|
4155
|
+
incompleteReviews.push(i);
|
|
4156
|
+
parts.push(`review${i}: node(id: ${JSON.stringify(review.id)}) { ... on PullRequestReview { ${page} } }`);
|
|
4157
|
+
}
|
|
4158
|
+
}
|
|
4159
|
+
if (parts.length === 0) {
|
|
4160
|
+
return true;
|
|
4161
|
+
}
|
|
4162
|
+
const query = `query {${parts.join("\n")}}
|
|
4163
|
+
${MENTION_TEXT_FRAGMENT}`;
|
|
4164
|
+
const data = await runGraphql(query);
|
|
4165
|
+
const pages = [];
|
|
4166
|
+
if (raw.comments.pageInfo.hasNextPage) {
|
|
4167
|
+
pages.push([raw.comments, data.comments?.pullRequest?.comments]);
|
|
4168
|
+
}
|
|
4169
|
+
if (raw.reviews.pageInfo.hasNextPage) {
|
|
4170
|
+
pages.push([raw.reviews, data.reviews?.pullRequest?.reviews]);
|
|
4171
|
+
}
|
|
4172
|
+
for (const i of incompleteReviews) {
|
|
4173
|
+
const review = raw.reviews.nodes[i];
|
|
4174
|
+
if (review != null) {
|
|
4175
|
+
pages.push([review.comments, data[`review${i}`]?.comments]);
|
|
4176
|
+
}
|
|
4177
|
+
}
|
|
4178
|
+
if (pages.some(([, next]) => next === void 0)) {
|
|
4179
|
+
return false;
|
|
4180
|
+
}
|
|
4181
|
+
for (const [list, next] of pages) {
|
|
4182
|
+
appendPage(list, next);
|
|
4183
|
+
}
|
|
4184
|
+
}
|
|
4185
|
+
}
|
|
4186
|
+
function appendPage(list, next) {
|
|
4187
|
+
if (next !== void 0) {
|
|
4188
|
+
list.nodes.push(...next.nodes);
|
|
4189
|
+
list.pageInfo = next.pageInfo;
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4192
|
+
function present(nodes) {
|
|
4193
|
+
return nodes.filter((node) => node !== null);
|
|
4194
|
+
}
|
|
4195
|
+
async function fetchPrMentionDetails(prs) {
|
|
4196
|
+
const parts = prs.map((pr, i) => {
|
|
4197
|
+
const selection = `...MentionText ${textPage("comments", COMMENTS_PAGE)} ${reviewsPage()}`;
|
|
4198
|
+
return `pr${i}: ${pullRequestSelection(pr, selection)}`;
|
|
4199
|
+
});
|
|
4200
|
+
const query = `query {${parts.join("\n")}}
|
|
4201
|
+
${MENTION_TEXT_FRAGMENT}`;
|
|
4202
|
+
const data = await runGraphql(query);
|
|
4203
|
+
const raws = prs.map((pr, i) => data[`pr${i}`]?.pullRequest ?? null);
|
|
4204
|
+
for (const [i, raw] of raws.entries()) {
|
|
4205
|
+
if (raw !== null && !await completeMentionPages(prs[i], raw)) {
|
|
4206
|
+
raws[i] = null;
|
|
4207
|
+
}
|
|
4208
|
+
}
|
|
4209
|
+
return raws.map((raw) => {
|
|
4210
|
+
if (raw === null) {
|
|
4211
|
+
return null;
|
|
4212
|
+
}
|
|
4213
|
+
return {
|
|
4214
|
+
...raw,
|
|
4215
|
+
comments: present(raw.comments.nodes),
|
|
4216
|
+
reviews: present(raw.reviews.nodes).map((review) => {
|
|
4217
|
+
return { ...review, comments: present(review.comments.nodes) };
|
|
4218
|
+
})
|
|
4219
|
+
};
|
|
4220
|
+
});
|
|
4221
|
+
}
|
|
3683
4222
|
|
|
3684
4223
|
// src/data.ts
|
|
3685
4224
|
var BATCH_SIZE = 25;
|
|
@@ -3706,7 +4245,7 @@ function createLimiter(maxConcurrent) {
|
|
|
3706
4245
|
};
|
|
3707
4246
|
}
|
|
3708
4247
|
var limit = createLimiter(MAX_CONCURRENT_BATCHES);
|
|
3709
|
-
async function fetchMissing(misses, fetchBatch, found, cache, onBatchDone) {
|
|
4248
|
+
async function fetchMissing(misses, fetchBatch, found, cache, onBatchDone, cacheable = (pr) => pr.state !== "open") {
|
|
3710
4249
|
const batches = [];
|
|
3711
4250
|
for (let offset = 0; offset < misses.length; offset += BATCH_SIZE) {
|
|
3712
4251
|
batches.push(misses.slice(offset, offset + BATCH_SIZE));
|
|
@@ -3720,7 +4259,7 @@ async function fetchMissing(misses, fetchBatch, found, cache, onBatchDone) {
|
|
|
3720
4259
|
const details = detailsList[i];
|
|
3721
4260
|
const key = prKey(pr.repo, pr.number);
|
|
3722
4261
|
found.set(key, details);
|
|
3723
|
-
if (details !== null && pr
|
|
4262
|
+
if (details !== null && cacheable(pr)) {
|
|
3724
4263
|
cache.set(key, details);
|
|
3725
4264
|
}
|
|
3726
4265
|
}
|
|
@@ -3888,12 +4427,153 @@ async function fetchSizeRaw(prs, onProgress, options = {}) {
|
|
|
3888
4427
|
}
|
|
3889
4428
|
return { sizes, cacheHits };
|
|
3890
4429
|
}
|
|
4430
|
+
function collectMentionedPrs(mentioned) {
|
|
4431
|
+
return mentioned.map((item) => {
|
|
4432
|
+
return {
|
|
4433
|
+
repo: item.repository.nameWithOwner,
|
|
4434
|
+
number: item.number,
|
|
4435
|
+
title: item.title,
|
|
4436
|
+
url: item.url,
|
|
4437
|
+
state: item.state,
|
|
4438
|
+
createdAt: new Date(item.createdAt),
|
|
4439
|
+
updatedAt: new Date(item.updatedAt)
|
|
4440
|
+
};
|
|
4441
|
+
});
|
|
4442
|
+
}
|
|
4443
|
+
function mentionPattern(user) {
|
|
4444
|
+
const escaped = user.replaceAll(/[$()*+.?[\\\]^{|}]/g, String.raw`\$&`);
|
|
4445
|
+
return new RegExp(String.raw`(?<![\w/-])@${escaped}(?![\w-])`, "i");
|
|
4446
|
+
}
|
|
4447
|
+
function latestOf(...times) {
|
|
4448
|
+
let latest = Number.NEGATIVE_INFINITY;
|
|
4449
|
+
for (const time of times) {
|
|
4450
|
+
if (time !== null) {
|
|
4451
|
+
latest = Math.max(latest, new Date(time).getTime());
|
|
4452
|
+
}
|
|
4453
|
+
}
|
|
4454
|
+
return new Date(latest);
|
|
4455
|
+
}
|
|
4456
|
+
function findMentions(details, user) {
|
|
4457
|
+
const pattern = mentionPattern(user);
|
|
4458
|
+
const own = user.toLowerCase();
|
|
4459
|
+
const sources = [];
|
|
4460
|
+
const addText = (text, floor = null) => {
|
|
4461
|
+
sources.push({
|
|
4462
|
+
id: text.id,
|
|
4463
|
+
body: text.body,
|
|
4464
|
+
at: latestOf(text.publishedAt ?? text.createdAt, text.lastEditedAt, floor),
|
|
4465
|
+
login: text.author?.login ?? null
|
|
4466
|
+
});
|
|
4467
|
+
};
|
|
4468
|
+
addText(details);
|
|
4469
|
+
for (const comment of details.comments) {
|
|
4470
|
+
addText(comment);
|
|
4471
|
+
}
|
|
4472
|
+
for (const review of details.reviews) {
|
|
4473
|
+
if (review.submittedAt === null) {
|
|
4474
|
+
continue;
|
|
4475
|
+
}
|
|
4476
|
+
sources.push({
|
|
4477
|
+
id: review.id,
|
|
4478
|
+
body: review.body,
|
|
4479
|
+
at: latestOf(review.submittedAt, review.lastEditedAt),
|
|
4480
|
+
login: review.author?.login ?? null
|
|
4481
|
+
});
|
|
4482
|
+
for (const comment of review.comments) {
|
|
4483
|
+
addText(comment, review.submittedAt);
|
|
4484
|
+
}
|
|
4485
|
+
}
|
|
4486
|
+
const mentions = [];
|
|
4487
|
+
for (const source of sources) {
|
|
4488
|
+
if (source.login?.toLowerCase() !== own && pattern.test(source.body)) {
|
|
4489
|
+
mentions.push({ id: source.id, at: source.at });
|
|
4490
|
+
}
|
|
4491
|
+
}
|
|
4492
|
+
return mentions;
|
|
4493
|
+
}
|
|
4494
|
+
async function fetchMentionsRaw(prs, user, onProgress, options = {}) {
|
|
4495
|
+
const cache = new PrCache("mentions");
|
|
4496
|
+
const found = /* @__PURE__ */ new Map();
|
|
4497
|
+
const misses = [];
|
|
4498
|
+
const login = user.toLowerCase();
|
|
4499
|
+
for (const pr of prs) {
|
|
4500
|
+
const key = prKey(pr.repo, pr.number);
|
|
4501
|
+
const cached = options.bypassCache === true ? void 0 : cache.get(key);
|
|
4502
|
+
if (cached?.user === login && cached.updatedAt === pr.updatedAt.toISOString()) {
|
|
4503
|
+
found.set(key, cached);
|
|
4504
|
+
} else {
|
|
4505
|
+
misses.push(pr);
|
|
4506
|
+
}
|
|
4507
|
+
}
|
|
4508
|
+
const cacheHits = prs.length - misses.length;
|
|
4509
|
+
onProgress?.(cacheHits, prs.length);
|
|
4510
|
+
const fetchBatch = async (batch) => {
|
|
4511
|
+
const detailsList = await fetchPrMentionDetails(batch);
|
|
4512
|
+
return batch.map((pr, i) => {
|
|
4513
|
+
const details = detailsList[i];
|
|
4514
|
+
if (details === null) {
|
|
4515
|
+
return null;
|
|
4516
|
+
}
|
|
4517
|
+
return {
|
|
4518
|
+
user: login,
|
|
4519
|
+
updatedAt: pr.updatedAt.toISOString(),
|
|
4520
|
+
mentions: findMentions(details, user).map(({ id, at }) => {
|
|
4521
|
+
return { id, at: at.toISOString() };
|
|
4522
|
+
})
|
|
4523
|
+
};
|
|
4524
|
+
});
|
|
4525
|
+
};
|
|
4526
|
+
await fetchMissing(
|
|
4527
|
+
misses,
|
|
4528
|
+
fetchBatch,
|
|
4529
|
+
found,
|
|
4530
|
+
cache,
|
|
4531
|
+
(completed) => {
|
|
4532
|
+
onProgress?.(cacheHits + completed, prs.length);
|
|
4533
|
+
},
|
|
4534
|
+
() => true
|
|
4535
|
+
);
|
|
4536
|
+
cache.save();
|
|
4537
|
+
const mentions = [];
|
|
4538
|
+
const since = options.since?.getTime() ?? Number.NEGATIVE_INFINITY;
|
|
4539
|
+
for (const pr of prs) {
|
|
4540
|
+
const entry = found.get(prKey(pr.repo, pr.number));
|
|
4541
|
+
if (entry == null) {
|
|
4542
|
+
mentions.push({ pr, mentions: null, earlier: [] });
|
|
4543
|
+
continue;
|
|
4544
|
+
}
|
|
4545
|
+
const inWindow = [];
|
|
4546
|
+
const earlier = [];
|
|
4547
|
+
for (const { id, at } of entry.mentions) {
|
|
4548
|
+
const time = new Date(at);
|
|
4549
|
+
if (time.getTime() >= since) {
|
|
4550
|
+
inWindow.push({ id, at: time });
|
|
4551
|
+
} else {
|
|
4552
|
+
earlier.push(id);
|
|
4553
|
+
}
|
|
4554
|
+
}
|
|
4555
|
+
if (inWindow.length > 0) {
|
|
4556
|
+
mentions.push({ pr, mentions: inWindow, earlier });
|
|
4557
|
+
}
|
|
4558
|
+
}
|
|
4559
|
+
return { mentions, cacheHits };
|
|
4560
|
+
}
|
|
3891
4561
|
|
|
3892
4562
|
// src/tui/data/load.ts
|
|
3893
4563
|
function reviveRawData(data) {
|
|
4564
|
+
const mentions = data.mentions ?? null;
|
|
3894
4565
|
return {
|
|
3895
4566
|
...data,
|
|
3896
4567
|
fetchedAt: new Date(data.fetchedAt),
|
|
4568
|
+
mentions: mentions === null ? null : mentions.map((entry) => {
|
|
4569
|
+
return {
|
|
4570
|
+
pr: { ...entry.pr, createdAt: new Date(entry.pr.createdAt), updatedAt: new Date(entry.pr.updatedAt) },
|
|
4571
|
+
mentions: entry.mentions === null ? null : entry.mentions.map((mention) => {
|
|
4572
|
+
return { id: mention.id, at: new Date(mention.at) };
|
|
4573
|
+
}),
|
|
4574
|
+
earlier: entry.earlier ?? []
|
|
4575
|
+
};
|
|
4576
|
+
}),
|
|
3897
4577
|
reviewResults: data.reviewResults.map((result) => {
|
|
3898
4578
|
const pr = { ...result.pr, createdAt: new Date(result.pr.createdAt) };
|
|
3899
4579
|
if (result.kind === "pending") {
|
|
@@ -3920,6 +4600,19 @@ function reviveRawData(data) {
|
|
|
3920
4600
|
})
|
|
3921
4601
|
};
|
|
3922
4602
|
}
|
|
4603
|
+
function cutMentions(entries, cutoff) {
|
|
4604
|
+
return entries.flatMap((entry) => {
|
|
4605
|
+
if (entry.mentions === null) {
|
|
4606
|
+
return entry.pr.updatedAt >= cutoff ? [entry] : [];
|
|
4607
|
+
}
|
|
4608
|
+
const mentions = entry.mentions.filter((mention) => mention.at >= cutoff);
|
|
4609
|
+
if (mentions.length === 0) {
|
|
4610
|
+
return [];
|
|
4611
|
+
}
|
|
4612
|
+
const cut = entry.mentions.filter((mention) => mention.at < cutoff).map((mention) => mention.id);
|
|
4613
|
+
return [{ ...entry, mentions, earlier: [...entry.earlier, ...cut] }];
|
|
4614
|
+
});
|
|
4615
|
+
}
|
|
3923
4616
|
function loadSnapshot(options) {
|
|
3924
4617
|
const stored = readCacheFile("snapshot");
|
|
3925
4618
|
if (stored?.params === void 0) {
|
|
@@ -3952,11 +4645,13 @@ function loadSnapshot(options) {
|
|
|
3952
4645
|
const cutoff = new Date(sinceIso);
|
|
3953
4646
|
const reviewResults = data.reviewResults.filter((result) => result.pr.createdAt >= cutoff);
|
|
3954
4647
|
const sizes = data.sizes.filter((entry) => entry.pr.createdAt >= cutoff);
|
|
4648
|
+
const mentions = data.mentions === null ? null : cutMentions(data.mentions, cutoff);
|
|
3955
4649
|
return {
|
|
3956
4650
|
...data,
|
|
3957
4651
|
sinceIso,
|
|
3958
4652
|
reviewResults,
|
|
3959
4653
|
sizes,
|
|
4654
|
+
mentions,
|
|
3960
4655
|
/**
|
|
3961
4656
|
* The creation dates of inaccessible authored PRs are unknown, so the
|
|
3962
4657
|
* inaccessible count carries over unchanged.
|
|
@@ -3974,27 +4669,30 @@ function saveSnapshot(options, data) {
|
|
|
3974
4669
|
};
|
|
3975
4670
|
writeCacheFile("snapshot", { params, data });
|
|
3976
4671
|
}
|
|
3977
|
-
async function loadData(options, onPhase, { bypassCache = false } = {}) {
|
|
4672
|
+
async function loadData(options, onPhase, { bypassCache = false, mentions = false } = {}) {
|
|
3978
4673
|
const sinceIso = parseSince(options.since).toISOString().slice(0, 10);
|
|
3979
4674
|
onPhase({ phase: "search" });
|
|
3980
4675
|
const repoNames = options.repos.split(",").map((name) => name.trim()).filter((name) => name !== "");
|
|
3981
4676
|
const [user, repos] = await Promise.all([resolveUser(options.user, bypassCache), resolveRepos(repoNames)]);
|
|
3982
4677
|
const includeDrafts = options.includeDrafts;
|
|
3983
|
-
const [requested, reviewed, authored] = await Promise.all([
|
|
4678
|
+
const [requested, reviewed, authored, mentioned] = await Promise.all([
|
|
3984
4679
|
searchPrs({ user, sinceIso, repos, includeDrafts, mode: "requested" }),
|
|
3985
4680
|
searchPrs({ user, sinceIso, repos, includeDrafts, mode: "reviewed" }),
|
|
3986
|
-
searchPrs({ user, sinceIso, repos, includeDrafts, mode: "authored" })
|
|
4681
|
+
searchPrs({ user, sinceIso, repos, includeDrafts, mode: "authored" }),
|
|
4682
|
+
mentions ? searchPrs({ user, sinceIso, repos, includeDrafts, mode: "mentioned" }) : null
|
|
3987
4683
|
]);
|
|
3988
|
-
const reviewPrs = collectReviewPrs(requested, reviewed);
|
|
3989
|
-
const authoredPrs = collectAuthoredPrs(authored);
|
|
3990
|
-
const
|
|
3991
|
-
const
|
|
4684
|
+
const reviewPrs = collectReviewPrs(requested.items, reviewed.items);
|
|
4685
|
+
const authoredPrs = collectAuthoredPrs(authored.items);
|
|
4686
|
+
const mentionedPrs = mentioned === null ? null : collectMentionedPrs(mentioned.items);
|
|
4687
|
+
const searchCapped = [requested, reviewed, authored, mentioned].some((result) => result?.capped === true);
|
|
4688
|
+
const progress = { review: 0, sizes: 0, mentions: 0 };
|
|
4689
|
+
const total = reviewPrs.length + authoredPrs.length + (mentionedPrs?.length ?? 0);
|
|
3992
4690
|
const report = () => {
|
|
3993
|
-
onPhase({ phase: "details", done: progress.review + progress.sizes, total });
|
|
4691
|
+
onPhase({ phase: "details", done: progress.review + progress.sizes + progress.mentions, total });
|
|
3994
4692
|
};
|
|
3995
4693
|
report();
|
|
3996
4694
|
const countedStates = options.reviewTypes === "" ? void 0 : parseReviewTypes(options.reviewTypes);
|
|
3997
|
-
const [review, size] = await Promise.all([
|
|
4695
|
+
const [review, size, mention] = await Promise.all([
|
|
3998
4696
|
reviewPrs.length === 0 ? { results: [], cacheHits: 0 } : fetchReviewRaw(
|
|
3999
4697
|
reviewPrs,
|
|
4000
4698
|
user,
|
|
@@ -4011,6 +4709,18 @@ async function loadData(options, onPhase, { bypassCache = false } = {}) {
|
|
|
4011
4709
|
report();
|
|
4012
4710
|
},
|
|
4013
4711
|
{ bypassCache }
|
|
4712
|
+
),
|
|
4713
|
+
mentionedPrs === null || mentionedPrs.length === 0 ? { mentions: [], cacheHits: 0 } : fetchMentionsRaw(
|
|
4714
|
+
mentionedPrs,
|
|
4715
|
+
user,
|
|
4716
|
+
(done) => {
|
|
4717
|
+
progress.mentions = done;
|
|
4718
|
+
report();
|
|
4719
|
+
},
|
|
4720
|
+
{
|
|
4721
|
+
bypassCache,
|
|
4722
|
+
since: new Date(sinceIso)
|
|
4723
|
+
}
|
|
4014
4724
|
)
|
|
4015
4725
|
]);
|
|
4016
4726
|
const data = {
|
|
@@ -4020,7 +4730,8 @@ async function loadData(options, onPhase, { bypassCache = false } = {}) {
|
|
|
4020
4730
|
reviewResults: review.results,
|
|
4021
4731
|
sizes: size.sizes,
|
|
4022
4732
|
authoredTotal: authoredPrs.length,
|
|
4023
|
-
|
|
4733
|
+
mentions: mentionedPrs === null ? null : mention.mentions,
|
|
4734
|
+
searchCapped,
|
|
4024
4735
|
fetchedAt: /* @__PURE__ */ new Date()
|
|
4025
4736
|
};
|
|
4026
4737
|
saveSnapshot(options, data);
|
|
@@ -4203,7 +4914,7 @@ function buildStatsReport(raw, options) {
|
|
|
4203
4914
|
};
|
|
4204
4915
|
}
|
|
4205
4916
|
function exportFile() {
|
|
4206
|
-
return
|
|
4917
|
+
return join6(process.cwd(), "pr-stats.json");
|
|
4207
4918
|
}
|
|
4208
4919
|
function exportStatsFile(raw, options) {
|
|
4209
4920
|
writeFileAtomic(exportFile(), `${JSON.stringify(buildStatsReport(raw, options), null, 2)}
|
|
@@ -4273,6 +4984,12 @@ var SETTINGS = [
|
|
|
4273
4984
|
label: "Desktop notifications",
|
|
4274
4985
|
hint: "notifies you when a load finds a PR newly awaiting your review or a review re-requested from you"
|
|
4275
4986
|
},
|
|
4987
|
+
{
|
|
4988
|
+
key: "notifyMentions",
|
|
4989
|
+
section: "Notifications",
|
|
4990
|
+
label: "Mention notifications",
|
|
4991
|
+
hint: "also notifies you when someone @-mentions you on a PR, your own PRs included \xB7 needs desktop notifications and mention tracking on"
|
|
4992
|
+
},
|
|
4276
4993
|
{
|
|
4277
4994
|
key: "notifyChannel",
|
|
4278
4995
|
section: "Notifications",
|
|
@@ -4291,9 +5008,15 @@ var SETTINGS = [
|
|
|
4291
5008
|
label: "Copy instead of open",
|
|
4292
5009
|
hint: "enter and a click on a PR reference copy its link to the clipboard instead of opening the browser"
|
|
4293
5010
|
},
|
|
5011
|
+
{
|
|
5012
|
+
key: "trackMentions",
|
|
5013
|
+
section: "Awaiting you",
|
|
5014
|
+
label: "Track mentions",
|
|
5015
|
+
hint: "searches the PRs that @-mention you on every load and lists the unread ones in the Mentions inbox \xB7 off skips the search"
|
|
5016
|
+
},
|
|
4294
5017
|
{
|
|
4295
5018
|
key: "snoozeDuration",
|
|
4296
|
-
section: "
|
|
5019
|
+
section: "Awaiting you",
|
|
4297
5020
|
label: "Default snooze",
|
|
4298
5021
|
hint: "the duration the snooze dialog starts with when s snoozes a PR, like 30m, 2h, or 1d \xB7 enter edits the value"
|
|
4299
5022
|
},
|
|
@@ -4556,6 +5279,8 @@ function SettingsModal({
|
|
|
4556
5279
|
autoReload: autoReload2,
|
|
4557
5280
|
reloadInterval: reloadInterval2,
|
|
4558
5281
|
notifications: notifications2,
|
|
5282
|
+
trackMentions: trackMentions2,
|
|
5283
|
+
notifyMentions: notifyMentions2,
|
|
4559
5284
|
notifyChannel: notifyChannel2,
|
|
4560
5285
|
copyLinks: copyLinks2,
|
|
4561
5286
|
snoozeDuration: snoozeDuration2,
|
|
@@ -4585,6 +5310,8 @@ function SettingsModal({
|
|
|
4585
5310
|
autoReload: autoReload2,
|
|
4586
5311
|
reloadInterval: reloadInterval2,
|
|
4587
5312
|
notifications: notifications2,
|
|
5313
|
+
trackMentions: trackMentions2,
|
|
5314
|
+
notifyMentions: notifyMentions2,
|
|
4588
5315
|
channelValue,
|
|
4589
5316
|
deliveryValue,
|
|
4590
5317
|
copyLinks: copyLinks2,
|
|
@@ -4608,6 +5335,8 @@ function SettingValue({
|
|
|
4608
5335
|
autoReload: autoReload2,
|
|
4609
5336
|
reloadInterval: reloadInterval2,
|
|
4610
5337
|
notifications: notifications2,
|
|
5338
|
+
trackMentions: trackMentions2,
|
|
5339
|
+
notifyMentions: notifyMentions2,
|
|
4611
5340
|
channelValue,
|
|
4612
5341
|
deliveryValue,
|
|
4613
5342
|
copyLinks: copyLinks2,
|
|
@@ -4635,6 +5364,16 @@ function SettingValue({
|
|
|
4635
5364
|
case "notifications": {
|
|
4636
5365
|
return /* @__PURE__ */ jsx12(ToggleValue, { value: notifications2 ? "yes" : "no", isSelected });
|
|
4637
5366
|
}
|
|
5367
|
+
case "notifyMentions": {
|
|
5368
|
+
return /* @__PURE__ */ jsx12(
|
|
5369
|
+
ToggleValue,
|
|
5370
|
+
{
|
|
5371
|
+
value: notifyMentions2 ? "yes" : "no",
|
|
5372
|
+
isSelected,
|
|
5373
|
+
active: notifications2 && trackMentions2
|
|
5374
|
+
}
|
|
5375
|
+
);
|
|
5376
|
+
}
|
|
4638
5377
|
case "notifyChannel": {
|
|
4639
5378
|
return /* @__PURE__ */ jsx12(ToggleValue, { value: channelValue, isSelected });
|
|
4640
5379
|
}
|
|
@@ -4644,6 +5383,9 @@ function SettingValue({
|
|
|
4644
5383
|
case "copyLinks": {
|
|
4645
5384
|
return /* @__PURE__ */ jsx12(ToggleValue, { value: copyLinks2 ? "yes" : "no", isSelected });
|
|
4646
5385
|
}
|
|
5386
|
+
case "trackMentions": {
|
|
5387
|
+
return /* @__PURE__ */ jsx12(ToggleValue, { value: trackMentions2 ? "yes" : "no", isSelected });
|
|
5388
|
+
}
|
|
4647
5389
|
case "snoozeDuration": {
|
|
4648
5390
|
if (isEditing) {
|
|
4649
5391
|
return /* @__PURE__ */ jsx12(ModalInput, { width: 16, value: snoozeDuration2, onDraft, onSubmit });
|
|
@@ -4667,15 +5409,15 @@ function SettingValue({
|
|
|
4667
5409
|
}
|
|
4668
5410
|
}
|
|
4669
5411
|
}
|
|
4670
|
-
function ToggleValue({ value: value2, isSelected }) {
|
|
5412
|
+
function ToggleValue({ value: value2, isSelected, active = true }) {
|
|
4671
5413
|
if (isSelected) {
|
|
4672
5414
|
return /* @__PURE__ */ jsxs11("text", { wrapMode: "none", children: [
|
|
4673
5415
|
/* @__PURE__ */ jsx12("span", { fg: theme.muted, children: "\u2039 " }),
|
|
4674
|
-
/* @__PURE__ */ jsx12("b", { fg: theme.text, children: value2 }),
|
|
5416
|
+
/* @__PURE__ */ jsx12("b", { fg: active ? theme.text : theme.muted, children: value2 }),
|
|
4675
5417
|
/* @__PURE__ */ jsx12("span", { fg: theme.muted, children: " \u203A" })
|
|
4676
5418
|
] });
|
|
4677
5419
|
}
|
|
4678
|
-
return /* @__PURE__ */ jsx12("text", { wrapMode: "none", fg: theme.muted, children: value2 });
|
|
5420
|
+
return /* @__PURE__ */ jsx12("text", { wrapMode: "none", fg: active ? theme.muted : theme.dim, children: value2 });
|
|
4679
5421
|
}
|
|
4680
5422
|
function IntervalValue({ value: value2, active, isSelected }) {
|
|
4681
5423
|
if (!active) {
|
|
@@ -4789,6 +5531,8 @@ function Modals({
|
|
|
4789
5531
|
autoReload: autoReload2,
|
|
4790
5532
|
reloadInterval: reloadInterval2,
|
|
4791
5533
|
notifications: notifications2,
|
|
5534
|
+
trackMentions: trackMentions2,
|
|
5535
|
+
notifyMentions: notifyMentions2,
|
|
4792
5536
|
notifyChannel: notifyChannel2,
|
|
4793
5537
|
copyLinks: copyLinks2,
|
|
4794
5538
|
snoozeDuration: snoozeDuration2,
|
|
@@ -4829,6 +5573,8 @@ function Modals({
|
|
|
4829
5573
|
autoReload: autoReload2,
|
|
4830
5574
|
reloadInterval: reloadInterval2,
|
|
4831
5575
|
notifications: notifications2,
|
|
5576
|
+
trackMentions: trackMentions2,
|
|
5577
|
+
notifyMentions: notifyMentions2,
|
|
4832
5578
|
notifyChannel: notifyChannel2,
|
|
4833
5579
|
copyLinks: copyLinks2,
|
|
4834
5580
|
snoozeDuration: snoozeDuration2,
|
|
@@ -4896,6 +5642,54 @@ function diffReviewRequests(previous, results) {
|
|
|
4896
5642
|
}
|
|
4897
5643
|
return { baseline, newRequests, reRequests };
|
|
4898
5644
|
}
|
|
5645
|
+
var OBSERVATION_MARGIN_MS = 10 * 60 * 1e3;
|
|
5646
|
+
function diffMentions(previous, entries, observedAt) {
|
|
5647
|
+
const seen = new Set(previous?.seen);
|
|
5648
|
+
const unread = new Map(previous?.unread);
|
|
5649
|
+
const newMentions = [];
|
|
5650
|
+
for (const entry of entries) {
|
|
5651
|
+
const key = `${entry.pr.repo}#${entry.pr.number}`;
|
|
5652
|
+
const cutoff = previous === null ? observedAt.getTime() : previous.unread.get(key) ?? previous.observedAt;
|
|
5653
|
+
if (entry.mentions === null) {
|
|
5654
|
+
unread.set(key, cutoff);
|
|
5655
|
+
continue;
|
|
5656
|
+
}
|
|
5657
|
+
unread.delete(key);
|
|
5658
|
+
let isNew = false;
|
|
5659
|
+
for (const mention of entry.mentions) {
|
|
5660
|
+
if (previous !== null && !previous.seen.has(mention.id) && mention.at.getTime() > cutoff - OBSERVATION_MARGIN_MS) {
|
|
5661
|
+
isNew = true;
|
|
5662
|
+
}
|
|
5663
|
+
seen.add(mention.id);
|
|
5664
|
+
}
|
|
5665
|
+
for (const id of entry.earlier) {
|
|
5666
|
+
seen.add(id);
|
|
5667
|
+
}
|
|
5668
|
+
if (isNew) {
|
|
5669
|
+
newMentions.push(entry.pr);
|
|
5670
|
+
}
|
|
5671
|
+
}
|
|
5672
|
+
return { baseline: { seen, observedAt: observedAt.getTime(), unread }, newMentions };
|
|
5673
|
+
}
|
|
5674
|
+
function saveMentionBaseline(key, baseline) {
|
|
5675
|
+
if (baseline === null) {
|
|
5676
|
+
return writeCacheFile("mention-baseline", null);
|
|
5677
|
+
}
|
|
5678
|
+
const stored = {
|
|
5679
|
+
key,
|
|
5680
|
+
seen: [...baseline.seen],
|
|
5681
|
+
observedAt: baseline.observedAt,
|
|
5682
|
+
unread: [...baseline.unread]
|
|
5683
|
+
};
|
|
5684
|
+
return writeCacheFile("mention-baseline", stored);
|
|
5685
|
+
}
|
|
5686
|
+
function loadMentionBaseline(key) {
|
|
5687
|
+
const stored = readCacheFile("mention-baseline");
|
|
5688
|
+
if (stored === null || stored.key !== key) {
|
|
5689
|
+
return null;
|
|
5690
|
+
}
|
|
5691
|
+
return { seen: new Set(stored.seen), observedAt: stored.observedAt, unread: new Map(stored.unread) };
|
|
5692
|
+
}
|
|
4899
5693
|
var MAX_LISTED = 3;
|
|
4900
5694
|
var TEST_NOTIFICATION = {
|
|
4901
5695
|
title: "pr-stats",
|
|
@@ -4917,6 +5711,12 @@ function describeSnoozeWakeUps(prs) {
|
|
|
4917
5711
|
}
|
|
4918
5712
|
return [describe(prs, "Snooze ended on", "snoozed PRs are back in your queue")];
|
|
4919
5713
|
}
|
|
5714
|
+
function describeMentions(changes) {
|
|
5715
|
+
if (changes.newMentions.length === 0) {
|
|
5716
|
+
return [];
|
|
5717
|
+
}
|
|
5718
|
+
return [describe(changes.newMentions, "Mentioned on", "PRs mention you")];
|
|
5719
|
+
}
|
|
4920
5720
|
function describe(prs, singleTitle, pluralTitle) {
|
|
4921
5721
|
if (prs.length === 1) {
|
|
4922
5722
|
const [pr] = prs;
|
|
@@ -4982,7 +5782,7 @@ function useDeferredLoading(isLoading, { showDelay = 300, minDuration = 500 } =
|
|
|
4982
5782
|
|
|
4983
5783
|
// src/tui/hooks/useLoader.ts
|
|
4984
5784
|
import { useEffect as useEffect6, useRef as useRef4, useState as useState3 } from "react";
|
|
4985
|
-
function useLoader(options, noCache2, callbacks = {}) {
|
|
5785
|
+
function useLoader(options, { noCache: noCache2, mentions }, callbacks = {}) {
|
|
4986
5786
|
const { onSnapshot, onLoaded } = callbacks;
|
|
4987
5787
|
const [startupSnapshot] = useState3(() => noCache2 ? null : loadSnapshot(options));
|
|
4988
5788
|
const [raw, setRaw] = useState3(startupSnapshot);
|
|
@@ -5003,7 +5803,7 @@ function useLoader(options, noCache2, callbacks = {}) {
|
|
|
5003
5803
|
}
|
|
5004
5804
|
};
|
|
5005
5805
|
try {
|
|
5006
|
-
const data = await loadData(options, publishPhase, { bypassCache });
|
|
5806
|
+
const data = await loadData(options, publishPhase, { bypassCache, mentions });
|
|
5007
5807
|
if (disposedRef.current) {
|
|
5008
5808
|
return;
|
|
5009
5809
|
}
|
|
@@ -5056,10 +5856,86 @@ function useLoader(options, noCache2, callbacks = {}) {
|
|
|
5056
5856
|
};
|
|
5057
5857
|
}
|
|
5058
5858
|
|
|
5059
|
-
// src/tui/hooks/
|
|
5859
|
+
// src/tui/hooks/useMentionNotifications.ts
|
|
5060
5860
|
import { useRef as useRef5 } from "react";
|
|
5061
|
-
function
|
|
5861
|
+
function useMentionNotifications(enabled2, notify, onError, restore) {
|
|
5062
5862
|
const baselineRef = useRef5(null);
|
|
5863
|
+
const restoredRef = useRef5(!restore);
|
|
5864
|
+
return (key, mentions, observedAt) => {
|
|
5865
|
+
if (!restoredRef.current) {
|
|
5866
|
+
restoredRef.current = true;
|
|
5867
|
+
const restored = loadMentionBaseline(key);
|
|
5868
|
+
if (restored !== null) {
|
|
5869
|
+
baselineRef.current = { key, baseline: restored };
|
|
5870
|
+
}
|
|
5871
|
+
}
|
|
5872
|
+
if (mentions === null) {
|
|
5873
|
+
baselineRef.current = null;
|
|
5874
|
+
saveMentionBaseline(key, null);
|
|
5875
|
+
return;
|
|
5876
|
+
}
|
|
5877
|
+
const previous = baselineRef.current?.key === key ? baselineRef.current.baseline : null;
|
|
5878
|
+
const changes = diffMentions(previous, mentions, observedAt);
|
|
5879
|
+
baselineRef.current = { key, baseline: changes.baseline };
|
|
5880
|
+
saveMentionBaseline(key, changes.baseline);
|
|
5881
|
+
if (!enabled2) {
|
|
5882
|
+
return;
|
|
5883
|
+
}
|
|
5884
|
+
for (const notification of describeMentions(changes)) {
|
|
5885
|
+
notify(notification.title, notification.body, onError);
|
|
5886
|
+
}
|
|
5887
|
+
};
|
|
5888
|
+
}
|
|
5889
|
+
|
|
5890
|
+
// src/tui/hooks/useMentionReads.ts
|
|
5891
|
+
import { useRef as useRef6, useState as useState4 } from "react";
|
|
5892
|
+
var NO_READS = emptyMentionReads();
|
|
5893
|
+
function useMentionReads(initial2) {
|
|
5894
|
+
const [state, setState] = useState4({ all: initial2, user: null });
|
|
5895
|
+
const latest = useRef6(state);
|
|
5896
|
+
const commit = (user, apply) => {
|
|
5897
|
+
const { all } = latest.current;
|
|
5898
|
+
const reads = all.get(user) ?? NO_READS;
|
|
5899
|
+
const next = apply(reads);
|
|
5900
|
+
const changed = next !== reads;
|
|
5901
|
+
if (!changed && user === latest.current.user) {
|
|
5902
|
+
return true;
|
|
5903
|
+
}
|
|
5904
|
+
latest.current = { all: changed ? new Map(all).set(user, next) : all, user };
|
|
5905
|
+
setState(latest.current);
|
|
5906
|
+
return changed ? writeMentionReads(latest.current.all) : true;
|
|
5907
|
+
};
|
|
5908
|
+
const change = (apply) => {
|
|
5909
|
+
const { user } = latest.current;
|
|
5910
|
+
return user === null ? true : commit(user, apply);
|
|
5911
|
+
};
|
|
5912
|
+
return {
|
|
5913
|
+
reads: state.user === null ? NO_READS : state.all.get(state.user) ?? NO_READS,
|
|
5914
|
+
observe: (data) => {
|
|
5915
|
+
commit(
|
|
5916
|
+
data.user.toLowerCase(),
|
|
5917
|
+
(reads) => data.mentions === null ? unseedMentionReads(reads) : seedMentionReads(reads, {
|
|
5918
|
+
at: data.fetchedAt.getTime(),
|
|
5919
|
+
ids: data.mentions.flatMap(mentionIdsOf)
|
|
5920
|
+
})
|
|
5921
|
+
);
|
|
5922
|
+
},
|
|
5923
|
+
markRead: (ref, mark) => change((reads) => markMentionRead(reads, ref, mark)),
|
|
5924
|
+
markAllRead: (marks) => change((reads) => {
|
|
5925
|
+
let next = reads;
|
|
5926
|
+
for (const { ref, mark } of marks) {
|
|
5927
|
+
next = markMentionRead(next, ref, mark);
|
|
5928
|
+
}
|
|
5929
|
+
return next;
|
|
5930
|
+
}),
|
|
5931
|
+
markUnread: (ref) => change((reads) => markMentionUnread(reads, ref))
|
|
5932
|
+
};
|
|
5933
|
+
}
|
|
5934
|
+
|
|
5935
|
+
// src/tui/hooks/useReviewNotifications.ts
|
|
5936
|
+
import { useRef as useRef7 } from "react";
|
|
5937
|
+
function useReviewNotifications(enabled2, notify, onError) {
|
|
5938
|
+
const baselineRef = useRef7(null);
|
|
5063
5939
|
return (key, results) => {
|
|
5064
5940
|
const previous = baselineRef.current?.key === key ? baselineRef.current.baseline : null;
|
|
5065
5941
|
const changes = diffReviewRequests(previous, results);
|
|
@@ -5074,17 +5950,17 @@ function useReviewNotifications(enabled2, notify, onError) {
|
|
|
5074
5950
|
}
|
|
5075
5951
|
|
|
5076
5952
|
// src/tui/hooks/useSnoozes.ts
|
|
5077
|
-
import { useState as
|
|
5953
|
+
import { useState as useState5 } from "react";
|
|
5078
5954
|
function useSnoozes(initial2) {
|
|
5079
|
-
const [snoozes2, setSnoozes] =
|
|
5955
|
+
const [snoozes2, setSnoozes] = useState5(initial2);
|
|
5080
5956
|
const commit = (next) => {
|
|
5081
5957
|
setSnoozes(next);
|
|
5082
5958
|
return writeSnoozes(next);
|
|
5083
5959
|
};
|
|
5084
5960
|
return {
|
|
5085
5961
|
snoozes: snoozes2,
|
|
5086
|
-
add: (snooze) => commit([...snoozes2.filter((entry) => entry
|
|
5087
|
-
remove: (
|
|
5962
|
+
add: (snooze) => commit([...snoozes2.filter((entry) => !snoozeMatches(entry, snooze)), snooze]),
|
|
5963
|
+
remove: (targets) => commit(snoozes2.filter((entry) => !targets.some((target) => snoozeMatches(entry, target))))
|
|
5088
5964
|
};
|
|
5089
5965
|
}
|
|
5090
5966
|
|
|
@@ -5175,49 +6051,110 @@ function buildSizeRepoOptions(raw) {
|
|
|
5175
6051
|
})
|
|
5176
6052
|
];
|
|
5177
6053
|
}
|
|
5178
|
-
function
|
|
5179
|
-
|
|
5180
|
-
|
|
6054
|
+
function pickerOf(countsByRepo, zero, order, detail) {
|
|
6055
|
+
if (countsByRepo.size < 2) {
|
|
6056
|
+
return [];
|
|
6057
|
+
}
|
|
6058
|
+
const entries = [...countsByRepo.entries()].toSorted((a, b) => order(a[1], b[1]) || a[0].localeCompare(b[0]));
|
|
6059
|
+
const totals = zero();
|
|
6060
|
+
for (const [, counts] of entries) {
|
|
6061
|
+
for (const [key, count2] of Object.entries(counts)) {
|
|
6062
|
+
totals[key] += count2;
|
|
6063
|
+
}
|
|
6064
|
+
}
|
|
6065
|
+
return [
|
|
6066
|
+
{ repo: null, label: "All repos", detail: detail(totals) },
|
|
6067
|
+
...entries.map(([repo, counts]) => {
|
|
6068
|
+
return { repo, label: repo, detail: detail(counts) };
|
|
6069
|
+
})
|
|
6070
|
+
];
|
|
5181
6071
|
}
|
|
5182
|
-
function
|
|
6072
|
+
function bump(countsByRepo, repo, key) {
|
|
6073
|
+
const counts = countsByRepo.get(repo);
|
|
6074
|
+
if (counts !== void 0) {
|
|
6075
|
+
counts[key] += 1;
|
|
6076
|
+
}
|
|
6077
|
+
}
|
|
6078
|
+
function reviewRepos(raw, zero) {
|
|
5183
6079
|
const countsByRepo = /* @__PURE__ */ new Map();
|
|
5184
|
-
const countsOf = (repo) => {
|
|
5185
|
-
const counts = countsByRepo.get(repo) ?? { awaiting: 0, snoozed: 0, reviewing: 0 };
|
|
5186
|
-
countsByRepo.set(repo, counts);
|
|
5187
|
-
return counts;
|
|
5188
|
-
};
|
|
5189
6080
|
for (const result of raw.reviewResults) {
|
|
5190
|
-
|
|
6081
|
+
if (!countsByRepo.has(result.pr.repo)) {
|
|
6082
|
+
countsByRepo.set(result.pr.repo, zero());
|
|
6083
|
+
}
|
|
5191
6084
|
}
|
|
6085
|
+
return countsByRepo;
|
|
6086
|
+
}
|
|
6087
|
+
function zeroPending() {
|
|
6088
|
+
return { awaiting: 0, snoozed: 0 };
|
|
6089
|
+
}
|
|
6090
|
+
function pendingDetail(counts) {
|
|
6091
|
+
const awaiting = `${counts.awaiting} ${counts.awaiting === 1 ? "PR" : "PRs"} awaiting your review`;
|
|
6092
|
+
return awaiting + (counts.snoozed > 0 ? `, ${counts.snoozed} snoozed` : "");
|
|
6093
|
+
}
|
|
6094
|
+
function buildPendingRepoOptions(raw, snoozes2 = [], now = Date.now()) {
|
|
6095
|
+
const countsByRepo = reviewRepos(raw, zeroPending);
|
|
5192
6096
|
if (countsByRepo.size < 2) {
|
|
5193
6097
|
return [];
|
|
5194
6098
|
}
|
|
5195
|
-
const
|
|
5196
|
-
const { awaiting, snoozed } = splitSnoozed(stats.pending, snoozes2, now);
|
|
6099
|
+
const { awaiting, snoozed } = splitSnoozed(pendingRequests(raw.reviewResults), snoozes2, now);
|
|
5197
6100
|
for (const entry of awaiting) {
|
|
5198
|
-
|
|
6101
|
+
bump(countsByRepo, entry.pr.repo, "awaiting");
|
|
5199
6102
|
}
|
|
5200
6103
|
for (const entry of snoozed) {
|
|
5201
|
-
|
|
6104
|
+
bump(countsByRepo, entry.pr.repo, "snoozed");
|
|
5202
6105
|
}
|
|
5203
|
-
|
|
5204
|
-
|
|
6106
|
+
return pickerOf(countsByRepo, zeroPending, (a, b) => b.awaiting - a.awaiting || b.snoozed - a.snoozed, pendingDetail);
|
|
6107
|
+
}
|
|
6108
|
+
function zeroReviewed() {
|
|
6109
|
+
return { reviewed: 0 };
|
|
6110
|
+
}
|
|
6111
|
+
function reviewedDetail(counts) {
|
|
6112
|
+
return `${counts.reviewed} reviewed ${counts.reviewed === 1 ? "PR" : "PRs"} still open`;
|
|
6113
|
+
}
|
|
6114
|
+
function buildReviewedRepoOptions(raw) {
|
|
6115
|
+
const countsByRepo = reviewRepos(raw, zeroReviewed);
|
|
6116
|
+
if (countsByRepo.size < 2) {
|
|
6117
|
+
return [];
|
|
5205
6118
|
}
|
|
5206
|
-
const
|
|
5207
|
-
(
|
|
5208
|
-
);
|
|
5209
|
-
const totals = { awaiting: 0, snoozed: 0, reviewing: 0 };
|
|
5210
|
-
for (const [, counts] of entries) {
|
|
5211
|
-
totals.awaiting += counts.awaiting;
|
|
5212
|
-
totals.snoozed += counts.snoozed;
|
|
5213
|
-
totals.reviewing += counts.reviewing;
|
|
6119
|
+
for (const entry of latestReviews(raw.reviewResults)) {
|
|
6120
|
+
bump(countsByRepo, entry.pr.repo, "reviewed");
|
|
5214
6121
|
}
|
|
5215
|
-
return
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
6122
|
+
return pickerOf(countsByRepo, zeroReviewed, (a, b) => b.reviewed - a.reviewed, reviewedDetail);
|
|
6123
|
+
}
|
|
6124
|
+
function zeroMentions() {
|
|
6125
|
+
return { unread: 0, snoozed: 0, read: 0 };
|
|
6126
|
+
}
|
|
6127
|
+
function mentionDetail(counts) {
|
|
6128
|
+
const unread = `${counts.unread} unread ${counts.unread === 1 ? "mention" : "mentions"}`;
|
|
6129
|
+
return unread + (counts.snoozed > 0 ? `, ${counts.snoozed} snoozed` : "") + (counts.read > 0 ? `, ${counts.read} read` : "");
|
|
6130
|
+
}
|
|
6131
|
+
function buildMentionRepoOptions(raw, snoozes2 = [], reads = emptyMentionReads(), now = Date.now()) {
|
|
6132
|
+
const countsByRepo = /* @__PURE__ */ new Map();
|
|
6133
|
+
const items = mentionItems(raw.mentions ?? []);
|
|
6134
|
+
for (const item of items) {
|
|
6135
|
+
if (!countsByRepo.has(item.pr.repo)) {
|
|
6136
|
+
countsByRepo.set(item.pr.repo, zeroMentions());
|
|
6137
|
+
}
|
|
6138
|
+
}
|
|
6139
|
+
if (countsByRepo.size < 2) {
|
|
6140
|
+
return [];
|
|
6141
|
+
}
|
|
6142
|
+
const { unread, snoozed, read } = splitMentions(items, reads, snoozes2, now);
|
|
6143
|
+
for (const [state, group] of [
|
|
6144
|
+
["unread", unread],
|
|
6145
|
+
["snoozed", snoozed],
|
|
6146
|
+
["read", read]
|
|
6147
|
+
]) {
|
|
6148
|
+
for (const item of group) {
|
|
6149
|
+
bump(countsByRepo, item.pr.repo, state);
|
|
6150
|
+
}
|
|
6151
|
+
}
|
|
6152
|
+
return pickerOf(
|
|
6153
|
+
countsByRepo,
|
|
6154
|
+
zeroMentions,
|
|
6155
|
+
(a, b) => b.unread - a.unread || b.snoozed - a.snoozed || b.read - a.read,
|
|
6156
|
+
mentionDetail
|
|
6157
|
+
);
|
|
5221
6158
|
}
|
|
5222
6159
|
function openDetail(count2) {
|
|
5223
6160
|
return `${count2} open ${count2 === 1 ? "PR" : "PRs"}`;
|
|
@@ -6641,7 +7578,7 @@ function resolveScope(scope, repos) {
|
|
|
6641
7578
|
}
|
|
6642
7579
|
return dropVanishedRepo(scope, repos);
|
|
6643
7580
|
}
|
|
6644
|
-
function useViewModel(raw, options, width, scopes, grouping, expanded, snoozes2, themeEpoch) {
|
|
7581
|
+
function useViewModel(raw, options, width, scopes, grouping, expanded, snoozes2, reads, themeEpoch) {
|
|
6645
7582
|
return useMemo(() => {
|
|
6646
7583
|
void themeEpoch;
|
|
6647
7584
|
configureTimeMode({
|
|
@@ -6662,33 +7599,44 @@ function useViewModel(raw, options, width, scopes, grouping, expanded, snoozes2,
|
|
|
6662
7599
|
};
|
|
6663
7600
|
const sizeTarget = options.sizeTarget === "" ? void 0 : parseSizeTarget(options.sizeTarget);
|
|
6664
7601
|
const pendingRepos = buildPendingRepoOptions(raw, snoozes2);
|
|
7602
|
+
const reviewedRepos = buildReviewedRepoOptions(raw);
|
|
7603
|
+
const mentionsRepos = buildMentionRepoOptions(raw, snoozes2, reads);
|
|
6665
7604
|
const openRepos = buildOpenRepoOptions(raw);
|
|
6666
7605
|
const mergedRepos = buildMergedRepoOptions(raw);
|
|
6667
|
-
const
|
|
7606
|
+
const reviewRepos2 = buildReviewRepoOptions(raw);
|
|
6668
7607
|
const sizeRepos = buildSizeRepoOptions(raw);
|
|
6669
7608
|
const commentRepos = buildCommentRepoOptions(raw);
|
|
6670
7609
|
const pendingScope = resolveScope(scopes.pending, pendingRepos);
|
|
7610
|
+
const reviewedScope = resolveScope(scopes.reviewed, reviewedRepos);
|
|
7611
|
+
const mentionsScope = resolveScope(scopes.mentions, mentionsRepos);
|
|
6671
7612
|
const openScope = resolveScope(scopes.open, openRepos);
|
|
6672
7613
|
const mergedScope = resolveScope(scopes.merged, mergedRepos);
|
|
6673
|
-
const reviewScope = resolveScope(scopes.review,
|
|
7614
|
+
const reviewScope = resolveScope(scopes.review, reviewRepos2);
|
|
6674
7615
|
const sizeScope = resolveScope(scopes.size, sizeRepos);
|
|
6675
7616
|
const commentScope = resolveScope(scopes.comment, commentRepos);
|
|
6676
7617
|
const review = reviewScope.view === "detail" ? buildReviewView(raw, reviewTarget, reviewScope.repo, width, expanded.review) : null;
|
|
6677
7618
|
return {
|
|
6678
7619
|
pendingRepos,
|
|
7620
|
+
reviewedRepos,
|
|
7621
|
+
mentionsRepos,
|
|
6679
7622
|
openRepos,
|
|
6680
7623
|
mergedRepos,
|
|
6681
|
-
reviewRepos,
|
|
7624
|
+
reviewRepos: reviewRepos2,
|
|
6682
7625
|
sizeRepos,
|
|
6683
7626
|
commentRepos,
|
|
6684
7627
|
pendingScope,
|
|
7628
|
+
reviewedScope,
|
|
7629
|
+
mentionsScope,
|
|
6685
7630
|
openScope,
|
|
6686
7631
|
mergedScope,
|
|
6687
7632
|
reviewScope,
|
|
6688
7633
|
sizeScope,
|
|
6689
7634
|
commentScope,
|
|
6690
|
-
pending: pendingScope.view === "detail" ? buildPendingReviewView(raw, pendingScope.repo, grouping.pending, snoozes2) : null,
|
|
7635
|
+
pending: pendingScope.view === "detail" ? buildPendingReviewView(raw, pendingScope.repo, grouping.pending, snoozes2, reads) : null,
|
|
7636
|
+
reviewed: reviewedScope.view === "detail" ? buildReviewedView(raw, reviewedScope.repo, grouping.reviewed, snoozes2, reads) : null,
|
|
7637
|
+
mentions: mentionsScope.view === "detail" ? buildMentionsView(raw, mentionsScope.repo, grouping.mentions, snoozes2, reads) : null,
|
|
6691
7638
|
open: openScope.view === "detail" ? buildOpenAuthoredView(raw, openScope.repo, grouping.open) : null,
|
|
7639
|
+
alerts: queueAlerts(raw, snoozes2, reads),
|
|
6692
7640
|
merged: mergedScope.view === "detail" ? buildMergedView(raw, mergedScope.repo, width, expanded.merged) : null,
|
|
6693
7641
|
review,
|
|
6694
7642
|
size: sizeScope.view === "detail" ? buildSizeView(raw, sizeTarget, sizeScope.repo, width) : null,
|
|
@@ -6705,16 +7653,21 @@ function useViewModel(raw, options, width, scopes, grouping, expanded, snoozes2,
|
|
|
6705
7653
|
options.sizeTarget,
|
|
6706
7654
|
width,
|
|
6707
7655
|
scopes.pending,
|
|
7656
|
+
scopes.reviewed,
|
|
7657
|
+
scopes.mentions,
|
|
6708
7658
|
scopes.open,
|
|
6709
7659
|
scopes.merged,
|
|
6710
7660
|
scopes.review,
|
|
6711
7661
|
scopes.size,
|
|
6712
7662
|
scopes.comment,
|
|
6713
7663
|
grouping.pending,
|
|
7664
|
+
grouping.reviewed,
|
|
7665
|
+
grouping.mentions,
|
|
6714
7666
|
grouping.open,
|
|
6715
7667
|
expanded.review,
|
|
6716
7668
|
expanded.merged,
|
|
6717
7669
|
snoozes2,
|
|
7670
|
+
reads,
|
|
6718
7671
|
themeEpoch
|
|
6719
7672
|
]);
|
|
6720
7673
|
}
|
|
@@ -6821,6 +7774,18 @@ function handleSettingsModalKey(key, context) {
|
|
|
6821
7774
|
context.dispatchUi({ type: "cacheActionReported", action: saveNotifications(next) ? "saved" : "notSaved" });
|
|
6822
7775
|
break;
|
|
6823
7776
|
}
|
|
7777
|
+
case "trackMentions": {
|
|
7778
|
+
const next = !context.trackMentions;
|
|
7779
|
+
context.setTrackMentions(next);
|
|
7780
|
+
context.dispatchUi({ type: "cacheActionReported", action: saveTrackMentions(next) ? "saved" : "notSaved" });
|
|
7781
|
+
break;
|
|
7782
|
+
}
|
|
7783
|
+
case "notifyMentions": {
|
|
7784
|
+
const next = !context.notifyMentions;
|
|
7785
|
+
context.setNotifyMentions(next);
|
|
7786
|
+
context.dispatchUi({ type: "cacheActionReported", action: saveNotifyMentions(next) ? "saved" : "notSaved" });
|
|
7787
|
+
break;
|
|
7788
|
+
}
|
|
6824
7789
|
case "notifyChannel": {
|
|
6825
7790
|
const index = NOTIFY_CHANNELS.indexOf(context.notifyChannel);
|
|
6826
7791
|
const step = key.name === "left" ? -1 : 1;
|
|
@@ -6926,25 +7891,30 @@ function handleThemeModalKey(key, context) {
|
|
|
6926
7891
|
}
|
|
6927
7892
|
}
|
|
6928
7893
|
}
|
|
6929
|
-
function queueTabOf(
|
|
6930
|
-
|
|
6931
|
-
|
|
7894
|
+
function queueTabOf(key, views) {
|
|
7895
|
+
if (key === "pending") {
|
|
7896
|
+
return { key, view: views?.pending ?? null, repos: views?.pendingRepos ?? [], scope: views?.pendingScope ?? null };
|
|
7897
|
+
}
|
|
7898
|
+
if (key === "reviewed") {
|
|
6932
7899
|
return {
|
|
6933
|
-
key
|
|
6934
|
-
view: views?.
|
|
6935
|
-
repos: views?.
|
|
6936
|
-
scope: views?.
|
|
7900
|
+
key,
|
|
7901
|
+
view: views?.reviewed ?? null,
|
|
7902
|
+
repos: views?.reviewedRepos ?? [],
|
|
7903
|
+
scope: views?.reviewedScope ?? null
|
|
6937
7904
|
};
|
|
6938
7905
|
}
|
|
6939
|
-
|
|
6940
|
-
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
7906
|
+
if (key === "mentions") {
|
|
7907
|
+
return {
|
|
7908
|
+
key,
|
|
7909
|
+
view: views?.mentions ?? null,
|
|
7910
|
+
repos: views?.mentionsRepos ?? [],
|
|
7911
|
+
scope: views?.mentionsScope ?? null
|
|
7912
|
+
};
|
|
7913
|
+
}
|
|
7914
|
+
return { key, view: views?.open ?? null, repos: views?.openRepos ?? [], scope: views?.openScope ?? null };
|
|
6945
7915
|
}
|
|
6946
|
-
function handleQueueKey(key, context) {
|
|
6947
|
-
const { key: tab, view, repos, scope } = queueTabOf(context);
|
|
7916
|
+
function handleQueueKey(key, queue, context) {
|
|
7917
|
+
const { key: tab, view, repos, scope } = queueTabOf(queue, context.views);
|
|
6948
7918
|
if (scope?.view === "list") {
|
|
6949
7919
|
switch (key.name) {
|
|
6950
7920
|
case "up":
|
|
@@ -7003,20 +7973,38 @@ function handleQueueKey(key, context) {
|
|
|
7003
7973
|
case "s": {
|
|
7004
7974
|
const cursor = context.browse.rowCursors[tab];
|
|
7005
7975
|
const row = rows[Math.min(cursor, rows.length - 1)];
|
|
7006
|
-
|
|
7976
|
+
const ask = row.pending !== void 0 ? { kind: "review", at: row.pending.requestedAt } : row.mention !== void 0 && row.mention.state !== "read" ? { kind: "mention", at: row.mention.mark.at, ids: row.mention.mark.ids } : null;
|
|
7977
|
+
if (ask === null) {
|
|
7007
7978
|
break;
|
|
7008
7979
|
}
|
|
7009
|
-
if (row.pending.snoozed) {
|
|
7010
|
-
context.unsnooze(row.ref);
|
|
7980
|
+
if (row.pending?.snoozed ?? row.mention?.state === "snoozed") {
|
|
7981
|
+
context.unsnooze({ kind: ask.kind, ref: row.ref });
|
|
7011
7982
|
break;
|
|
7012
7983
|
}
|
|
7013
|
-
context.dispatchUi({
|
|
7014
|
-
type: "snoozeModalOpened",
|
|
7015
|
-
target: { ref: row.ref, title: row.title, requestedAt: row.pending.requestedAt }
|
|
7016
|
-
});
|
|
7984
|
+
context.dispatchUi({ type: "snoozeModalOpened", target: { ...ask, ref: row.ref, title: row.title } });
|
|
7017
7985
|
context.beginEdit(context.snoozeDuration);
|
|
7018
7986
|
break;
|
|
7019
7987
|
}
|
|
7988
|
+
case "d": {
|
|
7989
|
+
if (key.shift) {
|
|
7990
|
+
const unread = unreadMentionRows(view);
|
|
7991
|
+
if (unread.length > 0) {
|
|
7992
|
+
context.markAllRead(unread);
|
|
7993
|
+
}
|
|
7994
|
+
break;
|
|
7995
|
+
}
|
|
7996
|
+
const cursor = context.browse.rowCursors[tab];
|
|
7997
|
+
const row = rows[Math.min(cursor, rows.length - 1)];
|
|
7998
|
+
if (row.mention === void 0) {
|
|
7999
|
+
break;
|
|
8000
|
+
}
|
|
8001
|
+
if (row.mention.state === "read") {
|
|
8002
|
+
context.markUnread(row);
|
|
8003
|
+
} else {
|
|
8004
|
+
context.markRead(row);
|
|
8005
|
+
}
|
|
8006
|
+
break;
|
|
8007
|
+
}
|
|
7020
8008
|
}
|
|
7021
8009
|
}
|
|
7022
8010
|
function statsTabOf(context) {
|
|
@@ -7120,12 +8108,15 @@ function handleAppKey(key, context) {
|
|
|
7120
8108
|
context.dispatchBrowse({ type: "tabCycled", delta: -1 });
|
|
7121
8109
|
} else if (key.name === "right" || key.name === "tab") {
|
|
7122
8110
|
context.dispatchBrowse({ type: "tabCycled", delta: 1 });
|
|
7123
|
-
} else if (context.browse.tab === 1 && key.name === "t") {
|
|
7124
|
-
context.dispatchBrowse({ type: "
|
|
7125
|
-
} else if (context.browse.tab === 0 || context.browse.tab === 1 && context.browse.authoredTab === "open") {
|
|
7126
|
-
handleQueueKey(key, context);
|
|
8111
|
+
} else if ((context.browse.tab === 0 || context.browse.tab === 1) && key.name === "t") {
|
|
8112
|
+
context.dispatchBrowse({ type: "subTabCycled", delta: key.shift ? -1 : 1 });
|
|
7127
8113
|
} else {
|
|
7128
|
-
|
|
8114
|
+
const queue = activeQueueTab(context.browse);
|
|
8115
|
+
if (queue === null) {
|
|
8116
|
+
handleStatsKey(key, context);
|
|
8117
|
+
} else {
|
|
8118
|
+
handleQueueKey(key, queue, context);
|
|
8119
|
+
}
|
|
7129
8120
|
}
|
|
7130
8121
|
}
|
|
7131
8122
|
|
|
@@ -7330,10 +8321,13 @@ function App({
|
|
|
7330
8321
|
initialAutoReload = false,
|
|
7331
8322
|
initialReloadInterval = DEFAULT_RELOAD_INTERVAL,
|
|
7332
8323
|
initialNotifications = false,
|
|
8324
|
+
initialTrackMentions = true,
|
|
8325
|
+
initialNotifyMentions = false,
|
|
7333
8326
|
initialNotifyChannel = "auto",
|
|
7334
8327
|
initialCopyLinks = false,
|
|
7335
8328
|
initialSnoozeDuration = DEFAULT_SNOOZE_DURATION,
|
|
7336
8329
|
initialSnoozes = [],
|
|
8330
|
+
initialMentionReads = /* @__PURE__ */ new Map(),
|
|
7337
8331
|
initialTheme = defaultThemeState(),
|
|
7338
8332
|
openUrl = openInBrowser,
|
|
7339
8333
|
copyUrl,
|
|
@@ -7346,17 +8340,20 @@ function App({
|
|
|
7346
8340
|
const copyLink = copyUrl ?? defaultCopyUrl;
|
|
7347
8341
|
const [ui, dispatchUi] = useReducer(uiReducer, initialUiState);
|
|
7348
8342
|
const [browse, dispatchBrowse] = useReducer(browseReducer, initialBrowseState);
|
|
7349
|
-
const [options, setOptions] =
|
|
7350
|
-
const [saved2, setSaved] =
|
|
7351
|
-
const [noCache2, setNoCache] =
|
|
7352
|
-
const [autoReload2, setAutoReload] =
|
|
7353
|
-
const [reloadInterval2, setReloadInterval] =
|
|
7354
|
-
const [notifications2, setNotifications] =
|
|
7355
|
-
const [
|
|
7356
|
-
const [
|
|
7357
|
-
const [
|
|
7358
|
-
const [
|
|
8343
|
+
const [options, setOptions] = useState6(initial2);
|
|
8344
|
+
const [saved2, setSaved] = useState6(initialSaved);
|
|
8345
|
+
const [noCache2, setNoCache] = useState6(initialNoCache);
|
|
8346
|
+
const [autoReload2, setAutoReload] = useState6(initialAutoReload);
|
|
8347
|
+
const [reloadInterval2, setReloadInterval] = useState6(initialReloadInterval);
|
|
8348
|
+
const [notifications2, setNotifications] = useState6(initialNotifications);
|
|
8349
|
+
const [trackMentions2, setTrackMentions] = useState6(initialTrackMentions);
|
|
8350
|
+
const [notifyMentions2, setNotifyMentions] = useState6(initialNotifyMentions);
|
|
8351
|
+
const [notifyChannel2, setNotifyChannel] = useState6(initialNotifyChannel);
|
|
8352
|
+
const [copyLinks2, setCopyLinks] = useState6(initialCopyLinks);
|
|
8353
|
+
const [snoozeDuration2, setSnoozeDuration] = useState6(initialSnoozeDuration);
|
|
8354
|
+
const [themeState, setThemeState] = useState6(initialTheme);
|
|
7359
8355
|
const snoozeStore = useSnoozes(initialSnoozes);
|
|
8356
|
+
const readStore = useMentionReads(initialMentionReads);
|
|
7360
8357
|
const defaultNotify = useMemo2(
|
|
7361
8358
|
() => createNotifier(notificationBoundary(renderer2), notifyChannel2),
|
|
7362
8359
|
[renderer2, notifyChannel2]
|
|
@@ -7368,9 +8365,38 @@ function App({
|
|
|
7368
8365
|
});
|
|
7369
8366
|
dispatchUi({ type: "successReported", message: `copied ${row.ref} to the clipboard` });
|
|
7370
8367
|
};
|
|
7371
|
-
const unsnooze = (
|
|
7372
|
-
snoozeStore.remove([
|
|
7373
|
-
dispatchUi({ type: "successReported", message: `unsnoozed ${ref}` });
|
|
8368
|
+
const unsnooze = (target) => {
|
|
8369
|
+
snoozeStore.remove([target]);
|
|
8370
|
+
dispatchUi({ type: "successReported", message: `unsnoozed ${target.ref}` });
|
|
8371
|
+
};
|
|
8372
|
+
const reportMark = (message, saved3) => {
|
|
8373
|
+
if (saved3) {
|
|
8374
|
+
dispatchUi({ type: "successReported", message });
|
|
8375
|
+
} else {
|
|
8376
|
+
dispatchUi({
|
|
8377
|
+
type: "openErrorReported",
|
|
8378
|
+
message: `${message} \xB7 the cache is disabled for this session, so the mark is not saved`
|
|
8379
|
+
});
|
|
8380
|
+
}
|
|
8381
|
+
};
|
|
8382
|
+
const markRead = (row) => {
|
|
8383
|
+
if (row.mention === void 0) {
|
|
8384
|
+
return;
|
|
8385
|
+
}
|
|
8386
|
+
if (row.mention.state === "snoozed") {
|
|
8387
|
+
snoozeStore.remove([{ kind: "mention", ref: row.ref }]);
|
|
8388
|
+
}
|
|
8389
|
+
reportMark(`marked ${row.ref} read`, readStore.markRead(row.ref, row.mention.mark));
|
|
8390
|
+
};
|
|
8391
|
+
const markAllRead = (rows) => {
|
|
8392
|
+
const marks = rows.flatMap((row) => row.mention === void 0 ? [] : [{ ref: row.ref, mark: row.mention.mark }]);
|
|
8393
|
+
reportMark(
|
|
8394
|
+
`marked ${marks.length} ${marks.length === 1 ? "mention" : "mentions"} read`,
|
|
8395
|
+
readStore.markAllRead(marks)
|
|
8396
|
+
);
|
|
8397
|
+
};
|
|
8398
|
+
const markUnread = (row) => {
|
|
8399
|
+
reportMark(`marked ${row.ref} unread`, readStore.markUnread(row.ref));
|
|
7374
8400
|
};
|
|
7375
8401
|
useEffect8(() => {
|
|
7376
8402
|
if (ui.successNotice === null) {
|
|
@@ -7383,32 +8409,51 @@ function App({
|
|
|
7383
8409
|
clearTimeout(timer);
|
|
7384
8410
|
};
|
|
7385
8411
|
}, [ui.successNotice]);
|
|
7386
|
-
const reviewScrollRef =
|
|
7387
|
-
const sizeScrollRef =
|
|
7388
|
-
const commentScrollRef =
|
|
7389
|
-
const mergedScrollRef =
|
|
8412
|
+
const reviewScrollRef = useRef8(null);
|
|
8413
|
+
const sizeScrollRef = useRef8(null);
|
|
8414
|
+
const commentScrollRef = useRef8(null);
|
|
8415
|
+
const mergedScrollRef = useRef8(null);
|
|
7390
8416
|
const notifyReviewChanges = useReviewNotifications(notifications2, notifier, (message) => {
|
|
7391
8417
|
dispatchUi({ type: "openErrorReported", message });
|
|
7392
8418
|
});
|
|
7393
|
-
const
|
|
7394
|
-
|
|
7395
|
-
|
|
8419
|
+
const notifyMentionChanges = useMentionNotifications(
|
|
8420
|
+
trackMentions2 && notifications2 && notifyMentions2,
|
|
8421
|
+
notifier,
|
|
8422
|
+
(message) => {
|
|
8423
|
+
dispatchUi({ type: "openErrorReported", message });
|
|
7396
8424
|
},
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
}
|
|
7409
|
-
|
|
8425
|
+
!initialNoCache
|
|
8426
|
+
);
|
|
8427
|
+
const baselineKey = (data) => `${fetchParamsKey(options)} ${data.user.toLowerCase()}`;
|
|
8428
|
+
const { raw, isSnapshot, loading, load, error, stale, reload } = useLoader(
|
|
8429
|
+
options,
|
|
8430
|
+
{ noCache: noCache2, mentions: trackMentions2 },
|
|
8431
|
+
{
|
|
8432
|
+
onSnapshot: (data) => {
|
|
8433
|
+
readStore.observe(data);
|
|
8434
|
+
notifyReviewChanges(baselineKey(data), data.reviewResults);
|
|
8435
|
+
notifyMentionChanges(baselineKey(data), data.mentions, data.fetchedAt);
|
|
8436
|
+
},
|
|
8437
|
+
onLoaded: (data) => {
|
|
8438
|
+
dispatchBrowse({
|
|
8439
|
+
type: "dataLoaded",
|
|
8440
|
+
repos: {
|
|
8441
|
+
pending: buildPendingRepoOptions(data),
|
|
8442
|
+
reviewed: buildReviewedRepoOptions(data),
|
|
8443
|
+
mentions: buildMentionRepoOptions(data),
|
|
8444
|
+
open: buildOpenRepoOptions(data),
|
|
8445
|
+
review: buildReviewRepoOptions(data),
|
|
8446
|
+
size: buildSizeRepoOptions(data),
|
|
8447
|
+
comment: buildCommentRepoOptions(data),
|
|
8448
|
+
merged: buildMergedRepoOptions(data)
|
|
8449
|
+
}
|
|
8450
|
+
});
|
|
8451
|
+
readStore.observe(data);
|
|
8452
|
+
notifyReviewChanges(baselineKey(data), data.reviewResults);
|
|
8453
|
+
notifyMentionChanges(baselineKey(data), data.mentions, data.fetchedAt);
|
|
8454
|
+
}
|
|
7410
8455
|
}
|
|
7411
|
-
|
|
8456
|
+
);
|
|
7412
8457
|
useAutoReload(autoReload2 ? reloadIntervalMs(reloadInterval2) : null, loading, reload);
|
|
7413
8458
|
useSnoozeWakeups(snoozeStore.snoozes, raw !== null, () => {
|
|
7414
8459
|
if (raw === null) {
|
|
@@ -7418,8 +8463,8 @@ function App({
|
|
|
7418
8463
|
if (due.length === 0) {
|
|
7419
8464
|
return;
|
|
7420
8465
|
}
|
|
7421
|
-
const woken = wokenPrs(due, raw.reviewResults);
|
|
7422
|
-
snoozeStore.remove(due
|
|
8466
|
+
const woken = [...wokenPrs(due, raw.reviewResults), ...wokenMentions(due, raw.mentions ?? [], readStore.reads)];
|
|
8467
|
+
snoozeStore.remove(due);
|
|
7423
8468
|
if (!notifications2) {
|
|
7424
8469
|
return;
|
|
7425
8470
|
}
|
|
@@ -7437,10 +8482,11 @@ function App({
|
|
|
7437
8482
|
browse.grouped,
|
|
7438
8483
|
browse.expanded,
|
|
7439
8484
|
snoozeStore.snoozes,
|
|
8485
|
+
readStore.reads,
|
|
7440
8486
|
themeState
|
|
7441
8487
|
);
|
|
7442
8488
|
const showLoad = useDeferredLoading(loading, isSnapshot ? { showDelay: 0 } : void 0);
|
|
7443
|
-
const draftRef =
|
|
8489
|
+
const draftRef = useRef8("");
|
|
7444
8490
|
const commitField = () => {
|
|
7445
8491
|
const field = FIELDS[ui.selectedField];
|
|
7446
8492
|
const value2 = draftRef.current.trim();
|
|
@@ -7501,7 +8547,13 @@ function App({
|
|
|
7501
8547
|
const value2 = draftRef.current.trim();
|
|
7502
8548
|
try {
|
|
7503
8549
|
const until = Date.now() + parseSnoozeDuration(value2);
|
|
7504
|
-
const saved3 = snoozeStore.add({
|
|
8550
|
+
const saved3 = snoozeStore.add({
|
|
8551
|
+
kind: target.kind,
|
|
8552
|
+
ref: target.ref,
|
|
8553
|
+
until,
|
|
8554
|
+
at: target.at,
|
|
8555
|
+
...target.ids === void 0 ? {} : { ids: target.ids }
|
|
8556
|
+
});
|
|
7505
8557
|
dispatchUi({
|
|
7506
8558
|
type: "snoozeCommitted",
|
|
7507
8559
|
message: `snoozed ${target.ref} until ${formatWakeTime(until)}`,
|
|
@@ -7540,6 +8592,8 @@ function App({
|
|
|
7540
8592
|
autoReload: autoReload2,
|
|
7541
8593
|
reloadInterval: reloadInterval2,
|
|
7542
8594
|
notifications: notifications2,
|
|
8595
|
+
trackMentions: trackMentions2,
|
|
8596
|
+
notifyMentions: notifyMentions2,
|
|
7543
8597
|
notifyChannel: notifyChannel2,
|
|
7544
8598
|
copyLinks: copyLinks2,
|
|
7545
8599
|
snoozeDuration: snoozeDuration2,
|
|
@@ -7554,6 +8608,8 @@ function App({
|
|
|
7554
8608
|
setNoCache,
|
|
7555
8609
|
setAutoReload,
|
|
7556
8610
|
setNotifications,
|
|
8611
|
+
setTrackMentions,
|
|
8612
|
+
setNotifyMentions,
|
|
7557
8613
|
setNotifyChannel,
|
|
7558
8614
|
setCopyLinks,
|
|
7559
8615
|
setThemeState,
|
|
@@ -7563,6 +8619,9 @@ function App({
|
|
|
7563
8619
|
notify: notifier,
|
|
7564
8620
|
copyRow,
|
|
7565
8621
|
unsnooze,
|
|
8622
|
+
markRead,
|
|
8623
|
+
markAllRead,
|
|
8624
|
+
markUnread,
|
|
7566
8625
|
beginEdit: (value2) => {
|
|
7567
8626
|
draftRef.current = value2;
|
|
7568
8627
|
dispatchUi({ type: "editStarted" });
|
|
@@ -7611,10 +8670,8 @@ function App({
|
|
|
7611
8670
|
width,
|
|
7612
8671
|
modal: ui.modal,
|
|
7613
8672
|
editing: ui.editing,
|
|
7614
|
-
|
|
7615
|
-
authoredTab: browse.authoredTab,
|
|
8673
|
+
browse,
|
|
7616
8674
|
views,
|
|
7617
|
-
pendingCursor: browse.rowCursors.pending,
|
|
7618
8675
|
copyLinks: copyLinks2,
|
|
7619
8676
|
openError: ui.openError,
|
|
7620
8677
|
successNotice: ui.successNotice === null ? null : ui.successNotice.text,
|
|
@@ -7631,6 +8688,8 @@ function App({
|
|
|
7631
8688
|
autoReload: autoReload2,
|
|
7632
8689
|
reloadInterval: reloadInterval2,
|
|
7633
8690
|
notifications: notifications2,
|
|
8691
|
+
trackMentions: trackMentions2,
|
|
8692
|
+
notifyMentions: notifyMentions2,
|
|
7634
8693
|
notifyChannel: notifyChannel2,
|
|
7635
8694
|
copyLinks: copyLinks2,
|
|
7636
8695
|
snoozeDuration: snoozeDuration2,
|
|
@@ -7697,10 +8756,13 @@ function bootstrap() {
|
|
|
7697
8756
|
autoReload: settings.autoReload === true,
|
|
7698
8757
|
reloadInterval: settings.reloadInterval ?? DEFAULT_RELOAD_INTERVAL,
|
|
7699
8758
|
notifications: settings.notifications === true,
|
|
8759
|
+
trackMentions: settings.trackMentions !== false,
|
|
8760
|
+
notifyMentions: settings.notifyMentions === true,
|
|
7700
8761
|
notifyChannel: settings.notifyChannel ?? "auto",
|
|
7701
8762
|
copyLinks: settings.copyLinks === true,
|
|
7702
8763
|
snoozeDuration: settings.snoozeDuration ?? DEFAULT_SNOOZE_DURATION,
|
|
7703
8764
|
snoozes: readSnoozes(),
|
|
8765
|
+
mentionReads: readMentionReads(),
|
|
7704
8766
|
theme: theme3,
|
|
7705
8767
|
json: values.json
|
|
7706
8768
|
};
|
|
@@ -7721,10 +8783,13 @@ var {
|
|
|
7721
8783
|
autoReload,
|
|
7722
8784
|
reloadInterval,
|
|
7723
8785
|
notifications,
|
|
8786
|
+
trackMentions,
|
|
8787
|
+
notifyMentions,
|
|
7724
8788
|
notifyChannel,
|
|
7725
8789
|
copyLinks,
|
|
7726
8790
|
snoozeDuration,
|
|
7727
8791
|
snoozes,
|
|
8792
|
+
mentionReads,
|
|
7728
8793
|
theme: theme2,
|
|
7729
8794
|
json
|
|
7730
8795
|
} = bootstrap();
|
|
@@ -7749,10 +8814,13 @@ createRoot(renderer).render(
|
|
|
7749
8814
|
initialAutoReload: autoReload,
|
|
7750
8815
|
initialReloadInterval: reloadInterval,
|
|
7751
8816
|
initialNotifications: notifications,
|
|
8817
|
+
initialTrackMentions: trackMentions,
|
|
8818
|
+
initialNotifyMentions: notifyMentions,
|
|
7752
8819
|
initialNotifyChannel: notifyChannel,
|
|
7753
8820
|
initialCopyLinks: copyLinks,
|
|
7754
8821
|
initialSnoozeDuration: snoozeDuration,
|
|
7755
8822
|
initialSnoozes: snoozes,
|
|
8823
|
+
initialMentionReads: mentionReads,
|
|
7756
8824
|
initialTheme: theme2,
|
|
7757
8825
|
onQuit: () => {
|
|
7758
8826
|
renderer.destroy();
|