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

Live Location Sharing - left panel warning with error #8201

Merged
merged 4 commits into from
Mar 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ limitations under the License.
*/

.mx_LeftPanelLiveShareWarning {
@mixin ButtonResetDefault;
width: 100%;
box-sizing: border-box;

Expand All @@ -29,3 +30,7 @@ limitations under the License.
// go above to get hover for title
z-index: 1;
}

.mx_LeftPanelLiveShareWarning__error {
background-color: $alert;
}
58 changes: 54 additions & 4 deletions src/components/views/beacon/LeftPanelLiveShareWarning.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,30 +21,80 @@ import { useEventEmitterState } from '../../../hooks/useEventEmitter';
import { _t } from '../../../languageHandler';
import { OwnBeaconStore, OwnBeaconStoreEvent } from '../../../stores/OwnBeaconStore';
import { Icon as LiveLocationIcon } from '../../../../res/img/location/live-location.svg';
import { ViewRoomPayload } from '../../../dispatcher/payloads/ViewRoomPayload';
import { Action } from '../../../dispatcher/actions';
import dispatcher from '../../../dispatcher/dispatcher';
import AccessibleButton from '../elements/AccessibleButton';

interface Props {
isMinimized?: boolean;
}

/**
* Choose the most relevant beacon
* and get its roomId
*/
const chooseBestBeaconRoomId = (liveBeaconIds, errorBeaconIds): string | undefined => {
// both lists are ordered by creation timestamp in store
// so select latest beacon
const beaconId = errorBeaconIds?.[0] ?? liveBeaconIds?.[0];
if (!beaconId) {
return undefined;
}
const beacon = OwnBeaconStore.instance.getBeaconById(beaconId);

return beacon?.roomId;
};

const LeftPanelLiveShareWarning: React.FC<Props> = ({ isMinimized }) => {
const isMonitoringLiveLocation = useEventEmitterState(
OwnBeaconStore.instance,
OwnBeaconStoreEvent.MonitoringLivePosition,
() => OwnBeaconStore.instance.isMonitoringLiveLocation,
);

const beaconIdsWithWireError = useEventEmitterState(
OwnBeaconStore.instance,
OwnBeaconStoreEvent.WireError,
() => OwnBeaconStore.instance.getLiveBeaconIdsWithWireError(),
);

const liveBeaconIds = useEventEmitterState(
OwnBeaconStore.instance,
OwnBeaconStoreEvent.LivenessChange,
() => OwnBeaconStore.instance.getLiveBeaconIds(),
);

const hasWireErrors = !!beaconIdsWithWireError.length;

if (!isMonitoringLiveLocation) {
return null;
}

return <div
const relevantBeaconRoomId = chooseBestBeaconRoomId(liveBeaconIds, beaconIdsWithWireError);

const onWarningClick = relevantBeaconRoomId ? () => {
dispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: relevantBeaconRoomId,
metricsTrigger: undefined,
});
} : undefined;

const label = hasWireErrors ?
_t('An error occured whilst sharing your live location') :
_t('You are sharing your live location');

return <AccessibleButton
className={classNames('mx_LeftPanelLiveShareWarning', {
'mx_LeftPanelLiveShareWarning__minimized': isMinimized,
'mx_LeftPanelLiveShareWarning__error': hasWireErrors,
})}
title={isMinimized ? _t('You are sharing your live location') : undefined}
title={isMinimized ? label : undefined}
onClick={onWarningClick}
>
{ isMinimized ? <LiveLocationIcon height={10} /> : _t('You are sharing your live location') }
</div>;
{ isMinimized ? <LiveLocationIcon height={10} /> : label }
</AccessibleButton>;
};

export default LeftPanelLiveShareWarning;
1 change: 1 addition & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -2896,6 +2896,7 @@
"Beta": "Beta",
"Leave the beta": "Leave the beta",
"Join the beta": "Join the beta",
"An error occured whilst sharing your live location": "An error occured whilst sharing your live location",
"You are sharing your live location": "You are sharing your live location",
"%(timeRemaining)s left": "%(timeRemaining)s left",
"An error occured whilst sharing your live location, please try again": "An error occured whilst sharing your live location, please try again",
Expand Down
26 changes: 18 additions & 8 deletions src/stores/OwnBeaconStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
ClearWatchCallback,
GeolocationError,
mapGeolocationPositionToTimedGeo,
sortBeaconsByLatestCreation,
TimedGeoUri,
watchPosition,
} from "../utils/beacon";
Expand Down Expand Up @@ -73,6 +74,10 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
* Reset on successful publish of location
*/
public readonly beaconWireErrorCounts = new Map<string, number>();
/**
* ids of live beacons
* ordered by creation time descending
*/
private liveBeaconIds = [];
private locationInterval: number;
private geolocationError: GeolocationError | undefined;
Expand Down Expand Up @@ -126,17 +131,17 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
// we don't actually do anything here
}

