diff --git a/activities/models/post_interaction.py b/activities/models/post_interaction.py
index cce88ab..fa85245 100644
--- a/activities/models/post_interaction.py
+++ b/activities/models/post_interaction.py
@@ -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
diff --git a/api/views/accounts.py b/api/views/accounts.py
index 3c6fab3..61d8590 100644
--- a/api/views/accounts.py
+++ b/api/views/accounts.py
@@ -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)
diff --git a/static/css/style.css b/static/css/style.css
index 7b03cb9..05e66bb 100644
--- a/static/css/style.css
+++ b/static/css/style.css
@@ -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;
}
diff --git a/templates/identity/_view_menu.html b/templates/identity/_view_menu.html
index 960b376..ae05f84 100644
--- a/templates/identity/_view_menu.html
+++ b/templates/identity/_view_menu.html
@@ -1,6 +1,4 @@
-
-
{% if request.identity == identity %}
@@ -29,6 +27,18 @@
+ {% endif %}
{% if request.user.admin %}
View in Admin
diff --git a/users/migrations/0008_follow_boosts.py b/users/migrations/0008_follow_boosts.py
new file mode 100644
index 0000000..c11814d
--- /dev/null
+++ b/users/migrations/0008_follow_boosts.py
@@ -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),
+ ),
+ ]
diff --git a/users/models/follow.py b/users/models/follow.py
index 201699d..dc116b6 100644
--- a/users/models/follow.py
+++ b/users/models/follow.py
@@ -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:
diff --git a/users/services/identity.py b/users/services/identity.py
index 7a621d1..51e8f36 100644
--- a/users/services/identity.py
+++ b/users/services/identity.py
@@ -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.
"""
+
+ follow = self.identity.inbound_follows.filter(
+ source=from_identity,
+ state__in=FollowStates.group_active(),
+ ).first()
+
return {
"id": self.identity.pk,
- "following": self.identity.inbound_follows.filter(
- source=from_identity,
- state__in=FollowStates.group_active(),
- ).exists(),
+ "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):
diff --git a/users/views/identity.py b/users/views/identity.py
index ac2dd6b..a71b8b4 100644
--- a/users/views/identity.py
+++ b/users/views/identity.py
@@ -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)