feat: add resolve_object_url helper and notification click-through with comment highlighting

Extract duplicate target-redirect logic from comments router into a new `resolve_object_url` database function, add `target_url` field to follow and message notifications, create `/notifications/open/{uid}` endpoint that marks notifications read and redirects to their target, implement client-side NotificationManager for card clicks and comment hash scrolling, and add comment-highlight CSS animation.
This commit is contained in:
2026-05-25 14:16:53 +00:00
parent e30f94c5df
commit 9c55ef272e
15 changed files with 309 additions and 47 deletions
+2
View File
@@ -1,6 +1,7 @@
import { ModalManager } from "./ModalManager.js";
import { FormManager } from "./FormManager.js";
import { VoteManager } from "./VoteManager.js";
import { NotificationManager } from "./NotificationManager.js";
import { MessageSearch } from "./MessageSearch.js";
import { ProfileEditor } from "./ProfileEditor.js";
import { MobileNav } from "./MobileNav.js";
@@ -15,6 +16,7 @@ class Application {
this.modals = new ModalManager();
this.forms = new FormManager();
this.votes = new VoteManager();
this.notifications = new NotificationManager();
this.messageSearch = new MessageSearch();
this.profile = new ProfileEditor();
this.mobileNav = new MobileNav();
@@ -0,0 +1,51 @@
import { Http } from "./Http.js";
export class NotificationManager {
constructor() {
this.initCardNavigation();
this.initDismiss();
this.initHashScroll();
}
initCardNavigation() {
document.querySelectorAll(".notification-card[data-href]").forEach((card) => {
card.addEventListener("click", (e) => {
if (e.target.closest("a, button")) {
return;
}
window.location.href = card.dataset.href;
});
});
}
initDismiss() {
document.querySelectorAll(".notification-dismiss").forEach((btn) => {
btn.addEventListener("click", (e) => {
e.preventDefault();
const uid = btn.dataset.uid;
if (!uid) {
return;
}
Http.postForm(`/notifications/mark-read/${uid}`);
});
});
}
initHashScroll() {
const hash = window.location.hash;
if (!hash.startsWith("#comment-")) {
return;
}
window.addEventListener("load", () => {
const target = document.getElementById(hash.slice(1));
if (!target) {
return;
}
requestAnimationFrame(() => {
target.scrollIntoView({ behavior: "smooth", block: "center" });
target.classList.add("comment-highlight");
setTimeout(() => target.classList.remove("comment-highlight"), 2000);
});
});
}
}
-13
View File
@@ -3,7 +3,6 @@ import { Http } from "./Http.js";
export class VoteManager {
constructor() {
this.initVoteButtons();
this.initNotificationDismiss();
}
initVoteButtons() {
@@ -15,16 +14,4 @@ export class VoteManager {
});
});
}
initNotificationDismiss() {
document.querySelectorAll(".notification-dismiss").forEach((btn) => {
btn.addEventListener("click", () => {
const uid = btn.dataset.uid;
if (!uid) {
return;
}
Http.postForm(`/notifications/mark-read/${uid}`);
});
});
}
}