perf: store message mentions for better performance (#4079)
Some checks failed
Backend Tests / run (push) Has been cancelled
Frontend Workflow / run (push) Has been cancelled
Static Code Analysis / run (push) Has been cancelled

This commit is contained in:
Sami Mazouz 2024-10-19 17:49:58 +01:00 committed by GitHub
parent 8742790980
commit e9be7b9aea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 272 additions and 45 deletions

View File

@ -9,9 +9,9 @@
namespace Flarum\Mentions\Formatter;
use Flarum\Database\AbstractModel;
use Flarum\Group\Group;
use Flarum\Locale\TranslatorInterface;
use Flarum\Post\Post;
use s9e\TextFormatter\Renderer;
use s9e\TextFormatter\Utils;
@ -25,8 +25,8 @@ class FormatGroupMentions
public function __invoke(Renderer $renderer, mixed $context, string $xml): string
{
return Utils::replaceAttributes($xml, 'GROUPMENTION', function ($attributes) use ($context) {
$group = (($context && isset($context->getRelations()['mentionsGroups'])) || $context instanceof Post)
? $context->mentionsGroups->find($attributes['id'])
$group = ($context instanceof AbstractModel && $context->isRelation('mentionsGroups'))
? $context->mentionsGroups->find($attributes['id']) // @phpstan-ignore-line
: Group::find($attributes['id']);
if ($group) {

View File

@ -9,11 +9,11 @@
namespace Flarum\Mentions\Formatter;
use Flarum\Database\AbstractModel;
use Flarum\Discussion\Discussion;
use Flarum\Http\SlugManager;
use Flarum\Locale\TranslatorInterface;
use Flarum\Post\Post;
use Psr\Http\Message\ServerRequestInterface as Request;
use s9e\TextFormatter\Renderer;
use s9e\TextFormatter\Utils;
@ -27,18 +27,12 @@ class FormatPostMentions
/**
* Configure rendering for post mentions.
*
* @param \s9e\TextFormatter\Renderer $renderer
* @param mixed $context
* @param string $xml
* @param \Psr\Http\Message\ServerRequestInterface|null $request
* @return string $xml to be rendered
*/
public function __invoke(Renderer $renderer, $context, $xml, Request $request = null)
public function __invoke(Renderer $renderer, mixed $context, string $xml): string
{
return Utils::replaceAttributes($xml, 'POSTMENTION', function ($attributes) use ($context) {
$post = (($context && isset($context->getRelations()['mentionsPosts'])) || $context instanceof Post)
? $context->mentionsPosts->find($attributes['id'])
$post = ($context instanceof AbstractModel && $context->isRelation('mentionsPosts'))
? $context->mentionsPosts->find($attributes['id']) // @phpstan-ignore-line
: Post::find($attributes['id']);
if ($post && $post->user) {

View File

@ -9,7 +9,7 @@
namespace Flarum\Mentions\Formatter;
use Flarum\Post\Post;
use Flarum\Database\AbstractModel;
use Flarum\Tags\Tag;
use Psr\Http\Message\ServerRequestInterface as Request;
use s9e\TextFormatter\Renderer;
@ -17,12 +17,12 @@ use s9e\TextFormatter\Utils;
class FormatTagMentions
{
public function __invoke(Renderer $renderer, mixed $context, ?string $xml, Request $request = null): string
public function __invoke(Renderer $renderer, mixed $context, string $xml, Request $request = null): string
{
return Utils::replaceAttributes($xml, 'TAGMENTION', function ($attributes) use ($context) {
/** @var Tag|null $tag */
$tag = (($context && isset($context->getRelations()['mentionsTags'])) || $context instanceof Post)
? $context->mentionsTags->find($attributes['id'])
$tag = ($context instanceof AbstractModel && $context->isRelation('mentionsTags'))
? $context->mentionsTags->find($attributes['id']) // @phpstan-ignore-line
: Tag::query()->find($attributes['id']);
if ($tag) {

View File

@ -9,9 +9,9 @@
namespace Flarum\Mentions\Formatter;
use Flarum\Database\AbstractModel;
use Flarum\Http\SlugManager;
use Flarum\Locale\TranslatorInterface;
use Flarum\Post\Post;
use Flarum\User\User;
use s9e\TextFormatter\Renderer;
use s9e\TextFormatter\Utils;
@ -27,8 +27,8 @@ class FormatUserMentions
public function __invoke(Renderer $renderer, mixed $context, string $xml): string
{
return Utils::replaceAttributes($xml, 'USERMENTION', function ($attributes) use ($context) {
$user = (($context && isset($context->getRelations()['mentionsUsers'])) || $context instanceof Post)
? $context->mentionsUsers->find($attributes['id'])
$user = ($context instanceof AbstractModel && $context->isRelation('mentionsUsers'))
? $context->mentionsUsers->find($attributes['id']) // @phpstan-ignore-line
: User::find($attributes['id']);
$attributes['deleted'] = false;

View File

@ -9,6 +9,7 @@
namespace Flarum\Mentions\Formatter;
use Flarum\Database\AbstractModel;
use Flarum\Locale\TranslatorInterface;
use Flarum\Post\Post;
use s9e\TextFormatter\Utils;
@ -33,8 +34,8 @@ class UnparsePostMentions
protected function updatePostMentionTags(mixed $context, string $xml): string
{
return Utils::replaceAttributes($xml, 'POSTMENTION', function ($attributes) use ($context) {
$post = (($context && isset($context->getRelations()['mentionsPosts'])) || $context instanceof Post)
? $context->mentionsPosts->find($attributes['id'])
$post = ($context instanceof AbstractModel && $context->isRelation('mentionsPosts'))
? $context->mentionsPosts->find($attributes['id']) // @phpstan-ignore-line
: Post::find($attributes['id']);
if ($post && $post->user) {

View File

@ -9,7 +9,7 @@
namespace Flarum\Mentions\Formatter;
use Flarum\Post\Post;
use Flarum\Database\AbstractModel;
use Flarum\Tags\Tag;
use s9e\TextFormatter\Utils;
@ -29,8 +29,8 @@ class UnparseTagMentions
{
return Utils::replaceAttributes($xml, 'TAGMENTION', function (array $attributes) use ($context) {
/** @var Tag|null $tag */
$tag = (($context && isset($context->getRelations()['mentionsTags'])) || $context instanceof Post)
? $context->mentionsTags->find($attributes['id'])
$tag = ($context instanceof AbstractModel && $context->isRelation('mentionsTags'))
? $context->mentionsTags->find($attributes['id']) // @phpstan-ignore-line
: Tag::query()->find($attributes['id']);
if ($tag) {

View File

@ -9,8 +9,8 @@
namespace Flarum\Mentions\Formatter;
use Flarum\Database\AbstractModel;
use Flarum\Locale\TranslatorInterface;
use Flarum\Post\Post;
use Flarum\User\User;
use s9e\TextFormatter\Utils;
@ -34,8 +34,8 @@ class UnparseUserMentions
protected function updateUserMentionTags(mixed $context, string $xml): string
{
return Utils::replaceAttributes($xml, 'USERMENTION', function ($attributes) use ($context) {
$user = (($context && isset($context->getRelations()['mentionsUsers'])) || $context instanceof Post)
? $context->mentionsUsers->find($attributes['id'])
$user = ($context instanceof AbstractModel && $context->isRelation('mentionsUsers'))
? $context->mentionsUsers->find($attributes['id']) // @phpstan-ignore-line
: User::find($attributes['id']);
$attributes['displayname'] = $user?->display_name ?? $this->translator->trans('core.lib.username.deleted_text');

View File

@ -83,5 +83,7 @@ return [
->type(Notification\MessageReceivedBlueprint::class, ['email']),
(new Extend\Event())
->listen(DialogMessage\Event\Created::class, Listener\SendNotificationWhenMessageSent::class),
->listen(DialogMessage\Event\Created::class, Listener\SendNotificationWhenMessageSent::class)
->listen(DialogMessage\Event\Created::class, Listener\UpdateMentionsMetadataWhenVisible::class)
->listen(DialogMessage\Event\Updated::class, Listener\UpdateMentionsMetadataWhenVisible::class),
];

View File

@ -134,7 +134,6 @@ export default class MessageComposer<CustomAttrs extends IMessageComposerAttrs =
})
.then((message) => {
this.composer.hide();
// @todo: app.dialogs.refresh();
// @ts-ignore
m.route.set(app.route('dialog', { id: message.data.relationships!.dialog.data.id }));
this.attrs.onsubmit?.(message);

View File

@ -117,9 +117,8 @@ export default class MessageStream<CustomAttrs extends IDialogStreamAttrs = IDia
.load(() => import('./MessageComposer'), {
user: app.session.user,
replyingTo: this.attrs.dialog,
onsubmit: (message: DialogMessage) => {
this.attrs.state.push(message);
setTimeout(() => this.scrollToBottom(), 50);
onsubmit: () => {
this.attrs.state.refresh().then(() => setTimeout(() => this.scrollToBottom(), 50));
},
})
.then(() => app.composer.show());

View File

@ -13,9 +13,9 @@ use Illuminate\Database\Schema\Blueprint;
return Migration::createTable(
'dialogs',
function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('first_message_id')->nullable();
$table->unsignedBigInteger('last_message_id')->nullable();
$table->increments('id');
$table->unsignedInteger('first_message_id')->nullable();
$table->unsignedInteger('last_message_id')->nullable();
$table->dateTime('last_message_at')->nullable();
$table->unsignedInteger('last_message_user_id')->nullable();
$table->foreign('last_message_user_id')->references('id')->on('users')->nullOnDelete();

View File

@ -13,8 +13,9 @@ use Illuminate\Database\Schema\Blueprint;
return Migration::createTable(
'dialog_messages',
function (Blueprint $table) {
$table->bigIncrements('id');
$table->foreignId('dialog_id')->constrained()->cascadeOnDelete();
$table->increments('id');
$table->unsignedInteger('dialog_id');
$table->foreign('dialog_id')->references('id')->on('dialogs')->cascadeOnDelete();
$table->unsignedInteger('user_id')->nullable();
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
$table->text('content');

View File

@ -14,10 +14,11 @@ return Migration::createTable(
'dialog_user',
function (Blueprint $table) {
$table->id();
$table->foreignId('dialog_id')->constrained()->cascadeOnDelete();
$table->unsignedInteger('dialog_id');
$table->foreign('dialog_id')->references('id')->on('dialogs')->cascadeOnDelete();
$table->unsignedInteger('user_id');
$table->dateTime('joined_at');
$table->unsignedBigInteger('last_read_message_id')->default(0);
$table->unsignedInteger('last_read_message_id')->default(0);
$table->dateTime('last_read_at')->nullable();
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
}

View File

@ -0,0 +1,24 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
return Migration::createTable(
'dialog_message_mentions_user',
function (Blueprint $table) {
$table->unsignedInteger('dialog_message_id');
$table->unsignedInteger('mentions_user_id');
$table->dateTime('created_at')->nullable()->useCurrent();
$table->primary(['dialog_message_id', 'mentions_user_id']);
$table->foreign('dialog_message_id')->references('id')->on('dialog_messages')->cascadeOnDelete();
$table->foreign('mentions_user_id')->references('id')->on('users')->cascadeOnDelete();
}
);

View File

@ -0,0 +1,24 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
return Migration::createTable(
'dialog_message_mentions_post',
function (Blueprint $table) {
$table->unsignedInteger('dialog_message_id');
$table->unsignedInteger('mentions_post_id');
$table->dateTime('created_at')->nullable()->useCurrent();
$table->primary(['dialog_message_id', 'mentions_post_id']);
$table->foreign('dialog_message_id')->references('id')->on('dialog_messages')->cascadeOnDelete();
$table->foreign('mentions_post_id')->references('id')->on('posts')->cascadeOnDelete();
}
);

View File

@ -0,0 +1,24 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
return Migration::createTable(
'dialog_message_mentions_group',
function (Blueprint $table) {
$table->unsignedInteger('dialog_message_id');
$table->unsignedInteger('mentions_group_id');
$table->dateTime('created_at')->nullable()->useCurrent();
$table->primary(['dialog_message_id', 'mentions_group_id']);
$table->foreign('dialog_message_id')->references('id')->on('dialog_messages')->cascadeOnDelete();
$table->foreign('mentions_group_id')->references('id')->on('groups')->cascadeOnDelete();
}
);

View File

@ -0,0 +1,24 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
return Migration::createTable(
'dialog_message_mentions_tag',
function (Blueprint $table) {
$table->unsignedInteger('dialog_message_id');
$table->unsignedInteger('mentions_tag_id');
$table->dateTime('created_at')->nullable()->useCurrent();
$table->primary(['dialog_message_id', 'mentions_tag_id']);
$table->foreign('dialog_message_id')->references('id')->on('dialog_messages')->cascadeOnDelete();
$table->foreign('mentions_tag_id')->references('id')->on('tags')->cascadeOnDelete();
}
);

View File

@ -16,6 +16,7 @@ use Flarum\Api\Resource;
use Flarum\Api\Schema;
use Flarum\Api\Sort\SortColumn;
use Flarum\Bus\Dispatcher;
use Flarum\Extension\ExtensionManager;
use Flarum\Foundation\ErrorHandling\LogReporter;
use Flarum\Foundation\ValidationException;
use Flarum\Locale\Translator;
@ -36,6 +37,7 @@ class DialogMessageResource extends Resource\AbstractDatabaseResource
protected Translator $translator,
protected LogReporter $log,
protected Dispatcher $bus,
protected ExtensionManager $extensions,
) {
}
@ -77,6 +79,20 @@ class DialogMessageResource extends Resource\AbstractDatabaseResource
}),
Endpoint\Index::make()
->authenticated()
->defaultInclude([
'user',
'mentionsUsers',
'mentionsPosts',
'mentionsGroups',
'mentionsTags',
])
->eagerLoad(function () {
if ($this->extensions->isEnabled('flarum-mentions')) {
return ['mentionsUsers', 'mentionsPosts', 'mentionsGroups', 'mentionsTags'];
}
return [];
})
->paginate(),
];
}
@ -126,6 +142,18 @@ class DialogMessageResource extends Resource\AbstractDatabaseResource
->includable()
->writableOnCreate()
->requiredOnCreateWithout(['attributes.users']),
Schema\Relationship\ToMany::make('mentionsUsers')
->type('users')
->includable(),
Schema\Relationship\ToMany::make('mentionsPosts')
->type('posts')
->includable(),
Schema\Relationship\ToMany::make('mentionsGroups')
->type('groups')
->includable(),
Schema\Relationship\ToMany::make('mentionsTags')
->type('tags')
->includable(),
];
}

View File

@ -10,12 +10,17 @@
namespace Flarum\Messages;
use Flarum\Database\AbstractModel;
use Flarum\Database\Eloquent\Collection;
use Flarum\Database\ScopeVisibilityTrait;
use Flarum\Formatter\Formattable;
use Flarum\Formatter\HasFormattedContent;
use Flarum\Foundation\EventGeneratorTrait;
use Flarum\Group\Group;
use Flarum\Post\Post;
use Flarum\Tags\Tag;
use Flarum\User\User;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
/**
* @property int $id
@ -26,6 +31,10 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property \Carbon\Carbon $updated_at
* @property-read Dialog $dialog
* @property-read User|null $user
* @property-read Collection<int, User> $mentionsUsers
* @property-read Collection<int, Post> $mentionsPosts
* @property-read Collection<int, Group> $mentionsGroups
* @property-read Collection<int, Tag> $mentionsTags
*/
class DialogMessage extends AbstractModel implements Formattable
{
@ -48,4 +57,24 @@ class DialogMessage extends AbstractModel implements Formattable
{
return $this->belongsTo(User::class);
}
public function mentionsUsers(): BelongsToMany
{
return $this->belongsToMany(User::class, 'dialog_message_mentions_user', 'dialog_message_id', 'mentions_user_id');
}
public function mentionsPosts(): BelongsToMany
{
return $this->belongsToMany(Post::class, 'dialog_message_mentions_post', 'dialog_message_id', 'mentions_post_id');
}
public function mentionsGroups(): BelongsToMany
{
return $this->belongsToMany(Group::class, 'dialog_message_mentions_group', 'dialog_message_id', 'mentions_group_id');
}
public function mentionsTags(): BelongsToMany
{
return $this->belongsToMany(Tag::class, 'dialog_message_mentions_tag', 'dialog_message_id', 'mentions_tag_id');
}
}

View File

@ -14,8 +14,8 @@ use Flarum\Messages\DialogMessage;
class Creating
{
public function __construct(
protected DialogMessage $message,
protected array $data
public DialogMessage $message,
public array $data
) {
}
}

View File

@ -14,7 +14,7 @@ use Flarum\Messages\DialogMessage;
class Updated
{
public function __construct(
protected DialogMessage $message
public DialogMessage $message
) {
}
}

View File

@ -14,8 +14,8 @@ use Flarum\Messages\DialogMessage;
class Updating
{
public function __construct(
protected DialogMessage $message,
protected array $data
public DialogMessage $message,
public array $data
) {
}
}

View File

@ -0,0 +1,77 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Messages\Listener;
use Flarum\Extension\ExtensionManager;
use Flarum\Messages\DialogMessage;
use s9e\TextFormatter\Utils;
class UpdateMentionsMetadataWhenVisible
{
public function __construct(
protected ExtensionManager $extensions
) {
}
public function handle(DialogMessage\Event\Created|DialogMessage\Event\Updated $event): void
{
if (! $event->message instanceof DialogMessage) {
return;
}
$content = $event->message->parsed_content;
$this->syncUserMentions(
$event->message,
Utils::getAttributeValues($content, 'USERMENTION', 'id')
);
$this->syncPostMentions(
$event->message,
Utils::getAttributeValues($content, 'POSTMENTION', 'id')
);
$this->syncGroupMentions(
$event->message,
Utils::getAttributeValues($content, 'GROUPMENTION', 'id')
);
if ($this->extensions->isEnabled('flarum-tags')) {
$this->syncTagMentions(
$event->message,
Utils::getAttributeValues($content, 'TAGMENTION', 'id')
);
}
}
protected function syncUserMentions(DialogMessage $message, array $mentioned): void
{
$message->mentionsUsers()->sync($mentioned);
$message->unsetRelation('mentionsUsers');
}
protected function syncPostMentions(DialogMessage $message, array $mentioned): void
{
$message->mentionsPosts()->sync($mentioned);
$message->unsetRelation('mentionsPosts');
}
protected function syncGroupMentions(DialogMessage $message, array $mentioned): void
{
$message->mentionsGroups()->sync($mentioned);
$message->unsetRelation('mentionsGroups');
}
protected function syncTagMentions(DialogMessage $message, array $mentioned): void
{
$message->mentionsTags()->sync($mentioned);
$message->unsetRelation('mentionsTags');
}
}