refactor: extract star/notification/badge helpers into database.py and add enrich_items content module

Introduce `update_target_stars`, `get_target_owner_uid`, and `VOTABLE_TARGETS`/`STAR_TARGETS` in database.py; replace inline badge insertion with `award_badge()` and inline notification creation with `create_notification()` across auth, comments, and follow routers; add `enrich_items()` in new content module to centralize post/gist list assembly and remove duplicated enrichment logic from feed and gists routers.
This commit is contained in:
2026-05-23 06:34:13 +00:00
parent ab1121a42a
commit d7d8314da0
34 changed files with 211 additions and 367 deletions
+3 -2
View File
@@ -1,3 +1,5 @@
import { Toast } from "./Toast.js";
export class AttachmentUploader {
constructor(form) {
this.form = form;
@@ -172,8 +174,7 @@ export class AttachmentUploader {
}
showError(msg) {
this.errorEl.textContent = msg;
setTimeout(() => { if (this.errorEl.textContent === msg) this.errorEl.textContent = ""; }, 5000);
Toast.flash(this.errorEl, msg, 5000, "");
}
}
+4 -6
View File
@@ -1,3 +1,5 @@
import { Toast } from "./Toast.js";
export class DomUtils {
constructor() {
this.initClipboardCopy();
@@ -14,9 +16,7 @@ export class DomUtils {
if (!source) return;
try {
await navigator.clipboard.writeText(source.textContent);
const original = btn.textContent;
btn.textContent = "Copied!";
setTimeout(() => { btn.textContent = original; }, 2000);
Toast.flash(btn, "Copied!", 2000);
} catch {
// silently fail
}
@@ -32,9 +32,7 @@ export class DomUtils {
const url = new URL(btn.dataset.share || window.location.href, window.location.href).href;
try {
await navigator.clipboard.writeText(url);
const original = btn.textContent;
btn.textContent = "Copied!";
setTimeout(() => { btn.textContent = original; }, 1000);
Toast.flash(btn, "Copied!", 1000);
} catch {
// silently fail
}
+3 -9
View File
@@ -1,3 +1,5 @@
import { TextInput } from "./TextInput.js";
export class EmojiPicker {
constructor(textarea) {
this.textarea = textarea;
@@ -35,15 +37,7 @@ export class EmojiPicker {
}
insert(unicode) {
const ta = this.textarea;
const start = ta.selectionStart;
const end = ta.selectionEnd;
const text = ta.value;
ta.value = text.substring(0, start) + unicode + text.substring(end);
const newPos = start + unicode.length;
ta.setSelectionRange(newPos, newPos);
ta.focus();
ta.dispatchEvent(new Event("input", { bubbles: true }));
TextInput.insertAtCursor(this.textarea, unicode);
}
toggle() {
+7 -8
View File
@@ -1,3 +1,7 @@
import { Http } from "./Http.js";
import { Avatar } from "./Avatar.js";
import { TextInput } from "./TextInput.js";
export class MentionInput {
constructor(element) {
this.input = element;
@@ -50,8 +54,7 @@ export class MentionInput {
async fetch(query) {
try {
const resp = await fetch("/profile/search?q=" + encodeURIComponent(query));
const data = await resp.json();
const data = await Http.getJson("/profile/search?q=" + encodeURIComponent(query));
const results = data.results || [];
if (results.length === 0) {
this.dropdown.style.display = "none";
@@ -71,7 +74,7 @@ export class MentionInput {
item.type = "button";
item.className = "mention-dropdown-item";
item.dataset.username = r.username;
item.innerHTML = '<img src="/avatar/multiavatar/' + encodeURIComponent(r.username) + '?size=24" class="avatar-img" style="width:24px;height:24px;border-radius:50%" alt="" loading="lazy"><span>@' + r.username + '</span>';
item.innerHTML = Avatar.imgHtml(r.username) + "<span>@" + r.username + "</span>";
item.addEventListener("mousedown", (e) => {
e.preventDefault();
this.insert(r.username);
@@ -121,11 +124,7 @@ export class MentionInput {
let after = val.substring(this.lastMatch.index + this.lastMatch.query.length + 1);
before = before.replace(/@+$/, "");
after = after.replace(/^@+/, "");
this.input.value = before + "@" + username + " " + after;
const newPos = before.length + username.length + 2;
this.input.setSelectionRange(newPos, newPos);
this.input.focus();
this.input.dispatchEvent(new Event("input", { bubbles: true }));
TextInput.applyValue(this.input, before + "@" + username + " " + after, before.length + username.length + 2);
this.dropdown.style.display = "none";
this.lastMatch = null;
}
+5 -3
View File
@@ -1,3 +1,6 @@
import { Http } from "./Http.js";
import { Avatar } from "./Avatar.js";
export class MessageSearch {
constructor() {
this.initMessageSearch();
@@ -26,8 +29,7 @@ export class MessageSearch {
}
debounceTimer = setTimeout(async () => {
try {
const resp = await fetch(`/messages/search?q=${encodeURIComponent(q)}`);
const data = await resp.json();
const data = await Http.getJson(`/messages/search?q=${encodeURIComponent(q)}`);
const results = data.results || [];
if (results.length === 0) {
dropdown.style.display = "none";
@@ -38,7 +40,7 @@ export class MessageSearch {
const item = document.createElement("a");
item.className = "search-dropdown-item";
item.href = `/messages?with_uid=${r.uid}`;
item.innerHTML = `<img src="/avatar/multiavatar/${encodeURIComponent(r.username)}?size=24" class="avatar-img" style="width:24px;height:24px;border-radius:50%" alt="" loading="lazy"><span>${r.username}</span>`;
item.innerHTML = `${Avatar.imgHtml(r.username)}<span>${r.username}</span>`;
dropdown.appendChild(item);
}
dropdown.style.display = "block";
+3 -2
View File
@@ -1,3 +1,5 @@
import { Http } from "./Http.js";
class ServiceMonitor {
constructor() {
this.pollInterval = 5000;
@@ -29,8 +31,7 @@ class ServiceMonitor {
async pollServices() {
try {
const resp = await fetch("/admin/services/data");
const data = await resp.json();
const data = await Http.getJson("/admin/services/data");
const container = document.getElementById("services-list");
if (!container) return;
for (const svc of data.services) {
+6 -19
View File
@@ -1,3 +1,5 @@
import { Http } from "./Http.js";
export class VoteManager {
constructor() {
this.initVoteButtons();
@@ -6,37 +8,22 @@ export class VoteManager {
initVoteButtons() {
document.querySelectorAll(".post-action-btn[data-vote]").forEach((btn) => {
btn.addEventListener("click", async () => {
btn.addEventListener("click", () => {
const targetUid = btn.dataset.target;
const targetType = btn.dataset.type || "post";
const value = btn.dataset.vote;
const form = document.createElement("form");
form.method = "POST";
form.action = `/votes/${targetType}/${targetUid}`;
const input = document.createElement("input");
input.type = "hidden";
input.name = "value";
input.value = value;
form.appendChild(input);
document.body.appendChild(form);
form.submit();
Http.postForm(`/votes/${targetType}/${targetUid}`, { value: btn.dataset.vote });
});
});
}
initNotificationDismiss() {
document.querySelectorAll(".notification-dismiss").forEach((btn) => {
btn.addEventListener("click", async () => {
btn.addEventListener("click", () => {
const uid = btn.dataset.uid;
if (!uid) {
return;
}
const form = document.createElement("form");
form.method = "POST";
form.action = `/notifications/mark-read/${uid}`;
document.body.appendChild(form);
form.submit();
Http.postForm(`/notifications/mark-read/${uid}`);
});
});
}