docs: add block/mute user relations, emoji-sync CLI, and uid indexes
DevPlace CI / test (push) Failing after 2m7s

- Add `/block`, `/mute` endpoints with block/unblock and mute/unmute functionality in `routers/relations.py`, hiding blocked users' content everywhere except their own profile while muting only suppresses notifications
- Introduce `devplace emoji-sync` CLI command to regenerate `static/js/emoji-shortcodes.js` from the emoji library, documented in `CLAUDE.md` and wired in `cli.py`
- Create `get_blocked_uids()` database helper and apply it in `content.py` `load_detail()` to filter blocked users' posts from detail views
- Implement `_uid_index()` and `_drop_index()` helpers in `database.py` for unique uid indexes across tables, with `user_relations` added to `SOFT_DELETE_TABLES`
- Document new routes in `AGENTS.md` and `README.md`, including emoji shortcodes rendering behavior distinct from the emoji picker
This commit is contained in:
2026-06-19 08:06:09 +00:00
parent f3a4667fce
commit 741d7aade6
136 changed files with 3025 additions and 564 deletions
@@ -19,6 +19,21 @@ export class AppContextMenu extends Component {
this.appendChild(menu);
this.menu = menu;
menu.addEventListener("click", (e) => e.stopPropagation());
menu.addEventListener("keydown", (e) => this.onKeydown(e));
}
onKeydown(e) {
const items = [...this.menu.querySelectorAll(".context-menu-item:not([disabled])")];
if (!items.length) return;
const index = items.indexOf(document.activeElement);
let next = null;
if (e.key === "ArrowDown") next = index + 1;
else if (e.key === "ArrowUp") next = index - 1;
else if (e.key === "Home") next = 0;
else if (e.key === "End") next = items.length - 1;
if (next === null) return;
e.preventDefault();
items[(next + items.length) % items.length].focus();
}
bindGlobal() {
@@ -42,18 +57,24 @@ export class AppContextMenu extends Component {
this.menu.textContent = "";
const list = document.createElement("ul");
list.className = "context-menu-list";
list.setAttribute("role", "none");
items.forEach((item) => {
const li = document.createElement("li");
if (item.separator) {
li.className = "context-menu-sep";
li.setAttribute("role", "separator");
list.appendChild(li);
return;
}
li.setAttribute("role", "none");
const btn = document.createElement("button");
btn.type = "button";
btn.className = "context-menu-item" + (item.danger ? " danger" : "");
btn.setAttribute("role", "menuitem");
btn.tabIndex = -1;
btn.disabled = !!item.disabled;
const icon = item.icon ? `<span class="context-menu-icon">${item.icon}</span>` : "";
if (item.disabled) btn.setAttribute("aria-disabled", "true");
const icon = item.icon ? `<span class="context-menu-icon" aria-hidden="true">${item.icon}</span>` : "";
btn.innerHTML = icon + '<span class="context-menu-label"></span>';
btn.querySelector(".context-menu-label").textContent = item.label;
if (!item.disabled && item.onSelect) {
@@ -84,6 +105,8 @@ export class AppContextMenu extends Component {
if (top + rect.height > offY + safeH) top = offY + safeH - rect.height - 8;
this.menu.style.left = Math.max(offX + 8, left) + "px";
this.menu.style.top = Math.max(offY + 8, top) + "px";
const first = this.menu.querySelector(".context-menu-item:not([disabled])");
if (first) setTimeout(() => first.focus(), 0);
}
close() {