[PLUGIN][TreeNotes] Correct cache issues and iterate functionality

- Replies ordering now correct
- Replies count added
- Posting adds new replies to cache (when concerning replies cache is not empty) and increments replies count
- Configuration to specify number of in-tree replies shown added
- TreeNotes templates was moved from core to plugin
- Button to read more replies was added
This commit is contained in:
Diogo Peralta Cordeiro 2022-02-27 00:42:59 +00:00
parent 2f539d176d
commit a9b34b75b6
No known key found for this signature in database
GPG Key ID: 18D2D35001FBFAB0
9 changed files with 111 additions and 38 deletions

View File

@ -39,7 +39,10 @@
{% for conversation in notes %} {% for conversation in notes %}
{% block current_note %} {% block current_note %}
{% if conversation is instanceof('array') %} {% if conversation is instanceof('array') %}
{% set args = { 'type': 'vanilla_full', 'note': conversation['note'], 'replies': conversation['replies'] | default, 'extra': { 'depth': 0 } } %} {% set args = {
'type': 'vanilla_full',
'conversation': conversation
} %}
{{ NoteFactory.constructor(args) }} {{ NoteFactory.constructor(args) }}
{# {% else %} {# {% else %}
{% set args = { 'type': 'vanilla_full', 'note': conversation, 'extra': { 'depth': 0 } } %} {% set args = { 'type': 'vanilla_full', 'note': conversation, 'extra': { 'depth': 0 } } %}

View File

@ -24,6 +24,7 @@ declare(strict_types = 1);
namespace Component\Posting; namespace Component\Posting;
use App\Core\ActorLocalRoles; use App\Core\ActorLocalRoles;
use App\Core\Cache;
use App\Core\DB\DB; use App\Core\DB\DB;
use App\Core\Event; use App\Core\Event;
use App\Core\Form; use App\Core\Form;
@ -332,6 +333,14 @@ class Posting extends Component
DB::persist($note); DB::persist($note);
Conversation::assignLocalConversation($note, $reply_to_id); Conversation::assignLocalConversation($note, $reply_to_id);
// Update replies cache
if (!\is_null($reply_to_id)) {
Cache::incr(Note::cacheKeys($reply_to_id)['replies-count']);
if (Cache::exists(Note::cacheKeys($reply_to_id)['replies'])) {
Cache::listPushRight(Note::cacheKeys($reply_to_id)['replies'], $note);
}
}
// Need file and note ids for the next step // Need file and note ids for the next step
$note->setUrl(Router::url('note_view', ['id' => $note->getId()], Router::ABSOLUTE_URL)); $note->setUrl(Router::url('note_view', ['id' => $note->getId()], Router::ABSOLUTE_URL));
if (!empty($content)) { if (!empty($content)) {

View File

@ -265,6 +265,14 @@ class Note extends Model
// Assign conversation to this note // Assign conversation to this note
Conversation::assignLocalConversation($obj, $reply_to); Conversation::assignLocalConversation($obj, $reply_to);
// Update replies cache
if (!\is_null($reply_to)) {
Cache::incr(GSNote::cacheKeys($reply_to)['replies-count']);
if (Cache::exists(GSNote::cacheKeys($reply_to)['replies'])) {
Cache::listPushRight(GSNote::cacheKeys($reply_to)['replies'], $obj);
}
}
$object_mention_ids = []; $object_mention_ids = [];
foreach ($type_note->get('tag') ?? [] as $ap_tag) { foreach ($type_note->get('tag') ?? [] as $ap_tag) {
switch ($ap_tag->get('type')) { switch ($ap_tag->get('type')) {

View File

@ -21,8 +21,11 @@ declare(strict_types = 1);
namespace Plugin\TreeNotes; namespace Plugin\TreeNotes;
use App\Core\Event;
use App\Core\Modules\Plugin; use App\Core\Modules\Plugin;
use App\Entity\Note; use App\Entity\Note;
use App\Util\Common;
use App\Util\Formatting;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
class TreeNotes extends Plugin class TreeNotes extends Plugin
@ -43,30 +46,43 @@ class TreeNotes extends Plugin
/** /**
* Formats general Feed view, allowing users to see a Note and its direct replies. * Formats general Feed view, allowing users to see a Note and its direct replies.
* These replies are then, shown independently of parent note, making sure that every single Note is shown at least once to users. * These replies are then, shown independently of parent note, making sure that every single Note is shown at least
* once to users.
* *
* The list is transversed in reverse to prevent any parent Note from being processed twice. At the same time, this allows all direct replies to be rendered inside the same, respective, parent Note. * The list is transversed in reverse to prevent any parent Note from being processed twice. At the same time,
* this allows all direct replies to be rendered inside the same, respective, parent Note.
* Moreover, this implies the Entity\Note::getReplies() query will only be performed once, for every Note. * Moreover, this implies the Entity\Note::getReplies() query will only be performed once, for every Note.
* *
* @param array $notes The Note list to be formatted, each element has two keys: 'note' (parent/current note), and 'replies' (array of notes in the same format) * @param array $notes The Note list to be formatted, each element has two keys: 'note' (parent/current note),
* and 'replies' (array of notes in the same format)
*/ */
private function feedFormatTree(array $notes): array private function feedFormatTree(array $notes): array
{ {
$tree = []; $tree = [];
$notes = array_reverse($notes); $notes = array_reverse($notes);
$max_replies_to_show = Common::config('plugin_tree_notes', 'feed_replies');
foreach ($notes as $note) { foreach ($notes as $note) {
if (!\is_null($children = $note->getReplies())) { if (!\is_null($children = $note->getReplies(limit: $max_replies_to_show))) {
$total_replies = $note->getRepliesCount();
$show_more = $total_replies > $max_replies_to_show;
$notes = array_filter($notes, fn (Note $n) => !\in_array($n, $children)); $notes = array_filter($notes, fn (Note $n) => !\in_array($n, $children));
$tree[] = [ $tree[] = [
'note' => $note, 'note' => $note,
'replies' => array_map( 'replies' => array_map(
fn ($n) => ['note' => $n, 'replies' => []], fn ($n) => [
'note' => $n,
'replies' => [],
'show_more' => ($n->getRepliesCount() > $max_replies_to_show),
'total_replies' => $n->getRepliesCount(),
],
$children, $children,
), ),
'total_replies' => $total_replies,
'show_more' => $show_more,
]; ];
} else { } else {
$tree[] = ['note' => $note, 'replies' => []]; $tree[] = ['note' => $note, 'replies' => [], 'show_more' => false];
} }
} }
@ -99,6 +115,24 @@ class TreeNotes extends Plugin
{ {
$children = array_filter($notes, fn (Note $note) => $note->getReplyTo() === $parent->getId()); $children = array_filter($notes, fn (Note $note) => $note->getReplyTo() === $parent->getId());
return ['note' => $parent, 'replies' => $this->conversationFormatTree($children, $notes)]; return [
'note' => $parent,
'replies' => $this->conversationFormatTree($children, $notes),
'show_more' => false, // It's always false, we're showing everyone
];
}
public function onAppendNoteBlock(Request $request, array $conversation, array &$res): bool
{
if (\array_key_exists('replies', $conversation)) {
$res[] = Formatting::twigRenderFile(
'tree_notes/note_replies_block.html.twig',
[
'nickname' => $conversation['note']->getActorNickname(),
'conversation' => $conversation,
],
);
}
return Event::next;
} }
} }

View File

@ -0,0 +1,19 @@
{% import '/cards/macros/note/factory.html.twig' as NoteFactory %}
<section class="note-replies" title="{{ 'Replies to ' | trans }}{{ nickname }}{{ '\'s note' | trans }}">
<div class="note-replies-start"></div>
<div class="u-in-reply-to replies">
{% for reply in conversation.replies %}
<span class="note-replies-indicator" role="presentation"></span>
{% set args = { 'type': 'vanilla_full', 'conversation': reply } %}
{{ NoteFactory.constructor(args) }}
{% endfor %}
{% if conversation.show_more %}
<a href="{{ conversation.note.getConversationUrl() }}">
{{ transchoice({
'1': 'Show an additional reply',
'other': 'Show # additional replies'
}, (conversation.total_replies - config('plugin_tree_notes', 'feed_replies'))) }}
</a>
{% endif %}
</div>
</section>

View File

@ -279,6 +279,9 @@ parameters:
feeds: feeds:
entries_per_page: 32 entries_per_page: 32
plugin_tree_notes:
feed_replies: 3
oauth2: oauth2:
private_key: '%kernel.project_dir%/file/oauth/private.key' private_key: '%kernel.project_dir%/file/oauth/private.key'
private_key_password: null private_key_password: null

View File

@ -269,6 +269,7 @@ class Note extends Entity
'links' => "note-links-{$note_id}", 'links' => "note-links-{$note_id}",
'tags' => "note-tags-{$note_id}", 'tags' => "note-tags-{$note_id}",
'replies' => "note-replies-{$note_id}", 'replies' => "note-replies-{$note_id}",
'replies-count' => "note-replies-count-{$note_id}",
]; ];
} }
@ -405,9 +406,24 @@ class Note extends Entity
/** /**
* Returns all **known** replies made to this entity * Returns all **known** replies made to this entity
*/ */
public function getReplies(): array public function getReplies(int $offset = 0, int $limit = null): array
{ {
return Cache::getList(self::cacheKeys($this->getId())['replies'], fn () => DB::findBy('note', ['reply_to' => $this->getId()], order_by: ['created' => 'DESC', 'id' => 'DESC'])); return Cache::getList(self::cacheKeys($this->getId())['replies'],
fn () => DB::findBy(self::class, [
'reply_to' => $this->getId()
],
order_by: ['created' => 'ASC', 'id' => 'ASC']),
left: $offset,
right: $limit
);
}
public function getRepliesCount(): int
{
return Cache::get(
self::cacheKeys($this->getId())['replies-count'],
fn() => DB::count(self::class, ['reply_to' => $this->getId()])
);
} }
/** /**

View File

@ -22,23 +22,6 @@
{% endif %} {% endif %}
{% endblock note_actions %} {% endblock note_actions %}
{% block note_replies %}
{% import '/cards/macros/note/factory.html.twig' as NoteFactory %}
{% if replies is defined and replies is not empty %}
<section class="note-replies" title="{{ 'Replies to ' | trans }}{{ nickname }}{{ '\'s note' | trans }}">
<div class="note-replies-start"></div>
<div class="u-in-reply-to replies">
{% for conversation in replies %}
<span class="note-replies-indicator" role="presentation"></span>
{% set args = { 'type': 'vanilla_full', 'note': conversation['note'], 'replies': conversation['replies'] | default, 'extra': extra } %}
{{ NoteFactory.constructor(args) }}
{% endfor %}
</div>
</section>
{% endif %}
{% endblock note_replies %}
{% block note_attachments %} {% block note_attachments %}
{% if hide_attachments is not defined %} {% if hide_attachments is not defined %}
{% if note.getAttachments() is not empty %} {% if note.getAttachments() is not empty %}

View File

@ -1,9 +1,6 @@
{# args: { 'type': { 'vanilla_full' }, 'note': note, ?'replies': { note, ?replies }, ?'extra': { 'foo': bar } #} {# args: { 'type': { 'vanilla_full' }, 'note': note, ?'replies': { note, ?replies }, ?'extra': { 'foo': bar } #}
{% macro vanilla_full(args) %} {% macro vanilla_full(args) %}
{% set note = args.note %} {% set note = args.conversation.note %}
{% if args.replies is defined %}{% set replies = args.replies %}{% else %}{% set replies = null %}{% endif %}
{% if args.extra is defined %}{% set extra = args.extra %}{% else %}{% set extra = null %}{% endif %}
{% set actor = note.getActor() %} {% set actor = note.getActor() %}
{% set nickname = actor.getNickname() %} {% set nickname = actor.getNickname() %}
@ -54,9 +51,10 @@
{{ block('note_complementary', 'cards/blocks/note.html.twig') }} {{ block('note_complementary', 'cards/blocks/note.html.twig') }}
</article> </article>
{% if replies is defined %} {% set additional_blocks = handle_event('AppendNoteBlock', app.request, args.conversation) %}
{{ block('note_replies', 'cards/blocks/note.html.twig') }} {% for block in additional_blocks %}
{% endif %} {{ block | raw }}
{% endfor %}
{% endmacro vanilla_full %} {% endmacro vanilla_full %}