better-review 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ seulement pour les PR qui me concernent. Recherche GitHub limitée à 1000 résu
|
|
|
60
60
|
| `WEB_PORT` | `5173` | Port Vite en développement |
|
|
61
61
|
| `GITHUB_TOKEN` | `gh auth token` | Token GitHub |
|
|
62
62
|
| `BR_CONFIG_DIR` | `~/.config/better-review` | `config.json` (repos / orgs suivis, `staleDays`) — éditable à la main, rechargé à chaud |
|
|
63
|
-
| `BR_DATA_DIR` | `~/.local/share/better-review` | `state.db` (SQLite : visites, hunks vus,
|
|
63
|
+
| `BR_DATA_DIR` | `~/.local/share/better-review` | `state.db` (SQLite : visites, hunks vus, commentaires lus, cache des diffs) |
|
|
64
64
|
| `BR_SESSION_TOKEN` | `BR_DATA_DIR/session-token` | Jeton de session imposé (scripts, tests) |
|
|
65
65
|
|
|
66
66
|
## Raccourcis (vue PR)
|
|
@@ -71,13 +71,30 @@ vu suivant), comme le bouton « Vu » d'un fichier · `h` hunk vu · `r` répond
|
|
|
71
71
|
`?` aide.
|
|
72
72
|
|
|
73
73
|
Commenter : clic sur un numéro de ligne (« + » au survol), côté ancien code pour une ligne supprimée ou côté
|
|
74
|
-
nouveau code pour une ligne ajoutée, `Maj+clic` pour une plage ; bouton « Commenter » dans
|
|
75
|
-
pour le fichier entier. Chaque commentaire part seul ou dans la review en attente.
|
|
74
|
+
nouveau code pour une ligne ajoutée, `Maj+clic` pour une plage (dans un même hunk) ; bouton « Commenter » dans
|
|
75
|
+
l'en-tête d'un fichier pour le fichier entier. Chaque commentaire part seul ou dans la review en attente. GitHub ne
|
|
76
|
+
place un commentaire que sur une ligne du diff de la PR : dans une plage « depuis ma dernière visite / review » ou un
|
|
77
|
+
commit, seules les lignes du nouveau code que ce diff montre aussi sont commentables.
|
|
76
78
|
|
|
77
79
|
Réactions emoji (celles de GitHub) sous chaque commentaire, review et la description : clic sur une réaction pour
|
|
78
|
-
ajouter / retirer la mienne, « ☺ » pour en choisir une autre
|
|
79
|
-
messages ») et s'ouvrent quand on y navigue. La CI de l'en-tête
|
|
80
|
-
toutes les 15 s tant que des checks tournent.
|
|
80
|
+
ajouter / retirer la mienne, « ☺ » pour en choisir une autre ; au survol, qui a réagi (noms chargés à ce moment-là).
|
|
81
|
+
Les conversations résolues sont repliées (« Afficher N messages ») et s'ouvrent quand on y navigue. La CI de l'en-tête
|
|
82
|
+
met les échecs en avant (détail au clic) et est sondée toutes les 15 s tant que des checks tournent.
|
|
83
|
+
|
|
84
|
+
Commentaires non lus (liseré orange) : leur bouton « Vu » se remplit pendant le temps de lecture estimé, tant que le
|
|
85
|
+
commentaire est à l'écran ; plein, il passe en « ✓ Vu » et le commentaire est lu (clic : lu tout de suite). Un thread se
|
|
86
|
+
lit dans l'ordre et pas quand il est replié ; répondre le lit jusqu'à la réponse. Ce qui précède ma première visite de la
|
|
87
|
+
PR, ou mon dernier commentaire / ma dernière review, compte comme lu. À l'accueil, « non lue » = jamais visitée, nouveaux
|
|
88
|
+
commits, ou dernier mot d'un autre (thread ou conversation) pas encore lu.
|
|
89
|
+
|
|
90
|
+
Liens GitHub dans les commentaires, reviews et la description : ils s'ouvrent dans better-review quand c'est possible —
|
|
91
|
+
fichier ou lignes de la PR (`/files#diff-…`, `/blob/<head ou branche>/…#L10-L20`, surlignées en jaune), conversation
|
|
92
|
+
(`#discussion_r…`), commit de la PR, autre PR. Une plage qui ne montre pas la cible passe à « Tous les changements ». Le
|
|
93
|
+
reste (issues, fichiers hors PR, sites externes) s'ouvre dans un nouvel onglet ; Ctrl / Cmd+clic garde le lien GitHub.
|
|
94
|
+
|
|
95
|
+
Plage : une PR s'ouvre toujours sur « Tous les changements ». « Depuis ma dernière visite / review » se choisit dans
|
|
96
|
+
le sélecteur ; ces deltas sont désactivés quand la branche a été rebasée depuis (l'ancien head n'est plus dans la PR et
|
|
97
|
+
le diff montrerait aussi les changements de la branche cible).
|
|
81
98
|
|
|
82
99
|
## Naviguer dans les fichiers
|
|
83
100
|
|
package/dist/cli.js
CHANGED
|
@@ -242,8 +242,8 @@ var recordBodyBufferedBeforeDisconnect = (incoming) => {
|
|
|
242
242
|
const incomingWithRecovery = incoming;
|
|
243
243
|
incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey] ??= incoming.readableLength;
|
|
244
244
|
};
|
|
245
|
-
var readBodyBufferedBeforeDisconnect = (incoming,
|
|
246
|
-
if (incoming.readableDidRead && !
|
|
245
|
+
var readBodyBufferedBeforeDisconnect = (incoming, chunks2) => {
|
|
246
|
+
if (incoming.readableDidRead && !chunks2 || !isRecoverableDisconnectedIncoming(incoming)) return;
|
|
247
247
|
const incomingWithRecovery = incoming;
|
|
248
248
|
if (incomingWithRecovery[bodyBufferedBeforeDisconnectKey] !== void 0) return incomingWithRecovery[bodyBufferedBeforeDisconnectKey];
|
|
249
249
|
let result;
|
|
@@ -251,7 +251,7 @@ var readBodyBufferedBeforeDisconnect = (incoming, chunks) => {
|
|
|
251
251
|
if (errored && errored.code !== "ECONNRESET") result = errored;
|
|
252
252
|
else if (incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey] !== void 0 && incoming.readableLength !== incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey]) result = newBodyUnusableError();
|
|
253
253
|
else {
|
|
254
|
-
const bodyChunks =
|
|
254
|
+
const bodyChunks = chunks2 ?? [];
|
|
255
255
|
const chunk = incoming.read();
|
|
256
256
|
if (chunk !== null) bodyChunks.push(toBufferChunk(chunk, incoming.readableEncoding));
|
|
257
257
|
const buffer = bodyChunks.length === 1 ? bodyChunks[0] : Buffer.concat(bodyChunks);
|
|
@@ -427,7 +427,7 @@ var readBodyDirect = (request) => {
|
|
|
427
427
|
return Promise.resolve(buffered);
|
|
428
428
|
}
|
|
429
429
|
const promise2 = new Promise((resolve, reject) => {
|
|
430
|
-
const
|
|
430
|
+
const chunks2 = [];
|
|
431
431
|
let settled = false;
|
|
432
432
|
const finish = (callback) => {
|
|
433
433
|
if (settled) return;
|
|
@@ -439,7 +439,7 @@ var readBodyDirect = (request) => {
|
|
|
439
439
|
const streamError = incoming.errored ?? error62;
|
|
440
440
|
if (!isRecoverableDisconnectedIncoming(incoming) || streamError && streamError.code !== "ECONNRESET") return false;
|
|
441
441
|
finish(() => {
|
|
442
|
-
const recovered = readBodyBufferedBeforeDisconnect(incoming,
|
|
442
|
+
const recovered = readBodyBufferedBeforeDisconnect(incoming, chunks2);
|
|
443
443
|
if (recovered instanceof Error) reject(recovered);
|
|
444
444
|
else if (recovered === void 0) reject(error62 ?? normalizeAbortError(request, incoming));
|
|
445
445
|
else {
|
|
@@ -450,11 +450,11 @@ var readBodyDirect = (request) => {
|
|
|
450
450
|
return true;
|
|
451
451
|
};
|
|
452
452
|
const onData = (chunk) => {
|
|
453
|
-
|
|
453
|
+
chunks2.push(toBufferChunk(chunk, incoming.readableEncoding));
|
|
454
454
|
};
|
|
455
455
|
const onEnd = () => {
|
|
456
456
|
finish(() => {
|
|
457
|
-
const buffer =
|
|
457
|
+
const buffer = chunks2.length === 1 ? chunks2[0] : Buffer.concat(chunks2);
|
|
458
458
|
request[bodyBufferKey] = buffer;
|
|
459
459
|
resolve(buffer);
|
|
460
460
|
});
|
|
@@ -25806,6 +25806,28 @@ var REACTION_FIELDS = (
|
|
|
25806
25806
|
}
|
|
25807
25807
|
`
|
|
25808
25808
|
);
|
|
25809
|
+
var REACTORS_QUERY = (
|
|
25810
|
+
/* GraphQL */
|
|
25811
|
+
`
|
|
25812
|
+
query Reactors($id: ID!, $first: Int!) {
|
|
25813
|
+
node(id: $id) {
|
|
25814
|
+
... on Reactable {
|
|
25815
|
+
reactionGroups {
|
|
25816
|
+
content
|
|
25817
|
+
reactors(first: $first) {
|
|
25818
|
+
totalCount
|
|
25819
|
+
nodes {
|
|
25820
|
+
... on Actor {
|
|
25821
|
+
login
|
|
25822
|
+
}
|
|
25823
|
+
}
|
|
25824
|
+
}
|
|
25825
|
+
}
|
|
25826
|
+
}
|
|
25827
|
+
}
|
|
25828
|
+
}
|
|
25829
|
+
`
|
|
25830
|
+
);
|
|
25809
25831
|
var REVIEW_COMMENT_FIELDS = (
|
|
25810
25832
|
/* GraphQL */
|
|
25811
25833
|
`
|
|
@@ -26214,6 +26236,7 @@ var CHECKS_QUERY = (
|
|
|
26214
26236
|
${CHECKS_FIELDS}
|
|
26215
26237
|
reviewThreads(first: 100) {
|
|
26216
26238
|
nodes {
|
|
26239
|
+
id
|
|
26217
26240
|
isResolved
|
|
26218
26241
|
}
|
|
26219
26242
|
}
|
|
@@ -26234,12 +26257,14 @@ var DETAILS_QUERY = (
|
|
|
26234
26257
|
${CHECKS_FIELDS}
|
|
26235
26258
|
viewerLatestReview {
|
|
26236
26259
|
state
|
|
26260
|
+
submittedAt
|
|
26237
26261
|
commit {
|
|
26238
26262
|
oid
|
|
26239
26263
|
}
|
|
26240
26264
|
}
|
|
26241
26265
|
comments(last: 1) {
|
|
26242
26266
|
nodes {
|
|
26267
|
+
id
|
|
26243
26268
|
author {
|
|
26244
26269
|
login
|
|
26245
26270
|
}
|
|
@@ -26248,9 +26273,11 @@ var DETAILS_QUERY = (
|
|
|
26248
26273
|
}
|
|
26249
26274
|
reviewThreads(first: 100) {
|
|
26250
26275
|
nodes {
|
|
26276
|
+
id
|
|
26251
26277
|
isResolved
|
|
26252
26278
|
comments(last: 1) {
|
|
26253
26279
|
nodes {
|
|
26280
|
+
id
|
|
26254
26281
|
author {
|
|
26255
26282
|
login
|
|
26256
26283
|
}
|
|
@@ -26314,6 +26341,14 @@ var INBOX_FINGERPRINT_QUERY = (
|
|
|
26314
26341
|
`
|
|
26315
26342
|
);
|
|
26316
26343
|
|
|
26344
|
+
// ../server/src/readState.ts
|
|
26345
|
+
function readBaseline(firstVisitAt, myActivity) {
|
|
26346
|
+
if (firstVisitAt === null) return Number.POSITIVE_INFINITY;
|
|
26347
|
+
let at = firstVisitAt;
|
|
26348
|
+
for (const date5 of myActivity) if (date5) at = Math.max(at, Date.parse(date5));
|
|
26349
|
+
return at;
|
|
26350
|
+
}
|
|
26351
|
+
|
|
26317
26352
|
// ../server/src/inbox.ts
|
|
26318
26353
|
var MERGED_WINDOW_MS = 24 * 36e5;
|
|
26319
26354
|
function scopeTerms(config2) {
|
|
@@ -26322,9 +26357,9 @@ function scopeTerms(config2) {
|
|
|
26322
26357
|
var SCOPE_CHUNK = 20;
|
|
26323
26358
|
function scopeChunks(config2) {
|
|
26324
26359
|
const terms = scopeTerms(config2);
|
|
26325
|
-
const
|
|
26326
|
-
for (let i = 0; i < terms.length; i += SCOPE_CHUNK)
|
|
26327
|
-
return
|
|
26360
|
+
const chunks2 = [];
|
|
26361
|
+
for (let i = 0; i < terms.length; i += SCOPE_CHUNK) chunks2.push(terms.slice(i, i + SCOPE_CHUNK).join(" "));
|
|
26362
|
+
return chunks2;
|
|
26328
26363
|
}
|
|
26329
26364
|
function searchQueries(scope, mergedSince) {
|
|
26330
26365
|
const open2 = `is:pr is:open archived:false ${scope}`;
|
|
@@ -26344,27 +26379,35 @@ function isApproved(decision, states) {
|
|
|
26344
26379
|
if (decision !== null) return decision === "APPROVED";
|
|
26345
26380
|
return states.includes("APPROVED") && !states.includes("CHANGES_REQUESTED");
|
|
26346
26381
|
}
|
|
26347
|
-
function toInboxPr(node2, me,
|
|
26382
|
+
function toInboxPr(node2, me, reads) {
|
|
26348
26383
|
const [owner = "", repo = ""] = node2.repository.nameWithOwner.split("/");
|
|
26384
|
+
const key = prKey(owner, repo, node2.number);
|
|
26385
|
+
const visit2 = reads.lastVisits.get(key);
|
|
26386
|
+
const myReview = node2.viewerLatestReview;
|
|
26387
|
+
const lastComment = node2.comments?.nodes[0];
|
|
26388
|
+
const lastInThreads = (node2.reviewThreads?.nodes ?? []).flatMap((t2) => {
|
|
26389
|
+
const last = t2.comments?.nodes[0];
|
|
26390
|
+
return last ? [{ threadId: t2.id, last }] : [];
|
|
26391
|
+
});
|
|
26392
|
+
const isMine = (c) => c.author?.login === me;
|
|
26393
|
+
const baseline = readBaseline(reads.firstVisits.get(key) ?? null, [
|
|
26394
|
+
myReview?.submittedAt ?? null,
|
|
26395
|
+
...lastInThreads.filter(({ last }) => isMine(last)).map(({ last }) => last.createdAt),
|
|
26396
|
+
...lastComment && isMine(lastComment) ? [lastComment.createdAt] : []
|
|
26397
|
+
]);
|
|
26398
|
+
const unreadBy = (c, read) => !isMine(c) && Date.parse(c.createdAt) > baseline && !read;
|
|
26399
|
+
const unreadComment = lastInThreads.some(({ threadId, last }) => unreadBy(last, reads.threads.get(threadId) === last.id)) || lastComment !== void 0 && unreadBy(lastComment, reads.entries.has(lastComment.id));
|
|
26349
26400
|
let threads;
|
|
26350
|
-
let lastOtherActivity = 0;
|
|
26351
26401
|
if (node2.reviewThreads) {
|
|
26352
26402
|
threads = { total: node2.reviewThreads.nodes.length, open: 0, awaitingMyReply: 0 };
|
|
26353
26403
|
for (const thread of node2.reviewThreads.nodes) {
|
|
26354
26404
|
const last = thread.comments?.nodes[0];
|
|
26355
|
-
const lastIsOther = last?.author?.login !== me;
|
|
26356
26405
|
if (!thread.isResolved) {
|
|
26357
26406
|
threads.open++;
|
|
26358
|
-
if (last &&
|
|
26407
|
+
if (last && !isMine(last)) threads.awaitingMyReply++;
|
|
26359
26408
|
}
|
|
26360
|
-
if (last && lastIsOther) lastOtherActivity = Math.max(lastOtherActivity, Date.parse(last.createdAt));
|
|
26361
26409
|
}
|
|
26362
26410
|
}
|
|
26363
|
-
const lastComment = node2.comments?.nodes[0];
|
|
26364
|
-
if (lastComment && lastComment.author?.login !== me) {
|
|
26365
|
-
lastOtherActivity = Math.max(lastOtherActivity, Date.parse(lastComment.createdAt));
|
|
26366
|
-
}
|
|
26367
|
-
const myReview = node2.viewerLatestReview;
|
|
26368
26411
|
return {
|
|
26369
26412
|
id: node2.id,
|
|
26370
26413
|
owner,
|
|
@@ -26400,7 +26443,7 @@ function toInboxPr(node2, me, visit2) {
|
|
|
26400
26443
|
}
|
|
26401
26444
|
} : {},
|
|
26402
26445
|
lastVisitAt: visit2?.visitedAt ?? null,
|
|
26403
|
-
unread: !visit2 || visit2.headOid !== node2.headRefOid ||
|
|
26446
|
+
unread: !visit2 || visit2.headOid !== node2.headRefOid || unreadComment
|
|
26404
26447
|
};
|
|
26405
26448
|
}
|
|
26406
26449
|
function stageOf(pr) {
|
|
@@ -26417,12 +26460,11 @@ function stageOf(pr) {
|
|
|
26417
26460
|
const reviewed = pr.reviewDecision === "CHANGES_REQUESTED" || pr.reviewCount > 0 || pr.reviews.length > 0 || (pr.threads?.total ?? 0) > 0;
|
|
26418
26461
|
return reviewed ? "review" : "waiting";
|
|
26419
26462
|
}
|
|
26420
|
-
function buildBoard(sources, me,
|
|
26463
|
+
function buildBoard(sources, me, reads, staleBefore) {
|
|
26421
26464
|
const nodes = /* @__PURE__ */ new Map();
|
|
26422
26465
|
for (const node2 of [...sources.open, ...sources.merged]) nodes.set(node2.id, node2);
|
|
26423
26466
|
const prs = [...nodes.values()].map((node2) => {
|
|
26424
|
-
const
|
|
26425
|
-
const pr = toInboxPr(node2, me, visits.get(prKey(owner, repo, node2.number)));
|
|
26467
|
+
const pr = toInboxPr(node2, me, reads);
|
|
26426
26468
|
const stage = stageOf(pr);
|
|
26427
26469
|
const mine = pr.author?.login === me;
|
|
26428
26470
|
const tags = mine ? ["mine"] : [];
|
|
@@ -26535,18 +26577,29 @@ async function loadSources(gh, config2, now) {
|
|
|
26535
26577
|
sources: { open: nodesOf(openIds), merged: nodesOf(mergedIds), requested, involved }
|
|
26536
26578
|
};
|
|
26537
26579
|
}
|
|
26538
|
-
async function loadInbox(gh, config2, viewerFallback,
|
|
26580
|
+
async function loadInbox(gh, config2, viewerFallback, store) {
|
|
26539
26581
|
const now = Date.now();
|
|
26540
26582
|
const { viewer: viewer2, truncated, sources } = await loadSources(gh, config2, now);
|
|
26541
26583
|
const me = viewer2 ?? await viewerFallback();
|
|
26542
|
-
const
|
|
26543
|
-
|
|
26544
|
-
|
|
26545
|
-
|
|
26584
|
+
const all = [...sources.open, ...sources.merged];
|
|
26585
|
+
const keys = [
|
|
26586
|
+
...new Set(
|
|
26587
|
+
all.map((n) => {
|
|
26588
|
+
const [owner = "", repo = ""] = n.repository.nameWithOwner.split("/");
|
|
26589
|
+
return prKey(owner, repo, n.number);
|
|
26590
|
+
})
|
|
26591
|
+
)
|
|
26592
|
+
];
|
|
26593
|
+
const reads = {
|
|
26594
|
+
lastVisits: store.lastVisits(keys),
|
|
26595
|
+
firstVisits: store.firstVisits(keys),
|
|
26596
|
+
threads: store.threadReads(all.flatMap((n) => n.reviewThreads?.nodes.map((t2) => t2.id) ?? [])),
|
|
26597
|
+
entries: store.entryReads(all.flatMap((n) => n.comments?.nodes.map((c) => c.id) ?? []))
|
|
26598
|
+
};
|
|
26546
26599
|
return {
|
|
26547
26600
|
viewer: me,
|
|
26548
26601
|
configured: config2.repos.length + config2.orgs.length > 0,
|
|
26549
|
-
prs: buildBoard(sources, me.login,
|
|
26602
|
+
prs: buildBoard(sources, me.login, reads, now - config2.staleDays * 864e5),
|
|
26550
26603
|
truncated,
|
|
26551
26604
|
fetchedAt: now
|
|
26552
26605
|
};
|
|
@@ -26597,11 +26650,19 @@ async function loadPr(gh, store, ref) {
|
|
|
26597
26650
|
)
|
|
26598
26651
|
]);
|
|
26599
26652
|
const reads = store.threadReads(threadNodes.map((t2) => t2.id));
|
|
26653
|
+
const entryIds = [...pr.comments.nodes.map((c) => c.id), ...pr.reviews.nodes.map((r) => r.id)];
|
|
26654
|
+
const entriesRead = store.entryReads(entryIds);
|
|
26600
26655
|
const visits = store.visits(key);
|
|
26601
|
-
const lastVisitAt = visits[0]?.visitedAt ?? 0;
|
|
26602
26656
|
const seenCommits = store.seenCommits(key);
|
|
26603
26657
|
const pending = pr.reviews.nodes.find((r) => r.state === "PENDING" && r.author?.login === me) ?? null;
|
|
26604
|
-
const
|
|
26658
|
+
const mine = (author) => author?.login === me;
|
|
26659
|
+
const baseline = readBaseline(store.firstVisits([key]).get(key) ?? null, [
|
|
26660
|
+
...threadNodes.flatMap((t2) => t2.comments.nodes.filter((c) => mine(c.author)).map((c) => c.createdAt)),
|
|
26661
|
+
...pr.comments.nodes.filter((c) => mine(c.author)).map((c) => c.createdAt),
|
|
26662
|
+
...pr.reviews.nodes.filter((r) => r.state !== "PENDING" && mine(r.author)).map((r) => r.submittedAt)
|
|
26663
|
+
]);
|
|
26664
|
+
const entryUnread = (id, author, createdAt) => !mine(author) && Date.parse(createdAt) > baseline && !entriesRead.has(id);
|
|
26665
|
+
const threads = threadNodes.map((t2) => toThread(t2, me, { lastReadId: reads.get(t2.id), baseline }));
|
|
26605
26666
|
const pendingComments = threads.reduce((n, t2) => n + t2.comments.filter((c) => c.pending).length, 0);
|
|
26606
26667
|
const reviewers = /* @__PURE__ */ new Map();
|
|
26607
26668
|
for (const r of pr.latestOpinionatedReviews.nodes) {
|
|
@@ -26624,7 +26685,7 @@ async function loadPr(gh, store, ref) {
|
|
|
26624
26685
|
reactions: toReactions(c.reactionGroups),
|
|
26625
26686
|
createdAt: c.createdAt,
|
|
26626
26687
|
author: c.author,
|
|
26627
|
-
unread: c.author
|
|
26688
|
+
unread: entryUnread(c.id, c.author, c.createdAt)
|
|
26628
26689
|
})),
|
|
26629
26690
|
...pr.reviews.nodes.filter((r) => r.state !== "PENDING" && (r.body.trim() !== "" || r.state !== "COMMENTED")).map((r) => ({
|
|
26630
26691
|
kind: "review",
|
|
@@ -26636,7 +26697,7 @@ async function loadPr(gh, store, ref) {
|
|
|
26636
26697
|
reactions: toReactions(r.reactionGroups),
|
|
26637
26698
|
createdAt: r.submittedAt ?? r.createdAt,
|
|
26638
26699
|
author: r.author,
|
|
26639
|
-
unread: r.author
|
|
26700
|
+
unread: entryUnread(r.id, r.author, r.submittedAt ?? r.createdAt)
|
|
26640
26701
|
}))
|
|
26641
26702
|
].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
26642
26703
|
return {
|
|
@@ -26702,6 +26763,19 @@ async function loadPr(gh, store, ref) {
|
|
|
26702
26763
|
function toReactions(groups) {
|
|
26703
26764
|
return (groups ?? []).filter((g) => g.reactors.totalCount > 0).map((g) => ({ content: g.content, count: g.reactors.totalCount, mine: g.viewerHasReacted }));
|
|
26704
26765
|
}
|
|
26766
|
+
var REACTORS_SHOWN = 20;
|
|
26767
|
+
async function loadReactors(gh, subjectId) {
|
|
26768
|
+
const data = await gh.graphql(REACTORS_QUERY, {
|
|
26769
|
+
id: subjectId,
|
|
26770
|
+
first: REACTORS_SHOWN
|
|
26771
|
+
});
|
|
26772
|
+
return (data.node?.reactionGroups ?? []).filter((g) => g.reactors.totalCount > 0).map((g) => ({
|
|
26773
|
+
content: g.content,
|
|
26774
|
+
// Deleted accounts come back null.
|
|
26775
|
+
logins: g.reactors.nodes.flatMap((n) => n?.login ? [n.login] : []),
|
|
26776
|
+
total: g.reactors.totalCount
|
|
26777
|
+
}));
|
|
26778
|
+
}
|
|
26705
26779
|
function toComment(c) {
|
|
26706
26780
|
return {
|
|
26707
26781
|
id: c.id,
|
|
@@ -26712,16 +26786,19 @@ function toComment(c) {
|
|
|
26712
26786
|
reactions: toReactions(c.reactionGroups),
|
|
26713
26787
|
createdAt: c.createdAt,
|
|
26714
26788
|
author: c.author,
|
|
26789
|
+
unread: false,
|
|
26715
26790
|
pending: c.state === "PENDING",
|
|
26716
26791
|
originalCommitOid: c.originalCommit?.oid ?? null,
|
|
26717
26792
|
canUpdate: c.viewerCanUpdate,
|
|
26718
26793
|
canDelete: c.viewerCanDelete
|
|
26719
26794
|
};
|
|
26720
26795
|
}
|
|
26721
|
-
function toThread(t2, me,
|
|
26722
|
-
const
|
|
26723
|
-
const
|
|
26724
|
-
|
|
26796
|
+
function toThread(t2, me, read) {
|
|
26797
|
+
const readIdx = read.lastReadId ? t2.comments.nodes.findIndex((c) => c.id === read.lastReadId) : -1;
|
|
26798
|
+
const comments = t2.comments.nodes.map((node2, i) => ({
|
|
26799
|
+
...toComment(node2),
|
|
26800
|
+
unread: i > readIdx && node2.author?.login !== me && node2.state !== "PENDING" && Date.parse(node2.createdAt) > read.baseline
|
|
26801
|
+
}));
|
|
26725
26802
|
return {
|
|
26726
26803
|
id: t2.id,
|
|
26727
26804
|
path: t2.path,
|
|
@@ -26739,8 +26816,7 @@ function toThread(t2, me, lastReadId) {
|
|
|
26739
26816
|
canReply: t2.viewerCanReply,
|
|
26740
26817
|
diffHunk: t2.comments.nodes[0]?.diffHunk ?? "",
|
|
26741
26818
|
comments,
|
|
26742
|
-
unread,
|
|
26743
|
-
lastReadCommentId: readIdx === -1 ? null : lastReadId ?? null,
|
|
26819
|
+
unread: comments.filter((c) => c.unread).length,
|
|
26744
26820
|
pending: comments.length > 0 && comments.every((c) => c.pending)
|
|
26745
26821
|
};
|
|
26746
26822
|
}
|
|
@@ -26901,12 +26977,21 @@ async function addThread(gh, prId, pendingReviewId, thread, mode) {
|
|
|
26901
26977
|
${THREAD_FIELDS}`,
|
|
26902
26978
|
{ input: { pullRequestReviewId: reviewId, ...threadInput(thread) } }
|
|
26903
26979
|
);
|
|
26980
|
+
if (!data.addPullRequestReviewThread.thread) {
|
|
26981
|
+
throw new GitHubError(
|
|
26982
|
+
`GitHub n\u2019a pas pu placer le commentaire sur ${thread.path} : position hors du diff de la PR.`,
|
|
26983
|
+
422
|
|
26984
|
+
);
|
|
26985
|
+
}
|
|
26904
26986
|
node2 = data.addPullRequestReviewThread.thread;
|
|
26905
26987
|
} catch (error62) {
|
|
26906
26988
|
if (!pendingReviewId) await discardReview(gh, reviewId).catch(() => void 0);
|
|
26907
26989
|
throw error62;
|
|
26908
26990
|
}
|
|
26909
|
-
const created = toThread(node2, "",
|
|
26991
|
+
const created = toThread(node2, "", {
|
|
26992
|
+
lastReadId: node2.comments.nodes.at(-1)?.id,
|
|
26993
|
+
baseline: Number.POSITIVE_INFINITY
|
|
26994
|
+
});
|
|
26910
26995
|
if (mode === "review") return { thread: created, pendingReviewId: reviewId };
|
|
26911
26996
|
await submitReview(gh, prId, reviewId, "COMMENT", "");
|
|
26912
26997
|
return {
|
|
@@ -28492,14 +28577,7 @@ var configRouter = router({
|
|
|
28492
28577
|
remove: procedure.input(trackedEntrySchema).mutation(({ ctx, input: input2 }) => ctx.config.remove(input2))
|
|
28493
28578
|
});
|
|
28494
28579
|
var inboxRouter = router({
|
|
28495
|
-
get: procedure.query(
|
|
28496
|
-
({ ctx }) => loadInbox(
|
|
28497
|
-
ctx.gh,
|
|
28498
|
-
ctx.config.get(),
|
|
28499
|
-
() => viewer(ctx),
|
|
28500
|
-
(keys) => ctx.store.lastVisits(keys)
|
|
28501
|
-
)
|
|
28502
|
-
),
|
|
28580
|
+
get: procedure.query(({ ctx }) => loadInbox(ctx.gh, ctx.config.get(), () => viewer(ctx), ctx.store)),
|
|
28503
28581
|
onChange: procedure.subscription(async function* ({ ctx, signal }) {
|
|
28504
28582
|
yield* pollChanges(
|
|
28505
28583
|
INBOX_POLL_MS,
|
|
@@ -28543,6 +28621,9 @@ var prRouter = router({
|
|
|
28543
28621
|
markThreadsRead: procedure.input(external_exports.array(external_exports.object({ threadId: external_exports.string(), lastCommentId: external_exports.string() }))).mutation(({ ctx, input: input2 }) => {
|
|
28544
28622
|
ctx.store.markThreadsRead(input2);
|
|
28545
28623
|
}),
|
|
28624
|
+
markEntriesRead: procedure.input(external_exports.array(external_exports.string()).min(1)).mutation(({ ctx, input: input2 }) => {
|
|
28625
|
+
ctx.store.markEntriesRead(input2);
|
|
28626
|
+
}),
|
|
28546
28627
|
reply: procedure.input(
|
|
28547
28628
|
external_exports.object({
|
|
28548
28629
|
threadId: external_exports.string(),
|
|
@@ -28554,6 +28635,7 @@ var prRouter = router({
|
|
|
28554
28635
|
),
|
|
28555
28636
|
setResolved: procedure.input(external_exports.object({ threadId: external_exports.string(), resolved: external_exports.boolean() })).mutation(({ ctx, input: input2 }) => setThreadResolved(ctx.gh, input2.threadId, input2.resolved)),
|
|
28556
28637
|
react: procedure.input(external_exports.object({ subjectId: external_exports.string(), content: reactionContentSchema, on: external_exports.boolean() })).mutation(({ ctx, input: input2 }) => setReaction(ctx.gh, input2.subjectId, input2.content, input2.on)),
|
|
28638
|
+
reactors: procedure.input(external_exports.object({ subjectId: external_exports.string() })).query(({ ctx, input: input2 }) => loadReactors(ctx.gh, input2.subjectId)),
|
|
28557
28639
|
addThread: procedure.input(
|
|
28558
28640
|
external_exports.object({
|
|
28559
28641
|
prId: external_exports.string(),
|
|
@@ -28763,8 +28845,24 @@ var MIGRATIONS = [
|
|
|
28763
28845
|
thread_id TEXT PRIMARY KEY,
|
|
28764
28846
|
last_comment_id TEXT NOT NULL,
|
|
28765
28847
|
read_at INTEGER NOT NULL
|
|
28766
|
-
)
|
|
28848
|
+
);`,
|
|
28849
|
+
// Read state of conversation entries (PR comments, reviews); first visit as the « already read » line.
|
|
28850
|
+
`CREATE TABLE entry_read (
|
|
28851
|
+
subject_id TEXT PRIMARY KEY,
|
|
28852
|
+
read_at INTEGER NOT NULL
|
|
28853
|
+
);
|
|
28854
|
+
CREATE TABLE pr_first_visit (
|
|
28855
|
+
pr_key TEXT PRIMARY KEY,
|
|
28856
|
+
visited_at INTEGER NOT NULL
|
|
28857
|
+
);
|
|
28858
|
+
INSERT INTO pr_first_visit (pr_key, visited_at) SELECT pr_key, MIN(visited_at) FROM pr_visit GROUP BY pr_key;`
|
|
28767
28859
|
];
|
|
28860
|
+
var IN_CHUNK = 500;
|
|
28861
|
+
function chunks(items) {
|
|
28862
|
+
const out = [];
|
|
28863
|
+
for (let i = 0; i < items.length; i += IN_CHUNK) out.push(items.slice(i, i + IN_CHUNK));
|
|
28864
|
+
return out;
|
|
28865
|
+
}
|
|
28768
28866
|
var Store = class {
|
|
28769
28867
|
db;
|
|
28770
28868
|
constructor(dir) {
|
|
@@ -28803,8 +28901,10 @@ var Store = class {
|
|
|
28803
28901
|
}
|
|
28804
28902
|
recordVisit(prKey2, headOid, commitOids) {
|
|
28805
28903
|
this.db.exec("BEGIN");
|
|
28904
|
+
const now = Date.now();
|
|
28806
28905
|
try {
|
|
28807
|
-
this.db.prepare("INSERT OR REPLACE INTO pr_visit (pr_key, head_oid, visited_at) VALUES (?, ?, ?)").run(prKey2, headOid,
|
|
28906
|
+
this.db.prepare("INSERT OR REPLACE INTO pr_visit (pr_key, head_oid, visited_at) VALUES (?, ?, ?)").run(prKey2, headOid, now);
|
|
28907
|
+
this.db.prepare("INSERT OR IGNORE INTO pr_first_visit (pr_key, visited_at) VALUES (?, ?)").run(prKey2, now);
|
|
28808
28908
|
const insert = this.db.prepare("INSERT OR IGNORE INTO commit_seen (pr_key, oid) VALUES (?, ?)");
|
|
28809
28909
|
for (const oid of commitOids) insert.run(prKey2, oid);
|
|
28810
28910
|
this.db.exec("COMMIT");
|
|
@@ -28837,13 +28937,26 @@ ${r.hash}`);
|
|
|
28837
28937
|
throw error62;
|
|
28838
28938
|
}
|
|
28839
28939
|
}
|
|
28940
|
+
/** First visit per PR in better-review: what was there before counts as read. */
|
|
28941
|
+
firstVisits(prKeys) {
|
|
28942
|
+
const result = /* @__PURE__ */ new Map();
|
|
28943
|
+
for (const keys of chunks(prKeys)) {
|
|
28944
|
+
const rows = this.db.prepare(
|
|
28945
|
+
`SELECT pr_key, visited_at FROM pr_first_visit WHERE pr_key IN (${keys.map(() => "?").join(",")})`
|
|
28946
|
+
).all(...keys);
|
|
28947
|
+
for (const row of rows) result.set(row.pr_key, row.visited_at);
|
|
28948
|
+
}
|
|
28949
|
+
return result;
|
|
28950
|
+
}
|
|
28951
|
+
/** Last comment read per thread. */
|
|
28840
28952
|
threadReads(threadIds) {
|
|
28841
28953
|
const result = /* @__PURE__ */ new Map();
|
|
28842
|
-
|
|
28843
|
-
|
|
28844
|
-
|
|
28845
|
-
|
|
28846
|
-
|
|
28954
|
+
for (const ids of chunks(threadIds)) {
|
|
28955
|
+
const rows = this.db.prepare(
|
|
28956
|
+
`SELECT thread_id, last_comment_id FROM thread_read WHERE thread_id IN (${ids.map(() => "?").join(",")})`
|
|
28957
|
+
).all(...ids);
|
|
28958
|
+
for (const row of rows) result.set(row.thread_id, row.last_comment_id);
|
|
28959
|
+
}
|
|
28847
28960
|
return result;
|
|
28848
28961
|
}
|
|
28849
28962
|
markThreadsRead(items) {
|
|
@@ -28853,6 +28966,20 @@ ${r.hash}`);
|
|
|
28853
28966
|
const now = Date.now();
|
|
28854
28967
|
for (const item of items) stmt.run(item.threadId, item.lastCommentId, now);
|
|
28855
28968
|
}
|
|
28969
|
+
/** Conversation entries (PR comments, reviews) read among `ids`. */
|
|
28970
|
+
entryReads(ids) {
|
|
28971
|
+
const result = /* @__PURE__ */ new Set();
|
|
28972
|
+
for (const chunk of chunks(ids)) {
|
|
28973
|
+
const rows = this.db.prepare(`SELECT subject_id FROM entry_read WHERE subject_id IN (${chunk.map(() => "?").join(",")})`).all(...chunk);
|
|
28974
|
+
for (const row of rows) result.add(row.subject_id);
|
|
28975
|
+
}
|
|
28976
|
+
return result;
|
|
28977
|
+
}
|
|
28978
|
+
markEntriesRead(ids) {
|
|
28979
|
+
const stmt = this.db.prepare("INSERT OR IGNORE INTO entry_read (subject_id, read_at) VALUES (?, ?)");
|
|
28980
|
+
const now = Date.now();
|
|
28981
|
+
for (const id of ids) stmt.run(id, now);
|
|
28982
|
+
}
|
|
28856
28983
|
};
|
|
28857
28984
|
|
|
28858
28985
|
// ../server/src/env.ts
|