{
{ _t(
"Debug logs contain application usage data including your " +
"username, the IDs or aliases of the rooms or groups you " +
- "have visited and the usernames of other users. They do " +
- "not contain messages.",
+ "have visited, which UI elements you last interacted with, " +
+ "and the usernames of other users. They do not contain messages.",
) }
@@ -211,7 +215,7 @@ export default class BugReportDialog extends React.Component {
{
a: (sub) =>
{ sub }
,
diff --git a/src/components/views/dialogs/CreateRoomDialog.tsx b/src/components/views/dialogs/CreateRoomDialog.tsx
index d573882f40..0da5f189bf 100644
--- a/src/components/views/dialogs/CreateRoomDialog.tsx
+++ b/src/components/views/dialogs/CreateRoomDialog.tsx
@@ -39,11 +39,13 @@ interface IProps {
defaultPublic?: boolean;
defaultName?: string;
parentSpace?: Room;
+ defaultEncrypted?: boolean;
onFinished(proceed: boolean, opts?: IOpts): void;
}
interface IState {
joinRule: JoinRule;
+ isPublic: boolean;
isEncrypted: boolean;
name: string;
topic: string;
@@ -74,8 +76,9 @@ export default class CreateRoomDialog extends React.Component {
const config = SdkConfig.get();
this.state = {
+ isPublic: this.props.defaultPublic || false,
+ isEncrypted: this.props.defaultEncrypted ?? privateShouldBeEncrypted(),
joinRule,
- isEncrypted: privateShouldBeEncrypted(),
name: this.props.defaultName || "",
topic: "",
alias: "",
diff --git a/src/components/views/dialogs/DeactivateAccountDialog.tsx b/src/components/views/dialogs/DeactivateAccountDialog.tsx
index 7221df222f..6548bd78fc 100644
--- a/src/components/views/dialogs/DeactivateAccountDialog.tsx
+++ b/src/components/views/dialogs/DeactivateAccountDialog.tsx
@@ -16,6 +16,7 @@ limitations under the License.
*/
import React from 'react';
+import { AuthType, IAuthData } from 'matrix-js-sdk/src/interactive-auth';
import Analytics from '../../../Analytics';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
@@ -65,7 +66,7 @@ export default class DeactivateAccountDialog extends React.Component {
+ private onStagePhaseChange = (stage: AuthType, phase: string): void => {
const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
body: _t("Confirm your account deactivation by using Single Sign On to prove your identity."),
@@ -115,7 +116,10 @@ export default class DeactivateAccountDialog extends React.Component {
+ private onUIAuthComplete = (auth: IAuthData): void => {
+ // XXX: this should be returning a promise to maintain the state inside the state machine correct
+ // but given that a deactivation is followed by a local logout and all object instances being thrown away
+ // this isn't done.
MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase).then(r => {
// Deactivation worked - logout & close this dialog
Analytics.trackEvent('Account', 'Deactivate Account');
@@ -180,7 +184,9 @@ export default class DeactivateAccountDialog extends React.Component {
const [rating, setRating] = useState("");
diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx
index 609829b833..1568e06720 100644
--- a/src/components/views/dialogs/InviteDialog.tsx
+++ b/src/components/views/dialogs/InviteDialog.tsx
@@ -55,7 +55,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
import { getAddressType } from "../../../UserAddress";
import BaseAvatar from '../avatars/BaseAvatar';
-import AccessibleButton from '../elements/AccessibleButton';
+import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
import { compare } from '../../../utils/strings';
import { IInvite3PID } from "matrix-js-sdk/src/@types/requests";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
@@ -394,6 +394,7 @@ export default class InviteDialog extends React.PureComponent void;
private debounceTimer: number = null; // actually number because we're in the browser
private editorRef = createRef();
+ private numberEntryFieldRef: React.RefObject = createRef();
private unmounted = false;
constructor(props) {
@@ -1283,13 +1284,27 @@ export default class InviteDialog extends React.PureComponent {
+ private onDigitPress = (digit: string, ev: ButtonEvent) => {
this.setState({ dialPadValue: this.state.dialPadValue + digit });
+
+ // Keep the number field focused so that keyboard entry is still available
+ // However, don't focus if this wasn't the result of directly clicking on the button,
+ // i.e someone using keyboard navigation.
+ if (ev.type === "click") {
+ this.numberEntryFieldRef.current?.focus();
+ }
};
- private onDeletePress = () => {
+ private onDeletePress = (ev: ButtonEvent) => {
if (this.state.dialPadValue.length === 0) return;
this.setState({ dialPadValue: this.state.dialPadValue.slice(0, -1) });
+
+ // Keep the number field focused so that keyboard entry is still available
+ // However, don't focus if this wasn't the result of directly clicking on the button,
+ // i.e someone using keyboard navigation.
+ if (ev.type === "click") {
+ this.numberEntryFieldRef.current?.focus();
+ }
};
private onTabChange = (tabId: TabId) => {
@@ -1543,6 +1558,7 @@ export default class InviteDialog extends React.PureComponent;
} else {
dialPadField =
{ this.props.showMenubar &&
-
-
- { this.props.showTitle && this._getTileTitle() }
-
-
- { this.props.showPopout && }
-
-
-
}
+
+
+ { this.props.showTitle && this._getTileTitle() }
+
+
+ { this.props.showPopout && }
+
+
+
}
{ appTileBody }
diff --git a/src/components/views/elements/DialPadBackspaceButton.tsx b/src/components/views/elements/DialPadBackspaceButton.tsx
index 69f0fcb39a..d64ced8239 100644
--- a/src/components/views/elements/DialPadBackspaceButton.tsx
+++ b/src/components/views/elements/DialPadBackspaceButton.tsx
@@ -15,11 +15,11 @@ limitations under the License.
*/
import * as React from "react";
-import AccessibleButton from "./AccessibleButton";
+import AccessibleButton, { ButtonEvent } from "./AccessibleButton";
interface IProps {
// Callback for when the button is pressed
- onBackspacePress: () => void;
+ onBackspacePress: (ev: ButtonEvent) => void;
}
export default class DialPadBackspaceButton extends React.PureComponent {
diff --git a/src/components/views/elements/ErrorBoundary.tsx b/src/components/views/elements/ErrorBoundary.tsx
index 334e569163..50ea7d9a56 100644
--- a/src/components/views/elements/ErrorBoundary.tsx
+++ b/src/components/views/elements/ErrorBoundary.tsx
@@ -71,12 +71,13 @@ export default class ErrorBoundary extends React.PureComponent<{}, IState> {
private onBugReport = (): void => {
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {
label: 'react-soft-crash',
+ error: this.state.error,
});
};
render() {
if (this.state.error) {
- const newIssueUrl = "https://github.com/vector-im/element-web/issues/new";
+ const newIssueUrl = "https://github.com/vector-im/element-web/issues/new/choose";
let bugReportSection;
if (SdkConfig.get().bug_report_endpoint_url) {
@@ -93,8 +94,9 @@ export default class ErrorBoundary extends React.PureComponent<{}, IState> {
"If you've submitted a bug via GitHub, debug logs can help " +
"us track down the problem. Debug logs contain application " +
"usage data including your username, the IDs or aliases of " +
- "the rooms or groups you have visited and the usernames of " +
- "other users. They do not contain messages.",
+ "the rooms or groups you have visited, which UI elements you " +
+ "last interacted with, and the usernames of other users. " +
+ "They do not contain messages.",
) }
{ _t("Submit debug logs") }
diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx
index bc868c35b3..8c9a3da060 100644
--- a/src/components/views/messages/CallEvent.tsx
+++ b/src/components/views/messages/CallEvent.tsx
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from 'react';
+import React, { createRef } from 'react';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { _t, _td } from '../../../languageHandler';
@@ -27,6 +27,8 @@ import classNames from 'classnames';
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
import { formatCallTime } from "../../../DateUtils";
+const MAX_NON_NARROW_WIDTH = 450 / 70 * 100;
+
interface IProps {
mxEvent: MatrixEvent;
callEventGrouper: CallEventGrouper;
@@ -35,6 +37,7 @@ interface IProps {
interface IState {
callState: CallState | CustomCallState;
silenced: boolean;
+ narrow: boolean;
}
const TEXTUAL_STATES: Map = new Map([
@@ -42,26 +45,42 @@ const TEXTUAL_STATES: Map = new Map([
[CallState.Connecting, _td("Connecting")],
]);
-export default class CallEvent extends React.Component {
+export default class CallEvent extends React.PureComponent {
+ private wrapperElement = createRef();
+ private resizeObserver: ResizeObserver;
+
constructor(props: IProps) {
super(props);
this.state = {
callState: this.props.callEventGrouper.state,
silenced: false,
+ narrow: false,
};
}
componentDidMount() {
this.props.callEventGrouper.addListener(CallEventGrouperEvent.StateChanged, this.onStateChanged);
this.props.callEventGrouper.addListener(CallEventGrouperEvent.SilencedChanged, this.onSilencedChanged);
+
+ this.resizeObserver = new ResizeObserver(this.resizeObserverCallback);
+ this.resizeObserver.observe(this.wrapperElement.current);
}
componentWillUnmount() {
this.props.callEventGrouper.removeListener(CallEventGrouperEvent.StateChanged, this.onStateChanged);
this.props.callEventGrouper.removeListener(CallEventGrouperEvent.SilencedChanged, this.onSilencedChanged);
+
+ this.resizeObserver.disconnect();
}
+ private resizeObserverCallback = (entries: ResizeObserverEntry[]): void => {
+ const wrapperElementEntry = entries.find((entry) => entry.target === this.wrapperElement.current);
+ if (!wrapperElementEntry) return;
+
+ this.setState({ narrow: wrapperElementEntry.contentRect.width < MAX_NON_NARROW_WIDTH });
+ };
+
private onSilencedChanged = (newState) => {
this.setState({ silenced: newState });
};
@@ -82,21 +101,32 @@ export default class CallEvent extends React.Component {
);
}
+ private renderSilenceIcon(): JSX.Element {
+ const silenceClass = classNames({
+ "mx_CallEvent_iconButton": true,
+ "mx_CallEvent_unSilence": this.state.silenced,
+ "mx_CallEvent_silence": !this.state.silenced,
+ });
+
+ return (
+
+ );
+ }
+
private renderContent(state: CallState | CustomCallState): JSX.Element {
if (state === CallState.Ringing) {
- const silenceClass = classNames({
- "mx_CallEvent_iconButton": true,
- "mx_CallEvent_unSilence": this.state.silenced,
- "mx_CallEvent_silence": !this.state.silenced,
- });
+ let silenceIcon;
+ if (!this.state.narrow) {
+ silenceIcon = this.renderSilenceIcon();
+ }
return (
-
+ { silenceIcon }
{
} else if (hangupReason === CallErrorCode.InviteTimeout) {
return (
- { _t("Missed call") }
+ { _t("No answer") }
{ this.renderCallBackButton(_t("Call back")) }
);
@@ -169,7 +199,7 @@ export default class CallEvent extends React.Component {
} else if (hangupReason === CallErrorCode.UserBusy) {
reason = _t("The user you called is busy.");
} else {
- reason = _t('Unknown failure: %(reason)s)', { reason: hangupReason });
+ reason = _t('Unknown failure: %(reason)s', { reason: hangupReason });
}
return (
@@ -215,35 +245,41 @@ export default class CallEvent extends React.Component {
const callState = this.state.callState;
const hangupReason = this.props.callEventGrouper.hangupReason;
const content = this.renderContent(callState);
- const className = classNames({
- mx_CallEvent: true,
+ const className = classNames("mx_CallEvent", {
mx_CallEvent_voice: isVoice,
mx_CallEvent_video: !isVoice,
- mx_CallEvent_missed: (
- callState === CustomCallState.Missed ||
- (callState === CallState.Ended && hangupReason === CallErrorCode.InviteTimeout)
- ),
+ mx_CallEvent_narrow: this.state.narrow,
+ mx_CallEvent_missed: callState === CustomCallState.Missed,
+ mx_CallEvent_noAnswer: callState === CallState.Ended && hangupReason === CallErrorCode.InviteTimeout,
+ mx_CallEvent_rejected: callState === CallState.Ended && this.props.callEventGrouper.gotRejected,
});
+ let silenceIcon;
+ if (this.state.narrow && this.state.callState === CallState.Ringing) {
+ silenceIcon = this.renderSilenceIcon();
+ }
return (
-
-
-
-
-
- { sender }
-
-
-
- { callType }
+
+
+ { silenceIcon }
+
+ { content }
- { content }
);
}
diff --git a/src/components/views/messages/MFileBody.tsx b/src/components/views/messages/MFileBody.tsx
index 13fc4b01e7..216a0f6cbf 100644
--- a/src/components/views/messages/MFileBody.tsx
+++ b/src/components/views/messages/MFileBody.tsx
@@ -178,7 +178,7 @@ export default class MFileBody extends React.Component
{
private onPlaceholderClick = async () => {
const mediaHelper = this.props.mediaEventHelper;
- if (mediaHelper.media.isEncrypted) {
+ if (mediaHelper?.media.isEncrypted) {
await this.decryptFile();
this.downloadFile(this.fileName, this.linkText);
} else {
@@ -192,7 +192,7 @@ export default class MFileBody extends React.Component {
};
public render() {
- const isEncrypted = this.props.mediaEventHelper.media.isEncrypted;
+ const isEncrypted = this.props.mediaEventHelper?.media.isEncrypted;
const contentUrl = this.getContentUrl();
const fileSize = this.content.info ? this.content.info.size : null;
const fileType = this.content.info ? this.content.info.mimetype : "application/octet-stream";
diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx
index e7b77b731f..8ead8d9ba2 100644
--- a/src/components/views/messages/MImageBody.tsx
+++ b/src/components/views/messages/MImageBody.tsx
@@ -47,6 +47,7 @@ interface IState {
};
hover: boolean;
showImage: boolean;
+ placeholder: 'no-image' | 'blurhash';
}
@replaceableComponent("views.messages.MImageBody")
@@ -68,6 +69,7 @@ export default class MImageBody extends React.Component {
loadedImageDimensions: null,
hover: false,
showImage: SettingsStore.getValue("showImages"),
+ placeholder: 'no-image',
};
}
@@ -277,6 +279,17 @@ export default class MImageBody extends React.Component {
this.downloadImage();
this.setState({ showImage: true });
} // else don't download anything because we don't want to display anything.
+
+ // Add a 150ms timer for blurhash to first appear.
+ if (this.media.isEncrypted) {
+ setTimeout(() => {
+ if (!this.state.imgLoaded || !this.state.imgError) {
+ this.setState({
+ placeholder: 'blurhash',
+ });
+ }
+ }, 150);
+ }
}
componentWillUnmount() {
@@ -380,7 +393,7 @@ export default class MImageBody extends React.Component {
const classes = classNames({
'mx_MImageBody_thumbnail': true,
- 'mx_MImageBody_thumbnail--blurhash': this.props.mxEvent.getContent().info[BLURHASH_FIELD],
+ 'mx_MImageBody_thumbnail--blurhash': this.props.mxEvent.getContent().info?.[BLURHASH_FIELD],
});
// This has incredibly broken types.
@@ -433,8 +446,15 @@ export default class MImageBody extends React.Component {
// Overidden by MStickerBody
protected getPlaceholder(width: number, height: number): JSX.Element {
- const blurhash = this.props.mxEvent.getContent().info[BLURHASH_FIELD];
- if (blurhash) return ;
+ const blurhash = this.props.mxEvent.getContent().info?.[BLURHASH_FIELD];
+
+ if (blurhash) {
+ if (this.state.placeholder === 'no-image') {
+ return ;
+ } else if (this.state.placeholder === 'blurhash') {
+ return ;
+ }
+ }
return (
);
diff --git a/src/components/views/messages/MStickerBody.tsx b/src/components/views/messages/MStickerBody.tsx
index 61be246ed9..365426245d 100644
--- a/src/components/views/messages/MStickerBody.tsx
+++ b/src/components/views/messages/MStickerBody.tsx
@@ -43,7 +43,7 @@ export default class MStickerBody extends MImageBody {
// Placeholder to show in place of the sticker image if
// img onLoad hasn't fired yet.
protected getPlaceholder(width: number, height: number): JSX.Element {
- if (this.props.mxEvent.getContent().info[BLURHASH_FIELD]) return super.getPlaceholder(width, height);
+ if (this.props.mxEvent.getContent().info?.[BLURHASH_FIELD]) return super.getPlaceholder(width, height);
return
;
}
diff --git a/src/components/views/messages/MVoiceOrAudioBody.tsx b/src/components/views/messages/MVoiceOrAudioBody.tsx
index 2d78ea192e..5a7e34b8a1 100644
--- a/src/components/views/messages/MVoiceOrAudioBody.tsx
+++ b/src/components/views/messages/MVoiceOrAudioBody.tsx
@@ -19,14 +19,12 @@ import MAudioBody from "./MAudioBody";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import MVoiceMessageBody from "./MVoiceMessageBody";
import { IBodyProps } from "./IBodyProps";
+import { isVoiceMessage } from "../../../utils/EventUtils";
@replaceableComponent("views.messages.MVoiceOrAudioBody")
export default class MVoiceOrAudioBody extends React.PureComponent {
public render() {
- // MSC2516 is a legacy identifier. See https://github.com/matrix-org/matrix-doc/pull/3245
- const isVoiceMessage = !!this.props.mxEvent.getContent()['org.matrix.msc2516.voice']
- || !!this.props.mxEvent.getContent()['org.matrix.msc3245.voice'];
- if (isVoiceMessage) {
+ if (isVoiceMessage(this.props.mxEvent)) {
return ;
} else {
return ;
diff --git a/src/components/views/messages/TileErrorBoundary.tsx b/src/components/views/messages/TileErrorBoundary.tsx
index c61771f396..a15806ae0c 100644
--- a/src/components/views/messages/TileErrorBoundary.tsx
+++ b/src/components/views/messages/TileErrorBoundary.tsx
@@ -51,6 +51,7 @@ export default class TileErrorBoundary extends React.Component {
private onBugReport = (): void => {
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {
label: 'react-soft-crash-tile',
+ error: this.state.error,
});
};
diff --git a/src/components/views/rooms/Autocomplete.tsx b/src/components/views/rooms/Autocomplete.tsx
index 6b5edcf91b..34909baef1 100644
--- a/src/components/views/rooms/Autocomplete.tsx
+++ b/src/components/views/rooms/Autocomplete.tsx
@@ -25,7 +25,6 @@ import SettingsStore from "../../../settings/SettingsStore";
import Autocompleter from '../../../autocomplete/Autocompleter';
import { replaceableComponent } from "../../../utils/replaceableComponent";
-const COMPOSER_SELECTED = 0;
const MAX_PROVIDER_MATCHES = 20;
export const generateCompletionDomId = (number) => `mx_Autocomplete_Completion_${number}`;
@@ -34,9 +33,9 @@ interface IProps {
// the query string for which to show autocomplete suggestions
query: string;
// method invoked with range and text content when completion is confirmed
- onConfirm: (ICompletion) => void;
+ onConfirm: (completion: ICompletion) => void;
// method invoked when selected (if any) completion changes
- onSelectionChange?: (ICompletion, number) => void;
+ onSelectionChange?: (partIndex: number) => void;
selection: ISelectionRange;
// The room in which we're autocompleting
room: Room;
@@ -71,7 +70,7 @@ export default class Autocomplete extends React.PureComponent {
completionList: [],
// how far down the completion list we are (THIS IS 1-INDEXED!)
- selectionOffset: COMPOSER_SELECTED,
+ selectionOffset: 1,
// whether we should show completions if they're available
shouldShowCompletions: true,
@@ -86,7 +85,7 @@ export default class Autocomplete extends React.PureComponent {
this.applyNewProps();
}
- private applyNewProps(oldQuery?: string, oldRoom?: Room) {
+ private applyNewProps(oldQuery?: string, oldRoom?: Room): void {
if (oldRoom && this.props.room.roomId !== oldRoom.roomId) {
this.autocompleter.destroy();
this.autocompleter = new Autocompleter(this.props.room);
@@ -104,7 +103,7 @@ export default class Autocomplete extends React.PureComponent {
this.autocompleter.destroy();
}
- complete(query: string, selection: ISelectionRange) {
+ private complete(query: string, selection: ISelectionRange): Promise {
this.queryRequested = query;
if (this.debounceCompletionsRequest) {
clearTimeout(this.debounceCompletionsRequest);
@@ -115,7 +114,7 @@ export default class Autocomplete extends React.PureComponent {
completions: [],
completionList: [],
// Reset selected completion
- selectionOffset: COMPOSER_SELECTED,
+ selectionOffset: 1,
// Hide the autocomplete box
hide: true,
});
@@ -135,7 +134,7 @@ export default class Autocomplete extends React.PureComponent {
});
}
- processQuery(query: string, selection: ISelectionRange) {
+ private processQuery(query: string, selection: ISelectionRange): Promise {
return this.autocompleter.getCompletions(
query, selection, this.state.forceComplete, MAX_PROVIDER_MATCHES,
).then((completions) => {
@@ -147,30 +146,35 @@ export default class Autocomplete extends React.PureComponent {
});
}
- processCompletions(completions: IProviderCompletions[]) {
+ private processCompletions(completions: IProviderCompletions[]): void {
const completionList = flatMap(completions, (provider) => provider.completions);
// Reset selection when completion list becomes empty.
- let selectionOffset = COMPOSER_SELECTED;
+ let selectionOffset = 1;
if (completionList.length > 0) {
/* If the currently selected completion is still in the completion list,
try to find it and jump to it. If not, select composer.
*/
- const currentSelection = this.state.selectionOffset === 0 ? null :
+ const currentSelection = this.state.selectionOffset <= 1 ? null :
this.state.completionList[this.state.selectionOffset - 1].completion;
selectionOffset = completionList.findIndex(
(completion) => completion.completion === currentSelection);
if (selectionOffset === -1) {
- selectionOffset = COMPOSER_SELECTED;
+ selectionOffset = 1;
} else {
selectionOffset++; // selectionOffset is 1-indexed!
}
}
- let hide = this.state.hide;
+ let hide = true;
// If `completion.command.command` is truthy, then a provider has matched with the query
const anyMatches = completions.some((completion) => !!completion.command.command);
- hide = !anyMatches;
+ if (anyMatches) {
+ hide = false;
+ if (this.props.onSelectionChange) {
+ this.props.onSelectionChange(selectionOffset - 1);
+ }
+ }
this.setState({
completions,
@@ -182,25 +186,25 @@ export default class Autocomplete extends React.PureComponent {
});
}
- hasSelection(): boolean {
+ public hasSelection(): boolean {
return this.countCompletions() > 0 && this.state.selectionOffset !== 0;
}
- countCompletions(): number {
+ public countCompletions(): number {
return this.state.completionList.length;
}
// called from MessageComposerInput
- moveSelection(delta: number) {
+ public moveSelection(delta: number): void {
const completionCount = this.countCompletions();
if (completionCount === 0) return; // there are no items to move the selection through
// Note: selectionOffset 0 represents the unsubstituted text, while 1 means first pill selected
- const index = (this.state.selectionOffset + delta + completionCount + 1) % (completionCount + 1);
- this.setSelection(index);
+ const index = (this.state.selectionOffset + delta + completionCount - 1) % completionCount;
+ this.setSelection(1 + index);
}
- onEscape(e: KeyboardEvent): boolean {
+ public onEscape(e: KeyboardEvent): boolean {
const completionCount = this.countCompletions();
if (completionCount === 0) {
// autocomplete is already empty, so don't preventDefault
@@ -213,16 +217,16 @@ export default class Autocomplete extends React.PureComponent {
this.hide();
}
- hide = () => {
+ private hide = (): void => {
this.setState({
hide: true,
- selectionOffset: 0,
+ selectionOffset: 1,
completions: [],
completionList: [],
});
};
- forceComplete() {
+ public forceComplete(): Promise {
return new Promise((resolve) => {
this.setState({
forceComplete: true,
@@ -235,8 +239,13 @@ export default class Autocomplete extends React.PureComponent {
});
}
- onCompletionClicked = (selectionOffset: number): boolean => {
- if (this.countCompletions() === 0 || selectionOffset === COMPOSER_SELECTED) {
+ public onConfirmCompletion = (): void => {
+ this.onCompletionClicked(this.state.selectionOffset);
+ };
+
+ private onCompletionClicked = (selectionOffset: number): boolean => {
+ const count = this.countCompletions();
+ if (count === 0 || selectionOffset < 1 || selectionOffset > count) {
return false;
}
@@ -246,10 +255,10 @@ export default class Autocomplete extends React.PureComponent {
return true;
};
- setSelection(selectionOffset: number) {
+ private setSelection(selectionOffset: number): void {
this.setState({ selectionOffset, hide: false });
if (this.props.onSelectionChange) {
- this.props.onSelectionChange(this.state.completionList[selectionOffset - 1], selectionOffset - 1);
+ this.props.onSelectionChange(selectionOffset - 1);
}
}
@@ -292,7 +301,7 @@ export default class Autocomplete extends React.PureComponent {
});
return completions.length > 0 ? (
-
+
{ completionResult.provider.getName() }
{ completionResult.provider.renderCompletions(completions) }
@@ -300,7 +309,7 @@ export default class Autocomplete extends React.PureComponent
{
}).filter((completion) => !!completion);
return !this.state.hide && renderedCompletions.length > 0 ? (
-
+
{ renderedCompletions }
) : null;
diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx
index 3f98d5d5e4..48f2e2a39b 100644
--- a/src/components/views/rooms/BasicMessageComposer.tsx
+++ b/src/components/views/rooms/BasicMessageComposer.tsx
@@ -133,6 +133,7 @@ export default class BasicMessageEditor extends React.Component
this.state = {
showPillAvatar: SettingsStore.getValue("Pill.shouldShowPillAvatar"),
surroundWith: SettingsStore.getValue("MessageComposerInput.surroundWith"),
+ showVisualBell: false,
};
this.emoticonSettingHandle = SettingsStore.watchSetting('MessageComposerInput.autoReplaceEmoji', null,
@@ -215,7 +216,11 @@ export default class BasicMessageEditor extends React.Component
if (isEmpty) {
this.formatBarRef.current.hide();
}
- this.setState({ autoComplete: this.props.model.autoComplete });
+ this.setState({
+ autoComplete: this.props.model.autoComplete,
+ // if a change is happening then clear the showVisualBell
+ showVisualBell: diff ? false : this.state.showVisualBell,
+ });
this.historyManager.tryPush(this.props.model, selection, inputType, diff);
let isTyping = !this.props.model.isEmpty;
@@ -435,7 +440,7 @@ export default class BasicMessageEditor extends React.Component
const model = this.props.model;
let handled = false;
- if (this.state.surroundWith && document.getSelection().type != "Caret") {
+ if (this.state.surroundWith && document.getSelection().type !== "Caret") {
// This surrounds the selected text with a character. This is
// intentionally left out of the keybinding manager as the keybinds
// here shouldn't be changeable
@@ -456,6 +461,44 @@ export default class BasicMessageEditor extends React.Component
}
}
+ const autocompleteAction = getKeyBindingsManager().getAutocompleteAction(event);
+ if (model.autoComplete?.hasCompletions()) {
+ const autoComplete = model.autoComplete;
+ switch (autocompleteAction) {
+ case AutocompleteAction.ForceComplete:
+ case AutocompleteAction.Complete:
+ autoComplete.confirmCompletion();
+ handled = true;
+ break;
+ case AutocompleteAction.PrevSelection:
+ autoComplete.selectPreviousSelection();
+ handled = true;
+ break;
+ case AutocompleteAction.NextSelection:
+ autoComplete.selectNextSelection();
+ handled = true;
+ break;
+ case AutocompleteAction.Cancel:
+ autoComplete.onEscape(event);
+ handled = true;
+ break;
+ default:
+ return; // don't preventDefault on anything else
+ }
+ } else if (autocompleteAction === AutocompleteAction.ForceComplete && !this.state.showVisualBell) {
+ // there is no current autocomplete window, try to open it
+ this.tabCompleteName();
+ handled = true;
+ } else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) {
+ this.formatBarRef.current.hide();
+ }
+
+ if (handled) {
+ event.preventDefault();
+ event.stopPropagation();
+ return;
+ }
+
const action = getKeyBindingsManager().getMessageComposerAction(event);
switch (action) {
case MessageComposerAction.FormatBold:
@@ -507,42 +550,6 @@ export default class BasicMessageEditor extends React.Component
handled = true;
break;
}
- if (handled) {
- event.preventDefault();
- event.stopPropagation();
- return;
- }
-
- const autocompleteAction = getKeyBindingsManager().getAutocompleteAction(event);
- if (model.autoComplete && model.autoComplete.hasCompletions()) {
- const autoComplete = model.autoComplete;
- switch (autocompleteAction) {
- case AutocompleteAction.CompleteOrPrevSelection:
- case AutocompleteAction.PrevSelection:
- autoComplete.selectPreviousSelection();
- handled = true;
- break;
- case AutocompleteAction.CompleteOrNextSelection:
- case AutocompleteAction.NextSelection:
- autoComplete.selectNextSelection();
- handled = true;
- break;
- case AutocompleteAction.Cancel:
- autoComplete.onEscape(event);
- handled = true;
- break;
- default:
- return; // don't preventDefault on anything else
- }
- } else if (autocompleteAction === AutocompleteAction.CompleteOrPrevSelection
- || autocompleteAction === AutocompleteAction.CompleteOrNextSelection) {
- // there is no current autocomplete window, try to open it
- this.tabCompleteName();
- handled = true;
- } else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) {
- this.formatBarRef.current.hide();
- }
-
if (handled) {
event.preventDefault();
event.stopPropagation();
@@ -577,6 +584,8 @@ export default class BasicMessageEditor extends React.Component
this.setState({ showVisualBell: true });
model.autoComplete.close();
}
+ } else {
+ this.setState({ showVisualBell: true });
}
} catch (err) {
console.error(err);
@@ -592,9 +601,8 @@ export default class BasicMessageEditor extends React.Component
this.props.model.autoComplete.onComponentConfirm(completion);
};
- private onAutoCompleteSelectionChange = (completion: ICompletion, completionIndex: number): void => {
+ private onAutoCompleteSelectionChange = (completionIndex: number): void => {
this.modifiedFlag = true;
- this.props.model.autoComplete.onComponentSelectionChange(completion);
this.setState({ completionIndex });
};
@@ -718,6 +726,11 @@ export default class BasicMessageEditor extends React.Component
};
const { completionIndex } = this.state;
+ const hasAutocomplete = Boolean(this.state.autoComplete);
+ let activeDescendant;
+ if (hasAutocomplete && completionIndex >= 0) {
+ activeDescendant = generateCompletionDomId(completionIndex);
+ }
return (
{ autoComplete }
@@ -736,10 +749,11 @@ export default class BasicMessageEditor extends React.Component
aria-label={this.props.label}
role="textbox"
aria-multiline="true"
- aria-autocomplete="both"
+ aria-autocomplete="list"
aria-haspopup="listbox"
- aria-expanded={Boolean(this.state.autoComplete)}
- aria-activedescendant={completionIndex >= 0 ? generateCompletionDomId(completionIndex) : undefined}
+ aria-expanded={hasAutocomplete}
+ aria-owns="mx_Autocomplete"
+ aria-activedescendant={activeDescendant}
dir="auto"
aria-disabled={this.props.disabled}
/>
diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx
index 884d004551..dd954e46ce 100644
--- a/src/components/views/rooms/EventTile.tsx
+++ b/src/components/views/rooms/EventTile.tsx
@@ -856,13 +856,19 @@ export default class EventTile extends React.Component {
render() {
const msgtype = this.props.mxEvent.getContent().msgtype;
- const { tileHandler, isBubbleMessage, isInfoMessage } = getEventDisplayInfo(this.props.mxEvent);
+ const eventType = this.props.mxEvent.getType() as EventType;
+ const {
+ tileHandler,
+ isBubbleMessage,
+ isInfoMessage,
+ isLeftAlignedBubbleMessage,
+ } = getEventDisplayInfo(this.props.mxEvent);
// This shouldn't happen: the caller should check we support this type
// before trying to instantiate us
if (!tileHandler) {
const { mxEvent } = this.props;
- console.warn(`Event type not supported: type:${mxEvent.getType()} isState:${mxEvent.isState()}`);
+ console.warn(`Event type not supported: type:${eventType} isState:${mxEvent.isState()}`);
return
{ _t('This event could not be displayed') }
@@ -878,6 +884,7 @@ export default class EventTile extends React.Component
{
const isEditing = !!this.props.editState;
const classes = classNames({
mx_EventTile_bubbleContainer: isBubbleMessage,
+ mx_EventTile_leftAlignedBubble: isLeftAlignedBubbleMessage,
mx_EventTile: true,
mx_EventTile_isEditing: isEditing,
mx_EventTile_info: isInfoMessage,
@@ -886,7 +893,10 @@ export default class EventTile extends React.Component {
mx_EventTile_sending: !isEditing && isSending,
mx_EventTile_highlight: this.props.tileShape === TileShape.Notif ? false : this.shouldHighlight(),
mx_EventTile_selected: this.props.isSelectedEvent,
- mx_EventTile_continuation: this.props.tileShape ? '' : this.props.continuation,
+ mx_EventTile_continuation: (
+ (this.props.tileShape ? '' : this.props.continuation) ||
+ eventType === EventType.CallInvite
+ ),
mx_EventTile_last: this.props.last,
mx_EventTile_lastInSection: this.props.lastInSection,
mx_EventTile_contextual: this.props.contextual,
@@ -932,8 +942,11 @@ export default class EventTile extends React.Component {
} else if (this.props.layout == Layout.IRC) {
avatarSize = 14;
needsSenderProfile = true;
- } else if (this.props.continuation && this.props.tileShape !== TileShape.FileGrid) {
- // no avatar or sender profile for continuation messages
+ } else if (
+ (this.props.continuation && this.props.tileShape !== TileShape.FileGrid) ||
+ eventType === EventType.CallInvite
+ ) {
+ // no avatar or sender profile for continuation messages and call tiles
avatarSize = 0;
needsSenderProfile = false;
} else {
diff --git a/src/components/views/rooms/MemberList.tsx b/src/components/views/rooms/MemberList.tsx
index 0c90d2ee09..415d7e942b 100644
--- a/src/components/views/rooms/MemberList.tsx
+++ b/src/components/views/rooms/MemberList.tsx
@@ -45,6 +45,8 @@ import BaseAvatar from '../avatars/BaseAvatar';
import { throttle } from 'lodash';
import SpaceStore from "../../../stores/SpaceStore";
+const getSearchQueryLSKey = (roomId: string) => `mx_MemberList_searchQuarry_${roomId}`;
+
const INITIAL_LOAD_NUM_MEMBERS = 30;
const INITIAL_LOAD_NUM_INVITED = 5;
const SHOW_MORE_INCREMENT = 100;
@@ -171,6 +173,13 @@ export default class MemberList extends React.Component {
}
private getMembersState(members: Array): IState {
+ let searchQuery;
+ try {
+ searchQuery = window.localStorage.getItem(getSearchQueryLSKey(this.props.roomId));
+ } catch (error) {
+ console.warn("Failed to get last the MemberList search query", error);
+ }
+
// set the state after determining showPresence to make sure it's
// taken into account while rendering
return {
@@ -184,7 +193,7 @@ export default class MemberList extends React.Component {
// in practice I find that a little constraining
truncateAtJoined: INITIAL_LOAD_NUM_MEMBERS,
truncateAtInvited: INITIAL_LOAD_NUM_INVITED,
- searchQuery: "",
+ searchQuery: searchQuery ?? "",
};
}
@@ -414,6 +423,12 @@ export default class MemberList extends React.Component {
};
private onSearchQueryChanged = (searchQuery: string): void => {
+ try {
+ window.localStorage.setItem(getSearchQueryLSKey(this.props.roomId), searchQuery);
+ } catch (error) {
+ console.warn("Failed to set the last MemberList search query", error);
+ }
+
this.setState({
searchQuery,
filteredJoinedMembers: this.filterMembers(this.state.members, 'join', searchQuery),
@@ -554,7 +569,9 @@ export default class MemberList extends React.Component {
+ onSearch={this.onSearchQueryChanged}
+ initialValue={this.state.searchQuery}
+ />
);
let previousPhase = RightPanelPhases.RoomSummary;
diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx
index 674bcdaec2..8a96b8a9ba 100644
--- a/src/components/views/rooms/NewRoomIntro.tsx
+++ b/src/components/views/rooms/NewRoomIntro.tsx
@@ -36,6 +36,7 @@ import { showSpaceInvite } from "../../../utils/space";
import { privateShouldBeEncrypted } from "../../../createRoom";
import EventTileBubble from "../messages/EventTileBubble";
import { ROOM_SECURITY_TAB } from "../dialogs/RoomSettingsDialog";
+import { MatrixClientPeg } from "../../../MatrixClientPeg";
function hasExpectedEncryptionSettings(matrixClient: MatrixClient, room: Room): boolean {
const isEncrypted: boolean = matrixClient.isRoomEncrypted(room.roomId);
@@ -191,11 +192,21 @@ const NewRoomIntro = () => {
});
}
- const sub2 = _t(
+ const subText = _t(
"Your private messages are normally encrypted, but this room isn't. "+
"Usually this is due to an unsupported device or method being used, " +
- "like email invites. Enable encryption in settings.", {},
- { a: sub => { sub } },
+ "like email invites.",
+ );
+
+ let subButton;
+ if (room.currentState.mayClientSendStateEvent(EventType.RoomEncryption, MatrixClientPeg.get())) {
+ subButton = (
+ { _t("Enable encryption in settings.") }
+ );
+ }
+
+ const subtitle = (
+ { subText } { subButton }
);
return
@@ -204,7 +215,7 @@ const NewRoomIntro = () => {
) }
diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx
index 8c0e09c76c..cf7d1ce945 100644
--- a/src/components/views/rooms/ReplyTile.tsx
+++ b/src/components/views/rooms/ReplyTile.tsx
@@ -25,8 +25,9 @@ import MImageReplyBody from "../messages/MImageReplyBody";
import * as sdk from '../../../index';
import { EventType, MsgType } from 'matrix-js-sdk/src/@types/event';
import { replaceableComponent } from '../../../utils/replaceableComponent';
-import { getEventDisplayInfo } from '../../../utils/EventUtils';
+import { getEventDisplayInfo, isVoiceMessage } from '../../../utils/EventUtils';
import MFileBody from "../messages/MFileBody";
+import MVoiceMessageBody from "../messages/MVoiceMessageBody";
interface IProps {
mxEvent: MatrixEvent;
@@ -95,7 +96,7 @@ export default class ReplyTile extends React.PureComponent {
const msgType = mxEvent.getContent().msgtype;
const evType = mxEvent.getType() as EventType;
- const { tileHandler, isInfoMessage } = getEventDisplayInfo(this.props.mxEvent);
+ const { tileHandler, isInfoMessage } = getEventDisplayInfo(mxEvent);
// This shouldn't happen: the caller should check we support this type
// before trying to instantiate us
if (!tileHandler) {
@@ -109,14 +110,14 @@ export default class ReplyTile extends React.PureComponent {
const EventTileType = sdk.getComponent(tileHandler);
const classes = classNames("mx_ReplyTile", {
- mx_ReplyTile_info: isInfoMessage && !this.props.mxEvent.isRedacted(),
+ mx_ReplyTile_info: isInfoMessage && !mxEvent.isRedacted(),
mx_ReplyTile_audio: msgType === MsgType.Audio,
mx_ReplyTile_video: msgType === MsgType.Video,
});
let permalink = "#";
if (this.props.permalinkCreator) {
- permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
+ permalink = this.props.permalinkCreator.forEvent(mxEvent.getId());
}
let sender;
@@ -129,7 +130,7 @@ export default class ReplyTile extends React.PureComponent {
if (needsSenderProfile) {
sender = ;
}
@@ -137,7 +138,7 @@ export default class ReplyTile extends React.PureComponent {
const msgtypeOverrides = {
[MsgType.Image]: MImageReplyBody,
// Override audio and video body with file body. We also hide the download/decrypt button using CSS
- [MsgType.Audio]: MFileBody,
+ [MsgType.Audio]: isVoiceMessage(mxEvent) ? MVoiceMessageBody : MFileBody,
[MsgType.Video]: MFileBody,
};
const evOverrides = {
@@ -151,14 +152,14 @@ export default class ReplyTile extends React.PureComponent {
{ sender }
diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.tsx
similarity index 78%
rename from src/components/views/settings/CrossSigningPanel.js
rename to src/components/views/settings/CrossSigningPanel.tsx
index 8b9d68bfa5..3fd67d6b5d 100644
--- a/src/components/views/settings/CrossSigningPanel.js
+++ b/src/components/views/settings/CrossSigningPanel.tsx
@@ -24,36 +24,41 @@ import Spinner from '../elements/Spinner';
import InteractiveAuthDialog from '../dialogs/InteractiveAuthDialog';
import ConfirmDestroyCrossSigningDialog from '../dialogs/security/ConfirmDestroyCrossSigningDialog';
import { replaceableComponent } from "../../../utils/replaceableComponent";
+import { MatrixEvent } from 'matrix-js-sdk/src';
+import SetupEncryptionDialog from '../dialogs/security/SetupEncryptionDialog';
+import { accessSecretStorage } from '../../../SecurityManager';
+
+interface IState {
+ error?: Error;
+ crossSigningPublicKeysOnDevice?: boolean;
+ crossSigningPrivateKeysInStorage?: boolean;
+ masterPrivateKeyCached?: boolean;
+ selfSigningPrivateKeyCached?: boolean;
+ userSigningPrivateKeyCached?: boolean;
+ homeserverSupportsCrossSigning?: boolean;
+ crossSigningReady?: boolean;
+}
@replaceableComponent("views.settings.CrossSigningPanel")
-export default class CrossSigningPanel extends React.PureComponent {
+export default class CrossSigningPanel extends React.PureComponent<{}, IState> {
+ private unmounted = false;
+
constructor(props) {
super(props);
- this._unmounted = false;
-
- this.state = {
- error: null,
- crossSigningPublicKeysOnDevice: null,
- crossSigningPrivateKeysInStorage: null,
- masterPrivateKeyCached: null,
- selfSigningPrivateKeyCached: null,
- userSigningPrivateKeyCached: null,
- homeserverSupportsCrossSigning: null,
- crossSigningReady: null,
- };
+ this.state = {};
}
- componentDidMount() {
+ public componentDidMount() {
const cli = MatrixClientPeg.get();
cli.on("accountData", this.onAccountData);
cli.on("userTrustStatusChanged", this.onStatusChanged);
cli.on("crossSigning.keysChanged", this.onStatusChanged);
- this._getUpdatedStatus();
+ this.getUpdatedStatus();
}
- componentWillUnmount() {
- this._unmounted = true;
+ public componentWillUnmount() {
+ this.unmounted = true;
const cli = MatrixClientPeg.get();
if (!cli) return;
cli.removeListener("accountData", this.onAccountData);
@@ -61,28 +66,37 @@ export default class CrossSigningPanel extends React.PureComponent {
cli.removeListener("crossSigning.keysChanged", this.onStatusChanged);
}
- onAccountData = (event) => {
+ private onAccountData = (event: MatrixEvent): void => {
const type = event.getType();
if (type.startsWith("m.cross_signing") || type.startsWith("m.secret_storage")) {
- this._getUpdatedStatus();
+ this.getUpdatedStatus();
}
};
- _onBootstrapClick = () => {
- this._bootstrapCrossSigning({ forceReset: false });
+ private onBootstrapClick = () => {
+ if (this.state.crossSigningPrivateKeysInStorage) {
+ Modal.createTrackedDialog(
+ "Verify session", "Verify session", SetupEncryptionDialog,
+ {}, null, /* priority = */ false, /* static = */ true,
+ );
+ } else {
+ // Trigger the flow to set up secure backup, which is what this will do when in
+ // the appropriate state.
+ accessSecretStorage();
+ }
};
- onStatusChanged = () => {
- this._getUpdatedStatus();
+ private onStatusChanged = () => {
+ this.getUpdatedStatus();
};
- async _getUpdatedStatus() {
+ private async getUpdatedStatus(): Promise {
const cli = MatrixClientPeg.get();
const pkCache = cli.getCrossSigningCacheCallbacks();
const crossSigning = cli.crypto.crossSigningInfo;
const secretStorage = cli.crypto.secretStorage;
- const crossSigningPublicKeysOnDevice = crossSigning.getId();
- const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage);
+ const crossSigningPublicKeysOnDevice = Boolean(crossSigning.getId());
+ const crossSigningPrivateKeysInStorage = Boolean(await crossSigning.isStoredInSecretStorage(secretStorage));
const masterPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("master"));
const selfSigningPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("self_signing"));
const userSigningPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("user_signing"));
@@ -110,8 +124,8 @@ export default class CrossSigningPanel extends React.PureComponent {
* 3. All keys are loaded and there's nothing to do.
* @param {bool} [forceReset] Bootstrap again even if keys already present
*/
- _bootstrapCrossSigning = async ({ forceReset = false }) => {
- this.setState({ error: null });
+ private bootstrapCrossSigning = async ({ forceReset = false }): Promise => {
+ this.setState({ error: undefined });
try {
const cli = MatrixClientPeg.get();
await cli.bootstrapCrossSigning({
@@ -135,20 +149,20 @@ export default class CrossSigningPanel extends React.PureComponent {
this.setState({ error: e });
console.error("Error bootstrapping cross-signing", e);
}
- if (this._unmounted) return;
- this._getUpdatedStatus();
- }
+ if (this.unmounted) return;
+ this.getUpdatedStatus();
+ };
- _resetCrossSigning = () => {
+ private resetCrossSigning = (): void => {
Modal.createDialog(ConfirmDestroyCrossSigningDialog, {
onFinished: (act) => {
if (!act) return;
- this._bootstrapCrossSigning({ forceReset: true });
+ this.bootstrapCrossSigning({ forceReset: true });
},
});
- }
+ };
- render() {
+ public render() {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
const {
error,
@@ -173,10 +187,14 @@ export default class CrossSigningPanel extends React.PureComponent {
summarisedStatus = { _t(
"Your homeserver does not support cross-signing.",
) }
;
- } else if (crossSigningReady) {
+ } else if (crossSigningReady && crossSigningPrivateKeysInStorage) {
summarisedStatus = ✅ { _t(
"Cross-signing is ready for use.",
) }
;
+ } else if (crossSigningReady && !crossSigningPrivateKeysInStorage) {
+ summarisedStatus = ⚠️ { _t(
+ "Cross-signing is ready but keys are not backed up.",
+ ) }
;
} else if (crossSigningPrivateKeysInStorage) {
summarisedStatus = { _t(
"Your account has a cross-signing identity in secret storage, " +
@@ -207,16 +225,20 @@ export default class CrossSigningPanel extends React.PureComponent {
// TODO: determine how better to expose this to users in addition to prompts at login/toast
if (!keysExistEverywhere && homeserverSupportsCrossSigning) {
+ let buttonCaption = _t("Set up Secure Backup");
+ if (crossSigningPrivateKeysInStorage) {
+ buttonCaption = _t("Verify this session");
+ }
actions.push(
-
- { _t("Set up") }
+
+ { buttonCaption }
,
);
}
if (keysExistAnywhere) {
actions.push(
-
+
{ _t("Reset") }
,
);
diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx
index ede9a5ddb5..d9e97d570b 100644
--- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx
+++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx
@@ -37,6 +37,8 @@ import RoomUpgradeWarningDialog from '../../../dialogs/RoomUpgradeWarningDialog'
import { upgradeRoom } from "../../../../../utils/RoomUpgrade";
import { arrayHasDiff } from "../../../../../utils/arrays";
import SettingsFlag from '../../../elements/SettingsFlag';
+import createRoom, { IOpts } from '../../../../../createRoom';
+import CreateRoomDialog from '../../../dialogs/CreateRoomDialog';
interface IProps {
roomId: string;
@@ -129,7 +131,38 @@ export default class SecurityRoomSettingsTab extends React.Component {
+ private onEncryptionChange = async () => {
+ if (this.state.joinRule == "public") {
+ const dialog = Modal.createTrackedDialog('Confirm Public Encrypted Room', '', QuestionDialog, {
+ title: _t('Are you sure you want to add encryption to this public room?'),
+ description:
+
{ _t(
+ "It's not recommended to add encryption to public rooms." +
+ "Anyone can find and join public rooms, so anyone can read messages in them. " +
+ "You'll get none of the benefits of encryption, and you won't be able to turn it " +
+ "off later. Encrypting messages in a public room will make receiving and sending " +
+ "messages slower.",
+ null,
+ { "b": (sub) => { sub } },
+ ) }
+
{ _t(
+ "To avoid these issues, create a new encrypted room for " +
+ "the conversation you plan to have.",
+ null,
+ { "a": (sub) => {
+ dialog.close();
+ this.createNewRoom(false, true);
+ }}> { sub } },
+ ) }
+
,
+
+ });
+
+ const { finished } = dialog;
+ const [confirm] = await finished;
+ if (!confirm) return;
+ }
+
Modal.createTrackedDialog('Enable encryption', '', QuestionDialog, {
title: _t('Enable encryption?'),
description: _t(
@@ -194,6 +227,41 @@ export default class SecurityRoomSettingsTab extends React.Component
+ { _t(
+ "It's not recommended to make encrypted rooms public. " +
+ "It will mean anyone can find and join the room, so anyone can read messages. " +
+ "You'll get none of the benefits of encryption. Encrypting messages in a public " +
+ "room will make receiving and sending messages slower.",
+ null,
+ { "b": (sub) => { sub } },
+ ) }
+ { _t(
+ "To avoid these issues, create a new public room for the conversation " +
+ "you plan to have.",
+ null,
+ {
+ "a": (sub) => {
+ dialog.close();
+ this.createNewRoom(true, false);
+ }}> { sub } ,
+ },
+ ) }
+
,
+ });
+
+ const { finished } = dialog;
+ const [confirm] = await finished;
+ if (!confirm) return;
+ }
+
if (beforeJoinRule === joinRule && !restrictedAllowRoomIds) return;
const content: IContent = {
@@ -254,6 +322,20 @@ export default class SecurityRoomSettingsTab extends React.Component
{
+ const modal = Modal.createTrackedDialog<[boolean, IOpts]>(
+ "Create Room",
+ "Create room after trying to make an E2EE room public",
+ CreateRoomDialog,
+ { defaultPublic, defaultEncrypted },
+ );
+ const [shouldCreate, opts] = await modal.finished;
+ if (shouldCreate) {
+ await createRoom(opts);
+ }
+ return shouldCreate;
+ };
+
private onHistoryRadioToggle = (history: HistoryVisibility) => {
const beforeHistory = this.state.history;
if (beforeHistory === history) return;
diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx
index 904fdf0914..6984ccc6f3 100644
--- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx
+++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx
@@ -268,7 +268,8 @@ export default class HelpUserSettingsTab extends React.Component
"If you've submitted a bug via GitHub, debug logs can help " +
"us track down the problem. Debug logs contain application " +
"usage data including your username, the IDs or aliases of " +
- "the rooms or groups you have visited and the usernames of " +
+ "the rooms or groups you have visited, which UI elements you " +
+ "last interacted with, and the usernames of " +
"other users. They do not contain messages.",
) }
diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx
index 40016af36f..d2e09c0d69 100644
--- a/src/components/views/spaces/SpacePanel.tsx
+++ b/src/components/views/spaces/SpacePanel.tsx
@@ -14,7 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { ComponentProps, Dispatch, ReactNode, SetStateAction, useEffect, useState } from "react";
+import React, {
+ ComponentProps,
+ Dispatch,
+ ReactNode,
+ SetStateAction,
+ useEffect,
+ useLayoutEffect,
+ useRef,
+ useState,
+} from "react";
import { DragDropContext, Draggable, Droppable } from "react-beautiful-dnd";
import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room";
@@ -43,6 +52,7 @@ import IconizedContextMenu, {
} from "../context_menus/IconizedContextMenu";
import SettingsStore from "../../../settings/SettingsStore";
import { SettingLevel } from "../../../settings/SettingLevel";
+import UIStore from "../../../stores/UIStore";
const useSpaces = (): [Room[], Room[], Room | null] => {
const invites = useEventEmitterState
(SpaceStore.instance, UPDATE_INVITED_SPACES, () => {
@@ -206,6 +216,11 @@ const InnerSpacePanel = React.memo(({ children, isPanelCo
const SpacePanel = () => {
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
+ const ref = useRef();
+ useLayoutEffect(() => {
+ UIStore.instance.trackElementDimensions("SpacePanel", ref.current);
+ return () => UIStore.instance.stopTrackingElementDimensions("SpacePanel");
+ }, []);
const onKeyDown = (ev: React.KeyboardEvent) => {
let handled = true;
@@ -280,6 +295,7 @@ const SpacePanel = () => {
onKeyDown={onKeyDownHandler}
role="tree"
aria-label={_t("Spaces")}
+ ref={ref}
>
{ (provided, snapshot) => (
diff --git a/src/components/views/voip/AudioFeed.tsx b/src/components/views/voip/AudioFeed.tsx
index 3049d80c72..d6d1261343 100644
--- a/src/components/views/voip/AudioFeed.tsx
+++ b/src/components/views/voip/AudioFeed.tsx
@@ -72,7 +72,7 @@ export default class AudioFeed extends React.Component {
}
};
- private playMedia() {
+ private async playMedia() {
const element = this.element.current;
if (!element) return;
this.onAudioOutputChanged(MediaDeviceHandler.getAudioOutput());
@@ -90,7 +90,7 @@ export default class AudioFeed extends React.Component {
// should serialise the ones that need to be serialised but then be able to interrupt
// them with another load() which will cancel the pending one, but since we don't call
// load() explicitly, it shouldn't be a problem. - Dave
- element.play();
+ await element.load();
} catch (e) {
logger.info("Failed to play media element with feed", this.props.feed, e);
}
diff --git a/src/components/views/voip/AudioFeedArrayForCall.tsx b/src/components/views/voip/AudioFeedArrayForCall.tsx
index 958ac2a8d4..a7dd0283ff 100644
--- a/src/components/views/voip/AudioFeedArrayForCall.tsx
+++ b/src/components/views/voip/AudioFeedArrayForCall.tsx
@@ -32,7 +32,7 @@ export default class AudioFeedArrayForCall extends React.Component {
constructor(props: IProps) {
super(props);
- const { primary, secondary } = this.getOrderedFeeds(this.props.call.getFeeds());
+ const { primary, secondary } = CallView.getOrderedFeeds(this.props.call.getFeeds());
this.state = {
isLocalOnHold: this.props.call.isLocalOnHold(),
@@ -147,7 +147,16 @@ export default class CallView extends React.Component {
dis.unregister(this.dispatcherRef);
}
- public componentDidUpdate(prevProps) {
+ static getDerivedStateFromProps(props: IProps): Partial {
+ const { primary, secondary } = CallView.getOrderedFeeds(props.call.getFeeds());
+
+ return {
+ primaryFeed: primary,
+ secondaryFeeds: secondary,
+ };
+ }
+
+ public componentDidUpdate(prevProps: IProps): void {
if (this.props.call === prevProps.call) return;
this.setState({
@@ -201,7 +210,7 @@ export default class CallView extends React.Component {
};
private onFeedsChanged = (newFeeds: Array) => {
- const { primary, secondary } = this.getOrderedFeeds(newFeeds);
+ const { primary, secondary } = CallView.getOrderedFeeds(newFeeds);
this.setState({
primaryFeed: primary,
secondaryFeeds: secondary,
@@ -226,7 +235,7 @@ export default class CallView extends React.Component {
this.buttonsRef.current?.showControls();
};
- private getOrderedFeeds(feeds: Array): { primary: CallFeed, secondary: Array } {
+ static getOrderedFeeds(feeds: Array): { primary: CallFeed, secondary: Array } {
let primary;
// Try to use a screensharing as primary, a remote one if possible
diff --git a/src/components/views/voip/DialPad.tsx b/src/components/views/voip/DialPad.tsx
index 3b4a29b3f9..46584e0870 100644
--- a/src/components/views/voip/DialPad.tsx
+++ b/src/components/views/voip/DialPad.tsx
@@ -15,7 +15,7 @@ limitations under the License.
*/
import * as React from "react";
-import AccessibleButton from "../elements/AccessibleButton";
+import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
import { replaceableComponent } from "../../../utils/replaceableComponent";
const BUTTONS = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#'];
@@ -30,12 +30,12 @@ interface IButtonProps {
kind: DialPadButtonKind;
digit?: string;
digitSubtext?: string;
- onButtonPress: (string) => void;
+ onButtonPress: (digit: string, ev: ButtonEvent) => void;
}
class DialPadButton extends React.PureComponent {
- onClick = () => {
- this.props.onButtonPress(this.props.digit);
+ onClick = (ev: ButtonEvent) => {
+ this.props.onButtonPress(this.props.digit, ev);
};
render() {
@@ -54,10 +54,10 @@ class DialPadButton extends React.PureComponent {
}
interface IProps {
- onDigitPress: (string) => void;
+ onDigitPress: (digit: string, ev: ButtonEvent) => void;
hasDial: boolean;
- onDeletePress?: (string) => void;
- onDialPress?: (string) => void;
+ onDeletePress?: (ev: ButtonEvent) => void;
+ onDialPress?: () => void;
}
@replaceableComponent("views.voip.DialPad")
diff --git a/src/components/views/voip/DialPadModal.tsx b/src/components/views/voip/DialPadModal.tsx
index a36fc37dff..4d69260565 100644
--- a/src/components/views/voip/DialPadModal.tsx
+++ b/src/components/views/voip/DialPadModal.tsx
@@ -15,7 +15,8 @@ limitations under the License.
*/
import * as React from "react";
-import AccessibleButton from "../elements/AccessibleButton";
+import { createRef } from "react";
+import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
import Field from "../elements/Field";
import DialPad from './DialPad';
import dis from '../../../dispatcher/dispatcher';
@@ -34,6 +35,8 @@ interface IState {
@replaceableComponent("views.voip.DialPadModal")
export default class DialpadModal extends React.PureComponent {
+ private numberEntryFieldRef: React.RefObject = createRef();
+
constructor(props) {
super(props);
this.state = {
@@ -54,13 +57,27 @@ export default class DialpadModal extends React.PureComponent {
this.onDialPress();
};
- onDigitPress = (digit) => {
+ onDigitPress = (digit: string, ev: ButtonEvent) => {
this.setState({ value: this.state.value + digit });
+
+ // Keep the number field focused so that keyboard entry is still available.
+ // However, don't focus if this wasn't the result of directly clicking on the button,
+ // i.e someone using keyboard navigation.
+ if (ev.type === "click") {
+ this.numberEntryFieldRef.current?.focus();
+ }
};
- onDeletePress = () => {
+ onDeletePress = (ev: ButtonEvent) => {
if (this.state.value.length === 0) return;
this.setState({ value: this.state.value.slice(0, -1) });
+
+ // Keep the number field focused so that keyboard entry is still available
+ // However, don't focus if this wasn't the result of directly clicking on the button,
+ // i.e someone using keyboard navigation.
+ if (ev.type === "click") {
+ this.numberEntryFieldRef.current?.focus();
+ }
};
onDialPress = async () => {
@@ -82,6 +99,7 @@ export default class DialpadModal extends React.PureComponent {
let dialPadField;
if (this.state.value.length !== 0) {
dialPadField = {
/>;
} else {
dialPadField = {
}
}
- private playMedia() {
+ private async playMedia() {
const element = this.element;
if (!element) return;
// We play audio in AudioFeed, not here
@@ -129,7 +129,7 @@ export default class VideoFeed extends React.PureComponent {
// should serialise the ones that need to be serialised but then be able to interrupt
// them with another load() which will cancel the pending one, but since we don't call
// load() explicitly, it shouldn't be a problem. - Dave
- element.play();
+ await element.play();
} catch (e) {
logger.info("Failed to play media element with feed", this.props.feed, e);
}
diff --git a/src/editor/autocomplete.ts b/src/editor/autocomplete.ts
index bf8f457d0c..10e1c60695 100644
--- a/src/editor/autocomplete.ts
+++ b/src/editor/autocomplete.ts
@@ -32,7 +32,6 @@ export type GetAutocompleterComponent = () => Autocomplete;
export type UpdateQuery = (test: string) => Promise;
export default class AutocompleteWrapperModel {
- private queryPart: Part;
private partIndex: number;
constructor(
@@ -45,10 +44,6 @@ export default class AutocompleteWrapperModel {
public onEscape(e: KeyboardEvent): void {
this.getAutocompleterComponent().onEscape(e);
- this.updateCallback({
- replaceParts: [this.partCreator.plain(this.queryPart.text)],
- close: true,
- });
}
public close(): void {
@@ -64,7 +59,8 @@ export default class AutocompleteWrapperModel {
return ac && ac.countCompletions() > 0;
}
- public onEnter(): void {
+ public async confirmCompletion(): Promise {
+ await this.getAutocompleterComponent().onConfirmCompletion();
this.updateCallback({ close: true });
}
@@ -76,8 +72,6 @@ export default class AutocompleteWrapperModel {
if (acComponent.countCompletions() === 0) {
// Force completions to show for the text currently entered
await acComponent.forceComplete();
- // Select the first item by moving "down"
- await acComponent.moveSelection(+1);
}
}
@@ -90,25 +84,10 @@ export default class AutocompleteWrapperModel {
}
public onPartUpdate(part: Part, pos: DocumentPosition): Promise {
- // cache the typed value and caret here
- // so we can restore it in onComponentSelectionChange when the value is undefined (meaning it should be the typed text)
- this.queryPart = part;
this.partIndex = pos.index;
return this.updateQuery(part.text);
}
- public onComponentSelectionChange(completion: ICompletion): void {
- if (!completion) {
- this.updateCallback({
- replaceParts: [this.queryPart],
- });
- } else {
- this.updateCallback({
- replaceParts: this.partForCompletion(completion),
- });
- }
- }
-
public onComponentConfirm(completion: ICompletion): void {
this.updateCallback({
replaceParts: this.partForCompletion(completion),
diff --git a/src/editor/model.ts b/src/editor/model.ts
index da1c2f47f5..212a7d17c0 100644
--- a/src/editor/model.ts
+++ b/src/editor/model.ts
@@ -237,7 +237,7 @@ export default class EditorModel {
}
}
}
- // not _autoComplete, only there if active part is autocomplete part
+ // not autoComplete, only there if active part is autocomplete part
if (this.autoComplete) {
return this.autoComplete.onPartUpdate(part, pos);
}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index c9dbc00a78..b9a6b5e04c 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1102,9 +1102,9 @@
"Change Password": "Change Password",
"Your homeserver does not support cross-signing.": "Your homeserver does not support cross-signing.",
"Cross-signing is ready for use.": "Cross-signing is ready for use.",
+ "Cross-signing is ready but keys are not backed up.": "Cross-signing is ready but keys are not backed up.",
"Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.",
"Cross-signing is not set up.": "Cross-signing is not set up.",
- "Set up": "Set up",
"Reset": "Reset",
"Cross-signing public keys:": "Cross-signing public keys:",
"in memory": "in memory",
@@ -1202,6 +1202,7 @@
"Algorithm:": "Algorithm:",
"Your keys are not being backed up from this session.": "Your keys are not being backed up from this session.",
"Back up your keys before signing out to avoid losing them.": "Back up your keys before signing out to avoid losing them.",
+ "Set up": "Set up",
"well formed": "well formed",
"unexpected type": "unexpected type",
"Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.": "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.",
@@ -1291,7 +1292,7 @@
"For help with using %(brand)s, click here or start a chat with our bot using the button below.": "For help with using %(brand)s, click here or start a chat with our bot using the button below.",
"Chat with %(brand)s Bot": "Chat with %(brand)s Bot",
"Bug reporting": "Bug reporting",
- "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.",
+ "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited, which UI elements you last interacted with, and the usernames of other users. They do not contain messages.": "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited, which UI elements you last interacted with, and the usernames of other users. They do not contain messages.",
"Submit debug logs": "Submit debug logs",
"To report a Matrix-related security issue, please read the Matrix.org Security Disclosure Policy.": "To report a Matrix-related security issue, please read the Matrix.org Security Disclosure Policy.",
"Help & About": "Help & About",
@@ -1449,9 +1450,15 @@
"Send %(eventType)s events": "Send %(eventType)s events",
"Permissions": "Permissions",
"Select the roles required to change various parts of the room": "Select the roles required to change various parts of the room",
+ "Are you sure you want to add encryption to this public room?": "Are you sure you want to add encryption to this public room?",
+ "It's not recommended to add encryption to public rooms.Anyone can find and join public rooms, so anyone can read messages in them. You'll get none of the benefits of encryption, and you won't be able to turn it off later. Encrypting messages in a public room will make receiving and sending messages slower.": "It's not recommended to add encryption to public rooms.Anyone can find and join public rooms, so anyone can read messages in them. You'll get none of the benefits of encryption, and you won't be able to turn it off later. Encrypting messages in a public room will make receiving and sending messages slower.",
+ "To avoid these issues, create a new encrypted room for the conversation you plan to have.": "To avoid these issues, create a new encrypted room for the conversation you plan to have.",
"Enable encryption?": "Enable encryption?",
"Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.": "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.",
"This upgrade will allow members of selected spaces access to this room without an invite.": "This upgrade will allow members of selected spaces access to this room without an invite.",
+ "Are you sure you want to make this encrypted room public?": "Are you sure you want to make this encrypted room public?",
+ "It's not recommended to make encrypted rooms public. It will mean anyone can find and join the room, so anyone can read messages. You'll get none of the benefits of encryption. Encrypting messages in a public room will make receiving and sending messages slower.": "It's not recommended to make encrypted rooms public. It will mean anyone can find and join the room, so anyone can read messages. You'll get none of the benefits of encryption. Encrypting messages in a public room will make receiving and sending messages slower.",
+ "To avoid these issues, create a new public room for the conversation you plan to have.": "To avoid these issues, create a new public room for the conversation you plan to have.",
"To link to this room, please add an address.": "To link to this room, please add an address.",
"Private (invite only)": "Private (invite only)",
"Only invited people can join.": "Only invited people can join.",
@@ -1571,7 +1578,8 @@
"Invite to just this room": "Invite to just this room",
"Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your room.",
"This is the start of .": "This is the start of .",
- "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings.": "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings.",
+ "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites.": "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites.",
+ "Enable encryption in settings.": "Enable encryption in settings.",
"End-to-end encryption isn't enabled": "End-to-end encryption isn't enabled",
"Unpin": "Unpin",
"View message": "View message",
@@ -1887,13 +1895,14 @@
"Connected": "Connected",
"Call declined": "Call declined",
"Call back": "Call back",
- "Missed call": "Missed call",
+ "No answer": "No answer",
"Could not connect media": "Could not connect media",
"Connection failed": "Connection failed",
"Their device couldn't start the camera or microphone": "Their device couldn't start the camera or microphone",
"An unknown error occurred": "An unknown error occurred",
- "Unknown failure: %(reason)s)": "Unknown failure: %(reason)s)",
+ "Unknown failure: %(reason)s": "Unknown failure: %(reason)s",
"Retry": "Retry",
+ "Missed call": "Missed call",
"The call is in an unknown state!": "The call is in an unknown state!",
"Sunday": "Sunday",
"Monday": "Monday",
@@ -2167,7 +2176,7 @@
"Failed to send logs: ": "Failed to send logs: ",
"Preparing to download logs": "Preparing to download logs",
"Reminder: Your browser is unsupported, so your experience may be unpredictable.": "Reminder: Your browser is unsupported, so your experience may be unpredictable.",
- "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.",
+ "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited, which UI elements you last interacted with, and the usernames of other users. They do not contain messages.": "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited, which UI elements you last interacted with, and the usernames of other users. They do not contain messages.",
"Before submitting logs, you must create a GitHub issue to describe your problem.": "Before submitting logs, you must create a GitHub issue to describe your problem.",
"Download logs": "Download logs",
"GitHub issue": "GitHub issue",
@@ -2843,22 +2852,18 @@
"You don't have permission": "You don't have permission",
"This room is suggested as a good one to join": "This room is suggested as a good one to join",
"Suggested": "Suggested",
- "Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.",
- "%(count)s rooms and %(numSpaces)s spaces|other": "%(count)s rooms and %(numSpaces)s spaces",
- "%(count)s rooms and %(numSpaces)s spaces|one": "%(count)s room and %(numSpaces)s spaces",
- "%(count)s rooms and 1 space|other": "%(count)s rooms and 1 space",
- "%(count)s rooms and 1 space|one": "%(count)s room and 1 space",
"Select a room below first": "Select a room below first",
"Failed to remove some rooms. Try again later": "Failed to remove some rooms. Try again later",
"Removing...": "Removing...",
"Mark as not suggested": "Mark as not suggested",
"Mark as suggested": "Mark as suggested",
+ "Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.",
"No results found": "No results found",
"You may want to try a different search or check for typos.": "You may want to try a different search or check for typos.",
+ "Results": "Results",
+ "Rooms and spaces": "Rooms and spaces",
"Space": "Space",
"Search names and descriptions": "Search names and descriptions",
- "If you can't find the room you're looking for, ask for an invite or create a new room.": "If you can't find the room you're looking for, ask for an invite or create a new room.",
- "Create room": "Create room",
"Private space": "Private space",
" invites you": " invites you",
"To view %(spaceName)s, turn on the Spaces beta": "To view %(spaceName)s, turn on the Spaces beta",
diff --git a/src/indexing/EventIndex.ts b/src/indexing/EventIndex.ts
index a7142010f2..e3deb7510d 100644
--- a/src/indexing/EventIndex.ts
+++ b/src/indexing/EventIndex.ts
@@ -21,7 +21,7 @@ import { Room } from 'matrix-js-sdk/src/models/room';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { EventTimelineSet } from 'matrix-js-sdk/src/models/event-timeline-set';
import { RoomState } from 'matrix-js-sdk/src/models/room-state';
-import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window';
+import { TimelineIndex, TimelineWindow } from 'matrix-js-sdk/src/timeline-window';
import { sleep } from "matrix-js-sdk/src/utils";
import { IResultRoomEvents } from "matrix-js-sdk/src/@types/search";
@@ -859,13 +859,27 @@ export default class EventIndex extends EventEmitter {
return Promise.resolve(true);
}
- const paginationMethod = async (timelineWindow, timeline, room, direction, limit) => {
- const timelineSet = timelineWindow._timelineSet;
- const token = timeline.timeline.getPaginationToken(direction);
+ const paginationMethod = async (
+ timelineWindow: TimelineWindow,
+ timelineIndex: TimelineIndex,
+ room: Room,
+ direction: Direction,
+ limit: number,
+ ) => {
+ const timeline = timelineIndex.timeline;
+ const timelineSet = timeline.getTimelineSet();
+ const token = timeline.getPaginationToken(direction);
- const ret = await this.populateFileTimeline(timelineSet, timeline.timeline, room, limit, token, direction);
+ const ret = await this.populateFileTimeline(
+ timelineSet,
+ timeline,
+ room,
+ limit,
+ token,
+ direction,
+ );
- timeline.pendingPaginate = null;
+ timelineIndex.pendingPaginate = null;
timelineWindow.extend(direction, limit);
return ret;
diff --git a/src/resizer/resizer.ts b/src/resizer/resizer.ts
index e430c68e17..0496615188 100644
--- a/src/resizer/resizer.ts
+++ b/src/resizer/resizer.ts
@@ -182,8 +182,6 @@ export default class Resizer {
private getResizeHandles() {
if (!this.container.children) return [];
- return Array.from(this.container.children).filter(el => {
- return this.isResizeHandle(el);
- }) as HTMLElement[];
+ return Array.from(this.container.querySelectorAll(`.${this.classNames.handle}`)) as HTMLElement[];
}
}
diff --git a/src/sentry.ts b/src/sentry.ts
new file mode 100644
index 0000000000..59152f66f2
--- /dev/null
+++ b/src/sentry.ts
@@ -0,0 +1,229 @@
+/*
+Copyright 2021 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 * as Sentry from "@sentry/browser";
+import PlatformPeg from "./PlatformPeg";
+import SdkConfig from "./SdkConfig";
+import { MatrixClientPeg } from "./MatrixClientPeg";
+import SettingsStore from "./settings/SettingsStore";
+import { MatrixClient } from "matrix-js-sdk";
+
+/* eslint-disable camelcase */
+
+type StorageContext = {
+ storageManager_persisted?: string;
+ storageManager_quota?: string;
+ storageManager_usage?: string;
+ storageManager_usageDetails?: string;
+};
+
+type UserContext = {
+ username: string;
+ enabled_labs: string;
+ low_bandwidth: string;
+};
+
+type CryptoContext = {
+ device_keys?: string;
+ cross_signing_ready?: string;
+ cross_signing_supported_by_hs?: string;
+ cross_signing_key?: string;
+ cross_signing_privkey_in_secret_storage?: string;
+ cross_signing_master_privkey_cached?: string;
+ cross_signing_user_signing_privkey_cached?: string;
+ secret_storage_ready?: string;
+ secret_storage_key_in_account?: string;
+ session_backup_key_in_secret_storage?: string;
+ session_backup_key_cached?: string;
+ session_backup_key_well_formed?: string;
+};
+
+type DeviceContext = {
+ device_id: string;
+ mx_local_settings: string;
+ modernizr_missing_features?: string;
+};
+
+type Contexts = {
+ user: UserContext;
+ crypto: CryptoContext;
+ device: DeviceContext;
+ storage: StorageContext;
+};
+
+/* eslint-enable camelcase */
+
+async function getStorageContext(): Promise {
+ const result = {};
+
+ // add storage persistence/quota information
+ if (navigator.storage && navigator.storage.persisted) {
+ try {
+ result["storageManager_persisted"] = String(await navigator.storage.persisted());
+ } catch (e) {}
+ } else if (document.hasStorageAccess) { // Safari
+ try {
+ result["storageManager_persisted"] = String(await document.hasStorageAccess());
+ } catch (e) {}
+ }
+ if (navigator.storage && navigator.storage.estimate) {
+ try {
+ const estimate = await navigator.storage.estimate();
+ result["storageManager_quota"] = String(estimate.quota);
+ result["storageManager_usage"] = String(estimate.usage);
+ if (estimate.usageDetails) {
+ const usageDetails = [];
+ Object.keys(estimate.usageDetails).forEach(k => {
+ usageDetails.push(`${k}: ${String(estimate.usageDetails[k])}`);
+ });
+ result[`storageManager_usage`] = usageDetails.join(", ");
+ }
+ } catch (e) {}
+ }
+
+ return result;
+}
+
+function getUserContext(client: MatrixClient): UserContext {
+ return {
+ "username": client.credentials.userId,
+ "enabled_labs": getEnabledLabs(),
+ "low_bandwidth": SettingsStore.getValue("lowBandwidth") ? "enabled" : "disabled",
+ };
+}
+
+function getEnabledLabs(): string {
+ const enabledLabs = SettingsStore.getFeatureSettingNames().filter(f => SettingsStore.getValue(f));
+ if (enabledLabs.length) {
+ return enabledLabs.join(", ");
+ }
+ return "";
+}
+
+async function getCryptoContext(client: MatrixClient): Promise {
+ if (!client.isCryptoEnabled()) {
+ return {};
+ }
+ const keys = [`ed25519:${client.getDeviceEd25519Key()}`];
+ if (client.getDeviceCurve25519Key) {
+ keys.push(`curve25519:${client.getDeviceCurve25519Key()}`);
+ }
+ const crossSigning = client.crypto.crossSigningInfo;
+ const secretStorage = client.crypto.secretStorage;
+ const pkCache = client.getCrossSigningCacheCallbacks();
+ const sessionBackupKeyFromCache = await client.crypto.getSessionBackupPrivateKey();
+
+ return {
+ "device_keys": keys.join(', '),
+ "cross_signing_ready": String(await client.isCrossSigningReady()),
+ "cross_signing_supported_by_hs":
+ String(await client.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")),
+ "cross_signing_key": crossSigning.getId(),
+ "cross_signing_privkey_in_secret_storage": String(
+ !!(await crossSigning.isStoredInSecretStorage(secretStorage))),
+ "cross_signing_master_privkey_cached": String(
+ !!(pkCache && await pkCache.getCrossSigningKeyCache("master"))),
+ "cross_signing_user_signing_privkey_cached": String(
+ !!(pkCache && await pkCache.getCrossSigningKeyCache("user_signing"))),
+ "secret_storage_ready": String(await client.isSecretStorageReady()),
+ "secret_storage_key_in_account": String(!!(await secretStorage.hasKey())),
+ "session_backup_key_in_secret_storage": String(!!(await client.isKeyBackupKeyStored())),
+ "session_backup_key_cached": String(!!sessionBackupKeyFromCache),
+ "session_backup_key_well_formed": String(sessionBackupKeyFromCache instanceof Uint8Array),
+ };
+}
+
+function getDeviceContext(client: MatrixClient): DeviceContext {
+ const result = {
+ "device_id": client?.deviceId,
+ "mx_local_settings": localStorage.getItem('mx_local_settings'),
+ };
+
+ if (window.Modernizr) {
+ const missingFeatures = Object.keys(window.Modernizr).filter(key => window.Modernizr[key] === false);
+ if (missingFeatures.length > 0) {
+ result["modernizr_missing_features"] = missingFeatures.join(", ");
+ }
+ }
+
+ return result;
+}
+
+async function getContexts(): Promise {
+ const client = MatrixClientPeg.get();
+ return {
+ "user": getUserContext(client),
+ "crypto": await getCryptoContext(client),
+ "device": getDeviceContext(client),
+ "storage": await getStorageContext(),
+ };
+}
+
+export async function sendSentryReport(userText: string, issueUrl: string, error: Error): Promise {
+ const sentryConfig = SdkConfig.get()["sentry"];
+ if (!sentryConfig) return;
+
+ const captureContext = {
+ "contexts": await getContexts(),
+ "extra": {
+ "user_text": userText,
+ "issue_url": issueUrl,
+ },
+ };
+
+ // If there's no error and no issueUrl, the report will just produce non-grouped noise in Sentry, so don't
+ // upload it
+ if (error) {
+ Sentry.captureException(error, captureContext);
+ } else if (issueUrl) {
+ Sentry.captureMessage(`Issue: ${issueUrl}`, captureContext);
+ }
+}
+
+interface ISentryConfig {
+ dsn: string;
+ environment?: string;
+}
+
+export async function initSentry(sentryConfig: ISentryConfig): Promise {
+ if (!sentryConfig) return;
+ const platform = PlatformPeg.get();
+ let appVersion = "unknown";
+ try {
+ appVersion = await platform.getAppVersion();
+ } catch (e) {}
+
+ Sentry.init({
+ dsn: sentryConfig.dsn,
+ release: `${platform.getHumanReadableName()}@${appVersion}`,
+ environment: sentryConfig.environment,
+ defaultIntegrations: false,
+ autoSessionTracking: false,
+ debug: true,
+ integrations: [
+ // specifically disable Integrations.GlobalHandlers, which hooks uncaught exceptions - we don't
+ // want to capture those at this stage, just explicit rageshakes
+ new Sentry.Integrations.InboundFilters(),
+ new Sentry.Integrations.FunctionToString(),
+ new Sentry.Integrations.Breadcrumbs(),
+ new Sentry.Integrations.UserAgent(),
+ new Sentry.Integrations.Dedupe(),
+ ],
+ // Set to 1.0 which is reasonable if we're only submitting Rageshakes; will need to be set < 1.0
+ // if we collect more frequently.
+ tracesSampleRate: 1.0,
+ });
+}
diff --git a/src/settings/controllers/NotificationControllers.ts b/src/settings/controllers/NotificationControllers.ts
index cc5c040a89..09e4e1dd1a 100644
--- a/src/settings/controllers/NotificationControllers.ts
+++ b/src/settings/controllers/NotificationControllers.ts
@@ -21,6 +21,7 @@ import { SettingLevel } from "../SettingLevel";
// XXX: This feels wrong.
import { PushProcessor } from "matrix-js-sdk/src/pushprocessor";
+import { PushRuleActionName } from "matrix-js-sdk/src/@types/PushRules";
// .m.rule.master being enabled means all events match that push rule
// default action on this rule is dont_notify, but it could be something else
@@ -35,7 +36,7 @@ export function isPushNotifyDisabled(): boolean {
}
// If the rule is enabled then check it does not notify on everything
- return masterRule.enabled && !masterRule.actions.includes("notify");
+ return masterRule.enabled && !masterRule.actions.includes(PushRuleActionName.Notify);
}
function getNotifier(): any { // TODO: [TS] Formal type that doesn't cause a cyclical reference.
diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx
index da18646d0f..c08c66714b 100644
--- a/src/stores/SpaceStore.tsx
+++ b/src/stores/SpaceStore.tsx
@@ -19,7 +19,7 @@ import { ListIteratee, Many, sortBy, throttle } from "lodash";
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
-import { ISpaceSummaryRoom } from "matrix-js-sdk/src/@types/spaces";
+import { IHierarchyRoom } from "matrix-js-sdk/src/@types/spaces";
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import { IRoomCapability } from "matrix-js-sdk/src/client";
@@ -64,7 +64,7 @@ export const UPDATE_SELECTED_SPACE = Symbol("selected-space");
export const UPDATE_HOME_BEHAVIOUR = Symbol("home-behaviour");
// Space Room ID/HOME_SPACE will be emitted when a Space's children change
-export interface ISuggestedRoom extends ISpaceSummaryRoom {
+export interface ISuggestedRoom extends IHierarchyRoom {
viaServers: string[];
}
@@ -303,18 +303,20 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
public fetchSuggestedRooms = async (space: Room, limit = MAX_SUGGESTED_ROOMS): Promise => {
try {
- const data = await this.matrixClient.getSpaceSummary(space.roomId, 0, true, false, limit);
+ const { rooms } = await this.matrixClient.getRoomHierarchy(space.roomId, limit, 1, true);
const viaMap = new EnhancedMap>();
- data.events.forEach(ev => {
- if (ev.type === EventType.SpaceChild && ev.content.via?.length) {
- ev.content.via.forEach(via => {
- viaMap.getOrCreate(ev.state_key, new Set()).add(via);
- });
- }
+ rooms.forEach(room => {
+ room.children_state.forEach(ev => {
+ if (ev.type === EventType.SpaceChild && ev.content.via?.length) {
+ ev.content.via.forEach(via => {
+ viaMap.getOrCreate(ev.state_key, new Set()).add(via);
+ });
+ }
+ });
});
- return data.rooms.filter(roomInfo => {
+ return rooms.filter(roomInfo => {
return roomInfo.room_type !== RoomType.Space
&& this.matrixClient.getRoom(roomInfo.room_id)?.getMyMembership() !== "join";
}).map(roomInfo => ({
diff --git a/src/stores/WidgetStore.ts b/src/stores/WidgetStore.ts
index 732428107f..e9820eee06 100644
--- a/src/stores/WidgetStore.ts
+++ b/src/stores/WidgetStore.ts
@@ -137,6 +137,20 @@ export default class WidgetStore extends AsyncStoreWithClient {
if (edited && !this.roomMap.has(room.roomId)) {
this.roomMap.set(room.roomId, roomInfo);
}
+
+ // If a persistent widget is active, check to see if it's just been removed.
+ // If it has, it needs to destroyed otherwise unmounting the node won't kill it
+ const persistentWidgetId = ActiveWidgetStore.getPersistentWidgetId();
+ if (persistentWidgetId) {
+ if (
+ ActiveWidgetStore.getRoomId(persistentWidgetId) === room.roomId &&
+ !roomInfo.widgets.some(w => w.id === persistentWidgetId)
+ ) {
+ console.log(`Persistent widget ${persistentWidgetId} removed from room ${room.roomId}: destroying.`);
+ ActiveWidgetStore.destroyPersistentWidget(persistentWidgetId);
+ }
+ }
+
this.emit(room.roomId);
}
diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts
index e2af1c7464..ee8d9bceae 100644
--- a/src/utils/EventUtils.ts
+++ b/src/utils/EventUtils.ts
@@ -103,6 +103,7 @@ export function getEventDisplayInfo(mxEvent: MatrixEvent): {
isInfoMessage: boolean;
tileHandler: string;
isBubbleMessage: boolean;
+ isLeftAlignedBubbleMessage: boolean;
} {
const content = mxEvent.getContent();
const msgtype = content.msgtype;
@@ -116,11 +117,15 @@ export function getEventDisplayInfo(mxEvent: MatrixEvent): {
(eventType === EventType.RoomMessage && msgtype && msgtype.startsWith("m.key.verification")) ||
(eventType === EventType.RoomCreate) ||
(eventType === EventType.RoomEncryption) ||
- (eventType === EventType.CallInvite) ||
(tileHandler === "messages.MJitsiWidgetEvent")
);
+ const isLeftAlignedBubbleMessage = (
+ !isBubbleMessage &&
+ eventType === EventType.CallInvite
+ );
let isInfoMessage = (
!isBubbleMessage &&
+ !isLeftAlignedBubbleMessage &&
eventType !== EventType.RoomMessage &&
eventType !== EventType.Sticker &&
eventType !== EventType.RoomCreate
@@ -137,5 +142,14 @@ export function getEventDisplayInfo(mxEvent: MatrixEvent): {
isInfoMessage = true;
}
- return { tileHandler, isInfoMessage, isBubbleMessage };
+ return { tileHandler, isInfoMessage, isBubbleMessage, isLeftAlignedBubbleMessage };
+}
+
+export function isVoiceMessage(mxEvent: MatrixEvent): boolean {
+ const content = mxEvent.getContent();
+ // MSC2516 is a legacy identifier. See https://github.com/matrix-org/matrix-doc/pull/3245
+ return (
+ !!content['org.matrix.msc2516.voice'] ||
+ !!content['org.matrix.msc3245.voice']
+ );
}
diff --git a/src/utils/drawable.ts b/src/utils/drawable.ts
new file mode 100644
index 0000000000..31f7bc8cec
--- /dev/null
+++ b/src/utils/drawable.ts
@@ -0,0 +1,36 @@
+/*
+Copyright 2021 New Vector Ltd
+
+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.
+*/
+
+/**
+ * Fetch an image using the best available method based on browser compatibility
+ * @param url the URL of the image to fetch
+ * @returns a canvas drawable object
+ */
+export async function getDrawable(url: string): Promise {
+ if ('createImageBitmap' in window) {
+ const response = await fetch(url);
+ const blob = await response.blob();
+ return await createImageBitmap(blob);
+ } else {
+ return new Promise((resolve, reject) => {
+ const img = document.createElement("img");
+ img.crossOrigin = "anonymous";
+ img.onload = () => resolve(img);
+ img.onerror = (e) => reject(e);
+ img.src = url;
+ });
+ }
+}
diff --git a/test/test-utils.js b/test/test-utils.js
index f62df53c3a..803d97c163 100644
--- a/test/test-utils.js
+++ b/test/test-utils.js
@@ -85,9 +85,8 @@ export function createTestClient() {
generateClientSecret: () => "t35tcl1Ent5ECr3T",
isGuest: () => false,
isCryptoEnabled: () => false,
- getSpaceSummary: jest.fn().mockReturnValue({
+ getRoomHierarchy: jest.fn().mockReturnValue({
rooms: [],
- events: [],
}),
// Used by various internal bits we aren't concerned with (yet)
diff --git a/yarn.lock b/yarn.lock
index a780d1ffa0..256fee5277 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,6 +2,28 @@
# yarn lockfile v1
+"@actions/core@^1.4.0":
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/@actions/core/-/core-1.4.0.tgz#cf2e6ee317e314b03886adfeb20e448d50d6e524"
+ integrity sha512-CGx2ilGq5i7zSLgiiGUtBCxhRRxibJYU6Fim0Q1Wg2aQL2LTnF27zbqZOrxfvFQ55eSBW0L8uVStgtKMpa0Qlg==
+
+"@actions/github@^5.0.0":
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/@actions/github/-/github-5.0.0.tgz#1754127976c50bd88b2e905f10d204d76d1472f8"
+ integrity sha512-QvE9eAAfEsS+yOOk0cylLBIO/d6WyWIOvsxxzdrPFaud39G6BOkUwScXZn1iBzQzHyu9SBkkLSWlohDWdsasAQ==
+ dependencies:
+ "@actions/http-client" "^1.0.11"
+ "@octokit/core" "^3.4.0"
+ "@octokit/plugin-paginate-rest" "^2.13.3"
+ "@octokit/plugin-rest-endpoint-methods" "^5.1.1"
+
+"@actions/http-client@^1.0.11":
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/@actions/http-client/-/http-client-1.0.11.tgz#c58b12e9aa8b159ee39e7dd6cbd0e91d905633c0"
+ integrity sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==
+ dependencies:
+ tunnel "0.0.6"
+
"@babel/cli@^7.12.10":
version "7.14.8"
resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.14.8.tgz#fac73c0e2328a8af9fd3560c06b096bfa3730933"
@@ -1331,7 +1353,7 @@
dependencies:
"@octokit/types" "^6.0.3"
-"@octokit/core@^3.5.0":
+"@octokit/core@^3.4.0", "@octokit/core@^3.5.0":
version "3.5.1"
resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.5.1.tgz#8601ceeb1ec0e1b1b8217b960a413ed8e947809b"
integrity sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw==
@@ -1367,6 +1389,18 @@
resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-9.3.0.tgz#160347858d727527901c6aae7f7d5c2414cc1f2e"
integrity sha512-oz60hhL+mDsiOWhEwrj5aWXTOMVtQgcvP+sRzX4C3cH7WOK9QSAoEtjWh0HdOf6V3qpdgAmUMxnQPluzDWR7Fw==
+"@octokit/openapi-types@^9.5.0":
+ version "9.7.0"
+ resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-9.7.0.tgz#9897cdefd629cd88af67b8dbe2e5fb19c63426b2"
+ integrity sha512-TUJ16DJU8mekne6+KVcMV5g6g/rJlrnIKn7aALG9QrNpnEipFc1xjoarh0PKaAWf2Hf+HwthRKYt+9mCm5RsRg==
+
+"@octokit/plugin-paginate-rest@^2.13.3":
+ version "2.15.1"
+ resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.15.1.tgz#264189dd3ce881c6c33758824aac05a4002e056a"
+ integrity sha512-47r52KkhQDkmvUKZqXzA1lKvcyJEfYh3TKAIe5+EzMeyDM3d+/s5v11i2gTk8/n6No6DPi3k5Ind6wtDbo/AEg==
+ dependencies:
+ "@octokit/types" "^6.24.0"
+
"@octokit/plugin-paginate-rest@^2.6.2":
version "2.15.0"
resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.15.0.tgz#9c956c3710b2bd786eb3814eaf5a2b17392c150d"
@@ -1387,6 +1421,14 @@
"@octokit/types" "^6.23.0"
deprecation "^2.3.1"
+"@octokit/plugin-rest-endpoint-methods@^5.1.1":
+ version "5.8.0"
+ resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.8.0.tgz#33b342fe41f2603fdf8b958e6652103bb3ea3f3b"
+ integrity sha512-qeLZZLotNkoq+it6F+xahydkkbnvSK0iDjlXFo3jNTB+Ss0qIbYQb9V/soKLMkgGw8Q2sHjY5YEXiA47IVPp4A==
+ dependencies:
+ "@octokit/types" "^6.25.0"
+ deprecation "^2.3.1"
+
"@octokit/request-error@^2.0.5", "@octokit/request-error@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.1.0.tgz#9e150357831bfc788d13a4fd4b1913d60c74d677"
@@ -1425,6 +1467,13 @@
dependencies:
"@octokit/openapi-types" "^9.3.0"
+"@octokit/types@^6.24.0", "@octokit/types@^6.25.0":
+ version "6.25.0"
+ resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.25.0.tgz#c8e37e69dbe7ce55ed98ee63f75054e7e808bf1a"
+ integrity sha512-bNvyQKfngvAd/08COlYIN54nRgxskmejgywodizQNyiKoXmWRAjKup2/LYwm+T9V0gsKH6tuld1gM0PzmOiB4Q==
+ dependencies:
+ "@octokit/openapi-types" "^9.5.0"
+
"@peculiar/asn1-schema@^2.0.27", "@peculiar/asn1-schema@^2.0.32":
version "2.0.37"
resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.0.37.tgz#700476512ab903d809f64a3040fb1b2fe6fb6d4b"
@@ -1453,11 +1502,74 @@
tslib "^2.2.0"
webcrypto-core "^1.2.0"
+"@sentry/browser@^6.11.0":
+ version "6.11.0"
+ resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.11.0.tgz#9e90bbc0488ebcdd1e67937d8d5b4f13c3f6dee0"
+ integrity sha512-Qr2QRA0t5/S9QQqxzYKvM9W8prvmiWuldfwRX4hubovXzcXLgUi4WK0/H612wSbYZ4dNAEcQbtlxFWJNN4wxdg==
+ dependencies:
+ "@sentry/core" "6.11.0"
+ "@sentry/types" "6.11.0"
+ "@sentry/utils" "6.11.0"
+ tslib "^1.9.3"
+
+"@sentry/core@6.11.0":
+ version "6.11.0"
+ resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.11.0.tgz#40e94043afcf6407a109be26655c77832c64e740"
+ integrity sha512-09TB+f3pqEq8LFahFWHO6I/4DxHo+NcS52OkbWMDqEi6oNZRD7PhPn3i14LfjsYVv3u3AESU8oxSEGbFrr2UjQ==
+ dependencies:
+ "@sentry/hub" "6.11.0"
+ "@sentry/minimal" "6.11.0"
+ "@sentry/types" "6.11.0"
+ "@sentry/utils" "6.11.0"
+ tslib "^1.9.3"
+
+"@sentry/hub@6.11.0":
+ version "6.11.0"
+ resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.11.0.tgz#ddf9ddb0577d1c8290dc02c0242d274fe84d6c16"
+ integrity sha512-pT9hf+ZJfVFpoZopoC+yJmFNclr4NPqPcl2cgguqCHb69DklD1NxgBNWK8D6X05qjnNFDF991U6t1mxP9HrGuw==
+ dependencies:
+ "@sentry/types" "6.11.0"
+ "@sentry/utils" "6.11.0"
+ tslib "^1.9.3"
+
+"@sentry/minimal@6.11.0":
+ version "6.11.0"
+ resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.11.0.tgz#806d5512658370e40827b3e3663061db708fff33"
+ integrity sha512-XkZ7qrdlGp4IM/gjGxf1Q575yIbl5RvPbg+WFeekpo16Ufvzx37Mr8c2xsZaWosISVyE6eyFpooORjUlzy8EDw==
+ dependencies:
+ "@sentry/hub" "6.11.0"
+ "@sentry/types" "6.11.0"
+ tslib "^1.9.3"
+
+"@sentry/tracing@^6.11.0":
+ version "6.11.0"
+ resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.11.0.tgz#9bd9287addea1ebc12c75b226f71c7713c0fac4f"
+ integrity sha512-9VA1/SY++WeoMQI4K6n/sYgIdRtCu9NLWqmGqu/5kbOtESYFgAt1DqSyqGCr00ZjQiC2s7tkDkTNZb38K6KytQ==
+ dependencies:
+ "@sentry/hub" "6.11.0"
+ "@sentry/minimal" "6.11.0"
+ "@sentry/types" "6.11.0"
+ "@sentry/utils" "6.11.0"
+ tslib "^1.9.3"
+
+"@sentry/types@6.11.0":
+ version "6.11.0"
+ resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.11.0.tgz#5122685478d32ddacd3a891cbcf550012df85f7c"
+ integrity sha512-gm5H9eZhL6bsIy/h3T+/Fzzz2vINhHhqd92CjHle3w7uXdTdFV98i2pDpErBGNTSNzbntqOMifYEB5ENtZAvcg==
+
"@sentry/types@^6.10.0":
version "6.10.0"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.10.0.tgz#6b1f44e5ed4dbc2710bead24d1b32fb08daf04e1"
integrity sha512-M7s0JFgG7/6/yNVYoPUbxzaXDhnzyIQYRRJJKRaTD77YO4MHvi4Ke8alBWqD5fer0cPIfcSkBqa9BLdqRqcMWw==
+"@sentry/utils@6.11.0":
+ version "6.11.0"
+ resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.11.0.tgz#d1dee4faf4d9c42c54bba88d5a66fb96b902a14c"
+ integrity sha512-IOvyFHcnbRQxa++jO+ZUzRvFHEJ1cZjrBIQaNVc0IYF0twUOB5PTP6joTcix38ldaLeapaPZ9LGfudbvYvxkdg==
+ dependencies:
+ "@sentry/types" "6.11.0"
+ tslib "^1.9.3"
+
"@sinonjs/commons@^1.7.0":
version "1.8.3"
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d"
@@ -1956,10 +2068,13 @@ ajv@^8.0.1:
require-from-string "^2.0.2"
uri-js "^4.2.2"
-"allchange@github:matrix-org/allchange":
- version "0.0.1"
- resolved "https://codeload.github.com/matrix-org/allchange/tar.gz/56b37b06339a3ac3fe771f3ec3d0bff798df8dab"
+allchange@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/allchange/-/allchange-1.0.0.tgz#f5177b7d97f8e97a2d059a1524db9a72d94dc6d2"
+ integrity sha512-O0VIaMIORxOaReyYEijDfKdpudJhbzzVYLdJR1aROyUgOLBEp9e5V/TDXQpjX23W90IFCSRZxsDb3exLRD05HA==
dependencies:
+ "@actions/core" "^1.4.0"
+ "@actions/github" "^5.0.0"
"@octokit/rest" "^18.6.7"
cli-color "^2.0.0"
js-yaml "^4.1.0"
@@ -5686,10 +5801,9 @@ mathml-tag-names@^2.1.3:
resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3"
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
-matrix-js-sdk@12.2.0:
- version "12.2.0"
- resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-12.2.0.tgz#e1dc7ddac054289cb24ee3d11dba8a5ba5ddecf5"
- integrity sha512-foSs3uKRc6uvFNhgY35eErBvLWVDd5RNIxxsdFKlmU3B+70YUf3BP3petyBNW34ORyOqNdX36IiApfLo3npNEw==
+"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
+ version "12.3.1"
+ resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/3216d7e5a7a333212b00d4d7578e29a9f0e247d8"
dependencies:
"@babel/runtime" "^7.12.5"
another-json "^0.2.0"
@@ -7898,6 +8012,11 @@ tunnel-agent@^0.6.0:
dependencies:
safe-buffer "^5.0.1"
+tunnel@0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c"
+ integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==
+
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
version "0.14.5"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"