diff --git a/app/Http/Controllers/BeatmapsController.php b/app/Http/Controllers/BeatmapsController.php index dca765187d0..be0feca32b1 100644 --- a/app/Http/Controllers/BeatmapsController.php +++ b/app/Http/Controllers/BeatmapsController.php @@ -24,7 +24,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() { @@ -74,7 +74,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.country', '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..ad64bb83332 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.country', 'user.team']) ->where($ppColumn, '>', 0) ->whereHas('user', function ($userQuery) { $userQuery->default(); @@ -307,6 +307,7 @@ public function kudosu() $page = min(get_int(request('page')) ?? 1, $maxPage); $scores = User::default() + ->with('team') ->orderBy('osu_kudostotal', 'desc') ->paginate(static::PAGE_SIZE, ['*'], 'page', $page, $maxResults); 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 306f4710502..737b66a56e6 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; @@ -127,7 +128,7 @@ * @property-read Collection $storeAddresses * @property-read Collection $supporterTagPurchases * @property-read Collection $supporterTags - * @property-read TeamMember|null $teamMembership + * @property-read Team|null $team * @property-read Collection $tokens * @property-read Collection $topicWatches * @property-read Collection $userAchievements @@ -297,9 +298,16 @@ public function userCountryHistory(): HasMany return $this->hasMany(UserCountryHistory::class); } - public function teamMembership(): HasOne + public function team(): HasOneThrough { - return $this->hasOne(TeamMember::class, 'user_id'); + return $this->hasOneThrough( + Team::class, + TeamMember::class, + 'user_id', + 'id', + 'user_id', + 'team_id', + ); } public function getAuthPassword() @@ -958,7 +966,7 @@ public function getAttribute($key) 'storeAddresses', 'supporterTagPurchases', 'supporterTags', - 'teamMembership', + 'team', 'tokens', 'topicWatches', 'userAchievements', 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..e18c90cf8f3 100644 --- a/app/Transformers/UserCompactTransformer.php +++ b/app/Transformers/UserCompactTransformer.php @@ -19,9 +19,11 @@ class UserCompactTransformer extends TransformerAbstract 'country', 'cover', 'groups', + 'team', ]; const CARD_INCLUDES_PRELOAD = [ + 'team', 'userGroups', ]; @@ -92,6 +94,7 @@ class UserCompactTransformer extends TransformerAbstract 'statistics', 'statistics_rulesets', 'support_level', + 'team', 'unread_pm_count', 'user_achievements', 'user_preferences', @@ -454,6 +457,13 @@ public function includeSupportLevel(User $user) return $this->primitive($user->supportLevel()); } + public function includeTeam(User $user) + { + return ($team = $user->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 6371fa373e9..0228a0d00a2 100644 --- a/resources/css/bem-index.less +++ b/resources/css/bem-index.less @@ -157,6 +157,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..75adb4e6dec 100644 --- a/resources/css/bem/beatmap-scoreboard-table.less +++ b/resources/css/bem/beatmap-scoreboard-table.less @@ -184,12 +184,9 @@ } &--user-link { - .link-inverted(); - .link-hover({ - text-decoration: underline; - }); - position: absolute; + display: flex; + gap: 5px; } &--zero { @@ -221,4 +218,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..ac8ad25ace0 --- /dev/null +++ b/resources/css/bem/flag-team.less @@ -0,0 +1,10 @@ +// 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; + width: 2em; + display: inline-block; + background-size: contain; + border-radius: min(0.25em, @border-radius-large); +} diff --git a/resources/css/bem/profile-info.less b/resources/css/bem/profile-info.less index ed0ea1854d2..802b81ba63c 100644 --- a/resources/css/bem/profile-info.less +++ b/resources/css/bem/profile-info.less @@ -123,7 +123,7 @@ &__flags { display: flex; - gap: 5px; + gap: 10px; margin-top: 10px; font-size: 15px; // icon size diff --git a/resources/css/bem/ranking-page-table.less b/resources/css/bem/ranking-page-table.less index e27cb807bba..5129952b76e 100644 --- a/resources/css/bem/ranking-page-table.less +++ b/resources/css/bem/ranking-page-table.less @@ -105,6 +105,12 @@ margin-left: 10px; } + &__flags { + display: inline-flex; + gap: 10px; + font-size: @flag-size-medium; // icon 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..72ada1397b6 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,18 @@ 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 7a9ea4359fb..6250d4d57e4 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'; @@ -94,6 +95,15 @@ export default class TopCard extends React.PureComponent { modifiers='flat' /> + + {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..57f3ff244c8 --- /dev/null +++ b/resources/js/components/flag-team.tsx @@ -0,0 +1,15 @@ +// 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'; + +export default function FlagTeam({ team }: { team: TeamJson }) { + 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 137f8c2d817..75cc94c65c4 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 fd74bb3d387..ef2dddff87c 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'; @@ -91,6 +92,15 @@ 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 8e2b5c4c3cc..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)