Skip to content

Commit 604abde

Browse files
authored
Merge pull request ppy#12086 from bdach/mp-history-ts/content
Convert multiplayer room history content component to typescript
2 parents 7246fac + 1387686 commit 604abde

File tree

4 files changed

+189
-124
lines changed

4 files changed

+189
-124
lines changed

resources/js/mp-history/content.coffee

-118
This file was deleted.

resources/js/mp-history/content.tsx

+187
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
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 ShowMoreLink from 'components/show-more-link';
5+
import LegacyMatchEvent from 'interfaces/legacy-match-event-json';
6+
import LegacyMatchEventJson from 'interfaces/legacy-match-event-json';
7+
import LegacyMatchGame from 'interfaces/legacy-match-game-json';
8+
import LegacyMatch from 'interfaces/legacy-match-json';
9+
import * as React from 'react';
10+
import { classWithModifiers } from 'utils/css';
11+
import { bottomPageDistance } from 'utils/html';
12+
import { trans } from 'utils/lang';
13+
import UserJson from '../interfaces/user-json';
14+
import Event from './event';
15+
import Game from './game';
16+
17+
interface Props {
18+
currentGameId?: number;
19+
events?: LegacyMatchEvent[];
20+
hasNext: boolean;
21+
hasPrevious: boolean;
22+
isAutoloading: boolean;
23+
loadingNext: boolean;
24+
loadingPrevious: boolean;
25+
loadNext: () => void;
26+
loadPrevious: () => void;
27+
match: LegacyMatch;
28+
users: Partial<Record<number, UserJson>>;
29+
}
30+
31+
interface Snapshot {
32+
reference?: {
33+
fn: () => number;
34+
prev: number;
35+
};
36+
scrollToLastEvent: boolean;
37+
}
38+
39+
export interface TeamScores {
40+
blue: number;
41+
red: number;
42+
}
43+
44+
export default class Content extends React.PureComponent<Props> {
45+
private inEvent = false;
46+
private scoresCache: Partial<Record<number, TeamScores>> = {};
47+
48+
componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<unknown>, snapshot?: Snapshot) {
49+
if (snapshot?.scrollToLastEvent) {
50+
$(window).stop().scrollTo(document.body.scrollHeight, 500);
51+
} else if (snapshot?.reference) {
52+
const referenceCurrent = snapshot.reference.fn();
53+
const documentScrollTopCurrent = window.scrollY;
54+
const documentScrollTopTarget = documentScrollTopCurrent + referenceCurrent - snapshot.reference.prev;
55+
window.scrollTo(window.scrollX, documentScrollTopTarget);
56+
}
57+
}
58+
59+
getSnapshotBeforeUpdate(prevProps: Readonly<Props>): Snapshot {
60+
const snapshot: Snapshot = {
61+
scrollToLastEvent: prevProps.isAutoloading && this.props.isAutoloading && bottomPageDistance() < 10,
62+
};
63+
64+
if (!snapshot.scrollToLastEvent) {
65+
const hadEvents = prevProps.events != null && prevProps.events.length > 0;
66+
const hasEvents = this.props.events != null && this.props.events.length > 0;
67+
if (hadEvents && hasEvents) {
68+
let referenceFn: () => number;
69+
70+
// This is to allow events to be added without moving currently
71+
// visible events on viewport.
72+
if (prevProps.events[0].id > this.props.events[0].id) {
73+
referenceFn = () => document.body.scrollHeight;
74+
} else {
75+
referenceFn = () => 0;
76+
}
77+
78+
snapshot.reference = {
79+
fn: referenceFn,
80+
prev: referenceFn(),
81+
};
82+
}
83+
}
84+
85+
return snapshot;
86+
}
87+
88+
render() {
89+
this.inEvent = false;
90+
91+
return (
92+
<div className='mp-history-content'>
93+
<h3 className='mp-history-content__item'>{this.props.match.name}</h3>
94+
{this.props.hasPrevious &&
95+
<div className={classWithModifiers('mp-history-content__item', ['more'])}>
96+
<ShowMoreLink
97+
callback={this.props.loadPrevious}
98+
direction='up'
99+
hasMore
100+
loading={this.props.loadingPrevious} />
101+
</div>}
102+
{this.props.events?.map((event) => this.renderEvent(event))}
103+
{this.closeEventsGroup()}
104+
{this.props.hasNext &&
105+
<div className={classWithModifiers('mp-history-content__item', ['more'])}>
106+
{this.props.isAutoloading && <div className='mp-history-content__autoload-label'>{trans('matches.match.in_progress_spinner_label')}</div>}
107+
<ShowMoreLink
108+
callback={this.props.loadNext}
109+
hasMore
110+
loading={this.props.isAutoloading || this.props.loadingNext} />
111+
</div>}
112+
</div>
113+
);
114+
}
115+
116+
private closeEventsGroup() {
117+
if (this.inEvent) {
118+
this.inEvent = false;
119+
return <div className={classWithModifiers('mp-history-content__item', ['event', 'event-close'])} />;
120+
}
121+
}
122+
123+
private openEventsGroup() {
124+
if (!this.inEvent) {
125+
this.inEvent = true;
126+
return <div className={classWithModifiers('mp-history-content__item', ['event', 'event-open'])} />;
127+
}
128+
}
129+
130+
private renderEvent(event: LegacyMatchEventJson) {
131+
if (event.detail.type === 'other') {
132+
if (event.game == null || (event.game.end_time == null && event.game.id !== this.props.currentGameId)) {
133+
return null;
134+
}
135+
136+
return (
137+
<React.Fragment key={event.id}>
138+
{this.closeEventsGroup()}
139+
140+
<div className="mp-history-content__item">
141+
<Game
142+
game={event.game}
143+
teamScores={this.teamScores(event.game)}
144+
users={this.props.users} />
145+
</div>
146+
</React.Fragment>
147+
);
148+
} else {
149+
return (
150+
<React.Fragment key={event.id}>
151+
{this.openEventsGroup()}
152+
153+
<div className={classWithModifiers('mp-history-content__item', ['event'])}>
154+
<Event
155+
key={event.id}
156+
event={event}
157+
users={this.props.users} />
158+
</div>
159+
</React.Fragment>
160+
);
161+
}
162+
}
163+
164+
private teamScores(game: LegacyMatchGame): TeamScores {
165+
// this only caches ended games which scores shouldn't change ever.
166+
const cachedScore = this.scoresCache[game.id];
167+
168+
if (cachedScore != null) {
169+
return cachedScore;
170+
}
171+
172+
const scores: TeamScores = { blue: 0, red: 0 };
173+
174+
if (game.end_time == null) {
175+
return scores;
176+
}
177+
178+
for (const score of game.scores) {
179+
if (!score.match.pass || score.match.team === 'none') {
180+
continue;
181+
}
182+
scores[score.match.team] += score.score;
183+
}
184+
185+
return this.scoresCache[game.id] = scores;
186+
}
187+
}

resources/js/mp-history/game.tsx

+1-5
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,10 @@ import * as React from 'react';
88
import { classWithModifiers } from 'utils/css';
99
import { formatNumber } from 'utils/html';
1010
import { trans, transExists } from 'utils/lang';
11+
import { TeamScores } from './content';
1112
import GameHeader from './game-header';
1213
import Score from './score';
1314

14-
interface TeamScores {
15-
blue: number;
16-
red: number;
17-
}
18-
1915
interface Props {
2016
game: LegacyMatchGameJson;
2117
teamScores: TeamScores;

resources/js/mp-history/main.coffee

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import HeaderV4 from 'components/header-v4'
55
import { route } from 'laroute'
66
import * as React from 'react'
77
import { div } from 'react-dom-factories'
8-
import { Content } from './content'
8+
import Content from './content'
99

1010
el = React.createElement
1111

0 commit comments

Comments
 (0)