Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lazy load profile page extra sections #9327

Closed
8 changes: 1 addition & 7 deletions app/Http/Controllers/UsersController.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ public function checkUsernameExists()
public function extraPages($_id, $page)
{
// TODO: counts basically duplicated from UserCompactTransformer
// TOOD: switch to cursor pagination?
switch ($page) {
case 'beatmaps':
return [
Expand Down Expand Up @@ -639,13 +640,6 @@ public function show($id, $mode = null)
'user' => $userArray,
];

// moved data
// TODO: lazy load
$this->parsePaginationParams();
foreach (static::LAZY_EXTRA_PAGES as $page) {
$initialData[$page] = $this->extraPages($id, $page);
}

return ext_view('users.show', compact('initialData', 'user'));
}
}
Expand Down
1 change: 1 addition & 0 deletions resources/assets/less/bem-index.less
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@
@import "bem/landing-nav";
@import "bem/landing-news";
@import "bem/landing-sitemap";
@import "bem/lazy-load";
@import "bem/line-chart";
@import "bem/link";
@import "bem/livestream-featured";
Expand Down
10 changes: 10 additions & 0 deletions resources/assets/less/bem/lazy-load.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

.lazy-load {
display: flex;
align-items: center;
justify-content: center;
// TODO: need better min-height for different sections?
min-height: 50px;
}
56 changes: 56 additions & 0 deletions resources/assets/lib/components/lazy-load.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

