diff --git a/activities/migrations/0018_timelineevent_dismissed.py b/activities/migrations/0018_timelineevent_dismissed.py new file mode 100644 index 0000000..ada2a5f --- /dev/null +++ b/activities/migrations/0018_timelineevent_dismissed.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.2 on 2023-07-09 17:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("activities", "0017_stator_next_change"), + ] + + operations = [ + migrations.AddField( + model_name="timelineevent", + name="dismissed", + field=models.BooleanField(default=False), + ), + ] diff --git a/activities/models/timeline_event.py b/activities/models/timeline_event.py index 33976b8..fe1be09 100644 --- a/activities/models/timeline_event.py +++ b/activities/models/timeline_event.py @@ -55,6 +55,7 @@ class TimelineEvent(models.Model): published = models.DateTimeField(default=timezone.now) seen = models.BooleanField(default=False) + dismissed = models.BooleanField(default=False) created = models.DateTimeField(auto_now_add=True) diff --git a/activities/services/timeline.py b/activities/services/timeline.py index c18685a..925606a 100644 --- a/activities/services/timeline.py +++ b/activities/services/timeline.py @@ -77,7 +77,7 @@ class TimelineService: def notifications(self, types: list[str]) -> models.QuerySet[TimelineEvent]: return ( self.event_queryset() - .filter(identity=self.identity, type__in=types) + .filter(identity=self.identity, type__in=types, dismissed=False) .order_by("-created") ) diff --git a/api/urls.py b/api/urls.py index faff8aa..b479208 100644 --- a/api/urls.py +++ b/api/urls.py @@ -76,7 +76,9 @@ urlpatterns = [ path("v1/statuses//source", statuses.status_source), # Notifications path("v1/notifications", notifications.notifications), + path("v1/notifications/clear", notifications.dismiss_notifications), path("v1/notifications/", notifications.get_notification), + path("v1/notifications//dismiss", notifications.dismiss_notification), # Polls path("v1/polls/", polls.get_poll), path("v1/polls//votes", polls.vote_poll), diff --git a/api/views/notifications.py b/api/views/notifications.py index 677a561..8f2bd51 100644 --- a/api/views/notifications.py +++ b/api/views/notifications.py @@ -72,3 +72,29 @@ def get_notification( id=id, ) return schemas.Notification.from_timeline_event(notification) + + +@scope_required("write:notifications") +@api_view.post +def dismiss_notifications(request: HttpRequest) -> dict: + TimelineService(request.identity).notifications( + list(NOTIFICATION_TYPES.values()) + ).update(dismissed=True) + + return {} + + +@scope_required("write:notifications") +@api_view.post +def dismiss_notification(request: HttpRequest, id: str) -> dict: + notification = get_object_or_404( + TimelineService(request.identity).notifications( + list(NOTIFICATION_TYPES.values()) + ), + id=id, + ) + + notification.dismissed = True + notification.save() + + return {} diff --git a/tests/api/notifications.py b/tests/api/notifications.py index cebf2c9..13c2515 100644 --- a/tests/api/notifications.py +++ b/tests/api/notifications.py @@ -11,11 +11,10 @@ def test_notifications(api_client, identity, remote_identity): subject_identity=remote_identity, ) - response = api_client.get("/api/v1/notifications").json() - - assert len(response) == 1 - assert response[0]["type"] == "follow" - assert response[0]["account"]["id"] == str(remote_identity.id) + data = api_client.get("/api/v1/notifications").json() + assert len(data) == 1 + assert data[0]["type"] == "follow" + assert data[0]["account"]["id"] == str(remote_identity.id) event.delete() @@ -28,8 +27,55 @@ def test_get_notification(api_client, identity, remote_identity): subject_identity=remote_identity, ) - response = api_client.get(f"/api/v1/notifications/{event.id}").json() - assert response["type"] == "follow" - assert response["account"]["id"] == str(remote_identity.id) + data = api_client.get(f"/api/v1/notifications/{event.id}").json() + assert data["type"] == "follow" + assert data["account"]["id"] == str(remote_identity.id) event.delete() + + +@pytest.mark.django_db +def test_dismiss_notifications(api_client, identity, identity2, remote_identity): + TimelineEvent.objects.create( + identity=identity, + type=TimelineEvent.Types.followed, + subject_identity=identity2, + ) + TimelineEvent.objects.create( + identity=identity, + type=TimelineEvent.Types.followed, + subject_identity=remote_identity, + ) + + data = api_client.get("/api/v1/notifications").json() + assert len(data) == 2 + + response = api_client.post("/api/v1/notifications/clear", {}) + assert response.status_code == 200 + assert response.json() == {} + + data = api_client.get("/api/v1/notifications").json() + assert len(data) == 0 + + TimelineEvent.objects.filter(identity=identity).delete() + + +@pytest.mark.django_db +def test_dismiss_notification(api_client, identity, remote_identity): + event = TimelineEvent.objects.create( + identity=identity, + type=TimelineEvent.Types.followed, + subject_identity=remote_identity, + ) + + data = api_client.get("/api/v1/notifications").json() + assert len(data) == 1 + + response = api_client.post(f"/api/v1/notifications/{event.id}/dismiss", {}) + assert response.status_code == 200 + assert response.json() == {} + + data = api_client.get("/api/v1/notifications").json() + assert len(data) == 0 + + TimelineEvent.objects.filter(identity=identity).delete()