Show post images

This commit is contained in:
Andrew Godwin 2022-11-16 23:00:10 -07:00
parent b13c239213
commit 716d8a766a
10 changed files with 209 additions and 10 deletions

View file

@ -43,7 +43,8 @@ the less sure I am about it.
- [x] Receive post edits - [x] Receive post edits
- [x] Set content warnings on posts - [x] Set content warnings on posts
- [x] Show content warnings on posts - [x] Show content warnings on posts
- [ ] Receive images on posts - [x] Receive images on posts
- [x] Receive reply info
- [x] Create boosts - [x] Create boosts
- [x] Receive boosts - [x] Receive boosts
- [x] Create likes - [x] Create likes
@ -77,6 +78,7 @@ the less sure I am about it.
- [ ] Attach images to posts - [ ] Attach images to posts
- [ ] Edit posts - [ ] Edit posts
- [ ] Delete posts - [ ] Delete posts
- [ ] Fetch remote post images locally and thumbnail
- [ ] Show follow pending states - [ ] Show follow pending states
- [ ] Manual approval of followers - [ ] Manual approval of followers
- [ ] Reply threading on post creation - [ ] Reply threading on post creation

View file

@ -1,6 +1,17 @@
from django.contrib import admin from django.contrib import admin
from activities.models import FanOut, Post, PostInteraction, TimelineEvent from activities.models import (
FanOut,
Post,
PostAttachment,
PostInteraction,
TimelineEvent,
)
class PostAttachmentInline(admin.StackedInline):
model = PostAttachment
extra = 0
@admin.register(Post) @admin.register(Post)
@ -8,6 +19,8 @@ class PostAdmin(admin.ModelAdmin):
list_display = ["id", "state", "author", "created"] list_display = ["id", "state", "author", "created"]
raw_id_fields = ["to", "mentions", "author"] raw_id_fields = ["to", "mentions", "author"]
actions = ["force_fetch"] actions = ["force_fetch"]
search_fields = ["content"]
inlines = [PostAttachmentInline]
readonly_fields = ["created", "updated", "object_json"] readonly_fields = ["created", "updated", "object_json"]
@admin.action(description="Force Fetch") @admin.action(description="Force Fetch")

View file

@ -0,0 +1,69 @@
# Generated by Django 4.1.3 on 2022-11-17 05:42
import django.db.models.deletion
from django.db import migrations, models
import activities.models.post_attachment
import stator.models
class Migration(migrations.Migration):
dependencies = [
("activities", "0007_post_edited"),
]
operations = [
migrations.CreateModel(
name="PostAttachment",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("state_ready", models.BooleanField(default=True)),
("state_changed", models.DateTimeField(auto_now_add=True)),
("state_attempted", models.DateTimeField(blank=True, null=True)),
("state_locked_until", models.DateTimeField(blank=True, null=True)),
(
"state",
stator.models.StateField(
choices=[("new", "new"), ("fetched", "fetched")],
default="new",
graph=activities.models.post_attachment.PostAttachmentStates,
max_length=100,
),
),
("mimetype", models.CharField(max_length=200)),
(
"file",
models.FileField(
blank=True, null=True, upload_to="attachments/%Y/%m/%d/"
),
),
("remote_url", models.CharField(blank=True, max_length=500, null=True)),
("name", models.TextField(blank=True, null=True)),
("width", models.IntegerField(blank=True, null=True)),
("height", models.IntegerField(blank=True, null=True)),
("focal_x", models.IntegerField(blank=True, null=True)),
("focal_y", models.IntegerField(blank=True, null=True)),
("blurhash", models.TextField(blank=True, null=True)),
(
"post",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="attachments",
to="activities.post",
),
),
],
options={
"abstract": False,
},
),
]

View file

@ -1,4 +1,5 @@
from .fan_out import FanOut, FanOutStates # noqa from .fan_out import FanOut, FanOutStates # noqa
from .post import Post, PostStates # noqa from .post import Post, PostStates # noqa
from .post_attachment import PostAttachment, PostAttachmentStates # noqa
from .post_interaction import PostInteraction, PostInteractionStates # noqa from .post_interaction import PostInteraction, PostInteractionStates # noqa
from .timeline_event import TimelineEvent # noqa from .timeline_event import TimelineEvent # noqa

