('canMentionGroups'),
diff --git a/extensions/mentions/js/src/forum/state/MentionedByModalState.ts b/extensions/mentions/js/src/forum/state/MentionedByModalState.ts
new file mode 100644
index 000000000..b2cdad8ca
--- /dev/null
+++ b/extensions/mentions/js/src/forum/state/MentionedByModalState.ts
@@ -0,0 +1,27 @@
+import PaginatedListState, { PaginatedListParams } from 'flarum/common/states/PaginatedListState';
+import Post from 'flarum/common/models/Post';
+
+export interface MentionedByModalParams extends PaginatedListParams {
+ filter: {
+ mentionedPost: string;
+ };
+ sort?: string;
+ page?: {
+ offset?: number;
+ limit: number;
+ };
+}
+
+export default class MentionedByModalState extends PaginatedListState {
+ constructor(params: P, page: number = 1) {
+ const limit = 10;
+
+ params.page = { ...(params.page || {}), limit };
+
+ super(params, page, limit);
+ }
+
+ get type(): string {
+ return 'posts';
+ }
+}
diff --git a/extensions/mentions/locale/en.yml b/extensions/mentions/locale/en.yml
index 0a1ea3fba..108af9379 100644
--- a/extensions/mentions/locale/en.yml
+++ b/extensions/mentions/locale/en.yml
@@ -25,6 +25,11 @@ flarum-mentions:
mention_tooltip: Mention a user, group or post
reply_to_post_text: "Reply to #{number}"
+ # These translations are used by the mentioned by modal dialog.
+ mentioned_by:
+ title: Replies to this post
+ load_more_button: => core.ref.load_more
+
# These translations are used by the Notifications dropdown, a.k.a. "the bell".
notifications:
others_text: => core.ref.some_others
@@ -34,6 +39,7 @@ flarum-mentions:
# These translations are displayed beneath individual posts.
post:
+ mentioned_by_more_text: "{count} more replies."
mentioned_by_self_text: "{users} replied to this." # Can be pluralized to agree with the number of users!
mentioned_by_text: "{users} replied to this." # Can be pluralized to agree with the number of users!
others_text: => core.ref.some_others
diff --git a/extensions/mentions/src/Api/LoadMentionedByRelationship.php b/extensions/mentions/src/Api/LoadMentionedByRelationship.php
new file mode 100644
index 000000000..e6ba9f4fb
--- /dev/null
+++ b/extensions/mentions/src/Api/LoadMentionedByRelationship.php
@@ -0,0 +1,68 @@
+with(['mentionsPosts', 'mentionsPosts.user', 'mentionsUsers'])
+ ->whereVisibleTo($actor)
+ ->oldest()
+ // Limiting a relationship results is only possible because
+ // the Post model uses the \Staudenmeir\EloquentEagerLimit\HasEagerLimit
+ // trait.
+ ->limit(self::$maxMentionedBy);
+ }
+
+ /**
+ * Called using the @see ApiController::prepareDataForSerialization extender.
+ */
+ public static function countRelation($controller, $data, ServerRequestInterface $request): void
+ {
+ $actor = RequestUtil::getActor($request);
+ $loadable = null;
+
+ if ($data instanceof Discussion) {
+ // @phpstan-ignore-next-line
+ $loadable = $data->newCollection($data->posts)->filter(function ($post) {
+ return $post instanceof Post;
+ });
+ } elseif ($data instanceof Collection) {
+ $loadable = $data;
+ } elseif ($data instanceof Post) {
+ $loadable = $data->newCollection([$data]);
+ }
+
+ if ($loadable) {
+ $loadable->loadCount([
+ 'mentionedBy' => function ($query) use ($actor) {
+ return $query->whereVisibleTo($actor);
+ }
+ ]);
+ }
+ }
+}
diff --git a/extensions/mentions/src/Filter/MentionedPostFilter.php b/extensions/mentions/src/Filter/MentionedPostFilter.php
new file mode 100644
index 000000000..e9b32945d
--- /dev/null
+++ b/extensions/mentions/src/Filter/MentionedPostFilter.php
@@ -0,0 +1,31 @@
+getQuery()
+ ->join('post_mentions_post', 'posts.id', '=', 'post_mentions_post.post_id')
+ ->where('post_mentions_post.mentions_post_id', $negate ? '!=' : '=', $mentionedId);
+ }
+}
diff --git a/extensions/mentions/src/FilterVisiblePosts.php b/extensions/mentions/src/FilterVisiblePosts.php
deleted file mode 100755
index 47aec39d2..000000000
--- a/extensions/mentions/src/FilterVisiblePosts.php
+++ /dev/null
@@ -1,100 +0,0 @@
-posts = $posts;
- }
-
- /**
- * Apply visibility permissions to API data.
- *
- * Each post in an API document has a relationship with posts that have
- * mentioned it (mentionedBy). This listener will manually filter these
- * additional posts so that the user can't see any posts which they don't
- * have access to.
- *
- * @param Controller\AbstractSerializeController $controller
- * @param mixed $data
- */
- public function __invoke(Controller\AbstractSerializeController $controller, $data, ServerRequestInterface $request)
- {
- $relations = [];
-
- // Firstly we gather a list of posts contained within the API document.
- // This will vary according to the API endpoint that is being accessed.
- if ($controller instanceof Controller\ShowDiscussionController) {
- $posts = $data->posts;
- } elseif ($controller instanceof Controller\ShowPostController
- || $controller instanceof Controller\CreatePostController
- || $controller instanceof Controller\UpdatePostController) {
- $relations = [
- 'mentionsUsers', 'mentionsPosts', 'mentionsPosts.user', 'mentionedBy', 'mentionsGroups',
- 'mentionedBy.mentionsPosts', 'mentionedBy.mentionsPosts.user', 'mentionedBy.mentionsUsers', 'mentionedBy.mentionsGroups.group'
- ];
-
- $posts = [$data];
- } elseif ($controller instanceof Controller\ListPostsController) {
- $posts = $data;
- }
-
- if (isset($posts)) {
- $posts = new Collection($posts);
- $actor = RequestUtil::getActor($request);
-
- $posts = $posts->filter(function ($post) {
- return $post instanceof CommentPost;
- });
-
- // Load all of the users that these posts mention. This way the data
- // will be ready to go when we need to sub in current usernames
- // during the rendering process.
- $posts->loadMissing($relations);
-
- // Construct a list of the IDs of all of the posts that these posts
- // have been mentioned in. We can then filter this list of IDs to
- // weed out all of the ones which the user is not meant to see.
- $ids = [];
-
- foreach ($posts as $post) {
- $ids = array_merge($ids, $post->mentionedBy->pluck('id')->all());
- }
-
- $ids = $this->posts->filterVisibleIds($ids, $actor);
-
- // Finally, go back through each of the posts and filter out any
- // of the posts in the relationship data that we now know are
- // invisible to the user.
- foreach ($posts as $post) {
- $post->setRelation('mentionedBy', $post->mentionedBy->filter(function ($post) use ($ids) {
- return array_search($post->id, $ids) !== false;
- }));
- }
- }
- }
-}
diff --git a/extensions/mentions/tests/integration/api/GroupMentionsTest.php b/extensions/mentions/tests/integration/api/GroupMentionsTest.php
index a5dbe9897..f001ba2bf 100644
--- a/extensions/mentions/tests/integration/api/GroupMentionsTest.php
+++ b/extensions/mentions/tests/integration/api/GroupMentionsTest.php
@@ -80,9 +80,11 @@ class GroupMentionsTest extends TestCase
$this->request('GET', '/api/posts/4')
);
- $this->assertEquals(200, $response->getStatusCode());
+ $contents = $response->getBody()->getContents();
- $response = json_decode($response->getBody(), true);
+ $this->assertEquals(200, $response->getStatusCode(), $contents);
+
+ $response = json_decode($contents, true);
$this->assertStringContainsString('GroupMention', $response['data']['attributes']['contentHtml']);
$this->assertStringContainsString('#80349E', $response['data']['attributes']['contentHtml']);
diff --git a/extensions/mentions/tests/integration/api/ListPostsTest.php b/extensions/mentions/tests/integration/api/ListPostsTest.php
index b3fd0b551..ab0964648 100644
--- a/extensions/mentions/tests/integration/api/ListPostsTest.php
+++ b/extensions/mentions/tests/integration/api/ListPostsTest.php
@@ -7,9 +7,10 @@
* LICENSE file that was distributed with this source code.
*/
-namespace Flarum\Tests\integration\api\discussions;
+namespace Flarum\Mentions\Tests\integration\api\discussions;
use Carbon\Carbon;
+use Flarum\Mentions\Api\LoadMentionedByRelationship;
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
use Flarum\Testing\integration\TestCase;
use Illuminate\Support\Arr;
@@ -107,4 +108,152 @@ class ListPostsTest extends TestCase
$ids = Arr::pluck($data, 'id');
$this->assertEqualsCanonicalizing(['3', '2'], $ids, 'IDs do not match');
}
+
+ protected function prepareMentionedByData(): void
+ {
+ $this->prepareDatabase([
+ 'discussions' => [
+ ['id' => 100, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 101, 'comment_count' => 12],
+ ],
+ 'posts' => [
+ ['id' => 101, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => 'text
'],
+ ['id' => 102, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => 'text
'],
+ ['id' => 103, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => 'text
', 'is_private' => 1],
+ ['id' => 104, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => 'text
'],
+ ['id' => 105, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => 'text
'],
+ ['id' => 106, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => 'text
'],
+ ['id' => 107, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => 'text
'],
+ ['id' => 108, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => 'text
'],
+ ['id' => 109, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => 'text
'],
+ ['id' => 110, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => 'text
'],
+ ['id' => 111, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => 'text
'],
+ ['id' => 112, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => 'text
'],
+ ],
+ 'post_mentions_post' => [
+ ['post_id' => 102, 'mentions_post_id' => 101],
+ ['post_id' => 103, 'mentions_post_id' => 101],
+ ['post_id' => 104, 'mentions_post_id' => 101],
+ ['post_id' => 105, 'mentions_post_id' => 101],
+ ['post_id' => 106, 'mentions_post_id' => 101],
+ ['post_id' => 107, 'mentions_post_id' => 101],
+ ['post_id' => 108, 'mentions_post_id' => 101],
+ ['post_id' => 109, 'mentions_post_id' => 101],
+ ['post_id' => 110, 'mentions_post_id' => 101],
+ ['post_id' => 111, 'mentions_post_id' => 101],
+ ['post_id' => 112, 'mentions_post_id' => 101],
+ ['post_id' => 103, 'mentions_post_id' => 112],
+ ],
+ ]);
+ }
+
+ /** @test */
+ public function mentioned_by_relation_returns_limited_results_and_shows_only_visible_posts_in_show_post_endpoint()
+ {
+ $this->prepareMentionedByData();
+
+ // List posts endpoint
+ $response = $this->send(
+ $this->request('GET', '/api/posts/101', [
+ 'authenticatedAs' => 2,
+ ])->withQueryParams([
+ 'include' => 'mentionedBy',
+ ])
+ );
+
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+
+ $this->assertEquals(200, $response->getStatusCode());
+
+ $mentionedBy = $data['relationships']['mentionedBy']['data'];
+
+ // Only displays a limited amount of mentioned by posts
+ $this->assertCount(LoadMentionedByRelationship::$maxMentionedBy, $mentionedBy);
+ // Of the limited amount of mentioned by posts, they must be visible to the actor
+ $this->assertEquals([102, 104, 105, 106], Arr::pluck($mentionedBy, 'id'));
+ }
+
+ /** @test */
+ public function mentioned_by_relation_returns_limited_results_and_shows_only_visible_posts_in_list_posts_endpoint()
+ {
+ $this->prepareMentionedByData();
+
+ // List posts endpoint
+ $response = $this->send(
+ $this->request('GET', '/api/posts', [
+ 'authenticatedAs' => 2,
+ ])->withQueryParams([
+ 'filter' => ['discussion' => 100],
+ 'include' => 'mentionedBy',
+ ])
+ );
+
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+
+ $this->assertEquals(200, $response->getStatusCode());
+
+ $mentionedBy = $data[0]['relationships']['mentionedBy']['data'];
+
+ // Only displays a limited amount of mentioned by posts
+ $this->assertCount(LoadMentionedByRelationship::$maxMentionedBy, $mentionedBy);
+ // Of the limited amount of mentioned by posts, they must be visible to the actor
+ $this->assertEquals([102, 104, 105, 106], Arr::pluck($mentionedBy, 'id'));
+ }
+
+ /**
+ * @dataProvider mentionedByIncludeProvider
+ * @test
+ */
+ public function mentioned_by_relation_returns_limited_results_and_shows_only_visible_posts_in_show_discussion_endpoint(string $include)
+ {
+ $this->prepareMentionedByData();
+
+ // Show discussion endpoint
+ $response = $this->send(
+ $this->request('GET', '/api/discussions/100', [
+ 'authenticatedAs' => 2,
+ ])->withQueryParams([
+ 'include' => $include,
+ ])
+ );
+
+ $included = json_decode($response->getBody()->getContents(), true)['included'];
+
+ $mentionedBy = collect($included)
+ ->where('type', 'posts')
+ ->where('id', 101)
+ ->first()['relationships']['mentionedBy']['data'];
+
+ // Only displays a limited amount of mentioned by posts
+ $this->assertCount(LoadMentionedByRelationship::$maxMentionedBy, $mentionedBy);
+ // Of the limited amount of mentioned by posts, they must be visible to the actor
+ $this->assertEquals([102, 104, 105, 106], Arr::pluck($mentionedBy, 'id'));
+ }
+
+ public function mentionedByIncludeProvider(): array
+ {
+ return [
+ ['posts,posts.mentionedBy'],
+ ['posts.mentionedBy'],
+ [''],
+ ];
+ }
+
+ /** @test */
+ public function mentioned_by_count_only_includes_visible_posts_to_actor()
+ {
+ $this->prepareMentionedByData();
+
+ // List posts endpoint
+ $response = $this->send(
+ $this->request('GET', '/api/posts/112', [
+ 'authenticatedAs' => 2,
+ ])
+ );
+
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+
+ $this->assertEquals(200, $response->getStatusCode());
+
+ $this->assertEquals(0, $data['attributes']['mentionedByCount']);
+ }
}
diff --git a/framework/core/composer.json b/framework/core/composer.json
index 05684fa49..bb1ebccc6 100644
--- a/framework/core/composer.json
+++ b/framework/core/composer.json
@@ -76,6 +76,7 @@
"psr/http-server-handler": "^1.0",
"psr/http-server-middleware": "^1.0",
"s9e/text-formatter": "^2.3.6",
+ "staudenmeir/eloquent-eager-limit": "^1.0",
"sycho/json-api": "^0.5.0",
"sycho/sourcemap": "^2.0.0",
"symfony/config": "^5.2.2",
diff --git a/framework/core/less/common/Dropdown.less b/framework/core/less/common/Dropdown.less
index bba41342b..da3163fa8 100644
--- a/framework/core/less/common/Dropdown.less
+++ b/framework/core/less/common/Dropdown.less
@@ -79,6 +79,11 @@
left: auto;
right: 0;
}
+.Dropdown-menu--inline {
+ position: relative;
+ display: block;
+ box-shadow: none;
+}
.Dropdown-header {
padding: 10px 15px;
color: var(--heading-color);
diff --git a/framework/core/less/forum/Post.less b/framework/core/less/forum/Post.less
index 22b34c3a8..b2516c6d2 100644
--- a/framework/core/less/forum/Post.less
+++ b/framework/core/less/forum/Post.less
@@ -353,11 +353,14 @@
word-wrap: break-word;
}
- .Avatar {
+ &-badge, .Avatar {
float: left;
margin-left: -50px;
.Avatar--size(32px);
}
+ &-badge {
+ color: var(--control-color);
+ }
.username {
color: var(--text-color);
font-weight: bold;
diff --git a/framework/core/src/Api/Controller/ShowDiscussionController.php b/framework/core/src/Api/Controller/ShowDiscussionController.php
index 33f7a7cb5..251254675 100644
--- a/framework/core/src/Api/Controller/ShowDiscussionController.php
+++ b/framework/core/src/Api/Controller/ShowDiscussionController.php
@@ -94,7 +94,8 @@ class ShowDiscussionController extends AbstractShowController
$discussion = $this->discussions->findOrFail($discussionId, $actor);
}
- if (in_array('posts', $include)) {
+ // If posts is included or a sub relation of post is included.
+ if (in_array('posts', $include) || Str::contains(implode(',', $include), 'posts.')) {
$postRelationships = $this->getPostRelationships($include);
$this->includePosts($discussion, $request, $postRelationships);
@@ -119,7 +120,7 @@ class ShowDiscussionController extends AbstractShowController
$offset = $this->getPostsOffset($request, $discussion, $limit);
$allPosts = $this->loadPostIds($discussion, $actor);
- $loadedPosts = $this->loadPosts($discussion, $actor, $offset, $limit, $include);
+ $loadedPosts = $this->loadPosts($discussion, $actor, $offset, $limit, $include, $request);
array_splice($allPosts, $offset, $limit, $loadedPosts);
@@ -136,11 +137,7 @@ class ShowDiscussionController extends AbstractShowController
return $discussion->posts()->whereVisibleTo($actor)->orderBy('number')->pluck('id')->all();
}
- /**
- * @param array $include
- * @return array
- */
- private function getPostRelationships(array $include)
+ private function getPostRelationships(array $include): array
{
$prefixLength = strlen($prefix = 'posts.');
$relationships = [];
@@ -183,11 +180,11 @@ class ShowDiscussionController extends AbstractShowController
* @param array $include
* @return mixed
*/
- private function loadPosts($discussion, $actor, $offset, $limit, array $include)
+ private function loadPosts($discussion, $actor, $offset, $limit, array $include, ServerRequestInterface $request)
{
$query = $discussion->posts()->whereVisibleTo($actor);
- $query->orderBy('number')->skip($offset)->take($limit)->with($include);
+ $query->orderBy('number')->skip($offset)->take($limit);
$posts = $query->get();
@@ -195,7 +192,7 @@ class ShowDiscussionController extends AbstractShowController
$post->discussion = $discussion;
}
- $this->loadRelations($posts, $include);
+ $this->loadRelations($posts, $include, $request);
return $posts->all();
}
@@ -221,8 +218,13 @@ class ShowDiscussionController extends AbstractShowController
$postCallableRelationships = $this->getPostRelationships(array_keys($addedCallableRelations));
- return array_intersect_key($addedCallableRelations, array_flip(array_map(function ($relation) {
+ $relationCallables = array_intersect_key($addedCallableRelations, array_flip(array_map(function ($relation) {
return "posts.$relation";
}, $postCallableRelationships)));
+
+ // remove posts. prefix from keys
+ return array_combine(array_map(function ($relation) {
+ return substr($relation, 6);
+ }, array_keys($relationCallables)), array_values($relationCallables));
}
}
diff --git a/framework/core/src/Api/Controller/ShowPostController.php b/framework/core/src/Api/Controller/ShowPostController.php
index 2762c1ee9..43d239b6b 100644
--- a/framework/core/src/Api/Controller/ShowPostController.php
+++ b/framework/core/src/Api/Controller/ShowPostController.php
@@ -12,6 +12,7 @@ namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\PostSerializer;
use Flarum\Http\RequestUtil;
use Flarum\Post\PostRepository;
+use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
@@ -52,6 +53,12 @@ class ShowPostController extends AbstractShowController
*/
protected function data(ServerRequestInterface $request, Document $document)
{
- return $this->posts->findOrFail(Arr::get($request->getQueryParams(), 'id'), RequestUtil::getActor($request));
+ $post = $this->posts->findOrFail(Arr::get($request->getQueryParams(), 'id'), RequestUtil::getActor($request));
+
+ $include = $this->extractInclude($request);
+
+ $this->loadRelations(new Collection([$post]), $include, $request);
+
+ return $post;
}
}
diff --git a/framework/core/src/Database/AbstractModel.php b/framework/core/src/Database/AbstractModel.php
index 86e6ef995..9805ecf0d 100644
--- a/framework/core/src/Database/AbstractModel.php
+++ b/framework/core/src/Database/AbstractModel.php
@@ -9,9 +9,11 @@
namespace Flarum\Database;
+use Flarum\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model as Eloquent;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Arr;
+use Illuminate\Support\Str;
use LogicException;
/**
@@ -61,6 +63,14 @@ abstract class AbstractModel extends Eloquent
*/
public static $defaults = [];
+ /**
+ * An alias for the table name, used in queries.
+ *
+ * @var string|null
+ * @internal
+ */
+ protected $tableAlias = null;
+
/**
* {@inheritdoc}
*/
@@ -213,4 +223,41 @@ abstract class AbstractModel extends Eloquent
return parent::__call($method, $arguments);
}
+
+ public function newModelQuery()
+ {
+ $query = parent::newModelQuery();
+
+ if ($this->tableAlias) {
+ $query->from($this->getTable().' as '.$this->tableAlias);
+ }
+
+ return $query;
+ }
+
+ public function qualifyColumn($column)
+ {
+ if (Str::contains($column, '.')) {
+ return $column;
+ }
+
+ return ($this->tableAlias ?? $this->getTable()).'.'.$column;
+ }
+
+ public function withTableAlias(callable $callback)
+ {
+ static $aliasCount = 0;
+ $this->tableAlias = 'flarum_reserved_'.++$aliasCount;
+
+ $result = $callback();
+
+ $this->tableAlias = null;
+
+ return $result;
+ }
+
+ public function newCollection(array $models = [])
+ {
+ return new Collection($models);
+ }
}
diff --git a/framework/core/src/Database/Eloquent/Collection.php b/framework/core/src/Database/Eloquent/Collection.php
new file mode 100644
index 000000000..fbdd67de7
--- /dev/null
+++ b/framework/core/src/Database/Eloquent/Collection.php
@@ -0,0 +1,50 @@
+isEmpty()) {
+ return $this;
+ }
+
+ return $this->first()->withTableAlias(function () use ($relations, $column, $function) {
+ return parent::loadAggregate($relations, $column, $function);
+ });
+ }
+}
diff --git a/framework/core/src/Extend/ApiController.php b/framework/core/src/Extend/ApiController.php
index 78cf72c6f..b87b84602 100644
--- a/framework/core/src/Extend/ApiController.php
+++ b/framework/core/src/Extend/ApiController.php
@@ -313,7 +313,7 @@ class ApiController implements ExtenderInterface
* Allows loading a relationship with additional query modification.
*
* @param string $relation: Relationship name, see load method description.
- * @param callable(\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Relations\Relation, \Psr\Http\Message\ServerRequestInterface|null, array): void $callback
+ * @param array|(callable(\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Relations\Relation, \Psr\Http\Message\ServerRequestInterface|null, array): void) $callback
*
* The callback to modify the query, should accept:
* - \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Relations\Relation $query: A query object.
@@ -322,7 +322,7 @@ class ApiController implements ExtenderInterface
*
* @return self
*/
- public function loadWhere(string $relation, callable $callback): self
+ public function loadWhere(string $relation, callable $callback): self // @phpstan-ignore-line
{
$this->loadCallables = array_merge($this->loadCallables, [$relation => $callback]);
diff --git a/framework/core/src/Post/Post.php b/framework/core/src/Post/Post.php
index bdabe071e..fa86b8db1 100644
--- a/framework/core/src/Post/Post.php
+++ b/framework/core/src/Post/Post.php
@@ -18,6 +18,7 @@ use Flarum\Post\Event\Deleted;
use Flarum\User\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\Expression;
+use Staudenmeir\EloquentEagerLimit\HasEagerLimit;
/**
* @property int $id
@@ -42,6 +43,7 @@ class Post extends AbstractModel
{
use EventGeneratorTrait;
use ScopeVisibilityTrait;
+ use HasEagerLimit;
protected $table = 'posts';