mirror of
https://github.com/jointakahe/takahe.git
synced 2024-11-26 01:01:00 +00:00
Option to show/hide boosts for a followed user (#317)
This commit is contained in:
parent
0b208d3bf7
commit
eea83214cb
8 changed files with 92 additions and 21 deletions
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
18
users/migrations/0008_follow_boosts.py
Normal file
18
users/migrations/0008_follow_boosts.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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:
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue