mirror of
https://github.com/jointakahe/takahe.git
synced 2024-11-22 07:10:59 +00:00
Show post images
This commit is contained in:
parent
b13c239213
commit
716d8a766a
10 changed files with 209 additions and 10 deletions
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
69
activities/migrations/0008_postattachment.py
Normal file
69
activities/migrations/0008_postattachment.py
Normal 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,
|
||||
},
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
55
activities/models/post_attachment.py
Normal file
55
activities/models/post_attachment.py
Normal 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",
|
||||
]
|
|
@ -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(
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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" %}
|
||||
|
|
Loading…
Reference in a new issue