the browser-facing portion of osu!
at master 6.9 kB view raw
1<?php 2 3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0. 4// See the LICENCE file in the repository root for full licence text. 5 6namespace App\Http\Controllers; 7 8use App\Exceptions\ModelNotSavedException; 9use App\Jobs\UpdateUserMappingFollowerCountCache; 10use App\Models\BeatmapDiscussion; 11use App\Models\Beatmapset; 12use App\Models\Comment; 13use App\Models\Follow; 14use App\Models\Forum\Topic; 15use App\Models\Forum\TopicTrack; 16use App\Models\Forum\TopicWatch; 17use App\Models\User; 18use App\Transformers\FollowCommentTransformer; 19use App\Transformers\FollowModdingTransformer; 20use DB; 21use Exception; 22 23class FollowsController extends Controller 24{ 25 private User $user; 26 27 public function __construct() 28 { 29 parent::__construct(); 30 31 $this->middleware('auth'); 32 } 33 34 public function destroy() 35 { 36 $params = $this->getParams(); 37 $follow = Follow::where($params)->first(); 38 39 if ($follow !== null) { 40 $follow->delete(); 41 42 if ($follow->subtype === 'mapping') { 43 dispatch(new UpdateUserMappingFollowerCountCache($follow->notifiable_id)); 44 } 45 } 46 47 return response([], 204); 48 } 49 50 public function index($subtype = null) 51 { 52 $this->user = auth()->user(); 53 54 $viewArgs = match ($subtype) { 55 'comment' => $this->indexComment(), 56 'forum_topic' => $this->indexForumTopic(), 57 'mapping' => $this->indexMapping(), 58 'modding' => $this->indexModding(), 59 default => null, 60 }; 61 62 if ($viewArgs === null) { 63 return ujs_redirect(route('follows.index', ['subtype' => Follow::DEFAULT_SUBTYPE])); 64 } 65 66 $viewArgs[1]['subtype'] = $subtype; 67 68 return ext_view(...$viewArgs); 69 } 70 71 public function store() 72 { 73 if (\Auth::user()->follows()->count() >= $GLOBALS['cfg']['osu']['user']['max_follows']) { 74 return error_popup(osu_trans('follows.store.too_many')); 75 } 76 $params = $this->getParams(); 77 $follow = new Follow($params); 78 79 try { 80 $follow->saveOrExplode(); 81 } catch (Exception $e) { 82 if ($e instanceof ModelNotSavedException) { 83 return error_popup($e->getMessage()); 84 } 85 86 if (!is_sql_unique_exception($e)) { 87 throw $e; 88 } 89 } 90 91 if ($params['subtype'] === 'mapping') { 92 dispatch(new UpdateUserMappingFollowerCountCache($params['notifiable_id'])); 93 } 94 95 return response(null, 204); 96 } 97 98 private function getParams() 99 { 100 $params = get_params(request()->all(), 'follow', ['notifiable_type:string', 'notifiable_id:int', 'subtype:string']); 101 $params['user_id'] = auth()->user()->getKey(); 102 103 return $params; 104 } 105 106 private function indexComment() 107 { 108 $followsQuery = Follow::where(['user_id' => $this->user->getKey(), 'subtype' => 'comment']); 109 110 $recentCommentIds = Comment 111 ::selectRaw('MAX(id) latest_comment_id, commentable_type, commentable_id') 112 ->whereIn( 113 DB::raw('(commentable_type, commentable_id)'), 114 (clone $followsQuery)->selectRaw('notifiable_type, notifiable_id') 115 )->groupBy('commentable_type', 'commentable_id') 116 ->pluck('latest_comment_id'); 117 118 $comments = Comment 119 ::whereIn('id', $recentCommentIds) 120 ->with('user') 121 ->get() 122 ->keyBy(function ($comment) { 123 return "{$comment->commentable_type}:{$comment->commentable_id}"; 124 }); 125 126 $follows = (clone $followsQuery) 127 ->with('notifiable') 128 ->get() 129 ->sortByDesc(function ($follow) use ($comments) { 130 $comment = $comments["{$follow->notifiable_type}:{$follow->notifiable_id}"] ?? null; 131 132 return $comment === null ? null : $comment->getKey(); 133 }); 134 135 $followsTransformer = new FollowCommentTransformer($comments); 136 $followsJson = json_collection($follows, $followsTransformer, ['commentable_meta', 'latest_comment.user']); 137 138 return ['follows.comment', compact('followsJson')]; 139 } 140 141 private function indexForumTopic() 142 { 143 $topics = Topic::watchedByUser($this->user)->paginate(); 144 $topicReadStatus = TopicTrack::readStatus($this->user, $topics); 145 $topicWatchStatus = TopicWatch::watchStatus($this->user, $topics); 146 147 $counts = [ 148 'total' => $topics->total(), 149 'unread' => TopicWatch::unreadCount($this->user), 150 ]; 151 152 return [ 153 'follows.forum_topic', 154 compact('topics', 'topicReadStatus', 'topicWatchStatus', 'counts'), 155 ]; 156 } 157 158 private function indexMapping() 159 { 160 $followsQuery = Follow::where(['user_id' => $this->user->getKey(), 'subtype' => 'mapping']); 161 162 $recentBeatmapsetIds = Beatmapset 163 ::selectRaw('MAX(beatmapset_id) latest_beatmapset_id, user_id') 164 ->whereIn( 165 'user_id', 166 (clone $followsQuery)->select('notifiable_id') 167 )->groupBy('user_id') 168 ->where('approved', '<>', Beatmapset::STATES['wip']) 169 ->pluck('latest_beatmapset_id'); 170 171 $beatmapsets = Beatmapset 172 ::whereIn('beatmapset_id', $recentBeatmapsetIds) 173 ->with('beatmaps') 174 ->get() 175 ->keyBy('user_id'); 176 177 $follows = (clone $followsQuery) 178 ->with('notifiable') 179 ->get() 180 ->sortByDesc(function ($follow) use ($beatmapsets) { 181 $beatmapset = $beatmapsets[$follow->notifiable_id] ?? null; 182 183 return $beatmapset === null ? null : $beatmapset->getKey(); 184 }); 185 186 $followsTransformer = new FollowModdingTransformer($beatmapsets); 187 $followsJson = json_collection($follows, $followsTransformer, ['latest_beatmapset.beatmaps', 'user']); 188 189 return ['follows.mapping', compact('followsJson')]; 190 } 191 192 private function indexModding() 193 { 194 $watches = $this->user 195 ->beatmapsetWatches() 196 ->visible() 197 ->orderBy('last_notified', 'DESC') 198 ->with('beatmapset') 199 ->paginate(); 200 $totalCount = $watches->total(); 201 $unreadCount = $this->user->beatmapsetWatches()->visible()->unread()->count(); 202 $openIssues = BeatmapDiscussion 203 ::whereIn('beatmapset_id', $watches->pluck('beatmapset_id')) 204 ->openIssues() 205 ->groupBy('beatmapset_id') 206 ->selectRaw('beatmapset_id, COUNT(*) AS open_count') 207 ->get() 208 ->keyBy('beatmapset_id'); 209 210 return ['follows.modding', compact('openIssues', 'watches', 'totalCount', 'unreadCount')]; 211 } 212}