notification
This commit is contained in:
parent
05821eaac1
commit
15a7b55642
31 changed files with 379 additions and 47 deletions
|
@ -66,6 +66,12 @@
|
|||
// margin-right: 4px;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.spacing {
|
||||
margin-bottom: calc(var(--pico-block-spacing-horizontal)/2);
|
||||
}
|
||||
|
|
|
@ -78,6 +78,9 @@
|
|||
</summary>
|
||||
<ul role="listbox" style="min-width:-webkit-max-content;" dir="rtl">
|
||||
{% if request.user.is_authenticated %}
|
||||
<li>
|
||||
<a href="{% url 'social:notification' %}">通知</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'users:data' %}">数据</a>
|
||||
</li>
|
||||
|
|
|
@ -178,6 +178,11 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
|||
obj = None
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def get_by_post_id(cls, post_id: int):
|
||||
pp = PiecePost.objects.filter(post_id=post_id).first()
|
||||
return pp.piece if pp else None
|
||||
|
||||
@classmethod
|
||||
def update_by_ap_object(cls, owner, item, obj, post_id, visibility):
|
||||
raise NotImplementedError("subclass must implement this")
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
{% load i18n %}
|
||||
<section class="replies" hx-target="this" hx-swap="outerHTML">
|
||||
{% for post in replies %}
|
||||
<div>
|
||||
|
@ -35,8 +36,9 @@
|
|||
<div id="replies_{{ post.pk }}"></div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="empty">暂无回应</div>
|
||||
<div class="empty">{% trans 'nothing so far.' %}</div>
|
||||
{% endfor %}
|
||||
{% if post %}
|
||||
<form class="reply"
|
||||
role="group"
|
||||
hx-post="{% url 'journal:post_reply' post.pk %}"
|
||||
|
@ -80,4 +82,5 @@
|
|||
<i class="fa-regular fa-paper-plane"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
|
1
social/templates/event/announcement.html
Normal file
1
social/templates/event/announcement.html
Normal file
|
@ -0,0 +1 @@
|
|||
unsupported notification: announcement
|
5
social/templates/event/boosted.html
Normal file
5
social/templates/event/boosted.html
Normal file
|
@ -0,0 +1,5 @@
|
|||
{% load bleach_tags %}
|
||||
boosted your post
|
||||
<blockquote class="tldr" _="on click toggle .tldr on me">
|
||||
{{ event.post.content|bleach:"a,p,span,br,div,img"|default:"<br>" }}
|
||||
</blockquote>
|
1
social/templates/event/boosted_collection.html
Normal file
1
social/templates/event/boosted_collection.html
Normal file
|
@ -0,0 +1 @@
|
|||
boosted your collection <a href="{{ event.piece.url }}">{{ event.piece.title }}</a>
|
8
social/templates/event/boosted_comment.html
Normal file
8
social/templates/event/boosted_comment.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
{% load duration %}
|
||||
boost your comment on <a href="{{ event.item.url }}">{{ event.item.display_title }}</a>
|
||||
<blockquote class="tldr" _="on click toggle .tldr on me">
|
||||
<span>
|
||||
{% if event.piece.rating_grade %}{{ event.piece.rating_grade|rating_star }}{% endif %}
|
||||
</span>
|
||||
{{ event.piece.html|safe }}
|
||||
</blockquote>
|
1
social/templates/event/boosted_review.html
Normal file
1
social/templates/event/boosted_review.html
Normal file
|
@ -0,0 +1 @@
|
|||
boosted your review <a href="{{ event.piece.url }}">{{ event.piece.title }}</a> on <a href="{{ event.item.url }}">{{ event.item.display_title }}</a>
|
8
social/templates/event/boosted_shelfmember.html
Normal file
8
social/templates/event/boosted_shelfmember.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
{% load duration %}
|
||||
boosted your mark on <a href="{{ event.item.url }}">{{ event.item.display_title }}</a>
|
||||
<blockquote class="tldr" _="on click toggle .tldr on me">
|
||||
<span>
|
||||
{% if event.piece.mark.rating_grade %}{{ event.piece.mark.rating_grade|rating_star }}{% endif %}
|
||||
</span>
|
||||
{{ event.piece.mark.comment.html|safe }}
|
||||
</blockquote>
|
12
social/templates/event/follow_requested.html
Normal file
12
social/templates/event/follow_requested.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
{% load bleach_tags %}
|
||||
requested to follow you
|
||||
<blockquote class="tldr" _="on click toggle .tldr on me">
|
||||
<span class="action">{% include 'users/profile_actions.html' with show_home=1 identity=event.identity %}</span>
|
||||
<div>
|
||||
<code class="{{ event.identity.id }}_handler"
|
||||
style="cursor:pointer"
|
||||
onmouseleave="$(this).removeAttr('data-tooltip')"
|
||||
onclick="navigator.clipboard.writeText(this.innerText);$(this).data('tooltip','copied');">@{{ event.identity.full_handle }}</code>
|
||||
</div>
|
||||
{{ event.identity.summary|bleach:"a,p,span,br"|default:"<br>" }}
|
||||
</blockquote>
|
12
social/templates/event/followed.html
Normal file
12
social/templates/event/followed.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
{% load bleach_tags %}
|
||||
followed you
|
||||
<blockquote class="tldr" _="on click toggle .tldr on me">
|
||||
<span class="action">{% include 'users/profile_actions.html' with show_home=1 identity=event.identity %}</span>
|
||||
<div>
|
||||
<code class="{{ event.identity.id }}_handler"
|
||||
style="cursor:pointer"
|
||||
onmouseleave="$(this).removeAttr('data-tooltip')"
|
||||
onclick="navigator.clipboard.writeText(this.innerText);$(this).data('tooltip','copied');">@{{ event.identity.full_handle }}</code>
|
||||
</div>
|
||||
{{ event.identity.summary|bleach:"a,p,span,br"|default:"<br>" }}
|
||||
</blockquote>
|
1
social/templates/event/identity_created.html
Normal file
1
social/templates/event/identity_created.html
Normal file
|
@ -0,0 +1 @@
|
|||
joined {{ site_name }}
|
5
social/templates/event/liked.html
Normal file
5
social/templates/event/liked.html
Normal file
|
@ -0,0 +1,5 @@
|
|||
{% load bleach_tags %}
|
||||
liked your post
|
||||
<blockquote class="tldr" _="on click toggle .tldr on me">
|
||||
{{ event.post.content|bleach:"a,p,span,br,div,img"|default:"<br>" }}
|
||||
</blockquote>
|
1
social/templates/event/liked_collection.html
Normal file
1
social/templates/event/liked_collection.html
Normal file
|
@ -0,0 +1 @@
|
|||
liked your collection <a href="{{ event.piece.url }}">{{ event.piece.title }}</a>
|
8
social/templates/event/liked_comment.html
Normal file
8
social/templates/event/liked_comment.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
{% load duration %}
|
||||
liked your comment on <a href="{{ event.item.url }}">{{ event.item.display_title }}</a>
|
||||
<blockquote class="tldr" _="on click toggle .tldr on me">
|
||||
<span>
|
||||
{% if event.piece.rating_grade %}{{ event.piece.rating_grade|rating_star }}{% endif %}
|
||||
</span>
|
||||
{{ event.piece.html|safe }}
|
||||
</blockquote>
|
1
social/templates/event/liked_review.html
Normal file
1
social/templates/event/liked_review.html
Normal file
|
@ -0,0 +1 @@
|
|||
liked your review <a href="{{ event.piece.url }}">{{ event.piece.title }}</a> on <a href="{{ event.item.url }}">{{ event.item.display_title }}</a>
|
8
social/templates/event/liked_shelfmember.html
Normal file
8
social/templates/event/liked_shelfmember.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
{% load duration %}
|
||||
liked your mark on <a href="{{ event.item.url }}">{{ event.item.display_title }}</a>
|
||||
<blockquote class="tldr" _="on click toggle .tldr on me">
|
||||
<span>
|
||||
{% if event.piece.mark.rating_grade %}{{ event.piece.mark.rating_grade|rating_star }}{% endif %}
|
||||
</span>
|
||||
{{ event.piece.mark.comment.html|safe }}
|
||||
</blockquote>
|
6
social/templates/event/mentioned.html
Normal file
6
social/templates/event/mentioned.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
{% load bleach_tags %}
|
||||
mentioned you
|
||||
<blockquote class="tldr" _="on click toggle .tldr on me">
|
||||
{{ event.post.content|bleach:"a,p,span,br,div,img"|default:"<br>" }}
|
||||
</blockquote>
|
||||
{% include "events/_post.html" with post=event.reply %}
|
2
social/templates/event/mentioned_collection.html
Normal file
2
social/templates/event/mentioned_collection.html
Normal file
|
@ -0,0 +1,2 @@
|
|||
replied to your collection <a href="{{ event.piece.url }}">{{ event.piece.title }}</a>
|
||||
{% include "events/_post.html" with post=event.reply %}
|
9
social/templates/event/mentioned_comment.html
Normal file
9
social/templates/event/mentioned_comment.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
{% load duration %}
|
||||
replied to your comment on <a href="{{ event.item.url }}">{{ event.item.display_title }}</a>
|
||||
<blockquote class="tldr" _="on click toggle .tldr on me">
|
||||
<span>
|
||||
{% if event.piece.rating_grade %}{{ event.piece.rating_grade|rating_star }}{% endif %}
|
||||
</span>
|
||||
{{ event.piece.html|safe }}
|
||||
</blockquote>
|
||||
{% include "events/_post.html" with post=event.reply %}
|
2
social/templates/event/mentioned_review.html
Normal file
2
social/templates/event/mentioned_review.html
Normal file
|
@ -0,0 +1,2 @@
|
|||
replied to your review <a href="{{ event.piece.url }}">{{ event.piece.title }}</a> on <a href="{{ event.item.url }}">{{ event.item.display_title }}</a>
|
||||
{% include "events/_post.html" with post=event.reply %}
|
9
social/templates/event/mentioned_shelfmember.html
Normal file
9
social/templates/event/mentioned_shelfmember.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
{% load duration %}
|
||||
replied to your mark on <a href="{{ event.item.url }}">{{ event.item.display_title }}</a>
|
||||
<blockquote class="tldr" _="on click toggle .tldr on me">
|
||||
<span>
|
||||
{% if event.piece.mark.rating_grade %}{{ event.piece.mark.rating_grade|rating_star }}{% endif %}
|
||||
</span>
|
||||
{{ event.piece.mark.comment.html|safe }}
|
||||
</blockquote>
|
||||
{% include "events/_post.html" with post=event.reply %}
|
1
social/templates/event/post.html
Normal file
1
social/templates/event/post.html
Normal file
|
@ -0,0 +1 @@
|
|||
unsupported notification: post
|
51
social/templates/events.html
Normal file
51
social/templates/events.html
Normal file
|
@ -0,0 +1,51 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
{% load humanize %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
{% load thumb %}
|
||||
{% load prettydate %}
|
||||
{% load user_actions %}
|
||||
{% load duration %}
|
||||
{% for event in events %}
|
||||
<section class="activity">
|
||||
<div class="avatar">
|
||||
<img src="{{ event.identity.avatar }}" alt="cover" />
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<span class="time">
|
||||
<span>{{ event.created|naturaltime }}</span>
|
||||
</span>
|
||||
<div>
|
||||
<span>
|
||||
<a href="{{ event.identity.url }}"
|
||||
class="nickname"
|
||||
title="@{{ event.identity.full_handle }}">{{ event.identity.display_name }}</a>
|
||||
</span>
|
||||
{% with "event/"|add:event.template|add:".html" as template %}
|
||||
{% include template %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% if forloop.last %}
|
||||
<div class="htmx-indicator"
|
||||
style="margin-left: 60px"
|
||||
hx-get="{% url 'social:events' %}?last={{ event.created|date:'Y-m-d H:i:s.uO'|urlencode }}"
|
||||
hx-trigger="revealed"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa-solid fa-compact-disc fa-spin loading"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% empty %}
|
||||
{% if request.GET.last %}
|
||||
<div class="empty">{% trans 'nothing more.' %}</div>
|
||||
{% else %}
|
||||
<div class="empty">{% trans 'nothing so far.' %}</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
17
social/templates/events/_post.html
Normal file
17
social/templates/events/_post.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
<div>
|
||||
<span class="action">
|
||||
{% include "action_reply_post.html" %}
|
||||
{% include "action_like_post.html" %}
|
||||
{% include "action_boost_post.html" %}
|
||||
{% include "action_open_post.html" %}
|
||||
</span>
|
||||
{% if post.summary %}
|
||||
<details>
|
||||
<summary>{{ post.summary }}</summary>
|
||||
{{ post.safe_content_local }}
|
||||
</details>
|
||||
{% else %}
|
||||
{{ post.safe_content_local }}
|
||||
{% endif %}
|
||||
<div id="replies_{{ post.pk }}"></div>
|
||||
</div>
|
38
social/templates/notification.html
Normal file
38
social/templates/notification.html
Normal file
|
@ -0,0 +1,38 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
{% load thumb %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh" class="feed-page">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ site_name }} - {% trans 'Notification' %}</title>
|
||||
{% include "common_libs.html" %}
|
||||
<script src="{{ cdn_url }}/npm/shikwasa@2.2.1/dist/shikwasa.min.js"></script>
|
||||
<link href="{{ cdn_url }}/npm/shikwasa@2.2.1/dist/style.min.css"
|
||||
rel="stylesheet"></link>
|
||||
<script src="{% static 'js/podcast.js' %}"></script>
|
||||
</head>
|
||||
<body>
|
||||
{% include "_header.html" with current="timeline" %}
|
||||
<main>
|
||||
<div class="grid__main">
|
||||
<h5>{% trans 'Notification' %}</h5>
|
||||
<div class="feed">
|
||||
<div hx-get="{% url 'social:events' %}"
|
||||
hx-trigger="intersect once delay:0.1s"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa-solid fa-compact-disc fa-spin loading"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include "_sidebar.html" with show_progress=1 identity=request.user.identity %}
|
||||
</main>
|
||||
{% include "_footer.html" %}
|
||||
</body>
|
||||
</html>
|
|
@ -6,4 +6,6 @@ app_name = "social"
|
|||
urlpatterns = [
|
||||
path("", feed, name="feed"),
|
||||
path("data", data, name="data"),
|
||||
path("notification", notification, name="notification"),
|
||||
path("events", events, name="events"),
|
||||
]
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import logging
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.exceptions import BadRequest
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
@ -9,11 +8,10 @@ from django.views.decorators.http import require_http_methods
|
|||
|
||||
from catalog.models import *
|
||||
from journal.models import *
|
||||
from takahe.utils import Takahe
|
||||
|
||||
from .models import *
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
PAGE_SIZE = 10
|
||||
|
||||
|
||||
|
@ -71,3 +69,84 @@ def data(request):
|
|||
)[:PAGE_SIZE],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
@login_required
|
||||
def notification(request):
|
||||
if not request.user.registration_complete:
|
||||
return redirect(reverse("users:register"))
|
||||
user = request.user
|
||||
podcast_ids = [
|
||||
p.item_id
|
||||
for p in user.shelf_manager.get_latest_members(
|
||||
ShelfType.PROGRESS, ItemCategory.Podcast
|
||||
)
|
||||
]
|
||||
recent_podcast_episodes = PodcastEpisode.objects.filter(
|
||||
program_id__in=podcast_ids
|
||||
).order_by("-pub_date")[:10]
|
||||
books_in_progress = Edition.objects.filter(
|
||||
id__in=[
|
||||
p.item_id
|
||||
for p in user.shelf_manager.get_latest_members(
|
||||
ShelfType.PROGRESS, ItemCategory.Book
|
||||
)[:10]
|
||||
]
|
||||
)
|
||||
tvshows_in_progress = Item.objects.filter(
|
||||
id__in=[
|
||||
p.item_id
|
||||
for p in user.shelf_manager.get_latest_members(
|
||||
ShelfType.PROGRESS, ItemCategory.TV
|
||||
)[:10]
|
||||
]
|
||||
)
|
||||
return render(
|
||||
request,
|
||||
"notification.html",
|
||||
{
|
||||
"recent_podcast_episodes": recent_podcast_episodes,
|
||||
"books_in_progress": books_in_progress,
|
||||
"tvshows_in_progress": tvshows_in_progress,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class NotificationEvent:
|
||||
def __init__(self, tle) -> None:
|
||||
self.event = tle
|
||||
self.type = tle.type
|
||||
self.template = tle.type
|
||||
self.created = tle.created
|
||||
self.identity = (
|
||||
APIdentity.objects.filter(pk=tle.subject_identity.pk).first()
|
||||
if tle.subject_identity
|
||||
else None
|
||||
)
|
||||
self.post = tle.subject_post
|
||||
if self.type == "mentioned":
|
||||
# for reply, self.post is the original post
|
||||
self.reply = self.post
|
||||
self.replies = [self.post]
|
||||
self.post = self.post.in_reply_to_post() if self.post else None
|
||||
self.piece = Piece.get_by_post_id(self.post.id) if self.post else None
|
||||
self.item = getattr(self.piece, "item") if hasattr(self.piece, "item") else None
|
||||
if self.piece and self.template in ["liked", "boost", "mentioned"]:
|
||||
cls = self.piece.__class__.__name__.lower()
|
||||
self.template += "_" + cls
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["GET"])
|
||||
def events(request):
|
||||
es = Takahe.get_events(request.user.identity.pk)
|
||||
last = request.GET.get("last")
|
||||
if last:
|
||||
es = es.filter(created__lt=last)
|
||||
nes = [NotificationEvent(e) for e in es[:PAGE_SIZE]]
|
||||
return render(
|
||||
request,
|
||||
"events.html",
|
||||
{"events": nes},
|
||||
)
|
||||
|
|
|
@ -942,3 +942,27 @@ class Takahe:
|
|||
.exclude(seen=user)
|
||||
):
|
||||
a.seen.add(user)
|
||||
|
||||
@staticmethod
|
||||
def get_events(identity_id: int):
|
||||
return (
|
||||
TimelineEvent.objects.select_related(
|
||||
"subject_post",
|
||||
"subject_post__author",
|
||||
"subject_post__author__domain",
|
||||
"subject_identity",
|
||||
"subject_identity__domain",
|
||||
"subject_post_interaction",
|
||||
"subject_post_interaction__identity",
|
||||
"subject_post_interaction__identity__domain",
|
||||
)
|
||||
.prefetch_related(
|
||||
"subject_post__attachments",
|
||||
"subject_post__mentions",
|
||||
"subject_post__emojis",
|
||||
)
|
||||
.filter(identity=identity_id)
|
||||
.exclude(type="post", subject_identity__isnull=True)
|
||||
.exclude(subject_identity_id=identity_id)
|
||||
.order_by("-created")
|
||||
)
|
||||
|
|
|
@ -31,10 +31,13 @@ _RESERVED_USERNAMES = [
|
|||
"__",
|
||||
"admin",
|
||||
"administrator",
|
||||
"service",
|
||||
"support",
|
||||
"system",
|
||||
"user",
|
||||
"users",
|
||||
"api",
|
||||
"bot",
|
||||
"me",
|
||||
]
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue