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
+1
View File
@@ -31,6 +31,7 @@
border-radius: var(--radius-lg);
padding: 1.25rem;
transition: all 0.2s;
cursor: pointer;
}
.notification-card.unread {
+16
View File
@@ -115,6 +115,22 @@
min-width: 0;
}
.comment-highlight {
animation: comment-highlight-fade 2s ease-out;
border-radius: var(--radius);
}
@keyframes comment-highlight-fade {
from {
background: var(--accent-light);
box-shadow: 0 0 0 4px var(--accent-light);
}
to {
background: transparent;
box-shadow: 0 0 0 4px transparent;
}
}
.comment-header {
display: flex;
align-items: center;
+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}`);
});
});
}
}