diff --git a/activities/services/post.py b/activities/services/post.py index f225fdd..f7e0b31 100644 --- a/activities/services/post.py +++ b/activities/services/post.py @@ -72,7 +72,12 @@ class PostService: def unboost_as(self, identity: Identity): self.uninteract_as(identity, PostInteraction.Types.boost) - def context(self, identity: Identity | None) -> tuple[list[Post], list[Post]]: + def context( + self, + identity: Identity | None, + num_ancestors: int = 10, + num_descendants: int = 50, + ) -> tuple[list[Post], list[Post]]: """ Returns ancestor/descendant information. @@ -82,7 +87,6 @@ class PostService: If identity is provided, includes mentions/followers-only posts they can see. Otherwise, shows unlisted and above only. """ - num_ancestors = 10 num_descendants = 50 # Retrieve ancestors via parent walk ancestors: list[Post] = [] diff --git a/activities/views/compose.py b/activities/views/compose.py index e1f640c..0b81e82 100644 --- a/activities/views/compose.py +++ b/activities/views/compose.py @@ -16,7 +16,7 @@ from activities.models import ( from core.files import blurhash_image, resize_image from core.html import FediverseHtmlParser from core.models import Config -from users.shortcuts import by_handle_or_404 +from users.shortcuts import by_handle_for_user_or_404 from django.contrib.auth.decorators import login_required @@ -167,10 +167,10 @@ class Compose(FormView): ) # Add their own timeline event for immediate visibility TimelineEvent.add_post(self.identity, post) - return redirect("/") + return redirect(self.identity.urls.view) 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.identity = by_handle_for_user_or_404(self.request, handle) self.post_obj = None if handle and post_id: self.post_obj = get_object_or_404(self.identity.posts, pk=post_id) @@ -190,94 +190,7 @@ class Compose(FormView): context = super().get_context_data(**kwargs) context["reply_to"] = self.reply_to context["identity"] = self.identity + context["section"] = "compose" if self.post_obj: context["post"] = self.post_obj return context - - -@method_decorator(login_required, name="dispatch") -class ImageUpload(FormView): - """ - Handles image upload - returns a new input type hidden to embed in - the main form that references an orphaned PostAttachment - """ - - template_name = "activities/_image_upload.html" - - class form_class(forms.Form): - image = forms.ImageField( - widget=forms.FileInput( - attrs={ - "_": f""" - on change - if me.files[0].size > {settings.SETUP.MEDIA_MAX_IMAGE_FILESIZE_MB * 1024 ** 2} - add [@disabled=] to #upload - - remove - make called errorlist - make
  • called error - set size_in_mb to (me.files[0].size / 1024 / 1024).toFixed(2) - put 'File must be {settings.SETUP.MEDIA_MAX_IMAGE_FILESIZE_MB}MB or less (actual: ' + size_in_mb + 'MB)' into error - put error into errorlist - put errorlist before me - else - remove @disabled from #upload - remove - end - end - """ - } - ) - ) - description = forms.CharField(required=False) - - def clean_image(self): - value = self.cleaned_data["image"] - max_mb = settings.SETUP.MEDIA_MAX_IMAGE_FILESIZE_MB - max_bytes = max_mb * 1024 * 1024 - if value.size > max_bytes: - # Erase the file from our data to stop trying to show it again - self.files = {} - raise forms.ValidationError( - f"File must be {max_mb}MB or less (actual: {value.size / 1024 ** 2:.2f})" - ) - return value - - def form_invalid(self, form): - return super().form_invalid(form) - - def form_valid(self, form): - # Make a PostAttachment - main_file = resize_image( - form.cleaned_data["image"], - size=(2000, 2000), - cover=False, - ) - thumbnail_file = resize_image( - form.cleaned_data["image"], - size=(400, 225), - cover=True, - ) - attachment = PostAttachment.objects.create( - blurhash=blurhash_image(thumbnail_file), - mimetype="image/webp", - width=main_file.image.width, - height=main_file.image.height, - name=form.cleaned_data.get("description"), - state=PostAttachmentStates.fetched, - author=self.identity, - ) - - attachment.file.save( - main_file.name, - main_file, - ) - attachment.thumbnail.save( - thumbnail_file.name, - thumbnail_file, - ) - attachment.save() - # Return the response, with a hidden input plus a note - return render( - self.request, "activities/_image_uploaded.html", {"attachment": attachment} - ) diff --git a/activities/views/explore.py b/activities/views/explore.py deleted file mode 100644 index ddb1e6c..0000000 --- a/activities/views/explore.py +++ /dev/null @@ -1,26 +0,0 @@ -from django.views.generic import ListView - -from activities.models import Hashtag - - -class ExploreTag(ListView): - - template_name = "activities/explore_tag.html" - extra_context = { - "current_page": "explore", - "allows_refresh": True, - } - paginate_by = 20 - - def get_queryset(self): - return ( - Hashtag.objects.public() - .filter( - stats__total__gt=0, - ) - .order_by("-stats__total") - )[:20] - - -class Explore(ExploreTag): - pass diff --git a/activities/views/hashtags.py b/activities/views/hashtags.py deleted file mode 100644 index bc939ab..0000000 --- a/activities/views/hashtags.py +++ /dev/null @@ -1,38 +0,0 @@ -from django.http import HttpRequest -from django.shortcuts import get_object_or_404, redirect, render -from django.utils.decorators import method_decorator -from django.views.generic import View - -from activities.models.hashtag import Hashtag -from django.contrib.auth.decorators import login_required - - -@method_decorator(login_required, name="dispatch") -class HashtagFollow(View): - """ - Follows/unfollows a hashtag with the current identity - """ - - undo = False - - def post(self, request: HttpRequest, hashtag): - hashtag = get_object_or_404( - Hashtag, - pk=hashtag, - ) - follow = None - if self.undo: - request.identity.hashtag_follows.filter(hashtag=hashtag).delete() - else: - follow = request.identity.hashtag_follows.get_or_create(hashtag=hashtag) - # Return either a redirect or a HTMX snippet - if request.htmx: - return render( - request, - "activities/_hashtag_follow.html", - { - "hashtag": hashtag, - "follow": follow, - }, - ) - return redirect(hashtag.urls.view) diff --git a/activities/views/posts.py b/activities/views/posts.py index 1f34341..bfac8eb 100644 --- a/activities/views/posts.py +++ b/activities/views/posts.py @@ -47,7 +47,9 @@ class Individual(TemplateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - ancestors, descendants = PostService(self.post_obj).context(None) + ancestors, descendants = PostService(self.post_obj).context( + identity=None, num_ancestors=2 + ) context.update( { @@ -69,27 +71,3 @@ class Individual(TemplateView): canonicalise(self.post_obj.to_ap(), include_security=True), content_type="application/activity+json", ) - - -@method_decorator(login_required, name="dispatch") -class Delete(TemplateView): - """ - Deletes a post - """ - - template_name = "activities/post_delete.html" - - def dispatch(self, request, handle, post_id): - # Make sure the request identity owns the post! - if handle != request.identity.handle: - raise PermissionDenied("Post author is not requestor") - self.identity = by_handle_or_404(self.request, handle, local=False) - self.post_obj = get_object_or_404(self.identity.posts, pk=post_id) - return super().dispatch(request) - - def get_context_data(self): - return {"post": self.post_obj} - - def post(self, request): - PostService(self.post_obj).delete() - return redirect("/") diff --git a/activities/views/search.py b/activities/views/search.py index 4c709e0..afff395 100644 --- a/activities/views/search.py +++ b/activities/views/search.py @@ -15,7 +15,7 @@ class Search(FormView): ) def form_valid(self, form): - searcher = SearchService(form.cleaned_data["query"], self.request.identity) + searcher = SearchService(form.cleaned_data["query"], identity=None) # Render results context = self.get_context_data(form=form) context["results"] = searcher.search_all() diff --git a/activities/views/timelines.py b/activities/views/timelines.py index 6404d8b..4f8dad4 100644 --- a/activities/views/timelines.py +++ b/activities/views/timelines.py @@ -1,3 +1,5 @@ +from typing import Any +from django.http import Http404 from django.core.paginator import Paginator from django.shortcuts import get_object_or_404, redirect from django.utils.decorators import method_decorator @@ -8,6 +10,7 @@ from activities.services import TimelineService from core.decorators import cache_page from django.contrib.auth.decorators import login_required from users.models import Bookmark, HashtagFollow, Identity +from users.views.base import IdentityViewMixin @method_decorator(login_required, name="dispatch") @@ -46,74 +49,15 @@ class Tag(ListView): return super().get(request, *args, **kwargs) def get_queryset(self): - return TimelineService(self.request.identity).hashtag(self.hashtag) + return TimelineService(None).hashtag(self.hashtag) def get_context_data(self): context = super().get_context_data() context["hashtag"] = self.hashtag - context["interactions"] = PostInteraction.get_post_interactions( - context["page_obj"], self.request.identity - ) - context["bookmarks"] = Bookmark.for_identity( - self.request.identity, context["page_obj"] - ) - context["follow"] = HashtagFollow.maybe_get( - self.request.identity, - self.hashtag, - ) return context -@method_decorator( - cache_page("cache_timeout_page_timeline", public_only=True), name="dispatch" -) -class Local(ListView): - template_name = "activities/local.html" - extra_context = { - "current_page": "local", - "allows_refresh": True, - } - paginate_by = 25 - - def get_queryset(self): - return TimelineService(self.request.identity).local() - - def get_context_data(self): - context = super().get_context_data() - context["interactions"] = PostInteraction.get_post_interactions( - context["page_obj"], self.request.identity - ) - context["bookmarks"] = Bookmark.for_identity( - self.request.identity, context["page_obj"] - ) - return context - - -@method_decorator(login_required, name="dispatch") -class Federated(ListView): - template_name = "activities/federated.html" - extra_context = { - "current_page": "federated", - "allows_refresh": True, - } - paginate_by = 25 - - def get_queryset(self): - return TimelineService(self.request.identity).federated() - - def get_context_data(self): - context = super().get_context_data() - context["interactions"] = PostInteraction.get_post_interactions( - context["page_obj"], self.request.identity - ) - context["bookmarks"] = Bookmark.for_identity( - self.request.identity, context["page_obj"] - ) - return context - - -@method_decorator(login_required, name="dispatch") -class Notifications(ListView): +class Notifications(IdentityViewMixin, ListView): template_name = "activities/notifications.html" extra_context = { "current_page": "notifications", @@ -125,7 +69,6 @@ class Notifications(ListView): "boosted": TimelineEvent.Types.boosted, "mentioned": TimelineEvent.Types.mentioned, "liked": TimelineEvent.Types.liked, - "identity_created": TimelineEvent.Types.identity_created, } def get_queryset(self): @@ -143,7 +86,7 @@ class Notifications(ListView): for type_name, type in self.notification_types.items(): if notification_options.get(type_name, True): types.append(type) - return TimelineService(self.request.identity).notifications(types) + return TimelineService(self.identity).notifications(types) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -164,12 +107,6 @@ class Notifications(ListView): events.append(event) # Retrieve what kinds of things to show context["events"] = events + context["identity"] = self.identity context["notification_options"] = self.request.session["notification_options"] - context["interactions"] = PostInteraction.get_event_interactions( - context["page_obj"], - self.request.identity, - ) - context["bookmarks"] = Bookmark.for_identity( - self.request.identity, context["page_obj"], "subject_post_id" - ) return context diff --git a/core/decorators.py b/core/decorators.py index dc8d4d2..dd4fc2f 100644 --- a/core/decorators.py +++ b/core/decorators.py @@ -20,16 +20,6 @@ def vary_by_ap_json(request, *args, **kwargs) -> str: return "not_ap" -def vary_by_identity(request, *args, **kwargs) -> str: - """ - Return a cache usable string token that is different based upon the - request.identity - """ - if request.identity: - return f"ident{request.identity.pk}" - return "identNone" - - def cache_page( timeout: int | str = "cache_timeout_page_default", *, diff --git a/core/views.py b/core/views.py index fb43c6e..7fae532 100644 --- a/core/views.py +++ b/core/views.py @@ -30,7 +30,7 @@ class About(TemplateView): template_name = "about.html" def get_context_data(self): - service = TimelineService(self.request.identity) + service = TimelineService(None) return { "current_page": "about", "content": mark_safe( diff --git a/static/img/apps/elk.svg b/static/img/apps/elk.svg new file mode 100755 index 0000000..aea8c9d --- /dev/null +++ b/static/img/apps/elk.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + diff --git a/static/img/apps/ivory.webp b/static/img/apps/ivory.webp new file mode 100755 index 0000000..6f37fca Binary files /dev/null and b/static/img/apps/ivory.webp differ diff --git a/static/img/apps/tusky.png b/static/img/apps/tusky.png new file mode 100755 index 0000000..8879519 Binary files /dev/null and b/static/img/apps/tusky.png differ diff --git a/takahe/urls.py b/takahe/urls.py index 354dff4..32607a7 100644 --- a/takahe/urls.py +++ b/takahe/urls.py @@ -5,9 +5,6 @@ from django.urls import include, path, re_path from activities.views import ( compose, debug, - explore, - follows, - hashtags, posts, search, timelines, @@ -22,28 +19,21 @@ from users.views import ( announcements, auth, identity, - report, settings, ) +from users.views.settings import follows urlpatterns = [ path("", core.homepage), path("robots.txt", core.RobotsTxt.as_view()), # Activity views - path("notifications/", timelines.Notifications.as_view(), name="notifications"), - path("local/", timelines.Local.as_view(), name="local"), - path("federated/", timelines.Federated.as_view(), name="federated"), + path( + "@/notifications/", + timelines.Notifications.as_view(), + name="notifications", + ), path("search/", search.Search.as_view(), name="search"), path("tags//", timelines.Tag.as_view(), name="tag"), - path("tags//follow/", hashtags.HashtagFollow.as_view()), - path("tags//unfollow/", hashtags.HashtagFollow.as_view(undo=True)), - path("explore/", explore.Explore.as_view(), name="explore"), - path("explore/tags/", explore.ExploreTag.as_view(), name="explore-tag"), - path( - "follows/", - follows.Follows.as_view(), - name="follows", - ), # Settings views path( "settings/", @@ -70,6 +60,11 @@ urlpatterns = [ settings.InterfacePage.as_view(), name="settings_interface", ), + path( + "@/settings/follows/", + settings.FollowsPage.as_view(), + name="settings_follows", + ), path( "@/settings/import_export/", settings.ImportExportPage.as_view(), @@ -240,22 +235,12 @@ urlpatterns = [ path("@/", identity.ViewIdentity.as_view()), path("@/inbox/", activitypub.Inbox.as_view()), path("@/outbox/", activitypub.Outbox.as_view()), - path("@/action/", identity.ActionIdentity.as_view()), path("@/rss/", identity.IdentityFeed()), - path("@/report/", report.SubmitReport.as_view()), 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/image_upload/", - compose.ImageUpload.as_view(), - name="compose_image_upload", - ), path("@/posts//", posts.Individual.as_view()), - path("@/posts//delete/", posts.Delete.as_view()), - path("@/posts//report/", report.SubmitReport.as_view()), - path("@/posts//edit/", compose.Compose.as_view()), # Authentication path("auth/login/", auth.Login.as_view(), name="login"), path("auth/logout/", auth.Logout.as_view(), name="logout"), diff --git a/templates/activities/_bookmark.html b/templates/activities/_bookmark.html deleted file mode 100644 index ab9731d..0000000 --- a/templates/activities/_bookmark.html +++ /dev/null @@ -1,9 +0,0 @@ -{% if post.pk in bookmarks %} - - - -{% else %} - - - -{% endif %} diff --git a/templates/activities/_hashtag_follow.html b/templates/activities/_hashtag_follow.html deleted file mode 100644 index cdf00c5..0000000 --- a/templates/activities/_hashtag_follow.html +++ /dev/null @@ -1,9 +0,0 @@ -{% if follow %} - -{% else %} - -{% endif %} diff --git a/templates/activities/_image_upload.html b/templates/activities/_image_upload.html deleted file mode 100644 index dec9160..0000000 --- a/templates/activities/_image_upload.html +++ /dev/null @@ -1,15 +0,0 @@ -
    - {% csrf_token %} - {% include "forms/_field.html" with field=form.image %} - {% include "forms/_field.html" with field=form.description %} -
    - - -
    -
    diff --git a/templates/activities/_image_uploaded.html b/templates/activities/_image_uploaded.html deleted file mode 100644 index e2424ce..0000000 --- a/templates/activities/_image_uploaded.html +++ /dev/null @@ -1,19 +0,0 @@ -
    - - -

    - {{ attachment.name|default:"(no description)" }} -

    -
    - -
    -
    -{% if request.htmx %} - -{% endif %} diff --git a/templates/activities/_post.html b/templates/activities/_post.html index fd10fa4..9fc52b3 100644 --- a/templates/activities/_post.html +++ b/templates/activities/_post.html @@ -76,15 +76,15 @@
    - + - + - + @@ -96,14 +96,7 @@ Report - {% if post.author == request.identity %} - - Edit - - - Delete - - {% elif not post.local and post.url %} + {% if not post.local and post.url %} See Original diff --git a/templates/activities/compose.html b/templates/activities/compose.html index ff0b3ec..b7b4cdf 100644 --- a/templates/activities/compose.html +++ b/templates/activities/compose.html @@ -1,41 +1,26 @@ -{% extends "base.html" %} +{% extends "settings/base.html" %} {% block title %}Compose{% endblock %} -{% block content %} +{% block settings_content %}
    {% csrf_token %}
    - Content + Compose {% if reply_to %} {% include "activities/_mini_post.html" with post=reply_to %} {% endif %} +

    For more advanced posting options, like editing and image uploads, please use an app.

    {{ form.reply_to }} {{ form.id }} {% include "forms/_field.html" with field=form.text %} {% include "forms/_field.html" with field=form.content_warning %} {% include "forms/_field.html" with field=form.visibility %}
    -
    - Images - {% if post %} - {% for attachment in post.attachments.all %} - {% include "activities/_image_uploaded.html" %} - {% endfor %} - {% endif %} - {% if not post or post.attachments.count < 4 %} - - {% endif %} -
    {{ config.post_length }} - +
    {% endblock %} diff --git a/templates/activities/federated.html b/templates/activities/federated.html deleted file mode 100644 index 9e61b6b..0000000 --- a/templates/activities/federated.html +++ /dev/null @@ -1,3 +0,0 @@ -{% extends "activities/local.html" %} - -{% block title %}Federated Timeline{% endblock %} diff --git a/templates/activities/local.html b/templates/activities/local.html deleted file mode 100644 index 5a9b6a4..0000000 --- a/templates/activities/local.html +++ /dev/null @@ -1,25 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Local Timeline{% endblock %} - -{% block content %} - {% if page_obj.number == 1 %} - {% include "_announcements.html" %} - {% endif %} - - {% for post in page_obj %} - {% include "activities/_post.html" with feedindex=forloop.counter %} - {% empty %} - No posts yet. - {% endfor %} - - -{% endblock %} diff --git a/templates/activities/notifications.html b/templates/activities/notifications.html index d4116dd..96ffd9a 100644 --- a/templates/activities/notifications.html +++ b/templates/activities/notifications.html @@ -1,55 +1,50 @@ -{% extends "base.html" %} +{% extends "settings/base.html" %} {% block title %}Notifications{% endblock %} -{% block content %} +{% block settings_content %} {% if page_obj.number == 1 %} {% include "_announcements.html" %} {% endif %} -
    - {% if notification_options.followed %} - Followers - {% else %} - Followers - {% endif %} - {% if notification_options.boosted %} - Boosts - {% else %} - Boosts - {% endif %} - {% if notification_options.liked %} - Likes - {% else %} - Likes - {% endif %} - {% if notification_options.mentioned %} - Mentions - {% else %} - Mentions - {% endif %} - {% if request.user.admin %} - {% if notification_options.identity_created %} - New Identities +
    - {% for event in events %} - {% include "activities/_event.html" %} - {% empty %} - No notifications yet. - {% endfor %} + {% for event in events %} + {% include "activities/_event.html" %} + {% empty %} + No notifications yet. + {% endfor %} - + {% endblock %} diff --git a/templates/activities/post.html b/templates/activities/post.html index cb7e60e..5fb8554 100644 --- a/templates/activities/post.html +++ b/templates/activities/post.html @@ -7,11 +7,13 @@ {% endblock %} {% block content %} - {% for ancestor in ancestors reversed %} - {% include "activities/_post.html" with post=ancestor reply=True link_original=False %} - {% endfor %} - {% include "activities/_post.html" %} - {% for descendant in descendants %} - {% include "activities/_post.html" with post=descendant reply=True link_original=False %} - {% endfor %} + {% endblock %} diff --git a/templates/activities/tag.html b/templates/activities/tag.html index e0e35bf..b084cb3 100644 --- a/templates/activities/tag.html +++ b/templates/activities/tag.html @@ -3,27 +3,26 @@ {% block title %}#{{ hashtag.display_name }} Timeline{% endblock %} {% block content %} -
    -