Start of new UI refactor

This commit is contained in:
Andrew Godwin 2023-04-27 00:54:38 -06:00
parent 7331591432
commit 5d68e3aaac
65 changed files with 519 additions and 1000 deletions

View file

@ -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(

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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",

View file

@ -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)

View file

@ -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,

View file

@ -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,

View file

@ -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;
} }

View file

@ -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"

View file

@ -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"),

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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,41 +73,47 @@
</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">
<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-star"></i>
<i class="fa-solid fa-bars"></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 &amp; Replies
</a> </a>
<menu> <a href="{{ post.urls.action_report }}" role="menuitem">
<a href="{{ post.urls.view }}" role="menuitem"> <i class="fa-solid fa-flag"></i> Report
<i class="fa-solid fa-comment"></i> View Post &amp; Replies </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>
<a href="{{ post.urls.action_report }}" role="menuitem"> <a href="{{ post.urls.action_delete }}" role="menuitem">
<i class="fa-solid fa-flag"></i> Report <i class="fa-solid fa-trash"></i> Delete
</a> </a>
{% if post.author == request.identity %} {% elif not post.local and post.url %}
<a href="{{ post.urls.action_edit }}" role="menuitem"> <a href="{{ post.url }}" role="menuitem">
<i class="fa-solid fa-pen-to-square"></i> Edit <i class="fa-solid fa-arrow-up-right-from-square"></i> See Original
</a> </a>
<a href="{{ post.urls.action_delete }}" role="menuitem"> {% endif %}
<i class="fa-solid fa-trash"></i> Delete {% if request.user.admin %}
</a> <a href="{{ post.urls.admin_edit }}" role="menuitem">
{% elif not post.local and post.url %} <i class="fa-solid fa-gear"></i> View In Admin
<a href="{{ post.url }}" role="menuitem"> </a>
<i class="fa-solid fa-arrow-up-right-from-square"></i> See Original {% endif %}
</a> </menu>
{% endif %} </div>
{% 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 %}
</div> </div>

View file

@ -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>

View file

@ -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

View file

@ -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 {% empty %}
</time> <p class="option empty">You have no identities.</p>
</div> {% endfor %}
{% include "activities/_post.html" with post=event.subject_post %} <a href="{% url "identity_create" %}" class="option new">
{% endif %} <i class="fa-solid fa-plus"></i> Create a new identity
{% empty %} </a>
Nothing to show yet. </section>
{% endfor %} <section>
<h1 class="above">Apps</h1>
<div class="pagination"> <p>
{% if page_obj.has_previous and not request.htmx %} To see your timelines, compose new messages, and follow people,
<a class="button" href=".?page={{ page_obj.previous_page_number }}">Previous Page</a> you will need to use a Mastodon-compatible app. Our favourites
{% endif %} are listed below.
</p>
{% if page_obj.has_next %} <div class="flex-icons">
<a class="button" href=".?page={{ page_obj.next_page_number }}" hx-get=".?page={{ page_obj.next_page_number }}" hx-select=".left-column > *:not(.view-options)" hx-target=".pagination" hx-swap="outerHTML" {% if config_identity.infinite_scroll %}hx-trigger="revealed"{% endif %}>Next Page</a> <a href="#">
{% endif %} <img src="{% static "img/apps/elk.svg" %}" alt="Elk logo">
</div> <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 %} {% endblock %}

View file

@ -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>

View file

@ -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">

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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 %}

View file

@ -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>

View file

@ -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>

View file

@ -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 %}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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 %}

View file

@ -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>

View file

@ -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>
<a class="logo" href="/">
<img src="{{ config.site_icon }}" width="32">
{{ config.site_name }}
</a>
<menu> <menu>
<a class="logo" href="/">
<img src="{{ config.site_icon }}" width="32">
{{ config.site_name }}
</a>
{% 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 %}
{{ request.identity.username }}
<img src="{{ request.identity.local_icon_url.relative }}" title="{{ request.identity.handle }}">
{% endif %}
</a>
{% else %} {% else %}
<div class="gap"></div> <a href="{% url "login" %}" title="Login"><i class="fa-solid fa-right-to-bracket"></i></a>
<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 %} {% block 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>
{% endblock %} {% endblock %}
{% endblock %} {% endblock %}
</main> </main>
{% block footer %} {% block footer %}
{% include "_footer.html" %}
{% endblock %} {% endblock %}
</body> </body>

View file

@ -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 %}

