diff --git a/app/Http/Controllers/BeatmapsController.php b/app/Http/Controllers/BeatmapsController.php index 33b76791953..58ba53c7a35 100644 --- a/app/Http/Controllers/BeatmapsController.php +++ b/app/Http/Controllers/BeatmapsController.php @@ -25,7 +25,7 @@ class BeatmapsController extends Controller { const DEFAULT_API_INCLUDES = ['beatmapset.ratings', 'failtimes', 'max_combo']; - const DEFAULT_SCORE_INCLUDES = ['user', 'user.country', 'user.cover']; + const DEFAULT_SCORE_INCLUDES = ['user', 'user.country', 'user.cover', 'user.team']; public function __construct() { @@ -75,7 +75,7 @@ private static function beatmapScores(string $id, ?string $scoreTransformerType, 'type' => $type, 'user' => $currentUser, ]); - $scores = $esFetch->all()->loadMissing(['beatmap', 'user.country', 'processHistory']); + $scores = $esFetch->all()->loadMissing(['beatmap', 'user.team', 'processHistory']); $userScore = $esFetch->userBest(); $scoreTransformer = new ScoreTransformer($scoreTransformerType); diff --git a/app/Http/Controllers/Forum/TopicsController.php b/app/Http/Controllers/Forum/TopicsController.php index d8c4e34b628..ce704bcfe5c 100644 --- a/app/Http/Controllers/Forum/TopicsController.php +++ b/app/Http/Controllers/Forum/TopicsController.php @@ -396,6 +396,7 @@ public function show($id) 'user.country', 'user.rank', 'user.supporterTagPurchases', + 'user.team', 'user.userGroups', ]); diff --git a/app/Http/Controllers/RankingController.php b/app/Http/Controllers/RankingController.php index 51980dc82d8..aa5c98fa3ac 100644 --- a/app/Http/Controllers/RankingController.php +++ b/app/Http/Controllers/RankingController.php @@ -174,7 +174,7 @@ public function index($mode, $type) $table = (new $class())->getTable(); $ppColumn = $class::ppColumn(); $stats = $class - ::with(['user', 'user.country']) + ::with(['user', 'user.team']) ->where($ppColumn, '>', 0) ->whereHas('user', function ($userQuery) { $userQuery->default(); diff --git a/app/Http/Controllers/UsersController.php b/app/Http/Controllers/UsersController.php index cd72c10642a..b358af1928c 100644 --- a/app/Http/Controllers/UsersController.php +++ b/app/Http/Controllers/UsersController.php @@ -931,6 +931,7 @@ private function showUserIncludes() 'statistics.country_rank', 'statistics.rank', 'statistics.variants', + 'team', 'user_achievements', ]; diff --git a/app/Models/DeletedUser.php b/app/Models/DeletedUser.php index 56c14151d46..4e1e3252502 100644 --- a/app/Models/DeletedUser.php +++ b/app/Models/DeletedUser.php @@ -7,6 +7,7 @@ class DeletedUser extends User { + public null $team = null; public $user_avatar = null; public $username = '[deleted user]'; diff --git a/app/Models/Multiplayer/Room.php b/app/Models/Multiplayer/Room.php index 8ea4ebc3674..1cf6bd075ec 100644 --- a/app/Models/Multiplayer/Room.php +++ b/app/Models/Multiplayer/Room.php @@ -676,7 +676,7 @@ public function startPlay(User $user, PlaylistItem $playlistItem, int $buildId) public function topScores() { - return $this->userHighScores()->forRanking()->with('user.country'); + return $this->userHighScores()->forRanking()->with(['user.country', 'user.team']); } private function assertHostRoomAllowance() diff --git a/app/Models/Spotlight.php b/app/Models/Spotlight.php index 3fe43f972db..1c470a04442 100644 --- a/app/Models/Spotlight.php +++ b/app/Models/Spotlight.php @@ -96,7 +96,7 @@ public function ranking(string $mode) // These models will not have the correct table name set on them // as they get overriden when Laravel hydrates them. return $this->userStats($mode) - ->with(['user', 'user.country']) + ->with(['user', 'user.country', 'user.team']) ->whereHas('user', function ($userQuery) { $model = new User(); $userQuery diff --git a/app/Models/User.php b/app/Models/User.php index 4b951acefbf..d8da32fd2cb 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -37,6 +37,7 @@ use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasOne; +use Illuminate\Database\Eloquent\Relations\HasOneThrough; use Illuminate\Database\QueryException; use Laravel\Passport\HasApiTokens; use League\OAuth2\Server\Exception\OAuthServerException; @@ -297,6 +298,18 @@ public function userCountryHistory(): HasMany return $this->hasMany(UserCountryHistory::class); } + public function team(): HasOneThrough + { + return $this->hasOneThrough( + Team::class, + TeamMember::class, + 'user_id', + 'id', + 'user_id', + 'team_id', + ); + } + public function teamMembership(): HasOne { return $this->hasOne(TeamMember::class, 'user_id'); @@ -957,6 +970,7 @@ public function getAttribute($key) 'storeAddresses', 'supporterTagPurchases', 'supporterTags', + 'team', 'teamMembership', 'tokens', 'topicWatches', diff --git a/app/Transformers/CurrentUserTransformer.php b/app/Transformers/CurrentUserTransformer.php index 6e89c338df2..beccb8c54b2 100644 --- a/app/Transformers/CurrentUserTransformer.php +++ b/app/Transformers/CurrentUserTransformer.php @@ -16,6 +16,7 @@ public function __construct() 'friends', 'groups', 'is_admin', + 'team', 'unread_pm_count', 'user_preferences', ]; diff --git a/app/Transformers/TeamTransformer.php b/app/Transformers/TeamTransformer.php new file mode 100644 index 00000000000..e2e1bb15055 --- /dev/null +++ b/app/Transformers/TeamTransformer.php @@ -0,0 +1,23 @@ +. 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\Team; + +class TeamTransformer extends TransformerAbstract +{ + public function transform(Team $team): array + { + return [ + 'id' => $team->getKey(), + 'logo' => $team->logo()->url(), + 'name' => $team->name, + 'short_name' => $team->short_name, + ]; + } +} diff --git a/app/Transformers/UserCompactTransformer.php b/app/Transformers/UserCompactTransformer.php index 21df9c260e5..de346531342 100644 --- a/app/Transformers/UserCompactTransformer.php +++ b/app/Transformers/UserCompactTransformer.php @@ -19,10 +19,12 @@ class UserCompactTransformer extends TransformerAbstract 'country', 'cover', 'groups', + 'team', ]; const CARD_INCLUDES_PRELOAD = [ 'userGroups', + 'team', ]; // Paired with static::listIncludesPreload @@ -92,6 +94,7 @@ class UserCompactTransformer extends TransformerAbstract 'statistics', 'statistics_rulesets', 'support_level', + 'team', 'unread_pm_count', 'user_achievements', 'user_preferences', @@ -454,6 +457,15 @@ public function includeSupportLevel(User $user) return $this->primitive($user->supportLevel()); } + public function includeTeam(User $user) + { + $team = $user->team; + + return $team === null + ? $this->null() + : $this->item($team, new TeamTransformer()); + } + public function includeUnreadPmCount(User $user) { // legacy pm has been turned off diff --git a/resources/css/bem-index.less b/resources/css/bem-index.less index 9ab4c02c49f..6ba39c5602c 100644 --- a/resources/css/bem-index.less +++ b/resources/css/bem-index.less @@ -155,6 +155,7 @@ @import "bem/fileupload"; @import "bem/fixed-bar"; @import "bem/flag-country"; +@import "bem/flag-team"; @import "bem/floating-toolbar"; @import "bem/floating-toolbar-button"; @import "bem/follow-mapper"; diff --git a/resources/css/bem/beatmap-scoreboard-table.less b/resources/css/bem/beatmap-scoreboard-table.less index 35711f1da30..d532919cc3c 100644 --- a/resources/css/bem/beatmap-scoreboard-table.less +++ b/resources/css/bem/beatmap-scoreboard-table.less @@ -184,11 +184,6 @@ } &--user-link { - .link-inverted(); - .link-hover({ - text-decoration: underline; - }); - position: absolute; } @@ -221,4 +216,11 @@ opacity: 1; } } + + &__user-link { + .link-inverted(); + .link-hover({ + text-decoration: underline; + }); + } } diff --git a/resources/css/bem/flag-team.less b/resources/css/bem/flag-team.less new file mode 100644 index 00000000000..3976e0cf4c2 --- /dev/null +++ b/resources/css/bem/flag-team.less @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +.flag-team { + height: 1em; + aspect-ratio: 2; + display: inline-block; + background-size: cover; + border-radius: @border-radius-large; + + &--small { + border-radius: 3px; + } +} diff --git a/resources/css/bem/forum-post-info.less b/resources/css/bem/forum-post-info.less index 22dee15e953..6aeffc7bdf8 100644 --- a/resources/css/bem/forum-post-info.less +++ b/resources/css/bem/forum-post-info.less @@ -37,6 +37,7 @@ width: 100%; display: flex; justify-content: center; + font-size: 20px; // flag size } &--group-badge { diff --git a/resources/css/bem/profile-info.less b/resources/css/bem/profile-info.less index 7cd9658deeb..1849acaf00e 100644 --- a/resources/css/bem/profile-info.less +++ b/resources/css/bem/profile-info.less @@ -111,6 +111,7 @@ } &__flag-flag { + display: contents; font-size: var(--icon-height); // icon size } diff --git a/resources/css/bem/ranking-page-table.less b/resources/css/bem/ranking-page-table.less index e27cb807bba..7bf75a1ba4f 100644 --- a/resources/css/bem/ranking-page-table.less +++ b/resources/css/bem/ranking-page-table.less @@ -105,6 +105,12 @@ margin-left: 10px; } + &__flag-team { + display: inline-flex; + margin-left: 10px; + font-size: 20px; // flag size + } + &__user-link { display: flex; align-items: center; diff --git a/resources/js/beatmapsets-show/scoreboard/table-row.tsx b/resources/js/beatmapsets-show/scoreboard/table-row.tsx index a26908235e0..16cc334abaa 100644 --- a/resources/js/beatmapsets-show/scoreboard/table-row.tsx +++ b/resources/js/beatmapsets-show/scoreboard/table-row.tsx @@ -2,10 +2,12 @@ // See the LICENCE file in the repository root for full licence text. import FlagCountry from 'components/flag-country'; +import FlagTeam from 'components/flag-team'; import Mod from 'components/mod'; import { PlayDetailMenu } from 'components/play-detail-menu'; import ScoreValue from 'components/score-value'; import ScoreboardTime from 'components/scoreboard-time'; +import UserLink from 'components/user-link'; import BeatmapJson from 'interfaces/beatmap-json'; import { SoloScoreJsonForBeatmap } from 'interfaces/solo-score-json'; import { route } from 'laroute'; @@ -113,14 +115,23 @@ export default class ScoreboardTableRow extends React.Component { ) : ( - - {score.user.username} - - + + + {score.user.team != null && + <> + + + + {' '} + + } + + + )} diff --git a/resources/js/beatmapsets-show/scoreboard/top-card.tsx b/resources/js/beatmapsets-show/scoreboard/top-card.tsx index 68a87e5cadb..5bcdfed6163 100644 --- a/resources/js/beatmapsets-show/scoreboard/top-card.tsx +++ b/resources/js/beatmapsets-show/scoreboard/top-card.tsx @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. import FlagCountry from 'components/flag-country'; +import FlagTeam from 'components/flag-team'; import Mod from 'components/mod'; import ScorePin from 'components/score-pin'; import ScoreValue from 'components/score-value'; @@ -80,19 +81,30 @@ export default class TopCard extends React.PureComponent { /> - - - +
+ + + + + {this.props.score.user.team != null && + + + + } +
diff --git a/resources/js/components/flag-team.tsx b/resources/js/components/flag-team.tsx new file mode 100644 index 00000000000..beba955a73f --- /dev/null +++ b/resources/js/components/flag-team.tsx @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +import TeamJson from 'interfaces/team-json'; +import * as React from 'react'; +import { classWithModifiers, Modifiers } from 'utils/css'; + +interface Props { + modifiers?: Modifiers; + team: TeamJson; +} + +export default function TeamCountry({ team, modifiers }: Props) { + return ( + + ); +} diff --git a/resources/js/components/user-card.tsx b/resources/js/components/user-card.tsx index 8c5ca1ac5a4..403ef104f45 100644 --- a/resources/js/components/user-card.tsx +++ b/resources/js/components/user-card.tsx @@ -14,6 +14,7 @@ import { trans } from 'utils/lang'; import { present } from 'utils/string'; import { giftSupporterTagUrl } from 'utils/url'; import FlagCountry from './flag-country'; +import FlagTeam from './flag-team'; import FollowUserMappingButton from './follow-user-mapping-button'; import { PopupMenuPersistent } from './popup-menu-persistent'; import { ReportReportable } from './report-reportable'; @@ -226,6 +227,15 @@ export class UserCard extends React.PureComponent { + {this.user.team != null && ( + + + + )} + {this.props.mode === 'card' && ( <> {this.user.is_supporter && ( diff --git a/resources/js/interfaces/team-json.ts b/resources/js/interfaces/team-json.ts new file mode 100644 index 00000000000..16270293c1b --- /dev/null +++ b/resources/js/interfaces/team-json.ts @@ -0,0 +1,9 @@ +// Copyright (c) ppy Pty Ltd . 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 TeamJson { + id: number; + logo: string | null; + name: string; + short_name: string; +} diff --git a/resources/js/interfaces/user-json.ts b/resources/js/interfaces/user-json.ts index 8f21baba7a9..cc1c9a9ef37 100644 --- a/resources/js/interfaces/user-json.ts +++ b/resources/js/interfaces/user-json.ts @@ -6,6 +6,7 @@ import DailyChallengeUserStatsJson from './daily-challenge-user-stats-json'; import ProfileBannerJson from './profile-banner'; import RankHighestJson from './rank-highest-json'; import RankHistoryJson from './rank-history-json'; +import TeamJson from './team-json'; import UserAccountHistoryJson from './user-account-history-json'; import UserAchievementJson from './user-achievement-json'; import UserBadgeJson from './user-badge-json'; @@ -66,6 +67,7 @@ interface UserJsonAvailableIncludes { statistics: UserStatisticsJson; statistics_rulesets: UserStatisticsRulesetsJson; support_level: number; + team: TeamJson; unread_pm_count: number; user_achievements: UserAchievementJson[]; user_preferences: UserPreferencesJson; diff --git a/resources/js/profile-page/cover.tsx b/resources/js/profile-page/cover.tsx index d342b7ba233..bac7e06ab70 100644 --- a/resources/js/profile-page/cover.tsx +++ b/resources/js/profile-page/cover.tsx @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. import FlagCountry from 'components/flag-country'; +import FlagTeam from 'components/flag-team'; import { Spinner } from 'components/spinner'; import UserAvatar from 'components/user-avatar'; import UserGroupBadges from 'components/user-group-badges'; @@ -93,6 +94,17 @@ export default class Cover extends React.Component { {this.props.user.country.name} } + {this.props.user.team != null && + + + + + {this.props.user.team.name} + + }
{this.renderIcons()}
diff --git a/resources/views/forum/topics/_post_info.blade.php b/resources/views/forum/topics/_post_info.blade.php index 1cd1800a49d..db4bd562cbd 100644 --- a/resources/views/forum/topics/_post_info.blade.php +++ b/resources/views/forum/topics/_post_info.blade.php @@ -71,6 +71,17 @@ class="forum-post-info__row forum-post-info__row--title" @endif + @if (($team = $user->team) !== null) + + @endif + @if ($user->country !== null) @endif diff --git a/resources/views/multiplayer/rooms/_rankings_table.blade.php b/resources/views/multiplayer/rooms/_rankings_table.blade.php index 24132cb2d99..4ee3d494476 100644 --- a/resources/views/multiplayer/rooms/_rankings_table.blade.php +++ b/resources/views/multiplayer/rooms/_rankings_table.blade.php @@ -39,6 +39,16 @@ 'country' => $score->user->country, 'modifiers' => 'medium', ]) + @if (($team = $score->user->team) !== null) + + logo()->url(), false) !!} + > + + + @endif $score->user->country, 'modifiers' => 'medium', ]) + @if (($team = $score->user->team) !== null) + + logo()->url(), false) !!} + > + +
+ @endif $user->country_acronym, 'modifiers' => 'medium', ]) + @if (($team = $user->team) !== null) + + logo()->url(), false) !!} + > + + + @endif {!! link_to_user($user, null, '', ['ranking-page-table__user-link-text']) !!} diff --git a/resources/views/rankings/performance.blade.php b/resources/views/rankings/performance.blade.php index b957f678581..b8df007e472 100644 --- a/resources/views/rankings/performance.blade.php +++ b/resources/views/rankings/performance.blade.php @@ -79,6 +79,9 @@ + @php + $countries = app('countries'); + @endphp @foreach ($scores as $index => $score) @@ -112,15 +115,25 @@ class="{{ class_with_modifiers('ranking-page-table__column', 'rank-change-icon', href="{{ route('rankings', [ 'mode' => $mode, 'type' => 'performance', - 'country' => $score->user->country->acronym, + 'country' => $score->user->country_acronym, 'variant' => $variant, ]) }}" > @include('objects._flag_country', [ - 'country' => $score->user->country, + 'country' => $countries->byCode($score->user->country_acronym), 'modifiers' => 'medium', ]) + @if (($team = $score->user->team) !== null) + + logo()->url(), false) !!} + > + + + @endif + @php + $countries = app('countries'); + @endphp @foreach ($scores as $index => $score) @@ -49,12 +52,22 @@