Merge remote-tracking branch 'origin/develop' into dbkr/crosssigningpanel_ts
This commit is contained in:
commit
c79b6a412d
29 changed files with 499 additions and 488 deletions
|
@ -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) => (
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
300
src/components/structures/InteractiveAuth.tsx
Normal file
300
src/components/structures/InteractiveAuth.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -529,24 +529,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
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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)",
|
||||
"Retry": "Retry",
|
||||
"Missed call": "Missed call",
|
||||
"The call is in an unknown state!": "The call is in an unknown state!",
|
||||
"Sunday": "Sunday",
|
||||
"Monday": "Monday",
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue