@gperezmz/bb-plugin-thread-glance 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.css +1 -1
- package/dist/app.js +9 -9
- package/dist/app.meta.json +1 -1
- package/dist/server.js.map +1 -1
- package/dist/server.meta.json +1 -1
- package/features/thread-list/components/ThreadRowView.tsx +160 -129
- package/features/thread-list/components/glyphs.tsx +10 -29
- package/features/thread-list/model/labels.ts +20 -6
- package/features/thread-list/model/state.ts +9 -21
- package/features/thread-list/model/time.ts +7 -0
- package/features/thread-list/model/trees.ts +11 -11
- package/features/thread-list/model/view.ts +14 -12
- package/features/thread-list/testing/browser.css +13 -0
- package/package.json +5 -1
package/dist/server.meta.json
CHANGED
|
@@ -19,12 +19,12 @@ import { chipLabel, rowAriaLabel } from "../model/labels";
|
|
|
19
19
|
import { rowIndent } from "../model/layout";
|
|
20
20
|
import { rowMenuItems } from "../model/menu";
|
|
21
21
|
import { noteText } from "../model/notes";
|
|
22
|
-
import { pluginStatusWins } from "../model/state";
|
|
23
|
-
import { trailingTime } from "../model/time";
|
|
22
|
+
import { chipTone, pluginStatusWins } from "../model/state";
|
|
23
|
+
import { TRAILING_SLOT_SIZERS, trailingTime } from "../model/time";
|
|
24
24
|
import type { ThreadRow } from "../model/view";
|
|
25
25
|
import type { DraggedThread } from "../model/drag";
|
|
26
26
|
import type { RowController } from "./controller";
|
|
27
|
-
import {
|
|
27
|
+
import { ChipStateGlyph, GlyphIcon, NoteLine, PluginStatusGlyph, TONE_CLASS } from "./glyphs";
|
|
28
28
|
import { ProviderBadge } from "./ProviderBadge";
|
|
29
29
|
import { PullRequestBadge } from "./PullRequestBadge";
|
|
30
30
|
import { RenameEditor } from "./RenameEditor";
|
|
@@ -299,8 +299,12 @@ export const ThreadRowView = memo(function ThreadRowView({
|
|
|
299
299
|
const twoLines = note !== null || row.branchLine !== null;
|
|
300
300
|
const dimmed = row.dimmed && !editing;
|
|
301
301
|
const menuShowing = menuOpen || contextOpen;
|
|
302
|
-
//
|
|
303
|
-
|
|
302
|
+
// A pressed shortcut's pill takes the machine's place, as it takes the time's.
|
|
303
|
+
const showMachine = row.machine !== null && shortcut === null;
|
|
304
|
+
const badgesShown = row.harness || showMachine;
|
|
305
|
+
const actionsShown = !compact && shortcut === null;
|
|
306
|
+
// Desktop: on hover the harness and machine fade out for Mark read and
|
|
307
|
+
// Archive, and the time for "…". Compact: nothing fades.
|
|
304
308
|
const fadeClass = compact
|
|
305
309
|
? ""
|
|
306
310
|
: menuShowing
|
|
@@ -364,7 +368,6 @@ export const ThreadRowView = memo(function ThreadRowView({
|
|
|
364
368
|
/>
|
|
365
369
|
<span className="pointer-events-none relative flex size-4 shrink-0 items-center justify-center">
|
|
366
370
|
{stateSlot}
|
|
367
|
-
{row.childDot !== null ? <ChildDot flag={row.childDot} /> : null}
|
|
368
371
|
</span>
|
|
369
372
|
{row.nested ? (
|
|
370
373
|
// Tight against the title, and over the row's gap, so it adds 8px.
|
|
@@ -440,141 +443,169 @@ export const ThreadRowView = memo(function ThreadRowView({
|
|
|
440
443
|
<PullRequestBadge threadId={thread.id} />
|
|
441
444
|
</span>
|
|
442
445
|
) : null}
|
|
443
|
-
{
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
"pointer-events-auto relative z-10 inline-flex h-5 shrink-0 items-center gap-0.5 rounded-md px-0.5 text-[11px] leading-none tabular-nums text-muted-foreground outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-sidebar-ring",
|
|
458
|
-
dimmed && QUIET_TEXT,
|
|
459
|
-
)}
|
|
460
|
-
>
|
|
461
|
-
{chip.count}
|
|
462
|
-
<Icon
|
|
463
|
-
name={ICONS.expand}
|
|
464
|
-
aria-hidden
|
|
465
|
-
className={cn("size-3 transition-transform duration-150", chip.expanded && "rotate-90")}
|
|
466
|
-
/>
|
|
467
|
-
</button>
|
|
468
|
-
) : null}
|
|
469
|
-
{!editing && row.harness ? (
|
|
470
|
-
<span
|
|
471
|
-
className={cn(
|
|
472
|
-
"pointer-events-none relative inline-flex shrink-0 transition-opacity",
|
|
473
|
-
// The hover actions take this slot too, as bb's trailing slot does.
|
|
474
|
-
shortcut === null && fadeClass,
|
|
475
|
-
)}
|
|
476
|
-
>
|
|
477
|
-
<ProviderBadge display={provider} mode={controller.harnessIcon} />
|
|
478
|
-
</span>
|
|
479
|
-
) : null}
|
|
480
|
-
{!editing ? (
|
|
481
|
-
<span className="relative flex h-6 min-w-8 shrink-0 items-center justify-end gap-1">
|
|
482
|
-
{shortcut !== null ? (
|
|
483
|
-
<kbd aria-hidden className="pointer-events-none rounded border border-border px-1 font-sans text-[10px] leading-4 text-muted-foreground">
|
|
484
|
-
{shortcut.label}
|
|
485
|
-
</kbd>
|
|
486
|
-
) : (
|
|
487
|
-
<>
|
|
488
|
-
{row.machine !== null ? (
|
|
446
|
+
{!editing && (badgesShown || actionsShown) ? (
|
|
447
|
+
// One cell: the harness and machine at rest, Mark read and Archive
|
|
448
|
+
// on hover. The actions take no width at rest, so no title is
|
|
449
|
+
// shorter for them; on hover the title gives up only what they
|
|
450
|
+
// need beyond the badges they replace.
|
|
451
|
+
<span className="relative -ml-1.5 grid shrink-0 items-center">
|
|
452
|
+
{badgesShown ? (
|
|
453
|
+
<span className={cn("flex items-center gap-1.5 pl-1.5 [grid-area:1/1] transition-opacity", shortcut === null && fadeClass)}>
|
|
454
|
+
{row.harness ? (
|
|
455
|
+
<span className="pointer-events-none relative inline-flex shrink-0">
|
|
456
|
+
<ProviderBadge display={provider} mode={controller.harnessIcon} />
|
|
457
|
+
</span>
|
|
458
|
+
) : null}
|
|
459
|
+
{showMachine ? (
|
|
489
460
|
<span
|
|
490
461
|
title={`On ${row.machine}`}
|
|
491
462
|
aria-label={`On ${row.machine}`}
|
|
492
|
-
className=
|
|
463
|
+
className="pointer-events-none max-w-20 truncate text-[11px] text-muted-foreground"
|
|
493
464
|
>
|
|
494
465
|
{row.machine}
|
|
495
466
|
</span>
|
|
496
467
|
) : null}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
468
|
+
</span>
|
|
469
|
+
) : null}
|
|
470
|
+
{actionsShown ? (
|
|
471
|
+
<span
|
|
472
|
+
className={cn(
|
|
473
|
+
"items-center justify-self-end gap-0.5 pl-1.5 [grid-area:1/1]",
|
|
474
|
+
menuShowing ? "flex" : "hidden group-hover/row:flex group-focus-within/row:flex",
|
|
475
|
+
)}
|
|
476
|
+
>
|
|
477
|
+
{row.treeUnread ? (
|
|
478
|
+
<button
|
|
479
|
+
type="button"
|
|
480
|
+
aria-label="Mark read"
|
|
481
|
+
title="Mark read"
|
|
482
|
+
className={ROW_ICON_BUTTON}
|
|
483
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
484
|
+
onClick={(event) => {
|
|
485
|
+
event.preventDefault();
|
|
486
|
+
event.stopPropagation();
|
|
487
|
+
controller.onMenuAction("mark-read", thread);
|
|
488
|
+
}}
|
|
507
489
|
>
|
|
508
|
-
{
|
|
509
|
-
</
|
|
490
|
+
<Icon name={ICONS.markRead} aria-hidden className="size-4" />
|
|
491
|
+
</button>
|
|
510
492
|
) : null}
|
|
511
|
-
<
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
493
|
+
<button
|
|
494
|
+
type="button"
|
|
495
|
+
aria-label={archived ? "Unarchive thread" : "Archive thread"}
|
|
496
|
+
title={archived ? "Unarchive" : "Archive"}
|
|
497
|
+
className={ROW_ICON_BUTTON}
|
|
498
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
499
|
+
onClick={(event) => {
|
|
500
|
+
event.preventDefault();
|
|
501
|
+
event.stopPropagation();
|
|
502
|
+
controller.onMenuAction(archived ? "unarchive" : "archive", thread);
|
|
503
|
+
}}
|
|
520
504
|
>
|
|
521
|
-
{
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
505
|
+
<Icon name={archived ? ICONS.unarchive : ICONS.archive} aria-hidden className="size-4" />
|
|
506
|
+
</button>
|
|
507
|
+
</span>
|
|
508
|
+
) : null}
|
|
509
|
+
</span>
|
|
510
|
+
) : null}
|
|
511
|
+
{!editing ? (
|
|
512
|
+
// The chip sits 4px before the trailing slot, whose width never
|
|
513
|
+
// changes, so hover never moves it.
|
|
514
|
+
<span className="relative flex shrink-0 items-center gap-1">
|
|
515
|
+
{chip !== null ? (
|
|
516
|
+
<button
|
|
517
|
+
type="button"
|
|
518
|
+
aria-expanded={chip.expanded}
|
|
519
|
+
aria-label={chipLabel(thread.displayTitle, chip)}
|
|
520
|
+
title={chipLabel(thread.displayTitle, chip)}
|
|
521
|
+
onClick={(event) => {
|
|
522
|
+
event.preventDefault();
|
|
523
|
+
event.stopPropagation();
|
|
524
|
+
controller.onToggleChip(row);
|
|
525
|
+
}}
|
|
526
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
527
|
+
onKeyDown={(event) => event.stopPropagation()}
|
|
528
|
+
className={cn(
|
|
529
|
+
"pointer-events-auto relative z-10 inline-flex h-5 shrink-0 items-center gap-0.5 rounded-md px-0.5 text-[11px] leading-none tabular-nums outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
|
|
530
|
+
chip.flag === null
|
|
531
|
+
? cn("text-muted-foreground hover:text-foreground", dimmed && QUIET_TEXT)
|
|
532
|
+
: TONE_CLASS[chipTone(chip.flag)],
|
|
533
|
+
)}
|
|
534
|
+
>
|
|
535
|
+
{chip.flag !== null ? <ChipStateGlyph flag={chip.flag} /> : null}
|
|
536
|
+
{chip.count > 0 ? chip.count : null}
|
|
537
|
+
<Icon
|
|
538
|
+
name={ICONS.expand}
|
|
539
|
+
aria-hidden
|
|
540
|
+
className={cn("size-3 transition-transform duration-150", chip.expanded && "rotate-90")}
|
|
541
|
+
/>
|
|
542
|
+
</button>
|
|
543
|
+
) : null}
|
|
544
|
+
{/* The trailing slot: one grid cell, as wide as "…" or the widest
|
|
545
|
+
time, whichever is wider, holding the time at rest and "…" on hover. */}
|
|
546
|
+
<span data-trailing-slot="" className="relative grid h-6 shrink-0 items-center">
|
|
547
|
+
<span aria-hidden className="invisible w-6 [grid-area:1/1]" />
|
|
548
|
+
{TRAILING_SLOT_SIZERS.map((text) => (
|
|
549
|
+
<span key={text} aria-hidden className="invisible text-[11px] font-medium tabular-nums [grid-area:1/1]">
|
|
550
|
+
{text}
|
|
551
|
+
</span>
|
|
552
|
+
))}
|
|
553
|
+
{shortcut !== null ? (
|
|
554
|
+
<kbd aria-hidden className="pointer-events-none justify-self-end rounded border border-border px-1 font-sans text-[10px] leading-4 text-muted-foreground [grid-area:1/1]">
|
|
555
|
+
{shortcut.label}
|
|
556
|
+
</kbd>
|
|
557
|
+
) : (
|
|
558
|
+
<>
|
|
559
|
+
{time !== null ? (
|
|
560
|
+
<span
|
|
561
|
+
title={time.label}
|
|
562
|
+
aria-label={time.label}
|
|
563
|
+
className={cn(
|
|
564
|
+
"pointer-events-none justify-self-end text-[11px] tabular-nums transition-opacity [grid-area:1/1]",
|
|
565
|
+
// A running timer reads as work, an age as history.
|
|
566
|
+
time.kind === "timer" ? "font-medium text-[var(--timeline-accent)]" : "text-muted-foreground",
|
|
567
|
+
fadeClass,
|
|
568
|
+
)}
|
|
549
569
|
>
|
|
550
|
-
|
|
551
|
-
</
|
|
570
|
+
{time.text}
|
|
571
|
+
</span>
|
|
552
572
|
) : null}
|
|
553
|
-
<
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
573
|
+
<span
|
|
574
|
+
className={cn(
|
|
575
|
+
"flex items-center justify-self-end transition-opacity [grid-area:1/1]",
|
|
576
|
+
compact
|
|
577
|
+
? "relative"
|
|
578
|
+
: menuShowing
|
|
579
|
+
? "opacity-100"
|
|
580
|
+
: "opacity-0 group-hover/row:opacity-100 group-focus-within/row:opacity-100",
|
|
581
|
+
)}
|
|
582
|
+
>
|
|
583
|
+
<DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>
|
|
584
|
+
<DropdownMenuTrigger asChild>
|
|
585
|
+
<button
|
|
586
|
+
type="button"
|
|
587
|
+
aria-label="Thread actions"
|
|
588
|
+
// Phones open the menu by long-press, as bb's lists do;
|
|
589
|
+
// the button stays for keyboards and screen readers.
|
|
590
|
+
className={compact ? "sr-only" : ROW_ICON_BUTTON}
|
|
591
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
592
|
+
onClick={(event) => event.stopPropagation()}
|
|
593
|
+
>
|
|
594
|
+
<Icon name={ICONS.more} aria-hidden className="size-4" />
|
|
595
|
+
</button>
|
|
596
|
+
</DropdownMenuTrigger>
|
|
597
|
+
<RowDropdownMenuContent
|
|
598
|
+
items={menuItems}
|
|
599
|
+
sections={controller.sections}
|
|
600
|
+
currentSectionId={thread.sectionId}
|
|
601
|
+
onAction={onMenuAction}
|
|
602
|
+
onCloseAutoFocus={onMenuCloseAutoFocus}
|
|
603
|
+
/>
|
|
604
|
+
</DropdownMenu>
|
|
605
|
+
</span>
|
|
606
|
+
</>
|
|
607
|
+
)}
|
|
608
|
+
</span>
|
|
578
609
|
</span>
|
|
579
610
|
) : null}
|
|
580
611
|
</div>
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
// State glyphs, the
|
|
2
|
-
// decided; tone and animation come from host token
|
|
1
|
+
// State glyphs, the children chip's state glyph and header counters. They
|
|
2
|
+
// draw what the model decided; tone and animation come from host token
|
|
3
|
+
// classes only.
|
|
3
4
|
import { experimental_Icon as Icon } from "@get-bb/plugin-sdk/app";
|
|
4
5
|
import type { PluginSidebarThreadRowStatus } from "@get-bb/plugin-sdk/app";
|
|
5
6
|
import { cn } from "@/lib/utils";
|
|
6
7
|
import type { Counters } from "../model/counters";
|
|
7
8
|
import type { RowNote } from "../model/notes";
|
|
8
|
-
import {
|
|
9
|
+
import { FLAG_GLYPHS, type Flag, type Glyph, type Tone } from "../model/state";
|
|
9
10
|
|
|
10
11
|
export const TONE_CLASS: Record<Tone, string> = {
|
|
11
12
|
attention: "text-attention",
|
|
@@ -18,32 +19,6 @@ export const TONE_CLASS: Record<Tone, string> = {
|
|
|
18
19
|
none: "",
|
|
19
20
|
};
|
|
20
21
|
|
|
21
|
-
// The child dot's colours: the tone of the state it stands for, and the
|
|
22
|
-
// unread dot's accent for unread.
|
|
23
|
-
const DOT_TONE_CLASS: Record<DotTone, string> = {
|
|
24
|
-
attention: "bg-attention",
|
|
25
|
-
destructive: "bg-destructive",
|
|
26
|
-
working: "bg-[var(--timeline-accent)]",
|
|
27
|
-
unread: "bg-[var(--timeline-accent)]",
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* The child dot on a parent's status glyph, at its lower right corner. A
|
|
32
|
-
* ring of the sidebar's colour keeps it apart from the glyph under it.
|
|
33
|
-
*/
|
|
34
|
-
export function ChildDot({ flag }: { flag: Flag }) {
|
|
35
|
-
return (
|
|
36
|
-
<span
|
|
37
|
-
aria-hidden
|
|
38
|
-
data-child-dot={flag}
|
|
39
|
-
className={cn(
|
|
40
|
-
"pointer-events-none absolute -right-0.5 -bottom-0.5 size-[7px] rounded-full ring-[1.5px] ring-sidebar",
|
|
41
|
-
DOT_TONE_CLASS[dotTone(flag)],
|
|
42
|
-
)}
|
|
43
|
-
/>
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
22
|
const SHINE = "animate-shine-icon motion-reduce:animate-none";
|
|
48
23
|
const SPIN = "animate-spin motion-reduce:animate-none";
|
|
49
24
|
|
|
@@ -139,6 +114,12 @@ export function FlagGlyph({ flag, className }: { flag: Flag; className?: string
|
|
|
139
114
|
return <GlyphIcon glyph={FLAG_GLYPHS[flag]} className={className} />;
|
|
140
115
|
}
|
|
141
116
|
|
|
117
|
+
/** The state glyph that leads a children chip, smaller than the Status column's; the chip's label names it. */
|
|
118
|
+
export function ChipStateGlyph({ flag }: { flag: Flag }) {
|
|
119
|
+
if (FLAG_GLYPHS[flag].icon === "dot") return <UnreadDot className="size-[5px]" />;
|
|
120
|
+
return <FlagGlyph flag={flag} className="size-3" />;
|
|
121
|
+
}
|
|
122
|
+
|
|
142
123
|
const COUNTER_ITEMS: readonly {
|
|
143
124
|
key: keyof Counters;
|
|
144
125
|
flag: Flag;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Accessible names: everything a tooltip says is in the aria-label.
|
|
2
|
-
import type { OlderRow, SettledRow, ThreadRow } from "./view";
|
|
3
|
-
import {
|
|
2
|
+
import type { Chip, OlderRow, SettledRow, ThreadRow } from "./view";
|
|
3
|
+
import type { Flag } from "./state";
|
|
4
4
|
import { formatDateTime } from "./details";
|
|
5
5
|
import type { OrganizationMode } from "@/shared/preferences";
|
|
6
6
|
|
|
@@ -20,7 +20,6 @@ export function rowAriaLabel(
|
|
|
20
20
|
): string {
|
|
21
21
|
const parts = [stateText(row, options.pluginLabel), options.providerName];
|
|
22
22
|
if (row.parentTitle !== null && row.depth > 0) parts.push(`child of ${row.parentTitle}`);
|
|
23
|
-
if (row.childDot !== null) parts.push(`child threads: ${FLAG_GLYPHS[row.childDot].label}`);
|
|
24
23
|
if (row.crossGroupLabel !== null) parts.push(row.crossGroupLabel.toLowerCase());
|
|
25
24
|
if (row.hiddenBadge) parts.push("hidden thread");
|
|
26
25
|
if (row.info.unread && row.info.state.kind !== "unread") parts.push("unread");
|
|
@@ -28,9 +27,24 @@ export function rowAriaLabel(
|
|
|
28
27
|
return `Open ${row.info.thread.displayTitle} — ${parts.join("; ")}`;
|
|
29
28
|
}
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
/** What a children chip's state says, uncounted: the state can come from any depth, the count only from direct children. */
|
|
31
|
+
const CHIP_STATE_TEXT: Readonly<Record<Flag, string>> = {
|
|
32
|
+
"waits-on-you": "waiting on you below",
|
|
33
|
+
"unread-failed": "failed below",
|
|
34
|
+
"queue-failed": "queued message failed below",
|
|
35
|
+
offline: "machine offline below",
|
|
36
|
+
working: "working below",
|
|
37
|
+
unread: "unread below",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* "Show 2 child threads of Release, working below"; with no visible child,
|
|
42
|
+
* "Show hidden child threads of Release, waiting on you below".
|
|
43
|
+
*/
|
|
44
|
+
export function chipLabel(title: string, chip: Chip): string {
|
|
45
|
+
const children = chip.count === 0 ? "hidden child threads" : `${chip.count} ${chip.count === 1 ? "child thread" : "child threads"}`;
|
|
46
|
+
const state = chip.flag === null ? "" : `, ${CHIP_STATE_TEXT[chip.flag]}`;
|
|
47
|
+
return `${chip.expanded ? "Collapse" : "Show"} ${children} of ${title}${state}`;
|
|
34
48
|
}
|
|
35
49
|
|
|
36
50
|
/** The text and accessible name of an open tree's fold row. */
|
|
@@ -271,17 +271,17 @@ export function hiddenThreadFlags(flags: ReadonlySet<Flag>): Set<Flag> {
|
|
|
271
271
|
return kept;
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
/** Flags a hidden thread adds to
|
|
275
|
-
const
|
|
274
|
+
/** Flags a hidden thread adds to the children chip above it: what asks for you or failed, and offline. */
|
|
275
|
+
const HIDDEN_CHIP_FLAGS: ReadonlySet<Flag> = new Set<Flag>(["waits-on-you", "unread-failed", "queue-failed", "offline"]);
|
|
276
276
|
|
|
277
277
|
/**
|
|
278
|
-
* The flags a thread adds to the
|
|
278
|
+
* The flags a thread adds to the children chip of the threads above it. A
|
|
279
279
|
* hidden thread adds only waits-on-you, failed, queued message failed and
|
|
280
280
|
* offline; which trees need attention reads `hiddenThreadFlags` instead,
|
|
281
281
|
* so this leaves it unchanged.
|
|
282
282
|
*/
|
|
283
|
-
export function
|
|
284
|
-
return isHidden ? new Set([...flags].filter((flag) =>
|
|
283
|
+
export function chipFlagsOf(flags: ReadonlySet<Flag>, isHidden: boolean): Set<Flag> {
|
|
284
|
+
return isHidden ? new Set([...flags].filter((flag) => HIDDEN_CHIP_FLAGS.has(flag))) : new Set(flags);
|
|
285
285
|
}
|
|
286
286
|
|
|
287
287
|
/** The most urgent flag, or null. */
|
|
@@ -289,22 +289,10 @@ export function mostUrgent(flags: ReadonlySet<Flag>): Flag | null {
|
|
|
289
289
|
return FLAG_ORDER.find((flag) => flags.has(flag)) ?? null;
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
-
/** The
|
|
293
|
-
export
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
switch (flag) {
|
|
297
|
-
case "waits-on-you":
|
|
298
|
-
case "offline":
|
|
299
|
-
return "attention";
|
|
300
|
-
case "unread-failed":
|
|
301
|
-
case "queue-failed":
|
|
302
|
-
return "destructive";
|
|
303
|
-
case "working":
|
|
304
|
-
return "working";
|
|
305
|
-
case "unread":
|
|
306
|
-
return "unread";
|
|
307
|
-
}
|
|
292
|
+
/** The tone a children chip takes for a state: its glyph's, and the unread dot's accent for unread. */
|
|
293
|
+
export function chipTone(flag: Flag): Tone {
|
|
294
|
+
const tone = FLAG_GLYPHS[flag].tone;
|
|
295
|
+
return tone === "none" ? "working" : tone;
|
|
308
296
|
}
|
|
309
297
|
|
|
310
298
|
export const FLAG_GLYPHS: Readonly<Record<Flag, Glyph & { label: string }>> = {
|
|
@@ -105,3 +105,10 @@ export function stateSince(
|
|
|
105
105
|
if (at === undefined) return null;
|
|
106
106
|
return formatDuration(now - at);
|
|
107
107
|
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The widest texts `trailingTime` gives up to 99 weeks, one per unit. The
|
|
111
|
+
* trailing slot is as wide as the widest of them, so times line up down the
|
|
112
|
+
* list; an older row widens its own.
|
|
113
|
+
*/
|
|
114
|
+
export const TRAILING_SLOT_SIZERS = ["now", "<1m", "59m", "23h", "6d", "99w"] as const;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import type { PluginSidebarThread } from "@get-bb/plugin-sdk/app";
|
|
4
4
|
import {
|
|
5
5
|
computeState,
|
|
6
|
-
|
|
6
|
+
chipFlagsOf,
|
|
7
7
|
hiddenThreadFlags,
|
|
8
8
|
isQuietThread,
|
|
9
9
|
isUnread,
|
|
@@ -29,8 +29,8 @@ export interface ThreadInfo {
|
|
|
29
29
|
unread: boolean;
|
|
30
30
|
/** Own flags; for hidden threads only waits-on-you and unread-failed. */
|
|
31
31
|
flags: ReadonlySet<Flag>;
|
|
32
|
-
/** What it adds to the
|
|
33
|
-
|
|
32
|
+
/** What it adds to the children chip of the threads above it (see `chipFlagsOf`). */
|
|
33
|
+
chipFlags: ReadonlySet<Flag>;
|
|
34
34
|
/**
|
|
35
35
|
* The flags of this thread that make it need attention, for collapsed
|
|
36
36
|
* groups, the counters, the need-you filter and auto-reveal: a child counts
|
|
@@ -55,14 +55,14 @@ export interface ThreadInfo {
|
|
|
55
55
|
note: RowNote | null;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
/** What a thread's descendants add up to, for its chip
|
|
58
|
+
/** What a thread's descendants add up to, for its chip and folding. */
|
|
59
59
|
export interface Subtree {
|
|
60
60
|
/** Descendants, depth-first in creation order, hidden ones included. */
|
|
61
61
|
descendants: ThreadInfo[];
|
|
62
62
|
/** Union of the descendants' Needs attention flags and working, archived ones left out. */
|
|
63
63
|
flags: ReadonlySet<Flag>;
|
|
64
|
-
/** Union of the descendants' `
|
|
65
|
-
|
|
64
|
+
/** Union of the descendants' `chipFlags`, archived ones left out: the chip's state. */
|
|
65
|
+
chipFlags: ReadonlySet<Flag>;
|
|
66
66
|
/** Visible descendants at any depth. */
|
|
67
67
|
visibleCount: number;
|
|
68
68
|
/** Visible direct children: the chip's number. */
|
|
@@ -167,7 +167,7 @@ export function buildForest(inputs: ForestInputs): Forest {
|
|
|
167
167
|
state,
|
|
168
168
|
unread,
|
|
169
169
|
flags: thread.isHidden ? hiddenThreadFlags(flags) : flags,
|
|
170
|
-
|
|
170
|
+
chipFlags: chipFlagsOf(flags, thread.isHidden),
|
|
171
171
|
attentionFlags: new Set<Flag>(),
|
|
172
172
|
quiet: isQuietThread(state, unread, isActive),
|
|
173
173
|
quietIgnoringOpen: false,
|
|
@@ -216,7 +216,7 @@ export function buildForest(inputs: ForestInputs): Forest {
|
|
|
216
216
|
if (cached !== undefined) return cached;
|
|
217
217
|
const descendants: ThreadInfo[] = [];
|
|
218
218
|
const flags = new Set<Flag>();
|
|
219
|
-
const
|
|
219
|
+
const chipFlags = new Set<Flag>();
|
|
220
220
|
let visibleCount = 0;
|
|
221
221
|
let childCount = 0;
|
|
222
222
|
let quietIgnoringOpen = true;
|
|
@@ -229,10 +229,10 @@ export function buildForest(inputs: ForestInputs): Forest {
|
|
|
229
229
|
if (!child.thread.isArchived) {
|
|
230
230
|
for (const flag of child.attentionFlags) flags.add(flag);
|
|
231
231
|
if (child.flags.has("working")) flags.add("working");
|
|
232
|
-
for (const flag of child.
|
|
232
|
+
for (const flag of child.chipFlags) chipFlags.add(flag);
|
|
233
233
|
}
|
|
234
234
|
for (const flag of below.flags) flags.add(flag);
|
|
235
|
-
for (const flag of below.
|
|
235
|
+
for (const flag of below.chipFlags) chipFlags.add(flag);
|
|
236
236
|
if (child.thread.isHidden ? child.attentionFlags.size > 0 : !child.quietIgnoringOpen) quietIgnoringOpen = false;
|
|
237
237
|
if (!child.thread.isHidden) {
|
|
238
238
|
visibleCount += 1;
|
|
@@ -242,7 +242,7 @@ export function buildForest(inputs: ForestInputs): Forest {
|
|
|
242
242
|
if (!below.quietIgnoringOpen) quietIgnoringOpen = false;
|
|
243
243
|
}
|
|
244
244
|
path.delete(id);
|
|
245
|
-
const subtree: Subtree = { descendants, flags,
|
|
245
|
+
const subtree: Subtree = { descendants, flags, chipFlags, visibleCount, childCount, quietIgnoringOpen };
|
|
246
246
|
subtrees.set(id, subtree);
|
|
247
247
|
return subtree;
|
|
248
248
|
};
|