From dc06f6810a649d18663da4dfe90d063760d2155d Mon Sep 17 00:00:00 2001 From: clayton Date: Tue, 1 Aug 2023 19:42:22 -0700 Subject: [PATCH 1/4] Update `GroupHistoryController` to support view The groups included in the response are used for building a group selector, as well as assigning colors to the events based on their affected group --- .../Controllers/GroupHistoryController.php | 16 ++++++++++++++-- app/Libraries/OsuAuthorize.php | 10 ++++++++++ app/Libraries/RouteSection.php | 3 +++ resources/lang/en/page_title.php | 3 +++ resources/views/group_history/index.blade.php | 19 +++++++++++++++++++ 5 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 resources/views/group_history/index.blade.php diff --git a/app/Http/Controllers/GroupHistoryController.php b/app/Http/Controllers/GroupHistoryController.php index e6d7949a0b4..0d409450179 100644 --- a/app/Http/Controllers/GroupHistoryController.php +++ b/app/Http/Controllers/GroupHistoryController.php @@ -7,6 +7,7 @@ namespace App\Http\Controllers; +use App\Models\Group; use App\Models\User; use App\Models\UserGroupEvent; @@ -56,7 +57,7 @@ public function index() if ($skipQuery) { $cursor = null; - $events = []; + $events = collect(); } else { $cursorHelper = UserGroupEvent::makeDbCursorHelper($params['sort']); [$events, $hasMore] = $query @@ -66,9 +67,20 @@ public function index() $cursor = $cursorHelper->next($events, $hasMore); } - return [ + $eventGroupIds = $events->pluck('group_id'); + $groups = app('groups')->all()->filter( + fn (Group $group) => + $eventGroupIds->contains($group->getKey()) || + priv_check('GroupShow', $group)->can(), + ); + $json = [ 'events' => json_collection($events, 'UserGroupEvent'), + 'groups' => json_collection($groups, 'Group'), ...cursor_for_response($cursor), ]; + + return is_json_request() + ? $json + : ext_view('group_history.index', compact('json')); } } diff --git a/app/Libraries/OsuAuthorize.php b/app/Libraries/OsuAuthorize.php index dc353501019..ecf4c70240d 100644 --- a/app/Libraries/OsuAuthorize.php +++ b/app/Libraries/OsuAuthorize.php @@ -21,6 +21,7 @@ use App\Models\Forum\Topic; use App\Models\Forum\TopicCover; use App\Models\Genre; +use App\Models\Group; use App\Models\Language; use App\Models\LegacyMatch\LegacyMatch; use App\Models\Multiplayer\Room; @@ -1768,6 +1769,15 @@ public function checkForumTopicVote(?User $user, Topic $topic): string return 'ok'; } + public function checkGroupShow(?User $user, Group $group): string + { + if ($group->hasListing() || $user?->isGroup($group)) { + return 'ok'; + } + + return 'unauthorized'; + } + public function checkIsOwnClient(?User $user, Client $client): string { if ($user === null || $user->getKey() !== $client->user_id) { diff --git a/app/Libraries/RouteSection.php b/app/Libraries/RouteSection.php index af03c091baf..ef13fd82132 100644 --- a/app/Libraries/RouteSection.php +++ b/app/Libraries/RouteSection.php @@ -70,6 +70,9 @@ class RouteSection 'friends_controller' => [ '_' => 'home', ], + 'group_history_controller' => [ + '_' => 'home', + ], 'groups_controller' => [ '_' => 'home', ], diff --git a/resources/lang/en/page_title.php b/resources/lang/en/page_title.php index 8d627192274..ce369e7ac1b 100644 --- a/resources/lang/en/page_title.php +++ b/resources/lang/en/page_title.php @@ -66,6 +66,9 @@ 'contests_controller' => [ '_' => 'contests', ], + 'group_history_controller' => [ + '_' => 'group history', + ], 'groups_controller' => [ 'show' => 'groups', ], diff --git a/resources/views/group_history/index.blade.php b/resources/views/group_history/index.blade.php new file mode 100644 index 00000000000..d235a94efa3 --- /dev/null +++ b/resources/views/group_history/index.blade.php @@ -0,0 +1,19 @@ +{{-- + 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. +--}} +@extends('master') + +@section('content') +
+@endsection + +@section("script") + @parent + + + + @include('layout._react_js', ['src' => 'js/group-history.js']) +@endsection From 6bff3e1b3076d040820b9083df9faddb87b36183 Mon Sep 17 00:00:00 2001 From: clayton Date: Tue, 1 Aug 2023 20:00:37 -0700 Subject: [PATCH 2/4] Add group history view --- package.json | 2 +- resources/css/bem-index.less | 3 + resources/css/bem/group-history-event.less | 64 ++++++++ .../css/bem/group-history-search-form.less | 63 ++++++++ resources/css/bem/group-history.less | 22 +++ resources/css/bem/show-more-link.less | 3 +- resources/js/entrypoints/group-history.tsx | 17 ++ resources/js/group-history/event.tsx | 92 +++++++++++ resources/js/group-history/events.tsx | 29 ++++ resources/js/group-history/group-store.ts | 45 ++++++ resources/js/group-history/json.ts | 11 ++ resources/js/group-history/main.tsx | 133 ++++++++++++++++ resources/js/group-history/query.ts | 55 +++++++ resources/js/group-history/search-form.tsx | 148 ++++++++++++++++++ resources/lang/en/group_history.php | 38 +++++ yarn.lock | 8 +- 16 files changed, 727 insertions(+), 6 deletions(-) create mode 100644 resources/css/bem/group-history-event.less create mode 100644 resources/css/bem/group-history-search-form.less create mode 100644 resources/css/bem/group-history.less create mode 100644 resources/js/entrypoints/group-history.tsx create mode 100644 resources/js/group-history/event.tsx create mode 100644 resources/js/group-history/events.tsx create mode 100644 resources/js/group-history/group-store.ts create mode 100644 resources/js/group-history/json.ts create mode 100644 resources/js/group-history/main.tsx create mode 100644 resources/js/group-history/query.ts create mode 100644 resources/js/group-history/search-form.tsx create mode 100644 resources/lang/en/group_history.php diff --git a/package.json b/package.json index f69f4f855f0..4c0264aeacd 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@discordapp/twemoji": "^14.0.2", - "@fortawesome/fontawesome-free": "^5.6.3", + "@fortawesome/fontawesome-free": "^5.15.4", "@types/bootstrap": "^3.3.0", "@types/d3": "^7.1.0", "@types/grecaptcha": "^3.0.1", diff --git a/resources/css/bem-index.less b/resources/css/bem-index.less index 71b4044667c..4a90ab34aff 100644 --- a/resources/css/bem-index.less +++ b/resources/css/bem-index.less @@ -178,6 +178,9 @@ @import "bem/game-mode"; @import "bem/game-mode-link"; @import "bem/grid-items"; +@import "bem/group-history"; +@import "bem/group-history-event"; +@import "bem/group-history-search-form"; @import "bem/header-buttons"; @import "bem/header-nav-mobile"; @import "bem/header-nav-v4"; diff --git a/resources/css/bem/group-history-event.less b/resources/css/bem/group-history-event.less new file mode 100644 index 00000000000..047659bae5d --- /dev/null +++ b/resources/css/bem/group-history-event.less @@ -0,0 +1,64 @@ +// 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. + +.group-history-event { + @_top: group-history-event; + + align-items: center; + display: flex; + font-size: @font-size--title-small; + gap: 15px; + + &__icon { + @icons: { + group-add: users; + group-remove: users-slash; + group-rename: users-cog; + user-add: user-plus; + user-add-playmodes: user-tag; + user-remove: user-minus; + user-remove-playmodes: user-tag; + user-set-default: user-cog; + }; + each(@icons, { + .@{_top}--@{key} & { + @icon-var: 'fa-var-@{value}'; + --icon: @@icon-var; + } + }); + + .fas(); + background-color: var(--group-colour, @osu-colour-b1); + border-radius: 10000px; + color: @osu-colour-b6; + padding: 3px 6px; + + &::before { + content: var(--icon); + } + } + + &__info { + color: @osu-colour-f1; + display: flex; + flex-direction: column; + flex-shrink: 0; + font-size: @font-size--normal; + gap: 0 15px; + + @media @desktop { + flex-direction: row-reverse; + } + } + + &__message { + @bold-events: group-add, group-remove, group-rename; + each(@bold-events, { + .@{_top}--@{value} & { + font-weight: bold; + } + }); + + flex-grow: 1; + } +} diff --git a/resources/css/bem/group-history-search-form.less b/resources/css/bem/group-history-search-form.less new file mode 100644 index 00000000000..8ba7d3657e5 --- /dev/null +++ b/resources/css/bem/group-history-search-form.less @@ -0,0 +1,63 @@ +// 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. + +.group-history-search-form { + --input-bg: @osu-colour-b5; + --input-border-radius: @border-radius--large; + background: @osu-colour-b4; + + &__content { + --vertical-gutter: 20px; + .default-gutter-v2(); + padding-top: var(--vertical-gutter); + padding-bottom: var(--vertical-gutter); + + &--buttons { + --vertical-gutter: 10px; + background-color: @osu-colour-b3; + display: flex; + gap: 10px; + justify-content: center; + } + + &--inputs { + display: grid; + gap: 10px; + grid-template-columns: repeat(2, 1fr) repeat(2, 180px); + + @media @mobile { + grid-template-columns: repeat(2, 1fr); + + > :nth-child(-n + 2) { + grid-column: span 2; + } + } + } + } + + &__input { + .reset-input(); + font-size: @font-size--title-small-3; + width: 100%; + } + + &__label { + color: var(--label-colour); + padding-bottom: 5px; + } + + &__select-container { + position: relative; + + &::after { + .fas(); + .center-content(); + content: @fa-var-chevron-down; + height: 100%; + padding-left: 10px; + position: absolute; + right: 5px; + pointer-events: none; + } + } +} diff --git a/resources/css/bem/group-history.less b/resources/css/bem/group-history.less new file mode 100644 index 00000000000..bda5856168d --- /dev/null +++ b/resources/css/bem/group-history.less @@ -0,0 +1,22 @@ +// 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. + +.group-history { + &__events { + display: flex; + flex-direction: column; + gap: 10px; + } + + &__none { + font-size: @font-size--title-small-3; + margin: 0; + text-align: center; + } + + &__staff-log { + font-size: @font-size--normal; + margin: 20px 0 0; + text-align: center; + } +} diff --git a/resources/css/bem/show-more-link.less b/resources/css/bem/show-more-link.less index 7e60b002891..226815b2e71 100644 --- a/resources/css/bem/show-more-link.less +++ b/resources/css/bem/show-more-link.less @@ -38,7 +38,8 @@ margin: 40px 0; } - &--chat-conversation-earlier-messages { + &--chat-conversation-earlier-messages, + &--group-history { margin: 20px auto 0; } diff --git a/resources/js/entrypoints/group-history.tsx b/resources/js/entrypoints/group-history.tsx new file mode 100644 index 00000000000..a7cc98bb67d --- /dev/null +++ b/resources/js/entrypoints/group-history.tsx @@ -0,0 +1,17 @@ +// 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 groupStore from 'group-history/group-store'; +import GroupHistoryJson from 'group-history/json'; +import Main from 'group-history/main'; +import core from 'osu-core-singleton'; +import * as React from 'react'; +import { parseJson } from 'utils/json'; + +core.reactTurbolinks.register('group-history', () => { + const json: GroupHistoryJson = parseJson('json-group-history'); + + groupStore.updateMany(json.groups); + + return
; +}); diff --git a/resources/js/group-history/event.tsx b/resources/js/group-history/event.tsx new file mode 100644 index 00000000000..7bcf1117c76 --- /dev/null +++ b/resources/js/group-history/event.tsx @@ -0,0 +1,92 @@ +// 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 StringWithComponent from 'components/string-with-component'; +import TimeWithTooltip from 'components/time-with-tooltip'; +import UserLink from 'components/user-link'; +import UserGroupEventJson from 'interfaces/user-group-event-json'; +import { route } from 'laroute'; +import { kebabCase } from 'lodash'; +import * as React from 'react'; +import { classWithModifiers, groupColour } from 'utils/css'; +import { trans, transArray } from 'utils/lang'; +import groupStore from './group-store'; + +interface Props { + event: UserGroupEventJson; +} + +export default class Event extends React.PureComponent { + private get messageMappings() { + const event = this.props.event; + const mappings: Record = { + group: ( + + {event.group_name} + + ), + }; + + if ('playmodes' in event && event.playmodes != null) { + mappings.playmodes = transArray( + event.playmodes.map((mode) => trans(`beatmaps.mode.${mode}`)), + ); + } + + if ('previous_group_name' in event) { + mappings.previous_group = ( + + {event.previous_group_name} + + ); + } + + if (event.user_id != null) { + mappings.user = ; + } + + return mappings; + } + + private get messagePattern() { + const event = this.props.event; + const type = event.type === 'user_add' && event.playmodes != null + ? 'user_add_with_playmodes' + : event.type; + + return trans(`group_history.event.message.${type}`); + } + + render() { + return ( +
+ +
+ +
+
+ + {this.props.event.actor?.id != null && ( + + , + }} + pattern={trans('group_history.event.actor')} + /> + + )} +
+
+ ); + } +} diff --git a/resources/js/group-history/events.tsx b/resources/js/group-history/events.tsx new file mode 100644 index 00000000000..86aa5524594 --- /dev/null +++ b/resources/js/group-history/events.tsx @@ -0,0 +1,29 @@ +// 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 UserGroupEventJson from 'interfaces/user-group-event-json'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { trans } from 'utils/lang'; +import Event from './event'; + +interface Props { + events: UserGroupEventJson[]; +} + +@observer +export default class Events extends React.Component { + render() { + return this.props.events.length > 0 ? ( +
+ {this.props.events.map((event) => ( + + ))} +
+ ) : ( +

+ {trans('group_history.none')} +

+ ); + } +} diff --git a/resources/js/group-history/group-store.ts b/resources/js/group-history/group-store.ts new file mode 100644 index 00000000000..ac7b3fec0bd --- /dev/null +++ b/resources/js/group-history/group-store.ts @@ -0,0 +1,45 @@ +// 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 GroupJson from 'interfaces/group-json'; +import { sortBy } from 'lodash'; +import { action, computed, makeObservable, observable } from 'mobx'; + +class GroupStore { + @observable byId = observable.map(); + + @computed + get byIdentifier() { + return this.groups.reduce( + (prev, group) => { + prev.set(group.identifier, group); + return prev; + }, + new Map(), + ); + } + + @computed + get groups() { + return sortBy([...this.byId.values()], 'name'); + } + + constructor() { + makeObservable(this); + } + + @action + update(group: GroupJson): void { + this.byId.set(group.id, group); + } + + @action + updateMany(groups: GroupJson[]): void { + for (const group of groups) { + this.update(group); + } + } +} + +const groupStore = new GroupStore(); +export default groupStore; diff --git a/resources/js/group-history/json.ts b/resources/js/group-history/json.ts new file mode 100644 index 00000000000..36176688b71 --- /dev/null +++ b/resources/js/group-history/json.ts @@ -0,0 +1,11 @@ +// 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 GroupJson from 'interfaces/group-json'; +import UserGroupEventJson from 'interfaces/user-group-event-json'; + +export default interface GroupHistoryJson { + cursor_string: string | null; + events: UserGroupEventJson[]; + groups: GroupJson[]; +} diff --git a/resources/js/group-history/main.tsx b/resources/js/group-history/main.tsx new file mode 100644 index 00000000000..9d28d247f79 --- /dev/null +++ b/resources/js/group-history/main.tsx @@ -0,0 +1,133 @@ +// 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 HeaderV4 from 'components/header-v4'; +import ShowMoreLink from 'components/show-more-link'; +import StringWithComponent from 'components/string-with-component'; +import UserGroupEventJson from 'interfaces/user-group-event-json'; +import { route } from 'laroute'; +import { action, makeObservable, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { onErrorWithCallback } from 'utils/ajax'; +import { trans } from 'utils/lang'; +import { wikiUrl } from 'utils/url'; +import Events from './events'; +import groupStore from './group-store'; +import GroupHistoryJson from './json'; +import { getQueryFromUrl, GroupHistoryQuery, setUrlFromQuery } from './query'; +import SearchForm from './search-form'; + +interface Props { + cursorString: string | null; + events: UserGroupEventJson[]; +} + +@observer +export default class Main extends React.Component { + @observable private currentQuery: GroupHistoryQuery; + @observable private cursorString: string | null; + @observable private events: UserGroupEventJson[]; + @observable private loading?: 'more' | 'new'; + @observable private newQuery: GroupHistoryQuery; + private xhr?: JQuery.jqXHR; + + constructor(props: Props) { + super(props); + + const { parseError, query } = getQueryFromUrl(); + + this.currentQuery = query; + this.cursorString = props.cursorString; + this.events = props.events; + this.newQuery = { ...this.currentQuery }; + + makeObservable(this); + + if (parseError) { + this.onSearch(); + } + } + + componentWillUnmount() { + this.xhr?.abort(); + } + + render() { + return ( + <> + +
+ +
+
+ + +