View file

@ -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 %} {% endif %}
<form action="{{ identity.urls.action }}" method="POST" class="inline-menu"> {% if request.user.admin or request.user.moderator %}
{% csrf_token %} <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">
{% if outbound_block %} <i class="fa-solid fa-bars"></i>
<input type="hidden" name="action" value="unblock"> </a>
<button class="destructive" title="Unblock"><i class="fa-solid fa-ban"></i> <menu>
Unblock
</button>
{% elif outbound_follow %}
<input type="hidden" name="action" value="unfollow">
<button class="destructive" title="Unfollow"><i class="fa-solid fa-user-minus"></i>
{% if outbound_follow.pending %}Follow Pending{% else %}Unfollow{% endif %}
</button>
{% else %}
<input type="hidden" name="action" value="follow">
<button><i class="fa-solid fa-user-plus"></i> Follow</button>
{% endif %}
</form>
{% endif %}
<a title="Menu" class="menu button" _="on click or keyup[key is 'Enter'] toggle .enabled on the next <menu/> then halt" aria-haspopup="menu" tabindex="0">
<i class="fa-solid fa-bars"></i>
</a>
<menu>
{% if outbound_follow %}
<form action="{{ identity.urls.action }}" method="POST" class="inline">
{% csrf_token %}
{% if outbound_follow.boosts %}
<input type="hidden" name="action" value="hide_boosts">
<button role="menuitem"><i class="fa-solid fa-retweet"></i> Hide boosts</button>
{% else %}
<input type="hidden" name="action" value="show_boosts">
<button role="menuitem"><i class="fa-solid fa-retweet"></i> Show boosts</button>
{% endif %}
</form>
{% endif %}
{% if request.identity != identity and not outbound_block %}
<form action="{{ identity.urls.action }}" method="POST" class="inline">
{% csrf_token %}
<input type="hidden" name="action" value="block">
<button role="menuitem"><i class="fa-solid fa-ban"></i> Block user</button>
</form>
{% if outbound_mute %}
<form action="{{ identity.urls.action }}" method="POST" class="inline">
{% csrf_token %}
<input type="hidden" name="action" value="unmute">
<button role="menuitem"><i class="fa-solid fa-comment-slash"></i> Unmute user</button>
</form>
{% else %}
<form action="{{ identity.urls.action }}" method="POST" class="inline">
{% csrf_token %}
<input type="hidden" name="action" value="mute">
<button role="menuitem"><i class="fa-solid fa-comment-slash"></i> Mute user</button>
</form>
{% endif %}
{% endif %}
{% if request.user.admin %}
<a href="{{ identity.urls.admin_edit }}" role="menuitem"> <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"> </menu>
<i class="fa-solid fa-gear"></i> View in djadmin {% endif %}
</a>
{% endif %}
</menu>
</div> </div>
</div> </div>

View file

@ -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 %}

View file

@ -12,89 +12,75 @@
{% 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 %}
<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 %}
{% endif %} {% 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 %} {% 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 %}

View file

@ -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 &amp; Security"> <a href="{% url "settings_security" %}" {% if section == "security" %}class="selected"{% endif %} title="Login &amp; 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>

View file

@ -2,6 +2,12 @@
{% block title %}{% block subtitle %}{% endblock %} - Settings{% endblock %} {% block title %}{% block subtitle %}{% endblock %} - Settings{% endblock %}
{% block right_content %} {% block content %}
{% include "settings/_menu.html" %} <div class="settings">
{% include "settings/_menu.html" %}
<div class="settings-content">
{% block settings_content %}
{% endblock %}
</div>
</div>
{% endblock %} {% endblock %}

View file

@ -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 %}

View file

@ -2,7 +2,7 @@
{% block subtitle %}Login &amp; Security{% endblock %} {% block subtitle %}Login &amp; Security{% endblock %}
{% block content %} {% block settings_content %}
<form action="." method="POST"> <form action="." method="POST">
{% csrf_token %} {% csrf_token %}
<fieldset> <fieldset>

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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)

View file

@ -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

View file

@ -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/"

View file

@ -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()

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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")

View file

@ -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",
} }

View file

@ -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"],
} }

View file

@ -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(".")

View file

@ -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

View file

@ -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):