View file

@ -146,6 +146,9 @@ class Post(StatorModel):
def __str__(self): def __str__(self):
return f"{self.author} #{self.id}" return f"{self.author} #{self.id}"
def get_absolute_url(self):
return self.urls.view
@property @property
def safe_content(self): def safe_content(self):
return sanitize_post(self.content) return sanitize_post(self.content)
@ -244,11 +247,12 @@ class Post(StatorModel):
raise KeyError(f"No post with ID {data['id']}", data) raise KeyError(f"No post with ID {data['id']}", data)
if update or created: if update or created:
post.content = sanitize_post(data["content"]) post.content = sanitize_post(data["content"])
post.summary = data.get("summary", None) post.summary = data.get("summary")
post.sensitive = data.get("as:sensitive", False) post.sensitive = data.get("as:sensitive", False)
post.url = data.get("url", None) post.url = data.get("url")
post.published = parse_ld_date(data.get("published", None)) post.published = parse_ld_date(data.get("published"))
post.edited = parse_ld_date(data.get("updated", None)) post.edited = parse_ld_date(data.get("updated"))
post.in_reply_to = data.get("inReplyTo")
# Mentions and hashtags # Mentions and hashtags
post.hashtags = [] post.hashtags = []
for tag in get_list(data, "tag"): for tag in get_list(data, "tag"):
@ -270,6 +274,26 @@ class Post(StatorModel):
for target in targets: for target in targets:
if target.lower() == "as:public": if target.lower() == "as:public":
post.visibility = Post.Visibilities.public post.visibility = Post.Visibilities.public
# Attachments
# These have no IDs, so we have to wipe them each time
post.attachments.all().delete()
for attachment in get_list(data, "attachment"):
if "http://joinmastodon.org/ns#focalPoint" in attachment:
focal_x, focal_y = attachment[
"http://joinmastodon.org/ns#focalPoint"
]["@list"]
else:
focal_x, focal_y = None, None
post.attachments.create(
remote_url=attachment["url"],
mimetype=attachment["mediaType"],
name=attachment.get("name"),
width=attachment.get("width"),
height=attachment.get("height"),
blurhash=attachment.get("http://joinmastodon.org/ns#blurhash"),
focal_x=focal_x,
focal_y=focal_y,
)
post.save() post.save()
return post return post
@ -308,9 +332,13 @@ class Post(StatorModel):
raise ValueError("Create actor does not match its Post object", data) raise ValueError("Create actor does not match its Post object", data)
# Create it # Create it
post = cls.by_ap(data["object"], create=True, update=True) post = cls.by_ap(data["object"], create=True, update=True)
# Make timeline events for followers # Make timeline events for followers if it's not a reply
for follow in Follow.objects.filter(target=post.author, source__local=True): # TODO: _do_ show replies to people we follow somehow
TimelineEvent.add_post(follow.source, post) if not post.in_reply_to:
for follow in Follow.objects.filter(
target=post.author, source__local=True
):
TimelineEvent.add_post(follow.source, post)
# Make timeline events for mentions if they're local # Make timeline events for mentions if they're local
for mention in post.mentions.all(): for mention in post.mentions.all():
if mention.local: if mention.local:

View file

