Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit aac97e0

Browse files
authored
Reload map on reconnect (#8848)
PSD-282
1 parent 9333e60 commit aac97e0

File tree

6 files changed

+157
-29
lines changed

6 files changed

+157
-29
lines changed

src/components/views/messages/MImageBody.tsx

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,10 @@ limitations under the License.
1717

1818
import React, { ComponentProps, createRef } from 'react';
1919
import { Blurhash } from "react-blurhash";
20-
import { SyncState } from 'matrix-js-sdk/src/sync';
2120
import classNames from 'classnames';
2221
import { CSSTransition, SwitchTransition } from 'react-transition-group';
2322
import { logger } from "matrix-js-sdk/src/logger";
24-
import { ClientEvent } from "matrix-js-sdk/src/client";
23+
import { ClientEvent, ClientEventHandlerMap } from "matrix-js-sdk/src/client";
2524

2625
import MFileBody from './MFileBody';
2726
import Modal from '../../../Modal';
@@ -38,6 +37,7 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
3837
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
3938
import { blobIsAnimated, mayBeAnimated } from '../../../utils/Image';
4039
import { presentableTextForFile } from "../../../utils/FileUtils";
40+
import { createReconnectedListener } from '../../../utils/connection';
4141

4242
enum Placeholder {
4343
NoImage,
@@ -68,10 +68,13 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
6868
private image = createRef<HTMLImageElement>();
6969
private timeout?: number;
7070
private sizeWatcher: string;
71+
private reconnectedListener: ClientEventHandlerMap[ClientEvent.Sync];
7172

7273
constructor(props: IBodyProps) {
7374
super(props);
7475

76+
this.reconnectedListener = createReconnectedListener(this.clearError);
77+
7578
this.state = {
7679
imgError: false,
7780
imgLoaded: false,
@@ -81,20 +84,6 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
8184
};
8285
}
8386

84-
// FIXME: factor this out and apply it to MVideoBody and MAudioBody too!
85-
private onClientSync = (syncState: SyncState, prevState: SyncState): void => {
86-
if (this.unmounted) return;
87-
// Consider the client reconnected if there is no error with syncing.
88-
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
89-
const reconnected = syncState !== SyncState.Error && prevState !== syncState;
90-
if (reconnected && this.state.imgError) {
91-
// Load the image again
92-
this.setState({
93-
imgError: false,
94-
});
95-
}
96-
};
97-
9887
protected showImage(): void {
9988
localStorage.setItem("mx_ShowImage_" + this.props.mxEvent.getId(), "true");
10089
this.setState({ showImage: true });
@@ -159,11 +148,17 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
159148
imgElement.src = this.state.thumbUrl ?? this.state.contentUrl;
160149
};
161150

151+
private clearError = () => {
152+
MatrixClientPeg.get().off(ClientEvent.Sync, this.reconnectedListener);
153+
this.setState({ imgError: false });
154+
};
155+
162156
private onImageError = (): void => {
163157
this.clearBlurhashTimeout();
164158
this.setState({
165159
imgError: true,
166160
});
161+
MatrixClientPeg.get().on(ClientEvent.Sync, this.reconnectedListener);
167162
};
168163

169164
private onImageLoad = (): void => {
@@ -317,7 +312,6 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
317312

318313
componentDidMount() {
319314
this.unmounted = false;
320-
MatrixClientPeg.get().on(ClientEvent.Sync, this.onClientSync);
321315

322316
const showImage = this.state.showImage ||
323317
localStorage.getItem("mx_ShowImage_" + this.props.mxEvent.getId()) === "true";
@@ -347,7 +341,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
347341

348342
componentWillUnmount() {
349343
this.unmounted = true;
350-
MatrixClientPeg.get().removeListener(ClientEvent.Sync, this.onClientSync);
344+
MatrixClientPeg.get().off(ClientEvent.Sync, this.reconnectedListener);
351345
this.clearBlurhashTimeout();
352346
SettingsStore.unwatchSetting(this.sizeWatcher);
353347
if (this.state.isAnimated && this.state.thumbUrl) {

src/components/views/messages/MLocationBody.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
import React from 'react';
1818
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
1919
import { randomString } from 'matrix-js-sdk/src/randomstring';
20+
import { ClientEvent, ClientEventHandlerMap } from 'matrix-js-sdk/src/matrix';
2021

2122
import { _t } from '../../../languageHandler';
2223
import Modal from '../../../Modal';
@@ -33,6 +34,7 @@ import LocationViewDialog from '../location/LocationViewDialog';
3334
import Map from '../location/Map';
3435
import SmartMarker from '../location/SmartMarker';
3536
import { IBodyProps } from "./IBodyProps";
37+
import { createReconnectedListener } from '../../../utils/connection';
3638

3739
interface IState {
3840
error: Error;
@@ -42,6 +44,7 @@ export default class MLocationBody extends React.Component<IBodyProps, IState> {
4244
public static contextType = MatrixClientContext;
4345
public context!: React.ContextType<typeof MatrixClientContext>;
4446
private mapId: string;
47+
private reconnectedListener: ClientEventHandlerMap[ClientEvent.Sync];
4548

4649
constructor(props: IBodyProps) {
4750
super(props);
@@ -51,6 +54,8 @@ export default class MLocationBody extends React.Component<IBodyProps, IState> {
5154
const idSuffix = `${props.mxEvent.getId()}_${randomString(8)}`;
5255
this.mapId = `mx_MLocationBody_${idSuffix}`;
5356

57+
this.reconnectedListener = createReconnectedListener(this.clearError);
58+
5459
this.state = {
5560
error: undefined,
5661
};
@@ -69,10 +74,20 @@ export default class MLocationBody extends React.Component<IBodyProps, IState> {
6974
);
7075
};
7176

72-
private onError = (error) => {
77+
private clearError = () => {
78+
this.context.off(ClientEvent.Sync, this.reconnectedListener);
79+
this.setState({ error: undefined });
80+
};
81+
82+
private onError = (error: Error) => {
7383
this.setState({ error });
84+
this.context.on(ClientEvent.Sync, this.reconnectedListener);
7485
};
7586

87+
componentWillUnmount(): void {
88+
this.context.off(ClientEvent.Sync, this.reconnectedListener);
89+
}
90+
7691
render(): React.ReactElement<HTMLDivElement> {
7792
return this.state.error ?
7893
<LocationBodyFallbackContent error={this.state.error} event={this.props.mxEvent} /> :

src/utils/connection.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { ClientEvent, ClientEventHandlerMap } from "matrix-js-sdk/src/matrix";
18+
import { SyncState } from "matrix-js-sdk/src/sync";
19+
20+
/**
21+
* Creates a MatrixClient event listener function that can be used to get notified about reconnects.
22+
* @param callback The callback to be called on reconnect
23+
*/
24+
export const createReconnectedListener = (callback: () => void): ClientEventHandlerMap[ClientEvent.Sync] => {
25+
return (syncState: SyncState, prevState: SyncState) => {
26+
if (syncState !== SyncState.Error && prevState !== syncState) {
27+
// Consider the client reconnected if there is no error with syncing.
28+
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
29+
callback();
30+
}
31+
};
32+
};

test/components/views/messages/MLocationBody-test.tsx

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ limitations under the License.
1717
import React from 'react';
1818
import { mount } from "enzyme";
1919
import { LocationAssetType } from "matrix-js-sdk/src/@types/location";
20-
import { RoomMember } from 'matrix-js-sdk/src/matrix';
20+
import { ClientEvent, RoomMember } from 'matrix-js-sdk/src/matrix';
2121
import maplibregl from 'maplibre-gl';
2222
import { logger } from 'matrix-js-sdk/src/logger';
2323
import { act } from 'react-dom/test-utils';
24+
import { SyncState } from 'matrix-js-sdk/src/sync';
2425

2526
import MLocationBody from "../../../../src/components/views/messages/MLocationBody";
2627
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
@@ -56,6 +57,19 @@ describe("MLocationBody", () => {
5657
wrappingComponent: MatrixClientContext.Provider,
5758
wrappingComponentProps: { value: mockClient },
5859
});
60+
const getMapErrorComponent = () => {
61+
const mockMap = new maplibregl.Map();
62+
mockClient.getClientWellKnown.mockReturnValue({
63+
[TILE_SERVER_WK_KEY.name]: { map_style_url: 'bad-tile-server.com' },
64+
});
65+
const component = getComponent();
66+
67+
// simulate error initialising map in maplibregl
68+
// @ts-ignore
69+
mockMap.emit('error', { status: 404 });
70+
71+
return component;
72+
};
5973

6074
beforeAll(() => {
6175
maplibregl.AttributionControl = jest.fn();
@@ -86,18 +100,17 @@ describe("MLocationBody", () => {
86100
});
87101

88102
it('displays correct fallback content when map_style_url is misconfigured', () => {
89-
const mockMap = new maplibregl.Map();
90-
mockClient.getClientWellKnown.mockReturnValue({
91-
[TILE_SERVER_WK_KEY.name]: { map_style_url: 'bad-tile-server.com' },
92-
});
93-
const component = getComponent();
94-
95-
// simulate error initialising map in maplibregl
96-
// @ts-ignore
97-
mockMap.emit('error', { status: 404 });
103+
const component = getMapErrorComponent();
98104
component.setProps({});
99105
expect(component.find(".mx_EventTile_body")).toMatchSnapshot();
100106
});
107+
108+
it('should clear the error on reconnect', () => {
109+
const component = getMapErrorComponent();
110+
expect((component.state() as React.ComponentState).error).toBeDefined();
111+
mockClient.emit(ClientEvent.Sync, SyncState.Reconnecting, SyncState.Error);
112+
expect((component.state() as React.ComponentState).error).toBeUndefined();
113+
});
101114
});
102115

103116
describe('without error', () => {

test/components/views/messages/__snapshots__/MLocationBody-test.tsx.snap

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ exports[`MLocationBody <MLocationBody> without error renders map correctly 1`] =
120120
"error": Array [
121121
[Function],
122122
[Function],
123+
[Function],
124+
[Function],
125+
[Function],
126+
[Function],
123127
],
124128
},
125129
"_eventsCount": 1,
@@ -130,12 +134,20 @@ exports[`MLocationBody <MLocationBody> without error renders map correctly 1`] =
130134
mockConstructor {},
131135
"top-right",
132136
],
137+
Array [
138+
mockConstructor {},
139+
"top-right",
140+
],
133141
],
134142
"results": Array [
135143
Object {
136144
"type": "return",
137145
"value": undefined,
138146
},
147+
Object {
148+
"type": "return",
149+
"value": undefined,
150+
},
139151
],
140152
},
141153
"fitBounds": [MockFunction],
@@ -148,12 +160,22 @@ exports[`MLocationBody <MLocationBody> without error renders map correctly 1`] =
148160
"lon": -0.1276,
149161
},
150162
],
163+
Array [
164+
Object {
165+
"lat": 51.5076,
166+
"lon": -0.1276,
167+
},
168+
],
151169
],
152170
"results": Array [
153171
Object {
154172
"type": "return",
155173
"value": undefined,
156174
},
175+
Object {
176+
"type": "return",
177+
"value": undefined,
178+
},
157179
],
158180
},
159181
"setStyle": [MockFunction],

test/utils/connection-test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { ClientEvent, ClientEventHandlerMap } from "matrix-js-sdk/src/matrix";
18+
import { SyncState } from "matrix-js-sdk/src/sync";
19+
20+
import { createReconnectedListener } from "../../src/utils/connection";
21+
22+
describe("createReconnectedListener", () => {
23+
let reconnectedListener: ClientEventHandlerMap[ClientEvent.Sync];
24+
let onReconnect: jest.Mock;
25+
26+
beforeEach(() => {
27+
onReconnect = jest.fn();
28+
reconnectedListener = createReconnectedListener(onReconnect);
29+
});
30+
31+
[
32+
[SyncState.Prepared, SyncState.Syncing],
33+
[SyncState.Syncing, SyncState.Reconnecting],
34+
[SyncState.Reconnecting, SyncState.Syncing],
35+
].forEach(([from, to]) => {
36+
it(`should invoke the callback on a transition from ${from} to ${to}`, () => {
37+
reconnectedListener(to, from);
38+
expect(onReconnect).toBeCalled();
39+
});
40+
});
41+
42+
[
43+
[SyncState.Syncing, SyncState.Syncing],
44+
[SyncState.Catchup, SyncState.Error],
45+
[SyncState.Reconnecting, SyncState.Error],
46+
].forEach(([from, to]) => {
47+
it(`should not invoke the callback on a transition from ${from} to ${to}`, () => {
48+
reconnectedListener(to, from);
49+
expect(onReconnect).not.toBeCalled();
50+
});
51+
});
52+
});

0 commit comments

Comments
 (0)