mirror of
https://github.com/jointakahe/takahe.git
synced 2024-11-21 23:01:00 +00:00
UI/Domains Refactor
Redoes the UI to remove timelines, promote domains, and a lot of other things to support the refactor.
This commit is contained in:
parent
7331591432
commit
8f57aa5f37
135 changed files with 1966 additions and 2381 deletions
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
|
@ -18,14 +18,10 @@ jobs:
|
|||
python-version: ["3.10", "3.11"]
|
||||
db:
|
||||
- "postgres://postgres:postgres@localhost/postgres"
|
||||
- "sqlite:///takahe.db"
|
||||
include:
|
||||
- db: "postgres://postgres:postgres@localhost/postgres"
|
||||
db_name: postgres
|
||||
search: true
|
||||
- db: "sqlite:///takahe.db"
|
||||
db_name: sqlite
|
||||
search: false
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
|
|
24
activities/migrations/0014_post_content_vector_gin.py
Normal file
24
activities/migrations/0014_post_content_vector_gin.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 4.2 on 2023-04-29 18:49
|
||||
|
||||
import django.contrib.postgres.indexes
|
||||
import django.contrib.postgres.search
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("activities", "0013_postattachment_author"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name="post",
|
||||
index=django.contrib.postgres.indexes.GinIndex(
|
||||
django.contrib.postgres.search.SearchVector(
|
||||
"content", config="english"
|
||||
),
|
||||
name="content_vector_gin",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -11,6 +11,7 @@ import httpx
|
|||
import urlman
|
||||
from asgiref.sync import async_to_sync, sync_to_async
|
||||
from django.contrib.postgres.indexes import GinIndex
|
||||
from django.contrib.postgres.search import SearchVector
|
||||
from django.db import models, transaction
|
||||
from django.template import loader
|
||||
from django.template.defaultfilters import linebreaks_filter
|
||||
|
@ -312,6 +313,10 @@ class Post(StatorModel):
|
|||
class Meta:
|
||||
indexes = [
|
||||
GinIndex(fields=["hashtags"], name="hashtags_gin"),
|
||||
GinIndex(
|
||||
SearchVector("content", config="english"),
|
||||
name="content_vector_gin",
|
||||
),
|
||||
models.Index(
|
||||
fields=["visibility", "local", "published"],
|
||||
name="ix_post_local_public_published",
|
||||
|
|
|
@ -72,7 +72,12 @@ class PostService:
|
|||
def unboost_as(self, identity: Identity):
|
||||
self.uninteract_as(identity, PostInteraction.Types.boost)
|
||||
|
||||
def context(self, identity: Identity | None) -> tuple[list[Post], list[Post]]:
|
||||
def context(
|
||||
self,
|
||||
identity: Identity | None,
|
||||
num_ancestors: int = 10,
|
||||
num_descendants: int = 50,
|
||||
) -> tuple[list[Post], list[Post]]:
|
||||
"""
|
||||
Returns ancestor/descendant information.
|
||||
|
||||
|
@ -82,7 +87,6 @@ class PostService:
|
|||
If identity is provided, includes mentions/followers-only posts they
|
||||
can see. Otherwise, shows unlisted and above only.
|
||||
"""
|
||||
num_ancestors = 10
|
||||
num_descendants = 50
|
||||
# Retrieve ancestors via parent walk
|
||||
ancestors: list[Post] = []
|
||||
|
|
|
@ -123,6 +123,12 @@ class SearchService:
|
|||
results.add(hashtag)
|
||||
return results
|
||||
|
||||
def search_post_content(self):
|
||||
"""
|
||||
Searches for posts on an identity via full text search
|
||||
"""
|
||||
return self.identity.posts.filter(content__search=self.query)[:50]
|
||||
|
||||
def search_all(self):
|
||||
"""
|
||||
Returns all possible results for a search
|
||||
|
|
|
@ -47,12 +47,15 @@ class TimelineService:
|
|||
)
|
||||
|
||||
def local(self) -> models.QuerySet[Post]:
|
||||
return (
|
||||
queryset = (
|
||||
PostService.queryset()
|
||||
.local_public()
|
||||
.filter(author__restriction=Identity.Restriction.none)
|
||||
.order_by("-id")
|
||||
)
|
||||
if self.identity is not None:
|
||||
queryset = queryset.filter(author__domain=self.identity.domain)
|
||||
return queryset
|
||||
|
||||
def federated(self) -> models.QuerySet[Post]:
|
||||
return (
|
||||
|
|
|
@ -1,27 +1,17 @@
|
|||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.contrib import messages
|
||||
from django.shortcuts import redirect
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.generic import FormView
|
||||
|
||||
from activities.models import (
|
||||
Post,
|
||||
PostAttachment,
|
||||
PostAttachmentStates,
|
||||
PostStates,
|
||||
TimelineEvent,
|
||||
)
|
||||
from activities.models import Post, PostAttachment, PostAttachmentStates, TimelineEvent
|
||||
from core.files import blurhash_image, resize_image
|
||||
from core.html import FediverseHtmlParser
|
||||
from core.models import Config
|
||||
from users.decorators import identity_required
|
||||
from users.views.base import IdentityViewMixin
|
||||
|
||||
|
||||
@method_decorator(identity_required, name="dispatch")
|
||||
class Compose(FormView):
|
||||
|
||||
class Compose(IdentityViewMixin, FormView):
|
||||
template_name = "activities/compose.html"
|
||||
|
||||
class form_class(forms.Form):
|
||||
|
@ -33,6 +23,7 @@ class Compose(FormView):
|
|||
},
|
||||
)
|
||||
)
|
||||
|
||||
visibility = forms.ChoiceField(
|
||||
choices=[
|
||||
(Post.Visibilities.public, "Public"),
|
||||
|
@ -42,6 +33,7 @@ class Compose(FormView):
|
|||
(Post.Visibilities.mentioned, "Mentioned Only"),
|
||||
],
|
||||
)
|
||||
|
||||
content_warning = forms.CharField(
|
||||
required=False,
|
||||
label=Config.lazy_system_value("content_warning_text"),
|
||||
|
@ -52,11 +44,42 @@ 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 __init__(self, request, *args, **kwargs):
|
||||
image = forms.ImageField(
|
||||
required=False,
|
||||
help_text="Optional - For multiple image uploads and cropping, please use an app",
|
||||
widget=forms.FileInput(
|
||||
attrs={
|
||||
"_": f"""
|
||||
on change
|
||||
if me.files[0].size > {settings.SETUP.MEDIA_MAX_IMAGE_FILESIZE_MB * 1024 ** 2}
|
||||
add [@disabled=] to #upload
|
||||
|
||||
remove <ul.errorlist/>
|
||||
make <ul.errorlist/> called errorlist
|
||||
make <li/> called error
|
||||
set size_in_mb to (me.files[0].size / 1024 / 1024).toFixed(2)
|
||||
put 'File must be {settings.SETUP.MEDIA_MAX_IMAGE_FILESIZE_MB}MB or less (actual: ' + size_in_mb + 'MB)' into error
|
||||
put error into errorlist
|
||||
put errorlist before me
|
||||
else
|
||||
remove @disabled from #upload
|
||||
remove <ul.errorlist/>
|
||||
end
|
||||
end
|
||||
"""
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
image_caption = forms.CharField(
|
||||
required=False,
|
||||
help_text="Provide an image caption for the visually impaired",
|
||||
)
|
||||
|
||||
def __init__(self, identity, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.request = request
|
||||
self.identity = identity
|
||||
self.fields["text"].widget.attrs[
|
||||
"_"
|
||||
] = rf"""
|
||||
|
@ -83,7 +106,7 @@ class Compose(FormView):
|
|||
def clean_text(self):
|
||||
text = self.cleaned_data.get("text")
|
||||
# Check minimum interval
|
||||
last_post = self.request.identity.posts.order_by("-created").first()
|
||||
last_post = self.identity.posts.order_by("-created").first()
|
||||
if (
|
||||
last_post
|
||||
and (timezone.now() - last_post.created).total_seconds()
|
||||
|
@ -102,139 +125,9 @@ class Compose(FormView):
|
|||
)
|
||||
return text
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
return self.form_class(request=self.request, **self.get_form_kwargs())
|
||||
|
||||
def get_initial(self):
|
||||
initial = super().get_initial()
|
||||
if self.post_obj:
|
||||
initial.update(
|
||||
{
|
||||
"reply_to": self.reply_to.pk if self.reply_to else "",
|
||||
"visibility": self.post_obj.visibility,
|
||||
"text": FediverseHtmlParser(self.post_obj.content).plain_text,
|
||||
"content_warning": self.post_obj.summary,
|
||||
}
|
||||
)
|
||||
else:
|
||||
initial[
|
||||
"visibility"
|
||||
] = self.request.identity.config_identity.default_post_visibility
|
||||
if self.reply_to:
|
||||
initial["reply_to"] = self.reply_to.pk
|
||||
if self.reply_to.visibility == Post.Visibilities.public:
|
||||
initial[
|
||||
"visibility"
|
||||
] = self.request.identity.config_identity.default_reply_visibility
|
||||
else:
|
||||
initial["visibility"] = self.reply_to.visibility
|
||||
initial["content_warning"] = self.reply_to.summary
|
||||
# Build a set of mentions for the content to start as
|
||||
mentioned = {self.reply_to.author}
|
||||
mentioned.update(self.reply_to.mentions.all())
|
||||
mentioned.discard(self.request.identity)
|
||||
initial["text"] = "".join(
|
||||
f"@{identity.handle} "
|
||||
for identity in mentioned
|
||||
if identity.username
|
||||
)
|
||||
return initial
|
||||
|
||||
def form_valid(self, form):
|
||||
# Gather any attachment objects now, they're not in the form proper
|
||||
attachments = []
|
||||
if "attachment" in self.request.POST:
|
||||
attachments = PostAttachment.objects.filter(
|
||||
pk__in=self.request.POST.getlist("attachment", [])
|
||||
)
|
||||
# Dispatch based on edit or not
|
||||
if self.post_obj:
|
||||
self.post_obj.edit_local(
|
||||
content=form.cleaned_data["text"],
|
||||
summary=form.cleaned_data.get("content_warning"),
|
||||
visibility=form.cleaned_data["visibility"],
|
||||
attachments=attachments,
|
||||
)
|
||||
self.post_obj.transition_perform(PostStates.edited)
|
||||
else:
|
||||
post = Post.create_local(
|
||||
author=self.request.identity,
|
||||
content=form.cleaned_data["text"],
|
||||
summary=form.cleaned_data.get("content_warning"),
|
||||
visibility=form.cleaned_data["visibility"],
|
||||
reply_to=self.reply_to,
|
||||
attachments=attachments,
|
||||
)
|
||||
# Add their own timeline event for immediate visibility
|
||||
TimelineEvent.add_post(self.request.identity, post)
|
||||
return redirect("/")
|
||||
|
||||
def dispatch(self, request, handle=None, post_id=None, *args, **kwargs):
|
||||
self.post_obj = None
|
||||
if handle and post_id:
|
||||
# Make sure the request identity owns the post!
|
||||
if handle != request.identity.handle:
|
||||
raise PermissionDenied("Post author is not requestor")
|
||||
|
||||
self.post_obj = get_object_or_404(request.identity.posts, pk=post_id)
|
||||
|
||||
# Grab the reply-to post info now
|
||||
self.reply_to = None
|
||||
reply_to_id = request.POST.get("reply_to") or 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
|
||||
if self.post_obj:
|
||||
context["post"] = self.post_obj
|
||||
return context
|
||||
|
||||
|
||||
@method_decorator(identity_required, name="dispatch")
|
||||
class ImageUpload(FormView):
|
||||
"""
|
||||
Handles image upload - returns a new input type hidden to embed in
|
||||
the main form that references an orphaned PostAttachment
|
||||
"""
|
||||
|
||||
template_name = "activities/_image_upload.html"
|
||||
|
||||
class form_class(forms.Form):
|
||||
image = forms.ImageField(
|
||||
widget=forms.FileInput(
|
||||
attrs={
|
||||
"_": f"""
|
||||
on change
|
||||
if me.files[0].size > {settings.SETUP.MEDIA_MAX_IMAGE_FILESIZE_MB * 1024 ** 2}
|
||||
add [@disabled=] to #upload
|
||||
|
||||
remove <ul.errorlist/>
|
||||
make <ul.errorlist/> called errorlist
|
||||
make <li/> called error
|
||||
set size_in_mb to (me.files[0].size / 1024 / 1024).toFixed(2)
|
||||
put 'File must be {settings.SETUP.MEDIA_MAX_IMAGE_FILESIZE_MB}MB or less (actual: ' + size_in_mb + 'MB)' into error
|
||||
put error into errorlist
|
||||
put errorlist before me
|
||||
else
|
||||
remove @disabled from #upload
|
||||
remove <ul.errorlist/>
|
||||
end
|
||||
end
|
||||
"""
|
||||
}
|
||||
)
|
||||
)
|
||||
description = forms.CharField(required=False)
|
||||
|
||||
def clean_image(self):
|
||||
value = self.cleaned_data["image"]
|
||||
value = self.cleaned_data.get("image")
|
||||
if value:
|
||||
max_mb = settings.SETUP.MEDIA_MAX_IMAGE_FILESIZE_MB
|
||||
max_bytes = max_mb * 1024 * 1024
|
||||
if value.size > max_bytes:
|
||||
|
@ -245,11 +138,18 @@ class ImageUpload(FormView):
|
|||
)
|
||||
return value
|
||||
|
||||
def form_invalid(self, form):
|
||||
return super().form_invalid(form)
|
||||
def get_form(self, form_class=None):
|
||||
return self.form_class(identity=self.identity, **self.get_form_kwargs())
|
||||
|
||||
def get_initial(self):
|
||||
initial = super().get_initial()
|
||||
initial["visibility"] = self.identity.config_identity.default_post_visibility
|
||||
return initial
|
||||
|
||||
def form_valid(self, form):
|
||||
# Make a PostAttachment
|
||||
# See if we need to make an image attachment
|
||||
attachments = []
|
||||
if form.cleaned_data.get("image"):
|
||||
main_file = resize_image(
|
||||
form.cleaned_data["image"],
|
||||
size=(2000, 2000),
|
||||
|
@ -265,11 +165,10 @@ class ImageUpload(FormView):
|
|||
mimetype="image/webp",
|
||||
width=main_file.image.width,
|
||||
height=main_file.image.height,
|
||||
name=form.cleaned_data.get("description"),
|
||||
name=form.cleaned_data.get("image_caption"),
|
||||
state=PostAttachmentStates.fetched,
|
||||
author=self.request.identity,
|
||||
author=self.identity,
|
||||
)
|
||||
|
||||
attachment.file.save(
|
||||
main_file.name,
|
||||
main_file,
|
||||
|
@ -279,7 +178,22 @@ class ImageUpload(FormView):
|
|||
thumbnail_file,
|
||||
)
|
||||
attachment.save()
|
||||
# Return the response, with a hidden input plus a note
|
||||
return render(
|
||||
self.request, "activities/_image_uploaded.html", {"attachment": attachment}
|
||||
attachments.append(attachment)
|
||||
# Create the post
|
||||
post = Post.create_local(
|
||||
author=self.identity,
|
||||
content=form.cleaned_data["text"],
|
||||
summary=form.cleaned_data.get("content_warning"),
|
||||
visibility=form.cleaned_data["visibility"],
|
||||
attachments=attachments,
|
||||
)
|
||||
# Add their own timeline event for immediate visibility
|
||||
TimelineEvent.add_post(self.identity, post)
|
||||
messages.success(self.request, "Your post was created.")
|
||||
return redirect(".")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["identity"] = self.identity
|
||||
context["section"] = "compose"
|
||||
return context
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
from django.views.generic import ListView
|
||||
|
||||
from activities.models import Hashtag
|
||||
|
||||
|
||||
class ExploreTag(ListView):
|
||||
|
||||
template_name = "activities/explore_tag.html"
|
||||
extra_context = {
|
||||
"current_page": "explore",
|
||||
"allows_refresh": True,
|
||||
}
|
||||
paginate_by = 20
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
Hashtag.objects.public()
|
||||
.filter(
|
||||
stats__total__gt=0,
|
||||
)
|
||||
.order_by("-stats__total")
|
||||
)[:20]
|
||||
|
||||
|
||||
class Explore(ExploreTag):
|
||||
pass
|
|
@ -1,38 +0,0 @@
|
|||
from django.http import HttpRequest
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.generic import View
|
||||
|
||||
from activities.models.hashtag import Hashtag
|
||||
from users.decorators import identity_required
|
||||
|
||||
|
||||
@method_decorator(identity_required, name="dispatch")
|
||||
class HashtagFollow(View):
|
||||
"""
|
||||
Follows/unfollows a hashtag with the current identity
|
||||
"""
|
||||
|
||||
undo = False
|
||||
|
||||
def post(self, request: HttpRequest, hashtag):
|
||||
hashtag = get_object_or_404(
|
||||
Hashtag,
|
||||
pk=hashtag,
|
||||
)
|
||||
follow = None
|
||||
if self.undo:
|
||||
request.identity.hashtag_follows.filter(hashtag=hashtag).delete()
|
||||
else:
|
||||
follow = request.identity.hashtag_follows.get_or_create(hashtag=hashtag)
|
||||
# Return either a redirect or a HTMX snippet
|
||||
if request.htmx:
|
||||
return render(
|
||||
request,
|
||||
"activities/_hashtag_follow.html",
|
||||
{
|
||||
"hashtag": hashtag,
|
||||
"follow": follow,
|
||||
},
|
||||
)
|
||||
return redirect(hashtag.urls.view)
|
|
@ -1,15 +1,13 @@
|
|||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import Http404, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.vary import vary_on_headers
|
||||
from django.views.generic import TemplateView, View
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from activities.models import Post, PostInteraction, PostStates
|
||||
from activities.models import Post, PostStates
|
||||
from activities.services import PostService
|
||||
from core.decorators import cache_page_by_ap_json
|
||||
from core.ld import canonicalise
|
||||
from users.decorators import identity_required
|
||||
from users.models import Identity
|
||||
from users.shortcuts import by_handle_or_404
|
||||
|
||||
|
@ -19,7 +17,6 @@ from users.shortcuts import by_handle_or_404
|
|||
)
|
||||
@method_decorator(vary_on_headers("Accept"), name="dispatch")
|
||||
class Individual(TemplateView):
|
||||
|
||||
template_name = "activities/post.html"
|
||||
|
||||
identity: Identity
|
||||
|
@ -32,7 +29,7 @@ class Individual(TemplateView):
|
|||
self.post_obj = get_object_or_404(
|
||||
PostService.queryset()
|
||||
.filter(author=self.identity)
|
||||
.visible_to(request.identity, include_replies=True),
|
||||
.unlisted(include_replies=True),
|
||||
pk=post_id,
|
||||
)
|
||||
if self.post_obj.state in [PostStates.deleted, PostStates.deleted_fanned_out]:
|
||||
|
@ -49,20 +46,17 @@ class Individual(TemplateView):
|
|||
context = super().get_context_data(**kwargs)
|
||||
|
||||
ancestors, descendants = PostService(self.post_obj).context(
|
||||
self.request.identity
|
||||
identity=None, num_ancestors=2
|
||||
)
|
||||
|
||||
context.update(
|
||||
{
|
||||
"identity": self.identity,
|
||||
"post": self.post_obj,
|
||||
"interactions": PostInteraction.get_post_interactions(
|
||||
[self.post_obj] + ancestors + descendants,
|
||||
self.request.identity,
|
||||
),
|
||||
"link_original": True,
|
||||
"ancestors": ancestors,
|
||||
"descendants": descendants,
|
||||
"public_styling": True,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -76,128 +70,3 @@ class Individual(TemplateView):
|
|||
canonicalise(self.post_obj.to_ap(), include_security=True),
|
||||
content_type="application/activity+json",
|
||||
)
|
||||
|
||||
|
||||
@method_decorator(identity_required, name="dispatch")
|
||||
class Like(View):
|
||||
"""
|
||||
Adds/removes a like from the current identity to the post
|
||||
"""
|
||||
|
||||
undo = False
|
||||
|
||||
def post(self, request, handle, post_id):
|
||||
identity = by_handle_or_404(self.request, handle, local=False)
|
||||
post = get_object_or_404(
|
||||
PostService.queryset()
|
||||
.filter(author=identity)
|
||||
.visible_to(request.identity, include_replies=True),
|
||||
pk=post_id,
|
||||
)
|
||||
service = PostService(post)
|
||||
if self.undo:
|
||||
service.unlike_as(request.identity)
|
||||
else:
|
||||
service.like_as(request.identity)
|
||||
# Return either a redirect or a HTMX snippet
|
||||
if request.htmx:
|
||||
return render(
|
||||
request,
|
||||
"activities/_like.html",
|
||||
{
|
||||
"post": post,
|
||||
"interactions": {"like": set() if self.undo else {post.pk}},
|
||||
},
|
||||
)
|
||||
return redirect(post.urls.view)
|
||||
|
||||
|
||||
@method_decorator(identity_required, name="dispatch")
|
||||
class Boost(View):
|
||||
"""
|
||||
Adds/removes a boost from the current identity to the post
|
||||
"""
|
||||
|
||||
undo = False
|
||||
|
||||
def post(self, request, handle, post_id):
|
||||
identity = by_handle_or_404(self.request, handle, local=False)
|
||||
post = get_object_or_404(
|
||||
PostService.queryset()
|
||||
.filter(author=identity)
|
||||
.visible_to(request.identity, include_replies=True),
|
||||
pk=post_id,
|
||||
)
|
||||
service = PostService(post)
|
||||
if self.undo:
|
||||
service.unboost_as(request.identity)
|
||||
else:
|
||||
service.boost_as(request.identity)
|
||||
# Return either a redirect or a HTMX snippet
|
||||
if request.htmx:
|
||||
return render(
|
||||
request,
|
||||
"activities/_boost.html",
|
||||
{
|
||||
"post": post,
|
||||
"interactions": {"boost": set() if self.undo else {post.pk}},
|
||||
},
|
||||
)
|
||||
return redirect(post.urls.view)
|
||||
|
||||
|
||||
@method_decorator(identity_required, name="dispatch")
|
||||
class Bookmark(View):
|
||||
"""
|
||||
Adds/removes a bookmark from the current identity to the post
|
||||
"""
|
||||
|
||||
undo = False
|
||||
|
||||
def post(self, request, handle, post_id):
|
||||
identity = by_handle_or_404(self.request, handle, local=False)
|
||||
post = get_object_or_404(
|
||||
PostService.queryset()
|
||||
.filter(author=identity)
|
||||
.visible_to(request.identity, include_replies=True),
|
||||
pk=post_id,
|
||||
)
|
||||
if self.undo:
|
||||
request.identity.bookmarks.filter(post=post).delete()
|
||||
else:
|
||||
request.identity.bookmarks.get_or_create(post=post)
|
||||
# Return either a redirect or a HTMX snippet
|
||||
if request.htmx:
|
||||
return render(
|
||||
request,
|
||||
"activities/_bookmark.html",
|
||||
{
|
||||
"post": post,
|
||||
"bookmarks": set() if self.undo else {post.pk},
|
||||
},
|
||||
)
|
||||
return redirect(post.urls.view)
|
||||
|
||||
|
||||
@method_decorator(identity_required, name="dispatch")
|
||||
class Delete(TemplateView):
|
||||
"""
|
||||
Deletes a post
|
||||
"""
|
||||
|
||||
template_name = "activities/post_delete.html"
|
||||
|
||||
def dispatch(self, request, handle, post_id):
|
||||
# Make sure the request identity owns the post!
|
||||
if handle != request.identity.handle:
|
||||
raise PermissionDenied("Post author is not requestor")
|
||||
self.identity = by_handle_or_404(self.request, handle, local=False)
|
||||
self.post_obj = get_object_or_404(self.identity.posts, pk=post_id)
|
||||
return super().dispatch(request)
|
||||
|
||||
def get_context_data(self):
|
||||
return {"post": self.post_obj}
|
||||
|
||||
def post(self, request):
|
||||
PostService(self.post_obj).delete()
|
||||
return redirect("/")
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
from django import forms
|
||||
from django.views.generic import FormView
|
||||
|
||||
from activities.services import SearchService
|
||||
|
||||
|
||||
class Search(FormView):
|
||||
|
||||
template_name = "activities/search.html"
|
||||
|
||||
class form_class(forms.Form):
|
||||
query = forms.CharField(
|
||||
help_text="Search for:\nA user by @username@domain or their profile URL\nA hashtag by #tagname\nA post by its URL",
|
||||
widget=forms.TextInput(attrs={"type": "search", "autofocus": "autofocus"}),
|
||||
)
|
||||
|
||||
def form_valid(self, form):
|
||||
searcher = SearchService(form.cleaned_data["query"], self.request.identity)
|
||||
# Render results
|
||||
context = self.get_context_data(form=form)
|
||||
context["results"] = searcher.search_all()
|
||||
return self.render_to_response(context)
|
|
@ -1,53 +1,35 @@
|
|||
from django.core.paginator import Paginator
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.generic import ListView, TemplateView
|
||||
|
||||
from activities.models import Hashtag, PostInteraction, TimelineEvent
|
||||
from activities.models import Hashtag, TimelineEvent
|
||||
from activities.services import TimelineService
|
||||
from core.decorators import cache_page
|
||||
from users.decorators import identity_required
|
||||
from users.models import Bookmark, HashtagFollow
|
||||
|
||||
from .compose import Compose
|
||||
from users.models import Identity
|
||||
from users.views.base import IdentityViewMixin
|
||||
|
||||
|
||||
@method_decorator(identity_required, name="dispatch")
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class Home(TemplateView):
|
||||
"""
|
||||
Homepage for logged-in users - shows identities primarily.
|
||||
"""
|
||||
|
||||
template_name = "activities/home.html"
|
||||
|
||||
form_class = Compose.form_class
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
return self.form_class(request=self.request, **self.get_form_kwargs())
|
||||
|
||||
def get_context_data(self):
|
||||
events = TimelineService(self.request.identity).home()
|
||||
paginator = Paginator(events, 25)
|
||||
page_number = self.request.GET.get("page")
|
||||
event_page = paginator.get_page(page_number)
|
||||
context = {
|
||||
"interactions": PostInteraction.get_event_interactions(
|
||||
event_page,
|
||||
self.request.identity,
|
||||
),
|
||||
"bookmarks": Bookmark.for_identity(
|
||||
self.request.identity, event_page, "subject_post_id"
|
||||
),
|
||||
"current_page": "home",
|
||||
"allows_refresh": True,
|
||||
"page_obj": event_page,
|
||||
"form": self.form_class(request=self.request),
|
||||
return {
|
||||
"identities": Identity.objects.filter(
|
||||
users__pk=self.request.user.pk
|
||||
).order_by("created"),
|
||||
}
|
||||
return context
|
||||
|
||||
|
||||
@method_decorator(
|
||||
cache_page("cache_timeout_page_timeline", public_only=True), name="dispatch"
|
||||
)
|
||||
class Tag(ListView):
|
||||
|
||||
template_name = "activities/tag.html"
|
||||
extra_context = {
|
||||
"current_page": "tag",
|
||||
|
@ -64,77 +46,15 @@ class Tag(ListView):
|
|||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
return TimelineService(self.request.identity).hashtag(self.hashtag)
|
||||
return TimelineService(None).hashtag(self.hashtag)
|
||||
|
||||
def get_context_data(self):
|
||||
context = super().get_context_data()
|
||||
context["hashtag"] = self.hashtag
|
||||
context["interactions"] = PostInteraction.get_post_interactions(
|
||||
context["page_obj"], self.request.identity
|
||||
)
|
||||
context["bookmarks"] = Bookmark.for_identity(
|
||||
self.request.identity, context["page_obj"]
|
||||
)
|
||||
context["follow"] = HashtagFollow.maybe_get(
|
||||
self.request.identity,
|
||||
self.hashtag,
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
@method_decorator(
|
||||
cache_page("cache_timeout_page_timeline", public_only=True), name="dispatch"
|
||||
)
|
||||
class Local(ListView):
|
||||
|
||||
template_name = "activities/local.html"
|
||||
extra_context = {
|
||||
"current_page": "local",
|
||||
"allows_refresh": True,
|
||||
}
|
||||
paginate_by = 25
|
||||
|
||||
def get_queryset(self):
|
||||
return TimelineService(self.request.identity).local()
|
||||
|
||||
def get_context_data(self):
|
||||
context = super().get_context_data()
|
||||
context["interactions"] = PostInteraction.get_post_interactions(
|
||||
context["page_obj"], self.request.identity
|
||||
)
|
||||
context["bookmarks"] = Bookmark.for_identity(
|
||||
self.request.identity, context["page_obj"]
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
@method_decorator(identity_required, name="dispatch")
|
||||
class Federated(ListView):
|
||||
|
||||
template_name = "activities/federated.html"
|
||||
extra_context = {
|
||||
"current_page": "federated",
|
||||
"allows_refresh": True,
|
||||
}
|
||||
paginate_by = 25
|
||||
|
||||
def get_queryset(self):
|
||||
return TimelineService(self.request.identity).federated()
|
||||
|
||||
def get_context_data(self):
|
||||
context = super().get_context_data()
|
||||
context["interactions"] = PostInteraction.get_post_interactions(
|
||||
context["page_obj"], self.request.identity
|
||||
)
|
||||
context["bookmarks"] = Bookmark.for_identity(
|
||||
self.request.identity, context["page_obj"]
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
@method_decorator(identity_required, name="dispatch")
|
||||
class Notifications(ListView):
|
||||
|
||||
class Notifications(IdentityViewMixin, ListView):
|
||||
template_name = "activities/notifications.html"
|
||||
extra_context = {
|
||||
"current_page": "notifications",
|
||||
|
@ -146,7 +66,6 @@ class Notifications(ListView):
|
|||
"boosted": TimelineEvent.Types.boosted,
|
||||
"mentioned": TimelineEvent.Types.mentioned,
|
||||
"liked": TimelineEvent.Types.liked,
|
||||
"identity_created": TimelineEvent.Types.identity_created,
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
|
@ -164,7 +83,7 @@ class Notifications(ListView):
|
|||
for type_name, type in self.notification_types.items():
|
||||
if notification_options.get(type_name, True):
|
||||
types.append(type)
|
||||
return TimelineService(self.request.identity).notifications(types)
|
||||
return TimelineService(self.identity).notifications(types)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
@ -185,12 +104,6 @@ class Notifications(ListView):
|
|||
events.append(event)
|
||||
# Retrieve what kinds of things to show
|
||||
context["events"] = events
|
||||
context["identity"] = self.identity
|
||||
context["notification_options"] = self.request.session["notification_options"]
|
||||
context["interactions"] = PostInteraction.get_event_interactions(
|
||||
context["page_obj"],
|
||||
self.request.identity,
|
||||
)
|
||||
context["bookmarks"] = Bookmark.for_identity(
|
||||
self.request.identity, context["page_obj"], "subject_post_id"
|
||||
)
|
||||
return context
|
||||
|
|
|
@ -6,8 +6,7 @@ from django.http import JsonResponse
|
|||
|
||||
def identity_required(function):
|
||||
"""
|
||||
API version of the identity_required decorator that just makes sure the
|
||||
token is tied to one, not an app only.
|
||||
Makes sure the token is tied to an identity, not an app only.
|
||||
"""
|
||||
|
||||
@wraps(function)
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import secrets
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
|
@ -17,3 +19,23 @@ class Application(models.Model):
|
|||
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
updated = models.DateTimeField(auto_now=True)
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
client_name: str,
|
||||
redirect_uris: str,
|
||||
website: str | None,
|
||||
scopes: str | None = None,
|
||||
):
|
||||
client_id = "tk-" + secrets.token_urlsafe(16)
|
||||
client_secret = secrets.token_urlsafe(40)
|
||||
|
||||
return cls.objects.create(
|
||||
name=client_name,
|
||||
website=website,
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
redirect_uris=redirect_uris,
|
||||
scopes=scopes or "read",
|
||||
)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import urlman
|
||||
from django.db import models
|
||||
|
||||
|
||||
|
@ -37,6 +38,9 @@ class Token(models.Model):
|
|||
updated = models.DateTimeField(auto_now=True)
|
||||
revoked = models.DateTimeField(blank=True, null=True)
|
||||
|
||||
class urls(urlman.Urls):
|
||||
edit = "/@{self.identity.handle}/settings/tokens/{self.id}/"
|
||||
|
||||
def has_scope(self, scope: str):
|
||||
"""
|
||||
Returns if this token has the given scope.
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import secrets
|
||||
|
||||
from hatchway import QueryOrBody, api_view
|
||||
|
||||
from .. import schemas
|
||||
|
@ -14,14 +12,10 @@ def add_app(
|
|||
scopes: QueryOrBody[None | str] = None,
|
||||
website: QueryOrBody[None | str] = None,
|
||||
) -> schemas.Application:
|
||||
client_id = "tk-" + secrets.token_urlsafe(16)
|
||||
client_secret = secrets.token_urlsafe(40)
|
||||
application = Application.objects.create(
|
||||
name=client_name,
|
||||
application = Application.create(
|
||||
client_name=client_name,
|
||||
website=website,
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
redirect_uris=redirect_uris,
|
||||
scopes=scopes or "read",
|
||||
scopes=scopes,
|
||||
)
|
||||
return schemas.Application.from_orm(application)
|
||||
|
|
|
@ -4,9 +4,6 @@ from core.models import Config
|
|||
def config_context(request):
|
||||
return {
|
||||
"config": Config.system,
|
||||
"config_identity": (
|
||||
request.identity.config_identity if request.identity else None
|
||||
),
|
||||
"top_section": request.path.strip("/").split("/")[0],
|
||||
"opengraph_defaults": {
|
||||
"og:site_name": Config.system.site_name,
|
||||
|
|
|
@ -20,16 +20,6 @@ def vary_by_ap_json(request, *args, **kwargs) -> str:
|
|||
return "not_ap"
|
||||
|
||||
|
||||
def vary_by_identity(request, *args, **kwargs) -> str:
|
||||
"""
|
||||
Return a cache usable string token that is different based upon the
|
||||
request.identity
|
||||
"""
|
||||
if request.identity:
|
||||
return f"ident{request.identity.pk}"
|
||||
return "identNone"
|
||||
|
||||
|
||||
def cache_page(
|
||||
timeout: int | str = "cache_timeout_page_default",
|
||||
*,
|
||||
|
|
36
core/migrations/0002_domain_config.py
Normal file
36
core/migrations/0002_domain_config.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
# Generated by Django 4.2 on 2023-04-29 18:49
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("users", "0016_hashtagfollow"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("core", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name="config",
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="config",
|
||||
name="domain",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="configs",
|
||||
to="users.domain",
|
||||
),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="config",
|
||||
unique_together={("key", "user", "identity", "domain")},
|
||||
),
|
||||
]
|
|
@ -43,6 +43,14 @@ class Config(models.Model):
|
|||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
domain = models.ForeignKey(
|
||||
"users.domain",
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name="configs",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
json = models.JSONField(blank=True, null=True)
|
||||
image = models.ImageField(
|
||||
blank=True,
|
||||
|
@ -52,7 +60,7 @@ class Config(models.Model):
|
|||
|
||||
class Meta:
|
||||
unique_together = [
|
||||
("key", "user", "identity"),
|
||||
("key", "user", "identity", "domain"),
|
||||
]
|
||||
|
||||
system: ClassVar["Config.ConfigOptions"] # type: ignore
|
||||
|
@ -86,7 +94,7 @@ class Config(models.Model):
|
|||
"""
|
||||
return cls.load_values(
|
||||
cls.SystemOptions,
|
||||
{"identity__isnull": True, "user__isnull": True},
|
||||
{"identity__isnull": True, "user__isnull": True, "domain__isnull": True},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
@ -96,7 +104,7 @@ class Config(models.Model):
|
|||
"""
|
||||
return await sync_to_async(cls.load_values)(
|
||||
cls.SystemOptions,
|
||||
{"identity__isnull": True, "user__isnull": True},
|
||||
{"identity__isnull": True, "user__isnull": True, "domain__isnull": True},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
@ -106,7 +114,7 @@ class Config(models.Model):
|
|||
"""
|
||||
return cls.load_values(
|
||||
cls.UserOptions,
|
||||
{"identity__isnull": True, "user": user},
|
||||
{"identity__isnull": True, "user": user, "domain__isnull": True},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
@ -116,7 +124,7 @@ class Config(models.Model):
|
|||
"""
|
||||
return await sync_to_async(cls.load_values)(
|
||||
cls.UserOptions,
|
||||
{"identity__isnull": True, "user": user},
|
||||
{"identity__isnull": True, "user": user, "domain__isnull": True},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
@ -126,7 +134,7 @@ class Config(models.Model):
|
|||
"""
|
||||
return cls.load_values(
|
||||
cls.IdentityOptions,
|
||||
{"identity": identity, "user__isnull": True},
|
||||
{"identity": identity, "user__isnull": True, "domain__isnull": True},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
@ -136,7 +144,27 @@ class Config(models.Model):
|
|||
"""
|
||||
return await sync_to_async(cls.load_values)(
|
||||
cls.IdentityOptions,
|
||||
{"identity": identity, "user__isnull": True},
|
||||
{"identity": identity, "user__isnull": True, "domain__isnull": True},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def load_domain(cls, domain):
|
||||
"""
|
||||
Loads an domain config options object
|
||||
"""
|
||||
return cls.load_values(
|
||||
cls.DomainOptions,
|
||||
{"domain": domain, "user__isnull": True, "identity__isnull": True},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def aload_domain(cls, domain):
|
||||
"""
|
||||
Async loads an domain config options object
|
||||
"""
|
||||
return await sync_to_async(cls.load_values)(
|
||||
cls.DomainOptions,
|
||||
{"domain": domain, "user__isnull": True, "identity__isnull": True},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
@ -170,7 +198,7 @@ class Config(models.Model):
|
|||
key,
|
||||
value,
|
||||
cls.SystemOptions,
|
||||
{"identity__isnull": True, "user__isnull": True},
|
||||
{"identity__isnull": True, "user__isnull": True, "domain__isnull": True},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
@ -179,7 +207,7 @@ class Config(models.Model):
|
|||
key,
|
||||
value,
|
||||
cls.UserOptions,
|
||||
{"identity__isnull": True, "user": user},
|
||||
{"identity__isnull": True, "user": user, "domain__isnull": True},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
@ -188,7 +216,16 @@ class Config(models.Model):
|
|||
key,
|
||||
value,
|
||||
cls.IdentityOptions,
|
||||
{"identity": identity, "user__isnull": True},
|
||||
{"identity": identity, "user__isnull": True, "domain__isnull": True},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def set_domain(cls, domain, key, value):
|
||||
cls.set_value(
|
||||
key,
|
||||
value,
|
||||
cls.DomainOptions,
|
||||
{"domain": domain, "user__isnull": True, "identity__isnull": True},
|
||||
)
|
||||
|
||||
class SystemOptions(pydantic.BaseModel):
|
||||
|
@ -210,6 +247,7 @@ class Config(models.Model):
|
|||
policy_terms: str = ""
|
||||
policy_privacy: str = ""
|
||||
policy_rules: str = ""
|
||||
policy_issues: str = ""
|
||||
|
||||
signup_allowed: bool = True
|
||||
signup_text: str = ""
|
||||
|
@ -239,20 +277,23 @@ class Config(models.Model):
|
|||
custom_head: str | None
|
||||
|
||||
class UserOptions(pydantic.BaseModel):
|
||||
|
||||
pass
|
||||
light_theme: bool = False
|
||||
|
||||
class IdentityOptions(pydantic.BaseModel):
|
||||
|
||||
toot_mode: bool = False
|
||||
default_post_visibility: int = 0 # Post.Visibilities.public
|
||||
default_reply_visibility: int = 1 # Post.Visibilities.unlisted
|
||||
visible_follows: bool = True
|
||||
light_theme: bool = False
|
||||
search_enabled: bool = True
|
||||
|
||||
# wellness Options
|
||||
# Wellness Options
|
||||
visible_reaction_counts: bool = True
|
||||
expand_linked_cws: bool = True
|
||||
infinite_scroll: bool = True
|
||||
|
||||
custom_css: str | None
|
||||
class DomainOptions(pydantic.BaseModel):
|
||||
|
||||
site_name: str = ""
|
||||
site_icon: UploadedImage | None = None
|
||||
hide_login: bool = False
|
||||
custom_css: str = ""
|
||||
single_user: str = ""
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
import json
|
||||
from typing import ClassVar
|
||||
|
||||
import markdown_it
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.templatetags.static import static
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.views.decorators.cache import cache_control
|
||||
from django.views.generic import TemplateView, View
|
||||
from django.views.static import serve
|
||||
|
||||
|
@ -21,17 +18,18 @@ from core.models import Config
|
|||
def homepage(request):
|
||||
if request.user.is_authenticated:
|
||||
return Home.as_view()(request)
|
||||
elif request.domain.config_domain.single_user:
|
||||
return redirect(f"/@{request.domain.config_domain.single_user}/")
|
||||
else:
|
||||
return About.as_view()(request)
|
||||
|
||||
|
||||
@method_decorator(cache_page(public_only=True), name="dispatch")
|
||||
class About(TemplateView):
|
||||
|
||||
template_name = "about.html"
|
||||
|
||||
def get_context_data(self):
|
||||
service = TimelineService(self.request.identity)
|
||||
service = TimelineService(None)
|
||||
return {
|
||||
"current_page": "about",
|
||||
"content": mark_safe(
|
||||
|
@ -87,46 +85,6 @@ class RobotsTxt(TemplateView):
|
|||
}
|
||||
|
||||
|
||||
@method_decorator(cache_control(max_age=60 * 15), name="dispatch")
|
||||
class AppManifest(StaticContentView):
|
||||
"""
|
||||
Serves a PWA manifest file. This is a view as we want to drive some
|
||||
items from settings.
|
||||
|
||||
NOTE: If this view changes to need runtime Config, it should change from
|
||||
StaticContentView to View, otherwise the settings will only get
|
||||
picked up during boot time.
|
||||
"""
|
||||
|
||||
content_type = "application/json"
|
||||
|
||||
def get_static_content(self) -> str | bytes:
|
||||
return json.dumps(
|
||||
{
|
||||
"$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",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class FlatPage(TemplateView):
|
||||
"""
|
||||
Serves a "flat page" from a config option,
|
||||
|
|
102
docs/releases/0.9.rst
Normal file
102
docs/releases/0.9.rst
Normal file
|
@ -0,0 +1,102 @@
|
|||
0.9
|
||||
===
|
||||
|
||||
*Not yet released*
|
||||
|
||||
This release is a large overhaul Takahē that removes all timeline UI elements
|
||||
in the web interface in favour of apps, while reworking the remaining pages
|
||||
to be a pleasant profile viewing, post viewing, and settings experience.
|
||||
|
||||
We've also started on our path of making individual domains much more
|
||||
customisable; you can now theme them individually, the Local timeline is now
|
||||
domain-specific, and domains can be set to serve single user profiles.
|
||||
|
||||
This release's major changes:
|
||||
|
||||
* The Home, Notifications, Local and Federated timelines have been removed
|
||||
from the web UI. They still function for apps.
|
||||
|
||||
* The ability to like, boost, bookmark and reply to posts has been removed from
|
||||
the web UI. They still function for apps.
|
||||
|
||||
* The web Compose tool has been considerably simplified and relegated to a new
|
||||
"tools" section; most users should now use an app for composing posts.
|
||||
|
||||
* The Follows page is now in settings and is view-only.
|
||||
|
||||
* Identity profiles and individual post pages are now considerably simplified
|
||||
and have no sidebar.
|
||||
|
||||
* A Search feature is now available for posts from a single identity on its
|
||||
profile page; users can turn this on or off in their identity's profile
|
||||
settings.
|
||||
|
||||
* Domains can now have their own site name, site icon, and custom CSS
|
||||
|
||||
* Domains can be set to a "single user mode" where they redirect to a user
|
||||
profile, rather than showing their own homepage.
|
||||
|
||||
* Added an Authorized Apps identity settings screen, that allows seeing what apps you've
|
||||
authorized, revocation of app access, and generating your own personal API
|
||||
tokens.
|
||||
|
||||
* Added a Delete Profile settings screen that allows self-serve identity deletion.
|
||||
|
||||
* The logged-in homepage now shows a list of identities to select from as well
|
||||
as a set of recommended apps to use for timeline interfaces.
|
||||
|
||||
* We have totally dropped our alpha-quality SQLite support; it just doesn't have
|
||||
sufficient full-text-search and JSON operator support, unfortunately.
|
||||
|
||||
There are many minor changes to support the new direction; important ones include:
|
||||
|
||||
* The dark/light mode toggle is now a User (login) setting, not an Identity setting
|
||||
|
||||
* Identity selection is no longer part of a session - now, multiple identity
|
||||
settings pages can be opened at once.
|
||||
|
||||
* The ability for users to add their own custom CSS has been removed, as it
|
||||
was potentially confusing with our upcoming profile customization work (it
|
||||
only ever applied to your own session, and with timelines gone, it no longer
|
||||
makes much sense!)
|
||||
|
||||
* API pagination has been further improved, specifically for Elk compatibility
|
||||
|
||||
* Server admins can now add a "Report a Problem" footer link with either
|
||||
hosted content or an external link.
|
||||
|
||||
This is a large change in direction, and we hope that it will match the way
|
||||
that people use Takahē and its multi-domain support far better. For more
|
||||
discussion and rationale on the change, see `Andrew's blog post about it <https://aeracode.org/2023/04/29/refactor-treat/>`_.
|
||||
|
||||
Our future plans include stability and polish in order to get us to a 1.0 release,
|
||||
as well as allowing users to customize their profiles more, account import
|
||||
support, and protocol enhancements like automatic fetching of replies for
|
||||
non-local posts. If you're curious about what we're up to, or have an idea,
|
||||
we're very happy to chat about it in our Discord!
|
||||
|
||||
If you'd like to help with code, design, other areas, see
|
||||
:doc:`/contributing` to see how to get in touch.
|
||||
|
||||
You can download images from `Docker Hub <https://hub.docker.com/r/jointakahe/takahe>`_,
|
||||
or use the image name ``jointakahe/takahe:0.9``.
|
||||
|
||||
|
||||
Upgrade Notes
|
||||
-------------
|
||||
|
||||
Despite the large refactor to the UI, Takahē's internals are not significantly
|
||||
changed, and this upgrade is operationally like any other minor release.
|
||||
|
||||
Migrations
|
||||
~~~~~~~~~~
|
||||
|
||||
There are new database migrations; they are backwards-compatible, so please
|
||||
apply them before restarting your webservers and stator processes.
|
||||
|
||||
One of the migrations involves adding a large search index and may take some time to
|
||||
process (on the order of minutes) if you have a large database.
|
||||
|
||||
You may wish to bring your site down into
|
||||
a maintenance mode before applying it to reduce the chance of lock conflicts
|
||||
slowing things down, or causing request timeouts.
|
|
@ -9,7 +9,7 @@ django-hatchway~=0.5.1
|
|||
django-htmx~=1.13.0
|
||||
django-oauth-toolkit~=2.2.0
|
||||
django-storages[google,boto3]~=1.13.1
|
||||
django~=4.1
|
||||
django~=4.1.0
|
||||
email-validator~=1.3.0
|
||||
gunicorn~=20.1.0
|
||||
httpx~=0.23
|
||||
|
|
|
@ -82,13 +82,14 @@ td a {
|
|||
/* Base template styling */
|
||||
|
||||
:root {
|
||||
--color-highlight: #449c8c;
|
||||
|
||||
--color-bg-main: #26323c;
|
||||
--color-bg-menu: #2e3e4c;
|
||||
--color-bg-box: #1a2631;
|
||||
--color-bg-error: rgb(87, 32, 32);
|
||||
--color-highlight: #449c8c;
|
||||
--color-delete: #8b2821;
|
||||
--color-header-menu: rgba(0, 0, 0, 0.5);
|
||||
--color-header-menu: rgba(255, 255, 255, 0.8);
|
||||
--color-main-shadow: rgba(0, 0, 0, 0.6);
|
||||
--size-main-shadow: 50px;
|
||||
|
||||
|
@ -96,40 +97,36 @@ td a {
|
|||
--color-text-dull: #99a;
|
||||
--color-text-main: #fff;
|
||||
--color-text-link: var(--color-highlight);
|
||||
--color-text-in-highlight: #fff;
|
||||
--color-text-in-highlight: var(--color-text-main);
|
||||
|
||||
--color-input-background: #26323c;
|
||||
--color-input-background: var(--color-bg-main);
|
||||
--color-input-border: #000;
|
||||
--color-input-border-active: #444b5d;
|
||||
--color-button-secondary: #2e3e4c;
|
||||
--color-button-disabled: #7c9c97;
|
||||
|
||||
--sm-header-height: 50px;
|
||||
--sm-sidebar-width: 50px;
|
||||
|
||||
--md-sidebar-width: 250px;
|
||||
--md-header-height: 50px;
|
||||
--width-sidebar-small: 200px;
|
||||
--width-sidebar-medium: 250px;
|
||||
|
||||
--emoji-height: 1.1em;
|
||||
}
|
||||
|
||||
body.light-theme {
|
||||
--color-bg-main: #dfe3e7;
|
||||
--color-bg-menu: #cfd6dd;
|
||||
--color-bg-box: #f2f5f8;
|
||||
body.theme-light {
|
||||
--color-bg-main: #d4dee7;
|
||||
--color-bg-menu: #c0ccd8;
|
||||
--color-bg-box: #f0f3f5;
|
||||
--color-bg-error: rgb(219, 144, 144);
|
||||
--color-highlight: #449c8c;
|
||||
--color-delete: #884743;
|
||||
--color-header-menu: rgba(0, 0, 0, 0.1);
|
||||
--color-header-menu: rgba(0, 0, 0, 0.7);
|
||||
--color-main-shadow: rgba(0, 0, 0, 0.1);
|
||||
--size-main-shadow: 20px;
|
||||
|
||||
--color-text-duller: #3a3b3d;
|
||||
--color-text-dull: rgb(44, 44, 48);
|
||||
--color-text-duller: #4f5157;
|
||||
--color-text-dull: rgb(62, 62, 68);
|
||||
--color-text-main: rgb(0, 0, 0);
|
||||
--color-text-link: var(--color-highlight);
|
||||
|
||||
--color-input-background: #fff;
|
||||
--color-input-background: var(--color-bg-main);
|
||||
--color-input-border: rgb(109, 109, 109);
|
||||
--color-input-border-active: #464646;
|
||||
--color-button-secondary: #5c6770;
|
||||
|
@ -145,15 +142,14 @@ body {
|
|||
}
|
||||
|
||||
main {
|
||||
width: 1100px;
|
||||
width: 800px;
|
||||
margin: 20px auto;
|
||||
box-shadow: 0 0 var(--size-main-shadow) var(--color-main-shadow);
|
||||
box-shadow: none;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.no-sidebar main {
|
||||
box-shadow: none;
|
||||
max-width: 800px;
|
||||
body.wide main {
|
||||
width: 1100px;
|
||||
}
|
||||
|
||||
footer {
|
||||
|
@ -172,10 +168,8 @@ footer a {
|
|||
|
||||
header {
|
||||
display: flex;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.no-sidebar header {
|
||||
height: 42px;
|
||||
margin: -20px 0 20px 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
@ -184,11 +178,11 @@ header .logo {
|
|||
font-family: "Raleway";
|
||||
font-weight: bold;
|
||||
background: var(--color-highlight);
|
||||
border-radius: 5px 0 0 0;
|
||||
border-radius: 0 0 5px 5px;
|
||||
text-transform: lowercase;
|
||||
padding: 10px 11px 9px 10px;
|
||||
height: 50px;
|
||||
font-size: 130%;
|
||||
padding: 6px 8px 5px 7px;
|
||||
height: 42px;
|
||||
font-size: 120%;
|
||||
color: var(--color-text-in-highlight);
|
||||
border-bottom: 3px solid rgba(0, 0, 0, 0);
|
||||
z-index: 10;
|
||||
|
@ -196,10 +190,6 @@ header .logo {
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.no-sidebar header .logo {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
header .logo:hover {
|
||||
border-bottom: 3px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
@ -211,31 +201,25 @@ header .logo img {
|
|||
}
|
||||
|
||||
header menu {
|
||||
flex-grow: 1;
|
||||
flex-grow: 0;
|
||||
display: flex;
|
||||
list-style-type: none;
|
||||
justify-content: flex-start;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.no-sidebar header menu {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
header menu a {
|
||||
padding: 10px 20px 4px 20px;
|
||||
color: var(--color-text-main);
|
||||
padding: 6px 10px 4px 10px;
|
||||
color: var(--color-header-menu);
|
||||
line-height: 30px;
|
||||
border-bottom: 3px solid rgba(0, 0, 0, 0);
|
||||
margin: 0 2px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.no-sidebar header menu a {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
body.has-banner header menu a {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-right: 0;
|
||||
header menu a.logo {
|
||||
width: auto;
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
header menu a:hover,
|
||||
|
@ -243,10 +227,10 @@ header menu a.selected {
|
|||
border-bottom: 3px solid var(--color-highlight);
|
||||
}
|
||||
|
||||
.no-sidebar header menu a:hover:not(.logo) {
|
||||
header menu a:hover:not(.logo) {
|
||||
border-bottom: 3px solid rgba(0, 0, 0, 0);
|
||||
background-color: var(--color-bg-menu);
|
||||
border-radius: 5px;
|
||||
border-radius: 0 0 5px 5px;
|
||||
}
|
||||
|
||||
header menu a i {
|
||||
|
@ -279,26 +263,6 @@ header menu .gap {
|
|||
flex-grow: 1;
|
||||
}
|
||||
|
||||
header menu a.identity {
|
||||
border-right: 0;
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
background: var(--color-bg-menu) !important;
|
||||
border-radius: 0 5px 0 0;
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
header menu a.identity i {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 0 7px 2px 0;
|
||||
}
|
||||
|
||||
header menu a.identity a.view-profile {
|
||||
display: inline-block;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
header menu a img {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
|
@ -313,7 +277,8 @@ header menu a small {
|
|||
}
|
||||
|
||||
nav {
|
||||
padding: 10px 10px 20px 0;
|
||||
padding: 10px 0px 20px 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
nav hr {
|
||||
|
@ -334,23 +299,23 @@ nav h3:first-child {
|
|||
nav a {
|
||||
display: block;
|
||||
color: var(--color-text-dull);
|
||||
padding: 7px 18px 7px 13px;
|
||||
border-left: 3px solid transparent;
|
||||
padding: 7px 18px 7px 10px;
|
||||
border-right: 3px solid transparent;
|
||||
}
|
||||
|
||||
nav a.selected {
|
||||
color: var(--color-text-main);
|
||||
background: var(--color-bg-main);
|
||||
border-radius: 0 5px 5px 0;
|
||||
border-radius: 5px 0 0 5px;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
color: var(--color-text-main);
|
||||
border-left: 3px solid var(--color-highlight);
|
||||
border-right: 3px solid var(--color-highlight);
|
||||
}
|
||||
|
||||
nav a.selected:hover {
|
||||
border-left: 3px solid transparent;
|
||||
border-right: 3px solid transparent;
|
||||
}
|
||||
|
||||
nav a.danger {
|
||||
|
@ -368,48 +333,54 @@ nav a i {
|
|||
display: inline-block;
|
||||
}
|
||||
|
||||
nav .identity-banner {
|
||||
margin: 5px 0 10px 7px;
|
||||
}
|
||||
|
||||
nav .identity-banner img.icon {
|
||||
max-width: 32px;
|
||||
max-height: 32px;
|
||||
}
|
||||
|
||||
nav .identity-banner .avatar-link {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
nav .identity-banner .handle {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
nav .identity-banner div.link {
|
||||
color: var(--color-text-main);
|
||||
}
|
||||
|
||||
nav .identity-banner a,
|
||||
nav .identity-banner a:hover {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* Left-right columns */
|
||||
|
||||
.columns {
|
||||
.settings {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.left-column {
|
||||
.settings .settings-content {
|
||||
flex-grow: 1;
|
||||
width: 300px;
|
||||
max-width: 900px;
|
||||
padding: 15px;
|
||||
padding: 0 15px 15px 15px;
|
||||
}
|
||||
|
||||
.left-column h1 {
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.left-column h1 small {
|
||||
font-size: 60%;
|
||||
color: var(--color-text-dull);
|
||||
display: block;
|
||||
margin: -10px 0 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.left-column h2 {
|
||||
margin: 10px 0 10px 0;
|
||||
}
|
||||
|
||||
.left-column h3 {
|
||||
margin: 10px 0 0 0;
|
||||
}
|
||||
|
||||
.right-column {
|
||||
width: var(--md-sidebar-width);
|
||||
.settings nav {
|
||||
width: var(--width-sidebar-medium);
|
||||
background: var(--color-bg-menu);
|
||||
border-radius: 0 0 5px 0;
|
||||
}
|
||||
|
||||
.right-column h2 {
|
||||
.settings nav h2 {
|
||||
background: var(--color-highlight);
|
||||
color: var(--color-text-in-highlight);
|
||||
padding: 8px 10px;
|
||||
|
@ -418,12 +389,6 @@ nav a i {
|
|||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.right-column footer {
|
||||
padding: 0 10px 20px 10px;
|
||||
font-size: 90%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
img.emoji {
|
||||
height: var(--emoji-height);
|
||||
vertical-align: baseline;
|
||||
|
@ -433,27 +398,51 @@ img.emoji {
|
|||
content: "…";
|
||||
}
|
||||
|
||||
/* Generic markdown styling and sections */
|
||||
/* Generic styling and sections */
|
||||
|
||||
.no-sidebar section {
|
||||
max-width: 700px;
|
||||
section {
|
||||
background: var(--color-bg-box);
|
||||
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1);
|
||||
margin: 25px auto 45px auto;
|
||||
margin: 0 auto 45px auto;
|
||||
padding: 5px 15px;
|
||||
}
|
||||
|
||||
.no-sidebar section:last-of-type {
|
||||
margin-bottom: 10px;
|
||||
section:first-of-type {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.no-sidebar section.shell {
|
||||
section.invisible {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.no-sidebar #main-content>h1 {
|
||||
section h1.above {
|
||||
position: relative;
|
||||
top: -35px;
|
||||
left: -15px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
font-size: 120%;
|
||||
color: var(--color-text-main);
|
||||
margin-bottom: -20px;
|
||||
}
|
||||
|
||||
section p {
|
||||
margin: 5px 0 10px 0;
|
||||
}
|
||||
|
||||
section:last-of-type {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
section.shell {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#main-content>h1 {
|
||||
max-width: 700px;
|
||||
margin: 25px auto 5px auto;
|
||||
font-weight: bold;
|
||||
|
@ -462,7 +451,7 @@ img.emoji {
|
|||
color: var(--color-text-main);
|
||||
}
|
||||
|
||||
.no-sidebar h1+section {
|
||||
h1+section {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
|
@ -493,9 +482,7 @@ p.authorization-code {
|
|||
|
||||
.icon-menu .option {
|
||||
display: block;
|
||||
margin: 0 0 20px 0;
|
||||
background: var(--color-bg-box);
|
||||
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1);
|
||||
margin: 0 0 10px 0;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
padding: 10px 20px;
|
||||
|
@ -596,6 +583,40 @@ p.authorization-code {
|
|||
color: var(--color-text-dull);
|
||||
}
|
||||
|
||||
/* Icon/app listings */
|
||||
|
||||
.flex-icons {
|
||||
display: flex;
|
||||
list-style-type: none;
|
||||
margin: 20px 0 10px 0;
|
||||
padding: 0;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex-icons a {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
margin: 0 0 15px 0;
|
||||
}
|
||||
|
||||
.flex-icons a img {
|
||||
max-width: 64px;
|
||||
max-height: 64px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.flex-icons a h2 {
|
||||
margin: 7px 0 0 72px;
|
||||
font-size: 110%;
|
||||
}
|
||||
|
||||
.flex-icons a i {
|
||||
margin: 0 0 0 72px;
|
||||
font-size: 90%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Item tables */
|
||||
|
||||
table.items {
|
||||
|
@ -685,7 +706,7 @@ table.items td.actions a.danger:hover {
|
|||
|
||||
/* Forms */
|
||||
|
||||
.no-sidebar form {
|
||||
section form {
|
||||
max-width: 500px;
|
||||
margin: 40px auto;
|
||||
}
|
||||
|
@ -716,29 +737,11 @@ form.inline {
|
|||
|
||||
div.follow {
|
||||
float: right;
|
||||
margin: 20px 0 0 0;
|
||||
margin: 30px 0 0 0;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.follow-hashtag {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.follow.has-reverse {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.follow .reverse-follow {
|
||||
display: block;
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
|
||||
div.follow button,
|
||||
div.follow .button {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
div.follow .actions {
|
||||
/* display: flex; */
|
||||
position: relative;
|
||||
|
@ -748,69 +751,8 @@ div.follow .actions {
|
|||
align-content: center;
|
||||
}
|
||||
|
||||
div.follow .actions a {
|
||||
border-radius: 4px;
|
||||
min-width: 40px;
|
||||
.follow .actions button {
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
div.follow .actions menu {
|
||||
display: none;
|
||||
background-color: var(--color-bg-menu);
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 43px;
|
||||
}
|
||||
|
||||
|
||||
div.follow .actions menu.enabled {
|
||||
display: block;
|
||||
min-width: 160px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
div.follow .actions menu a {
|
||||
text-align: left;
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
padding: 4px 10px;
|
||||
color: var(--color-text-dull);
|
||||
}
|
||||
|
||||
.follow .actions menu button {
|
||||
background: none !important;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
padding: 4px 10px;
|
||||
color: var(--color-text-dull);
|
||||
}
|
||||
|
||||
.follow .actions menu button i {
|
||||
margin-right: 4px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.follow .actions button:hover {
|
||||
color: var(--color-text-main);
|
||||
}
|
||||
|
||||
.follow .actions menu a i {
|
||||
margin-right: 4px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
div.follow .actions a:hover {
|
||||
color: var(--color-text-main);
|
||||
}
|
||||
|
||||
div.follow .actions a.active {
|
||||
color: var(--color-text-link);
|
||||
}
|
||||
|
||||
form.inline-menu {
|
||||
|
@ -1118,12 +1060,9 @@ button i:first-child,
|
|||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
form .field.multi-option {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
form .field.multi-option {
|
||||
form .multi-option {
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
form .option.option-row {
|
||||
|
@ -1156,6 +1095,25 @@ blockquote {
|
|||
border-left: 2px solid var(--color-bg-menu);
|
||||
}
|
||||
|
||||
.secret .label {
|
||||
background-color: var(--color-bg-menu);
|
||||
padding: 3px 7px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.secret.visible .label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.secret .value {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.secret.visible .value {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
|
||||
/* Logged out homepage */
|
||||
|
||||
|
@ -1174,42 +1132,48 @@ blockquote {
|
|||
|
||||
/* Identities */
|
||||
|
||||
h1.identity {
|
||||
margin: 0 0 20px 0;
|
||||
section.identity {
|
||||
overflow: hidden;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
h1.identity .banner {
|
||||
section.identity .banner {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
width: calc(100% + 30px);
|
||||
margin: -65px -15px 20px -15px;
|
||||
margin: -5px -15px 0px -15px;
|
||||
border-radius: 5px 0 0 0;
|
||||
}
|
||||
|
||||
h1.identity .icon {
|
||||
section.identity .icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
float: left;
|
||||
margin: 0 20px 0 0;
|
||||
margin: 15px 20px 15px 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
h1.identity .emoji {
|
||||
section.identity .emoji {
|
||||
height: var(--emoji-height);
|
||||
}
|
||||
|
||||
h1.identity small {
|
||||
section.identity h1 {
|
||||
margin: 25px 0 0 0;
|
||||
}
|
||||
|
||||
section.identity small {
|
||||
display: block;
|
||||
font-size: 60%;
|
||||
font-size: 100%;
|
||||
font-weight: normal;
|
||||
color: var(--color-text-dull);
|
||||
margin: -5px 0 0 0;
|
||||
}
|
||||
|
||||
.bio {
|
||||
margin: 0 0 20px 0;
|
||||
section.identity .bio {
|
||||
clear: left;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.bio .emoji {
|
||||
|
@ -1220,14 +1184,19 @@ h1.identity small {
|
|||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.identity-metadata {
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.identity-metadata .metadata-pair {
|
||||
display: block;
|
||||
margin: 0px 0 10px 0;
|
||||
background: var(--color-bg-box);
|
||||
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1);
|
||||
display: inline-block;
|
||||
width: 300px;
|
||||
margin: 0px 0 5px 0;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
padding: 10px 20px;
|
||||
border: 2px solid rgba(255, 255, 255, 0);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
|
@ -1235,24 +1204,18 @@ h1.identity small {
|
|||
|
||||
.identity-metadata .metadata-pair .metadata-name {
|
||||
display: inline-block;
|
||||
min-width: 80px;
|
||||
min-width: 90px;
|
||||
margin-right: 15px;
|
||||
text-align: right;
|
||||
color: var(--color-text-dull);
|
||||
}
|
||||
|
||||
.identity-metadata .metadata-pair .metadata-name::after {
|
||||
padding-left: 3px;
|
||||
|
||||
color: var(--color-text-dull);
|
||||
}
|
||||
|
||||
.system-note {
|
||||
background: var(--color-bg-menu);
|
||||
color: var(--color-text-dull);
|
||||
border-radius: 3px;
|
||||
padding: 5px 8px;
|
||||
margin: 15px 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.system-note a {
|
||||
|
@ -1321,13 +1284,12 @@ table.metadata td .emoji {
|
|||
}
|
||||
|
||||
.view-options {
|
||||
margin: 0 0 10px 0px;
|
||||
margin-bottom: 10px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.view-options.follows {
|
||||
margin: 0 0 20px 0px;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.view-options a:not(.button) {
|
||||
|
@ -1358,17 +1320,23 @@ table.metadata td .emoji {
|
|||
min-width: 16px;
|
||||
}
|
||||
|
||||
/* Announcements */
|
||||
/* Announcements/Flash messages */
|
||||
|
||||
.announcement {
|
||||
.announcement,
|
||||
.message {
|
||||
background-color: var(--color-highlight);
|
||||
border-radius: 5px;
|
||||
margin: 0 0 20px 0;
|
||||
margin: 10px 0 0 0;
|
||||
padding: 5px 30px 5px 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.announcement .dismiss {
|
||||
.message {
|
||||
background-color: var(--color-bg-menu);
|
||||
}
|
||||
|
||||
.announcement .dismiss,
|
||||
.message .dismiss {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 10px;
|
||||
|
@ -1405,6 +1373,11 @@ table.metadata td .emoji {
|
|||
}
|
||||
|
||||
|
||||
.identity-banner img.icon {
|
||||
max-width: 64px;
|
||||
max-height: 64px;
|
||||
}
|
||||
|
||||
|
||||
/* Posts */
|
||||
|
||||
|
@ -1577,7 +1550,7 @@ form .post {
|
|||
.post .actions {
|
||||
display: flex;
|
||||
position: relative;
|
||||
justify-content: space-between;
|
||||
justify-content: right;
|
||||
padding: 8px 0 0 0;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
|
@ -1593,6 +1566,12 @@ form .post {
|
|||
color: var(--color-text-dull);
|
||||
}
|
||||
|
||||
.post .actions a.no-action:hover {
|
||||
background-color: transparent;
|
||||
cursor: default;
|
||||
color: var(--color-text-dull);
|
||||
}
|
||||
|
||||
.post .actions a:hover {
|
||||
background-color: var(--color-bg-main);
|
||||
}
|
||||
|
@ -1750,52 +1729,30 @@ form .post {
|
|||
color: var(--color-text-dull);
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
main {
|
||||
max-width: 900px;
|
||||
@media (max-width: 1120px),
|
||||
(display-mode: standalone) {
|
||||
|
||||
body.wide main {
|
||||
width: 100%;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 920px),
|
||||
@media (max-width: 850px),
|
||||
(display-mode: standalone) {
|
||||
.left-column {
|
||||
margin: var(--md-header-height) var(--md-sidebar-width) 0 0;
|
||||
}
|
||||
|
||||
.right-column {
|
||||
width: var(--md-sidebar-width);
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
right: 0;
|
||||
top: var(--md-header-height);
|
||||
overflow-y: auto;
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
.right-column nav {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body:not(.no-sidebar) header {
|
||||
height: var(--md-header-height);
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
body:not(.no-sidebar) header menu a {
|
||||
background: var(--color-header-menu);
|
||||
}
|
||||
|
||||
main {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
margin: 20px auto 20px auto;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.no-sidebar main {
|
||||
margin: 20px auto 20px auto;
|
||||
.settings nav {
|
||||
width: var(--width-sidebar-small);
|
||||
}
|
||||
|
||||
.post .attachments a.image img {
|
||||
|
@ -1805,72 +1762,6 @@ form .post {
|
|||
|
||||
|
||||
@media (max-width: 750px) {
|
||||
header {
|
||||
height: var(--sm-header-height);
|
||||
}
|
||||
|
||||
header menu a.identity {
|
||||
width: var(--sm-sidebar-width);
|
||||
padding: 10px 10px 0 0;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
header menu a.identity i {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
#main-content {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.left-column {
|
||||
margin: var(--sm-header-height) var(--sm-sidebar-width) 0 0;
|
||||
}
|
||||
|
||||
.right-column {
|
||||
width: var(--sm-sidebar-width);
|
||||
top: var(--sm-header-height);
|
||||
}
|
||||
|
||||
.right-column nav {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.right-column nav hr {
|
||||
display: block;
|
||||
color: var(--color-text-dull);
|
||||
margin: 20px 10px;
|
||||
}
|
||||
|
||||
.right-column nav a {
|
||||
padding: 10px 0 10px 10px;
|
||||
}
|
||||
|
||||
.right-column nav a i {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.right-column nav a .fa-solid {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.right-column nav a span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.right-column h3 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.right-column h2,
|
||||
.right-column .compose {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.right-column footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.post {
|
||||
margin-bottom: 15px;
|
||||
|
@ -1881,6 +1772,10 @@ form .post {
|
|||
|
||||
@media (max-width: 550px) {
|
||||
|
||||
main {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.post .content,
|
||||
.post .summary,
|
||||
.post .edited,
|
||||
|
|
42
static/img/apps/elk.svg
Executable file
42
static/img/apps/elk.svg
Executable file
|
@ -0,0 +1,42 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="250"
|
||||
height="250"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg30"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs34" />
|
||||
<mask
|
||||
id="a"
|
||||
width="240"
|
||||
height="234"
|
||||
x="4"
|
||||
y="1"
|
||||
maskUnits="userSpaceOnUse"
|
||||
style="mask-type:alpha">
|
||||
<path
|
||||
fill="#D9D9D9"
|
||||
d="M244 123c0 64.617-38.383 112-103 112-64.617 0-103-30.883-103-95.5C38 111.194-8.729 36.236 8 16 29.46-9.959 88.689 6 125 6c64.617 0 119 52.383 119 117Z"
|
||||
id="path19" />
|
||||
</mask>
|
||||
<g
|
||||
mask="url(#a)"
|
||||
id="g28"
|
||||
transform="matrix(0.90923731,0,0,1.0049564,13.520015,-3.1040835)">
|
||||
<path
|
||||
fill="#ea9e44"
|
||||
d="m 116.94,88.1 c -13.344,1.552 -20.436,-2.019 -24.706,10.71 0,0 14.336,21.655 52.54,21.112 -2.135,8.848 -1.144,15.368 -1.144,23.207 0,26.079 -20.589,48.821 -65.961,48.821 -23.03,0 -51.015,4.191 -72.367,15.911 -15.175,8.305 -27.048,20.336 -32.302,37.023 l 5.956,8.461 11.4,0.155 v 47.889 l -13.91,21.966 3.998,63.645 H -6.364 L -5.22,335.773 C 1.338,331.892 16.36,321.802 29.171,306.279 46.557,285.4 59.902,255.052 44.193,217.486 l 11.744,-5.045 c 12.887,30.814 8.388,57.514 -2.898,79.013 21.58,-0.698 40.11,-2.095 55.819,-4.734 l -3.584,-43.698 12.659,-1.087 L 129.98,387 h 13.116 l 2.212,-94.459 c 10.447,-4.502 34.239,-21.034 45.372,-78.47 1.372,-6.986 2.135,-12.885 2.516,-17.93 1.754,-12.806 2.745,-27.243 3.051,-43.698 l -18.683,-5.976 h 57.42 l 5.567,-12.807 c -5.414,0.233 -11.896,-2.639 -11.896,-2.639 l 1.297,-6.209 H 242 L 176.801,90.428 c -7.244,2.794 -14.87,6.442 -20.208,10.866 -4.27,-3.105 -19.063,-12.807 -39.653,-13.195 z"
|
||||
id="path22" />
|
||||
<path
|
||||
fill="#c16929"
|
||||
d="M 6.217,24.493 18.494,21 c 5.948,21.577 13.345,33.375 22.648,39.352 8.388,5.099 19.75,5.239 31.799,4.579 C 69.433,63.767 66.154,62.137 63.104,59.886 56.317,54.841 50.522,46.458 46.175,31.246 l 12.201,-3.649 c 3.279,11.488 7.092,18.085 12.201,21.888 5.11,3.726 11.286,4.657 18.606,5.433 13.726,1.553 30.884,2.174 52.312,12.264 2.898,1.086 5.872,2.483 8.769,4.036 -0.381,-0.776 -0.762,-1.553 -1.296,-2.406 -3.66,-5.822 -10.828,-11.953 -24.097,-16.92 l 4.27,-12.109 c 21.581,7.917 30.121,19.171 33.553,28.097 3.965,10.168 1.525,18.124 1.525,18.124 -3.05,1.009 -6.1,2.406 -9.608,3.492 -6.634,-4.579 -12.887,-8.033 -18.835,-10.75 C 113.814,70.442 92.31,76.108 73.246,77.893 58.91,79.213 45.794,78.591 34.432,71.295 23.222,64.155 13.385,50.495 6.217,24.493 Z"
|
||||
id="path24" />
|
||||
<path
|
||||
fill="#c16929"
|
||||
d="M 90.098,45.294 C 87.582,39.55 86.057,32.487 86.743,23.794 l 12.659,0.932 c -0.763,10.555 2.897,17.696 7.015,22.353 -5.338,-0.931 -10.447,-1.04 -16.319,-1.785 z m 80.069,-1.32 8.312,-9.702 c 21.58,19.094 8.159,46.415 8.159,46.415 l -11.819,-1.32 c -0.382,-6.24 -1.144,-17.836 -6.635,-24.371 3.584,1.84 6.635,3.865 9.99,6.908 0,-5.666 -1.754,-12.341 -8.007,-17.93 z"
|
||||
id="path26" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.9 KiB |
BIN
static/img/apps/ivory.webp
Executable file
BIN
static/img/apps/ivory.webp
Executable file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
static/img/apps/tusky.png
Executable file
BIN
static/img/apps/tusky.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
|
@ -5,6 +5,7 @@ import signal
|
|||
import time
|
||||
import traceback
|
||||
import uuid
|
||||
from collections.abc import Callable
|
||||
|
||||
from asgiref.sync import async_to_sync, sync_to_async
|
||||
from django.conf import settings
|
||||
|
@ -21,7 +22,7 @@ class LoopingTask:
|
|||
copy running at a time.
|
||||
"""
|
||||
|
||||
def __init__(self, callable):
|
||||
def __init__(self, callable: Callable):
|
||||
self.callable = callable
|
||||
self.task: asyncio.Task | None = None
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
__version__ = "0.8.0"
|
||||
__version__ = "0.9.0-dev"
|
||||
|
|
|
@ -194,6 +194,7 @@ INSTALLED_APPS = [
|
|||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"django.contrib.postgres",
|
||||
"corsheaders",
|
||||
"django_htmx",
|
||||
"hatchway",
|
||||
|
@ -220,7 +221,7 @@ MIDDLEWARE = [
|
|||
"core.middleware.HeadersMiddleware",
|
||||
"core.middleware.ConfigLoadingMiddleware",
|
||||
"api.middleware.ApiTokenMiddleware",
|
||||
"users.middleware.IdentityMiddleware",
|
||||
"users.middleware.DomainMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "takahe.urls"
|
||||
|
|
127
takahe/urls.py
127
takahe/urls.py
|
@ -2,49 +2,18 @@ from django.conf import settings as djsettings
|
|||
from django.contrib import admin as djadmin
|
||||
from django.urls import include, path, re_path
|
||||
|
||||
from activities.views import (
|
||||
compose,
|
||||
debug,
|
||||
explore,
|
||||
follows,
|
||||
hashtags,
|
||||
posts,
|
||||
search,
|
||||
timelines,
|
||||
)
|
||||
from activities.views import compose, debug, posts, timelines
|
||||
from api.views import oauth
|
||||
from core import views as core
|
||||
from mediaproxy import views as mediaproxy
|
||||
from stator import views as stator
|
||||
from users.views import (
|
||||
activitypub,
|
||||
admin,
|
||||
announcements,
|
||||
auth,
|
||||
identity,
|
||||
report,
|
||||
settings,
|
||||
)
|
||||
from users.views import activitypub, admin, announcements, auth, identity, settings
|
||||
|
||||
urlpatterns = [
|
||||
path("", core.homepage),
|
||||
path("robots.txt", core.RobotsTxt.as_view()),
|
||||
path("manifest.json", core.AppManifest.as_view()),
|
||||
# Activity views
|
||||
path("notifications/", timelines.Notifications.as_view(), name="notifications"),
|
||||
path("local/", timelines.Local.as_view(), name="local"),
|
||||
path("federated/", timelines.Federated.as_view(), name="federated"),
|
||||
path("search/", search.Search.as_view(), name="search"),
|
||||
path("tags/<hashtag>/", timelines.Tag.as_view(), name="tag"),
|
||||
path("tags/<hashtag>/follow/", hashtags.HashtagFollow.as_view()),
|
||||
path("tags/<hashtag>/unfollow/", hashtags.HashtagFollow.as_view(undo=True)),
|
||||
path("explore/", explore.Explore.as_view(), name="explore"),
|
||||
path("explore/tags/", explore.ExploreTag.as_view(), name="explore-tag"),
|
||||
path(
|
||||
"follows/",
|
||||
follows.Follows.as_view(),
|
||||
name="follows",
|
||||
),
|
||||
# Settings views
|
||||
path(
|
||||
"settings/",
|
||||
|
@ -56,31 +25,66 @@ urlpatterns = [
|
|||
settings.SecurityPage.as_view(),
|
||||
name="settings_security",
|
||||
),
|
||||
path(
|
||||
"settings/profile/",
|
||||
settings.ProfilePage.as_view(),
|
||||
name="settings_profile",
|
||||
),
|
||||
path(
|
||||
"settings/interface/",
|
||||
settings.InterfacePage.as_view(),
|
||||
name="settings_interface",
|
||||
),
|
||||
path(
|
||||
"settings/import_export/",
|
||||
"@<handle>/settings/",
|
||||
settings.SettingsRoot.as_view(),
|
||||
name="settings",
|
||||
),
|
||||
path(
|
||||
"@<handle>/settings/profile/",
|
||||
settings.ProfilePage.as_view(),
|
||||
name="settings_profile",
|
||||
),
|
||||
path(
|
||||
"@<handle>/settings/posting/",
|
||||
settings.PostingPage.as_view(),
|
||||
name="settings_posting",
|
||||
),
|
||||
path(
|
||||
"@<handle>/settings/follows/",
|
||||
settings.FollowsPage.as_view(),
|
||||
name="settings_follows",
|
||||
),
|
||||
path(
|
||||
"@<handle>/settings/import_export/",
|
||||
settings.ImportExportPage.as_view(),
|
||||
name="settings_import_export",
|
||||
),
|
||||
path(
|
||||
"settings/import_export/following.csv",
|
||||
"@<handle>/settings/import_export/following.csv",
|
||||
settings.CsvFollowing.as_view(),
|
||||
name="settings_export_following_csv",
|
||||
),
|
||||
path(
|
||||
"settings/import_export/followers.csv",
|
||||
"@<handle>/settings/import_export/followers.csv",
|
||||
settings.CsvFollowers.as_view(),
|
||||
name="settings_export_followers_csv",
|
||||
),
|
||||
path(
|
||||
"@<handle>/settings/tokens/",
|
||||
settings.TokensRoot.as_view(),
|
||||
name="settings_tokens",
|
||||
),
|
||||
path(
|
||||
"@<handle>/settings/tokens/create/",
|
||||
settings.TokenCreate.as_view(),
|
||||
name="settings_token_create",
|
||||
),
|
||||
path(
|
||||
"@<handle>/settings/tokens/<pk>/",
|
||||
settings.TokenEdit.as_view(),
|
||||
name="settings_token_edit",
|
||||
),
|
||||
path(
|
||||
"@<handle>/settings/delete/",
|
||||
settings.DeleteIdentity.as_view(),
|
||||
name="settings_delete",
|
||||
),
|
||||
path(
|
||||
"admin/",
|
||||
admin.AdminRoot.as_view(),
|
||||
|
@ -236,30 +240,18 @@ urlpatterns = [
|
|||
path("@<handle>/", identity.ViewIdentity.as_view()),
|
||||
path("@<handle>/inbox/", activitypub.Inbox.as_view()),
|
||||
path("@<handle>/outbox/", activitypub.Outbox.as_view()),
|
||||
path("@<handle>/action/", identity.ActionIdentity.as_view()),
|
||||
path("@<handle>/rss/", identity.IdentityFeed()),
|
||||
path("@<handle>/report/", report.SubmitReport.as_view()),
|
||||
path("@<handle>/following/", identity.IdentityFollows.as_view(inbound=False)),
|
||||
path("@<handle>/followers/", identity.IdentityFollows.as_view(inbound=True)),
|
||||
path("@<handle>/search/", identity.IdentitySearch.as_view()),
|
||||
path(
|
||||
"@<handle>/notifications/",
|
||||
timelines.Notifications.as_view(),
|
||||
name="notifications",
|
||||
),
|
||||
# Posts
|
||||
path("compose/", compose.Compose.as_view(), name="compose"),
|
||||
path(
|
||||
"compose/image_upload/",
|
||||
compose.ImageUpload.as_view(),
|
||||
name="compose_image_upload",
|
||||
),
|
||||
path("@<handle>/compose/", compose.Compose.as_view(), name="compose"),
|
||||
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>/unlike/", posts.Like.as_view(undo=True)),
|
||||
path("@<handle>/posts/<int:post_id>/boost/", posts.Boost.as_view()),
|
||||
path("@<handle>/posts/<int:post_id>/unboost/", posts.Boost.as_view(undo=True)),
|
||||
path("@<handle>/posts/<int:post_id>/bookmark/", posts.Bookmark.as_view()),
|
||||
path(
|
||||
"@<handle>/posts/<int:post_id>/unbookmark/", posts.Bookmark.as_view(undo=True)
|
||||
),
|
||||
path("@<handle>/posts/<int:post_id>/delete/", posts.Delete.as_view()),
|
||||
path("@<handle>/posts/<int:post_id>/report/", report.SubmitReport.as_view()),
|
||||
path("@<handle>/posts/<int:post_id>/edit/", compose.Compose.as_view()),
|
||||
# Authentication
|
||||
path("auth/login/", auth.Login.as_view(), name="login"),
|
||||
path("auth/logout/", auth.Logout.as_view(), name="logout"),
|
||||
|
@ -267,26 +259,29 @@ urlpatterns = [
|
|||
path("auth/signup/<token>/", auth.Signup.as_view(), name="signup"),
|
||||
path("auth/reset/", auth.TriggerReset.as_view(), name="trigger_reset"),
|
||||
path("auth/reset/<token>/", auth.PerformReset.as_view(), name="password_reset"),
|
||||
# Identity selection
|
||||
path("@<handle>/activate/", identity.ActivateIdentity.as_view()),
|
||||
path("identity/select/", identity.SelectIdentity.as_view(), name="identity_select"),
|
||||
# Identity handling
|
||||
path("identity/create/", identity.CreateIdentity.as_view(), name="identity_create"),
|
||||
# Flat pages
|
||||
path("about/", core.About.as_view(), name="about"),
|
||||
path(
|
||||
"pages/privacy/",
|
||||
core.FlatPage.as_view(title="Privacy Policy", config_option="policy_privacy"),
|
||||
name="privacy",
|
||||
name="policy_privacy",
|
||||
),
|
||||
path(
|
||||
"pages/terms/",
|
||||
core.FlatPage.as_view(title="Terms of Service", config_option="policy_terms"),
|
||||
name="terms",
|
||||
name="policy_terms",
|
||||
),
|
||||
path(
|
||||
"pages/rules/",
|
||||
core.FlatPage.as_view(title="Server Rules", config_option="policy_rules"),
|
||||
name="rules",
|
||||
name="policy_rules",
|
||||
),
|
||||
path(
|
||||
"pages/issues/",
|
||||
core.FlatPage.as_view(title="Report a Problem", config_option="policy_issues"),
|
||||
name="policy_issues",
|
||||
),
|
||||
# Annoucements
|
||||
path("announcements/<id>/dismiss/", announcements.AnnouncementDismiss.as_view()),
|
||||
|
|
|
@ -4,3 +4,13 @@
|
|||
{{ announcement.html }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if messages %}
|
||||
<ul class="messages">
|
||||
{% for message in messages %}
|
||||
<div class="message {{ message.tags }}">
|
||||
<a class="dismiss" title="Dismiss" _="on click remove closest <div/>"><i class="fa-solid fa-xmark"></i></a>
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<footer>
|
||||
{% if config.site_about %}<a href="{% url "about" %}">About</a>{% endif %}
|
||||
{% if config.policy_rules %}<a href="{% url "rules" %}">Server Rules</a>{% endif %}
|
||||
{% if config.policy_terms %}<a href="{% url "terms" %}">Terms of Service</a>{% endif %}
|
||||
{% if config.policy_privacy %}<a href="{% url "privacy" %}">Privacy Policy</a>{% endif %}
|
||||
{% if config.policy_rules %}<a href="{% url "policy_rules" %}">Server Rules</a>{% endif %}
|
||||
{% if config.policy_terms %}<a href="{% url "policy_terms" %}">Terms of Service</a>{% endif %}
|
||||
{% if config.policy_privacy %}<a href="{% url "policy_privacy" %}">Privacy Policy</a>{% endif %}
|
||||
{% if config.policy_issues %}<a href="{% url "policy_issues" %}">Report a Problem</a>{% endif %}
|
||||
<a href="https://jointakahe.org">Takahē {{ config.version }}</a>
|
||||
</footer>
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
{% if post.pk in bookmarks %}
|
||||
<a title="Unbookmark" class="active" hx-trigger="click, keyup[key=='Enter']" hx-post="{{ post.urls.action_unbookmark }}" hx-swap="outerHTML" tabindex="0">
|
||||
<i class="fa-solid fa-bookmark"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a title="Bookmark" hx-trigger="click, keyup[key=='Enter']" hx-post="{{ post.urls.action_bookmark }}" hx-swap="outerHTML" tabindex="0">
|
||||
<i class="fa-regular fa-bookmark"></i>
|
||||
</a>
|
||||
{% endif %}
|
|
@ -1,11 +0,0 @@
|
|||
{% if post.pk in interactions.boost %}
|
||||
<a title="Unboost" class="active" hx-trigger="click, keyup[key=='Enter']" hx-post="{{ post.urls.action_unboost }}" hx-swap="outerHTML" tabindex="0">
|
||||
<i class="fa-solid fa-retweet"></i>
|
||||
<span class="like-count">{{ post.stats_with_defaults.boosts }}</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<a title="Boost" hx-trigger="click, keyup[key=='Enter']" hx-post="{{ post.urls.action_boost }}" hx-swap="outerHTML" tabindex="0">
|
||||
<i class="fa-solid fa-retweet"></i>
|
||||
<span class="like-count">{{ post.stats_with_defaults.boosts }}</span>
|
||||
</a>
|
||||
{% endif %}
|
|
@ -1,11 +0,0 @@
|
|||
<a class="option" href="{{ hashtag.urls.timeline }}">
|
||||
<i class="fa-solid fa-hashtag"></i>
|
||||
<span class="handle">
|
||||
{{ hashtag.display_name }}
|
||||
</span>
|
||||
{% if not hide_stats %}
|
||||
<span>
|
||||
Post count: {{ hashtag.stats.total }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</a>
|
|
@ -1,9 +0,0 @@
|
|||
{% if follow %}
|
||||
<button title="Unfollow" class="active" hx-trigger="click, keyup[key=='Enter']" hx-post="{{ hashtag.urls.unfollow }}" hx-swap="outerHTML" tabindex="0">
|
||||
Unfollow
|
||||
</button>
|
||||
{% else %}
|
||||
<button title="Follow" hx-trigger="click, keyup[key=='Enter']" hx-post="{{ hashtag.urls.follow }}" hx-swap="outerHTML" tabindex="0">
|
||||
Follow
|
||||
</button>
|
||||
{% endif %}
|
|
@ -1,15 +0,0 @@
|
|||
<form
|
||||
hx-encoding='multipart/form-data'
|
||||
hx-post='{% url "compose_image_upload" %}'
|
||||
hx-target="this"
|
||||
hx-swap="outerHTML"
|
||||
_="on htmx:xhr:progress(loaded, total)
|
||||
set #attachmentProgress.value to (loaded/total)*100">
|
||||
{% csrf_token %}
|
||||
{% include "forms/_field.html" with field=form.image %}
|
||||
{% include "forms/_field.html" with field=form.description %}
|
||||
<div class="buttons">
|
||||
<button id="upload" _="on click show #attachmentProgress with display:block then hide me">Upload</button>
|
||||
<progress id="attachmentProgress" value="0" max="100"></progress>
|
||||
</div>
|
||||
</form>
|
|
@ -1,19 +0,0 @@
|
|||
<div class="uploaded-image">
|
||||
<input type="hidden" name="attachment" value="{{ attachment.pk }}">
|
||||
<img src="{{ attachment.thumbnail_url.relative }}">
|
||||
<p>
|
||||
{{ attachment.name|default:"(no description)" }}
|
||||
</p>
|
||||
<div class="buttons">
|
||||
<button class="button delete left" _="on click remove closest .uploaded-image">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
{% if request.htmx %}
|
||||
<button class="add-image"
|
||||
hx-get='{% url "compose_image_upload" %}'
|
||||
hx-target="this"
|
||||
hx-swap="outerHTML"
|
||||
_="on load if length of <.uploaded-image/> > 3 then hide me">
|
||||
Add Image
|
||||
</button>
|
||||
{% endif %}
|
|
@ -1,11 +0,0 @@
|
|||
{% if post.pk in interactions.like %}
|
||||
<a title="Unlike" class="active" hx-trigger="click, keyup[key=='Enter']" hx-post="{{ post.urls.action_unlike }}" hx-swap="outerHTML" role="menuitem" tabindex="0">
|
||||
<i class="fa-solid fa-star"></i>
|
||||
<span class="like-count">{{ post.stats_with_defaults.likes }}</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<a title="Like" hx-trigger="click, keyup[key=='Enter']" hx-post="{{ post.urls.action_like }}" hx-swap="outerHTML" role="menuitem" tabindex="0">
|
||||
<i class="fa-solid fa-star"></i>
|
||||
<span class="like-count">{{ post.stats_with_defaults.likes }}</span>
|
||||
</a>
|
||||
{% endif %}
|
|
@ -1,86 +0,0 @@
|
|||
<nav>
|
||||
<a href="/" {% if current_page == "home" %}class="selected"{% endif %} title="Home">
|
||||
<i class="fa-solid fa-home"></i>
|
||||
<span>Home</span>
|
||||
</a>
|
||||
{% if request.user.is_authenticated %}
|
||||
<a href="{% url "notifications" %}" {% if current_page == "notifications" %}class="selected"{% endif %} title="Notifications">
|
||||
<i class="fa-solid fa-at"></i>
|
||||
<span>Notifications</span>
|
||||
</a>
|
||||
{% comment %}
|
||||
Not sure we want to show this quite yet
|
||||
<a href="{% url "explore" %}" {% if current_page == "explore" %}class="selected"{% endif %} title="Explore">
|
||||
<i class="fa-solid fa-hashtag"></i>
|
||||
<span>Explore</span>
|
||||
</a>
|
||||
{% endcomment %}
|
||||
<a href="{% url "local" %}" {% if current_page == "local" %}class="selected"{% endif %} title="Local">
|
||||
<i class="fa-solid fa-city"></i>
|
||||
<span>Local</span>
|
||||
</a>
|
||||
<a href="{% url "federated" %}" {% if current_page == "federated" %}class="selected"{% endif %} title="Federated">
|
||||
<i class="fa-solid fa-globe"></i>
|
||||
<span>Federated</span>
|
||||
</a>
|
||||
<a href="{% url "follows" %}" {% if section == "follows" %}class="selected"{% endif %} title="Follows">
|
||||
<i class="fa-solid fa-arrow-right-arrow-left"></i>
|
||||
<span>Follows</span>
|
||||
</a>
|
||||
<hr>
|
||||
<h3></h3>
|
||||
<a href="{% url "compose" %}" {% if top_section == "compose" %}class="selected"{% endif %} title="Compose">
|
||||
<i class="fa-solid fa-feather"></i>
|
||||
<span>Compose</span>
|
||||
</a>
|
||||
<a href="{% url "search" %}" {% if top_section == "search" %}class="selected"{% endif %} title="Search">
|
||||
<i class="fa-solid fa-search"></i>
|
||||
<span>Search</span>
|
||||
</a>
|
||||
{% if current_page == "tag" %}
|
||||
<a href="{% url "tag" hashtag.hashtag %}" class="selected" title="Tag {{ hashtag.display_name }}">
|
||||
<i class="fa-solid fa-hashtag"></i>
|
||||
<span>{{ hashtag.display_name }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url "settings" %}" {% if top_section == "settings" %}class="selected"{% endif %} title="Settings">
|
||||
<i class="fa-solid fa-gear"></i>
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
<a href="{% url "identity_select" %}" title="Select Identity">
|
||||
<i class="fa-solid fa-users-viewfinder"></i>
|
||||
<span>Select Identity</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url "local" %}" {% if current_page == "local" %}class="selected"{% endif %} title="Local Posts">
|
||||
<i class="fa-solid fa-city"></i>
|
||||
<span>Local Posts</span>
|
||||
</a>
|
||||
<a href="{% url "explore" %}" {% if current_page == "explore" %}class="selected"{% endif %} title="Explore">
|
||||
<i class="fa-solid fa-hashtag"></i>
|
||||
<span>Explore</span>
|
||||
</a>
|
||||
<h3></h3>
|
||||
{% if config.signup_allowed %}
|
||||
<a href="{% url "signup" %}" {% if current_page == "signup" %}class="selected"{% endif %} title="Create Account">
|
||||
<i class="fa-solid fa-user-plus"></i>
|
||||
<span>Create Account</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</nav>
|
||||
|
||||
{% if current_page == "home" %}
|
||||
<h2>Compose</h2>
|
||||
<form action="{% url "compose" %}" method="POST" class="compose">
|
||||
{% csrf_token %}
|
||||
{{ form.text }}
|
||||
{{ form.content_warning }}
|
||||
<input type="hidden" name="visibility" value="{{ config_identity.default_post_visibility }}">
|
||||
<div class="buttons">
|
||||
<span id="character-counter">{{ config.post_length }}</span>
|
||||
<button class="toggle" _="on click or keyup[key is 'Enter'] toggle .enabled then toggle .hidden on #id_content_warning then halt">CW</button>
|
||||
<button id="post-button">{% if config_identity.toot_mode %}Toot!{% else %}Post{% endif %}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
|
@ -27,11 +27,7 @@
|
|||
</div>
|
||||
|
||||
{% if post.summary %}
|
||||
{% if config_identity.expand_linked_cws %}
|
||||
<div class="summary" _="on click or keyup[key is 'Enter'] toggle .enabled on <.{{ post.summary_class }} .summary/> then toggle .hidden on <.{{ post.summary_class }} .content/> then halt" tabindex="0">
|
||||
{% else %}
|
||||
<div class="summary" _="on click or keyup[key is 'Enter'] toggle .enabled then toggle .hidden on the next .content then halt" tabindex="0">
|
||||
{% endif %}
|
||||
{{ post.summary }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -77,12 +73,19 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if request.identity %}
|
||||
<div class="actions" role="menubar">
|
||||
{% include "activities/_reply.html" %}
|
||||
{% include "activities/_like.html" %}
|
||||
{% include "activities/_boost.html" %}
|
||||
{% include "activities/_bookmark.html" %}
|
||||
<div class="actions">
|
||||
<a title="Replies" href="{% if not post.local and post.url %}{{ post.url }}{% else %}{{ post.urls.view }}{% endif %}">
|
||||
<i class="fa-solid fa-reply"></i>
|
||||
<span class="like-count">{{ post.stats_with_defaults.replies|default:"0" }}</span>
|
||||
</a>
|
||||
<a title="Likes" class="no-action">
|
||||
<i class="fa-solid fa-star"></i>
|
||||
<span class="like-count">{{ post.stats_with_defaults.likes|default:"0" }}</span>
|
||||
</a>
|
||||
<a title="Boosts" class="no-action">
|
||||
<i class="fa-solid fa-retweet"></i>
|
||||
<span class="like-count">{{ post.stats_with_defaults.boosts|default:"0" }}</span>
|
||||
</a>
|
||||
<a title="Menu" class="menu" _="on click or keyup[key is 'Enter'] toggle .enabled on the next <menu/> then halt" role="menuitem" aria-haspopup="menu" tabindex="0">
|
||||
<i class="fa-solid fa-bars"></i>
|
||||
</a>
|
||||
|
@ -90,17 +93,7 @@
|
|||
<a href="{{ post.urls.view }}" role="menuitem">
|
||||
<i class="fa-solid fa-comment"></i> View Post & Replies
|
||||
</a>
|
||||
<a href="{{ post.urls.action_report }}" role="menuitem">
|
||||
<i class="fa-solid fa-flag"></i> Report
|
||||
</a>
|
||||
{% if post.author == request.identity %}
|
||||
<a href="{{ post.urls.action_edit }}" role="menuitem">
|
||||
<i class="fa-solid fa-pen-to-square"></i> Edit
|
||||
</a>
|
||||
<a href="{{ post.urls.action_delete }}" role="menuitem">
|
||||
<i class="fa-solid fa-trash"></i> Delete
|
||||
</a>
|
||||
{% elif not post.local and post.url %}
|
||||
{% if not post.local and post.url %}
|
||||
<a href="{{ post.url }}" role="menuitem">
|
||||
<i class="fa-solid fa-arrow-up-right-from-square"></i> See Original
|
||||
</a>
|
||||
|
@ -112,6 +105,5 @@
|
|||
{% endif %}
|
||||
</menu>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
|
||||
<a title="Reply" href="{{ post.urls.action_reply }}" role="menuitem">
|
||||
<i class="fa-solid fa-reply"></i>
|
||||
<span class="like-count">{{ post.stats_with_defaults.replies }}</span>
|
||||
</a>
|
|
@ -1,41 +1,26 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "settings/base.html" %}
|
||||
|
||||
{% block title %}Compose{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="POST">
|
||||
{% block settings_content %}
|
||||
<form action="." method="POST" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
<legend>Content</legend>
|
||||
{% if reply_to %}
|
||||
<label>Replying to</label>
|
||||
{% include "activities/_mini_post.html" with post=reply_to %}
|
||||
{% endif %}
|
||||
{{ form.reply_to }}
|
||||
<legend>Compose</legend>
|
||||
<p><i>For more advanced posting options, like editing and multiple image uploads, please use an app.</i></p>
|
||||
{{ form.id }}
|
||||
{% 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 %}
|
||||
{% include "forms/_field.html" with field=form.content_warning %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Images</legend>
|
||||
{% if post %}
|
||||
{% for attachment in post.attachments.all %}
|
||||
{% include "activities/_image_uploaded.html" %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if not post or post.attachments.count < 4 %}
|
||||
<button class="add-image"
|
||||
hx-get='{% url "compose_image_upload" %}'
|
||||
hx-target="this"
|
||||
hx-swap="outerHTML">
|
||||
Add Image
|
||||
</button>
|
||||
{% endif %}
|
||||
<legend>Image</legend>
|
||||
{% include "forms/_field.html" with field=form.image %}
|
||||
{% include "forms/_field.html" with field=form.image_caption %}
|
||||
</fieldset>
|
||||
<div class="buttons">
|
||||
<span id="character-counter">{{ config.post_length }}</span>
|
||||
<button id="post-button">{% if post %}Save Edits{% elif config_identity.toot_mode %}Toot!{% else %}Post{% endif %}</button>
|
||||
<button id="post-button">{% if post %}Save Edits{% else %}Post{% endif %}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
{% block title %}Debug JSON{% endblock %}
|
||||
|
||||
{% block body_class %}no-sidebar{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="POST">
|
||||
{% csrf_token %}
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}#{{ hashtag.display_name }} Timeline{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="timeline-name">Explore Trending Tags</div>
|
||||
|
||||
<section class="icon-menu">
|
||||
{% for hashtag in page_obj %}
|
||||
{% include "activities/_hashtag.html" %}
|
||||
{% empty %}
|
||||
No tags are trending yet.
|
||||
{% endfor %}
|
||||
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -1,3 +0,0 @@
|
|||
{% extends "activities/local.html" %}
|
||||
|
||||
{% block title %}Federated Timeline{% endblock %}
|
|
@ -1,60 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% load activity_tags %}
|
||||
|
||||
{% block subtitle %}Follows{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="view-options">
|
||||
{% if inbound %}
|
||||
<a href=".">Following ({{ num_outbound }})</a>
|
||||
<a href="." class="selected">Followers ({{ num_inbound }})</a>
|
||||
{% else %}
|
||||
<a href=".?inbound=true" class="selected">Following ({{ num_outbound }})</a>
|
||||
<a href=".?inbound=true">Followers ({{ num_inbound }})</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<section class="icon-menu">
|
||||
{% for identity in page_obj %}
|
||||
<a class="option " href="{{ identity.urls.view }}">
|
||||
<div class="option-content">
|
||||
{% include "identity/_identity_banner.html" with identity=identity link_avatar=False link_handle=False %}
|
||||
</div>
|
||||
<div class="option-actions">
|
||||
{% if identity.id in outbound_ids %}
|
||||
<span class="pill">Following</span>
|
||||
{% endif %}
|
||||
{% if identity.id in inbound_ids %}
|
||||
<span class="pill">Follows You</span>
|
||||
{% endif %}
|
||||
|
||||
{% if inbound %}
|
||||
<form action="{{ identity.urls.action }}" method="POST" class="follow">
|
||||
{% csrf_token %}
|
||||
{% if identity.id in outbound_ids %}
|
||||
<input type="hidden" name="action" value="unfollow">
|
||||
<button class="destructive">Unfollow</button>
|
||||
{% else %}
|
||||
<input type="hidden" name="action" value="follow">
|
||||
<button>Follow</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endif %}
|
||||
<time>{{ identity.follow_date | timedeltashort }} ago</time>
|
||||
</div>
|
||||
</a>
|
||||
{% empty %}
|
||||
<p class="option empty">You {% if inbound %}have no followers{% else %}are not following anyone{% endif %}.</p>
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
<div class="pagination">
|
||||
{% if page_obj.has_previous %}
|
||||
<a class="button" href=".?page={{ page_obj.previous_page_number }}{% if inbound %}&inbound=true{% endif %}">Previous Page</a>
|
||||
{% endif %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a class="button" href=".?page={{ page_obj.next_page_number }}{% if inbound %}&inbound=true{% endif %}">Next Page</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,37 +1,52 @@
|
|||
{% extends "base.html" %}
|
||||
{% load activity_tags %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Home{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if page_obj.number == 1 %}
|
||||
{% include "_announcements.html" %}
|
||||
{% endif %}
|
||||
{% for event in page_obj %}
|
||||
{% if event.type == "post" %}
|
||||
{% include "activities/_post.html" with post=event.subject_post %}
|
||||
{% elif event.type == "boost" %}
|
||||
<div class="boost-banner">
|
||||
<a href="{{ event.subject_identity.urls.view }}">
|
||||
{{ event.subject_identity.html_name_or_handle }}
|
||||
</a> boosted
|
||||
<time>
|
||||
{{ event.subject_post_interaction.published | timedeltashort }} ago
|
||||
</time>
|
||||
</div>
|
||||
{% include "activities/_post.html" with post=event.subject_post %}
|
||||
{% endif %}
|
||||
<section class="icon-menu">
|
||||
<h1 class="above">Identities</h1>
|
||||
{% for identity in identities %}
|
||||
<a class="option" href="{{ identity.urls.view }}">
|
||||
<img src="{{ identity.local_icon_url.relative }}">
|
||||
<span class="handle">
|
||||
{{ identity.html_name_or_handle }}
|
||||
<small>@{{ identity.handle }}</small>
|
||||
</span>
|
||||
<button class="right secondary" _="on click go to url {{ identity.urls.view }} then halt">View</button>
|
||||
<button class="right secondary" _="on click go to url {{ identity.urls.settings }} then halt">Settings</button>
|
||||
</a>
|
||||
{% empty %}
|
||||
Nothing to show yet.
|
||||
<p class="option empty">You have no identities.</p>
|
||||
{% endfor %}
|
||||
|
||||
<div class="pagination">
|
||||
{% if page_obj.has_previous and not request.htmx %}
|
||||
<a class="button" href=".?page={{ page_obj.previous_page_number }}">Previous Page</a>
|
||||
{% endif %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a class="button" href=".?page={{ page_obj.next_page_number }}" hx-get=".?page={{ page_obj.next_page_number }}" hx-select=".left-column > *:not(.view-options)" hx-target=".pagination" hx-swap="outerHTML" {% if config_identity.infinite_scroll %}hx-trigger="revealed"{% endif %}>Next Page</a>
|
||||
{% endif %}
|
||||
<a href="{% url "identity_create" %}" class="option new">
|
||||
<i class="fa-solid fa-plus"></i> Create a new identity
|
||||
</a>
|
||||
</section>
|
||||
<section>
|
||||
<h1 class="above">Apps</h1>
|
||||
<p>
|
||||
To see your timelines, compose and edit messages, and follow people,
|
||||
you will need to use a Mastodon-compatible app. Our favourites
|
||||
are listed below, but there's lots more options out there!
|
||||
</p>
|
||||
<div class="flex-icons">
|
||||
<a href="https://elk.zone/">
|
||||
<img src="{% static "img/apps/elk.svg" %}" alt="Elk logo">
|
||||
<h2>Elk</h2>
|
||||
<i>Web/Mobile (Free)</i>
|
||||
</a>
|
||||
<a href="https://tusky.app/">
|
||||
<img src="{% static "img/apps/tusky.png" %}" alt="Tusky logo">
|
||||
<h2>Tusky</h2>
|
||||
<i>Android (Free)</i>
|
||||
</a>
|
||||
<a href="https://tapbots.com/ivory/">
|
||||
<img src="{% static "img/apps/ivory.webp" %}" alt="Ivory logo">
|
||||
<h2>Ivory</h2>
|
||||
<i>iOS (Paid)</i>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Local Timeline{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if page_obj.number == 1 %}
|
||||
{% include "_announcements.html" %}
|
||||
{% endif %}
|
||||
|
||||
{% for post in page_obj %}
|
||||
{% include "activities/_post.html" with feedindex=forloop.counter %}
|
||||
{% empty %}
|
||||
No posts yet.
|
||||
{% endfor %}
|
||||
|
||||
<div class="pagination">
|
||||
{% if page_obj.has_previous and not request.htmx %}
|
||||
<a class="button" href=".?page={{ page_obj.previous_page_number }}">Previous Page</a>
|
||||
{% endif %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a class="button" href=".?page={{ page_obj.next_page_number }}" hx-get=".?page={{ page_obj.next_page_number }}" hx-select=".left-column > *:not(.view-options)" hx-target=".pagination" hx-swap="outerHTML" {% if config_identity.infinite_scroll %}hx-trigger="revealed"{% endif %}>Next Page</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,12 +1,10 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "settings/base.html" %}
|
||||
|
||||
{% block title %}Notifications{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if page_obj.number == 1 %}
|
||||
{% include "_announcements.html" %}
|
||||
{% endif %}
|
||||
{% block settings_content %}
|
||||
|
||||
<section class="invisible">
|
||||
<div class="view-options">
|
||||
{% if notification_options.followed %}
|
||||
<a href=".?followed=false" class="selected"><i class="fa-solid fa-check"></i> Followers</a>
|
||||
|
@ -28,13 +26,6 @@
|
|||
{% else %}
|
||||
<a href=".?mentioned=true"><i class="fa-solid fa-xmark"></i> Mentions</a>
|
||||
{% endif %}
|
||||
{% if request.user.admin %}
|
||||
{% if notification_options.identity_created %}
|
||||
<a href=".?identity_created=false" class="selected"><i class="fa-solid fa-check"></i> New Identities</a>
|
||||
{% else %}
|
||||
<a href=".?identity_created=true"><i class="fa-solid fa-xmark"></i> New Identities</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% for event in events %}
|
||||
|
@ -43,13 +34,5 @@
|
|||
No notifications yet.
|
||||
{% endfor %}
|
||||
|
||||
<div class="pagination">
|
||||
{% if page_obj.has_previous and not request.htmx %}
|
||||
<a class="button" href=".?page={{ page_obj.previous_page_number }}">Previous Page</a>
|
||||
{% endif %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a class="button" href=".?page={{ page_obj.next_page_number }}" hx-get=".?page={{ page_obj.next_page_number }}" hx-select=".left-column > *:not(.view-options)" hx-target=".pagination" hx-swap="outerHTML" {% if config_identity.infinite_scroll %}hx-trigger="revealed"{% endif %}>Next Page</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="invisible">
|
||||
{% for ancestor in ancestors reversed %}
|
||||
{% include "activities/_post.html" with post=ancestor reply=True link_original=False %}
|
||||
{% endfor %}
|
||||
|
@ -14,4 +15,5 @@
|
|||
{% for descendant in descendants %}
|
||||
{% include "activities/_post.html" with post=descendant reply=True link_original=False %}
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Search{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="POST">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
{% include "forms/_field.html" with field=form.query %}
|
||||
</fieldset>
|
||||
<div class="buttons">
|
||||
<button>Search</button>
|
||||
</div>
|
||||
</form>
|
||||
{% if results.identities %}
|
||||
<h2>People</h2>
|
||||
{% for identity in results.identities %}
|
||||
{% include "activities/_identity.html" %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if results.posts %}
|
||||
<h2>Posts</h2>
|
||||
<section class="icon-menu">
|
||||
{% for post in results.posts %}
|
||||
{% include "activities/_post.html" %}
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
{% if results.hashtags %}
|
||||
<h2>Hashtags</h2>
|
||||
<section class="icon-menu">
|
||||
{% for hashtag in results.hashtags %}
|
||||
{% include "activities/_hashtag.html" with hide_stats=True %}
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
{% if results and not results.identities and not results.hashtags and not results.posts %}
|
||||
<h2>No results</h2>
|
||||
<p>
|
||||
We could not find anything matching your query.
|
||||
</p>
|
||||
<p>
|
||||
If you're trying to find a post or profile on another server,
|
||||
try again in a few moments - if the other end is overloaded, it
|
||||
can take some time to fetch the details.
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -3,10 +3,8 @@
|
|||
{% block title %}#{{ hashtag.display_name }} Timeline{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="invisible">
|
||||
<div class="timeline-name">
|
||||
<div class="inline follow follow-hashtag">
|
||||
{% include "activities/_hashtag_follow.html" %}
|
||||
</div>
|
||||
<div class="hashtag">
|
||||
<i class="fa-solid fa-hashtag"></i>{{ hashtag.display_name }}
|
||||
</div>
|
||||
|
@ -23,7 +21,8 @@
|
|||
{% endif %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a class="button" href=".?page={{ page_obj.next_page_number }} {% if config_identity.infinite_scroll %}hx-trigger="revealed"{% endif %}">Next Page</a>
|
||||
<a class="button" href=".?page={{ page_obj.next_page_number }}">Next Page</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
61
templates/admin/_menu.html
Normal file
61
templates/admin/_menu.html
Normal file
|
@ -0,0 +1,61 @@
|
|||
<nav>
|
||||
{% if request.user.moderator or request.user.admin %}
|
||||
<h3>Moderation</h3>
|
||||
<a href="{% url "admin_identities" %}" {% if section == "identities" %}class="selected"{% endif %} title="Identities">
|
||||
<i class="fa-solid fa-id-card"></i>
|
||||
<span>Identities</span>
|
||||
</a>
|
||||
<a href="{% url "admin_invites" %}" {% if section == "invites" %}class="selected"{% endif %} title="Invites">
|
||||
<i class="fa-solid fa-envelope"></i>
|
||||
<span>Invites</span>
|
||||
</a>
|
||||
<a href="{% url "admin_hashtags" %}" {% if section == "hashtags" %}class="selected"{% endif %} title="Hashtags">
|
||||
<i class="fa-solid fa-hashtag"></i>
|
||||
<span>Hashtags</span>
|
||||
</a>
|
||||
<a href="{% url "admin_emoji" %}" {% if section == "emoji" %}class="selected"{% endif %} title="Emoji">
|
||||
<i class="fa-solid fa-icons"></i>
|
||||
<span>Emoji</span>
|
||||
</a>
|
||||
<a href="{% url "admin_reports" %}" {% if section == "reports" %}class="selected"{% endif %} title="Reports">
|
||||
<i class="fa-solid fa-flag"></i>
|
||||
<span>Reports</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if request.user.admin %}
|
||||
<hr>
|
||||
<h3>Administration</h3>
|
||||
<a href="{% url "admin_basic" %}" {% if section == "basic" %}class="selected"{% endif %} title="Basic">
|
||||
<i class="fa-solid fa-book"></i>
|
||||
<span>Basic</span>
|
||||
</a>
|
||||
<a href="{% url "admin_policies" %}" {% if section == "policies" %}class="selected"{% endif %} title="Policies">
|
||||
<i class="fa-solid fa-file-lines"></i>
|
||||
<span>Policies</span>
|
||||
</a>
|
||||
<a href="{% url "admin_announcements" %}" {% if section == "announcements" %}class="selected"{% endif %} title="Announcements">
|
||||
<i class="fa-solid fa-bullhorn"></i>
|
||||
<span>Announcements</span>
|
||||
</a>
|
||||
<a href="{% url "admin_domains" %}" {% if section == "domains" %}class="selected"{% endif %} title="Domains">
|
||||
<i class="fa-solid fa-globe"></i>
|
||||
<span>Domains</span>
|
||||
</a>
|
||||
<a href="{% url "admin_federation" %}" {% if section == "federation" %}class="selected"{% endif %} title="Federation">
|
||||
<i class="fa-solid fa-diagram-project"></i>
|
||||
<span>Federation</span>
|
||||
</a>
|
||||
<a href="{% url "admin_users" %}" {% if section == "users" %}class="selected"{% endif %} title="Users">
|
||||
<i class="fa-solid fa-users"></i>
|
||||
<span>Users</span>
|
||||
</a>
|
||||
<a href="{% url "admin_stator" %}" {% if section == "stator" %}class="selected"{% endif %} title="Stator">
|
||||
<i class="fa-solid fa-clock-rotate-left"></i>
|
||||
<span>Stator</span>
|
||||
</a>
|
||||
<a href="/djadmin" title="Django Admin" class="danger">
|
||||
<i class="fa-solid fa-gear"></i>
|
||||
<span>Django Admin</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</nav>
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base_main.html" %}
|
||||
|
||||
{% block subtitle %}Create Announcement{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." method="POST">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
{% block title %}Delete Announcement - Admin{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<h1>Confirm Delete</h1>
|
||||
<section>
|
||||
<form action="." method="POST">
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base_main.html" %}
|
||||
|
||||
{% block subtitle %}Announcement #{{ announcement.pk }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." method="POST">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base_main.html" %}
|
||||
|
||||
{% block subtitle %}Announcements{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<div class="view-options">
|
||||
<a href="{% url "admin_announcement_create" %}" class="button"><i class="fa-solid fa-plus"></i> Create</a>
|
||||
</div>
|
||||
|
|
15
templates/admin/base_main.html
Normal file
15
templates/admin/base_main.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{% block subtitle %}{% endblock %} - Administration{% endblock %}
|
||||
|
||||
{% block body_class %}wide{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="settings">
|
||||
{% include "admin/_menu.html" %}
|
||||
<div class="settings-content">
|
||||
{% block settings_content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base_main.html" %}
|
||||
|
||||
{% block title %}Add Domain - Admin{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." method="POST">
|
||||
<h1>Add A Domain</h1>
|
||||
<p>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base_main.html" %}
|
||||
|
||||
{% block title %}Delete {{ domain.domain }} - Admin{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." method="POST">
|
||||
{% csrf_token %}
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base_main.html" %}
|
||||
|
||||
{% block subtitle %}{{ domain.domain }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="POST">
|
||||
{% block settings_content %}
|
||||
<form action="." method="POST" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
<legend>Domain Details</legend>
|
||||
|
@ -16,6 +16,14 @@
|
|||
{% include "forms/_field.html" with field=form.default %}
|
||||
{% include "forms/_field.html" with field=form.users %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Appearance</legend>
|
||||
{% include "forms/_field.html" with field=form.site_name %}
|
||||
{% include "forms/_field.html" with field=form.site_icon %}
|
||||
{% include "forms/_field.html" with field=form.hide_login %}
|
||||
{% include "forms/_field.html" with field=form.single_user %}
|
||||
{% include "forms/_field.html" with field=form.custom_css %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Admin Notes</legend>
|
||||
{% include "forms/_field.html" with field=form.notes %}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base_main.html" %}
|
||||
|
||||
{% block subtitle %}Domains{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<div class="view-options">
|
||||
<span class="spacer"></span>
|
||||
<a href="{% url "admin_domains_create" %}" class="button"><i class="fa-solid fa-plus"></i> Add Domain</a>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base_main.html" %}
|
||||
{% load activity_tags %}
|
||||
|
||||
{% block subtitle %}Emoji{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." class="search">
|
||||
<input type="search" name="query" value="{{ query }}" placeholder="Search by shortcode or domain">
|
||||
{% if local_only %}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base_main.html" %}
|
||||
|
||||
{% block subtitle %}{{ emoji.shortcode }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." method="POST" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base_main.html" %}
|
||||
{% load activity_tags %}
|
||||
|
||||
{% block subtitle %}Federation{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." class="search">
|
||||
<input type="search" name="query" value="{{ query }}" placeholder="Search by domain">
|
||||
<button><i class="fa-solid fa-search"></i></button>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base_main.html" %}
|
||||
|
||||
{% block subtitle %}{{ domain.domain }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." method="POST">
|
||||
{% csrf_token %}
|
||||
<h1>{{ domain }}</h1>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base_main.html" %}
|
||||
|
||||
{% block subtitle %}{{ hashtag.hashtag }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." method="POST">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base_main.html" %}
|
||||
|
||||
{% block subtitle %}Hashtags{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<table class="items">
|
||||
{% for hashtag in page_obj %}
|
||||
<tr>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base_main.html" %}
|
||||
{% load activity_tags %}
|
||||
|
||||
{% block subtitle %}Identities{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." class="search">
|
||||
<input type="search" name="query" value="{{ query }}" placeholder="Search by name/username">
|
||||
{% if local_only %}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base_main.html" %}
|
||||
|
||||
{% block subtitle %}{{ identity.name_or_handle }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<h1>{{ identity.html_name_or_handle }} <small>{{ identity.handle }}</small></h1>
|
||||
<form action="." method="POST">
|
||||
{% csrf_token %}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base_main.html" %}
|
||||
|
||||
{% block subtitle %}Create Invite{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." method="POST">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base_main.html" %}
|
||||
|
||||
{% block subtitle %}View Invite{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." method="POST">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base_main.html" %}
|
||||
{% load activity_tags %}
|
||||
|
||||
{% block subtitle %}Invites{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<div class="view-options">
|
||||
<span class="spacer"></span>
|
||||
<a href="{% url "admin_invite_create" %}" class="button">Create New</a>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base_main.html" %}
|
||||
|
||||
{% block subtitle %}Report {{ report.pk }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." method="POST">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base_main.html" %}
|
||||
{% load activity_tags %}
|
||||
|
||||
{% block subtitle %}Reports{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<div class="view-options">
|
||||
{% if all %}
|
||||
<a href="." class="selected"><i class="fa-solid fa-check"></i> Show Resolved</a>
|
||||
|
|
20
templates/admin/settings.html
Normal file
20
templates/admin/settings.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
{% extends "admin/base_main.html" %}
|
||||
|
||||
{% block subtitle %}{{ section.title }}{% endblock %}
|
||||
|
||||
{% block settings_content %}
|
||||
<form action="." method="POST" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% for title, fields in fieldsets.items %}
|
||||
<fieldset>
|
||||
<legend>{{ title }}</legend>
|
||||
{% for field in fields %}
|
||||
{% include "forms/_field.html" %}
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
{% endfor %}
|
||||
<div class="buttons">
|
||||
<button>Save</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base_main.html" %}
|
||||
|
||||
{% block subtitle %}Stator{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
{% for model, stats in model_stats.items %}
|
||||
<fieldset>
|
||||
<legend>{{ model }}</legend>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base_main.html" %}
|
||||
|
||||
{% block subtitle %}{{ editing_user.email }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<h1>{{ editing_user.email }}</h1>
|
||||
<form action="." method="POST">
|
||||
{% csrf_token %}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base_main.html" %}
|
||||
{% load activity_tags %}
|
||||
|
||||
{% block subtitle %}Users{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." class="search">
|
||||
<input type="search" name="query" value="{{ query }}" placeholder="Search by email">
|
||||
<button><i class="fa-solid fa-search"></i></button>
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
<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/font_awesome/all.min.css" %}" type="text/css" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="shortcut icon" href="{{ config.site_icon }}">
|
||||
<script src="{% static "js/hyperscript.min.js" %}"></script>
|
||||
<script src="{% static "js/htmx.min.js" %}"></script>
|
||||
|
@ -18,14 +17,15 @@
|
|||
--color-highlight: {{ config.highlight_color }};
|
||||
--color-text-link: {{ config.highlight_color }};
|
||||
}
|
||||
{% if not config_identity.visible_reaction_counts %}
|
||||
.like-count {
|
||||
display: none;
|
||||
}
|
||||
{% endif %}
|
||||
</style>
|
||||
{% if config_identity.custom_css %}
|
||||
<style>{{ config_identity.custom_css|safe }}</style>
|
||||
{% if identity and public_styling %}
|
||||
{% if identity.domain.config_domain.custom_css %}
|
||||
<style>{{ identity.domain.config_domain.custom_css|safe }}</style>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if request.domain.config_domain.custom_css %}
|
||||
<style>{{ request.domain.config_domain.custom_css|safe }}</style>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% block opengraph %}
|
||||
{% include "_opengraph.html" with opengraph_local=opengraph_defaults %}
|
||||
|
@ -33,67 +33,51 @@
|
|||
{% block extra_head %}{% endblock %}
|
||||
{% block custom_head %}{% if config.custom_head %}{{ config.custom_head|safe }}{% endif %}{% endblock %}
|
||||
</head>
|
||||
<body class="{% if config_identity.light_theme %}light-theme {% endif %}{% block body_class %}{% endblock %}" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
|
||||
<body class="{% if user.config_user.light_theme %}theme-light {% endif %}{% block body_class %}{% endblock %}" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
|
||||
<a id="skip-to-main" class="screenreader-text" href="#main-content">Skip to Content</a>
|
||||
<a id="skip-to-nav" class="screenreader-text" href="#side-navigation">Skip to Navigation</a>
|
||||
<main>
|
||||
{% block body_main %}
|
||||
<header>
|
||||
<a class="logo" href="/">
|
||||
<img src="{{ config.site_icon }}" width="32">
|
||||
{{ config.site_name }}
|
||||
</a>
|
||||
<menu>
|
||||
<a class="logo" href="/">
|
||||
{% if identity and public_styling %}
|
||||
<img src="{{ identity.domain.config_domain.site_icon|default:config.site_icon }}" width="32">
|
||||
{{ identity.domain.config_domain.site_name|default:config.site_name }}
|
||||
{% else %}
|
||||
<img src="{{ request.domain.config_domain.site_icon|default:config.site_icon }}" width="32">
|
||||
{{ request.domain.config_domain.site_name|default:config.site_name }}
|
||||
{% endif %}
|
||||
</a>
|
||||
{% if user.is_authenticated %}
|
||||
<a href="{% url "compose" %}" title="Compose" role="menuitem" {% if top_section == "compose" %}class="selected"{% endif %}>
|
||||
<i class="fa-solid fa-feather"></i>
|
||||
</a>
|
||||
<a href="{% url "search" %}" title="Search" role="menuitem" class="search {% if top_section == "search" %}selected{% endif %}">
|
||||
<i class="fa-solid fa-search"></i>
|
||||
</a>
|
||||
{% if allows_refresh %}
|
||||
<a href="." title="Refresh" role="menuitem" hx-get="." hx-select=".left-column" hx-target=".left-column" hx-swap="outerHTML" hx-trigger="click, every 120s[isAtTopOfPage()]">
|
||||
<i class="fa-solid fa-rotate"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="gap"></div>
|
||||
<a href="{{ request.identity.urls.view }}" role="menuitem" class="identity">
|
||||
{% if not request.identity %}
|
||||
No Identity
|
||||
<img src="{% static "img/unknown-icon-128.png" %}" title="No identity selected">
|
||||
<a href="/" title="My Account"><i class="fa-solid fa-user"></i></a>
|
||||
{% if identity %}
|
||||
<a href="{{ identity.urls.settings }}" title="Settings"><i class="fa-solid fa-gear"></i></a>
|
||||
{% else %}
|
||||
{{ request.identity.username }}
|
||||
<img src="{{ request.identity.local_icon_url.relative }}" title="{{ request.identity.handle }}">
|
||||
<a href="/settings" title="Settings"><i class="fa-solid fa-gear"></i></a>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="gap"></div>
|
||||
<a href="{% url "login" %}" role="menuitem" class="identity"><i class="fa-solid fa-right-to-bracket"></i> Login</a>
|
||||
{% if user.admin %}
|
||||
<a href="/admin" title="Administration"><i class="fa-solid fa-screwdriver-wrench"></i></a>
|
||||
{% endif %}
|
||||
{% elif not request.domain.config_domain.hide_login %}
|
||||
<a href="{% url "login" %}" title="Login"><i class="fa-solid fa-right-to-bracket"></i></a>
|
||||
{% endif %}
|
||||
</menu>
|
||||
</header>
|
||||
|
||||
{% block full_content %}
|
||||
{% block announcements %}
|
||||
{% include "_announcements.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% include "activities/_image_viewer.html" %}
|
||||
{% block pre_content %}
|
||||
{% endblock %}
|
||||
<div class="columns">
|
||||
<div class="left-column" id="main-content">
|
||||
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
<div class="right-column" id="side-navigation">
|
||||
{% block right_content %}
|
||||
{% include "activities/_menu.html" %}
|
||||
{% endblock %}
|
||||
{% include "_footer.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
|
||||
{% block footer %}
|
||||
{% include "_footer.html" %}
|
||||
{% endblock %}
|
||||
|
||||
</body>
|
||||
|
|
|
@ -1,38 +1,5 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block body_class %}no-sidebar{% endblock %}
|
||||
|
||||
{% block opengraph %}
|
||||
{# Error pages don't have the context loaded, so disable opengraph to keep it from spewing errors #}
|
||||
{% endblock %}
|
||||
|
||||
{% block body_main %}
|
||||
<header>
|
||||
<menu>
|
||||
{% if not request.user.is_authenticated and current_page == "about" and config.signup_allowed %}
|
||||
<a href="{% url "signup" %}">Sign Up</a>
|
||||
{% else %}
|
||||
<a href="/">Home</a>
|
||||
{% endif %}
|
||||
<a class="logo" href="/">
|
||||
<img src="{{ config.site_icon }}" width="32">
|
||||
{{ config.site_name }}
|
||||
</a>
|
||||
{% if request.user.is_authenticated %}
|
||||
<a href="javascript:history.back()">Back</a>
|
||||
{% else %}
|
||||
<a href="{% url "login" %}">Login</a>
|
||||
{% endif %}
|
||||
</menu>
|
||||
</header>
|
||||
|
||||
<div id="main-content">
|
||||
{% include "activities/_image_viewer.html" %}
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block footer %}
|
||||
{% include "_footer.html" %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -28,12 +28,23 @@
|
|||
{# fmt:on #}
|
||||
</script>
|
||||
|
||||
{% include "forms/_field.html" %}
|
||||
<div class="field">
|
||||
<div class="label-input">
|
||||
<label for="{{ field.id_for_label }}">
|
||||
{{ field.label }}
|
||||
{% if field.field.required %}<small>(Required)</small>{% endif %}
|
||||
</label>
|
||||
{% if field.help_text %}
|
||||
<p class="help">
|
||||
{{ field.help_text|safe|linebreaksbr }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{{ field.errors }}
|
||||
{{ field }}
|
||||
|
||||
<div class="field multi-option">
|
||||
<section class="icon-menu">
|
||||
<div class="multi-option">
|
||||
{# fmt:off #}
|
||||
<span id="new_{{ field.name }}" stlye="display: none"
|
||||
<span id="new_{{ field.name }}" style="display: none"
|
||||
_="on load
|
||||
get the (value of #id_{{ field.name }}) as Object
|
||||
set items to it
|
||||
|
@ -74,9 +85,8 @@
|
|||
then halt" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endwith %}
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
<a href="{{ identity.urls.view }}" class="handle">
|
||||
{% endif %}
|
||||
<div class="link">{{ identity.html_name_or_handle }}</div>
|
||||
<small>@{{ identity.handle }}</small>
|
||||
<small>@{{ identity.username }}@{{ identity.domain_id }}</small>
|
||||
{% if link_handle is False %}
|
||||
</div>
|
||||
{% else %}
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
<nav>
|
||||
<a href="{% url "identity_select" %}" {% if identities %}class="selected"{% endif %} title="Select Identity">
|
||||
<i class="fa-solid fa-users-viewfinder"></i>
|
||||
<span>Select Identity</span>
|
||||
</a>
|
||||
<a href="{% url "identity_create" %}" {% if form %}class="selected"{% endif %} title="Create Identity">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
<span>Create Identity</span>
|
||||
</a>
|
||||
<a href="{% url "logout" %}" title="Logout">
|
||||
<i class="fa-solid fa-right-from-bracket"></i>
|
||||
<span>Logout</span>
|
||||
</a>
|
||||
{% if request.user.admin %}
|
||||
<hr>
|
||||
<h3>Administration</h3>
|
||||
<a href="{% url "admin_domains" %}" {% if section == "domains" %}class="selected"{% endif %} title="Domains">
|
||||
<i class="fa-solid fa-globe"></i>
|
||||
<span>Domains</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</nav>
|
10
templates/identity/_tabs.html
Normal file
10
templates/identity/_tabs.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
<section class="view-options">
|
||||
<a href="{{ identity.urls.view }}" {% if not section %}class="selected"{% endif %}><strong>{{ post_count }}</strong> Posts</a>
|
||||
{% if identity.local and identity.config_identity.visible_follows %}
|
||||
<a href="{{ identity.urls.following }}" {% if not inbound and section == "follows" %}class="selected"{% endif %}><strong>{{ following_count }}</strong> Following</a>
|
||||
<a href="{{ identity.urls.followers }}" {% if inbound and section == "follows" %}class="selected"{% endif %}><strong>{{ followers_count }}</strong> Follower{{ followers_count|pluralize }}</a>
|
||||
{% endif %}
|
||||
{% if identity.local and identity.config_identity.search_enabled %}
|
||||
<a href="{{ identity.urls.search }}" {% if section == "search" %}class="selected"{% endif %}>Search</a>
|
||||
{% endif %}
|
||||
</section>
|
|
@ -1,78 +0,0 @@
|
|||
<div class="inline follow {% if inbound_follow %}has-reverse{% endif %}">
|
||||
<div class="actions" role="menubar">
|
||||
{% if request.identity == identity %}
|
||||
<a href="{% url "settings_profile" %}" class="button" title="Edit Profile">
|
||||
<i class="fa-solid fa-user-edit"></i> Edit
|
||||
</a>
|
||||
{% elif not inbound_block %}
|
||||
{% if inbound_follow or outbound_mute %}
|
||||
<span class="reverse-follow">
|
||||
{% if inbound_follow %}Follows You{% endif %}{% if inbound_follow and outbound_mute %},{% endif %}
|
||||
{% if outbound_mute %}Muted{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
<form action="{{ identity.urls.action }}" method="POST" class="inline-menu">
|
||||
{% csrf_token %}
|
||||
{% if outbound_block %}
|
||||
<input type="hidden" name="action" value="unblock">
|
||||
<button class="destructive" title="Unblock"><i class="fa-solid fa-ban"></i>
|
||||
Unblock
|
||||
</button>
|
||||
{% elif outbound_follow %}
|
||||
<input type="hidden" name="action" value="unfollow">
|
||||
<button class="destructive" title="Unfollow"><i class="fa-solid fa-user-minus"></i>
|
||||
{% if outbound_follow.pending %}Follow Pending{% else %}Unfollow{% endif %}
|
||||
</button>
|
||||
{% else %}
|
||||
<input type="hidden" name="action" value="follow">
|
||||
<button><i class="fa-solid fa-user-plus"></i> Follow</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endif %}
|
||||
<a title="Menu" class="menu button" _="on click or keyup[key is 'Enter'] toggle .enabled on the next <menu/> then halt" aria-haspopup="menu" tabindex="0">
|
||||
<i class="fa-solid fa-bars"></i>
|
||||
</a>
|
||||
<menu>
|
||||
{% if outbound_follow %}
|
||||
<form action="{{ identity.urls.action }}" method="POST" class="inline">
|
||||
{% csrf_token %}
|
||||
{% if outbound_follow.boosts %}
|
||||
<input type="hidden" name="action" value="hide_boosts">
|
||||
<button role="menuitem"><i class="fa-solid fa-retweet"></i> Hide boosts</button>
|
||||
{% else %}
|
||||
<input type="hidden" name="action" value="show_boosts">
|
||||
<button role="menuitem"><i class="fa-solid fa-retweet"></i> Show boosts</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if request.identity != identity and not outbound_block %}
|
||||
<form action="{{ identity.urls.action }}" method="POST" class="inline">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="block">
|
||||
<button role="menuitem"><i class="fa-solid fa-ban"></i> Block user</button>
|
||||
</form>
|
||||
{% if outbound_mute %}
|
||||
<form action="{{ identity.urls.action }}" method="POST" class="inline">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="unmute">
|
||||
<button role="menuitem"><i class="fa-solid fa-comment-slash"></i> Unmute user</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form action="{{ identity.urls.action }}" method="POST" class="inline">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="mute">
|
||||
<button role="menuitem"><i class="fa-solid fa-comment-slash"></i> Mute user</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if request.user.admin %}
|
||||
<a href="{{ identity.urls.admin_edit }}" role="menuitem">
|
||||
<i class="fa-solid fa-user-gear"></i> View in Admin
|
||||
</a>
|
||||
<a href="{{ identity.urls.djadmin_edit }}" role="menuitem">
|
||||
<i class="fa-solid fa-gear"></i> View in djadmin
|
||||
</a>
|
||||
{% endif %}
|
||||
</menu>
|
||||
</div>
|
||||
</div>
|
|
@ -1,7 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{% block subtitle %}{% endblock %} - Settings{% endblock %}
|
||||
|
||||
{% block right_content %}
|
||||
{% include "identity/_menu.html" %}
|
||||
{% endblock %}
|
|
@ -1,12 +1,15 @@
|
|||
{% extends "identity/base.html" %}
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Create Identity{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="POST">
|
||||
<h1>Create New Identity</h1>
|
||||
<p>You can have multiple identities - they are totally separate, and share
|
||||
nothing apart from your login details. Use them for alternates, projects, and more.</p>
|
||||
<p>
|
||||
You can have multiple identities - they are totally separate, and share
|
||||
nothing apart from your login details. They can also be shared among multiple
|
||||
users, so you can use them for company or project accounts.
|
||||
</p>
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
<legend>Identity Details</legend>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{% extends "identity/view.html" %}
|
||||
|
||||
{% block title %}{% if self.inbound %}Followers{% else %}Following{% endif %} - {{ identity }}{% endblock %}
|
||||
{% block title %}{% if inbound %}Followers{% else %}Following{% endif %} - {{ identity }}{% endblock %}
|
||||
|
||||
{% block subcontent %}
|
||||
|
||||
|
@ -8,7 +8,7 @@
|
|||
{% include "activities/_identity.html" %}
|
||||
{% empty %}
|
||||
<span class="empty">
|
||||
This person has no {% if self.inbound %}followers{% else %}follows{% endif %} yet.
|
||||
This person has no {% if inbound %}followers{% else %}follows{% endif %} yet.
|
||||
</span>
|
||||
{% endfor %}
|
||||
|
||||
|
|
23
templates/identity/search.html
Normal file
23
templates/identity/search.html
Normal file
|
@ -0,0 +1,23 @@
|
|||
{% extends "identity/view.html" %}
|
||||
|
||||
{% block title %}Search - {{ identity }}{% endblock %}
|
||||
|
||||
{% block subcontent %}
|
||||
|
||||
<form action="." method="post">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
{% include "forms/_field.html" with field=form.query %}
|
||||
</fieldset>
|
||||
<div class="buttons">
|
||||
<button>Search</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% for post in results %}
|
||||
{% include "activities/_post.html" %}
|
||||
{% empty %}
|
||||
<p class="empty">No posts were found that match your search.</p>
|
||||
{% endfor %}
|
||||
|
||||
{% endblock %}
|
|
@ -1,23 +0,0 @@
|
|||
{% extends "identity/base.html" %}
|
||||
|
||||
{% block title %}Select Identity{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="icon-menu">
|
||||
{% for identity in identities %}
|
||||
<a class="option" href="{{ identity.urls.activate }}">
|
||||
<img src="{{ identity.local_icon_url.relative }}">
|
||||
<span class="handle">
|
||||
{{ identity.html_name_or_handle }}
|
||||
<small>@{{ identity.handle }}</small>
|
||||
</span>
|
||||
<button class="right secondary" _="on click go to url {{ identity.urls.view }} then halt">View</button>
|
||||
</a>
|
||||
{% empty %}
|
||||
<p class="option empty">You have no identities.</p>
|
||||
{% endfor %}
|
||||
<a href="{% url "identity_create" %}" class="option new">
|
||||
<i class="fa-solid fa-plus"></i> Create a new identity
|
||||
</a>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -12,11 +12,8 @@
|
|||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block body_class %}has-banner{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if not request.htmx %}
|
||||
<h1 class="identity">
|
||||
<section class="identity">
|
||||
{% if identity.local_image_url %}
|
||||
<img src="{{ identity.local_image_url.relative }}" class="banner">
|
||||
{% endif %}
|
||||
|
@ -30,9 +27,22 @@
|
|||
>
|
||||
</span>
|
||||
|
||||
{% if request.identity %}{% include "identity/_view_menu.html" %}{% endif %}
|
||||
<div class="inline follow">
|
||||
<div class="actions" role="menubar">
|
||||
{% if request.user in identity.users.all %}
|
||||
<a href="{% url "settings_profile" handle=identity.handle %}" class="button" title="Edit Profile">
|
||||
<i class="fa-solid fa-user-edit"></i> Settings
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if request.user.admin or request.user.moderator %}
|
||||
<a href="{% url "settings_profile" handle=identity.handle %}" class="button danger" title="View in Admin">
|
||||
<i class="fa-solid fa-screwdriver-wrench"></i> Admin
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ identity.html_name_or_handle }}
|
||||
<h1>{{ identity.html_name_or_handle }}</h1>
|
||||
<small>
|
||||
@{{ identity.handle }}
|
||||
<a title="Copy handle"
|
||||
|
@ -46,54 +56,36 @@
|
|||
<i class="fa-solid fa-copy"></i>
|
||||
</a>
|
||||
</small>
|
||||
</h1>
|
||||
{% endif %}
|
||||
|
||||
{% if inbound_block %}
|
||||
<p class="system-note">
|
||||
This user has blocked you.
|
||||
</p>
|
||||
{% else %}
|
||||
|
||||
{% if not request.htmx %}
|
||||
{% if identity.summary %}
|
||||
<div class="bio">
|
||||
{{ identity.safe_summary }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section class="invisible identity-metadata">
|
||||
{% if identity.metadata %}
|
||||
<div class="identity-metadata">
|
||||
{% for entry in identity.safe_metadata %}
|
||||
<div class="metadata-pair">
|
||||
<span class="metadata-name">{{ entry.name }}</span>
|
||||
<span class="metadata-value">{{ entry.value }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="view-options follows">
|
||||
<a href="{{ identity.urls.view }}" {% if not follows_page %}class="selected"{% endif %}><strong>{{ post_count }}</strong> posts</a>
|
||||
{% if identity.local and identity.config_identity.visible_follows %}
|
||||
<a href="{{ identity.urls.following }}" {% if not inbound and follows_page %}class="selected"{% endif %}><strong>{{ following_count }}</strong> following</a>
|
||||
<a href="{{ identity.urls.followers }}" {% if inbound and follows_page %}class="selected"{% endif %}><strong>{{ followers_count }}</strong> follower{{ followers_count|pluralize }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% if not identity.local %}
|
||||
<section class="system-note">
|
||||
{% if identity.outdated and not identity.name %}
|
||||
<p class="system-note">
|
||||
The system is still fetching this profile. Refresh to see updates.
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="system-note">
|
||||
This is a member of another server.
|
||||
<a href="{{ identity.profile_uri|default:identity.actor_uri }}">See their original profile ➔</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</section>
|
||||
{% else %}
|
||||
|
||||
{% include "identity/_tabs.html" %}
|
||||
|
||||
{% block subcontent %}
|
||||
|
||||
|
@ -104,10 +96,10 @@
|
|||
{% if identity.local %}
|
||||
No posts yet.
|
||||
{% else %}
|
||||
No posts have been received/retrieved by this server yet.
|
||||
No posts have been received by this server yet.
|
||||
|
||||
{% if identity.profile_uri %}
|
||||
You might find historical posts at
|
||||
You may find historical posts at
|
||||
<a href="{{ identity.profile_uri }}">their original profile ➔</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
@ -120,7 +112,7 @@
|
|||
{% endif %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a class="button" href=".?page={{ page_obj.next_page_number }}" hx-get=".?page={{ page_obj.next_page_number }}" hx-select=".left-column > *:not(.view-options)" hx-target=".pagination" hx-swap="outerHTML" {% if config_identity.infinite_scroll %}hx-trigger="revealed"{% endif %}>Next Page</a>
|
||||
<a class="button" href=".?page={{ page_obj.next_page_number }}" hx-get=".?page={{ page_obj.next_page_number }}" hx-select=".left-column > *:not(.view-options)" hx-target=".pagination" hx-swap="outerHTML">Next Page</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,85 +1,62 @@
|
|||
<nav>
|
||||
<h3>Identity</h3>
|
||||
<a href="{% url "settings_profile" %}" {% if section == "profile" %}class="selected"{% endif %} title="Profile">
|
||||
{% if identity %}
|
||||
{% include "identity/_identity_banner.html" %}
|
||||
<h3>Identity Settings</h3>
|
||||
<a href="{% url "settings_profile" handle=identity.handle %}" {% if section == "profile" %}class="selected"{% endif %} title="Profile">
|
||||
<i class="fa-solid fa-user"></i>
|
||||
<span>Profile</span>
|
||||
</a>
|
||||
<a href="{% url "settings_interface" %}" {% if section == "interface" %}class="selected"{% endif %} title="Interface">
|
||||
<i class="fa-solid fa-display"></i>
|
||||
<span>Interface</span>
|
||||
<a href="{% url "settings_posting" handle=identity.handle %}" {% if section == "posting" %}class="selected"{% endif %} title="Posting">
|
||||
<i class="fa-solid fa-message"></i>
|
||||
<span>Posting</span>
|
||||
</a>
|
||||
<a href="{% url "settings_import_export" %}" {% if section == "importexport" %}class="selected"{% endif %} title="Interface">
|
||||
<a href="{% url "settings_import_export" handle=identity.handle %}" {% if section == "importexport" %}class="selected"{% endif %} title="Interface">
|
||||
<i class="fa-solid fa-cloud-arrow-up"></i>
|
||||
<span>Import/Export</span>
|
||||
</a>
|
||||
<a href="{% url "settings_tokens" handle=identity.handle %}" {% if section == "tokens" %}class="selected"{% endif %} title="Authorized Apps">
|
||||
<i class="fa-solid fa-window-restore"></i>
|
||||
<span>Authorized Apps</span>
|
||||
</a>
|
||||
<a href="{% url "settings_delete" handle=identity.handle %}" {% if section == "delete" %}class="selected"{% endif %} title="Delete Identity">
|
||||
<i class="fa-solid fa-user-slash"></i>
|
||||
<span>Delete Identity</span>
|
||||
</a>
|
||||
<hr>
|
||||
<h3>Tools</h3>
|
||||
<a href="{% url "settings_follows" handle=identity.handle %}" {% if section == "follows" %}class="selected"{% endif %} title="Follows">
|
||||
<i class="fa-solid fa-arrow-right-arrow-left"></i>
|
||||
<span>Follows</span>
|
||||
</a>
|
||||
<a href="{% url "compose" handle=identity.handle %}" {% if section == "compose" %}class="selected"{% endif %} title="Compose">
|
||||
<i class="fa-solid fa-pen-to-square"></i>
|
||||
<span>Compose</span>
|
||||
</a>
|
||||
<hr>
|
||||
<h3>User Settings</h3>
|
||||
<a href="{% url "settings" %}" title="Switch to User Settings">
|
||||
<i class="fa-solid fa-arrow-right"></i>
|
||||
<span>Go to User Settings</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<h3>Account</h3>
|
||||
<a href="{% url "settings_security" %}" {% if section == "security" %}class="selected"{% endif %} title="Login & Security">
|
||||
<i class="fa-solid fa-key"></i>
|
||||
<span>Login & Security</span>
|
||||
</a>
|
||||
<a href="{% url "settings_interface" %}" {% if section == "interface" %}class="selected"{% endif %} title="Interface">
|
||||
<i class="fa-solid fa-display"></i>
|
||||
<span>Interface</span>
|
||||
</a>
|
||||
<a href="{% url "logout" %}">
|
||||
<i class="fa-solid fa-right-from-bracket" title="Logout"></i>
|
||||
<span>Logout</span>
|
||||
</a>
|
||||
{% if request.user.moderator or request.user.admin %}
|
||||
<hr>
|
||||
<h3>Moderation</h3>
|
||||
<a href="{% url "admin_identities" %}" {% if section == "identities" %}class="selected"{% endif %} title="Identities">
|
||||
<i class="fa-solid fa-id-card"></i>
|
||||
<span>Identities</span>
|
||||
</a>
|
||||
<a href="{% url "admin_invites" %}" {% if section == "invites" %}class="selected"{% endif %} title="Invites">
|
||||
<i class="fa-solid fa-envelope"></i>
|
||||
<span>Invites</span>
|
||||
</a>
|
||||
<a href="{% url "admin_hashtags" %}" {% if section == "hashtags" %}class="selected"{% endif %} title="Hashtags">
|
||||
<i class="fa-solid fa-hashtag"></i>
|
||||
<span>Hashtags</span>
|
||||
</a>
|
||||
<a href="{% url "admin_emoji" %}" {% if section == "emoji" %}class="selected"{% endif %} title="Emoji">
|
||||
<i class="fa-solid fa-icons"></i>
|
||||
<span>Emoji</span>
|
||||
</a>
|
||||
<a href="{% url "admin_reports" %}" {% if section == "reports" %}class="selected"{% endif %} title="Reports">
|
||||
<i class="fa-solid fa-flag"></i>
|
||||
<span>Reports</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if request.user.admin %}
|
||||
<hr>
|
||||
<h3>Administration</h3>
|
||||
<a href="{% url "admin_basic" %}" {% if section == "basic" %}class="selected"{% endif %} title="Basic">
|
||||
<i class="fa-solid fa-book"></i>
|
||||
<span>Basic</span>
|
||||
</a>
|
||||
<a href="{% url "admin_policies" %}" {% if section == "policies" %}class="selected"{% endif %} title="Policies">
|
||||
<i class="fa-solid fa-file-lines"></i>
|
||||
<span>Policies</span>
|
||||
</a>
|
||||
<a href="{% url "admin_announcements" %}" {% if section == "announcements" %}class="selected"{% endif %} title="Announcements">
|
||||
<i class="fa-solid fa-bullhorn"></i>
|
||||
<span>Announcements</span>
|
||||
</a>
|
||||
<a href="{% url "admin_domains" %}" {% if section == "domains" %}class="selected"{% endif %} title="Domains">
|
||||
<i class="fa-solid fa-globe"></i>
|
||||
<span>Domains</span>
|
||||
</a>
|
||||
<a href="{% url "admin_federation" %}" {% if section == "federation" %}class="selected"{% endif %} title="Federation">
|
||||
<i class="fa-solid fa-diagram-project"></i>
|
||||
<span>Federation</span>
|
||||
</a>
|
||||
<a href="{% url "admin_users" %}" {% if section == "users" %}class="selected"{% endif %} title="Users">
|
||||
<i class="fa-solid fa-users"></i>
|
||||
<span>Users</span>
|
||||
</a>
|
||||
<a href="{% url "admin_stator" %}" {% if section == "stator" %}class="selected"{% endif %} title="Stator">
|
||||
<i class="fa-solid fa-clock-rotate-left"></i>
|
||||
<span>Stator</span>
|
||||
</a>
|
||||
<a href="/djadmin" title="Django Admin" class="danger">
|
||||
<i class="fa-solid fa-gear"></i>
|
||||
<span>Django Admin</span>
|
||||
<h3>Identity Settings</h3>
|
||||
<a href="/" title="Go to Identity Select">
|
||||
<i class="fa-solid fa-arrow-right"></i>
|
||||
<span>Go to Identity Select</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
|
|
|
@ -2,6 +2,14 @@
|
|||
|
||||
{% block title %}{% block subtitle %}{% endblock %} - Settings{% endblock %}
|
||||
|
||||
{% block right_content %}
|
||||
{% block body_class %}wide{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="settings">
|
||||
{% include "settings/_menu.html" %}
|
||||
<div class="settings-content">
|
||||
{% block settings_content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
31
templates/settings/delete.html
Normal file
31
templates/settings/delete.html
Normal file
|
@ -0,0 +1,31 @@
|
|||
{% extends "settings/base.html" %}
|
||||
|
||||
{% block subtitle %}Token{% endblock %}
|
||||
|
||||
{% block settings_content %}
|
||||
<h1>Deleting {{ identity.handle }}</h1>
|
||||
<form action="." method="POST">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
<legend>Confirmation</legend>
|
||||
<p>
|
||||
Deleting this account is <i>permanent and irreversible</i>. Once you
|
||||
start the deletion process, all your posts, interactions and profile
|
||||
data will be gone, and other servers will be contacted to remove
|
||||
your data as well.
|
||||
</p>
|
||||
<p>
|
||||
Some other servers in the Fediverse may maintain a copy of your
|
||||
profile and posts; we do not control them, and cannot speed up
|
||||
or affect your data's removal. Most data will disappear from
|
||||
the network after a couple of weeks.
|
||||
</p>
|
||||
{% include "forms/_field.html" with field=form.confirmation %}
|
||||
</fieldset>
|
||||
|
||||
<div class="buttons">
|
||||
<a href="{% url "settings" handle=identity.handle %}" class="button secondary left">Cancel</a>
|
||||
<button class="danger">Delete {{ identity.handle }}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
56
templates/settings/follows.html
Normal file
56
templates/settings/follows.html
Normal file
|
@ -0,0 +1,56 @@
|
|||
{% extends "settings/base.html" %}
|
||||
|
||||
{% block subtitle %}Follows{% endblock %}
|
||||
|
||||
{% block settings_content %}
|
||||
<div class="view-options">
|
||||
{% if inbound %}
|
||||
<a href=".">Following ({{ num_outbound }})</a>
|
||||
<a href="." class="selected">Followers ({{ num_inbound }})</a>
|
||||
{% else %}
|
||||
<a href=".?inbound=true" class="selected">Following ({{ num_outbound }})</a>
|
||||
<a href=".?inbound=true">Followers ({{ num_inbound }})</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<table class="items">
|
||||
{% for other_identity in page_obj %}
|
||||
<tr>
|
||||
<td class="icon">
|
||||
<a href="{{ other_identity.urls.view }}" class="overlay"></a>
|
||||
<img
|
||||
src="{{ other_identity.local_icon_url.relative }}"
|
||||
class="icon"
|
||||
alt="Avatar for {{ other_identity.name_or_handle }}"
|
||||
loading="lazy"
|
||||
data-handle="{{ other_identity.name_or_handle }}"
|
||||
_="on error set my.src to generate_avatar(@data-handle)"
|
||||
>
|
||||
</td>
|
||||
<td class="name">
|
||||
<a href="{{ other_identity.urls.view }}" class="overlay"></a>
|
||||
{{ other_identity.html_name_or_handle }}
|
||||
<small>@{{ other_identity.handle }}</small>
|
||||
</td>
|
||||
<td class="stat">
|
||||
{% if other_identity.id in outbound_ids %}
|
||||
<span class="pill">Following</span>
|
||||
{% endif %}
|
||||
{% if other_identity.id in inbound_ids %}
|
||||
<span class="pill">Follows You</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="actions">
|
||||
<a href="{{ other_identity.urls.view }}" title="View"><i class="fa-solid fa-eye"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr class="empty"><td>You {% if inbound %}have no followers{% else %}are not following anyone{% endif %}.</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% if inbound %}
|
||||
{% include "admin/_pagination.html" with nouns="follower,followers" %}
|
||||
{% else %}
|
||||
{% include "admin/_pagination.html" with nouns="follow,follows" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
{% block subtitle %}Import/Export{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." method="POST" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
|
@ -32,7 +32,7 @@
|
|||
<small>{{ numbers.outbound_follows }} {{ numbers.outbound_follows|pluralize:"follow,follows" }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url "settings_export_following_csv" %}">Download CSV</a>
|
||||
<a href="{% url "settings_export_following_csv" handle=identity.handle %}">Download CSV</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -41,7 +41,7 @@
|
|||
<small>{{ numbers.inbound_follows }} {{ numbers.inbound_follows|pluralize:"follower,followers" }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url "settings_export_followers_csv" %}">Download CSV</a>
|
||||
<a href="{% url "settings_export_followers_csv" handle=identity.handle %}">Download CSV</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
{% block subtitle %}Login & Security{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." method="POST">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue