Add search and better notifications
This commit is contained in:
		
							parent
							
								
									2154e6f022
								
							
						
					
					
						commit
						0851fbd1ec
					
				@ -55,11 +55,12 @@ the less sure I am about it.
 | 
			
		||||
- [x] Receive follow undos
 | 
			
		||||
- [ ] Do outgoing mentions properly
 | 
			
		||||
- [x] Home timeline (posts and boosts from follows)
 | 
			
		||||
- [ ] Notifications page (followed, boosted, liked)
 | 
			
		||||
- [x] Notifications page (followed, boosted, liked)
 | 
			
		||||
- [x] Local timeline
 | 
			
		||||
- [x] Federated timeline
 | 
			
		||||
- [x] Profile pages
 | 
			
		||||
- [ ] Settable icon and background image for profiles
 | 
			
		||||
- [x] Settable icon and background image for profiles
 | 
			
		||||
- [x] User search
 | 
			
		||||
- [ ] Following page
 | 
			
		||||
- [ ] Followers page
 | 
			
		||||
- [x] Multiple domain support
 | 
			
		||||
@ -88,6 +89,7 @@ the less sure I am about it.
 | 
			
		||||
- [ ] Emoji fetching and display
 | 
			
		||||
- [ ] Emoji creation
 | 
			
		||||
- [ ] Image descriptions
 | 
			
		||||
- [ ] Hashtag search
 | 
			
		||||
- [ ] Flag for moderation
 | 
			
		||||
- [ ] Moderation queue
 | 
			
		||||
- [ ] User management page
 | 
			
		||||
 | 
			
		||||
@ -36,6 +36,7 @@ class PostAdmin(admin.ModelAdmin):
 | 
			
		||||
@admin.register(TimelineEvent)
 | 
			
		||||
class TimelineEventAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ["id", "identity", "created", "type"]
 | 
			
		||||
    readonly_fields = ["created"]
 | 
			
		||||
    raw_id_fields = [
 | 
			
		||||
        "identity",
 | 
			
		||||
        "subject_post",
 | 
			
		||||
 | 
			
		||||
@ -37,7 +37,6 @@ class FanOutStates(StateGraph):
 | 
			
		||||
                    private_key=post.author.private_key,
 | 
			
		||||
                    key_id=post.author.public_key_id,
 | 
			
		||||
                )
 | 
			
		||||
            return cls.sent
 | 
			
		||||
        # Handle boosts/likes
 | 
			
		||||
        elif fan_out.type == FanOut.Types.interaction:
 | 
			
		||||
            interaction = await fan_out.subject_post_interaction.afetch_full()
 | 
			
		||||
@ -74,6 +73,7 @@ class FanOutStates(StateGraph):
 | 
			
		||||
                )
 | 
			
		||||
        else:
 | 
			
		||||
            raise ValueError(f"Cannot fan out with type {fan_out.type}")
 | 
			
		||||
        return cls.sent
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FanOut(StatorModel):
 | 
			
		||||
 | 
			
		||||
