mirror of
https://github.com/jointakahe/takahe.git
synced 2024-11-26 01:01:00 +00:00
Start of new UI refactor
This commit is contained in:
parent
7331591432
commit
5d68e3aaac
65 changed files with 519 additions and 1000 deletions
|
@ -16,12 +16,12 @@ from activities.models import (
|
||||||
from core.files import blurhash_image, resize_image
|
from core.files import blurhash_image, resize_image
|
||||||
from core.html import FediverseHtmlParser
|
from core.html import FediverseHtmlParser
|
||||||
from core.models import Config
|
from core.models import Config
|
||||||
from users.decorators import identity_required
|
from users.shortcuts import by_handle_or_404
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(identity_required, name="dispatch")
|
@method_decorator(login_required, name="dispatch")
|
||||||
class Compose(FormView):
|
class Compose(FormView):
|
||||||
|
|
||||||
template_name = "activities/compose.html"
|
template_name = "activities/compose.html"
|
||||||
|
|
||||||
class form_class(forms.Form):
|
class form_class(forms.Form):
|
||||||
|
@ -54,9 +54,9 @@ class Compose(FormView):
|
||||||
)
|
)
|
||||||
reply_to = forms.CharField(widget=forms.HiddenInput(), required=False)
|
reply_to = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||||
|
|
||||||
def __init__(self, request, *args, **kwargs):
|
def __init__(self, identity, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.request = request
|
self.identity = identity
|
||||||
self.fields["text"].widget.attrs[
|
self.fields["text"].widget.attrs[
|
||||||
"_"
|
"_"
|
||||||
] = rf"""
|
] = rf"""
|
||||||
|
@ -83,7 +83,7 @@ class Compose(FormView):
|
||||||
def clean_text(self):
|
def clean_text(self):
|
||||||
text = self.cleaned_data.get("text")
|
text = self.cleaned_data.get("text")
|
||||||
# Check minimum interval
|
# Check minimum interval
|
||||||
last_post = self.request.identity.posts.order_by("-created").first()
|
last_post = self.identity.posts.order_by("-created").first()
|
||||||
if (
|
if (
|
||||||
last_post
|
last_post
|
||||||
and (timezone.now() - last_post.created).total_seconds()
|
and (timezone.now() - last_post.created).total_seconds()
|
||||||
|
@ -103,7 +103,7 @@ class Compose(FormView):
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def get_form(self, form_class=None):
|
def get_form(self, form_class=None):
|
||||||
return self.form_class(request=self.request, **self.get_form_kwargs())
|
return self.form_class(identity=self.identity, **self.get_form_kwargs())
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
initial = super().get_initial()
|
initial = super().get_initial()
|
||||||
|
@ -119,20 +119,20 @@ class Compose(FormView):
|
||||||
else:
|
else:
|
||||||
initial[
|
initial[
|
||||||
"visibility"
|
"visibility"
|
||||||
] = self.request.identity.config_identity.default_post_visibility
|
] = self.identity.config_identity.default_post_visibility
|
||||||
if self.reply_to:
|
if self.reply_to:
|
||||||
initial["reply_to"] = self.reply_to.pk
|
initial["reply_to"] = self.reply_to.pk
|
||||||
if self.reply_to.visibility == Post.Visibilities.public:
|
if self.reply_to.visibility == Post.Visibilities.public:
|
||||||
initial[
|
initial[
|
||||||
"visibility"
|
"visibility"
|
||||||
] = self.request.identity.config_identity.default_reply_visibility
|
] = self.identity.config_identity.default_reply_visibility
|
||||||
else:
|
else:
|
||||||
initial["visibility"] = self.reply_to.visibility
|
initial["visibility"] = self.reply_to.visibility
|
||||||
initial["content_warning"] = self.reply_to.summary
|
initial["content_warning"] = self.reply_to.summary
|
||||||
# Build a set of mentions for the content to start as
|
# Build a set of mentions for the content to start as
|
||||||
mentioned = {self.reply_to.author}
|
mentioned = {self.reply_to.author}
|
||||||
mentioned.update(self.reply_to.mentions.all())
|
mentioned.update(self.reply_to.mentions.all())
|
||||||
mentioned.discard(self.request.identity)
|
mentioned.discard(self.identity)
|
||||||
initial["text"] = "".join(
|
initial["text"] = "".join(
|
||||||
f"@{identity.handle} "
|
f"@{identity.handle} "
|
||||||
for identity in mentioned
|
for identity in mentioned
|
||||||
|
@ -158,7 +158,7 @@ class Compose(FormView):
|
||||||
self.post_obj.transition_perform(PostStates.edited)
|
self.post_obj.transition_perform(PostStates.edited)
|
||||||
else:
|
else:
|
||||||
post = Post.create_local(
|
post = Post.create_local(
|
||||||
author=self.request.identity,
|
author=self.identity,
|
||||||
content=form.cleaned_data["text"],
|
content=form.cleaned_data["text"],
|
||||||
summary=form.cleaned_data.get("content_warning"),
|
summary=form.cleaned_data.get("content_warning"),
|
||||||
visibility=form.cleaned_data["visibility"],
|
visibility=form.cleaned_data["visibility"],
|
||||||
|
@ -166,17 +166,14 @@ class Compose(FormView):
|
||||||
attachments=attachments,
|
attachments=attachments,
|
||||||
)
|
)
|
||||||
# Add their own timeline event for immediate visibility
|
# Add their own timeline event for immediate visibility
|
||||||
TimelineEvent.add_post(self.request.identity, post)
|
TimelineEvent.add_post(self.identity, post)
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
|
|
||||||
def dispatch(self, request, handle=None, post_id=None, *args, **kwargs):
|
def dispatch(self, request, handle=None, post_id=None, *args, **kwargs):
|
||||||
|
self.identity = by_handle_or_404(self.request, handle, local=True, fetch=False)
|
||||||
self.post_obj = None
|
self.post_obj = None
|
||||||
if handle and post_id:
|
if handle and post_id:
|
||||||
# Make sure the request identity owns the post!
|
self.post_obj = get_object_or_404(self.identity.posts, pk=post_id)
|
||||||
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
|
# Grab the reply-to post info now
|
||||||
self.reply_to = None
|
self.reply_to = None
|
||||||
|
@ -192,12 +189,13 @@ class Compose(FormView):
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context["reply_to"] = self.reply_to
|
context["reply_to"] = self.reply_to
|
||||||
|
context["identity"] = self.identity
|
||||||
if self.post_obj:
|
if self.post_obj:
|
||||||
context["post"] = self.post_obj
|
context["post"] = self.post_obj
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(identity_required, name="dispatch")
|
@method_decorator(login_required, name="dispatch")
|
||||||
class ImageUpload(FormView):
|
class ImageUpload(FormView):
|
||||||
"""
|
"""
|
||||||
Handles image upload - returns a new input type hidden to embed in
|
Handles image upload - returns a new input type hidden to embed in
|
||||||
|
@ -267,7 +265,7 @@ class ImageUpload(FormView):
|
||||||
height=main_file.image.height,
|
height=main_file.image.height,
|
||||||
name=form.cleaned_data.get("description"),
|
name=form.cleaned_data.get("description"),
|
||||||
state=PostAttachmentStates.fetched,
|
state=PostAttachmentStates.fetched,
|
||||||
author=self.request.identity,
|
author=self.identity,
|
||||||
)
|
)
|
||||||
|
|
||||||
attachment.file.save(
|
attachment.file.save(
|
||||||
|
|
|
@ -2,11 +2,11 @@ from django.db import models
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.generic import ListView
|
from django.views.generic import ListView
|
||||||
|
|
||||||
from users.decorators import identity_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from users.models import Follow, FollowStates, IdentityStates
|
from users.models import Follow, FollowStates, IdentityStates
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(identity_required, name="dispatch")
|
@method_decorator(login_required, name="dispatch")
|
||||||
class Follows(ListView):
|
class Follows(ListView):
|
||||||
"""
|
"""
|
||||||
Shows followers/follows.
|
Shows followers/follows.
|
||||||
|
|
|
@ -4,10 +4,10 @@ from django.utils.decorators import method_decorator
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
from activities.models.hashtag import Hashtag
|
from activities.models.hashtag import Hashtag
|
||||||
from users.decorators import identity_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(identity_required, name="dispatch")
|
@method_decorator(login_required, name="dispatch")
|
||||||
class HashtagFollow(View):
|
class HashtagFollow(View):
|
||||||
"""
|
"""
|
||||||
Follows/unfollows a hashtag with the current identity
|
Follows/unfollows a hashtag with the current identity
|
||||||
|
|
|
@ -9,7 +9,7 @@ from activities.models import Post, PostInteraction, PostStates
|
||||||
from activities.services import PostService
|
from activities.services import PostService
|
||||||
from core.decorators import cache_page_by_ap_json
|
from core.decorators import cache_page_by_ap_json
|
||||||
from core.ld import canonicalise
|
from core.ld import canonicalise
|
||||||
from users.decorators import identity_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from users.models import Identity
|
from users.models import Identity
|
||||||
from users.shortcuts import by_handle_or_404
|
from users.shortcuts import by_handle_or_404
|
||||||
|
|
||||||
|
@ -19,7 +19,6 @@ from users.shortcuts import by_handle_or_404
|
||||||
)
|
)
|
||||||
@method_decorator(vary_on_headers("Accept"), name="dispatch")
|
@method_decorator(vary_on_headers("Accept"), name="dispatch")
|
||||||
class Individual(TemplateView):
|
class Individual(TemplateView):
|
||||||
|
|
||||||
template_name = "activities/post.html"
|
template_name = "activities/post.html"
|
||||||
|
|
||||||
identity: Identity
|
identity: Identity
|
||||||
|
@ -32,7 +31,7 @@ class Individual(TemplateView):
|
||||||
self.post_obj = get_object_or_404(
|
self.post_obj = get_object_or_404(
|
||||||
PostService.queryset()
|
PostService.queryset()
|
||||||
.filter(author=self.identity)
|
.filter(author=self.identity)
|
||||||
.visible_to(request.identity, include_replies=True),
|
.unlisted(include_replies=True),
|
||||||
pk=post_id,
|
pk=post_id,
|
||||||
)
|
)
|
||||||
if self.post_obj.state in [PostStates.deleted, PostStates.deleted_fanned_out]:
|
if self.post_obj.state in [PostStates.deleted, PostStates.deleted_fanned_out]:
|
||||||
|
@ -48,18 +47,12 @@ class Individual(TemplateView):
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
ancestors, descendants = PostService(self.post_obj).context(
|
ancestors, descendants = PostService(self.post_obj).context(None)
|
||||||
self.request.identity
|
|
||||||
)
|
|
||||||
|
|
||||||
context.update(
|
context.update(
|
||||||
{
|
{
|
||||||
"identity": self.identity,
|
"identity": self.identity,
|
||||||
"post": self.post_obj,
|
"post": self.post_obj,
|
||||||
"interactions": PostInteraction.get_post_interactions(
|
|
||||||
[self.post_obj] + ancestors + descendants,
|
|
||||||
self.request.identity,
|
|
||||||
),
|
|
||||||
"link_original": True,
|
"link_original": True,
|
||||||
"ancestors": ancestors,
|
"ancestors": ancestors,
|
||||||
"descendants": descendants,
|
"descendants": descendants,
|
||||||
|
@ -78,108 +71,7 @@ class Individual(TemplateView):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(identity_required, name="dispatch")
|
@method_decorator(login_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):
|
class Delete(TemplateView):
|
||||||
"""
|
"""
|
||||||
Deletes a post
|
Deletes a post
|
||||||
|
|
|
@ -6,48 +6,30 @@ from django.views.generic import ListView, TemplateView
|
||||||
from activities.models import Hashtag, PostInteraction, TimelineEvent
|
from activities.models import Hashtag, PostInteraction, TimelineEvent
|
||||||
from activities.services import TimelineService
|
from activities.services import TimelineService
|
||||||
from core.decorators import cache_page
|
from core.decorators import cache_page
|
||||||
from users.decorators import identity_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from users.models import Bookmark, HashtagFollow
|
from users.models import Bookmark, HashtagFollow, Identity
|
||||||
|
|
||||||
from .compose import Compose
|
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(identity_required, name="dispatch")
|
@method_decorator(login_required, name="dispatch")
|
||||||
class Home(TemplateView):
|
class Home(TemplateView):
|
||||||
|
"""
|
||||||
|
Homepage for logged-in users - shows identities primarily.
|
||||||
|
"""
|
||||||
|
|
||||||
template_name = "activities/home.html"
|
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):
|
def get_context_data(self):
|
||||||
events = TimelineService(self.request.identity).home()
|
return {
|
||||||
paginator = Paginator(events, 25)
|
"identities": Identity.objects.filter(
|
||||||
page_number = self.request.GET.get("page")
|
users__pk=self.request.user.pk
|
||||||
event_page = paginator.get_page(page_number)
|
).order_by("created"),
|
||||||
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 context
|
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(
|
@method_decorator(
|
||||||
cache_page("cache_timeout_page_timeline", public_only=True), name="dispatch"
|
cache_page("cache_timeout_page_timeline", public_only=True), name="dispatch"
|
||||||
)
|
)
|
||||||
class Tag(ListView):
|
class Tag(ListView):
|
||||||
|
|
||||||
template_name = "activities/tag.html"
|
template_name = "activities/tag.html"
|
||||||
extra_context = {
|
extra_context = {
|
||||||
"current_page": "tag",
|
"current_page": "tag",
|
||||||
|
@ -86,7 +68,6 @@ class Tag(ListView):
|
||||||
cache_page("cache_timeout_page_timeline", public_only=True), name="dispatch"
|
cache_page("cache_timeout_page_timeline", public_only=True), name="dispatch"
|
||||||
)
|
)
|
||||||
class Local(ListView):
|
class Local(ListView):
|
||||||
|
|
||||||
template_name = "activities/local.html"
|
template_name = "activities/local.html"
|
||||||
extra_context = {
|
extra_context = {
|
||||||
"current_page": "local",
|
"current_page": "local",
|
||||||
|
@ -108,9 +89,8 @@ class Local(ListView):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(identity_required, name="dispatch")
|
@method_decorator(login_required, name="dispatch")
|
||||||
class Federated(ListView):
|
class Federated(ListView):
|
||||||
|
|
||||||
template_name = "activities/federated.html"
|
template_name = "activities/federated.html"
|
||||||
extra_context = {
|
extra_context = {
|
||||||
"current_page": "federated",
|
"current_page": "federated",
|
||||||
|
@ -132,9 +112,8 @@ class Federated(ListView):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(identity_required, name="dispatch")
|
@method_decorator(login_required, name="dispatch")
|
||||||
class Notifications(ListView):
|
class Notifications(ListView):
|
||||||
|
|
||||||
template_name = "activities/notifications.html"
|
template_name = "activities/notifications.html"
|
||||||
extra_context = {
|
extra_context = {
|
||||||
"current_page": "notifications",
|
"current_page": "notifications",
|
||||||
|
|
|
@ -6,8 +6,7 @@ from django.http import JsonResponse
|
||||||
|
|
||||||
def identity_required(function):
|
def identity_required(function):
|
||||||
"""
|
"""
|
||||||
API version of the identity_required decorator that just makes sure the
|
Makes sure the token is tied to an identity, not an app only.
|
||||||
token is tied to one, not an app only.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@wraps(function)
|
@wraps(function)
|
||||||
|
|
|
@ -4,9 +4,6 @@ from core.models import Config
|
||||||
def config_context(request):
|
def config_context(request):
|
||||||
return {
|
return {
|
||||||
"config": Config.system,
|
"config": Config.system,
|
||||||
"config_identity": (
|
|
||||||
request.identity.config_identity if request.identity else None
|
|
||||||
),
|
|
||||||
"top_section": request.path.strip("/").split("/")[0],
|
"top_section": request.path.strip("/").split("/")[0],
|
||||||
"opengraph_defaults": {
|
"opengraph_defaults": {
|
||||||
"og:site_name": Config.system.site_name,
|
"og:site_name": Config.system.site_name,
|
||||||
|
|
|
@ -27,7 +27,6 @@ def homepage(request):
|
||||||
|
|
||||||
@method_decorator(cache_page(public_only=True), name="dispatch")
|
@method_decorator(cache_page(public_only=True), name="dispatch")
|
||||||
class About(TemplateView):
|
class About(TemplateView):
|
||||||
|
|
||||||
template_name = "about.html"
|
template_name = "about.html"
|
||||||
|
|
||||||
def get_context_data(self):
|
def get_context_data(self):
|
||||||
|
@ -87,46 +86,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):
|
class FlatPage(TemplateView):
|
||||||
"""
|
"""
|
||||||
Serves a "flat page" from a config option,
|
Serves a "flat page" from a config option,
|
||||||
|
|
|
@ -147,13 +147,8 @@ body {
|
||||||
main {
|
main {
|
||||||
width: 1100px;
|
width: 1100px;
|
||||||
margin: 20px auto;
|
margin: 20px auto;
|
||||||
box-shadow: 0 0 var(--size-main-shadow) var(--color-main-shadow);
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-sidebar main {
|
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
max-width: 800px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
|
@ -172,10 +167,8 @@ footer a {
|
||||||
|
|
||||||
header {
|
header {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 50px;
|
height: 42px;
|
||||||
}
|
margin-top: -20px;
|
||||||
|
|
||||||
.no-sidebar header {
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,11 +177,11 @@ header .logo {
|
||||||
font-family: "Raleway";
|
font-family: "Raleway";
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
background: var(--color-highlight);
|
background: var(--color-highlight);
|
||||||
border-radius: 5px 0 0 0;
|
border-radius: 0 0 5px 5px;
|
||||||
text-transform: lowercase;
|
text-transform: lowercase;
|
||||||
padding: 10px 11px 9px 10px;
|
padding: 6px 8px 5px 7px;
|
||||||
height: 50px;
|
height: 42px;
|
||||||
font-size: 130%;
|
font-size: 120%;
|
||||||
color: var(--color-text-in-highlight);
|
color: var(--color-text-in-highlight);
|
||||||
border-bottom: 3px solid rgba(0, 0, 0, 0);
|
border-bottom: 3px solid rgba(0, 0, 0, 0);
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
@ -196,10 +189,6 @@ header .logo {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-sidebar header .logo {
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
header .logo:hover {
|
header .logo:hover {
|
||||||
border-bottom: 3px solid rgba(255, 255, 255, 0.3);
|
border-bottom: 3px solid rgba(255, 255, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
@ -211,31 +200,25 @@ header .logo img {
|
||||||
}
|
}
|
||||||
|
|
||||||
header menu {
|
header menu {
|
||||||
flex-grow: 1;
|
flex-grow: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-sidebar header menu {
|
|
||||||
flex-grow: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
header menu a {
|
header menu a {
|
||||||
padding: 10px 20px 4px 20px;
|
padding: 6px 10px 4px 10px;
|
||||||
color: var(--color-text-main);
|
color: var(--color-text-main);
|
||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
border-bottom: 3px solid rgba(0, 0, 0, 0);
|
border-bottom: 3px solid rgba(0, 0, 0, 0);
|
||||||
|
margin: 0 2px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-sidebar header menu a {
|
header menu a.logo {
|
||||||
margin: 0 10px;
|
width: auto;
|
||||||
}
|
margin-right: 7px;
|
||||||
|
|
||||||
body.has-banner header menu a {
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
border-right: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
header menu a:hover,
|
header menu a:hover,
|
||||||
|
@ -243,10 +226,10 @@ header menu a.selected {
|
||||||
border-bottom: 3px solid var(--color-highlight);
|
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);
|
border-bottom: 3px solid rgba(0, 0, 0, 0);
|
||||||
background-color: var(--color-bg-menu);
|
background-color: var(--color-bg-menu);
|
||||||
border-radius: 5px;
|
border-radius: 0 0 5px 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
header menu a i {
|
header menu a i {
|
||||||
|
@ -279,26 +262,6 @@ header menu .gap {
|
||||||
flex-grow: 1;
|
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 {
|
header menu a img {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
@ -313,7 +276,7 @@ header menu a small {
|
||||||
}
|
}
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
padding: 10px 10px 20px 0;
|
padding: 10px 0px 20px 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav hr {
|
nav hr {
|
||||||
|
@ -334,23 +297,23 @@ nav h3:first-child {
|
||||||
nav a {
|
nav a {
|
||||||
display: block;
|
display: block;
|
||||||
color: var(--color-text-dull);
|
color: var(--color-text-dull);
|
||||||
padding: 7px 18px 7px 13px;
|
padding: 7px 18px 7px 10px;
|
||||||
border-left: 3px solid transparent;
|
border-right: 3px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav a.selected {
|
nav a.selected {
|
||||||
color: var(--color-text-main);
|
color: var(--color-text-main);
|
||||||
background: var(--color-bg-main);
|
background: var(--color-bg-main);
|
||||||
border-radius: 0 5px 5px 0;
|
border-radius: 5px 0 0 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav a:hover {
|
nav a:hover {
|
||||||
color: var(--color-text-main);
|
color: var(--color-text-main);
|
||||||
border-left: 3px solid var(--color-highlight);
|
border-right: 3px solid var(--color-highlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
nav a.selected:hover {
|
nav a.selected:hover {
|
||||||
border-left: 3px solid transparent;
|
border-right: 3px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav a.danger {
|
nav a.danger {
|
||||||
|
@ -368,48 +331,55 @@ nav a i {
|
||||||
display: inline-block;
|
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 */
|
/* Left-right columns */
|
||||||
|
|
||||||
.columns {
|
.settings {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-column {
|
.settings .settings-content {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
padding: 15px;
|
padding: 0 15px 15px 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-column h1 {
|
.settings nav {
|
||||||
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);
|
width: var(--md-sidebar-width);
|
||||||
background: var(--color-bg-menu);
|
background: var(--color-bg-menu);
|
||||||
border-radius: 0 0 5px 0;
|
border-radius: 0 0 5px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-column h2 {
|
.settings nav h2 {
|
||||||
background: var(--color-highlight);
|
background: var(--color-highlight);
|
||||||
color: var(--color-text-in-highlight);
|
color: var(--color-text-in-highlight);
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
|
@ -418,12 +388,6 @@ nav a i {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-column footer {
|
|
||||||
padding: 0 10px 20px 10px;
|
|
||||||
font-size: 90%;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
img.emoji {
|
img.emoji {
|
||||||
height: var(--emoji-height);
|
height: var(--emoji-height);
|
||||||
vertical-align: baseline;
|
vertical-align: baseline;
|
||||||
|
@ -433,27 +397,52 @@ img.emoji {
|
||||||
content: "…";
|
content: "…";
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Generic markdown styling and sections */
|
/* Generic styling and sections */
|
||||||
|
|
||||||
.no-sidebar section {
|
section {
|
||||||
max-width: 700px;
|
max-width: 700px;
|
||||||
background: var(--color-bg-box);
|
background: var(--color-bg-box);
|
||||||
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1);
|
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1);
|
||||||
margin: 25px auto 45px auto;
|
margin: 0 auto 45px auto;
|
||||||
padding: 5px 15px;
|
padding: 5px 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-sidebar section:last-of-type {
|
section:first-of-type {
|
||||||
margin-bottom: 10px;
|
margin-top: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-sidebar section.shell {
|
section.invisible {
|
||||||
background: none;
|
background: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
padding: 0;
|
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;
|
max-width: 700px;
|
||||||
margin: 25px auto 5px auto;
|
margin: 25px auto 5px auto;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
@ -462,7 +451,7 @@ img.emoji {
|
||||||
color: var(--color-text-main);
|
color: var(--color-text-main);
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-sidebar h1+section {
|
h1+section {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -493,9 +482,7 @@ p.authorization-code {
|
||||||
|
|
||||||
.icon-menu .option {
|
.icon-menu .option {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 0 20px 0;
|
margin: 0 0 10px 0;
|
||||||
background: var(--color-bg-box);
|
|
||||||
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1);
|
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
|
@ -596,6 +583,40 @@ p.authorization-code {
|
||||||
color: var(--color-text-dull);
|
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 */
|
/* Item tables */
|
||||||
|
|
||||||
table.items {
|
table.items {
|
||||||
|
@ -685,7 +706,7 @@ table.items td.actions a.danger:hover {
|
||||||
|
|
||||||
/* Forms */
|
/* Forms */
|
||||||
|
|
||||||
.no-sidebar form {
|
section form {
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
margin: 40px auto;
|
margin: 40px auto;
|
||||||
}
|
}
|
||||||
|
@ -1174,42 +1195,48 @@ blockquote {
|
||||||
|
|
||||||
/* Identities */
|
/* Identities */
|
||||||
|
|
||||||
h1.identity {
|
section.identity {
|
||||||
margin: 0 0 20px 0;
|
overflow: hidden;
|
||||||
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1.identity .banner {
|
section.identity .banner {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
display: block;
|
display: block;
|
||||||
width: calc(100% + 30px);
|
width: calc(100% + 30px);
|
||||||
margin: -65px -15px 20px -15px;
|
margin: -5px -15px 20px -15px;
|
||||||
border-radius: 5px 0 0 0;
|
border-radius: 5px 0 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1.identity .icon {
|
section.identity .icon {
|
||||||
width: 80px;
|
width: 80px;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
float: left;
|
float: left;
|
||||||
margin: 0 20px 0 0;
|
margin: 0 20px 15px 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1.identity .emoji {
|
section.identity .emoji {
|
||||||
height: var(--emoji-height);
|
height: var(--emoji-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
h1.identity small {
|
section.identity h1 {
|
||||||
|
margin: 30px 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.identity small {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 60%;
|
font-size: 100%;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
color: var(--color-text-dull);
|
color: var(--color-text-dull);
|
||||||
margin: -5px 0 0 0;
|
margin: -5px 0 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bio {
|
section.identity .bio {
|
||||||
margin: 0 0 20px 0;
|
clear: left;
|
||||||
|
margin: 0 0 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bio .emoji {
|
.bio .emoji {
|
||||||
|
@ -1220,14 +1247,19 @@ h1.identity small {
|
||||||
margin: 0 0 10px 0;
|
margin: 0 0 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.identity-metadata {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.identity-metadata .metadata-pair {
|
.identity-metadata .metadata-pair {
|
||||||
display: block;
|
display: inline-block;
|
||||||
margin: 0px 0 10px 0;
|
width: 300px;
|
||||||
background: var(--color-bg-box);
|
margin: 0px 0 5px 0;
|
||||||
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1);
|
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
padding: 10px 20px;
|
|
||||||
border: 2px solid rgba(255, 255, 255, 0);
|
border: 2px solid rgba(255, 255, 255, 0);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -1235,24 +1267,18 @@ h1.identity small {
|
||||||
|
|
||||||
.identity-metadata .metadata-pair .metadata-name {
|
.identity-metadata .metadata-pair .metadata-name {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
min-width: 80px;
|
min-width: 90px;
|
||||||
margin-right: 15px;
|
margin-right: 15px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
color: var(--color-text-dull);
|
color: var(--color-text-dull);
|
||||||
}
|
}
|
||||||
|
|
||||||
.identity-metadata .metadata-pair .metadata-name::after {
|
|
||||||
padding-left: 3px;
|
|
||||||
|
|
||||||
color: var(--color-text-dull);
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-note {
|
.system-note {
|
||||||
background: var(--color-bg-menu);
|
background: var(--color-bg-menu);
|
||||||
color: var(--color-text-dull);
|
color: var(--color-text-dull);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
padding: 5px 8px;
|
padding: 5px 8px;
|
||||||
margin: 15px 0;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.system-note a {
|
.system-note a {
|
||||||
|
@ -1321,13 +1347,12 @@ table.metadata td .emoji {
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-options {
|
.view-options {
|
||||||
margin: 0 0 10px 0px;
|
margin-bottom: 10px;
|
||||||
|
padding: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
background: none;
|
||||||
|
box-shadow: none;
|
||||||
.view-options.follows {
|
|
||||||
margin: 0 0 20px 0px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-options a:not(.button) {
|
.view-options a:not(.button) {
|
||||||
|
@ -1405,6 +1430,11 @@ table.metadata td .emoji {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.identity-banner img.icon {
|
||||||
|
max-width: 64px;
|
||||||
|
max-height: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Posts */
|
/* Posts */
|
||||||
|
|
||||||
|
@ -1776,28 +1806,13 @@ form .post {
|
||||||
padding: 0;
|
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 {
|
main {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 20px auto 20px auto;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-sidebar main {
|
|
||||||
margin: 20px auto 20px auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post .attachments a.image img {
|
.post .attachments a.image img {
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -220,7 +220,6 @@ MIDDLEWARE = [
|
||||||
"core.middleware.HeadersMiddleware",
|
"core.middleware.HeadersMiddleware",
|
||||||
"core.middleware.ConfigLoadingMiddleware",
|
"core.middleware.ConfigLoadingMiddleware",
|
||||||
"api.middleware.ApiTokenMiddleware",
|
"api.middleware.ApiTokenMiddleware",
|
||||||
"users.middleware.IdentityMiddleware",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = "takahe.urls"
|
ROOT_URLCONF = "takahe.urls"
|
||||||
|
|
|
@ -29,7 +29,6 @@ from users.views import (
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", core.homepage),
|
path("", core.homepage),
|
||||||
path("robots.txt", core.RobotsTxt.as_view()),
|
path("robots.txt", core.RobotsTxt.as_view()),
|
||||||
path("manifest.json", core.AppManifest.as_view()),
|
|
||||||
# Activity views
|
# Activity views
|
||||||
path("notifications/", timelines.Notifications.as_view(), name="notifications"),
|
path("notifications/", timelines.Notifications.as_view(), name="notifications"),
|
||||||
path("local/", timelines.Local.as_view(), name="local"),
|
path("local/", timelines.Local.as_view(), name="local"),
|
||||||
|
@ -57,27 +56,32 @@ urlpatterns = [
|
||||||
name="settings_security",
|
name="settings_security",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"settings/profile/",
|
"@<handle>/settings/",
|
||||||
|
settings.SettingsRoot.as_view(),
|
||||||
|
name="settings",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"@<handle>/settings/profile/",
|
||||||
settings.ProfilePage.as_view(),
|
settings.ProfilePage.as_view(),
|
||||||
name="settings_profile",
|
name="settings_profile",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"settings/interface/",
|
"@<handle>/settings/interface/",
|
||||||
settings.InterfacePage.as_view(),
|
settings.InterfacePage.as_view(),
|
||||||
name="settings_interface",
|
name="settings_interface",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"settings/import_export/",
|
"@<handle>/settings/import_export/",
|
||||||
settings.ImportExportPage.as_view(),
|
settings.ImportExportPage.as_view(),
|
||||||
name="settings_import_export",
|
name="settings_import_export",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"settings/import_export/following.csv",
|
"@<handle>/settings/import_export/following.csv",
|
||||||
settings.CsvFollowing.as_view(),
|
settings.CsvFollowing.as_view(),
|
||||||
name="settings_export_following_csv",
|
name="settings_export_following_csv",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"settings/import_export/followers.csv",
|
"@<handle>/settings/import_export/followers.csv",
|
||||||
settings.CsvFollowers.as_view(),
|
settings.CsvFollowers.as_view(),
|
||||||
name="settings_export_followers_csv",
|
name="settings_export_followers_csv",
|
||||||
),
|
),
|
||||||
|
@ -242,21 +246,13 @@ urlpatterns = [
|
||||||
path("@<handle>/following/", identity.IdentityFollows.as_view(inbound=False)),
|
path("@<handle>/following/", identity.IdentityFollows.as_view(inbound=False)),
|
||||||
path("@<handle>/followers/", identity.IdentityFollows.as_view(inbound=True)),
|
path("@<handle>/followers/", identity.IdentityFollows.as_view(inbound=True)),
|
||||||
# Posts
|
# Posts
|
||||||
path("compose/", compose.Compose.as_view(), name="compose"),
|
path("@<handle>/compose/", compose.Compose.as_view(), name="compose"),
|
||||||
path(
|
path(
|
||||||
"compose/image_upload/",
|
"@<handle>/compose/image_upload/",
|
||||||
compose.ImageUpload.as_view(),
|
compose.ImageUpload.as_view(),
|
||||||
name="compose_image_upload",
|
name="compose_image_upload",
|
||||||
),
|
),
|
||||||
path("@<handle>/posts/<int:post_id>/", posts.Individual.as_view()),
|
path("@<handle>/posts/<int:post_id>/", posts.Individual.as_view()),
|
||||||
path("@<handle>/posts/<int:post_id>/like/", posts.Like.as_view()),
|
|
||||||
path("@<handle>/posts/<int:post_id>/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>/delete/", posts.Delete.as_view()),
|
||||||
path("@<handle>/posts/<int:post_id>/report/", report.SubmitReport.as_view()),
|
path("@<handle>/posts/<int:post_id>/report/", report.SubmitReport.as_view()),
|
||||||
path("@<handle>/posts/<int:post_id>/edit/", compose.Compose.as_view()),
|
path("@<handle>/posts/<int:post_id>/edit/", compose.Compose.as_view()),
|
||||||
|
@ -267,9 +263,7 @@ urlpatterns = [
|
||||||
path("auth/signup/<token>/", auth.Signup.as_view(), name="signup"),
|
path("auth/signup/<token>/", auth.Signup.as_view(), name="signup"),
|
||||||
path("auth/reset/", auth.TriggerReset.as_view(), name="trigger_reset"),
|
path("auth/reset/", auth.TriggerReset.as_view(), name="trigger_reset"),
|
||||||
path("auth/reset/<token>/", auth.PerformReset.as_view(), name="password_reset"),
|
path("auth/reset/<token>/", auth.PerformReset.as_view(), name="password_reset"),
|
||||||
# Identity selection
|
# Identity handling
|
||||||
path("@<handle>/activate/", identity.ActivateIdentity.as_view()),
|
|
||||||
path("identity/select/", identity.SelectIdentity.as_view(), name="identity_select"),
|
|
||||||
path("identity/create/", identity.CreateIdentity.as_view(), name="identity_create"),
|
path("identity/create/", identity.CreateIdentity.as_view(), name="identity_create"),
|
||||||
# Flat pages
|
# Flat pages
|
||||||
path("about/", core.About.as_view(), name="about"),
|
path("about/", core.About.as_view(), name="about"),
|
||||||
|
|
|
@ -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 @@
|
||||||
{% 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 %}
|
|
|
@ -27,11 +27,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if post.summary %}
|
{% 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">
|
<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 }}
|
{{ post.summary }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -77,12 +73,19 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if request.identity %}
|
<div class="actions">
|
||||||
<div class="actions" role="menubar">
|
<a title="Replies">
|
||||||
{% include "activities/_reply.html" %}
|
<i class="fa-solid fa-reply"></i>
|
||||||
{% include "activities/_like.html" %}
|
<span class="like-count">{{ post.stats_with_defaults.replies }}</span>
|
||||||
{% include "activities/_boost.html" %}
|
</a>
|
||||||
{% include "activities/_bookmark.html" %}
|
<a title="Likes">
|
||||||
|
<i class="fa-solid fa-star"></i>
|
||||||
|
<span class="like-count">{{ post.stats_with_defaults.likes }}</span>
|
||||||
|
</a>
|
||||||
|
<a title="Boosts">
|
||||||
|
<i class="fa-solid fa-retweet"></i>
|
||||||
|
<span class="like-count">{{ post.stats_with_defaults.boosts }}</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">
|
<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>
|
<i class="fa-solid fa-bars"></i>
|
||||||
</a>
|
</a>
|
||||||
|
@ -112,6 +115,5 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</menu>
|
</menu>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</div>
|
</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>
|
|
|
@ -26,7 +26,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not post or post.attachments.count < 4 %}
|
{% if not post or post.attachments.count < 4 %}
|
||||||
<button class="add-image"
|
<button class="add-image"
|
||||||
hx-get='{% url "compose_image_upload" %}'
|
hx-get='{% url "compose_image_upload" handle=identity.handle %}'
|
||||||
hx-target="this"
|
hx-target="this"
|
||||||
hx-swap="outerHTML">
|
hx-swap="outerHTML">
|
||||||
Add Image
|
Add Image
|
||||||
|
|
|
@ -1,37 +1,52 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load activity_tags %}
|
{% load activity_tags %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
{% block title %}Home{% endblock %}
|
{% block title %}Home{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if page_obj.number == 1 %}
|
<section class="icon-menu">
|
||||||
{% include "_announcements.html" %}
|
<h1 class="above">Identities</h1>
|
||||||
{% endif %}
|
{% for identity in identities %}
|
||||||
{% for event in page_obj %}
|
<a class="option" href="{{ identity.urls.view }}">
|
||||||
{% if event.type == "post" %}
|
<img src="{{ identity.local_icon_url.relative }}">
|
||||||
{% include "activities/_post.html" with post=event.subject_post %}
|
<span class="handle">
|
||||||
{% elif event.type == "boost" %}
|
{{ identity.html_name_or_handle }}
|
||||||
<div class="boost-banner">
|
<small>@{{ identity.handle }}</small>
|
||||||
<a href="{{ event.subject_identity.urls.view }}">
|
</span>
|
||||||
{{ event.subject_identity.html_name_or_handle }}
|
<button class="right secondary" _="on click go to url {{ identity.urls.view }} then halt">View</button>
|
||||||
</a> boosted
|
<button class="right secondary" _="on click go to url {{ identity.urls.settings }} then halt">Settings</button>
|
||||||
<time>
|
</a>
|
||||||
{{ event.subject_post_interaction.published | timedeltashort }} ago
|
|
||||||
</time>
|
|
||||||
</div>
|
|
||||||
{% include "activities/_post.html" with post=event.subject_post %}
|
|
||||||
{% endif %}
|
|
||||||
{% empty %}
|
{% empty %}
|
||||||
Nothing to show yet.
|
<p class="option empty">You have no identities.</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
<a href="{% url "identity_create" %}" class="option new">
|
||||||
<div class="pagination">
|
<i class="fa-solid fa-plus"></i> Create a new identity
|
||||||
{% if page_obj.has_previous and not request.htmx %}
|
</a>
|
||||||
<a class="button" href=".?page={{ page_obj.previous_page_number }}">Previous Page</a>
|
</section>
|
||||||
{% endif %}
|
<section>
|
||||||
|
<h1 class="above">Apps</h1>
|
||||||
{% if page_obj.has_next %}
|
<p>
|
||||||
<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>
|
To see your timelines, compose new messages, and follow people,
|
||||||
{% endif %}
|
you will need to use a Mastodon-compatible app. Our favourites
|
||||||
|
are listed below.
|
||||||
|
</p>
|
||||||
|
<div class="flex-icons">
|
||||||
|
<a href="#">
|
||||||
|
<img src="{% static "img/apps/elk.svg" %}" alt="Elk logo">
|
||||||
|
<h2>Elk</h2>
|
||||||
|
<i>Web/Mobile (Free)</i>
|
||||||
|
</a>
|
||||||
|
<a href="#">
|
||||||
|
<img src="{% static "img/apps/tusky.png" %}" alt="Tusky logo">
|
||||||
|
<h2>Tusky</h2>
|
||||||
|
<i>Android (Free)</i>
|
||||||
|
</a>
|
||||||
|
<a href="#">
|
||||||
|
<img src="{% static "img/apps/ivory.webp" %}" alt="Ivory logo">
|
||||||
|
<h2>Ivory</h2>
|
||||||
|
<i>iOS (Paid)</i>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{% extends "settings/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
|
|
||||||
{% block subtitle %}Create Announcement{% endblock %}
|
{% block subtitle %}Create Announcement{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block settings_content %}
|
||||||
<form action="." method="POST">
|
<form action="." method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
{% block title %}Delete Announcement - Admin{% endblock %}
|
{% block title %}Delete Announcement - Admin{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block settings_content %}
|
||||||
<h1>Confirm Delete</h1>
|
<h1>Confirm Delete</h1>
|
||||||
<section>
|
<section>
|
||||||
<form action="." method="POST">
|
<form action="." method="POST">
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{% extends "settings/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
|
|
||||||
{% block subtitle %}Announcement #{{ announcement.pk }}{% endblock %}
|
{% block subtitle %}Announcement #{{ announcement.pk }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block settings_content %}
|
||||||
<form action="." method="POST">
|
<form action="." method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{% extends "settings/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
|
|
||||||
{% block subtitle %}Announcements{% endblock %}
|
{% block subtitle %}Announcements{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block settings_content %}
|
||||||
<div class="view-options">
|
<div class="view-options">
|
||||||
<a href="{% url "admin_announcement_create" %}" class="button"><i class="fa-solid fa-plus"></i> Create</a>
|
<a href="{% url "admin_announcement_create" %}" class="button"><i class="fa-solid fa-plus"></i> Create</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{% extends "settings/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
|
|
||||||
{% block title %}Add Domain - Admin{% endblock %}
|
{% block title %}Add Domain - Admin{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block settings_content %}
|
||||||
<form action="." method="POST">
|
<form action="." method="POST">
|
||||||
<h1>Add A Domain</h1>
|
<h1>Add A Domain</h1>
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{% extends "settings/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
|
|
||||||
{% block title %}Delete {{ domain.domain }} - Admin{% endblock %}
|
{% block title %}Delete {{ domain.domain }} - Admin{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block settings_content %}
|
||||||
<form action="." method="POST">
|
<form action="." method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{% extends "settings/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
|
|
||||||
{% block subtitle %}{{ domain.domain }}{% endblock %}
|
{% block subtitle %}{{ domain.domain }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block settings_content %}
|
||||||
<form action="." method="POST">
|
<form action="." method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{% extends "settings/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
|
|
||||||
{% block subtitle %}Domains{% endblock %}
|
{% block subtitle %}Domains{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block settings_content %}
|
||||||
<div class="view-options">
|
<div class="view-options">
|
||||||
<span class="spacer"></span>
|
<span class="spacer"></span>
|
||||||
<a href="{% url "admin_domains_create" %}" class="button"><i class="fa-solid fa-plus"></i> Add Domain</a>
|
<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.html" %}
|
||||||
{% load activity_tags %}
|
{% load activity_tags %}
|
||||||
|
|
||||||
{% block subtitle %}Emoji{% endblock %}
|
{% block subtitle %}Emoji{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block settings_content %}
|
||||||
<form action="." class="search">
|
<form action="." class="search">
|
||||||
<input type="search" name="query" value="{{ query }}" placeholder="Search by shortcode or domain">
|
<input type="search" name="query" value="{{ query }}" placeholder="Search by shortcode or domain">
|
||||||
{% if local_only %}
|
{% if local_only %}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{% extends "settings/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
|
|
||||||
{% block subtitle %}{{ emoji.shortcode }}{% endblock %}
|
{% block subtitle %}{{ emoji.shortcode }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block settings_content %}
|
||||||
<form action="." method="POST" enctype="multipart/form-data">
|
<form action="." method="POST" enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
{% extends "settings/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
{% load activity_tags %}
|
{% load activity_tags %}
|
||||||
|
|
||||||
{% block subtitle %}Federation{% endblock %}
|
{% block subtitle %}Federation{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block settings_content %}
|
||||||
<form action="." class="search">
|
<form action="." class="search">
|
||||||
<input type="search" name="query" value="{{ query }}" placeholder="Search by domain">
|
<input type="search" name="query" value="{{ query }}" placeholder="Search by domain">
|
||||||
<button><i class="fa-solid fa-search"></i></button>
|
<button><i class="fa-solid fa-search"></i></button>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{% extends "settings/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
|
|
||||||
{% block subtitle %}{{ domain.domain }}{% endblock %}
|
{% block subtitle %}{{ domain.domain }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block settings_content %}
|
||||||
<form action="." method="POST">
|
<form action="." method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<h1>{{ domain }}</h1>
|
<h1>{{ domain }}</h1>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{% extends "settings/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
|
|
||||||
{% block subtitle %}{{ hashtag.hashtag }}{% endblock %}
|
{% block subtitle %}{{ hashtag.hashtag }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block settings_content %}
|
||||||
<form action="." method="POST">
|
<form action="." method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{% extends "settings/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
|
|
||||||
{% block subtitle %}Hashtags{% endblock %}
|
{% block subtitle %}Hashtags{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block settings_content %}
|
||||||
<table class="items">
|
<table class="items">
|
||||||
{% for hashtag in page_obj %}
|
{% for hashtag in page_obj %}
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
{% extends "settings/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
{% load activity_tags %}
|
{% load activity_tags %}
|
||||||
|
|
||||||
{% block subtitle %}Identities{% endblock %}
|
{% block subtitle %}Identities{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block settings_content %}
|
||||||
<form action="." class="search">
|
<form action="." class="search">
|
||||||
<input type="search" name="query" value="{{ query }}" placeholder="Search by name/username">
|
<input type="search" name="query" value="{{ query }}" placeholder="Search by name/username">
|
||||||
{% if local_only %}
|
{% if local_only %}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{% extends "settings/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
|
|
||||||
{% block subtitle %}{{ identity.name_or_handle }}{% endblock %}
|
{% block subtitle %}{{ identity.name_or_handle }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block settings_content %}
|
||||||
<h1>{{ identity.html_name_or_handle }} <small>{{ identity.handle }}</small></h1>
|
<h1>{{ identity.html_name_or_handle }} <small>{{ identity.handle }}</small></h1>
|
||||||
<form action="." method="POST">
|
<form action="." method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{% extends "settings/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
|
|
||||||
{% block subtitle %}Create Invite{% endblock %}
|
{% block subtitle %}Create Invite{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block settings_content %}
|
||||||
<form action="." method="POST">
|
<form action="." method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{% extends "settings/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
|
|
||||||
{% block subtitle %}View Invite{% endblock %}
|
{% block subtitle %}View Invite{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block settings_content %}
|
||||||
<form action="." method="POST">
|
<form action="." method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
{% extends "settings/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
{% load activity_tags %}
|
{% load activity_tags %}
|
||||||
|
|
||||||
{% block subtitle %}Invites{% endblock %}
|
{% block subtitle %}Invites{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block settings_content %}
|
||||||
<div class="view-options">
|
<div class="view-options">
|
||||||
<span class="spacer"></span>
|
<span class="spacer"></span>
|
||||||
<a href="{% url "admin_invite_create" %}" class="button">Create New</a>
|
<a href="{% url "admin_invite_create" %}" class="button">Create New</a>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{% extends "settings/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
|
|
||||||
{% block subtitle %}Report {{ report.pk }}{% endblock %}
|
{% block subtitle %}Report {{ report.pk }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block settings_content %}
|
||||||
<form action="." method="POST">
|
<form action="." method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
{% extends "settings/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
{% load activity_tags %}
|
{% load activity_tags %}
|
||||||
|
|
||||||
{% block subtitle %}Reports{% endblock %}
|
{% block subtitle %}Reports{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block settings_content %}
|
||||||
<div class="view-options">
|
<div class="view-options">
|
||||||
{% if all %}
|
{% if all %}
|
||||||
<a href="." class="selected"><i class="fa-solid fa-check"></i> Show Resolved</a>
|
<a href="." class="selected"><i class="fa-solid fa-check"></i> Show Resolved</a>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{% extends "settings/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
|
|
||||||
{% block subtitle %}Stator{% endblock %}
|
{% block subtitle %}Stator{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block settings_content %}
|
||||||
{% for model, stats in model_stats.items %}
|
{% for model, stats in model_stats.items %}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{{ model }}</legend>
|
<legend>{{ model }}</legend>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{% extends "settings/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
|
|
||||||
{% block subtitle %}{{ editing_user.email }}{% endblock %}
|
{% block subtitle %}{{ editing_user.email }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block settings_content %}
|
||||||
<h1>{{ editing_user.email }}</h1>
|
<h1>{{ editing_user.email }}</h1>
|
||||||
<form action="." method="POST">
|
<form action="." method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
{% extends "settings/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
{% load activity_tags %}
|
{% load activity_tags %}
|
||||||
|
|
||||||
{% block subtitle %}Users{% endblock %}
|
{% block subtitle %}Users{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block settings_content %}
|
||||||
<form action="." class="search">
|
<form action="." class="search">
|
||||||
<input type="search" name="query" value="{{ query }}" placeholder="Search by email">
|
<input type="search" name="query" value="{{ query }}" placeholder="Search by email">
|
||||||
<button><i class="fa-solid fa-search"></i></button>
|
<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 "css/style.css" %}" type="text/css" media="screen" />
|
||||||
<link rel="stylesheet" href="{% static "fonts/raleway/raleway.css" %}" type="text/css" />
|
<link rel="stylesheet" href="{% static "fonts/raleway/raleway.css" %}" type="text/css" />
|
||||||
<link rel="stylesheet" href="{% static "fonts/font_awesome/all.min.css" %}" type="text/css" />
|
<link rel="stylesheet" href="{% static "fonts/font_awesome/all.min.css" %}" type="text/css" />
|
||||||
<link rel="manifest" href="/manifest.json" />
|
|
||||||
<link rel="shortcut icon" href="{{ config.site_icon }}">
|
<link rel="shortcut icon" href="{{ config.site_icon }}">
|
||||||
<script src="{% static "js/hyperscript.min.js" %}"></script>
|
<script src="{% static "js/hyperscript.min.js" %}"></script>
|
||||||
<script src="{% static "js/htmx.min.js" %}"></script>
|
<script src="{% static "js/htmx.min.js" %}"></script>
|
||||||
|
@ -39,61 +38,28 @@
|
||||||
<main>
|
<main>
|
||||||
{% block body_main %}
|
{% block body_main %}
|
||||||
<header>
|
<header>
|
||||||
|
<menu>
|
||||||
<a class="logo" href="/">
|
<a class="logo" href="/">
|
||||||
<img src="{{ config.site_icon }}" width="32">
|
<img src="{{ config.site_icon }}" width="32">
|
||||||
{{ config.site_name }}
|
{{ config.site_name }}
|
||||||
</a>
|
</a>
|
||||||
<menu>
|
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<a href="{% url "compose" %}" title="Compose" role="menuitem" {% if top_section == "compose" %}class="selected"{% endif %}>
|
<a href="/" title="My Account"><i class="fa-solid fa-user"></i></a>
|
||||||
<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">
|
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ request.identity.username }}
|
<a href="{% url "login" %}" title="Login"><i class="fa-solid fa-right-to-bracket"></i></a>
|
||||||
<img src="{{ request.identity.local_icon_url.relative }}" title="{{ request.identity.handle }}">
|
|
||||||
{% 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>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<a href="/settings" title="Settings"><i class="fa-solid fa-gear"></i></a>
|
||||||
|
<a href="/admin" title="Administration"><i class="fa-solid fa-screwdriver-wrench"></i></a>
|
||||||
</menu>
|
</menu>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{% block full_content %}
|
|
||||||
{% include "activities/_image_viewer.html" %}
|
|
||||||
{% block pre_content %}
|
|
||||||
{% endblock %}
|
|
||||||
<div class="columns">
|
|
||||||
<div class="left-column" id="main-content">
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
|
||||||
<div class="right-column" id="side-navigation">
|
|
||||||
{% block right_content %}
|
|
||||||
{% include "activities/_menu.html" %}
|
|
||||||
{% endblock %}
|
|
||||||
{% include "_footer.html" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{% block footer %}
|
{% block footer %}
|
||||||
|
{% include "_footer.html" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -1,38 +1,5 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block body_class %}no-sidebar{% endblock %}
|
|
||||||
|
|
||||||
{% block opengraph %}
|
{% block opengraph %}
|
||||||
{# Error pages don't have the context loaded, so disable opengraph to keep it from spewing errors #}
|
{# Error pages don't have the context loaded, so disable opengraph to keep it from spewing errors #}
|
||||||
{% endblock %}
|
{% 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 %}
|
|
||||||
|
|
|
@ -1,78 +1,19 @@
|
||||||
<div class="inline follow {% if inbound_follow %}has-reverse{% endif %}">
|
<div class="inline follow {% if inbound_follow %}has-reverse{% endif %}">
|
||||||
<div class="actions" role="menubar">
|
<div class="actions" role="menubar">
|
||||||
{% if request.identity == identity %}
|
{% if request.user in identity.users.all %}
|
||||||
<a href="{% url "settings_profile" %}" class="button" title="Edit Profile">
|
<a href="{% url "settings_profile" handle=identity.handle %}" class="button" title="Edit Profile">
|
||||||
<i class="fa-solid fa-user-edit"></i> Edit
|
<i class="fa-solid fa-user-edit"></i> Edit
|
||||||
</a>
|
</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 %}
|
{% endif %}
|
||||||
|
{% if request.user.admin or request.user.moderator %}
|
||||||
<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">
|
<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>
|
<i class="fa-solid fa-bars"></i>
|
||||||
</a>
|
</a>
|
||||||
<menu>
|
<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">
|
<a href="{{ identity.urls.admin_edit }}" role="menuitem">
|
||||||
<i class="fa-solid fa-user-gear"></i> View in Admin
|
<i class="fa-solid fa-user-gear"></i> View in Admin
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ identity.urls.djadmin_edit }}" role="menuitem">
|
|
||||||
<i class="fa-solid fa-gear"></i> View in djadmin
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</menu>
|
</menu>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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 %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body_class %}has-banner{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if not request.htmx %}
|
<section class="identity">
|
||||||
<h1 class="identity">
|
|
||||||
{% if identity.local_image_url %}
|
{% if identity.local_image_url %}
|
||||||
<img src="{{ identity.local_image_url.relative }}" class="banner">
|
<img src="{{ identity.local_image_url.relative }}" class="banner">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -30,9 +27,9 @@
|
||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{% if request.identity %}{% include "identity/_view_menu.html" %}{% endif %}
|
{% if request.user.moderator or request.user.admin %}{% include "identity/_view_menu.html" %}{% endif %}
|
||||||
|
|
||||||
{{ identity.html_name_or_handle }}
|
<h1>{{ identity.html_name_or_handle }}</h1>
|
||||||
<small>
|
<small>
|
||||||
@{{ identity.handle }}
|
@{{ identity.handle }}
|
||||||
<a title="Copy handle"
|
<a title="Copy handle"
|
||||||
|
@ -46,55 +43,44 @@
|
||||||
<i class="fa-solid fa-copy"></i>
|
<i class="fa-solid fa-copy"></i>
|
||||||
</a>
|
</a>
|
||||||
</small>
|
</small>
|
||||||
</h1>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if inbound_block %}
|
|
||||||
<p class="system-note">
|
|
||||||
This user has blocked you.
|
|
||||||
</p>
|
|
||||||
{% else %}
|
|
||||||
|
|
||||||
{% if not request.htmx %}
|
|
||||||
{% if identity.summary %}
|
{% if identity.summary %}
|
||||||
<div class="bio">
|
<div class="bio">
|
||||||
{{ identity.safe_summary }}
|
{{ identity.safe_summary }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="invisible identity-metadata">
|
||||||
{% if identity.metadata %}
|
{% if identity.metadata %}
|
||||||
<div class="identity-metadata">
|
|
||||||
{% for entry in identity.safe_metadata %}
|
{% for entry in identity.safe_metadata %}
|
||||||
<div class="metadata-pair">
|
<div class="metadata-pair">
|
||||||
<span class="metadata-name">{{ entry.name }}</span>
|
<span class="metadata-name">{{ entry.name }}</span>
|
||||||
<span class="metadata-value">{{ entry.value }}</span>
|
<span class="metadata-value">{{ entry.value }}</span>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
|
||||||
<div class="view-options follows">
|
{% if not identity.local %}
|
||||||
|
<section class="system-note">
|
||||||
|
{% if identity.outdated and not identity.name %}
|
||||||
|
The system is still fetching this profile. Refresh to see updates.
|
||||||
|
{% else %}
|
||||||
|
This is a member of another server.
|
||||||
|
<a href="{{ identity.profile_uri|default:identity.actor_uri }}">See their original profile ➔</a>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
<section class="view-options">
|
||||||
<a href="{{ identity.urls.view }}" {% if not follows_page %}class="selected"{% endif %}><strong>{{ post_count }}</strong> posts</a>
|
<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 %}
|
{% 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.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>
|
<a href="{{ identity.urls.followers }}" {% if inbound and follows_page %}class="selected"{% endif %}><strong>{{ followers_count }}</strong> follower{{ followers_count|pluralize }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
{% if not identity.local %}
|
|
||||||
{% 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 class="invisible">
|
||||||
{% block subcontent %}
|
{% block subcontent %}
|
||||||
|
|
||||||
{% for post in page_obj %}
|
{% for post in page_obj %}
|
||||||
|
@ -125,5 +111,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
<nav>
|
<nav>
|
||||||
<h3>Identity</h3>
|
{% if identity %}
|
||||||
<a href="{% url "settings_profile" %}" {% if section == "profile" %}class="selected"{% endif %} title="Profile">
|
{% include "identity/_identity_banner.html" %}
|
||||||
|
<a href="{% url "settings_profile" handle=identity.handle %}" {% if section == "profile" %}class="selected"{% endif %} title="Profile">
|
||||||
<i class="fa-solid fa-user"></i>
|
<i class="fa-solid fa-user"></i>
|
||||||
<span>Profile</span>
|
<span>Profile</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url "settings_interface" %}" {% if section == "interface" %}class="selected"{% endif %} title="Interface">
|
<a href="{% url "settings_interface" handle=identity.handle %}" {% if section == "interface" %}class="selected"{% endif %} title="Interface">
|
||||||
<i class="fa-solid fa-display"></i>
|
<i class="fa-solid fa-display"></i>
|
||||||
<span>Interface</span>
|
<span>Interface</span>
|
||||||
</a>
|
</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>
|
<i class="fa-solid fa-cloud-arrow-up"></i>
|
||||||
<span>Import/Export</span>
|
<span>Import/Export</span>
|
||||||
</a>
|
</a>
|
||||||
<hr>
|
<hr>
|
||||||
|
{% endif %}
|
||||||
<h3>Account</h3>
|
<h3>Account</h3>
|
||||||
<a href="{% url "settings_security" %}" {% if section == "security" %}class="selected"{% endif %} title="Login & Security">
|
<a href="{% url "settings_security" %}" {% if section == "security" %}class="selected"{% endif %} title="Login & Security">
|
||||||
<i class="fa-solid fa-key"></i>
|
<i class="fa-solid fa-key"></i>
|
||||||
|
@ -22,64 +24,4 @@
|
||||||
<i class="fa-solid fa-right-from-bracket" title="Logout"></i>
|
<i class="fa-solid fa-right-from-bracket" title="Logout"></i>
|
||||||
<span>Logout</span>
|
<span>Logout</span>
|
||||||
</a>
|
</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>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
@ -2,6 +2,12 @@
|
||||||
|
|
||||||
{% block title %}{% block subtitle %}{% endblock %} - Settings{% endblock %}
|
{% block title %}{% block subtitle %}{% endblock %} - Settings{% endblock %}
|
||||||
|
|
||||||
{% block right_content %}
|
{% block content %}
|
||||||
|
<div class="settings">
|
||||||
{% include "settings/_menu.html" %}
|
{% include "settings/_menu.html" %}
|
||||||
|
<div class="settings-content">
|
||||||
|
{% block settings_content %}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
{% block subtitle %}Import/Export{% endblock %}
|
{% block subtitle %}Import/Export{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block settings_content %}
|
||||||
<form action="." method="POST" enctype="multipart/form-data">
|
<form action="." method="POST" enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
{% block subtitle %}Login & Security{% endblock %}
|
{% block subtitle %}Login & Security{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block settings_content %}
|
||||||
<form action="." method="POST">
|
<form action="." method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
{% block subtitle %}Profile{% endblock %}
|
{% block subtitle %}Profile{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block settings_content %}
|
||||||
<form action="." method="POST" enctype="multipart/form-data"
|
<form action="." method="POST" enctype="multipart/form-data"
|
||||||
_="on submit metadata.collectMetadataFields()">
|
_="on submit metadata.collectMetadataFields()">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
{% block subtitle %}{{ section.title }}{% endblock %}
|
{% block subtitle %}{{ section.title }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block settings_content %}
|
||||||
<form action="." method="POST" enctype="multipart/form-data">
|
<form action="." method="POST" enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% for title, fields in fieldsets.items %}
|
{% for title, fields in fieldsets.items %}
|
||||||
|
|
|
@ -5,30 +5,6 @@ from django.contrib.auth.views import redirect_to_login
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
|
|
||||||
|
|
||||||
def identity_required(function):
|
|
||||||
"""
|
|
||||||
Decorator for views that ensures an active identity is selected.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@wraps(function)
|
|
||||||
def inner(request, *args, **kwargs):
|
|
||||||
# They do have to be logged in
|
|
||||||
if not request.user.is_authenticated:
|
|
||||||
return redirect_to_login(next=request.get_full_path())
|
|
||||||
# If there's no active one, try to auto-select one
|
|
||||||
if request.identity is None:
|
|
||||||
possible_identities = list(request.user.identities.all())
|
|
||||||
if len(possible_identities) != 1:
|
|
||||||
# OK, send them to the identity selection page to select/create one
|
|
||||||
return HttpResponseRedirect("/identity/select/")
|
|
||||||
identity = possible_identities[0]
|
|
||||||
request.session["identity_id"] = identity.pk
|
|
||||||
request.identity = identity
|
|
||||||
return function(request, *args, **kwargs)
|
|
||||||
|
|
||||||
return inner
|
|
||||||
|
|
||||||
|
|
||||||
def moderator_required(function):
|
def moderator_required(function):
|
||||||
return user_passes_test(
|
return user_passes_test(
|
||||||
lambda user: user.is_authenticated and (user.admin or user.moderator)
|
lambda user: user.is_authenticated and (user.admin or user.moderator)
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
from users.models import Identity, User
|
|
||||||
|
|
||||||
|
|
||||||
class IdentityMiddleware:
|
|
||||||
"""
|
|
||||||
Adds a request.identity object which is either the current session's
|
|
||||||
identity, or None if they have not picked one yet/it's invalid.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, get_response):
|
|
||||||
self.get_response = get_response
|
|
||||||
|
|
||||||
def __call__(self, request):
|
|
||||||
# The API middleware might have set identity already
|
|
||||||
if not hasattr(request, "identity"):
|
|
||||||
# See if we have one in the session
|
|
||||||
identity_id = request.session.get("identity_id")
|
|
||||||
if not identity_id:
|
|
||||||
request.identity = None
|
|
||||||
else:
|
|
||||||
# Pull it out of the DB and assign it
|
|
||||||
try:
|
|
||||||
request.identity = Identity.objects.get(id=identity_id)
|
|
||||||
User.objects.filter(pk=request.user.pk).update(
|
|
||||||
last_seen=timezone.now()
|
|
||||||
)
|
|
||||||
except Identity.DoesNotExist:
|
|
||||||
request.identity = None
|
|
||||||
|
|
||||||
response = self.get_response(request)
|
|
||||||
|
|
||||||
if request.user:
|
|
||||||
response.headers["X-Takahe-User"] = str(request.user)
|
|
||||||
if request.identity:
|
|
||||||
response.headers["X-Takahe-Identity"] = str(request.identity)
|
|
||||||
|
|
||||||
return response
|
|
|
@ -111,7 +111,6 @@ class IdentityStates(StateGraph):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def handle_outdated(cls, identity: "Identity"):
|
async def handle_outdated(cls, identity: "Identity"):
|
||||||
|
|
||||||
# Local identities never need fetching
|
# Local identities never need fetching
|
||||||
if identity.local:
|
if identity.local:
|
||||||
return cls.updated
|
return cls.updated
|
||||||
|
@ -231,6 +230,7 @@ class Identity(StatorModel):
|
||||||
|
|
||||||
class urls(urlman.Urls):
|
class urls(urlman.Urls):
|
||||||
view = "/@{self.username}@{self.domain_id}/"
|
view = "/@{self.username}@{self.domain_id}/"
|
||||||
|
settings = "{view}settings/"
|
||||||
action = "{view}action/"
|
action = "{view}action/"
|
||||||
followers = "{view}followers/"
|
followers = "{view}followers/"
|
||||||
following = "{view}following/"
|
following = "{view}following/"
|
||||||
|
|
|
@ -14,6 +14,7 @@ class AdminSettingsPage(SettingsPage):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
options_class = Config.SystemOptions
|
options_class = Config.SystemOptions
|
||||||
|
template_name = "admin/settings.html"
|
||||||
|
|
||||||
def load_config(self):
|
def load_config(self):
|
||||||
return Config.load_system()
|
return Config.load_system()
|
||||||
|
|
|
@ -3,12 +3,12 @@ from django.shortcuts import get_object_or_404
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
from users.decorators import identity_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from users.models import Announcement
|
from users.models import Announcement
|
||||||
from users.services import AnnouncementService
|
from users.services import AnnouncementService
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(identity_required, name="dispatch")
|
@method_decorator(login_required, name="dispatch")
|
||||||
class AnnouncementDismiss(View):
|
class AnnouncementDismiss(View):
|
||||||
"""
|
"""
|
||||||
Dismisses an announcement for the current user
|
Dismisses an announcement for the current user
|
||||||
|
|
|
@ -17,7 +17,7 @@ from activities.services import TimelineService
|
||||||
from core.decorators import cache_page, cache_page_by_ap_json
|
from core.decorators import cache_page, cache_page_by_ap_json
|
||||||
from core.ld import canonicalise
|
from core.ld import canonicalise
|
||||||
from core.models import Config
|
from core.models import Config
|
||||||
from users.decorators import identity_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from users.models import Domain, FollowStates, Identity, IdentityStates
|
from users.models import Domain, FollowStates, Identity, IdentityStates
|
||||||
from users.services import IdentityService
|
from users.services import IdentityService
|
||||||
from users.shortcuts import by_handle_or_404
|
from users.shortcuts import by_handle_or_404
|
||||||
|
@ -65,15 +65,11 @@ class ViewIdentity(ListView):
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return TimelineService(self.request.identity).identity_public(self.identity)
|
return TimelineService(None).identity_public(self.identity)
|
||||||
|
|
||||||
def get_context_data(self):
|
def get_context_data(self):
|
||||||
context = super().get_context_data()
|
context = super().get_context_data()
|
||||||
context["identity"] = self.identity
|
context["identity"] = self.identity
|
||||||
context["interactions"] = PostInteraction.get_post_interactions(
|
|
||||||
context["page_obj"],
|
|
||||||
self.request.identity,
|
|
||||||
)
|
|
||||||
context["post_count"] = self.identity.posts.count()
|
context["post_count"] = self.identity.posts.count()
|
||||||
if self.identity.config_identity.visible_follows:
|
if self.identity.config_identity.visible_follows:
|
||||||
context["followers_count"] = self.identity.inbound_follows.filter(
|
context["followers_count"] = self.identity.inbound_follows.filter(
|
||||||
|
@ -82,10 +78,6 @@ class ViewIdentity(ListView):
|
||||||
context["following_count"] = self.identity.outbound_follows.filter(
|
context["following_count"] = self.identity.outbound_follows.filter(
|
||||||
state__in=FollowStates.group_active()
|
state__in=FollowStates.group_active()
|
||||||
).count()
|
).count()
|
||||||
if self.request.identity:
|
|
||||||
context.update(
|
|
||||||
IdentityService(self.identity).relationships(self.request.identity)
|
|
||||||
)
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
@ -242,7 +234,7 @@ class IdentityFollows(ListView):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(identity_required, name="dispatch")
|
@method_decorator(login_required, name="dispatch")
|
||||||
class ActionIdentity(View):
|
class ActionIdentity(View):
|
||||||
def post(self, request, handle):
|
def post(self, request, handle):
|
||||||
identity = by_handle_or_404(self.request, handle, local=False)
|
identity = by_handle_or_404(self.request, handle, local=False)
|
||||||
|
@ -269,30 +261,6 @@ class ActionIdentity(View):
|
||||||
return redirect(identity.urls.view)
|
return redirect(identity.urls.view)
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(login_required, name="dispatch")
|
|
||||||
class SelectIdentity(TemplateView):
|
|
||||||
template_name = "identity/select.html"
|
|
||||||
|
|
||||||
def get_context_data(self):
|
|
||||||
return {
|
|
||||||
"identities": Identity.objects.filter(users__pk=self.request.user.pk),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(login_required, name="dispatch")
|
|
||||||
class ActivateIdentity(View):
|
|
||||||
def get(self, request, handle):
|
|
||||||
identity = by_handle_or_404(request, handle)
|
|
||||||
if not identity.users.filter(pk=request.user.pk).exists():
|
|
||||||
raise Http404()
|
|
||||||
request.session["identity_id"] = identity.id
|
|
||||||
# Get next URL, not allowing offsite links
|
|
||||||
next = request.GET.get("next") or "/"
|
|
||||||
if ":" in next:
|
|
||||||
next = "/"
|
|
||||||
return redirect("/")
|
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(login_required, name="dispatch")
|
@method_decorator(login_required, name="dispatch")
|
||||||
class CreateIdentity(FormView):
|
class CreateIdentity(FormView):
|
||||||
template_name = "identity/create.html"
|
template_name = "identity/create.html"
|
||||||
|
|
|
@ -3,12 +3,12 @@ from django.shortcuts import get_object_or_404, render
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.generic import FormView
|
from django.views.generic import FormView
|
||||||
|
|
||||||
from users.decorators import identity_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from users.models import Report
|
from users.models import Report
|
||||||
from users.shortcuts import by_handle_or_404
|
from users.shortcuts import by_handle_or_404
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(identity_required, name="dispatch")
|
@method_decorator(login_required, name="dispatch")
|
||||||
class SubmitReport(FormView):
|
class SubmitReport(FormView):
|
||||||
"""
|
"""
|
||||||
Submits a report on a user or a post
|
Submits a report on a user or a post
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.generic import RedirectView
|
from django.views.generic import View
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
|
||||||
from users.decorators import identity_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from users.views.settings.import_export import ( # noqa
|
from users.views.settings.import_export import ( # noqa
|
||||||
CsvFollowers,
|
CsvFollowers,
|
||||||
CsvFollowing,
|
CsvFollowing,
|
||||||
|
@ -13,6 +14,14 @@ from users.views.settings.security import SecurityPage # noqa
|
||||||
from users.views.settings.settings_page import SettingsPage # noqa
|
from users.views.settings.settings_page import SettingsPage # noqa
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(identity_required, name="dispatch")
|
@method_decorator(login_required, name="dispatch")
|
||||||
class SettingsRoot(RedirectView):
|
class SettingsRoot(View):
|
||||||
pattern_name = "settings_profile"
|
"""
|
||||||
|
Redirects to a root settings page (varying on if there is an identity
|
||||||
|
in the URL or not)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get(self, request, handle: str | None = None):
|
||||||
|
if handle:
|
||||||
|
return redirect("settings_profile", handle=handle)
|
||||||
|
return redirect("settings_security")
|
||||||
|
|
|
@ -6,11 +6,11 @@ from django.shortcuts import redirect
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.generic import FormView, View
|
from django.views.generic import FormView, View
|
||||||
|
|
||||||
from users.decorators import identity_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from users.models import Follow, InboxMessage
|
from users.models import Follow, InboxMessage
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(identity_required, name="dispatch")
|
@method_decorator(login_required, name="dispatch")
|
||||||
class ImportExportPage(FormView):
|
class ImportExportPage(FormView):
|
||||||
"""
|
"""
|
||||||
Lets the identity's profile be edited
|
Lets the identity's profile be edited
|
||||||
|
@ -116,7 +116,6 @@ class CsvView(View):
|
||||||
|
|
||||||
|
|
||||||
class CsvFollowing(CsvView):
|
class CsvFollowing(CsvView):
|
||||||
|
|
||||||
columns = {
|
columns = {
|
||||||
"Account address": "get_handle",
|
"Account address": "get_handle",
|
||||||
"Show boosts": "boosts",
|
"Show boosts": "boosts",
|
||||||
|
@ -140,7 +139,6 @@ class CsvFollowing(CsvView):
|
||||||
|
|
||||||
|
|
||||||
class CsvFollowers(CsvView):
|
class CsvFollowers(CsvView):
|
||||||
|
|
||||||
columns = {
|
columns = {
|
||||||
"Account address": "get_handle",
|
"Account address": "get_handle",
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,14 +3,9 @@ from users.views.settings.settings_page import SettingsPage
|
||||||
|
|
||||||
|
|
||||||
class InterfacePage(SettingsPage):
|
class InterfacePage(SettingsPage):
|
||||||
|
|
||||||
section = "interface"
|
section = "interface"
|
||||||
|
|
||||||
options = {
|
options = {
|
||||||
"toot_mode": {
|
|
||||||
"title": "I Will Toot As I Please",
|
|
||||||
"help_text": "Changes all 'Post' buttons to 'Toot!'",
|
|
||||||
},
|
|
||||||
"default_post_visibility": {
|
"default_post_visibility": {
|
||||||
"title": "Default Post Visibility",
|
"title": "Default Post Visibility",
|
||||||
"help_text": "Visibility to use as default for new posts.",
|
"help_text": "Visibility to use as default for new posts.",
|
||||||
|
@ -21,23 +16,11 @@ class InterfacePage(SettingsPage):
|
||||||
"help_text": "Visibility to use as default for replies.",
|
"help_text": "Visibility to use as default for replies.",
|
||||||
"choices": Post.Visibilities.choices,
|
"choices": Post.Visibilities.choices,
|
||||||
},
|
},
|
||||||
"visible_reaction_counts": {
|
|
||||||
"title": "Show Boost and Like Counts",
|
|
||||||
"help_text": "Disable to hide the number of Likes and Boosts on a 'Post'",
|
|
||||||
},
|
|
||||||
"custom_css": {
|
"custom_css": {
|
||||||
"title": "Custom CSS",
|
"title": "Custom CSS",
|
||||||
"help_text": "Theme the website however you'd like, just for you. You should probably not use this unless you know what you're doing.",
|
"help_text": "Theme the website however you'd like, just for you. You should probably not use this unless you know what you're doing.",
|
||||||
"display": "textarea",
|
"display": "textarea",
|
||||||
},
|
},
|
||||||
"expand_linked_cws": {
|
|
||||||
"title": "Expand linked Content Warnings",
|
|
||||||
"help_text": "Expands all CWs of the same type at once, rather than one at a time.",
|
|
||||||
},
|
|
||||||
"infinite_scroll": {
|
|
||||||
"title": "Infinite Scroll",
|
|
||||||
"help_text": "Automatically loads more posts when you get to the bottom of a timeline.",
|
|
||||||
},
|
|
||||||
"light_theme": {
|
"light_theme": {
|
||||||
"title": "Light Mode",
|
"title": "Light Mode",
|
||||||
"help_text": "Use a light theme rather than the default dark theme.",
|
"help_text": "Use a light theme rather than the default dark theme.",
|
||||||
|
@ -45,7 +28,6 @@ class InterfacePage(SettingsPage):
|
||||||
}
|
}
|
||||||
|
|
||||||
layout = {
|
layout = {
|
||||||
"Posting": ["toot_mode", "default_post_visibility", "default_reply_visibility"],
|
"Posting": ["default_post_visibility", "default_reply_visibility"],
|
||||||
"Wellness": ["infinite_scroll", "visible_reaction_counts", "expand_linked_cws"],
|
|
||||||
"Appearance": ["light_theme", "custom_css"],
|
"Appearance": ["light_theme", "custom_css"],
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,12 +6,13 @@ from django.views.generic import FormView
|
||||||
|
|
||||||
from core.html import FediverseHtmlParser
|
from core.html import FediverseHtmlParser
|
||||||
from core.models.config import Config
|
from core.models.config import Config
|
||||||
from users.decorators import identity_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from users.models import IdentityStates
|
from users.models import IdentityStates
|
||||||
|
from users.shortcuts import by_handle_or_404
|
||||||
from users.services import IdentityService
|
from users.services import IdentityService
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(identity_required, name="dispatch")
|
@method_decorator(login_required, name="dispatch")
|
||||||
class ProfilePage(FormView):
|
class ProfilePage(FormView):
|
||||||
"""
|
"""
|
||||||
Lets the identity's profile be edited
|
Lets the identity's profile be edited
|
||||||
|
@ -61,28 +62,35 @@ class ProfilePage(FormView):
|
||||||
return None
|
return None
|
||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
|
def dispatch(self, request, handle: str, *args, **kwargs):
|
||||||
|
self.identity = by_handle_or_404(self.request, handle, local=True, fetch=False)
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context["identity"] = self.identity
|
||||||
|
return context
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
identity = self.request.identity
|
|
||||||
return {
|
return {
|
||||||
"name": identity.name,
|
"name": self.identity.name,
|
||||||
"summary": (
|
"summary": (
|
||||||
FediverseHtmlParser(identity.summary).plain_text
|
FediverseHtmlParser(self.identity.summary).plain_text
|
||||||
if identity.summary
|
if self.identity.summary
|
||||||
else ""
|
else ""
|
||||||
),
|
),
|
||||||
"icon": identity.icon and identity.icon.url,
|
"icon": self.identity.icon and self.identity.icon.url,
|
||||||
"image": identity.image and identity.image.url,
|
"image": self.identity.image and self.identity.image.url,
|
||||||
"discoverable": identity.discoverable,
|
"discoverable": self.identity.discoverable,
|
||||||
"visible_follows": identity.config_identity.visible_follows,
|
"visible_follows": self.identity.config_identity.visible_follows,
|
||||||
"metadata": identity.metadata or [],
|
"metadata": self.identity.metadata or [],
|
||||||
}
|
}
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
# Update basic info
|
# Update basic info
|
||||||
identity = self.request.identity
|
service = IdentityService(self.identity)
|
||||||
service = IdentityService(identity)
|
self.identity.name = form.cleaned_data["name"]
|
||||||
identity.name = form.cleaned_data["name"]
|
self.identity.discoverable = form.cleaned_data["discoverable"]
|
||||||
identity.discoverable = form.cleaned_data["discoverable"]
|
|
||||||
service.set_summary(form.cleaned_data["summary"])
|
service.set_summary(form.cleaned_data["summary"])
|
||||||
# Resize images
|
# Resize images
|
||||||
icon = form.cleaned_data.get("icon")
|
icon = form.cleaned_data.get("icon")
|
||||||
|
@ -91,20 +99,20 @@ class ProfilePage(FormView):
|
||||||
service.set_icon(icon)
|
service.set_icon(icon)
|
||||||
if isinstance(image, File):
|
if isinstance(image, File):
|
||||||
service.set_image(image)
|
service.set_image(image)
|
||||||
identity.metadata = form.cleaned_data.get("metadata")
|
self.identity.metadata = form.cleaned_data.get("metadata")
|
||||||
|
|
||||||
# Clear images if specified
|
# Clear images if specified
|
||||||
if "icon__clear" in self.request.POST:
|
if "icon__clear" in self.request.POST:
|
||||||
identity.icon = None
|
self.identity.icon = None
|
||||||
if "image__clear" in self.request.POST:
|
if "image__clear" in self.request.POST:
|
||||||
identity.image = None
|
self.identity.image = None
|
||||||
|
|
||||||
# Save and propagate
|
# Save and propagate
|
||||||
identity.save()
|
self.identity.save()
|
||||||
identity.transition_perform(IdentityStates.edited)
|
self.identity.transition_perform(IdentityStates.edited)
|
||||||
|
|
||||||
# Save profile-specific identity Config
|
# Save profile-specific identity Config
|
||||||
Config.set_identity(
|
Config.set_identity(
|
||||||
identity, "visible_follows", form.cleaned_data["visible_follows"]
|
self.identity, "visible_follows", form.cleaned_data["visible_follows"]
|
||||||
)
|
)
|
||||||
return redirect(".")
|
return redirect(".")
|
||||||
|
|
|
@ -2,10 +2,10 @@ from django import forms
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.generic import FormView
|
from django.views.generic import FormView
|
||||||
|
|
||||||
from users.decorators import identity_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(identity_required, name="dispatch")
|
@method_decorator(login_required, name="dispatch")
|
||||||
class SecurityPage(FormView):
|
class SecurityPage(FormView):
|
||||||
"""
|
"""
|
||||||
Lets the identity's profile be edited
|
Lets the identity's profile be edited
|
||||||
|
|
|
@ -8,10 +8,11 @@ from django.utils.decorators import method_decorator
|
||||||
from django.views.generic import FormView
|
from django.views.generic import FormView
|
||||||
|
|
||||||
from core.models.config import Config, UploadedImage
|
from core.models.config import Config, UploadedImage
|
||||||
from users.decorators import identity_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from users.shortcuts import by_handle_or_404
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(identity_required, name="dispatch")
|
@method_decorator(login_required, name="dispatch")
|
||||||
class SettingsPage(FormView):
|
class SettingsPage(FormView):
|
||||||
"""
|
"""
|
||||||
Shows a settings page dynamically created from our settings layout
|
Shows a settings page dynamically created from our settings layout
|
||||||
|
@ -67,11 +68,16 @@ class SettingsPage(FormView):
|
||||||
# Create a form class dynamically (yeah, right?) and return that
|
# Create a form class dynamically (yeah, right?) and return that
|
||||||
return type("SettingsForm", (forms.Form,), fields)
|
return type("SettingsForm", (forms.Form,), fields)
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
if "handle" in kwargs:
|
||||||
|
self.identity = by_handle_or_404(request, kwargs["handle"])
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def load_config(self):
|
def load_config(self):
|
||||||
return Config.load_identity(self.request.identity)
|
return Config.load_identity(self.identity)
|
||||||
|
|
||||||
def save_config(self, key, value):
|
def save_config(self, key, value):
|
||||||
Config.set_identity(self.request.identity, key, value)
|
Config.set_identity(self.identity, key, value)
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
config = self.load_config()
|
config = self.load_config()
|
||||||
|
@ -87,6 +93,8 @@ class SettingsPage(FormView):
|
||||||
context["fieldsets"] = {}
|
context["fieldsets"] = {}
|
||||||
for title, fields in self.layout.items():
|
for title, fields in self.layout.items():
|
||||||
context["fieldsets"][title] = [context["form"][field] for field in fields]
|
context["fieldsets"][title] = [context["form"][field] for field in fields]
|
||||||
|
if hasattr(self, "identity"):
|
||||||
|
context["identity"] = self.identity
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
|
|
Loading…
Reference in a new issue