Option to show/hide boosts for a followed user (#317)

This commit is contained in:
Cosmin Stejerean 2022-12-30 14:03:11 -08:00 committed by GitHub
parent 0b208d3bf7
commit eea83214cb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 92 additions and 21 deletions

View file

@ -32,9 +32,9 @@ class PostInteractionStates(StateGraph):
interaction = await instance.afetch_full() interaction = await instance.afetch_full()
# Boost: send a copy to all people who follow this user # Boost: send a copy to all people who follow this user
if interaction.type == interaction.Types.boost: if interaction.type == interaction.Types.boost:
async for follow in interaction.identity.inbound_follows.select_related( async for follow in interaction.identity.inbound_follows.filter(
"source", "target" boosts=True
): ).select_related("source", "target"):
if follow.source.local or follow.target.local: if follow.source.local or follow.target.local:
await FanOut.objects.acreate( await FanOut.objects.acreate(
type=FanOut.Types.interaction, type=FanOut.Types.interaction,
@ -294,7 +294,7 @@ class PostInteraction(StatorModel):
# Boosts (announces) go to everyone who follows locally # Boosts (announces) go to everyone who follows locally
if interaction.type == cls.Types.boost: if interaction.type == cls.Types.boost:
for follow in Follow.objects.filter( for follow in Follow.objects.filter(
target=interaction.identity, source__local=True target=interaction.identity, source__local=True, boosts=True
): ):
TimelineEvent.add_post_interaction(follow.source, interaction) TimelineEvent.add_post_interaction(follow.source, interaction)
# Likes go to just the author of the post # Likes go to just the author of the post

View file

@ -175,12 +175,12 @@ def account_statuses(
@api_router.post("/v1/accounts/{id}/follow", response=schemas.Relationship) @api_router.post("/v1/accounts/{id}/follow", response=schemas.Relationship)
@identity_required @identity_required
def account_follow(request, id: str): def account_follow(request, id: str, reblogs: bool = True):
identity = get_object_or_404( identity = get_object_or_404(
Identity.objects.exclude(restriction=Identity.Restriction.blocked), pk=id Identity.objects.exclude(restriction=Identity.Restriction.blocked), pk=id
) )
service = IdentityService(identity) service = IdentityService(identity)
service.follow_from(request.identity) service.follow_from(request.identity, boosts=reblogs)
return service.mastodon_json_relationship(request.identity) return service.mastodon_json_relationship(request.identity)

View file

@ -595,6 +595,8 @@ fieldset legend {
.right-column form, .right-column form,
form.inline { form.inline {
padding: 0; padding: 0;
margin: 0;
display:inline;
} }
div.follow-profile { div.follow-profile {
@ -604,11 +606,11 @@ div.follow-profile {
text-align: center; text-align: center;
} }
div.follow-profile.has-reverse { .follow-profile.has-reverse {
margin-top: 0; margin-top: 0;
} }
div.follow-profile .reverse-follow { .follow-profile .reverse-follow {
display: block; display: block;
margin: 0 0 5px 0; margin: 0 0 5px 0;
} }
@ -644,6 +646,7 @@ div.follow-profile .actions menu {
top: 43px; top: 43px;
} }
div.follow-profile .actions menu.enabled { div.follow-profile .actions menu.enabled {
display: block; display: block;
min-width: 160px; min-width: 160px;
@ -658,7 +661,27 @@ div.follow-profile .actions menu a {
color: var(--color-text-dull); color: var(--color-text-dull);
} }
div.follow-profile .actions menu a i { .follow-profile .actions menu button {
background: none !important;
border: none;
cursor: pointer;
text-align: left;
display: block;
font-size: 15px;
padding: 4px 10px;
color: var(--color-text-dull);
}
.follow-profile .actions menu button i {
margin-right: 4px;
width: 16px;
}
.follow-profile .actions button:hover {
color: var(--color-text-main);
}
.follow-profile .actions menu a i {
margin-right: 4px; margin-right: 4px;
width: 16px; width: 16px;
} }

View file

@ -1,6 +1,4 @@
<div class="inline follow-profile {% if reverse_follow %}has-reverse{% endif %}"> <div class="inline follow-profile {% if reverse_follow %}has-reverse{% endif %}">
<div class="actions" role="menubar"> <div class="actions" role="menubar">
{% if request.identity == identity %} {% if request.identity == identity %}
<a href="{% url "settings_profile" %}" class="button" title="Edit Profile"> <a href="{% url "settings_profile" %}" class="button" title="Edit Profile">
@ -29,6 +27,18 @@
<i class="fa-solid fa-bars"></i> <i class="fa-solid fa-bars"></i>
</a> </a>
<menu> <menu>
{% if follow %}
<form action="{{ identity.urls.action }}" method="POST" class="inline">
{% csrf_token %}
{% if follow.boosts %}
<input type="hidden" name="action" value="hide_boosts">
<button role="menuitem"><i class="fa-solid fa-retweet"></i> Hide boosts</button>
{% else %}
<input type="hidden" name="action" value="show_boosts">
<button role="menuitem"><i class="fa-solid fa-retweet"></i> Show boosts</button>
{% endif %}
</form>
{% endif %}
{% if request.user.admin %} {% if request.user.admin %}
<a href="{{ identity.urls.admin_edit }}" role="menuitem"> <a href="{{ identity.urls.admin_edit }}" role="menuitem">
<i class="fa-solid fa-user-gear"></i> View in Admin <i class="fa-solid fa-user-gear"></i> View in Admin

View file

@ -0,0 +1,18 @@
# Generated by Django 4.1.4 on 2022-12-29 19:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("users", "0007_remove_invite_email_invite_expires_invite_uses"),
]
operations = [
migrations.AddField(
model_name="follow",
name="boosts",
field=models.BooleanField(default=True),
),
]

View file

@ -112,6 +112,10 @@ class Follow(StatorModel):
related_name="inbound_follows", related_name="inbound_follows",
) )
boosts = models.BooleanField(
default=True, help_text="Also follow boosts from this user"
)
uri = models.CharField(blank=True, null=True, max_length=500) uri = models.CharField(blank=True, null=True, max_length=500)
note = models.TextField(blank=True, null=True) note = models.TextField(blank=True, null=True)
@ -139,7 +143,7 @@ class Follow(StatorModel):
return None return None
@classmethod @classmethod
def create_local(cls, source, target): def create_local(cls, source, target, boosts=True):
""" """
Creates a Follow from a local Identity to the target Creates a Follow from a local Identity to the target
(which can be local or remote). (which can be local or remote).
@ -150,8 +154,13 @@ class Follow(StatorModel):
raise ValueError("You cannot initiate follows from a remote Identity") raise ValueError("You cannot initiate follows from a remote Identity")
try: try:
follow = Follow.objects.get(source=source, target=target) follow = Follow.objects.get(source=source, target=target)
if follow.boosts != boosts:
follow.boosts = boosts
follow.save()
except Follow.DoesNotExist: except Follow.DoesNotExist:
follow = Follow.objects.create(source=source, target=target, uri="") follow = Follow.objects.create(
source=source, target=target, boosts=boosts, uri=""
)
follow.uri = source.actor_uri + f"follow/{follow.pk}/" follow.uri = source.actor_uri + f"follow/{follow.pk}/"
# TODO: Local follow approvals # TODO: Local follow approvals
if target.local: if target.local:

View file

@ -31,16 +31,20 @@ class IdentityService:
.select_related("domain") .select_related("domain")
) )
def follow_from(self, from_identity: Identity) -> Follow: def follow_from(self, from_identity: Identity, boosts=True) -> Follow:
""" """
Follows a user (or does nothing if already followed). Follows a user (or does nothing if already followed).
Returns the follow. Returns the follow.
""" """
existing_follow = Follow.maybe_get(from_identity, self.identity) existing_follow = Follow.maybe_get(from_identity, self.identity)
if not existing_follow: if not existing_follow:
return Follow.create_local(from_identity, self.identity) return Follow.create_local(from_identity, self.identity, boosts=boosts)
elif existing_follow.state not in FollowStates.group_active(): elif existing_follow.state not in FollowStates.group_active():
existing_follow.transition_perform(FollowStates.unrequested) existing_follow.transition_perform(FollowStates.unrequested)
if existing_follow.boosts != boosts:
existing_follow.boosts = boosts
existing_follow.save()
return cast(Follow, existing_follow) return cast(Follow, existing_follow)
def unfollow_from(self, from_identity: Identity): def unfollow_from(self, from_identity: Identity):
@ -56,17 +60,20 @@ class IdentityService:
Returns a Relationship object for the from_identity's relationship Returns a Relationship object for the from_identity's relationship
with this identity. with this identity.
""" """
return {
"id": self.identity.pk, follow = self.identity.inbound_follows.filter(
"following": self.identity.inbound_follows.filter(
source=from_identity, source=from_identity,
state__in=FollowStates.group_active(), state__in=FollowStates.group_active(),
).exists(), ).first()
return {
"id": self.identity.pk,
"following": follow is not None,
"followed_by": self.identity.outbound_follows.filter( "followed_by": self.identity.outbound_follows.filter(
target=from_identity, target=from_identity,
state__in=FollowStates.group_active(), state__in=FollowStates.group_active(),
).exists(), ).exists(),
"showing_reblogs": True, "showing_reblogs": follow.boosts,
"notifying": False, "notifying": False,
"blocking": False, "blocking": False,
"blocked_by": False, "blocked_by": False,
@ -75,7 +82,7 @@ class IdentityService:
"requested": False, "requested": False,
"domain_blocking": False, "domain_blocking": False,
"endorsed": False, "endorsed": False,
"note": "", "note": follow.note or "",
} }
def set_summary(self, summary: str): def set_summary(self, summary: str):

View file

@ -179,6 +179,10 @@ class ActionIdentity(View):
IdentityService(identity).follow_from(self.request.identity) IdentityService(identity).follow_from(self.request.identity)
elif action == "unfollow": elif action == "unfollow":
IdentityService(identity).unfollow_from(self.request.identity) IdentityService(identity).unfollow_from(self.request.identity)
elif action == "hide_boosts":
IdentityService(identity).follow_from(self.request.identity, boosts=False)
elif action == "show_boosts":
IdentityService(identity).follow_from(self.request.identity, boosts=True)
else: else:
raise ValueError(f"Cannot handle identity action {action}") raise ValueError(f"Cannot handle identity action {action}")
return redirect(identity.urls.view) return redirect(identity.urls.view)