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

Commit

Permalink
Live Location Sharing - left panel warning with error (#8201)
Browse files Browse the repository at this point in the history
* add error style to left panel beacon warning

Signed-off-by: Kerry Archibald <[email protected]>

* test

Signed-off-by: Kerry Archibald <[email protected]>

* add beacon sort util

* link to latest beacon room from left panel warning

Signed-off-by: Kerry Archibald <[email protected]>
  • Loading branch information
Kerry authored Mar 31, 2022
1 parent 1175226 commit 4922e19
Show file tree
Hide file tree
Showing 8 changed files with 289 additions and 24 deletions.
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

0 comments on commit 4922e19

Please sign in to comment.