[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
Signed by: diogo
GPG Key ID: 18D2D35001FBFAB0
9 changed files with 111 additions and 38 deletions

View File

@ -39,7 +39,10 @@
{% for conversation in notes %}
{% block current_note %}
{% 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) }}
{# {% else %}
{% set args = { 'type': 'vanilla_full', 'note': conversation, 'extra': { 'depth': 0 } } %}

View File

@ -24,6 +24,7 @@ declare(strict_types = 1);
namespace Component\Posting;
use App\Core\ActorLocalRoles;
use App\Core\Cache;
use App\Core\DB\DB;
use App\Core\Event;
use App\Core\Form;
@ -332,6 +333,14 @@ class Posting extends Component
DB::persist($note);
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
$note->setUrl(Router::url('note_view', ['id' => $note->getId()], Router::ABSOLUTE_URL));
if (!empty($content)) {
@ -373,7 +382,7 @@ class Posting extends Component
$activity,
[
'note-attention' => $attention_ids,
'object' => F\unique(F\flat_map($mentions, fn (array $m) => F\map($m['mentioned'] ?? [], fn (Actor $a) => $a->getId()))),
'object' => F\unique(F\flat_map($mentions, fn (array $m) => F\map($m['mentioned'] ?? [], fn (Actor $a) => $a->getId()))),
],
_m('{nickname} created a note {note_id}.', [
'{nickname}' => $actor->getNickname(),

View File

@ -265,6 +265,14 @@ class Note extends Model
// Assign conversation to this note
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 = [];
foreach ($type_note->get('tag') ?? [] as $ap_tag) {
switch ($ap_tag->get('type')) {

View File

@ -21,8 +21,11 @@ declare(strict_types = 1);
namespace Plugin\TreeNotes;
use App\Core\Event;
use App\Core\Modules\Plugin;
use App\Entity\Note;
use App\Util\Common;
use App\Util\Formatting;
use Symfony\Component\HttpFoundation\Request;
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.
* 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.
*
* @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
{
$tree = [];
$notes = array_reverse($notes);
$tree = [];
$notes = array_reverse($notes);
$max_replies_to_show = Common::config('plugin_tree_notes', 'feed_replies');
foreach ($notes as $note) {
if (!\is_null($children = $note->getReplies())) {
$notes = array_filter($notes, fn (Note $n) => !\in_array($n, $children));
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));
$tree[] = [
'note' => $note,
'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,
),
'total_replies' => $total_replies,
'show_more' => $show_more,
];
} 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());
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:
entries_per_page: 32
plugin_tree_notes:
feed_replies: 3
oauth2:
private_key: '%kernel.project_dir%/file/oauth/private.key'
private_key_password: null

View File

@ -269,6 +269,7 @@ class Note extends Entity
'links' => "note-links-{$note_id}",
'tags' => "note-tags-{$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
*/
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 %}
{% 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 %}
{% if hide_attachments is not defined %}
{% if note.getAttachments() is not empty %}

View File

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