From ec634f2ad382f659eedae884154d0db9a2a006b2 Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Thu, 24 Nov 2022 15:17:32 -0700 Subject: Initial reply-to feature --- activities/models/post.py | 24 ++++++++++++++++++++++++ activities/views/posts.py | 32 ++++++++++++++++++++++++++++---- 2 files changed, 52 insertions(+), 4 deletions(-) (limited to 'activities') 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 -- cgit v1.2.3