. // }}} 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 { /** * Formatting notes without taking a direct reply out of context * Show whole conversation in conversation related routes. */ public function onFormatNoteList(array $notes_in, array &$notes_out, Request $request) { if (str_starts_with($request->get('_route'), 'conversation')) { $parents = $this->conversationFormat($notes_in); $notes_out = $this->conversationFormatTree($parents, $notes_in); } else { $notes_out = $this->feedFormatTree($notes_in); } } /** * 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. * * 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) */ private function feedFormatTree(array $notes): array { $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(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' => [], // We want only one depth level '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' => [], 'show_more' => false]; } } return array_reverse($tree); } /** * Filters given Note list off any children, returning only initial Notes of a Conversation. * * @param array $notes_in Notes to be filtered * * @return array All initial Conversation Notes in given list */ private function conversationFormat(array $notes_in): array { return array_filter($notes_in, static fn (Note $note) => \is_null($note->getReplyTo())); } private function conversationFormatTree(array $parents, array $notes): array { $subtree = []; foreach ($parents as $p) { $subtree[] = $this->conversationFormatSubTree($p, $notes); } return $subtree; } private function conversationFormatSubTree(Note $parent, array $notes) { $children = array_filter($notes, fn (Note $note) => $note->getReplyTo() === $parent->getId()); 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; } }