diff --git a/activities/views/compose.py b/activities/views/compose.py index 8cd378d..e1f640c 100644 --- a/activities/views/compose.py +++ b/activities/views/compose.py @@ -16,12 +16,12 @@ from activities.models import ( from core.files import blurhash_image, resize_image from core.html import FediverseHtmlParser from core.models import Config -from users.decorators import identity_required +from users.shortcuts import by_handle_or_404 +from django.contrib.auth.decorators import login_required -@method_decorator(identity_required, name="dispatch") +@method_decorator(login_required, name="dispatch") class Compose(FormView): - template_name = "activities/compose.html" class form_class(forms.Form): @@ -54,9 +54,9 @@ class Compose(FormView): ) reply_to = forms.CharField(widget=forms.HiddenInput(), required=False) - def __init__(self, request, *args, **kwargs): + def __init__(self, identity, *args, **kwargs): super().__init__(*args, **kwargs) - self.request = request + self.identity = identity self.fields["text"].widget.attrs[ "_" ] = rf""" @@ -83,7 +83,7 @@ class Compose(FormView): def clean_text(self): text = self.cleaned_data.get("text") # Check minimum interval - last_post = self.request.identity.posts.order_by("-created").first() + last_post = self.identity.posts.order_by("-created").first() if ( last_post and (timezone.now() - last_post.created).total_seconds() @@ -103,7 +103,7 @@ class Compose(FormView): return text def get_form(self, form_class=None): - return self.form_class(request=self.request, **self.get_form_kwargs()) + return self.form_class(identity=self.identity, **self.get_form_kwargs()) def get_initial(self): initial = super().get_initial() @@ -119,20 +119,20 @@ class Compose(FormView): else: initial[ "visibility" - ] = self.request.identity.config_identity.default_post_visibility + ] = self.identity.config_identity.default_post_visibility if self.reply_to: initial["reply_to"] = self.reply_to.pk if self.reply_to.visibility == Post.Visibilities.public: initial[ "visibility" - ] = self.request.identity.config_identity.default_reply_visibility + ] = self.identity.config_identity.default_reply_visibility else: initial["visibility"] = self.reply_to.visibility initial["content_warning"] = self.reply_to.summary # Build a set of mentions for the content to start as mentioned = {self.reply_to.author} mentioned.update(self.reply_to.mentions.all()) - mentioned.discard(self.request.identity) + mentioned.discard(self.identity) initial["text"] = "".join( f"@{identity.handle} " for identity in mentioned @@ -158,7 +158,7 @@ class Compose(FormView): self.post_obj.transition_perform(PostStates.edited) else: post = Post.create_local( - author=self.request.identity, + author=self.identity, content=form.cleaned_data["text"], summary=form.cleaned_data.get("content_warning"), visibility=form.cleaned_data["visibility"], @@ -166,17 +166,14 @@ class Compose(FormView): attachments=attachments, ) # Add their own timeline event for immediate visibility - TimelineEvent.add_post(self.request.identity, post) + TimelineEvent.add_post(self.identity, post) return redirect("/") def dispatch(self, request, handle=None, post_id=None, *args, **kwargs): + self.identity = by_handle_or_404(self.request, handle, local=True, fetch=False) self.post_obj = None if handle and post_id: - # Make sure the request identity owns the post! - if handle != request.identity.handle: - raise PermissionDenied("Post author is not requestor") - - self.post_obj = get_object_or_404(request.identity.posts, pk=post_id) + self.post_obj = get_object_or_404(self.identity.posts, pk=post_id) # Grab the reply-to post info now self.reply_to = None @@ -192,12 +189,13 @@ class Compose(FormView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["reply_to"] = self.reply_to + context["identity"] = self.identity if self.post_obj: context["post"] = self.post_obj return context -@method_decorator(identity_required, name="dispatch") +@method_decorator(login_required, name="dispatch") class ImageUpload(FormView): """ Handles image upload - returns a new input type hidden to embed in @@ -267,7 +265,7 @@ class ImageUpload(FormView): height=main_file.image.height, name=form.cleaned_data.get("description"), state=PostAttachmentStates.fetched, - author=self.request.identity, + author=self.identity, ) attachment.file.save( diff --git a/activities/views/follows.py b/activities/views/follows.py index 9713296..3e1eaab 100644 --- a/activities/views/follows.py +++ b/activities/views/follows.py @@ -2,11 +2,11 @@ from django.db import models from django.utils.decorators import method_decorator from django.views.generic import ListView -from users.decorators import identity_required +from django.contrib.auth.decorators import login_required from users.models import Follow, FollowStates, IdentityStates -@method_decorator(identity_required, name="dispatch") +@method_decorator(login_required, name="dispatch") class Follows(ListView): """ Shows followers/follows. diff --git a/activities/views/hashtags.py b/activities/views/hashtags.py index e5d5c44..bc939ab 100644 --- a/activities/views/hashtags.py +++ b/activities/views/hashtags.py @@ -4,10 +4,10 @@ from django.utils.decorators import method_decorator from django.views.generic import View from activities.models.hashtag import Hashtag -from users.decorators import identity_required +from django.contrib.auth.decorators import login_required -@method_decorator(identity_required, name="dispatch") +@method_decorator(login_required, name="dispatch") class HashtagFollow(View): """ Follows/unfollows a hashtag with the current identity diff --git a/activities/views/posts.py b/activities/views/posts.py index d8e6fdc..1f34341 100644 --- a/activities/views/posts.py +++ b/activities/views/posts.py @@ -9,7 +9,7 @@ from activities.models import Post, PostInteraction, PostStates from activities.services import PostService from core.decorators import cache_page_by_ap_json from core.ld import canonicalise -from users.decorators import identity_required +from django.contrib.auth.decorators import login_required from users.models import Identity from users.shortcuts import by_handle_or_404 @@ -19,7 +19,6 @@ from users.shortcuts import by_handle_or_404 ) @method_decorator(vary_on_headers("Accept"), name="dispatch") class Individual(TemplateView): - template_name = "activities/post.html" identity: Identity @@ -32,7 +31,7 @@ class Individual(TemplateView): self.post_obj = get_object_or_404( PostService.queryset() .filter(author=self.identity) - .visible_to(request.identity, include_replies=True), + .unlisted(include_replies=True), pk=post_id, ) if self.post_obj.state in [PostStates.deleted, PostStates.deleted_fanned_out]: @@ -48,18 +47,12 @@ class Individual(TemplateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - ancestors, descendants = PostService(self.post_obj).context( - self.request.identity - ) + ancestors, descendants = PostService(self.post_obj).context(None) context.update( { "identity": self.identity, "post": self.post_obj, - "interactions": PostInteraction.get_post_interactions( - [self.post_obj] + ancestors + descendants, - self.request.identity, - ), "link_original": True, "ancestors": ancestors, "descendants": descendants, @@ -78,108 +71,7 @@ class Individual(TemplateView): ) -@method_decorator(identity_required, name="dispatch") -class Like(View): - """ - Adds/removes a like from the current identity to the post - """ - - undo = False - - def post(self, request, handle, post_id): - identity = by_handle_or_404(self.request, handle, local=False) - post = get_object_or_404( - PostService.queryset() - .filter(author=identity) - .visible_to(request.identity, include_replies=True), - pk=post_id, - ) - service = PostService(post) - if self.undo: - service.unlike_as(request.identity) - else: - service.like_as(request.identity) - # Return either a redirect or a HTMX snippet - if request.htmx: - return render( - request, - "activities/_like.html", - { - "post": post, - "interactions": {"like": set() if self.undo else {post.pk}}, - }, - ) - return redirect(post.urls.view) - - -@method_decorator(identity_required, name="dispatch") -class Boost(View): - """ - Adds/removes a boost from the current identity to the post - """ - - undo = False - - def post(self, request, handle, post_id): - identity = by_handle_or_404(self.request, handle, local=False) - post = get_object_or_404( - PostService.queryset() - .filter(author=identity) - .visible_to(request.identity, include_replies=True), - pk=post_id, - ) - service = PostService(post) - if self.undo: - service.unboost_as(request.identity) - else: - service.boost_as(request.identity) - # Return either a redirect or a HTMX snippet - if request.htmx: - return render( - request, - "activities/_boost.html", - { - "post": post, - "interactions": {"boost": set() if self.undo else {post.pk}}, - }, - ) - return redirect(post.urls.view) - - -@method_decorator(identity_required, name="dispatch") -class Bookmark(View): - """ - Adds/removes a bookmark from the current identity to the post - """ - - undo = False - - def post(self, request, handle, post_id): - identity = by_handle_or_404(self.request, handle, local=False) - post = get_object_or_404( - PostService.queryset() - .filter(author=identity) - .visible_to(request.identity, include_replies=True), - pk=post_id, - ) - if self.undo: - request.identity.bookmarks.filter(post=post).delete() - else: - request.identity.bookmarks.get_or_create(post=post) - # Return either a redirect or a HTMX snippet - if request.htmx: - return render( - request, - "activities/_bookmark.html", - { - "post": post, - "bookmarks": set() if self.undo else {post.pk}, - }, - ) - return redirect(post.urls.view) - - -@method_decorator(identity_required, name="dispatch") +@method_decorator(login_required, name="dispatch") class Delete(TemplateView): """ Deletes a post diff --git a/activities/views/timelines.py b/activities/views/timelines.py index 2e7b710..6404d8b 100644 --- a/activities/views/timelines.py +++ b/activities/views/timelines.py @@ -6,48 +6,30 @@ from django.views.generic import ListView, TemplateView from activities.models import Hashtag, PostInteraction, TimelineEvent from activities.services import TimelineService from core.decorators import cache_page -from users.decorators import identity_required -from users.models import Bookmark, HashtagFollow - -from .compose import Compose +from django.contrib.auth.decorators import login_required +from users.models import Bookmark, HashtagFollow, Identity -@method_decorator(identity_required, name="dispatch") +@method_decorator(login_required, name="dispatch") class Home(TemplateView): + """ + Homepage for logged-in users - shows identities primarily. + """ template_name = "activities/home.html" - form_class = Compose.form_class - - def get_form(self, form_class=None): - return self.form_class(request=self.request, **self.get_form_kwargs()) - def get_context_data(self): - events = TimelineService(self.request.identity).home() - paginator = Paginator(events, 25) - page_number = self.request.GET.get("page") - event_page = paginator.get_page(page_number) - context = { - "interactions": PostInteraction.get_event_interactions( - event_page, - self.request.identity, - ), - "bookmarks": Bookmark.for_identity( - self.request.identity, event_page, "subject_post_id" - ), - "current_page": "home", - "allows_refresh": True, - "page_obj": event_page, - "form": self.form_class(request=self.request), + return { + "identities": Identity.objects.filter( + users__pk=self.request.user.pk + ).order_by("created"), } - return context @method_decorator( cache_page("cache_timeout_page_timeline", public_only=True), name="dispatch" ) class Tag(ListView): - template_name = "activities/tag.html" extra_context = { "current_page": "tag", @@ -86,7 +68,6 @@ class Tag(ListView): cache_page("cache_timeout_page_timeline", public_only=True), name="dispatch" ) class Local(ListView): - template_name = "activities/local.html" extra_context = { "current_page": "local", @@ -108,9 +89,8 @@ class Local(ListView): return context -@method_decorator(identity_required, name="dispatch") +@method_decorator(login_required, name="dispatch") class Federated(ListView): - template_name = "activities/federated.html" extra_context = { "current_page": "federated", @@ -132,9 +112,8 @@ class Federated(ListView): return context -@method_decorator(identity_required, name="dispatch") +@method_decorator(login_required, name="dispatch") class Notifications(ListView): - template_name = "activities/notifications.html" extra_context = { "current_page": "notifications", diff --git a/api/decorators.py b/api/decorators.py index 09550ee..411e314 100644 --- a/api/decorators.py +++ b/api/decorators.py @@ -6,8 +6,7 @@ from django.http import JsonResponse def identity_required(function): """ - API version of the identity_required decorator that just makes sure the - token is tied to one, not an app only. + Makes sure the token is tied to an identity, not an app only. """ @wraps(function) diff --git a/core/context.py b/core/context.py index d94e645..14e02bb 100644 --- a/core/context.py +++ b/core/context.py @@ -4,9 +4,6 @@ from core.models import Config def config_context(request): return { "config": Config.system, - "config_identity": ( - request.identity.config_identity if request.identity else None - ), "top_section": request.path.strip("/").split("/")[0], "opengraph_defaults": { "og:site_name": Config.system.site_name, diff --git a/core/views.py b/core/views.py index c782728..fb43c6e 100644 --- a/core/views.py +++ b/core/views.py @@ -27,7 +27,6 @@ def homepage(request): @method_decorator(cache_page(public_only=True), name="dispatch") class About(TemplateView): - template_name = "about.html" def get_context_data(self): @@ -87,46 +86,6 @@ class RobotsTxt(TemplateView): } -@method_decorator(cache_control(max_age=60 * 15), name="dispatch") -class AppManifest(StaticContentView): - """ - Serves a PWA manifest file. This is a view as we want to drive some - items from settings. - - NOTE: If this view changes to need runtime Config, it should change from - StaticContentView to View, otherwise the settings will only get - picked up during boot time. - """ - - content_type = "application/json" - - def get_static_content(self) -> str | bytes: - return json.dumps( - { - "$schema": "https://json.schemastore.org/web-manifest-combined.json", - "name": "Takahē", - "short_name": "Takahē", - "start_url": "/", - "display": "standalone", - "background_color": "#26323c", - "theme_color": "#26323c", - "description": "An ActivityPub server", - "icons": [ - { - "src": static("img/icon-128.png"), - "sizes": "128x128", - "type": "image/png", - }, - { - "src": static("img/icon-1024.png"), - "sizes": "1024x1024", - "type": "image/png", - }, - ], - } - ) - - class FlatPage(TemplateView): """ Serves a "flat page" from a config option, diff --git a/static/css/style.css b/static/css/style.css index c8c5740..f2345d2 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -147,13 +147,8 @@ body { main { width: 1100px; margin: 20px auto; - box-shadow: 0 0 var(--size-main-shadow) var(--color-main-shadow); - border-radius: 5px; -} - -.no-sidebar main { box-shadow: none; - max-width: 800px; + border-radius: 5px; } footer { @@ -172,10 +167,8 @@ footer a { header { display: flex; - height: 50px; -} - -.no-sidebar header { + height: 42px; + margin-top: -20px; justify-content: center; } @@ -184,11 +177,11 @@ header .logo { font-family: "Raleway"; font-weight: bold; background: var(--color-highlight); - border-radius: 5px 0 0 0; + border-radius: 0 0 5px 5px; text-transform: lowercase; - padding: 10px 11px 9px 10px; - height: 50px; - font-size: 130%; + padding: 6px 8px 5px 7px; + height: 42px; + font-size: 120%; color: var(--color-text-in-highlight); border-bottom: 3px solid rgba(0, 0, 0, 0); z-index: 10; @@ -196,10 +189,6 @@ header .logo { white-space: nowrap; } -.no-sidebar header .logo { - border-radius: 5px; -} - header .logo:hover { border-bottom: 3px solid rgba(255, 255, 255, 0.3); } @@ -211,31 +200,25 @@ header .logo img { } header menu { - flex-grow: 1; + flex-grow: 0; display: flex; list-style-type: none; justify-content: flex-start; z-index: 10; } -.no-sidebar header menu { - flex-grow: 0; -} - header menu a { - padding: 10px 20px 4px 20px; + padding: 6px 10px 4px 10px; color: var(--color-text-main); line-height: 30px; border-bottom: 3px solid rgba(0, 0, 0, 0); + margin: 0 2px; + text-align: center; } -.no-sidebar header menu a { - margin: 0 10px; -} - -body.has-banner header menu a { - background: rgba(0, 0, 0, 0.5); - border-right: 0; +header menu a.logo { + width: auto; + margin-right: 7px; } header menu a:hover, @@ -243,10 +226,10 @@ header menu a.selected { border-bottom: 3px solid var(--color-highlight); } -.no-sidebar header menu a:hover:not(.logo) { +header menu a:hover:not(.logo) { border-bottom: 3px solid rgba(0, 0, 0, 0); background-color: var(--color-bg-menu); - border-radius: 5px; + border-radius: 0 0 5px 5px; } header menu a i { @@ -279,26 +262,6 @@ header menu .gap { flex-grow: 1; } -header menu a.identity { - border-right: 0; - text-align: right; - padding-right: 10px; - background: var(--color-bg-menu) !important; - border-radius: 0 5px 0 0; - width: 250px; -} - -header menu a.identity i { - display: inline-block; - vertical-align: middle; - padding: 0 7px 2px 0; -} - -header menu a.identity a.view-profile { - display: inline-block; - margin-right: 20px; -} - header menu a img { display: inline-block; vertical-align: middle; @@ -313,7 +276,7 @@ header menu a small { } nav { - padding: 10px 10px 20px 0; + padding: 10px 0px 20px 5px; } nav hr { @@ -334,23 +297,23 @@ nav h3:first-child { nav a { display: block; color: var(--color-text-dull); - padding: 7px 18px 7px 13px; - border-left: 3px solid transparent; + padding: 7px 18px 7px 10px; + border-right: 3px solid transparent; } nav a.selected { color: var(--color-text-main); background: var(--color-bg-main); - border-radius: 0 5px 5px 0; + border-radius: 5px 0 0 5px; } nav a:hover { color: var(--color-text-main); - border-left: 3px solid var(--color-highlight); + border-right: 3px solid var(--color-highlight); } nav a.selected:hover { - border-left: 3px solid transparent; + border-right: 3px solid transparent; } nav a.danger { @@ -368,48 +331,55 @@ nav a i { display: inline-block; } +nav .identity-banner { + margin: 5px 0 10px 7px; +} + +nav .identity-banner img.icon { + max-width: 32px; + max-height: 32px; +} + +nav .identity-banner .avatar-link { + padding: 4px; +} + +nav .identity-banner .handle { + word-wrap: break-word; +} + +nav .identity-banner div.link { + color: var(--color-text-main); +} + +nav .identity-banner a, +nav .identity-banner a:hover { + border-right: none; +} + /* Left-right columns */ -.columns { +.settings { display: flex; align-items: stretch; justify-content: center; + margin-top: 20px; } -.left-column { +.settings .settings-content { flex-grow: 1; width: 300px; max-width: 900px; - padding: 15px; + padding: 0 15px 15px 15px; } -.left-column h1 { - margin: 0 0 10px 0; -} - -.left-column h1 small { - font-size: 60%; - color: var(--color-text-dull); - display: block; - margin: -10px 0 0 0; - padding: 0; -} - -.left-column h2 { - margin: 10px 0 10px 0; -} - -.left-column h3 { - margin: 10px 0 0 0; -} - -.right-column { +.settings nav { width: var(--md-sidebar-width); background: var(--color-bg-menu); border-radius: 0 0 5px 0; } -.right-column h2 { +.settings nav h2 { background: var(--color-highlight); color: var(--color-text-in-highlight); padding: 8px 10px; @@ -418,12 +388,6 @@ nav a i { text-transform: uppercase; } -.right-column footer { - padding: 0 10px 20px 10px; - font-size: 90%; - text-align: left; -} - img.emoji { height: var(--emoji-height); vertical-align: baseline; @@ -433,27 +397,52 @@ img.emoji { content: "…"; } -/* Generic markdown styling and sections */ +/* Generic styling and sections */ -.no-sidebar section { +section { max-width: 700px; background: var(--color-bg-box); box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1); - margin: 25px auto 45px auto; + margin: 0 auto 45px auto; padding: 5px 15px; } -.no-sidebar section:last-of-type { - margin-bottom: 10px; +section:first-of-type { + margin-top: 30px; } -.no-sidebar section.shell { +section.invisible { background: none; box-shadow: none; padding: 0; } -.no-sidebar #main-content>h1 { +section h1.above { + position: relative; + top: -35px; + left: -15px; + font-weight: bold; + text-transform: uppercase; + font-size: 120%; + color: var(--color-text-main); + margin-bottom: -20px; +} + +section p { + margin: 5px 0 10px 0; +} + +section:last-of-type { + margin-bottom: 10px; +} + +section.shell { + background: none; + box-shadow: none; + padding: 0; +} + +#main-content>h1 { max-width: 700px; margin: 25px auto 5px auto; font-weight: bold; @@ -462,7 +451,7 @@ img.emoji { color: var(--color-text-main); } -.no-sidebar h1+section { +h1+section { margin-top: 0; } @@ -493,9 +482,7 @@ p.authorization-code { .icon-menu .option { display: block; - margin: 0 0 20px 0; - background: var(--color-bg-box); - box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1); + margin: 0 0 10px 0; color: inherit; text-decoration: none; padding: 10px 20px; @@ -596,6 +583,40 @@ p.authorization-code { color: var(--color-text-dull); } +/* Icon/app listings */ + +.flex-icons { + display: flex; + list-style-type: none; + margin: 20px 0 10px 0; + padding: 0; + flex-wrap: wrap; + justify-content: space-between; +} + +.flex-icons a { + display: inline-block; + width: 200px; + margin: 0 0 15px 0; +} + +.flex-icons a img { + max-width: 64px; + max-height: 64px; + float: left; +} + +.flex-icons a h2 { + margin: 7px 0 0 72px; + font-size: 110%; +} + +.flex-icons a i { + margin: 0 0 0 72px; + font-size: 90%; + display: block; +} + /* Item tables */ table.items { @@ -685,7 +706,7 @@ table.items td.actions a.danger:hover { /* Forms */ -.no-sidebar form { +section form { max-width: 500px; margin: 40px auto; } @@ -1174,42 +1195,48 @@ blockquote { /* Identities */ -h1.identity { - margin: 0 0 20px 0; +section.identity { + overflow: hidden; + margin-bottom: 10px; } -h1.identity .banner { +section.identity .banner { width: 100%; height: 200px; object-fit: cover; display: block; width: calc(100% + 30px); - margin: -65px -15px 20px -15px; + margin: -5px -15px 20px -15px; border-radius: 5px 0 0 0; } -h1.identity .icon { +section.identity .icon { width: 80px; height: 80px; float: left; - margin: 0 20px 0 0; + margin: 0 20px 15px 0; cursor: pointer; } -h1.identity .emoji { +section.identity .emoji { height: var(--emoji-height); } -h1.identity small { +section.identity h1 { + margin: 30px 0 0 0; +} + +section.identity small { display: block; - font-size: 60%; + font-size: 100%; font-weight: normal; color: var(--color-text-dull); margin: -5px 0 0 0; } -.bio { - margin: 0 0 20px 0; +section.identity .bio { + clear: left; + margin: 0 0 10px 0; } .bio .emoji { @@ -1220,14 +1247,19 @@ h1.identity small { margin: 0 0 10px 0; } +.identity-metadata { + margin-bottom: 10px; + display: flex; + flex-wrap: wrap; + justify-content: center; +} + .identity-metadata .metadata-pair { - display: block; - margin: 0px 0 10px 0; - background: var(--color-bg-box); - box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1); + display: inline-block; + width: 300px; + margin: 0px 0 5px 0; color: inherit; text-decoration: none; - padding: 10px 20px; border: 2px solid rgba(255, 255, 255, 0); border-radius: 10px; overflow: hidden; @@ -1235,24 +1267,18 @@ h1.identity small { .identity-metadata .metadata-pair .metadata-name { display: inline-block; - min-width: 80px; + min-width: 90px; margin-right: 15px; text-align: right; color: var(--color-text-dull); } -.identity-metadata .metadata-pair .metadata-name::after { - padding-left: 3px; - - color: var(--color-text-dull); -} - .system-note { background: var(--color-bg-menu); color: var(--color-text-dull); border-radius: 3px; padding: 5px 8px; - margin: 15px 0; + margin-bottom: 20px; } .system-note a { @@ -1321,13 +1347,12 @@ table.metadata td .emoji { } .view-options { - margin: 0 0 10px 0px; + margin-bottom: 10px; + padding: 0; display: flex; flex-wrap: wrap; -} - -.view-options.follows { - margin: 0 0 20px 0px; + background: none; + box-shadow: none; } .view-options a:not(.button) { @@ -1405,6 +1430,11 @@ table.metadata td .emoji { } +.identity-banner img.icon { + max-width: 64px; + max-height: 64px; +} + /* Posts */ @@ -1776,28 +1806,13 @@ form .post { padding: 0; } - body:not(.no-sidebar) header { - height: var(--md-header-height); - position: fixed; - width: 100%; - z-index: 9; - } - - body:not(.no-sidebar) header menu a { - background: var(--color-header-menu); - } - main { width: 100%; - margin: 0; + margin: 20px auto 20px auto; box-shadow: none; border-radius: 0; } - .no-sidebar main { - margin: 20px auto 20px auto; - } - .post .attachments a.image img { max-height: 300px; } diff --git a/takahe/settings.py b/takahe/settings.py index 58a8730..71465ca 100644 --- a/takahe/settings.py +++ b/takahe/settings.py @@ -220,7 +220,6 @@ MIDDLEWARE = [ "core.middleware.HeadersMiddleware", "core.middleware.ConfigLoadingMiddleware", "api.middleware.ApiTokenMiddleware", - "users.middleware.IdentityMiddleware", ] ROOT_URLCONF = "takahe.urls" diff --git a/takahe/urls.py b/takahe/urls.py index e78ebd5..354dff4 100644 --- a/takahe/urls.py +++ b/takahe/urls.py @@ -29,7 +29,6 @@ from users.views import ( urlpatterns = [ path("", core.homepage), path("robots.txt", core.RobotsTxt.as_view()), - path("manifest.json", core.AppManifest.as_view()), # Activity views path("notifications/", timelines.Notifications.as_view(), name="notifications"), path("local/", timelines.Local.as_view(), name="local"), @@ -57,27 +56,32 @@ urlpatterns = [ name="settings_security", ), path( - "settings/profile/", + "@/settings/", + settings.SettingsRoot.as_view(), + name="settings", + ), + path( + "@/settings/profile/", settings.ProfilePage.as_view(), name="settings_profile", ), path( - "settings/interface/", + "@/settings/interface/", settings.InterfacePage.as_view(), name="settings_interface", ), path( - "settings/import_export/", + "@/settings/import_export/", settings.ImportExportPage.as_view(), name="settings_import_export", ), path( - "settings/import_export/following.csv", + "@/settings/import_export/following.csv", settings.CsvFollowing.as_view(), name="settings_export_following_csv", ), path( - "settings/import_export/followers.csv", + "@/settings/import_export/followers.csv", settings.CsvFollowers.as_view(), name="settings_export_followers_csv", ), @@ -242,21 +246,13 @@ urlpatterns = [ path("@/following/", identity.IdentityFollows.as_view(inbound=False)), path("@/followers/", identity.IdentityFollows.as_view(inbound=True)), # Posts - path("compose/", compose.Compose.as_view(), name="compose"), + path("@/compose/", compose.Compose.as_view(), name="compose"), path( - "compose/image_upload/", + "@/compose/image_upload/", compose.ImageUpload.as_view(), name="compose_image_upload", ), path("@/posts//", posts.Individual.as_view()), - path("@/posts//like/", posts.Like.as_view()), - path("@/posts//unlike/", posts.Like.as_view(undo=True)), - path("@/posts//boost/", posts.Boost.as_view()), - path("@/posts//unboost/", posts.Boost.as_view(undo=True)), - path("@/posts//bookmark/", posts.Bookmark.as_view()), - path( - "@/posts//unbookmark/", posts.Bookmark.as_view(undo=True) - ), path("@/posts//delete/", posts.Delete.as_view()), path("@/posts//report/", report.SubmitReport.as_view()), path("@/posts//edit/", compose.Compose.as_view()), @@ -267,9 +263,7 @@ urlpatterns = [ path("auth/signup//", auth.Signup.as_view(), name="signup"), path("auth/reset/", auth.TriggerReset.as_view(), name="trigger_reset"), path("auth/reset//", auth.PerformReset.as_view(), name="password_reset"), - # Identity selection - path("@/activate/", identity.ActivateIdentity.as_view()), - path("identity/select/", identity.SelectIdentity.as_view(), name="identity_select"), + # Identity handling path("identity/create/", identity.CreateIdentity.as_view(), name="identity_create"), # Flat pages path("about/", core.About.as_view(), name="about"), diff --git a/templates/activities/_boost.html b/templates/activities/_boost.html deleted file mode 100644 index 5f10856..0000000 --- a/templates/activities/_boost.html +++ /dev/null @@ -1,11 +0,0 @@ -{% if post.pk in interactions.boost %} - - - - -{% else %} - - - - -{% endif %} diff --git a/templates/activities/_like.html b/templates/activities/_like.html deleted file mode 100644 index 6c45710..0000000 --- a/templates/activities/_like.html +++ /dev/null @@ -1,11 +0,0 @@ -{% if post.pk in interactions.like %} - - - - -{% else %} - - - - -{% endif %} diff --git a/templates/activities/_post.html b/templates/activities/_post.html index a983004..fd10fa4 100644 --- a/templates/activities/_post.html +++ b/templates/activities/_post.html @@ -27,11 +27,7 @@ {% if post.summary %} - {% if config_identity.expand_linked_cws %} -
- {% else %} -
- {% endif %} +
{{ post.summary }}
{% endif %} @@ -77,41 +73,47 @@
{% endif %} - {% if request.identity %} -
diff --git a/templates/activities/_reply.html b/templates/activities/_reply.html deleted file mode 100644 index 7ae3f88..0000000 --- a/templates/activities/_reply.html +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/templates/activities/compose.html b/templates/activities/compose.html index e587f16..ff0b3ec 100644 --- a/templates/activities/compose.html +++ b/templates/activities/compose.html @@ -26,7 +26,7 @@ {% endif %} {% if not post or post.attachments.count < 4 %} + + + {% empty %} +

You have no identities.

+ {% endfor %} + + Create a new identity + + +
+

Apps

+

+ To see your timelines, compose new messages, and follow people, + you will need to use a Mastodon-compatible app. Our favourites + are listed below. +

+ +
{% endblock %} diff --git a/templates/admin/announcement_create.html b/templates/admin/announcement_create.html index c89e521..079b6d9 100644 --- a/templates/admin/announcement_create.html +++ b/templates/admin/announcement_create.html @@ -1,8 +1,8 @@ -{% extends "settings/base.html" %} +{% extends "admin/base.html" %} {% block subtitle %}Create Announcement{% endblock %} -{% block content %} +{% block settings_content %}
{% csrf_token %}
diff --git a/templates/admin/announcement_delete.html b/templates/admin/announcement_delete.html index 574253d..928e9e0 100644 --- a/templates/admin/announcement_delete.html +++ b/templates/admin/announcement_delete.html @@ -2,7 +2,7 @@ {% block title %}Delete Announcement - Admin{% endblock %} -{% block content %} +{% block settings_content %}

Confirm Delete

diff --git a/templates/admin/announcement_edit.html b/templates/admin/announcement_edit.html index 426ad1c..a209197 100644 --- a/templates/admin/announcement_edit.html +++ b/templates/admin/announcement_edit.html @@ -1,8 +1,8 @@ -{% extends "settings/base.html" %} +{% extends "admin/base.html" %} {% block subtitle %}Announcement #{{ announcement.pk }}{% endblock %} -{% block content %} +{% block settings_content %} {% csrf_token %}
diff --git a/templates/admin/announcements.html b/templates/admin/announcements.html index 5ac1778..9aa84cd 100644 --- a/templates/admin/announcements.html +++ b/templates/admin/announcements.html @@ -1,8 +1,8 @@ -{% extends "settings/base.html" %} +{% extends "admin/base.html" %} {% block subtitle %}Announcements{% endblock %} -{% block content %} +{% block settings_content %} diff --git a/templates/admin/domain_create.html b/templates/admin/domain_create.html index 0cf5dc0..f84d724 100644 --- a/templates/admin/domain_create.html +++ b/templates/admin/domain_create.html @@ -1,8 +1,8 @@ -{% extends "settings/base.html" %} +{% extends "admin/base.html" %} {% block title %}Add Domain - Admin{% endblock %} -{% block content %} +{% block settings_content %}

Add A Domain

diff --git a/templates/admin/domain_delete.html b/templates/admin/domain_delete.html index c660090..1fa1808 100644 --- a/templates/admin/domain_delete.html +++ b/templates/admin/domain_delete.html @@ -1,8 +1,8 @@ -{% extends "settings/base.html" %} +{% extends "admin/base.html" %} {% block title %}Delete {{ domain.domain }} - Admin{% endblock %} -{% block content %} +{% block settings_content %} {% csrf_token %} diff --git a/templates/admin/domain_edit.html b/templates/admin/domain_edit.html index d10b04c..24f7302 100644 --- a/templates/admin/domain_edit.html +++ b/templates/admin/domain_edit.html @@ -1,8 +1,8 @@ -{% extends "settings/base.html" %} +{% extends "admin/base.html" %} {% block subtitle %}{{ domain.domain }}{% endblock %} -{% block content %} +{% block settings_content %} {% csrf_token %}

diff --git a/templates/admin/domains.html b/templates/admin/domains.html index a99e76e..1fa9751 100644 --- a/templates/admin/domains.html +++ b/templates/admin/domains.html @@ -1,8 +1,8 @@ -{% extends "settings/base.html" %} +{% extends "admin/base.html" %} {% block subtitle %}Domains{% endblock %} -{% block content %} +{% block settings_content %}
Add Domain diff --git a/templates/admin/emoji.html b/templates/admin/emoji.html index 670db55..44f64d5 100644 --- a/templates/admin/emoji.html +++ b/templates/admin/emoji.html @@ -1,9 +1,9 @@ -{% extends "settings/base.html" %} +{% extends "admin/base.html" %} {% load activity_tags %} {% block subtitle %}Emoji{% endblock %} -{% block content %} +{% block settings_content %} {% if local_only %} diff --git a/templates/admin/emoji_create.html b/templates/admin/emoji_create.html index 128bfcf..7051b00 100644 --- a/templates/admin/emoji_create.html +++ b/templates/admin/emoji_create.html @@ -1,8 +1,8 @@ -{% extends "settings/base.html" %} +{% extends "admin/base.html" %} {% block subtitle %}{{ emoji.shortcode }}{% endblock %} -{% block content %} +{% block settings_content %} {% csrf_token %}
diff --git a/templates/admin/federation.html b/templates/admin/federation.html index 442958f..ddbc91d 100644 --- a/templates/admin/federation.html +++ b/templates/admin/federation.html @@ -1,9 +1,9 @@ -{% extends "settings/base.html" %} +{% extends "admin/base.html" %} {% load activity_tags %} {% block subtitle %}Federation{% endblock %} -{% block content %} +{% block settings_content %} diff --git a/templates/admin/federation_edit.html b/templates/admin/federation_edit.html index e4d7226..a9e3d6d 100644 --- a/templates/admin/federation_edit.html +++ b/templates/admin/federation_edit.html @@ -1,8 +1,8 @@ -{% extends "settings/base.html" %} +{% extends "admin/base.html" %} {% block subtitle %}{{ domain.domain }}{% endblock %} -{% block content %} +{% block settings_content %} {% csrf_token %}

{{ domain }}

diff --git a/templates/admin/hashtag_edit.html b/templates/admin/hashtag_edit.html index 0bc345d..0a0ed48 100644 --- a/templates/admin/hashtag_edit.html +++ b/templates/admin/hashtag_edit.html @@ -1,8 +1,8 @@ -{% extends "settings/base.html" %} +{% extends "admin/base.html" %} {% block subtitle %}{{ hashtag.hashtag }}{% endblock %} -{% block content %} +{% block settings_content %} {% csrf_token %}
diff --git a/templates/admin/hashtags.html b/templates/admin/hashtags.html index 13bd804..5d0d443 100644 --- a/templates/admin/hashtags.html +++ b/templates/admin/hashtags.html @@ -1,8 +1,8 @@ -{% extends "settings/base.html" %} +{% extends "admin/base.html" %} {% block subtitle %}Hashtags{% endblock %} -{% block content %} +{% block settings_content %} {% for hashtag in page_obj %} diff --git a/templates/admin/identities.html b/templates/admin/identities.html index 3e41332..11bd525 100644 --- a/templates/admin/identities.html +++ b/templates/admin/identities.html @@ -1,9 +1,9 @@ -{% extends "settings/base.html" %} +{% extends "admin/base.html" %} {% load activity_tags %} {% block subtitle %}Identities{% endblock %} -{% block content %} +{% block settings_content %} {% if local_only %} diff --git a/templates/admin/identity_edit.html b/templates/admin/identity_edit.html index d7de0ca..7a50409 100644 --- a/templates/admin/identity_edit.html +++ b/templates/admin/identity_edit.html @@ -1,8 +1,8 @@ -{% extends "settings/base.html" %} +{% extends "admin/base.html" %} {% block subtitle %}{{ identity.name_or_handle }}{% endblock %} -{% block content %} +{% block settings_content %}

{{ identity.html_name_or_handle }} {{ identity.handle }}

{% csrf_token %} diff --git a/templates/admin/invite_create.html b/templates/admin/invite_create.html index 4708d52..02e4b15 100644 --- a/templates/admin/invite_create.html +++ b/templates/admin/invite_create.html @@ -1,8 +1,8 @@ -{% extends "settings/base.html" %} +{% extends "admin/base.html" %} {% block subtitle %}Create Invite{% endblock %} -{% block content %} +{% block settings_content %} {% csrf_token %}
diff --git a/templates/admin/invite_view.html b/templates/admin/invite_view.html index 595d612..1489fe7 100644 --- a/templates/admin/invite_view.html +++ b/templates/admin/invite_view.html @@ -1,8 +1,8 @@ -{% extends "settings/base.html" %} +{% extends "admin/base.html" %} {% block subtitle %}View Invite{% endblock %} -{% block content %} +{% block settings_content %} {% csrf_token %}
diff --git a/templates/admin/invites.html b/templates/admin/invites.html index 42c0667..f5f2cbf 100644 --- a/templates/admin/invites.html +++ b/templates/admin/invites.html @@ -1,9 +1,9 @@ -{% extends "settings/base.html" %} +{% extends "admin/base.html" %} {% load activity_tags %} {% block subtitle %}Invites{% endblock %} -{% block content %} +{% block settings_content %}
Create New diff --git a/templates/admin/report_view.html b/templates/admin/report_view.html index 521639f..3ca87be 100644 --- a/templates/admin/report_view.html +++ b/templates/admin/report_view.html @@ -1,8 +1,8 @@ -{% extends "settings/base.html" %} +{% extends "admin/base.html" %} {% block subtitle %}Report {{ report.pk }}{% endblock %} -{% block content %} +{% block settings_content %} {% csrf_token %}
diff --git a/templates/admin/reports.html b/templates/admin/reports.html index 453f84b..f70cbcb 100644 --- a/templates/admin/reports.html +++ b/templates/admin/reports.html @@ -1,9 +1,9 @@ -{% extends "settings/base.html" %} +{% extends "admin/base.html" %} {% load activity_tags %} {% block subtitle %}Reports{% endblock %} -{% block content %} +{% block settings_content %}
{% if all %} Show Resolved diff --git a/templates/admin/stator.html b/templates/admin/stator.html index a80d4ae..d93b0f8 100644 --- a/templates/admin/stator.html +++ b/templates/admin/stator.html @@ -1,8 +1,8 @@ -{% extends "settings/base.html" %} +{% extends "admin/base.html" %} {% block subtitle %}Stator{% endblock %} -{% block content %} +{% block settings_content %} {% for model, stats in model_stats.items %}
{{ model }} diff --git a/templates/admin/user_edit.html b/templates/admin/user_edit.html index 0e8b19a..130fa85 100644 --- a/templates/admin/user_edit.html +++ b/templates/admin/user_edit.html @@ -1,8 +1,8 @@ -{% extends "settings/base.html" %} +{% extends "admin/base.html" %} {% block subtitle %}{{ editing_user.email }}{% endblock %} -{% block content %} +{% block settings_content %}

{{ editing_user.email }}

{% csrf_token %} diff --git a/templates/admin/users.html b/templates/admin/users.html index f5fe0fb..9472b84 100644 --- a/templates/admin/users.html +++ b/templates/admin/users.html @@ -1,9 +1,9 @@ -{% extends "settings/base.html" %} +{% extends "admin/base.html" %} {% load activity_tags %} {% block subtitle %}Users{% endblock %} -{% block content %} +{% block settings_content %} diff --git a/templates/base.html b/templates/base.html index 1ef74f3..eb28a49 100644 --- a/templates/base.html +++ b/templates/base.html @@ -8,7 +8,6 @@ - @@ -39,61 +38,28 @@
{% block body_main %}
- + {% if user.is_authenticated %} - - - - - - - {% if allows_refresh %} - - - - {% endif %} -
- - {% if not request.identity %} - No Identity - - {% else %} - {{ request.identity.username }} - - {% endif %} - + {% else %} -
- Login + {% endif %} + +
- {% block full_content %} - {% include "activities/_image_viewer.html" %} - {% block pre_content %} - {% endblock %} -
-
- {% block content %} - {% endblock %} -
-
- {% block right_content %} - {% include "activities/_menu.html" %} - {% endblock %} - {% include "_footer.html" %} -
-
+ {% block content %} {% endblock %} {% endblock %}
{% block footer %} + {% include "_footer.html" %} {% endblock %} diff --git a/templates/base_plain.html b/templates/base_plain.html index a046826..9e00127 100644 --- a/templates/base_plain.html +++ b/templates/base_plain.html @@ -1,38 +1,5 @@ {% extends "base.html" %} -{% block body_class %}no-sidebar{% endblock %} - {% block opengraph %} {# Error pages don't have the context loaded, so disable opengraph to keep it from spewing errors #} {% endblock %} - -{% block body_main %} -
- - {% if not request.user.is_authenticated and current_page == "about" and config.signup_allowed %} - Sign Up - {% else %} - Home - {% endif %} - - {% if request.user.is_authenticated %} - Back - {% else %} - Login - {% endif %} - -
- -
- {% include "activities/_image_viewer.html" %} - {% block content %} - {% endblock %} -
-{% endblock %} - -{% block footer %} - {% include "_footer.html" %} -{% endblock %} diff --git a/templates/identity/_view_menu.html b/templates/identity/_view_menu.html index 4c05723..a9f5ac2 100644 --- a/templates/identity/_view_menu.html +++ b/templates/identity/_view_menu.html @@ -1,78 +1,19 @@ diff --git a/templates/identity/select.html b/templates/identity/select.html deleted file mode 100644 index 9d3a677..0000000 --- a/templates/identity/select.html +++ /dev/null @@ -1,23 +0,0 @@ -{% extends "identity/base.html" %} - -{% block title %}Select Identity{% endblock %} - -{% block content %} -
- {% for identity in identities %} - - - - {{ identity.html_name_or_handle }} - @{{ identity.handle }} - - - - {% empty %} -

You have no identities.

- {% endfor %} - - Create a new identity - -
-{% endblock %} diff --git a/templates/identity/view.html b/templates/identity/view.html index cc7326f..71b5242 100644 --- a/templates/identity/view.html +++ b/templates/identity/view.html @@ -12,89 +12,75 @@ {% endif %} {% endblock %} -{% block body_class %}has-banner{% endblock %} - {% block content %} - {% if not request.htmx %} -

- {% if identity.local_image_url %} - - {% endif %} - - - Profile image for {{ identity.name }} - - - {% if request.identity %}{% include "identity/_view_menu.html" %}{% endif %} - - {{ identity.html_name_or_handle }} - - @{{ identity.handle }} - - - - -

- {% endif %} - - {% if inbound_block %} -

- This user has blocked you. -

- {% else %} - - {% if not request.htmx %} - {% if identity.summary %} -
- {{ identity.safe_summary }} -
- {% endif %} - - {% if identity.metadata %} - - {% endif %} - -
- {{ post_count }} posts - {% if identity.local and identity.config_identity.visible_follows %} - {{ following_count }} following - {{ followers_count }} follower{{ followers_count|pluralize }} - {% endif %} -
- - {% if not identity.local %} - {% 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. - See their original profile ➔ -

- {% endif %} - {% endif %} +
+ {% if identity.local_image_url %} + {% endif %} + + Profile image for {{ identity.name }} + + + {% if request.user.moderator or request.user.admin %}{% include "identity/_view_menu.html" %}{% endif %} + +

{{ identity.html_name_or_handle }}

+ + @{{ identity.handle }} + + + + + {% if identity.summary %} +
+ {{ identity.safe_summary }} +
+ {% endif %} +
+ + + + {% if not identity.local %} +
+ {% 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. + See their original profile ➔ + {% endif %} +
+ {% else %} + +
+ {{ post_count }} posts + {% if identity.local and identity.config_identity.visible_follows %} + {{ following_count }} following + {{ followers_count }} follower{{ followers_count|pluralize }} + {% endif %} +
+ +
{% endblock %} + {% endif %} {% endblock %} diff --git a/templates/settings/_menu.html b/templates/settings/_menu.html index 86db673..a6dcb98 100644 --- a/templates/settings/_menu.html +++ b/templates/settings/_menu.html @@ -1,18 +1,20 @@ diff --git a/templates/settings/base.html b/templates/settings/base.html index d5efa69..5d7596d 100644 --- a/templates/settings/base.html +++ b/templates/settings/base.html @@ -2,6 +2,12 @@ {% block title %}{% block subtitle %}{% endblock %} - Settings{% endblock %} -{% block right_content %} - {% include "settings/_menu.html" %} +{% block content %} +
+ {% include "settings/_menu.html" %} +
+ {% block settings_content %} + {% endblock %} +
+
{% endblock %} diff --git a/templates/settings/import_export.html b/templates/settings/import_export.html index d0d5de9..5705cad 100644 --- a/templates/settings/import_export.html +++ b/templates/settings/import_export.html @@ -2,7 +2,7 @@ {% block subtitle %}Import/Export{% endblock %} -{% block content %} +{% block settings_content %}
{% csrf_token %} diff --git a/templates/settings/login_security.html b/templates/settings/login_security.html index 9a3b22d..740e3ed 100644 --- a/templates/settings/login_security.html +++ b/templates/settings/login_security.html @@ -2,7 +2,7 @@ {% block subtitle %}Login & Security{% endblock %} -{% block content %} +{% block settings_content %} {% csrf_token %}
diff --git a/templates/settings/profile.html b/templates/settings/profile.html index 3e878dd..6b5a712 100644 --- a/templates/settings/profile.html +++ b/templates/settings/profile.html @@ -2,7 +2,7 @@ {% block subtitle %}Profile{% endblock %} -{% block content %} +{% block settings_content %} {% csrf_token %} diff --git a/templates/settings/settings.html b/templates/settings/settings.html index 36a6c10..6490483 100644 --- a/templates/settings/settings.html +++ b/templates/settings/settings.html @@ -2,7 +2,7 @@ {% block subtitle %}{{ section.title }}{% endblock %} -{% block content %} +{% block settings_content %} {% csrf_token %} {% for title, fields in fieldsets.items %} diff --git a/users/decorators.py b/users/decorators.py index 395488b..9136a78 100644 --- a/users/decorators.py +++ b/users/decorators.py @@ -5,30 +5,6 @@ from django.contrib.auth.views import redirect_to_login from django.http import HttpResponseRedirect -def identity_required(function): - """ - Decorator for views that ensures an active identity is selected. - """ - - @wraps(function) - def inner(request, *args, **kwargs): - # They do have to be logged in - if not request.user.is_authenticated: - return redirect_to_login(next=request.get_full_path()) - # If there's no active one, try to auto-select one - if request.identity is None: - possible_identities = list(request.user.identities.all()) - if len(possible_identities) != 1: - # OK, send them to the identity selection page to select/create one - return HttpResponseRedirect("/identity/select/") - identity = possible_identities[0] - request.session["identity_id"] = identity.pk - request.identity = identity - return function(request, *args, **kwargs) - - return inner - - def moderator_required(function): return user_passes_test( lambda user: user.is_authenticated and (user.admin or user.moderator) diff --git a/users/middleware.py b/users/middleware.py deleted file mode 100644 index 7192954..0000000 --- a/users/middleware.py +++ /dev/null @@ -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 diff --git a/users/models/identity.py b/users/models/identity.py index ddbd956..d98e6f0 100644 --- a/users/models/identity.py +++ b/users/models/identity.py @@ -111,7 +111,6 @@ class IdentityStates(StateGraph): @classmethod async def handle_outdated(cls, identity: "Identity"): - # Local identities never need fetching if identity.local: return cls.updated @@ -231,6 +230,7 @@ class Identity(StatorModel): class urls(urlman.Urls): view = "/@{self.username}@{self.domain_id}/" + settings = "{view}settings/" action = "{view}action/" followers = "{view}followers/" following = "{view}following/" diff --git a/users/views/admin/settings.py b/users/views/admin/settings.py index 240d029..e5ca414 100644 --- a/users/views/admin/settings.py +++ b/users/views/admin/settings.py @@ -14,6 +14,7 @@ class AdminSettingsPage(SettingsPage): """ options_class = Config.SystemOptions + template_name = "admin/settings.html" def load_config(self): return Config.load_system() diff --git a/users/views/announcements.py b/users/views/announcements.py index b85f182..0bbc78f 100644 --- a/users/views/announcements.py +++ b/users/views/announcements.py @@ -3,12 +3,12 @@ from django.shortcuts import get_object_or_404 from django.utils.decorators import method_decorator from django.views.generic import View -from users.decorators import identity_required +from django.contrib.auth.decorators import login_required from users.models import Announcement from users.services import AnnouncementService -@method_decorator(identity_required, name="dispatch") +@method_decorator(login_required, name="dispatch") class AnnouncementDismiss(View): """ Dismisses an announcement for the current user diff --git a/users/views/identity.py b/users/views/identity.py index 84664f2..aacf7d2 100644 --- a/users/views/identity.py +++ b/users/views/identity.py @@ -17,7 +17,7 @@ from activities.services import TimelineService from core.decorators import cache_page, cache_page_by_ap_json from core.ld import canonicalise from core.models import Config -from users.decorators import identity_required +from django.contrib.auth.decorators import login_required from users.models import Domain, FollowStates, Identity, IdentityStates from users.services import IdentityService from users.shortcuts import by_handle_or_404 @@ -65,15 +65,11 @@ class ViewIdentity(ListView): ) def get_queryset(self): - return TimelineService(self.request.identity).identity_public(self.identity) + return TimelineService(None).identity_public(self.identity) def get_context_data(self): context = super().get_context_data() context["identity"] = self.identity - context["interactions"] = PostInteraction.get_post_interactions( - context["page_obj"], - self.request.identity, - ) context["post_count"] = self.identity.posts.count() if self.identity.config_identity.visible_follows: context["followers_count"] = self.identity.inbound_follows.filter( @@ -82,10 +78,6 @@ class ViewIdentity(ListView): context["following_count"] = self.identity.outbound_follows.filter( state__in=FollowStates.group_active() ).count() - if self.request.identity: - context.update( - IdentityService(self.identity).relationships(self.request.identity) - ) return context @@ -242,7 +234,7 @@ class IdentityFollows(ListView): return context -@method_decorator(identity_required, name="dispatch") +@method_decorator(login_required, name="dispatch") class ActionIdentity(View): def post(self, request, handle): identity = by_handle_or_404(self.request, handle, local=False) @@ -269,30 +261,6 @@ class ActionIdentity(View): return redirect(identity.urls.view) -@method_decorator(login_required, name="dispatch") -class SelectIdentity(TemplateView): - template_name = "identity/select.html" - - def get_context_data(self): - return { - "identities": Identity.objects.filter(users__pk=self.request.user.pk), - } - - -@method_decorator(login_required, name="dispatch") -class ActivateIdentity(View): - def get(self, request, handle): - identity = by_handle_or_404(request, handle) - if not identity.users.filter(pk=request.user.pk).exists(): - raise Http404() - request.session["identity_id"] = identity.id - # Get next URL, not allowing offsite links - next = request.GET.get("next") or "/" - if ":" in next: - next = "/" - return redirect("/") - - @method_decorator(login_required, name="dispatch") class CreateIdentity(FormView): template_name = "identity/create.html" diff --git a/users/views/report.py b/users/views/report.py index f7e29ed..74e0c30 100644 --- a/users/views/report.py +++ b/users/views/report.py @@ -3,12 +3,12 @@ from django.shortcuts import get_object_or_404, render from django.utils.decorators import method_decorator from django.views.generic import FormView -from users.decorators import identity_required +from django.contrib.auth.decorators import login_required from users.models import Report from users.shortcuts import by_handle_or_404 -@method_decorator(identity_required, name="dispatch") +@method_decorator(login_required, name="dispatch") class SubmitReport(FormView): """ Submits a report on a user or a post diff --git a/users/views/settings/__init__.py b/users/views/settings/__init__.py index e58c4bf..12cb10d 100644 --- a/users/views/settings/__init__.py +++ b/users/views/settings/__init__.py @@ -1,7 +1,8 @@ from django.utils.decorators import method_decorator -from django.views.generic import RedirectView +from django.views.generic import View +from django.shortcuts import redirect -from users.decorators import identity_required +from django.contrib.auth.decorators import login_required from users.views.settings.import_export import ( # noqa CsvFollowers, CsvFollowing, @@ -13,6 +14,14 @@ from users.views.settings.security import SecurityPage # noqa from users.views.settings.settings_page import SettingsPage # noqa -@method_decorator(identity_required, name="dispatch") -class SettingsRoot(RedirectView): - pattern_name = "settings_profile" +@method_decorator(login_required, name="dispatch") +class SettingsRoot(View): + """ + Redirects to a root settings page (varying on if there is an identity + in the URL or not) + """ + + def get(self, request, handle: str | None = None): + if handle: + return redirect("settings_profile", handle=handle) + return redirect("settings_security") diff --git a/users/views/settings/import_export.py b/users/views/settings/import_export.py index bdde3ba..b892ec3 100644 --- a/users/views/settings/import_export.py +++ b/users/views/settings/import_export.py @@ -6,11 +6,11 @@ from django.shortcuts import redirect from django.utils.decorators import method_decorator from django.views.generic import FormView, View -from users.decorators import identity_required +from django.contrib.auth.decorators import login_required from users.models import Follow, InboxMessage -@method_decorator(identity_required, name="dispatch") +@method_decorator(login_required, name="dispatch") class ImportExportPage(FormView): """ Lets the identity's profile be edited @@ -116,7 +116,6 @@ class CsvView(View): class CsvFollowing(CsvView): - columns = { "Account address": "get_handle", "Show boosts": "boosts", @@ -140,7 +139,6 @@ class CsvFollowing(CsvView): class CsvFollowers(CsvView): - columns = { "Account address": "get_handle", } diff --git a/users/views/settings/interface.py b/users/views/settings/interface.py index 556e850..ecf420b 100644 --- a/users/views/settings/interface.py +++ b/users/views/settings/interface.py @@ -3,14 +3,9 @@ from users.views.settings.settings_page import SettingsPage class InterfacePage(SettingsPage): - section = "interface" options = { - "toot_mode": { - "title": "I Will Toot As I Please", - "help_text": "Changes all 'Post' buttons to 'Toot!'", - }, "default_post_visibility": { "title": "Default Post Visibility", "help_text": "Visibility to use as default for new posts.", @@ -21,23 +16,11 @@ class InterfacePage(SettingsPage): "help_text": "Visibility to use as default for replies.", "choices": Post.Visibilities.choices, }, - "visible_reaction_counts": { - "title": "Show Boost and Like Counts", - "help_text": "Disable to hide the number of Likes and Boosts on a 'Post'", - }, "custom_css": { "title": "Custom CSS", "help_text": "Theme the website however you'd like, just for you. You should probably not use this unless you know what you're doing.", "display": "textarea", }, - "expand_linked_cws": { - "title": "Expand linked Content Warnings", - "help_text": "Expands all CWs of the same type at once, rather than one at a time.", - }, - "infinite_scroll": { - "title": "Infinite Scroll", - "help_text": "Automatically loads more posts when you get to the bottom of a timeline.", - }, "light_theme": { "title": "Light Mode", "help_text": "Use a light theme rather than the default dark theme.", @@ -45,7 +28,6 @@ class InterfacePage(SettingsPage): } layout = { - "Posting": ["toot_mode", "default_post_visibility", "default_reply_visibility"], - "Wellness": ["infinite_scroll", "visible_reaction_counts", "expand_linked_cws"], + "Posting": ["default_post_visibility", "default_reply_visibility"], "Appearance": ["light_theme", "custom_css"], } diff --git a/users/views/settings/profile.py b/users/views/settings/profile.py index bcbea8a..2e5c91f 100644 --- a/users/views/settings/profile.py +++ b/users/views/settings/profile.py @@ -6,12 +6,13 @@ from django.views.generic import FormView from core.html import FediverseHtmlParser from core.models.config import Config -from users.decorators import identity_required +from django.contrib.auth.decorators import login_required from users.models import IdentityStates +from users.shortcuts import by_handle_or_404 from users.services import IdentityService -@method_decorator(identity_required, name="dispatch") +@method_decorator(login_required, name="dispatch") class ProfilePage(FormView): """ Lets the identity's profile be edited @@ -61,28 +62,35 @@ class ProfilePage(FormView): return None return metadata + def dispatch(self, request, handle: str, *args, **kwargs): + self.identity = by_handle_or_404(self.request, handle, local=True, fetch=False) + return super().dispatch(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["identity"] = self.identity + return context + def get_initial(self): - identity = self.request.identity return { - "name": identity.name, + "name": self.identity.name, "summary": ( - FediverseHtmlParser(identity.summary).plain_text - if identity.summary + FediverseHtmlParser(self.identity.summary).plain_text + if self.identity.summary else "" ), - "icon": identity.icon and identity.icon.url, - "image": identity.image and identity.image.url, - "discoverable": identity.discoverable, - "visible_follows": identity.config_identity.visible_follows, - "metadata": identity.metadata or [], + "icon": self.identity.icon and self.identity.icon.url, + "image": self.identity.image and self.identity.image.url, + "discoverable": self.identity.discoverable, + "visible_follows": self.identity.config_identity.visible_follows, + "metadata": self.identity.metadata or [], } def form_valid(self, form): # Update basic info - identity = self.request.identity - service = IdentityService(identity) - identity.name = form.cleaned_data["name"] - identity.discoverable = form.cleaned_data["discoverable"] + service = IdentityService(self.identity) + self.identity.name = form.cleaned_data["name"] + self.identity.discoverable = form.cleaned_data["discoverable"] service.set_summary(form.cleaned_data["summary"]) # Resize images icon = form.cleaned_data.get("icon") @@ -91,20 +99,20 @@ class ProfilePage(FormView): service.set_icon(icon) if isinstance(image, File): service.set_image(image) - identity.metadata = form.cleaned_data.get("metadata") + self.identity.metadata = form.cleaned_data.get("metadata") # Clear images if specified if "icon__clear" in self.request.POST: - identity.icon = None + self.identity.icon = None if "image__clear" in self.request.POST: - identity.image = None + self.identity.image = None # Save and propagate - identity.save() - identity.transition_perform(IdentityStates.edited) + self.identity.save() + self.identity.transition_perform(IdentityStates.edited) # Save profile-specific identity Config Config.set_identity( - identity, "visible_follows", form.cleaned_data["visible_follows"] + self.identity, "visible_follows", form.cleaned_data["visible_follows"] ) return redirect(".") diff --git a/users/views/settings/security.py b/users/views/settings/security.py index 31c2b22..695d88f 100644 --- a/users/views/settings/security.py +++ b/users/views/settings/security.py @@ -2,10 +2,10 @@ from django import forms from django.utils.decorators import method_decorator from django.views.generic import FormView -from users.decorators import identity_required +from django.contrib.auth.decorators import login_required -@method_decorator(identity_required, name="dispatch") +@method_decorator(login_required, name="dispatch") class SecurityPage(FormView): """ Lets the identity's profile be edited diff --git a/users/views/settings/settings_page.py b/users/views/settings/settings_page.py index a92d507..7f6fcf8 100644 --- a/users/views/settings/settings_page.py +++ b/users/views/settings/settings_page.py @@ -8,10 +8,11 @@ from django.utils.decorators import method_decorator from django.views.generic import FormView from core.models.config import Config, UploadedImage -from users.decorators import identity_required +from django.contrib.auth.decorators import login_required +from users.shortcuts import by_handle_or_404 -@method_decorator(identity_required, name="dispatch") +@method_decorator(login_required, name="dispatch") class SettingsPage(FormView): """ Shows a settings page dynamically created from our settings layout @@ -67,11 +68,16 @@ class SettingsPage(FormView): # Create a form class dynamically (yeah, right?) and return that return type("SettingsForm", (forms.Form,), fields) + def dispatch(self, request, *args, **kwargs): + if "handle" in kwargs: + self.identity = by_handle_or_404(request, kwargs["handle"]) + return super().dispatch(request, *args, **kwargs) + def load_config(self): - return Config.load_identity(self.request.identity) + return Config.load_identity(self.identity) def save_config(self, key, value): - Config.set_identity(self.request.identity, key, value) + Config.set_identity(self.identity, key, value) def get_initial(self): config = self.load_config() @@ -87,6 +93,8 @@ class SettingsPage(FormView): context["fieldsets"] = {} for title, fields in self.layout.items(): context["fieldsets"][title] = [context["form"][field] for field in fields] + if hasattr(self, "identity"): + context["identity"] = self.identity return context def form_valid(self, form):