Merge branch 'develop' into gsouquet/fix-backdrop-filter

* develop: (22 commits)
  Fix long display names in call toasts
  Fix import
  Add MatrixEvent type
  Convert CrossSigningPanel to TS
  Fix PiP of held calls
  Use new call state icons
  Add declined call buttons
  Add "No answer" state
  Left align call tiles
  Fix tab trapping behaviour
  add comment
  Iterate PR based on feedback
  Iterate PR, merge types with @types/PushRules
  Remove misplaced bracket in a translation string
  delint and improve ts
  Convert SearchResult, InteractiveAuth, PushProcessor and Scheduler to Typescript
  remove dead code and fix some types
  delint
  post-merge fixes, the new keybindings stuff made it messy
  Improve VoiceOver & WebKit accessibility support
  ...
This commit is contained in:
Dariusz Niemczyk 2021-08-16 13:16:28 +02:00
commit 8bd5441fae
No known key found for this signature in database
GPG key ID: 28DFE7164F497CB6
31 changed files with 548 additions and 527 deletions

View file

@ -16,7 +16,6 @@ limitations under the License.
.mx_CallEvent_wrapper {
display: flex;
justify-content: center;
width: 100%;
.mx_CallEvent {
@ -28,7 +27,7 @@ limitations under the License.
background-color: $dark-panel-bg-color;
border-radius: 8px;
width: 75%;
width: 65%;
box-sizing: border-box;
height: 60px;
margin: 4px 0;
@ -81,6 +80,16 @@ limitations under the License.
mask-image: url('$(res)/img/voip/missed-video.svg');
}
&.mx_CallEvent_voice.mx_CallEvent_rejected .mx_CallEvent_type_icon::before,
&.mx_CallEvent_voice.mx_CallEvent_noAnswer .mx_CallEvent_type_icon::before {
mask-image: url('$(res)/img/voip/declined-voice.svg');
}
&.mx_CallEvent_video.mx_CallEvent_rejected .mx_CallEvent_type_icon::before,
&.mx_CallEvent_video.mx_CallEvent_noAnswer .mx_CallEvent_type_icon::before {
mask-image: url('$(res)/img/voip/declined-video.svg');
}
.mx_CallEvent_info {
display: flex;
flex-direction: row;

View file

@ -30,7 +30,14 @@ limitations under the License.
font-size: $font-15px;
line-height: $font-18px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-top: 2px;
margin-right: 6px;
max-width: 200px;
}
.mx_CallEvent_type {

View file

@ -43,6 +43,7 @@ limitations under the License.
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.20);
border-radius: 8px;
.mx_CallView_video_hold,
.mx_CallView_voice {
height: 180px;
}

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 4.81815C0 3.76379 0.89543 2.90906 2 2.90906H9.33333C10.4379 2.90906 11.3333 3.76379 11.3333 4.81815V11.1818C11.3333 12.2361 10.4379 13.0909 9.33333 13.0909H2C0.895429 13.0909 0 12.2361 0 11.1818V4.81815ZM12.6667 6.09089L14.9169 4.37255C15.3534 4.03921 16 4.33587 16 4.86947V11.1305C16 11.6641 15.3534 11.9607 14.9169 11.6274L12.6667 9.90907V6.09089ZM7.82332 5.81539C7.96503 5.95709 7.96503 6.18685 7.82332 6.32855L6.17983 7.97204L7.89372 9.68593C8.03543 9.82763 8.03543 10.0574 7.89372 10.1991C7.75201 10.3408 7.52226 10.3408 7.38055 10.1991L5.66667 8.48521L3.95278 10.1991C3.81107 10.3408 3.58132 10.3408 3.43961 10.1991C3.29791 10.0574 3.29791 9.82763 3.43961 9.68593L5.1535 7.97204L3.51001 6.32855C3.36831 6.18685 3.36831 5.95709 3.51001 5.81539C3.65172 5.67368 3.88147 5.67368 4.02318 5.81539L5.66667 7.45887L7.31015 5.81539C7.45186 5.67368 7.68161 5.67368 7.82332 5.81539Z" fill="#737D8C"/>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.35116 10.6409C6.11185 11.4622 7.94306 12.8843 8.44217 13.1761C8.47169 13.1934 8.50549 13.2135 8.54328 13.2359C9.30489 13.6887 11.6913 15.1074 13.4301 13.7797C14.7772 12.7511 14.3392 11.594 13.8858 11.2501C13.5754 11.0086 12.6608 10.3431 11.8003 9.74356C10.9553 9.15489 10.4844 9.62653 10.1659 9.94543C10.1601 9.95129 10.1543 9.9571 10.1485 9.96285L9.50791 10.6035C9.34477 10.7666 9.0966 10.7071 8.8589 10.5204C8.00599 9.87084 7.37856 9.24399 7.06465 8.93008L7.06201 8.92744C6.74815 8.61357 6.12909 7.99392 5.47955 7.14101C5.29283 6.90331 5.23329 6.65515 5.39643 6.49201L6.03708 5.85136C6.04283 5.84561 6.04864 5.83981 6.0545 5.83396C6.3734 5.51555 6.84504 5.04464 6.25636 4.19966C5.65687 3.33915 4.9913 2.42455 4.74984 2.11412C4.40588 1.66071 3.2488 1.22269 2.22021 2.5698C0.89255 4.30858 2.31122 6.69502 2.76397 7.45663C2.78644 7.49443 2.80653 7.52822 2.82379 7.55774C3.11562 8.05685 4.52989 9.88025 5.35116 10.6409Z" fill="#737D8C"/>
<path d="M13.7979 2.05203C13.9599 1.8876 13.9599 1.62101 13.7979 1.45658C13.636 1.29214 13.3734 1.29214 13.2114 1.45658L11.3332 3.36362L9.4549 1.45658C9.29295 1.29214 9.03037 1.29214 8.86842 1.45658C8.70647 1.62101 8.70647 1.8876 8.86842 2.05203L10.7467 3.95907L8.78797 5.9478C8.62602 6.11223 8.62602 6.37883 8.78797 6.54326C8.94992 6.70769 9.21249 6.70769 9.37444 6.54326L11.3332 4.55453L13.2919 6.54326C13.4538 6.70769 13.7164 6.70769 13.8784 6.54326C14.0403 6.37883 14.0403 6.11223 13.8784 5.9478L11.9196 3.95907L13.7979 2.05203Z" fill="#737D8C"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -86,7 +86,7 @@ import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/
import EventEmitter from 'events';
import SdkConfig from './SdkConfig';
import { ensureDMExists, findDMForUser } from './createRoom';
import { IPushRule, RuleId, TweakName, Tweaks } from "matrix-js-sdk/src/@types/PushRules";
import { RuleId, TweakName, Tweaks } from "matrix-js-sdk/src/@types/PushRules";
import { PushProcessor } from 'matrix-js-sdk/src/pushprocessor';
import { WidgetLayoutStore, Container } from './stores/widgets/WidgetLayoutStore';
import { getIncomingCallToastKey } from './toasts/IncomingCallToast';
@ -484,7 +484,7 @@ export default class CallHandler extends EventEmitter {
switch (newState) {
case CallState.Ringing: {
const incomingCallPushRule = (
new PushProcessor(MatrixClientPeg.get()).getPushRuleById(RuleId.IncomingCall) as IPushRule
new PushProcessor(MatrixClientPeg.get()).getPushRuleById(RuleId.IncomingCall)
);
const pushRuleEnabled = incomingCallPushRule?.enabled;
const tweakSetToRing = incomingCallPushRule?.actions.some((action: Tweaks) => (

View file

@ -161,31 +161,29 @@ const messageComposerBindings = (): KeyBinding<MessageComposerAction>[] => {
const autocompleteBindings = (): KeyBinding<AutocompleteAction>[] => {
return [
{
action: AutocompleteAction.CompleteOrNextSelection,
action: AutocompleteAction.ForceComplete,
keyCombo: {
key: Key.TAB,
},
},
{
action: AutocompleteAction.CompleteOrNextSelection,
action: AutocompleteAction.ForceComplete,
keyCombo: {
key: Key.TAB,
ctrlKey: true,
},
},
{
action: AutocompleteAction.CompleteOrPrevSelection,
action: AutocompleteAction.Complete,
keyCombo: {
key: Key.TAB,
shiftKey: true,
key: Key.ENTER,
},
},
{
action: AutocompleteAction.CompleteOrPrevSelection,
action: AutocompleteAction.Complete,
keyCombo: {
key: Key.TAB,
key: Key.ENTER,
ctrlKey: true,
shiftKey: true,
},
},
{

View file

@ -52,13 +52,11 @@ export enum MessageComposerAction {
/** Actions for text editing autocompletion */
export enum AutocompleteAction {
/**
* Select previous selection or, if the autocompletion window is not shown, open the window and select the first
* selection.
*/
CompleteOrPrevSelection = 'ApplySelection',
/** Select next selection or, if the autocompletion window is not shown, open it and select the first selection */
CompleteOrNextSelection = 'CompleteOrNextSelection',
/** Accepts chosen autocomplete selection */
Complete = 'Complete',
/** Accepts chosen autocomplete selection or,
* if the autocompletion window is not shown, open the window and select the first selection */
ForceComplete = 'ForceComplete',
/** Move to the previous autocomplete selection */
PrevSelection = 'PrevSelection',
/** Move to the next autocomplete selection */

View file

@ -27,11 +27,11 @@ export interface ICommand {
};
}
export default class AutocompleteProvider {
export default abstract class AutocompleteProvider {
commandRegex: RegExp;
forcedCommandRegex: RegExp;
constructor(commandRegex?: RegExp, forcedCommandRegex?: RegExp) {
protected constructor(commandRegex?: RegExp, forcedCommandRegex?: RegExp) {
if (commandRegex) {
if (!commandRegex.global) {
throw new Error('commandRegex must have global flag set');
@ -93,23 +93,16 @@ export default class AutocompleteProvider {
};
}
async getCompletions(
abstract getCompletions(
query: string,
selection: ISelectionRange,
force = false,
limit = -1,
): Promise<ICompletion[]> {
return [];
}
force: boolean,
limit: number,
): Promise<ICompletion[]>;
getName(): string {
return 'Default Provider';
}
abstract getName(): string;
renderCompletions(completions: React.ReactNode[]): React.ReactNode | null {
console.error('stub; should be implemented in subclasses');
return null;
}
abstract renderCompletions(completions: React.ReactNode[]): React.ReactNode | null;
// Whether we should provide completions even if triggered forcefully, without a sigil.
shouldForceComplete(): boolean {

View file

@ -96,7 +96,7 @@ export default class CommandProvider extends AutocompleteProvider {
return (
<div
className="mx_Autocomplete_Completion_container_block"
role="listbox"
role="presentation"
aria-label={_t("Command Autocomplete")}
>
{ completions }

View file

@ -116,7 +116,7 @@ export default class CommunityProvider extends AutocompleteProvider {
return (
<div
className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate"
role="listbox"
role="presentation"
aria-label={_t("Community Autocomplete")}
>
{ completions }

View file

@ -105,7 +105,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
return (
<div
className="mx_Autocomplete_Completion_container_block"
role="listbox"
role="presentation"
aria-label={_t("DuckDuckGo Results")}
>
{ completions }

View file

@ -139,7 +139,7 @@ export default class EmojiProvider extends AutocompleteProvider {
return (
<div
className="mx_Autocomplete_Completion_container_pill"
role="listbox"
role="presentation"
aria-label={_t("Emoji Autocomplete")}
>
{ completions }

View file

@ -70,7 +70,7 @@ export default class NotifProvider extends AutocompleteProvider {
return (
<div
className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate"
role="listbox"
role="presentation"
aria-label={_t("Notification Autocomplete")}
>
{ completions }

View file

@ -134,7 +134,7 @@ export default class RoomProvider extends AutocompleteProvider {
return (
<div
className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate"
role="listbox"
role="presentation"
aria-label={_t("Room Autocomplete")}
>
{ completions }

View file

@ -181,7 +181,7 @@ export default class UserProvider extends AutocompleteProvider {
return (
<div
className="mx_Autocomplete_Completion_container_pill"
role="listbox"
role="presentation"
aria-label={_t("User Autocomplete")}
>
{ completions }

View file

@ -1,300 +0,0 @@
/*
Copyright 2017 Vector Creations Ltd.
Copyright 2019, 2020 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 { InteractiveAuth } from "matrix-js-sdk/src/interactive-auth";
import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import getEntryComponentForLoginType from '../views/auth/InteractiveAuthEntryComponents';
import * as sdk from '../../index';
import { replaceableComponent } from "../../utils/replaceableComponent";
export const ERROR_USER_CANCELLED = new Error("User cancelled auth session");
@replaceableComponent("structures.InteractiveAuthComponent")
export default class InteractiveAuthComponent extends React.Component {
static propTypes = {
// matrix client to use for UI auth requests
matrixClient: PropTypes.object.isRequired,
// response from initial request. If not supplied, will do a request on
// mount.
authData: PropTypes.shape({
flows: PropTypes.array,
params: PropTypes.object,
session: PropTypes.string,
}),
// callback
makeRequest: PropTypes.func.isRequired,
// callback called when the auth process has finished,
// successfully or unsuccessfully.
// @param {bool} status True if the operation requiring
// auth was completed sucessfully, false if canceled.
// @param {object} result The result of the authenticated call
// if successful, otherwise the error object.
// @param {object} extra Additional information about the UI Auth
// process:
// * emailSid {string} If email auth was performed, the sid of
// the auth session.
// * clientSecret {string} The client secret used in auth
// sessions with the identity server.
onAuthFinished: PropTypes.func.isRequired,
// Inputs provided by the user to the auth process
// and used by various stages. As passed to js-sdk
// interactive-auth
inputs: PropTypes.object,
// As js-sdk interactive-auth
requestEmailToken: PropTypes.func,
sessionId: PropTypes.string,
clientSecret: PropTypes.string,
emailSid: PropTypes.string,
// If true, poll to see if the auth flow has been completed
// out-of-band
poll: PropTypes.bool,
// If true, components will be told that the 'Continue' button
// is managed by some other party and should not be managed by
// the component itself.
continueIsManaged: PropTypes.bool,
// Called when the stage changes, or the stage's phase changes. First
// argument is the stage, second is the phase. Some stages do not have
// phases and will be counted as 0 (numeric).
onStagePhaseChange: PropTypes.func,
// continueText and continueKind are passed straight through to the AuthEntryComponent.
continueText: PropTypes.string,
continueKind: PropTypes.string,
};
constructor(props) {
super(props);
this.state = {
authStage: null,
busy: false,
errorText: null,
stageErrorText: null,
submitButtonEnabled: false,
};
this._unmounted = false;
this._authLogic = new InteractiveAuth({
authData: this.props.authData,
doRequest: this._requestCallback,
busyChanged: this._onBusyChanged,
inputs: this.props.inputs,
stateUpdated: this._authStateUpdated,
matrixClient: this.props.matrixClient,
sessionId: this.props.sessionId,
clientSecret: this.props.clientSecret,
emailSid: this.props.emailSid,
requestEmailToken: this._requestEmailToken,
});
this._intervalId = null;
if (this.props.poll) {
this._intervalId = setInterval(() => {
this._authLogic.poll();
}, 2000);
}
this._stageComponent = createRef();
}
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount() { // eslint-disable-line camelcase
this._authLogic.attemptAuth().then((result) => {
const extra = {
emailSid: this._authLogic.getEmailSid(),
clientSecret: this._authLogic.getClientSecret(),
};
this.props.onAuthFinished(true, result, extra);
}).catch((error) => {
this.props.onAuthFinished(false, error);
console.error("Error during user-interactive auth:", error);
if (this._unmounted) {
return;
}
const msg = error.message || error.toString();
this.setState({
errorText: msg,
});
});
}
componentWillUnmount() {
this._unmounted = true;
if (this._intervalId !== null) {
clearInterval(this._intervalId);
}
}
_requestEmailToken = async (...args) => {
this.setState({
busy: true,
});
try {
return await this.props.requestEmailToken(...args);
} finally {
this.setState({
busy: false,
});
}
};
tryContinue = () => {
if (this._stageComponent.current && this._stageComponent.current.tryContinue) {
this._stageComponent.current.tryContinue();
}
};
_authStateUpdated = (stageType, stageState) => {
const oldStage = this.state.authStage;
this.setState({
busy: false,
authStage: stageType,
stageState: stageState,
errorText: stageState.error,
}, () => {
if (oldStage !== stageType) {
this._setFocus();
} else if (
!stageState.error && this._stageComponent.current &&
this._stageComponent.current.attemptFailed
) {
this._stageComponent.current.attemptFailed();
}
});
};
_requestCallback = (auth) => {
// This wrapper just exists because the js-sdk passes a second
// 'busy' param for backwards compat. This throws the tests off
// so discard it here.
return this.props.makeRequest(auth);
};
_onBusyChanged = (busy) => {
// if we've started doing stuff, reset the error messages
if (busy) {
this.setState({
busy: true,
errorText: null,
stageErrorText: null,
});
}
// The JS SDK eagerly reports itself as "not busy" right after any
// immediate work has completed, but that's not really what we want at
// the UI layer, so we ignore this signal and show a spinner until
// there's a new screen to show the user. This is implemented by setting
// `busy: false` in `_authStateUpdated`.
// See also https://github.com/vector-im/element-web/issues/12546
};
_setFocus() {
if (this._stageComponent.current && this._stageComponent.current.focus) {
this._stageComponent.current.focus();
}
}
_submitAuthDict = authData => {
this._authLogic.submitAuthDict(authData);
};
_onPhaseChange = newPhase => {
if (this.props.onStagePhaseChange) {
this.props.onStagePhaseChange(this.state.authStage, newPhase || 0);
}
};
_onStageCancel = () => {
this.props.onAuthFinished(false, ERROR_USER_CANCELLED);
};
_renderCurrentStage() {
const stage = this.state.authStage;
if (!stage) {
if (this.state.busy) {
const Loader = sdk.getComponent("elements.Spinner");
return <Loader />;
} else {
return null;
}
}
const StageComponent = getEntryComponentForLoginType(stage);
return (
<StageComponent
ref={this._stageComponent}
loginType={stage}
matrixClient={this.props.matrixClient}
authSessionId={this._authLogic.getSessionId()}
clientSecret={this._authLogic.getClientSecret()}
stageParams={this._authLogic.getStageParams(stage)}
submitAuthDict={this._submitAuthDict}
errorText={this.state.stageErrorText}
busy={this.state.busy}
inputs={this.props.inputs}
stageState={this.state.stageState}
fail={this._onAuthStageFailed}
setEmailSid={this._setEmailSid}
showContinue={!this.props.continueIsManaged}
onPhaseChange={this._onPhaseChange}
continueText={this.props.continueText}
continueKind={this.props.continueKind}
onCancel={this._onStageCancel}
/>
);
}
_onAuthStageFailed = e => {
this.props.onAuthFinished(false, e);
};
_setEmailSid = sid => {
this._authLogic.setEmailSid(sid);
};
render() {
let error = null;
if (this.state.errorText) {
error = (
<div className="error">
{ this.state.errorText }
</div>
);
}
return (
<div>
<div>
{ this._renderCurrentStage() }
{ error }
</div>
</div>
);
}
}

View file

@ -0,0 +1,300 @@
/*
Copyright 2017 - 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 {
AuthType,
IAuthData,
IAuthDict,
IInputs,
InteractiveAuth,
IStageStatus,
} from "matrix-js-sdk/src/interactive-auth";
import { MatrixClient } from "matrix-js-sdk/src/client";
import React, { createRef } from 'react';
import getEntryComponentForLoginType, { IStageComponent } from '../views/auth/InteractiveAuthEntryComponents';
import Spinner from "../views/elements/Spinner";
import { replaceableComponent } from "../../utils/replaceableComponent";
export const ERROR_USER_CANCELLED = new Error("User cancelled auth session");
interface IProps {
// matrix client to use for UI auth requests
matrixClient: MatrixClient;
// response from initial request. If not supplied, will do a request on mount.
authData?: IAuthData;
// Inputs provided by the user to the auth process
// and used by various stages. As passed to js-sdk
// interactive-auth
inputs?: IInputs;
sessionId?: string;
clientSecret?: string;
emailSid?: string;
// If true, poll to see if the auth flow has been completed out-of-band
poll?: boolean;
// If true, components will be told that the 'Continue' button
// is managed by some other party and should not be managed by
// the component itself.
continueIsManaged?: boolean;
// continueText and continueKind are passed straight through to the AuthEntryComponent.
continueText?: string;
continueKind?: string;
// callback
makeRequest(auth: IAuthData): Promise<IAuthData>;
// callback called when the auth process has finished,
// successfully or unsuccessfully.
// @param {boolean} status True if the operation requiring
// auth was completed successfully, false if canceled.
// @param {object} result The result of the authenticated call
// if successful, otherwise the error object.
// @param {object} extra Additional information about the UI Auth
// process:
// * emailSid {string} If email auth was performed, the sid of
// the auth session.
// * clientSecret {string} The client secret used in auth
// sessions with the ID server.
onAuthFinished(
status: boolean,
result: IAuthData | Error,
extra?: { emailSid?: string, clientSecret?: string },
): void;
// As js-sdk interactive-auth
requestEmailToken?(email: string, secret: string, attempt: number, session: string): Promise<{ sid: string }>;
// Called when the stage changes, or the stage's phase changes. First
// argument is the stage, second is the phase. Some stages do not have
// phases and will be counted as 0 (numeric).
onStagePhaseChange?(stage: string, phase: string | number): void;
}
interface IState {
authStage?: AuthType;
stageState?: IStageStatus;
busy: boolean;
errorText?: string;
stageErrorText?: string;
submitButtonEnabled: boolean;
}
@replaceableComponent("structures.InteractiveAuthComponent")
export default class InteractiveAuthComponent extends React.Component<IProps, IState> {
private readonly authLogic: InteractiveAuth;
private readonly intervalId: number = null;
private readonly stageComponent = createRef<IStageComponent>();
private unmounted = false;
constructor(props) {
super(props);
this.state = {
authStage: null,
busy: false,
errorText: null,
stageErrorText: null,
submitButtonEnabled: false,
};
this.authLogic = new InteractiveAuth({
authData: this.props.authData,
doRequest: this.requestCallback,
busyChanged: this.onBusyChanged,
inputs: this.props.inputs,
stateUpdated: this.authStateUpdated,
matrixClient: this.props.matrixClient,
sessionId: this.props.sessionId,
clientSecret: this.props.clientSecret,
emailSid: this.props.emailSid,
requestEmailToken: this.requestEmailToken,
});
if (this.props.poll) {
this.intervalId = setInterval(() => {
this.authLogic.poll();
}, 2000);
}
}
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount() { // eslint-disable-line @typescript-eslint/naming-convention, camelcase
this.authLogic.attemptAuth().then((result) => {
const extra = {
emailSid: this.authLogic.getEmailSid(),
clientSecret: this.authLogic.getClientSecret(),
};
this.props.onAuthFinished(true, result, extra);
}).catch((error) => {
this.props.onAuthFinished(false, error);
console.error("Error during user-interactive auth:", error);
if (this.unmounted) {
return;
}
const msg = error.message || error.toString();
this.setState({
errorText: msg,
});
});
}
componentWillUnmount() {
this.unmounted = true;
if (this.intervalId !== null) {
clearInterval(this.intervalId);
}
}
private requestEmailToken = async (
email: string,
secret: string,
attempt: number,
session: string,
): Promise<{sid: string}> => {
this.setState({
busy: true,
});
try {
return await this.props.requestEmailToken(email, secret, attempt, session);
} finally {
this.setState({
busy: false,
});
}
};
private tryContinue = (): void => {
this.stageComponent.current?.tryContinue?.();
};
private authStateUpdated = (stageType: AuthType, stageState: IStageStatus): void => {
const oldStage = this.state.authStage;
this.setState({
busy: false,
authStage: stageType,
stageState: stageState,
errorText: stageState.error,
}, () => {
if (oldStage !== stageType) {
this.setFocus();
} else if (!stageState.error) {
this.stageComponent.current?.attemptFailed?.();
}
});
};
private requestCallback = (auth: IAuthData, background: boolean): Promise<IAuthData> => {
// This wrapper just exists because the js-sdk passes a second
// 'busy' param for backwards compat. This throws the tests off
// so discard it here.
return this.props.makeRequest(auth);
};
private onBusyChanged = (busy: boolean): void => {
// if we've started doing stuff, reset the error messages
if (busy) {
this.setState({
busy: true,
errorText: null,
stageErrorText: null,
});
}
// The JS SDK eagerly reports itself as "not busy" right after any
// immediate work has completed, but that's not really what we want at
// the UI layer, so we ignore this signal and show a spinner until
// there's a new screen to show the user. This is implemented by setting
// `busy: false` in `authStateUpdated`.
// See also https://github.com/vector-im/element-web/issues/12546
};
private setFocus(): void {
this.stageComponent.current?.focus?.();
}
private submitAuthDict = (authData: IAuthDict): void => {
this.authLogic.submitAuthDict(authData);
};
private onPhaseChange = (newPhase: number): void => {
this.props.onStagePhaseChange?.(this.state.authStage, newPhase || 0);
};
private onStageCancel = (): void => {
this.props.onAuthFinished(false, ERROR_USER_CANCELLED);
};
private renderCurrentStage(): JSX.Element {
const stage = this.state.authStage;
if (!stage) {
if (this.state.busy) {
return <Spinner />;
} else {
return null;
}
}
const StageComponent = getEntryComponentForLoginType(stage);
return (
<StageComponent
ref={this.stageComponent as any}
loginType={stage}
matrixClient={this.props.matrixClient}
authSessionId={this.authLogic.getSessionId()}
clientSecret={this.authLogic.getClientSecret()}
stageParams={this.authLogic.getStageParams(stage)}
submitAuthDict={this.submitAuthDict}
errorText={this.state.stageErrorText}
busy={this.state.busy}
inputs={this.props.inputs}
stageState={this.state.stageState}
fail={this.onAuthStageFailed}
setEmailSid={this.setEmailSid}
showContinue={!this.props.continueIsManaged}
onPhaseChange={this.onPhaseChange}
continueText={this.props.continueText}
continueKind={this.props.continueKind}
onCancel={this.onStageCancel}
/>
);
}
private onAuthStageFailed = (e: Error): void => {
this.props.onAuthFinished(false, e);
};
private setEmailSid = (sid: string): void => {
this.authLogic.setEmailSid(sid);
};
render() {
let error = null;
if (this.state.errorText) {
error = (
<div className="error">
{ this.state.errorText }
</div>
);
}
return (
<div>
<div>
{ this.renderCurrentStage() }
{ error }
</div>
</div>
);
}
}

View file

@ -542,24 +542,24 @@ class LoggedInView extends React.Component<IProps, IState> {
}
const isModifier = ev.key === Key.ALT || ev.key === Key.CONTROL || ev.key === Key.META || ev.key === Key.SHIFT;
if (!isModifier && !ev.altKey && !ev.ctrlKey && !ev.metaKey) {
if (!isModifier && !ev.ctrlKey && !ev.metaKey) {
// The above condition is crafted to _allow_ characters with Shift
// already pressed (but not the Shift key down itself).
const isClickShortcut = ev.target !== document.body &&
(ev.key === Key.SPACE || ev.key === Key.ENTER);
// Do not capture the context menu key to improve keyboard accessibility
if (ev.key === Key.CONTEXT_MENU) {
return;
}
// We explicitly allow alt to be held due to it being a common accent modifier.
// XXX: Forwarding Dead keys in this way does not work as intended but better to at least
// move focus to the composer so the user can re-type the dead key correctly.
const isPrintable = ev.key.length === 1 || ev.key === "Dead";
if (!isClickShortcut && ev.key !== Key.TAB && !canElementReceiveInput(ev.target)) {
// If the user is entering a printable character outside of an input field
// redirect it to the composer for them.
if (!isClickShortcut && isPrintable && !canElementReceiveInput(ev.target)) {
// synchronous dispatch so we focus before key generates input
dis.fire(Action.FocusSendMessageComposer, true);
ev.stopPropagation();
// we should *not* preventDefault() here as
// that would prevent typing in the now-focussed composer
// we should *not* preventDefault() here as that would prevent typing in the now-focused composer
}
}
};

View file

@ -17,6 +17,7 @@ limitations under the License.
import React, { ChangeEvent, createRef, FormEvent, MouseEvent } from 'react';
import classNames from 'classnames';
import { MatrixClient } from "matrix-js-sdk/src/client";
import { AuthType, IAuthDict, IInputs, IStageStatus } from 'matrix-js-sdk/src/interactive-auth';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
@ -74,33 +75,6 @@ import CaptchaForm from "./CaptchaForm";
* focus: set the input focus appropriately in the form.
*/
enum AuthType {
Password = "m.login.password",
Recaptcha = "m.login.recaptcha",
Terms = "m.login.terms",
Email = "m.login.email.identity",
Msisdn = "m.login.msisdn",
Sso = "m.login.sso",
SsoUnstable = "org.matrix.login.sso",
}
/* eslint-disable camelcase */
interface IAuthDict {
type?: AuthType;
// TODO: Remove `user` once servers support proper UIA
// See https://github.com/vector-im/element-web/issues/10312
user?: string;
identifier?: any;
password?: string;
response?: string;
// TODO: Remove `threepid_creds` once servers support proper UIA
// See https://github.com/vector-im/element-web/issues/10312
// See https://github.com/matrix-org/matrix-doc/issues/2220
threepid_creds?: any;
threepidCreds?: any;
}
/* eslint-enable camelcase */
export const DEFAULT_PHASE = 0;
interface IAuthEntryProps {
@ -835,7 +809,26 @@ export class FallbackAuthEntry extends React.Component<IAuthEntryProps> {
}
}
export default function getEntryComponentForLoginType(loginType: AuthType): typeof React.Component {
export interface IStageComponentProps extends IAuthEntryProps {
clientSecret?: string;
stageParams?: Record<string, any>;
inputs?: IInputs;
stageState?: IStageStatus;
showContinue?: boolean;
continueText?: string;
continueKind?: string;
fail?(e: Error): void;
setEmailSid?(sid: string): void;
onCancel?(): void;
}
export interface IStageComponent extends React.ComponentClass<React.PropsWithRef<IStageComponentProps>> {
tryContinue?(): void;
attemptFailed?(): void;
focus?(): void;
}
export default function getEntryComponentForLoginType(loginType: AuthType): IStageComponent {
switch (loginType) {
case AuthType.Password:
return PasswordAuthEntry;

View file

@ -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<IProps, ISt
this.initAuth(/* shouldErase= */false);
}
private onStagePhaseChange = (stage: string, phase: string): void => {
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<IProps, ISt
this.setState({ errStr: _t("There was a problem communicating with the server. Please try again.") });
};
private onUIAuthComplete = (auth: any): void => {
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<IProps, ISt
<InteractiveAuth
matrixClient={MatrixClientPeg.get()}
authData={this.state.authData}
makeRequest={this.onUIAuthComplete}
// XXX: onUIAuthComplete breaches the expected method contract, it gets away with it because it
// knows the entire app is about to die as a result of the account deactivation.
makeRequest={this.onUIAuthComplete as any}
onAuthFinished={this.onUIAuthFinished}
onStagePhaseChange={this.onStagePhaseChange}
continueText={this.state.continueText}

View file

@ -175,7 +175,7 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
} else if (hangupReason === CallErrorCode.InviteTimeout) {
return (
<div className="mx_CallEvent_content">
{ _t("Missed call") }
{ _t("No answer") }
{ this.renderCallBackButton(_t("Call back")) }
</div>
);
@ -199,7 +199,7 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
} 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 (
@ -249,10 +249,9 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
mx_CallEvent_voice: isVoice,
mx_CallEvent_video: !isVoice,
mx_CallEvent_narrow: this.state.narrow,
mx_CallEvent_missed: (
callState === CustomCallState.Missed ||
(callState === CallState.Ended && hangupReason === CallErrorCode.InviteTimeout)
),
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) {

View file

@ -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<IProps, IState> {
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<IProps, IState> {
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<IProps, IState> {
this.autocompleter.destroy();
}
complete(query: string, selection: ISelectionRange) {
private complete(query: string, selection: ISelectionRange): Promise<void> {
this.queryRequested = query;
if (this.debounceCompletionsRequest) {
clearTimeout(this.debounceCompletionsRequest);
@ -115,7 +114,7 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
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<IProps, IState> {
});
}
processQuery(query: string, selection: ISelectionRange) {
private processQuery(query: string, selection: ISelectionRange): Promise<void> {
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<IProps, IState> {
});
}
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<IProps, IState> {
});
}
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<IProps, IState> {
this.hide();
}
hide = () => {
private hide = (): void => {
this.setState({
hide: true,
selectionOffset: 0,
selectionOffset: 1,
completions: [],
completionList: [],
});
};
forceComplete() {
public forceComplete(): Promise<number> {
return new Promise((resolve) => {
this.setState({
forceComplete: true,
@ -235,8 +239,13 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
});
}
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<IProps, IState> {
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<IProps, IState> {
});
return completions.length > 0 ? (
<div key={i} className="mx_Autocomplete_ProviderSection">
<div key={i} className="mx_Autocomplete_ProviderSection" role="presentation">
<div className="mx_Autocomplete_provider_name">{ completionResult.provider.getName() }</div>
{ completionResult.provider.renderCompletions(completions) }
</div>
@ -300,7 +309,7 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
}).filter((completion) => !!completion);
return !this.state.hide && renderedCompletions.length > 0 ? (
<div className="mx_Autocomplete" ref={this.containerRef}>
<div id="mx_Autocomplete" className="mx_Autocomplete" ref={this.containerRef} role="listbox">
{ renderedCompletions }
</div>
) : null;

View file

@ -133,6 +133,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
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<IProps, IState>
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<IProps, IState>
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<IProps, IState>
}
}
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<IProps, IState>
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<IProps, IState>
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<IProps, IState>
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<IProps, IState>
};
const { completionIndex } = this.state;
const hasAutocomplete = Boolean(this.state.autoComplete);
let activeDescendant;
if (hasAutocomplete && completionIndex >= 0) {
activeDescendant = generateCompletionDomId(completionIndex);
}
return (<div className={wrapperClasses}>
{ autoComplete }
@ -736,10 +749,11 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
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}
/>

View file

@ -932,8 +932,11 @@ export default class EventTile extends React.Component<IProps, IState> {
} 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) ||
this.props.mxEvent.getType() === EventType.CallInvite
) {
// no avatar or sender profile for continuation messages and call tiles
avatarSize = 0;
needsSenderProfile = false;
} else {

View file

@ -24,36 +24,39 @@ 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';
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 +64,28 @@ 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 = () => {
this.bootstrapCrossSigning({ forceReset: false });
};
onStatusChanged = () => {
this._getUpdatedStatus();
private onStatusChanged = () => {
this.getUpdatedStatus();
};
async _getUpdatedStatus() {
private async getUpdatedStatus(): Promise<void> {
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 +113,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<void> => {
this.setState({ error: undefined });
try {
const cli = MatrixClientPeg.get();
await cli.bootstrapCrossSigning({
@ -135,20 +138,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,
@ -208,7 +211,7 @@ 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) {
actions.push(
<AccessibleButton key="setup" kind="primary" onClick={this._onBootstrapClick}>
<AccessibleButton key="setup" kind="primary" onClick={this.onBootstrapClick}>
{ _t("Set up") }
</AccessibleButton>,
);
@ -216,7 +219,7 @@ export default class CrossSigningPanel extends React.PureComponent {
if (keysExistAnywhere) {
actions.push(
<AccessibleButton key="reset" kind="danger" onClick={this._resetCrossSigning}>
<AccessibleButton key="reset" kind="danger" onClick={this.resetCrossSigning}>
{ _t("Reset") }
</AccessibleButton>,
);

View file

@ -32,7 +32,6 @@ export type GetAutocompleterComponent = () => Autocomplete;
export type UpdateQuery = (test: string) => Promise<void>;
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<void> {
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<void> {
// 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),

View file

@ -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);
}

View file

@ -1887,13 +1887,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",

View file

@ -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.

View file

@ -116,14 +116,14 @@ 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")
);
let isInfoMessage = (
!isBubbleMessage &&
eventType !== EventType.RoomMessage &&
eventType !== EventType.Sticker &&
eventType !== EventType.RoomCreate
eventType !== EventType.RoomCreate &&
eventType !== EventType.CallInvite
);
// If we're showing hidden events in the timeline, we should use the