the browser-facing portion of osu!
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge remote-tracking branch 'origin/master' into spotlights-sort

nanaya 6323b5aa bbc161c4

+286 -203
+1 -1
app/Http/Controllers/MatchesController.php
··· 30 30 ->getWithHasMore(); 31 31 32 32 return [ 33 - 'cursor' => $hasMore ? $cursorHelper->next($matches) : null, 34 33 'matches' => json_collection($matches, 'LegacyMatch\LegacyMatch'), 35 34 'params' => ['limit' => $limit, 'sort' => $cursorHelper->getSortName()], 35 + ...cursor_for_response($cursorHelper->next($matches, $hasMore)), 36 36 ]; 37 37 } 38 38
+3 -4
app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php
··· 74 74 } 75 75 } 76 76 77 - $nextCursor = $hasMore ? $cursorHelper->next($highScores) : null; 78 - 79 - return array_merge([ 77 + return [ 80 78 'params' => ['limit' => $limit, 'sort' => $cursorHelper->getSortName()], 81 79 'scores' => $scoresJson, 82 80 'total' => $total, 83 81 'user_score' => $userScoreJson ?? null, 84 - ], cursor_for_response($nextCursor)); 82 + ...cursor_for_response($cursorHelper->next($highScores, $hasMore)), 83 + ]; 85 84 } 86 85 87 86 /**
+3 -3
app/Http/Controllers/NewsController.php
··· 93 93 return ext_view("news.index-{$format}", compact('posts'), $format); 94 94 } 95 95 96 - $nextCursor = $hasMore ? $search['cursorHelper']->next($posts) : null; 97 - $postsJson = array_merge([ 96 + $postsJson = [ 98 97 'news_posts' => json_collection($posts, 'NewsPost', ['preview']), 99 98 'news_sidebar' => $this->sidebarMeta($posts[0] ?? null), 100 99 'search' => $search['params'], 101 - ], cursor_for_response($nextCursor)); 100 + ...cursor_for_response($search['cursorHelper']->next($posts, $hasMore)), 101 + ]; 102 102 103 103 if (is_json_request()) { 104 104 return $postsJson;
+5 -1
app/Libraries/DbCursorHelper.php
··· 75 75 return $ret; 76 76 } 77 77 78 - public function next($items) 78 + public function next($items, bool $hasMore = true) 79 79 { 80 + if (!$hasMore) { 81 + return null; 82 + } 83 + 80 84 if (is_array($items)) { 81 85 $lastItem = array_last($items); 82 86 } elseif ($items instanceof Collection) {
+9 -6
app/Models/BeatmapPack.php
··· 21 21 class BeatmapPack extends Model 22 22 { 23 23 const DEFAULT_TYPE = 'standard'; 24 - private static $tagMappings = [ 24 + 25 + // also display order for listing page 26 + const TAG_MAPPINGS = [ 25 27 'standard' => 'S', 28 + 'featured' => 'F', 29 + 'chart' => 'R', 26 30 'theme' => 'T', 27 31 'artist' => 'A', 28 - 'chart' => 'R', 29 32 ]; 30 33 31 34 protected $table = 'osu_beatmappacks'; ··· 41 44 42 45 public static function getPacks($type) 43 46 { 44 - if (!in_array($type, array_keys(static::$tagMappings), true)) { 45 - return; 46 - } 47 + $tag = static::TAG_MAPPINGS[$type] ?? null; 47 48 48 - $tag = static::$tagMappings[$type]; 49 + if ($tag === null) { 50 + return null; 51 + } 49 52 50 53 return static::default()->where('tag', 'like', "{$tag}%")->orderBy('pack_id', 'desc'); 51 54 }
+4 -3
app/Models/Multiplayer/Room.php
··· 128 128 $response['type_group'] = $typeGroup; 129 129 } 130 130 131 - $nextCursor = $hasMore ? $search['cursorHelper']->next($rooms) : null; 132 - 133 - return array_merge($response, cursor_for_response($nextCursor)); 131 + return [ 132 + ...$response, 133 + ...cursor_for_response($search['cursorHelper']->next($rooms, $hasMore)), 134 + ]; 134 135 } 135 136 136 137 public static function search(array $rawParams, ?int $maxLimit = null)
+3 -3
app/Transformers/Multiplayer/ScoreTransformer.php
··· 74 74 ->limit($limit) 75 75 ->getWithHasMore(); 76 76 77 - $nextCursor = $hasMore ? $cursorHelper->next($highScores) : null; 78 - $ret[$type] = array_merge([ 77 + $ret[$type] = [ 79 78 'scores' => json_collection($highScores->pluck('score'), new static(), static::BASE_INCLUDES), 80 79 'params' => ['limit' => $limit, 'sort' => $cursorHelper->getSortName()], 81 - ], cursor_for_response($nextCursor)); 80 + ...cursor_for_response($cursorHelper->next($highScores, $hasMore)), 81 + ]; 82 82 } 83 83 84 84 return $this->primitive($ret);
+15
app/helpers.php
··· 780 780 return starts_with(request()->headers->get('referer'), config('app.url').'/'); 781 781 } 782 782 783 + function forum_user_link(int $id, string $username, string|null $colour, int|null $currentUserId): string 784 + { 785 + $icon = tag('span', [ 786 + 'class' => 'forum-user-icon', 787 + 'style' => user_color_style($colour, 'background-color'), 788 + ]); 789 + 790 + $link = link_to_user($id, $username, null, []); 791 + if ($currentUserId === $id) { 792 + $link = tag('strong', null, $link); 793 + } 794 + 795 + return "{$icon} {$link}"; 796 + } 797 + 783 798 function is_api_request() 784 799 { 785 800 return request()->is('api/*');
+1
resources/css/bem/profile-info.less
··· 153 153 } 154 154 155 155 &__info { 156 + .default-text-shadow(); 156 157 margin-left: var(--info-margin); 157 158 display: flex; 158 159 flex-direction: column;
+1
resources/css/bem/profile-previous-usernames.less
··· 11 11 display: flex; 12 12 align-items: baseline; 13 13 transition: all 200ms cubic-bezier(.22,.61,.36,1); 14 + text-shadow: initial; 14 15 15 16 @media @desktop { 16 17 width: 300px;
+36 -15
resources/js/beatmapsets-show/header.tsx
··· 9 9 import { UserLink } from 'components/user-link'; 10 10 import UserListPopup, { createTooltip } from 'components/user-list-popup'; 11 11 import { route } from 'laroute'; 12 - import { action, computed, makeObservable } from 'mobx'; 13 - import { observer } from 'mobx-react'; 12 + import { action, autorun, computed, makeObservable, observable } from 'mobx'; 13 + import { disposeOnUnmount, observer } from 'mobx-react'; 14 14 import core from 'osu-core-singleton'; 15 15 import * as React from 'react'; 16 16 import { renderToStaticMarkup } from 'react-dom/server'; ··· 40 40 41 41 @observer 42 42 export default class Header extends React.Component<Props> { 43 + private readonly favouriteIconRef = React.createRef<HTMLSpanElement>(); 44 + @observable private hoveredFavouriteIcon = false; 45 + 43 46 private get controller() { 44 47 return this.props.controller; 45 48 } ··· 73 76 makeObservable(this); 74 77 } 75 78 79 + componentDidMount() { 80 + disposeOnUnmount(this, autorun(this.updateFavouritePopup)); 81 + } 82 + 76 83 render() { 77 84 const favouriteButton = this.controller.beatmapset.has_favourited 78 85 ? { ··· 117 124 } 118 125 119 126 <span 127 + ref={this.favouriteIconRef} 120 128 className={classWithModifiers('beatmapset-header__value', { 'has-favourites': this.controller.beatmapset.favourite_count > 0 })} 121 129 onMouseOver={this.onEnterFavouriteIcon} 122 130 onTouchStart={this.onEnterFavouriteIcon} ··· 222 230 }; 223 231 224 232 @action 225 - private readonly onEnterFavouriteIcon = (event: React.MouseEvent<HTMLSpanElement> | React.TouchEvent<HTMLSpanElement>) => { 226 - const target = event.currentTarget; 227 - 228 - if (this.filteredFavourites.length < 1) { 229 - if (target._tooltip === '1') { 230 - target._tooltip = ''; 231 - $(target).qtip('destroy', true); 232 - } 233 - 234 - return; 235 - } 236 - 237 - createTooltip(target, 'right center', action(() => this.favouritePopup)); 233 + private readonly onEnterFavouriteIcon = () => { 234 + this.hoveredFavouriteIcon = true; 238 235 }; 239 236 240 237 private renderAvailabilityInfo() { ··· 364 361 </div> 365 362 ); 366 363 } 364 + 365 + private readonly updateFavouritePopup = () => { 366 + if (!this.hoveredFavouriteIcon) { 367 + return; 368 + } 369 + 370 + const target = this.favouriteIconRef.current; 371 + 372 + if (target == null) { 373 + throw new Error('favourite icon is missing'); 374 + } 375 + 376 + if (this.filteredFavourites.length < 1) { 377 + if (target._tooltip === '1') { 378 + target._tooltip = ''; 379 + $(target).qtip('destroy', true); 380 + } 381 + 382 + return; 383 + } 384 + 385 + createTooltip(target, 'right center', ''); 386 + $(target).qtip('set', { 'content.text': this.favouritePopup }); 387 + }; 367 388 }
+1 -1
resources/js/components/beatmapset-events.tsx
··· 6 6 import UserJson from 'interfaces/user-json'; 7 7 import * as React from 'react'; 8 8 9 - interface Props { 9 + export interface Props { 10 10 events: BeatmapsetEventJson[]; 11 11 mode: EventViewMode; 12 12 users: Partial<Record<string, UserJson>>;
+1 -1
resources/js/components/beatmapset-panel.tsx
··· 29 29 export const beatmapsetCardSizes = ['normal', 'extra'] as const; 30 30 export type BeatmapsetCardSize = typeof beatmapsetCardSizes[number]; 31 31 32 - interface Props { 32 + export interface Props { 33 33 beatmapset: BeatmapsetExtendedJson; 34 34 } 35 35
+10 -8
resources/js/components/user-card.tsx
··· 8 8 import * as _ from 'lodash'; 9 9 import core from 'osu-core-singleton'; 10 10 import * as React from 'react'; 11 - import { classWithModifiers } from 'utils/css'; 11 + import { classWithModifiers, Modifiers } from 'utils/css'; 12 12 import { trans } from 'utils/lang'; 13 13 import { present } from 'utils/string'; 14 14 import FlagCountry from './flag-country'; ··· 28 28 interface Props { 29 29 activated: boolean; 30 30 mode: ViewMode; 31 - modifiers: string[]; 31 + modifiers?: Modifiers; 32 32 user?: UserJson | null; 33 33 } 34 34 ··· 41 41 static defaultProps = { 42 42 activated: false, 43 43 mode: 'card', 44 - modifiers: [], 45 44 }; 46 45 47 46 static userLoading: UserJson = { ··· 114 113 return <UserCardBrick {...this.props} user={this.props.user} />; 115 114 } 116 115 117 - const modifiers = this.props.modifiers.slice(); 118 - // Setting the active modifiers from the parent causes unwanted renders unless deep comparison is used. 119 - modifiers.push(this.props.activated ? 'active' : 'highlightable'); 120 - modifiers.push(this.props.mode); 116 + const blockClass = classWithModifiers( 117 + 'user-card', 118 + this.props.modifiers, 119 + this.props.mode, 120 + // Setting the active modifiers from the parent causes unwanted renders unless deep comparison is used. 121 + this.props.activated ? 'active' : 'highlightable', 122 + ); 121 123 122 124 this.url = this.isUserVisible ? route('users.show', { user: this.user.id }) : undefined; 123 125 124 126 return ( 125 - <div className={classWithModifiers('user-card', modifiers)}> 127 + <div className={blockClass}> 126 128 {this.renderBackground()} 127 129 128 130 <div className='user-card__card'>
+7 -9
resources/js/components/user-cards.tsx
··· 4 4 import UserJson from 'interfaces/user-json'; 5 5 import * as React from 'react'; 6 6 import { activeKeyDidChange, ContainerContext, KeyContext, State as ActiveKeyState } from 'stateful-activation-context'; 7 - import { classWithModifiers } from 'utils/css'; 7 + import { classWithModifiers, mergeModifiers, Modifiers } from 'utils/css'; 8 8 import { UserCard, ViewMode } from './user-card'; 9 9 10 10 interface Props { 11 - modifiers: string[]; 11 + modifiers?: Modifiers; 12 12 users: UserJson[]; 13 13 viewMode: ViewMode; 14 14 } 15 15 16 16 export class UserCards extends React.PureComponent<Props> { 17 - static defaultProps = { 18 - modifiers: [], 19 - }; 20 - 21 17 readonly activeKeyDidChange = activeKeyDidChange.bind(this); 22 18 readonly state: ActiveKeyState = {}; 23 19 24 20 render() { 25 - const classMods = this.state.activeKey != null ? ['menu-active'] : []; 26 - classMods.push(this.props.viewMode); 21 + const classMods = { 22 + 'menu-active': this.state.activeKey != null, 23 + [this.props.viewMode]: true, 24 + }; 27 25 28 26 return ( 29 27 <ContainerContext.Provider value={{ activeKeyDidChange: this.activeKeyDidChange }}> ··· 37 35 <UserCard 38 36 activated={activated} 39 37 mode={this.props.viewMode} 40 - modifiers={['has-outline', ...this.props.modifiers]} 38 + modifiers={mergeModifiers('has-outline', this.props.modifiers)} 41 39 user={user} 42 40 /> 43 41 </KeyContext.Provider>
+1 -1
resources/js/entrypoints/app.ts
··· 21 21 import 'osu-core-singleton'; 22 22 import 'main.coffee'; 23 23 24 - import 'register-components.coffee'; 24 + import 'register-components';
-121
resources/js/register-components.coffee
··· 1 - # Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0. 2 - # See the LICENCE file in the repository root for full licence text. 3 - 4 - import BeatmapsetEvents from 'components/beatmapset-events' 5 - import BeatmapsetPanel from 'components/beatmapset-panel' 6 - import BlockButton from 'components/block-button' 7 - import ChatIcon from 'components/chat-icon' 8 - import { Comments } from 'components/comments' 9 - import { CommentsManager } from 'components/comments-manager' 10 - import CountdownTimer from 'components/countdown-timer' 11 - import { LandingNews } from 'components/landing-news' 12 - import MainNotificationIcon from 'components/main-notification-icon' 13 - import QuickSearchButton from 'components/quick-search-button' 14 - import RankingCountryFilter from 'components/ranking-country-filter' 15 - import RankingSelectOptions from 'components/ranking-select-options' 16 - import RankingUserFilter from 'components/ranking-user-filter' 17 - import RankingVariantFilter from 'components/ranking-variant-filter' 18 - import SpotlightSelectOptions from 'components/spotlight-select-options' 19 - import { UserCard } from 'components/user-card' 20 - import { UserCardStore } from 'components/user-card-store' 21 - import { startListening, UserCardTooltip } from 'components/user-card-tooltip' 22 - import { UserCards } from 'components/user-cards' 23 - import { WikiSearch } from 'components/wiki-search' 24 - import { keyBy } from 'lodash' 25 - import { observable } from 'mobx' 26 - import { deletedUser } from 'models/user' 27 - import NotificationWidget from 'notification-widget/main' 28 - import NotificationWorker from 'notifications/worker' 29 - import QuickSearch from 'quick-search/main' 30 - import QuickSearchWorker from 'quick-search/worker' 31 - import SocketWorker from 'socket-worker' 32 - import core from 'osu-core-singleton' 33 - import { createElement } from 'react' 34 - import { parseJson, parseJsonNullable } from 'utils/json' 35 - 36 - # Globally init countdown timers 37 - core.reactTurbolinks.register 'countdownTimer', (container) -> 38 - createElement CountdownTimer, deadline: container.dataset.deadline 39 - 40 - # Globally init block buttons 41 - core.reactTurbolinks.register 'blockButton', (container) -> 42 - createElement BlockButton, 43 - userId: parseInt(container.dataset.target) 44 - 45 - core.reactTurbolinks.register 'beatmap-discussion-events', (container) -> 46 - props = { 47 - events: parseJson('json-events') 48 - mode: 'list' 49 - } 50 - 51 - # TODO: move to store? 52 - users = parseJson('json-users') 53 - props.users = _.keyBy(users, 'id') 54 - props.users[null] = props.users[undefined] = deletedUser.toJson() 55 - 56 - createElement BeatmapsetEvents, props 57 - 58 - 59 - core.reactTurbolinks.register 'beatmapset-panel', (container) -> 60 - createElement BeatmapsetPanel, observable(JSON.parse(container.dataset.beatmapsetPanel)) 61 - 62 - core.reactTurbolinks.register 'ranking-select-options', -> 63 - createElement RankingSelectOptions, parseJson('json-ranking-select-options') 64 - 65 - core.reactTurbolinks.register 'spotlight-select-options', -> 66 - createElement SpotlightSelectOptions, parseJson('json-spotlight-select-options') 67 - 68 - core.reactTurbolinks.register 'comments', (container) -> 69 - props = JSON.parse(container.dataset.props) 70 - props.component = Comments 71 - 72 - createElement CommentsManager, props 73 - 74 - core.reactTurbolinks.register 'chat-icon', (container) -> 75 - createElement ChatIcon, type: container.dataset.type 76 - 77 - core.reactTurbolinks.register 'main-notification-icon', (container) -> 78 - createElement MainNotificationIcon, type: container.dataset.type 79 - 80 - core.reactTurbolinks.register 'notification-widget', (container) -> 81 - createElement NotificationWidget, (try JSON.parse(container.dataset.notificationWidget)) 82 - 83 - quickSearchWorker = new QuickSearchWorker() 84 - core.reactTurbolinks.register 'quick-search', -> 85 - createElement QuickSearch, worker: quickSearchWorker 86 - 87 - core.reactTurbolinks.register 'quick-search-button', -> 88 - createElement QuickSearchButton, worker: quickSearchWorker 89 - 90 - core.reactTurbolinks.register 'ranking-country-filter', -> 91 - createElement RankingCountryFilter, parseJsonNullable('json-country-filter') 92 - 93 - core.reactTurbolinks.register 'ranking-user-filter', -> 94 - createElement RankingUserFilter, parseJsonNullable('json-user-filter') 95 - 96 - core.reactTurbolinks.register 'ranking-variant-filter', -> 97 - createElement RankingVariantFilter, parseJsonNullable('json-variant-filter') 98 - 99 - core.reactTurbolinks.register 'user-card', (container) -> 100 - createElement UserCard, 101 - modifiers: try JSON.parse(container.dataset.modifiers) 102 - user: if container.dataset.isCurrentUser then currentUser else try JSON.parse(container.dataset.user) 103 - 104 - core.reactTurbolinks.register 'user-card-store', (container) -> 105 - createElement UserCardStore, user: JSON.parse(container.dataset.user) 106 - 107 - core.reactTurbolinks.register 'user-card-tooltip', (container) -> 108 - createElement UserCardTooltip, 109 - container: container 110 - lookup: container.dataset.lookup 111 - 112 - $(document).ready startListening 113 - core.reactTurbolinks.register 'user-cards', (container) -> 114 - createElement UserCards, 115 - modifiers: try JSON.parse(container.dataset.modifiers) 116 - users: try JSON.parse(container.dataset.users) 117 - 118 - core.reactTurbolinks.register 'wiki-search', -> createElement(WikiSearch) 119 - 120 - core.reactTurbolinks.register 'landing-news', -> 121 - createElement LandingNews, posts: parseJson('json-posts')
+156
resources/js/register-components.tsx
··· 1 + // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0. 2 + // See the LICENCE file in the repository root for full licence text. 3 + 4 + import BeatmapsetEvents, { Props as BeatmapsetEventsProps } from 'components/beatmapset-events'; 5 + import BeatmapsetPanel, { Props as BeatmapsetPanelProps } from 'components/beatmapset-panel'; 6 + import BlockButton from 'components/block-button'; 7 + import ChatIcon from 'components/chat-icon'; 8 + import { Comments } from 'components/comments'; 9 + import { CommentsManager, Props as CommentsManagerProps } from 'components/comments-manager'; 10 + import CountdownTimer from 'components/countdown-timer'; 11 + import { LandingNews } from 'components/landing-news'; 12 + import MainNotificationIcon from 'components/main-notification-icon'; 13 + import QuickSearchButton from 'components/quick-search-button'; 14 + import RankingCountryFilter from 'components/ranking-country-filter'; 15 + import RankingSelectOptions from 'components/ranking-select-options'; 16 + import RankingUserFilter from 'components/ranking-user-filter'; 17 + import RankingVariantFilter from 'components/ranking-variant-filter'; 18 + import SpotlightSelectOptions from 'components/spotlight-select-options'; 19 + import { UserCard } from 'components/user-card'; 20 + import { UserCardStore } from 'components/user-card-store'; 21 + import { startListening, UserCardTooltip } from 'components/user-card-tooltip'; 22 + import { UserCards } from 'components/user-cards'; 23 + import { WikiSearch } from 'components/wiki-search'; 24 + import { keyBy } from 'lodash'; 25 + import { observable } from 'mobx'; 26 + import { deletedUser } from 'models/user'; 27 + import NotificationWidget from 'notification-widget/main'; 28 + import core from 'osu-core-singleton'; 29 + import QuickSearch from 'quick-search/main'; 30 + import QuickSearchWorker from 'quick-search/worker'; 31 + import * as React from 'react'; 32 + import { parseJson } from 'utils/json'; 33 + 34 + function reqJson<T>(input: string|undefined): T { 35 + // This will throw when input is missing and thus parsing empty string. 36 + return JSON.parse(input ?? '') as T; 37 + } 38 + 39 + function reqStr(input: string|undefined) { 40 + if (input == null) { 41 + throw new Error('unexpected undefined value'); 42 + } 43 + 44 + return input; 45 + } 46 + 47 + core.reactTurbolinks.register('countdownTimer', (container) => ( 48 + <CountdownTimer deadline={reqStr(container.dataset.deadline)} /> 49 + )); 50 + 51 + core.reactTurbolinks.register('blockButton', (container) => ( 52 + <BlockButton userId={parseInt(reqStr(container.dataset.target), 10)} /> 53 + )); 54 + 55 + core.reactTurbolinks.register('beatmap-discussion-events', () => { 56 + const props: BeatmapsetEventsProps = { 57 + events: parseJson('json-events'), 58 + mode: 'list', 59 + users: keyBy(parseJson('json-users'), 'id'), 60 + }; 61 + 62 + // TODO: move to store? 63 + // eslint-disable-next-line id-blacklist 64 + props.users.null = props.users.undefined = deletedUser.toJson(); 65 + 66 + return <BeatmapsetEvents {...props} />; 67 + }); 68 + 69 + core.reactTurbolinks.register('beatmapset-panel', (container) => { 70 + const props: BeatmapsetPanelProps = reqJson(container.dataset.beatmapsetPanel); 71 + 72 + return <BeatmapsetPanel {...observable(props)} />; 73 + }); 74 + 75 + core.reactTurbolinks.register('ranking-select-options', () => ( 76 + <RankingSelectOptions {...parseJson('json-ranking-select-options')} /> 77 + )); 78 + 79 + core.reactTurbolinks.register('spotlight-select-options', () => ( 80 + <SpotlightSelectOptions {...parseJson('json-spotlight-select-options')} /> 81 + )); 82 + 83 + core.reactTurbolinks.register('comments', (container) => { 84 + const props = { 85 + ...reqJson<Omit<CommentsManagerProps, 'component'>>(container.dataset.props), 86 + component: Comments, 87 + }; 88 + 89 + return <CommentsManager {...props} />; 90 + }); 91 + 92 + core.reactTurbolinks.register('chat-icon', (container) => ( 93 + <ChatIcon type={container.dataset.type} /> 94 + )); 95 + 96 + core.reactTurbolinks.register('main-notification-icon', (container) => ( 97 + <MainNotificationIcon type={container.dataset.type} /> 98 + )); 99 + 100 + core.reactTurbolinks.register('notification-widget', (container) => ( 101 + <NotificationWidget {...reqJson(container.dataset.notificationWidget)} /> 102 + )); 103 + 104 + const quickSearchWorker = new QuickSearchWorker(); 105 + core.reactTurbolinks.register('quick-search', () => ( 106 + <QuickSearch worker={quickSearchWorker} /> 107 + )); 108 + 109 + core.reactTurbolinks.register('quick-search-button', () => ( 110 + <QuickSearchButton worker={quickSearchWorker} /> 111 + )); 112 + 113 + core.reactTurbolinks.register('ranking-country-filter', () => ( 114 + <RankingCountryFilter {...parseJson('json-country-filter')} /> 115 + )); 116 + 117 + core.reactTurbolinks.register('ranking-user-filter', () => ( 118 + <RankingUserFilter {...parseJson('json-user-filter')} /> 119 + )); 120 + 121 + core.reactTurbolinks.register('ranking-variant-filter', () => ( 122 + <RankingVariantFilter {...parseJson('json-variant-filter')} /> 123 + )); 124 + 125 + core.reactTurbolinks.register('user-card', (container) => ( 126 + <UserCard 127 + modifiers={reqJson(container.dataset.modifiers ?? 'null')} 128 + user={container.dataset.isCurrentUser === '1' ? core.currentUser : reqJson(container.dataset.user ?? 'null')} 129 + /> 130 + )); 131 + 132 + core.reactTurbolinks.register('user-card-store', (container) => ( 133 + <UserCardStore user={reqJson(container.dataset.user)} /> 134 + )); 135 + 136 + core.reactTurbolinks.register('user-card-tooltip', (container) => ( 137 + <UserCardTooltip 138 + container={container} 139 + lookup={reqStr(container.dataset.lookup)} 140 + /> 141 + )); 142 + 143 + $(document).ready(startListening); 144 + core.reactTurbolinks.register('user-cards', (container) => ( 145 + <UserCards 146 + modifiers={reqJson(container.dataset.modifiers ?? 'null')} 147 + users={reqJson(container.dataset.users ?? '[]')} 148 + viewMode='card' 149 + /> 150 + )); 151 + 152 + core.reactTurbolinks.register('wiki-search', () => <WikiSearch />); 153 + 154 + core.reactTurbolinks.register('landing-news', () => ( 155 + <LandingNews posts={parseJson('json-posts')} /> 156 + ));
+1
resources/lang/en/beatmappacks.php
··· 34 34 'mode' => [ 35 35 'artist' => 'Artist/Album', 36 36 'chart' => 'Spotlights', 37 + 'featured' => 'Featured Artist', 37 38 'standard' => 'Standard', 38 39 'theme' => 'Theme', 39 40 ],
+3 -6
resources/views/forum/forums/_forum.blade.php
··· 44 44 <div> 45 45 {!! osu_trans('forum.topic.latest_post', [ 46 46 'when' => timeago($lastTopic->topic_last_post_time), 47 - 'user' => tag('span', [ 48 - 'class' => 'forum-user-icon', 49 - 'style' => user_color_style($lastTopic->topic_last_poster_colour, 'background-color'), 50 - ]).' '.link_to_user( 47 + 'user' => forum_user_link( 51 48 $lastTopic->topic_last_poster_id, 52 49 $lastTopic->topic_last_poster_name, 53 - null, 54 - [] 50 + $lastTopic->topic_last_poster_colour, 51 + $currentUserId, 55 52 ), 56 53 ]) !!} 57 54 </div>
+12 -16
resources/views/forum/forums/_topic.blade.php
··· 72 72 73 73 <span class="forum-topic-entry__detail"> 74 74 {!! osu_trans('forum.topic.started_by', [ 75 - 'user' => tag('span', [ 76 - 'class' => 'forum-user-icon', 77 - 'style' => user_color_style($topic->topic_first_poster_colour, 'background-color'), 78 - ]).' '.link_to_user( 75 + 'user' => forum_user_link( 79 76 $topic->topic_poster, 80 77 $topic->topic_first_poster_name, 81 - null, 82 - [] 83 - ) 78 + $topic->topic_first_poster_colour, 79 + $currentUserId, 80 + ), 84 81 ]) !!} 85 82 </span> 86 83 </div> ··· 133 130 <div class="u-ellipsis-overflow"> 134 131 {!! osu_trans( 135 132 $topic->topic_replies === 0 ? 'forum.topic.started_by_verbose' : 'forum.topic.latest_reply_by', 136 - ['user' => tag('span', [ 137 - 'class' => 'forum-user-icon', 138 - 'style' => user_color_style($topic->topic_last_poster_colour, 'background-color'), 139 - ]).' '.link_to_user( 140 - $topic->topic_last_poster_id, 141 - $topic->topic_last_poster_name, 142 - null, 143 - [] 144 - )] 133 + [ 134 + 'user' => forum_user_link( 135 + $topic->topic_last_poster_id, 136 + $topic->topic_last_poster_name, 137 + $topic->topic_last_poster_colour, 138 + $currentUserId, 139 + ), 140 + ] 145 141 ) !!} 146 142 </div> 147 143
+4 -1
resources/views/forum/forums/_topics.blade.php
··· 7 7 {{ osu_trans('forum.forums.topics.empty') }} 8 8 </li> 9 9 @else 10 + @php 11 + $currentUserId = Auth::user()?->getKey(); 12 + @endphp 10 13 @foreach($topics as $topic) 11 - @include($row ?? 'forum.forums._topic') 14 + @include($row ?? 'forum.forums._topic', compact('currentUserId')) 12 15 @endforeach 13 16 @endif
+4 -1
resources/views/forum/forums/index.blade.php
··· 7 7 'searchParams' => ['mode' => 'forum_post'], 8 8 ]) 9 9 10 + @php 11 + $currentUserId = Auth::user()?->getKey(); 12 + @endphp 10 13 @section('content') 11 14 @include('forum._header') 12 15 ··· 49 52 50 53 <ul class="forum-list__items"> 51 54 @foreach ($category->subforums as $forum) 52 - @include('forum.forums._forum', compact('forum')) 55 + @include('forum.forums._forum', compact('currentUserId', 'forum')) 53 56 @endforeach 54 57 </ul> 55 58 </div>
+4 -1
resources/views/forum/forums/show.blade.php
··· 12 12 'titlePrepend' => $forum->forum_name, 13 13 ]) 14 14 15 + @php 16 + $currentUserId = Auth::user()?->getKey(); 17 + @endphp 15 18 @section('content') 16 19 @include('forum._header', [ 17 20 'background' => $cover['fileUrl'] ?? null, ··· 36 39 37 40 <ul class="forum-list__items"> 38 41 @foreach ($forum->subforums as $subforum) 39 - @include('forum.forums._forum', ['forum' => $subforum]) 42 + @include('forum.forums._forum', ['currentUserId' => $currentUserId, 'forum' => $subforum]) 40 43 @endforeach 41 44 </ul> 42 45 </div>
+1 -1
resources/views/packs/index.blade.php
··· 11 11 12 12 <div class="osu-page"> 13 13 <ul class="page-mode"> 14 - @foreach(['standard', 'chart', 'theme', 'artist'] as $mode) 14 + @foreach(App\Models\BeatmapPack::TAG_MAPPINGS as $mode => $tagPrefix) 15 15 <li class="page-mode__item"> 16 16 @include('packs._type', ['current' => $type, 'type' => $mode, 'title' => osu_trans("beatmappacks.mode.{$mode}")]) 17 17 @endforeach