message pagination

This commit is contained in:
Michael Woods
2025-12-28 19:11:48 -05:00
parent 4e24bb4fb4
commit fd9d113eef
2 changed files with 139 additions and 41 deletions

View File

@@ -1,5 +1,5 @@
# packetserver/http/routers/messages.py
from fastapi import APIRouter, Depends, Query, HTTPException, Path, Request
from fastapi import APIRouter, Depends, Query, HTTPException, Path, Request, Query
from fastapi.responses import HTMLResponse
from typing import Optional
from datetime import datetime
@@ -11,6 +11,7 @@ from pydantic import BaseModel, Field, validator
from packetserver.http.dependencies import get_current_http_user
from packetserver.http.auth import HttpUser
from packetserver.http.database import DbDependency
from packetserver.http.server import templates
html_router = APIRouter(tags=["messages-html"])
@@ -27,58 +28,78 @@ class MarkRetrievedRequest(BaseModel):
return v
@router.get("/messages")
async def get_messages(
@router.get("/messages", response_class=HTMLResponse)
async def message_list(
request: Request,
db: DbDependency,
current_user: HttpUser = Depends(get_current_http_user),
type: str = Query("received", description="received, sent, or all"),
limit: Optional[int] = Query(20, le=100, description="Max messages to return (default 20, max 100)"),
since: Optional[str] = Query(None, description="ISO UTC timestamp filter (e.g. 2025-12-01T00:00:00Z)"),
msg_type: str = Query("received", alias="type"), # received, sent, all
search: Optional[str] = Query(None),
page: int = Query(1, ge=1),
per_page: int = Query(50, ge=1, le=200),
):
if limit is None or limit < 1:
limit = 20
username = current_user.username.upper().strip()
valid_types = {"received", "sent", "all"}
if msg_type not in valid_types:
msg_type = "received"
username = current_user.username
with db.transaction() as conn:
root = conn.root()
user_messages = root["messages"].get(username, [])
if 'messages' not in root:
root['messages'] = PersistentMapping()
if username not in root['messages']:
root['messages'][username] = persistent.list.PersistentList()
# Convert to list of dicts (as current code does)
messages = []
for msg_id, msg in user_messages.items():
messages.append({
"id": msg_id,
"from": msg["from"],
"to": msg["to"],
"text": msg["text"],
"timestamp": msg["timestamp"],
})
mailbox = root['messages'][username]
# Filter by type
if msg_type == "received":
filtered = [m for m in messages if username in m["to"]]
elif msg_type == "sent":
filtered = [m for m in messages if m["from"] == username]
else: # all
filtered = messages
since_dt = None
if since:
try:
since_dt = datetime.fromisoformat(since.replace("Z", "+00:00"))
except ValueError:
raise HTTPException(status_code=400, detail="Invalid 'since' format")
# Apply search
if search:
search_lower = search.strip().lower()
filtered = [
m for m in filtered
if (search_lower in m["from"].lower() or
any(search_lower in t.lower() for t in m["to"]) or
search_lower in m["text"].lower())
]
messages = []
for msg in mailbox:
if type == "received" and msg.msg_from == username:
continue
if type == "sent" and msg.msg_from != username:
continue
if since_dt and msg.sent_at < since_dt:
continue
# Sort newest first
filtered.sort(key=lambda m: m["timestamp"], reverse=True)
messages.append({
"id": str(msg.msg_id),
"from": msg.msg_from,
"to": list(msg.msg_to) if isinstance(msg.msg_to, tuple) else [msg.msg_to],
"sent_at": msg.sent_at.isoformat() + "Z",
"text": msg.text,
"has_attachments": len(msg.attachments) > 0,
"retrieved": msg.retrieved,
})
# Pagination
total = len(filtered)
start = (page - 1) * per_page
paginated = filtered[start:start + per_page]
total_pages = (total + per_page - 1) // per_page
messages.sort(key=lambda m: m["sent_at"], reverse=True)
return {"messages": messages[:limit], "total_returned": len(messages[:limit])}
return templates.TemplateResponse(
"message_list.html",
{
"request": request,
"messages": paginated,
"current_user": current_user,
"total": total,
"page": page,
"per_page": per_page,
"total_pages": total_pages,
"current_type": msg_type,
"current_search": search,
}
)
@router.get("/messages/{msg_id}")
async def get_message(

View File

@@ -10,6 +10,45 @@
</button>
</div>
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-3">
<!-- Type tabs -->
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link {% if current_type == 'received' %}active{% endif %}"
href="?type=received{% if current_search %}&search={{ current_search }}{% endif %}">Received</a>
</li>
<li class="nav-item">
<a class="nav-link {% if current_type == 'sent' %}active{% endif %}"
href="?type=sent{% if current_search %}&search={{ current_search }}{% endif %}">Sent</a>
</li>
<li class="nav-item">
<a class="nav-link {% if current_type == 'all' %}active{% endif %}"
href="?type=all{% if current_search %}&search={{ current_search }}{% endif %}">All</a>
</li>
</ul>
<!-- Search form -->
<form method="get" class="d-flex">
<input type="hidden" name="type" value="{{ current_type }}">
<input type="text" name="search" class="form-control me-2" placeholder="Search messages..."
value="{{ current_search or '' }}">
<button type="submit" class="btn btn-outline-primary">Search</button>
{% if current_search %}
<a href="?type={{ current_type }}" class="btn btn-outline-secondary ms-2">Clear</a>
{% endif %}
</form>
</div>
{% if total == 0 %}
<div class="alert alert-info">
{% if current_search %}
No messages found matching "{{ current_search }}".
{% else %}
No messages yet.
{% endif %}
</div>
{% endif %}
<div class="mb-3">
<a href="?msg_type=received" class="btn btn-sm {% if msg_type == 'received' %}btn-primary{% else %}btn-outline-primary{% endif %}">Received</a>
<a href="?msg_type=sent" class="btn btn-sm {% if msg_type == 'sent' %}btn-primary{% else %}btn-outline-primary{% endif %}">Sent</a>
@@ -17,6 +56,9 @@
</div>
{% if messages %}
<p class="text-muted text-center mb-3">
Showing {{ (page-1)*per_page + 1 }}{{ min(page*per_page, total) }} of {{ total }} messages
</p>
<ul class="message-list">
{% for msg in messages %}
<li>
@@ -30,6 +72,41 @@
</li>
{% endfor %}
</ul>
{% if total_pages > 1 %}
<nav aria-label="Message pagination" class="mt-4">
<ul class="pagination justify-content-center">
<!-- Previous -->
<li class="page-item {% if page == 1 %}disabled{% endif %}">
<a class="page-link" href="?page={{ page - 1 }}&type={{ current_type }}{% if current_search %}&search={{ current_search }}{% endif %}">Previous</a>
</li>
<!-- Page numbers (simple: show current ±2, plus first/last) -->
{% set nearby = range(max(1, page-2), min(total_pages+1, page+3)) %}
{% if 1 not in nearby %}
<li class="page-item"><a class="page-link" href="?page=1&type={{ current_type }}{% if current_search %}&search={{ current_search }}{% endif %}">1</a></li>
{% if page > 4 %}<li class="page-item disabled"><span class="page-link">...</span></li>{% endif %}
{% endif %}
{% for p in nearby %}
<li class="page-item {% if p == page %}active{% endif %}">
<a class="page-link" href="?page={{ p }}&type={{ current_type }}{% if current_search %}&search={{ current_search }}{% endif %}">{{ p }}</a>
</li>
{% endfor %}
{% if total_pages not in nearby %}
{% if page < total_pages - 3 %}<li class="page-item disabled"><span class="page-link">...</span></li>{% endif %}
<li class="page-item"><a class="page-link" href="?page={{ total_pages }}&type={{ current_type }}{% if current_search %}&search={{ current_search }}{% endif %}">{{ total_pages }}</a></li>
{% endif %}
<!-- Next -->
<li class="page-item {% if page == total_pages %}disabled{% endif %}">
<a class="page-link" href="?page={{ page + 1 }}&type={{ current_type }}{% if current_search %}&search={{ current_search }}{% endif %}">Next</a>
</li>
</ul>
</nav>
{% endif %}
{% else %}
<p>No messages found.</p>
{% endif %}