mirror of
https://github.com/jointakahe/takahe.git
synced 2024-12-22 21:46:27 +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.html import FediverseHtmlParser
|
||||
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):
|
||||
|
||||
template_name = "activities/compose.html"
|
||||
|
||||
class form_class(forms.Form):
|
||||
|
@ -54,9 +54,9 @@ class Compose(FormView):
|
|||
)
|
||||
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)
|
||||
self.request = request
|
||||
self.identity = identity
|
||||
self.fields["text"].widget.attrs[
|
||||
"_"
|
||||
] = rf"""
|
||||
|
@ -83,7 +83,7 @@ class Compose(FormView):
|
|||
def clean_text(self):
|
||||
text = self.cleaned_data.get("text")
|
||||
# Check minimum interval
|
||||
last_post = self.request.identity.posts.order_by("-created").first()
|
||||
last_post = self.identity.posts.order_by("-created").first()
|
||||
if (
|
||||
last_post
|
||||
and (timezone.now() - last_post.created).total_seconds()
|
||||
|
@ -103,7 +103,7 @@ class Compose(FormView):
|
|||
return text
|
||||
|
||||
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):
|
||||
initial = super().get_initial()
|
||||
|
@ -119,20 +119,20 @@ class Compose(FormView):
|
|||
else:
|
||||
initial[
|
||||
"visibility"
|
||||
] = self.request.identity.config_identity.default_post_visibility
|
||||
] = self.identity.config_identity.default_post_visibility
|
||||
if self.reply_to:
|
||||
initial["reply_to"] = self.reply_to.pk
|
||||
if self.reply_to.visibility == Post.Visibilities.public:
|
||||
initial[
|
||||
"visibility"
|
||||
] = self.request.identity.config_identity.default_reply_visibility
|
||||
] = self.identity.config_identity.default_reply_visibility
|
||||
else:
|
||||
initial["visibility"] = self.reply_to.visibility
|
||||
initial["content_warning"] = self.reply_to.summary
|
||||
# Build a set of mentions for the content to start as
|
||||
mentioned = {self.reply_to.author}
|
||||
mentioned.update(self.reply_to.mentions.all())
|
||||
mentioned.discard(self.request.identity)
|
||||
mentioned.discard(self.identity)
|
||||
initial["text"] = "".join(
|
||||
f"@{identity.handle} "
|
||||
for identity in mentioned
|
||||
|
@ -158,7 +158,7 @@ class Compose(FormView):
|
|||
self.post_obj.transition_perform(PostStates.edited)
|
||||
else:
|
||||
post = Post.create_local(
|
||||
author=self.request.identity,
|
||||
author=self.identity,
|
||||
content=form.cleaned_data["text"],
|
||||
summary=form.cleaned_data.get("content_warning"),
|
||||
visibility=form.cleaned_data["visibility"],
|
||||
|
@ -166,17 +166,14 @@ class Compose(FormView):
|
|||
attachments=attachments,
|
||||
)
|
||||
# Add their own timeline event for immediate visibility
|
||||
TimelineEvent.add_post(self.request.identity, post)
|
||||
TimelineEvent.add_post(self.identity, post)
|
||||
return redirect("/")
|
||||
|
||||
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
|
||||
if handle and post_id:
|
||||
# Make sure the request identity owns the post!
|
||||
if handle != request.identity.handle:
|
||||
raise PermissionDenied("Post author is not requestor")
|
||||
|
||||
self.post_obj = get_object_or_404(request.identity.posts, pk=post_id)
|
||||
self.post_obj = get_object_or_404(self.identity.posts, pk=post_id)
|
||||
|
||||
# Grab the reply-to post info now
|
||||
self.reply_to = None
|
||||
|
@ -192,12 +189,13 @@ class Compose(FormView):
|
|||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["reply_to"] = self.reply_to
|
||||
context["identity"] = self.identity
|
||||
if self.post_obj:
|
||||
context["post"] = self.post_obj
|
||||
return context
|
||||
|
||||
|
||||
@method_decorator(identity_required, name="dispatch")
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class ImageUpload(FormView):
|
||||
"""
|
||||
Handles image upload - returns a new input type hidden to embed in
|
||||
|
@ -267,7 +265,7 @@ class ImageUpload(FormView):
|
|||
height=main_file.image.height,
|
||||
name=form.cleaned_data.get("description"),
|
||||
state=PostAttachmentStates.fetched,
|
||||
author=self.request.identity,
|
||||
author=self.identity,
|
||||
)
|
||||
|
||||
attachment.file.save(
|
||||
|
|
|
@ -2,11 +2,11 @@ from django.db import models
|
|||
from django.utils.decorators import method_decorator
|
||||
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
|
||||
|
||||
|
||||
@method_decorator(identity_required, name="dispatch")
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class Follows(ListView):
|
||||
"""
|
||||
Shows followers/follows.
|
||||
|
|
|
@ -4,10 +4,10 @@ from django.utils.decorators import method_decorator
|
|||
from django.views.generic import View
|
||||
|
||||
from activities.models.hashtag import Hashtag
|
||||
from users.decorators import identity_required
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
|
||||
@method_decorator(identity_required, name="dispatch")
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class HashtagFollow(View):
|
||||
"""
|
||||
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 core.decorators import cache_page_by_ap_json
|
||||
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.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")
|
||||
class Individual(TemplateView):
|
||||
|
||||
template_name = "activities/post.html"
|
||||
|
||||
identity: Identity
|
||||
|
@ -32,7 +31,7 @@ class Individual(TemplateView):
|
|||
self.post_obj = get_object_or_404(
|
||||
PostService.queryset()
|
||||
.filter(author=self.identity)
|
||||
.visible_to(request.identity, include_replies=True),
|
||||
.unlisted(include_replies=True),
|
||||
pk=post_id,
|
||||
)
|
||||
if self.post_obj.state in [PostStates.deleted, PostStates.deleted_fanned_out]:
|
||||
|
@ -48,18 +47,12 @@ class Individual(TemplateView):
|
|||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
ancestors, descendants = PostService(self.post_obj).context(
|
||||
self.request.identity
|
||||
)
|
||||
ancestors, descendants = PostService(self.post_obj).context(None)
|
||||
|
||||
context.update(
|
||||
{
|
||||
"identity": self.identity,
|
||||
"post": self.post_obj,
|
||||
"interactions": PostInteraction.get_post_interactions(
|
||||
[self.post_obj] + ancestors + descendants,
|
||||
self.request.identity,
|
||||
),
|
||||
"link_original": True,
|
||||
"ancestors": ancestors,
|
||||
"descendants": descendants,
|
||||
|
@ -78,108 +71,7 @@ class Individual(TemplateView):
|
|||
)
|
||||
|
||||
|
||||
@method_decorator(identity_required, name="dispatch")
|
||||
class Like(View):
|
||||
"""
|
||||
Adds/removes a like from the current identity to the post
|
||||
"""
|
||||
|
||||
undo = False
|
||||
|
||||
def post(self, request, handle, post_id):
|
||||
identity = by_handle_or_404(self.request, handle, local=False)
|
||||
post = get_object_or_404(
|
||||
PostService.queryset()
|
||||
.filter(author=identity)
|
||||
.visible_to(request.identity, include_replies=True),
|
||||
pk=post_id,
|
||||
)
|
||||
service = PostService(post)
|
||||
if self.undo:
|
||||
service.unlike_as(request.identity)
|
||||
else:
|
||||
service.like_as(request.identity)
|
||||
# Return either a redirect or a HTMX snippet
|
||||
if request.htmx:
|
||||
return render(
|
||||
request,
|
||||
"activities/_like.html",
|
||||
{
|
||||
"post": post,
|
||||
"interactions": {"like": set() if self.undo else {post.pk}},
|
||||
},
|
||||
)
|
||||
return redirect(post.urls.view)
|
||||
|
||||
|
||||
@method_decorator(identity_required, name="dispatch")
|
||||
class Boost(View):
|
||||
"""
|
||||
Adds/removes a boost from the current identity to the post
|
||||
"""
|
||||
|
||||
undo = False
|
||||
|
||||
def post(self, request, handle, post_id):
|
||||
identity = by_handle_or_404(self.request, handle, local=False)
|
||||
post = get_object_or_404(
|
||||
PostService.queryset()
|
||||
.filter(author=identity)
|
||||
.visible_to(request.identity, include_replies=True),
|
||||
pk=post_id,
|
||||
)
|
||||
service = PostService(post)
|
||||
if self.undo:
|
||||
service.unboost_as(request.identity)
|
||||
else:
|
||||
service.boost_as(request.identity)
|
||||
# Return either a redirect or a HTMX snippet
|
||||
if request.htmx:
|
||||
return render(
|
||||
request,
|
||||
"activities/_boost.html",
|
||||
{
|
||||
"post": post,
|
||||
"interactions": {"boost": set() if self.undo else {post.pk}},
|
||||
},
|
||||
)
|
||||
return redirect(post.urls.view)
|
||||
|
||||
|
||||
@method_decorator(identity_required, name="dispatch")
|
||||
class Bookmark(View):
|
||||
"""
|
||||
Adds/removes a bookmark from the current identity to the post
|
||||
"""
|
||||
|
||||
undo = False
|
||||
|
||||
def post(self, request, handle, post_id):
|
||||
identity = by_handle_or_404(self.request, handle, local=False)
|
||||
post = get_object_or_404(
|
||||
PostService.queryset()
|
||||
.filter(author=identity)
|
||||
.visible_to(request.identity, include_replies=True),
|
||||
pk=post_id,
|
||||
)
|
||||
if self.undo:
|
||||
request.identity.bookmarks.filter(post=post).delete()
|
||||
else:
|
||||
request.identity.bookmarks.get_or_create(post=post)
|
||||
# Return either a redirect or a HTMX snippet
|
||||
if request.htmx:
|
||||
return render(
|
||||
request,
|
||||
"activities/_bookmark.html",
|
||||
{
|
||||
"post": post,
|
||||
"bookmarks": set() if self.undo else {post.pk},
|
||||
},
|
||||
)
|
||||
return redirect(post.urls.view)
|
||||
|
||||
|
||||
@method_decorator(identity_required, name="dispatch")
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class Delete(TemplateView):
|
||||
"""
|
||||
Deletes a post
|
||||
|
|
|
@ -6,48 +6,30 @@ from django.views.generic import ListView, TemplateView
|
|||
from activities.models import Hashtag, PostInteraction, TimelineEvent
|
||||
from activities.services import TimelineService
|
||||
from core.decorators import cache_page
|
||||
from users.decorators import identity_required
|
||||
from users.models import Bookmark, HashtagFollow
|
||||
|
||||
from .compose import Compose
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from users.models import Bookmark, HashtagFollow, Identity
|
||||
|
||||
|
||||
@method_decorator(identity_required, name="dispatch")
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class Home(TemplateView):
|
||||
"""
|
||||
Homepage for logged-in users - shows identities primarily.
|
||||
"""
|
||||
|
||||
template_name = "activities/home.html"
|
||||
|
||||
form_class = Compose.form_class
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
return self.form_class(request=self.request, **self.get_form_kwargs())
|
||||
|
||||
def get_context_data(self):
|
||||
events = TimelineService(self.request.identity).home()
|
||||
paginator = Paginator(events, 25)
|
||||
page_number = self.request.GET.get("page")
|
||||
event_page = paginator.get_page(page_number)
|
||||
context = {
|
||||
"interactions": PostInteraction.get_event_interactions(
|
||||
event_page,
|
||||
self.request.identity,
|
||||
),
|
||||
"bookmarks": Bookmark.for_identity(
|
||||
self.request.identity, event_page, "subject_post_id"
|
||||
),
|
||||
"current_page": "home",
|
||||
"allows_refresh": True,
|
||||
"page_obj": event_page,
|
||||
"form": self.form_class(request=self.request),
|
||||
return {
|
||||
"identities": Identity.objects.filter(
|
||||
users__pk=self.request.user.pk
|
||||
).order_by("created"),
|
||||
}
|
||||
return context
|
||||
|
||||
|
||||
@method_decorator(
|
||||
cache_page("cache_timeout_page_timeline", public_only=True), name="dispatch"
|
||||
)
|
||||
class Tag(ListView):
|
||||
|
||||
template_name = "activities/tag.html"
|
||||
extra_context = {
|
||||
"current_page": "tag",
|
||||
|
@ -86,7 +68,6 @@ class Tag(ListView):
|
|||
cache_page("cache_timeout_page_timeline", public_only=True), name="dispatch"
|
||||
)
|
||||
class Local(ListView):
|
||||
|
||||
template_name = "activities/local.html"
|
||||
extra_context = {
|
||||
"current_page": "local",
|
||||
|
@ -108,9 +89,8 @@ class Local(ListView):
|
|||
return context
|
||||
|
||||
|
||||
@method_decorator(identity_required, name="dispatch")
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class Federated(ListView):
|
||||
|
||||
template_name = "activities/federated.html"
|
||||
extra_context = {
|
||||
"current_page": "federated",
|
||||
|
@ -132,9 +112,8 @@ class Federated(ListView):
|
|||
return context
|
||||
|
||||
|
||||
@method_decorator(identity_required, name="dispatch")
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class Notifications(ListView):
|
||||
|
||||
template_name = "activities/notifications.html"
|
||||
extra_context = {
|
||||
"current_page": "notifications",
|
||||
|
|
|
@ -6,8 +6,7 @@ from django.http import JsonResponse
|
|||
|
||||
def identity_required(function):
|
||||
"""
|
||||
API version of the identity_required decorator that just makes sure the
|
||||
token is tied to one, not an app only.
|
||||
Makes sure the token is tied to an identity, not an app only.
|
||||
"""
|
||||
|
||||
@wraps(function)
|
||||
|
|
|
@ -4,9 +4,6 @@ from core.models import Config
|
|||
def config_context(request):
|
||||
return {
|
||||
"config": Config.system,
|
||||
"config_identity": (
|
||||
request.identity.config_identity if request.identity else None
|
||||
),
|
||||
"top_section": request.path.strip("/").split("/")[0],
|
||||
"opengraph_defaults": {
|
||||
"og:site_name": Config.system.site_name,
|
||||
|
|
|
@ -27,7 +27,6 @@ def homepage(request):
|
|||
|
||||
@method_decorator(cache_page(public_only=True), name="dispatch")
|
||||
class About(TemplateView):
|
||||
|
||||
template_name = "about.html"
|
||||
|
||||
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):
|
||||
"""
|
||||
Serves a "flat page" from a config option,
|
||||
|
|
|
@ -147,13 +147,8 @@ body {
|
|||
main {
|
||||
width: 1100px;
|
||||
margin: 20px auto;
|
||||
box-shadow: 0 0 var(--size-main-shadow) var(--color-main-shadow);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.no-sidebar main {
|
||||
box-shadow: none;
|
||||
max-width: 800px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
footer {
|
||||
|
@ -172,10 +167,8 @@ footer a {
|
|||
|
||||
header {
|
||||
display: flex;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.no-sidebar header {
|
||||
height: 42px;
|
||||
margin-top: -20px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
@ -184,11 +177,11 @@ header .logo {
|
|||
font-family: "Raleway";
|
||||
font-weight: bold;
|
||||
background: var(--color-highlight);
|
||||
border-radius: 5px 0 0 0;
|
||||
border-radius: 0 0 5px 5px;
|
||||
text-transform: lowercase;
|
||||
padding: 10px 11px 9px 10px;
|
||||
height: 50px;
|
||||
font-size: 130%;
|
||||
padding: 6px 8px 5px 7px;
|
||||
height: 42px;
|
||||
font-size: 120%;
|
||||
color: var(--color-text-in-highlight);
|
||||
border-bottom: 3px solid rgba(0, 0, 0, 0);
|
||||
z-index: 10;
|
||||
|
@ -196,10 +189,6 @@ header .logo {
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.no-sidebar header .logo {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
header .logo:hover {
|
||||
border-bottom: 3px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
@ -211,31 +200,25 @@ header .logo img {
|
|||
}
|
||||
|
||||
header menu {
|
||||
flex-grow: 1;
|
||||
flex-grow: 0;
|
||||
display: flex;
|
||||
list-style-type: none;
|
||||
justify-content: flex-start;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.no-sidebar header menu {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
header menu a {
|
||||
padding: 10px 20px 4px 20px;
|
||||
padding: 6px 10px 4px 10px;
|
||||
color: var(--color-text-main);
|
||||
line-height: 30px;
|
||||
border-bottom: 3px solid rgba(0, 0, 0, 0);
|
||||
margin: 0 2px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.no-sidebar header menu a {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
body.has-banner header menu a {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-right: 0;
|
||||
header menu a.logo {
|
||||
width: auto;
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
header menu a:hover,
|
||||
|
@ -243,10 +226,10 @@ header menu a.selected {
|
|||
border-bottom: 3px solid var(--color-highlight);
|
||||
}
|
||||
|
||||
.no-sidebar header menu a:hover:not(.logo) {
|
||||
header menu a:hover:not(.logo) {
|
||||
border-bottom: 3px solid rgba(0, 0, 0, 0);
|
||||
background-color: var(--color-bg-menu);
|
||||
border-radius: 5px;
|
||||
border-radius: 0 0 5px 5px;
|
||||
}
|
||||
|
||||
header menu a i {
|
||||
|
@ -279,26 +262,6 @@ header menu .gap {
|
|||
flex-grow: 1;
|
||||
}
|
||||
|
||||
header menu a.identity {
|
||||
border-right: 0;
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
background: var(--color-bg-menu) !important;
|
||||
border-radius: 0 5px 0 0;
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
header menu a.identity i {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 0 7px 2px 0;
|
||||
}
|
||||
|
||||
header menu a.identity a.view-profile {
|
||||
display: inline-block;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
header menu a img {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
|
@ -313,7 +276,7 @@ header menu a small {
|
|||
}
|
||||
|
||||
nav {
|
||||
padding: 10px 10px 20px 0;
|
||||
padding: 10px 0px 20px 5px;
|
||||
}
|
||||
|
||||
nav hr {
|
||||
|
@ -334,23 +297,23 @@ nav h3:first-child {
|
|||
nav a {
|
||||
display: block;
|
||||
color: var(--color-text-dull);
|
||||
padding: 7px 18px 7px 13px;
|
||||
border-left: 3px solid transparent;
|
||||
padding: 7px 18px 7px 10px;
|
||||
border-right: 3px solid transparent;
|
||||
}
|
||||
|
||||
nav a.selected {
|
||||
color: var(--color-text-main);
|
||||
background: var(--color-bg-main);
|
||||
border-radius: 0 5px 5px 0;
|
||||
border-radius: 5px 0 0 5px;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
color: var(--color-text-main);
|
||||
border-left: 3px solid var(--color-highlight);
|
||||
border-right: 3px solid var(--color-highlight);
|
||||
}
|
||||
|
||||
nav a.selected:hover {
|
||||
border-left: 3px solid transparent;
|
||||
border-right: 3px solid transparent;
|
||||
}
|
||||
|
||||
nav a.danger {
|
||||
|
@ -368,48 +331,55 @@ nav a i {
|
|||
display: inline-block;
|
||||
}
|
||||
|
||||
nav .identity-banner {
|
||||
margin: 5px 0 10px 7px;
|
||||
}
|
||||
|
||||
nav .identity-banner img.icon {
|
||||
max-width: 32px;
|
||||
max-height: 32px;
|
||||
}
|
||||
|
||||
nav .identity-banner .avatar-link {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
nav .identity-banner .handle {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
nav .identity-banner div.link {
|
||||
color: var(--color-text-main);
|
||||
}
|
||||
|
||||
nav .identity-banner a,
|
||||
nav .identity-banner a:hover {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* Left-right columns */
|
||||
|
||||
.columns {
|
||||
.settings {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.left-column {
|
||||
.settings .settings-content {
|
||||
flex-grow: 1;
|
||||
width: 300px;
|
||||
max-width: 900px;
|
||||
padding: 15px;
|
||||
padding: 0 15px 15px 15px;
|
||||
}
|
||||
|
||||
.left-column h1 {
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.left-column h1 small {
|
||||
font-size: 60%;
|
||||
color: var(--color-text-dull);
|
||||
display: block;
|
||||
margin: -10px 0 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.left-column h2 {
|
||||
margin: 10px 0 10px 0;
|
||||
}
|
||||
|
||||
.left-column h3 {
|
||||
margin: 10px 0 0 0;
|
||||
}
|
||||
|
||||
.right-column {
|
||||
.settings nav {
|
||||
width: var(--md-sidebar-width);
|
||||
background: var(--color-bg-menu);
|
||||
border-radius: 0 0 5px 0;
|
||||
}
|
||||
|
||||
.right-column h2 {
|
||||
.settings nav h2 {
|
||||
background: var(--color-highlight);
|
||||
color: var(--color-text-in-highlight);
|
||||
padding: 8px 10px;
|
||||
|
@ -418,12 +388,6 @@ nav a i {
|
|||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.right-column footer {
|
||||
padding: 0 10px 20px 10px;
|
||||
font-size: 90%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
img.emoji {
|
||||
height: var(--emoji-height);
|
||||
vertical-align: baseline;
|
||||
|
@ -433,27 +397,52 @@ img.emoji {
|
|||
content: "…";
|
||||
}
|
||||
|
||||
/* Generic markdown styling and sections */
|
||||
/* Generic styling and sections */
|
||||
|
||||
.no-sidebar section {
|
||||
section {
|
||||
max-width: 700px;
|
||||
background: var(--color-bg-box);
|
||||
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1);
|
||||
margin: 25px auto 45px auto;
|
||||
margin: 0 auto 45px auto;
|
||||
padding: 5px 15px;
|
||||
}
|
||||
|
||||
.no-sidebar section:last-of-type {
|
||||
margin-bottom: 10px;
|
||||
section:first-of-type {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.no-sidebar section.shell {
|
||||
section.invisible {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.no-sidebar #main-content>h1 {
|
||||
section h1.above {
|
||||
position: relative;
|
||||
top: -35px;
|
||||
left: -15px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
font-size: 120%;
|
||||
color: var(--color-text-main);
|
||||
margin-bottom: -20px;
|
||||
}
|
||||
|
||||
section p {
|
||||
margin: 5px 0 10px 0;
|
||||
}
|
||||
|
||||
section:last-of-type {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
section.shell {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#main-content>h1 {
|
||||
max-width: 700px;
|
||||
margin: 25px auto 5px auto;
|
||||
font-weight: bold;
|
||||
|
@ -462,7 +451,7 @@ img.emoji {
|
|||
color: var(--color-text-main);
|
||||
}
|
||||
|
||||
.no-sidebar h1+section {
|
||||
h1+section {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
|
@ -493,9 +482,7 @@ p.authorization-code {
|
|||
|
||||
.icon-menu .option {
|
||||
display: block;
|
||||
margin: 0 0 20px 0;
|
||||
background: var(--color-bg-box);
|
||||
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1);
|
||||
margin: 0 0 10px 0;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
padding: 10px 20px;
|
||||
|
@ -596,6 +583,40 @@ p.authorization-code {
|
|||
color: var(--color-text-dull);
|
||||
}
|
||||
|
||||
/* Icon/app listings */
|
||||
|
||||
.flex-icons {
|
||||
display: flex;
|
||||
list-style-type: none;
|
||||
margin: 20px 0 10px 0;
|
||||
padding: 0;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex-icons a {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
margin: 0 0 15px 0;
|
||||
}
|
||||
|
||||
.flex-icons a img {
|
||||
max-width: 64px;
|
||||
max-height: 64px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.flex-icons a h2 {
|
||||
margin: 7px 0 0 72px;
|
||||
font-size: 110%;
|
||||
}
|
||||
|
||||
.flex-icons a i {
|
||||
margin: 0 0 0 72px;
|
||||
font-size: 90%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Item tables */
|
||||
|
||||
table.items {
|
||||
|
@ -685,7 +706,7 @@ table.items td.actions a.danger:hover {
|
|||
|
||||
/* Forms */
|
||||
|
||||
.no-sidebar form {
|
||||
section form {
|
||||
max-width: 500px;
|
||||
margin: 40px auto;
|
||||
}
|
||||
|
@ -1174,42 +1195,48 @@ blockquote {
|
|||
|
||||
/* Identities */
|
||||
|
||||
h1.identity {
|
||||
margin: 0 0 20px 0;
|
||||
section.identity {
|
||||
overflow: hidden;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
h1.identity .banner {
|
||||
section.identity .banner {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
width: calc(100% + 30px);
|
||||
margin: -65px -15px 20px -15px;
|
||||
margin: -5px -15px 20px -15px;
|
||||
border-radius: 5px 0 0 0;
|
||||
}
|
||||
|
||||
h1.identity .icon {
|
||||
section.identity .icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
float: left;
|
||||
margin: 0 20px 0 0;
|
||||
margin: 0 20px 15px 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
h1.identity .emoji {
|
||||
section.identity .emoji {
|
||||
height: var(--emoji-height);
|
||||
}
|
||||
|
||||
h1.identity small {
|
||||
section.identity h1 {
|
||||
margin: 30px 0 0 0;
|
||||
}
|
||||
|
||||
section.identity small {
|
||||
display: block;
|
||||
font-size: 60%;
|
||||
font-size: 100%;
|
||||
font-weight: normal;
|
||||
color: var(--color-text-dull);
|
||||
margin: -5px 0 0 0;
|
||||
}
|
||||
|
||||
.bio {
|
||||
margin: 0 0 20px 0;
|
||||
section.identity .bio {
|
||||
clear: left;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.bio .emoji {
|
||||
|
@ -1220,14 +1247,19 @@ h1.identity small {
|
|||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.identity-metadata {
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.identity-metadata .metadata-pair {
|
||||
display: block;
|
||||
margin: 0px 0 10px 0;
|
||||
background: var(--color-bg-box);
|
||||
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1);
|
||||
display: inline-block;
|
||||
width: 300px;
|
||||
margin: 0px 0 5px 0;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
padding: 10px 20px;
|
||||
border: 2px solid rgba(255, 255, 255, 0);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
|
@ -1235,24 +1267,18 @@ h1.identity small {
|
|||
|
||||
.identity-metadata .metadata-pair .metadata-name {
|
||||
display: inline-block;
|
||||
min-width: 80px;
|
||||
min-width: 90px;
|
||||
margin-right: 15px;
|
||||
text-align: right;
|
||||
color: var(--color-text-dull);
|
||||
}
|
||||
|
||||
.identity-metadata .metadata-pair .metadata-name::after {
|
||||
padding-left: 3px;
|
||||
|
||||
color: var(--color-text-dull);
|
||||
}
|
||||
|
||||
.system-note {
|
||||
background: var(--color-bg-menu);
|
||||
color: var(--color-text-dull);
|
||||
border-radius: 3px;
|
||||
padding: 5px 8px;
|
||||
margin: 15px 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.system-note a {
|
||||
|
@ -1321,13 +1347,12 @@ table.metadata td .emoji {
|
|||
}
|
||||
|
||||
.view-options {
|
||||
margin: 0 0 10px 0px;
|
||||
margin-bottom: 10px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.view-options.follows {
|
||||
margin: 0 0 20px 0px;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.view-options a:not(.button) {
|
||||
|
@ -1405,6 +1430,11 @@ table.metadata td .emoji {
|
|||
}
|
||||
|
||||
|
||||
.identity-banner img.icon {
|
||||
max-width: 64px;
|
||||
max-height: 64px;
|
||||
}
|
||||
|
||||
|
||||
/* Posts */
|
||||
|
||||
|
@ -1776,28 +1806,13 @@ form .post {
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
body:not(.no-sidebar) header {
|
||||
height: var(--md-header-height);
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
body:not(.no-sidebar) header menu a {
|
||||
background: var(--color-header-menu);
|
||||
}
|
||||
|
||||
main {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
margin: 20px auto 20px auto;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.no-sidebar main {
|
||||
margin: 20px auto 20px auto;
|
||||
}
|
||||
|
||||
.post .attachments a.image img {
|
||||
max-height: 300px;
|
||||
}
|
||||
|
|
|
@ -220,7 +220,6 @@ MIDDLEWARE = [
|
|||
"core.middleware.HeadersMiddleware",
|
||||
"core.middleware.ConfigLoadingMiddleware",
|
||||
"api.middleware.ApiTokenMiddleware",
|
||||
"users.middleware.IdentityMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "takahe.urls"
|
||||
|
|
|
@ -29,7 +29,6 @@ from users.views import (
|
|||
urlpatterns = [
|
||||
path("", core.homepage),
|
||||
path("robots.txt", core.RobotsTxt.as_view()),
|
||||
path("manifest.json", core.AppManifest.as_view()),
|
||||
# Activity views
|
||||
path("notifications/", timelines.Notifications.as_view(), name="notifications"),
|
||||
path("local/", timelines.Local.as_view(), name="local"),
|
||||
|
@ -57,27 +56,32 @@ urlpatterns = [
|
|||
name="settings_security",
|
||||
),
|
||||
path(
|
||||
"settings/profile/",
|
||||
"@<handle>/settings/",
|
||||
settings.SettingsRoot.as_view(),
|
||||
name="settings",
|
||||
),
|
||||
path(
|
||||
"@<handle>/settings/profile/",
|
||||
settings.ProfilePage.as_view(),
|
||||
name="settings_profile",
|
||||
),
|
||||
path(
|
||||
"settings/interface/",
|
||||
"@<handle>/settings/interface/",
|
||||
settings.InterfacePage.as_view(),
|
||||
name="settings_interface",
|
||||
),
|
||||
path(
|
||||
"settings/import_export/",
|
||||
"@<handle>/settings/import_export/",
|
||||
settings.ImportExportPage.as_view(),
|
||||
name="settings_import_export",
|
||||
),
|
||||
path(
|
||||
"settings/import_export/following.csv",
|
||||
"@<handle>/settings/import_export/following.csv",
|
||||
settings.CsvFollowing.as_view(),
|
||||
name="settings_export_following_csv",
|
||||
),
|
||||
path(
|
||||
"settings/import_export/followers.csv",
|
||||
"@<handle>/settings/import_export/followers.csv",
|
||||
settings.CsvFollowers.as_view(),
|
||||
name="settings_export_followers_csv",
|
||||
),
|
||||
|
@ -242,21 +246,13 @@ urlpatterns = [
|
|||
path("@<handle>/following/", identity.IdentityFollows.as_view(inbound=False)),
|
||||
path("@<handle>/followers/", identity.IdentityFollows.as_view(inbound=True)),
|
||||
# Posts
|
||||
path("compose/", compose.Compose.as_view(), name="compose"),
|
||||
path("@<handle>/compose/", compose.Compose.as_view(), name="compose"),
|
||||
path(
|
||||
"compose/image_upload/",
|
||||
"@<handle>/compose/image_upload/",
|
||||
compose.ImageUpload.as_view(),
|
||||
name="compose_image_upload",
|
||||
),
|
||||
path("@<handle>/posts/<int:post_id>/", posts.Individual.as_view()),
|
||||
path("@<handle>/posts/<int:post_id>/like/", posts.Like.as_view()),
|
||||
path("@<handle>/posts/<int:post_id>/unlike/", posts.Like.as_view(undo=True)),
|
||||
path("@<handle>/posts/<int:post_id>/boost/", posts.Boost.as_view()),
|
||||
path("@<handle>/posts/<int:post_id>/unboost/", posts.Boost.as_view(undo=True)),
|
||||
path("@<handle>/posts/<int:post_id>/bookmark/", posts.Bookmark.as_view()),
|
||||
path(
|
||||
"@<handle>/posts/<int:post_id>/unbookmark/", posts.Bookmark.as_view(undo=True)
|
||||
),
|
||||
path("@<handle>/posts/<int:post_id>/delete/", posts.Delete.as_view()),
|
||||
path("@<handle>/posts/<int:post_id>/report/", report.SubmitReport.as_view()),
|
||||
path("@<handle>/posts/<int:post_id>/edit/", compose.Compose.as_view()),
|
||||
|
@ -267,9 +263,7 @@ urlpatterns = [
|
|||
path("auth/signup/<token>/", auth.Signup.as_view(), name="signup"),
|
||||
path("auth/reset/", auth.TriggerReset.as_view(), name="trigger_reset"),
|
||||
path("auth/reset/<token>/", auth.PerformReset.as_view(), name="password_reset"),
|
||||
# Identity selection
|
||||
path("@<handle>/activate/", identity.ActivateIdentity.as_view()),
|
||||
path("identity/select/", identity.SelectIdentity.as_view(), name="identity_select"),
|
||||
# Identity handling
|
||||
path("identity/create/", identity.CreateIdentity.as_view(), name="identity_create"),
|
||||
# Flat pages
|
||||
path("about/", core.About.as_view(), name="about"),
|
||||
|
|
|
@ -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>
|
||||
|
||||
{% if post.summary %}
|
||||
{% if config_identity.expand_linked_cws %}
|
||||
<div class="summary" _="on click or keyup[key is 'Enter'] toggle .enabled on <.{{ post.summary_class }} .summary/> then toggle .hidden on <.{{ post.summary_class }} .content/> then halt" tabindex="0">
|
||||
{% else %}
|
||||
<div class="summary" _="on click or keyup[key is 'Enter'] toggle .enabled then toggle .hidden on the next .content then halt" tabindex="0">
|
||||
{% endif %}
|
||||
<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">
|
||||
{{ post.summary }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -77,41 +73,47 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if request.identity %}
|
||||
<div class="actions" role="menubar">
|
||||
{% include "activities/_reply.html" %}
|
||||
{% include "activities/_like.html" %}
|
||||
{% include "activities/_boost.html" %}
|
||||
{% include "activities/_bookmark.html" %}
|
||||
<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>
|
||||
<div class="actions">
|
||||
<a title="Replies">
|
||||
<i class="fa-solid fa-reply"></i>
|
||||
<span class="like-count">{{ post.stats_with_defaults.replies }}</span>
|
||||
</a>
|
||||
<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">
|
||||
<i class="fa-solid fa-bars"></i>
|
||||
</a>
|
||||
<menu>
|
||||
<a href="{{ post.urls.view }}" role="menuitem">
|
||||
<i class="fa-solid fa-comment"></i> View Post & Replies
|
||||
</a>
|
||||
<menu>
|
||||
<a href="{{ post.urls.view }}" role="menuitem">
|
||||
<i class="fa-solid fa-comment"></i> View Post & Replies
|
||||
<a href="{{ post.urls.action_report }}" role="menuitem">
|
||||
<i class="fa-solid fa-flag"></i> Report
|
||||
</a>
|
||||
{% if post.author == request.identity %}
|
||||
<a href="{{ post.urls.action_edit }}" role="menuitem">
|
||||
<i class="fa-solid fa-pen-to-square"></i> Edit
|
||||
</a>
|
||||
<a href="{{ post.urls.action_report }}" role="menuitem">
|
||||
<i class="fa-solid fa-flag"></i> Report
|
||||
<a href="{{ post.urls.action_delete }}" role="menuitem">
|
||||
<i class="fa-solid fa-trash"></i> Delete
|
||||
</a>
|
||||
{% if post.author == request.identity %}
|
||||
<a href="{{ post.urls.action_edit }}" role="menuitem">
|
||||
<i class="fa-solid fa-pen-to-square"></i> Edit
|
||||
</a>
|
||||
<a href="{{ post.urls.action_delete }}" role="menuitem">
|
||||
<i class="fa-solid fa-trash"></i> Delete
|
||||
</a>
|
||||
{% elif not post.local and post.url %}
|
||||
<a href="{{ post.url }}" role="menuitem">
|
||||
<i class="fa-solid fa-arrow-up-right-from-square"></i> See Original
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if request.user.admin %}
|
||||
<a href="{{ post.urls.admin_edit }}" role="menuitem">
|
||||
<i class="fa-solid fa-gear"></i> View In Admin
|
||||
</a>
|
||||
{% endif %}
|
||||
</menu>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elif not post.local and post.url %}
|
||||
<a href="{{ post.url }}" role="menuitem">
|
||||
<i class="fa-solid fa-arrow-up-right-from-square"></i> See Original
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if request.user.admin %}
|
||||
<a href="{{ post.urls.admin_edit }}" role="menuitem">
|
||||
<i class="fa-solid fa-gear"></i> View In Admin
|
||||
</a>
|
||||
{% endif %}
|
||||
</menu>
|
||||
</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 %}
|
||||
{% if not post or post.attachments.count < 4 %}
|
||||
<button class="add-image"
|
||||
hx-get='{% url "compose_image_upload" %}'
|
||||
hx-get='{% url "compose_image_upload" handle=identity.handle %}'
|
||||
hx-target="this"
|
||||
hx-swap="outerHTML">
|
||||
Add Image
|
||||
|
|
|
@ -1,37 +1,52 @@
|
|||
{% extends "base.html" %}
|
||||
{% load activity_tags %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Home{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if page_obj.number == 1 %}
|
||||
{% include "_announcements.html" %}
|
||||
{% endif %}
|
||||
{% for event in page_obj %}
|
||||
{% if event.type == "post" %}
|
||||
{% include "activities/_post.html" with post=event.subject_post %}
|
||||
{% elif event.type == "boost" %}
|
||||
<div class="boost-banner">
|
||||
<a href="{{ event.subject_identity.urls.view }}">
|
||||
{{ event.subject_identity.html_name_or_handle }}
|
||||
</a> boosted
|
||||
<time>
|
||||
{{ event.subject_post_interaction.published | timedeltashort }} ago
|
||||
</time>
|
||||
</div>
|
||||
{% include "activities/_post.html" with post=event.subject_post %}
|
||||
{% endif %}
|
||||
{% empty %}
|
||||
Nothing to show yet.
|
||||
{% endfor %}
|
||||
|
||||
<div class="pagination">
|
||||
{% if page_obj.has_previous and not request.htmx %}
|
||||
<a class="button" href=".?page={{ page_obj.previous_page_number }}">Previous Page</a>
|
||||
{% endif %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a class="button" href=".?page={{ page_obj.next_page_number }}" hx-get=".?page={{ page_obj.next_page_number }}" hx-select=".left-column > *:not(.view-options)" hx-target=".pagination" hx-swap="outerHTML" {% if config_identity.infinite_scroll %}hx-trigger="revealed"{% endif %}>Next Page</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<section class="icon-menu">
|
||||
<h1 class="above">Identities</h1>
|
||||
{% for identity in identities %}
|
||||
<a class="option" href="{{ identity.urls.view }}">
|
||||
<img src="{{ identity.local_icon_url.relative }}">
|
||||
<span class="handle">
|
||||
{{ identity.html_name_or_handle }}
|
||||
<small>@{{ identity.handle }}</small>
|
||||
</span>
|
||||
<button class="right secondary" _="on click go to url {{ identity.urls.view }} then halt">View</button>
|
||||
<button class="right secondary" _="on click go to url {{ identity.urls.settings }} then halt">Settings</button>
|
||||
</a>
|
||||
{% empty %}
|
||||
<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>
|
||||
<section>
|
||||
<h1 class="above">Apps</h1>
|
||||
<p>
|
||||
To see your timelines, compose new messages, and follow people,
|
||||
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>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block subtitle %}Create Announcement{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." method="POST">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
{% block title %}Delete Announcement - Admin{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<h1>Confirm Delete</h1>
|
||||
<section>
|
||||
<form action="." method="POST">
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block subtitle %}Announcement #{{ announcement.pk }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." method="POST">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block subtitle %}Announcements{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<div class="view-options">
|
||||
<a href="{% url "admin_announcement_create" %}" class="button"><i class="fa-solid fa-plus"></i> Create</a>
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block title %}Add Domain - Admin{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." method="POST">
|
||||
<h1>Add A Domain</h1>
|
||||
<p>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block title %}Delete {{ domain.domain }} - Admin{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." method="POST">
|
||||
{% csrf_token %}
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block subtitle %}{{ domain.domain }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." method="POST">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block subtitle %}Domains{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<div class="view-options">
|
||||
<span class="spacer"></span>
|
||||
<a href="{% url "admin_domains_create" %}" class="button"><i class="fa-solid fa-plus"></i> Add Domain</a>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base.html" %}
|
||||
{% load activity_tags %}
|
||||
|
||||
{% block subtitle %}Emoji{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." class="search">
|
||||
<input type="search" name="query" value="{{ query }}" placeholder="Search by shortcode or domain">
|
||||
{% if local_only %}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block subtitle %}{{ emoji.shortcode }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." method="POST" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base.html" %}
|
||||
{% load activity_tags %}
|
||||
|
||||
{% block subtitle %}Federation{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." class="search">
|
||||
<input type="search" name="query" value="{{ query }}" placeholder="Search by domain">
|
||||
<button><i class="fa-solid fa-search"></i></button>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block subtitle %}{{ domain.domain }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." method="POST">
|
||||
{% csrf_token %}
|
||||
<h1>{{ domain }}</h1>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block subtitle %}{{ hashtag.hashtag }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." method="POST">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block subtitle %}Hashtags{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<table class="items">
|
||||
{% for hashtag in page_obj %}
|
||||
<tr>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base.html" %}
|
||||
{% load activity_tags %}
|
||||
|
||||
{% block subtitle %}Identities{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." class="search">
|
||||
<input type="search" name="query" value="{{ query }}" placeholder="Search by name/username">
|
||||
{% if local_only %}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block subtitle %}{{ identity.name_or_handle }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<h1>{{ identity.html_name_or_handle }} <small>{{ identity.handle }}</small></h1>
|
||||
<form action="." method="POST">
|
||||
{% csrf_token %}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block subtitle %}Create Invite{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." method="POST">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block subtitle %}View Invite{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." method="POST">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base.html" %}
|
||||
{% load activity_tags %}
|
||||
|
||||
{% block subtitle %}Invites{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<div class="view-options">
|
||||
<span class="spacer"></span>
|
||||
<a href="{% url "admin_invite_create" %}" class="button">Create New</a>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block subtitle %}Report {{ report.pk }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." method="POST">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base.html" %}
|
||||
{% load activity_tags %}
|
||||
|
||||
{% block subtitle %}Reports{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<div class="view-options">
|
||||
{% if all %}
|
||||
<a href="." class="selected"><i class="fa-solid fa-check"></i> Show Resolved</a>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block subtitle %}Stator{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
{% for model, stats in model_stats.items %}
|
||||
<fieldset>
|
||||
<legend>{{ model }}</legend>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block subtitle %}{{ editing_user.email }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<h1>{{ editing_user.email }}</h1>
|
||||
<form action="." method="POST">
|
||||
{% csrf_token %}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{% extends "settings/base.html" %}
|
||||
{% extends "admin/base.html" %}
|
||||
{% load activity_tags %}
|
||||
|
||||
{% block subtitle %}Users{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." class="search">
|
||||
<input type="search" name="query" value="{{ query }}" placeholder="Search by email">
|
||||
<button><i class="fa-solid fa-search"></i></button>
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
<link rel="stylesheet" href="{% static "css/style.css" %}" type="text/css" media="screen" />
|
||||
<link rel="stylesheet" href="{% static "fonts/raleway/raleway.css" %}" type="text/css" />
|
||||
<link rel="stylesheet" href="{% static "fonts/font_awesome/all.min.css" %}" type="text/css" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="shortcut icon" href="{{ config.site_icon }}">
|
||||
<script src="{% static "js/hyperscript.min.js" %}"></script>
|
||||
<script src="{% static "js/htmx.min.js" %}"></script>
|
||||
|
@ -39,61 +38,28 @@
|
|||
<main>
|
||||
{% block body_main %}
|
||||
<header>
|
||||
<a class="logo" href="/">
|
||||
<img src="{{ config.site_icon }}" width="32">
|
||||
{{ config.site_name }}
|
||||
</a>
|
||||
<menu>
|
||||
<a class="logo" href="/">
|
||||
<img src="{{ config.site_icon }}" width="32">
|
||||
{{ config.site_name }}
|
||||
</a>
|
||||
{% if user.is_authenticated %}
|
||||
<a href="{% url "compose" %}" title="Compose" role="menuitem" {% if top_section == "compose" %}class="selected"{% endif %}>
|
||||
<i class="fa-solid fa-feather"></i>
|
||||
</a>
|
||||
<a href="{% url "search" %}" title="Search" role="menuitem" class="search {% if top_section == "search" %}selected{% endif %}">
|
||||
<i class="fa-solid fa-search"></i>
|
||||
</a>
|
||||
{% if allows_refresh %}
|
||||
<a href="." title="Refresh" role="menuitem" hx-get="." hx-select=".left-column" hx-target=".left-column" hx-swap="outerHTML" hx-trigger="click, every 120s[isAtTopOfPage()]">
|
||||
<i class="fa-solid fa-rotate"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="gap"></div>
|
||||
<a href="{{ request.identity.urls.view }}" role="menuitem" class="identity">
|
||||
{% if not request.identity %}
|
||||
No Identity
|
||||
<img src="{% static "img/unknown-icon-128.png" %}" title="No identity selected">
|
||||
{% else %}
|
||||
{{ request.identity.username }}
|
||||
<img src="{{ request.identity.local_icon_url.relative }}" title="{{ request.identity.handle }}">
|
||||
{% endif %}
|
||||
</a>
|
||||
<a href="/" title="My Account"><i class="fa-solid fa-user"></i></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>
|
||||
<a href="{% url "login" %}" title="Login"><i class="fa-solid fa-right-to-bracket"></i></a>
|
||||
{% 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>
|
||||
</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 %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
<div class="right-column" id="side-navigation">
|
||||
{% block right_content %}
|
||||
{% include "activities/_menu.html" %}
|
||||
{% endblock %}
|
||||
{% include "_footer.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
|
||||
{% block footer %}
|
||||
{% include "_footer.html" %}
|
||||
{% endblock %}
|
||||
|
||||
</body>
|
||||
|
|
|
@ -1,38 +1,5 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block body_class %}no-sidebar{% endblock %}
|
||||
|
||||
{% block opengraph %}
|
||||
{# Error pages don't have the context loaded, so disable opengraph to keep it from spewing errors #}
|
||||
{% endblock %}
|
||||
|
||||
{% block body_main %}
|
||||
<header>
|
||||
<menu>
|
||||
{% if not request.user.is_authenticated and current_page == "about" and config.signup_allowed %}
|
||||
<a href="{% url "signup" %}">Sign Up</a>
|
||||
{% else %}
|
||||
<a href="/">Home</a>
|
||||
{% endif %}
|
||||
<a class="logo" href="/">
|
||||
<img src="{{ config.site_icon }}" width="32">
|
||||
{{ config.site_name }}
|
||||
</a>
|
||||
{% if request.user.is_authenticated %}
|
||||
<a href="javascript:history.back()">Back</a>
|
||||
{% else %}
|
||||
<a href="{% url "login" %}">Login</a>
|
||||
{% endif %}
|
||||
</menu>
|
||||
</header>
|
||||
|
||||
<div id="main-content">
|
||||
{% include "activities/_image_viewer.html" %}
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block footer %}
|
||||
{% include "_footer.html" %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,78 +1,19 @@
|
|||
<div class="inline follow {% if inbound_follow %}has-reverse{% endif %}">
|
||||
<div class="actions" role="menubar">
|
||||
{% if request.identity == identity %}
|
||||
<a href="{% url "settings_profile" %}" class="button" title="Edit Profile">
|
||||
{% if request.user in identity.users.all %}
|
||||
<a href="{% url "settings_profile" handle=identity.handle %}" class="button" title="Edit Profile">
|
||||
<i class="fa-solid fa-user-edit"></i> Edit
|
||||
</a>
|
||||
{% elif not inbound_block %}
|
||||
{% if inbound_follow or outbound_mute %}
|
||||
<span class="reverse-follow">
|
||||
{% if inbound_follow %}Follows You{% endif %}{% if inbound_follow and outbound_mute %},{% endif %}
|
||||
{% if outbound_mute %}Muted{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
<form action="{{ identity.urls.action }}" method="POST" class="inline-menu">
|
||||
{% csrf_token %}
|
||||
{% if outbound_block %}
|
||||
<input type="hidden" name="action" value="unblock">
|
||||
<button class="destructive" title="Unblock"><i class="fa-solid fa-ban"></i>
|
||||
Unblock
|
||||
</button>
|
||||
{% elif outbound_follow %}
|
||||
<input type="hidden" name="action" value="unfollow">
|
||||
<button class="destructive" title="Unfollow"><i class="fa-solid fa-user-minus"></i>
|
||||
{% if outbound_follow.pending %}Follow Pending{% else %}Unfollow{% endif %}
|
||||
</button>
|
||||
{% else %}
|
||||
<input type="hidden" name="action" value="follow">
|
||||
<button><i class="fa-solid fa-user-plus"></i> Follow</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endif %}
|
||||
<a title="Menu" class="menu button" _="on click or keyup[key is 'Enter'] toggle .enabled on the next <menu/> then halt" aria-haspopup="menu" tabindex="0">
|
||||
<i class="fa-solid fa-bars"></i>
|
||||
</a>
|
||||
<menu>
|
||||
{% if outbound_follow %}
|
||||
<form action="{{ identity.urls.action }}" method="POST" class="inline">
|
||||
{% csrf_token %}
|
||||
{% if outbound_follow.boosts %}
|
||||
<input type="hidden" name="action" value="hide_boosts">
|
||||
<button role="menuitem"><i class="fa-solid fa-retweet"></i> Hide boosts</button>
|
||||
{% else %}
|
||||
<input type="hidden" name="action" value="show_boosts">
|
||||
<button role="menuitem"><i class="fa-solid fa-retweet"></i> Show boosts</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if request.identity != identity and not outbound_block %}
|
||||
<form action="{{ identity.urls.action }}" method="POST" class="inline">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="block">
|
||||
<button role="menuitem"><i class="fa-solid fa-ban"></i> Block user</button>
|
||||
</form>
|
||||
{% if outbound_mute %}
|
||||
<form action="{{ identity.urls.action }}" method="POST" class="inline">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="unmute">
|
||||
<button role="menuitem"><i class="fa-solid fa-comment-slash"></i> Unmute user</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form action="{{ identity.urls.action }}" method="POST" class="inline">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="mute">
|
||||
<button role="menuitem"><i class="fa-solid fa-comment-slash"></i> Mute user</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if request.user.admin %}
|
||||
{% 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">
|
||||
<i class="fa-solid fa-bars"></i>
|
||||
</a>
|
||||
<menu>
|
||||
<a href="{{ identity.urls.admin_edit }}" role="menuitem">
|
||||
<i class="fa-solid fa-user-gear"></i> View in Admin
|
||||
</a>
|
||||
<a href="{{ identity.urls.djadmin_edit }}" role="menuitem">
|
||||
<i class="fa-solid fa-gear"></i> View in djadmin
|
||||
</a>
|
||||
{% endif %}
|
||||
</menu>
|
||||
</menu>
|
||||
{% endif %}
|
||||
</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,89 +12,75 @@
|
|||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block body_class %}has-banner{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if not request.htmx %}
|
||||
<h1 class="identity">
|
||||
{% if identity.local_image_url %}
|
||||
<img src="{{ identity.local_image_url.relative }}" class="banner">
|
||||
{% endif %}
|
||||
|
||||
<span
|
||||
_="on click halt the event then call imageviewer.show(me)"
|
||||
>
|
||||
<img src="{{ identity.local_icon_url.relative }}" class="icon"
|
||||
data-original-url="{{ identity.local_icon_url.relative }}"
|
||||
alt="Profile image for {{ identity.name }}"
|
||||
>
|
||||
</span>
|
||||
|
||||
{% if request.identity %}{% include "identity/_view_menu.html" %}{% endif %}
|
||||
|
||||
{{ identity.html_name_or_handle }}
|
||||
<small>
|
||||
@{{ identity.handle }}
|
||||
<a title="Copy handle"
|
||||
class="copy"
|
||||
tabindex="0"
|
||||
_="on click or keyup[key is 'Enter']
|
||||
writeText('@{{ identity.handle }}') into the navigator's clipboard
|
||||
then add .copied
|
||||
wait 2s
|
||||
then remove .copied">
|
||||
<i class="fa-solid fa-copy"></i>
|
||||
</a>
|
||||
</small>
|
||||
</h1>
|
||||
{% endif %}
|
||||
|
||||
{% if inbound_block %}
|
||||
<p class="system-note">
|
||||
This user has blocked you.
|
||||
</p>
|
||||
{% else %}
|
||||
|
||||
{% if not request.htmx %}
|
||||
{% if identity.summary %}
|
||||
<div class="bio">
|
||||
{{ identity.safe_summary }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if identity.metadata %}
|
||||
<div class="identity-metadata">
|
||||
{% for entry in identity.safe_metadata %}
|
||||
<div class="metadata-pair">
|
||||
<span class="metadata-name">{{ entry.name }}</span>
|
||||
<span class="metadata-value">{{ entry.value }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="view-options follows">
|
||||
<a href="{{ identity.urls.view }}" {% if not follows_page %}class="selected"{% endif %}><strong>{{ post_count }}</strong> posts</a>
|
||||
{% if identity.local and identity.config_identity.visible_follows %}
|
||||
<a href="{{ identity.urls.following }}" {% if not inbound and follows_page %}class="selected"{% endif %}><strong>{{ following_count }}</strong> following</a>
|
||||
<a href="{{ identity.urls.followers }}" {% if inbound and follows_page %}class="selected"{% endif %}><strong>{{ followers_count }}</strong> follower{{ followers_count|pluralize }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% 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 %}
|
||||
<section class="identity">
|
||||
{% if identity.local_image_url %}
|
||||
<img src="{{ identity.local_image_url.relative }}" class="banner">
|
||||
{% endif %}
|
||||
|
||||
<span
|
||||
_="on click halt the event then call imageviewer.show(me)"
|
||||
>
|
||||
<img src="{{ identity.local_icon_url.relative }}" class="icon"
|
||||
data-original-url="{{ identity.local_icon_url.relative }}"
|
||||
alt="Profile image for {{ identity.name }}"
|
||||
>
|
||||
</span>
|
||||
|
||||
{% if request.user.moderator or request.user.admin %}{% include "identity/_view_menu.html" %}{% endif %}
|
||||
|
||||
<h1>{{ identity.html_name_or_handle }}</h1>
|
||||
<small>
|
||||
@{{ identity.handle }}
|
||||
<a title="Copy handle"
|
||||
class="copy"
|
||||
tabindex="0"
|
||||
_="on click or keyup[key is 'Enter']
|
||||
writeText('@{{ identity.handle }}') into the navigator's clipboard
|
||||
then add .copied
|
||||
wait 2s
|
||||
then remove .copied">
|
||||
<i class="fa-solid fa-copy"></i>
|
||||
</a>
|
||||
</small>
|
||||
{% if identity.summary %}
|
||||
<div class="bio">
|
||||
{{ identity.safe_summary }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section class="invisible identity-metadata">
|
||||
{% if identity.metadata %}
|
||||
{% for entry in identity.safe_metadata %}
|
||||
<div class="metadata-pair">
|
||||
<span class="metadata-name">{{ entry.name }}</span>
|
||||
<span class="metadata-value">{{ entry.value }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
{% 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>
|
||||
{% if identity.local and identity.config_identity.visible_follows %}
|
||||
<a href="{{ identity.urls.following }}" {% if not inbound and follows_page %}class="selected"{% endif %}><strong>{{ following_count }}</strong> following</a>
|
||||
<a href="{{ identity.urls.followers }}" {% if inbound and follows_page %}class="selected"{% endif %}><strong>{{ followers_count }}</strong> follower{{ followers_count|pluralize }}</a>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section class="invisible">
|
||||
{% block subcontent %}
|
||||
|
||||
{% for post in page_obj %}
|
||||
|
@ -125,5 +111,6 @@
|
|||
</div>
|
||||
|
||||
{% endblock %}
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
<nav>
|
||||
<h3>Identity</h3>
|
||||
<a href="{% url "settings_profile" %}" {% if section == "profile" %}class="selected"{% endif %} title="Profile">
|
||||
{% if identity %}
|
||||
{% 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>
|
||||
<span>Profile</span>
|
||||
</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>
|
||||
<span>Interface</span>
|
||||
</a>
|
||||
<a href="{% url "settings_import_export" %}" {% if section == "importexport" %}class="selected"{% endif %} title="Interface">
|
||||
<a href="{% url "settings_import_export" handle=identity.handle %}" {% if section == "importexport" %}class="selected"{% endif %} title="Interface">
|
||||
<i class="fa-solid fa-cloud-arrow-up"></i>
|
||||
<span>Import/Export</span>
|
||||
</a>
|
||||
<hr>
|
||||
{% endif %}
|
||||
<h3>Account</h3>
|
||||
<a href="{% url "settings_security" %}" {% if section == "security" %}class="selected"{% endif %} title="Login & Security">
|
||||
<i class="fa-solid fa-key"></i>
|
||||
|
@ -22,64 +24,4 @@
|
|||
<i class="fa-solid fa-right-from-bracket" title="Logout"></i>
|
||||
<span>Logout</span>
|
||||
</a>
|
||||
{% if request.user.moderator or request.user.admin %}
|
||||
<hr>
|
||||
<h3>Moderation</h3>
|
||||
<a href="{% url "admin_identities" %}" {% if section == "identities" %}class="selected"{% endif %} title="Identities">
|
||||
<i class="fa-solid fa-id-card"></i>
|
||||
<span>Identities</span>
|
||||
</a>
|
||||
<a href="{% url "admin_invites" %}" {% if section == "invites" %}class="selected"{% endif %} title="Invites">
|
||||
<i class="fa-solid fa-envelope"></i>
|
||||
<span>Invites</span>
|
||||
</a>
|
||||
<a href="{% url "admin_hashtags" %}" {% if section == "hashtags" %}class="selected"{% endif %} title="Hashtags">
|
||||
<i class="fa-solid fa-hashtag"></i>
|
||||
<span>Hashtags</span>
|
||||
</a>
|
||||
<a href="{% url "admin_emoji" %}" {% if section == "emoji" %}class="selected"{% endif %} title="Emoji">
|
||||
<i class="fa-solid fa-icons"></i>
|
||||
<span>Emoji</span>
|
||||
</a>
|
||||
<a href="{% url "admin_reports" %}" {% if section == "reports" %}class="selected"{% endif %} title="Reports">
|
||||
<i class="fa-solid fa-flag"></i>
|
||||
<span>Reports</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if request.user.admin %}
|
||||
<hr>
|
||||
<h3>Administration</h3>
|
||||
<a href="{% url "admin_basic" %}" {% if section == "basic" %}class="selected"{% endif %} title="Basic">
|
||||
<i class="fa-solid fa-book"></i>
|
||||
<span>Basic</span>
|
||||
</a>
|
||||
<a href="{% url "admin_policies" %}" {% if section == "policies" %}class="selected"{% endif %} title="Policies">
|
||||
<i class="fa-solid fa-file-lines"></i>
|
||||
<span>Policies</span>
|
||||
</a>
|
||||
<a href="{% url "admin_announcements" %}" {% if section == "announcements" %}class="selected"{% endif %} title="Announcements">
|
||||
<i class="fa-solid fa-bullhorn"></i>
|
||||
<span>Announcements</span>
|
||||
</a>
|
||||
<a href="{% url "admin_domains" %}" {% if section == "domains" %}class="selected"{% endif %} title="Domains">
|
||||
<i class="fa-solid fa-globe"></i>
|
||||
<span>Domains</span>
|
||||
</a>
|
||||
<a href="{% url "admin_federation" %}" {% if section == "federation" %}class="selected"{% endif %} title="Federation">
|
||||
<i class="fa-solid fa-diagram-project"></i>
|
||||
<span>Federation</span>
|
||||
</a>
|
||||
<a href="{% url "admin_users" %}" {% if section == "users" %}class="selected"{% endif %} title="Users">
|
||||
<i class="fa-solid fa-users"></i>
|
||||
<span>Users</span>
|
||||
</a>
|
||||
<a href="{% url "admin_stator" %}" {% if section == "stator" %}class="selected"{% endif %} title="Stator">
|
||||
<i class="fa-solid fa-clock-rotate-left"></i>
|
||||
<span>Stator</span>
|
||||
</a>
|
||||
<a href="/djadmin" title="Django Admin" class="danger">
|
||||
<i class="fa-solid fa-gear"></i>
|
||||
<span>Django Admin</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
|
|
|
@ -2,6 +2,12 @@
|
|||
|
||||
{% block title %}{% block subtitle %}{% endblock %} - Settings{% endblock %}
|
||||
|
||||
{% block right_content %}
|
||||
{% include "settings/_menu.html" %}
|
||||
{% block content %}
|
||||
<div class="settings">
|
||||
{% include "settings/_menu.html" %}
|
||||
<div class="settings-content">
|
||||
{% block settings_content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
{% block subtitle %}Import/Export{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." method="POST" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
{% block subtitle %}Login & Security{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." method="POST">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
{% block subtitle %}Profile{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." method="POST" enctype="multipart/form-data"
|
||||
_="on submit metadata.collectMetadataFields()">
|
||||
{% csrf_token %}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
{% block subtitle %}{{ section.title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." method="POST" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% for title, fields in fieldsets.items %}
|
||||
|
|
|
@ -5,30 +5,6 @@ from django.contrib.auth.views import redirect_to_login
|
|||
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):
|
||||
return user_passes_test(
|
||||
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
|
||||
async def handle_outdated(cls, identity: "Identity"):
|
||||
|
||||
# Local identities never need fetching
|
||||
if identity.local:
|
||||
return cls.updated
|
||||
|
@ -231,6 +230,7 @@ class Identity(StatorModel):
|
|||
|
||||
class urls(urlman.Urls):
|
||||
view = "/@{self.username}@{self.domain_id}/"
|
||||
settings = "{view}settings/"
|
||||
action = "{view}action/"
|
||||
followers = "{view}followers/"
|
||||
following = "{view}following/"
|
||||
|
|
|
@ -14,6 +14,7 @@ class AdminSettingsPage(SettingsPage):
|
|||
"""
|
||||
|
||||
options_class = Config.SystemOptions
|
||||
template_name = "admin/settings.html"
|
||||
|
||||
def load_config(self):
|
||||
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.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.services import AnnouncementService
|
||||
|
||||
|
||||
@method_decorator(identity_required, name="dispatch")
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class AnnouncementDismiss(View):
|
||||
"""
|
||||
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.ld import canonicalise
|
||||
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.services import IdentityService
|
||||
from users.shortcuts import by_handle_or_404
|
||||
|
@ -65,15 +65,11 @@ class ViewIdentity(ListView):
|
|||
)
|
||||
|
||||
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):
|
||||
context = super().get_context_data()
|
||||
context["identity"] = self.identity
|
||||
context["interactions"] = PostInteraction.get_post_interactions(
|
||||
context["page_obj"],
|
||||
self.request.identity,
|
||||
)
|
||||
context["post_count"] = self.identity.posts.count()
|
||||
if self.identity.config_identity.visible_follows:
|
||||
context["followers_count"] = self.identity.inbound_follows.filter(
|
||||
|
@ -82,10 +78,6 @@ class ViewIdentity(ListView):
|
|||
context["following_count"] = self.identity.outbound_follows.filter(
|
||||
state__in=FollowStates.group_active()
|
||||
).count()
|
||||
if self.request.identity:
|
||||
context.update(
|
||||
IdentityService(self.identity).relationships(self.request.identity)
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
|
@ -242,7 +234,7 @@ class IdentityFollows(ListView):
|
|||
return context
|
||||
|
||||
|
||||
@method_decorator(identity_required, name="dispatch")
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class ActionIdentity(View):
|
||||
def post(self, request, handle):
|
||||
identity = by_handle_or_404(self.request, handle, local=False)
|
||||
|
@ -269,30 +261,6 @@ class ActionIdentity(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")
|
||||
class CreateIdentity(FormView):
|
||||
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.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.shortcuts import by_handle_or_404
|
||||
|
||||
|
||||
@method_decorator(identity_required, name="dispatch")
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class SubmitReport(FormView):
|
||||
"""
|
||||
Submits a report on a user or a post
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
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
|
||||
CsvFollowers,
|
||||
CsvFollowing,
|
||||
|
@ -13,6 +14,14 @@ from users.views.settings.security import SecurityPage # noqa
|
|||
from users.views.settings.settings_page import SettingsPage # noqa
|
||||
|
||||
|
||||
@method_decorator(identity_required, name="dispatch")
|
||||
class SettingsRoot(RedirectView):
|
||||
pattern_name = "settings_profile"
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class SettingsRoot(View):
|
||||
"""
|
||||
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.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
|
||||
|
||||
|
||||
@method_decorator(identity_required, name="dispatch")
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class ImportExportPage(FormView):
|
||||
"""
|
||||
Lets the identity's profile be edited
|
||||
|
@ -116,7 +116,6 @@ class CsvView(View):
|
|||
|
||||
|
||||
class CsvFollowing(CsvView):
|
||||
|
||||
columns = {
|
||||
"Account address": "get_handle",
|
||||
"Show boosts": "boosts",
|
||||
|
@ -140,7 +139,6 @@ class CsvFollowing(CsvView):
|
|||
|
||||
|
||||
class CsvFollowers(CsvView):
|
||||
|
||||
columns = {
|
||||
"Account address": "get_handle",
|
||||
}
|
||||
|
|
|
@ -3,14 +3,9 @@ from users.views.settings.settings_page import SettingsPage
|
|||
|
||||
|
||||
class InterfacePage(SettingsPage):
|
||||
|
||||
section = "interface"
|
||||
|
||||
options = {
|
||||
"toot_mode": {
|
||||
"title": "I Will Toot As I Please",
|
||||
"help_text": "Changes all 'Post' buttons to 'Toot!'",
|
||||
},
|
||||
"default_post_visibility": {
|
||||
"title": "Default Post Visibility",
|
||||
"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.",
|
||||
"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": {
|
||||
"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.",
|
||||
"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": {
|
||||
"title": "Light Mode",
|
||||
"help_text": "Use a light theme rather than the default dark theme.",
|
||||
|
@ -45,7 +28,6 @@ class InterfacePage(SettingsPage):
|
|||
}
|
||||
|
||||
layout = {
|
||||
"Posting": ["toot_mode", "default_post_visibility", "default_reply_visibility"],
|
||||
"Wellness": ["infinite_scroll", "visible_reaction_counts", "expand_linked_cws"],
|
||||
"Posting": ["default_post_visibility", "default_reply_visibility"],
|
||||
"Appearance": ["light_theme", "custom_css"],
|
||||
}
|
||||
|
|
|
@ -6,12 +6,13 @@ from django.views.generic import FormView
|
|||
|
||||
from core.html import FediverseHtmlParser
|
||||
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.shortcuts import by_handle_or_404
|
||||
from users.services import IdentityService
|
||||
|
||||
|
||||
@method_decorator(identity_required, name="dispatch")
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class ProfilePage(FormView):
|
||||
"""
|
||||
Lets the identity's profile be edited
|
||||
|
@ -61,28 +62,35 @@ class ProfilePage(FormView):
|
|||
return None
|
||||
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):
|
||||
identity = self.request.identity
|
||||
return {
|
||||
"name": identity.name,
|
||||
"name": self.identity.name,
|
||||
"summary": (
|
||||
FediverseHtmlParser(identity.summary).plain_text
|
||||
if identity.summary
|
||||
FediverseHtmlParser(self.identity.summary).plain_text
|
||||
if self.identity.summary
|
||||
else ""
|
||||
),
|
||||
"icon": identity.icon and identity.icon.url,
|
||||
"image": identity.image and identity.image.url,
|
||||
"discoverable": identity.discoverable,
|
||||
"visible_follows": identity.config_identity.visible_follows,
|
||||
"metadata": identity.metadata or [],
|
||||
"icon": self.identity.icon and self.identity.icon.url,
|
||||
"image": self.identity.image and self.identity.image.url,
|
||||
"discoverable": self.identity.discoverable,
|
||||
"visible_follows": self.identity.config_identity.visible_follows,
|
||||
"metadata": self.identity.metadata or [],
|
||||
}
|
||||
|
||||
def form_valid(self, form):
|
||||
# Update basic info
|
||||
identity = self.request.identity
|
||||
service = IdentityService(identity)
|
||||
identity.name = form.cleaned_data["name"]
|
||||
identity.discoverable = form.cleaned_data["discoverable"]
|
||||
service = IdentityService(self.identity)
|
||||
self.identity.name = form.cleaned_data["name"]
|
||||
self.identity.discoverable = form.cleaned_data["discoverable"]
|
||||
service.set_summary(form.cleaned_data["summary"])
|
||||
# Resize images
|
||||
icon = form.cleaned_data.get("icon")
|
||||
|
@ -91,20 +99,20 @@ class ProfilePage(FormView):
|
|||
service.set_icon(icon)
|
||||
if isinstance(image, File):
|
||||
service.set_image(image)
|
||||
identity.metadata = form.cleaned_data.get("metadata")
|
||||
self.identity.metadata = form.cleaned_data.get("metadata")
|
||||
|
||||
# Clear images if specified
|
||||
if "icon__clear" in self.request.POST:
|
||||
identity.icon = None
|
||||
self.identity.icon = None
|
||||
if "image__clear" in self.request.POST:
|
||||
identity.image = None
|
||||
self.identity.image = None
|
||||
|
||||
# Save and propagate
|
||||
identity.save()
|
||||
identity.transition_perform(IdentityStates.edited)
|
||||
self.identity.save()
|
||||
self.identity.transition_perform(IdentityStates.edited)
|
||||
|
||||
# Save profile-specific identity Config
|
||||
Config.set_identity(
|
||||
identity, "visible_follows", form.cleaned_data["visible_follows"]
|
||||
self.identity, "visible_follows", form.cleaned_data["visible_follows"]
|
||||
)
|
||||
return redirect(".")
|
||||
|
|
|
@ -2,10 +2,10 @@ from django import forms
|
|||
from django.utils.decorators import method_decorator
|
||||
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):
|
||||
"""
|
||||
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 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):
|
||||
"""
|
||||
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
|
||||
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):
|
||||
return Config.load_identity(self.request.identity)
|
||||
return Config.load_identity(self.identity)
|
||||
|
||||
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):
|
||||
config = self.load_config()
|
||||
|
@ -87,6 +93,8 @@ class SettingsPage(FormView):
|
|||
context["fieldsets"] = {}
|
||||
for title, fields in self.layout.items():
|
||||
context["fieldsets"][title] = [context["form"][field] for field in fields]
|
||||
if hasattr(self, "identity"):
|
||||
context["identity"] = self.identity
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
|
|
Loading…
Reference in a new issue