Add search and better notifications

pull/3/head
Andrew Godwin 2022-11-17 18:52:00 -07:00
rodzic 2154e6f022
commit 0851fbd1ec
18 zmienionych plików z 197 dodań i 80 usunięć

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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