import { Spinner } from 'components/spinner';
import { action, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';

interface Props {
onLoad: () => PromiseLike<unknown>;
}

@observer
export default class LazyLoad extends React.Component<React.PropsWithChildren<Props>> {
@observable loaded = false;

private observer: IntersectionObserver;
private ref = React.createRef<HTMLDivElement>();

constructor(props: React.PropsWithChildren<Props>) {
super(props);

this.observer = new IntersectionObserver((entries) => {
if (entries.some((entry) => entry.isIntersecting)) {
this.load();
}
});

makeObservable(this);
}

componentDidMount() {
if (this.ref.current == null) return;

this.observer.observe(this.ref.current);
}

componentWillUnmount() {
this.observer.disconnect();
}


render() {
if (!this.loaded) {
return <div ref={this.ref} className='lazy-load'><Spinner /></div>;
}

return this.props.children;
}

@action
private load() {
this.observer.disconnect();
this.props.onLoad().then(action(() => this.loaded = true));
}
}
105 changes: 78 additions & 27 deletions resources/assets/lib/components/profile-page-kudosu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,23 @@
// See the LICENCE file in the repository root for full licence text.

import KudosuHistoryJson from 'interfaces/kudosu-history-json';
import { action, makeObservable, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import ExtraHeader from 'profile-page/extra-header';
import getPage, { PageSectionWithoutCountJson } from 'profile-page/extra-page';
import * as React from 'react';
import { formatNumber } from 'utils/html';
import { OffsetPaginatorJson } from 'utils/offset-paginator';
import { parseJsonNullable, storeJson } from 'utils/json';
import { apiShowMoreRecentlyReceivedKudosu, OffsetPaginatorJson } from 'utils/offset-paginator';
import { wikiUrl } from 'utils/url';
import LazyLoad from './lazy-load';
import ShowMoreLink from './show-more-link';
import StringWithComponent from './string-with-component';
import TimeWithTooltip from './time-with-tooltip';
import ValueDisplay from './value-display';

const jsonId = 'kudosu';

function Entry({ kudosu }: { kudosu: KudosuHistoryJson }) {
const textMappings = {
amount: (
Expand Down Expand Up @@ -46,48 +52,89 @@ function Entry({ kudosu }: { kudosu: KudosuHistoryJson }) {
}

interface Props {
kudosu: OffsetPaginatorJson<KudosuHistoryJson>;
kudosu?: OffsetPaginatorJson<KudosuHistoryJson>;
name: string;
onShowMore: () => void;
total: number;
userId: number;
withEdit: boolean;
}

@observer
export default class ProfilePageKudosu extends React.Component<Props> {
@observable
private kudosu?: OffsetPaginatorJson<KudosuHistoryJson>;
private showMoreXhr?: JQuery.jqXHR<KudosuHistoryJson[]>;
private xhr?: JQuery.jqXHR<PageSectionWithoutCountJson<KudosuHistoryJson>>;

constructor(props: Props) {
super(props);

this.kudosu = parseJsonNullable(jsonId) ?? props.kudosu;

makeObservable(this);
}

componentWillUnmount(){
this.xhr?.abort();
this.showMoreXhr?.abort();
}

render() {
return (
<div className='page-extra'>
<ExtraHeader name={this.props.name} withEdit={this.props.withEdit} />

<div className='kudosu-box'>
<ValueDisplay
description={(
<StringWithComponent
mappings={{
link: (
<a href={wikiUrl('Kudosu')}>
{osu.trans('users.show.extra.kudosu.total_info.link')}
</a>
),
}}
pattern={osu.trans('users.show.extra.kudosu.total_info._')}
/>
)}
label={osu.trans('users.show.extra.kudosu.total')}
modifiers='kudosu'
value={formatNumber(this.props.total)}
/>
</div>
<LazyLoad onLoad={this.handleOnLoad} >
<div className='kudosu-box'>
<ValueDisplay
description={(
<StringWithComponent
mappings={{
link: (
<a href={wikiUrl('Kudosu')}>
{osu.trans('users.show.extra.kudosu.total_info.link')}
</a>
),
}}
pattern={osu.trans('users.show.extra.kudosu.total_info._')}
/>
)}
label={osu.trans('users.show.extra.kudosu.total')}
modifiers='kudosu'
value={formatNumber(this.props.total)}
/>
</div>

{this.renderEntries()}
{this.renderEntries()}
</LazyLoad>
</div>
);
}

@action
private readonly handleOnLoad = () => {
if (this.kudosu != null) return Promise.resolve();
this.xhr = getPage({ id: this.props.userId }, 'kudosu');

this.xhr.done((json) => runInAction(() => {
this.kudosu = json;
this.saveState();
}));

return this.xhr;
};

@action
private readonly handleShowMore = () => {
if (this.kudosu == null) return;

this.showMoreXhr = apiShowMoreRecentlyReceivedKudosu(this.kudosu, this.props.userId).done(this.saveState);
};

private renderEntries() {
if (this.props.kudosu.items.length === 0) {
if (this.kudosu == null) return null;

if (this.kudosu.items.length === 0) {
return (
<div className='profile-extra-entries profile-extra-entries--kudosu'>
{osu.trans('users.show.extra.kudosu.entry.empty')}
Expand All @@ -97,16 +144,20 @@ export default class ProfilePageKudosu extends React.Component<Props> {

return (
<ul className='profile-extra-entries profile-extra-entries--kudosu'>
{Array.isArray(this.props.kudosu.items) && this.props.kudosu.items.map((kudosu) => <Entry key={kudosu.id} kudosu={kudosu} />)}
{this.kudosu.items.map((kudosu) => <Entry key={kudosu.id} kudosu={kudosu} />)}

<li className='profile-extra-entries__item'>
<ShowMoreLink
{...this.props.kudosu.pagination}
callback={this.props.onShowMore}
{...this.kudosu.pagination}
callback={this.handleShowMore}
modifiers='profile-page'
/>
</li>
</ul>
);
}

private readonly saveState = () => {
storeJson(jsonId, this.kudosu);
};
}
27 changes: 4 additions & 23 deletions resources/assets/lib/modding-profile/kudosu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import KudosuHistoryJson from 'interfaces/kudosu-history-json';
import { makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { jsonClone, parseJsonNullable, storeJson } from 'utils/json';
import { apiShowMoreRecentlyReceivedKudosu, hasMoreCheck, OffsetPaginatorJson } from 'utils/offset-paginator';
import { jsonClone } from 'utils/json';
import { hasMoreCheck, OffsetPaginatorJson } from 'utils/offset-paginator';

interface Props {
expectedInitialCount: number;
Expand All @@ -19,8 +19,6 @@ interface Props {

type MobxState = OffsetPaginatorJson<KudosuHistoryJson>;

const jsonId = 'kudosu';

@observer
export default class Kudosu extends React.Component<Props> {
@observable private mobxState: MobxState = {
Expand All @@ -33,14 +31,8 @@ export default class Kudosu extends React.Component<Props> {
super(props);

// TODO: this should be handled by Main component instead.
const savedState = parseJsonNullable<MobxState>(jsonId);

if (savedState == null) {
this.mobxState.items = jsonClone(props.initialKudosu);
this.mobxState.pagination.hasMore = hasMoreCheck(props.expectedInitialCount, this.mobxState.items);
} else {
this.mobxState = savedState;
}
this.mobxState.items = jsonClone(props.initialKudosu);
this.mobxState.pagination.hasMore = hasMoreCheck(props.expectedInitialCount, this.mobxState.items);

makeObservable(this);
}
Expand All @@ -54,21 +46,10 @@ export default class Kudosu extends React.Component<Props> {
<ProfilePageKudosu
kudosu={this.mobxState}
name={this.props.name}
onShowMore={this.onShowMore}
total={this.props.total}
userId={this.props.userId}
withEdit={false}
/>
);
}

private readonly onShowMore = () => {
this.xhr = apiShowMoreRecentlyReceivedKudosu(this.mobxState, this.props.userId)
.done(this.saveState);
};


private readonly saveState = () => {
storeJson(jsonId, this.mobxState);
};
}
9 changes: 8 additions & 1 deletion resources/assets/lib/profile-page/beatmapsets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.

import BeatmapsetPanel from 'components/beatmapset-panel';
import LazyLoad from 'components/lazy-load';
import ProfilePageExtraSectionTitle from 'components/profile-page-extra-section-title';
import ShowMoreLink from 'components/show-more-link';
import { observer } from 'mobx-react';
Expand Down Expand Up @@ -42,17 +43,23 @@ export default class Beatmapsets extends React.Component<ExtraPageProps> {
return (
<div className='page-extra'>
<ExtraHeader name={this.props.name} withEdit={this.props.controller.withEdit} />
{sectionKeys.map(this.renderBeatmapsets)}
<LazyLoad onLoad={this.handleOnLoad}>
{sectionKeys.map(this.renderBeatmapsets)}
</LazyLoad>
</div>
);
}

private readonly handleOnLoad = () => this.props.controller.getBeatmapsets();

private readonly onShowMore = (section: BeatmapsetSection) => {
this.props.controller.apiShowMore(section);
};

private readonly renderBeatmapsets = (section: typeof sectionKeys[number]) => {
const state = this.props.controller.state.beatmapsets;
if (state == null) return;

const count = state[section.key].count;
const beatmapsets = state[section.key].items;
const pagination = state[section.key].pagination;
Expand Down