summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--static/css/style.css26
-rw-r--r--takahe/urls.py7
-rw-r--r--templates/identity/view.html29
-rw-r--r--templates/settings/_menu.html3
-rw-r--r--templates/settings/follows.html25
-rw-r--r--users/models/follow.py4
-rw-r--r--users/views/follows.py33
-rw-r--r--users/views/identity.py14
8 files changed, 125 insertions, 16 deletions
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),
@@ -32,6 +32,11 @@ urlpatterns = [
name="settings_profile",
),
path(
+ "settings/follows/",
+ follows.FollowsPage.as_view(),
+ name="settings_follows",
+ ),
+ path(
"settings/interface/",
settings.InterfacePage.as_view(),
name="settings_interface",
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 @@
<img src="{{ identity.local_icon_url }}" class="icon">
{% if request.identity %}
- <form action="{{ identity.urls.action }}" method="POST" class="inline follow">
- {% csrf_token %}
- {% if follow %}
- <input type="hidden" name="action" value="unfollow">
- <button>Unfollow</button>
- {% else %}
- <input type="hidden" name="action" value="follow">
- <button>Follow</button>
- {% endif %}
- </form>
+ {% if identity == request.identity %}
+ <form class="inline follow">
+ <a class="button" href="{% url "settings_profile" %}">Edit Profile</a>
+ </form>
+ {% else %}
+ <form action="{{ identity.urls.action }}" method="POST" class="inline follow {% if reverse_follow %}has-reverse{% endif %}">
+ {% csrf_token %}
+ {% if reverse_follow %}
+ <span class="reverse-follow">Follows You</span>
+ {% endif %}
+ {% if follow %}
+ <input type="hidden" name="action" value="unfollow">
+ <button>Unfollow</button>
+ {% else %}
+ <input type="hidden" name="action" value="follow">
+ <button>Follow</button>
+ {% endif %}
+ </form>
+ {% endif %}
{% endif %}
{{ identity.name_or_handle }} <small>@{{ identity.handle }}</small>
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 @@
<a href="{% url "settings_interface" %}" {% if section == "interface" %}class="selected"{% endif %}>
<i class="fa-solid fa-display"></i> Interface
</a>
+ <a href="{% url "settings_follows" %}" {% if section == "follows" %}class="selected"{% endif %}>
+ <i class="fa-solid fa-arrow-right-arrow-left"></i> Follows
+ </a>
{% if request.user.admin %}
<h3>Account</h3>
<a href="{% url "settings_security" %}" {% if section == "security" %}class="selected"{% endif %}>
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 %}
+ <section class="icon-menu">
+ {% for identity, details in identities %}
+ <a class="option" href="{{ identity.urls.view }}">
+ <img src="{{ identity.local_icon_url }}">
+ <span class="handle">
+ {{ identity.name_or_handle }}
+ <small>@{{ identity.handle }}</small>
+ </span>
+ {% if details.outbound %}
+ <span class="pill">Following</span>
+ {% endif %}
+ {% if details.inbound %}
+ <span class="pill">Follows You</span>
+ {% endif %}
+ </a>
+ {% empty %}
+ <p class="option empty">You have no follows.</p>
+ {% endfor %}
+ </section>
+{% 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
@@ -24,6 +24,10 @@ class FollowStates(StateGraph):
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"):
"""
Follows that are unrequested need us to deliver the Follow object
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,
}