mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-25 15:55:22 +00:00
Update FollowerService
This commit is contained in:
parent
ff48400a38
commit
c8824d1b51
3 changed files with 172 additions and 120 deletions
|
@ -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()
|
||||
|
|
|
@ -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 [];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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">@{{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">@{{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">@{{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;
|
||||
|
|
Loading…
Reference in a new issue