Browse Source

Merge branch 'master' into notification-api

pull/4475/head
bakaneko 3 months ago
parent
commit
a613910c95
No account linked to committer's email address

+ 24
- 0
app/Http/Controllers/LegacyInterOpController.php View File

@@ -25,7 +25,9 @@ use App\Libraries\Session\Store as SessionStore;
25 25
 use App\Libraries\UserBestScoresCheck;
26 26
 use App\Models\Beatmap;
27 27
 use App\Models\Beatmapset;
28
+use App\Models\Forum;
28 29
 use App\Models\NewsPost;
30
+use App\Models\Notification;
29 31
 use App\Models\User;
30 32
 use Illuminate\Foundation\Bus\DispatchesJobs;
31 33
 
@@ -43,6 +45,28 @@ class LegacyInterOpController extends Controller
43 45
         return ['success' => true];
44 46
     }
45 47
 
48
+    public function generateNotification()
49
+    {
50
+        $params = request()->all();
51
+
52
+        if (!isset($params['name'])) {
53
+            abort(422, 'missing notification name');
54
+        }
55
+
56
+        if ($params['name'] === Notification::FORUM_TOPIC_REPLY) {
57
+            $post = Forum\Post::find($params['post_id'] ?? null);
58
+            $user = optional($post)->user;
59
+
60
+            if ($post === null || $user === null) {
61
+                abort(422, 'post is missing or it contains invalid user');
62
+            }
63
+
64
+            broadcast_notification($params['name'], $post, $user);
65
+
66
+            return response(null, 204);
67
+        }
68
+    }
69
+
46 70
     public function news()
