Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 99 additions & 16 deletions plugins/thread-hover-cards/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,49 @@ function renderError(card: HTMLElement): void {
);
}

type HoverCardRenderState = "complete" | "error" | "loading" | "summary";

function setHoverCardRenderState(
card: HTMLElement,
state: HoverCardRenderState,
): void {
card.dataset.bbHoverCardRenderState = state;
if (state === "loading" || state === "summary") {
card.setAttribute("aria-busy", "true");
} else {
card.removeAttribute("aria-busy");
}
}

function nextPaint(): Promise<void> {
return new Promise((resolve) => requestAnimationFrame(() => resolve()));
}

async function waitForCardAssets(card: HTMLElement): Promise<void> {
const fonts = document.fonts?.ready;
const images = Array.from(card.querySelectorAll("img"));
await Promise.all([
fonts ?? Promise.resolve(),
...images.map((image) => {
if (image.complete) return image.decode?.().catch(() => undefined);
return new Promise<void>((resolve) => {
image.addEventListener("load", () => resolve(), { once: true });
image.addEventListener("error", () => resolve(), { once: true });
});
}),
]);
}

async function markHoverCardComplete(
card: HTMLElement,
isCurrent: () => boolean,
): Promise<void> {
await waitForCardAssets(card);
await nextPaint();
await nextPaint();
if (isCurrent()) setHoverCardRenderState(card, "complete");
}

function renderSummary(card: HTMLElement, summary: ThreadSummary): void {
const header = element("div", "bb-thread-hover-card__header");
const provider = element("div", "bb-thread-hover-card__provider");
Expand Down Expand Up @@ -1497,7 +1540,7 @@ function installHoverCards({ onOpen }: ThreadHoverCardOptions): HoverCardControl
threadId: string,
generation: number,
hoverCard: HTMLDivElement,
): void {
): Promise<void> {
const cached = cache.get(threadId);
if (
cached?.timingFetchedAt !== null &&
Expand All @@ -1511,10 +1554,10 @@ function installHoverCards({ onOpen }: ThreadHoverCardOptions): HoverCardControl
outcome: "ok",
threadId,
});
return;
return Promise.resolve();
}

void requestTiming(threadId)
return requestTiming(threadId)
.then((timing) => {
const current = cache.get(threadId);
if (!current) return;
Expand All @@ -1530,9 +1573,11 @@ function installHoverCards({ onOpen }: ThreadHoverCardOptions): HoverCardControl
) {
timingRetriedForSummary.set(threadId, summaryStartedAt);
timingRetryScheduled.add(threadId);
queueMicrotask(() => {
timingRetryScheduled.delete(threadId);
refreshTiming(threadId, generation, hoverCard);
return new Promise<void>((resolve) => {
queueMicrotask(() => {
timingRetryScheduled.delete(threadId);
void refreshTiming(threadId, generation, hoverCard).then(resolve);
});
});
}
return;
Expand Down Expand Up @@ -1578,9 +1623,9 @@ function installHoverCards({ onOpen }: ThreadHoverCardOptions): HoverCardControl
threadId: string,
generation: number,
hoverCard: HTMLDivElement,
): void {
): Promise<void> {
const cached = cache.get(threadId);
if (!cached?.summary.repository.isGitRepository) return;
if (!cached?.summary.repository.isGitRepository) return Promise.resolve();
const repositoryKey = pullRequestRepositoryIdentity(
cached.summary.repository,
);
Expand All @@ -1595,10 +1640,10 @@ function installHoverCards({ onOpen }: ThreadHoverCardOptions): HoverCardControl
outcome: "ok",
threadId,
});
return;
return Promise.resolve();
}

