From a9b34b75b611253cf4eff8b6904ced9330a8924d Mon Sep 17 00:00:00 2001 From: Diogo Peralta Cordeiro Date: Sun, 27 Feb 2022 00:42:59 +0000 Subject: [PATCH] [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 --- .../templates/collection/notes.html.twig | 5 +- components/Posting/Posting.php | 11 +++- plugins/ActivityPub/Util/Model/Note.php | 8 +++ plugins/TreeNotes/TreeNotes.php | 54 +++++++++++++++---- .../tree_notes/note_replies_block.html.twig | 19 +++++++ social.yaml | 3 ++ src/Entity/Note.php | 20 ++++++- templates/cards/blocks/note.html.twig | 17 ------ templates/cards/macros/note/types.html.twig | 12 ++--- 9 files changed, 111 insertions(+), 38 deletions(-) create mode 100644 plugins/TreeNotes/templates/tree_notes/note_replies_block.html.twig diff --git a/components/Collection/templates/collection/notes.html.twig b/components/Collection/templates/collection/notes.html.twig index b6bf264c24..b006940b98 100644 --- a/components/Collection/templates/collection/notes.html.twig +++ b/components/Collection/templates/collection/notes.html.twig @@ -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 } } %} diff --git a/components/Posting/Posting.php b/components/Posting/Posting.php index ebf17030ba..3f60f57872 100644 --- a/components/Posting/Posting.php +++ b/components/Posting/Posting.php @@ -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(), diff --git a/plugins/ActivityPub/Util/Model/Note.php b/plugins/ActivityPub/Util/Model/Note.php index a5b4d6730d..e65f9e1e14 100644 --- a/plugins/ActivityPub/Util/Model/Note.php +++ b/plugins/ActivityPub/Util/Model/Note.php @@ -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')) { diff --git a/plugins/TreeNotes/TreeNotes.php b/plugins/TreeNotes/TreeNotes.php index 4e168489da..45ae565953 100644 --- a/plugins/TreeNotes/TreeNotes.php +++ b/plugins/TreeNotes/TreeNotes.php @@ -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; } } diff --git a/plugins/TreeNotes/templates/tree_notes/note_replies_block.html.twig b/plugins/TreeNotes/templates/tree_notes/note_replies_block.html.twig new file mode 100644 index 0000000000..09a47d082e --- /dev/null +++ b/plugins/TreeNotes/templates/tree_notes/note_replies_block.html.twig @@ -0,0 +1,19 @@ +{% import '/cards/macros/note/factory.html.twig' as NoteFactory %} +
+
+
+ {% for reply in conversation.replies %} + + {% set args = { 'type': 'vanilla_full', 'conversation': reply } %} + {{ NoteFactory.constructor(args) }} + {% endfor %} + {% if conversation.show_more %} + + {{ transchoice({ + '1': 'Show an additional reply', + 'other': 'Show # additional replies' + }, (conversation.total_replies - config('plugin_tree_notes', 'feed_replies'))) }} + + {% endif %} +
+
\ No newline at end of file diff --git a/social.yaml b/social.yaml index 11a612ad91..9f0b01fb27 100644 --- a/social.yaml +++ b/social.yaml @@ -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 diff --git a/src/Entity/Note.php b/src/Entity/Note.php index 4e423935b6..6eb8d530dc 100644 --- a/src/Entity/Note.php +++ b/src/Entity/Note.php @@ -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()]) + ); } /** diff --git a/templates/cards/blocks/note.html.twig b/templates/cards/blocks/note.html.twig index e63c5acd8f..b003866fcf 100644 --- a/templates/cards/blocks/note.html.twig +++ b/templates/cards/blocks/note.html.twig @@ -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 %} -
-
-
- {% for conversation in replies %} - - {% set args = { 'type': 'vanilla_full', 'note': conversation['note'], 'replies': conversation['replies'] | default, 'extra': extra } %} - {{ NoteFactory.constructor(args) }} - {% endfor %} -
-
- {% endif %} -{% endblock note_replies %} - {% block note_attachments %} {% if hide_attachments is not defined %} {% if note.getAttachments() is not empty %} diff --git a/templates/cards/macros/note/types.html.twig b/templates/cards/macros/note/types.html.twig index 196a46f779..918518f974 100644 --- a/templates/cards/macros/note/types.html.twig +++ b/templates/cards/macros/note/types.html.twig @@ -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') }} - {% 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 %}