summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAndrew Godwin2022-11-24 15:17:32 -0700
committerAndrew Godwin2022-11-24 15:17:32 -0700
commitec634f2ad382f659eedae884154d0db9a2a006b2 (patch)
tree2a33846e24ab6669e27d3e54b81127a720157479
parent4c00e11d63c082b57c1aec8f7c3e58820b56674a (diff)
downloadtakahe-ec634f2ad382f659eedae884154d0db9a2a006b2.tar.gz
takahe-ec634f2ad382f659eedae884154d0db9a2a006b2.tar.bz2
takahe-ec634f2ad382f659eedae884154d0db9a2a006b2.zip
Initial reply-to feature
-rw-r--r--activities/models/post.py24
-rw-r--r--activities/views/posts.py32
-rw-r--r--docs/features.rst2
-rw-r--r--static/css/style.css4
-rw-r--r--templates/activities/_post.html1
-rw-r--r--templates/activities/_reply.html4
-rw-r--r--templates/activities/compose.html4
7 files changed, 65 insertions, 6 deletions
diff --git a/activities/models/post.py b/activities/models/post.py
index 876d422..c86ec6a 100644
--- a/activities/models/post.py
+++ b/activities/models/post.py
@@ -3,6 +3,7 @@ from typing import Dict, Optional
import httpx
import urlman
+from asgiref.sync import sync_to_async
from django.db import models, transaction
from django.template.defaultfilters import linebreaks_filter
from django.utils import timezone
@@ -42,6 +43,10 @@ class PostStates(StateGraph):
if post.visibility != Post.Visibilities.mentioned:
async for follower in post.author.inbound_follows.select_related("source"):
targets.add(follower.source)
+ # If it's a reply, always include the original author if we know them
+ reply_post = await post.ain_reply_to_post()
+ if reply_post:
+ targets.add(reply_post.author)
# Fan out to each one
for follow in targets:
await FanOut.objects.acreate(
@@ -141,6 +146,7 @@ class Post(StatorModel):
action_unlike = "{view}unlike/"
action_boost = "{view}boost/"
action_unboost = "{view}unboost/"
+ action_reply = "/compose/?reply_to={self.id}"
def get_scheme(self, url):
return "https"
@@ -164,6 +170,18 @@ class Post(StatorModel):
else:
return self.object_uri
+ def in_reply_to_post(self) -> Optional["Post"]:
+ """
+ Returns the actual Post object we're replying to, if we can find it
+ """
+ return (
+ Post.objects.filter(object_uri=self.in_reply_to)
+ .select_related("author")
+ .first()
+ )
+
+ ain_reply_to_post = sync_to_async(in_reply_to_post)
+
### Content cleanup and extraction ###
mention_regex = re.compile(
@@ -229,6 +247,7 @@ class Post(StatorModel):
content: str,
summary: Optional[str] = None,
visibility: int = Visibilities.public,
+ reply_to: Optional["Post"] = None,
) -> "Post":
with transaction.atomic():
# Find mentions in this post
@@ -247,6 +266,8 @@ class Post(StatorModel):
)
if identity is not None:
mentions.add(identity)
+ if reply_to:
+ mentions.add(reply_to.author)
# Strip all HTML and apply linebreaks filter
content = linebreaks_filter(strip_html(content))
# Make the Post object
@@ -257,6 +278,7 @@ class Post(StatorModel):
sensitive=bool(summary),
local=True,
visibility=visibility,
+ in_reply_to=reply_to.object_uri if reply_to else None,
)
post.object_uri = post.urls.object_uri
post.url = post.absolute_object_uri()
@@ -284,6 +306,8 @@ class Post(StatorModel):
}
if self.summary:
value["summary"] = self.summary
+ if self.in_reply_to:
+ value["inReplyTo"] = self.in_reply_to
# Mentions
for mention in self.mentions.all():
value["tag"].append(
diff --git a/activities/views/posts.py b/activities/views/posts.py
index fce17e3..a53d401 100644
--- a/activities/views/posts.py
+++ b/activities/views/posts.py
@@ -143,6 +143,7 @@ class Compose(FormView):
),
help_text="Optional - Post will be hidden behind this text until clicked",
)
+ reply_to = forms.CharField(widget=forms.HiddenInput(), required=False)
def clean_text(self):
text = self.cleaned_data.get("text")
@@ -155,10 +156,13 @@ class Compose(FormView):
)
return text
- def get_form_class(self):
- form = super().get_form_class()
- form.declared_fields["text"]
- return form
+ def get_initial(self):
+ initial = super().get_initial()
+ if self.reply_to:
+ initial["reply_to"] = self.reply_to.pk
+ initial["visibility"] = Post.Visibilities.unlisted
+ initial["text"] = f"@{self.reply_to.author.handle} "
+ return initial
def form_valid(self, form):
post = Post.create_local(
@@ -166,7 +170,27 @@ class Compose(FormView):
content=form.cleaned_data["text"],
summary=form.cleaned_data.get("content_warning"),
visibility=form.cleaned_data["visibility"],
+ reply_to=self.reply_to,
)
# Add their own timeline event for immediate visibility
TimelineEvent.add_post(self.request.identity, post)
return redirect("/")
+
+ def dispatch(self, request, *args, **kwargs):
+ # Grab the reply-to post info now
+ self.reply_to = None
+ reply_to_id = self.request.POST.get("reply_to") or self.request.GET.get(
+ "reply_to"
+ )
+ if reply_to_id:
+ try:
+ self.reply_to = Post.objects.get(pk=reply_to_id)
+ except Post.DoesNotExist:
+ pass
+ # Keep going with normal rendering
+ return super().dispatch(request, *args, **kwargs)
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context["reply_to"] = self.reply_to
+ return context
diff --git a/docs/features.rst b/docs/features.rst
index 0d2e3ac..0cc178a 100644
--- a/docs/features.rst
+++ b/docs/features.rst
@@ -37,12 +37,14 @@ Features planned for releases up to 1.0:
* Server defederation (blocking)
* IP and email domain banning
* Mastodon-compatible client API for use with apps
+* RSS feeds for users' public posts
Features that may make it into 1.0, or might be further out:
* Creating polls on posts, and handling received polls
* Filter system for Home timeline
* Hashtag trending system with moderation
+* Mastodon-compatible account migration target/source
* Relay support
Features on the long-term roadmap:
diff --git a/static/css/style.css b/static/css/style.css
index 5c966a7..642057f 100644
--- a/static/css/style.css
+++ b/static/css/style.css
@@ -714,11 +714,11 @@ h1.identity small {
display: block;
float: right;
color: var(--color-text-duller);
- width: 60px;
+ width: 65px;
text-align: center;
background-color: var(--color-bg-main);
border-radius: 3px;
- padding: 3px 5px;
+ padding: 3px 3px;
}
.post time i {
diff --git a/templates/activities/_post.html b/templates/activities/_post.html
index 11e4494..dbdc99a 100644
--- a/templates/activities/_post.html
+++ b/templates/activities/_post.html
@@ -27,6 +27,7 @@
{% if request.identity %}
<div class="actions">
+ {% include "activities/_reply.html" %}
{% include "activities/_like.html" %}
{% include "activities/_boost.html" %}
</div>
diff --git a/templates/activities/_reply.html b/templates/activities/_reply.html
new file mode 100644
index 0000000..a6cc81a
--- /dev/null
+++ b/templates/activities/_reply.html
@@ -0,0 +1,4 @@
+
+<a title="Reply" href="{{ post.urls.action_reply }}">
+ <i class="fa-solid fa-reply"></i>
+</a>
diff --git a/templates/activities/compose.html b/templates/activities/compose.html
index 55b4eb3..9d02988 100644
--- a/templates/activities/compose.html
+++ b/templates/activities/compose.html
@@ -7,6 +7,10 @@
{% csrf_token %}
<fieldset>
<legend>Content</legend>
+ {% if reply_to %}
+ <p>Replying to <a href="{{ reply_to.urls.view }}">{{ reply_to }}</a></p>
+ {% endif %}
+ {{ form.reply_to }}
{% include "forms/_field.html" with field=form.text %}
{% include "forms/_field.html" with field=form.content_warning %}
{% include "forms/_field.html" with field=form.visibility %}