Initial reply-to feature
This commit is contained in:
		
							parent
							
								
									4c00e11d63
								
							
						
					
					
						commit
						ec634f2ad3
					
				@ -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(
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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:
 | 
			
		||||
 | 
			
		||||
@ -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 {
 | 
			
		||||
 | 
			
		||||
@ -27,6 +27,7 @@
 | 
			
		||||
 | 
			
		||||
    {% if request.identity %}
 | 
			
		||||
    <div class="actions">
 | 
			
		||||
        {% include "activities/_reply.html" %}
 | 
			
		||||
        {% include "activities/_like.html" %}
 | 
			
		||||
        {% include "activities/_boost.html" %}
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4
									
								
								templates/activities/_reply.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								templates/activities/_reply.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
			
		||||
 | 
			
		||||
<a title="Reply" href="{{ post.urls.action_reply }}">
 | 
			
		||||
    <i class="fa-solid fa-reply"></i>
 | 
			
		||||
</a>
 | 
			
		||||
@ -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 %}
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user