mirror of
https://github.com/jointakahe/takahe.git
synced 2024-11-26 01:01:00 +00:00
Tag and visibility handling
This commit is contained in:
parent
906ed2f27c
commit
495e955378
16 changed files with 289 additions and 48 deletions
|
@ -36,12 +36,13 @@ the less sure I am about it.
|
||||||
### Alpha
|
### Alpha
|
||||||
|
|
||||||
- [x] Create posts
|
- [x] Create posts
|
||||||
- [ ] Set post visibility
|
- [x] Set post visibility
|
||||||
- [x] Receive posts
|
- [x] Receive posts
|
||||||
- [ ] Handle received post visibility
|
- [x] Handle received post visibility (unlisted vs public only)
|
||||||
- [x] Receive post deletions
|
- [x] Receive post deletions
|
||||||
|
- [ ] Receive post edits
|
||||||
- [x] Set content warnings on posts
|
- [x] Set content warnings on posts
|
||||||
- [ ] Show content warnings on posts
|
- [x] Show content warnings on posts
|
||||||
- [ ] Attach images to posts
|
- [ ] Attach images to posts
|
||||||
- [ ] Receive images on posts
|
- [ ] Receive images on posts
|
||||||
- [x] Create boosts
|
- [x] Create boosts
|
||||||
|
@ -52,6 +53,7 @@ the less sure I am about it.
|
||||||
- [ ] Undo follows
|
- [ ] Undo follows
|
||||||
- [x] Receive and accept follows
|
- [x] Receive and accept follows
|
||||||
- [x] Receive follow undos
|
- [x] Receive follow undos
|
||||||
|
- [ ] Do mentions properly
|
||||||
- [x] Home timeline (posts and boosts from follows)
|
- [x] Home timeline (posts and boosts from follows)
|
||||||
- [ ] Notifications page (followed, boosted, liked)
|
- [ ] Notifications page (followed, boosted, liked)
|
||||||
- [x] Local timeline
|
- [x] Local timeline
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
# Generated by Django 4.1.3 on 2022-11-16 20:18
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
(
|
||||||
|
"activities",
|
||||||
|
"0004_rename_authored_post_published_alter_fanout_type_and_more",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="post",
|
||||||
|
name="hashtags",
|
||||||
|
field=models.JSONField(default=[]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="fanout",
|
||||||
|
name="type",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("post", "Post"),
|
||||||
|
("interaction", "Interaction"),
|
||||||
|
("undo_interaction", "Undo Interaction"),
|
||||||
|
],
|
||||||
|
max_length=100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="timelineevent",
|
||||||
|
name="type",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("post", "Post"),
|
||||||
|
("boost", "Boost"),
|
||||||
|
("mentioned", "Mentioned"),
|
||||||
|
("liked", "Liked"),
|
||||||
|
("followed", "Followed"),
|
||||||
|
("boosted", "Boosted"),
|
||||||
|
],
|
||||||
|
max_length=100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -8,7 +8,7 @@ from django.utils import timezone
|
||||||
from activities.models.fan_out import FanOut
|
from activities.models.fan_out import FanOut
|
||||||
from activities.models.timeline_event import TimelineEvent
|
from activities.models.timeline_event import TimelineEvent
|
||||||
from core.html import sanitize_post
|
from core.html import sanitize_post
|
||||||
from core.ld import canonicalise, format_ld_date, parse_ld_date
|
from core.ld import canonicalise, format_ld_date, get_list, parse_ld_date
|
||||||
from stator.models import State, StateField, StateGraph, StatorModel
|
from stator.models import State, StateField, StateGraph, StatorModel
|
||||||
from users.models.follow import Follow
|
from users.models.follow import Follow
|
||||||
from users.models.identity import Identity
|
from users.models.identity import Identity
|
||||||
|
@ -25,7 +25,32 @@ class PostStates(StateGraph):
|
||||||
"""
|
"""
|
||||||
Creates all needed fan-out objects for a new Post.
|
Creates all needed fan-out objects for a new Post.
|
||||||
"""
|
"""
|
||||||
await instance.afan_out()
|
post = await instance.afetch_full()
|
||||||
|
# Non-local posts should not be here
|
||||||
|
if not post.local:
|
||||||
|
raise ValueError("Trying to run handle_new on a non-local post!")
|
||||||
|
# Build list of targets - mentions always included
|
||||||
|
targets = set()
|
||||||
|
async for mention in post.mentions.all():
|
||||||
|
targets.add(mention)
|
||||||
|
# Then, if it's not mentions only, also deliver to followers
|
||||||
|
if post.visibility != Post.Visibilities.mentioned:
|
||||||
|
async for follower in post.author.inbound_follows.select_related("source"):
|
||||||
|
targets.add(follower.source)
|
||||||
|
# Fan out to each one
|
||||||
|
for follow in targets:
|
||||||
|
await FanOut.objects.acreate(
|
||||||
|
identity=follow,
|
||||||
|
type=FanOut.Types.post,
|
||||||
|
subject_post=post,
|
||||||
|
)
|
||||||
|
# And one for themselves if they're local
|
||||||
|
if post.author.local:
|
||||||
|
await FanOut.objects.acreate(
|
||||||
|
identity_id=post.author_id,
|
||||||
|
type=FanOut.Types.post,
|
||||||
|
subject_post=post,
|
||||||
|
)
|
||||||
return cls.fanned_out
|
return cls.fanned_out
|
||||||
|
|
||||||
|
|
||||||
|
@ -91,6 +116,9 @@ class Post(StatorModel):
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Hashtags in the post
|
||||||
|
hashtags = models.JSONField(default=[])
|
||||||
|
|
||||||
# When the post was originally created (as opposed to when we received it)
|
# When the post was originally created (as opposed to when we received it)
|
||||||
published = models.DateTimeField(default=timezone.now)
|
published = models.DateTimeField(default=timezone.now)
|
||||||
|
|
||||||
|
@ -133,7 +161,11 @@ class Post(StatorModel):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_local(
|
def create_local(
|
||||||
cls, author: Identity, content: str, summary: Optional[str] = None
|
cls,
|
||||||
|
author: Identity,
|
||||||
|
content: str,
|
||||||
|
summary: Optional[str] = None,
|
||||||
|
visibility: int = Visibilities.public,
|
||||||
) -> "Post":
|
) -> "Post":
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
post = cls.objects.create(
|
post = cls.objects.create(
|
||||||
|
@ -142,6 +174,7 @@ class Post(StatorModel):
|
||||||
summary=summary or None,
|
summary=summary or None,
|
||||||
sensitive=bool(summary),
|
sensitive=bool(summary),
|
||||||
local=True,
|
local=True,
|
||||||
|
visibility=visibility,
|
||||||
)
|
)
|
||||||
post.object_uri = post.urls.object_uri
|
post.object_uri = post.urls.object_uri
|
||||||
post.url = post.urls.view_nice
|
post.url = post.urls.view_nice
|
||||||
|
@ -150,29 +183,6 @@ class Post(StatorModel):
|
||||||
|
|
||||||
### ActivityPub (outbound) ###
|
### ActivityPub (outbound) ###
|
||||||
|
|
||||||
async def afan_out(self):
|
|
||||||
"""
|
|
||||||
Creates FanOuts for a new post
|
|
||||||
"""
|
|
||||||
# Send a copy to all people who follow this user
|
|
||||||
post = await self.afetch_full()
|
|
||||||
async for follow in post.author.inbound_follows.select_related(
|
|
||||||
"source", "target"
|
|
||||||
):
|
|
||||||
if follow.source.local or follow.target.local:
|
|
||||||
await FanOut.objects.acreate(
|
|
||||||
identity_id=follow.source_id,
|
|
||||||
type=FanOut.Types.post,
|
|
||||||
subject_post=post,
|
|
||||||
)
|
|
||||||
# And one for themselves if they're local
|
|
||||||
if post.author.local:
|
|
||||||
await FanOut.objects.acreate(
|
|
||||||
identity_id=post.author_id,
|
|
||||||
type=FanOut.Types.post,
|
|
||||||
subject_post=post,
|
|
||||||
)
|
|
||||||
|
|
||||||
def to_ap(self) -> Dict:
|
def to_ap(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Returns the AP JSON for this object
|
Returns the AP JSON for this object
|
||||||
|
@ -185,7 +195,7 @@ class Post(StatorModel):
|
||||||
"content": self.safe_content,
|
"content": self.safe_content,
|
||||||
"to": "as:Public",
|
"to": "as:Public",
|
||||||
"as:sensitive": self.sensitive,
|
"as:sensitive": self.sensitive,
|
||||||
"url": self.urls.view_nice if self.local else self.url,
|
"url": str(self.urls.view_nice if self.local else self.url),
|
||||||
}
|
}
|
||||||
if self.summary:
|
if self.summary:
|
||||||
value["summary"] = self.summary
|
value["summary"] = self.summary
|
||||||
|
@ -236,8 +246,24 @@ class Post(StatorModel):
|
||||||
post.url = data.get("url", None)
|
post.url = data.get("url", None)
|
||||||
post.published = parse_ld_date(data.get("published", None))
|
post.published = parse_ld_date(data.get("published", None))
|
||||||
# TODO: to
|
# TODO: to
|
||||||
# TODO: mentions
|
# Mentions and hashtags
|
||||||
# TODO: visibility
|
post.hashtags = []
|
||||||
|
for tag in get_list(data, "tag"):
|
||||||
|
if tag["type"].lower() == "mention":
|
||||||
|
mention_identity = Identity.by_actor_uri(tag["href"], create=True)
|
||||||
|
post.mentions.add(mention_identity)
|
||||||
|
elif tag["type"].lower() == "as:hashtag":
|
||||||
|
post.hashtags.append(tag["name"].lstrip("#"))
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown tag type {tag['type']}")
|
||||||
|
# Visibility and to
|
||||||
|
# (a post is public if it's ever to/cc as:Public, otherwise we
|
||||||
|
# regard it as unlisted for now)
|
||||||
|
targets = get_list(data, "to") + get_list(data, "cc")
|
||||||
|
post.visibility = Post.Visibilities.unlisted
|
||||||
|
for target in targets:
|
||||||
|
if target.lower() == "as:public":
|
||||||
|
post.visibility = Post.Visibilities.public
|
||||||
post.save()
|
post.save()
|
||||||
return post
|
return post
|
||||||
|
|
||||||
|
@ -275,9 +301,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 as appropriate
|
# Make timeline events for followers
|
||||||
for follow in Follow.objects.filter(target=post.author, source__local=True):
|
for follow in Follow.objects.filter(target=post.author, source__local=True):
|
||||||
TimelineEvent.add_post(follow.source, post)
|
TimelineEvent.add_post(follow.source, post)
|
||||||
|
# Make timeline events for mentions if they're local
|
||||||
|
for mention in post.mentions.all():
|
||||||
|
if mention.local:
|
||||||
|
TimelineEvent.add_mentioned(mention, post)
|
||||||
# Force it into fanned_out as it's not ours
|
# Force it into fanned_out as it's not ours
|
||||||
post.transition_perform(PostStates.fanned_out)
|
post.transition_perform(PostStates.fanned_out)
|
||||||
|
|
||||||
|
|
|
@ -81,6 +81,17 @@ class TimelineEvent(models.Model):
|
||||||
subject_post=post,
|
subject_post=post,
|
||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_mentioned(cls, identity, post):
|
||||||
|
"""
|
||||||
|
Adds a mention of identity by post
|
||||||
|
"""
|
||||||
|
return cls.objects.get_or_create(
|
||||||
|
identity=identity,
|
||||||
|
type=cls.Types.mentioned,
|
||||||
|
subject_post=post,
|
||||||
|
)[0]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_post_interaction(cls, identity, interaction):
|
def add_post_interaction(cls, identity, interaction):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
|
from django import forms
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
|
from django.template.defaultfilters import linebreaks_filter
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.generic import TemplateView, View
|
from django.views.generic import FormView, TemplateView, View
|
||||||
|
|
||||||
from activities.models import PostInteraction, PostInteractionStates
|
from activities.models import Post, PostInteraction, PostInteractionStates
|
||||||
from users.decorators import identity_required
|
from users.decorators import identity_required
|
||||||
from users.shortcuts import by_handle_or_404
|
from users.shortcuts import by_handle_or_404
|
||||||
|
|
||||||
|
|
||||||
class Post(TemplateView):
|
class Individual(TemplateView):
|
||||||
|
|
||||||
template_name = "activities/post.html"
|
template_name = "activities/post.html"
|
||||||
|
|
||||||
|
@ -100,3 +102,44 @@ class Boost(View):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return redirect(post.urls.view)
|
return redirect(post.urls.view)
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(identity_required, name="dispatch")
|
||||||
|
class Compose(FormView):
|
||||||
|
|
||||||
|
template_name = "activities/compose.html"
|
||||||
|
|
||||||
|
class form_class(forms.Form):
|
||||||
|
text = forms.CharField(
|
||||||
|
widget=forms.Textarea(
|
||||||
|
attrs={
|
||||||
|
"placeholder": "What's on your mind?",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
visibility = forms.ChoiceField(
|
||||||
|
choices=[
|
||||||
|
(Post.Visibilities.public, "Public"),
|
||||||
|
(Post.Visibilities.unlisted, "Unlisted"),
|
||||||
|
(Post.Visibilities.followers, "Followers & Mentioned Only"),
|
||||||
|
(Post.Visibilities.mentioned, "Mentioned Only"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
content_warning = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
widget=forms.TextInput(
|
||||||
|
attrs={
|
||||||
|
"placeholder": "Content Warning",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
help_text="Optional - Post will be hidden behind this text until clicked",
|
||||||
|
)
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
Post.create_local(
|
||||||
|
author=self.request.identity,
|
||||||
|
content=linebreaks_filter(form.cleaned_data["text"]),
|
||||||
|
summary=form.cleaned_data.get("content_warning"),
|
||||||
|
visibility=form.cleaned_data["visibility"],
|
||||||
|
)
|
||||||
|
return redirect("/")
|
||||||
|
|
|
@ -95,12 +95,9 @@ class Notifications(TemplateView):
|
||||||
|
|
||||||
def get_context_data(self):
|
def get_context_data(self):
|
||||||
context = super().get_context_data()
|
context = super().get_context_data()
|
||||||
context["events"] = (
|
context["events"] = TimelineEvent.objects.filter(
|
||||||
TimelineEvent.objects.filter(
|
|
||||||
identity=self.request.identity,
|
identity=self.request.identity,
|
||||||
)
|
type__in=[TimelineEvent.Types.mentioned, TimelineEvent.Types.boosted],
|
||||||
.exclude(type__in=[TimelineEvent.Types.post, TimelineEvent.Types.boost])
|
).select_related("subject_post", "subject_post__author", "subject_identity")
|
||||||
.select_related("subject_post", "subject_post__author", "subject_identity")
|
|
||||||
)
|
|
||||||
context["current_page"] = "notifications"
|
context["current_page"] = "notifications"
|
||||||
return context
|
return context
|
||||||
|
|
12
core/ld.py
12
core/ld.py
|
@ -414,6 +414,18 @@ def canonicalise(json_data: Dict, include_security: bool = False) -> Dict:
|
||||||
return jsonld.compact(jsonld.expand(json_data), context)
|
return jsonld.compact(jsonld.expand(json_data), context)
|
||||||
|
|
||||||
|
|
||||||
|
def get_list(container, key) -> List:
|
||||||
|
"""
|
||||||
|
Given a JSON-LD value (that can be either a list, or a dict if it's just
|
||||||
|
one item), always returns a list"""
|
||||||
|
if key not in container:
|
||||||
|
return []
|
||||||
|
value = container[key]
|
||||||
|
if not isinstance(value, list):
|
||||||
|
return [value]
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
def format_ld_date(value: datetime.datetime) -> str:
|
def format_ld_date(value: datetime.datetime) -> str:
|
||||||
return value.strftime(DATETIME_FORMAT)
|
return value.strftime(DATETIME_FORMAT)
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
from django.views.generic import TemplateView
|
from django.http import JsonResponse
|
||||||
|
from django.templatetags.static import static
|
||||||
|
from django.views.generic import TemplateView, View
|
||||||
|
|
||||||
from activities.views.timelines import Home
|
from activities.views.timelines import Home
|
||||||
from users.models import Identity
|
from users.models import Identity
|
||||||
|
@ -19,3 +21,36 @@ class LoggedOutHomepage(TemplateView):
|
||||||
return {
|
return {
|
||||||
"identities": Identity.objects.filter(local=True),
|
"identities": Identity.objects.filter(local=True),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AppManifest(View):
|
||||||
|
"""
|
||||||
|
Serves a PWA manifest file. This is a view as we want to drive some
|
||||||
|
items from settings.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
return JsonResponse(
|
||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/web-manifest-combined.json",
|
||||||
|
"name": "Takahē",
|
||||||
|
"short_name": "Takahē",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#26323c",
|
||||||
|
"theme_color": "#26323c",
|
||||||
|
"description": "An ActivityPub server",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": static("img/icon-128.png"),
|
||||||
|
"sizes": "128x128",
|
||||||
|
"type": "image/png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": static("img/icon-1024.png"),
|
||||||
|
"sizes": "1024x1024",
|
||||||
|
"type": "image/png",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
|
@ -101,7 +101,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
width: 850px;
|
width: 900px;
|
||||||
margin: 20px auto;
|
margin: 20px auto;
|
||||||
box-shadow: 0 0 50px rgba(0, 0, 0, 0.6);
|
box-shadow: 0 0 50px rgba(0, 0, 0, 0.6);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
@ -520,6 +520,10 @@ h1.identity small {
|
||||||
color: var(--color-text-duller);
|
color: var(--color-text-duller);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post time i {
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
.post .summary {
|
.post .summary {
|
||||||
margin: 12px 0 4px 64px;
|
margin: 12px 0 4px 64px;
|
||||||
padding: 3px 6px;
|
padding: 3px 6px;
|
||||||
|
@ -583,3 +587,20 @@ h1.identity small {
|
||||||
.boost-banner a {
|
.boost-banner a {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@media (max-width: 920px) or (display-mode: standalone) {
|
||||||
|
|
||||||
|
main {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
header .logo {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ from users.views import activitypub, auth, identity
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", core.homepage),
|
path("", core.homepage),
|
||||||
|
path("manifest.json", core.AppManifest.as_view()),
|
||||||
# Activity views
|
# Activity views
|
||||||
path("notifications/", timelines.Notifications.as_view()),
|
path("notifications/", timelines.Notifications.as_view()),
|
||||||
path("local/", timelines.Local.as_view()),
|
path("local/", timelines.Local.as_view()),
|
||||||
|
@ -18,7 +19,8 @@ urlpatterns = [
|
||||||
path("@<handle>/actor/inbox/", activitypub.Inbox.as_view()),
|
path("@<handle>/actor/inbox/", activitypub.Inbox.as_view()),
|
||||||
path("@<handle>/action/", identity.ActionIdentity.as_view()),
|
path("@<handle>/action/", identity.ActionIdentity.as_view()),
|
||||||
# Posts
|
# Posts
|
||||||
path("@<handle>/posts/<int:post_id>/", posts.Post.as_view()),
|
path("compose/", posts.Compose.as_view()),
|
||||||
|
path("@<handle>/posts/<int:post_id>/", posts.Individual.as_view()),
|
||||||
path("@<handle>/posts/<int:post_id>/like/", posts.Like.as_view()),
|
path("@<handle>/posts/<int:post_id>/like/", posts.Like.as_view()),
|
||||||
path("@<handle>/posts/<int:post_id>/unlike/", posts.Like.as_view(undo=True)),
|
path("@<handle>/posts/<int:post_id>/unlike/", posts.Like.as_view(undo=True)),
|
||||||
path("@<handle>/posts/<int:post_id>/boost/", posts.Boost.as_view()),
|
path("@<handle>/posts/<int:post_id>/boost/", posts.Boost.as_view()),
|
||||||
|
|
|
@ -14,6 +14,10 @@
|
||||||
{{ event.subject_identity.name_or_handle }} followed you
|
{{ event.subject_identity.name_or_handle }} followed you
|
||||||
{% elif event.type == "like" %}
|
{% elif event.type == "like" %}
|
||||||
{{ event.subject_identity.name_or_handle }} liked {{ event.subject_post }}
|
{{ event.subject_identity.name_or_handle }} liked {{ event.subject_post }}
|
||||||
|
{% elif event.type == "mentioned" %}
|
||||||
|
{{ event.subject_post.author.name_or_handle }} mentioned you in {{ event.subject_post }}
|
||||||
|
{% elif event.type == "boosted" %}
|
||||||
|
{{ event.subject_identity.name_or_handle }} boosted your post {{ event.subject_post }}
|
||||||
{% else %}
|
{% else %}
|
||||||
Unknown event type {{event.type}}
|
Unknown event type {{event.type}}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -9,6 +9,15 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<time>
|
<time>
|
||||||
|
{% if post.visibility == 0 %}
|
||||||
|
<i class="visibility fa-solid fa-earth-oceania" title="Public"></i>
|
||||||
|
{% elif post.visibility == 1 %}
|
||||||
|
<i class="visibility fa-solid fa-lock-open" title="Unlisted"></i>
|
||||||
|
{% elif post.visibility == 2 %}
|
||||||
|
<i class="visibility fa-solid fa-lock" title="Followers Only"></i>
|
||||||
|
{% elif post.visibility == 3 %}
|
||||||
|
<i class="visibility fa-solid fa-at" title="Mentioned Only"></i>
|
||||||
|
{% endif %}
|
||||||
<a href="{{ post.url }}">
|
<a href="{{ post.url }}">
|
||||||
{% if post.published %}
|
{% if post.published %}
|
||||||
{{ post.published | timedeltashort }}
|
{{ post.published | timedeltashort }}
|
||||||
|
@ -36,6 +45,11 @@
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
{% include "activities/_like.html" %}
|
{% include "activities/_like.html" %}
|
||||||
{% include "activities/_boost.html" %}
|
{% include "activities/_boost.html" %}
|
||||||
|
{% if request.user.admin %}
|
||||||
|
<a title="Admin" href="/djadmin/activities/post/{{ post.pk }}/change/">
|
||||||
|
<i class="fa-solid fa-file-code"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
19
templates/activities/compose.html
Normal file
19
templates/activities/compose.html
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Compose{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<nav>
|
||||||
|
<a href="." class="selected">Compose</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<form action="." method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for field in form %}
|
||||||
|
{% include "forms/_field.html" %}
|
||||||
|
{% endfor %}
|
||||||
|
<div class="buttons">
|
||||||
|
<button>Post</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
|
@ -26,7 +26,7 @@
|
||||||
|
|
||||||
<div class="right-column">
|
<div class="right-column">
|
||||||
<h2>Compose</h2>
|
<h2>Compose</h2>
|
||||||
<form action="." method="POST" class="compose">
|
<form action="/compose/" method="POST" class="compose">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form.text }}
|
{{ form.text }}
|
||||||
{{ form.content_warning }}
|
{{ form.content_warning }}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
<link rel="stylesheet" href="{% static "css/style.css" %}" type="text/css" media="screen" />
|
<link rel="stylesheet" href="{% static "css/style.css" %}" type="text/css" media="screen" />
|
||||||
<link rel="stylesheet" href="{% static "fonts/raleway/raleway.css" %}" type="text/css" />
|
<link rel="stylesheet" href="{% static "fonts/raleway/raleway.css" %}" type="text/css" />
|
||||||
<link rel="stylesheet" href="{% static "fonts/font_awesome/all.min.css" %}" type="text/css" />
|
<link rel="stylesheet" href="{% static "fonts/font_awesome/all.min.css" %}" type="text/css" />
|
||||||
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<script src="{% static "js/hyperscript.min.js" %}"></script>
|
<script src="{% static "js/hyperscript.min.js" %}"></script>
|
||||||
<script src="{% static "js/htmx.min.js" %}"></script>
|
<script src="{% static "js/htmx.min.js" %}"></script>
|
||||||
{% block extra_head %}{% endblock %}
|
{% block extra_head %}{% endblock %}
|
||||||
|
@ -22,7 +23,8 @@
|
||||||
</a>
|
</a>
|
||||||
<menu>
|
<menu>
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<a href="#"><i class="fa-solid fa-gear"></i> Settings</a>
|
<a href="/compose/"><i class="fa-solid fa-feather"></i> Compose</a>
|
||||||
|
<a href="/settings/"><i class="fa-solid fa-gear"></i> Settings</a>
|
||||||
<div class="gap"></div>
|
<div class="gap"></div>
|
||||||
<a href="/identity/select/" class="identity">
|
<a href="/identity/select/" class="identity">
|
||||||
{% if not request.identity %}
|
{% if not request.identity %}
|
||||||
|
|
|
@ -44,6 +44,7 @@ class FollowAdmin(admin.ModelAdmin):
|
||||||
@admin.register(InboxMessage)
|
@admin.register(InboxMessage)
|
||||||
class InboxMessageAdmin(admin.ModelAdmin):
|
class InboxMessageAdmin(admin.ModelAdmin):
|
||||||
list_display = ["id", "state", "state_attempted", "message_type", "message_actor"]
|
list_display = ["id", "state", "state_attempted", "message_type", "message_actor"]
|
||||||
|
search_fields = ["message"]
|
||||||
actions = ["reset_state"]
|
actions = ["reset_state"]
|
||||||
|
|
||||||
@admin.action(description="Reset State")
|
@admin.action(description="Reset State")
|
||||||
|
|
Loading…
Reference in a new issue