diff --git a/static/css/style.css b/static/css/style.css
index 43d4448..eba0e4d 100644
--- a/static/css/style.css
+++ b/static/css/style.css
@@ -335,6 +335,18 @@ nav a i {
font-size: 200%;
}
+.icon-menu .option .handle {
+ margin-right: 20px;
+}
+
+.icon-menu .option .pill {
+ display: inline-block;
+ padding: 5px 8px;
+ background: var(--color-highlight);
+ border-radius: 5px;
+ margin: 0 5px 0 5px;
+}
+
.handle {
vertical-align: middle;
display: inline-block;
@@ -375,6 +387,20 @@ form.follow {
float: right;
margin: 20px 0 0 0;
font-size: 16px;
+ text-align: center;
+}
+
+form.follow.has-reverse {
+ margin-top: 0;
+}
+
+form.follow .reverse-follow {
+ display: block;
+ margin: 0 0 5px 0;
+}
+
+form.follow button {
+ margin: 0;
}
form h1 {
diff --git a/takahe/urls.py b/takahe/urls.py
index 0ea49d0..044599a 100644
--- a/takahe/urls.py
+++ b/takahe/urls.py
@@ -6,7 +6,7 @@ from django.views.static import serve
from activities.views import posts, search, timelines
from core import views as core
from stator import views as stator
-from users.views import activitypub, admin, auth, identity, settings
+from users.views import activitypub, admin, auth, follows, identity, settings
urlpatterns = [
path("", core.homepage),
@@ -31,6 +31,11 @@ urlpatterns = [
settings.ProfilePage.as_view(),
name="settings_profile",
),
+ path(
+ "settings/follows/",
+ follows.FollowsPage.as_view(),
+ name="settings_follows",
+ ),
path(
"settings/interface/",
settings.InterfacePage.as_view(),
diff --git a/templates/identity/view.html b/templates/identity/view.html
index f877f59..d584022 100644
--- a/templates/identity/view.html
+++ b/templates/identity/view.html
@@ -13,16 +13,25 @@
{% if request.identity %}
-
+ {% if identity == request.identity %}
+
+ {% else %}
+
+ {% endif %}
{% endif %}
{{ identity.name_or_handle }} @{{ identity.handle }}
diff --git a/templates/settings/_menu.html b/templates/settings/_menu.html
index cc87941..dd43912 100644
--- a/templates/settings/_menu.html
+++ b/templates/settings/_menu.html
@@ -6,6 +6,9 @@
Interface
+
+ Follows
+
{% if request.user.admin %}
Account
diff --git a/templates/settings/follows.html b/templates/settings/follows.html
new file mode 100644
index 0000000..5f43d05
--- /dev/null
+++ b/templates/settings/follows.html
@@ -0,0 +1,25 @@
+{% extends "settings/base.html" %}
+
+{% block subtitle %}Follows{% endblock %}
+
+{% block content %}
+
+{% endblock %}
diff --git a/users/models/follow.py b/users/models/follow.py
index d2ee493..e741c56 100644
--- a/users/models/follow.py
+++ b/users/models/follow.py
@@ -23,6 +23,10 @@ class FollowStates(StateGraph):
accepted.transitions_to(undone)
undone.transitions_to(undone_remotely)
+ @classmethod
+ def group_active(cls):
+ return [cls.unrequested, cls.local_requested, cls.accepted]
+
@classmethod
async def handle_unrequested(cls, instance: "Follow"):
"""
diff --git a/users/views/follows.py b/users/views/follows.py
new file mode 100644
index 0000000..9030efe
--- /dev/null
+++ b/users/views/follows.py
@@ -0,0 +1,33 @@
+from django.utils.decorators import method_decorator
+from django.views.generic import TemplateView
+
+from users.decorators import identity_required
+from users.models import FollowStates
+
+
+@method_decorator(identity_required, name="dispatch")
+class FollowsPage(TemplateView):
+ """
+ Shows followers/follows.
+ """
+
+ template_name = "settings/follows.html"
+
+ def get_context_data(self):
+ # Gather all identities with a following relationship with us
+ identities = {}
+ for outbound_follow in self.request.identity.outbound_follows.filter(
+ state__in=FollowStates.group_active()
+ ):
+ identities.setdefault(outbound_follow.target, {})[
+ "outbound"
+ ] = outbound_follow
+ for inbound_follow in self.request.identity.inbound_follows.filter(
+ state__in=FollowStates.group_active()
+ ):
+ identities.setdefault(inbound_follow.source, {})["inbound"] = inbound_follow
+
+ return {
+ "section": "follows",
+ "identities": sorted(identities.items(), key=lambda i: i[0].username),
+ }
diff --git a/users/views/identity.py b/users/views/identity.py
index b83ba9a..ae8e5b0 100644
--- a/users/views/identity.py
+++ b/users/views/identity.py
@@ -28,18 +28,22 @@ class ViewIdentity(TemplateView):
if identity.data_age > Config.system.identity_max_age:
identity.transition_perform(IdentityStates.outdated)
follow = None
+ reverse_follow = None
if self.request.identity:
follow = Follow.maybe_get(self.request.identity, identity)
- if follow and follow.state not in [
- FollowStates.unrequested,
- FollowStates.local_requested,
- FollowStates.accepted,
- ]:
+ if follow and follow.state not in FollowStates.group_active():
follow = None
+ reverse_follow = Follow.maybe_get(identity, self.request.identity)
+ if (
+ reverse_follow
+ and reverse_follow.state not in FollowStates.group_active()
+ ):
+ reverse_follow = None
return {
"identity": identity,
"posts": posts,
"follow": follow,
+ "reverse_follow": reverse_follow,
}