瀏覽代碼

Update FollowerService

Daniel Supernault 4 年之前
父節點
當前提交
c8824d1b51
共有 3 個文件被更改,包括 170 次插入118 次删除
  1. 8 0
      app/Http/Controllers/PublicApiController.php
  2. 70 41
      app/Services/FollowerService.php
  3. 92 77
      resources/assets/js/components/Profile.vue

+ 8 - 0
app/Http/Controllers/PublicApiController.php

@@ -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()

+ 70 - 41
app/Services/FollowerService.php

@@ -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 $follower_prefix = 'px:profile:followers-v1.3:';
-	public static $following_prefix = 'px:profile:following-v1.3:';
+	public static function add($actor, $target)
+	{
+		Redis::zadd(self::FOLLOWING_KEY . $actor, $target, $target);
+		Redis::zadd(self::FOLLOWERS_KEY . $target, $actor, $actor);
+	}
 
 
-	public static function build()
+	public static function remove($actor, $target)
 	{
 	{
-		return new self();
+		Redis::zrem(self::FOLLOWING_KEY . $actor, $target);
+		Redis::zrem(self::FOLLOWERS_KEY . $target, $actor);
 	}
 	}
 
 
-	public function profile(Profile $profile)
+	public static function followers($id, $start = 0, $stop = 10)
 	{
 	{
-		$this->profile = $profile;
-		self::$follower_prefix .= $profile->id;
-		self::$following_prefix .= $profile->id;
-		return $this;
+		return Redis::zrange(self::FOLLOWERS_KEY . $id, $start, $stop);
 	}
 	}
 
 
-	public function followers($limit = 100, $offset = 1)
+	public static function following($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::FOLLOWING_KEY . $id, $start, $stop);
 	}
 	}
 
 
+	public static function follows(string $actor, string $target)
+	{
+		return Follower::whereProfileId($actor)->whereFollowingId($target)->exists();
+	}
 
 
-	public function following($limit = 100, $offset = 1)
+	public static function audience($profile)
 	{
 	{
-		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 (new self)->getAudienceInboxes($profile);
 	}
 	}
 
 
-	public static function follows(string $actor, string $target)
+	protected function getAudienceInboxes($profile)
 	{
 	{
-		$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);
+		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 [];
 	}
 	}
 
 
 }
 }

+ 92 - 77
resources/assets/js/components/Profile.vue

@@ -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="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 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 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>
-			<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 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 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 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>
 				</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="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()">
-				<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>
 		</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="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">
 				<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">
-					<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>
 					</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()">
-				<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>
 		</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"
-	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>
 </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;