···238238$ php artisan migrate:fresh --seed
239239```
240240241241-Run the above command to rebuild the database and seed with sample data. In order for the seeder to seed beatmaps, you must enter a valid osu! API key as the value of the `OSU_API_KEY` property in the `.env` configuration file, as the seeder obtains beatmap data from the osu! API. The key can be obtained at [the "osu! API Access" page](https://old.ppy.sh/p/api), which is currently only available on the old site.
241241+Run the above command to rebuild the database and seed with sample data. In order for the seeder to seed beatmaps, you must enter a valid osu! API key as the value of the `OSU_API_KEY` property in the `.env` configuration file, as the seeder obtains beatmap data from the osu! API. The key can be obtained from [the "Legacy API" section of your account settings page](https://osu.ppy.sh/home/account/edit#legacy-api).
242242243243## Continuous asset generation while developing
244244
+8-1
app/Console/Commands/NotificationsSendMail.php
···6161 foreach ($userIds->chunk($chunkSize) as $chunk) {
6262 $users = User::whereIn('user_id', $chunk)->get();
6363 foreach ($users as $user) {
6464- dispatch(new UserNotificationDigest($user, $fromId, $toId));
6464+ $job = new UserNotificationDigest($user, $fromId, $toId);
6565+ try {
6666+ $job->handle();
6767+ } catch (\Exception $e) {
6868+ // catch exception and queue job to be rerun to avoid job exploding and preventing other notifications from being processed.
6969+ log_error($e);
7070+ dispatch($job);
7171+ }
6572 }
6673 }
6774
···1717{
1818 abstract protected function newReportableExtraParams(): array;
19192020+ public function reportableAdditionalInfo(): ?string
2121+ {
2222+ return null;
2323+ }
2424+2025 public function reportedIn()
2126 {
2227 return $this->morphMany(UserReport::class, 'reportable');
···3641 $attributes['comments'] = $params['comments'] ?? '';
3742 $attributes['reporter_id'] = $reporter->getKey();
38433939- if (array_key_exists('reason', $params)) {
4444+ if (present($params['reason'] ?? null)) {
4045 $attributes['reason'] = $params['reason'];
4146 }
4247
+1
app/Models/Traits/ReportableInterface.php
···10101111interface ReportableInterface
1212{
1313+ public function reportableAdditionalInfo(): ?string;
1314 public function reportBy(User $reporter, array $params): ?UserReport;
1415 public function trashed();
1516}
+8-4
app/Models/User.php
···2525use Carbon\Carbon;
2626use DB;
2727use Ds\Set;
2828-use Egulias\EmailValidator\EmailValidator;
2929-use Egulias\EmailValidator\Validation\NoRFCWarningsValidation;
3028use Exception;
3129use Hash;
3230use Illuminate\Auth\Authenticatable;
···20001998 $this->currentPassword = $value;
20011999 }
2002200020012001+ /**
20022002+ * Enables email presence and confirmation field equality check.
20032003+ */
20032004 public function validateEmailConfirmation()
20042005 {
20052006 $this->validateEmailConfirmation = true;
···22462247 }
2247224822482249 if ($this->validateEmailConfirmation) {
22502250+ if ($this->user_email === null) {
22512251+ $this->validationErrors()->add('user_email', '.required');
22522252+ }
22532253+22492254 if ($this->user_email !== $this->emailConfirmation) {
22502255 $this->validationErrors()->add('user_email_confirmation', '.wrong_email_confirmation');
22512256 }
···2296230122972302 public function isValidEmail()
22982303 {
22992299- $emailValidator = new EmailValidator();
23002300- if (!$emailValidator->isValid($this->user_email, new NoRFCWarningsValidation())) {
23042304+ if (!is_valid_email_format($this->user_email)) {
23012305 $this->validationErrors()->add('user_email', '.invalid_email');
2302230623032307 // no point validating further if address isn't valid.
···1515import BeatmapsetDiscussionJson from 'interfaces/beatmapset-discussion-json';
1616import { BeatmapsetDiscussionMessagePostJson } from 'interfaces/beatmapset-discussion-post-json';
1717import BeatmapsetExtendedJson from 'interfaces/beatmapset-extended-json';
1818-import { BeatmapsetWithDiscussionsJson } from 'interfaces/beatmapset-json';
1818+import BeatmapsetWithDiscussionsJson from 'interfaces/beatmapset-with-discussions-json';
1919import UserJson from 'interfaces/user-json';
2020import { route } from 'laroute';
2121import { isEqual } from 'lodash';
···11// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
22// See the LICENCE file in the repository root for full licence text.
3344-import { Comments } from 'components/comments';
44+import Comments from 'components/comments';
55import { CommentsManager } from 'components/comments-manager';
66import HeaderV4 from 'components/header-v4';
77import NotificationBanner from 'components/notification-banner';
···11# Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
22# See the LICENCE file in the repository root for full licence text.
3344-import { Main } from 'comments-index/main'
44+import CommentsIndex from 'comments-index'
55import { CommentsManager } from 'components/comments-manager'
66import core from 'osu-core-singleton'
77import { createElement } from 'react'
···1313 core.dataStore.uiState.initializeWithCommentBundleJson(commentBundle)
14141515 createElement CommentsManager,
1616- component: Main
1616+ component: CommentsIndex
1717 user: commentBundle.user
+2-2
resources/js/entrypoints/comments-show.coffee
···11# Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
22# See the LICENCE file in the repository root for full licence text.
3344-import { Main } from 'comments-show/main'
44+import CommentsShow from 'comments-show'
55import { CommentsManager } from 'components/comments-manager'
66import core from 'osu-core-singleton'
77import { createElement } from 'react'
···1313 core.dataStore.uiState.initializeWithCommentBundleJson(commentBundle)
14141515 createElement CommentsManager,
1616- component: Main
1616+ component: CommentsShow
···11// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
22// See the LICENCE file in the repository root for full licence text.
3344-import { BeatmapsetWithDiscussionsJson } from './beatmapset-json';
44+import BeatmapsetWithDiscussionsJson from 'interfaces/beatmapset-with-discussions-json';
5566export interface BeatmapsetDiscussionPostStoreResponseJson {
77 beatmap_discussion_id: number;
···11+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
22+// See the LICENCE file in the repository root for full licence text.
33+44+import BeatmapsetExtendedJson from './beatmapset-extended-json';
55+66+type DiscussionsRequiredAttributes = 'beatmaps' | 'current_user_attributes' | 'discussions' | 'events' | 'nominations' | 'related_users';
77+type BeatmapsetWithDiscussionsJson = BeatmapsetExtendedJson & Required<Pick<BeatmapsetExtendedJson, DiscussionsRequiredAttributes>>;
88+99+export default BeatmapsetWithDiscussionsJson;
+8
resources/js/interfaces/legacy-api-key-json.ts
···11+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
22+// See the LICENCE file in the repository root for full licence text.
33+44+export default interface LegacyApiKeyJson {
55+ api_key: string;
66+ app_name: string;
77+ app_url: string;
88+}
+6
resources/js/interfaces/legacy-irc-key-json.ts
···11+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
22+// See the LICENCE file in the repository root for full licence text.
33+44+export default interface LegacyIrcKeyJson {
55+ token: string;
66+}
···22// See the LICENCE file in the repository root for full licence text.
3344import AdminMenu from 'components/admin-menu';
55-import { Comments } from 'components/comments';
55+import Comments from 'components/comments';
66import { CommentsManager } from 'components/comments-manager';
77import NewsHeader from 'components/news-header';
88import StringWithComponent from 'components/string-with-component';
···3131import NotificationsWorker from 'notifications/worker';
3232import SocketWorker from 'socket-worker';
3333import RootDataStore from 'stores/root-data-store';
3434+import { parseJsonNullable } from 'utils/json';
34353536// will this replace main.coffee eventually?
3637export default class OsuCore {
3737- beatmapsetSearchController: BeatmapsetSearchController;
3838- readonly captcha = new Captcha();
3939- readonly chatWorker = new ChatWorker();
4040- readonly clickMenu = new ClickMenu();
3838+ readonly beatmapsetSearchController;
3939+ readonly captcha;
4040+ readonly chatWorker;
4141+ readonly clickMenu;
4142 @observable currentUser?: CurrentUserJson;
4242- readonly currentUserModel = new UserModel(this);
4343- dataStore: RootDataStore;
4444- readonly enchant: Enchant;
4545- readonly forumPoll = new ForumPoll();
4646- readonly forumPostEdit = new ForumPostEdit();
4747- readonly forumPostInput = new ForumPostInput();
4848- readonly forumPostReport = new ForumPostReport();
4949- readonly localtime = new Localtime();
5050- readonly mobileToggle = new MobileToggle();
5151- notificationsWorker: NotificationsWorker;
5252- readonly osuAudio: OsuAudio;
5353- readonly reactTurbolinks: ReactTurbolinks;
5454- readonly referenceLinkTooltip = new ReferenceLinkTooltip();
5555- readonly scorePins = new ScorePins();
5656- @observable scrolling = false;
5757- socketWorker: SocketWorker;
5858- readonly stickyHeader = new StickyHeader();
5959- readonly timeago = new Timeago();
6060- readonly turbolinksReload = new TurbolinksReload();
6161- readonly userLogin: UserLogin;
6262- userLoginObserver: UserLoginObserver;
6363- readonly userPreferences = new UserPreferences();
6464- readonly userVerification = new UserVerification();
6565- windowFocusObserver: WindowFocusObserver;
6666- readonly windowSize = new WindowSize();
4343+ readonly currentUserModel;
4444+ readonly dataStore;
4545+ readonly enchant;
4646+ readonly forumPoll;
4747+ readonly forumPostEdit;
4848+ readonly forumPostInput;
4949+ readonly forumPostReport;
5050+ readonly localtime;
5151+ readonly mobileToggle;
5252+ readonly notificationsWorker;
5353+ readonly osuAudio;
5454+ readonly reactTurbolinks;
5555+ readonly referenceLinkTooltip;
5656+ readonly scorePins;
5757+ readonly socketWorker;
5858+ readonly stickyHeader;
5959+ readonly timeago;
6060+ readonly turbolinksReload;
6161+ readonly userLogin;
6262+ readonly userLoginObserver;
6363+ readonly userPreferences;
6464+ readonly userVerification;
6565+ readonly windowFocusObserver;
6666+ readonly windowSize;
67676868 @computed
6969 get currentUserOrFail() {
···7575 }
76767777 constructor() {
7878- // refresh current user on page reload (and initial page load)
7979- $(document).on('turbolinks:load.osu-core', this.onPageLoad);
7878+ // Set current user on first page load. Further updates are done in
7979+ // reactTurbolinks before the new page is rendered.
8080+ // This needs to be fired before everything else (turbolinks:load etc).
8181+ const isLoading = document.readyState === 'loading';
8282+ if (isLoading) {
8383+ document.addEventListener('DOMContentLoaded', this.updateCurrentUser);
8484+ }
8085 $.subscribe('user:update', this.onCurrentUserUpdate);
81868787+ this.captcha = new Captcha();
8888+ this.chatWorker = new ChatWorker();
8989+ this.clickMenu = new ClickMenu();
9090+ this.currentUserModel = new UserModel(this);
9191+ this.forumPoll = new ForumPoll();
9292+ this.forumPostEdit = new ForumPostEdit();
9393+ this.forumPostInput = new ForumPostInput();
9494+ this.forumPostReport = new ForumPostReport();
9595+ this.localtime = new Localtime();
9696+ this.mobileToggle = new MobileToggle();
9797+ this.referenceLinkTooltip = new ReferenceLinkTooltip();
9898+ this.scorePins = new ScorePins();
9999+ this.stickyHeader = new StickyHeader();
100100+ this.timeago = new Timeago();
101101+ this.turbolinksReload = new TurbolinksReload();
102102+ this.userPreferences = new UserPreferences();
103103+ this.userVerification = new UserVerification();
104104+ this.windowSize = new WindowSize();
105105+82106 this.enchant = new Enchant(this.turbolinksReload);
83107 this.osuAudio = new OsuAudio(this.userPreferences);
8484- this.reactTurbolinks = new ReactTurbolinks(this.turbolinksReload);
108108+ this.reactTurbolinks = new ReactTurbolinks(this, this.turbolinksReload);
85109 this.userLogin = new UserLogin(this.captcha);
86110 // should probably figure how to conditionally or lazy initialize these so they don't all init when not needed.
87111 // TODO: requires dynamic imports to lazy load modules.
···95119 this.notificationsWorker = new NotificationsWorker(this.socketWorker);
9612097121 makeObservable(this);
122122+123123+ if (!isLoading) {
124124+ this.updateCurrentUser();
125125+ }
98126 }
99127100100- private onCurrentUserUpdate = (event: unknown, user: CurrentUserJson) => {
101101- this.setCurrentUser(user);
128128+ readonly updateCurrentUser = () => {
129129+ // Remove from DOM so only new data is parsed on navigation.
130130+ const currentUser = parseJsonNullable<typeof window.currentUser>('json-current-user', true);
131131+132132+ if (currentUser != null) {
133133+ window.currentUser = currentUser;
134134+ this.setCurrentUser(window.currentUser);
135135+ }
102136 };
103137104104- private onPageLoad = () => {
105105- this.setCurrentUser(window.currentUser);
138138+ private readonly onCurrentUserUpdate = (event: unknown, user: CurrentUserJson) => {
139139+ this.setCurrentUser(user);
106140 };
107141108142 @action
109109- private setCurrentUser = (userOrEmpty: typeof window.currentUser) => {
143143+ private readonly setCurrentUser = (userOrEmpty: typeof window.currentUser) => {
110144 const user = userOrEmpty.id == null ? undefined : userOrEmpty;
111145112146 if (user != null) {
+1-1
resources/js/register-components.tsx
···55import BeatmapsetEvents, { Props as BeatmapsetEventsProps } from 'components/beatmapset-events';
66import BlockButton from 'components/block-button';
77import ChatIcon from 'components/chat-icon';
88-import { Comments } from 'components/comments';
88+import Comments from 'components/comments';
99import { CommentsManager, Props as CommentsManagerProps } from 'components/comments-manager';
1010import CountdownTimer from 'components/countdown-timer';
1111import { LandingNews } from 'components/landing-news';
···175175 ],
176176177177 'nominations' => [
178178+ 'already_nominated' => 'You\'ve already nominated this beatmap.',
179179+ 'cannot_nominate' => 'You cannot nominate this beatmap game mode.',
178180 'delete' => 'Delete',
179181 'delete_own_confirm' => 'Are you sure? The beatmap will be deleted and you will be redirected back to your profile.',
180182 'delete_other_confirm' => 'Are you sure? The beatmap will be deleted and you will be redirected back to the user\'s profile.',