forked from retoor/devplacepy
docs: add block/mute user relations, emoji-sync CLI, and uid indexes
- 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:
@@ -53,19 +53,20 @@
|
||||
{% endif %}
|
||||
</head>
|
||||
<body data-user-uid="{{ user['uid'] if user else '' }}">
|
||||
<nav class="topnav">
|
||||
<a class="skip-link" href="#main-content">Skip to main content</a>
|
||||
<nav class="topnav" aria-label="Primary">
|
||||
<div class="topnav-inner">
|
||||
<a href="/feed" class="topnav-logo">Dev<span>Place</span></a>
|
||||
<div class="topnav-links">
|
||||
<a href="/" class="topnav-link {% if request.url.path == '/' %}active{% endif %}"><span class="icon">π </span> Home</a>
|
||||
<a href="/feed" class="topnav-link {% if request.url.path == '/feed' %}active{% endif %}"><span class="icon">π</span> Posts</a>
|
||||
<a href="/news" class="topnav-link {% if request.url.path == '/news' or request.url.path.startswith('/news/') %}active{% endif %}"><span class="icon">π°</span> News</a>
|
||||
<a href="/gists" class="topnav-link {% if request.url.path == '/gists' or request.url.path.startswith('/gists/') %}active{% endif %}"><span class="icon">π</span> Gists</a>
|
||||
<a href="/projects" class="topnav-link topnav-link-feature {% if request.url.path == '/projects' or request.url.path.startswith('/projects/') %}active{% endif %}"><span class="icon">π</span> Projects</a>
|
||||
<a href="/leaderboard" class="topnav-link {% if request.url.path == '/leaderboard' or request.url.path.startswith('/leaderboard/') %}active{% endif %}"><span class="icon">π</span> Leaderboard</a>
|
||||
<div class="topnav-tools-dropdown {% if request.url.path.startswith('/tools') %}active{% endif %}">
|
||||
<button type="button" class="topnav-link topnav-tools-toggle" aria-haspopup="true" aria-expanded="false"><span class="icon">π§°</span> Tools <span class="topnav-caret">βΎ</span></button>
|
||||
<div class="dropdown-menu">
|
||||
<a href="/" class="topnav-link {{ nav_active(request, '/') }}"><span class="icon">π </span> Home</a>
|
||||
<a href="/feed" class="topnav-link {{ nav_active(request, '/feed') }}"><span class="icon">π</span> Posts</a>
|
||||
<a href="/news" class="topnav-link {{ nav_active(request, '/news') }}"><span class="icon">π°</span> News</a>
|
||||
<a href="/gists" class="topnav-link {{ nav_active(request, '/gists') }}"><span class="icon">π</span> Gists</a>
|
||||
<a href="/projects" class="topnav-link {{ nav_active(request, '/projects') }}"><span class="icon">π</span> Projects</a>
|
||||
<a href="/leaderboard" class="topnav-link {{ nav_active(request, '/leaderboard') }}"><span class="icon">π</span> Leaderboard</a>
|
||||
<div class="topnav-tools-dropdown {{ nav_active(request, '/tools') }}">
|
||||
<button type="button" class="topnav-link topnav-tools-toggle" aria-expanded="false" aria-controls="tools-menu"><span class="icon">π§°</span> Tools <span class="topnav-caret">βΎ</span></button>
|
||||
<div class="dropdown-menu" id="tools-menu">
|
||||
<a href="/tools/seo" class="dropdown-item"><span class="icon">π</span> SEO Diagnostics</a>
|
||||
<a href="/tools/deepsearch" class="dropdown-item"><span class="icon">π§ </span> DeepSearch</a>
|
||||
</div>
|
||||
@@ -74,30 +75,30 @@
|
||||
<div class="topnav-right">
|
||||
{% if user %}
|
||||
{% set msg_unread = get_unread_messages(user["uid"]) %}
|
||||
<a href="/messages" class="topnav-icon {% if request.url.path == '/messages' or request.url.path.startswith('/messages/') %}active{% endif %}" data-counter="messages" title="Messages" aria-label="Messages">
|
||||
<span class="nav-bell">βοΈ<span class="nav-badge" data-counter-badge {% if msg_unread == 0 %}hidden{% endif %}>{{ msg_unread }}</span></span>
|
||||
<a href="/messages" class="topnav-icon {{ nav_active(request, '/messages') }}" data-counter="messages" data-counter-noun="messages" title="Messages" aria-label="{% if msg_unread %}{{ msg_unread }} unread messages{% else %}Messages, no unread{% endif %}">
|
||||
<span class="nav-bell" aria-hidden="true">βοΈ<span class="nav-badge" data-counter-badge {% if msg_unread == 0 %}hidden{% endif %}>{{ msg_unread }}</span></span>
|
||||
</a>
|
||||
<button type="button" class="topnav-icon" data-push-enable hidden title="Enable push notifications" aria-label="Enable push notifications">
|
||||
<span class="nav-bell">π</span>
|
||||
</button>
|
||||
{% set unread_count = get_unread_count(user["uid"]) %}
|
||||
<a href="/notifications" class="topnav-icon" data-counter="notifications" title="Notifications" aria-label="Notifications">
|
||||
<span class="nav-bell">π<span class="nav-badge" data-counter-badge {% if unread_count == 0 %}hidden{% endif %}>{{ unread_count }}</span></span>
|
||||
<a href="/notifications" class="topnav-icon" data-counter="notifications" data-counter-noun="notifications" title="Notifications" aria-label="{% if unread_count %}{{ unread_count }} unread notifications{% else %}Notifications, no unread{% endif %}">
|
||||
<span class="nav-bell" aria-hidden="true">π<span class="nav-badge" data-counter-badge {% if unread_count == 0 %}hidden{% endif %}>{{ unread_count }}</span></span>
|
||||
</a>
|
||||
{% if is_admin(user) %}
|
||||
<a href="/admin" class="topnav-icon {% if request.url.path == '/admin' or request.url.path.startswith('/admin/') %}active{% endif %}" title="Admin" aria-label="Admin">
|
||||
<a href="/admin" class="topnav-icon {{ nav_active(request, '/admin') }}" title="Admin" aria-label="Admin">
|
||||
<span class="nav-bell">βοΈ</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="topnav-user-dropdown">
|
||||
<button type="button" class="topnav-user" aria-label="User menu">
|
||||
<button type="button" class="topnav-user" aria-label="User menu" aria-expanded="false" aria-controls="user-menu">
|
||||
<img src="{{ avatar_url('multiavatar', user['username'], 32) }}" class="avatar-img avatar-sm" alt="{{ user['username'] }}" loading="lazy">
|
||||
<div class="topnav-user-info">
|
||||
<span class="topnav-user-name">{{ user['username'] }}</span>
|
||||
<span class="topnav-user-level">Level {{ user.get('level', 1) }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<div class="dropdown-menu" id="user-menu">
|
||||
<a href="/profile/{{ user['username'] }}" class="dropdown-item"><span class="icon">π€</span> Profile</a>
|
||||
<a href="/devii/" class="dropdown-item" data-devii-open><span class="icon">π€</span> Devii</a>
|
||||
<a href="/bookmarks/saved" class="dropdown-item"><span class="icon">π</span> Saved</a>
|
||||
@@ -108,42 +109,42 @@
|
||||
<a href="/auth/login" class="topnav-link"><span class="icon">π</span> Login</a>
|
||||
<a href="/auth/signup" class="btn btn-primary btn-sm"><span class="icon">β¨</span>Sign Up</a>
|
||||
{% endif %}
|
||||
<button type="button" class="topnav-hamburger" id="hamburger-btn" aria-label="Toggle menu">β°</button>
|
||||
<button type="button" class="topnav-hamburger" id="hamburger-btn" aria-label="Toggle menu" aria-controls="mobile-panel" aria-expanded="false">β°</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="topnav-mobile-overlay" id="mobile-overlay"></div>
|
||||
<div class="topnav-mobile-panel" id="mobile-panel">
|
||||
<nav class="topnav-mobile-links">
|
||||
<a href="/" class="topnav-mobile-link {% if request.url.path == '/' %}active{% endif %}"><span class="icon">π </span> Home</a>
|
||||
<a href="/feed" class="topnav-mobile-link {% if request.url.path == '/feed' %}active{% endif %}"><span class="icon">π</span> Posts</a>
|
||||
<a href="/news" class="topnav-mobile-link {% if request.url.path == '/news' or request.url.path.startswith('/news/') %}active{% endif %}"><span class="icon">π°</span> News</a>
|
||||
<a href="/gists" class="topnav-mobile-link {% if request.url.path == '/gists' or request.url.path.startswith('/gists/') %}active{% endif %}"><span class="icon">π</span> Gists</a>
|
||||
<a href="/projects" class="topnav-mobile-link {% if request.url.path == '/projects' or request.url.path.startswith('/projects/') %}active{% endif %}"><span class="icon">π</span> Projects</a>
|
||||
<a href="/leaderboard" class="topnav-mobile-link {% if request.url.path == '/leaderboard' or request.url.path.startswith('/leaderboard/') %}active{% endif %}"><span class="icon">π</span> Leaderboard</a>
|
||||
<div class="topnav-mobile-overlay" id="mobile-overlay" aria-hidden="true"></div>
|
||||
<div class="topnav-mobile-panel" id="mobile-panel" inert>
|
||||
<nav class="topnav-mobile-links" aria-label="Mobile">
|
||||
<a href="/" class="topnav-mobile-link {{ nav_active(request, '/') }}"><span class="icon">π </span> Home</a>
|
||||
<a href="/feed" class="topnav-mobile-link {{ nav_active(request, '/feed') }}"><span class="icon">π</span> Posts</a>
|
||||
<a href="/news" class="topnav-mobile-link {{ nav_active(request, '/news') }}"><span class="icon">π°</span> News</a>
|
||||
<a href="/gists" class="topnav-mobile-link {{ nav_active(request, '/gists') }}"><span class="icon">π</span> Gists</a>
|
||||
<a href="/projects" class="topnav-mobile-link {{ nav_active(request, '/projects') }}"><span class="icon">π</span> Projects</a>
|
||||
<a href="/leaderboard" class="topnav-mobile-link {{ nav_active(request, '/leaderboard') }}"><span class="icon">π</span> Leaderboard</a>
|
||||
<div class="topnav-mobile-divider"></div>
|
||||
<div class="topnav-mobile-section-label">Tools</div>
|
||||
<a href="/tools/seo" class="topnav-mobile-link {% if request.url.path == '/tools/seo' or request.url.path.startswith('/tools/seo/') %}active{% endif %}"><span class="icon">π</span> SEO Diagnostics</a>
|
||||
<a href="/tools/deepsearch" class="topnav-mobile-link {% if request.url.path == '/tools/deepsearch' or request.url.path.startswith('/tools/deepsearch/') %}active{% endif %}"><span class="icon">π§ </span> DeepSearch</a>
|
||||
<a href="/tools/seo" class="topnav-mobile-link {{ nav_active(request, '/tools/seo') }}"><span class="icon">π</span> SEO Diagnostics</a>
|
||||
<a href="/tools/deepsearch" class="topnav-mobile-link {{ nav_active(request, '/tools/deepsearch') }}"><span class="icon">π§ </span> DeepSearch</a>
|
||||
{% if user %}
|
||||
<a href="/messages" class="topnav-mobile-link {% if request.url.path == '/messages' or request.url.path.startswith('/messages/') %}active{% endif %}" data-counter="messages"><span class="icon">βοΈ</span> Messages<span class="nav-badge nav-badge-inline" data-counter-badge {% if get_unread_messages(user["uid"]) == 0 %}hidden{% endif %}>{{ get_unread_messages(user["uid"]) }}</span></a>
|
||||
<a href="/notifications" class="topnav-mobile-link {% if request.url.path == '/notifications' or request.url.path.startswith('/notifications/') %}active{% endif %}" data-counter="notifications"><span class="icon">π</span> Notifications<span class="nav-badge nav-badge-inline" data-counter-badge {% if get_unread_count(user["uid"]) == 0 %}hidden{% endif %}>{{ get_unread_count(user["uid"]) }}</span></a>
|
||||
<a href="/messages" class="topnav-mobile-link {{ nav_active(request, '/messages') }}" data-counter="messages" data-counter-noun="messages" aria-label="{% if get_unread_messages(user['uid']) %}Messages, {{ get_unread_messages(user['uid']) }} unread{% else %}Messages, no unread{% endif %}"><span class="icon">βοΈ</span> Messages<span class="nav-badge nav-badge-inline" data-counter-badge {% if get_unread_messages(user["uid"]) == 0 %}hidden{% endif %} aria-hidden="true">{{ get_unread_messages(user["uid"]) }}</span></a>
|
||||
<a href="/notifications" class="topnav-mobile-link {{ nav_active(request, '/notifications') }}" data-counter="notifications" data-counter-noun="notifications" aria-label="{% if get_unread_count(user['uid']) %}Notifications, {{ get_unread_count(user['uid']) }} unread{% else %}Notifications, no unread{% endif %}"><span class="icon">π</span> Notifications<span class="nav-badge nav-badge-inline" data-counter-badge {% if get_unread_count(user["uid"]) == 0 %}hidden{% endif %} aria-hidden="true">{{ get_unread_count(user["uid"]) }}</span></a>
|
||||
{% if is_admin(user) %}
|
||||
<div class="topnav-mobile-divider"></div>
|
||||
<div class="topnav-mobile-section-label">Admin</div>
|
||||
<a href="/admin" class="topnav-mobile-link {% if request.url.path == '/admin' %}active{% endif %}"><span class="icon">π‘οΈ</span> Dashboard</a>
|
||||
<a href="/admin/users" class="topnav-mobile-link {% if request.url.path == '/admin/users' or request.url.path.startswith('/admin/users/') %}active{% endif %}"><span class="icon">π₯</span> Users</a>
|
||||
<a href="/admin/news" class="topnav-mobile-link {% if request.url.path == '/admin/news' or request.url.path.startswith('/admin/news/') %}active{% endif %}"><span class="icon">π°</span> News</a>
|
||||
<a href="/admin/media" class="topnav-mobile-link {% if request.url.path == '/admin/media' or request.url.path.startswith('/admin/media/') %}active{% endif %}"><span class="icon">πΌοΈ</span> Media</a>
|
||||
<a href="/admin/trash" class="topnav-mobile-link {% if request.url.path == '/admin/trash' or request.url.path.startswith('/admin/trash/') %}active{% endif %}"><span class="icon">ποΈ</span> Trash</a>
|
||||
<a href="/admin/services" class="topnav-mobile-link {% if request.url.path == '/admin/services' or request.url.path.startswith('/admin/services/') %}active{% endif %}"><span class="icon">βοΈ</span> Services</a>
|
||||
<a href="/admin/containers" class="topnav-mobile-link {% if request.url.path == '/admin/containers' or request.url.path.startswith('/admin/containers/') %}active{% endif %}"><span class="icon">π¦</span> Containers</a>
|
||||
<a href="/admin/bots" class="topnav-mobile-link {% if request.url.path == '/admin/bots' or request.url.path.startswith('/admin/bots/') %}active{% endif %}"><span class="icon">π€</span> Bot Monitor</a>
|
||||
<a href="/admin/ai-usage" class="topnav-mobile-link {% if request.url.path == '/admin/ai-usage' or request.url.path.startswith('/admin/ai-usage/') %}active{% endif %}"><span class="icon">π</span> AI usage</a>
|
||||
<a href="/admin/audit-log" class="topnav-mobile-link {% if request.url.path == '/admin/audit-log' or request.url.path.startswith('/admin/audit-log/') %}active{% endif %}"><span class="icon">π</span> Audit Log</a>
|
||||
<a href="/admin/notifications" class="topnav-mobile-link {% if request.url.path == '/admin/notifications' or request.url.path.startswith('/admin/notifications/') %}active{% endif %}"><span class="icon">π</span> Notifications</a>
|
||||
<a href="/admin/settings" class="topnav-mobile-link {% if request.url.path == '/admin/settings' or request.url.path.startswith('/admin/settings/') %}active{% endif %}"><span class="icon">π§</span> Settings</a>
|
||||
<a href="/admin" class="topnav-mobile-link {{ nav_active(request, '/admin', exact=True) }}"><span class="icon">π‘οΈ</span> Dashboard</a>
|
||||
<a href="/admin/users" class="topnav-mobile-link {{ nav_active(request, '/admin/users') }}"><span class="icon">π₯</span> Users</a>
|
||||
<a href="/admin/news" class="topnav-mobile-link {{ nav_active(request, '/admin/news') }}"><span class="icon">π°</span> News</a>
|
||||
<a href="/admin/media" class="topnav-mobile-link {{ nav_active(request, '/admin/media') }}"><span class="icon">πΌοΈ</span> Media</a>
|
||||
<a href="/admin/trash" class="topnav-mobile-link {{ nav_active(request, '/admin/trash') }}"><span class="icon">ποΈ</span> Trash</a>
|
||||
<a href="/admin/services" class="topnav-mobile-link {{ nav_active(request, '/admin/services') }}"><span class="icon">βοΈ</span> Services</a>
|
||||
<a href="/admin/containers" class="topnav-mobile-link {{ nav_active(request, '/admin/containers') }}"><span class="icon">π¦</span> Containers</a>
|
||||
<a href="/admin/bots" class="topnav-mobile-link {{ nav_active(request, '/admin/bots') }}"><span class="icon">π€</span> Bot Monitor</a>
|
||||
<a href="/admin/ai-usage" class="topnav-mobile-link {{ nav_active(request, '/admin/ai-usage') }}"><span class="icon">π</span> AI usage</a>
|
||||
<a href="/admin/audit-log" class="topnav-mobile-link {{ nav_active(request, '/admin/audit-log') }}"><span class="icon">π</span> Audit Log</a>
|
||||
<a href="/admin/notifications" class="topnav-mobile-link {{ nav_active(request, '/admin/notifications') }}"><span class="icon">π</span> Notifications</a>
|
||||
<a href="/admin/settings" class="topnav-mobile-link {{ nav_active(request, '/admin/settings') }}"><span class="icon">π§</span> Settings</a>
|
||||
{% endif %}
|
||||
<div class="topnav-mobile-divider"></div>
|
||||
<div class="topnav-mobile-user">
|
||||
@@ -155,7 +156,7 @@
|
||||
</div>
|
||||
<a href="/profile/{{ user['username'] }}" class="topnav-mobile-link"><span class="icon">π€</span> Profile</a>
|
||||
<a href="/devii/" class="topnav-mobile-link" data-devii-open><span class="icon">π€</span> Devii</a>
|
||||
<a href="/bookmarks/saved" class="topnav-mobile-link {% if request.url.path == '/bookmarks/saved' or request.url.path.startswith('/bookmarks/') %}active{% endif %}"><span class="icon">π</span> Saved</a>
|
||||
<a href="/bookmarks/saved" class="topnav-mobile-link {{ nav_active(request, '/bookmarks') }}"><span class="icon">π</span> Saved</a>
|
||||
<a href="/auth/logout" class="topnav-mobile-link"><span class="icon">πͺ</span> Logout</a>
|
||||
{% else %}
|
||||
<div class="topnav-mobile-divider"></div>
|
||||
@@ -181,7 +182,7 @@
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
<main class="page" data-page-type="{{ page_type_for(request) }}">
|
||||
<main id="main-content" class="page" tabindex="-1" data-page-type="{{ page_type_for(request) }}">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
@@ -194,7 +195,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{%- set _response_time = response_time_ms(request) -%}
|
||||
{%- if _response_time %}<div class="response-time-indicator" title="Server response time">{{ _response_time }} ms</div>{% endif -%}
|
||||
{%- if _response_time %}<div class="response-time-indicator" title="Server response time" aria-hidden="true">{{ _response_time }} ms</div>{% endif -%}
|
||||
|
||||
{% if user %}
|
||||
<template id="comment-reply-template">
|
||||
|
||||
Reference in New Issue
Block a user