+ + {trans('group_history.staff_log.wiki_articles')} + + ), + }} + pattern={trans('group_history.staff_log._')} + /> +

+
+ + ); + } + + @action + private loadEvents(query: GroupHistoryQuery & { cursor_string?: string }) { + this.xhr?.abort(); + this.loading = query.cursor_string == null ? 'new' : 'more'; + + this.xhr = $.ajax( + route('group-history.index'), + { + data: query, + dataType: 'JSON', + method: 'GET', + }, + ); + this.xhr + .done(action((response: GroupHistoryJson) => { + this.cursorString = response.cursor_string; + groupStore.updateMany(response.groups); + + if (query.cursor_string == null) { + this.currentQuery = { ...query }; + this.events = response.events; + setUrlFromQuery(query); + } else { + this.events.push(...response.events); + } + })) + .fail(onErrorWithCallback(() => this.loadEvents(query))) + .always(action(() => this.loading = undefined)); + } + + private readonly onSearch = () => this.loadEvents(this.newQuery); + + private readonly onShowMore = () => { + if (this.cursorString != null) { + this.loadEvents({ + ...this.currentQuery, + cursor_string: this.cursorString, + }); + } + }; +} diff --git a/resources/js/group-history/query.ts b/resources/js/group-history/query.ts new file mode 100644 index 00000000000..ed266607bdc --- /dev/null +++ b/resources/js/group-history/query.ts @@ -0,0 +1,55 @@ +// 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 moment from 'moment'; +import { currentUrlParams } from 'utils/turbolinks'; +import { updateQueryString } from 'utils/url'; +import groupStore from './group-store'; + +export interface GroupHistoryQuery { + after?: string; + before?: string; + group?: string; + user?: string; +} + +export const emptyQuery: Readonly = Object.freeze({ + after: undefined, + before: undefined, + group: undefined, + user: undefined, +}); + +const paramValidators: Record boolean> = { + after: (value: string) => moment(value, moment.HTML5_FMT.DATE, true).isValid(), + before: (value: string) => moment(value, moment.HTML5_FMT.DATE, true).isValid(), + group: (value: string) => groupStore.byIdentifier.has(value), + user: (value: string) => value.length > 0, +}; + +export function getQueryFromUrl(): { parseError: boolean; query: GroupHistoryQuery } { + const params = currentUrlParams(); + let parseError = false; + const query: GroupHistoryQuery = {}; + + for (const key of Object.keys(emptyQuery) as (keyof GroupHistoryQuery)[]) { + const value = params.get(key); + + if (value != null && !paramValidators[key](value)) { + parseError = true; + query[key] = undefined; + } else { + query[key] = value ?? undefined; + } + } + + return { parseError, query }; +} + +export function setUrlFromQuery(query: Readonly): void { + history.replaceState( + history.state, + '', + updateQueryString(null, query), + ); +} diff --git a/resources/js/group-history/search-form.tsx b/resources/js/group-history/search-form.tsx new file mode 100644 index 00000000000..bca1da12ef7 --- /dev/null +++ b/resources/js/group-history/search-form.tsx @@ -0,0 +1,148 @@ +// 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 BigButton from 'components/big-button'; +import InputContainer from 'components/input-container'; +import { isEqual } from 'lodash'; +import { action, computed, makeObservable } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { trans } from 'utils/lang'; +import groupStore from './group-store'; +import { emptyQuery, GroupHistoryQuery } from './query'; + +const bn = 'group-history-search-form'; + +interface Props { + currentQuery: GroupHistoryQuery; + loading: boolean; + newQuery: GroupHistoryQuery; + onSearch: () => void; +} + +@observer +export default class SearchForm extends React.Component { + @computed + private get newQueryIsEmpty() { + return isEqual(this.props.newQuery, emptyQuery); + } + + @computed + private get newQueryIsSame() { + return isEqual(this.props.newQuery, this.props.currentQuery); + } + + constructor(props: Props) { + super(props); + makeObservable(this); + } + + render() { + return ( +
+
+ +
+ +
+
+ + + + + + + + + +
+
+ + +
+
+ ); + } + + @action + private readonly onDateChange = (event: React.ChangeEvent) => { + event.preventDefault(); + + this.props.newQuery[event.currentTarget.name as 'after' | 'before'] = + event.currentTarget.value || undefined; + }; + + @action + private readonly onGroupChange = (event: React.ChangeEvent) => { + event.preventDefault(); + + this.props.newQuery.group = event.currentTarget.value || undefined; + }; + + @action + private readonly onReset = (event: React.MouseEvent) => { + event.preventDefault(); + + Object.assign(this.props.newQuery, emptyQuery); + }; + + private readonly onSubmit = (event: React.FormEvent) => { + event.preventDefault(); + + if (!this.newQueryIsSame) { + this.props.onSearch(); + } + }; + + @action + private readonly onUserChange = (event: React.ChangeEvent) => { + event.preventDefault(); + + this.props.newQuery.user = event.currentTarget.value || undefined; + }; +} diff --git a/resources/lang/en/group_history.php b/resources/lang/en/group_history.php new file mode 100644 index 00000000000..805d3675399 --- /dev/null +++ b/resources/lang/en/group_history.php @@ -0,0 +1,38 @@ +. Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +return [ + 'none' => 'No group history found!', + + 'event' => [ + 'actor' => 'by :user', + + 'message' => [ + 'group_add' => ':group created.', + 'group_remove' => ':group deleted.', + 'group_rename' => ':previous_group renamed to :group.', + 'user_add' => ':user added to :group.', + 'user_add_with_playmodes' => ':user added to :group for :playmodes.', + 'user_add_playmodes' => ':playmodes added to :user\'s :group membership.', + 'user_remove' => ':user removed from :group.', + 'user_remove_playmodes' => ':playmodes removed from :user\'s :group membership.', + 'user_set_default' => ':user\'s default group set to :group.', + ], + ], + + 'form' => [ + 'after' => 'After', + 'before' => 'Before', + 'group' => 'Group', + 'group_all' => 'All groups', + 'user' => 'User', + 'user_prompt' => 'Username or ID', + ], + + 'staff_log' => [ + '_' => 'Older group history can be found in :wiki_articles.', + 'wiki_articles' => 'the staff log wiki articles', + ], +]; diff --git a/yarn.lock b/yarn.lock index 2155208e7a3..b070815c1e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -227,10 +227,10 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" -"@fortawesome/fontawesome-free@^5.6.3": - version "5.9.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.9.0.tgz#1aa5c59efb1b8c6eb6277d1e3e8c8f31998b8c8e" - integrity sha512-g795BBEzM/Hq2SYNPm/NQTIp3IWd4eXSH0ds87Na2jnrAUFX3wkyZAI4Gwj9DOaWMuz2/01i8oWI7P7T/XLkhg== +"@fortawesome/fontawesome-free@^5.15.4": + version "5.15.4" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz#ecda5712b61ac852c760d8b3c79c96adca5554e5" + integrity sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg== "@istanbuljs/schema@^0.1.2": version "0.1.2" From ebcc04d5b190a38cd19b2e2ff67f0d73e0ededc7 Mon Sep 17 00:00:00 2001 From: clayton Date: Tue, 1 Aug 2023 20:01:23 -0700 Subject: [PATCH 3/4] Only show actor for default group events if user is privileged --- app/Libraries/OsuAuthorize.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Libraries/OsuAuthorize.php b/app/Libraries/OsuAuthorize.php index ecf4c70240d..bc9116222b0 100644 --- a/app/Libraries/OsuAuthorize.php +++ b/app/Libraries/OsuAuthorize.php @@ -1927,6 +1927,10 @@ public function checkScorePin(?User $user, ScoreBest|Solo\Score $score): string public function checkUserGroupEventShowActor(?User $user, UserGroupEvent $event): string { + if ($event->group->identifier === 'default') { + return $user?->isPrivileged() ? 'ok' : 'unauthorized'; + } + if ($user?->isGroup($event->group)) { return 'ok'; } From 09c679318d45e92e04d2f4eb44262364bf73a35f Mon Sep 17 00:00:00 2001 From: clayton Date: Sun, 6 Aug 2023 18:46:25 -0700 Subject: [PATCH 4/4] Add history button to group pages --- resources/css/bem/btn-osu-big.less | 4 ++++ resources/css/bem/user-list.less | 4 ++++ resources/js/components/user-list.tsx | 12 +++++++++++- resources/lang/en/group_history.php | 1 + 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/resources/css/bem/btn-osu-big.less b/resources/css/bem/btn-osu-big.less index 51b87cbbcbd..2394496ecaa 100644 --- a/resources/css/bem/btn-osu-big.less +++ b/resources/css/bem/btn-osu-big.less @@ -349,6 +349,10 @@ margin-top: 10px; } + &--user-list-title { + flex-shrink: 0; + } + &--user-page-edit { border-radius: 10000px; width: 140px; diff --git a/resources/css/bem/user-list.less b/resources/css/bem/user-list.less index e50a718ec8c..df96dc1277b 100644 --- a/resources/css/bem/user-list.less +++ b/resources/css/bem/user-list.less @@ -17,6 +17,10 @@ &__title { .default-gutter-v2(); + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 10px; padding-top: 20px; padding-bottom: 0; margin: 0; diff --git a/resources/js/components/user-list.tsx b/resources/js/components/user-list.tsx index 7aac01adf38..3825a7e212d 100644 --- a/resources/js/components/user-list.tsx +++ b/resources/js/components/user-list.tsx @@ -4,6 +4,7 @@ import GameMode from 'interfaces/game-mode'; import GroupJson from 'interfaces/group-json'; import UserJson from 'interfaces/user-json'; +import { route } from 'laroute'; import { usernameSortAscending } from 'models/user'; import * as moment from 'moment'; import core from 'osu-core-singleton'; @@ -12,6 +13,7 @@ import { classWithModifiers } from 'utils/css'; import { trans } from 'utils/lang'; import { currentUrlParams } from 'utils/turbolinks'; import { updateQueryString } from 'utils/url'; +import BigButton from './big-button'; import { Sort } from './sort'; import { ViewMode, viewModes } from './user-card'; import { UserCards } from './user-cards'; @@ -151,7 +153,15 @@ export class UserList extends React.PureComponent {
{this.props.group != null && ( -

{this.props.group.name}

+

+ {this.props.group.name} + +

)} {this.props.group?.description != null && ( diff --git a/resources/lang/en/group_history.php b/resources/lang/en/group_history.php index 805d3675399..ec3f31dcb14 100644 --- a/resources/lang/en/group_history.php +++ b/resources/lang/en/group_history.php @@ -5,6 +5,7 @@ return [ 'none' => 'No group history found!', + 'view' => 'View group history', 'event' => [ 'actor' => 'by :user',