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] Set 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] Receive boosts
- [x] Create likes
@ -77,6 +78,7 @@ the less sure I am about it.
- [ ] Attach images to posts
- [ ] Edit posts
- [ ] Delete posts
- [ ] Fetch remote post images locally and thumbnail
- [ ] Show follow pending states
- [ ] Manual approval of followers
- [ ] Reply threading on post creation

View file

@ -1,6 +1,17 @@
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)
@ -8,6 +19,8 @@ class PostAdmin(admin.ModelAdmin):
list_display = ["id", "state", "author", "created"]
raw_id_fields = ["to", "mentions", "author"]
actions = ["force_fetch"]
search_fields = ["content"]
inlines = [PostAttachmentInline]
readonly_fields = ["created", "updated", "object_json"]
@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 .post import Post, PostStates # noqa
from .post_attachment import PostAttachment, PostAttachmentStates # noqa
from .post_interaction import PostInteraction, PostInteractionStates # noqa
from .timeline_event import TimelineEvent # noqa

View file

@ -146,6 +146,9 @@ class Post(StatorModel):
def __str__(self):
return f"{self.author} #{self.id}"
def get_absolute_url(self):
return self.urls.view
@property
def safe_content(self):
return sanitize_post(self.content)
@ -244,11 +247,12 @@ class Post(StatorModel):
raise KeyError(f"No post with ID {data['id']}", data)
if update or created:
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.url = data.get("url", None)
post.published = parse_ld_date(data.get("published", None))
post.edited = parse_ld_date(data.get("updated", None))
post.url = data.get("url")
post.published = parse_ld_date(data.get("published"))
post.edited = parse_ld_date(data.get("updated"))
post.in_reply_to = data.get("inReplyTo")
# Mentions and hashtags
post.hashtags = []
for tag in get_list(data, "tag"):
@ -270,6 +274,26 @@ class Post(StatorModel):
for target in targets:
if target.lower() == "as: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()
return post
@ -308,9 +332,13 @@ class Post(StatorModel):
raise ValueError("Create actor does not match its Post object", data)
# Create it
post = cls.by_ap(data["object"], create=True, update=True)
# Make timeline events for followers
for follow in Follow.objects.filter(target=post.author, source__local=True):
TimelineEvent.add_post(follow.source, post)
# Make timeline events for followers if it's not a reply
# TODO: _do_ show replies to people we follow somehow
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
for mention in post.mentions.all():
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):
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:
# Undo any likes on the post
for interaction in PostInteraction.objects.filter(

View file

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

View file

@ -570,6 +570,22 @@ h1.identity small {
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 {
padding-left: 64px;
}

View file

@ -41,6 +41,16 @@
{{ post.safe_content }}
</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 %}
<div class="actions">
{% include "activities/_like.html" %}