diff --git a/components/Conversation/Controller/Reply.php b/components/Conversation/Controller/Reply.php index ad72deaddf..36aecdca05 100644 --- a/components/Conversation/Controller/Reply.php +++ b/components/Conversation/Controller/Reply.php @@ -74,7 +74,15 @@ class Reply extends FeedController throw new NoSuchNoteException(); } - // TODO shouldn't this be the posting form? + /* + * TODO shouldn't this be the posting form? + * Posting needs to be improved to do that. Currently, if it was used here, + * there are only slow ways to retrieve the resulting note. + * Not only is it part of a right panel event, there's an immediate redirect exception + * after submitting it. + * That event needs to be extended to allow this component to automagically fill the To: field and get the + * resulting note + */ $form = Form::create([ ['content', TextareaType::class, ['label' => _m('Reply'), 'label_attr' => ['class' => 'section-form-label'], 'help' => _m('Please input your reply.')]], FormFields::language( diff --git a/components/Conversation/Conversation.php b/components/Conversation/Conversation.php index c0523d4eed..a9167bdaf7 100644 --- a/components/Conversation/Conversation.php +++ b/components/Conversation/Conversation.php @@ -43,7 +43,7 @@ use Symfony\Component\HttpFoundation\Request; class Conversation extends Component { /** - * HTML rendering event that adds the repeat form as a note + * HTML rendering event that adds a reply link as a note * action, if a user is logged in */ public function onAddNoteActions(Request $request, Note $note, array &$actions): bool @@ -125,6 +125,14 @@ class Conversation extends Component return Event::next; } + public function onProcessNoteContent(Note $note, string $content): bool + { + // If the source lacks capability of sending the "reply_to" + // metadata, let's try to find an inline reply_to-reference. + // TODO: preg match any reply_to reference and handle reply to funky business (see Link component) + return Event::next; + } + /** * @return bool */ diff --git a/components/Conversation/Entity/Conversation.php b/components/Conversation/Entity/Conversation.php index c1441a03a1..4e432ef79c 100644 --- a/components/Conversation/Entity/Conversation.php +++ b/components/Conversation/Entity/Conversation.php @@ -47,8 +47,8 @@ class Conversation extends Entity // {{{ Autocode // @codeCoverageIgnoreStart private int $id; - private string $uri; // varchar(191) unique_key not 255 because utf8mb4 takes more space - private int $note_id; + private string $uri; + private int $initial_note_id; public function setId(int $id): self { @@ -72,15 +72,15 @@ class Conversation extends Entity return $this->uri; } - public function setNoteId(int $note_id): self + public function setInitialNoteId(int $initial_note_id): self { - $this->note_id = $note_id; + $this->initial_note_id = $initial_note_id; return $this; } - public function getNoteId(): int + public function getInitialNoteId(): int { - return $this->note_id; + return $this->initial_note_id; } @@ -94,14 +94,14 @@ class Conversation extends Entity 'fields' => [ 'id' => ['type' => 'serial', 'not null' => true, 'description' => 'Serial identifier, since any additional meaning would require updating its value for every reply upon receiving a new aparent root'], 'uri' => ['type' => 'varchar', 'not null' => true, 'length' => 191, 'description' => 'URI of the conversation'], - 'note_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Note.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'Root of note for this conversation'], + 'initial_note_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Note.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'Initial note seen on this host for this conversation'], ], 'primary key' => ['id'], 'unique keys' => [ 'conversation_uri_uniq' => ['uri'], ], 'foreign keys' => [ - 'note_id_to_id_fkey' => ['note', ['note_id' => 'id']], + 'initial_note_id_to_id_fkey' => ['note', ['initial_note_id' => 'id']], ], ]; } diff --git a/src/Entity/Note.php b/src/Entity/Note.php index e056fdc99f..046b532a16 100644 --- a/src/Entity/Note.php +++ b/src/Entity/Note.php @@ -58,8 +58,8 @@ class Note extends Entity private int $scope = 1; private ?string $url; private ?int $language_id; - private \DateTimeInterface $created; - private \DateTimeInterface $modified; + private DateTimeInterface $created; + private DateTimeInterface $modified; public function setId(int $id): self { @@ -193,29 +193,28 @@ class Note extends Entity return $this->language_id; } - public function setCreated(\DateTimeInterface $created): self + public function setCreated(DateTimeInterface $created): self { $this->created = $created; return $this; } - public function getCreated(): \DateTimeInterface + public function getCreated(): DateTimeInterface { return $this->created; } - public function setModified(\DateTimeInterface $modified): self + public function setModified(DateTimeInterface $modified): self { $this->modified = $modified; return $this; } - public function getModified(): \DateTimeInterface + public function getModified(): DateTimeInterface { return $this->modified; } - // @codeCoverageIgnoreEnd // }}} Autocode @@ -326,15 +325,29 @@ class Note extends Entity }); } + /** + * Returns this Note's reply_to/parent. + * + * If we don't know the reply, we might know the **Conversation**! + * This will happen if a known remote user replies to an + * unknown remote user - within a known Conversation. + * + * As such, **do not take for granted** that this is a root + * Note of a Conversation, in case this returns null! + * Whenever a note is created, checks should be made + * to guarantee that the latest info is correct. + */ public function getReplyToNote(): ?self { return self::getByPK($this->getReplyTo()); } - public function getReplies() + /** + * Returns all **known** replies made to this entity + */ + public function getReplies(): array { - $id = $this->getId(); - return Cache::get('note-replies-' . $id, fn () => DB::dql('select n from note n where n.reply_to = :id', ['id' => $id])); + return Cache::get('note-replies-' . $this->getId(), fn () => DB::dql('select n from note n where n.reply_to = :id', ['id' => $this->getId()])); } /** @@ -433,20 +446,20 @@ class Note extends Entity return [ 'name' => 'note', 'fields' => [ - 'id' => ['type' => 'serial', 'not null' => true], - 'actor_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'who made the note'], - '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)'], - 'conversation_id' => ['type' => 'serial', 'not null' => true, 'foreign key' => true, 'target' => 'Conversation.id', 'multiplicity' => 'one to one', 'description' => 'the conversation identifier'], - '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', 'not null' => true, '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"'], - '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_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Language.id', 'multiplicity' => 'one to many', 'description' => 'The language for this note'], - 'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'], - 'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'], + 'id' => ['type' => 'serial', 'not null' => true], + 'actor_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'who made the note'], + '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)'], + 'conversation_id' => ['type' => 'serial', 'not null' => true, 'foreign key' => true, 'target' => 'Conversation.id', 'multiplicity' => 'one to one', 'description' => 'the conversation identifier'], + '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', 'not null' => true, '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"'], + '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_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Language.id', 'multiplicity' => 'one to many', 'description' => 'The language for this note'], + 'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'], + 'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'], ], 'primary key' => ['id'], 'indexes' => [