[PLUGIN][Reply] Separated replies from Note table.

[PLUGIN][Repeat] Deleted unnecessary card note template, info now to
appended at the end of note.
[PLUGIN][TreeNotes] WIP to accomodate reply plugin changes.
[TWIG][Runtime] Removed getAdditionalTemplateVars event.
This commit is contained in:
Eliseu Amaro 2021-11-07 01:32:06 +00:00
parent 7d8819a3da
commit f2f1bdc145
Signed by: eliseuamaro
GPG Key ID: 96DA09D4B97BC2D5
14 changed files with 291 additions and 232 deletions

View File

@ -155,10 +155,10 @@ class Posting extends Component
* @throws ClientException
* @throws ServerException
*/
public static function storeLocalNote(Actor $actor, string $content, string $content_type, array $attachments, ?Note $reply_to = null, ?Note $repeat_of = null)
public static function storeLocalNote(Actor $actor, string $content, string $content_type, array $attachments)
{
$rendered = null;
Event::handle('RenderNoteContent', [$content, $content_type, &$rendered, $actor, $reply_to]);
Event::handle('RenderNoteContent', [$content, $content_type, &$rendered, $actor]);
$note = Note::create([
'actor_id' => $actor->getId(),
'content' => $content,
@ -195,6 +195,8 @@ class Posting extends Component
}
DB::flush();
return $note;
}
public function onRenderNoteContent(string $content, string $content_type, ?string &$rendered, Actor $author, ?Note $reply_to = null)

View File

@ -5,7 +5,7 @@ to take advantage of such phenomena, which is sometimes taken to great effects i
Patterns emerge when concepts and actions, interlinked, construct a predictable outcome. With a common design language,
we hope to achieve such predictability, and supply an innate understanding of user interaction.
The goal isn't to have one and only design language, but to encourage new interfaces to take similar steps on their
The goal isn't to have one and only design language, but to encourage new themes/interfaces to take similar steps on their
design processes.
## Predictability and user experience
@ -18,10 +18,14 @@ Web technologies as a whole contain a set of constraints for organizing web page
a common structural basis.
Users accustomed to surfing the Web know which user interactions are acceptable and which aren't.
The key puzzle is how users come to know these restrictions of their Web UI.
The key puzzle is how users come to know these restrictions of their Web UI. This is the crux of any
accessible Web page, an hierarchy needs to be followed as well as common standards.
### Canons of page construction
The aforementioned comparison between books and Web pages isn't just a coincidence, given the resemblance between the
two mediums. From their presentation to fundamental theory, it's only natural to apply core book design ideas to the Web.
### User customization

View File

@ -28,11 +28,14 @@ use App\Core\Event;
use App\Core\Modules\NoteHandlerPlugin;
use App\Core\Router\RouteLoader;
use App\Core\Router\Router;
use App\Entity\Actor;
use App\Entity\Note;
use App\Util\Common;
use App\Util\Exception\InvalidFormException;
use App\Util\Exception\NoSuchNoteException;
use App\Util\Exception\NotFoundException;
use App\Util\Exception\RedirectException;
use App\Util\Formatting;
use App\Util\Nickname;
use phpDocumentor\Reflection\PseudoTypes\NumericString;
use Symfony\Component\HttpFoundation\Request;
@ -87,6 +90,14 @@ class Favourite extends NoteHandlerPlugin
return Event::next;
}
public function onAppendCardNote(array $vars, array &$result) {
// if note is the original, append on end "user favourited this"
$actor = $vars['actor'];
$note = $vars['note'];
return Event::next;
}
public function onAddRoute(RouteLoader $r): bool
{
// Add/remove note to/from favourites

View File

@ -25,6 +25,7 @@ use App\Core\DB\DB;
use App\Core\Event;
use App\Core\Router\RouteLoader;
use App\Core\Router\Router;
use App\Entity\Actor;
use App\Util\Exception\DuplicateFoundException;
use App\Util\Exception\InvalidFormException;
use App\Util\Exception\NoSuchNoteException;
@ -89,28 +90,15 @@ class Repeat extends NoteHandlerPlugin
return Event::next;
}
public function onOverrideTemplateImport(string $current_template, string $default, string &$response)
{
switch ($current_template) {
case '/network/feed.html.twig':
$response = "plugins/repeat/cards/note/view.html.twig";
return Event::stop;
}
public function onAppendCardNote(array $vars, array &$result) {
// if note is the original and user isn't the one who repeated, append on end "user repeated this"
// if user is the one who repeated, append on end "you repeated this, remove repeat?"
$actor = $vars['actor'];
$note = $vars['note'];
return Event::next;
}
public function onGetAdditionalTemplateVars(array $vars, array &$result)
{
$note_id = $vars['note_id'];
$opts = ['id' => $note_id];
$is_repeat = DB::count('note_repeat', $opts) >= 1;
$result = ['is_repeat' => (bool)$is_repeat];
return Event::stop;
}
public function onAddRoute(RouteLoader $r): bool
{
// Add/remove note to/from repeats

View File

@ -1,54 +0,0 @@
{% extends '/cards/note/view.html.twig' %}
{% block note_author_repeated %}
{# Microformat's h-card properties indicates a face icon is a "u-logo" #}
<a href="{{ actor_url }}" class="note-author u-url">
<strong class="note-author-fullname">
{% if fullname is not null %}
{{ fullname }}
{% else %}
{{ nickname }}
{% endif %}
</strong>
<em class="note-author-nickname">{{ nickname }} {{ "repeated the following note" | trans }}</em>
</a>
{% endblock note_author_repeated %}
{% macro macro_note(note, replies) %}
{% set nickname = note.getActorNickname() %}
{% set fullname = note.getActorFullname() %}
{% set actor_url = note.getActor().getUrl() %}
{% set additional_vars = handle_event('GetAdditionalTemplateVars', {'note_id': note.getId(), 'actor_id': note.getActorId()}) %}
{% if additional_vars['is_repeat'] %}
<article class="h-entry hentry note">
{{ block('note_sidebar') }}
<div class="note-wrapper">
<div tabindex="0" title="{{ 'Begin a note by the user: ' | trans }} {{ nickname }}." class="note-info">
{{ block('note_author_repeated') }}
{{ block('note_actions') }}
</div>
<section tabindex="0" role="dialog" class="e-content entry-content note-content">
{{ _self.macro_note_minimal(note) }}
</section>
{{ block('note_replies') }}
</div>
</article>
{% else %}
<article class="h-entry hentry note">
{{ block('note_sidebar') }}
<div class="note-wrapper">
<div tabindex="0" title="{{ 'Begin a note by the user: ' | trans }} {{ nickname }}." class="note-info">
{{ block('note_author') }}
{{ block('note_actions') }}
</div>
<section tabindex="0" role="dialog" class="e-content entry-content note-content">
{{ block('note_text') }}
{{ block('note_attachments') }}
{{ block('note_links') }}
</section>
{{ block('note_replies') }}
</div>
</article>
{% endif %}
{% endmacro macro_note %}

View File

@ -29,6 +29,9 @@ namespace Plugin\Reply\Controller;
use App\Core\Controller;
use App\Core\DB\DB;
use App\Core\Form;
use App\Entity\Actor;
use Component\Posting\Posting;
use Plugin\Reply\Entity\NoteReply;
use function App\Core\I18n\_m;
use App\Core\Log;
use App\Core\Router\Router;
@ -38,7 +41,6 @@ use App\Util\Exception\ClientException;
use App\Util\Exception\InvalidFormException;
use App\Util\Exception\NoSuchNoteException;
use App\Util\Exception\RedirectException;
use Component\Posting\Posting;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
@ -49,12 +51,13 @@ class Reply extends Controller
/**
* Controller for the note reply non-JS page
*/
public function handle(Request $request, string $reply_to)
public function replyAddNote(Request $request, int $id)
{
$user = Common::ensureLoggedIn();
$actor_id = $user->getId();
$note = DB::find('note', ['id' => (int) $reply_to]);
if ($note === null || !$note->isVisibleTo($user)) {
$note = Note::getWithPK($id);
if (is_null($note) || !$note->isVisibleTo($user)) {
throw new NoSuchNoteException();
}
@ -74,35 +77,56 @@ class Reply extends Controller
if ($form->isSubmitted()) {
$data = $form->getData();
if ($form->isValid()) {
Posting::storeLocalNote(
actor: $user->getActor(),
// Create a new note with the same content as the original
$reply = Posting::storeLocalNote(
actor: Actor::getWithPK($actor_id),
content: $data['content'],
content_type: 'text/plain', // TODO
attachments: $data['attachments'],
reply_to: $reply_to,
repeat_of: null,
);
$return = $this->string('return_to');
if (!\is_null($return)) {
DB::persist($reply);
// Update DB
DB::flush();
// Find the id of the note we just created
$reply_id = $reply->getId();
$og_id = $note->getId();
// Add it to note_repeat table
if (!is_null($reply_id)) {
DB::persist(NoteReply::create([
'id' => $reply_id,
'actor_id' => $actor_id,
'reply_to' => $og_id
]));
}
// Update DB one last time
DB::flush();
if (array_key_exists('from', $get_params = $this->params())) {
// Prevent open redirect
if (Router::isAbsolute($return)) {
Log::warning("Actor {$actor_id} attempted to reply to a note and then get redirected to another host, or the URL was invalid ({$return})");
if (Router::isAbsolute($get_params['from'])) {
Log::warning("Actor {$actor_id} attempted to reply to a note and then get redirected to another host, or the URL was invalid ({$get_params['from']})");
throw new ClientException(_m('Can not redirect to outside the website from here'), 400); // 400 Bad request (deceptive)
} else {
throw new RedirectException(url: $return);
# TODO anchor on element id
throw new RedirectException($get_params['from']);
}
} else {
throw new RedirectException('root'); // If we don't have a URL to return to, go to the instance root
}
} else {
throw new InvalidFormException();
}
}
return [
'_template' => 'note/reply.html.twig',
'_template' => 'reply/add_reply.html.twig',
'note' => $note,
'reply' => $form->createView(),
'add_reply' => $form->createView(),
];
}
}

View File

@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
// {{{ License
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
// }}}
namespace Plugin\Reply\Entity;
use App\Core\DB\DB;
use App\Core\Entity;
use App\Entity\Note;
use function PHPUnit\Framework\isEmpty;
/**
* Entity for notices
*
* @category DB
* @package GNUsocial
*
* @author Eliseu Amaro <mail@eliseuama.ro>
* @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
class NoteReply extends Entity
{
private int $id;
private int $actor_id;
private int $reply_to;
public function setId(int $id): self
{
$this->id = $id;
return $this;
}
public function getId(): int
{
return $this->id;
}
public function setActorId(int $actor_id): self
{
$this->actor_id = $actor_id;
return $this;
}
public function getActorId(): ?int
{
return $this->actor_id;
}
public function setReplyTo(int $reply_to): self
{
$this->reply_to = $reply_to;
return $this;
}
public function getReplyTo(): self
{
return $this->reply_to;
}
public static function getNoteReplies(Note $note): array
{
return DB::sql("select n.id from note n cross join note_reply nr where n.id = :note_id",
['n' => 'App\Entity\Note', 'note_id' => $note->getId()],
);
}
public static function getReplyToNote(Note $note): ?int
{
$result = DB::dql('select nr.reply_to from note_reply nr '
. 'where nr.id = :note_id', ['note_id' => $note->getId()]);
if (!isEmpty($result)) {
return $result['reply_to'];
}
return null;
}
public static function schemaDef(): array
{
return [
'name' => 'note_reply',
'fields' => [
'id' => ['type' => 'int', 'not null' => true, 'description' => 'The id of the reply itself'],
'actor_id' => ['type' => 'int', 'not null' => true, 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'description' => 'Who made this reply'],
'reply_to' => ['type' => 'int', 'not null' => true, 'foreign key' => true, 'target' => 'Note.id', 'multiplicity' => 'one to one', 'description' => 'Note this is a reply of'],
],
'primary key' => ['id'],
'foreign keys' => [
'note_reply_to_id_fkey' => ['note', ['reply_to' => 'id']],
'actor_reply_to_id_fkey' => ['actor', ['actor_id' => 'id']],
],
'indexes' => [
'note_reply_to_idx' => ['reply_to'],
],
];
}
}

View File

@ -23,80 +23,102 @@ declare(strict_types = 1);
namespace Plugin\Reply;
use App\Core\DB\DB;
use App\Core\Event;
use App\Core\Form;
use App\Core\Modules\NoteHandlerPlugin;
use App\Core\Router\Router;
use App\Entity\Actor;
use App\Entity\Note;
use App\Util\Common;
use App\Util\Exception\InvalidFormException;
use App\Util\Exception\NoSuchNoteException;
use App\Util\Exception\NotFoundException;
use App\Util\Exception\RedirectException;
use Component\Posting\Posting;
use App\Util\Formatting;
use Plugin\Reply\Controller\Reply as ReplyController;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Plugin\Reply\Entity\NoteReply;
use Symfony\Component\HttpFoundation\Request;
use function PHPUnit\Framework\isEmpty;
class Reply extends NoteHandlerPlugin
{
/**
* HTML rendering event that adds the repeat form as a note
* action, if a user is logged in
*
* @throws InvalidFormException
* @throws NoSuchNoteException
* @throws RedirectException
*
* @return bool Event hook
*/
public function onAddNoteActions(Request $request, Note $note, array &$actions): bool
{
if (is_null(Common::user())) {
return Event::next;
}
// Generating URL for repeat action route
$args = ['id' => $note->getId()];
$type = Router::ABSOLUTE_PATH;
$reply_action_url = Router::url('reply_add', $args, $type);
// Concatenating get parameter to redirect the user to where he came from
$reply_action_url .= '?from=' . substr($request->getQueryString(), 2);
$reply_action = [
"url" => $reply_action_url,
"classes" => "button-container reply-button-container note-actions-unset",
"id" => "reply-button-container-" . $note->getId()
];
$actions[] = $reply_action;
return Event::next;
}
public function onAppendCardNote(array $vars, array &$result) {
// if note is the original, append on end "user replied to this"
// if note is the reply itself: append on end "in response to user in conversation"
$actor = $vars['actor'];
$note = $vars['note'];
if ($actor !== null) {
try {
try {
$complementary_info = '';
$note_replies[] = NoteReply::getNoteReplies($note);
if (isEmpty($note_replies)) {
return Event::next;
}
dd($note_replies);
foreach ($note_replies as $reply) {
$reply_actor = Actor::getWithPK($reply['actor_id']);
$reply_actor_url = $reply_actor->getUrl();
$reply_actor_nickname = $reply_actor->getNickname();
$complementary_info .= "<a href={$reply_actor_url}>{$reply_actor_nickname}</a>, ";
}
$complementary_info = rtrim(trim($complementary_info), ',');
$complementary_info .= ' replied to this note.';
$result[] = Formatting::twigRenderString($complementary_info, []);
} catch (NotFoundException $e) {
return Event::next;
}
} catch (NotFoundException $e) {
return Event::next;
}
}
return Event::next;
}
public function onAddRoute($r)
{
$r->connect('note_reply', '/note/reply/{reply_to<\\d*>}', ReplyController::class);
$r->connect('reply_add', '/object/note/{id<\d+>}/reply', [ReplyController::class, 'replyAddNote']);
return Event::next;
}
// TODO: Refactoring to link instead of a form
/**
* HTML rendering event that adds the reply form as a note action,
* if a user is logged in
*
* @throws RedirectException
*/
/* public function onAddNoteActions(Request $request, Note $note, array &$actions)
{
if (($user = Common::user()) === null) {
return Event::next;
}
$form = Form::create([
['content', HiddenType::class, ['label' => ' ', 'required' => false]],
['attachments', HiddenType::class, ['label' => ' ', 'required' => false]],
['note_id', HiddenType::class, ['data' => $note->getId()]],
['reply', SubmitType::class,
[
'label' => ' ',
'attr' => [
'class' => 'note-actions-unset button-container reply-button-container',
'title' => 'Reply to this note!',
],
],
],
]);
// Handle form
$ret = self::noteActionHandle($request, $form, $note, 'reply', function ($note, $data, $user) use ($request) {
if ($data['content'] !== null) {
// JS submitted
// TODO Implement in JS
Posting::storeLocalNote(
actor: $user->getActor(),
content: $data['content'],
content_type: 'text/plain',
attachments: $data['attachments'],
reply_to: $data['reply_to'],
repeat_of: null,
);
} else {
// JS disabled, redirect
throw new RedirectException('note_reply', ['reply_to' => $note->getId(), 'return_to' => $request->getRequestUri()]);
return Event::stop;
}
});
if ($ret !== null) {
return $ret;
}
$actions[] = $form->createView();
return Event::next;
}*/
}

View File

@ -1,25 +1,19 @@
{% extends 'stdgrid.html.twig' %}
{% block meta %}
{{ parent() }}
{% endblock %}
{% import "/cards/note/view.html.twig" as noteView %}
{% block title %}{{ 'Reply to ' | trans }}{{ note.getActorNickname() }}{{ '\'s note.' | trans }}{% endblock %}
{% block header %}
{% block stylesheets %}
{{ parent() }}
{% endblock %}
{% block left %}
{{ parent() }}
{% endblock %}
<link rel="stylesheet" href="{{ asset('assets/default_theme/css/pages/feeds.css') }}" type="text/css">
{% endblock stylesheets %}
{% block body %}
{{ parent() }}
<div class="page">
<div class="main">
{% include '/cards/note/view.html.twig' with {'note': note} only %}
{{ form(reply) }}
{{ noteView.macro_note_minimal(note) }}
{{ form(add_reply) }}
</div>
</div>
{% endblock body %}

View File

@ -23,6 +23,7 @@ namespace Plugin\TreeNotes;
use App\Core\Modules\Plugin;
use App\Entity\Note;
use Plugin\Reply\Entity\NoteReply;
class TreeNotes extends Plugin
{
@ -31,7 +32,7 @@ class TreeNotes extends Plugin
*/
public function onFormatNoteList(array $notes_in, ?array &$notes_out)
{
$roots = array_filter($notes_in, fn (Note $note) => $note->getReplyTo() == null);
$roots = array_filter($notes_in, fn (Note $note) => NoteReply::getReplyToNote($note) == null);
$notes_out = $this->build_tree($roots, $notes_in);
}
@ -46,7 +47,7 @@ class TreeNotes extends Plugin
private function build_subtree(Note $parent, array $notes)
{
$children = array_filter($notes, fn (Note $n) => $parent->getId() == $n->getReplyTo());
$children = array_filter($notes, fn (Note $note) => $parent->getId() == NoteReply::getReplyToNote($note));
return ['note' => $parent, 'replies' => $this->build_tree($children, $notes)];
}
}

View File

@ -49,11 +49,9 @@ class Note extends Entity
private ?string $content_type = null;
private ?string $content = null;
private ?string $rendered = null;
private ?int $reply_to;
private ?bool $is_local;
private ?string $source;
private ?int $conversation;
private ?int $repeat_of;
private int $scope = VisibilityScope::PUBLIC;
private string $url;
private string $language;
@ -118,17 +116,6 @@ class Note extends Entity
return $this->rendered;
}
public function setReplyTo(?int $reply_to): self
{
$this->reply_to = $reply_to;
return $this;
}
public function getReplyTo(): ?int
{
return $this->reply_to;
}
public function setIsLocal(?bool $is_local): self
{
$this->is_local = $is_local;
@ -162,17 +149,6 @@ class Note extends Entity
return $this->conversation;
}
/* public function setRepeatOf(?int $repeat_of): self
{
$this->repeat_of = $repeat_of;
return $this;
}
public function getRepeatOf(): ?int
{
return $this->repeat_of;
}*/
public function setScope(int $scope): self
{
$this->scope = $scope;
@ -291,27 +267,6 @@ class Note extends Entity
});
}
public function getReplies(): array
{
return Cache::getList('note-replies-' . $this->id, fn () => DB::dql('select n from note n where n.reply_to = :id', ['id' => $this->id]));
}
public function getReplyToNickname(): ?string
{
if (!empty($this->reply_to)) {
return Cache::get('note-reply-to-' . $this->id, function () {
return DB::dql(
<<<'EOF'
select g from note n join
actor g with n.actor_id = g.id where n.reply_to = :reply
EOF,
['reply' => $this->reply_to],
)[0]->getNickname();
});
}
return null;
}
/**
* Whether this note is visible to the given actor
*/
@ -354,11 +309,9 @@ class Note extends Entity
'content' => ['type' => 'text', 'description' => 'note content'],
'content_type' => ['type' => 'varchar', 'not null' => true, 'default' => 'text/plain', 'length' => 129, 'description' => 'A note can be written in a multitude of formats such as text/plain, text/markdown, application/x-latex, and text/html'],
'rendered' => ['type' => 'text', 'description' => 'rendered note content, so we can keep the microtags (if not local)'],
'reply_to' => ['type' => 'int', 'foreign key' => true, 'target' => 'Note.id', 'multiplicity' => 'one to one', 'description' => 'note replied to, null if root of a conversation'],
'is_local' => ['type' => 'bool', 'description' => 'was this note generated by a local actor'],
'source' => ['type' => 'varchar', 'foreign key' => true, 'length' => 32, 'target' => 'NoteSource.code', 'multiplicity' => 'many to one', 'description' => 'fkey to source of note, like "web", "im", or "clientname"'],
'conversation' => ['type' => 'int', 'foreign key' => true, 'target' => 'Conversation.id', 'multiplicity' => 'one to one', 'description' => 'the local conversation id'],
// 'repeat_of' => ['type' => 'int', 'foreign key' => true, 'target' => 'Note.id', 'multiplicity' => 'one to one', 'description' => 'note this is a repeat of'],
'scope' => ['type' => 'int', 'not null' => true, 'default' => VisibilityScope::PUBLIC, 'description' => 'bit map for distribution scope; 0 = everywhere; 1 = this server only; 2 = addressees; 4 = groups; 8 = subscribers; 16 = messages; null = default'],
'url' => ['type' => 'text', 'description' => 'Permalink to Note'],
'language' => ['type' => 'int', 'foreign key' => true, 'target' => 'Language.id', 'multiplicity' => 'one to many', 'description' => 'The language for this note'],
@ -370,7 +323,6 @@ class Note extends Entity
'note_created_id_is_local_idx' => ['created', 'is_local'],
'note_actor_created_idx' => ['actor_id', 'created'],
'note_is_local_created_actor_idx' => ['is_local', 'created', 'actor_id'],
// 'note_repeat_of_created_idx' => ['repeat_of', 'created'],
'note_conversation_created_idx' => ['conversation', 'created'],
'note_reply_to_idx' => ['reply_to'],
],

View File

@ -134,15 +134,6 @@ class Runtime implements RuntimeExtensionInterface, EventSubscriberInterface
return $result;
}
public function getAdditionalTemplateVars(array $vars): array
{
$result = [];
if (Event::handle('GetAdditionalTemplateVars', [$vars, &$result]) !== Event::stop) {
return [];
}
return $result;
}
// ----------------------------------------------------------
/**

View File

@ -79,21 +79,29 @@
{% macro macro_note(note, replies) %}
{% set nickname = note.getActorNickname() %}
{% set fullname = note.getActorFullname() %}
{% set actor_url = note.getActor().getUrl() %}
{% set actor = note.getActor() %}
{% set actor_url = actor.getUrl() %}
<article class="h-entry hentry note">
{{ block('note_sidebar') }}
<div class="note-wrapper">
<div tabindex="0" title="{{ 'Begin a note by the user: ' | trans }} {{ nickname }}." class="note-info">
{{ block('note_author') }}
{{ block('note_reply_to') }}
{{ block('note_actions') }}
</div>
<section tabindex="0" role="dialog" class="e-content entry-content note-content">
{{ block('note_text') }}
{{ block('note_attachments') }}
{{ block('note_links') }}
</section>
{% for block in handle_event('AppendCardNote', {'note': note, 'actor': actor}) %}
<aside class="note-complementary">
{{ block | raw }}
</aside>
{% endfor %}
{{ block('note_replies') }}
</div>
</article>

View File

@ -1,7 +1,5 @@
{% extends 'stdgrid.html.twig' %}
{% set override_import = handle_override_template_import('/network/feed.html.twig', '/cards/note/view.html.twig') %}
{% import override_import as noteView %}
{% import '/cards/note/view.html.twig' as noteView %}
{% block title %}{% if page_title is defined %}{{ page_title | trans }}{% endif %}{% endblock %}