Reduxify a bunch of the state, add seekTime URL parameter (#12)

* Start the process of reduxifying. Some of it works

* better seeking

* Seeking and stuff

* Add segment functionality

* Fix tests

* Fix more tests
main
Chris Vickery 2018-08-15 12:53:21 -07:00 committed by GitHub
parent dff700a462
commit e755a4e911
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1067 additions and 459 deletions

View File

@ -35,6 +35,7 @@
"localforage": "^1.7.1",
"moment": "^2.18.1",
"node-sass": "^4.7.2",
"obstruction": "^2.1.0",
"prettier": "^1.9.2",
"prop-types": "^15.5.10",
"raven-js": "^3.16.0",
@ -45,10 +46,13 @@
"react-infinite": "^0.11.0",
"react-list": "^0.8.6",
"react-measure": "^2.0.2",
"react-redux": "^5.0.7",
"react-scripts": "1.0.17",
"react-test-renderer": "^16.2.0",
"react-vega": "^3.0.0",
"react-visibility-sensor": "^3.10.1",
"redux": "^4.0.0",
"redux-thunk": "^2.3.0",
"simple-statistics": "^4.1.0",
"socket.io-client": "^2.0.3",
"stream-selector": "^0.1.1",

View File

@ -1,4 +1,6 @@
import React, { Component } from "react";
import { connect, Provider } from "react-redux";
import Obstruction from "obstruction";
import Moment from "moment";
import PropTypes from "prop-types";
import cx from "classnames";
@ -29,13 +31,15 @@ import UnloggerClient from "./api/unlogger";
import * as ObjectUtils from "./utils/object";
import { hash } from "./utils/string";
import { selectRoute, setLoading, loadRoutes } from "./actions";
const RLogDownloader = require("./workers/rlog-downloader.worker.js");
const LogCSVDownloader = require("./workers/dbc-csv-downloader.worker.js");
const MessageParser = require("./workers/message-parser.worker.js");
const CanOffsetFinder = require("./workers/can-offset-finder.worker.js");
const CanStreamerWorker = require("./workers/CanStreamerWorker.worker.js");
export default class CanExplorer extends Component {
class CanExplorer extends Component {
static propTypes = {
dongleId: PropTypes.string,
name: PropTypes.string,
@ -44,7 +48,8 @@ export default class CanExplorer extends Component {
githubAuthToken: PropTypes.string,
autoplay: PropTypes.bool,
max: PropTypes.number,
url: PropTypes.string
url: PropTypes.string,
selectedParts: PropTypes.array
};
constructor(props) {
@ -52,13 +57,10 @@ export default class CanExplorer extends Component {
this.state = {
messages: {},
selectedMessages: [],
route: null,
routes: [],
canFrameOffset: -1,
firstCanTime: 0,
lastBusTime: null,
selectedMessage: null,
currentParts: [0, 0],
showOnboarding: false,
showLoadDbc: false,
showSaveDbc: false,
@ -68,7 +70,7 @@ export default class CanExplorer extends Component {
dbcText: props.dbc ? props.dbc.text() : new DBC().text(),
dbcFilename: props.dbcFilename ? props.dbcFilename : "New_DBC",
dbcLastSaved: null,
seekTime: 0,
seekTime: props.seekTime ? props.seekTime : 0,
seekIndex: 0,
maxByteStateChangeCount: 0,
isLoading: true,
@ -134,13 +136,8 @@ export default class CanExplorer extends Component {
url: url,
start_time: startTime
};
this.setState(
{
route,
currentParts: [0, Math.min(max, PART_SEGMENT_LENGTH - 1)]
},
this.initCanData
);
this.props.dispatch(selectRoute(route));
this.initCanData();
} else if (auth.isAuthenticated() && !name) {
Routes.fetchRoutes()
.then(routes => {
@ -148,7 +145,7 @@ export default class CanExplorer extends Component {
Object.keys(routes).forEach(route => {
_routes.push(routes[route]);
});
this.setState({ routes: _routes });
this.props.dispatch(loadRoutes(_routes));
if (!_routes[name]) {
this.showOnboarding();
}
@ -162,14 +159,8 @@ export default class CanExplorer extends Component {
if (routes && routes[name]) {
// this makes fullname = dongleId + '|' + name
const route = routes[name];
const newState = {
route,
currentParts: [
0,
Math.min(route.proclog, PART_SEGMENT_LENGTH - 1)
]
};
this.setState(newState, this.initCanData);
this.props.dispatch(selectRoute(route));
this.initCanData();
} else {
this.showOnboarding();
}
@ -180,10 +171,18 @@ export default class CanExplorer extends Component {
} else {
this.showOnboarding();
}
this.onPartChange(this.props.selectedParts[0]);
}
componentWillReceiveProps(newProps) {
if (this.props.selectedParts[0] !== newProps.selectedParts[0]) {
this.onPartChange(newProps.selectedParts[0]);
}
}
initCanData() {
const { route } = this.state;
const { route } = this.props;
const offsetFinder = new CanOffsetFinder();
offsetFinder.postMessage({
@ -195,13 +194,13 @@ export default class CanExplorer extends Component {
const { canFrameOffset, firstCanTime } = e.data;
this.setState({ canFrameOffset, firstCanTime }, () => {
this.spawnWorker(this.state.currentParts);
this.spawnWorker(this.props.selectedParts);
});
};
}
onDbcSelected(dbcFilename, dbc) {
const { route } = this.state;
const { route } = this.props;
this.hideLoadDbc();
this.persistDbc({ dbcFilename, dbc });
@ -217,7 +216,7 @@ export default class CanExplorer extends Component {
},
() => {
// Pass DBC text to webworker b/c can't pass instance of es6 class
this.spawnWorker(this.state.currentParts);
this.spawnWorker(this.props.selectedParts);
}
);
} else {
@ -265,7 +264,8 @@ export default class CanExplorer extends Component {
}
downloadRawLogAsCSV(handler) {
// Trigger file processing and dowload in worker
const { firstCanTime, canFrameOffset, route } = this.state;
const { firstCanTime, canFrameOffset } = this.state;
const { route } = this.props;
const worker = new LogCSVDownloader();
worker.onmessage = handler;
@ -328,8 +328,8 @@ export default class CanExplorer extends Component {
spawnWorker(parts, options) {
console.log("Spawning worker for", parts);
if (!this.state.isLoading) {
this.setState({ isLoading: true });
if (!this.props.isLoading) {
this.props.dispatch(setLoading(true));
}
// options is object of {part, prevMsgEntries, spawnWorkerHash, prepend}
const [minPart, maxPart] = parts;
@ -351,11 +351,11 @@ export default class CanExplorer extends Component {
const {
dbc,
dbcFilename,
route,
firstCanTime,
canFrameOffset,
maxByteStateChangeCount
} = this.state;
const { route } = this.props;
// var worker = new CanFetcher();
var worker = new RLogDownloader();
@ -500,7 +500,7 @@ export default class CanExplorer extends Component {
}
persistDbc({ dbcFilename, dbc }) {
const { route } = this.state;
const { route } = this.props;
if (route) {
persistDbc(route.fullname, { dbcFilename, dbc });
} else {
@ -529,21 +529,22 @@ export default class CanExplorer extends Component {
}
partChangeDebounced = debounce(() => {
const { currentParts } = this.state;
this.spawnWorker(currentParts);
const { selectedParts } = this.props;
this.spawnWorker(selectedParts);
}, 500);
onPartChange(part) {
let { currentParts, canFrameOffset, route, messages } = this.state;
let { canFrameOffset, messages } = this.state;
let { route, selectedParts } = this.props;
if (canFrameOffset === -1 || part + PART_SEGMENT_LENGTH > route.proclog) {
return;
}
// determine new parts to load, whether to prepend or append
const currentPartSpan = currentParts[1] - currentParts[0] + 1;
const currentPartSpan = selectedParts[1] - selectedParts[0] + 1;
// update current parts
currentParts = [part, part + currentPartSpan - 1];
selectedParts = [part, part + currentPartSpan - 1];
// update messages to only preserve entries in new part range
const messagesKvPairs = Object.entries(messages).map(
@ -558,10 +559,7 @@ export default class CanExplorer extends Component {
messages = ObjectUtils.fromArray(messagesKvPairs);
// update state then load new parts
this.setState(
{ currentParts, messages, seekTime: part * 60 },
this.partChangeDebounced
);
this.setState({ messages }, this.partChangeDebounced);
}
showEditMessageModal(msgKey) {
@ -615,11 +613,12 @@ export default class CanExplorer extends Component {
seekIndex = 0;
}
this.setState({ seekIndex, seekTime });
this.setState({ seekIndex });
}
onMessageSelected(msgKey) {
let { seekTime, seekIndex, messages } = this.state;
let { messages } = this.state;
let { seekTime, seekIndex } = this.props;
const msg = messages[msgKey];
if (seekTime > 0 && msg.entries.length > 0) {
@ -643,7 +642,7 @@ export default class CanExplorer extends Component {
}
loginWithGithub() {
const { route } = this.state;
const { route } = this.props;
return (
<a
href={GithubAuth.authorizeUrl(
@ -803,120 +802,129 @@ export default class CanExplorer extends Component {
render() {
return (
<div
id="cabana"
className={cx({ "is-showing-modal": this.showingModal() })}
>
{this.state.isLoading ? (
<LoadingBar isLoading={this.state.isLoading} />
) : null}
<div className="cabana-header">
<a className="cabana-header-logo" href="">
Comma Cabana
</a>
<div className="cabana-header-account">
{this.state.isGithubAuthenticated ? (
<div>
<p>GitHub Authenticated</p>
<p
className="cabana-header-account-signout"
onClick={this.githubSignOut}
>
Sign out
</p>
</div>
) : (
this.loginWithGithub()
)}
<Provider store={this.props.store}>
<div
id="cabana"
className={cx({ "is-showing-modal": this.showingModal() })}
>
{this.state.isLoading ? (
<LoadingBar isLoading={this.state.isLoading} />
) : null}
<div className="cabana-header">
<a className="cabana-header-logo" href="">
Comma Cabana
</a>
<div className="cabana-header-account">
{this.state.isGithubAuthenticated ? (
<div>
<p>GitHub Authenticated</p>
<p
className="cabana-header-account-signout"
onClick={this.githubSignOut}
>
Sign out
</p>
</div>
) : (
this.loginWithGithub()
)}
</div>
</div>
</div>
<div className="cabana-window">
<Meta
url={this.state.route ? this.state.route.url : null}
messages={this.state.messages}
selectedMessages={this.state.selectedMessages}
updateSelectedMessages={this.updateSelectedMessages}
showEditMessageModal={this.showEditMessageModal}
currentParts={this.state.currentParts}
onMessageSelected={this.onMessageSelected}
onMessageUnselected={this.onMessageUnselected}
showLoadDbc={this.showLoadDbc}
showSaveDbc={this.showSaveDbc}
dbcFilename={this.state.dbcFilename}
dbcLastSaved={this.state.dbcLastSaved}
dongleId={this.props.dongleId}
name={this.props.name}
route={this.state.route}
seekTime={this.state.seekTime}
seekIndex={this.state.seekIndex}
maxByteStateChangeCount={this.state.maxByteStateChangeCount}
isDemo={this.props.isDemo}
live={this.state.live}
saveLog={debounce(this.downloadLogAsCSV, 500)}
/>
{this.state.route || this.state.live ? (
<Explorer
url={this.state.route ? this.state.route.url : null}
live={this.state.live}
<div className="cabana-window">
<Meta
url={this.props.route ? this.props.route.url : null}
messages={this.state.messages}
selectedMessage={this.state.selectedMessage}
onConfirmedSignalChange={this.onConfirmedSignalChange}
onSeek={this.onSeek}
onUserSeek={this.onUserSeek}
canFrameOffset={this.state.canFrameOffset}
firstCanTime={this.state.firstCanTime}
seekTime={this.state.seekTime}
seekIndex={this.state.seekIndex}
currentParts={this.state.currentParts}
partsLoaded={this.state.partsLoaded}
autoplay={this.props.autoplay}
selectedMessages={this.state.selectedMessages}
updateSelectedMessages={this.updateSelectedMessages}
showEditMessageModal={this.showEditMessageModal}
onPartChange={this.onPartChange}
routeStartTime={
this.state.route ? this.state.route.start_time : Moment()
}
partsCount={this.state.route ? this.state.route.proclog : 0}
onMessageSelected={this.onMessageSelected}
onMessageUnselected={this.onMessageUnselected}
showLoadDbc={this.showLoadDbc}
showSaveDbc={this.showSaveDbc}
dbcFilename={this.state.dbcFilename}
dbcLastSaved={this.state.dbcLastSaved}
dongleId={this.props.dongleId}
name={this.props.name}
route={this.props.route}
seekTime={this.props.seekTime}
seekIndex={this.props.seekIndex}
maxByteStateChangeCount={this.state.maxByteStateChangeCount}
isDemo={this.props.isDemo}
live={this.state.live}
saveLog={debounce(this.downloadLogAsCSV, 500)}
/>
{this.props.route || this.state.live ? (
<Explorer
url={this.props.route ? this.props.route.url : null}
live={this.state.live}
messages={this.state.messages}
selectedMessage={this.state.selectedMessage}
onConfirmedSignalChange={this.onConfirmedSignalChange}
canFrameOffset={this.state.canFrameOffset}
firstCanTime={this.state.firstCanTime}
seekTime={this.props.seekTime}
seekIndex={this.props.seekIndex}
partsLoaded={this.state.partsLoaded}
autoplay={this.props.autoplay}
showEditMessageModal={this.showEditMessageModal}
onPartChange={this.onPartChange}
routeStartTime={
this.props.route ? this.props.route.start_time : Moment()
}
partsCount={this.props.route ? this.props.route.proclog : 0}
/>
) : null}
</div>
{this.state.showOnboarding ? (
<OnboardingModal
handlePandaConnect={this.handlePandaConnect}
attemptingPandaConnection={this.state.attemptingPandaConnection}
routes={this.props.routes}
/>
) : null}
{this.state.showLoadDbc ? (
<LoadDbcModal
onDbcSelected={this.onDbcSelected}
handleClose={this.hideLoadDbc}
openDbcClient={this.openDbcClient}
loginWithGithub={this.loginWithGithub()}
/>
) : null}
{this.state.showSaveDbc ? (
<SaveDbcModal
dbc={this.state.dbc}
sourceDbcFilename={this.state.dbcFilename}
onDbcSaved={this.onDbcSaved}
handleClose={this.hideSaveDbc}
openDbcClient={this.openDbcClient}
hasGithubAuth={this.props.githubAuthToken !== null}
loginWithGithub={this.loginWithGithub()}
/>
) : null}
{this.state.showEditMessageModal ? (
<EditMessageModal
handleClose={this.hideEditMessageModal}
handleSave={this.onMessageFrameEdited}
message={this.state.messages[this.state.editMessageModalMessage]}
/>
) : null}
</div>
{this.state.showOnboarding ? (
<OnboardingModal
handlePandaConnect={this.handlePandaConnect}
attemptingPandaConnection={this.state.attemptingPandaConnection}
routes={this.state.routes}
/>
) : null}
{this.state.showLoadDbc ? (
<LoadDbcModal
onDbcSelected={this.onDbcSelected}
handleClose={this.hideLoadDbc}
openDbcClient={this.openDbcClient}
loginWithGithub={this.loginWithGithub()}
/>
) : null}
{this.state.showSaveDbc ? (
<SaveDbcModal
dbc={this.state.dbc}
sourceDbcFilename={this.state.dbcFilename}
onDbcSaved={this.onDbcSaved}
handleClose={this.hideSaveDbc}
openDbcClient={this.openDbcClient}
hasGithubAuth={this.props.githubAuthToken !== null}
loginWithGithub={this.loginWithGithub()}
/>
) : null}
{this.state.showEditMessageModal ? (
<EditMessageModal
handleClose={this.hideEditMessageModal}
handleSave={this.onMessageFrameEdited}
message={this.state.messages[this.state.editMessageModalMessage]}
/>
) : null}
</div>
</Provider>
);
}
}
const stateToProps = Obstruction({
seekTime: "playback.seekTime",
seekIndex: "playback.seekIndex",
selectedParts: "playback.selectedParts",
isLoading: "playback.isLoading",
route: "route.route",
routes: "route.routes"
});
export default connect(stateToProps)(CanExplorer);

View File

@ -4,6 +4,9 @@ import React from "react";
import { shallow, mount, render } from "enzyme";
import { StyleSheetTestUtils } from "aphrodite";
import createStore from "../../store";
const store = createStore();
test("CanExplorer renders", () => {
const canExplorer = shallow(<CanExplorer />);
const canExplorer = shallow(<CanExplorer store={store} />);
});

View File

@ -4,28 +4,34 @@ import CanGraph from "../../components/CanGraph";
import React from "react";
import { shallow, mount, render } from "enzyme";
import { Provider } from "react-redux";
import createStore from "../../store";
const store = createStore();
test("CanGraph successfully mounts with minimal default props", () => {
const component = shallow(
<CanGraph
onGraphRefAvailable={() => {}}
unplot={() => {}}
messages={{}}
messageId={null}
messageName={null}
signalSpec={null}
onSegmentChanged={() => {}}
segment={[]}
data={{}}
onRelativeTimeClick={() => {}}
currentTime={0}
onDragStart={() => {}}
onDragEnd={() => {}}
container={null}
dragPos={null}
canReceiveGraphDrop={false}
plottedSignals={[]}
live={true}
/>
<Provider store={store}>
<CanGraph
onGraphRefAvailable={() => {}}
unplot={() => {}}
messages={{}}
messageId={null}
messageName={null}
signalSpec={null}
onSegmentChanged={() => {}}
segment={[]}
data={{}}
onRelativeTimeClick={() => {}}
currentTime={0}
onDragStart={() => {}}
onDragEnd={() => {}}
container={null}
dragPos={null}
canReceiveGraphDrop={false}
plottedSignals={[]}
live={true}
/>
</Provider>
);
expect(component.exists()).toBe(true);
});

View File

@ -4,20 +4,26 @@ import CanGraphList from "../../components/CanGraphList";
import React from "react";
import { shallow, mount, render } from "enzyme";
import { Provider } from "react-redux";
import createStore from "../../store";
const store = createStore();
test("CanGraphList successfully mounts with minimal default props", () => {
const component = shallow(
<CanGraphList
plottedSignals={[]}
messages={{}}
graphData={[]}
onGraphTimeClick={() => {}}
seekTime={0}
onSegmentChanged={() => {}}
onSignalUnplotPressed={() => {}}
segment={[]}
mergePlots={() => {}}
live={true}
/>
<Provider store={store}>
<CanGraphList
plottedSignals={[]}
messages={{}}
graphData={[]}
onGraphTimeClick={() => {}}
seekTime={0}
onSegmentChanged={() => {}}
onSignalUnplotPressed={() => {}}
segment={[]}
mergePlots={() => {}}
live={true}
/>
</Provider>
);
expect(component.exists()).toBe(true);
});

View File

@ -4,18 +4,24 @@ import CanLog from "../../components/CanLog";
import React from "react";
import { shallow, mount, render } from "enzyme";
import { Provider } from "react-redux";
import createStore from "../../store";
const store = createStore();
test("CanLog successfully mounts with minimal default props", () => {
const component = shallow(
<CanLog
message={null}
messageIndex={0}
segmentIndices={[]}
plottedSignals={[]}
onSignalPlotPressed={() => {}}
onSignalUnplotPressed={() => {}}
showAddSignal={() => {}}
onMessageExpanded={() => {}}
/>
<Provider store={store}>
<CanLog
message={null}
messageIndex={0}
segmentIndices={[]}
plottedSignals={[]}
onSignalPlotPressed={() => {}}
onSignalUnplotPressed={() => {}}
showAddSignal={() => {}}
onMessageExpanded={() => {}}
/>
</Provider>
);
expect(component.exists()).toBe(true);
});

View File

@ -5,28 +5,34 @@ import React from "react";
import Moment from "moment";
import { shallow, mount, render } from "enzyme";
import { Provider } from "react-redux";
import createStore from "../../store";
const store = createStore();
test("Explorer successfully mounts with minimal default props", () => {
const component = shallow(
<Explorer
url={null}
live={true}
messages={{}}
selectedMessage={null}
onConfirmedSignalChange={() => {}}
onSeek={() => {}}
onUserSeek={() => {}}
canFrameOffset={0}
firstCanTime={0}
seekTime={0}
seekIndex={0}
currentParts={[0, 0]}
partsLoaded={0}
autoplay={true}
showEditMessageModal={() => {}}
onPartChange={() => {}}
routeStartTime={Moment()}
partsCount={0}
/>
<Provider store={store}>
<Explorer
url={null}
live={true}
messages={{}}
selectedMessage={null}
onConfirmedSignalChange={() => {}}
onSeek={() => {}}
onUserSeek={() => {}}
canFrameOffset={0}
firstCanTime={0}
seekTime={0}
seekIndex={0}
currentParts={[0, 0]}
partsLoaded={0}
autoplay={true}
showEditMessageModal={() => {}}
onPartChange={() => {}}
routeStartTime={Moment()}
partsCount={0}
/>
</Provider>
);
expect(component.exists()).toBe(true);
});

View File

@ -2,23 +2,29 @@ import HLS from "../../components/HLS";
import React from "react";
import { shallow, mount, render } from "enzyme";
import { Provider } from "react-redux";
import createStore from "../../store";
const store = createStore();
test("HLS successfully mounts with minimal default props", () => {
const component = shallow(
<HLS
source={"http://comma.ai"}
startTime={0}
playbackSpeed={1}
onVideoElementAvailable={() => {}}
playing={false}
onClick={() => {}}
onLoadStart={() => {}}
onLoadEnd={() => {}}
onUserSeek={() => {}}
onPlaySeek={() => {}}
segmentProgress={() => {}}
shouldRestart={false}
onRestart={() => {}}
/>
<Provider store={store}>
<HLS
source={"http://comma.ai"}
startTime={0}
playbackSpeed={1}
onVideoElementAvailable={() => {}}
playing={false}
onClick={() => {}}
onLoadStart={() => {}}
onLoadEnd={() => {}}
onUserSeek={() => {}}
onPlaySeek={() => {}}
segmentProgress={() => {}}
shouldRestart={false}
onRestart={() => {}}
/>
</Provider>
);
expect(component.exists()).toBe(true);
});

View File

@ -7,10 +7,16 @@ import MessageBytes from "../../components/MessageBytes";
import DbcUtils from "../../utils/dbc";
import DBC from "../../models/can/dbc";
import { Provider } from "react-redux";
import createStore from "../../store";
const store = createStore();
test("MessageBytes successfully mounts with minimal default props", () => {
const message = DbcUtils.createMessageSpec(new DBC(), 0, "0", 1);
const component = shallow(
<MessageBytes seekTime={0} message={message} live={true} />
<Provider store={store}>
<MessageBytes seekTime={0} message={message} live={true} />
</Provider>
);
expect(component.exists()).toBe(true);
});

View File

@ -4,30 +4,36 @@ import Meta from "../../components/Meta";
import React from "react";
import { shallow, mount, render } from "enzyme";
import { Provider } from "react-redux";
import createStore from "../../store";
const store = createStore();
test("Meta successfully mounts with minimal default props", () => {
const component = shallow(
<Meta
url={null}
messages={{}}
selectedMessages={[]}
updateSelectedMessages={() => {}}
showEditMessageModal={() => {}}
currentParts={[]}
onMessageSelected={() => {}}
onMessageUnselected={() => {}}
showLoadDbc={() => {}}
showSaveDbc={() => {}}
dbcFilename={null}
dbcLastSaved={null}
dongleId={null}
name={null}
route={null}
seekTime={0}
seekIndex={0}
maxByteStateChangeCount={0}
isDemo={false}
live={true}
/>
<Provider store={store}>
<Meta
url={null}
messages={{}}
selectedMessages={[]}
updateSelectedMessages={() => {}}
showEditMessageModal={() => {}}
currentParts={[]}
onMessageSelected={() => {}}
onMessageUnselected={() => {}}
showLoadDbc={() => {}}
showSaveDbc={() => {}}
dbcFilename={null}
dbcLastSaved={null}
dongleId={null}
name={null}
route={null}
seekTime={0}
seekIndex={0}
maxByteStateChangeCount={0}
isDemo={false}
live={true}
/>
</Provider>
);
expect(component.exists()).toBe(true);
});

View File

@ -4,9 +4,15 @@ import PartSelector from "../../components/PartSelector";
import React from "react";
import { shallow, mount, render } from "enzyme";
import { Provider } from "react-redux";
import createStore from "../../store";
const store = createStore();
test("PartSelector successfully mounts with minimal default props", () => {
const component = shallow(
<PartSelector onPartChange={() => {}} partsCount={0} />
<Provider store={store}>
<PartSelector onPartChange={() => {}} partsCount={0} />
</Provider>
);
expect(component.exists()).toBe(true);
});

View File

@ -4,21 +4,26 @@ import RouteSeeker from "../../components/RouteSeeker";
import React from "react";
import { shallow, mount, render } from "enzyme";
import { Provider } from "react-redux";
import createStore from "../../store";
const store = createStore();
test("RouteSeeker successfully mounts with minimal default props", () => {
const component = shallow(
<RouteSeeker
nearestFrameTime={0}
segmentProgress={() => {}}
secondsLoaded={0}
segmentIndices={[]}
onUserSeek={() => {}}
onPlaySeek={() => {}}
videoElement={null}
onPlay={() => {}}
onPause={() => {}}
playing={false}
ratioTime={() => {}}
/>
<Provider store={store}>
<RouteSeeker
nearestFrameTime={0}
segmentProgress={() => {}}
secondsLoaded={0}
onUserSeek={() => {}}
onPlaySeek={() => {}}
videoElement={null}
onPlay={() => {}}
onPause={() => {}}
playing={false}
ratioTime={() => {}}
/>
</Provider>
);
expect(component.exists()).toBe(true);
});

View File

@ -5,6 +5,10 @@ import React from "react";
import { shallow, mount, render } from "enzyme";
import { StyleSheetTestUtils } from "aphrodite";
import { Provider } from "react-redux";
import createStore from "../../store";
const store = createStore();
// Prevents style injection from firing after test finishes
// and jsdom is torn down.
beforeEach(() => {
@ -16,23 +20,25 @@ afterEach(() => {
test("RouteVideoSync successfully mounts with minimal default props", () => {
const component = shallow(
<RouteVideoSync
message={null}
secondsLoaded={0}
startOffset={0}
seekIndex={0}
userSeekIndex={0}
playing={false}
url={"http://comma.ai"}
canFrameOffset={0}
firstCanTime={0}
onVideoClick={() => {}}
onPlaySeek={() => {}}
onUserSeek={() => {}}
onPlay={() => {}}
onPause={() => {}}
userSeekTime={0}
/>
<Provider store={store}>
<RouteVideoSync
message={null}
secondsLoaded={0}
startOffset={0}
seekIndex={0}
userSeekIndex={0}
playing={false}
url={"http://comma.ai"}
canFrameOffset={0}
firstCanTime={0}
onVideoClick={() => {}}
onPlaySeek={() => {}}
onUserSeek={() => {}}
onPlay={() => {}}
onPause={() => {}}
userSeekTime={0}
/>
</Provider>
);
expect(component.exists()).toBe(true);
});

View File

@ -0,0 +1,166 @@
import {
ACTION_SEEK,
ACTION_SELECT_PART,
ACTION_AUTO_SEEK,
ACTION_SET_LOADING,
ACTION_SELECT_ROUTE,
ACTION_LOAD_ROUTES,
ACTION_SET_MAX_TIME,
ACTION_SELECT_SEGMENT
} from "./types";
import { PART_SEGMENT_LENGTH } from "../config";
export function seek(time, index) {
// console.log("Seek happened", time, index);
return function(dispatch, getState) {
const state = getState();
let suggestedPart = state.playback.selectedParts[0];
if (
time / 60 < state.playback.selectedParts[0] ||
Math.floor(time / 60) > state.playback.selectedParts[1]
) {
suggestedPart = getPartForTime(time);
}
if (suggestedPart > state.route.maxParts - PART_SEGMENT_LENGTH + 1) {
suggestedPart = state.route.maxParts - PART_SEGMENT_LENGTH + 1;
}
let maxPart = suggestedPart + PART_SEGMENT_LENGTH - 1;
if (maxPart > state.route.maxParts) {
maxPart = state.route.maxParts;
}
if (suggestedPart !== state.playback.selectedParts[0]) {
console.log(
"changing part in a seek",
state.playback.selectedParts[0],
suggestedPart
);
}
dispatch({
type: ACTION_SEEK,
selectedParts: [suggestedPart, maxPart],
time,
index
});
};
}
export function autoSeek(time) {
// console.log("autoSeek happened", time);
return function(dispatch, getState) {
const state = getState();
let maxTime = Math.min(
state.playback.maxTime,
(1 + state.playback.selectedParts[1]) * 60
);
if (state.segment.segment.length) {
time = Math.max(state.segment.segment[0], time);
maxTime = Math.min(maxTime, state.segment.segment[1]);
}
if (time >= maxTime) {
time = state.playback.selectedParts[0] * 60;
dispatch({
type: ACTION_SEEK,
selectedParts: state.playback.selectedParts,
time
});
} else {
dispatch({
type: ACTION_AUTO_SEEK,
time
});
}
};
}
export function selectPart(part) {
return function(dispatch, getState) {
const state = getState();
let selectedPart = part;
if (selectedPart < 0) {
selectedPart = state.playback.selectedParts[0];
}
if (selectedPart > state.route.maxParts - PART_SEGMENT_LENGTH + 1) {
selectedPart = state.route.maxParts - PART_SEGMENT_LENGTH + 1;
}
let maxPart = selectedPart + PART_SEGMENT_LENGTH - 1;
if (maxPart > state.route.maxParts) {
maxPart = state.route.maxParts;
}
if (selectedPart !== state.playback.selectedParts[0]) {
console.log(
"selecting new part!",
state.playback.selectedParts[0],
selectedPart
);
}
let seekTime = Math.min(
getEndTimeForPart(selectedPart),
state.playback.seekTime
);
seekTime = Math.max(getTimeForPart(selectedPart), seekTime);
dispatch({
type: ACTION_SELECT_PART,
selectedParts: [selectedPart, maxPart],
part,
seekTime
});
};
}
export function setLoading(isLoading) {
return {
type: ACTION_SET_LOADING,
isLoading
};
}
export function selectRoute(route) {
return function(dispatch, getState) {
dispatch({
type: ACTION_SELECT_ROUTE,
route
});
dispatch(selectPart(-1));
};
}
export function loadRoutes(routes) {
return {
type: ACTION_LOAD_ROUTES,
routes
};
}
export function setMaxTime(maxTime) {
return {
type: ACTION_SET_MAX_TIME,
maxTime
};
}
export function selectSegment(segment, segmentIndices) {
console.log("Sertting segment to", segment, segmentIndices);
return {
type: ACTION_SELECT_SEGMENT,
segment,
segmentIndices
};
}
function getPartForTime(time) {
return Math.floor(time / 60);
}
function getTimeForPart(part) {
return Math.floor(part * 60);
}
function getEndTimeForPart(part) {
return Math.floor((part + PART_SEGMENT_LENGTH) * 60 - 1);
}

View File

@ -0,0 +1,8 @@
export const ACTION_SEEK = "ACTION_SEEK";
export const ACTION_SELECT_PART = "ACTION_SELECT_PART";
export const ACTION_AUTO_SEEK = "ACTION_AUTO_SEEK";
export const ACTION_SET_LOADING = "ACTION_SET_LOADING";
export const ACTION_LOAD_ROUTES = "ACTION_LOAD_ROUTES";
export const ACTION_SELECT_ROUTE = "ACTION_SELECT_ROUTE";
export const ACTION_SET_MAX_TIME = "ACTION_SET_MAX_TIME";
export const ACTION_SELECT_SEGMENT = "ACTION_SELECT_SEGMENT";

View File

@ -1,4 +1,6 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import Obstruction from "obstruction";
import Measure from "react-measure";
import PropTypes from "prop-types";
import cx from "classnames";
@ -12,7 +14,7 @@ const DefaultPlotInnerStyle = {
left: 0
};
export default class CanGraph extends Component {
class CanGraph extends Component {
static emptyTable = [];
static propTypes = {
@ -24,7 +26,7 @@ export default class CanGraph extends Component {
segment: PropTypes.array,
unplot: PropTypes.func,
onRelativeTimeClick: PropTypes.func,
currentTime: PropTypes.number,
seekTime: PropTypes.number,
onSegmentChanged: PropTypes.func,
onDragStart: PropTypes.func,
onDragEnd: PropTypes.func,
@ -100,8 +102,8 @@ export default class CanGraph extends Component {
segmentChanged = true;
}
if (!nextProps.live && nextProps.currentTime !== this.props.currentTime) {
this.view.signal("videoTime", nextProps.currentTime);
if (!nextProps.live && nextProps.seekTime !== this.props.seekTime) {
this.view.signal("videoTime", nextProps.seekTime);
segmentChanged = true;
}
@ -308,3 +310,10 @@ export default class CanGraph extends Component {
);
}
}
const stateToProps = Obstruction({
segment: "segment.segment",
seekTime: "playback.seekTime"
});
export default connect(stateToProps)(CanGraph);

View File

@ -1,17 +1,18 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import Obstruction from "obstruction";
import PropTypes from "prop-types";
import CanGraph from "./CanGraph";
require("element-closest");
export default class CanGraphList extends Component {
class CanGraphList extends Component {
static propTypes = {
plottedSignals: PropTypes.array.isRequired,
messages: PropTypes.object.isRequired,
graphData: PropTypes.array.isRequired,
onGraphTimeClick: PropTypes.func.isRequired,
seekTime: PropTypes.number.isRequired,
onSegmentChanged: PropTypes.func.isRequired,
onSignalUnplotPressed: PropTypes.func.isRequired,
segment: PropTypes.array.isRequired,
@ -152,10 +153,8 @@ export default class CanGraphList extends Component {
messageName={msg.frame ? msg.frame.name : null}
signalSpec={Object.assign(Object.create(signal), signal)}
onSegmentChanged={this.props.onSegmentChanged}
segment={this.props.segment}
data={this.props.graphData[index]}
onRelativeTimeClick={this.props.onGraphTimeClick}
currentTime={this.props.seekTime}
onDragStart={this.onGraphDragStart}
onDragEnd={this.onGraphDragEnd}
container={this.plotListRef}
@ -185,3 +184,9 @@ export default class CanGraphList extends Component {
);
}
}
const stateToProps = Obstruction({
segment: "segment.segment"
});
export default connect(stateToProps)(CanGraphList);

View File

@ -1,10 +1,12 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import Obstruction from "obstruction";
import PropTypes from "prop-types";
import ReactList from "react-list";
import cx from "classnames";
export default class CanLog extends Component {
class CanLog extends Component {
static ITEMS_PER_PAGE = 50;
static propTypes = {
@ -308,3 +310,9 @@ export default class CanLog extends Component {
);
}
}
const stateToProps = Obstruction({
segmentIndices: "segment.segmentIndices"
});
export default connect(stateToProps)(CanLog);

View File

@ -1,4 +1,6 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import Obstruction from "obstruction";
import PropTypes from "prop-types";
import cx from "classnames";
@ -13,7 +15,9 @@ import PartSelector from "./PartSelector";
import PlaySpeedSelector from "./PlaySpeedSelector";
import GraphData from "../models/graph-data";
export default class Explorer extends Component {
import { seek, selectSegment } from "../actions";
class Explorer extends Component {
static propTypes = {
selectedMessage: PropTypes.string,
url: PropTypes.string,
@ -22,7 +26,6 @@ export default class Explorer extends Component {
onConfirmedSignalChange: PropTypes.func.isRequired,
canFrameOffset: PropTypes.number,
firstCanTime: PropTypes.number,
onSeek: PropTypes.func.isRequired,
autoplay: PropTypes.bool.isRequired,
onPartChange: PropTypes.func.isRequired,
partsCount: PropTypes.number
@ -34,11 +37,9 @@ export default class Explorer extends Component {
this.state = {
plottedSignals: [],
graphData: [],
segment: [],
segmentIndices: [],
shouldShowAddSignal: true,
userSeekIndex: 0,
userSeekTime: props.currentParts[0] * 60,
userSeekTime: this.props.seekTime,
playing: props.autoplay,
signals: {},
playSpeed: 1
@ -48,8 +49,6 @@ export default class Explorer extends Component {
this.onSegmentChanged = this.onSegmentChanged.bind(this);
this.showAddSignal = this.showAddSignal.bind(this);
this.onGraphTimeClick = this.onGraphTimeClick.bind(this);
this.onUserSeek = this.onUserSeek.bind(this);
this.onPlaySeek = this.onPlaySeek.bind(this);
this.onPlay = this.onPlay.bind(this);
this.onPause = this.onPause.bind(this);
this.onVideoClick = this.onVideoClick.bind(this);
@ -109,6 +108,8 @@ export default class Explorer extends Component {
const curMessage = this.props.messages[this.props.selectedMessage];
let { plottedSignals, graphData } = this.state;
this.checkSeek(nextProps);
if (Object.keys(nextProps.messages).length === 0) {
this.resetSegment();
}
@ -156,27 +157,12 @@ export default class Explorer extends Component {
// corresponding to old message segment/seek times.
let { segment, segmentIndices } = this.clipSegment(
this.state.segment,
this.state.segmentIndices,
this.props.segment,
this.props.segmentIndices,
nextMessage
);
const nextSeekMsgEntry = nextMessage.entries[nextProps.seekIndex];
let nextSeekTime;
if (nextSeekMsgEntry) {
nextSeekTime = nextSeekMsgEntry.relTime;
} else if (segment.length === 2) {
nextSeekTime = segment[0];
} else {
nextSeekTime = nextMessage.entries[0];
}
this.setState({
segment,
segmentIndices,
userSeekIndex: nextProps.seekIndex,
userSeekTime: nextSeekTime
});
this.props.dispatch(selectSegment(segment, segmentIndices));
}
if (
@ -185,16 +171,16 @@ export default class Explorer extends Component {
nextMessage.entries.length !== curMessage.entries.length
) {
let { segment, segmentIndices } = this.clipSegment(
this.state.segment,
this.state.segmentIndices,
this.props.segment,
this.props.segmentIndices,
nextMessage
);
this.setState({ segment, segmentIndices });
this.props.dispatch(selectSegment(segment, segmentIndices));
}
const partsDidChange =
JSON.stringify(nextProps.currentParts) !==
JSON.stringify(this.props.currentParts);
JSON.stringify(nextProps.selectedParts) !==
JSON.stringify(this.props.selectedParts);
if (plottedSignals.length > 0) {
if (graphData.length !== plottedSignals.length || partsDidChange) {
@ -233,8 +219,8 @@ export default class Explorer extends Component {
const { userSeekTime } = this.state;
const nextSeekTime =
userSeekTime -
this.props.currentParts[0] * 60 +
nextProps.currentParts[0] * 60;
this.props.selectedParts[0] * 60 +
nextProps.selectedParts[0] * 60;
this.setState({ userSeekTime: nextSeekTime });
}
}
@ -246,11 +232,14 @@ export default class Explorer extends Component {
}
timeWindow() {
const { routeStartTime, currentParts } = this.props;
const { routeStartTime, selectedParts } = this.props;
if (routeStartTime) {
const partStartOffset = currentParts[0] * 60,
partEndOffset = (currentParts[1] + 1) * 60;
const partStartOffset = selectedParts[0] * 60;
const partEndOffset = Math.min(
this.props.maxTime,
(selectedParts[1] + 1) * 60
);
const windowStartTime = routeStartTime
.clone()
@ -339,11 +328,10 @@ export default class Explorer extends Component {
const segmentIndices = Entries.findSegmentIndices(entries, segment, true);
this.setState({
segment,
segmentIndices,
userSeekIndex: segmentIndices[0],
userSeekTime: segment[0]
});
this.props.dispatch(selectSegment(segment, segmentIndices));
}, 250);
onSegmentChanged(messageId, segment) {
@ -353,8 +341,7 @@ export default class Explorer extends Component {
}
resetSegment() {
const { segment, segmentIndices } = this.state;
const { messages, selectedMessage } = this.props;
const { segment, segmentIndices, messages, selectedMessage } = this.props;
if (segment.length > 0 || segmentIndices.length > 0) {
let userSeekTime = 0;
if (
@ -364,11 +351,10 @@ export default class Explorer extends Component {
userSeekTime = messages[selectedMessage].entries[0].relTime;
}
this.setState({
segment: [],
segmentIndices: [],
userSeekIndex: 0,
userSeekTime
});
this.props.dispatch(selectSegment([], []));
}
}
@ -380,13 +366,18 @@ export default class Explorer extends Component {
this.setState({ shouldShowAddSignal: !this.state.shouldShowAddSignal });
}
indexFromSeekTime(time) {
indexFromSeekTime(time, entries) {
// returns index guaranteed to be in [0, entries.length - 1]
const { entries } = this.props.messages[this.props.selectedMessage];
if (entries.length === 0) return null;
if (!entries) {
entries = this.props.messages[this.props.selectedMessage].entries;
}
const { segmentIndices } = this.state;
if (entries.length === 0) {
return null;
}
const { segmentIndices } = this.props;
if (segmentIndices.length === 2) {
for (let i = segmentIndices[0]; i <= segmentIndices[1]; i++) {
if (entries[i].relTime >= time) {
@ -404,32 +395,33 @@ export default class Explorer extends Component {
}
}
onUserSeek(time) {
this.setState({ userSeekTime: time });
const message = this.props.messages[this.props.selectedMessage];
checkSeek(newProps) {
const message = newProps.messages[newProps.selectedMessage];
if (!message) {
this.props.onUserSeek(time);
this.props.onSeek(0, time);
// remove seekIndex
if (newProps.seekIndex) {
this.props.dispatch(seek(newProps.seekTime));
}
return;
}
const { entries } = message;
const userSeekIndex = this.indexFromSeekTime(time);
const userSeekIndex = this.indexFromSeekTime(newProps.seekTime, entries);
if (userSeekIndex) {
const seekTime = entries[userSeekIndex].relTime;
this.setState({ userSeekIndex, userSeekTime: seekTime });
this.props.onSeek(userSeekIndex, seekTime);
} else {
this.props.onUserSeek(time);
this.setState({ userSeekTime: time });
this.setState({ indexSeekTime: seekTime });
if (userSeekIndex !== newProps.seekIndex) {
this.props.dispatch(seek(newProps.seekTime, userSeekIndex));
}
} else if (newProps.seekIndex) {
this.props.dispatch(seek(newProps.seekTime));
}
}
onPlaySeek(time) {
const message = this.props.messages[this.props.selectedMessage];
if (!message || message.entries.length === 0) {
this.props.onSeek(0, time);
return;
}
@ -464,19 +456,19 @@ export default class Explorer extends Component {
this.setState({ playing: false });
}
secondsLoadedRouteRelative(currentParts) {
return (currentParts[1] - currentParts[0] + 1) * 60;
secondsLoadedRouteRelative(selectedParts) {
return (selectedParts[1] - selectedParts[0] + 1) * 60;
}
secondsLoaded() {
const message = this.props.messages[this.props.selectedMessage];
if (!message || message.entries.length === 0) {
return this.secondsLoadedRouteRelative(this.props.currentParts);
return this.secondsLoadedRouteRelative(this.props.selectedParts);
}
const { entries } = message;
const { segment } = this.state;
const { segment } = this.props;
if (segment.length === 2) {
return segment[1] - segment[0];
} else {
@ -485,14 +477,14 @@ export default class Explorer extends Component {
}
startOffset() {
const partOffset = this.props.currentParts[0] * 60;
const partOffset = this.props.selectedParts[0] * 60;
const message = this.props.messages[this.props.selectedMessage];
if (!message || message.entries.length === 0) {
return partOffset;
}
const { entries } = message;
const { segment } = this.state;
const { segment } = this.props;
let startTime;
if (segment.length === 2) {
startTime = segment[0];
@ -502,7 +494,7 @@ export default class Explorer extends Component {
if (
startTime > partOffset &&
startTime < (this.props.currentParts[1] + 1) * 60
startTime < (this.props.selectedParts[1] + 1) * 60
) {
// startTime is within bounds of currently selected parts
return startTime;
@ -661,10 +653,7 @@ export default class Explorer extends Component {
/>
<div className="cabana-explorer-visuals-header">
{this.timeWindow()}
<PartSelector
onPartChange={this.props.onPartChange}
partsCount={this.props.partsCount}
/>
<PartSelector partsCount={this.props.partsCount} />
</div>
<RouteVideoSync
message={this.props.messages[this.props.selectedMessage]}
@ -677,7 +666,6 @@ export default class Explorer extends Component {
canFrameOffset={this.props.canFrameOffset}
firstCanTime={this.props.firstCanTime}
onVideoClick={this.onVideoClick}
onPlaySeek={this.onPlaySeek}
onUserSeek={this.onUserSeek}
onPlay={this.onPlay}
onPause={this.onPause}
@ -686,7 +674,7 @@ export default class Explorer extends Component {
/>
</div>
) : null}
{this.state.segment.length > 0 ? (
{this.props.segment.length > 0 ? (
<div
className={"cabana-explorer-visuals-segmentreset"}
onClick={() => {
@ -701,10 +689,8 @@ export default class Explorer extends Component {
messages={this.props.messages}
graphData={this.state.graphData}
onGraphTimeClick={this.onGraphTimeClick}
seekTime={this.props.seekTime}
onSegmentChanged={this.onSegmentChanged}
onSignalUnplotPressed={this.onSignalUnplotPressed}
segment={this.state.segment}
mergePlots={this.mergePlots}
live={this.props.live}
/>
@ -713,3 +699,14 @@ export default class Explorer extends Component {
);
}
}
const stateToProps = Obstruction({
selectedParts: "playback.selectedParts",
seekTime: "playback.seekTime",
seekIndex: "playback.seekIndex",
maxTime: "playback.maxTime",
segment: "segment.segment",
segmentIndices: "segment.segmentIndices"
});
export default connect(stateToProps)(Explorer);

View File

@ -1,8 +1,12 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import Obstruction from "obstruction";
import PropTypes from "prop-types";
import Hls from "hls.js/lib";
export default class HLS extends Component {
import { setLoading, seek, autoSeek, setMaxTime } from "../actions";
class HLS extends Component {
static propTypes = {
source: PropTypes.string.isRequired,
startTime: PropTypes.number.isRequired,
@ -10,14 +14,24 @@ export default class HLS extends Component {
playing: PropTypes.bool.isRequired,
onVideoElementAvailable: PropTypes.func,
onClick: PropTypes.func,
onLoadStart: PropTypes.func,
onLoadEnd: PropTypes.func,
onPlaySeek: PropTypes.func,
segmentProgress: PropTypes.func,
shouldRestart: PropTypes.bool,
onRestart: PropTypes.func
};
constructor(props) {
super(props);
this.state = {
seekingTime: null
};
this.onLoadStart = this.onLoadStart.bind(this);
this.onLoadEnd = this.onLoadEnd.bind(this);
this.onEnded = this.onEnded.bind(this);
}
componentWillReceiveProps(nextProps) {
if (
(nextProps.shouldRestart ||
@ -46,12 +60,23 @@ export default class HLS extends Component {
} else {
this.videoElement.pause();
}
if (
this.videoElement &&
Math.abs(this.videoElement.currentTime - nextProps.seekTime) > 1.0
) {
this.videoElement.currentTime = nextProps.seekTime;
this.setState({
seekingTime: nextProps.seekTime
});
this.props.dispatch(setLoading(true));
}
}
onSeeking = () => {
if (!this.props.playing) {
this.props.onLoadStart();
this.props.onPlaySeek(this.videoElement.currentTime);
this.onLoadStart();
this.props.dispatch(seek(this.videoElement.currentTime));
}
};
@ -61,10 +86,10 @@ export default class HLS extends Component {
onSeeked = () => {
if (!this.props.playing) {
if (this.shouldInitVideoTime) {
this.videoElement.currentTime = this.props.startTime;
// this.videoElement.currentTime = this.props.startTime;
this.shouldInitVideoTime = false;
}
this.props.onLoadEnd();
this.onLoadEnd();
}
};
@ -85,9 +110,33 @@ export default class HLS extends Component {
// destroy hls video source
if (this.player) {
this.player.destroy();
this.player = null;
}
}
onLoadStart() {
if (!this.state.seekingTime) {
this.videoElement.currentTime = this.props.seekTime;
this.setState({
seekingTime: this.props.seekTime
});
}
this.props.dispatch(setLoading(true));
}
onLoadEnd() {
if (this.videoElement.duration !== this.props.maxTime) {
this.props.dispatch(setMaxTime(this.videoElement.duration));
}
this.props.dispatch(setLoading(false));
this.setState({
seekingTime: null
});
}
onEnded() {
this.props.dispatch(autoSeek(this.props.maxTime));
}
render() {
return (
<div
@ -98,14 +147,24 @@ export default class HLS extends Component {
ref={video => {
this.videoElement = video;
}}
seek={this.props.seekTime}
autoPlay={this.props.playing}
muted
onWaiting={this.props.onLoadStart}
onPlaying={this.props.onLoadEnd}
onWaiting={this.onLoadStart}
onPlaying={this.onLoadEnd}
onSeeking={this.onSeeking}
onSeeked={this.onSeeked}
onEnded={this.onEnded}
/>
</div>
);
}
}
const stateToProps = Obstruction({
isLoading: "playback.isLoading",
seekTime: "playback.seekTime",
maxTime: "playback.maxTime"
});
export default connect(stateToProps)(HLS);

View File

@ -1,7 +1,9 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import Obstruction from "obstruction";
import PropTypes from "prop-types";
export default class MessageBytes extends Component {
class MessageBytes extends Component {
static propTypes = {
seekTime: PropTypes.number.isRequired,
message: PropTypes.object.isRequired,
@ -14,7 +16,7 @@ export default class MessageBytes extends Component {
this.state = {
isVisible: true,
lastMessageIndex: 0,
lastSeekTime: 0
lastSeekTime: props.seekTime
};
this.onVisibilityChange = this.onVisibilityChange.bind(this);
@ -136,3 +138,10 @@ export default class MessageBytes extends Component {
);
}
}
const stateToProps = Obstruction({
seekTime: "playback.seekTime",
seekItem: "playback.seekItem"
});
export default connect(stateToProps)(MessageBytes);

View File

@ -1,4 +1,6 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import Obstruction from "obstruction";
import cx from "classnames";
import PropTypes from "prop-types";
import Clipboard from "clipboard";
@ -8,7 +10,7 @@ import MessageBytes from "./MessageBytes";
import { GITHUB_AUTH_TOKEN_KEY } from "../config";
const { ckmeans } = require("simple-statistics");
export default class Meta extends Component {
class Meta extends Component {
static propTypes = {
onMessageSelected: PropTypes.func,
onMessageUnselected: PropTypes.func,
@ -25,7 +27,7 @@ export default class Meta extends Component {
showEditMessageModal: PropTypes.func,
route: PropTypes.object,
partsLoaded: PropTypes.number,
currentParts: PropTypes.array,
selectedParts: PropTypes.array,
seekTime: PropTypes.number,
loginWithGithub: PropTypes.element,
isDemo: PropTypes.bool,
@ -275,13 +277,7 @@ export default class Meta extends Component {
<td>{msg.entries.length}</td>
<td>
<div className="cabana-meta-messages-list-item-bytes">
<MessageBytes
key={msg.id}
message={msg}
seekIndex={this.props.seekIndex}
seekTime={this.props.seekTime}
live={this.props.live}
/>
<MessageBytes key={msg.id} message={msg} live={this.props.live} />
</div>
</td>
</tr>
@ -447,3 +443,10 @@ export default class Meta extends Component {
);
}
}
const stateToProps = Obstruction({
route: "playback.route",
seekTime: "playback.seekTime"
});
export default connect(stateToProps)(Meta);

View File

@ -1,24 +1,33 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import Obstruction from "obstruction";
import PropTypes from "prop-types";
import { selectPart } from "../actions";
import { PART_SEGMENT_LENGTH } from "../config";
export default class PartSelector extends Component {
class PartSelector extends Component {
static selectorWidth = 150;
static propTypes = {
onPartChange: PropTypes.func.isRequired,
partsCount: PropTypes.number.isRequired
maxParts: PropTypes.number.isRequired,
selectedParts: PropTypes.array,
seekTime: PropTypes.number
};
constructor(props) {
super(props);
this.state = {
selectedPartStyle: this.makePartStyle(props.partsCount, 0),
selectedPart: 0,
selectedPartStyle: this.makePartStyle(
props.maxParts,
props.selectedParts[0]
),
isDragging: false
};
console.log("Constructing");
this.selectNextPart = this.selectNextPart.bind(this);
this.selectPrevPart = this.selectPrevPart.bind(this);
this.onSelectedPartDragStart = this.onSelectedPartDragStart.bind(this);
@ -27,18 +36,23 @@ export default class PartSelector extends Component {
this.onClick = this.onClick.bind(this);
}
makePartStyle(partsCount, selectedPart) {
makePartStyle(maxParts, selectedPart) {
maxParts = maxParts + 1;
console.log("Making styles for", maxParts, selectedPart);
return {
left: selectedPart / partsCount * PartSelector.selectorWidth,
width: PART_SEGMENT_LENGTH / partsCount * PartSelector.selectorWidth
left: selectedPart / maxParts * PartSelector.selectorWidth,
width: (PART_SEGMENT_LENGTH - 1) / maxParts * PartSelector.selectorWidth
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.partsCount !== this.props.partsCount) {
if (
nextProps.maxParts !== this.props.maxParts ||
nextProps.selectedParts[0] !== this.props.selectedParts[0]
) {
const selectedPartStyle = this.makePartStyle(
nextProps.partsCount,
this.state.selectedPart
nextProps.maxParts,
nextProps.selectedParts[0]
);
this.setState({ selectedPartStyle });
}
@ -47,23 +61,19 @@ export default class PartSelector extends Component {
selectPart(part) {
part = Math.max(
0,
Math.min(this.props.partsCount - PART_SEGMENT_LENGTH, part)
Math.min(this.props.maxParts - PART_SEGMENT_LENGTH + 1, part)
);
if (part === this.state.selectedPart) {
if (part === this.props.selectedParts[0]) {
return;
}
this.props.onPartChange(part);
this.setState({
selectedPart: part,
selectedPartStyle: this.makePartStyle(this.props.partsCount, part)
});
this.props.dispatch(selectPart(part));
}
selectNextPart() {
let { selectedPart } = this.state;
selectedPart++;
if (selectedPart + PART_SEGMENT_LENGTH >= this.props.partsCount) {
let { selectedParts, maxParts } = this.props;
let selectedPart = selectedParts[0] + 1;
if (selectedPart + PART_SEGMENT_LENGTH > maxParts) {
return;
}
@ -71,8 +81,8 @@ export default class PartSelector extends Component {
}
selectPrevPart() {
let { selectedPart } = this.state;
selectedPart--;
let { selectedParts } = this.props;
let selectedPart = selectedParts[0] - 1;
if (selectedPart < 0) {
return;
}
@ -83,7 +93,7 @@ export default class PartSelector extends Component {
partAtClientX(clientX) {
const rect = this.selectorRect.getBoundingClientRect();
const x = clientX - rect.left;
return Math.floor(x * this.props.partsCount / PartSelector.selectorWidth);
return Math.floor(x * this.props.maxParts / PartSelector.selectorWidth);
}
onSelectedPartDragStart(e) {
@ -110,9 +120,8 @@ export default class PartSelector extends Component {
render() {
const { selectedPartStyle } = this.state;
if (this.props.partsCount <= PART_SEGMENT_LENGTH) {
if (this.props.maxParts <= PART_SEGMENT_LENGTH) {
// all parts are available so no need to render the partselector
return null;
}
return (
@ -135,3 +144,11 @@ export default class PartSelector extends Component {
);
}
}
const stateToProps = Obstruction({
seekTime: "playback.seekTime",
selectedParts: "playback.selectedParts",
maxParts: "route.maxParts"
});
export default connect(stateToProps)(PartSelector);

View File

@ -1,14 +1,16 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import Obstruction from "obstruction";
import PropTypes from "prop-types";
import PlayButton from "../PlayButton";
import debounce from "../../utils/debounce";
export default class RouteSeeker extends Component {
import { autoSeek, seek } from "../../actions";
class RouteSeeker extends Component {
static propTypes = {
secondsLoaded: PropTypes.number.isRequired,
segmentIndices: PropTypes.arrayOf(PropTypes.number),
onUserSeek: PropTypes.func,
onPlaySeek: PropTypes.func,
video: PropTypes.node,
onPause: PropTypes.func,
onPlay: PropTypes.func,
@ -88,7 +90,7 @@ export default class RouteSeeker extends Component {
return 100 * (x / this.progressBar.offsetWidth);
}
updateDraggingSeek = debounce(ratio => this.props.onUserSeek(ratio), 250);
updateDraggingSeek = debounce(ratio => this.props.dispatch(seek(ratio)), 250);
onMouseMove(e) {
const markerOffsetPct = this.mouseEventXOffsetPercent(e);
@ -111,7 +113,7 @@ export default class RouteSeeker extends Component {
const ratio = Math.max(0, markerOffsetPct / 100);
if (this.state.isDragging) {
this.updateSeekedBar(ratio);
this.updateDraggingSeek(ratio);
// this.updateDraggingSeek(ratio);
}
this.setState({
@ -138,7 +140,16 @@ export default class RouteSeeker extends Component {
let ratio = this.mouseEventXOffsetPercent(e) / 100;
ratio = Math.min(1, Math.max(0, ratio));
this.updateSeekedBar(ratio);
this.props.onUserSeek(ratio);
this.seek(this.props.ratioTime(ratio));
}
seek(time) {
this.isSeeking = true;
this.props.dispatch(seek(time));
const { videoElement } = this.props;
if (videoElement) {
videoElement.currentTime = time;
}
}
onPlay() {
@ -153,12 +164,17 @@ export default class RouteSeeker extends Component {
executePlayTimer() {
const { videoElement } = this.props;
if (videoElement === null) {
if (this.isSeeking || !videoElement) {
this.isSeeking = false;
this.playTimer = window.requestAnimationFrame(this.executePlayTimer);
return;
}
const { currentTime } = videoElement;
if (!currentTime) {
this.playTimer = window.requestAnimationFrame(this.executePlayTimer);
return;
}
let newRatio = this.props.segmentProgress(currentTime);
if (newRatio === this.state.ratio) {
@ -168,12 +184,13 @@ export default class RouteSeeker extends Component {
if (newRatio >= 1) {
newRatio = 0;
this.props.onUserSeek(newRatio);
// whats this?
// this.props.dispatch(seek(newRatio));
}
if (newRatio >= 0) {
this.updateSeekedBar(newRatio);
this.props.onPlaySeek(currentTime);
this.props.dispatch(autoSeek(currentTime));
}
this.playTimer = window.requestAnimationFrame(this.executePlayTimer);
@ -235,3 +252,9 @@ export default class RouteSeeker extends Component {
);
}
}
const stateToProps = Obstruction({
segmentIndices: "segment.segmentIndices"
});
export default connect(stateToProps)(RouteSeeker);

View File

@ -1,4 +1,6 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import Obstruction from "obstruction";
import PropTypes from "prop-types";
import { StyleSheet, css } from "aphrodite/no-important";
@ -7,6 +9,8 @@ import { cameraPath } from "../api/routes";
import Video from "../api/video";
import RouteSeeker from "./RouteSeeker/RouteSeeker";
import { seek } from "../actions";
const Styles = StyleSheet.create({
loadingOverlay: {
position: "absolute",
@ -44,7 +48,7 @@ const Styles = StyleSheet.create({
}
});
export default class RouteVideoSync extends Component {
class RouteVideoSync extends Component {
static propTypes = {
userSeekIndex: PropTypes.number.isRequired,
secondsLoaded: PropTypes.number.isRequired,
@ -54,8 +58,6 @@ export default class RouteVideoSync extends Component {
canFrameOffset: PropTypes.number.isRequired,
url: PropTypes.string.isRequired,
playing: PropTypes.bool.isRequired,
onPlaySeek: PropTypes.func.isRequired,
onUserSeek: PropTypes.func.isRequired,
onPlay: PropTypes.func.isRequired,
onPause: PropTypes.func.isRequired,
userSeekTime: PropTypes.number.isRequired
@ -64,14 +66,10 @@ export default class RouteVideoSync extends Component {
constructor(props) {
super(props);
this.state = {
shouldShowJpeg: true,
isLoading: true,
videoElement: null,
shouldRestartHls: false
};
this.onLoadStart = this.onLoadStart.bind(this);
this.onLoadEnd = this.onLoadEnd.bind(this);
this.segmentProgress = this.segmentProgress.bind(this);
this.onVideoElementAvailable = this.onVideoElementAvailable.bind(this);
this.onUserSeek = this.onUserSeek.bind(this);
@ -93,7 +91,7 @@ export default class RouteVideoSync extends Component {
nearestFrameUrl() {
const { url } = this.props;
const sec = Math.round(this.props.userSeekTime);
const sec = Math.round(this.props.seekTime);
return cameraPath(url, sec);
}
@ -109,34 +107,30 @@ export default class RouteVideoSync extends Component {
);
}
onLoadStart() {
this.setState({
shouldShowJpeg: true,
isLoading: true
});
}
onLoadEnd() {
this.setState({
shouldShowJpeg: false,
isLoading: false
});
}
segmentProgress(currentTime) {
// returns progress as number in [0,1]
if (currentTime < this.props.startOffset) {
currentTime = this.props.startOffset;
}
const ratio =
(currentTime - this.props.startOffset) / this.props.secondsLoaded;
let partMaxTime = Math.min(
this.props.maxTime,
(1 + this.props.selectedParts[1]) * 60
);
let partDuration = partMaxTime - this.props.startOffset;
const ratio = (currentTime - this.props.startOffset) / partDuration;
return Math.max(0, Math.min(1, ratio));
}
ratioTime(ratio) {
return ratio * this.props.secondsLoaded + this.props.startOffset;
let partMaxTime = Math.min(
this.props.maxTime,
(1 + this.props.selectedParts[1]) * 60
);
let partDuration = partMaxTime - this.props.startOffset;
return ratio * partDuration + this.props.startOffset;
}
onVideoElementAvailable(videoElement) {
@ -146,7 +140,8 @@ export default class RouteVideoSync extends Component {
onUserSeek(ratio) {
/* ratio in [0,1] */
const funcSeekToRatio = () => this.props.onUserSeek(this.ratioTime(ratio));
const funcSeekToRatio = () =>
this.props.dispatch(seek(this.ratioTime(ratio)));
if (ratio === 0) {
this.setState({ shouldRestartHls: true }, funcSeekToRatio);
} else {
@ -161,8 +156,8 @@ export default class RouteVideoSync extends Component {
render() {
return (
<div className="cabana-explorer-visuals-camera">
{this.state.isLoading ? this.loadingOverlay() : null}
{this.state.shouldShowJpeg ? (
{this.props.isLoading ? this.loadingOverlay() : null}
{this.props.isLoading ? (
<img
src={this.nearestFrameUrl()}
className={css(Styles.img)}
@ -177,10 +172,6 @@ export default class RouteVideoSync extends Component {
onVideoElementAvailable={this.onVideoElementAvailable}
playing={this.props.playing}
onClick={this.props.onVideoClick}
onLoadStart={this.onLoadStart}
onLoadEnd={this.onLoadEnd}
onUserSeek={this.onUserSeek}
onPlaySeek={this.props.onPlaySeek}
segmentProgress={this.segmentProgress}
shouldRestart={this.state.shouldRestartHls}
onRestart={this.onHlsRestart}
@ -190,9 +181,6 @@ export default class RouteVideoSync extends Component {
nearestFrameTime={this.props.userSeekTime}
segmentProgress={this.segmentProgress}
secondsLoaded={this.props.secondsLoaded}
segmentIndices={this.props.segmentIndices}
onUserSeek={this.onUserSeek}
onPlaySeek={this.props.onPlaySeek}
videoElement={this.state.videoElement}
onPlay={this.props.onPlay}
onPause={this.props.onPause}
@ -203,3 +191,11 @@ export default class RouteVideoSync extends Component {
);
}
}
const stateToProps = Obstruction({
selectedParts: "playback.selectedParts",
seekTime: "playback.seekTime",
maxTime: "playback.maxTime"
});
export default connect(stateToProps)(RouteVideoSync);

View File

@ -10,13 +10,20 @@ import {
fetchPersistedGithubAuthToken,
persistGithubAuthToken
} from "./api/localstorage";
import createStore from "./store";
import "./index.css";
const store = createStore();
Sentry.init();
const routeFullName = getUrlParameter("route");
let isDemo = !routeFullName;
let props = { autoplay: true, isDemo };
let props = {
autoplay: true,
isDemo,
store
};
let persistedDbc = null;
if (routeFullName) {

View File

@ -0,0 +1,10 @@
import { combineReducers } from "redux";
import playback from "./playback";
import route from "./route";
import segment from "./segments";
export default combineReducers({
playback: playback,
route: route,
segment: segment
});

View File

@ -0,0 +1,82 @@
import {
ACTION_SEEK,
ACTION_SELECT_PART,
ACTION_AUTO_SEEK,
ACTION_SET_LOADING,
ACTION_SET_MAX_TIME
} from "../actions/types";
import { PART_SEGMENT_LENGTH } from "../config";
import { getUrlParameter } from "../utils/url";
var initialSeekTime = getUrlParameter("seekTime");
if (initialSeekTime) {
initialSeekTime = Number(initialSeekTime);
} else {
initialSeekTime = 0;
}
const initialState = {
seekTime: initialSeekTime,
selectedParts: [
getPartForTime(initialSeekTime),
getPartForTime(initialSeekTime) + PART_SEGMENT_LENGTH - 1
],
maxParts: 0,
isLoading: true,
seekIndex: 0
};
export default function playback(state, action) {
state = doThing(state, action);
if (!state.seekTime && state.seekTime !== 0) {
debugger;
}
return state;
function doThing(state, action) {
if (!state) {
return initialState;
}
switch (action.type) {
case ACTION_SEEK:
return {
...state,
seekTime: action.time,
userSeekTime: action.time,
selectedParts: action.selectedParts,
seekIndex: action.index || 0
};
case ACTION_AUTO_SEEK:
// auto-seek from video timestamp updates
return {
...state,
seekTime: action.time
};
case ACTION_SELECT_PART:
return {
...state,
seekTime: action.seekTime || state.seekTime,
selectedParts: action.selectedParts
};
case ACTION_SET_LOADING:
return {
...state,
isLoading: action.isLoading
};
case ACTION_SET_MAX_TIME:
return {
...state,
maxTime: action.maxTime
};
default:
break;
}
return state;
}
}
function getPartForTime(time) {
return Math.floor(time / 60);
}

View File

@ -0,0 +1,30 @@
import { ACTION_LOAD_ROUTES, ACTION_SELECT_ROUTE } from "../actions/types";
const initialState = {
routes: [],
route: null
};
export default function reducer(state, action) {
if (!state) {
return initialState;
}
switch (action.type) {
case ACTION_LOAD_ROUTES:
return {
...state,
routes: action.routes
};
case ACTION_SELECT_ROUTE:
return {
...state,
route: action.route,
maxParts: action.route.proclog
};
default:
break;
}
return state;
}

View File

@ -0,0 +1,26 @@
import { ACTION_SELECT_SEGMENT } from "../actions/types";
const initialState = {
segment: [],
segmentIndices: []
};
export default function reducer(state, action) {
if (!state) {
state = initialState;
}
switch (action.type) {
case ACTION_SELECT_SEGMENT:
console.log("Set segment", action);
return {
...state,
segment: action.segment,
segmentIndices: action.segmentIndices
};
default:
break;
}
return state;
}

27
src/store.js 100644
View File

@ -0,0 +1,27 @@
import window from "global/window";
import * as Redux from "redux";
import thunk from "redux-thunk";
import * as Actions from "./actions";
import reducer from "./reducers";
let composeEnhancers;
if (
process.env.NODE_ENV !== "production" &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
) {
composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
actionCreators: Object.values(Actions).filter(f => f.name !== "updateState")
});
} else {
composeEnhancers = Redux.compose;
}
export default function createStore() {
const store = Redux.createStore(
reducer,
composeEnhancers(Redux.applyMiddleware(thunk))
);
return store;
}

View File

@ -225,7 +225,7 @@ anymatch@^2.0.0:
micromatch "^3.1.4"
normalize-path "^2.1.1"
ap@^0.2.0:
ap@^0.2.0, ap@~0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/ap/-/ap-0.2.0.tgz#ae0942600b29912f0d2b14ec60c45e8f330b6110"
@ -2785,6 +2785,10 @@ dot-prop@^4.1.0:
dependencies:
is-obj "^1.0.0"
dot-prop@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-2.1.0.tgz#6bd199d80792d2323a2b7eb8175f4b32d76a7e72"
dotenv@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-2.0.0.tgz#bd759c357aaa70365e01c96b7b0bec08a6e0d949"
@ -4137,6 +4141,10 @@ hoek@4.x.x:
version "4.2.1"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb"
hoist-non-react-statics@^2.5.0:
version "2.5.5"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
home-or-tmp@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
@ -4450,7 +4458,7 @@ interpret@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614"
invariant@^2.2.2:
invariant@^2.0.0, invariant@^2.2.2:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
dependencies:
@ -4681,6 +4689,10 @@ is-obj@^1.0.0, is-obj@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
is-object@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470"
is-observable@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/is-observable/-/is-observable-0.2.0.tgz#b361311d83c6e5d726cabf5e250b0237106f5ae2"
@ -5490,6 +5502,10 @@ locate-path@^2.0.0:
p-locate "^2.0.0"
path-exists "^3.0.0"
lodash-es@^4.17.5:
version "4.17.10"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.10.tgz#62cd7104cdf5dd87f235a837f0ede0e8e5117e05"
lodash._reinterpolate@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
@ -5551,7 +5567,7 @@ lodash.uniq@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lodash@~4.17.4:
"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.3.0, lodash@~4.17.4:
version "4.17.10"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
@ -6259,6 +6275,16 @@ object.values@^1.0.4:
function-bind "^1.1.0"
has "^1.0.1"
obstruction@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/obstruction/-/obstruction-2.1.0.tgz#905afff04474bb6f5023979a68161d41eda23be3"
dependencies:
ap "~0.2.0"
dot-prop "~2.1.0"
is-object "~1.0.1"
isarray "0.0.1"
map-obj "~1.0.1"
obtain-unicode@~0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/obtain-unicode/-/obtain-unicode-0.0.5.tgz#655f337a8f135280495d77a60efc601f9af5d5dc"
@ -7233,6 +7259,17 @@ react-reconciler@^0.7.0:
object-assign "^4.1.1"
prop-types "^15.6.0"
react-redux@^5.0.7:
version "5.0.7"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.7.tgz#0dc1076d9afb4670f993ffaef44b8f8c1155a4c8"
dependencies:
hoist-non-react-statics "^2.5.0"
invariant "^2.0.0"
lodash "^4.17.5"
lodash-es "^4.17.5"
loose-envify "^1.1.0"
prop-types "^15.6.0"
react-scripts@1.0.17:
version "1.0.17"
resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-1.0.17.tgz#c30029123b561a060227af4d7797d50a222d3fbf"
@ -7394,6 +7431,17 @@ reduce-function-call@^1.0.1:
dependencies:
balanced-match "^0.4.2"
redux-thunk@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
redux@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.0.tgz#aa698a92b729315d22b34a0553d7e6533555cc03"
dependencies:
loose-envify "^1.1.0"
symbol-observable "^1.2.0"
regenerate@^1.2.1:
version "1.3.3"
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.3.tgz#0c336d3980553d755c39b586ae3b20aa49c82b7f"
@ -8416,6 +8464,10 @@ symbol-observable@^0.2.2:
version "0.2.4"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-0.2.4.tgz#95a83db26186d6af7e7a18dbd9760a2f86d08f40"
symbol-observable@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
symbol-tree@^3.2.1:
version "3.2.2"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6"