message pagination
This commit is contained in:
@@ -164,23 +164,77 @@ async def mark_message_retrieved(
|
|||||||
|
|
||||||
@html_router.get("/messages", response_class=HTMLResponse)
|
@html_router.get("/messages", response_class=HTMLResponse)
|
||||||
async def message_list_page(
|
async def message_list_page(
|
||||||
db: DbDependency,
|
|
||||||
request: Request,
|
request: Request,
|
||||||
type: str = Query("received", alias="msg_type"), # matches your filter links
|
db: DbDependency,
|
||||||
limit: Optional[int] = Query(50, le=100),
|
current_user: HttpUser = Depends(get_current_http_user),
|
||||||
current_user: HttpUser = Depends(get_current_http_user)
|
msg_type: str = Query("received", alias="type"), # Change alias to "type" for cleaner URLs
|
||||||
|
search: Optional[str] = Query(None),
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
per_page: int = Query(50, ge=1, le=200),
|
||||||
):
|
):
|
||||||
from packetserver.http.server import templates
|
from packetserver.http.server import templates # Local import – safe from circular
|
||||||
# Directly call the existing API endpoint function
|
|
||||||
api_resp = await get_messages(db, current_user=current_user, type=type, limit=limit, since=None)
|
username = current_user.username.upper().strip()
|
||||||
messages = api_resp["messages"]
|
|
||||||
|
valid_types = {"received", "sent", "all"}
|
||||||
|
if msg_type not in valid_types:
|
||||||
|
msg_type = "received"
|
||||||
|
|
||||||
|
with db.transaction() as conn:
|
||||||
|
root = conn.root()
|
||||||
|
mailbox = root.get("messages", {}).get(username, [])
|
||||||
|
|
||||||
|
# Build full list of message dicts (similar to API)
|
||||||
|
messages = []
|
||||||
|
for msg in mailbox:
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Type filter
|
||||||
|
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:
|
||||||
|
filtered = messages
|
||||||
|
|
||||||
|
# Search filter (case-insensitive across from/to/text)
|
||||||
|
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()
|
||||||
|
]
|
||||||
|
|
||||||
|
# Sort newest first
|
||||||
|
filtered.sort(key=lambda m: m["sent_at"], reverse=True)
|
||||||
|
|
||||||
|
# Pagination
|
||||||
|
total = len(filtered)
|
||||||
|
start = (page - 1) * per_page
|
||||||
|
paginated = filtered[start:start + per_page]
|
||||||
|
total_pages = (total + per_page - 1) // per_page if total > 0 else 1
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"message_list.html",
|
"message_list.html",
|
||||||
{
|
{
|
||||||
"request": request,
|
"request": request,
|
||||||
"messages": messages,
|
"messages": paginated,
|
||||||
"msg_type": type,
|
"current_type": msg_type, # For tabs/links
|
||||||
"current_user": current_user.username
|
"current_search": search, # For preserving/clearing search
|
||||||
}
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"per_page": per_page,
|
||||||
|
"total_pages": total_pages,
|
||||||
|
"current_user": current_user,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
@@ -1,38 +1,89 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}Messages - PacketServer{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<h1>Messages</h1>
|
<h1>Messages</h1>
|
||||||
<div class="mb-4 text-end">
|
|
||||||
|
<div class="mb-4 d-flex justify-content-between align-items-start flex-wrap gap-3">
|
||||||
|
<div>
|
||||||
|
<!-- 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 %}&page=1">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 %}&page=1">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 %}&page=1">All</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<!-- 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>
|
||||||
|
</form>
|
||||||
|
{% if current_search %}
|
||||||
|
<a href="?type={{ current_type }}" class="btn btn-outline-secondary">Clear</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Compose button -->
|
||||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#composeModal">
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#composeModal">
|
||||||
Compose New Message
|
Compose New Message
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
{% if total > 0 %}
|
||||||
<a href="?msg_type=received" class="btn btn-sm {% if msg_type == 'received' %}btn-primary{% else %}btn-outline-primary{% endif %}">Received</a>
|
<p class="text-muted mb-3">
|
||||||
<a href="?msg_type=sent" class="btn btn-sm {% if msg_type == 'sent' %}btn-primary{% else %}btn-outline-primary{% endif %}">Sent</a>
|
Showing {{ ((page-1)*per_page) + 1 }}–{{ page*per_page if page*per_page < total else total }} of {{ total }} messages
|
||||||
<a href="?msg_type=all" class="btn btn-sm {% if msg_type == 'all' %}btn-primary{% else %}btn-outline-primary{% endif %}">All</a>
|
</p>
|
||||||
</div>
|
{% else %}
|
||||||
|
<p class="text-muted mb-3">No messages</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
<ul class="message-list">
|
<ul class="message-list">
|
||||||
{% for msg in messages %}
|
{% for msg in messages %}
|
||||||
<li>
|
<li>
|
||||||
<strong><a href="/dashboard/message/{{ msg.id }}">{{ msg.text[:60] }}{% if msg.text|length > 60 %}...{% endif %}</a></strong>
|
<strong><a href="/dashboard/message/{{ msg.id }}">{{ msg.text[:60] }}{% if msg.text|length > 60 %}...{% endif %}</a></strong>
|
||||||
{% if msg.has_attachments %}<span class="text-info"> (Attachments)</span>{% endif %}
|
{% if msg.has_attachments %}<span class="text-info">(Attachments)</span>{% endif %}
|
||||||
{% if not msg.retrieved %}<span class="text-warning"> (Unread)</span>{% endif %}
|
{% if not msg.retrieved %}<span class="text-warning">(Unread)</span>{% endif %}
|
||||||
<span class="meta">
|
<span class="meta">
|
||||||
From: {{ msg.from }} | To: {{ msg.to | join(', ') }} | {{ msg.sent_at[:10] }} {{ msg.sent_at[11:19] }}
|
From: {{ msg.from }} | To: {{ msg.to | join(', ') }} | {{ msg.sent_at[:10] }} {{ msg.sent_at[11:19] }}
|
||||||
</span>
|
</span>
|
||||||
<div class="preview">{{ msg.text[:200] }}{% if msg.text|length > 200 %}...{% endif %}</div>
|
<div class="preview">{{ msg.text[:200] }}{% if msg.text|length > 200 %}...{% endif %}</div>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>No messages found.</p>
|
<div class="alert alert-info">
|
||||||
|
{% if current_search %}No messages found matching "{{ current_search }}".{% else %}No messages yet.{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Pagination (only if >1 page) -->
|
||||||
|
{% if total_pages > 1 %}
|
||||||
|
<nav aria-label="Messages pagination" class="mt-4">
|
||||||
|
<ul class="pagination justify-content-center">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{% for p in range(1, total_pages + 1) %}
|
||||||
|
<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 %}
|
||||||
|
|
||||||
|
<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 %}
|
{% endif %}
|
||||||
|
|
||||||
<p><a href="/dashboard">← Back to Dashboard</a></p>
|
<p><a href="/dashboard">← Back to Dashboard</a></p>
|
||||||
{% endblock %}
|
|
||||||
Reference in New Issue
Block a user