Merge branch 'develop' into travis/remove-skinning
This commit is contained in:
commit
97efdf7094
54 changed files with 1559 additions and 431 deletions
2
.github/workflows/test_coverage.yml
vendored
2
.github/workflows/test_coverage.yml
vendored
|
@ -16,7 +16,7 @@ jobs:
|
||||||
# If this is a pull request, make sure we check out its head rather than the
|
# If this is a pull request, make sure we check out its head rather than the
|
||||||
# automatically generated merge commit, so that the coverage diff excludes
|
# automatically generated merge commit, so that the coverage diff excludes
|
||||||
# unrelated changes in the base branch
|
# unrelated changes in the base branch
|
||||||
ref: ${{ github.event.type == 'PullRequestEvent' && github.event.pull_request.head.sha || '' }}
|
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || '' }}
|
||||||
|
|
||||||
- name: Yarn cache
|
- name: Yarn cache
|
||||||
uses: c-hive/gha-yarn-cache@v2
|
uses: c-hive/gha-yarn-cache@v2
|
||||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_LeftPanelLiveShareWarning {
|
.mx_LeftPanelLiveShareWarning {
|
||||||
|
@mixin ButtonResetDefault;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
@ -29,3 +30,7 @@ limitations under the License.
|
||||||
// go above to get hover for title
|
// go above to get hover for title
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_LeftPanelLiveShareWarning__error {
|
||||||
|
background-color: $alert;
|
||||||
|
}
|
||||||
|
|
|
@ -48,3 +48,13 @@ limitations under the License.
|
||||||
.mx_RoomLiveShareWarning_spinner {
|
.mx_RoomLiveShareWarning_spinner {
|
||||||
margin-right: $spacing-16;
|
margin-right: $spacing-16;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_RoomLiveShareWarning_closeButton {
|
||||||
|
@mixin ButtonResetDefault;
|
||||||
|
margin-left: $spacing-16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomLiveShareWarning_closeButtonIcon {
|
||||||
|
height: $font-18px;
|
||||||
|
padding: $spacing-4;
|
||||||
|
}
|
||||||
|
|
|
@ -28,3 +28,8 @@ limitations under the License.
|
||||||
// colors icon
|
// colors icon
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_StyledLiveBeaconIcon.mx_StyledLiveBeaconIcon_error {
|
||||||
|
background-color: $alert;
|
||||||
|
border-color: $alert;
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
Copyright 2021 - 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -29,6 +29,7 @@ limitations under the License.
|
||||||
|
|
||||||
contain: content;
|
contain: content;
|
||||||
|
|
||||||
|
// Waveforms are present in live recording only
|
||||||
.mx_Waveform {
|
.mx_Waveform {
|
||||||
.mx_Waveform_bar {
|
.mx_Waveform_bar {
|
||||||
background-color: $quaternary-content;
|
background-color: $quaternary-content;
|
||||||
|
@ -46,11 +47,22 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_Clock {
|
.mx_Clock {
|
||||||
width: $font-42px; // we're not using a monospace font, so fake it
|
width: $font-42px; // we're not using a monospace font, so fake it
|
||||||
|
min-width: $font-42px; // force sensible layouts in awkward flexboxes (file panel, for example)
|
||||||
padding-right: 6px; // with the fixed width this ends up as a visual 8px most of the time, as intended.
|
padding-right: 6px; // with the fixed width this ends up as a visual 8px most of the time, as intended.
|
||||||
padding-left: 8px; // isolate from recording circle / play control
|
padding-left: 8px; // isolate from recording circle / play control
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_VoiceMessagePrimaryContainer_noWaveform {
|
// For timeline-rendered playback, mirror the values for where the clock is in
|
||||||
max-width: 162px; // with all the padding this results in 185px wide
|
// the waveform version.
|
||||||
|
.mx_SeekBar {
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-right: 6px;
|
||||||
|
|
||||||
|
& + .mx_Clock {
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
// Take the padding off the clock because it's accounted for in the seek bar
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
Copyright 2021 - 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -35,6 +35,12 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_TimelineCard_timeline {
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative; // offset parent for jump to bottom button
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_AutoHideScrollbar {
|
.mx_AutoHideScrollbar {
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
width: calc(100% - 10px);
|
width: calc(100% - 10px);
|
||||||
|
@ -119,8 +125,3 @@ limitations under the License.
|
||||||
flex-basis: 48px; // 12 (padding on message list) + 36 (padding on event lines)
|
flex-basis: 48px; // 12 (padding on message list) + 36 (padding on event lines)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_TimelineCard_timeline {
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative; // offset parent for jump to bottom button
|
|
||||||
}
|
|
||||||
|
|
|
@ -970,6 +970,7 @@ $threadInfoLineHeight: calc(2 * $font-12px);
|
||||||
.mx_EventTile_content,
|
.mx_EventTile_content,
|
||||||
.mx_HiddenBody,
|
.mx_HiddenBody,
|
||||||
.mx_RedactedBody,
|
.mx_RedactedBody,
|
||||||
|
.mx_UnknownBody,
|
||||||
.mx_MPollBody,
|
.mx_MPollBody,
|
||||||
.mx_ReplyChain_wrapper {
|
.mx_ReplyChain_wrapper {
|
||||||
margin-left: 36px;
|
margin-left: 36px;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015 - 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -98,6 +98,14 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// With maximised widgets, the panel fits in better when rounded
|
||||||
|
.mx_MainSplit_maximisedWidget .mx_RoomPreviewBar_panel {
|
||||||
|
margin: $container-gap-width;
|
||||||
|
margin-right: calc($container-gap-width / 2); // Shared with right panel
|
||||||
|
margin-top: 0; // Already covered by apps drawer
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_RoomPreviewBar_dialog {
|
.mx_RoomPreviewBar_dialog {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
box-sizing: content;
|
box-sizing: content;
|
||||||
|
|
|
@ -49,6 +49,7 @@ import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
||||||
import UploadFailureDialog from "./components/views/dialogs/UploadFailureDialog";
|
import UploadFailureDialog from "./components/views/dialogs/UploadFailureDialog";
|
||||||
import UploadConfirmDialog from "./components/views/dialogs/UploadConfirmDialog";
|
import UploadConfirmDialog from "./components/views/dialogs/UploadConfirmDialog";
|
||||||
import { createThumbnail } from "./utils/image-media";
|
import { createThumbnail } from "./utils/image-media";
|
||||||
|
import { attachRelation } from "./components/views/rooms/SendMessageComposer";
|
||||||
|
|
||||||
// scraped out of a macOS hidpi (5660ppm) screenshot png
|
// scraped out of a macOS hidpi (5660ppm) screenshot png
|
||||||
// 5669 px (x-axis) , 5669 px (y-axis) , per metre
|
// 5669 px (x-axis) , 5669 px (y-axis) , per metre
|
||||||
|
@ -147,15 +148,20 @@ async function infoForImageFile(matrixClient: MatrixClient, roomId: string, imag
|
||||||
const result = await createThumbnail(imageElement.img, imageElement.width, imageElement.height, thumbnailType);
|
const result = await createThumbnail(imageElement.img, imageElement.width, imageElement.height, thumbnailType);
|
||||||
const imageInfo = result.info;
|
const imageInfo = result.info;
|
||||||
|
|
||||||
// we do all sizing checks here because we still rely on thumbnail generation for making a blurhash from.
|
// For lesser supported image types, always include the thumbnail even if it is larger
|
||||||
const sizeDifference = imageFile.size - imageInfo.thumbnail_info.size;
|
if (!["image/avif", "image/webp"].includes(imageFile.type)) {
|
||||||
if (
|
// we do all sizing checks here because we still rely on thumbnail generation for making a blurhash from.
|
||||||
imageFile.size <= IMAGE_SIZE_THRESHOLD_THUMBNAIL || // image is small enough already
|
const sizeDifference = imageFile.size - imageInfo.thumbnail_info.size;
|
||||||
(sizeDifference <= IMAGE_THUMBNAIL_MIN_REDUCTION_SIZE && // thumbnail is not sufficiently smaller than original
|
if (
|
||||||
sizeDifference <= (imageFile.size * IMAGE_THUMBNAIL_MIN_REDUCTION_PERCENT))
|
// image is small enough already
|
||||||
) {
|
imageFile.size <= IMAGE_SIZE_THRESHOLD_THUMBNAIL ||
|
||||||
delete imageInfo["thumbnail_info"];
|
// thumbnail is not sufficiently smaller than original
|
||||||
return imageInfo;
|
(sizeDifference <= IMAGE_THUMBNAIL_MIN_REDUCTION_SIZE &&
|
||||||
|
sizeDifference <= (imageFile.size * IMAGE_THUMBNAIL_MIN_REDUCTION_PERCENT))
|
||||||
|
) {
|
||||||
|
delete imageInfo["thumbnail_info"];
|
||||||
|
return imageInfo;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadResult = await uploadFile(matrixClient, roomId, result.thumbnail);
|
const uploadResult = await uploadFile(matrixClient, roomId, result.thumbnail);
|
||||||
|
@ -474,10 +480,7 @@ export default class ContentMessages {
|
||||||
msgtype: "", // set later
|
msgtype: "", // set later
|
||||||
};
|
};
|
||||||
|
|
||||||
if (relation) {
|
attachRelation(content, relation);
|
||||||
content["m.relates_to"] = relation;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (replyToEvent) {
|
if (replyToEvent) {
|
||||||
addReplyToMessageContent(content, replyToEvent, {
|
addReplyToMessageContent(content, replyToEvent, {
|
||||||
includeLegacyFallback: false,
|
includeLegacyFallback: false,
|
||||||
|
|
20
src/Editing.ts
Normal file
20
src/Editing.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { TimelineRenderingType } from "./contexts/RoomContext";
|
||||||
|
|
||||||
|
export const editorRoomKey = (roomId: string, context: TimelineRenderingType) => `mx_edit_room_${roomId}_${context}`;
|
||||||
|
export const editorStateKey = (eventId: string) => `mx_edit_state_${eventId}`;
|
|
@ -126,7 +126,7 @@ export function showAnyInviteErrors(
|
||||||
// user. This usually means that no other users were attempted, making it
|
// user. This usually means that no other users were attempted, making it
|
||||||
// pointless for us to list who failed exactly.
|
// pointless for us to list who failed exactly.
|
||||||
Modal.createTrackedDialog('Failed to invite users to the room', '', ErrorDialog, {
|
Modal.createTrackedDialog('Failed to invite users to the room', '', ErrorDialog, {
|
||||||
title: _t("Failed to invite users to the room:", { roomName: room.name }),
|
title: _t("Failed to invite users to %(roomName)s", { roomName: room.name }),
|
||||||
description: inviter.getErrorText(failedUsers[0]),
|
description: inviter.getErrorText(failedUsers[0]),
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -21,6 +21,8 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { MatrixClientPeg } from "./MatrixClientPeg";
|
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||||
import shouldHideEvent from './shouldHideEvent';
|
import shouldHideEvent from './shouldHideEvent';
|
||||||
import { haveRendererForEvent } from "./events/EventTileFactory";
|
import { haveRendererForEvent } from "./events/EventTileFactory";
|
||||||
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
|
import { RoomNotificationStateStore } from "./stores/notifications/RoomNotificationStateStore";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if this event arriving in a room should affect the room's
|
* Returns true if this event arriving in a room should affect the room's
|
||||||
|
@ -57,14 +59,21 @@ export function doesRoomHaveUnreadMessages(room: Room): boolean {
|
||||||
// despite the name of the method :((
|
// despite the name of the method :((
|
||||||
const readUpToId = room.getEventReadUpTo(myUserId);
|
const readUpToId = room.getEventReadUpTo(myUserId);
|
||||||
|
|
||||||
// as we don't send RRs for our own messages, make sure we special case that
|
if (!SettingsStore.getValue("feature_thread")) {
|
||||||
// if *we* sent the last message into the room, we consider it not unread!
|
// as we don't send RRs for our own messages, make sure we special case that
|
||||||
// Should fix: https://github.com/vector-im/element-web/issues/3263
|
// if *we* sent the last message into the room, we consider it not unread!
|
||||||
// https://github.com/vector-im/element-web/issues/2427
|
// Should fix: https://github.com/vector-im/element-web/issues/3263
|
||||||
// ...and possibly some of the others at
|
// https://github.com/vector-im/element-web/issues/2427
|
||||||
// https://github.com/vector-im/element-web/issues/3363
|
// ...and possibly some of the others at
|
||||||
if (room.timeline.length && room.timeline[room.timeline.length - 1].getSender() === myUserId) {
|
// https://github.com/vector-im/element-web/issues/3363
|
||||||
return false;
|
if (room.timeline.length && room.timeline[room.timeline.length - 1].getSender() === myUserId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const threadState = RoomNotificationStateStore.instance.getThreadsRoomState(room);
|
||||||
|
if (threadState.color > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the read receipt relates to an event is that part of a thread
|
// if the read receipt relates to an event is that part of a thread
|
||||||
|
|
|
@ -53,6 +53,7 @@ import { Action } from '../../dispatcher/actions';
|
||||||
import { getEventDisplayInfo } from "../../utils/EventRenderingUtils";
|
import { getEventDisplayInfo } from "../../utils/EventRenderingUtils";
|
||||||
import { IReadReceiptInfo } from "../views/rooms/ReadReceiptMarker";
|
import { IReadReceiptInfo } from "../views/rooms/ReadReceiptMarker";
|
||||||
import { haveRendererForEvent } from "../../events/EventTileFactory";
|
import { haveRendererForEvent } from "../../events/EventTileFactory";
|
||||||
|
import { editorRoomKey } from "../../Editing";
|
||||||
|
|
||||||
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||||
const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
|
const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
|
||||||
|
@ -306,9 +307,10 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
const pendingEditItem = this.pendingEditItem;
|
const pendingEditItem = this.pendingEditItem;
|
||||||
if (!this.props.editState && this.props.room && pendingEditItem) {
|
if (!this.props.editState && this.props.room && pendingEditItem) {
|
||||||
|
const event = this.props.room.findEventById(pendingEditItem);
|
||||||
defaultDispatcher.dispatch({
|
defaultDispatcher.dispatch({
|
||||||
action: Action.EditEvent,
|
action: Action.EditEvent,
|
||||||
event: this.props.room.findEventById(pendingEditItem),
|
event: !event?.isRedacted() ? event : null,
|
||||||
timelineRenderingType: this.context.timelineRenderingType,
|
timelineRenderingType: this.context.timelineRenderingType,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -612,13 +614,15 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
if (!this.props.room) {
|
if (!this.props.room) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return localStorage.getItem(`mx_edit_room_${this.props.room.roomId}_${this.context.timelineRenderingType}`);
|
return localStorage.getItem(editorRoomKey(this.props.room.roomId, this.context.timelineRenderingType));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getEventTiles(): ReactNode[] {
|
private getEventTiles(): ReactNode[] {
|
||||||
let i;
|
let i;
|
||||||
|
|
||||||
|
@ -721,10 +725,8 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
): ReactNode[] {
|
): ReactNode[] {
|
||||||
const ret = [];
|
const ret = [];
|
||||||
|
|
||||||
const isEditing = this.props.editState &&
|
const isEditing = this.props.editState?.getEvent().getId() === mxEv.getId();
|
||||||
this.props.editState.getEvent().getId() === mxEv.getId();
|
// local echoes have a fake date, which could even be yesterday. Treat them as 'today' for the date separators.
|
||||||
// local echoes have a fake date, which could even be yesterday. Treat them
|
|
||||||
// as 'today' for the date separators.
|
|
||||||
let ts1 = mxEv.getTs();
|
let ts1 = mxEv.getTs();
|
||||||
let eventDate = mxEv.getDate();
|
let eventDate = mxEv.getDate();
|
||||||
if (mxEv.status) {
|
if (mxEv.status) {
|
||||||
|
|
|
@ -364,7 +364,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
this.checkWidgets(this.state.room);
|
this.checkWidgets(this.state.room);
|
||||||
};
|
};
|
||||||
|
|
||||||
private checkWidgets = (room) => {
|
private checkWidgets = (room: Room): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
hasPinnedWidgets: WidgetLayoutStore.instance.hasPinnedWidgets(room),
|
hasPinnedWidgets: WidgetLayoutStore.instance.hasPinnedWidgets(room),
|
||||||
mainSplitContentType: this.getMainSplitContentType(room),
|
mainSplitContentType: this.getMainSplitContentType(room),
|
||||||
|
@ -372,7 +372,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private getMainSplitContentType = (room) => {
|
private getMainSplitContentType = (room: Room) => {
|
||||||
if (SettingsStore.getValue("feature_voice_rooms") && room.isCallRoom()) {
|
if (SettingsStore.getValue("feature_voice_rooms") && room.isCallRoom()) {
|
||||||
return MainSplitContentType.Video;
|
return MainSplitContentType.Video;
|
||||||
}
|
}
|
||||||
|
@ -1981,11 +1981,11 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
);
|
);
|
||||||
|
|
||||||
let messageComposer; let searchInfo;
|
let messageComposer; let searchInfo;
|
||||||
const canSpeak = (
|
const showComposer = (
|
||||||
// joined and not showing search results
|
// joined and not showing search results
|
||||||
myMembership === 'join' && !this.state.searchResults
|
myMembership === 'join' && !this.state.searchResults
|
||||||
);
|
);
|
||||||
if (canSpeak) {
|
if (showComposer) {
|
||||||
messageComposer =
|
messageComposer =
|
||||||
<MessageComposer
|
<MessageComposer
|
||||||
room={this.state.room}
|
room={this.state.room}
|
||||||
|
@ -2101,10 +2101,12 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
|
|
||||||
const showChatEffects = SettingsStore.getValue('showChatEffects');
|
const showChatEffects = SettingsStore.getValue('showChatEffects');
|
||||||
|
|
||||||
let mainSplitBody;
|
let mainSplitBody: React.ReactFragment;
|
||||||
|
let mainSplitContentClassName: string;
|
||||||
// Decide what to show in the main split
|
// Decide what to show in the main split
|
||||||
switch (this.state.mainSplitContentType) {
|
switch (this.state.mainSplitContentType) {
|
||||||
case MainSplitContentType.Timeline:
|
case MainSplitContentType.Timeline:
|
||||||
|
mainSplitContentClassName = "mx_MainSplit_timeline";
|
||||||
mainSplitBody = <>
|
mainSplitBody = <>
|
||||||
<Measured
|
<Measured
|
||||||
sensor={this.roomViewBody.current}
|
sensor={this.roomViewBody.current}
|
||||||
|
@ -2124,16 +2126,21 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
</>;
|
</>;
|
||||||
break;
|
break;
|
||||||
case MainSplitContentType.MaximisedWidget:
|
case MainSplitContentType.MaximisedWidget:
|
||||||
mainSplitBody = <AppsDrawer
|
mainSplitContentClassName = "mx_MainSplit_maximisedWidget";
|
||||||
room={this.state.room}
|
mainSplitBody = <>
|
||||||
userId={this.context.credentials.userId}
|
<AppsDrawer
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
room={this.state.room}
|
||||||
showApps={true}
|
userId={this.context.credentials.userId}
|
||||||
/>;
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
|
showApps={true}
|
||||||
|
/>
|
||||||
|
{ previewBar }
|
||||||
|
</>;
|
||||||
break;
|
break;
|
||||||
case MainSplitContentType.Video: {
|
case MainSplitContentType.Video: {
|
||||||
const app = getVoiceChannel(this.state.room.roomId);
|
const app = getVoiceChannel(this.state.room.roomId);
|
||||||
if (!app) break;
|
if (!app) break;
|
||||||
|
mainSplitContentClassName = "mx_MainSplit_video";
|
||||||
mainSplitBody = <AppTile
|
mainSplitBody = <AppTile
|
||||||
app={app}
|
app={app}
|
||||||
room={this.state.room}
|
room={this.state.room}
|
||||||
|
@ -2145,6 +2152,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const mainSplitContentClasses = classNames("mx_RoomView_body", mainSplitContentClassName);
|
||||||
|
|
||||||
let excludedRightPanelPhaseButtons = [RightPanelPhases.Timeline];
|
let excludedRightPanelPhaseButtons = [RightPanelPhases.Timeline];
|
||||||
let onAppsClick = this.onAppsClick;
|
let onAppsClick = this.onAppsClick;
|
||||||
let onForgetClick = this.onForgetClick;
|
let onForgetClick = this.onForgetClick;
|
||||||
|
@ -2160,6 +2169,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
onForgetClick = null;
|
onForgetClick = null;
|
||||||
onSearchClick = null;
|
onSearchClick = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RoomContext.Provider value={this.state}>
|
<RoomContext.Provider value={this.state}>
|
||||||
<main className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}>
|
<main className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}>
|
||||||
|
@ -2181,7 +2191,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
excludedRightPanelPhaseButtons={excludedRightPanelPhaseButtons}
|
excludedRightPanelPhaseButtons={excludedRightPanelPhaseButtons}
|
||||||
/>
|
/>
|
||||||
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
|
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
|
||||||
<div className="mx_RoomView_body" ref={this.roomViewBody} data-layout={this.state.layout}>
|
<div className={mainSplitContentClasses} ref={this.roomViewBody} data-layout={this.state.layout}>
|
||||||
{ mainSplitBody }
|
{ mainSplitBody }
|
||||||
</div>
|
</div>
|
||||||
</MainSplit>
|
</MainSplit>
|
||||||
|
|
|
@ -50,6 +50,7 @@ import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
|
||||||
import Measured from '../views/elements/Measured';
|
import Measured from '../views/elements/Measured';
|
||||||
import PosthogTrackers from "../../PosthogTrackers";
|
import PosthogTrackers from "../../PosthogTrackers";
|
||||||
import { ButtonEvent } from "../views/elements/AccessibleButton";
|
import { ButtonEvent } from "../views/elements/AccessibleButton";
|
||||||
|
import RoomViewStore from '../../stores/RoomViewStore';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -104,9 +105,19 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||||
public componentWillUnmount(): void {
|
public componentWillUnmount(): void {
|
||||||
this.teardownThread();
|
this.teardownThread();
|
||||||
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
|
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
|
||||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
const roomId = this.props.mxEvent.getRoomId();
|
||||||
|
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||||
room.removeListener(ThreadEvent.New, this.onNewThread);
|
room.removeListener(ThreadEvent.New, this.onNewThread);
|
||||||
SettingsStore.unwatchSetting(this.layoutWatcherRef);
|
SettingsStore.unwatchSetting(this.layoutWatcherRef);
|
||||||
|
|
||||||
|
const hasRoomChanged = RoomViewStore.getRoomId() !== roomId;
|
||||||
|
if (this.props.isInitialEventHighlighted && !hasRoomChanged) {
|
||||||
|
dis.dispatch<ViewRoomPayload>({
|
||||||
|
action: Action.ViewRoom,
|
||||||
|
room_id: this.props.room.roomId,
|
||||||
|
metricsTrigger: undefined, // room doesn't change
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(prevProps) {
|
public componentDidUpdate(prevProps) {
|
||||||
|
@ -204,7 +215,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onScroll = (): void => {
|
private resetHighlightedEvent = (): void => {
|
||||||
if (this.props.initialEvent && this.props.isInitialEventHighlighted) {
|
if (this.props.initialEvent && this.props.isInitialEventHighlighted) {
|
||||||
dis.dispatch<ViewRoomPayload>({
|
dis.dispatch<ViewRoomPayload>({
|
||||||
action: Action.ViewRoom,
|
action: Action.ViewRoom,
|
||||||
|
@ -361,7 +372,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||||
editState={this.state.editState}
|
editState={this.state.editState}
|
||||||
eventId={this.props.initialEvent?.getId()}
|
eventId={this.props.initialEvent?.getId()}
|
||||||
highlightedEventId={highlightedEventId}
|
highlightedEventId={highlightedEventId}
|
||||||
onUserScroll={this.onScroll}
|
onUserScroll={this.resetHighlightedEvent}
|
||||||
onPaginationRequest={this.onPaginationRequest}
|
onPaginationRequest={this.onPaginationRequest}
|
||||||
/>
|
/>
|
||||||
</div> }
|
</div> }
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
Copyright 2021 - 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef, ReactNode, RefObject } from "react";
|
import React, { ReactNode } from "react";
|
||||||
|
|
||||||
import PlayPauseButton from "./PlayPauseButton";
|
import PlayPauseButton from "./PlayPauseButton";
|
||||||
import { formatBytes } from "../../../utils/FormattingUtils";
|
import { formatBytes } from "../../../utils/FormattingUtils";
|
||||||
|
@ -23,40 +23,8 @@ import { _t } from "../../../languageHandler";
|
||||||
import SeekBar from "./SeekBar";
|
import SeekBar from "./SeekBar";
|
||||||
import PlaybackClock from "./PlaybackClock";
|
import PlaybackClock from "./PlaybackClock";
|
||||||
import AudioPlayerBase from "./AudioPlayerBase";
|
import AudioPlayerBase from "./AudioPlayerBase";
|
||||||
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
|
||||||
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
|
||||||
|
|
||||||
export default class AudioPlayer extends AudioPlayerBase {
|
export default class AudioPlayer extends AudioPlayerBase {
|
||||||
private playPauseRef: RefObject<PlayPauseButton> = createRef();
|
|
||||||
private seekRef: RefObject<SeekBar> = createRef();
|
|
||||||
|
|
||||||
private onKeyDown = (ev: React.KeyboardEvent) => {
|
|
||||||
let handled = true;
|
|
||||||
const action = getKeyBindingsManager().getAccessibilityAction(ev);
|
|
||||||
|
|
||||||
switch (action) {
|
|
||||||
case KeyBindingAction.Space:
|
|
||||||
this.playPauseRef.current?.toggleState();
|
|
||||||
break;
|
|
||||||
case KeyBindingAction.ArrowLeft:
|
|
||||||
this.seekRef.current?.left();
|
|
||||||
break;
|
|
||||||
case KeyBindingAction.ArrowRight:
|
|
||||||
this.seekRef.current?.right();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
handled = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// stopPropagation() prevents the FocusComposer catch-all from triggering,
|
|
||||||
// but we need to do it on key down instead of press (even though the user
|
|
||||||
// interaction is typically on press).
|
|
||||||
if (handled) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
protected renderFileSize(): string {
|
protected renderFileSize(): string {
|
||||||
const bytes = this.props.playback.sizeBytes;
|
const bytes = this.props.playback.sizeBytes;
|
||||||
if (!bytes) return null;
|
if (!bytes) return null;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
Copyright 2021 - 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,14 +14,18 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { ReactNode } from "react";
|
import React, { createRef, ReactNode, RefObject } from "react";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
import { Playback, PlaybackState } from "../../../audio/Playback";
|
import { Playback, PlaybackState } from "../../../audio/Playback";
|
||||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
|
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||||
|
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||||
|
import SeekBar from "./SeekBar";
|
||||||
|
import PlayPauseButton from "./PlayPauseButton";
|
||||||
|
|
||||||
interface IProps {
|
export interface IProps {
|
||||||
// Playback instance to render. Cannot change during component lifecycle: create
|
// Playback instance to render. Cannot change during component lifecycle: create
|
||||||
// an all-new component instead.
|
// an all-new component instead.
|
||||||
playback: Playback;
|
playback: Playback;
|
||||||
|
@ -34,8 +38,11 @@ interface IState {
|
||||||
error?: boolean;
|
error?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default abstract class AudioPlayerBase extends React.PureComponent<IProps, IState> {
|
export default abstract class AudioPlayerBase<T extends IProps = IProps> extends React.PureComponent<T, IState> {
|
||||||
constructor(props: IProps) {
|
protected seekRef: RefObject<SeekBar> = createRef();
|
||||||
|
protected playPauseRef: RefObject<PlayPauseButton> = createRef();
|
||||||
|
|
||||||
|
constructor(props: T) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
// Playback instances can be reused in the composer
|
// Playback instances can be reused in the composer
|
||||||
|
@ -54,6 +61,33 @@ export default abstract class AudioPlayerBase extends React.PureComponent<IProps
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected onKeyDown = (ev: React.KeyboardEvent) => {
|
||||||
|
let handled = true;
|
||||||
|
const action = getKeyBindingsManager().getAccessibilityAction(ev);
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case KeyBindingAction.Space:
|
||||||
|
this.playPauseRef.current?.toggleState();
|
||||||
|
break;
|
||||||
|
case KeyBindingAction.ArrowLeft:
|
||||||
|
this.seekRef.current?.left();
|
||||||
|
break;
|
||||||
|
case KeyBindingAction.ArrowRight:
|
||||||
|
this.seekRef.current?.right();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
handled = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// stopPropagation() prevents the FocusComposer catch-all from triggering,
|
||||||
|
// but we need to do it on key down instead of press (even though the user
|
||||||
|
// interaction is typically on press).
|
||||||
|
if (handled) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private onPlaybackUpdate = (ev: PlaybackState) => {
|
private onPlaybackUpdate = (ev: PlaybackState) => {
|
||||||
this.setState({ playbackPhase: ev });
|
this.setState({ playbackPhase: ev });
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
Copyright 2021 - 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -18,28 +18,49 @@ import React, { ReactNode } from "react";
|
||||||
|
|
||||||
import PlayPauseButton from "./PlayPauseButton";
|
import PlayPauseButton from "./PlayPauseButton";
|
||||||
import PlaybackClock from "./PlaybackClock";
|
import PlaybackClock from "./PlaybackClock";
|
||||||
|
import AudioPlayerBase, { IProps as IAudioPlayerBaseProps } from "./AudioPlayerBase";
|
||||||
|
import SeekBar from "./SeekBar";
|
||||||
import PlaybackWaveform from "./PlaybackWaveform";
|
import PlaybackWaveform from "./PlaybackWaveform";
|
||||||
import AudioPlayerBase from "./AudioPlayerBase";
|
|
||||||
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
|
||||||
|
|
||||||
export default class RecordingPlayback extends AudioPlayerBase {
|
interface IProps extends IAudioPlayerBaseProps {
|
||||||
static contextType = RoomContext;
|
/**
|
||||||
public context!: React.ContextType<typeof RoomContext>;
|
* When true, use a waveform instead of a seek bar
|
||||||
|
*/
|
||||||
|
withWaveform?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
private get isWaveformable(): boolean {
|
export default class RecordingPlayback extends AudioPlayerBase<IProps> {
|
||||||
return this.context.timelineRenderingType !== TimelineRenderingType.Notification
|
// This component is rendered in two ways: the composer and timeline. They have different
|
||||||
&& this.context.timelineRenderingType !== TimelineRenderingType.File
|
// rendering properties (specifically the difference of a waveform or not).
|
||||||
&& this.context.timelineRenderingType !== TimelineRenderingType.Pinned;
|
|
||||||
|
private renderWaveformLook(): ReactNode {
|
||||||
|
return <>
|
||||||
|
<PlaybackClock playback={this.props.playback} />
|
||||||
|
<PlaybackWaveform playback={this.props.playback} />
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderSeekableLook(): ReactNode {
|
||||||
|
return <>
|
||||||
|
<SeekBar
|
||||||
|
playback={this.props.playback}
|
||||||
|
tabIndex={-1} // prevent tabbing into the bar
|
||||||
|
playbackPhase={this.state.playbackPhase}
|
||||||
|
ref={this.seekRef}
|
||||||
|
/>
|
||||||
|
<PlaybackClock playback={this.props.playback} />
|
||||||
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected renderComponent(): ReactNode {
|
protected renderComponent(): ReactNode {
|
||||||
const shapeClass = !this.isWaveformable ? 'mx_VoiceMessagePrimaryContainer_noWaveform' : '';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'mx_MediaBody mx_VoiceMessagePrimaryContainer ' + shapeClass}>
|
<div className="mx_MediaBody mx_VoiceMessagePrimaryContainer" onKeyDown={this.onKeyDown}>
|
||||||
<PlayPauseButton playback={this.props.playback} playbackPhase={this.state.playbackPhase} />
|
<PlayPauseButton
|
||||||
<PlaybackClock playback={this.props.playback} />
|
playback={this.props.playback}
|
||||||
{ this.isWaveformable && <PlaybackWaveform playback={this.props.playback} /> }
|
playbackPhase={this.state.playbackPhase}
|
||||||
|
ref={this.playPauseRef}
|
||||||
|
/>
|
||||||
|
{ this.props.withWaveform ? this.renderWaveformLook() : this.renderSeekableLook() }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,11 +21,31 @@ import { useEventEmitterState } from '../../../hooks/useEventEmitter';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { OwnBeaconStore, OwnBeaconStoreEvent } from '../../../stores/OwnBeaconStore';
|
import { OwnBeaconStore, OwnBeaconStoreEvent } from '../../../stores/OwnBeaconStore';
|
||||||
import { Icon as LiveLocationIcon } from '../../../../res/img/location/live-location.svg';
|
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 {
|
interface Props {
|
||||||
isMinimized?: boolean;
|
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 LeftPanelLiveShareWarning: React.FC<Props> = ({ isMinimized }) => {
|
||||||
const isMonitoringLiveLocation = useEventEmitterState(
|
const isMonitoringLiveLocation = useEventEmitterState(
|
||||||
OwnBeaconStore.instance,
|
OwnBeaconStore.instance,
|
||||||
|
@ -33,18 +53,48 @@ const LeftPanelLiveShareWarning: React.FC<Props> = ({ isMinimized }) => {
|
||||||
() => OwnBeaconStore.instance.isMonitoringLiveLocation,
|
() => 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) {
|
if (!isMonitoringLiveLocation) {
|
||||||
return null;
|
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', {
|
className={classNames('mx_LeftPanelLiveShareWarning', {
|
||||||
'mx_LeftPanelLiveShareWarning__minimized': isMinimized,
|
'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') }
|
{ isMinimized ? <LiveLocationIcon height={10} /> : label }
|
||||||
</div>;
|
</AccessibleButton>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LeftPanelLiveShareWarning;
|
export default LeftPanelLiveShareWarning;
|
||||||
|
|
|
@ -18,19 +18,16 @@ import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Room, Beacon } from 'matrix-js-sdk/src/matrix';
|
import { Room, Beacon } from 'matrix-js-sdk/src/matrix';
|
||||||
|
|
||||||
|
import { formatDuration } from '../../../DateUtils';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { useEventEmitterState } from '../../../hooks/useEventEmitter';
|
import { useEventEmitterState } from '../../../hooks/useEventEmitter';
|
||||||
import { OwnBeaconStore, OwnBeaconStoreEvent } from '../../../stores/OwnBeaconStore';
|
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
|
||||||
import StyledLiveBeaconIcon from './StyledLiveBeaconIcon';
|
|
||||||
import { formatDuration } from '../../../DateUtils';
|
|
||||||
import { getBeaconMsUntilExpiry, sortBeaconsByLatestExpiry } from '../../../utils/beacon';
|
|
||||||
import Spinner from '../elements/Spinner';
|
|
||||||
import { useInterval } from '../../../hooks/useTimeout';
|
import { useInterval } from '../../../hooks/useTimeout';
|
||||||
|
import { OwnBeaconStore, OwnBeaconStoreEvent } from '../../../stores/OwnBeaconStore';
|
||||||
interface Props {
|
import { getBeaconMsUntilExpiry, sortBeaconsByLatestExpiry } from '../../../utils/beacon';
|
||||||
roomId: Room['roomId'];
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
}
|
import Spinner from '../elements/Spinner';
|
||||||
|
import StyledLiveBeaconIcon from './StyledLiveBeaconIcon';
|
||||||
|
import { Icon as CloseIcon } from '../../../../res/img/image-view/close.svg';
|
||||||
|
|
||||||
const MINUTE_MS = 60000;
|
const MINUTE_MS = 60000;
|
||||||
const HOUR_MS = MINUTE_MS * 60;
|
const HOUR_MS = MINUTE_MS * 60;
|
||||||
|
@ -72,33 +69,28 @@ const useMsRemaining = (beacon: Beacon): number => {
|
||||||
type LiveBeaconsState = {
|
type LiveBeaconsState = {
|
||||||
beacon?: Beacon;
|
beacon?: Beacon;
|
||||||
onStopSharing?: () => void;
|
onStopSharing?: () => void;
|
||||||
|
onResetWireError?: () => void;
|
||||||
stoppingInProgress?: boolean;
|
stoppingInProgress?: boolean;
|
||||||
|
hasStopSharingError?: boolean;
|
||||||
|
hasWireError?: boolean;
|
||||||
};
|
};
|
||||||
const useLiveBeacons = (roomId: Room['roomId']): LiveBeaconsState => {
|
const useLiveBeacons = (liveBeaconIds: string[], roomId: string): LiveBeaconsState => {
|
||||||
const [stoppingInProgress, setStoppingInProgress] = useState(false);
|
const [stoppingInProgress, setStoppingInProgress] = useState(false);
|
||||||
|
const [error, setError] = useState<Error>();
|
||||||
|
|
||||||
// do we have an active geolocation.watchPosition
|
const hasWireError = useEventEmitterState(
|
||||||
const isMonitoringLiveLocation = useEventEmitterState(
|
|
||||||
OwnBeaconStore.instance,
|
OwnBeaconStore.instance,
|
||||||
OwnBeaconStoreEvent.MonitoringLivePosition,
|
OwnBeaconStoreEvent.WireError,
|
||||||
() => OwnBeaconStore.instance.isMonitoringLiveLocation,
|
() =>
|
||||||
);
|
OwnBeaconStore.instance.hasWireErrors(roomId),
|
||||||
|
|
||||||
const liveBeaconIds = useEventEmitterState(
|
|
||||||
OwnBeaconStore.instance,
|
|
||||||
OwnBeaconStoreEvent.LivenessChange,
|
|
||||||
() => OwnBeaconStore.instance.getLiveBeaconIds(roomId),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// reset stopping in progress on change in live ids
|
// reset stopping in progress on change in live ids
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setStoppingInProgress(false);
|
setStoppingInProgress(false);
|
||||||
|
setError(undefined);
|
||||||
}, [liveBeaconIds]);
|
}, [liveBeaconIds]);
|
||||||
|
|
||||||
if (!isMonitoringLiveLocation || !liveBeaconIds?.length) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// select the beacon with latest expiry to display expiry time
|
// select the beacon with latest expiry to display expiry time
|
||||||
const beacon = liveBeaconIds.map(beaconId => OwnBeaconStore.instance.getBeaconById(beaconId))
|
const beacon = liveBeaconIds.map(beaconId => OwnBeaconStore.instance.getBeaconById(beaconId))
|
||||||
.sort(sortBeaconsByLatestExpiry)
|
.sort(sortBeaconsByLatestExpiry)
|
||||||
|
@ -112,11 +104,23 @@ const useLiveBeacons = (roomId: Room['roomId']): LiveBeaconsState => {
|
||||||
// only clear loading in case of error
|
// only clear loading in case of error
|
||||||
// to avoid flash of not-loading state
|
// to avoid flash of not-loading state
|
||||||
// after beacons have been stopped but we wait for sync
|
// after beacons have been stopped but we wait for sync
|
||||||
|
setError(error);
|
||||||
setStoppingInProgress(false);
|
setStoppingInProgress(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { onStopSharing, beacon, stoppingInProgress };
|
const onResetWireError = () => {
|
||||||
|
liveBeaconIds.map(beaconId => OwnBeaconStore.instance.resetWireError(beaconId));
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
onStopSharing,
|
||||||
|
onResetWireError,
|
||||||
|
beacon,
|
||||||
|
stoppingInProgress,
|
||||||
|
hasWireError,
|
||||||
|
hasStopSharingError: !!error,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const LiveTimeRemaining: React.FC<{ beacon: Beacon }> = ({ beacon }) => {
|
const LiveTimeRemaining: React.FC<{ beacon: Beacon }> = ({ beacon }) => {
|
||||||
|
@ -131,39 +135,103 @@ const LiveTimeRemaining: React.FC<{ beacon: Beacon }> = ({ beacon }) => {
|
||||||
>{ liveTimeRemaining }</span>;
|
>{ liveTimeRemaining }</span>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const RoomLiveShareWarning: React.FC<Props> = ({ roomId }) => {
|
const getLabel = (hasWireError: boolean, hasStopSharingError: boolean): string => {
|
||||||
|
if (hasWireError) {
|
||||||
|
return _t('An error occured whilst sharing your live location, please try again');
|
||||||
|
}
|
||||||
|
if (hasStopSharingError) {
|
||||||
|
return _t('An error occurred while stopping your live location, please try again');
|
||||||
|
}
|
||||||
|
return _t('You are sharing your live location');
|
||||||
|
};
|
||||||
|
|
||||||
|
interface RoomLiveShareWarningInnerProps {
|
||||||
|
liveBeaconIds: string[];
|
||||||
|
roomId: Room['roomId'];
|
||||||
|
}
|
||||||
|
const RoomLiveShareWarningInner: React.FC<RoomLiveShareWarningInnerProps> = ({ liveBeaconIds, roomId }) => {
|
||||||
const {
|
const {
|
||||||
onStopSharing,
|
onStopSharing,
|
||||||
|
onResetWireError,
|
||||||
beacon,
|
beacon,
|
||||||
stoppingInProgress,
|
stoppingInProgress,
|
||||||
} = useLiveBeacons(roomId);
|
hasStopSharingError,
|
||||||
|
hasWireError,
|
||||||
|
} = useLiveBeacons(liveBeaconIds, roomId);
|
||||||
|
|
||||||
if (!beacon) {
|
if (!beacon) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasError = hasStopSharingError || hasWireError;
|
||||||
|
|
||||||
|
const onButtonClick = () => {
|
||||||
|
if (hasWireError) {
|
||||||
|
onResetWireError();
|
||||||
|
} else {
|
||||||
|
onStopSharing();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return <div
|
return <div
|
||||||
className={classNames('mx_RoomLiveShareWarning')}
|
className={classNames('mx_RoomLiveShareWarning')}
|
||||||
>
|
>
|
||||||
<StyledLiveBeaconIcon className="mx_RoomLiveShareWarning_icon" />
|
<StyledLiveBeaconIcon className="mx_RoomLiveShareWarning_icon" withError={hasError} />
|
||||||
|
|
||||||
<span className="mx_RoomLiveShareWarning_label">
|
<span className="mx_RoomLiveShareWarning_label">
|
||||||
{ _t('You are sharing your live location') }
|
{ getLabel(hasWireError, hasStopSharingError) }
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{ stoppingInProgress ?
|
{ stoppingInProgress &&
|
||||||
<span className='mx_RoomLiveShareWarning_spinner'><Spinner h={16} w={16} /></span> :
|
<span className='mx_RoomLiveShareWarning_spinner'><Spinner h={16} w={16} /></span>
|
||||||
<LiveTimeRemaining beacon={beacon} />
|
|
||||||
}
|
}
|
||||||
|
{ !stoppingInProgress && !hasError && <LiveTimeRemaining beacon={beacon} /> }
|
||||||
|
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
data-test-id='room-live-share-stop-sharing'
|
data-test-id='room-live-share-primary-button'
|
||||||
onClick={onStopSharing}
|
onClick={onButtonClick}
|
||||||
kind='danger'
|
kind='danger'
|
||||||
element='button'
|
element='button'
|
||||||
disabled={stoppingInProgress}
|
disabled={stoppingInProgress}
|
||||||
>
|
>
|
||||||
{ _t('Stop sharing') }
|
{ hasError ? _t('Retry') : _t('Stop sharing') }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
{ hasWireError && <AccessibleButton
|
||||||
|
data-test-id='room-live-share-wire-error-close-button'
|
||||||
|
title={_t('Stop sharing and close')}
|
||||||
|
element='button'
|
||||||
|
className='mx_RoomLiveShareWarning_closeButton'
|
||||||
|
onClick={onStopSharing}
|
||||||
|
>
|
||||||
|
<CloseIcon className='mx_RoomLiveShareWarning_closeButtonIcon' />
|
||||||
|
</AccessibleButton> }
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
roomId: Room['roomId'];
|
||||||
|
}
|
||||||
|
const RoomLiveShareWarning: React.FC<Props> = ({ roomId }) => {
|
||||||
|
// do we have an active geolocation.watchPosition
|
||||||
|
const isMonitoringLiveLocation = useEventEmitterState(
|
||||||
|
OwnBeaconStore.instance,
|
||||||
|
OwnBeaconStoreEvent.MonitoringLivePosition,
|
||||||
|
() => OwnBeaconStore.instance.isMonitoringLiveLocation,
|
||||||
|
);
|
||||||
|
|
||||||
|
const liveBeaconIds = useEventEmitterState(
|
||||||
|
OwnBeaconStore.instance,
|
||||||
|
OwnBeaconStoreEvent.LivenessChange,
|
||||||
|
() => OwnBeaconStore.instance.getLiveBeaconIds(roomId),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isMonitoringLiveLocation || !liveBeaconIds.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// split into outer/inner to avoid watching various parts of live beacon state
|
||||||
|
// when there are none
|
||||||
|
return <RoomLiveShareWarningInner liveBeaconIds={liveBeaconIds} roomId={roomId} />;
|
||||||
|
};
|
||||||
|
|
||||||
export default RoomLiveShareWarning;
|
export default RoomLiveShareWarning;
|
||||||
|
|
|
@ -19,10 +19,14 @@ import classNames from 'classnames';
|
||||||
|
|
||||||
import { Icon as LiveLocationIcon } from '../../../../res/img/location/live-location.svg';
|
import { Icon as LiveLocationIcon } from '../../../../res/img/location/live-location.svg';
|
||||||
|
|
||||||
const StyledLiveBeaconIcon: React.FC<React.SVGProps<SVGSVGElement>> = ({ className, ...props }) =>
|
interface Props extends React.SVGProps<SVGSVGElement> {
|
||||||
|
// use error styling when true
|
||||||
|
withError?: boolean;
|
||||||
|
}
|
||||||
|
const StyledLiveBeaconIcon: React.FC<Props> = ({ className, withError, ...props }) =>
|
||||||
<LiveLocationIcon
|
<LiveLocationIcon
|
||||||
{...props}
|
{...props}
|
||||||
className={classNames('mx_StyledLiveBeaconIcon', className)}
|
className={classNames('mx_StyledLiveBeaconIcon', className, { 'mx_StyledLiveBeaconIcon_error': withError })}
|
||||||
/>;
|
/>;
|
||||||
|
|
||||||
export default StyledLiveBeaconIcon;
|
export default StyledLiveBeaconIcon;
|
||||||
|
|
|
@ -219,6 +219,9 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
const isUploading = ContentMessages.sharedInstance().getCurrentUploads(this.props.composerRelation).length > 0;
|
const isUploading = ContentMessages.sharedInstance().getCurrentUploads(this.props.composerRelation).length > 0;
|
||||||
|
|
||||||
|
const myMembership = this.props.room.getMyMembership();
|
||||||
|
const showComposer = myMembership === "join";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RoomContext.Provider value={{
|
<RoomContext.Provider value={{
|
||||||
...this.context,
|
...this.context,
|
||||||
|
@ -268,15 +271,17 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
||||||
<UploadBar room={this.props.room} relation={this.props.composerRelation} />
|
<UploadBar room={this.props.room} relation={this.props.composerRelation} />
|
||||||
) }
|
) }
|
||||||
|
|
||||||
<MessageComposer
|
{ showComposer && (
|
||||||
room={this.props.room}
|
<MessageComposer
|
||||||
relation={this.props.composerRelation}
|
room={this.props.room}
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
relation={this.props.composerRelation}
|
||||||
replyToEvent={this.state.replyToEvent}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
replyToEvent={this.state.replyToEvent}
|
||||||
e2eStatus={this.props.e2eStatus}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
compact={true}
|
e2eStatus={this.props.e2eStatus}
|
||||||
/>
|
compact={true}
|
||||||
|
/>
|
||||||
|
) }
|
||||||
</BaseCard>
|
</BaseCard>
|
||||||
</RoomContext.Provider>
|
</RoomContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -430,8 +430,7 @@ const UserOptionsSection: React.FC<{
|
||||||
const roomId = member && member.roomId ? member.roomId : RoomViewStore.instance.getRoomId();
|
const roomId = member && member.roomId ? member.roomId : RoomViewStore.instance.getRoomId();
|
||||||
const onInviteUserButton = async (ev: ButtonEvent) => {
|
const onInviteUserButton = async (ev: ButtonEvent) => {
|
||||||
try {
|
try {
|
||||||
// We use a MultiInviter to re-use the invite logic, even though
|
// We use a MultiInviter to re-use the invite logic, even though we're only inviting one user.
|
||||||
// we're only inviting one user.
|
|
||||||
const inviter = new MultiInviter(roomId);
|
const inviter = new MultiInviter(roomId);
|
||||||
await inviter.invite([member.userId]).then(() => {
|
await inviter.invite([member.userId]).then(() => {
|
||||||
if (inviter.getCompletionState(member.userId) !== "invited") {
|
if (inviter.getCompletionState(member.userId) !== "invited") {
|
||||||
|
|
|
@ -46,6 +46,7 @@ import { ComposerType } from "../../../dispatcher/payloads/ComposerInsertPayload
|
||||||
import { getSlashCommand, isSlashCommand, runSlashCommand, shouldSendAnyway } from "../../../editor/commands";
|
import { getSlashCommand, isSlashCommand, runSlashCommand, shouldSendAnyway } from "../../../editor/commands";
|
||||||
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||||
import { PosthogAnalytics } from "../../../PosthogAnalytics";
|
import { PosthogAnalytics } from "../../../PosthogAnalytics";
|
||||||
|
import { editorRoomKey, editorStateKey } from "../../../Editing";
|
||||||
|
|
||||||
function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
|
function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
|
||||||
const html = mxEvent.getContent().formatted_body;
|
const html = mxEvent.getContent().formatted_body;
|
||||||
|
@ -222,11 +223,11 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
|
||||||
}
|
}
|
||||||
|
|
||||||
private get editorRoomKey(): string {
|
private get editorRoomKey(): string {
|
||||||
return `mx_edit_room_${this.getRoom().roomId}_${this.context.timelineRenderingType}`;
|
return editorRoomKey(this.props.editState.getEvent().getRoomId(), this.context.timelineRenderingType);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get editorStateKey(): string {
|
private get editorStateKey(): string {
|
||||||
return `mx_edit_state_${this.props.editState.getEvent().getId()}`;
|
return editorStateKey(this.props.editState.getEvent().getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private get events(): MatrixEvent[] {
|
private get events(): MatrixEvent[] {
|
||||||
|
@ -314,6 +315,9 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
|
||||||
this.cancelPreviousPendingEdit();
|
this.cancelPreviousPendingEdit();
|
||||||
createRedactEventDialog({
|
createRedactEventDialog({
|
||||||
mxEvent: editedEvent,
|
mxEvent: editedEvent,
|
||||||
|
onCloseDialog: () => {
|
||||||
|
this.cancelEdit();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -225,17 +225,6 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
||||||
.getStateEvents(EventType.RoomJoinRules, "")?.getContent<IJoinRuleEventContent>().join_rule;
|
.getStateEvents(EventType.RoomJoinRules, "")?.getContent<IJoinRuleEventContent>().join_rule;
|
||||||
}
|
}
|
||||||
|
|
||||||
private roomName(atStart = false): string {
|
|
||||||
const name = this.props.room ? this.props.room.name : this.props.roomAlias;
|
|
||||||
if (name) {
|
|
||||||
return name;
|
|
||||||
} else if (atStart) {
|
|
||||||
return _t("This room");
|
|
||||||
} else {
|
|
||||||
return _t("this room");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getMyMember(): RoomMember {
|
private getMyMember(): RoomMember {
|
||||||
return this.props.room?.getMember(MatrixClientPeg.get().getUserId());
|
return this.props.room?.getMember(MatrixClientPeg.get().getUserId());
|
||||||
}
|
}
|
||||||
|
@ -287,6 +276,8 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const brand = SdkConfig.get().brand;
|
const brand = SdkConfig.get().brand;
|
||||||
|
const roomName = this.props.room?.name ?? this.props.roomAlias ?? "";
|
||||||
|
const isSpace = this.props.room?.isSpaceRoom() ?? this.props.oobData?.roomType === RoomType.Space;
|
||||||
|
|
||||||
let showSpinner = false;
|
let showSpinner = false;
|
||||||
let title;
|
let title;
|
||||||
|
@ -302,7 +293,12 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
||||||
const messageCase = this.getMessageCase();
|
const messageCase = this.getMessageCase();
|
||||||
switch (messageCase) {
|
switch (messageCase) {
|
||||||
case MessageCase.Joining: {
|
case MessageCase.Joining: {
|
||||||
title = this.props.oobData?.roomType === RoomType.Space ? _t("Joining space …") : _t("Joining room …");
|
if (this.props.oobData?.roomType || isSpace) {
|
||||||
|
title = isSpace ? _t("Joining space …") : _t("Joining room …");
|
||||||
|
} else {
|
||||||
|
title = _t("Joining …");
|
||||||
|
}
|
||||||
|
|
||||||
showSpinner = true;
|
showSpinner = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -328,7 +324,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
||||||
footer = (
|
footer = (
|
||||||
<div>
|
<div>
|
||||||
<Spinner w={20} h={20} />
|
<Spinner w={20} h={20} />
|
||||||
{ _t("Loading room preview") }
|
{ _t("Loading preview") }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -336,37 +332,56 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
case MessageCase.Kicked: {
|
case MessageCase.Kicked: {
|
||||||
const { memberName, reason } = this.getKickOrBanInfo();
|
const { memberName, reason } = this.getKickOrBanInfo();
|
||||||
title = _t("You were removed from %(roomName)s by %(memberName)s",
|
if (roomName) {
|
||||||
{ memberName, roomName: this.roomName() });
|
title = _t("You were removed from %(roomName)s by %(memberName)s",
|
||||||
|
{ memberName, roomName });
|
||||||
|
} else {
|
||||||
|
title = _t("You were removed by %(memberName)s", { memberName });
|
||||||
|
}
|
||||||
subTitle = reason ? _t("Reason: %(reason)s", { reason }) : null;
|
subTitle = reason ? _t("Reason: %(reason)s", { reason }) : null;
|
||||||
|
|
||||||
if (this.joinRule() === "invite") {
|
if (isSpace) {
|
||||||
primaryActionLabel = _t("Forget this room");
|
primaryActionLabel = _t("Forget this space");
|
||||||
primaryActionHandler = this.props.onForgetClick;
|
|
||||||
} else {
|
} else {
|
||||||
|
primaryActionLabel = _t("Forget this room");
|
||||||
|
}
|
||||||
|
primaryActionHandler = this.props.onForgetClick;
|
||||||
|
|
||||||
|
if (this.joinRule() !== JoinRule.Invite) {
|
||||||
|
secondaryActionLabel = primaryActionLabel;
|
||||||
|
secondaryActionHandler = primaryActionHandler;
|
||||||
|
|
||||||
primaryActionLabel = _t("Re-join");
|
primaryActionLabel = _t("Re-join");
|
||||||
primaryActionHandler = this.props.onJoinClick;
|
primaryActionHandler = this.props.onJoinClick;
|
||||||
secondaryActionLabel = _t("Forget this room");
|
|
||||||
secondaryActionHandler = this.props.onForgetClick;
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MessageCase.Banned: {
|
case MessageCase.Banned: {
|
||||||
const { memberName, reason } = this.getKickOrBanInfo();
|
const { memberName, reason } = this.getKickOrBanInfo();
|
||||||
title = _t("You were banned from %(roomName)s by %(memberName)s",
|
if (roomName) {
|
||||||
{ memberName, roomName: this.roomName() });
|
title = _t("You were banned from %(roomName)s by %(memberName)s", { memberName, roomName });
|
||||||
|
} else {
|
||||||
|
title = _t("You were banned by %(memberName)s", { memberName });
|
||||||
|
}
|
||||||
subTitle = reason ? _t("Reason: %(reason)s", { reason }) : null;
|
subTitle = reason ? _t("Reason: %(reason)s", { reason }) : null;
|
||||||
primaryActionLabel = _t("Forget this room");
|
if (isSpace) {
|
||||||
|
primaryActionLabel = _t("Forget this space");
|
||||||
|
} else {
|
||||||
|
primaryActionLabel = _t("Forget this room");
|
||||||
|
}
|
||||||
primaryActionHandler = this.props.onForgetClick;
|
primaryActionHandler = this.props.onForgetClick;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MessageCase.OtherThreePIDError: {
|
case MessageCase.OtherThreePIDError: {
|
||||||
title = _t("Something went wrong with your invite to %(roomName)s",
|
if (roomName) {
|
||||||
{ roomName: this.roomName() });
|
title = _t("Something went wrong with your invite to %(roomName)s", { roomName });
|
||||||
|
} else {
|
||||||
|
title = _t("Something went wrong with your invite.");
|
||||||
|
}
|
||||||
const joinRule = this.joinRule();
|
const joinRule = this.joinRule();
|
||||||
const errCodeMessage = _t(
|
const errCodeMessage = _t(
|
||||||
"An error (%(errcode)s) was returned while trying to validate your " +
|
"An error (%(errcode)s) was returned while trying to validate your " +
|
||||||
"invite. You could try to pass this information on to a room admin.",
|
"invite. You could try to pass this information on to the person who invited you.",
|
||||||
{ errcode: this.state.threePidFetchError.errcode || _t("unknown error code") },
|
{ errcode: this.state.threePidFetchError.errcode || _t("unknown error code") },
|
||||||
);
|
);
|
||||||
switch (joinRule) {
|
switch (joinRule) {
|
||||||
|
@ -379,7 +394,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
||||||
primaryActionHandler = this.props.onJoinClick;
|
primaryActionHandler = this.props.onJoinClick;
|
||||||
break;
|
break;
|
||||||
case "public":
|
case "public":
|
||||||
subTitle = _t("You can still join it because this is a public room.");
|
subTitle = _t("You can still join here.");
|
||||||
primaryActionLabel = _t("Join the discussion");
|
primaryActionLabel = _t("Join the discussion");
|
||||||
primaryActionHandler = this.props.onJoinClick;
|
primaryActionHandler = this.props.onJoinClick;
|
||||||
break;
|
break;
|
||||||
|
@ -392,14 +407,22 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MessageCase.InvitedEmailNotFoundInAccount: {
|
case MessageCase.InvitedEmailNotFoundInAccount: {
|
||||||
title = _t(
|
if (roomName) {
|
||||||
"This invite to %(roomName)s was sent to %(email)s which is not " +
|
title = _t(
|
||||||
"associated with your account",
|
"This invite to %(roomName)s was sent to %(email)s which is not " +
|
||||||
{
|
"associated with your account",
|
||||||
roomName: this.roomName(),
|
{
|
||||||
email: this.props.invitedEmail,
|
roomName,
|
||||||
},
|
email: this.props.invitedEmail,
|
||||||
);
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
title = _t(
|
||||||
|
"This invite was sent to %(email)s which is not associated with your account",
|
||||||
|
{ email: this.props.invitedEmail },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
subTitle = _t(
|
subTitle = _t(
|
||||||
"Link this email with your account in Settings to receive invites " +
|
"Link this email with your account in Settings to receive invites " +
|
||||||
"directly in %(brand)s.",
|
"directly in %(brand)s.",
|
||||||
|
@ -410,13 +433,18 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MessageCase.InvitedEmailNoIdentityServer: {
|
case MessageCase.InvitedEmailNoIdentityServer: {
|
||||||
title = _t(
|
if (roomName) {
|
||||||
"This invite to %(roomName)s was sent to %(email)s",
|
title = _t(
|
||||||
{
|
"This invite to %(roomName)s was sent to %(email)s",
|
||||||
roomName: this.roomName(),
|
{
|
||||||
email: this.props.invitedEmail,
|
roomName,
|
||||||
},
|
email: this.props.invitedEmail,
|
||||||
);
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
title = _t("This invite was sent to %(email)s", { email: this.props.invitedEmail });
|
||||||
|
}
|
||||||
|
|
||||||
subTitle = _t(
|
subTitle = _t(
|
||||||
"Use an identity server in Settings to receive invites directly in %(brand)s.",
|
"Use an identity server in Settings to receive invites directly in %(brand)s.",
|
||||||
{ brand },
|
{ brand },
|
||||||
|
@ -426,13 +454,18 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MessageCase.InvitedEmailMismatch: {
|
case MessageCase.InvitedEmailMismatch: {
|
||||||
title = _t(
|
if (roomName) {
|
||||||
"This invite to %(roomName)s was sent to %(email)s",
|
title = _t(
|
||||||
{
|
"This invite to %(roomName)s was sent to %(email)s",
|
||||||
roomName: this.roomName(),
|
{
|
||||||
email: this.props.invitedEmail,
|
roomName,
|
||||||
},
|
email: this.props.invitedEmail,
|
||||||
);
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
title = _t("This invite was sent to %(email)s", { email: this.props.invitedEmail });
|
||||||
|
}
|
||||||
|
|
||||||
subTitle = _t(
|
subTitle = _t(
|
||||||
"Share this email in Settings to receive invites directly in %(brand)s.",
|
"Share this email in Settings to receive invites directly in %(brand)s.",
|
||||||
{ brand },
|
{ brand },
|
||||||
|
@ -458,16 +491,14 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
const isDM = this.isDMInvite();
|
const isDM = this.isDMInvite();
|
||||||
if (isDM) {
|
if (isDM) {
|
||||||
title = _t("Do you want to chat with %(user)s?",
|
title = _t("Do you want to chat with %(user)s?", { user: inviteMember.name });
|
||||||
{ user: inviteMember.name });
|
|
||||||
subTitle = [
|
subTitle = [
|
||||||
avatar,
|
avatar,
|
||||||
_t("<userName/> wants to chat", {}, { userName: () => inviterElement }),
|
_t("<userName/> wants to chat", {}, { userName: () => inviterElement }),
|
||||||
];
|
];
|
||||||
primaryActionLabel = _t("Start chatting");
|
primaryActionLabel = _t("Start chatting");
|
||||||
} else {
|
} else {
|
||||||
title = _t("Do you want to join %(roomName)s?",
|
title = _t("Do you want to join %(roomName)s?", { roomName });
|
||||||
{ roomName: this.roomName() });
|
|
||||||
subTitle = [
|
subTitle = [
|
||||||
avatar,
|
avatar,
|
||||||
_t("<userName/> invited you", {}, { userName: () => inviterElement }),
|
_t("<userName/> invited you", {}, { userName: () => inviterElement }),
|
||||||
|
@ -500,27 +531,35 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
case MessageCase.ViewingRoom: {
|
case MessageCase.ViewingRoom: {
|
||||||
if (this.props.canPreview) {
|
if (this.props.canPreview) {
|
||||||
title = _t("You're previewing %(roomName)s. Want to join it?",
|
title = _t("You're previewing %(roomName)s. Want to join it?", { roomName });
|
||||||
{ roomName: this.roomName() });
|
} else if (roomName) {
|
||||||
|
title = _t("%(roomName)s can't be previewed. Do you want to join it?", { roomName });
|
||||||
} else {
|
} else {
|
||||||
title = _t("%(roomName)s can't be previewed. Do you want to join it?",
|
title = _t("There's no preview, would you like to join?");
|
||||||
{ roomName: this.roomName(true) });
|
|
||||||
}
|
}
|
||||||
primaryActionLabel = _t("Join the discussion");
|
primaryActionLabel = _t("Join the discussion");
|
||||||
primaryActionHandler = this.props.onJoinClick;
|
primaryActionHandler = this.props.onJoinClick;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MessageCase.RoomNotFound: {
|
case MessageCase.RoomNotFound: {
|
||||||
title = _t("%(roomName)s does not exist.", { roomName: this.roomName(true) });
|
if (roomName) {
|
||||||
subTitle = _t("This room doesn't exist. Are you sure you're at the right place?");
|
title = _t("%(roomName)s does not exist.", { roomName });
|
||||||
|
} else {
|
||||||
|
title = _t("This room or space does not exist.");
|
||||||
|
}
|
||||||
|
subTitle = _t("Are you sure you're at the right place?");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MessageCase.OtherError: {
|
case MessageCase.OtherError: {
|
||||||
title = _t("%(roomName)s is not accessible at this time.", { roomName: this.roomName(true) });
|
if (roomName) {
|
||||||
|
title = _t("%(roomName)s is not accessible at this time.", { roomName });
|
||||||
|
} else {
|
||||||
|
title = _t("This room or space is not accessible at this time.");
|
||||||
|
}
|
||||||
subTitle = [
|
subTitle = [
|
||||||
_t("Try again later, or ask a room admin to check if you have access."),
|
_t("Try again later, or ask a room or space admin to check if you have access."),
|
||||||
_t(
|
_t(
|
||||||
"%(errcode)s was returned while trying to access the room. " +
|
"%(errcode)s was returned while trying to access the room or space. " +
|
||||||
"If you think you're seeing this message in error, please " +
|
"If you think you're seeing this message in error, please " +
|
||||||
"<issueLink>submit a bug report</issueLink>.",
|
"<issueLink>submit a bug report</issueLink>.",
|
||||||
{ errcode: this.props.error.errcode },
|
{ errcode: this.props.error.errcode },
|
||||||
|
|
|
@ -59,14 +59,12 @@ import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||||
import { PosthogAnalytics } from "../../../PosthogAnalytics";
|
import { PosthogAnalytics } from "../../../PosthogAnalytics";
|
||||||
import { addReplyToMessageContent } from '../../../utils/Reply';
|
import { addReplyToMessageContent } from '../../../utils/Reply';
|
||||||
|
|
||||||
export function attachRelation(
|
// Merges favouring the given relation
|
||||||
content: IContent,
|
export function attachRelation(content: IContent, relation?: IEventRelation): void {
|
||||||
relation?: IEventRelation,
|
|
||||||
): void {
|
|
||||||
if (relation) {
|
if (relation) {
|
||||||
content['m.relates_to'] = {
|
content['m.relates_to'] = {
|
||||||
...relation, // the composer can have a default
|
...(content['m.relates_to'] || {}),
|
||||||
...content['m.relates_to'],
|
...relation,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -99,6 +97,7 @@ export function createMessageContent(
|
||||||
content.formatted_body = formattedBody;
|
content.formatted_body = formattedBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
attachRelation(content, relation);
|
||||||
if (replyToEvent) {
|
if (replyToEvent) {
|
||||||
addReplyToMessageContent(content, replyToEvent, {
|
addReplyToMessageContent(content, replyToEvent, {
|
||||||
permalinkCreator,
|
permalinkCreator,
|
||||||
|
@ -106,13 +105,6 @@ export function createMessageContent(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (relation) {
|
|
||||||
content['m.relates_to'] = {
|
|
||||||
...relation,
|
|
||||||
...content['m.relates_to'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -90,17 +90,16 @@ export const ThreadMessagePreview = ({ thread, showDisplayname = false }: IPrevi
|
||||||
}, [lastReply, replacingEventId]);
|
}, [lastReply, replacingEventId]);
|
||||||
if (!preview) return null;
|
if (!preview) return null;
|
||||||
|
|
||||||
const sender = thread.roomState.getSentinelMember(lastReply.getSender());
|
|
||||||
return <>
|
return <>
|
||||||
<MemberAvatar
|
<MemberAvatar
|
||||||
member={sender}
|
member={lastReply.sender}
|
||||||
fallbackUserId={lastReply.getSender()}
|
fallbackUserId={lastReply.getSender()}
|
||||||
width={24}
|
width={24}
|
||||||
height={24}
|
height={24}
|
||||||
className="mx_ThreadInfo_avatar"
|
className="mx_ThreadInfo_avatar"
|
||||||
/>
|
/>
|
||||||
{ showDisplayname && <div className="mx_ThreadInfo_sender">
|
{ showDisplayname && <div className="mx_ThreadInfo_sender">
|
||||||
{ sender?.name ?? lastReply.getSender() }
|
{ lastReply.sender?.name ?? lastReply.getSender() }
|
||||||
</div> }
|
</div> }
|
||||||
<div className="mx_ThreadInfo_content">
|
<div className="mx_ThreadInfo_content">
|
||||||
<span className="mx_ThreadInfo_message-preview">
|
<span className="mx_ThreadInfo_message-preview">
|
||||||
|
|
|
@ -231,7 +231,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
if (!this.state.recorder) return null; // no recorder means we're not recording: no waveform
|
if (!this.state.recorder) return null; // no recorder means we're not recording: no waveform
|
||||||
|
|
||||||
if (this.state.recordingPhase !== RecordingState.Started) {
|
if (this.state.recordingPhase !== RecordingState.Started) {
|
||||||
return <RecordingPlayback playback={this.state.recorder.getPlayback()} />;
|
return <RecordingPlayback playback={this.state.recorder.getPlayback()} withWaveform={true} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// only other UI is the recording-in-progress UI
|
// only other UI is the recording-in-progress UI
|
||||||
|
|
|
@ -97,6 +97,7 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
|
||||||
render() {
|
render() {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const room = client.getRoom(this.props.roomId);
|
const room = client.getRoom(this.props.roomId);
|
||||||
|
const isSpace = room.isSpaceRoom();
|
||||||
|
|
||||||
let unfederatableSection;
|
let unfederatableSection;
|
||||||
const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, '');
|
const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, '');
|
||||||
|
@ -120,7 +121,9 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
|
||||||
) }
|
) }
|
||||||
</p>
|
</p>
|
||||||
<AccessibleButton onClick={this.upgradeRoom} kind='primary'>
|
<AccessibleButton onClick={this.upgradeRoom} kind='primary'>
|
||||||
{ _t("Upgrade this room to the recommended room version") }
|
{ isSpace
|
||||||
|
? _t("Upgrade this space to the recommended room version")
|
||||||
|
: _t("Upgrade this room to the recommended room version") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -128,12 +131,16 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
|
||||||
|
|
||||||
let oldRoomLink;
|
let oldRoomLink;
|
||||||
if (this.state.oldRoomId) {
|
if (this.state.oldRoomId) {
|
||||||
let name = _t("this room");
|
let copy: string;
|
||||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
if (isSpace) {
|
||||||
if (room && room.name) name = room.name;
|
copy = _t("View older version of %(spaceName)s.", { spaceName: room.name });
|
||||||
|
} else {
|
||||||
|
copy = _t("View older messages in %(roomName)s.", { roomName: room.name });
|
||||||
|
}
|
||||||
|
|
||||||
oldRoomLink = (
|
oldRoomLink = (
|
||||||
<AccessibleButton element='a' onClick={this.onOldRoomClicked}>
|
<AccessibleButton element='a' onClick={this.onOldRoomClicked}>
|
||||||
{ _t("View older messages in %(roomName)s.", { roomName: name }) }
|
{ copy }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -218,7 +218,7 @@ function parseNode(n: Node, pc: PartCreator, mkListItem?: (li: Node) => Part[]):
|
||||||
return parts;
|
return parts;
|
||||||
}
|
}
|
||||||
case "OL": {
|
case "OL": {
|
||||||
let counter = 1;
|
let counter = (n as HTMLOListElement).start ?? 1;
|
||||||
const parts = parseChildren(n, pc, li => {
|
const parts = parseChildren(n, pc, li => {
|
||||||
const parts = [pc.plain(`${counter}. `), ...parseChildren(li, pc)];
|
const parts = [pc.plain(`${counter}. `), ...parseChildren(li, pc)];
|
||||||
counter++;
|
counter++;
|
||||||
|
|
|
@ -384,7 +384,7 @@
|
||||||
"Custom (%(level)s)": "Custom (%(level)s)",
|
"Custom (%(level)s)": "Custom (%(level)s)",
|
||||||
"Failed to invite": "Failed to invite",
|
"Failed to invite": "Failed to invite",
|
||||||
"Operation failed": "Operation failed",
|
"Operation failed": "Operation failed",
|
||||||
"Failed to invite users to the room:": "Failed to invite users to the room:",
|
"Failed to invite users to %(roomName)s": "Failed to invite users to %(roomName)s",
|
||||||
"We sent the others, but the below people couldn't be invited to <RoomName/>": "We sent the others, but the below people couldn't be invited to <RoomName/>",
|
"We sent the others, but the below people couldn't be invited to <RoomName/>": "We sent the others, but the below people couldn't be invited to <RoomName/>",
|
||||||
"Some invites couldn't be sent": "Some invites couldn't be sent",
|
"Some invites couldn't be sent": "Some invites couldn't be sent",
|
||||||
"You need to be logged in.": "You need to be logged in.",
|
"You need to be logged in.": "You need to be logged in.",
|
||||||
|
@ -694,12 +694,16 @@
|
||||||
"Not a valid %(brand)s keyfile": "Not a valid %(brand)s keyfile",
|
"Not a valid %(brand)s keyfile": "Not a valid %(brand)s keyfile",
|
||||||
"Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?",
|
"Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?",
|
||||||
"Unrecognised address": "Unrecognised address",
|
"Unrecognised address": "Unrecognised address",
|
||||||
|
"You do not have permission to invite people to this space.": "You do not have permission to invite people to this space.",
|
||||||
"You do not have permission to invite people to this room.": "You do not have permission to invite people to this room.",
|
"You do not have permission to invite people to this room.": "You do not have permission to invite people to this room.",
|
||||||
"User %(userId)s is already invited to the room": "User %(userId)s is already invited to the room",
|
"User is already invited to the space": "User is already invited to the space",
|
||||||
"User %(userId)s is already in the room": "User %(userId)s is already in the room",
|
"User is already invited to the room": "User is already invited to the room",
|
||||||
"User %(user_id)s does not exist": "User %(user_id)s does not exist",
|
"User is already in the space": "User is already in the space",
|
||||||
"User %(user_id)s may or may not exist": "User %(user_id)s may or may not exist",
|
"User is already in the room": "User is already in the room",
|
||||||
|
"User does not exist": "User does not exist",
|
||||||
|
"User may or may not exist": "User may or may not exist",
|
||||||
"The user must be unbanned before they can be invited.": "The user must be unbanned before they can be invited.",
|
"The user must be unbanned before they can be invited.": "The user must be unbanned before they can be invited.",
|
||||||
|
"The user's homeserver does not support the version of the space.": "The user's homeserver does not support the version of the space.",
|
||||||
"The user's homeserver does not support the version of the room.": "The user's homeserver does not support the version of the room.",
|
"The user's homeserver does not support the version of the room.": "The user's homeserver does not support the version of the room.",
|
||||||
"Unknown server error": "Unknown server error",
|
"Unknown server error": "Unknown server error",
|
||||||
"Use a few words, avoid common phrases": "Use a few words, avoid common phrases",
|
"Use a few words, avoid common phrases": "Use a few words, avoid common phrases",
|
||||||
|
@ -815,12 +819,12 @@
|
||||||
"Update %(brand)s": "Update %(brand)s",
|
"Update %(brand)s": "Update %(brand)s",
|
||||||
"New version of %(brand)s is available": "New version of %(brand)s is available",
|
"New version of %(brand)s is available": "New version of %(brand)s is available",
|
||||||
"Guest": "Guest",
|
"Guest": "Guest",
|
||||||
"There was an error joining the room": "There was an error joining the room",
|
"There was an error joining.": "There was an error joining.",
|
||||||
"Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.",
|
"Sorry, your homeserver is too old to participate here.": "Sorry, your homeserver is too old to participate here.",
|
||||||
"Please contact your homeserver administrator.": "Please contact your homeserver administrator.",
|
"Please contact your homeserver administrator.": "Please contact your homeserver administrator.",
|
||||||
"The person who invited you already left the room.": "The person who invited you already left the room.",
|
"The person who invited you has already left.": "The person who invited you has already left.",
|
||||||
"The person who invited you already left the room, or their server is offline.": "The person who invited you already left the room, or their server is offline.",
|
"The person who invited you has already left, or their server is offline.": "The person who invited you has already left, or their server is offline.",
|
||||||
"Failed to join room": "Failed to join room",
|
"Failed to join": "Failed to join",
|
||||||
"All rooms": "All rooms",
|
"All rooms": "All rooms",
|
||||||
"Home": "Home",
|
"Home": "Home",
|
||||||
"Favourites": "Favourites",
|
"Favourites": "Favourites",
|
||||||
|
@ -889,7 +893,7 @@
|
||||||
"Jump to date (adds /jumptodate and jump to date headers)": "Jump to date (adds /jumptodate and jump to date headers)",
|
"Jump to date (adds /jumptodate and jump to date headers)": "Jump to date (adds /jumptodate and jump to date headers)",
|
||||||
"Don't send read receipts": "Don't send read receipts",
|
"Don't send read receipts": "Don't send read receipts",
|
||||||
"Location sharing - pin drop (under active development)": "Location sharing - pin drop (under active development)",
|
"Location sharing - pin drop (under active development)": "Location sharing - pin drop (under active development)",
|
||||||
"Location sharing - share your current location with live updates (under active development)": "Location sharing - share your current location with live updates (under active development)",
|
"Live location sharing - share current location (active development, and temporarily, locations persist in room history)": "Live location sharing - share current location (active development, and temporarily, locations persist in room history)",
|
||||||
"Font size": "Font size",
|
"Font size": "Font size",
|
||||||
"Use custom size": "Use custom size",
|
"Use custom size": "Use custom size",
|
||||||
"Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing",
|
"Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing",
|
||||||
|
@ -1518,8 +1522,9 @@
|
||||||
"Voice & Video": "Voice & Video",
|
"Voice & Video": "Voice & Video",
|
||||||
"This room is not accessible by remote Matrix servers": "This room is not accessible by remote Matrix servers",
|
"This room is not accessible by remote Matrix servers": "This room is not accessible by remote Matrix servers",
|
||||||
"<b>Warning</b>: Upgrading a room will <i>not automatically migrate room members to the new version of the room.</i> We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "<b>Warning</b>: Upgrading a room will <i>not automatically migrate room members to the new version of the room.</i> We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.",
|
"<b>Warning</b>: Upgrading a room will <i>not automatically migrate room members to the new version of the room.</i> We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "<b>Warning</b>: Upgrading a room will <i>not automatically migrate room members to the new version of the room.</i> We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.",
|
||||||
|
"Upgrade this space to the recommended room version": "Upgrade this space to the recommended room version",
|
||||||
"Upgrade this room to the recommended room version": "Upgrade this room to the recommended room version",
|
"Upgrade this room to the recommended room version": "Upgrade this room to the recommended room version",
|
||||||
"this room": "this room",
|
"View older version of %(spaceName)s.": "View older version of %(spaceName)s.",
|
||||||
"View older messages in %(roomName)s.": "View older messages in %(roomName)s.",
|
"View older messages in %(roomName)s.": "View older messages in %(roomName)s.",
|
||||||
"Space information": "Space information",
|
"Space information": "Space information",
|
||||||
"Internal room ID": "Internal room ID",
|
"Internal room ID": "Internal room ID",
|
||||||
|
@ -1779,29 +1784,35 @@
|
||||||
"Currently removing messages in %(count)s rooms|one": "Currently removing messages in %(count)s room",
|
"Currently removing messages in %(count)s rooms|one": "Currently removing messages in %(count)s room",
|
||||||
"%(spaceName)s menu": "%(spaceName)s menu",
|
"%(spaceName)s menu": "%(spaceName)s menu",
|
||||||
"Home options": "Home options",
|
"Home options": "Home options",
|
||||||
"This room": "This room",
|
|
||||||
"Joining space …": "Joining space …",
|
"Joining space …": "Joining space …",
|
||||||
"Joining room …": "Joining room …",
|
"Joining room …": "Joining room …",
|
||||||
|
"Joining …": "Joining …",
|
||||||
"Loading …": "Loading …",
|
"Loading …": "Loading …",
|
||||||
"Rejecting invite …": "Rejecting invite …",
|
"Rejecting invite …": "Rejecting invite …",
|
||||||
"Join the conversation with an account": "Join the conversation with an account",
|
"Join the conversation with an account": "Join the conversation with an account",
|
||||||
"Sign Up": "Sign Up",
|
"Sign Up": "Sign Up",
|
||||||
"Loading room preview": "Loading room preview",
|
"Loading preview": "Loading preview",
|
||||||
"You were removed from %(roomName)s by %(memberName)s": "You were removed from %(roomName)s by %(memberName)s",
|
"You were removed from %(roomName)s by %(memberName)s": "You were removed from %(roomName)s by %(memberName)s",
|
||||||
|
"You were removed by %(memberName)s": "You were removed by %(memberName)s",
|
||||||
"Reason: %(reason)s": "Reason: %(reason)s",
|
"Reason: %(reason)s": "Reason: %(reason)s",
|
||||||
|
"Forget this space": "Forget this space",
|
||||||
"Forget this room": "Forget this room",
|
"Forget this room": "Forget this room",
|
||||||
"Re-join": "Re-join",
|
"Re-join": "Re-join",
|
||||||
"You were banned from %(roomName)s by %(memberName)s": "You were banned from %(roomName)s by %(memberName)s",
|
"You were banned from %(roomName)s by %(memberName)s": "You were banned from %(roomName)s by %(memberName)s",
|
||||||
|
"You were banned by %(memberName)s": "You were banned by %(memberName)s",
|
||||||
"Something went wrong with your invite to %(roomName)s": "Something went wrong with your invite to %(roomName)s",
|
"Something went wrong with your invite to %(roomName)s": "Something went wrong with your invite to %(roomName)s",
|
||||||
"An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to a room admin.": "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to a room admin.",
|
"Something went wrong with your invite.": "Something went wrong with your invite.",
|
||||||
|
"An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to the person who invited you.": "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to the person who invited you.",
|
||||||
"unknown error code": "unknown error code",
|
"unknown error code": "unknown error code",
|
||||||
"You can only join it with a working invite.": "You can only join it with a working invite.",
|
"You can only join it with a working invite.": "You can only join it with a working invite.",
|
||||||
"Try to join anyway": "Try to join anyway",
|
"Try to join anyway": "Try to join anyway",
|
||||||
"You can still join it because this is a public room.": "You can still join it because this is a public room.",
|
"You can still join here.": "You can still join here.",
|
||||||
"Join the discussion": "Join the discussion",
|
"Join the discussion": "Join the discussion",
|
||||||
"This invite to %(roomName)s was sent to %(email)s which is not associated with your account": "This invite to %(roomName)s was sent to %(email)s which is not associated with your account",
|
"This invite to %(roomName)s was sent to %(email)s which is not associated with your account": "This invite to %(roomName)s was sent to %(email)s which is not associated with your account",
|
||||||
|
"This invite was sent to %(email)s which is not associated with your account": "This invite was sent to %(email)s which is not associated with your account",
|
||||||
"Link this email with your account in Settings to receive invites directly in %(brand)s.": "Link this email with your account in Settings to receive invites directly in %(brand)s.",
|
"Link this email with your account in Settings to receive invites directly in %(brand)s.": "Link this email with your account in Settings to receive invites directly in %(brand)s.",
|
||||||
"This invite to %(roomName)s was sent to %(email)s": "This invite to %(roomName)s was sent to %(email)s",
|
"This invite to %(roomName)s was sent to %(email)s": "This invite to %(roomName)s was sent to %(email)s",
|
||||||
|
"This invite was sent to %(email)s": "This invite was sent to %(email)s",
|
||||||
"Use an identity server in Settings to receive invites directly in %(brand)s.": "Use an identity server in Settings to receive invites directly in %(brand)s.",
|
"Use an identity server in Settings to receive invites directly in %(brand)s.": "Use an identity server in Settings to receive invites directly in %(brand)s.",
|
||||||
"Share this email in Settings to receive invites directly in %(brand)s.": "Share this email in Settings to receive invites directly in %(brand)s.",
|
"Share this email in Settings to receive invites directly in %(brand)s.": "Share this email in Settings to receive invites directly in %(brand)s.",
|
||||||
"Do you want to chat with %(user)s?": "Do you want to chat with %(user)s?",
|
"Do you want to chat with %(user)s?": "Do you want to chat with %(user)s?",
|
||||||
|
@ -1813,11 +1824,14 @@
|
||||||
"Reject & Ignore user": "Reject & Ignore user",
|
"Reject & Ignore user": "Reject & Ignore user",
|
||||||
"You're previewing %(roomName)s. Want to join it?": "You're previewing %(roomName)s. Want to join it?",
|
"You're previewing %(roomName)s. Want to join it?": "You're previewing %(roomName)s. Want to join it?",
|
||||||
"%(roomName)s can't be previewed. Do you want to join it?": "%(roomName)s can't be previewed. Do you want to join it?",
|
"%(roomName)s can't be previewed. Do you want to join it?": "%(roomName)s can't be previewed. Do you want to join it?",
|
||||||
|
"There's no preview, would you like to join?": "There's no preview, would you like to join?",
|
||||||
"%(roomName)s does not exist.": "%(roomName)s does not exist.",
|
"%(roomName)s does not exist.": "%(roomName)s does not exist.",
|
||||||
"This room doesn't exist. Are you sure you're at the right place?": "This room doesn't exist. Are you sure you're at the right place?",
|
"This room or space does not exist.": "This room or space does not exist.",
|
||||||
|
"Are you sure you're at the right place?": "Are you sure you're at the right place?",
|
||||||
"%(roomName)s is not accessible at this time.": "%(roomName)s is not accessible at this time.",
|
"%(roomName)s is not accessible at this time.": "%(roomName)s is not accessible at this time.",
|
||||||
"Try again later, or ask a room admin to check if you have access.": "Try again later, or ask a room admin to check if you have access.",
|
"This room or space is not accessible at this time.": "This room or space is not accessible at this time.",
|
||||||
"%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please <issueLink>submit a bug report</issueLink>.": "%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please <issueLink>submit a bug report</issueLink>.",
|
"Try again later, or ask a room or space admin to check if you have access.": "Try again later, or ask a room or space admin to check if you have access.",
|
||||||
|
"%(errcode)s was returned while trying to access the room or space. If you think you're seeing this message in error, please <issueLink>submit a bug report</issueLink>.": "%(errcode)s was returned while trying to access the room or space. If you think you're seeing this message in error, please <issueLink>submit a bug report</issueLink>.",
|
||||||
"Appearance": "Appearance",
|
"Appearance": "Appearance",
|
||||||
"Show rooms with unread messages first": "Show rooms with unread messages first",
|
"Show rooms with unread messages first": "Show rooms with unread messages first",
|
||||||
"Show previews of messages": "Show previews of messages",
|
"Show previews of messages": "Show previews of messages",
|
||||||
|
@ -2882,9 +2896,13 @@
|
||||||
"Beta": "Beta",
|
"Beta": "Beta",
|
||||||
"Leave the beta": "Leave the beta",
|
"Leave the beta": "Leave the beta",
|
||||||
"Join the beta": "Join 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",
|
"You are sharing your live location": "You are sharing your live location",
|
||||||
"%(timeRemaining)s left": "%(timeRemaining)s left",
|
"%(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",
|
||||||
|
"An error occurred while stopping your live location, please try again": "An error occurred while stopping your live location, please try again",
|
||||||
"Stop sharing": "Stop sharing",
|
"Stop sharing": "Stop sharing",
|
||||||
|
"Stop sharing and close": "Stop sharing and close",
|
||||||
"Avatar": "Avatar",
|
"Avatar": "Avatar",
|
||||||
"This room is public": "This room is public",
|
"This room is public": "This room is public",
|
||||||
"Away": "Away",
|
"Away": "Away",
|
||||||
|
|
|
@ -400,7 +400,10 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
isFeature: true,
|
isFeature: true,
|
||||||
labsGroup: LabGroup.Messaging,
|
labsGroup: LabGroup.Messaging,
|
||||||
supportedLevels: LEVELS_FEATURE,
|
supportedLevels: LEVELS_FEATURE,
|
||||||
displayName: _td("Location sharing - share your current location with live updates (under active development)"),
|
displayName: _td(
|
||||||
|
`Live location sharing - share current location ` +
|
||||||
|
`(active development, and temporarily, locations persist in room history)`,
|
||||||
|
),
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
"baseFontSize": {
|
"baseFontSize": {
|
||||||
|
|
|
@ -20,6 +20,9 @@ import {
|
||||||
BeaconEvent,
|
BeaconEvent,
|
||||||
MatrixEvent,
|
MatrixEvent,
|
||||||
Room,
|
Room,
|
||||||
|
RoomMember,
|
||||||
|
RoomState,
|
||||||
|
RoomStateEvent,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import {
|
import {
|
||||||
BeaconInfoState, makeBeaconContent, makeBeaconInfoContent,
|
BeaconInfoState, makeBeaconContent, makeBeaconInfoContent,
|
||||||
|
@ -35,6 +38,7 @@ import {
|
||||||
ClearWatchCallback,
|
ClearWatchCallback,
|
||||||
GeolocationError,
|
GeolocationError,
|
||||||
mapGeolocationPositionToTimedGeo,
|
mapGeolocationPositionToTimedGeo,
|
||||||
|
sortBeaconsByLatestCreation,
|
||||||
TimedGeoUri,
|
TimedGeoUri,
|
||||||
watchPosition,
|
watchPosition,
|
||||||
} from "../utils/beacon";
|
} from "../utils/beacon";
|
||||||
|
@ -45,13 +49,17 @@ const isOwnBeacon = (beacon: Beacon, userId: string): boolean => beacon.beaconIn
|
||||||
export enum OwnBeaconStoreEvent {
|
export enum OwnBeaconStoreEvent {
|
||||||
LivenessChange = 'OwnBeaconStore.LivenessChange',
|
LivenessChange = 'OwnBeaconStore.LivenessChange',
|
||||||
MonitoringLivePosition = 'OwnBeaconStore.MonitoringLivePosition',
|
MonitoringLivePosition = 'OwnBeaconStore.MonitoringLivePosition',
|
||||||
|
WireError = 'WireError',
|
||||||
}
|
}
|
||||||
|
|
||||||
const MOVING_UPDATE_INTERVAL = 2000;
|
const MOVING_UPDATE_INTERVAL = 2000;
|
||||||
const STATIC_UPDATE_INTERVAL = 30000;
|
const STATIC_UPDATE_INTERVAL = 30000;
|
||||||
|
|
||||||
|
const BAIL_AFTER_CONSECUTIVE_ERROR_COUNT = 2;
|
||||||
|
|
||||||
type OwnBeaconStoreState = {
|
type OwnBeaconStoreState = {
|
||||||
beacons: Map<string, Beacon>;
|
beacons: Map<string, Beacon>;
|
||||||
|
beaconWireErrors: Map<string, Beacon>;
|
||||||
beaconsByRoomId: Map<Room['roomId'], Set<string>>;
|
beaconsByRoomId: Map<Room['roomId'], Set<string>>;
|
||||||
liveBeaconIds: string[];
|
liveBeaconIds: string[];
|
||||||
};
|
};
|
||||||
|
@ -60,6 +68,16 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
||||||
// users beacons, keyed by event type
|
// users beacons, keyed by event type
|
||||||
public readonly beacons = new Map<string, Beacon>();
|
public readonly beacons = new Map<string, Beacon>();
|
||||||
public readonly beaconsByRoomId = new Map<Room['roomId'], Set<string>>();
|
public readonly beaconsByRoomId = new Map<Room['roomId'], Set<string>>();
|
||||||
|
/**
|
||||||
|
* Track over the wire errors for published positions
|
||||||
|
* Counts consecutive wire errors per beacon
|
||||||
|
* 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 liveBeaconIds = [];
|
||||||
private locationInterval: number;
|
private locationInterval: number;
|
||||||
private geolocationError: GeolocationError | undefined;
|
private geolocationError: GeolocationError | undefined;
|
||||||
|
@ -90,6 +108,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
||||||
protected async onNotReady() {
|
protected async onNotReady() {
|
||||||
this.matrixClient.removeListener(BeaconEvent.LivenessChange, this.onBeaconLiveness);
|
this.matrixClient.removeListener(BeaconEvent.LivenessChange, this.onBeaconLiveness);
|
||||||
this.matrixClient.removeListener(BeaconEvent.New, this.onNewBeacon);
|
this.matrixClient.removeListener(BeaconEvent.New, this.onNewBeacon);
|
||||||
|
this.matrixClient.removeListener(RoomStateEvent.Members, this.onRoomStateMembers);
|
||||||
|
|
||||||
this.beacons.forEach(beacon => beacon.destroy());
|
this.beacons.forEach(beacon => beacon.destroy());
|
||||||
|
|
||||||
|
@ -97,11 +116,13 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
||||||
this.beacons.clear();
|
this.beacons.clear();
|
||||||
this.beaconsByRoomId.clear();
|
this.beaconsByRoomId.clear();
|
||||||
this.liveBeaconIds = [];
|
this.liveBeaconIds = [];
|
||||||
|
this.beaconWireErrorCounts.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async onReady(): Promise<void> {
|
protected async onReady(): Promise<void> {
|
||||||
this.matrixClient.on(BeaconEvent.LivenessChange, this.onBeaconLiveness);
|
this.matrixClient.on(BeaconEvent.LivenessChange, this.onBeaconLiveness);
|
||||||
this.matrixClient.on(BeaconEvent.New, this.onNewBeacon);
|
this.matrixClient.on(BeaconEvent.New, this.onNewBeacon);
|
||||||
|
this.matrixClient.on(RoomStateEvent.Members, this.onRoomStateMembers);
|
||||||
|
|
||||||
this.initialiseBeaconState();
|
this.initialiseBeaconState();
|
||||||
}
|
}
|
||||||
|
@ -110,20 +131,51 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
||||||
// we don't actually do anything here
|
// we don't actually do anything here
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasLiveBeacons(roomId?: string): boolean {
|
public hasLiveBeacons = (roomId?: string): boolean => {
|
||||||
return !!this.getLiveBeaconIds(roomId).length;
|
return !!this.getLiveBeaconIds(roomId).length;
|
||||||
}
|
};
|
||||||
|
|
||||||
public getLiveBeaconIds(roomId?: string): string[] {
|
/**
|
||||||
|
* Some live beacon has a wire error
|
||||||
|
* Optionally filter by room
|
||||||
|
*/
|
||||||
|
public hasWireErrors = (roomId?: string): boolean => {
|
||||||
|
return this.getLiveBeaconIds(roomId).some(this.beaconHasWireError);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a beacon has failed to publish position
|
||||||
|
* past the allowed consecutive failure count (BAIL_AFTER_CONSECUTIVE_ERROR_COUNT)
|
||||||
|
* Then consider it to have an error
|
||||||
|
*/
|
||||||
|
public beaconHasWireError = (beaconId: string): boolean => {
|
||||||
|
return this.beaconWireErrorCounts.get(beaconId) >= BAIL_AFTER_CONSECUTIVE_ERROR_COUNT;
|
||||||
|
};
|
||||||
|
|
||||||
|
public resetWireError = (beaconId: string): void => {
|
||||||
|
this.incrementBeaconWireErrorCount(beaconId, false);
|
||||||
|
|
||||||
|
// always publish to all live beacons together
|
||||||
|
// instead of just one that was changed
|
||||||
|
// to keep lastPublishedTimestamp simple
|
||||||
|
// and extra published locations don't hurt
|
||||||
|
this.publishCurrentLocationToBeacons();
|
||||||
|
};
|
||||||
|
|
||||||
|
public getLiveBeaconIds = (roomId?: string): string[] => {
|
||||||
if (!roomId) {
|
if (!roomId) {
|
||||||
return this.liveBeaconIds;
|
return this.liveBeaconIds;
|
||||||
}
|
}
|
||||||
return this.liveBeaconIds.filter(beaconId => this.beaconsByRoomId.get(roomId)?.has(beaconId));
|
return this.liveBeaconIds.filter(beaconId => this.beaconsByRoomId.get(roomId)?.has(beaconId));
|
||||||
}
|
};
|
||||||
|
|
||||||
public getBeaconById(beaconId: string): Beacon | undefined {
|
public getLiveBeaconIdsWithWireError = (roomId?: string): string[] => {
|
||||||
|
return this.getLiveBeaconIds(roomId).filter(this.beaconHasWireError);
|
||||||
|
};
|
||||||
|
|
||||||
|
public getBeaconById = (beaconId: string): Beacon | undefined => {
|
||||||
return this.beacons.get(beaconId);
|
return this.beacons.get(beaconId);
|
||||||
}
|
};
|
||||||
|
|
||||||
public stopBeacon = async (beaconInfoType: string): Promise<void> => {
|
public stopBeacon = async (beaconInfoType: string): Promise<void> => {
|
||||||
const beacon = this.beacons.get(beaconInfoType);
|
const beacon = this.beacons.get(beaconInfoType);
|
||||||
|
@ -136,6 +188,10 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
||||||
return await this.updateBeaconEvent(beacon, { live: false });
|
return await this.updateBeaconEvent(beacon, { live: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listeners
|
||||||
|
*/
|
||||||
|
|
||||||
private onNewBeacon = (_event: MatrixEvent, beacon: Beacon): void => {
|
private onNewBeacon = (_event: MatrixEvent, beacon: Beacon): void => {
|
||||||
if (!isOwnBeacon(beacon, this.matrixClient.getUserId())) {
|
if (!isOwnBeacon(beacon, this.matrixClient.getUserId())) {
|
||||||
return;
|
return;
|
||||||
|
@ -160,6 +216,40 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
||||||
this.emit(OwnBeaconStoreEvent.LivenessChange, this.getLiveBeaconIds());
|
this.emit(OwnBeaconStoreEvent.LivenessChange, this.getLiveBeaconIds());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for changes in membership in rooms with beacons
|
||||||
|
* and stop monitoring beacons in rooms user is no longer member of
|
||||||
|
*/
|
||||||
|
private onRoomStateMembers = (_event: MatrixEvent, roomState: RoomState, member: RoomMember): void => {
|
||||||
|
// no beacons for this room, ignore
|
||||||
|
if (
|
||||||
|
!this.beaconsByRoomId.has(roomState.roomId) ||
|
||||||
|
member.userId !== this.matrixClient.getUserId()
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO check powerlevels here
|
||||||
|
// in PSF-797
|
||||||
|
|
||||||
|
// stop watching beacons in rooms where user is no longer a member
|
||||||
|
if (member.membership === 'leave' || member.membership === 'ban') {
|
||||||
|
this.beaconsByRoomId.get(roomState.roomId).forEach(this.removeBeacon);
|
||||||
|
this.beaconsByRoomId.delete(roomState.roomId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State management
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Live beacon ids that do not have wire errors
|
||||||
|
*/
|
||||||
|
private get healthyLiveBeaconIds() {
|
||||||
|
return this.liveBeaconIds.filter(beaconId => !this.beaconHasWireError(beaconId));
|
||||||
|
}
|
||||||
|
|
||||||
private initialiseBeaconState = () => {
|
private initialiseBeaconState = () => {
|
||||||
const userId = this.matrixClient.getUserId();
|
const userId = this.matrixClient.getUserId();
|
||||||
const visibleRooms = this.matrixClient.getVisibleRooms();
|
const visibleRooms = this.matrixClient.getVisibleRooms();
|
||||||
|
@ -187,10 +277,26 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
||||||
beacon.monitorLiveness();
|
beacon.monitorLiveness();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove listeners for a given beacon
|
||||||
|
* remove from state
|
||||||
|
* and update liveness if changed
|
||||||
|
*/
|
||||||
|
private removeBeacon = (beaconId: string): void => {
|
||||||
|
if (!this.beacons.has(beaconId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.beacons.get(beaconId).destroy();
|
||||||
|
this.beacons.delete(beaconId);
|
||||||
|
|
||||||
|
this.checkLiveness();
|
||||||
|
};
|
||||||
|
|
||||||
private checkLiveness = (): void => {
|
private checkLiveness = (): void => {
|
||||||
const prevLiveBeaconIds = this.getLiveBeaconIds();
|
const prevLiveBeaconIds = this.getLiveBeaconIds();
|
||||||
this.liveBeaconIds = [...this.beacons.values()]
|
this.liveBeaconIds = [...this.beacons.values()]
|
||||||
.filter(beacon => beacon.isLive)
|
.filter(beacon => beacon.isLive)
|
||||||
|
.sort(sortBeaconsByLatestCreation)
|
||||||
.map(beacon => beacon.identifier);
|
.map(beacon => beacon.identifier);
|
||||||
|
|
||||||
const diff = arrayDiff(prevLiveBeaconIds, this.liveBeaconIds);
|
const diff = arrayDiff(prevLiveBeaconIds, this.liveBeaconIds);
|
||||||
|
@ -218,20 +324,9 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private updateBeaconEvent = async (beacon: Beacon, update: Partial<BeaconInfoState>): Promise<void> => {
|
/**
|
||||||
const { description, timeout, timestamp, live, assetType } = {
|
* Geolocation
|
||||||
...beacon.beaconInfo,
|
*/
|
||||||
...update,
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateContent = makeBeaconInfoContent(timeout,
|
|
||||||
live,
|
|
||||||
description,
|
|
||||||
assetType,
|
|
||||||
timestamp);
|
|
||||||
|
|
||||||
await this.matrixClient.unstable_setLiveBeacon(beacon.roomId, beacon.beaconInfoEventType, updateContent);
|
|
||||||
};
|
|
||||||
|
|
||||||
private togglePollingLocation = () => {
|
private togglePollingLocation = () => {
|
||||||
if (!!this.liveBeaconIds.length) {
|
if (!!this.liveBeaconIds.length) {
|
||||||
|
@ -270,17 +365,6 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
||||||
this.emit(OwnBeaconStoreEvent.MonitoringLivePosition);
|
this.emit(OwnBeaconStoreEvent.MonitoringLivePosition);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onWatchedPosition = (position: GeolocationPosition) => {
|
|
||||||
const timedGeoPosition = mapGeolocationPositionToTimedGeo(position);
|
|
||||||
|
|
||||||
// if this is our first position, publish immediateley
|
|
||||||
if (!this.lastPublishedPositionTimestamp) {
|
|
||||||
this.publishLocationToBeacons(timedGeoPosition);
|
|
||||||
} else {
|
|
||||||
this.debouncedPublishLocationToBeacons(timedGeoPosition);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private stopPollingLocation = () => {
|
private stopPollingLocation = () => {
|
||||||
clearInterval(this.locationInterval);
|
clearInterval(this.locationInterval);
|
||||||
this.locationInterval = undefined;
|
this.locationInterval = undefined;
|
||||||
|
@ -295,40 +379,14 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
||||||
this.emit(OwnBeaconStoreEvent.MonitoringLivePosition);
|
this.emit(OwnBeaconStoreEvent.MonitoringLivePosition);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
private onWatchedPosition = (position: GeolocationPosition) => {
|
||||||
* Sends m.location events to all live beacons
|
const timedGeoPosition = mapGeolocationPositionToTimedGeo(position);
|
||||||
* Sets last published beacon
|
|
||||||
*/
|
|
||||||
private publishLocationToBeacons = async (position: TimedGeoUri) => {
|
|
||||||
this.lastPublishedPositionTimestamp = Date.now();
|
|
||||||
// TODO handle failure in individual beacon without rejecting rest
|
|
||||||
await Promise.all(this.liveBeaconIds.map(beaconId =>
|
|
||||||
this.sendLocationToBeacon(this.beacons.get(beaconId), position)),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
private debouncedPublishLocationToBeacons = debounce(this.publishLocationToBeacons, MOVING_UPDATE_INTERVAL);
|
// if this is our first position, publish immediateley
|
||||||
|
if (!this.lastPublishedPositionTimestamp) {
|
||||||
/**
|
this.publishLocationToBeacons(timedGeoPosition);
|
||||||
* Sends m.location event to referencing given beacon
|
} else {
|
||||||
*/
|
this.debouncedPublishLocationToBeacons(timedGeoPosition);
|
||||||
private sendLocationToBeacon = async (beacon: Beacon, { geoUri, timestamp }: TimedGeoUri) => {
|
|
||||||
const content = makeBeaconContent(geoUri, timestamp, beacon.beaconInfoId);
|
|
||||||
await this.matrixClient.sendEvent(beacon.roomId, M_BEACON.name, content);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current location
|
|
||||||
* (as opposed to using watched location)
|
|
||||||
* and publishes it to all live beacons
|
|
||||||
*/
|
|
||||||
private publishCurrentLocationToBeacons = async () => {
|
|
||||||
try {
|
|
||||||
const position = await getCurrentPosition();
|
|
||||||
// TODO error handling
|
|
||||||
this.publishLocationToBeacons(mapGeolocationPositionToTimedGeo(position));
|
|
||||||
} catch (error) {
|
|
||||||
this.onGeolocationError(error?.message);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -350,4 +408,89 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
||||||
// TODO may need adjustment when PSF-797 is done
|
// TODO may need adjustment when PSF-797 is done
|
||||||
await Promise.all(this.liveBeaconIds.map(this.stopBeacon));
|
await Promise.all(this.liveBeaconIds.map(this.stopBeacon));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current location
|
||||||
|
* (as opposed to using watched location)
|
||||||
|
* and publishes it to all live beacons
|
||||||
|
*/
|
||||||
|
private publishCurrentLocationToBeacons = async () => {
|
||||||
|
try {
|
||||||
|
const position = await getCurrentPosition();
|
||||||
|
this.publishLocationToBeacons(mapGeolocationPositionToTimedGeo(position));
|
||||||
|
} catch (error) {
|
||||||
|
this.onGeolocationError(error?.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MatrixClient api
|
||||||
|
*/
|
||||||
|
|
||||||
|
private updateBeaconEvent = async (beacon: Beacon, update: Partial<BeaconInfoState>): Promise<void> => {
|
||||||
|
const { description, timeout, timestamp, live, assetType } = {
|
||||||
|
...beacon.beaconInfo,
|
||||||
|
...update,
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateContent = makeBeaconInfoContent(timeout,
|
||||||
|
live,
|
||||||
|
description,
|
||||||
|
assetType,
|
||||||
|
timestamp);
|
||||||
|
|
||||||
|
await this.matrixClient.unstable_setLiveBeacon(beacon.roomId, beacon.beaconInfoEventType, updateContent);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends m.location events to all live beacons
|
||||||
|
* Sets last published beacon
|
||||||
|
*/
|
||||||
|
private publishLocationToBeacons = async (position: TimedGeoUri) => {
|
||||||
|
this.lastPublishedPositionTimestamp = Date.now();
|
||||||
|
await Promise.all(this.healthyLiveBeaconIds.map(beaconId =>
|
||||||
|
this.sendLocationToBeacon(this.beacons.get(beaconId), position)),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private debouncedPublishLocationToBeacons = debounce(this.publishLocationToBeacons, MOVING_UPDATE_INTERVAL);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends m.location event to referencing given beacon
|
||||||
|
*/
|
||||||
|
private sendLocationToBeacon = async (beacon: Beacon, { geoUri, timestamp }: TimedGeoUri) => {
|
||||||
|
const content = makeBeaconContent(geoUri, timestamp, beacon.beaconInfoId);
|
||||||
|
try {
|
||||||
|
await this.matrixClient.sendEvent(beacon.roomId, M_BEACON.name, content);
|
||||||
|
this.incrementBeaconWireErrorCount(beacon.identifier, false);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
this.incrementBeaconWireErrorCount(beacon.identifier, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manage beacon wire error count
|
||||||
|
* - clear count for beacon when not error
|
||||||
|
* - increment count for beacon when is error
|
||||||
|
* - emit if beacon error count crossed threshold
|
||||||
|
*/
|
||||||
|
private incrementBeaconWireErrorCount = (beaconId: string, isError: boolean): void => {
|
||||||
|
const hadError = this.beaconHasWireError(beaconId);
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
// increment error count
|
||||||
|
this.beaconWireErrorCounts.set(
|
||||||
|
beaconId,
|
||||||
|
(this.beaconWireErrorCounts.get(beaconId) ?? 0) + 1,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// clear any error count
|
||||||
|
this.beaconWireErrorCounts.delete(beaconId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.beaconHasWireError(beaconId) !== hadError) {
|
||||||
|
this.emit(OwnBeaconStoreEvent.WireError, beaconId);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -428,14 +428,14 @@ export class RoomViewStore extends Store<ActionPayload> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public showJoinRoomError(err: MatrixError, roomId: string) {
|
public showJoinRoomError(err: MatrixError, roomId: string) {
|
||||||
let msg: ReactNode = err.message ? err.message : JSON.stringify(err);
|
let description: ReactNode = err.message ? err.message : JSON.stringify(err);
|
||||||
logger.log("Failed to join room:", msg);
|
logger.log("Failed to join room:", description);
|
||||||
|
|
||||||
if (err.name === "ConnectionError") {
|
if (err.name === "ConnectionError") {
|
||||||
msg = _t("There was an error joining the room");
|
description = _t("There was an error joining.");
|
||||||
} else if (err.errcode === 'M_INCOMPATIBLE_ROOM_VERSION') {
|
} else if (err.errcode === 'M_INCOMPATIBLE_ROOM_VERSION') {
|
||||||
msg = <div>
|
description = <div>
|
||||||
{ _t("Sorry, your homeserver is too old to participate in this room.") }<br />
|
{ _t("Sorry, your homeserver is too old to participate here.") }<br />
|
||||||
{ _t("Please contact your homeserver administrator.") }
|
{ _t("Please contact your homeserver administrator.") }
|
||||||
</div>;
|
</div>;
|
||||||
} else if (err.httpStatus === 404) {
|
} else if (err.httpStatus === 404) {
|
||||||
|
@ -444,16 +444,16 @@ export class RoomViewStore extends Store<ActionPayload> {
|
||||||
if (invitingUserId) {
|
if (invitingUserId) {
|
||||||
// if the inviting user is on the same HS, there can only be one cause: they left.
|
// if the inviting user is on the same HS, there can only be one cause: they left.
|
||||||
if (invitingUserId.endsWith(`:${this.matrixClient.getDomain()}`)) {
|
if (invitingUserId.endsWith(`:${this.matrixClient.getDomain()}`)) {
|
||||||
msg = _t("The person who invited you already left the room.");
|
description = _t("The person who invited you has already left.");
|
||||||
} else {
|
} else {
|
||||||
msg = _t("The person who invited you already left the room, or their server is offline.");
|
description = _t("The person who invited you has already left, or their server is offline.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Modal.createTrackedDialog('Failed to join room', '', ErrorDialog, {
|
Modal.createTrackedDialog('Failed to join room', '', ErrorDialog, {
|
||||||
title: _t("Failed to join room"),
|
title: _t("Failed to join"),
|
||||||
description: msg,
|
description,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,17 +25,21 @@ import { EffectiveMembership, getEffectiveMembership } from "../../utils/members
|
||||||
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
|
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
|
||||||
import * as RoomNotifs from '../../RoomNotifs';
|
import * as RoomNotifs from '../../RoomNotifs';
|
||||||
import * as Unread from '../../Unread';
|
import * as Unread from '../../Unread';
|
||||||
import { NotificationState } from "./NotificationState";
|
import { NotificationState, NotificationStateEvents } from "./NotificationState";
|
||||||
import { getUnsentMessages } from "../../components/structures/RoomStatusBar";
|
import { getUnsentMessages } from "../../components/structures/RoomStatusBar";
|
||||||
|
import { ThreadsRoomNotificationState } from "./ThreadsRoomNotificationState";
|
||||||
|
|
||||||
export class RoomNotificationState extends NotificationState implements IDestroyable {
|
export class RoomNotificationState extends NotificationState implements IDestroyable {
|
||||||
constructor(public readonly room: Room) {
|
constructor(public readonly room: Room, private readonly threadsState?: ThreadsRoomNotificationState) {
|
||||||
super();
|
super();
|
||||||
this.room.on(RoomEvent.Receipt, this.handleReadReceipt);
|
this.room.on(RoomEvent.Receipt, this.handleReadReceipt);
|
||||||
this.room.on(RoomEvent.Timeline, this.handleRoomEventUpdate);
|
this.room.on(RoomEvent.Timeline, this.handleRoomEventUpdate);
|
||||||
this.room.on(RoomEvent.Redaction, this.handleRoomEventUpdate);
|
this.room.on(RoomEvent.Redaction, this.handleRoomEventUpdate);
|
||||||
this.room.on(RoomEvent.MyMembership, this.handleMembershipUpdate);
|
this.room.on(RoomEvent.MyMembership, this.handleMembershipUpdate);
|
||||||
this.room.on(RoomEvent.LocalEchoUpdated, this.handleLocalEchoUpdated);
|
this.room.on(RoomEvent.LocalEchoUpdated, this.handleLocalEchoUpdated);
|
||||||
|
if (threadsState) {
|
||||||
|
threadsState.on(NotificationStateEvents.Update, this.handleThreadsUpdate);
|
||||||
|
}
|
||||||
MatrixClientPeg.get().on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
MatrixClientPeg.get().on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
||||||
MatrixClientPeg.get().on(ClientEvent.AccountData, this.handleAccountDataUpdate);
|
MatrixClientPeg.get().on(ClientEvent.AccountData, this.handleAccountDataUpdate);
|
||||||
this.updateNotificationState();
|
this.updateNotificationState();
|
||||||
|
@ -52,12 +56,19 @@ export class RoomNotificationState extends NotificationState implements IDestroy
|
||||||
this.room.removeListener(RoomEvent.Redaction, this.handleRoomEventUpdate);
|
this.room.removeListener(RoomEvent.Redaction, this.handleRoomEventUpdate);
|
||||||
this.room.removeListener(RoomEvent.MyMembership, this.handleMembershipUpdate);
|
this.room.removeListener(RoomEvent.MyMembership, this.handleMembershipUpdate);
|
||||||
this.room.removeListener(RoomEvent.LocalEchoUpdated, this.handleLocalEchoUpdated);
|
this.room.removeListener(RoomEvent.LocalEchoUpdated, this.handleLocalEchoUpdated);
|
||||||
|
if (this.threadsState) {
|
||||||
|
this.threadsState.removeListener(NotificationStateEvents.Update, this.handleThreadsUpdate);
|
||||||
|
}
|
||||||
if (MatrixClientPeg.get()) {
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
MatrixClientPeg.get().removeListener(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
||||||
MatrixClientPeg.get().removeListener(ClientEvent.AccountData, this.handleAccountDataUpdate);
|
MatrixClientPeg.get().removeListener(ClientEvent.AccountData, this.handleAccountDataUpdate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleThreadsUpdate = () => {
|
||||||
|
this.updateNotificationState();
|
||||||
|
};
|
||||||
|
|
||||||
private handleLocalEchoUpdated = () => {
|
private handleLocalEchoUpdated = () => {
|
||||||
this.updateNotificationState();
|
this.updateNotificationState();
|
||||||
};
|
};
|
||||||
|
|
|
@ -82,12 +82,13 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
|
||||||
*/
|
*/
|
||||||
public getRoomState(room: Room): RoomNotificationState {
|
public getRoomState(room: Room): RoomNotificationState {
|
||||||
if (!this.roomMap.has(room)) {
|
if (!this.roomMap.has(room)) {
|
||||||
this.roomMap.set(room, new RoomNotificationState(room));
|
|
||||||
// Not very elegant, but that way we ensure that we start tracking
|
// Not very elegant, but that way we ensure that we start tracking
|
||||||
// threads notification at the same time at rooms.
|
// threads notification at the same time at rooms.
|
||||||
// There are multiple entry points, and it's unclear which one gets
|
// There are multiple entry points, and it's unclear which one gets
|
||||||
// called first
|
// called first
|
||||||
this.roomThreadsMap.set(room, new ThreadsRoomNotificationState(room));
|
const threadState = new ThreadsRoomNotificationState(room);
|
||||||
|
this.roomThreadsMap.set(room, threadState);
|
||||||
|
this.roomMap.set(room, new RoomNotificationState(room, threadState));
|
||||||
}
|
}
|
||||||
return this.roomMap.get(room);
|
return this.roomMap.get(room);
|
||||||
}
|
}
|
||||||
|
|
|
@ -253,6 +253,9 @@ export default class RightPanelStore extends ReadyWatchingStore {
|
||||||
private filterValidCards(rightPanelForRoom?: IRightPanelForRoom) {
|
private filterValidCards(rightPanelForRoom?: IRightPanelForRoom) {
|
||||||
if (!rightPanelForRoom?.history) return;
|
if (!rightPanelForRoom?.history) return;
|
||||||
rightPanelForRoom.history = rightPanelForRoom.history.filter((card) => this.isCardStateValid(card));
|
rightPanelForRoom.history = rightPanelForRoom.history.filter((card) => this.isCardStateValid(card));
|
||||||
|
if (!rightPanelForRoom.history.length) {
|
||||||
|
rightPanelForRoom.isOpen = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private isCardStateValid(card: IRightPanelCard) {
|
private isCardStateValid(card: IRightPanelCard) {
|
||||||
|
@ -263,7 +266,11 @@ export default class RightPanelStore extends ReadyWatchingStore {
|
||||||
// or potentially other errors.
|
// or potentially other errors.
|
||||||
// (A nicer fix could be to indicate, that the right panel is loading if there is missing state data and re-emit if the data is available)
|
// (A nicer fix could be to indicate, that the right panel is loading if there is missing state data and re-emit if the data is available)
|
||||||
switch (card.phase) {
|
switch (card.phase) {
|
||||||
|
case RightPanelPhases.ThreadPanel:
|
||||||
|
if (!SettingsStore.getValue("feature_thread")) return false;
|
||||||
|
break;
|
||||||
case RightPanelPhases.ThreadView:
|
case RightPanelPhases.ThreadView:
|
||||||
|
if (!SettingsStore.getValue("feature_thread")) return false;
|
||||||
if (!card.state.threadHeadEvent) {
|
if (!card.state.threadHeadEvent) {
|
||||||
console.warn("removed card from right panel because of missing threadHeadEvent in card state");
|
console.warn("removed card from right panel because of missing threadHeadEvent in card state");
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,8 @@
|
||||||
import { arrayHasDiff } from "./arrays";
|
import { arrayHasDiff } from "./arrays";
|
||||||
|
|
||||||
export function mayBeAnimated(mimeType: string): boolean {
|
export function mayBeAnimated(mimeType: string): boolean {
|
||||||
return ["image/gif", "image/webp", "image/png", "image/apng"].includes(mimeType);
|
// AVIF animation support at the time of writing is only available in Chrome hence not having `blobIsAnimated` check
|
||||||
|
return ["image/gif", "image/webp", "image/png", "image/apng", "image/avif"].includes(mimeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
function arrayBufferRead(arr: ArrayBuffer, start: number, len: number): Uint8Array {
|
function arrayBufferRead(arr: ArrayBuffer, start: number, len: number): Uint8Array {
|
||||||
|
|
|
@ -203,18 +203,32 @@ export default class MultiInviter {
|
||||||
|
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
|
|
||||||
let errorText;
|
const isSpace = this.roomId && this.matrixClient.getRoom(this.roomId)?.isSpaceRoom();
|
||||||
|
|
||||||
|
let errorText: string;
|
||||||
let fatal = false;
|
let fatal = false;
|
||||||
switch (err.errcode) {
|
switch (err.errcode) {
|
||||||
case "M_FORBIDDEN":
|
case "M_FORBIDDEN":
|
||||||
errorText = _t('You do not have permission to invite people to this room.');
|
if (isSpace) {
|
||||||
|
errorText = _t('You do not have permission to invite people to this space.');
|
||||||
|
} else {
|
||||||
|
errorText = _t('You do not have permission to invite people to this room.');
|
||||||
|
}
|
||||||
fatal = true;
|
fatal = true;
|
||||||
break;
|
break;
|
||||||
case USER_ALREADY_INVITED:
|
case USER_ALREADY_INVITED:
|
||||||
errorText = _t("User %(userId)s is already invited to the room", { userId: address });
|
if (isSpace) {
|
||||||
|
errorText = _t("User is already invited to the space");
|
||||||
|
} else {
|
||||||
|
errorText = _t("User is already invited to the room");
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case USER_ALREADY_JOINED:
|
case USER_ALREADY_JOINED:
|
||||||
errorText = _t("User %(userId)s is already in the room", { userId: address });
|
if (isSpace) {
|
||||||
|
errorText = _t("User is already in the space");
|
||||||
|
} else {
|
||||||
|
errorText = _t("User is already in the room");
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "M_LIMIT_EXCEEDED":
|
case "M_LIMIT_EXCEEDED":
|
||||||
// we're being throttled so wait a bit & try again
|
// we're being throttled so wait a bit & try again
|
||||||
|
@ -224,10 +238,10 @@ export default class MultiInviter {
|
||||||
return;
|
return;
|
||||||
case "M_NOT_FOUND":
|
case "M_NOT_FOUND":
|
||||||
case "M_USER_NOT_FOUND":
|
case "M_USER_NOT_FOUND":
|
||||||
errorText = _t("User %(user_id)s does not exist", { user_id: address });
|
errorText = _t("User does not exist");
|
||||||
break;
|
break;
|
||||||
case "M_PROFILE_UNDISCLOSED":
|
case "M_PROFILE_UNDISCLOSED":
|
||||||
errorText = _t("User %(user_id)s may or may not exist", { user_id: address });
|
errorText = _t("User may or may not exist");
|
||||||
break;
|
break;
|
||||||
case "M_PROFILE_NOT_FOUND":
|
case "M_PROFILE_NOT_FOUND":
|
||||||
if (!ignoreProfile) {
|
if (!ignoreProfile) {
|
||||||
|
@ -241,7 +255,11 @@ export default class MultiInviter {
|
||||||
errorText = _t("The user must be unbanned before they can be invited.");
|
errorText = _t("The user must be unbanned before they can be invited.");
|
||||||
break;
|
break;
|
||||||
case "M_UNSUPPORTED_ROOM_VERSION":
|
case "M_UNSUPPORTED_ROOM_VERSION":
|
||||||
errorText = _t("The user's homeserver does not support the version of the room.");
|
if (isSpace) {
|
||||||
|
errorText = _t("The user's homeserver does not support the version of the space.");
|
||||||
|
} else {
|
||||||
|
errorText = _t("The user's homeserver does not support the version of the room.");
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IContent, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { IContent, IEventRelation, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import sanitizeHtml from "sanitize-html";
|
import sanitizeHtml from "sanitize-html";
|
||||||
import escapeHtml from "escape-html";
|
import escapeHtml from "escape-html";
|
||||||
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
|
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
|
||||||
|
@ -22,7 +22,6 @@ import { MsgType } from "matrix-js-sdk/src/@types/event";
|
||||||
|
|
||||||
import { PERMITTED_URL_SCHEMES } from "../HtmlUtils";
|
import { PERMITTED_URL_SCHEMES } from "../HtmlUtils";
|
||||||
import { makeUserPermalink, RoomPermalinkCreator } from "./permalinks/Permalinks";
|
import { makeUserPermalink, RoomPermalinkCreator } from "./permalinks/Permalinks";
|
||||||
import { RecursivePartial } from "../@types/common";
|
|
||||||
import SettingsStore from "../settings/SettingsStore";
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
|
|
||||||
export function getParentEventId(ev?: MatrixEvent): string | undefined {
|
export function getParentEventId(ev?: MatrixEvent): string | undefined {
|
||||||
|
@ -144,30 +143,17 @@ export function getNestedReplyText(
|
||||||
return { body, html };
|
return { body, html };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeReplyMixIn(ev?: MatrixEvent): RecursivePartial<IContent> {
|
export function makeReplyMixIn(ev?: MatrixEvent): IEventRelation {
|
||||||
if (!ev) return {};
|
if (!ev) return {};
|
||||||
|
|
||||||
const mixin: RecursivePartial<IContent> = {
|
const mixin: IEventRelation = {
|
||||||
'm.relates_to': {
|
'm.in_reply_to': {
|
||||||
'm.in_reply_to': {
|
'event_id': ev.getId(),
|
||||||
'event_id': ev.getId(),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
if (SettingsStore.getValue("feature_thread") && ev.threadRootId) {
|
||||||
* If the event replied is part of a thread
|
mixin.is_falling_back = false;
|
||||||
* Add the `m.thread` relation so that clients
|
|
||||||
* that know how to handle that relation will
|
|
||||||
* be able to render them more accurately
|
|
||||||
*/
|
|
||||||
if (ev.isThreadRelation || ev.isThreadRoot) {
|
|
||||||
mixin['m.relates_to'] = {
|
|
||||||
...mixin['m.relates_to'],
|
|
||||||
is_falling_back: false,
|
|
||||||
rel_type: THREAD_RELATION_TYPE.name,
|
|
||||||
event_id: ev.threadRootId,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return mixin;
|
return mixin;
|
||||||
|
@ -206,12 +192,13 @@ export function addReplyToMessageContent(
|
||||||
includeLegacyFallback: true,
|
includeLegacyFallback: true,
|
||||||
},
|
},
|
||||||
): void {
|
): void {
|
||||||
const replyContent = makeReplyMixIn(replyToEvent);
|
content["m.relates_to"] = {
|
||||||
Object.assign(content, replyContent);
|
...(content["m.relates_to"] || {}),
|
||||||
|
...makeReplyMixIn(replyToEvent),
|
||||||
|
};
|
||||||
|
|
||||||
if (opts.includeLegacyFallback) {
|
if (opts.includeLegacyFallback) {
|
||||||
// Part of Replies fallback support - prepend the text we're sending
|
// Part of Replies fallback support - prepend the text we're sending with the text we're replying to
|
||||||
// with the text we're replying to
|
|
||||||
const nestedReply = getNestedReplyText(replyToEvent, opts.permalinkCreator);
|
const nestedReply = getNestedReplyText(replyToEvent, opts.permalinkCreator);
|
||||||
if (nestedReply) {
|
if (nestedReply) {
|
||||||
if (content.formatted_body) {
|
if (content.formatted_body) {
|
||||||
|
|
|
@ -34,3 +34,7 @@ export const getBeaconExpiryTimestamp = (beacon: Beacon): number =>
|
||||||
|
|
||||||
export const sortBeaconsByLatestExpiry = (left: Beacon, right: Beacon): number =>
|
export const sortBeaconsByLatestExpiry = (left: Beacon, right: Beacon): number =>
|
||||||
getBeaconExpiryTimestamp(right) - getBeaconExpiryTimestamp(left);
|
getBeaconExpiryTimestamp(right) - getBeaconExpiryTimestamp(left);
|
||||||
|
|
||||||
|
// aka sort by timestamp descending
|
||||||
|
export const sortBeaconsByLatestCreation = (left: Beacon, right: Beacon): number =>
|
||||||
|
right.beaconInfo.timestamp - left.beaconInfo.timestamp;
|
||||||
|
|
|
@ -54,6 +54,7 @@ const ALLOWED_BLOB_MIMETYPES = [
|
||||||
'image/png',
|
'image/png',
|
||||||
'image/apng',
|
'image/apng',
|
||||||
'image/webp',
|
'image/webp',
|
||||||
|
'image/avif',
|
||||||
|
|
||||||
'video/mp4',
|
'video/mp4',
|
||||||
'video/webm',
|
'video/webm',
|
||||||
|
|
|
@ -26,6 +26,7 @@ import RoomContext, { TimelineRenderingType } from '../../../../src/contexts/Roo
|
||||||
import { createAudioContext } from '../../../../src/audio/compat';
|
import { createAudioContext } from '../../../../src/audio/compat';
|
||||||
import { findByTestId, flushPromises } from '../../../test-utils';
|
import { findByTestId, flushPromises } from '../../../test-utils';
|
||||||
import PlaybackWaveform from '../../../../src/components/views/audio_messages/PlaybackWaveform';
|
import PlaybackWaveform from '../../../../src/components/views/audio_messages/PlaybackWaveform';
|
||||||
|
import SeekBar from "../../../../src/components/views/audio_messages/SeekBar";
|
||||||
|
|
||||||
jest.mock('../../../../src/audio/compat', () => ({
|
jest.mock('../../../../src/audio/compat', () => ({
|
||||||
createAudioContext: jest.fn(),
|
createAudioContext: jest.fn(),
|
||||||
|
@ -55,7 +56,7 @@ describe('<RecordingPlayback />', () => {
|
||||||
const mockChannelData = new Float32Array();
|
const mockChannelData = new Float32Array();
|
||||||
|
|
||||||
const defaultRoom = { roomId: '!room:server.org', timelineRenderingType: TimelineRenderingType.File };
|
const defaultRoom = { roomId: '!room:server.org', timelineRenderingType: TimelineRenderingType.File };
|
||||||
const getComponent = (props: { playback: Playback }, room = defaultRoom) =>
|
const getComponent = (props: React.ComponentProps<typeof RecordingPlayback>, room = defaultRoom) =>
|
||||||
mount(<RecordingPlayback {...props} />, {
|
mount(<RecordingPlayback {...props} />, {
|
||||||
wrappingComponent: RoomContext.Provider,
|
wrappingComponent: RoomContext.Provider,
|
||||||
wrappingComponentProps: { value: room },
|
wrappingComponentProps: { value: room },
|
||||||
|
@ -127,34 +128,19 @@ describe('<RecordingPlayback />', () => {
|
||||||
expect(playback.toggle).toHaveBeenCalled();
|
expect(playback.toggle).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it('should render a seek bar by default', () => {
|
||||||
[TimelineRenderingType.Notification],
|
|
||||||
[TimelineRenderingType.File],
|
|
||||||
[TimelineRenderingType.Pinned],
|
|
||||||
])('does not render waveform when timeline rendering type for room is %s', (timelineRenderingType) => {
|
|
||||||
const playback = new Playback(new ArrayBuffer(8));
|
const playback = new Playback(new ArrayBuffer(8));
|
||||||
const room = {
|
const component = getComponent({ playback });
|
||||||
...defaultRoom,
|
|
||||||
timelineRenderingType,
|
|
||||||
};
|
|
||||||
const component = getComponent({ playback }, room);
|
|
||||||
|
|
||||||
expect(component.find(PlaybackWaveform).length).toBeFalsy();
|
expect(component.find(PlaybackWaveform).length).toBeFalsy();
|
||||||
|
expect(component.find(SeekBar).length).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it('should render a waveform when requested', () => {
|
||||||
[TimelineRenderingType.Room],
|
|
||||||
[TimelineRenderingType.Thread],
|
|
||||||
[TimelineRenderingType.ThreadsList],
|
|
||||||
[TimelineRenderingType.Search],
|
|
||||||
])('renders waveform when timeline rendering type for room is %s', (timelineRenderingType) => {
|
|
||||||
const playback = new Playback(new ArrayBuffer(8));
|
const playback = new Playback(new ArrayBuffer(8));
|
||||||
const room = {
|
const component = getComponent({ playback, withWaveform: true });
|
||||||
...defaultRoom,
|
|
||||||
timelineRenderingType,
|
|
||||||
};
|
|
||||||
const component = getComponent({ playback }, room);
|
|
||||||
|
|
||||||
expect(component.find(PlaybackWaveform).length).toBeTruthy();
|
expect(component.find(PlaybackWaveform).length).toBeTruthy();
|
||||||
|
expect(component.find(SeekBar).length).toBeFalsy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,16 +17,22 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mocked } from 'jest-mock';
|
import { mocked } from 'jest-mock';
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { Beacon } from 'matrix-js-sdk/src/matrix';
|
||||||
|
|
||||||
import LeftPanelLiveShareWarning from '../../../../src/components/views/beacon/LeftPanelLiveShareWarning';
|
import LeftPanelLiveShareWarning from '../../../../src/components/views/beacon/LeftPanelLiveShareWarning';
|
||||||
import { OwnBeaconStore, OwnBeaconStoreEvent } from '../../../../src/stores/OwnBeaconStore';
|
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', () => {
|
jest.mock('../../../../src/stores/OwnBeaconStore', () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const EventEmitter = require("events");
|
const EventEmitter = require("events");
|
||||||
class MockOwnBeaconStore extends EventEmitter {
|
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 {
|
return {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -43,32 +49,136 @@ describe('<LeftPanelLiveShareWarning />', () => {
|
||||||
const getComponent = (props = {}) =>
|
const getComponent = (props = {}) =>
|
||||||
mount(<LeftPanelLiveShareWarning {...defaultProps} {...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', () => {
|
it('renders nothing when user has no live beacons', () => {
|
||||||
const component = getComponent();
|
const component = getComponent();
|
||||||
expect(component.html()).toBe(null);
|
expect(component.html()).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when user has live location monitor', () => {
|
describe('when user has live location monitor', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
mocked(OwnBeaconStore.instance).getBeaconById.mockImplementation(beaconId => {
|
||||||
|
if (beaconId === beacon1.identifier) {
|
||||||
|
return beacon1;
|
||||||
|
}
|
||||||
|
return beacon2;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mocked(OwnBeaconStore.instance).isMonitoringLiveLocation = true;
|
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', () => {
|
it('renders correctly when not minimized', () => {
|
||||||
const component = getComponent();
|
const component = getComponent();
|
||||||
expect(component).toMatchSnapshot();
|
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', () => {
|
it('renders correctly when minimized', () => {
|
||||||
const component = getComponent({ isMinimized: true });
|
const component = getComponent({ isMinimized: true });
|
||||||
expect(component).toMatchSnapshot();
|
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 () => {
|
it('removes itself when user stops having live beacons', async () => {
|
||||||
const component = getComponent({ isMinimized: true });
|
const component = getComponent({ isMinimized: true });
|
||||||
// started out rendered
|
// started out rendered
|
||||||
expect(component.html()).toBeTruthy();
|
expect(component.html()).toBeTruthy();
|
||||||
|
|
||||||
mocked(OwnBeaconStore.instance).isMonitoringLiveLocation = false;
|
act(() => {
|
||||||
OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.MonitoringLivePosition);
|
mocked(OwnBeaconStore.instance).isMonitoringLiveLocation = false;
|
||||||
|
OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.MonitoringLivePosition);
|
||||||
|
});
|
||||||
|
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
component.setProps({});
|
component.setProps({});
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { OwnBeaconStore, OwnBeaconStoreEvent } from '../../../../src/stores/OwnB
|
||||||
import {
|
import {
|
||||||
advanceDateAndTime,
|
advanceDateAndTime,
|
||||||
findByTestId,
|
findByTestId,
|
||||||
|
flushPromisesWithFakeTimers,
|
||||||
getMockClientWithEventEmitter,
|
getMockClientWithEventEmitter,
|
||||||
makeBeaconInfoEvent,
|
makeBeaconInfoEvent,
|
||||||
mockGeolocation,
|
mockGeolocation,
|
||||||
|
@ -95,10 +96,11 @@ describe('<RoomLiveShareWarning />', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockGeolocation();
|
mockGeolocation();
|
||||||
jest.spyOn(global.Date, 'now').mockReturnValue(now);
|
jest.spyOn(global.Date, 'now').mockReturnValue(now);
|
||||||
mockClient.unstable_setLiveBeacon.mockClear();
|
mockClient.unstable_setLiveBeacon.mockReset().mockResolvedValue({ event_id: '1' });
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
jest.spyOn(OwnBeaconStore.instance, 'hasWireErrors').mockRestore();
|
||||||
await resetAsyncStoreWithClient(OwnBeaconStore.instance);
|
await resetAsyncStoreWithClient(OwnBeaconStore.instance);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -236,13 +238,37 @@ describe('<RoomLiveShareWarning />', () => {
|
||||||
const component = getComponent({ roomId: room2Id });
|
const component = getComponent({ roomId: room2Id });
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
findByTestId(component, 'room-live-share-stop-sharing').at(0).simulate('click');
|
findByTestId(component, 'room-live-share-primary-button').at(0).simulate('click');
|
||||||
component.setProps({});
|
component.setProps({});
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockClient.unstable_setLiveBeacon).toHaveBeenCalledTimes(2);
|
expect(mockClient.unstable_setLiveBeacon).toHaveBeenCalledTimes(2);
|
||||||
expect(component.find('Spinner').length).toBeTruthy();
|
expect(component.find('Spinner').length).toBeTruthy();
|
||||||
expect(findByTestId(component, 'room-live-share-stop-sharing').at(0).props().disabled).toBeTruthy();
|
expect(findByTestId(component, 'room-live-share-primary-button').at(0).props().disabled).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays error when stop sharing fails', async () => {
|
||||||
|
const component = getComponent({ roomId: room1Id });
|
||||||
|
|
||||||
|
// fail first time
|
||||||
|
mockClient.unstable_setLiveBeacon
|
||||||
|
.mockRejectedValueOnce(new Error('oups'))
|
||||||
|
.mockResolvedValue(({ event_id: '1' }));
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
findByTestId(component, 'room-live-share-primary-button').at(0).simulate('click');
|
||||||
|
await flushPromisesWithFakeTimers();
|
||||||
|
});
|
||||||
|
component.setProps({});
|
||||||
|
|
||||||
|
expect(component.html()).toMatchSnapshot();
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
findByTestId(component, 'room-live-share-primary-button').at(0).simulate('click');
|
||||||
|
component.setProps({});
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockClient.unstable_setLiveBeacon).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays again with correct state after stopping a beacon', () => {
|
it('displays again with correct state after stopping a beacon', () => {
|
||||||
|
@ -251,7 +277,7 @@ describe('<RoomLiveShareWarning />', () => {
|
||||||
|
|
||||||
// stop the beacon
|
// stop the beacon
|
||||||
act(() => {
|
act(() => {
|
||||||
findByTestId(component, 'room-live-share-stop-sharing').at(0).simulate('click');
|
findByTestId(component, 'room-live-share-primary-button').at(0).simulate('click');
|
||||||
});
|
});
|
||||||
// time travel until room1Beacon1 is expired
|
// time travel until room1Beacon1 is expired
|
||||||
act(() => {
|
act(() => {
|
||||||
|
@ -267,9 +293,83 @@ describe('<RoomLiveShareWarning />', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// button not disabled and expiry time shown
|
// button not disabled and expiry time shown
|
||||||
expect(findByTestId(component, 'room-live-share-stop-sharing').at(0).props().disabled).toBeFalsy();
|
expect(findByTestId(component, 'room-live-share-primary-button').at(0).props().disabled).toBeFalsy();
|
||||||
expect(findByTestId(component, 'room-live-share-expiry').text()).toEqual('1h left');
|
expect(findByTestId(component, 'room-live-share-expiry').text()).toEqual('1h left');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('with wire errors', () => {
|
||||||
|
it('displays wire error when mounted with wire errors', async () => {
|
||||||
|
const hasWireErrorsSpy = jest.spyOn(OwnBeaconStore.instance, 'hasWireErrors').mockReturnValue(true);
|
||||||
|
const component = getComponent({ roomId: room2Id });
|
||||||
|
|
||||||
|
expect(component).toMatchSnapshot();
|
||||||
|
expect(hasWireErrorsSpy).toHaveBeenCalledWith(room2Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays wire error when wireError event is emitted and beacons have errors', async () => {
|
||||||
|
const hasWireErrorsSpy = jest.spyOn(OwnBeaconStore.instance, 'hasWireErrors').mockReturnValue(false);
|
||||||
|
const component = getComponent({ roomId: room2Id });
|
||||||
|
|
||||||
|
// update mock and emit event
|
||||||
|
act(() => {
|
||||||
|
hasWireErrorsSpy.mockReturnValue(true);
|
||||||
|
OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.WireError, room2Beacon1.getType());
|
||||||
|
});
|
||||||
|
component.setProps({});
|
||||||
|
|
||||||
|
// renders wire error ui
|
||||||
|
expect(component.find('.mx_RoomLiveShareWarning_label').text()).toEqual(
|
||||||
|
'An error occured whilst sharing your live location, please try again',
|
||||||
|
);
|
||||||
|
expect(findByTestId(component, 'room-live-share-wire-error-close-button').length).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stops displaying wire error when errors are cleared', async () => {
|
||||||
|
const hasWireErrorsSpy = jest.spyOn(OwnBeaconStore.instance, 'hasWireErrors').mockReturnValue(true);
|
||||||
|
const component = getComponent({ roomId: room2Id });
|
||||||
|
|
||||||
|
// update mock and emit event
|
||||||
|
act(() => {
|
||||||
|
hasWireErrorsSpy.mockReturnValue(false);
|
||||||
|
OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.WireError, room2Beacon1.getType());
|
||||||
|
});
|
||||||
|
component.setProps({});
|
||||||
|
|
||||||
|
// renders error-free ui
|
||||||
|
expect(component.find('.mx_RoomLiveShareWarning_label').text()).toEqual(
|
||||||
|
'You are sharing your live location',
|
||||||
|
);
|
||||||
|
expect(findByTestId(component, 'room-live-share-wire-error-close-button').length).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clicking retry button resets wire errors', async () => {
|
||||||
|
jest.spyOn(OwnBeaconStore.instance, 'hasWireErrors').mockReturnValue(true);
|
||||||
|
const resetErrorSpy = jest.spyOn(OwnBeaconStore.instance, 'resetWireError');
|
||||||
|
|
||||||
|
const component = getComponent({ roomId: room2Id });
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
findByTestId(component, 'room-live-share-primary-button').at(0).simulate('click');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resetErrorSpy).toHaveBeenCalledWith(room2Beacon1.getType());
|
||||||
|
expect(resetErrorSpy).toHaveBeenCalledWith(room2Beacon2.getType());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clicking close button stops beacons', async () => {
|
||||||
|
jest.spyOn(OwnBeaconStore.instance, 'hasWireErrors').mockReturnValue(true);
|
||||||
|
const stopBeaconSpy = jest.spyOn(OwnBeaconStore.instance, 'stopBeacon');
|
||||||
|
|
||||||
|
const component = getComponent({ roomId: room2Id });
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
findByTestId(component, 'room-live-share-wire-error-close-button').at(0).simulate('click');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(stopBeaconSpy).toHaveBeenCalledWith(room2Beacon1.getType());
|
||||||
|
expect(stopBeaconSpy).toHaveBeenCalledWith(room2Beacon2.getType());
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,23 +4,73 @@ exports[`<LeftPanelLiveShareWarning /> when user has live location monitor rende
|
||||||
<LeftPanelLiveShareWarning
|
<LeftPanelLiveShareWarning
|
||||||
isMinimized={true}
|
isMinimized={true}
|
||||||
>
|
>
|
||||||
<div
|
<AccessibleButton
|
||||||
className="mx_LeftPanelLiveShareWarning mx_LeftPanelLiveShareWarning__minimized"
|
className="mx_LeftPanelLiveShareWarning mx_LeftPanelLiveShareWarning__minimized"
|
||||||
|
element="div"
|
||||||
|
onClick={[Function]}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
title="You are sharing your live location"
|
title="You are sharing your live location"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
height={10}
|
className="mx_AccessibleButton mx_LeftPanelLiveShareWarning mx_LeftPanelLiveShareWarning__minimized"
|
||||||
/>
|
onClick={[Function]}
|
||||||
</div>
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
title="You are sharing your live location"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
height={10}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</AccessibleButton>
|
||||||
</LeftPanelLiveShareWarning>
|
</LeftPanelLiveShareWarning>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<LeftPanelLiveShareWarning /> when user has live location monitor renders correctly when not minimized 1`] = `
|
exports[`<LeftPanelLiveShareWarning /> when user has live location monitor renders correctly when not minimized 1`] = `
|
||||||
<LeftPanelLiveShareWarning>
|
<LeftPanelLiveShareWarning>
|
||||||
<div
|
<AccessibleButton
|
||||||
className="mx_LeftPanelLiveShareWarning"
|
className="mx_LeftPanelLiveShareWarning"
|
||||||
|
element="div"
|
||||||
|
onClick={[Function]}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
You are sharing your live location
|
<div
|
||||||
</div>
|
className="mx_AccessibleButton mx_LeftPanelLiveShareWarning"
|
||||||
|
onClick={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
You are sharing your live location
|
||||||
|
</div>
|
||||||
|
</AccessibleButton>
|
||||||
|
</LeftPanelLiveShareWarning>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<LeftPanelLiveShareWarning /> when user has live location monitor renders wire error 1`] = `
|
||||||
|
<LeftPanelLiveShareWarning>
|
||||||
|
<AccessibleButton
|
||||||
|
className="mx_LeftPanelLiveShareWarning mx_LeftPanelLiveShareWarning__error"
|
||||||
|
element="div"
|
||||||
|
onClick={[Function]}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="mx_AccessibleButton mx_LeftPanelLiveShareWarning mx_LeftPanelLiveShareWarning__error"
|
||||||
|
onClick={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
An error occured whilst sharing your live location
|
||||||
|
</div>
|
||||||
|
</AccessibleButton>
|
||||||
</LeftPanelLiveShareWarning>
|
</LeftPanelLiveShareWarning>
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -1,5 +1,86 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`<RoomLiveShareWarning /> when user has live beacons and geolocation is available renders correctly with one live beacon in room 1`] = `"<div class=\\"mx_RoomLiveShareWarning\\"><div class=\\"mx_StyledLiveBeaconIcon mx_RoomLiveShareWarning_icon\\"></div><span class=\\"mx_RoomLiveShareWarning_label\\">You are sharing your live location</span><span data-test-id=\\"room-live-share-expiry\\" class=\\"mx_RoomLiveShareWarning_expiry\\">1h left</span><button data-test-id=\\"room-live-share-stop-sharing\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger\\">Stop sharing</button></div>"`;
|
exports[`<RoomLiveShareWarning /> when user has live beacons and geolocation is available renders correctly with one live beacon in room 1`] = `"<div class=\\"mx_RoomLiveShareWarning\\"><div class=\\"mx_StyledLiveBeaconIcon mx_RoomLiveShareWarning_icon\\"></div><span class=\\"mx_RoomLiveShareWarning_label\\">You are sharing your live location</span><span data-test-id=\\"room-live-share-expiry\\" class=\\"mx_RoomLiveShareWarning_expiry\\">1h left</span><button data-test-id=\\"room-live-share-primary-button\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger\\">Stop sharing</button></div>"`;
|
||||||
|
|
||||||
exports[`<RoomLiveShareWarning /> when user has live beacons and geolocation is available renders correctly with two live beacons in room 1`] = `"<div class=\\"mx_RoomLiveShareWarning\\"><div class=\\"mx_StyledLiveBeaconIcon mx_RoomLiveShareWarning_icon\\"></div><span class=\\"mx_RoomLiveShareWarning_label\\">You are sharing your live location</span><span data-test-id=\\"room-live-share-expiry\\" class=\\"mx_RoomLiveShareWarning_expiry\\">12h left</span><button data-test-id=\\"room-live-share-stop-sharing\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger\\">Stop sharing</button></div>"`;
|
exports[`<RoomLiveShareWarning /> when user has live beacons and geolocation is available renders correctly with two live beacons in room 1`] = `"<div class=\\"mx_RoomLiveShareWarning\\"><div class=\\"mx_StyledLiveBeaconIcon mx_RoomLiveShareWarning_icon\\"></div><span class=\\"mx_RoomLiveShareWarning_label\\">You are sharing your live location</span><span data-test-id=\\"room-live-share-expiry\\" class=\\"mx_RoomLiveShareWarning_expiry\\">12h left</span><button data-test-id=\\"room-live-share-primary-button\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger\\">Stop sharing</button></div>"`;
|
||||||
|
|
||||||
|
exports[`<RoomLiveShareWarning /> when user has live beacons and geolocation is available stopping beacons displays error when stop sharing fails 1`] = `"<div class=\\"mx_RoomLiveShareWarning\\"><div class=\\"mx_StyledLiveBeaconIcon mx_RoomLiveShareWarning_icon mx_StyledLiveBeaconIcon_error\\"></div><span class=\\"mx_RoomLiveShareWarning_label\\">An error occurred while stopping your live location, please try again</span><button data-test-id=\\"room-live-share-primary-button\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger\\">Retry</button></div>"`;
|
||||||
|
|
||||||
|
exports[`<RoomLiveShareWarning /> when user has live beacons and geolocation is available with wire errors displays wire error when mounted with wire errors 1`] = `
|
||||||
|
<RoomLiveShareWarning
|
||||||
|
roomId="$room2:server.org"
|
||||||
|
>
|
||||||
|
<RoomLiveShareWarningInner
|
||||||
|
liveBeaconIds={
|
||||||
|
Array [
|
||||||
|
"org.matrix.msc3489.beacon_info.@alice:server.org.3",
|
||||||
|
"org.matrix.msc3489.beacon_info.@alice:server.org.4",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
roomId="$room2:server.org"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="mx_RoomLiveShareWarning"
|
||||||
|
>
|
||||||
|
<StyledLiveBeaconIcon
|
||||||
|
className="mx_RoomLiveShareWarning_icon"
|
||||||
|
withError={true}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="mx_StyledLiveBeaconIcon mx_RoomLiveShareWarning_icon mx_StyledLiveBeaconIcon_error"
|
||||||
|
/>
|
||||||
|
</StyledLiveBeaconIcon>
|
||||||
|
<span
|
||||||
|
className="mx_RoomLiveShareWarning_label"
|
||||||
|
>
|
||||||
|
An error occured whilst sharing your live location, please try again
|
||||||
|
</span>
|
||||||
|
<AccessibleButton
|
||||||
|
data-test-id="room-live-share-primary-button"
|
||||||
|
disabled={false}
|
||||||
|
element="button"
|
||||||
|
kind="danger"
|
||||||
|
onClick={[Function]}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger"
|
||||||
|
data-test-id="room-live-share-primary-button"
|
||||||
|
onClick={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</AccessibleButton>
|
||||||
|
<AccessibleButton
|
||||||
|
className="mx_RoomLiveShareWarning_closeButton"
|
||||||
|
data-test-id="room-live-share-wire-error-close-button"
|
||||||
|
element="button"
|
||||||
|
onClick={[Function]}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
title="Stop sharing and close"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="mx_AccessibleButton mx_RoomLiveShareWarning_closeButton"
|
||||||
|
data-test-id="room-live-share-wire-error-close-button"
|
||||||
|
onClick={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
title="Stop sharing and close"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="mx_RoomLiveShareWarning_closeButtonIcon"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
</RoomLiveShareWarningInner>
|
||||||
|
</RoomLiveShareWarning>
|
||||||
|
`;
|
||||||
|
|
|
@ -107,7 +107,7 @@ describe('<RoomPreviewBar />', () => {
|
||||||
const component = getComponent({ joining: true });
|
const component = getComponent({ joining: true });
|
||||||
|
|
||||||
expect(isSpinnerRendered(component)).toBeTruthy();
|
expect(isSpinnerRendered(component)).toBeTruthy();
|
||||||
expect(getMessage(component).textContent).toEqual('Joining room …');
|
expect(getMessage(component).textContent).toEqual('Joining …');
|
||||||
});
|
});
|
||||||
it('renders rejecting message', () => {
|
it('renders rejecting message', () => {
|
||||||
const component = getComponent({ rejecting: true });
|
const component = getComponent({ rejecting: true });
|
||||||
|
|
|
@ -54,11 +54,11 @@ exports[`<RoomPreviewBar /> with an error renders other errors 1`] = `
|
||||||
RoomPreviewBar-test-room is not accessible at this time.
|
RoomPreviewBar-test-room is not accessible at this time.
|
||||||
</h3>
|
</h3>
|
||||||
<p>
|
<p>
|
||||||
Try again later, or ask a room admin to check if you have access.
|
Try again later, or ask a room or space admin to check if you have access.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span>
|
<span>
|
||||||
Something_else was returned while trying to access the room. If you think you're seeing this message in error, please
|
Something_else was returned while trying to access the room or space. If you think you're seeing this message in error, please
|
||||||
<a
|
<a
|
||||||
href="https://github.com/vector-im/element-web/issues/new/choose"
|
href="https://github.com/vector-im/element-web/issues/new/choose"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
|
@ -80,7 +80,7 @@ exports[`<RoomPreviewBar /> with an error renders room not found error 1`] = `
|
||||||
RoomPreviewBar-test-room does not exist.
|
RoomPreviewBar-test-room does not exist.
|
||||||
</h3>
|
</h3>
|
||||||
<p>
|
<p>
|
||||||
This room doesn't exist. Are you sure you're at the right place?
|
Are you sure you're at the right place?
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
@ -93,7 +93,7 @@ exports[`<RoomPreviewBar /> with an invite with an invited email when client fai
|
||||||
Something went wrong with your invite to RoomPreviewBar-test-room
|
Something went wrong with your invite to RoomPreviewBar-test-room
|
||||||
</h3>
|
</h3>
|
||||||
<p>
|
<p>
|
||||||
An error (unknown error code) was returned while trying to validate your invite. You could try to pass this information on to a room admin.
|
An error (unknown error code) was returned while trying to validate your invite. You could try to pass this information on to the person who invited you.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -14,7 +14,14 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Room, Beacon, BeaconEvent, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
import {
|
||||||
|
Room,
|
||||||
|
Beacon,
|
||||||
|
BeaconEvent,
|
||||||
|
MatrixEvent,
|
||||||
|
RoomStateEvent,
|
||||||
|
RoomMember,
|
||||||
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import { makeBeaconContent } from "matrix-js-sdk/src/content-helpers";
|
import { makeBeaconContent } from "matrix-js-sdk/src/content-helpers";
|
||||||
import { M_BEACON, M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon";
|
import { M_BEACON, M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
@ -23,6 +30,7 @@ import { OwnBeaconStore, OwnBeaconStoreEvent } from "../../src/stores/OwnBeaconS
|
||||||
import {
|
import {
|
||||||
advanceDateAndTime,
|
advanceDateAndTime,
|
||||||
flushPromisesWithFakeTimers,
|
flushPromisesWithFakeTimers,
|
||||||
|
makeMembershipEvent,
|
||||||
resetAsyncStoreWithClient,
|
resetAsyncStoreWithClient,
|
||||||
setupAsyncStoreWithClient,
|
setupAsyncStoreWithClient,
|
||||||
} from "../test-utils";
|
} from "../test-utils";
|
||||||
|
@ -158,7 +166,7 @@ describe('OwnBeaconStore', () => {
|
||||||
geolocation = mockGeolocation();
|
geolocation = mockGeolocation();
|
||||||
mockClient.getVisibleRooms.mockReturnValue([]);
|
mockClient.getVisibleRooms.mockReturnValue([]);
|
||||||
mockClient.unstable_setLiveBeacon.mockClear().mockResolvedValue({ event_id: '1' });
|
mockClient.unstable_setLiveBeacon.mockClear().mockResolvedValue({ event_id: '1' });
|
||||||
mockClient.sendEvent.mockClear().mockResolvedValue({ event_id: '1' });
|
mockClient.sendEvent.mockReset().mockResolvedValue({ event_id: '1' });
|
||||||
jest.spyOn(global.Date, 'now').mockReturnValue(now);
|
jest.spyOn(global.Date, 'now').mockReturnValue(now);
|
||||||
jest.spyOn(OwnBeaconStore.instance, 'emit').mockRestore();
|
jest.spyOn(OwnBeaconStore.instance, 'emit').mockRestore();
|
||||||
jest.spyOn(logger, 'error').mockRestore();
|
jest.spyOn(logger, 'error').mockRestore();
|
||||||
|
@ -243,6 +251,7 @@ describe('OwnBeaconStore', () => {
|
||||||
|
|
||||||
expect(removeSpy.mock.calls[0]).toEqual(expect.arrayContaining([BeaconEvent.LivenessChange]));
|
expect(removeSpy.mock.calls[0]).toEqual(expect.arrayContaining([BeaconEvent.LivenessChange]));
|
||||||
expect(removeSpy.mock.calls[1]).toEqual(expect.arrayContaining([BeaconEvent.New]));
|
expect(removeSpy.mock.calls[1]).toEqual(expect.arrayContaining([BeaconEvent.New]));
|
||||||
|
expect(removeSpy.mock.calls[2]).toEqual(expect.arrayContaining([RoomStateEvent.Members]));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('destroys beacons', async () => {
|
it('destroys beacons', async () => {
|
||||||
|
@ -509,6 +518,112 @@ describe('OwnBeaconStore', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('on room membership changes', () => {
|
||||||
|
it('ignores events for rooms without beacons', async () => {
|
||||||
|
const membershipEvent = makeMembershipEvent(room2Id, aliceId);
|
||||||
|
// no beacons for room2
|
||||||
|
const [, room2] = makeRoomsWithStateEvents([
|
||||||
|
alicesRoom1BeaconInfo,
|
||||||
|
]);
|
||||||
|
const store = await makeOwnBeaconStore();
|
||||||
|
const emitSpy = jest.spyOn(store, 'emit');
|
||||||
|
const oldLiveBeaconIds = store.getLiveBeaconIds();
|
||||||
|
|
||||||
|
mockClient.emit(
|
||||||
|
RoomStateEvent.Members,
|
||||||
|
membershipEvent,
|
||||||
|
room2.currentState,
|
||||||
|
new RoomMember(room2Id, aliceId),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(emitSpy).not.toHaveBeenCalled();
|
||||||
|
// strictly equal
|
||||||
|
expect(store.getLiveBeaconIds()).toBe(oldLiveBeaconIds);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores events for membership changes that are not current user', async () => {
|
||||||
|
// bob joins room1
|
||||||
|
const membershipEvent = makeMembershipEvent(room1Id, bobId);
|
||||||
|
const member = new RoomMember(room1Id, bobId);
|
||||||
|
member.setMembershipEvent(membershipEvent);
|
||||||
|
|
||||||
|
const [room1] = makeRoomsWithStateEvents([
|
||||||
|
alicesRoom1BeaconInfo,
|
||||||
|
]);
|
||||||
|
const store = await makeOwnBeaconStore();
|
||||||
|
const emitSpy = jest.spyOn(store, 'emit');
|
||||||
|
const oldLiveBeaconIds = store.getLiveBeaconIds();
|
||||||
|
|
||||||
|
mockClient.emit(
|
||||||
|
RoomStateEvent.Members,
|
||||||
|
membershipEvent,
|
||||||
|
room1.currentState,
|
||||||
|
member,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(emitSpy).not.toHaveBeenCalled();
|
||||||
|
// strictly equal
|
||||||
|
expect(store.getLiveBeaconIds()).toBe(oldLiveBeaconIds);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores events for membership changes that are not leave/ban', async () => {
|
||||||
|
// alice joins room1
|
||||||
|
const membershipEvent = makeMembershipEvent(room1Id, aliceId);
|
||||||
|
const member = new RoomMember(room1Id, aliceId);
|
||||||
|
member.setMembershipEvent(membershipEvent);
|
||||||
|
|
||||||
|
const [room1] = makeRoomsWithStateEvents([
|
||||||
|
alicesRoom1BeaconInfo,
|
||||||
|
alicesRoom2BeaconInfo,
|
||||||
|
]);
|
||||||
|
const store = await makeOwnBeaconStore();
|
||||||
|
const emitSpy = jest.spyOn(store, 'emit');
|
||||||
|
const oldLiveBeaconIds = store.getLiveBeaconIds();
|
||||||
|
|
||||||
|
mockClient.emit(
|
||||||
|
RoomStateEvent.Members,
|
||||||
|
membershipEvent,
|
||||||
|
room1.currentState,
|
||||||
|
member,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(emitSpy).not.toHaveBeenCalled();
|
||||||
|
// strictly equal
|
||||||
|
expect(store.getLiveBeaconIds()).toBe(oldLiveBeaconIds);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('destroys and removes beacons when current user leaves room', async () => {
|
||||||
|
// alice leaves room1
|
||||||
|
const membershipEvent = makeMembershipEvent(room1Id, aliceId, 'leave');
|
||||||
|
const member = new RoomMember(room1Id, aliceId);
|
||||||
|
member.setMembershipEvent(membershipEvent);
|
||||||
|
|
||||||
|
const [room1] = makeRoomsWithStateEvents([
|
||||||
|
alicesRoom1BeaconInfo,
|
||||||
|
alicesRoom2BeaconInfo,
|
||||||
|
]);
|
||||||
|
const store = await makeOwnBeaconStore();
|
||||||
|
const room1BeaconInstance = store.beacons.get(alicesRoom1BeaconInfo.getType());
|
||||||
|
const beaconDestroySpy = jest.spyOn(room1BeaconInstance, 'destroy');
|
||||||
|
const emitSpy = jest.spyOn(store, 'emit');
|
||||||
|
|
||||||
|
mockClient.emit(
|
||||||
|
RoomStateEvent.Members,
|
||||||
|
membershipEvent,
|
||||||
|
room1.currentState,
|
||||||
|
member,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(emitSpy).toHaveBeenCalledWith(
|
||||||
|
OwnBeaconStoreEvent.LivenessChange,
|
||||||
|
// other rooms beacons still live
|
||||||
|
[alicesRoom2BeaconInfo.getType()],
|
||||||
|
);
|
||||||
|
expect(beaconDestroySpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(store.getLiveBeaconIds(room1Id)).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('stopBeacon()', () => {
|
describe('stopBeacon()', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
makeRoomsWithStateEvents([
|
makeRoomsWithStateEvents([
|
||||||
|
@ -581,7 +696,7 @@ describe('OwnBeaconStore', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('sending positions', () => {
|
describe('publishing positions', () => {
|
||||||
it('stops watching position when user has no more live beacons', async () => {
|
it('stops watching position when user has no more live beacons', async () => {
|
||||||
// geolocation is only going to emit 1 position
|
// geolocation is only going to emit 1 position
|
||||||
geolocation.watchPosition.mockImplementation(
|
geolocation.watchPosition.mockImplementation(
|
||||||
|
@ -710,6 +825,141 @@ describe('OwnBeaconStore', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when publishing position fails', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
geolocation.watchPosition.mockImplementation(
|
||||||
|
watchPositionMockImplementation([0, 1000, 3000, 3000, 3000]),
|
||||||
|
);
|
||||||
|
|
||||||
|
// eat expected console error logs
|
||||||
|
jest.spyOn(logger, 'error').mockImplementation(() => { });
|
||||||
|
});
|
||||||
|
|
||||||
|
// we need to advance time and then flush promises
|
||||||
|
// individually for each call to sendEvent
|
||||||
|
// otherwise the sendEvent doesn't reject/resolve and update state
|
||||||
|
// before the next call
|
||||||
|
// advance and flush every 1000ms
|
||||||
|
// until given ms is 'elapsed'
|
||||||
|
const advanceAndFlushPromises = async (timeMs: number) => {
|
||||||
|
while (timeMs > 0) {
|
||||||
|
jest.advanceTimersByTime(1000);
|
||||||
|
await flushPromisesWithFakeTimers();
|
||||||
|
timeMs -= 1000;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
it('continues publishing positions after one publish error', async () => {
|
||||||
|
// fail to send first event, then succeed
|
||||||
|
mockClient.sendEvent.mockRejectedValueOnce(new Error('oups')).mockResolvedValue({ event_id: '1' });
|
||||||
|
makeRoomsWithStateEvents([
|
||||||
|
alicesRoom1BeaconInfo,
|
||||||
|
]);
|
||||||
|
const store = await makeOwnBeaconStore();
|
||||||
|
// wait for store to settle
|
||||||
|
await flushPromisesWithFakeTimers();
|
||||||
|
|
||||||
|
await advanceAndFlushPromises(50000);
|
||||||
|
|
||||||
|
// called for each position from watchPosition
|
||||||
|
expect(mockClient.sendEvent).toHaveBeenCalledTimes(5);
|
||||||
|
expect(store.beaconHasWireError(alicesRoom1BeaconInfo.getType())).toBe(false);
|
||||||
|
expect(store.hasWireErrors()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('continues publishing positions when a beacon fails intermittently', async () => {
|
||||||
|
// every second event rejects
|
||||||
|
// meaning this beacon has more errors than the threshold
|
||||||
|
// but they are not consecutive
|
||||||
|
mockClient.sendEvent
|
||||||
|
.mockRejectedValueOnce(new Error('oups'))
|
||||||
|
.mockResolvedValueOnce({ event_id: '1' })
|
||||||
|
.mockRejectedValueOnce(new Error('oups'))
|
||||||
|
.mockResolvedValueOnce({ event_id: '1' })
|
||||||
|
.mockRejectedValueOnce(new Error('oups'));
|
||||||
|
|
||||||
|
makeRoomsWithStateEvents([
|
||||||
|
alicesRoom1BeaconInfo,
|
||||||
|
]);
|
||||||
|
const store = await makeOwnBeaconStore();
|
||||||
|
const emitSpy = jest.spyOn(store, 'emit');
|
||||||
|
// wait for store to settle
|
||||||
|
await flushPromisesWithFakeTimers();
|
||||||
|
|
||||||
|
await advanceAndFlushPromises(50000);
|
||||||
|
|
||||||
|
// called for each position from watchPosition
|
||||||
|
expect(mockClient.sendEvent).toHaveBeenCalledTimes(5);
|
||||||
|
expect(store.beaconHasWireError(alicesRoom1BeaconInfo.getType())).toBe(false);
|
||||||
|
expect(store.hasWireErrors()).toBe(false);
|
||||||
|
expect(emitSpy).not.toHaveBeenCalledWith(
|
||||||
|
OwnBeaconStoreEvent.WireError, alicesRoom1BeaconInfo.getType(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stops publishing positions when a beacon fails consistently', async () => {
|
||||||
|
// always fails to send events
|
||||||
|
mockClient.sendEvent.mockRejectedValue(new Error('oups'));
|
||||||
|
makeRoomsWithStateEvents([
|
||||||
|
alicesRoom1BeaconInfo,
|
||||||
|
]);
|
||||||
|
const store = await makeOwnBeaconStore();
|
||||||
|
const emitSpy = jest.spyOn(store, 'emit');
|
||||||
|
// wait for store to settle
|
||||||
|
await flushPromisesWithFakeTimers();
|
||||||
|
|
||||||
|
// 5 positions from watchPosition in this period
|
||||||
|
await advanceAndFlushPromises(50000);
|
||||||
|
|
||||||
|
// only two allowed failures
|
||||||
|
expect(mockClient.sendEvent).toHaveBeenCalledTimes(2);
|
||||||
|
expect(store.beaconHasWireError(alicesRoom1BeaconInfo.getType())).toBe(true);
|
||||||
|
expect(store.hasWireErrors()).toBe(true);
|
||||||
|
expect(emitSpy).toHaveBeenCalledWith(
|
||||||
|
OwnBeaconStoreEvent.WireError, alicesRoom1BeaconInfo.getType(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('restarts publishing a beacon after resetting wire error', async () => {
|
||||||
|
// always fails to send events
|
||||||
|
mockClient.sendEvent.mockRejectedValue(new Error('oups'));
|
||||||
|
makeRoomsWithStateEvents([
|
||||||
|
alicesRoom1BeaconInfo,
|
||||||
|
]);
|
||||||
|
const store = await makeOwnBeaconStore();
|
||||||
|
const emitSpy = jest.spyOn(store, 'emit');
|
||||||
|
// wait for store to settle
|
||||||
|
await flushPromisesWithFakeTimers();
|
||||||
|
|
||||||
|
// 3 positions from watchPosition in this period
|
||||||
|
await advanceAndFlushPromises(4000);
|
||||||
|
|
||||||
|
// only two allowed failures
|
||||||
|
expect(mockClient.sendEvent).toHaveBeenCalledTimes(2);
|
||||||
|
expect(store.beaconHasWireError(alicesRoom1BeaconInfo.getType())).toBe(true);
|
||||||
|
expect(store.hasWireErrors()).toBe(true);
|
||||||
|
expect(store.hasWireErrors(room1Id)).toBe(true);
|
||||||
|
expect(emitSpy).toHaveBeenCalledWith(
|
||||||
|
OwnBeaconStoreEvent.WireError, alicesRoom1BeaconInfo.getType(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// reset emitSpy mock counts to asser on wireError again
|
||||||
|
emitSpy.mockClear();
|
||||||
|
store.resetWireError(alicesRoom1BeaconInfo.getType());
|
||||||
|
|
||||||
|
expect(store.beaconHasWireError(alicesRoom1BeaconInfo.getType())).toBe(false);
|
||||||
|
|
||||||
|
// 2 more positions from watchPosition in this period
|
||||||
|
await advanceAndFlushPromises(10000);
|
||||||
|
|
||||||
|
// 2 from before, 2 new ones
|
||||||
|
expect(mockClient.sendEvent).toHaveBeenCalledTimes(4);
|
||||||
|
expect(emitSpy).toHaveBeenCalledWith(
|
||||||
|
OwnBeaconStoreEvent.WireError, alicesRoom1BeaconInfo.getType(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('publishes subsequent positions', async () => {
|
it('publishes subsequent positions', async () => {
|
||||||
// modern fake timers + debounce + promises are not friends
|
// modern fake timers + debounce + promises are not friends
|
||||||
// just testing that positions are published
|
// just testing that positions are published
|
||||||
|
|
|
@ -2,6 +2,7 @@ export * from './beacon';
|
||||||
export * from './client';
|
export * from './client';
|
||||||
export * from './location';
|
export * from './location';
|
||||||
export * from './platform';
|
export * from './platform';
|
||||||
|
export * from './room';
|
||||||
export * from './test-utils';
|
export * from './test-utils';
|
||||||
export * from './voice';
|
export * from './voice';
|
||||||
export * from './wrappers';
|
export * from './wrappers';
|
||||||
|
|
34
test/test-utils/room.ts
Normal file
34
test/test-utils/room.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
EventType,
|
||||||
|
} from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { mkEvent } from "./test-utils";
|
||||||
|
|
||||||
|
export const makeMembershipEvent = (
|
||||||
|
roomId: string, userId: string, membership = 'join',
|
||||||
|
) => mkEvent({
|
||||||
|
event: true,
|
||||||
|
type: EventType.RoomMember,
|
||||||
|
room: roomId,
|
||||||
|
user: userId,
|
||||||
|
skey: userId,
|
||||||
|
content: { membership },
|
||||||
|
ts: Date.now(),
|
||||||
|
});
|
||||||
|
|
|
@ -16,7 +16,11 @@ limitations under the License.
|
||||||
|
|
||||||
import { Beacon } from "matrix-js-sdk/src/matrix";
|
import { Beacon } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { msUntilExpiry, sortBeaconsByLatestExpiry } from "../../../src/utils/beacon";
|
import {
|
||||||
|
msUntilExpiry,
|
||||||
|
sortBeaconsByLatestExpiry,
|
||||||
|
sortBeaconsByLatestCreation,
|
||||||
|
} from "../../../src/utils/beacon";
|
||||||
import { makeBeaconInfoEvent } from "../../test-utils";
|
import { makeBeaconInfoEvent } from "../../test-utils";
|
||||||
|
|
||||||
describe('beacon utils', () => {
|
describe('beacon utils', () => {
|
||||||
|
@ -80,4 +84,35 @@ describe('beacon utils', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('sortBeaconsByLatestCreation()', () => {
|
||||||
|
const roomId = '!room:server';
|
||||||
|
const aliceId = '@alive:server';
|
||||||
|
|
||||||
|
// 12h old, 12h left
|
||||||
|
const beacon1 = new Beacon(makeBeaconInfoEvent(aliceId,
|
||||||
|
roomId,
|
||||||
|
{ timeout: HOUR_MS * 24, timestamp: now - 12 * HOUR_MS },
|
||||||
|
'$1',
|
||||||
|
));
|
||||||
|
// 10h left
|
||||||
|
const beacon2 = new Beacon(makeBeaconInfoEvent(aliceId,
|
||||||
|
roomId,
|
||||||
|
{ timeout: HOUR_MS * 10, timestamp: now },
|
||||||
|
'$2',
|
||||||
|
));
|
||||||
|
|
||||||
|
// 1ms left
|
||||||
|
const beacon3 = new Beacon(makeBeaconInfoEvent(aliceId,
|
||||||
|
roomId,
|
||||||
|
{ timeout: HOUR_MS + 1, timestamp: now - HOUR_MS },
|
||||||
|
'$3',
|
||||||
|
));
|
||||||
|
|
||||||
|
it('sorts beacons by descending creation time', () => {
|
||||||
|
expect([beacon1, beacon2, beacon3].sort(sortBeaconsByLatestCreation)).toEqual([
|
||||||
|
beacon2, beacon3, beacon1,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -2197,9 +2197,9 @@ ansi-escapes@^4.2.1:
|
||||||
type-fest "^0.21.3"
|
type-fest "^0.21.3"
|
||||||
|
|
||||||
ansi-regex@^4.1.0:
|
ansi-regex@^4.1.0:
|
||||||
version "4.1.0"
|
version "4.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
|
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed"
|
||||||
integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
|
integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==
|
||||||
|
|
||||||
ansi-regex@^5.0.0, ansi-regex@^5.0.1:
|
ansi-regex@^5.0.0, ansi-regex@^5.0.1:
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
|
|
Loading…
Reference in a new issue