@ -66,7 +66,7 @@ class TimelineEvent(models.Model):
 | 
			
		||||
        """
 | 
			
		||||
        return cls.objects.get_or_create(
 | 
			
		||||
            identity=identity,
 | 
			
		||||
            type=cls.Types.follow,
 | 
			
		||||
            type=cls.Types.followed,
 | 
			
		||||
            subject_identity=source_identity,
 | 
			
		||||
        )[0]
 | 
			
		||||
 | 
			
		||||
@ -90,6 +90,7 @@ class TimelineEvent(models.Model):
 | 
			
		||||
            identity=identity,
 | 
			
		||||
            type=cls.Types.mentioned,
 | 
			
		||||
            subject_post=post,
 | 
			
		||||
            subject_identity=post.author,
 | 
			
		||||
        )[0]
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,7 @@ from django.utils.decorators import method_decorator
 | 
			
		||||
from django.views.generic import FormView, TemplateView, View
 | 
			
		||||
 | 
			
		||||
from activities.models import Post, PostInteraction, PostInteractionStates
 | 
			
		||||
from core.models import Config
 | 
			
		||||
from users.decorators import identity_required
 | 
			
		||||
from users.shortcuts import by_handle_or_404
 | 
			
		||||
 | 
			
		||||
@ -112,6 +113,7 @@ class Compose(FormView):
 | 
			
		||||
    template_name = "activities/compose.html"
 | 
			
		||||
 | 
			
		||||
    class form_class(forms.Form):
 | 
			
		||||
 | 
			
		||||
        text = forms.CharField(
 | 
			
		||||
            widget=forms.Textarea(
 | 
			
		||||
                attrs={
 | 
			
		||||
@ -137,6 +139,22 @@ class Compose(FormView):
 | 
			
		||||
            help_text="Optional - Post will be hidden behind this text until clicked",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        def clean_text(self):
 | 
			
		||||
            text = self.cleaned_data.get("text")
 | 
			
		||||
            if not text:
 | 
			
		||||
                return text
 | 
			
		||||
            length = len(text)
 | 
			
		||||
            if length > Config.system.post_length:
 | 
			
		||||
                raise forms.ValidationError(
 | 
			
		||||
                    f"Maximum post length is {Config.system.post_length} characters (you have {length})"
 | 
			
		||||
                )
 | 
			
		||||
            return text
 | 
			
		||||
 | 
			
		||||
    def get_form_class(self):
 | 
			
		||||
        form = super().get_form_class()
 | 
			
		||||
        form.declared_fields["text"]
 | 
			
		||||
        return form
 | 
			
		||||
 | 
			
		||||
    def form_valid(self, form):
 | 
			
		||||
        Post.create_local(
 | 
			
		||||
            author=self.request.identity,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										32
									
								
								activities/views/search.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								activities/views/search.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.views.generic import FormView
 | 
			
		||||
 | 
			
		||||
from users.models import Identity
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Search(FormView):
 | 
			
		||||
 | 
			
		||||
    template_name = "activities/search.html"
 | 
			
		||||
 | 
			
		||||
    class form_class(forms.Form):
 | 
			
		||||
        query = forms.CharField()
 | 
			
		||||
 | 
			
		||||
    def form_valid(self, form):
 | 
			
		||||
        query = form.cleaned_data["query"].lstrip("@").lower()
 | 
			
		||||
        results = {"identities": set()}
 | 
			
		||||
        # Search identities
 | 
			
		||||
        if "@" in query:
 | 
			
		||||
            username, domain = query.split("@", 1)
 | 
			
		||||
            for identity in Identity.objects.filter(
 | 
			
		||||
                domain_id=domain, username=username
 | 
			
		||||
            )[:20]:
 | 
			
		||||
                results["identities"].add(identity)
 | 
			
		||||
        else:
 | 
			
		||||
            for identity in Identity.objects.filter(username=query)[:20]:
 | 
			
		||||
                results["identities"].add(identity)
 | 
			
		||||
            for identity in Identity.objects.filter(username__startswith=query)[:20]:
 | 
			
		||||
                results["identities"].add(identity)
 | 
			
		||||
        # Render results
 | 
			
		||||
        context = self.get_context_data(form=form)
 | 
			
		||||
        context["results"] = results
 | 
			
		||||
        return self.render_to_response(context)
 | 
			
		||||
@ -98,9 +98,18 @@ class Notifications(TemplateView):
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self):
 | 
			
		||||
        context = super().get_context_data()
 | 
			
		||||
        context["events"] = TimelineEvent.objects.filter(
 | 
			
		||||
            identity=self.request.identity,
 | 
			
		||||
            type__in=[TimelineEvent.Types.mentioned, TimelineEvent.Types.boosted],
 | 
			
		||||
        ).select_related("subject_post", "subject_post__author", "subject_identity")
 | 
			
		||||
        context["events"] = (
 | 
			
		||||
            TimelineEvent.objects.filter(
 | 
			
		||||
                identity=self.request.identity,
 | 
			
		||||
                type__in=[
 | 
			
		||||
                    TimelineEvent.Types.mentioned,
 | 
			
		||||
                    TimelineEvent.Types.boosted,
 | 
			
		||||
                    TimelineEvent.Types.liked,
 | 
			
		||||
                    TimelineEvent.Types.followed,
 | 
			
		||||
                ],
 | 
			
		||||
            )
 | 
			
		||||
            .order_by("-created")[:50]
 | 
			
		||||
            .select_related("subject_post", "subject_post__author", "subject_identity")
 | 
			
		||||
        )
 | 
			
		||||
        context["current_page"] = "notifications"
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
@ -247,6 +247,10 @@ nav a i {
 | 
			
		||||
    padding: 15px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.left-column h2 {
 | 
			
		||||
    margin: 10px 0 10px 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.right-column {
 | 
			
		||||
    width: 250px;
 | 
			
		||||
    background: var(--color-bg-menu);
 | 
			
		||||
@ -335,7 +339,7 @@ form.inline {
 | 
			
		||||
 | 
			
		||||
form.follow {
 | 
			
		||||
    float: right;
 | 
			
		||||
    margin: 20px 20px 0 0;
 | 
			
		||||
    margin: 20px 0 0 0;
 | 
			
		||||
    font-size: 16px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -530,12 +534,13 @@ form .button:hover {
 | 
			
		||||
/* Identities */
 | 
			
		||||
 | 
			
		||||
h1.identity {
 | 
			
		||||
    margin: 15px 0 20px 15px;
 | 
			
		||||
    margin: 0 0 20px 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
h1.identity .banner {
 | 
			
		||||
    width: 870px;
 | 
			
		||||
    height: auto;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 200px;
 | 
			
		||||
    object-fit: cover;
 | 
			
		||||
    display: block;
 | 
			
		||||
    margin: 0 0 20px 0;
 | 
			
		||||
}
 | 
			
		||||
@ -560,7 +565,7 @@ h1.identity small {
 | 
			
		||||
    color: var(--color-text-dull);
 | 
			
		||||
    border-radius: 3px;
 | 
			
		||||
    padding: 5px 8px;
 | 
			
		||||
    margin: 15px;
 | 
			
		||||
    margin: 15px 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.system-note a {
 | 
			
		||||
@ -658,6 +663,7 @@ h1.identity small {
 | 
			
		||||
.post .actions a {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    color: var(--color-text-dull);
 | 
			
		||||
    margin-right: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.post .actions a:hover {
 | 
			
		||||
@ -668,18 +674,42 @@ h1.identity small {
 | 
			
		||||
    color: var(--color-highlight);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.boost-banner {
 | 
			
		||||
.boost-banner,
 | 
			
		||||
.mention-banner,
 | 
			
		||||
.follow-banner,
 | 
			
		||||
.like-banner {
 | 
			
		||||
    padding: 0 0 3px 5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.boost-banner a,
 | 
			
		||||
.mention-banner a,
 | 
			
		||||
.follow-banner a,
 | 
			
		||||
.like-banner a {
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.boost-banner::before {
 | 
			
		||||
    content: "\f079";
 | 
			
		||||
    font: var(--fa-font-solid);
 | 
			
		||||
    margin-right: 4px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.boost-banner a {
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
.mention-banner::before {
 | 
			
		||||
    content: "\0040";
 | 
			
		||||
    font: var(--fa-font-solid);
 | 
			
		||||
    margin-right: 4px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.follow-banner::before {
 | 
			
		||||
    content: "\f007";
 | 
			
		||||
    font: var(--fa-font-solid);
 | 
			
		||||
    margin-right: 4px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.like-banner::before {
 | 
			
		||||
    content: "\f005";
 | 
			
		||||
    font: var(--fa-font-solid);
 | 
			
		||||
    margin-right: 4px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ from django.contrib import admin as djadmin
 | 
			
		||||
from django.urls import path, re_path
 | 
			
		||||
from django.views.static import serve
 | 
			
		||||
 | 
			
		||||
from activities.views import posts, timelines
 | 
			
		||||
from activities.views import posts, search, timelines
 | 
			
		||||
from core import views as core
 | 
			
		||||
from stator import views as stator
 | 
			
		||||
from users.views import activitypub, admin, auth, identity, settings
 | 
			
		||||
@ -14,9 +14,10 @@ urlpatterns = [
 | 
			
		||||
    path("", core.homepage),
 | 
			
		||||
    path("manifest.json", core.AppManifest.as_view()),
 | 
			
		||||
    # Activity views
 | 
			
		||||
    path("notifications/", timelines.Notifications.as_view()),
 | 
			
		||||
    path("local/", timelines.Local.as_view()),
 | 
			
		||||
    path("federated/", timelines.Federated.as_view()),
 | 
			
		||||
    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("search/", search.Search.as_view(), name="search"),
 | 
			
		||||
    path(
 | 
			
		||||
        "settings/",
 | 
			
		||||
        settings.SettingsRoot.as_view(),
 | 
			
		||||
@ -76,7 +77,7 @@ urlpatterns = [
 | 
			
		||||
    path("@<handle>/actor/inbox/", activitypub.Inbox.as_view()),
 | 
			
		||||
    path("@<handle>/action/", identity.ActionIdentity.as_view()),
 | 
			
		||||
    # Posts
 | 
			
		||||
    path("compose/", posts.Compose.as_view()),
 | 
			
		||||
    path("compose/", posts.Compose.as_view(), name="compose"),
 | 
			
		||||
    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)),
 | 
			
		||||
 | 
			
		||||
@ -1,24 +1,28 @@
 | 
			
		||||
{% load static %}
 | 
			
		||||
{% load activity_tags %}
 | 
			
		||||
<div class="post">
 | 
			
		||||
 | 
			
		||||
    <time>
 | 
			
		||||
        {% if event.published %}
 | 
			
		||||
            {{ event.published | timedeltashort }}
 | 
			
		||||
        {% else %}
 | 
			
		||||
            {{ event.created | timedeltashort }}
 | 
			
		||||
        {% endif %}
 | 
			
		||||
    </time>
 | 
			
		||||
 | 
			
		||||
    {% if event.type == "follow" %}
 | 
			
		||||
        {{ event.subject_identity.name_or_handle }} followed you
 | 
			
		||||
    {% elif event.type == "like" %}
 | 
			
		||||
        {{ event.subject_identity.name_or_handle }} liked {{ event.subject_post }}
 | 
			
		||||
    {% elif event.type == "mentioned" %}
 | 
			
		||||
        {{ event.subject_post.author.name_or_handle }} mentioned you in {{ event.subject_post }}
 | 
			
		||||
    {% elif event.type == "boosted" %}
 | 
			
		||||
        {{ event.subject_identity.name_or_handle }} boosted your post {{ event.subject_post }}
 | 
			
		||||
    {% else %}
 | 
			
		||||
        Unknown event type {{event.type}}
 | 
			
		||||
    {% endif %}
 | 
			
		||||
</div>
 | 
			
		||||
{% if event.type == "followed" %}
 | 
			
		||||
    <div class="follow-banner">
 | 
			
		||||
        <a href="{{ event.subject_identity.urls.view }}">
 | 
			
		||||
            {{ event.subject_identity.name_or_handle }}
 | 
			
		||||
        </a> followed you
 | 
			
		||||
    </div>
 | 
			
		||||
    {% include "activities/_identity.html" with identity=event.subject_identity created=event.created %}
 | 
			
		||||
{% elif event.type == "liked" %}
 | 
			
		||||
    <div class="like-banner">
 | 
			
		||||
        <a href="{{ event.subject_identity.urls.view }}">
 | 
			
		||||
            {{ event.subject_identity.name_or_handle }}
 | 
			
		||||
        </a> liked your post
 | 
			
		||||
    </div>
 | 
			
		||||
    {% include "activities/_post.html" with post=event.subject_post %}
 | 
			
		||||
{% elif event.type == "mentioned" %}
 | 
			
		||||
    <div class="mention-banner">
 | 
			
		||||
        <a href="{{ event.subject_identity.urls.view }}">
 | 
			
		||||
            {{ event.subject_identity.name_or_handle }}
 | 
			
		||||
        </a> mentioned you
 | 
			
		||||
    </div>
 | 
			
		||||
    {% include "activities/_post.html" with post=event.subject_post %}
 | 
			
		||||
{% elif event.type == "boosted" %}
 | 
			
		||||
    {{ event.subject_identity.name_or_handle }} boosted your post {{ event.subject_post }}
 | 
			
		||||
{% else %}
 | 
			
		||||
    Unknown event type {{event.type}}
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										15
									
								
								templates/activities/_identity.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								templates/activities/_identity.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
			
		||||
{% load activity_tags %}
 | 
			
		||||
<div class="post user">
 | 
			
		||||
 | 
			
		||||
    <img src="{{ identity.local_icon_url }}" class="icon">
 | 
			
		||||
 | 
			
		||||
    {% if created %}
 | 
			
		||||
    <time>
 | 
			
		||||
        {{ event.created | timedeltashort }}
 | 
			
		||||
    </time>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
    <a href="{{ identity.urls.view }}" class="handle">
 | 
			
		||||
        {{ identity.name_or_handle }} <small>@{{ identity.handle }}</small>
 | 
			
		||||
    </a>
 | 
			
		||||
</div>
 | 
			
		||||
@ -19,6 +19,7 @@
 | 
			
		||||
        {% csrf_token %}
 | 
			
		||||
        {{ form.text }}
 | 
			
		||||
        {{ form.content_warning }}
 | 
			
		||||
        <input type="hidden" name="visibility" value="0">
 | 
			
		||||
        <div class="buttons">
 | 
			
		||||
            <span class="button toggle" _="on click toggle .enabled then toggle .hidden on #id_content_warning">CW</span>
 | 
			
		||||
            <button>{% if config_identity.toot_mode %}Toot!{% else %}Post{% endif %}</button>
 | 
			
		||||
@ -51,11 +51,6 @@
 | 
			
		||||
    <div class="actions">
 | 
			
		||||
        {% include "activities/_like.html" %}
 | 
			
		||||
        {% include "activities/_boost.html" %}
 | 
			
		||||
        {% if request.user.admin %}
 | 
			
		||||
            <a title="Admin" href="/djadmin/activities/post/{{ post.pk }}/change/">
 | 
			
		||||
                <i class="fa-solid fa-file-code"></i>
 | 
			
		||||
            </a>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
    </div>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@ -3,15 +3,5 @@
 | 
			
		||||
{% block title %}Post by {{ post.author.name_or_handle }}{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
    <nav>
 | 
			
		||||
        <a href="." class="selected">Post</a>
 | 
			
		||||
    </nav>
 | 
			
		||||
 | 
			
		||||
    <section class="columns">
 | 
			
		||||
 | 
			
		||||
        <div class="left-column">
 | 
			
		||||
            {% include "activities/_post.html" %}
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
    </section>
 | 
			
		||||
    {% include "activities/_post.html" %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										21
									
								
								templates/activities/search.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								templates/activities/search.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
			
		||||
{% extends "base.html" %}
 | 
			
		||||
 | 
			
		||||
{% block title %}Search{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
    <form action="." method="POST">
 | 
			
		||||
        {% csrf_token %}
 | 
			
		||||
        <fieldset>
 | 
			
		||||
            {% include "forms/_field.html" with field=form.query %}
 | 
			
		||||
        </fieldset>
 | 
			
		||||
        <div class="buttons">
 | 
			
		||||
            <button>Search</button>
 | 
			
		||||
        </div>
 | 
			
		||||
    </form>
 | 
			
		||||
    {% if results.identities %}
 | 
			
		||||
        <h2>People</h2>
 | 
			
		||||
        {% for identity in results.identities %}
 | 
			
		||||
            {% include "activities/_identity.html" %}
 | 
			
		||||
        {% endfor %}
 | 
			
		||||
    {% endif %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@ -28,10 +28,10 @@
 | 
			
		||||
            </a>
 | 
			
		||||
            <menu>
 | 
			
		||||
                {% if user.is_authenticated %}
 | 
			
		||||
                    <a href="/compose/" title="Compose" {% if top_section == "compose" %}class="selected"{% endif %}>
 | 
			
		||||
                    <a href="{% url "compose" %}" title="Compose" {% if top_section == "compose" %}class="selected"{% endif %}>
 | 
			
		||||
                        <i class="fa-solid fa-feather"></i> Compose
 | 
			
		||||
                    </a>
 | 
			
		||||
                    <a href="#" title="Search" {% if top_section == "search" %}class="selected"{% endif %}>
 | 
			
		||||
                    <a href="{% url "search" %}" title="Search" {% if top_section == "search" %}class="selected"{% endif %}>
 | 
			
		||||
                        <i class="fa-solid fa-search"></i> Search
 | 
			
		||||
                    </a>
 | 
			
		||||
                    <a href="{% url "settings" %}" title="Settings" {% if top_section == "settings" %}class="selected"{% endif %}>
 | 
			
		||||
@ -67,7 +67,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="right-column">
 | 
			
		||||
                {% block right_content %}
 | 
			
		||||
                    {% include "activities/_home_menu.html" %}
 | 
			
		||||
                    {% include "activities/_menu.html" %}
 | 
			
		||||
                {% endblock %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
@ -1,17 +1,13 @@
 | 
			
		||||
{% extends "base.html" %}
 | 
			
		||||
{% load static %}
 | 
			
		||||
 | 
			
		||||
{% block title %}{{ identity }}{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
    <nav>
 | 
			
		||||
        <a href="." class="selected">Profile</a>
 | 
			
		||||
    </nav>
 | 
			
		||||
 | 
			
		||||
    <h1 class="identity">
 | 
			
		||||
        {% if identity.local_image_url %}
 | 
			
		||||
            <img src="{{ identity.local_image_url }}" class="banner">
 | 
			
		||||
        {% endif %}
 | 
			
		||||
 | 
			
		||||
        <img src="{{ identity.local_icon_url }}" class="icon">
 | 
			
		||||
 | 
			
		||||
        {% if request.identity %}
 | 
			
		||||
@ -43,13 +39,9 @@
 | 
			
		||||
        {% endif %}
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
    <section class="columns">
 | 
			
		||||
        <div class="left-column">
 | 
			
		||||
            {% for post in posts %}
 | 
			
		||||
                {% include "activities/_post.html" %}
 | 
			
		||||
            {% empty %}
 | 
			
		||||
                No posts yet.
 | 
			
		||||
            {% endfor %}
 | 
			
		||||
        </div>
 | 
			
		||||
    </section>
 | 
			
		||||
    {% for post in posts %}
 | 
			
		||||
        {% include "activities/_post.html" %}
 | 
			
		||||
    {% empty %}
 | 
			
		||||
        No posts yet.
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db import models, transaction
 | 
			
		||||
 | 
			
		||||
from core.ld import canonicalise
 | 
			
		||||
from core.signatures import HttpSignature
 | 
			
		||||
@ -218,9 +218,14 @@ class Follow(StatorModel):
 | 
			
		||||
        """
 | 
			
		||||
        Handles an incoming follow request
 | 
			
		||||
        """
 | 
			
		||||
        follow = cls.by_ap(data, create=True)
 | 
			
		||||
        # Force it into remote_requested so we send an accept
 | 
			
		||||
        follow.transition_perform(FollowStates.remote_requested)
 | 
			
		||||
        from activities.models import TimelineEvent
 | 
			
		||||
 | 
			
		||||
        with transaction.atomic():
 | 
			
		||||
            follow = cls.by_ap(data, create=True)
 | 
			
		||||
            # Force it into remote_requested so we send an accept
 | 
			
		||||
            follow.transition_perform(FollowStates.remote_requested)
 | 
			
		||||
            # Add a timeline event
 | 
			
		||||
            TimelineEvent.add_follow(follow.target, follow.source)
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def handle_accept_ap(cls, data):
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user