the browser-facing portion of osu!

Report chat message for user card on chat page

nanaya 1b34545f 5ac13f91

+99 -16
+24 -1
resources/js/chat/message-group.tsx
··· 3 3 4 4 import UserAvatar from 'components/user-avatar'; 5 5 import UserLink from 'components/user-link'; 6 + import Reportable from 'interfaces/reportable'; 7 + import { last } from 'lodash'; 6 8 import { observer } from 'mobx-react'; 7 9 import Message from 'models/chat/message'; 8 10 import * as moment from 'moment'; ··· 17 19 18 20 @observer 19 21 export default class MessageGroup extends React.Component<Props> { 22 + private get reportable() { 23 + const lastMessage = last(this.props.messages); 24 + 25 + if (lastMessage == null) { 26 + throw new Error('invalid reportable access on empty message group'); 27 + } 28 + 29 + if (typeof lastMessage.messageId !== 'number') { 30 + return undefined; 31 + } 32 + 33 + return { 34 + id: lastMessage.messageId.toString(), 35 + type: 'message', 36 + } satisfies Reportable; 37 + } 38 + 20 39 render(): React.ReactNode { 21 40 const messages = this.props.messages; 22 41 ··· 29 48 return ( 30 49 <div className={classWithModifiers('chat-message-group', { own: sender.id === core.currentUser?.id })}> 31 50 <div className='chat-message-group__sender'> 32 - <UserLink tooltipPosition='top center' user={sender}> 51 + <UserLink 52 + reportable={this.reportable} 53 + tooltipPosition='top center' 54 + user={sender} 55 + > 33 56 <div className='chat-message-group__avatar'> 34 57 <UserAvatar modifiers='full-circle' user={{ avatar_url: sender.avatarUrl }} /> 35 58 </div>
+25 -9
resources/js/components/report-form.tsx
··· 2 2 // See the LICENCE file in the repository root for full licence text. 3 3 4 4 import SelectOptions from 'components/select-options'; 5 + import { ReportableType } from 'interfaces/reportable'; 5 6 import { route } from 'laroute'; 6 7 import { action, computed, makeObservable, observable } from 'mobx'; 7 8 import { observer } from 'mobx-react'; ··· 26 27 />, root); 27 28 } 28 29 29 - export const reportableTypeToGroupKey = { 30 + 31 + type GroupKey = 32 + | 'beatmapset' 33 + | 'beatmapset_discussion_post' 34 + | 'comment' 35 + | 'forum_post' 36 + | 'message' 37 + | 'scores' 38 + | 'user'; 39 + export const reportableTypeToGroupKey: Record<ReportableType, GroupKey> = { 30 40 beatmapset: 'beatmapset', 31 41 beatmapset_discussion_post: 'beatmapset_discussion_post', 32 42 comment: 'comment', 33 43 forum_post: 'forum_post', 44 + message: 'message', 34 45 score_best_fruits: 'scores', 35 46 score_best_mania: 'scores', 36 47 score_best_osu: 'scores', ··· 38 49 solo_score: 'scores', 39 50 user: 'user', 40 51 } as const; 41 - export type ReportableType = keyof typeof reportableTypeToGroupKey; 42 - type GroupKey = typeof reportableTypeToGroupKey[ReportableType]; 43 52 44 53 // intended to be in display order, not alphabetical order. 45 54 /* eslint-disable sort-keys */ ··· 54 63 } as const; 55 64 /* eslint-enable sort-keys */ 56 65 57 - const availableOptionsByGroupKey: Partial<Record<GroupKey, (keyof typeof availableOptions)[]>> = { 66 + const reasons = { 58 67 beatmapset: ['UnwantedContent', 'Other'], 59 - beatmapset_discussion_post: ['Insults', 'Spam', 'UnwantedContent', 'Nonsense', 'Other'], 60 - comment: ['Insults', 'Spam', 'UnwantedContent', 'Nonsense', 'Other'], 61 - forum_post: ['Insults', 'Spam', 'UnwantedContent', 'Nonsense', 'Other'], 62 - scores: ['Cheating', 'MultipleAccounts', 'Other'], 68 + post: ['Insults', 'Spam', 'UnwantedContent', 'Nonsense', 'Other'], 69 + score: ['Cheating', 'MultipleAccounts', 'Other'], 70 + } as const; 71 + 72 + const availableOptionsByGroupKey: Partial<Record<GroupKey, readonly (keyof typeof availableOptions)[]>> = { 73 + beatmapset: reasons.beatmapset, 74 + beatmapset_discussion_post: reasons.post, 75 + comment: reasons.post, 76 + forum_post: reasons.post, 77 + message: reasons.post, 78 + scores: reasons.score, 63 79 }; 64 80 65 81 interface Props { ··· 83 99 private timeout: number | undefined; 84 100 85 101 private get canSubmit() { 86 - return !this.disabled && this.comments.length > 0; 102 + return !this.disabled && (this.comments.length > 0 || this.props.reportableType === 'message'); 87 103 } 88 104 89 105 private get groupKey() {
+2 -1
resources/js/components/report-reportable.tsx
··· 1 1 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0. 2 2 // See the LICENCE file in the repository root for full licence text. 3 3 4 + import { ReportableType } from 'interfaces/reportable'; 4 5 import * as React from 'react'; 5 6 import { trans } from 'utils/lang'; 6 - import { ReportableType, reportableTypeToGroupKey, showReportForm } from './report-form'; 7 + import { reportableTypeToGroupKey, showReportForm } from './report-form'; 7 8 8 9 type ReactButton = React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>; 9 10 type ReactButtonWithoutRef = Pick<ReactButton, Exclude<keyof ReactButton, 'ref'>>;
+16 -3
resources/js/components/user-card-tooltip.tsx
··· 1 1 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0. 2 2 // See the LICENCE file in the repository root for full licence text. 3 3 4 + import Reportable from 'interfaces/reportable'; 4 5 import UserJson from 'interfaces/user-json'; 5 6 import { route } from 'laroute'; 6 7 import * as _ from 'lodash'; ··· 86 87 card.classList.remove('js-react--user-card'); 87 88 card.classList.add('js-react--user-card-tooltip'); 88 89 card.dataset.lookup = userId; 89 - if (element.dataset.tooltipPosition != null) { 90 - card.dataset.tooltipPosition = element.dataset.tooltipPosition; 90 + for (const [key, value] of Object.entries(element.dataset)) { 91 + card.dataset[key] = value; 91 92 } 92 93 93 94 $(element).qtip(createTooltipOptions(card)); ··· 214 215 state: Readonly<State> = {}; 215 216 private readonly contextActiveKeyDidChange = contextActiveKeyDidChange.bind(this); 216 217 218 + private get reportable() { 219 + const dataString = this.props.container.dataset.reportable; 220 + 221 + return dataString == null 222 + ? undefined 223 + : JSON.parse(dataString) as Reportable; 224 + } 225 + 217 226 componentDidMount() { 218 227 this.getUser().then((user) => { 219 228 this.setState({ user }); ··· 237 246 <TooltipContext.Provider value={this.props.container}> 238 247 <ContainerContext.Provider value={{ activeKeyDidChange: this.activeKeyDidChange }}> 239 248 <KeyContext.Provider value={this.props.lookup}> 240 - <UserCard activated={activated} user={this.state.user} /> 249 + <UserCard 250 + activated={activated} 251 + reportable={this.reportable} 252 + user={this.state.user} 253 + /> 241 254 </KeyContext.Provider> 242 255 </ContainerContext.Provider> 243 256 </TooltipContext.Provider>
+4 -2
resources/js/components/user-card.tsx
··· 3 3 4 4 import BlockButton from 'components/block-button'; 5 5 import FriendButton from 'components/friend-button'; 6 + import Reportable from 'interfaces/reportable'; 6 7 import UserJson from 'interfaces/user-json'; 7 8 import { route } from 'laroute'; 8 9 import * as _ from 'lodash'; ··· 30 31 activated: boolean; 31 32 mode: ViewMode; 32 33 modifiers?: Modifiers; 34 + reportable?: Reportable; 33 35 user?: UserJson | null; 34 36 } 35 37 ··· 304 306 className='simple-menu__item' 305 307 icon 306 308 onFormOpen={dismiss} 307 - reportableId={this.user.id.toString()} 308 - reportableType='user' 309 + reportableId={this.props.reportable?.id ?? this.user.id.toString()} 310 + reportableType={this.props.reportable?.type ?? 'user'} 309 311 user={this.user} 310 312 /> 311 313 </div>
+3
resources/js/components/user-link.tsx
··· 1 1 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0. 2 2 // See the LICENCE file in the repository root for full licence text. 3 3 4 + import Reportable from 'interfaces/reportable'; 4 5 import Ruleset from 'interfaces/ruleset'; 5 6 import UserJson from 'interfaces/user-json'; 6 7 import { route } from 'laroute'; ··· 10 11 children?: React.ReactNode; 11 12 className?: string; 12 13 mode?: Ruleset; 14 + reportable?: Reportable; 13 15 tooltipPosition?: string; 14 16 user: Partial<Pick<UserJson, 'id' | 'username'>>; 15 17 } ··· 28 30 return ( 29 31 <a 30 32 className={className} 33 + data-reportable={JSON.stringify(this.props.reportable)} 31 34 data-tooltip-position={this.props.tooltipPosition} 32 35 data-user-id={this.props.user.id} 33 36 href={href}
+20
resources/js/interfaces/reportable.ts
··· 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 + export type ReportableType = 5 + | 'beatmapset' 6 + | 'beatmapset_discussion_post' 7 + | 'comment' 8 + | 'forum_post' 9 + | 'message' 10 + | 'score_best_fruits' 11 + | 'score_best_mania' 12 + | 'score_best_osu' 13 + | 'score_best_taiko' 14 + | 'solo_score' 15 + | 'user'; 16 + 17 + export default interface Reportable { 18 + id: string; 19 + type: ReportableType; 20 + }
+5
resources/lang/en/report.php
··· 24 24 'title' => 'Report :username\'s post?', 25 25 ], 26 26 27 + 'message' => [ 28 + 'button' => 'Report Message', 29 + 'title' => 'Report :username\'s message?', 30 + ], 31 + 27 32 'scores' => [ 28 33 'button' => 'Report Score', 29 34 'title' => 'Report :username\'s score?',