diff options
Diffstat (limited to 'packages/meshbay-hub/src')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/api/notifications.py | 61 | ||||
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/app.js | 8 |
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)); }); |