getUserDetails(props.uid),
+ osmComments: props =>
+ fetch(`${osmCommentsApi}/${props.changesetId}`).then(r => r.json()),
+ whosThat: props => fetch(`${whosThat}${props.user}`).then(r => r.json())
+ },
+ (nextProps, props) => props.changesetId !== nextProps.changesetId,
+ Changeset
+);
+
+export { Changeset };
diff --git a/src/components/changeset/user.js b/src/components/changeset/user.js
index 8b1ee4bd..cf1ab489 100644
--- a/src/components/changeset/user.js
+++ b/src/components/changeset/user.js
@@ -5,7 +5,12 @@ import AnchorifyText from 'react-anchorify-text';
import { Button } from '../button';
import AssemblyAnchor from '../assembly_anchor';
-export function User({ userDetails, filterChangesetsByUser }) {
+export function User({
+ userDetails,
+ osmComments,
+ whosThat,
+ filterChangesetsByUser
+}) {
return (
@@ -57,12 +62,10 @@ export function User({ userDetails, filterChangesetsByUser }) {
- {userDetails.has('otherNames') &&
- userDetails.get('otherNames').size > 1 &&
+ {whosThat &&
Past usernames:
- {userDetails
- .get('otherNames')
+ {whosThat
.slice(0, -1)
.map((e, k) => {e} )}
}
diff --git a/src/components/fetch_data_enhancer.js b/src/components/fetch_data_enhancer.js
new file mode 100644
index 00000000..8ede9dc5
--- /dev/null
+++ b/src/components/fetch_data_enhancer.js
@@ -0,0 +1,61 @@
+// @flow
+import React from 'react';
+import { Map, fromJS } from 'immutable';
+
+import { cancelablePromise } from '../utils/promise';
+import { getDisplayName } from '../utils/component';
+
+// If any network request fails it silents it
+// It also set states on first come first serve
+// basis.
+// @onUpdate a function which is equivalent to shouldComponentUpdate
+// on which fetching should rework
+export function withFetchDataSilent(
+ dataToFetch: Object,
+ onUpdate: (nextProps: Object, props: Object) => boolean,
+ WrappedComponent: Class>
+) {
+ class FetchDataEnhancer extends React.PureComponent {
+ state = {
+ data: Map()
+ };
+ static displayName = `HOCFetchData${getDisplayName(WrappedComponent)}`;
+ promises: Array<*>;
+ componentDidMount() {
+ this.initFetching(this.props);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (onUpdate(nextProps, this.props)) {
+ this.initFetching(nextProps);
+ }
+ }
+ initFetching(props) {
+ console.log('initialize fetching');
+ const keys = Object.keys(dataToFetch);
+ // Collect array of promises, one for each api request
+ this.promises = keys.map(key =>
+ cancelablePromise(dataToFetch[key](props))
+ );
+ this.promises.forEach((p, i) => {
+ p.promise
+ .then(x => {
+ console.log('zappening', keys[i]);
+ let data = this.state.data;
+ data = data.set(keys[i], fromJS(x));
+ this.setState({ data });
+ })
+ .catch(e => console.error(e));
+ });
+ }
+ componentWillUnmount() {
+ console.log('unmounting');
+ this.promises.forEach(p => p && p.cancel());
+ }
+ render() {
+ return ;
+ }
+ }
+
+ return FetchDataEnhancer;
+}
diff --git a/src/components/keyboard_enhancer.js b/src/components/keyboard_enhancer.js
new file mode 100644
index 00000000..7609396d
--- /dev/null
+++ b/src/components/keyboard_enhancer.js
@@ -0,0 +1,67 @@
+// @flow
+import React from 'react';
+import { Map } from 'immutable';
+
+import Mousetrap from 'mousetrap';
+import { getDisplayName } from '../utils/component';
+
+export function keyboardToggleEnhancer(
+ exclusive: boolean,
+ bindings: Array<{ label: string, bindings: Array }>,
+ WrappedComponent: Class>
+) {
+ return class wrapper extends React.PureComponent {
+ static displayName = `HOCKeyboard${getDisplayName(WrappedComponent)}`;
+ state = { bindings: Map() };
+ componentDidMount() {
+ bindings.forEach(item =>
+ Mousetrap.bind(item.bindings, () => {
+ if (exclusive) {
+ return this.exclusiveKeyToggle(item.label);
+ }
+ this.toggleKey(item.label);
+ })
+ );
+ }
+
+ // allow toggling the state of a particular key
+ toggleKey = label => {
+ let prev = this.state.bindings;
+ prev = prev.set(label, !prev.get(label));
+ this.setState({
+ bindings: prev
+ });
+ };
+
+ // exclusively toggle this label and switch off others
+ exclusiveKeyToggle = label => {
+ let newBindingState = Map();
+ const prevBindingValue = this.state.bindings.get(label);
+ newBindingState = newBindingState.set(label, !prevBindingValue);
+ this.replaceKeysState(newBindingState);
+ };
+
+ // DANGEROUS! replaces the entire binding state with whatever is provided
+ replaceKeysState = (bindings: Map) => {
+ this.setState({
+ bindings
+ });
+ };
+
+ componentWillUnmount() {
+ // unbind all bindings
+ bindings.forEach(item => item.bindings.forEach(b => Mousetrap.unbind(b)));
+ }
+ render() {
+ return (
+
+ );
+ }
+ };
+}
diff --git a/src/config/bindings.js b/src/config/bindings.js
index bc29eb8b..22876e79 100644
--- a/src/config/bindings.js
+++ b/src/config/bindings.js
@@ -1,18 +1,62 @@
// @flow
-export const FILTER_BINDING = '\\';
-export const NEXT_CHANGESET = ['down', 'right', 'space'];
-export const PREV_CHANGESET = ['up', 'left'];
-
-export const CHANGESET_DETAILS_SHOW_ALL = ['0'];
-export const CHANGESET_DETAILS_DETAILS = ['1'];
-export const CHANGESET_DETAILS_SUSPICIOUS = ['2'];
-export const CHANGESET_DETAILS_DISCUSSIONS = ['3'];
-export const CHANGESET_DETAILS_USER = ['4'];
-export const CHANGESET_DETAILS_MAP = ['5'];
-export const VERIFY_GOOD = ['G', 'g'];
-export const VERIFY_BAD = ['B', 'b'];
-export const VERIFY_CLEAR = ['C', 'c', 'u', 'U'];
-export const OPEN_IN_JOSM = ['J', 'j'];
-export const OPEN_IN_HDYC = ['H', 'h'];
-export const FILTER_BY_USER = ['A', 'a'];
+export const FILTER_BINDING = {
+ label: 'FILTER_BINDING',
+ bindings: ['\\']
+};
+export const NEXT_CHANGESET = {
+ label: 'NEXT_CHANGESET',
+ bindings: ['down', 'right', 'space']
+};
+export const PREV_CHANGESET = {
+ label: 'PREV_CHANGESET',
+ bindings: ['up', 'left']
+};
+export const CHANGESET_DETAILS_SHOW_ALL = {
+ label: 'CHANGESET_DETAILS_SHOW_ALL',
+ bindings: ['0']
+};
+export const CHANGESET_DETAILS_DETAILS = {
+ label: 'CHANGESET_DETAILS_DETAILS',
+ bindings: ['1']
+};
+export const CHANGESET_DETAILS_SUSPICIOUS = {
+ label: 'CHANGESET_DETAILS_SUSPICIOUS',
+ bindings: ['2']
+};
+export const CHANGESET_DETAILS_DISCUSSIONS = {
+ label: 'CHANGESET_DETAILS_DISCUSSIONS',
+ bindings: ['3']
+};
+export const CHANGESET_DETAILS_USER = {
+ label: 'CHANGESET_DETAILS_USER',
+ bindings: ['4']
+};
+export const CHANGESET_DETAILS_MAP = {
+ label: 'CHANGESET_DETAILS_MAP',
+ bindings: ['5']
+};
+export const VERIFY_GOOD = {
+ label: 'VERIFY_GOOD',
+ bindings: ['G', 'g']
+};
+export const VERIFY_BAD = {
+ label: 'VERIFY_BAD',
+ bindings: ['B', 'b']
+};
+export const VERIFY_CLEAR = {
+ label: 'VERIFY_CLEAR',
+ bindings: ['C', 'c', 'u', 'U']
+};
+export const OPEN_IN_JOSM = {
+ label: 'OPEN_IN_JOSM',
+ bindings: ['J', 'j']
+};
+export const OPEN_IN_HDYC = {
+ label: 'OPEN_IN_HDYC',
+ bindings: ['H', 'h']
+};
+export const FILTER_BY_USER = {
+ label: 'FILTER_BY_USER',
+ bindings: ['A', 'a']
+};
diff --git a/src/index.js b/src/index.js
index 63e6ad4b..91743530 100644
--- a/src/index.js
+++ b/src/index.js
@@ -9,7 +9,6 @@ import Raven from 'raven-js';
import { history } from './store/history';
import { store } from './store';
import { isDev, stack, appVersion } from './config';
-
import { registerServiceWorker } from './serviceworker';
import './assets/index.css';
diff --git a/src/utils/component.js b/src/utils/component.js
new file mode 100644
index 00000000..bb034b17
--- /dev/null
+++ b/src/utils/component.js
@@ -0,0 +1,3 @@
+export function getDisplayName(WrappedComponent) {
+ return WrappedComponent.displayName || WrappedComponent.name || 'Component';
+}
diff --git a/src/utils/promise.js b/src/utils/promise.js
index fbdd63b8..5be14328 100644
--- a/src/utils/promise.js
+++ b/src/utils/promise.js
@@ -1,7 +1,6 @@
// @flow
-export function cancelablePromise(
- promise: Promise<*>
-): { promise: Promise<*>, cancel: () => any } {
+export type cancelablePromiseType = { promise: Promise<*>, cancel: () => any };
+export function cancelablePromise(promise: Promise<*>): cancelablePromiseType {
let hasCanceled_ = false;
const wrappedPromise = new Promise((resolve, reject) => {
diff --git a/src/views/changeset.js b/src/views/changeset.js
index 0df8884c..067eee1b 100644
--- a/src/views/changeset.js
+++ b/src/views/changeset.js
@@ -1,7 +1,7 @@
// @flow
import React from 'react';
import { connect } from 'react-redux';
-import { List as ImmutableList, Map, fromJS } from 'immutable';
+import { Map, fromJS } from 'immutable';
import Mousetrap from 'mousetrap';
import { Changeset as ChangesetDumb } from '../components/changeset';
@@ -22,10 +22,10 @@ class Changeset extends React.PureComponent {
applyFilters: (Map) => mixed // base 0
};
componentDidMount() {
- Mousetrap.bind(FILTER_BY_USER, this.filterChangesetsByUser);
+ Mousetrap.bind(FILTER_BY_USER.bindings, this.filterChangesetsByUser);
}
componentWillUnmount() {
- FILTER_BY_USER.forEach(k => Mousetrap.unbind(k));
+ FILTER_BY_USER.bindings.forEach(k => Mousetrap.unbind(k));
}
filterChangesetsByUser = () => {
if (this.props.currentChangeset) {
@@ -69,6 +69,8 @@ class Changeset extends React.PureComponent {
}
return (
,
- userDetails: Map,
diff: number,
diffLoading: boolean,
pageIndex: number,
activeChangesetId: ?number,
- oAuthToken: ?string,
- token: ?string,
filters: Map>,
+ bindingsState: Map,
getChangesetsPage: (number, ?boolean) => mixed, // base 0
- getOAuthToken: () => mixed,
- getFinalToken: string => mixed,
- logUserOut: () => mixed,
push: Object => mixed,
applyFilters: (Map>) => mixed // base 0
};
@@ -65,6 +55,7 @@ class ChangesetsList extends React.PureComponent {
super(props);
this.props.getChangesetsPage(props.pageIndex);
}
+
goUpDownToChangeset = (direction: number) => {
if (!this.props.currentPage) return;
let features = this.props.currentPage.get('features');
@@ -83,8 +74,14 @@ class ChangesetsList extends React.PureComponent {
}
}
};
- componentDidMount() {
- Mousetrap.bind(FILTER_BINDING, () => {
+
+ componentWillReceiveProps(nextProps) {
+ const bindingsState = this.props.bindingsState;
+ const nextBindingsState = nextProps.bindingsState;
+ if (
+ bindingsState.get(FILTER_BINDING.label) !==
+ nextBindingsState.get(FILTER_BINDING.label)
+ ) {
if (this.props.location && this.props.location.pathname === '/filters') {
const location = {
...this.props.location, // clone it
@@ -98,16 +95,21 @@ class ChangesetsList extends React.PureComponent {
};
this.props.push(location);
}
- });
- Mousetrap.bind(NEXT_CHANGESET, e => {
- e.preventDefault();
+ }
+ if (
+ bindingsState.get(NEXT_CHANGESET.label) !==
+ nextBindingsState.get(NEXT_CHANGESET.label)
+ ) {
this.goUpDownToChangeset(1);
- });
- Mousetrap.bind(PREV_CHANGESET, e => {
- e.preventDefault();
+ }
+ if (
+ bindingsState.get(PREV_CHANGESET.label) !==
+ nextBindingsState.get(PREV_CHANGESET.label)
+ ) {
this.goUpDownToChangeset(-1);
- });
+ }
}
+
handleFilterOrderBy = (selected: Array<*>) => {
let mergedFilters;
mergedFilters = this.props.filters.set('order_by', fromJS(selected));
@@ -232,29 +234,28 @@ class ChangesetsList extends React.PureComponent {
}
}
+ChangesetsList = keyboardToggleEnhancer(
+ false,
+ [NEXT_CHANGESET, PREV_CHANGESET, FILTER_BINDING],
+ ChangesetsList
+);
+
ChangesetsList = connect(
(state: RootStateType, props) => ({
- routing: state.routing,
location: state.routing.location,
- currentPage: state.changesetsPage.get('currentPage'),
- pageIndex: state.changesetsPage.get('pageIndex') || 0,
- diffLoading: state.changesetsPage.get('diffLoading'),
- filters: state.changesetsPage.get('filters') || new Map(),
- diff: state.changesetsPage.get('diff'),
loading: state.changesetsPage.get('loading'),
error: state.changesetsPage.get('error'),
- oAuthToken: state.auth.get('oAuthToken'),
- userDetails: state.auth.get('userDetails'),
- token: state.auth.get('token'),
- activeChangesetId: state.changeset.get('changesetId')
+ currentPage: state.changesetsPage.get('currentPage'),
+ diff: state.changesetsPage.get('diff'),
+ diffLoading: state.changesetsPage.get('diffLoading'),
+ pageIndex: state.changesetsPage.get('pageIndex') || 0,
+ activeChangesetId: state.changeset.get('changesetId'),
+ filters: state.changesetsPage.get('filters') || Map()
}),
{
// actions
getChangesetsPage,
- getOAuthToken,
- getFinalToken,
applyFilters,
- logUserOut,
push
}
)(ChangesetsList);
diff --git a/src/views/navbar_changeset.js b/src/views/navbar_changeset.js
index 4ca517dc..decc3203 100644
--- a/src/views/navbar_changeset.js
+++ b/src/views/navbar_changeset.js
@@ -2,8 +2,8 @@
import React from 'react';
import { connect } from 'react-redux';
import { Map } from 'immutable';
-import Mousetrap from 'mousetrap';
+import { keyboardToggleEnhancer } from '../components/keyboard_enhancer';
import { Tags } from '../components/changeset/tags';
import { Link } from 'react-router-dom';
import { Navbar } from '../components/navbar';
@@ -42,55 +42,57 @@ class NavbarChangeset extends React.PureComponent {
boolean | -1
) => mixed
};
- componentDidMount() {
- Mousetrap.bind(VERIFY_BAD, () => {
- this.props.currentChangeset &&
- this.props.handleChangesetModifyHarmful(
- this.props.changesetId,
- this.props.currentChangeset,
- true
- );
- });
- Mousetrap.bind(VERIFY_CLEAR, () => {
- this.props.currentChangeset &&
- this.props.handleChangesetModifyHarmful(
- this.props.changesetId,
- this.props.currentChangeset,
- -1
- );
- });
- Mousetrap.bind(VERIFY_GOOD, () => {
- this.props.currentChangeset &&
- this.props.handleChangesetModifyHarmful(
- this.props.changesetId,
- this.props.currentChangeset,
- false
- );
- });
- Mousetrap.bind(OPEN_IN_JOSM, () => {
+ componentWillReceiveProps(nextProps) {
+ const bindingsState = this.props.bindingsState;
+ const nextBindingsState = nextProps.bindingsState;
+
+ if (!this.props.currentChangeset) return;
+ if (
+ bindingsState.get(VERIFY_BAD.label) !==
+ nextBindingsState.get(VERIFY_BAD.label)
+ ) {
+ this.props.handleChangesetModifyHarmful(
+ this.props.changesetId,
+ this.props.currentChangeset,
+ true
+ );
+ } else if (
+ bindingsState.get(VERIFY_CLEAR.label) !==
+ nextBindingsState.get(VERIFY_CLEAR.label)
+ ) {
+ this.props.handleChangesetModifyHarmful(
+ this.props.changesetId,
+ this.props.currentChangeset,
+ -1
+ );
+ } else if (
+ bindingsState.get(VERIFY_GOOD.label) !==
+ nextBindingsState.get(VERIFY_GOOD.label)
+ ) {
+ this.props.handleChangesetModifyHarmful(
+ this.props.changesetId,
+ this.props.currentChangeset,
+ false
+ );
+ } else if (
+ bindingsState.get(OPEN_IN_JOSM.label) !==
+ nextBindingsState.get(OPEN_IN_JOSM.label)
+ ) {
if (!this.props.changesetId) return;
const url = `https://127.0.0.1:8112/import?url=http://www.openstreetmap.org/api/0.6/changeset/${this
.props.changesetId}/download`;
window.open(url, '_blank');
- });
- Mousetrap.bind(OPEN_IN_HDYC, () => {
- if (!this.props.currentChangeset) return;
+ } else if (
+ bindingsState.get(OPEN_IN_HDYC.label) !==
+ nextBindingsState.get(OPEN_IN_HDYC.label)
+ ) {
const user: string = this.props.currentChangeset.getIn(
['properties', 'user'],
''
);
const url = `http://hdyc.neis-one.org/?${user}`;
window.open(url, '_blank');
- });
- }
- componentWillUnmount() {
- [
- ...VERIFY_BAD,
- ...VERIFY_GOOD,
- ...VERIFY_GOOD,
- ...OPEN_IN_JOSM,
- ...OPEN_IN_HDYC
- ].forEach(k => Mousetrap.unbind(k));
+ }
}
handleVerify = (arr: Array