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);
$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) {
return response()->json([]);
}
if(!$owner && $request->page > 5) {
return [];
}
$followers = $profile->followers()->orderByDesc('followers.created_at')->paginate(10);
$resource = new Fractal\Resource\Collection($followers, new AccountTransformer());
$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($profile->user->settings->show_profile_following == false && $owner == false, 404);
if(!$owner && $request->page > 5) {
return [];
}
if($search) {
abort_if(!$owner, 404);
$following = $profile->following()

View File

@ -6,66 +6,95 @@ use Illuminate\Support\Facades\Redis;
use App\{
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 $follower_prefix = 'px:profile:followers-v1.3:';
public static $following_prefix = 'px:profile:following-v1.3:';
public static function build()
public static function add($actor, $target)
{
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;
self::$follower_prefix .= $profile->id;
self::$following_prefix .= $profile->id;
return $this;
Redis::zrem(self::FOLLOWING_KEY . $actor, $target);
Redis::zrem(self::FOLLOWERS_KEY . $target, $actor);
}
public function followers($limit = 100, $offset = 1)
public static function followers($id, $start = 0, $stop = 10)
{
if(Redis::zcard(self::$follower_prefix) == 0) {
$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);
}
return Redis::zrange(self::FOLLOWERS_KEY . $id, $start, $stop);
}
public function following($limit = 100, $offset = 1)
public static function following($id, $start = 0, $stop = 10)
{
if(Redis::zcard(self::$following_prefix) == 0) {
$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);
}
return Redis::zrange(self::FOLLOWING_KEY . $id, $start, $stop);
}
public static function follows(string $actor, string $target)
{
$key = self::$follower_prefix . $target;
if(Redis::zcard($key) == 0) {
$p = Profile::findOrFail($target);
self::build()->profile($p)->followers(1);
self::build()->profile($p)->following(1);
return (bool) Redis::zrank($key, $actor);
} else {
return (bool) Redis::zrank($key, $actor);
return Follower::whereProfileId($actor)->whereFollowingId($target)->exists();
}
public static function audience($profile)
{
return (new self)->getAudienceInboxes($profile);
}
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 -->
<div class="d-none d-md-block pb-3">
<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 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>
<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">
@ -404,53 +404,54 @@
title="Following"
body-class="list-group-flush py-3 px-0"
dialog-class="follow-modal">
<div v-if="!loading" class="list-group" style="min-height: 60vh;">
<div v-if="owner == true" class="list-group-item border-0 pt-0 px-0 mt-n2 mb-3">
<span class="d-flex px-4 pb-0 align-items-center">
<i class="fas fa-search text-lighter"></i>
<input type="text" class="form-control border-0 shadow-0 no-focus" placeholder="Search Following..." v-model="followingModalSearch" v-on:keyup="followingModalSearchHandler">
</span>
<div v-if="!followingLoading" class="list-group" style="max-height: 60vh;">
<div v-if="!following.length" class="list-group-item border-0">
<p class="text-center mb-0 font-weight-bold text-muted py-5">
<span class="text-dark">{{profileUsername}}</span> is not following yet</p>
</div>
<div v-if="owner == true" class="btn-group rounded-0 mt-n3 mb-3 border-top" role="group" aria-label="Following">
<!-- <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> -->
<!-- <button type="button" class="btn btn-light py-3 rounded-0 text-muted font-weight-bold" style="font-size: 12px;">MUTED</button>
<button type="button" class="btn btn-light py-3 rounded-0 text-muted font-weight-bold" style="font-size: 12px;">BLOCKED</button> -->
</div>
<div v-else class="btn-group rounded-0 mt-n3 mb-3" role="group" aria-label="Following">
<!-- <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>
<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>
<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>
<div class="list-group-item border-0 py-1" v-for="(user, index) in following" :key="'following_'+index">
<div class="media">
<a :href="profileUrlRedirect(user)">
<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'">
</a>
<div class="media-body text-truncate">
<p class="mb-0" style="font-size: 14px">
<a :href="profileUrlRedirect(user)" class="font-weight-bold text-dark">
{{user.username}}
</a>
</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">
<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}}
</p>
</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 v-else>
<div v-if="owner == true" class="list-group-item border-0 pt-0 px-0 mt-n2 mb-3">
<span class="d-flex px-4 pb-0 align-items-center">
<i class="fas fa-search text-lighter"></i>
<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 class="list-group-item border-0 py-1 mb-1" v-for="(user, index) in following" :key="'following_'+index">
<div class="media">
<a :href="profileUrlRedirect(user)">
<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'">
</a>
<div class="media-body text-truncate">
<p class="mb-0" style="font-size: 14px">
<a :href="profileUrlRedirect(user)" class="font-weight-bold text-dark">
{{user.username}}
</a>
</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>
<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 v-if="followingModalSearch && following.length == 0" class="list-group-item border-0">
<div class="list-group-item border-0 pt-5">
<p class="p-3 text-center mb-0 lead">No Results Found</p>
<div v-if="followingModalSearch && following.length == 0" class="list-group-item border-0">
<div class="list-group-item border-0 pt-5">
<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 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 v-else class="text-center py-5">
<div class="spinner-border" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
</b-modal>
@ -463,31 +464,42 @@
body-class="list-group-flush py-3 px-0"
dialog-class="follow-modal"
>
<div class="list-group">
<div v-if="followers.length == 0" class="list-group-item border-0">
<div v-if="!followerLoading" class="list-group" style="max-height: 60vh;">
<div v-if="!followers.length" class="list-group-item border-0">
<p class="text-center mb-0 font-weight-bold text-muted py-5">
<span class="text-dark">{{profileUsername}}</span> has no followers yet</p>
</div>
<div class="list-group-item border-0 py-1" v-for="(user, index) in followers" :key="'follower_'+index">
<div class="media mb-0">
<a :href="profileUrlRedirect(user)">
<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'">
</a>
<div class="media-body mb-0">
<p class="mb-0" style="font-size: 14px">
<a :href="profileUrlRedirect(user)" class="font-weight-bold text-dark">
{{user.username}}
</a>
</p>
<p class="text-secondary mb-0" style="font-size: 13px">
{{user.display_name}}
</p>
<div v-else>
<div class="list-group-item border-0 py-1 mb-1" v-for="(user, index) in followers" :key="'follower_'+index">
<div class="media mb-0">
<a :href="profileUrlRedirect(user)">
<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'">
</a>
<div class="media-body mb-0">
<p class="mb-0" style="font-size: 14px">
<a :href="profileUrlRedirect(user)" class="font-weight-bold text-dark">
{{user.username}}
</a>
</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>
<!-- <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 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 v-else class="text-center py-5">
<div class="spinner-border" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
</b-modal>
@ -558,20 +570,20 @@
</div>
</b-modal>
<b-modal ref="embedModal"
id="ctx-embed-modal"
hide-header
hide-footer
centered
rounded
size="md"
body-class="p-2 rounded">
<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>
<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>
<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>
</b-modal>
id="ctx-embed-modal"
hide-header
hide-footer
centered
rounded
size="md"
body-class="p-2 rounded">
<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>
<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>
<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>
</b-modal>
</div>
</template>
<style type="text/css" scoped>
@ -652,7 +664,6 @@
<script type="text/javascript">
import VueMasonry from 'vue-masonry-css'
export default {
props: [
'profile-id',
@ -679,9 +690,11 @@
followers: [],
followerCursor: 1,
followerMore: true,
followerLoading: true,
following: [],
followingCursor: 1,
followingMore: true,
followingLoading: true,
warning: false,
sponsorList: [],
bookmarks: [],
@ -1121,6 +1134,7 @@
if(res.data.length < 10) {
this.followingMore = false;
}
this.followingLoading = false;
});
this.$refs.followingModal.show();
return;
@ -1150,6 +1164,7 @@
if(res.data.length < 10) {
this.followerMore = false;
}
this.followerLoading = false;
})
this.$refs.followerModal.show();
return;