diff --git a/static/css/style.css b/static/css/style.css index 8fdfe1a..cc47d59 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -904,7 +904,11 @@ table.metadata td .emoji { } .view-options { - margin: 0 0 10px 3px; + margin: 0 0 10px 0px; +} + +.view-options.follows { + margin: 0 0 20px 0px; } .view-options a { diff --git a/takahe/urls.py b/takahe/urls.py index 3f7d77b..47e7dfd 100644 --- a/takahe/urls.py +++ b/takahe/urls.py @@ -161,6 +161,8 @@ urlpatterns = [ path("@/action/", identity.ActionIdentity.as_view()), path("@/rss/", identity.IdentityFeed()), path("@/report/", report.SubmitReport.as_view()), + path("@/following/", identity.IdentityFollows.as_view(inbound=False)), + path("@/followers/", identity.IdentityFollows.as_view(inbound=True)), # Posts path("compose/", compose.Compose.as_view(), name="compose"), path( diff --git a/templates/identity/follows.html b/templates/identity/follows.html new file mode 100644 index 0000000..36f1094 --- /dev/null +++ b/templates/identity/follows.html @@ -0,0 +1,24 @@ +{% extends "identity/view.html" %} + +{% block title %}{% if self.inbound %}Followers{% else %}Following{% endif %} - {{ identity }}{% endblock %} + +{% block subcontent %} + + {% for identity in page_obj %} + {% include "activities/_identity.html" %} + {% empty %} + + This person has no {% if self.inbound %}followers{% else %}follows{% endif %} yet. + + {% endfor %} + +
+ {% if page_obj.has_previous %} + Previous Page + {% endif %} + {% if page_obj.has_next %} + Next Page + {% endif %} +
+ +{% endblock %} diff --git a/templates/identity/view.html b/templates/identity/view.html index 9ad8ce2..760ffd6 100644 --- a/templates/identity/view.html +++ b/templates/identity/view.html @@ -20,7 +20,7 @@ {% if request.identity %} {% if identity == request.identity %} - {% else %} @@ -72,13 +72,11 @@ {% endif %} - {% if identity.config_identity.visible_follows %} -
-
    -
  • {{ following_count }} following
  • -
  • {{ followers_count }} follower{{ followers_count|pluralize }}
  • -
-
+ {% if identity.local and identity.config_identity.visible_follows %} + {% endif %} {% if not identity.local %} @@ -94,24 +92,28 @@ {% endif %} {% endif %} - {% for post in page_obj %} - {% include "activities/_post.html" %} - {% empty %} - - {% if identity.local %} - No posts yet. - {% else %} - No posts have been received/retrieved by this server yet. + {% block subcontent %} - {% if identity.profile_uri %} - You might find historical posts at - their original profile ➔ + {% for post in page_obj %} + {% include "activities/_post.html" %} + {% empty %} + + {% if identity.local %} + No posts yet. + {% else %} + No posts have been received/retrieved by this server yet. + + {% if identity.profile_uri %} + You might find historical posts at + their original profile ➔ + {% endif %} {% endif %} - {% endif %} - - {% endfor %} + + {% endfor %} - {% if page_obj.has_next %} - - {% endif %} + {% if page_obj.has_next %} + + {% endif %} + + {% endblock %} {% endblock %} diff --git a/tests/users/views/settings/test_privacy.py b/tests/users/views/settings/test_privacy.py index 8c928b8..48c356b 100644 --- a/tests/users/views/settings/test_privacy.py +++ b/tests/users/views/settings/test_privacy.py @@ -21,6 +21,9 @@ def test_visible_follows_disabled(client, identity): """ Tests that disabling visible follows hides it from profile """ + Config.set_identity(identity, "visible_follows", True) + response = client.get(identity.urls.view) + assertContains(response, '
', status_code=200) Config.set_identity(identity, "visible_follows", False) response = client.get(identity.urls.view) - assertNotContains(response, '
', status_code=200) + assertNotContains(response, '
', status_code=200) diff --git a/users/models/identity.py b/users/models/identity.py index a12dbe3..589018b 100644 --- a/users/models/identity.py +++ b/users/models/identity.py @@ -219,6 +219,8 @@ class Identity(StatorModel): class urls(urlman.Urls): view = "/@{self.username}@{self.domain_id}/" action = "{view}action/" + followers = "{view}followers/" + following = "{view}following/" activate = "{view}activate/" admin = "/admin/identities/" admin_edit = "{admin}{self.pk}/" diff --git a/users/services/identity.py b/users/services/identity.py index 743258a..8cc7cb0 100644 --- a/users/services/identity.py +++ b/users/services/identity.py @@ -1,5 +1,7 @@ from typing import cast +from django.db import models + from users.models import Follow, FollowStates, Identity @@ -11,6 +13,16 @@ class IdentityService: def __init__(self, identity: Identity): self.identity = identity + def following(self) -> models.QuerySet[Identity]: + return Identity.objects.filter( + inbound_follows__source=self.identity + ).not_deleted() + + def followers(self) -> models.QuerySet[Identity]: + return Identity.objects.filter( + outbound_follows__target=self.identity + ).not_deleted() + def follow_from(self, from_identity: Identity) -> Follow: """ Follows a user (or does nothing if already followed). diff --git a/users/views/identity.py b/users/views/identity.py index 0a612a5..4b33868 100644 --- a/users/views/identity.py +++ b/users/views/identity.py @@ -29,7 +29,7 @@ class ViewIdentity(ListView): """ template_name = "identity/view.html" - paginate_by = 5 + paginate_by = 25 def get(self, request, handle): # Make sure we understand this handle @@ -140,6 +140,45 @@ class IdentityFeed(Feed): return item.published +class IdentityFollows(ListView): + """ + Shows following/followers for an identity. + """ + + template_name = "identity/follows.html" + paginate_by = 25 + inbound = False + + def get(self, request, handle): + self.identity = by_handle_or_404( + self.request, + handle, + local=False, + ) + if not Config.load_identity(self.identity).visible_follows: + raise Http404("Hidden follows") + return super().get(request, identity=self.identity) + + def get_queryset(self): + if self.inbound: + return IdentityService(self.identity).followers() + else: + return IdentityService(self.identity).following() + + def get_context_data(self): + context = super().get_context_data() + context["identity"] = self.identity + context["inbound"] = self.inbound + context["follows_page"] = True + context["followers_count"] = self.identity.inbound_follows.filter( + state__in=FollowStates.group_active() + ).count() + context["following_count"] = self.identity.outbound_follows.filter( + state__in=FollowStates.group_active() + ).count() + return context + + @method_decorator(identity_required, name="dispatch") class ActionIdentity(View): def post(self, request, handle):