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