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()
# Boost: send a copy to all people who follow this user
if interaction.type == interaction.Types.boost:
async for follow in interaction.identity.inbound_follows.select_related(
"source", "target"
):
async for follow in interaction.identity.inbound_follows.filter(
boosts=True
).select_related("source", "target"):
if follow.source.local or follow.target.local:
await FanOut.objects.acreate(
type=FanOut.Types.interaction,
@ -294,7 +294,7 @@ class PostInteraction(StatorModel):
# Boosts (announces) go to everyone who follows locally
if interaction.type == cls.Types.boost:
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)
# 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)
@identity_required
def account_follow(request, id: str):
def account_follow(request, id: str, reblogs: bool = True):
identity = get_object_or_404(
Identity.objects.exclude(restriction=Identity.Restriction.blocked), pk=id
)
service = IdentityService(identity)
service.follow_from(request.identity)
service.follow_from(request.identity, boosts=reblogs)
return service.mastodon_json_relationship(request.identity)

View file

@ -595,6 +595,8 @@ fieldset legend {
.right-column form,
form.inline {
padding: 0;
margin: 0;
display:inline;
}
div.follow-profile {
@ -604,11 +606,11 @@ div.follow-profile {
text-align: center;
}
div.follow-profile.has-reverse {
.follow-profile.has-reverse {
margin-top: 0;
}
div.follow-profile .reverse-follow {
.follow-profile .reverse-follow {
display: block;
margin: 0 0 5px 0;
}
@ -644,6 +646,7 @@ div.follow-profile .actions menu {
top: 43px;
}
div.follow-profile .actions menu.enabled {
display: block;
min-width: 160px;
@ -658,7 +661,27 @@ div.follow-profile .actions menu a {
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;
width: 16px;
}

View file

@ -1,6 +1,4 @@
<div class="inline follow-profile {% if reverse_follow %}has-reverse{% endif %}">
<div class="actions" role="menubar">
{% if request.identity == identity %}
<a href="{% url "settings_profile" %}" class="button" title="Edit Profile">
@ -29,6 +27,18 @@
<i class="fa-solid fa-bars"></i>
</a>
<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 %}
<a href="{{ identity.urls.admin_edit }}" role="menuitem">
<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",
)
boosts = models.BooleanField(
default=True, help_text="Also follow boosts from this user"
)
uri = models.CharField(blank=True, null=True, max_length=500)
note = models.TextField(blank=True, null=True)
@ -139,7 +143,7 @@ class Follow(StatorModel):
return None
@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
(which can be local or remote).
@ -150,8 +154,13 @@ class Follow(StatorModel):
raise ValueError("You cannot initiate follows from a remote Identity")
try:
follow = Follow.objects.get(source=source, target=target)
if follow.boosts != boosts:
follow.boosts = boosts
follow.save()
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}/"
# TODO: Local follow approvals
if target.local:

View file

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

View file

@ -179,6 +179,10 @@ class ActionIdentity(View):
IdentityService(identity).follow_from(self.request.identity)
elif action == "unfollow":
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:
raise ValueError(f"Cannot handle identity action {action}")
return redirect(identity.urls.view)