diff options
-rw-r--r-- | static/css/style.css | 26 | ||||
-rw-r--r-- | takahe/urls.py | 7 | ||||
-rw-r--r-- | templates/identity/view.html | 29 | ||||
-rw-r--r-- | templates/settings/_menu.html | 3 | ||||
-rw-r--r-- | templates/settings/follows.html | 25 | ||||
-rw-r--r-- | users/models/follow.py | 4 | ||||
-rw-r--r-- | users/views/follows.py | 33 | ||||
-rw-r--r-- | users/views/identity.py | 14 |
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, } |