public hasLiveBeacons(roomId?: string): boolean {
public hasLiveBeacons = (roomId?: string): boolean => {
return !!this.getLiveBeaconIds(roomId).length;
}
};

/**
* Some live beacon has a wire error
* Optionally filter by room
*/
public hasWireErrors(roomId?: string): boolean {
public hasWireErrors = (roomId?: string): boolean => {
return this.getLiveBeaconIds(roomId).some(this.beaconHasWireError);
}
};

/**
* If a beacon has failed to publish position
Expand All @@ -157,16 +162,20 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
this.publishCurrentLocationToBeacons();
};

public getLiveBeaconIds(roomId?: string): string[] {
public getLiveBeaconIds = (roomId?: string): string[] => {
if (!roomId) {
return this.liveBeaconIds;
}
return this.liveBeaconIds.filter(beaconId => this.beaconsByRoomId.get(roomId)?.has(beaconId));
}
};

public getLiveBeaconIdsWithWireError = (roomId?: string): string[] => {
return this.getLiveBeaconIds(roomId).filter(this.beaconHasWireError);
};

public getBeaconById(beaconId: string): Beacon | undefined {
public getBeaconById = (beaconId: string): Beacon | undefined => {
return this.beacons.get(beaconId);
}
};

public stopBeacon = async (beaconInfoType: string): Promise<void> => {
const beacon = this.beacons.get(beaconInfoType);
Expand Down Expand Up @@ -287,6 +296,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
const prevLiveBeaconIds = this.getLiveBeaconIds();
this.liveBeaconIds = [...this.beacons.values()]
.filter(beacon => beacon.isLive)
.sort(sortBeaconsByLatestCreation)
.map(beacon => beacon.identifier);

const diff = arrayDiff(prevLiveBeaconIds, this.liveBeaconIds);
Expand Down
4 changes: 4 additions & 0 deletions src/utils/beacon/duration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,7 @@ export const getBeaconExpiryTimestamp = (beacon: Beacon): number =>

export const sortBeaconsByLatestExpiry = (left: Beacon, right: Beacon): number =>
getBeaconExpiryTimestamp(right) - getBeaconExpiryTimestamp(left);

// aka sort by timestamp descending
export const sortBeaconsByLatestCreation = (left: Beacon, right: Beacon): number =>
right.beaconInfo.timestamp - left.beaconInfo.timestamp;
118 changes: 114 additions & 4 deletions test/components/views/beacon/LeftPanelLiveShareWarning-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,23 @@ limitations under the License.
import React from 'react';
import { mocked } from 'jest-mock';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { Beacon } from 'matrix-js-sdk/src/matrix';

import '../../../skinned-sdk';
import LeftPanelLiveShareWarning from '../../../../src/components/views/beacon/LeftPanelLiveShareWarning';
import { OwnBeaconStore, OwnBeaconStoreEvent } from '../../../../src/stores/OwnBeaconStore';
import { flushPromises } from '../../../test-utils';
import { flushPromises, makeBeaconInfoEvent } from '../../../test-utils';
import dispatcher from '../../../../src/dispatcher/dispatcher';
import { Action } from '../../../../src/dispatcher/actions';

jest.mock('../../../../src/stores/OwnBeaconStore', () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const EventEmitter = require("events");
class MockOwnBeaconStore extends EventEmitter {
public hasLiveBeacons = jest.fn().mockReturnValue(false);
public getLiveBeaconIdsWithWireError = jest.fn().mockReturnValue([]);
public getBeaconById = jest.fn();
public getLiveBeaconIds = jest.fn().mockReturnValue([]);
}
return {
// @ts-ignore
Expand All @@ -44,32 +50,136 @@ describe('<LeftPanelLiveShareWarning />', () => {
const getComponent = (props = {}) =>
mount(<LeftPanelLiveShareWarning {...defaultProps} {...props} />);

const roomId1 = '!room1:server';
const roomId2 = '!room2:server';
const aliceId = '@alive:server';

const now = 1647270879403;
const HOUR_MS = 3600000;

beforeEach(() => {
jest.spyOn(global.Date, 'now').mockReturnValue(now);
jest.spyOn(dispatcher, 'dispatch').mockClear().mockImplementation(() => { });
});

afterAll(() => {
jest.spyOn(global.Date, 'now').mockRestore();

jest.restoreAllMocks();
});
// 12h old, 12h left
const beacon1 = new Beacon(makeBeaconInfoEvent(aliceId,
roomId1,
{ timeout: HOUR_MS * 24, timestamp: now - 12 * HOUR_MS },
'$1',
));
// 10h left
const beacon2 = new Beacon(makeBeaconInfoEvent(aliceId,
roomId2,
{ timeout: HOUR_MS * 10, timestamp: now },
'$2',
));

it('renders nothing when user has no live beacons', () => {
const component = getComponent();
expect(component.html()).toBe(null);
});

describe('when user has live location monitor', () => {
beforeAll(() => {
mocked(OwnBeaconStore.instance).getBeaconById.mockImplementation(beaconId => {
if (beaconId === beacon1.identifier) {
return beacon1;
}
return beacon2;
});
});

beforeEach(() => {
mocked(OwnBeaconStore.instance).isMonitoringLiveLocation = true;
mocked(OwnBeaconStore.instance).getLiveBeaconIdsWithWireError.mockReturnValue([]);
mocked(OwnBeaconStore.instance).getLiveBeaconIds.mockReturnValue([beacon2.identifier, beacon1.identifier]);
});

it('renders correctly when not minimized', () => {
const component = getComponent();
expect(component).toMatchSnapshot();
});

it('goes to room of latest beacon when clicked', () => {
const component = getComponent();
const dispatchSpy = jest.spyOn(dispatcher, 'dispatch');

act(() => {
component.simulate('click');
});

expect(dispatchSpy).toHaveBeenCalledWith({
action: Action.ViewRoom,
metricsTrigger: undefined,
// latest beacon's room
room_id: roomId2,
});
});

it('renders correctly when minimized', () => {
const component = getComponent({ isMinimized: true });
expect(component).toMatchSnapshot();
});

it('renders wire error', () => {
mocked(OwnBeaconStore.instance).getLiveBeaconIdsWithWireError.mockReturnValue([beacon1.identifier]);
const component = getComponent();
expect(component).toMatchSnapshot();
});

it('goes to room of latest beacon with wire error when clicked', () => {
mocked(OwnBeaconStore.instance).getLiveBeaconIdsWithWireError.mockReturnValue([beacon1.identifier]);
const component = getComponent();
const dispatchSpy = jest.spyOn(dispatcher, 'dispatch');

act(() => {
component.simulate('click');
});

expect(dispatchSpy).toHaveBeenCalledWith({
action: Action.ViewRoom,
metricsTrigger: undefined,
// error beacon's room
room_id: roomId1,
});
});

it('goes back to default style when wire errors are cleared', () => {
mocked(OwnBeaconStore.instance).getLiveBeaconIdsWithWireError.mockReturnValue([beacon1.identifier]);
const component = getComponent();
// error mode
expect(component.find('.mx_LeftPanelLiveShareWarning').at(0).text()).toEqual(
'An error occured whilst sharing your live location',
);

act(() => {
mocked(OwnBeaconStore.instance).getLiveBeaconIdsWithWireError.mockReturnValue([]);
OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.WireError, 'abc');
});

component.setProps({});

// default mode
expect(component.find('.mx_LeftPanelLiveShareWarning').at(0).text()).toEqual(
'You are sharing your live location',
);
});

it('removes itself when user stops having live beacons', async () => {
const component = getComponent({ isMinimized: true });
// started out rendered
expect(component.html()).toBeTruthy();

mocked(OwnBeaconStore.instance).isMonitoringLiveLocation = false;
OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.MonitoringLivePosition);
act(() => {
mocked(OwnBeaconStore.instance).isMonitoringLiveLocation = false;
OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.MonitoringLivePosition);
});

await flushPromises();
component.setProps({});
Expand Down
Loading