Update FollowerService

This commit is contained in:
Daniel Supernault 2021-07-11 07:43:29 -06:00
parent ff48400a38
commit c8824d1b51
No known key found for this signature in database
GPG key ID: 0DEF1C662C9033F7
3 changed files with 172 additions and 120 deletions

View file

@ -573,9 +573,13 @@ class PublicApiController extends Controller
{ {
abort_unless(Auth::check(), 403); abort_unless(Auth::check(), 403);
$profile = Profile::with('user')->whereNull('status')->whereNull('domain')->findOrFail($id); $profile = Profile::with('user')->whereNull('status')->whereNull('domain')->findOrFail($id);
$owner = Auth::id() == $profile->user_id;
if(Auth::id() != $profile->user_id && $profile->is_private || !$profile->user->settings->show_profile_followers) { if(Auth::id() != $profile->user_id && $profile->is_private || !$profile->user->settings->show_profile_followers) {
return response()->json([]); return response()->json([]);
} }
if(!$owner && $request->page > 5) {
return [];
}
$followers = $profile->followers()->orderByDesc('followers.created_at')->paginate(10); $followers = $profile->followers()->orderByDesc('followers.created_at')->paginate(10);
$resource = new Fractal\Resource\Collection($followers, new AccountTransformer()); $resource = new Fractal\Resource\Collection($followers, new AccountTransformer());
$res = $this->fractal->createData($resource)->toArray(); $res = $this->fractal->createData($resource)->toArray();
@ -600,6 +604,10 @@ class PublicApiController extends Controller
abort_if($owner == false && $profile->is_private == true && !$profile->followedBy(Auth::user()->profile), 404); abort_if($owner == false && $profile->is_private == true && !$profile->followedBy(Auth::user()->profile), 404);
abort_if($profile->user->settings->show_profile_following == false && $owner == false, 404); abort_if($profile->user->settings->show_profile_following == false && $owner == false, 404);
if(!$owner && $request->page > 5) {
return [];
}
if($search) { if($search) {
abort_if(!$owner, 404); abort_if(!$owner, 404);
$following = $profile->following() $following = $profile->following()

View file

@ -6,66 +6,95 @@ use Illuminate\Support\Facades\Redis;
use App\{ use App\{
Follower, Follower,
Profile Profile,
User
}; };
class FollowerService { class FollowerService
{
const FOLLOWING_KEY = 'pf:services:follow:following:id:';
const FOLLOWERS_KEY = 'pf:services:follow:followers:id:';
protected $profile; public static function add($actor, $target)
public static $follower_prefix = 'px:profile:followers-v1.3:';
public static $following_prefix = 'px:profile:following-v1.3:';
public static function build()
{ {
return new self(); Redis::zadd(self::FOLLOWING_KEY . $actor, $target, $target);
Redis::zadd(self::FOLLOWERS_KEY . $target, $actor, $actor);
} }
public function profile(Profile $profile) public static function remove($actor, $target)
{ {
$this->profile = $profile; Redis::zrem(self::FOLLOWING_KEY . $actor, $target);
self::$follower_prefix .= $profile->id; Redis::zrem(self::FOLLOWERS_KEY . $target, $actor);
self::$following_prefix .= $profile->id;
return $this;
} }
public function followers($limit = 100, $offset = 1) public static function followers($id, $start = 0, $stop = 10)
{ {
if(Redis::zcard(self::$follower_prefix) == 0) { return Redis::zrange(self::FOLLOWERS_KEY . $id, $start, $stop);
$followers = $this->profile->followers()->pluck('profile_id');
$followers->map(function($i) {
Redis::zadd(self::$follower_prefix, $i, $i);
});
return Redis::zrevrange(self::$follower_prefix, $offset, $limit);
} else {
return Redis::zrevrange(self::$follower_prefix, $offset, $limit);
}
} }
public static function following($id, $start = 0, $stop = 10)
public function following($limit = 100, $offset = 1)
{ {
if(Redis::zcard(self::$following_prefix) == 0) { return Redis::zrange(self::FOLLOWING_KEY . $id, $start, $stop);
$following = $this->profile->following()->pluck('following_id');
$following->map(function($i) {
Redis::zadd(self::$following_prefix, $i, $i);
});
return Redis::zrevrange(self::$following_prefix, $offset, $limit);
} else {
return Redis::zrevrange(self::$following_prefix, $offset, $limit);
}
} }
public static function follows(string $actor, string $target) public static function follows(string $actor, string $target)
{ {
$key = self::$follower_prefix . $target; return Follower::whereProfileId($actor)->whereFollowingId($target)->exists();
if(Redis::zcard($key) == 0) { }
$p = Profile::findOrFail($target);
self::build()->profile($p)->followers(1); public static function audience($profile)
self::build()->profile($p)->following(1); {
return (bool) Redis::zrank($key, $actor); return (new self)->getAudienceInboxes($profile);
} else { }
return (bool) Redis::zrank($key, $actor);
protected function getAudienceInboxes($profile)
{
if($profile instanceOf User) {
return $profile
->profile
->followers()
->whereLocalProfile(false)
->get()
->map(function($follow) {
return $follow->sharedInbox ?? $follow->inbox_url;
})
->unique()
->values()
->toArray();
} }
if($profile instanceOf Profile) {
return $profile
->followers()
->whereLocalProfile(false)
->get()
->map(function($follow) {
return $follow->sharedInbox ?? $follow->inbox_url;
})
->unique()
->values()
->toArray();
}
if(is_string($profile) || is_integer($profile)) {
$profile = Profile::whereNull('domain')->find($profile);
if(!$profile) {
return [];
}
return $profile
->followers()
->whereLocalProfile(false)
->get()
->map(function($follow) {
return $follow->sharedInbox ?? $follow->inbox_url;
})
->unique()
->values()
->toArray();
}
return [];
} }
} }

View file

@ -85,10 +85,10 @@
<!-- DESKTOP PROFILE PICTURE --> <!-- DESKTOP PROFILE PICTURE -->
<div class="d-none d-md-block pb-3"> <div class="d-none d-md-block pb-3">
<div v-if="hasStory" class="has-story-lg cursor-pointer shadow-sm" @click="storyRedirect()"> <div v-if="hasStory" class="has-story-lg cursor-pointer shadow-sm" @click="storyRedirect()">
<img :alt="profileUsername + '\'s profile picture'" class="rounded-circle box-shadow cursor-pointer" :src="profile.avatar" width="150px" height="150px"> <img :alt="profileUsername + '\'s profile picture'" class="rounded-circle box-shadow cursor-pointer" :src="profile.avatar" width="150px" height="150px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
</div> </div>
<div v-else> <div v-else>
<img :alt="profileUsername + '\'s profile picture'" class="rounded-circle box-shadow" :src="profile.avatar" width="150px" height="150px"> <img :alt="profileUsername + '\'s profile picture'" class="rounded-circle box-shadow" :src="profile.avatar" width="150px" height="150px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
</div> </div>
<p v-if="sponsorList.patreon || sponsorList.liberapay || sponsorList.opencollective" class="text-center mt-3"> <p v-if="sponsorList.patreon || sponsorList.liberapay || sponsorList.opencollective" class="text-center mt-3">
<button type="button" @click="showSponsorModal" class="btn btn-outline-secondary font-weight-bold py-0"> <button type="button" @click="showSponsorModal" class="btn btn-outline-secondary font-weight-bold py-0">
@ -404,53 +404,54 @@
title="Following" title="Following"
body-class="list-group-flush py-3 px-0" body-class="list-group-flush py-3 px-0"
dialog-class="follow-modal"> dialog-class="follow-modal">
<div v-if="!loading" class="list-group" style="min-height: 60vh;"> <div v-if="!followingLoading" class="list-group" style="max-height: 60vh;">
<div v-if="owner == true" class="list-group-item border-0 pt-0 px-0 mt-n2 mb-3"> <div v-if="!following.length" class="list-group-item border-0">
<span class="d-flex px-4 pb-0 align-items-center"> <p class="text-center mb-0 font-weight-bold text-muted py-5">
<i class="fas fa-search text-lighter"></i> <span class="text-dark">{{profileUsername}}</span> is not following yet</p>
<input type="text" class="form-control border-0 shadow-0 no-focus" placeholder="Search Following..." v-model="followingModalSearch" v-on:keyup="followingModalSearchHandler">
</span>
</div> </div>
<div v-if="owner == true" class="btn-group rounded-0 mt-n3 mb-3 border-top" role="group" aria-label="Following"> <div v-else>
<!-- <button type="button" :class="[followingModalTab == 'following' ? ' btn btn-light py-3 rounded-0 font-weight-bold modal-tab-active' : 'btn btn-light py-3 rounded-0 font-weight-bold']" style="font-size: 12px;">FOLLOWING</button> --> <div v-if="owner == true" class="list-group-item border-0 pt-0 px-0 mt-n2 mb-3">
<!-- <button type="button" class="btn btn-light py-3 rounded-0 text-muted font-weight-bold" style="font-size: 12px;">MUTED</button> <span class="d-flex px-4 pb-0 align-items-center">
<button type="button" class="btn btn-light py-3 rounded-0 text-muted font-weight-bold" style="font-size: 12px;">BLOCKED</button> --> <i class="fas fa-search text-lighter"></i>
</div> <input type="text" class="form-control border-0 shadow-0 no-focus" placeholder="Search Following..." v-model="followingModalSearch" v-on:keyup="followingModalSearchHandler">
<div v-else class="btn-group rounded-0 mt-n3 mb-3" role="group" aria-label="Following"> </span>
<!-- <button type="button" class="btn btn-light py-3 rounded-0 border-primary border-left-0 border-right-0 border-top-0 font-weight-bold" style="font-size: 12px;" @click="followingModalTab = 'following'">FOLLOWING</button> </div>
<button type="button" class="btn btn-light py-3 rounded-0 text-muted font-weight-bold" style="font-size: 12px;" @click="followingModalTab = 'mutual'">MUTUAL</button> <div class="list-group-item border-0 py-1 mb-1" v-for="(user, index) in following" :key="'following_'+index">
<button type="button" class="btn btn-light py-3 rounded-0 text-muted font-weight-bold" style="font-size: 12px;" @click="followingModalTab = 'blocked'">BLOCKED</button> --> <div class="media">
</div> <a :href="profileUrlRedirect(user)">
<div class="list-group-item border-0 py-1" v-for="(user, index) in following" :key="'following_'+index"> <img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + 's avatar'" width="30px" loading="lazy" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0'">
<div class="media"> </a>
<a :href="profileUrlRedirect(user)"> <div class="media-body text-truncate">
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + 's avatar'" width="30px" loading="lazy" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0'"> <p class="mb-0" style="font-size: 14px">
</a> <a :href="profileUrlRedirect(user)" class="font-weight-bold text-dark">
<div class="media-body text-truncate"> {{user.username}}
<p class="mb-0" style="font-size: 14px"> </a>
<a :href="profileUrlRedirect(user)" class="font-weight-bold text-dark"> </p>
{{user.username}} <p v-if="!user.local" class="text-muted mb-0 text-break mr-3" style="font-size: 14px" :title="user.acct" data-toggle="dropdown" data-placement="bottom">
</a> <span class="font-weight-bold">{{user.acct.split('@')[0]}}</span><span class="text-lighter">&commat;{{user.acct.split('@')[1]}}</span>
</p> </p>
<p v-if="!user.local" class="text-muted mb-0 text-truncate mr-3" style="font-size: 14px" :title="user.acct" data-toggle="dropdown" data-placement="bottom"> <p v-else class="text-muted mb-0 text-truncate" style="font-size: 14px">
<span class="font-weight-bold">{{user.acct.split('@')[0]}}</span><span class="text-lighter">&commat;{{user.acct.split('@')[1]}}</span> {{user.display_name ? user.display_name : user.username}}
</p> </p>
<p v-else class="text-muted mb-0 text-truncate" style="font-size: 14px"> </div>
{{user.display_name}} <div v-if="owner">
</p> <a class="btn btn-outline-dark btn-sm font-weight-bold" href="#" @click.prevent="followModalAction(user.id, index, 'following')">Following</a>
</div> </div>
<div v-if="owner">
<a class="btn btn-outline-dark btn-sm font-weight-bold" href="#" @click.prevent="followModalAction(user.id, index, 'following')">Following</a>
</div> </div>
</div> </div>
</div> <div v-if="followingModalSearch && following.length == 0" class="list-group-item border-0">
<div v-if="followingModalSearch && following.length == 0" class="list-group-item border-0"> <div class="list-group-item border-0 pt-5">
<div class="list-group-item border-0 pt-5"> <p class="p-3 text-center mb-0 lead">No Results Found</p>
<p class="p-3 text-center mb-0 lead">No Results Found</p> </div>
</div>
<div v-if="following.length > 0 && followingMore" class="list-group-item text-center" v-on:click="followingLoadMore()">
<p class="mb-0 small text-muted font-weight-light cursor-pointer">Load more</p>
</div> </div>
</div> </div>
<div v-if="following.length > 0 && followingMore" class="list-group-item text-center" v-on:click="followingLoadMore()"> </div>
<p class="mb-0 small text-muted font-weight-light cursor-pointer">Load more</p> <div v-else class="text-center py-5">
<div class="spinner-border" role="status">
<span class="sr-only">Loading...</span>
</div> </div>
</div> </div>
</b-modal> </b-modal>
@ -463,31 +464,42 @@
body-class="list-group-flush py-3 px-0" body-class="list-group-flush py-3 px-0"
dialog-class="follow-modal" dialog-class="follow-modal"
> >
<div class="list-group"> <div v-if="!followerLoading" class="list-group" style="max-height: 60vh;">
<div v-if="followers.length == 0" class="list-group-item border-0"> <div v-if="!followers.length" class="list-group-item border-0">
<p class="text-center mb-0 font-weight-bold text-muted py-5"> <p class="text-center mb-0 font-weight-bold text-muted py-5">
<span class="text-dark">{{profileUsername}}</span> has no followers yet</p> <span class="text-dark">{{profileUsername}}</span> has no followers yet</p>
</div> </div>
<div class="list-group-item border-0 py-1" v-for="(user, index) in followers" :key="'follower_'+index">
<div class="media mb-0"> <div v-else>
<a :href="profileUrlRedirect(user)"> <div class="list-group-item border-0 py-1 mb-1" v-for="(user, index) in followers" :key="'follower_'+index">
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + 's avatar'" width="30px" height="30px" loading="lazy" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0'"> <div class="media mb-0">
</a> <a :href="profileUrlRedirect(user)">
<div class="media-body mb-0"> <img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + 's avatar'" width="30px" height="30px" loading="lazy" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0'">
<p class="mb-0" style="font-size: 14px"> </a>
<a :href="profileUrlRedirect(user)" class="font-weight-bold text-dark"> <div class="media-body mb-0">
{{user.username}} <p class="mb-0" style="font-size: 14px">
</a> <a :href="profileUrlRedirect(user)" class="font-weight-bold text-dark">
</p> {{user.username}}
<p class="text-secondary mb-0" style="font-size: 13px"> </a>
{{user.display_name}} </p>
</p> <p v-if="!user.local" class="text-muted mb-0 text-break mr-3" style="font-size: 14px" :title="user.acct" data-toggle="dropdown" data-placement="bottom">
<span class="font-weight-bold">{{user.acct.split('@')[0]}}</span><span class="text-lighter">&commat;{{user.acct.split('@')[1]}}</span>
</p>
<p v-else class="text-muted mb-0 text-truncate" style="font-size: 14px">
{{user.display_name ? user.display_name : user.username}}
</p>
</div>
<!-- <button class="btn btn-primary font-weight-bold btn-sm py-1">FOLLOW</button> -->
</div> </div>
<!-- <button class="btn btn-primary font-weight-bold btn-sm py-1">FOLLOW</button> --> </div>
<div v-if="followers.length && followerMore" class="list-group-item text-center" v-on:click="followersLoadMore()">
<p class="mb-0 small text-muted font-weight-light cursor-pointer">Load more</p>
</div> </div>
</div> </div>
<div v-if="followers.length && followerMore" class="list-group-item text-center" v-on:click="followersLoadMore()"> </div>
<p class="mb-0 small text-muted font-weight-light cursor-pointer">Load more</p> <div v-else class="text-center py-5">
<div class="spinner-border" role="status">
<span class="sr-only">Loading...</span>
</div> </div>
</div> </div>
</b-modal> </b-modal>
@ -558,20 +570,20 @@
</div> </div>
</b-modal> </b-modal>
<b-modal ref="embedModal" <b-modal ref="embedModal"
id="ctx-embed-modal" id="ctx-embed-modal"
hide-header hide-header
hide-footer hide-footer
centered centered
rounded rounded
size="md" size="md"
body-class="p-2 rounded"> body-class="p-2 rounded">
<div> <div>
<textarea class="form-control disabled text-monospace" rows="6" style="overflow-y:hidden;border: 1px solid #efefef; font-size: 12px; line-height: 18px; margin: 0 0 7px;resize:none;" v-model="ctxEmbedPayload" disabled=""></textarea> <textarea class="form-control disabled text-monospace" rows="6" style="overflow-y:hidden;border: 1px solid #efefef; font-size: 12px; line-height: 18px; margin: 0 0 7px;resize:none;" v-model="ctxEmbedPayload" disabled=""></textarea>
<hr> <hr>
<button :class="copiedEmbed ? 'btn btn-primary btn-block btn-sm py-1 font-weight-bold disabed': 'btn btn-primary btn-block btn-sm py-1 font-weight-bold'" @click="ctxCopyEmbed" :disabled="copiedEmbed">{{copiedEmbed ? 'Embed Code Copied!' : 'Copy Embed Code'}}</button> <button :class="copiedEmbed ? 'btn btn-primary btn-block btn-sm py-1 font-weight-bold disabed': 'btn btn-primary btn-block btn-sm py-1 font-weight-bold'" @click="ctxCopyEmbed" :disabled="copiedEmbed">{{copiedEmbed ? 'Embed Code Copied!' : 'Copy Embed Code'}}</button>
<p class="mb-0 px-2 small text-muted">By using this embed, you agree to our <a href="/site/terms">Terms of Use</a></p> <p class="mb-0 px-2 small text-muted">By using this embed, you agree to our <a href="/site/terms">Terms of Use</a></p>
</div> </div>
</b-modal> </b-modal>
</div> </div>
</template> </template>
<style type="text/css" scoped> <style type="text/css" scoped>
@ -652,7 +664,6 @@
<script type="text/javascript"> <script type="text/javascript">
import VueMasonry from 'vue-masonry-css' import VueMasonry from 'vue-masonry-css'
export default { export default {
props: [ props: [
'profile-id', 'profile-id',
@ -679,9 +690,11 @@
followers: [], followers: [],
followerCursor: 1, followerCursor: 1,
followerMore: true, followerMore: true,
followerLoading: true,
following: [], following: [],
followingCursor: 1, followingCursor: 1,
followingMore: true, followingMore: true,
followingLoading: true,
warning: false, warning: false,
sponsorList: [], sponsorList: [],
bookmarks: [], bookmarks: [],
@ -1121,6 +1134,7 @@
if(res.data.length < 10) { if(res.data.length < 10) {
this.followingMore = false; this.followingMore = false;
} }
this.followingLoading = false;
}); });
this.$refs.followingModal.show(); this.$refs.followingModal.show();
return; return;
@ -1150,6 +1164,7 @@
if(res.data.length < 10) { if(res.data.length < 10) {
this.followerMore = false; this.followerMore = false;
} }
this.followerLoading = false;
}) })
this.$refs.followerModal.show(); this.$refs.followerModal.show();
return; return;