diff --git a/src/components/Comment.svelte b/src/components/Comment.svelte
index 2c711f1f0..c4da7f301 100644
--- a/src/components/Comment.svelte
+++ b/src/components/Comment.svelte
@@ -1,12 +1,17 @@
-
+
+
+
{#if isReply}
@@ -163,6 +212,15 @@
• edited
{/if}
+
+
+
+
+
diff --git a/src/lib/router.ts b/src/lib/router.ts
index a39c1beab..211dc3c4c 100644
--- a/src/lib/router.ts
+++ b/src/lib/router.ts
@@ -219,8 +219,9 @@ async function navigate(
window.scrollTo(0, restoreScrollY ?? 0);
return;
}
- if (hash.length > 1) {
- const el = document.querySelector(hash);
+ const fragment = utils.fragmentId(hash);
+ if (fragment !== undefined) {
+ const el = document.getElementById(fragment);
if (el) {
const top =
el.getBoundingClientRect().top +
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 11c62c78c..68fd692b3 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -246,6 +246,18 @@ export function scrollIntoView(id: string, options?: ScrollIntoViewOptions) {
if (lineElement) lineElement.scrollIntoView(options);
}
+export function fragmentId(hash: string): string | undefined {
+ const fragment = hash.startsWith("#") ? hash.slice(1) : hash;
+ if (fragment === "") {
+ return undefined;
+ }
+ try {
+ return decodeURIComponent(fragment);
+ } catch {
+ return undefined;
+ }
+}
+
// Check whether the given path has a markdown file extension.
export function isMarkdownPath(path: string): boolean {
return /\.(md|mkd|markdown)$/i.test(path);
diff --git a/src/views/repos/Cob/Revision.svelte b/src/views/repos/Cob/Revision.svelte
index 214b392cd..c31ee709c 100644
--- a/src/views/repos/Cob/Revision.svelte
+++ b/src/views/repos/Cob/Revision.svelte
@@ -13,7 +13,7 @@
import * as utils from "@app/lib/utils";
import { HttpdClient } from "@http-client";
import { cachedGetDiff } from "@app/views/repos/router";
- import { onMount } from "svelte";
+ import { onMount, tick } from "svelte";
import CobCommitTeaser from "@app/views/repos/Cob/CobCommitTeaser.svelte";
import CommentComponent from "@app/components/Comment.svelte";
@@ -55,7 +55,41 @@
export let previousRevOid: string | undefined = undefined;
export let first: boolean;
- let expanded = initiallyExpanded;
+ function timelinesContain(commentId: string | undefined): boolean {
+ if (commentId === undefined) {
+ return false;
+ }
+ const inThread = (thread: { root: Comment; replies: Comment[] }) =>
+ thread.root.id === commentId ||
+ thread.replies.some(reply => reply.id === commentId);
+ return timelines.some(element => {
+ switch (element.type) {
+ case "thread":
+ return inThread(element.inner);
+ case "review": {
+ const review = element.inner[1];
+ return review.id === commentId || review.threads.some(inThread);
+ }
+ case "merge":
+ return false;
+ }
+ });
+ }
+
+ let expanded =
+ initiallyExpanded ||
+ timelinesContain(utils.fragmentId(window.location.hash));
+
+ async function expandToHash() {
+ const commentId = utils.fragmentId(window.location.hash);
+ if (expanded || commentId === undefined || !timelinesContain(commentId)) {
+ return;
+ }
+ expanded = true;
+ await tick();
+ utils.scrollIntoView(commentId);
+ }
+
const api = new HttpdClient(baseUrl);
const lastEdit = revisionEdits.at(-1);
@@ -273,6 +307,8 @@
}
+
+