Improve fade in of Webamp

Handle a bunch of unsubscribe conditions and also start fetching the skin immediately rather then waiting 400ms for the animation to complete.

In all, Webamp should load 400ms faster
This commit is contained in:
Jordan Eldredge 2018-12-02 18:19:40 -08:00
parent 8ad18359b8
commit 95fd90e5fa
3 changed files with 126 additions and 78 deletions

17
src/Disposable.js Normal file
View file

@ -0,0 +1,17 @@
export default class Disposable {
_teardowns = [];
add(...teardowns) {
this._teardowns.push(...teardowns);
}
dispose() {
this._teardowns.forEach(teardown => {
if (typeof teardown === "function") {
teardown();
} else if (typeof teardown.unsubscribe === "function") {
teardown.unsubscribe();
} else if (typeof teardown.dispose === "function") {
teardown.dispose();
}
});
}
}

View file

@ -4,50 +4,85 @@ import WebampComponent from "./WebampComponent";
import * as Utils from "./utils";
import * as Selectors from "./redux/selectors";
import { SCREENSHOT_HEIGHT, SCREENSHOT_WIDTH } from "./constants";
import { delay, first } from "rxjs/operators";
import { Subject, combineLatest, timer } from "rxjs";
import Disposable from "./Disposable";
class FocusedSkin extends React.Component {
constructor(props) {
super(props);
this._disposable = new Disposable();
// TODO: Handle the case were we come from a permalink
if (this.props.initialPosition == null) {
this.state = Object.assign({ loaded: false }, this._getCenteredState());
this.state = Object.assign(
{ loaded: false, transitionComplete: true },
this._getCenteredState()
);
} else {
this.state = {
loaded: false,
centered: false,
transitionComplete: false,
top: this.props.initialPosition.top,
left: this.props.initialPosition.left,
width: this.props.initialWidth,
height: this.props.initialHeight
};
}
this._webampLoadedEvents = new Subject();
this._transitionBeginEvents = new Subject();
}
componentDidMount() {
setTimeout(() => {
// TODO: Observe DOM and recenter
this.setState(this._getCenteredState());
}, 0);
if (!this.props.centered) {
this._disposable.add(
timer(0).subscribe(() => {
// TODO: Observe DOM and recenter
this.setState(this._getCenteredState());
this._transitionBeginEvents.next(null);
})
);
} else {
this._transitionBeginEvents.next(null);
}
const transitionComplete = this._transitionBeginEvents.pipe(delay(500));
// Emit after both Webamp has loaded, and the transition is complete
const startWebampFadein = combineLatest(
this._webampLoadedEvents,
transitionComplete
).pipe(first());
// This value matches the opacity transition timing for `#webamp` in CSS
const webampFadeinComplete = startWebampFadein.pipe(delay(400));
this._disposable.add(
startWebampFadein.subscribe(() => {
document.body.classList.add("webamp-loaded");
this._disposable.add(() => {
document.body.classList.remove("webamp-loaded");
});
}),
webampFadeinComplete.subscribe(() => {
this.setState({ loaded: true });
})
);
}
componentWillUnmount() {
document.body.classList.remove("webamp-loaded");
this._disposable.dispose();
}
handleWebampLoaded = () => {
document.body.classList.add("webamp-loaded");
setTimeout(
() => {
this.setState({ loaded: true });
},
// This matches the transition time that Webamp takes to fade in.
400
);
this._webampLoadedEvents.next(null);
};
_getCenteredState() {
// TODO: Observe DOM and recenter
const { windowWidth, windowHeight } = Utils.getWindowSize();
return {
centered: true,
top: (windowHeight - SCREENSHOT_HEIGHT) / 2,
left: (windowWidth - SCREENSHOT_WIDTH) / 2,
height: SCREENSHOT_HEIGHT,
@ -56,49 +91,63 @@ class FocusedSkin extends React.Component {
}
render() {
const { loaded } = this.state;
const transform = `translateX(${Math.round(
this.state.left
)}px) translateY(${Math.round(this.state.top)}px)`;
return (
<div
id="focused-skin"
style={{
position: "fixed",
height: this.state.height,
width: this.state.width,
transform: `translateX(${Math.round(
this.state.left
)}px) translateY(${Math.round(this.state.top)}px)`,
transition:
"all 400ms ease-out, height 400ms ease-out, width 400ms ease-out"
}}
>
<div style={{ width: "100%", height: "100%" }}>
<WebampComponent
key={this.props.hash} // Don't reuse instances
skinUrl={Utils.skinUrlFromHash(this.props.hash)}
loaded={this.handleWebampLoaded}
/>
{this.state.loaded || (
<img
className={"focused-preview"}
style={{
width: "100%",
height: "100%",
// Webamp measure the scrollHeight of the container. Making this a
// block element ensures the parent element's scrollHeight is not
// expanded.
display: "block",
zIndex: 1
}}
src={Utils.screenshotUrlFromHash(this.props.hash)}
<React.Fragment>
{this.state.centered && (
<div
style={{
position: "fixed",
height: SCREENSHOT_HEIGHT,
width: SCREENSHOT_WIDTH,
transform
}}
>
<WebampComponent
key={this.props.hash} // Don't reuse instances
skinUrl={Utils.skinUrlFromHash(this.props.hash)}
loaded={this.handleWebampLoaded}
/>
)}
</div>
<div className="metadata">
<div className="file-name">
{Utils.filenameFromHash(this.props.hash)}
</div>
<a href={Utils.skinUrlFromHash(this.props.hash)}>Download</a>
)}
<div
id="focused-skin"
style={{
position: "fixed",
height: this.state.height,
width: this.state.width,
transform,
transition:
"all 400ms ease-out, height 400ms ease-out, width 400ms ease-out"
}}
>
<div style={{ width: "100%", height: "100%" }}>
{loaded || (
<img
className={"focused-preview"}
style={{
width: "100%",
height: "100%",
// Webamp measure the scrollHeight of the container. Making this a
// block element ensures the parent element's scrollHeight is not
// expanded.
display: "block"
}}
src={Utils.screenshotUrlFromHash(this.props.hash)}
/>
)}
</div>
<div className="metadata">
<div className="file-name">
{Utils.filenameFromHash(this.props.hash)}
</div>
<a href={Utils.skinUrlFromHash(this.props.hash)}>Download</a>
</div>
</div>
</div>
</React.Fragment>
);
}
}

View file

@ -1,18 +1,7 @@
import React from "react";
import { connect } from "react-redux";
import * as ActionCreators from "./redux/actionCreators";
class Disposable {
_subscriptions = [];
add(subscription) {
this._subscriptions.push(subscription);
}
dispose() {
this._subscriptions.forEach(subscription => {
subscription();
});
}
}
import Disposable from "./Disposable";
class WebampComponent extends React.Component {
constructor(props) {
@ -61,19 +50,12 @@ class WebampComponent extends React.Component {
this._disposable.add(this._webamp.onClose(this.props.closeModal));
const renderTimeout = setTimeout(
async () => {
await this._webamp.renderWhenReady(this._ref);
if (!this._unmounted) {
this.props.loaded();
}
},
// This number must be higher than the transition time of the CSS animation that puts this in place.
500
);
this._disposable.add(() => {
clearTimeout(renderTimeout);
});
await this._webamp.renderWhenReady(this._ref);
if (!this._unmounted) {
this.props.loaded();
} else {
// TODO: Remove Webamp
}
}
render() {