@ -0,0 +1,55 @@
from django.db import models
from stator.models import State, StateField, StateGraph, StatorModel
class PostAttachmentStates(StateGraph):
new = State(try_interval=30000)
fetched = State()
new.transitions_to(fetched)
@classmethod
async def handle_new(cls, instance):
# TODO: Fetch images to our own media storage
pass
class PostAttachment(StatorModel):
"""
An attachment to a Post. Could be an image, a video, etc.
"""
post = models.ForeignKey(
"activities.post",
on_delete=models.CASCADE,
related_name="attachments",
)
state = StateField(graph=PostAttachmentStates)
mimetype = models.CharField(max_length=200)
# File may not be populated if it's remote and not cached on our side yet
file = models.FileField(upload_to="attachments/%Y/%m/%d/", null=True, blank=True)
remote_url = models.CharField(max_length=500, null=True, blank=True)
# This is the description for images, at least
name = models.TextField(null=True, blank=True)
width = models.IntegerField(null=True, blank=True)
height = models.IntegerField(null=True, blank=True)
focal_x = models.IntegerField(null=True, blank=True)
focal_y = models.IntegerField(null=True, blank=True)
blurhash = models.TextField(null=True, blank=True)
def is_image(self):
return self.mimetype in [
"image/apng",
"image/avif",
"image/gif",
"image/jpeg",
"image/png",
"image/webp",
]

View file

@ -36,7 +36,9 @@ class Like(View):
def post(self, request, handle, post_id): def post(self, request, handle, post_id):
identity = by_handle_or_404(self.request, handle, local=False) identity = by_handle_or_404(self.request, handle, local=False)
post = get_object_or_404(identity.posts, pk=post_id) post = get_object_or_404(
identity.posts.prefetch_related("attachments"), pk=post_id
)
if self.undo: if self.undo:
# Undo any likes on the post # Undo any likes on the post
for interaction in PostInteraction.objects.filter( for interaction in PostInteraction.objects.filter(

View file

@ -39,6 +39,7 @@ class Home(FormView):
type__in=[TimelineEvent.Types.post, TimelineEvent.Types.boost], type__in=[TimelineEvent.Types.post, TimelineEvent.Types.boost],
) )
.select_related("subject_post", "subject_post__author") .select_related("subject_post", "subject_post__author")
.prefetch_related("subject_post__attachments")
.order_by("-created")[:100] .order_by("-created")[:100]
) )
context["interactions"] = PostInteraction.get_event_interactions( context["interactions"] = PostInteraction.get_event_interactions(
@ -66,6 +67,7 @@ class Local(TemplateView):
context["posts"] = ( context["posts"] = (
Post.objects.filter(visibility=Post.Visibilities.public, author__local=True) Post.objects.filter(visibility=Post.Visibilities.public, author__local=True)
.select_related("author") .select_related("author")
.prefetch_related("attachments")
.order_by("-created")[:100] .order_by("-created")[:100]
) )
context["current_page"] = "local" context["current_page"] = "local"
@ -82,6 +84,7 @@ class Federated(TemplateView):
context["posts"] = ( context["posts"] = (
Post.objects.filter(visibility=Post.Visibilities.public) Post.objects.filter(visibility=Post.Visibilities.public)
.select_related("author") .select_related("author")
.prefetch_related("attachments")
.order_by("-created")[:100] .order_by("-created")[:100]
) )
context["current_page"] = "federated" context["current_page"] = "federated"

View file

@ -570,6 +570,22 @@ h1.identity small {
margin: 12px 0 4px 0; margin: 12px 0 4px 0;
} }
.post .attachments {
margin: 10px 0 10px 64px;
}
.post .attachments a.image {
display: inline-block;
border: 3px solid var(--color-bg-menu);
border-radius: 3px;
}
.post .attachments a.image img {
display: inline-block;
max-width: 200px;
max-height: 200px;
}
.post .actions { .post .actions {
padding-left: 64px; padding-left: 64px;
} }

View file

@ -41,6 +41,16 @@
{{ post.safe_content }} {{ post.safe_content }}
</div> </div>
{% if post.attachments.exists %}
<div class="attachments">
{% for attachment in post.attachments.all %}
{% if attachment.is_image %}
<a href="{{ attachment.remote_url }}" class="image"><img src="{{ attachment.remote_url }}" title="{{ attachment.name }}"></a>
{% endif %}
{% endfor %}
</div>
{% endif %}
{% if request.identity %} {% if request.identity %}
<div class="actions"> <div class="actions">
{% include "activities/_like.html" %} {% include "activities/_like.html" %}