summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/notifications.py61
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/app.js8
2 files changed, 54 insertions, 15 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/notifications.py b/packages/meshbay-hub/src/meshbay_hub/api/notifications.py
index ca56620..9d5c125 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/notifications.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/notifications.py
@@ -1,9 +1,28 @@
-"""Notification endpoints — /v1/notifications/*"""
+"""
+Notification endpoints — /v1/notifications/*
+
+**Dismissing one deletes it.** These are signals, not a record: the group is
+still there, the message is still in the chat, the invitation is still an
+invitation, so nothing is lost by dropping the row — which is what
+`purge_notifications` below has always said, now applied to one at a time.
+
+That resolves a disagreement between two halves that were each defensible
+alone. The interface treats a click as "this is gone" and removes the entry;
+the hub marked it read and kept it; and the next launch listed read entries
+too, so everything dismissed came back. Filtering the list to unread fixed
+what the user saw and left the rows accumulating for nothing, invisible for
+ever — which is the state this replaces.
+
+`Notification.read` is therefore **vestigial**: nothing stored can be read,
+because reading it deletes it. It stays because dropping a column is a
+migration for no gain, and `unread_only` stays because it is what an older
+interface asks for and it still answers correctly — every row is unread.
+"""
from fastapi import APIRouter, Depends, HTTPException
from datetime import datetime, timezone
-from sqlalchemy import delete, func, select, update
+from sqlalchemy import delete, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from meshbay_hub.api.deps import get_current_user
@@ -52,16 +71,27 @@ async def list_notifications(
}
+@router.delete("/{notification_id}")
@router.post("/{notification_id}/read")
-async def mark_read(
+async def dismiss(
notification_id: int,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
+ """
+ Dismiss one. The row goes.
+
+ Two paths to the same handler. `DELETE /{id}` says what happens and is what
+ the interface calls; `POST /{id}/read` is what every already-installed
+ client calls, and it has to keep working — the SPA ships inside the desktop
+ package, so a hub is always talking to some interface older than itself.
+ Giving the old path the new behaviour means those clients stop accumulating
+ rows too, rather than only the ones that have been updated.
+ """
notif = await db.get(Notification, notification_id)
if not notif or notif.user_id != current_user.id:
raise HTTPException(status_code=404, detail="Notification not found")
- notif.read = True
+ await db.delete(notif)
await db.commit()
return {"status": "ok"}
@@ -76,7 +106,8 @@ async def purge_notifications(
These are signals, not a record: the group is still there, the message is
still in the chat, the invitation is still an invitation. Nothing is lost by
- clearing the list, so it clears rather than marking a hundred rows read.
+ clearing the list, so it clears rather than marking a hundred rows read —
+ the reasoning the whole module now follows.
"""
result = await db.execute(
delete(Notification).where(Notification.user_id == current_user.id))
@@ -85,17 +116,23 @@ async def purge_notifications(
@router.post("/read-all")
-async def mark_all_read(
+async def dismiss_all(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
- await db.execute(
- update(Notification)
- .where(Notification.user_id == current_user.id, Notification.read == False) # noqa: E712
- .values(read=True)
- )
+ """
+ Dismiss every one — the same thing as `DELETE ""`, under the name an older
+ client knows it by.
+
+ Marking them read instead would put back exactly what this change removes:
+ rows the list can never show again. Nothing in this repository calls it,
+ but an endpoint that is reachable is an endpoint that can be called, and it
+ should not be the one route that still hoards.
+ """
+ result = await db.execute(
+ delete(Notification).where(Notification.user_id == current_user.id))
await db.commit()
- return {"status": "ok"}
+ return {"status": "ok", "removed": result.rowcount}
async def create_notification(
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js
index 7cc7c83..5311403 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js
@@ -650,7 +650,9 @@ function App() {
// the page navigates, which reads as "the click did nothing".
setNotifications(prev => prev.filter(n => n.id !== id));
setUnreadCount(c => Math.max(0, c - 1));
- hubFetch(`/v1/notifications/${id}/read`, { method: 'POST', token: user.token })
+ // DELETE, not `/read`: dismissing one drops the row. The old path still
+ // works and still deletes, for interfaces older than the hub.
+ hubFetch(`/v1/notifications/${id}`, { method: 'DELETE', token: user.token })
.catch(() => fetchNotifications());
}, [user, fetchNotifications]);
@@ -667,8 +669,8 @@ function App() {
if (!user) return;
setNotifications(prev => {
const gone = prev.filter(n => n.group_id === groupId && n.kind === 'group_invite');
- gone.forEach(n => hubFetch(`/v1/notifications/${n.id}/read`,
- { method: 'POST', token: user.token }).catch(() => {}));
+ gone.forEach(n => hubFetch(`/v1/notifications/${n.id}`,
+ { method: 'DELETE', token: user.token }).catch(() => {}));
if (gone.length) setUnreadCount(c => Math.max(0, c - gone.length));
return prev.filter(n => !gone.includes(n));
});