Skip to content

Commit

Permalink
perf(likes): limit likes relationship results (#3781)
Browse files Browse the repository at this point in the history
* perf(core,mentions): limit `mentionedBy` post relation results

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* Apply fixes from StyleCI

* chore: use a static property to allow customization

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* chore: use a static property to allow customization

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* chore: include count in show post endpoint

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* chore: consistent locale key format

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* chore: forgot to delete `FilterVisiblePosts`

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* test: `mentionedByCount` must not include invisible posts to actor

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* fix: visibility scoping on `mentionedByCount`

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* fix: `loadAggregates` conflicts with visibility scopers

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* Apply fixes from StyleCI

* chore: phpstan

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* perf(likes): limit `likes` relationship results

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* Apply fixes from StyleCI

* chore: simplify

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* test: `likesCount` is as expected

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* Apply fixes from StyleCI

---------

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>
Co-authored-by: StyleCI Bot <bot@styleci.io>
Co-authored-by: IanM <16573496+imorland@users.noreply.github.com>
  • Loading branch information
3 people committed Apr 19, 2023
1 parent 6b8e9ce commit d0669b0
Show file tree
Hide file tree
Showing 15 changed files with 468 additions and 52 deletions.
29 changes: 24 additions & 5 deletions extensions/likes/extend.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@
use Flarum\Api\Serializer\BasicUserSerializer;
use Flarum\Api\Serializer\PostSerializer;
use Flarum\Extend;
use Flarum\Likes\Api\LoadLikesRelationship;
use Flarum\Likes\Event\PostWasLiked;
use Flarum\Likes\Event\PostWasUnliked;
use Flarum\Likes\Notification\PostLikedBlueprint;
use Flarum\Likes\Query\LikedByFilter;
use Flarum\Likes\Query\LikedFilter;
use Flarum\Post\Filter\PostFilterer;
use Flarum\Post\Post;
use Flarum\User\Filter\UserFilterer;
use Flarum\User\User;

return [
Expand All @@ -41,19 +44,32 @@
->hasMany('likes', BasicUserSerializer::class)
->attribute('canLike', function (PostSerializer $serializer, $model) {
return (bool) $serializer->getActor()->can('like', $model);
})
->attribute('likesCount', function (PostSerializer $serializer, $model) {
return $model->getAttribute('likes_count') ?: 0;
}),

(new Extend\ApiController(Controller\ShowDiscussionController::class))
->addInclude('posts.likes'),
->addInclude('posts.likes')
->loadWhere('posts.likes', [LoadLikesRelationship::class, 'mutateRelation'])
->prepareDataForSerialization([LoadLikesRelationship::class, 'countRelation']),

(new Extend\ApiController(Controller\ListPostsController::class))
->addInclude('likes'),
->addInclude('likes')
->loadWhere('likes', [LoadLikesRelationship::class, 'mutateRelation'])
->prepareDataForSerialization([LoadLikesRelationship::class, 'countRelation']),
(new Extend\ApiController(Controller\ShowPostController::class))
->addInclude('likes'),
->addInclude('likes')
->loadWhere('likes', [LoadLikesRelationship::class, 'mutateRelation'])
->prepareDataForSerialization([LoadLikesRelationship::class, 'countRelation']),
(new Extend\ApiController(Controller\CreatePostController::class))
->addInclude('likes'),
->addInclude('likes')
->loadWhere('likes', [LoadLikesRelationship::class, 'mutateRelation'])
->prepareDataForSerialization([LoadLikesRelationship::class, 'countRelation']),
(new Extend\ApiController(Controller\UpdatePostController::class))
->addInclude('likes'),
->addInclude('likes')
->loadWhere('likes', [LoadLikesRelationship::class, 'mutateRelation'])
->prepareDataForSerialization([LoadLikesRelationship::class, 'countRelation']),

(new Extend\Event())
->listen(PostWasLiked::class, Listener\SendNotificationWhenPostIsLiked::class)
Expand All @@ -63,6 +79,9 @@
(new Extend\Filter(PostFilterer::class))
->addFilter(LikedByFilter::class),

(new Extend\Filter(UserFilterer::class))
->addFilter(LikedFilter::class),

(new Extend\Settings())
->default('flarum-likes.like_own_post', true),

Expand Down
9 changes: 9 additions & 0 deletions extensions/likes/js/src/@types/shims.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Post from 'flarum/common/models/Post';
import User from 'flarum/common/models/User';

declare module 'flarum/common/models/Post' {
export default interface Post {
likes(): User[];
likesCount(): number;
}
}
32 changes: 19 additions & 13 deletions extensions/likes/js/src/forum/addLikesList.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import username from 'flarum/common/helpers/username';
import icon from 'flarum/common/helpers/icon';

import PostLikesModal from './components/PostLikesModal';
import Button from '@flarum/core/src/common/components/Button';

export default function () {
extend(CommentPost.prototype, 'footerItems', function (items) {
Expand All @@ -15,7 +16,7 @@ export default function () {

if (likes && likes.length) {
const limit = 4;
const overLimit = likes.length > limit;
const overLimit = post.likesCount() > limit;

// Construct a list of names of users who have liked this post. Make sure the
// current user is first in the list, and cap a maximum of 4 items.
Expand All @@ -34,19 +35,24 @@ export default function () {
// others" name to the end of the list. Clicking on it will display a modal
// with a full list of names.
if (overLimit) {
const count = likes.length - names.length;
const count = post.likesCount() - names.length;
const label = app.translator.trans('flarum-likes.forum.post.others_link', { count });

names.push(
<a
href="#"
onclick={(e) => {
e.preventDefault();
app.modal.show(PostLikesModal, { post });
}}
>
{app.translator.trans('flarum-likes.forum.post.others_link', { count })}
</a>
);
if (app.forum.attribute('canSearchUsers')) {
names.push(
<Button
className="Button Button--ua-reset Button--text"
onclick={(e) => {
e.preventDefault();
app.modal.show(PostLikesModal, { post });
}}
>
{label}
</Button>
);
} else {
names.push(<span>{label}</span>);
}
}

items.add(
Expand Down
31 changes: 0 additions & 31 deletions extensions/likes/js/src/forum/components/PostLikesModal.js

This file was deleted.

72 changes: 72 additions & 0 deletions extensions/likes/js/src/forum/components/PostLikesModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import app from 'flarum/forum/app';
import Modal from 'flarum/common/components/Modal';
import Link from 'flarum/common/components/Link';
import avatar from 'flarum/common/helpers/avatar';
import username from 'flarum/common/helpers/username';
import type { IInternalModalAttrs } from 'flarum/common/components/Modal';
import type Post from 'flarum/common/models/Post';
import type Mithril from 'mithril';
import PostLikesModalState from '../states/PostLikesModalState';
import Button from '@flarum/core/src/common/components/Button';
import LoadingIndicator from '@flarum/core/src/common/components/LoadingIndicator';

export interface IPostLikesModalAttrs extends IInternalModalAttrs {
post: Post;
}

export default class PostLikesModal<CustomAttrs extends IPostLikesModalAttrs = IPostLikesModalAttrs> extends Modal<CustomAttrs, PostLikesModalState> {
oninit(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
super.oninit(vnode);

this.state = new PostLikesModalState({
filter: {
liked: this.attrs.post.id()!,
},
});

this.state.refresh();
}

className() {
return 'PostLikesModal Modal--small';
}

title() {
return app.translator.trans('flarum-likes.forum.post_likes.title');
}

content() {
return (
<>
<div className="Modal-body">
{this.state.isInitialLoading() ? (
<LoadingIndicator />
) : (
<ul className="PostLikesModal-list">
{this.state.getPages().map((page) =>
page.items.map((user) => (
<li>
<Link href={app.route.user(user)}>
{avatar(user)} {username(user)}
</Link>
</li>
))
)}
</ul>
)}
</div>
{this.state.hasNext() ? (
<div className="Modal-footer">
<div className="Form Form--centered">
<div className="Form-group">
<Button className="Button Button--block" onclick={() => this.state.loadNext()} loading={this.state.isLoadingNext()}>
{app.translator.trans('flarum-likes.forum.post_likes.load_more_button')}
</Button>
</div>
</div>
</div>
) : null}
</>
);
}
}
1 change: 1 addition & 0 deletions extensions/likes/js/src/forum/extend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ export default [

new Extend.Model(Post) //
.hasMany<User>('likes')
.attribute<number>('likesCount')
.attribute<boolean>('canLike'),
];
26 changes: 26 additions & 0 deletions extensions/likes/js/src/forum/states/PostLikesModalState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import PaginatedListState, { PaginatedListParams } from '@flarum/core/src/common/states/PaginatedListState';
import User from 'flarum/common/models/User';

export interface PostLikesModalListParams extends PaginatedListParams {
filter: {
liked: string;
};
page?: {
offset?: number;
limit: number;
};
}

export default class PostLikesModalState<P extends PostLikesModalListParams = PostLikesModalListParams> extends PaginatedListState<User, P> {
constructor(params: P, page: number = 1) {
const limit = 10;

params.page = { ...(params.page || {}), limit };

super(params, page, limit);
}

get type(): string {
return 'users';
}
}
1 change: 1 addition & 0 deletions extensions/likes/locale/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ flarum-likes:
# These translations are used by the Users Who Like This modal dialog.
post_likes:
title: Users Who Like This
load_more_button: => core.ref.load_more

# These translations are used in the Settings page.
settings:
Expand Down
61 changes: 61 additions & 0 deletions extensions/likes/src/Api/LoadLikesRelationship.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?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\Likes\Api;

use Flarum\Discussion\Discussion;
use Flarum\Http\RequestUtil;
use Flarum\Post\Post;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Query\Expression;
use Psr\Http\Message\ServerRequestInterface;

class LoadLikesRelationship
{
public static $maxLikes = 4;

public static function mutateRelation(BelongsToMany $query, ServerRequestInterface $request): BelongsToMany
{
$actor = RequestUtil::getActor($request);

$grammar = $query->getQuery()->getGrammar();

return $query
// So that we can tell if the current user has liked the post.
->orderBy(new Expression($grammar->wrap('user_id').' = '.$actor->id), 'desc')
// Limiting a relationship results is only possible because
// the Post model uses the \Staudenmeir\EloquentEagerLimit\HasEagerLimit
// trait.
->limit(self::$maxLikes);
}

/**
* Called using the @see ApiController::prepareDataForSerialization extender.
*/
public static function countRelation($controller, $data): void
{
$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('likes');
}
}
}
34 changes: 34 additions & 0 deletions extensions/likes/src/Query/LikedFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?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\Likes\Query;

use Flarum\Filter\FilterInterface;
use Flarum\Filter\FilterState;

class LikedFilter implements FilterInterface
{
public function getFilterKey(): string
{
return 'liked';
}

public function filter(FilterState $filterState, string $filterValue, bool $negate)
{
$likedId = trim($filterValue, '"');

$filterState
->getQuery()
->whereIn('id', function ($query) use ($likedId) {
$query->select('user_id')
->from('post_likes')
->where('post_id', $likedId);
}, 'and', $negate);
}
}
Loading

0 comments on commit d0669b0

Please sign in to comment.