Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use table-based user cover presets #11116

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/Http/Controllers/UsersController.php
Original file line number Diff line number Diff line change
Expand Up @@ -664,12 +664,17 @@ public function show($id, $mode = null)
return $userArray;
} else {
$achievements = json_collection(app('medals')->all(), 'Achievement');
$currentUser = \Auth::user();
if ($currentUser !== null && $currentUser->getKey() === $user->getKey()) {
$userCoverPresets = app('user-cover-presets')->json();
}

$initialData = [
'achievements' => $achievements,
'current_mode' => $currentMode,
'scores_notice' => $GLOBALS['cfg']['osu']['user']['profile_scores_notice'],
'user' => $userArray,
'user_cover_presets' => $userCoverPresets ?? [],
];

set_opengraph($user, 'show', $currentMode);
Expand Down
47 changes: 24 additions & 23 deletions app/Libraries/User/Cover.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,45 +8,36 @@
namespace App\Libraries\User;

use App\Models\User;
use App\Models\UserCoverPreset;

class Cover
{
const CUSTOM_COVER_MAX_DIMENSIONS = [2400, 640];

private const AVAILABLE_PRESET_IDS = ['1', '2', '3', '4', '5', '6', '7', '8'];

public function __construct(private User $user)
{
}

public static function isValidPresetId(int|null|string $presetId): bool
{
return $presetId !== null
&& in_array((string) $presetId, static::AVAILABLE_PRESET_IDS, true);
$this->setDefaultPresetId();
}

public function customUrl(): ?string
{
return $this->user->customCover()->url();
}

public function defaultPresetId(): string
public function presetId(): ?int
{
$id = max(0, $this->user->getKey() ?? 0);

return static::AVAILABLE_PRESET_IDS[$id % count(static::AVAILABLE_PRESET_IDS)];
}
if ($this->hasCustomCover()) {
return null;
}

public function presetId(): ?string
{
return $this->hasCustomCover()
? null
: (string) ($this->user->cover_preset_id ?? $this->defaultPresetId());
return $this->user->cover_preset_id;
}

public function set(?string $presetId, ?string $filePath): void
public function set(?int $presetId, ?string $filePath): void
{
$this->user->cover_preset_id = static::isValidPresetId($presetId) ? $presetId : null;
$this->user->cover_preset_id = isset($presetId)
? UserCoverPreset::active()->find($presetId)?->getKey()
: null;

if ($filePath !== null) {
$this->user->customCover()->store($filePath);
Expand All @@ -69,8 +60,18 @@ private function presetUrl(): ?string
{
$presetId = $this->presetId();

return $presetId === null
? null
: "{$GLOBALS['cfg']['app']['url']}/images/headers/profile-covers/c{$presetId}.jpg";
return isset($presetId)
? app('user-cover-presets')->find($presetId)?->file()->url()
: null;
}

private function setDefaultPresetId(): void
{
if (!$this->user->exists || isset($this->user->cover_preset_id) || isset($this->user->custom_cover_filename)) {
return;
}

$id = app('user-cover-presets')->defaultForUser($this->user)->getKey();
$this->user->update(['cover_preset_id' => $id]);
}
}
4 changes: 0 additions & 4 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -2398,10 +2398,6 @@ public function validationErrorsTranslationPrefix(): string

public function save(array $options = [])
{
if (!$this->exists) {
$this->cover_preset_id ??= $this->cover()->defaultPresetId();
}

if ($options['skipValidations'] ?? false) {
return parent::save($options);
}
Expand Down
6 changes: 6 additions & 0 deletions app/Models/UserCoverPreset.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

use App\Libraries\Uploader;
use App\Libraries\User\Cover;
use Illuminate\Database\Eloquent\Builder;

/**
* @property bool $active
Expand All @@ -19,6 +20,11 @@ class UserCoverPreset extends Model
{
private Uploader $file;

public function scopeActive(Builder $query): Builder
{
return $query->where('active', true)->whereNotNull('filename');
}

public function file(): Uploader
{
return $this->file ??= new Uploader(
Expand Down
1 change: 1 addition & 0 deletions app/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class AppServiceProvider extends ServiceProvider
'layout-cache' => Singletons\LayoutCache::class,
'medals' => Singletons\Medals::class,
'smilies' => Singletons\Smilies::class,
'user-cover-presets' => Singletons\UserCoverPresets::class,
];

const SINGLETONS = [
Expand Down
62 changes: 62 additions & 0 deletions app/Singletons/UserCoverPresets.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

declare(strict_types=1);

namespace App\Singletons;

use App\Models\User;
use App\Models\UserCoverPreset;
use App\Traits\Memoizes;
use App\Transformers\UserCoverPresetTransformer;
use Illuminate\Support\Collection;

class UserCoverPresets
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wonder if it's worth calling resetMemoized() when the covers are updated in the controller?

{
use Memoizes;

public function defaultForUser(User $user): UserCoverPreset
{
$userId = max(0, $user->getKey() ?? 0);

$active = $this->allActive();
$count = $active->count();

return $count === 0
? new UserCoverPreset()
: $active[$userId % $count];
}

public function find(int $id): ?UserCoverPreset
{
$allById = $this->memoize(
__FUNCTION__,
fn () => UserCoverPreset::all()->keyBy('id'),
);

// use key check as it may be null
if (!$allById->has($id)) {
$allById[$id] = UserCoverPreset::find($id);
}

return $allById[$id];
}

public function json(): array
{
return $this->memoize(
__FUNCTION__,
fn () => json_collection($this->allActive(), new UserCoverPresetTransformer()),
);
}

private function allActive(): Collection
{
return $this->memoize(
__FUNCTION__,
fn () => UserCoverPreset::active()->orderBy('id')->get(),
);
}
}
3 changes: 2 additions & 1 deletion app/Transformers/UserCompactTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,8 @@ public function includeCover(User $user)
return $this->primitive([
'custom_url' => $cover->customUrl(),
'url' => $cover->url(),
'id' => $cover->presetId(),
// cast to string for backward compatibility
'id' => get_string($user->cover_preset_id),
]);
}

Expand Down
22 changes: 22 additions & 0 deletions app/Transformers/UserCoverPresetTransformer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

declare(strict_types=1);

namespace App\Transformers;

use App\Models\UserCoverPreset;

class UserCoverPresetTransformer extends TransformerAbstract
{
public function transform(UserCoverPreset $preset)
{
return [
'active' => $preset->active,
'id' => $preset->getKey(),
'url' => $preset->file()->url(),
];
}
}
Binary file removed public/images/headers/profile-covers/c1.jpg
Binary file not shown.
Binary file removed public/images/headers/profile-covers/c1t.jpg
Binary file not shown.
Binary file removed public/images/headers/profile-covers/c2.jpg
Binary file not shown.
Binary file removed public/images/headers/profile-covers/c2t.jpg
Binary file not shown.
Binary file removed public/images/headers/profile-covers/c3.jpg
Binary file not shown.
Binary file removed public/images/headers/profile-covers/c3t.jpg
Binary file not shown.
Binary file removed public/images/headers/profile-covers/c4.jpg
Binary file not shown.
Binary file removed public/images/headers/profile-covers/c4t.jpg
Binary file not shown.
Binary file removed public/images/headers/profile-covers/c5.jpg
Binary file not shown.
Binary file removed public/images/headers/profile-covers/c5t.jpg
Binary file not shown.
Binary file removed public/images/headers/profile-covers/c6.jpg
Binary file not shown.
Binary file removed public/images/headers/profile-covers/c6t.jpg
Binary file not shown.
Binary file removed public/images/headers/profile-covers/c7.jpg
Binary file not shown.
Binary file removed public/images/headers/profile-covers/c7t.jpg
Binary file not shown.
Binary file removed public/images/headers/profile-covers/c8.jpg
Binary file not shown.
Binary file removed public/images/headers/profile-covers/c8t.jpg
Binary file not shown.
8 changes: 8 additions & 0 deletions resources/js/interfaces/user-cover-preset-json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

export default interface UserCoverPresetJson {
active: boolean;
id: number;
url: null | string;
}
6 changes: 5 additions & 1 deletion resources/js/profile-page/controller.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import KudosuHistoryJson from 'interfaces/kudosu-history-json';
import { ScoreCurrentUserPinJson } from 'interfaces/score-json';
import SoloScoreJson, { isSoloScoreJsonForUser, SoloScoreJsonForUser } from 'interfaces/solo-score-json';
import UserCoverJson from 'interfaces/user-cover-json';
import UserCoverPresetJson from 'interfaces/user-cover-preset-json';
import { ProfileExtraPage, profileExtraPages } from 'interfaces/user-extended-json';
import UserMonthlyPlaycountJson from 'interfaces/user-monthly-playcount-json';
import UserReplaysWatchedCountJson from 'interfaces/user-replays-watched-count-json';
Expand Down Expand Up @@ -67,6 +68,7 @@ interface InitialData {
current_mode: GameMode;
scores_notice: string | null;
user: ProfilePageUserJson;
user_cover_presets: UserCoverPresetJson[];
}

interface LazyPages {
Expand Down Expand Up @@ -103,6 +105,7 @@ export default class Controller {
@observable isUpdatingCover = false;
readonly scoresNotice: string | null;
@observable readonly state: State;
readonly userCoverPresets;
private xhr: Partial<Record<string, JQuery.jqXHR<unknown>>> = {};

get canUploadCover() {
Expand Down Expand Up @@ -137,6 +140,7 @@ export default class Controller {
this.currentMode = initialData.current_mode;
this.scoresNotice = initialData.scores_notice;
this.displayCoverUrl = this.state.user.cover.url;
this.userCoverPresets = initialData.user_cover_presets;

makeObservable(this);

Expand Down Expand Up @@ -191,7 +195,7 @@ export default class Controller {
}

@action
apiSetCover(id: string) {
apiSetCover(id: number) {
this.isUpdatingCover = true;

this.xhr.setCover?.abort();
Expand Down
22 changes: 17 additions & 5 deletions resources/js/profile-page/cover-selection.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

import { action, makeObservable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { classWithModifiers, Modifiers, urlPresence } from 'utils/css';
import { trans } from 'utils/lang';
import Controller from './controller';

const bn = 'profile-cover-selection';

interface Props {
confirmUpdate: boolean;
controller: Controller;
id: number;
isSelected: boolean;
modifiers?: Modifiers;
name: string;
thumbUrl: string | null;
url: string | null;
}

@observer
export default class CoverSelection extends React.PureComponent<Props> {
constructor(props: Props) {
super(props);
makeObservable(this);
}

render() {
return (
<button
Expand All @@ -27,7 +34,7 @@ export default class CoverSelection extends React.PureComponent<Props> {
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
style={{
backgroundImage: urlPresence(this.props.thumbUrl),
backgroundImage: urlPresence(this.props.url),
}}
>
{this.props.isSelected && (
Expand All @@ -39,10 +46,15 @@ export default class CoverSelection extends React.PureComponent<Props> {
);
}

@action
private readonly onClick = () => {
if (this.props.url == null) return;
if (this.props.url == null || this.props.isSelected) return;

if (this.props.confirmUpdate && !confirm(trans('users.show.edit.cover.holdover_remove_confirm'))) {
return;
}

this.props.controller.apiSetCover(this.props.name);
this.props.controller.apiSetCover(this.props.id);
};

private readonly onMouseEnter = () => {
Expand Down