Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf(likes): limit likes relationship results #3781

Merged
merged 18 commits into from
Apr 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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