47 71
     {
48 72
         $newsPosts = NewsPost::default()->limit(5)->get();

+ 5
- 1
app/Http/Controllers/NotificationsController.php View File

@@ -38,16 +38,20 @@ class NotificationsController extends Controller
38 38
 
39 39
     public function index()
40 40
     {
41
+        $withRead = get_bool(request('with_read')) ?? false;
41 42
         $hasMore = false;
42 43
         $userNotificationsQuery = auth()
43 44
             ->user()
44 45
             ->userNotifications()
45 46
             ->with('notification.notifiable')
46 47
             ->with('notification.source')
47
-            ->where('is_read', false)
48 48
             ->orderBy('notification_id', 'DESC')
49 49
             ->limit(static::LIMIT);
50 50
 
51
+        if (!$withRead) {
52
+            $userNotificationsQuery->where('is_read', false);
53
+        }
54
+
51 55
         $maxId = get_int(request('max_id'));
52 56
         if (isset($maxId)) {
53 57
             $userNotificationsQuery->where('notification_id', '<=', $maxId);

+ 1
- 1
app/Http/Controllers/UsersController.php View File

@@ -365,7 +365,7 @@ class UsersController extends Controller
365 365
 
366 366
     private function sanitizedLimitParam()
367 367
     {
368
-        return clamp(get_int(request('limit')) ?? 5, 1, 21);
368
+        return clamp(get_int(request('limit')) ?? 5, 1, 51);
369 369
     }
370 370
 
371 371
     private function getExtra($user, $page, $options, $perPage = 10, $offset = 0)

+ 19
- 5
app/Jobs/BroadcastNotification.php View File

@@ -50,7 +50,7 @@ class BroadcastNotification implements ShouldQueue
50 50
             ->all();
51 51
     }
52 52
 
53
-    public function __construct($name, $object, $source)
53
+    public function __construct($name, $object, $source = null)
54 54
     {
55 55
         $this->name = $name;
56 56
         $this->object = $object;
@@ -69,14 +69,16 @@ class BroadcastNotification implements ShouldQueue
69 69
 
70 70
         $this->notifiable = $this->notifiable ?? $this->object;
71 71
         $this->params['name'] = $this->name;
72
-        $this->params['details']['username'] = $this->source->username;
72
+        if ($this->source !== null) {
73
+            $this->params['details']['username'] = $this->source->username;
74
+        }
73 75
 
74 76
         if (is_array($this->receiverIds)) {
75 77
             switch (count($this->receiverIds)) {
76 78
                 case 0:
77 79
                     return;
78 80
                 case 1:
79
-                    if ($this->receiverIds[0] === $this->source->getKey()) {
81
+                    if ($this->receiverIds[0] === optional($this->source)->getKey()) {
80 82
                         return;
81 83
                     }
82 84
             }
@@ -84,7 +86,9 @@ class BroadcastNotification implements ShouldQueue
84 86
 
85 87
         $notification = new Notification($this->params);
86 88
         $notification->notifiable()->associate($this->notifiable);
87
-        $notification->source()->associate($this->source);
89
+        if ($this->source !== null) {
90
+            $notification->source()->associate($this->source);
91
+        }
88 92
 
89 93
         $notification->save();
90 94
 
@@ -95,7 +99,7 @@ class BroadcastNotification implements ShouldQueue
95 99
                 $receivers = User::whereIn('user_id', $this->receiverIds)->get();
96 100
 
97 101
                 foreach ($receivers as $receiver) {
98
-                    if ($receiver->getKey() !== $this->source->getKey()) {
102
+                    if ($receiver->getKey() !== optional($this->source)->getKey()) {
99 103
                         $notification->userNotifications()->create(['user_id' => $receiver->getKey()]);
100 104
                     }
101 105
                 }
@@ -177,6 +181,16 @@ class BroadcastNotification implements ShouldQueue
177 181
         ];
178 182
     }
179 183
 
184
+    private function onBeatmapsetRank()
185
+    {
186
+        $this->receiverIds = static::beatmapsetReceiverIds($this->object);
187
+
188
+        $this->params['details'] = [
189
+            'title' => $this->object->title,
190
+            'cover_url' => $this->object->coverURL('card'),
191
+        ];
192
+    }
193
+
180 194
     private function onBeatmapsetResetNominations()
181 195
     {
182 196
         $this->receiverIds = static::beatmapsetReceiverIds($this->object);

+ 1
- 0
app/Libraries/OsuAuthorize.php View File

@@ -364,6 +364,7 @@ class OsuAuthorize
364 364
     public function checkBeatmapsetNominate($user, $beatmapset)
365 365
     {
366 366
         $this->ensureLoggedIn($user);
367
+        $this->ensureCleanRecord($user);
367 368
 
368 369
         static $prefix = 'beatmap_discussion.nominate.';
369 370
 

+ 33
- 2
app/Models/Beatmapset.php View File

@@ -552,7 +552,9 @@ class Beatmapset extends Model implements AfterCommit
552 552
 
553 553
         if ($this->approved > 0) {
554 554
             $this->approved_date = $currentTime;
555
-            $this->approvedby_id = $user->user_id;
555
+            if ($user !== null) {
556
+                $this->approvedby_id = $user->user_id;
557
+            }
556 558
         } else {
557 559
             $this->approved_date = null;
558 560
             $this->approvedby_id = null;
@@ -564,9 +566,12 @@ class Beatmapset extends Model implements AfterCommit
564 566
             ->beatmaps()
565 567
             ->update(['approved' => $this->approved]);
566 568
 
569
+        if ($this->isScoreable() !== $oldScoreable || $this->isRanked()) {
570
+            dispatch(new RemoveBeatmapsetBestScores($this));
571
+        }
572
+
567 573
         if ($this->isScoreable() !== $oldScoreable) {
568 574
             $this->userRatings()->delete();
569
-            dispatch(new RemoveBeatmapsetBestScores($this));
570 575
         }
571 576
     }
572 577
 
@@ -700,6 +705,32 @@ class Beatmapset extends Model implements AfterCommit
700 705
         ];
701 706
     }
702 707
 
708
+    public function rank()
709
+    {
710
+        if (!$this->isQualified()) {
711
+            return false;
712
+        }
713
+
714
+        DB::transaction(function () {
715
+            $this->events()->create(['type' => BeatmapsetEvent::RANK]);
716
+
717
+            $this->update(['play_count' => 0]);
718
+            $this->beatmaps()->update(['playcount' => 0, 'passcount' => 0]);
719
+            $this->setApproved('ranked', null);
720
+
721
+            // global event
722
+            Event::generate('beatmapsetApprove', ['beatmapset' => $this]);
723
+
724
+            // enqueue a cover check job to ensure cover images are all present
725
+            $job = (new CheckBeatmapsetCovers($this))->onQueue('beatmap_high');
726
+            dispatch($job);
727
+
728
+            broadcast_notification(Notification::BEATMAPSET_RANK, $this);
729
+        });
730
+
731
+        return true;
732
+    }
733
+
703 734
     public function favourite($user)
704 735
     {
705 736
         DB::transaction(function () use ($user) {

+ 1
- 0
app/Models/Notification.php View File

@@ -31,6 +31,7 @@ class Notification extends Model
31 31
     const BEATMAPSET_LOVE = 'beatmapset_love';
32 32
     const BEATMAPSET_NOMINATE = 'beatmapset_nominate';
33 33
     const BEATMAPSET_QUALIFY = 'beatmapset_qualify';
34
+    const BEATMAPSET_RANK = 'beatmapset_rank';
34 35
     const BEATMAPSET_RESET_NOMINATIONS = 'beatmapset_reset_nominations';
35 36
     const CHANNEL_MESSAGE = 'channel_message';
36 37
     const FORUM_TOPIC_REPLY = 'forum_topic_reply';

+ 2
- 0
app/Models/Score/Best/Model.php View File

@@ -57,6 +57,8 @@ abstract class Model extends BaseModel
57 57
         if ($this->replay) {
58 58
             return new ReplayFile($this);
59 59
         }
60
+
61
+        return null;
60 62
     }
61 63
 
62 64
     public function weightedPp()

+ 9
- 1
resources/assets/coffee/react/_components/comment.coffee View File

@@ -427,7 +427,14 @@ export class Comment extends React.PureComponent
427 427
     @setState showNewReply: !@state.showNewReply
428 428
 
429 429
 
430
-  voteToggle: =>
430
+  voteToggle: (e) =>
431
+    target = e.target
432
+
433
+    if !currentUser.id?
434
+      userLogin.show target
435
+
436
+      return
437
+
431 438
     @setState postingVote: true
432 439
 
433 440
     if @hasVoted()
@@ -447,6 +454,7 @@ export class Comment extends React.PureComponent
447 454
       $.publish "commentVote:#{voteAction}", id: @props.comment.id
448 455
     .fail (xhr, status) =>
449 456
       return if status == 'abort'
457
+      return $(target).trigger('ajax:error', [xhr, status]) if xhr.status == 401
450 458
 
451 459
       osu.ajaxError xhr
452 460
 

+ 0
- 2
resources/assets/coffee/react/beatmap-discussions/discussion.coffee View File

@@ -137,8 +137,6 @@ export class Discussion extends React.PureComponent
137 137
 
138 138
 
139 139
   doVote: (e) =>
140
-    downvoting = e.currentTarget.dataset.score == '-1'
141
-
142 140
     LoadingOverlay.show()
143 141
 
144 142
     @voteXhr?.abort()

+ 1
- 1
resources/assets/coffee/react/profile-page/main.coffee View File

@@ -296,7 +296,7 @@ export class Main extends React.PureComponent
296 296
         component: AccountStanding
297 297
 
298 298
 
299
-  showMore: (e, {name, url, perPage = 20}) =>
299
+  showMore: (e, {name, url, perPage = 50}) =>
300 300
     offset = @state[name].length
301 301
 
302 302
     paginationState = _.cloneDeep @state.showMorePagination

+ 1
- 0
resources/assets/lib/notification-maps/category.ts View File

@@ -28,6 +28,7 @@ export const nameToCategory: CategoryMap = {
28 28
   beatmapset_love: 'beatmapset_state',
29 29
   beatmapset_nominate: 'beatmapset_state',
30 30
   beatmapset_qualify: 'beatmapset_state',
31
+  beatmapset_rank: 'beatmapset_state',
31 32
   beatmapset_reset_nominations: 'beatmapset_state',
32 33
   channel_message: 'channel',
33 34
   forum_topic_reply: 'forum_topic_reply',

+ 2
- 0
resources/assets/lib/notification-maps/icons.ts View File

@@ -36,6 +36,7 @@ export const nameToIcons: IconsMap = {
36 36
   beatmapset_love: ['fas fa-drafting-compass', 'fas fa-heart'],
37 37
   beatmapset_nominate: ['fas fa-drafting-compass', 'fas fa-vote-yea'],
38 38
   beatmapset_qualify: ['fas fa-drafting-compass', 'fas fa-check'],
39
+  beatmapset_rank: ['fas fa-drafting-compass', 'fas fa-check-double'],
39 40
   beatmapset_reset_nominations: ['fas fa-drafting-compass', 'fas fa-undo'],
40 41
   channel_message: ['fas fa-comments'],
41 42
   forum_topic_reply: ['fas fa-comment-medical'],
@@ -50,6 +51,7 @@ export const nameToIconsCompact: IconsMap = {
50 51
   beatmapset_love: ['fas fa-heart'],
51 52
   beatmapset_nominate: ['fas fa-vote-yea'],
52 53
   beatmapset_qualify: ['fas fa-check'],
54
+  beatmapset_rank: ['fas fa-check-double'],
53 55
   beatmapset_reset_nominations: ['fas fa-undo'],
54 56
   channel_message: ['fas fa-comments'],
55 57
   forum_topic_reply: ['fas fa-comment-medical'],

+ 2
- 0
resources/assets/lib/notification-maps/url.ts View File

@@ -50,6 +50,8 @@ export function urlSingular(item: Notification) {
50 50
         beatmapsetId: item.objectId,
51 51
         discussionId: item.details.discussionId,
52 52
       });
53
+    case 'beatmapset_rank':
54
+      return laroute.route('beatmapsets.show', { beatmapset: item.objectId });
53 55
     case 'channel_message':
54 56
       return laroute.route('chat.index', { sendto: item.sourceUserId });
55 57
     case 'forum_topic_reply':

+ 40
- 7
resources/assets/lib/notification-widget/worker.ts View File

@@ -73,6 +73,8 @@ export default class Worker {
73 73
   userId: number | null = null;
74 74
   @observable private active: boolean = false;
75 75
   @observable private items = observable.map<number, Notification>();
76
+  private refreshing = false;
77
+  private needsRefresh = false;
76 78
   private timeout: TimeoutCollection = {};
77 79
   private endpoint?: string;
78 80
   private ws: WebSocket | null | undefined;
@@ -82,7 +84,6 @@ export default class Worker {
82 84
     this.active = this.userId != null;
83 85
     this.updatePmNotification();
84 86
     this.loadMore();
85
-    this.connectWebSocket();
86 87
     $(document).on('turbolinks:load', this.updatePmNotification);
87 88
   }
88 89
 
@@ -91,10 +92,6 @@ export default class Worker {
91 92
       return;
92 93
     }
93 94
 
94
-    if (this.endpoint == null) {
95
-      return;
96
-    }
97
-
98 95
     if (this.timeout.connectWebSocket != null) {
99 96
       clearTimeout(this.timeout.connectWebSocket);
100 97
     }
@@ -112,6 +109,7 @@ export default class Worker {
112 109
       endpoint = `${protocol}//${window.location.host}${endpoint}`;
113 110
     }
114 111
     this.ws = new WebSocket(`${endpoint}?csrf=${token}`);
112
+    this.ws.onopen = () => this.refresh();
115 113
     this.ws.onclose = this.delayedConnectWebSocket;
116 114
     this.ws.onmessage = this.handleNewEvent;
117 115
   }
@@ -122,7 +120,10 @@ export default class Worker {
122 120
     }
123 121
 
124 122
     this.ws = null;
125
-    this.timeout.connectWebSocket = setTimeout(this.connectWebSocket, 10000);
123
+    this.timeout.connectWebSocket = setTimeout(() => {
124
+      this.needsRefresh = true;
125
+      this.connectWebSocket();
126
+    }, 10000);
126 127
   }
127 128
 
128 129
   delayedRetryInitialLoadMore = () => {
@@ -212,6 +213,38 @@ export default class Worker {
212 213
     }
213 214
   }
214 215
 
216
+  refresh = (maxId?: number) => {
217
+    if (!this.active || this.refreshing || !this.needsRefresh) {
218
+      return;
219
+    }
220
+
221
+    this.refreshing = true;
222
+
223
+    const params = { with_read: true, max_id: maxId };
224
+
225
+    this.xhr.refresh = $.get(laroute.route('notifications.index'), params)
226
+      .always(() => {
227
+        this.refreshing = false;
228
+        this.needsRefresh = false;
229
+      }).done((bundleJson: NotificationBundleJson) => {
230
+        const oldestNotification = _.minBy(bundleJson.notifications, 'id');
231
+        const minLoadedId = this.minLoadedId;
232
+
233
+        bundleJson.notifications.forEach(this.updateFromServer);
234
+        this.actualUnreadCount = bundleJson.unread_count;
235
+        this.hasMore = bundleJson.has_more;
236
+
237
+        if (bundleJson.has_more &&
238
+          oldestNotification != null &&
239
+          minLoadedId != null &&
240
+          oldestNotification.id > minLoadedId
241
+        ) {
242
+          this.needsRefresh = true;
243
+          this.refresh(oldestNotification.id - 1);
244
+        }
245
+      });
246
+  }
247
+
215 248
   sendMarkRead = (ids: number[]) => {
216 249
     const key = `sendMarkRead:${ids.join(':')}`;
217 250
 
@@ -298,6 +331,6 @@ export default class Worker {
298 331
       ret++;
299 332
     }
300 333
 
301
-    return ret;
334
+    return Math.max(ret, 0);
302 335
   }
303 336
 }

+ 1
- 1
resources/assets/lib/osu-core.ts View File

@@ -62,7 +62,7 @@ export default class OsuCore {
62 62
     $.subscribe('user:update', this.setUser);
63 63
   }
64 64
 
65
-  private setUser(event: JQuery.Event, user: UserJSON) {
65
+  private setUser = (event: JQuery.Event, user: UserJSON) => {
66 66
     this.dataStore.userStore.getOrCreate(user.id, user);
67 67
   }
68 68
 }

+ 2
- 0
resources/lang/en/notifications.php View File

@@ -46,6 +46,8 @@ return [
46 46
                 'beatmapset_nominate_compact' => 'Beatmap was nominated',
47 47
                 'beatmapset_qualify' => '":title" has gained enough nominations and entered the ranking queue',
48 48
                 'beatmapset_qualify_compact' => 'Beatmap entered ranking queue',
49
+                'beatmapset_rank' => '":title" has been ranked',
50
+                'beatmapset_rank_compact' => 'Beatmap was ranked',
49 51
                 'beatmapset_reset_nominations' => 'Nomination of ":title" has been reset',
50 52
                 'beatmapset_reset_nominations_compact' => 'Nomination was reset',
51 53
             ],

+ 1
- 0
routes/web.php View File

@@ -398,6 +398,7 @@ Route::group(['as' => 'api.', 'prefix' => 'api', 'middleware' => ['auth:api', 'r
398 398
 
399 399
 // Callbacks for legacy systems to interact with
400 400
 Route::group(['prefix' => '_lio', 'middleware' => 'lio'], function () {
401
+    Route::post('generate-notification', 'LegacyInterOpController@generateNotification');
401 402
     Route::post('/refresh-beatmapset-cache/{beatmapset}', 'LegacyInterOpController@refreshBeatmapsetCache');
402 403
     Route::post('/regenerate-beatmapset-covers/{beatmapset}', 'LegacyInterOpController@regenerateBeatmapsetCovers');
403 404
     Route::post('/user-best-scores-check/{user}', 'LegacyInterOpController@userBestScoresCheck');

+ 48
- 0
tests/Models/BeatmapsetTest.php View File

@@ -120,6 +120,54 @@ class BeatmapsetTest extends TestCase
120 120
         priv_check_user($nominator, 'BeatmapsetNominate', $beatmapset)->ensureCan();
121 121
     }
122 122
 
123
+    public function testRank()
124
+    {
125
+        $otherUser = factory(User::class)->create();
126
+
127
+        $beatmapset = $this->createBeatmapset([
128
+            'approved' => Beatmapset::STATES['qualified'],
129
+        ]);
130
+
131
+        $beatmap = $beatmapset->beatmaps()->first();
132
+        $beatmap->scoresBest()->create([
133
+            'user_id' => $otherUser->getKey(),
134
+        ]);
135
+        $scores = $beatmapset->beatmaps()->first()->scoresBest()->count();
136
+
137
+        $notifications = Notification::count();
138
+        $userNotifications = UserNotification::count();
139
+
140
+        $beatmapset->watches()->create(['user_id' => $otherUser->getKey()]);
141
+
142
+        $beatmapset->rank();
143
+
144
+        $this->assertTrue($beatmapset->fresh()->isRanked());
145
+        $this->assertSame($notifications + 1, UserNotification::count());
146
+        $this->assertSame($notifications + 1, Notification::count());
147
+        $this->assertNotSame(0, $scores);
148
+        $this->assertSame(0, $beatmap->scoresBest()->count());
149
+    }
150
+
151
+    public function testRankFromWrongState()
152
+    {
153
+        $beatmapset = $this->createBeatmapset([
154
+            'approved' => Beatmapset::STATES['pending'],
155
+        ]);
156
+
157
+        $notifications = Notification::count();
158
+        $userNotifications = UserNotification::count();
159
+
160
+        $otherUser = factory(User::class)->create();
161
+        $beatmapset->watches()->create(['user_id' => $otherUser->getKey()]);
162
+
163
+        $res = $beatmapset->rank();
164
+
165
+        $this->assertFalse($res);
166
+        $this->assertFalse($beatmapset->fresh()->isRanked());
167
+        $this->assertSame($notifications, UserNotification::count());
168
+        $this->assertSame($notifications, Notification::count());
169
+    }
170
+
123 171
     private function createBeatmapset($params = []) : Beatmapset
124 172
     {
125 173
         $defaultParams = [

Loading…
Cancel
Save