Rich chat messages (reactions, replies, files, images); RSS poll countdown; Speakers' Corner directory page updates

- Chat: extract reactions, quoted replies, file/image data in _normalize_item
- Chat: render emoji reaction pills, reply-quote blocks, inline image previews, file blocks with Accept/Download
- Chat: reply UI (hover → set reply → preview bar above compose → send with quotedItemId)
- Chat: emoji picker strip (6 quick-react emojis) on message hover
- Chat: POST /react and POST /file/{id}/receive and GET /file/{id}/download endpoints
- Chat: file decryption via core.chat_read_file (native libsimplex FFI), served with correct MIME type
- List: RSS bot cards show live next-poll countdown (ticks every second via status API poll_next field)
- Directory: rename SimpleXXX → Speakers' Corner Online Directory throughout
- Directory: add hero banner image, About page link, QR popout, title hyperlink
- Directory: new about.html — Online Safety Act, Digital ID, 65k arrests stat, community rules

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jon
2026-06-07 20:23:00 +01:00
parent 432e4a5e83
commit 7c712c9ee3
8 changed files with 912 additions and 34 deletions

View File

@@ -7,8 +7,10 @@ import os
from contextlib import asynccontextmanager
from pathlib import Path
import mimetypes
from fastapi import FastAPI, Form, HTTPException, Request
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse, Response
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
@@ -316,14 +318,61 @@ async def chat_send(request: Request, profile_id: int, chat_type: str, chat_id:
text = data.get("text", "").strip()
if not text:
raise HTTPException(400, "text required")
reply_to_id = data.get("reply_to_id")
try:
await pm.send_to_chat(profile_id, chat_type, chat_id, text)
await pm.send_to_chat(profile_id, chat_type, chat_id, text, reply_to_id=reply_to_id)
except Exception as e:
log.error("chat send failed (profile=%s %s/%s): %s", profile_id, chat_type, chat_id, e)
return JSONResponse({"ok": False, "error": str(e)})
return JSONResponse({"ok": True})
@app.post("/api/profiles/{profile_id}/chat/{chat_type}/{chat_id}/react")
async def chat_react(request: Request, profile_id: int, chat_type: str, chat_id: int):
_require_auth(request)
data = await request.json()
item_id = data.get("item_id")
emoji = data.get("emoji", "")
add = data.get("add", True)
if not item_id or not emoji:
raise HTTPException(400, "item_id and emoji required")
try:
await pm.send_reaction(profile_id, chat_type, chat_id, item_id, emoji, add)
except Exception as e:
log.error("react failed: %s", e)
return JSONResponse({"ok": False, "error": str(e)})
return JSONResponse({"ok": True})
@app.post("/api/profiles/{profile_id}/file/{file_id}/receive")
async def file_receive(request: Request, profile_id: int, file_id: int):
_require_auth(request)
try:
await pm.accept_file(profile_id, file_id)
except Exception as e:
log.error("file receive failed: %s", e)
return JSONResponse({"ok": False, "error": str(e)})
return JSONResponse({"ok": True})
@app.get("/api/profiles/{profile_id}/file/{file_id}/download")
async def file_download(request: Request, profile_id: int, file_id: int):
_require_auth(request)
try:
data, filename = await pm.read_file_bytes(profile_id, file_id)
except FileNotFoundError as e:
raise HTTPException(404, str(e))
except Exception as e:
log.error("file download failed: %s", e)
raise HTTPException(500, str(e))
mime = mimetypes.guess_type(filename)[0] or "application/octet-stream"
return Response(
content=data,
media_type=mime,
headers={"Content-Disposition": f'inline; filename="{filename}"'},
)
@app.post("/api/profiles/{profile_id}/chat/{chat_type}/{chat_id}/clear")
async def chat_clear(request: Request, profile_id: int, chat_type: str, chat_id: int):
_require_auth(request)
@@ -425,6 +474,7 @@ async def profile_status(request: Request, profile_id: int):
"contacts": len(bot.contacts) if bot else 0,
"groups": len(bot.groups) if bot else 0,
"log": bot.log_lines[-20:] if bot else [],
"poll_next": bot.poll_next if bot else 0,
})