TreeNotes.php 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. <?php
  2. declare(strict_types = 1);
  3. // {{{ License
  4. // This file is part of GNU social - https://www.gnu.org/software/social
  5. //
  6. // GNU social is free software: you can redistribute it and/or modify
  7. // it under the terms of the GNU Affero General Public License as published by
  8. // the Free Software Foundation, either version 3 of the License, or
  9. // (at your option) any later version.
  10. //
  11. // GNU social is distributed in the hope that it will be useful,
  12. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. // GNU Affero General Public License for more details.
  15. //
  16. // You should have received a copy of the GNU Affero General Public License
  17. // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
  18. // }}}
  19. namespace Plugin\TreeNotes;
  20. use App\Core\Event;
  21. use App\Core\Modules\Plugin;
  22. use App\Entity\Note;
  23. use App\Util\Common;
  24. use App\Util\Formatting;
  25. use EventResult;
  26. use Symfony\Component\HttpFoundation\Request;
  27. class TreeNotes extends Plugin
  28. {
  29. /**
  30. * Formatting notes without taking a direct reply out of context
  31. * Show whole conversation in conversation related routes.
  32. */
  33. public function onFormatNoteList(array $notes_in, array &$notes_out, Request $request): EventResult
  34. {
  35. if (str_starts_with($request->get('_route'), 'conversation')) {
  36. $parents = $this->conversationFormat($notes_in);
  37. $notes_out = $this->conversationFormatTree($parents, $notes_in);
  38. } else {
  39. $notes_out = $this->feedFormatTree($notes_in);
  40. }
  41. return Event::stop;
  42. }
  43. /**
  44. * Formats general Feed view, allowing users to see a Note and its direct replies.
  45. * These replies are then, shown independently of parent note, making sure that every single Note is shown at least
  46. * once to users.
  47. *
  48. * The list is transversed in reverse to prevent any parent Note from being processed twice. At the same time,
  49. * this allows all direct replies to be rendered inside the same, respective, parent Note.
  50. * Moreover, this implies the Entity\Note::getReplies() query will only be performed once, for every Note.
  51. *
  52. * @param array $notes The Note list to be formatted, each element has two keys: 'note' (parent/current note),
  53. * and 'replies' (array of notes in the same format)
  54. */
  55. private function feedFormatTree(array $notes): array
  56. {
  57. $tree = [];
  58. $notes = array_reverse($notes);
  59. $max_replies_to_show = Common::config('plugin_tree_notes', 'feed_replies');
  60. foreach ($notes as $note) {
  61. if (!\is_null($children = $note->getReplies(limit: $max_replies_to_show))) {
  62. $total_replies = $note->getRepliesCount();
  63. $show_more = $total_replies > $max_replies_to_show;
  64. $notes = array_filter($notes, fn (Note $n) => !\in_array($n, $children));
  65. $tree[] = [
  66. 'note' => $note,
  67. 'replies' => array_map(
  68. fn ($n) => [
  69. 'note' => $n,
  70. 'replies' => [], // We want only one depth level
  71. 'show_more' => ($n->getRepliesCount() > $max_replies_to_show),
  72. 'total_replies' => $n->getRepliesCount(),
  73. ],
  74. $children,
  75. ),
  76. 'total_replies' => $total_replies,
  77. 'show_more' => $show_more,
  78. ];
  79. } else {
  80. $tree[] = ['note' => $note, 'replies' => [], 'show_more' => false];
  81. }
  82. }
  83. return array_reverse($tree);
  84. }
  85. /**
  86. * Filters given Note list off any children, returning only initial Notes of a Conversation.
  87. *
  88. * @param array $notes_in Notes to be filtered
  89. *
  90. * @return array All initial Conversation Notes in given list
  91. */
  92. private function conversationFormat(array $notes_in): array
  93. {
  94. return array_filter($notes_in, static fn (Note $note) => \is_null($note->getReplyTo()));
  95. }
  96. private function conversationFormatTree(array $parents, array $notes): array
  97. {
  98. $subtree = [];
  99. foreach ($parents as $p) {
  100. $subtree[] = $this->conversationFormatSubTree($p, $notes);
  101. }
  102. return $subtree;
  103. }
  104. private function conversationFormatSubTree(Note $parent, array $notes)
  105. {
  106. $children = array_filter($notes, fn (Note $note) => $note->getReplyTo() === $parent->getId());
  107. return [
  108. 'note' => $parent,
  109. 'replies' => $this->conversationFormatTree($children, $notes),
  110. 'show_more' => false, // It's always false, we're showing everyone
  111. ];
  112. }
  113. public function onAppendNoteBlock(Request $request, array $conversation, array &$res): EventResult
  114. {
  115. if (\array_key_exists('replies', $conversation)) {
  116. $res[] = Formatting::twigRenderFile(
  117. 'tree_notes/note_replies_block.html.twig',
  118. [
  119. 'nickname' => $conversation['note']->getActorNickname(),
  120. 'conversation' => $conversation,
  121. ],
  122. );
  123. }
  124. return Event::next;
  125. }
  126. }