void requestPullRequest(threadId, repositoryKey)
return requestPullRequest(threadId, repositoryKey)
.then(({ pullRequest, repository }) => {
const current = cache.get(threadId);
if (
Expand Down Expand Up @@ -1641,6 +1686,27 @@ function installHoverCards({ onOpen }: ThreadHoverCardOptions): HoverCardControl
.catch(() => undefined);
}

function finishRendering(
threadId: string,
generation: number,
hoverCard: HTMLDivElement,
): void {
setHoverCardRenderState(hoverCard, "summary");
void Promise.all([
refreshTiming(threadId, generation, hoverCard),
refreshPullRequest(threadId, generation, hoverCard),
]).then(() =>
markHoverCardComplete(
hoverCard,
() =>
!disposed &&
generation === requestGeneration &&
activeThreadId === threadId &&
resolveActiveTrigger() !== null,
),
);
}

function resolveActiveTrigger(): HTMLAnchorElement | null {
if (!activeThreadId) return null;
if (
Expand Down Expand Up @@ -1723,6 +1789,7 @@ function installHoverCards({ onOpen }: ThreadHoverCardOptions): HoverCardControl
const generation = requestGeneration;
abortSupersededSummaryRequests(threadId);
const hoverCard = ensureCard();
setHoverCardRenderState(hoverCard, "loading");
hoverCard.hidden = false;
hoverCard.classList.remove("is-visible");
void hoverCard.offsetWidth;
Expand Down Expand Up @@ -1756,8 +1823,7 @@ function installHoverCards({ onOpen }: ThreadHoverCardOptions): HoverCardControl
threadId,
});
if (cacheIsFresh) {
refreshTiming(threadId, generation, hoverCard);
refreshPullRequest(threadId, generation, hoverCard);
finishRendering(threadId, generation, hoverCard);
return;
}
}
Expand Down Expand Up @@ -1799,8 +1865,7 @@ function installHoverCards({ onOpen }: ThreadHoverCardOptions): HoverCardControl
(replacementPullRequestLink ?? resolveActiveTrigger())?.focus();
}
requestAnimationFrame(positionCard);
refreshTiming(threadId, generation, hoverCard);
refreshPullRequest(threadId, generation, hoverCard);
finishRendering(threadId, generation, hoverCard);
})
.catch((error) => {
if (
Expand All @@ -1811,6 +1876,7 @@ function installHoverCards({ onOpen }: ThreadHoverCardOptions): HoverCardControl
resolveActiveTrigger()
) {
renderError(hoverCard);
setHoverCardRenderState(hoverCard, "error");
recordTiming({
cache: "miss",
durationMs: elapsedMs(requestedAt),
Expand Down Expand Up @@ -2226,16 +2292,24 @@ function installSectionHoverCards({
generation += 1;
const requestGeneration = generation;
const hoverCard = ensureCard();
setHoverCardRenderState(hoverCard, "loading");
hoverCard.hidden = false;
hoverCard.classList.remove("is-visible");
void hoverCard.offsetWidth;
hoverCard.classList.add("is-visible");
const key = keyOf(target);
const cached = cache.get(key);
if (cached) renderSectionSummary(hoverCard, cached.summary);
else renderLoading(hoverCard, "section");
if (cached) {
renderSectionSummary(hoverCard, cached.summary);
setHoverCardRenderState(hoverCard, "summary");
} else renderLoading(hoverCard, "section");
requestAnimationFrame(position);
if (cached && Date.now() - cached.fetchedAt < SECTION_SUMMARY_CACHE_TTL_MS) {
void markHoverCardComplete(
hoverCard,
() =>
!disposed && requestGeneration === generation && active !== null,
);
return;
}

Expand All @@ -2248,11 +2322,20 @@ function installSectionHoverCards({
if (disposed || requestGeneration !== generation) return;
renderSectionSummary(hoverCard, summary);
requestAnimationFrame(position);
setHoverCardRenderState(hoverCard, "summary");
void markHoverCardComplete(
hoverCard,
() =>
!disposed &&
requestGeneration === generation &&
active !== null,
);
})
.catch((error) => {
if (disposed || requestGeneration !== generation || cached) return;
if (isAbortError(error)) return;
renderError(hoverCard);
setHoverCardRenderState(hoverCard, "error");
requestAnimationFrame(position);
});
}
Expand Down
Loading
Loading