Merge branch 'develop' into travis/ft-sep1620/04-jitsi-hangup

This commit is contained in:
Travis Ralston 2020-09-28 13:43:47 -06:00
commit 9df175212e
144 changed files with 3643 additions and 1968 deletions

View file

@ -25,6 +25,7 @@ import EventIndexPeg from "../../indexing/EventIndexPeg";
import { _t } from '../../languageHandler';
import BaseCard from "../views/right_panel/BaseCard";
import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
import DesktopBuildsNotice, {WarningKind} from "../views/elements/DesktopBuildsNotice";
/*
* Component which shows the filtered file using a TimelinePanel
@ -222,6 +223,8 @@ class FilePanel extends React.Component {
<p>{_t('Attach files from chat or just drag and drop them anywhere in a room.')}</p>
</div>);
const isRoomEncrypted = this.noRoom ? false : MatrixClientPeg.get().isRoomEncrypted(this.props.roomId);
if (this.state.timelineSet) {
// console.log("rendering TimelinePanel for timelineSet " + this.state.timelineSet.room.roomId + " " +
// "(" + this.state.timelineSet._timelines.join(", ") + ")" + " with key " + this.props.roomId);
@ -232,6 +235,7 @@ class FilePanel extends React.Component {
previousPhase={RightPanelPhases.RoomSummary}
withoutScrollContainer
>
<DesktopBuildsNotice isRoomEncrypted={isRoomEncrypted} kind={WarningKind.Files} />
<TimelinePanel
manageReadReceipts={false}
manageReadMarkers={false}

View file

@ -52,7 +52,7 @@ interface IState {
// List of CSS classes which should be included in keyboard navigation within the room list
const cssClasses = [
"mx_RoomSearch_input",
"mx_RoomSearch_icon", // minimized <RoomSearch />
"mx_RoomSearch_minimizedHandle", // minimized <RoomSearch />
"mx_RoomSublist_headerText",
"mx_RoomTile",
"mx_RoomSublist_showNButton",

View file

@ -85,7 +85,6 @@ interface IProps {
threepidInvite?: IThreepidInvite;
roomOobData?: object;
currentRoomId: string;
ConferenceHandler?: object;
collapseLhs: boolean;
config: {
piwik: {
@ -637,7 +636,6 @@ class LoggedInView extends React.Component<IProps, IState> {
viaServers={this.props.viaServers}
key={this.props.currentRoomId || 'roomview'}
disabled={this.props.middleDisabled}
ConferenceHandler={this.props.ConferenceHandler}
resizeNotifier={this.props.resizeNotifier}
/>;
break;

View file

@ -79,6 +79,8 @@ import { SettingLevel } from "../../settings/SettingLevel";
import { leaveRoomBehaviour } from "../../utils/membership";
import CreateCommunityPrototypeDialog from "../views/dialogs/CreateCommunityPrototypeDialog";
import ThreepidInviteStore, { IThreepidInvite, IThreepidInviteWireFormat } from "../../stores/ThreepidInviteStore";
import {UIFeature} from "../../settings/UIFeature";
import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
/** constants for MatrixChat.state.view */
export enum Views {
@ -147,7 +149,6 @@ interface IRoomInfo {
interface IProps { // TODO type things better
config: Record<string, any>;
serverConfig?: ValidatedServerConfig;
ConferenceHandler?: any;
onNewScreen: (screen: string, replaceLast: boolean) => void;
enableGuest?: boolean;
// the queryParams extracted from the [real] query-string of the URI
@ -1015,6 +1016,18 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
private async createRoom(defaultPublic = false) {
const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
if (communityId) {
// double check the user will have permission to associate this room with the community
if (CommunityPrototypeStore.instance.isAdminOf(communityId)) {
Modal.createTrackedDialog('Pre-failure to create room', '', ErrorDialog, {
title: _t("Cannot create rooms in this community"),
description: _t("You do not have permission to create rooms in this community."),
});
return;
}
}
const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog');
const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, { defaultPublic });
@ -1372,15 +1385,19 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
ready: true,
});
});
cli.on('Call.incoming', function(call) {
// we dispatch this synchronously to make sure that the event
// handlers on the call are set up immediately (so that if
// we get an immediate hangup, we don't get a stuck call)
dis.dispatch({
action: 'incoming_call',
call: call,
}, true);
});
if (SettingsStore.getValue(UIFeature.Voip)) {
cli.on('Call.incoming', function(call) {
// we dispatch this synchronously to make sure that the event
// handlers on the call are set up immediately (so that if
// we get an immediate hangup, we don't get a stuck call)
dis.dispatch({
action: 'incoming_call',
call: call,
}, true);
});
}
cli.on('Session.logged_out', function(errObj) {
if (Lifecycle.isLoggingOut()) return;
@ -1496,12 +1513,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
if (haveNewVersion) {
Modal.createTrackedDialogAsync('New Recovery Method', 'New Recovery Method',
import('../../async-components/views/dialogs/keybackup/NewRecoveryMethodDialog'),
import('../../async-components/views/dialogs/security/NewRecoveryMethodDialog'),
{ newVersionInfo },
);
} else {
Modal.createTrackedDialogAsync('Recovery Method Removed', 'Recovery Method Removed',
import('../../async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog'),
import('../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog'),
);
}
});
@ -1838,7 +1855,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} else {
subtitle = `${this.subTitleStatus} ${subtitle}`;
}
document.title = `${SdkConfig.get().brand} ${subtitle}`;
const title = `${SdkConfig.get().brand} ${subtitle}`;
if (document.title !== title) {
document.title = title;
}
}
updateStatusIndicator(state: string, prevState: string) {
@ -1876,6 +1898,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
return this.props.makeRegistrationUrl(params);
};
/**
* After registration or login, we run various post-auth steps before entering the app
* proper, such setting up cross-signing or verifying the new session.
*
* Note: SSO users (and any others using token login) currently do not pass through
* this, as they instead jump straight into the app after `attemptTokenLogin`.
*/
onUserCompletedLoginFlow = async (credentials: object, password: string) => {
this.accountPassword = password;
// self-destruct the password after 5mins
@ -1942,7 +1971,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
render() {
const fragmentAfterLogin = this.getFragmentAfterLogin();
let view;
let view = null;
if (this.state.view === Views.LOADING) {
const Spinner = sdk.getComponent('elements.Spinner');
@ -2021,7 +2050,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} else if (this.state.view === Views.WELCOME) {
const Welcome = sdk.getComponent('auth.Welcome');
view = <Welcome />;
} else if (this.state.view === Views.REGISTER) {
} else if (this.state.view === Views.REGISTER && SettingsStore.getValue(UIFeature.Registration)) {
const Registration = sdk.getComponent('structures.auth.Registration');
const email = ThreepidInviteStore.instance.pickBestInvite()?.toEmail;
view = (
@ -2039,7 +2068,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
{...this.getServerProperties()}
/>
);
} else if (this.state.view === Views.FORGOT_PASSWORD) {
} else if (this.state.view === Views.FORGOT_PASSWORD && SettingsStore.getValue(UIFeature.PasswordReset)) {
const ForgotPassword = sdk.getComponent('structures.auth.ForgotPassword');
view = (
<ForgotPassword
@ -2050,6 +2079,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
/>
);
} else if (this.state.view === Views.LOGIN) {
const showPasswordReset = SettingsStore.getValue(UIFeature.PasswordReset);
const Login = sdk.getComponent('structures.auth.Login');
view = (
<Login
@ -2058,7 +2088,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
onRegisterClick={this.onRegisterClick}
fallbackHsUrl={this.getFallbackHsUrl()}
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
onForgotPasswordClick={this.onForgotPasswordClick}
onForgotPasswordClick={showPasswordReset ? this.onForgotPasswordClick : undefined}
onServerConfigChange={this.onServerConfigChange}
fragmentAfterLogin={fragmentAfterLogin}
{...this.getServerProperties()}

View file

@ -135,6 +135,9 @@ export default class MessagePanel extends React.Component {
// whether to use the irc layout
useIRCLayout: PropTypes.bool,
// whether or not to show flair at all
enableFlair: PropTypes.bool,
};
// Force props to be loaded for useIRCLayout
@ -515,10 +518,13 @@ export default class MessagePanel extends React.Component {
if (!grouper) {
const wantTile = this._shouldShowEvent(mxEv);
if (wantTile) {
const nextEvent = i < this.props.events.length - 1
? this.props.events[i + 1]
: null;
// make sure we unpack the array returned by _getTilesForEvent,
// otherwise react will auto-generate keys and we will end up
// replacing all of the DOM elements every time we paginate.
ret.push(...this._getTilesForEvent(prevEvent, mxEv, last));
ret.push(...this._getTilesForEvent(prevEvent, mxEv, last, nextEvent));
prevEvent = mxEv;
}
@ -534,7 +540,7 @@ export default class MessagePanel extends React.Component {
return ret;
}
_getTilesForEvent(prevEvent, mxEv, last) {
_getTilesForEvent(prevEvent, mxEv, last, nextEvent) {
const TileErrorBoundary = sdk.getComponent('messages.TileErrorBoundary');
const EventTile = sdk.getComponent('rooms.EventTile');
const DateSeparator = sdk.getComponent('messages.DateSeparator');
@ -559,6 +565,11 @@ export default class MessagePanel extends React.Component {
ret.push(dateSeparator);
}
let willWantDateSeparator = false;
if (nextEvent) {
willWantDateSeparator = this._wantsDateSeparator(mxEv, nextEvent.getDate() || new Date());
}
// is this a continuation of the previous message?
const continuation = !wantsDateSeparator && shouldFormContinuation(prevEvent, mxEv);
@ -579,7 +590,8 @@ export default class MessagePanel extends React.Component {
data-scroll-tokens={scrollToken}
>
<TileErrorBoundary mxEvent={mxEv}>
<EventTile mxEvent={mxEv}
<EventTile
mxEvent={mxEv}
continuation={continuation}
isRedacted={mxEv.isRedacted()}
replacingEventId={mxEv.replacingEventId()}
@ -594,10 +606,12 @@ export default class MessagePanel extends React.Component {
isTwelveHour={this.props.isTwelveHour}
permalinkCreator={this.props.permalinkCreator}
last={last}
lastInSection={willWantDateSeparator}
isSelectedEvent={highlight}
getRelationsForEvent={this.props.getRelationsForEvent}
showReactions={this.props.showReactions}
useIRCLayout={this.props.useIRCLayout}
enableFlair={this.props.enableFlair}
/>
</TileErrorBoundary>
</li>,

View file

@ -70,10 +70,10 @@ export default class RoomDirectory extends React.Component {
this.scrollPanel = null;
this.protocols = null;
this.setState({protocolsLoading: true});
this.state.protocolsLoading = true;
if (!MatrixClientPeg.get()) {
// We may not have a client yet when invoked from welcome page
this.setState({protocolsLoading: false});
this.state.protocolsLoading = false;
return;
}
@ -102,14 +102,16 @@ export default class RoomDirectory extends React.Component {
});
} else {
// We don't use the protocols in the communities v2 prototype experience
this.setState({protocolsLoading: false});
this.state.protocolsLoading = false;
// Grab the profile info async
FlairStore.getGroupProfileCached(MatrixClientPeg.get(), this.state.selectedCommunityId).then(profile => {
this.setState({communityName: profile.name});
});
}
}
componentDidMount() {
this.refreshRoomList();
}
@ -390,22 +392,12 @@ export default class RoomDirectory extends React.Component {
};
onPreviewClick = (ev, room) => {
this.props.onFinished();
dis.dispatch({
action: 'view_room',
room_id: room.room_id,
should_peek: true,
});
this.showRoom(room, null, false, true);
ev.stopPropagation();
};
onViewClick = (ev, room) => {
this.props.onFinished();
dis.dispatch({
action: 'view_room',
room_id: room.room_id,
should_peek: false,
});
this.showRoom(room);
ev.stopPropagation();
};
@ -426,11 +418,12 @@ export default class RoomDirectory extends React.Component {
this.showRoom(null, alias, autoJoin);
}
showRoom(room, room_alias, autoJoin=false) {
showRoom(room, room_alias, autoJoin = false, shouldPeek = false) {
this.props.onFinished();
const payload = {
action: 'view_room',
auto_join: autoJoin,
should_peek: shouldPeek,
};
if (room) {
// Don't let the user view a room they won't be able to either
@ -455,6 +448,7 @@ export default class RoomDirectory extends React.Component {
};
if (this.state.roomServer) {
payload.via_servers = [this.state.roomServer];
payload.opts = {
viaServers: [this.state.roomServer],
};

View file

@ -165,7 +165,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
icon = (
<AccessibleButton
title={_t("Search rooms")}
className="mx_RoomSearch_icon"
className="mx_RoomSearch_icon mx_RoomSearch_minimizedHandle"
onClick={this.openSearch}
/>
);

View file

@ -65,12 +65,10 @@ import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
import ForwardMessage from "../views/rooms/ForwardMessage";
import SearchBar from "../views/rooms/SearchBar";
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
import RoomRecoveryReminder from "../views/rooms/RoomRecoveryReminder";
import PinnedEventsPanel from "../views/rooms/PinnedEventsPanel";
import AuxPanel from "../views/rooms/AuxPanel";
import RoomHeader from "../views/rooms/RoomHeader";
import TintableSvg from "../views/elements/TintableSvg";
import type * as ConferenceHandler from '../../VectorConferenceHandler';
import {XOR} from "../../@types/common";
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
@ -85,8 +83,6 @@ if (DEBUG) {
}
interface IProps {
ConferenceHandler?: ConferenceHandler;
threepidInvite: IThreepidInvite,
// Any data about the room that would normally come from the homeserver
@ -182,7 +178,6 @@ export interface IState {
matrixClientIsReady: boolean;
showUrlPreview?: boolean;
e2eStatus?: E2EStatus;
displayConfCallNotification?: boolean;
rejecting?: boolean;
rejectError?: Error;
}
@ -489,8 +484,6 @@ export default class RoomView extends React.Component<IProps, IState> {
callState: callState,
});
this.updateConfCallNotification();
window.addEventListener('beforeunload', this.onPageUnload);
if (this.props.resizeNotifier) {
this.props.resizeNotifier.on("middlePanelResized", this.onResize);
@ -673,9 +666,10 @@ export default class RoomView extends React.Component<IProps, IState> {
handled = true;
}
break;
case Key.U: // Mac returns lowercase
case Key.U.toUpperCase():
if (isOnlyCtrlOrCmdIgnoreShiftKeyEvent(ev) && ev.shiftKey) {
dis.dispatch({ action: "upload_file" });
dis.dispatch({ action: "upload_file" }, true);
handled = true;
}
break;
@ -724,10 +718,6 @@ export default class RoomView extends React.Component<IProps, IState> {
callState = call.call_state;
}
// possibly remove the conf call notification if we're now in
// the conf
this.updateConfCallNotification();
this.setState({
callState: callState,
});
@ -816,12 +806,6 @@ export default class RoomView extends React.Component<IProps, IState> {
}
};
private onRoomRecoveryReminderDontAskAgain = () => {
// Called when the option to not ask again is set:
// force an update to hide the recovery reminder
this.forceUpdate();
};
private onKeyBackupStatus = () => {
// Key backup status changes affect whether the in-room recovery
// reminder is displayed.
@ -1024,9 +1008,6 @@ export default class RoomView extends React.Component<IProps, IState> {
// rate limited because a power level change will emit an event for every member in the room.
private updateRoomMembers = rateLimitedFunc((dueToMember) => {
// a member state changed in this room
// refresh the conf call notification state
this.updateConfCallNotification();
this.updateDMState();
let memberCountInfluence = 0;
@ -1055,30 +1036,6 @@ export default class RoomView extends React.Component<IProps, IState> {
this.setState({isAlone: joinedOrInvitedMemberCount === 1});
}
private updateConfCallNotification() {
const room = this.state.room;
if (!room || !this.props.ConferenceHandler) {
return;
}
const confMember = room.getMember(
this.props.ConferenceHandler.getConferenceUserIdForRoom(room.roomId),
);
if (!confMember) {
return;
}
const confCall = this.props.ConferenceHandler.getConferenceCallForRoom(confMember.roomId);
// A conf call notification should be displayed if there is an ongoing
// conf call but this cilent isn't a part of it.
this.setState({
displayConfCallNotification: (
(!confCall || confCall.call_state === "ended") &&
confMember.membership === "join"
),
});
}
private updateDMState() {
const room = this.state.room;
if (room.getMyMembership() != "join") {
@ -1687,7 +1644,7 @@ export default class RoomView extends React.Component<IProps, IState> {
if (!this.state.room) {
return null;
}
return CallHandler.getCallForRoom(this.state.room.roomId);
return CallHandler.sharedInstance().getCallForRoom(this.state.room.roomId);
}
// this has to be a proper method rather than an unnamed function,
@ -1858,13 +1815,6 @@ export default class RoomView extends React.Component<IProps, IState> {
this.state.room.userMayUpgradeRoom(this.context.credentials.userId)
);
const showRoomRecoveryReminder = (
this.context.isCryptoEnabled() &&
SettingsStore.getValue("showRoomRecoveryReminder") &&
this.context.isRoomEncrypted(this.state.room.roomId) &&
this.context.getKeyBackupEnabled() === false
);
const hiddenHighlightCount = this.getHiddenHighlightCount();
let aux = null;
@ -1879,13 +1829,11 @@ export default class RoomView extends React.Component<IProps, IState> {
searchInProgress={this.state.searchInProgress}
onCancelClick={this.onCancelSearchClick}
onSearch={this.onSearch}
isRoomEncrypted={this.context.isRoomEncrypted(this.state.room.roomId)}
/>;
} else if (showRoomUpgradeBar) {
aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />;
hideCancel = true;
} else if (showRoomRecoveryReminder) {
aux = <RoomRecoveryReminder onDontAskAgainSet={this.onRoomRecoveryReminderDontAskAgain} />;
hideCancel = true;
} else if (this.state.showingPinned) {
hideCancel = true; // has own cancel
aux = <PinnedEventsPanel room={this.state.room} onCancelClick={this.onPinnedClick} />;
@ -1940,9 +1888,7 @@ export default class RoomView extends React.Component<IProps, IState> {
room={this.state.room}
fullHeight={false}
userId={this.context.credentials.userId}
conferenceHandler={this.props.ConferenceHandler}
draggingFile={this.state.draggingFile}
displayConfCallNotification={this.state.displayConfCallNotification}
maxHeight={this.state.auxPanelMaxHeight}
showApps={this.state.showApps}
hideAppsDrawer={false}

View file

@ -35,6 +35,7 @@ import Timer from '../../utils/Timer';
import shouldHideEvent from '../../shouldHideEvent';
import EditorStateTransfer from '../../utils/EditorStateTransfer';
import {haveTileForEvent} from "../views/rooms/EventTile";
import {UIFeature} from "../../settings/UIFeature";
const PAGINATE_SIZE = 20;
const INITIAL_SIZE = 20;
@ -1446,6 +1447,7 @@ class TimelinePanel extends React.Component {
editState={this.state.editState}
showReactions={this.props.showReactions}
useIRCLayout={this.props.useIRCLayout}
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
/>
);
}

View file

@ -343,6 +343,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
let secondarySection = null;
if (prototypeCommunityName) {
const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
primaryHeader = (
<div className="mx_UserMenu_contextMenu_name">
<span className="mx_UserMenu_contextMenu_displayName">
@ -350,24 +351,36 @@ export default class UserMenu extends React.Component<IProps, IState> {
</span>
</div>
);
primaryOptionList = (
<IconizedContextMenuOptionList>
let settingsOption;
let inviteOption;
if (CommunityPrototypeStore.instance.canInviteTo(communityId)) {
inviteOption = (
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconInvite"
label={_t("Invite")}
onClick={this.onCommunityInviteClick}
/>
);
}
if (CommunityPrototypeStore.instance.isAdminOf(communityId)) {
settingsOption = (
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconSettings"
label={_t("Settings")}
aria-label={_t("Community settings")}
onClick={this.onCommunitySettingsClick}
/>
);
}
primaryOptionList = (
<IconizedContextMenuOptionList>
{settingsOption}
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconMembers"
label={_t("Members")}
onClick={this.onCommunityMembersClick}
/>
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconInvite"
label={_t("Invite")}
onClick={this.onCommunityInviteClick}
/>
{inviteOption}
</IconizedContextMenuOptionList>
);
secondarySection = (

View file

@ -16,8 +16,9 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import AsyncWrapper from '../../../AsyncWrapper';
import * as sdk from '../../../index';
import AuthPage from '../../views/auth/AuthPage';
import CompleteSecurityBody from '../../views/auth/CompleteSecurityBody';
import CreateCrossSigningDialog from '../../views/dialogs/security/CreateCrossSigningDialog';
export default class E2eSetup extends React.Component {
static propTypes = {
@ -25,21 +26,11 @@ export default class E2eSetup extends React.Component {
accountPassword: PropTypes.string,
};
constructor() {
super();
// awkwardly indented because https://github.com/eslint/eslint/issues/11310
this._createStorageDialogPromise =
import("../../../async-components/views/dialogs/secretstorage/CreateSecretStorageDialog");
}
render() {
const AuthPage = sdk.getComponent("auth.AuthPage");
const CompleteSecurityBody = sdk.getComponent("auth.CompleteSecurityBody");
return (
<AuthPage>
<CompleteSecurityBody>
<AsyncWrapper prom={this._createStorageDialogPromise}
hasCancel={false}
<CreateCrossSigningDialog
onFinished={this.props.onFinished}
accountPassword={this.props.accountPassword}
/>

View file

@ -28,6 +28,8 @@ import classNames from "classnames";
import AuthPage from "../../views/auth/AuthPage";
import SSOButton from "../../views/elements/SSOButton";
import PlatformPeg from '../../../PlatformPeg';
import SettingsStore from "../../../settings/SettingsStore";
import {UIFeature} from "../../../settings/UIFeature";
// For validating phone numbers without country codes
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
@ -679,7 +681,7 @@ export default class LoginComponent extends React.Component {
{_t("If you've joined lots of rooms, this might take a while")}
</div> }
</div>;
} else {
} else if (SettingsStore.getValue(UIFeature.Registration)) {
footer = (
<a className="mx_AuthBody_changeFlow" onClick={this.onTryRegisterClick} href="#">
{ _t('Create account') }

View file

@ -40,11 +40,7 @@ interface IProps {
onValidate(result: IValidationResult);
}
interface IState {
complexity: zxcvbn.ZXCVBNResult;
}
class PassphraseField extends PureComponent<IProps, IState> {
class PassphraseField extends PureComponent<IProps> {
static defaultProps = {
label: _td("Password"),
labelEnterPassword: _td("Enter password"),
@ -52,14 +48,16 @@ class PassphraseField extends PureComponent<IProps, IState> {
labelAllowedButUnsafe: _td("Password is allowed, but unsafe"),
};
state = { complexity: null };
public readonly validate = withValidation<this>({
description: function() {
const complexity = this.state.complexity;
public readonly validate = withValidation<this, zxcvbn.ZXCVBNResult>({
description: function(complexity) {
const score = complexity ? complexity.score : 0;
return <progress className="mx_PassphraseField_progress" max={4} value={score} />;
},
deriveData: async ({ value }) => {
if (!value) return null;
const { scorePassword } = await import('../../../utils/PasswordScorer');
return scorePassword(value);
},
rules: [
{
key: "required",
@ -68,28 +66,24 @@ class PassphraseField extends PureComponent<IProps, IState> {
},
{
key: "complexity",
test: async function({ value }) {
test: async function({ value }, complexity) {
if (!value) {
return false;
}
const { scorePassword } = await import('../../../utils/PasswordScorer');
const complexity = scorePassword(value);
this.setState({ complexity });
const safe = complexity.score >= this.props.minScore;
const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"];
return allowUnsafe || safe;
},
valid: function() {
valid: function(complexity) {
// Unsafe passwords that are valid are only possible through a
// configuration flag. We'll print some helper text to signal
// to the user that their password is allowed, but unsafe.
if (this.state.complexity.score >= this.props.minScore) {
if (complexity.score >= this.props.minScore) {
return _t(this.props.labelStrongPassword);
}
return _t(this.props.labelAllowedButUnsafe);
},
invalid: function() {
const complexity = this.state.complexity;
invalid: function(complexity) {
if (!complexity) {
return null;
}

View file

@ -15,10 +15,14 @@ limitations under the License.
*/
import React from 'react';
import classNames from "classnames";
import * as sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
import AuthPage from "./AuthPage";
import {_td} from "../../../languageHandler";
import SettingsStore from "../../../settings/SettingsStore";
import {UIFeature} from "../../../settings/UIFeature";
// translatable strings for Welcome pages
_td("Sign in with SSO");
@ -39,7 +43,9 @@ export default class Welcome extends React.PureComponent {
return (
<AuthPage>
<div className="mx_Welcome">
<div className={classNames("mx_Welcome", {
mx_WelcomePage_registrationDisabled: !SettingsStore.getValue(UIFeature.Registration),
})}>
<EmbeddedPage
className="mx_WelcomePage"
url={pageUrl}

View file

@ -45,7 +45,11 @@ export default class CreateRoomDialog extends React.Component {
detailsOpen: false,
noFederate: config.default_federate === false,
nameIsValid: false,
canChangeEncryption: true,
};
MatrixClientPeg.get().doesServerForceEncryptionForPreset("private")
.then(isForced => this.setState({canChangeEncryption: !isForced}));
}
_roomCreateOptions() {
@ -68,7 +72,13 @@ export default class CreateRoomDialog extends React.Component {
}
if (!this.state.isPublic) {
opts.encryption = this.state.isEncrypted;
if (this.state.canChangeEncryption) {
opts.encryption = this.state.isEncrypted;
} else {
// the server should automatically do this for us, but for safety
// we'll demand it too.
opts.encryption = true;
}
}
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
@ -208,7 +218,11 @@ export default class CreateRoomDialog extends React.Component {
if (!this.state.isPublic) {
let microcopy;
if (privateShouldBeEncrypted()) {
microcopy = _t("You cant disable this later. Bridges & most bots wont work yet.");
if (this.state.canChangeEncryption) {
microcopy = _t("You cant disable this later. Bridges & most bots wont work yet.");
} else {
microcopy = _t("Your server requires encryption to be enabled in private rooms.");
}
} else {
microcopy = _t("Your server admin has disabled end-to-end encryption by default " +
"in private rooms & Direct Messages.");
@ -219,6 +233,7 @@ export default class CreateRoomDialog extends React.Component {
onChange={this.onEncryptedChange}
value={this.state.isEncrypted}
className='mx_CreateRoomDialog_e2eSwitch' // for end-to-end tests
disabled={!this.state.canChangeEncryption}
/>
<p>{ microcopy }</p>
</React.Fragment>;

View file

@ -38,6 +38,8 @@ import {Action} from "../../../dispatcher/actions";
import {DefaultTagID} from "../../../stores/room-list/models";
import RoomListStore from "../../../stores/room-list/RoomListStore";
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
import SettingsStore from "../../../settings/SettingsStore";
import {UIFeature} from "../../../settings/UIFeature";
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
/* eslint-disable camelcase */
@ -549,7 +551,7 @@ export default class InviteDialog extends React.PureComponent {
if (this.state.filterText.startsWith('@')) {
// Assume mxid
newMember = new DirectoryMember({user_id: this.state.filterText, display_name: null, avatar_url: null});
} else {
} else if (SettingsStore.getValue(UIFeature.IdentityServer)) {
// Assume email
newMember = new ThreepidMember(this.state.filterText);
}
@ -734,7 +736,7 @@ export default class InviteDialog extends React.PureComponent {
this.setState({tryingIdentityServer: true});
return;
}
if (term.indexOf('@') > 0 && Email.looksValid(term)) {
if (term.indexOf('@') > 0 && Email.looksValid(term) && SettingsStore.getValue(UIFeature.IdentityServer)) {
// Start off by suggesting the plain email while we try and resolve it
// to a real account.
this.setState({
@ -1037,7 +1039,9 @@ export default class InviteDialog extends React.PureComponent {
}
_renderIdentityServerWarning() {
if (!this.state.tryingIdentityServer || this.state.canUseIdentityServer) {
if (!this.state.tryingIdentityServer || this.state.canUseIdentityServer ||
!SettingsStore.getValue(UIFeature.IdentityServer)
) {
return null;
}
@ -1086,22 +1090,38 @@ export default class InviteDialog extends React.PureComponent {
let buttonText;
let goButtonFn;
const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer);
const userId = MatrixClientPeg.get().getUserId();
if (this.props.kind === KIND_DM) {
title = _t("Direct Messages");
helpText = _t(
"Start a conversation with someone using their name, username (like <userId/>) or email address.",
{},
{userId: () => {
return <a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>;
}},
);
if (identityServersEnabled) {
helpText = _t(
"Start a conversation with someone using their name, username (like <userId/>) or email address.",
{},
{userId: () => {
return (
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>
);
}},
);
} else {
helpText = _t(
"Start a conversation with someone using their name or username (like <userId/>).",
{},
{userId: () => {
return (
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>
);
}},
);
}
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
const communityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
helpText = _t(
"Start a conversation with someone using their name, username (like <userId/>) or email address. " +
"This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click " +
"<a>here</a>.",
const inviteText = _t("This won't invite them to %(communityName)s. " +
"To invite someone to %(communityName)s, click <a>here</a>",
{communityName}, {
userId: () => {
return (
@ -1122,21 +1142,40 @@ export default class InviteDialog extends React.PureComponent {
},
},
);
helpText = <React.Fragment>
{ helpText } {inviteText}
</React.Fragment>;
}
buttonText = _t("Go");
goButtonFn = this._startDm;
} else { // KIND_INVITE
title = _t("Invite to this room");
helpText = _t(
"Invite someone using their name, username (like <userId/>), email address or <a>share this room</a>.",
{},
{
userId: () =>
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
a: (sub) =>
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{sub}</a>,
},
);
if (identityServersEnabled) {
helpText = _t(
"Invite someone using their name, username (like <userId/>), email address or " +
"<a>share this room</a>.",
{},
{
userId: () =>
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
a: (sub) =>
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{sub}</a>,
},
);
} else {
helpText = _t(
"Invite someone using their name, username (like <userId/>) or <a>share this room</a>.",
{},
{
userId: () =>
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
a: (sub) =>
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{sub}</a>,
},
);
}
buttonText = _t("Invite");
goButtonFn = this._inviteUsers;
}

View file

@ -20,7 +20,8 @@ import Modal from '../../../Modal';
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import RestoreKeyBackupDialog from './security/RestoreKeyBackupDialog';
export default class LogoutDialog extends React.Component {
defaultProps = {
@ -73,7 +74,7 @@ export default class LogoutDialog extends React.Component {
_onExportE2eKeysClicked() {
Modal.createTrackedDialogAsync('Export E2E Keys', '',
import('../../../async-components/views/dialogs/ExportE2eKeysDialog'),
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
{
matrixClient: MatrixClientPeg.get(),
},
@ -93,14 +94,13 @@ export default class LogoutDialog extends React.Component {
// A key backup exists for this account, but the creating device is not
// verified, so restore the backup which will give us the keys from it and
// allow us to trust it (ie. upload keys to it)
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
Modal.createTrackedDialog(
'Restore Backup', '', RestoreKeyBackupDialog, null, null,
/* priority = */ false, /* static = */ true,
);
} else {
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"),
import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog"),
null, null, /* priority = */ false, /* static = */ true,
);
}

View file

@ -29,6 +29,7 @@ import * as sdk from "../../../index";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import dis from "../../../dispatcher/dispatcher";
import SettingsStore from "../../../settings/SettingsStore";
import {UIFeature} from "../../../settings/UIFeature";
export const ROOM_GENERAL_TAB = "ROOM_GENERAL_TAB";
export const ROOM_SECURITY_TAB = "ROOM_SECURITY_TAB";
@ -96,12 +97,14 @@ export default class RoomSettingsDialog extends React.Component {
));
}
tabs.push(new Tab(
ROOM_ADVANCED_TAB,
_td("Advanced"),
"mx_RoomSettingsDialog_warningIcon",
<AdvancedRoomSettingsTab roomId={this.props.roomId} closeSettingsFn={this.props.onFinished} />,
));
if (SettingsStore.getValue(UIFeature.AdvancedSettings)) {
tabs.push(new Tab(
ROOM_ADVANCED_TAB,
_td("Advanced"),
"mx_RoomSettingsDialog_warningIcon",
<AdvancedRoomSettingsTab roomId={this.props.roomId} closeSettingsFn={this.props.onFinished} />,
));
}
return tabs;
}

View file

@ -32,6 +32,8 @@ import {copyPlaintext, selectText} from "../../../utils/strings";
import StyledCheckbox from '../elements/StyledCheckbox';
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
import { IDialogProps } from "./IDialogProps";
import SettingsStore from "../../../settings/SettingsStore";
import {UIFeature} from "../../../settings/UIFeature";
const socials = [
{
@ -197,6 +199,35 @@ export default class ShareDialog extends React.PureComponent<IProps, IState> {
const matrixToUrl = this.getUrl();
const encodedUrl = encodeURIComponent(matrixToUrl);
const showQrCode = SettingsStore.getValue(UIFeature.ShareQRCode);
const showSocials = SettingsStore.getValue(UIFeature.ShareSocial);
let qrSocialSection;
if (showQrCode || showSocials) {
qrSocialSection = <>
<hr />
<div className="mx_ShareDialog_split">
{ showQrCode && <div className="mx_ShareDialog_qrcode_container">
<QRCode data={matrixToUrl} width={256} />
</div> }
{ showSocials && <div className="mx_ShareDialog_social_container">
{ socials.map((social) => (
<a
rel="noreferrer noopener"
target="_blank"
key={social.name}
title={social.name}
href={social.url(encodedUrl)}
className="mx_ShareDialog_social_icon"
>
<img src={social.img} alt={social.name} height={64} width={64} />
</a>
)) }
</div> }
</div>
</>;
}
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return <BaseDialog
title={title}
@ -220,27 +251,7 @@ export default class ShareDialog extends React.PureComponent<IProps, IState> {
/>
</div>
{ checkbox }
<hr />
<div className="mx_ShareDialog_split">
<div className="mx_ShareDialog_qrcode_container">
<QRCode data={matrixToUrl} width={256} />
</div>
<div className="mx_ShareDialog_social_container">
{ socials.map((social) => (
<a
rel="noreferrer noopener"
target="_blank"
key={social.name}
title={social.name}
href={social.url(encodedUrl)}
className="mx_ShareDialog_social_icon"
>
<img src={social.img} alt={social.name} height={64} width={64} />
</a>
)) }
</div>
</div>
{ qrSocialSection }
</div>
</BaseDialog>;
}

View file

@ -32,6 +32,7 @@ import FlairUserSettingsTab from "../settings/tabs/user/FlairUserSettingsTab";
import * as sdk from "../../../index";
import SdkConfig from "../../../SdkConfig";
import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab";
import {UIFeature} from "../../../settings/UIFeature";
export const USER_GENERAL_TAB = "USER_GENERAL_TAB";
export const USER_APPEARANCE_TAB = "USER_APPEARANCE_TAB";
@ -86,12 +87,14 @@ export default class UserSettingsDialog extends React.Component {
"mx_UserSettingsDialog_appearanceIcon",
<AppearanceUserSettingsTab />,
));
tabs.push(new Tab(
USER_FLAIR_TAB,
_td("Flair"),
"mx_UserSettingsDialog_flairIcon",
<FlairUserSettingsTab />,
));
if (SettingsStore.getValue(UIFeature.Flair)) {
tabs.push(new Tab(
USER_FLAIR_TAB,
_td("Flair"),
"mx_UserSettingsDialog_flairIcon",
<FlairUserSettingsTab />,
));
}
tabs.push(new Tab(
USER_NOTIFICATIONS_TAB,
_td("Notifications"),
@ -104,12 +107,16 @@ export default class UserSettingsDialog extends React.Component {
"mx_UserSettingsDialog_preferencesIcon",
<PreferencesUserSettingsTab />,
));
tabs.push(new Tab(
USER_VOICE_TAB,
_td("Voice & Video"),
"mx_UserSettingsDialog_voiceIcon",
<VoiceUserSettingsTab />,
));
if (SettingsStore.getValue(UIFeature.Voip)) {
tabs.push(new Tab(
USER_VOICE_TAB,
_td("Voice & Video"),
"mx_UserSettingsDialog_voiceIcon",
<VoiceUserSettingsTab />,
));
}
tabs.push(new Tab(
USER_SECURITY_TAB,
_td("Security & Privacy"),

View file

@ -289,7 +289,12 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
content = <div>
<p>{_t("Use your Security Key to continue.")}</p>
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this._onRecoveryKeyNext} spellCheck={false}>
<form
className="mx_AccessSecretStorageDialog_primaryContainer"
onSubmit={this._onRecoveryKeyNext}
spellCheck={false}
autoComplete="off"
>
<div className="mx_AccessSecretStorageDialog_recoveryKeyEntry">
<div className="mx_AccessSecretStorageDialog_recoveryKeyEntry_textInput">
<Field
@ -298,6 +303,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
value={this.state.recoveryKey}
onChange={this._onRecoveryKeyChange}
forceValidity={this.state.recoveryKeyCorrect}
autoComplete="off"
/>
</div>
<span className="mx_AccessSecretStorageDialog_recoveryKeyEntry_entryControlSeparatorText">

View file

@ -16,8 +16,8 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
import * as sdk from "../../../index";
import {_t} from "../../../../languageHandler";
import * as sdk from "../../../../index";
export default class ConfirmDestroyCrossSigningDialog extends React.Component {
static propTypes = {

View file

@ -0,0 +1,187 @@
/*
Copyright 2018, 2019 New Vector 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 React from 'react';
import PropTypes from 'prop-types';
import { MatrixClientPeg } from '../../../../MatrixClientPeg';
import { _t } from '../../../../languageHandler';
import Modal from '../../../../Modal';
import { SSOAuthEntry } from '../../auth/InteractiveAuthEntryComponents';
import DialogButtons from '../../elements/DialogButtons';
import BaseDialog from '../BaseDialog';
import Spinner from '../../elements/Spinner';
import InteractiveAuthDialog from '../InteractiveAuthDialog';
/*
* Walks the user through the process of creating a cross-signing keys. In most
* cases, only a spinner is shown, but for more complex auth like SSO, the user
* may need to complete some steps to proceed.
*/
export default class CreateCrossSigningDialog extends React.PureComponent {
static propTypes = {
accountPassword: PropTypes.string,
};
constructor(props) {
super(props);
this.state = {
error: null,
// Does the server offer a UI auth flow with just m.login.password
// for /keys/device_signing/upload?
canUploadKeysWithPasswordOnly: null,
accountPassword: props.accountPassword || "",
};
if (this.state.accountPassword) {
// If we have an account password in memory, let's simplify and
// assume it means password auth is also supported for device
// signing key upload as well. This avoids hitting the server to
// test auth flows, which may be slow under high load.
this.state.canUploadKeysWithPasswordOnly = true;
} else {
this._queryKeyUploadAuth();
}
}
componentDidMount() {
this._bootstrapCrossSigning();
}
async _queryKeyUploadAuth() {
try {
await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {});
// We should never get here: the server should always require
// UI auth to upload device signing keys. If we do, we upload
// no keys which would be a no-op.
console.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!");
} catch (error) {
if (!error.data || !error.data.flows) {
console.log("uploadDeviceSigningKeys advertised no flows!");
return;
}
const canUploadKeysWithPasswordOnly = error.data.flows.some(f => {
return f.stages.length === 1 && f.stages[0] === 'm.login.password';
});
this.setState({
canUploadKeysWithPasswordOnly,
});
}
}
_doBootstrapUIAuth = async (makeRequest) => {
if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
await makeRequest({
type: 'm.login.password',
identifier: {
type: 'm.id.user',
user: MatrixClientPeg.get().getUserId(),
},
// TODO: Remove `user` once servers support proper UIA
// See https://github.com/matrix-org/synapse/issues/5665
user: MatrixClientPeg.get().getUserId(),
password: this.state.accountPassword,
});
} else {
const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
title: _t("Use Single Sign On to continue"),
body: _t("To continue, use Single Sign On to prove your identity."),
continueText: _t("Single Sign On"),
continueKind: "primary",
},
[SSOAuthEntry.PHASE_POSTAUTH]: {
title: _t("Confirm encryption setup"),
body: _t("Click the button below to confirm setting up encryption."),
continueText: _t("Confirm"),
continueKind: "primary",
},
};
const { finished } = Modal.createTrackedDialog(
'Cross-signing keys dialog', '', InteractiveAuthDialog,
{
title: _t("Setting up keys"),
matrixClient: MatrixClientPeg.get(),
makeRequest,
aestheticsForStagePhases: {
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
},
},
);
const [confirmed] = await finished;
if (!confirmed) {
throw new Error("Cross-signing key upload auth canceled");
}
}
}
_bootstrapCrossSigning = async () => {
this.setState({
error: null,
});
const cli = MatrixClientPeg.get();
try {
await cli.bootstrapCrossSigning({
authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
});
this.props.onFinished(true);
} catch (e) {
this.setState({ error: e });
console.error("Error bootstrapping cross-signing", e);
}
}
_onCancel = () => {
this.props.onFinished(false);
}
render() {
let content;
if (this.state.error) {
content = <div>
<p>{_t("Unable to set up keys")}</p>
<div className="mx_Dialog_buttons">
<DialogButtons primaryButton={_t('Retry')}
onPrimaryButtonClick={this._bootstrapCrossSigning}
onCancel={this._onCancel}
/>
</div>
</div>;
} else {
content = <div>
<Spinner />
</div>;
}
return (
<BaseDialog className="mx_CreateCrossSigningDialog"
onFinished={this.props.onFinished}
title={_t("Setting up keys")}
hasCancel={false}
fixedWidth={false}
>
<div>
{content}
</div>
</BaseDialog>
);
}
}

View file

@ -16,16 +16,16 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import SetupEncryptionBody from '../../structures/auth/SetupEncryptionBody';
import BaseDialog from './BaseDialog';
import { _t } from '../../../languageHandler';
import { SetupEncryptionStore, PHASE_DONE } from '../../../stores/SetupEncryptionStore';
import SetupEncryptionBody from '../../../structures/auth/SetupEncryptionBody';
import BaseDialog from '../BaseDialog';
import { _t } from '../../../../languageHandler';
import { SetupEncryptionStore, PHASE_DONE } from '../../../../stores/SetupEncryptionStore';
function iconFromPhase(phase) {
if (phase === PHASE_DONE) {
return require("../../../../res/img/e2e/verified.svg");
return require("../../../../../res/img/e2e/verified.svg");
} else {
return require("../../../../res/img/e2e/warning.svg");
return require("../../../../../res/img/e2e/warning.svg");
}
}

View file

@ -863,13 +863,13 @@ export default class AppTile extends React.Component {
{ /* Minimise widget */ }
{ showMinimiseButton && <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_minimise"
title={_t('Minimize apps')}
title={_t('Minimize widget')}
onClick={this._onMinimiseClick}
/> }
{ /* Maximise widget */ }
{ showMaximiseButton && <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_maximise"
title={_t('Maximize apps')}
title={_t('Maximize widget')}
onClick={this._onMinimiseClick}
/> }
{ /* Title */ }

View file

@ -0,0 +1,77 @@
/*
Copyright 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 EventIndexPeg from "../../../indexing/EventIndexPeg";
import { _t } from "../../../languageHandler";
import SdkConfig from "../../../SdkConfig";
import React from "react";
export enum WarningKind {
Files,
Search,
}
interface IProps {
isRoomEncrypted: boolean;
kind: WarningKind;
}
export default function DesktopBuildsNotice({isRoomEncrypted, kind}: IProps) {
if (!isRoomEncrypted) return null;
if (EventIndexPeg.get()) return null;
const {desktopBuilds, brand} = SdkConfig.get();
let text = null;
let logo = null;
if (desktopBuilds.available) {
logo = <img src={desktopBuilds.logo} />;
switch (kind) {
case WarningKind.Files:
text = _t("Use the <a>Desktop app</a> to see all encrypted files", {}, {
a: sub => (<a href={desktopBuilds.url} target="_blank" rel="noreferrer noopener">{sub}</a>),
});
break;
case WarningKind.Search:
text = _t("Use the <a>Desktop app</a> to search encrypted messages", {}, {
a: sub => (<a href={desktopBuilds.url} target="_blank" rel="noreferrer noopener">{sub}</a>),
});
break;
}
} else {
switch (kind) {
case WarningKind.Files:
text = _t("This version of %(brand)s does not support viewing some encrypted files", {brand});
break;
case WarningKind.Search:
text = _t("This version of %(brand)s does not support searching encrypted messages", {brand});
break;
}
}
// for safety
if (!text) {
console.warn("Unknown desktop builds warning kind: ", kind);
return null;
}
return (
<div className="mx_DesktopBuildsNotice">
{logo}
<span>{text}</span>
</div>
);
}

View file

@ -21,6 +21,8 @@ import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import * as Avatar from '../../../Avatar';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import EventTile from '../rooms/EventTile';
import SettingsStore from "../../../settings/SettingsStore";
import {UIFeature} from "../../../settings/UIFeature";
interface IProps {
/**
@ -121,7 +123,11 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
});
return <div className={className}>
<EventTile mxEvent={event} useIRCLayout={this.props.useIRCLayout} />
<EventTile
mxEvent={event}
useIRCLayout={this.props.useIRCLayout}
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
/>
</div>;
}
}

View file

@ -28,6 +28,7 @@ import escapeHtml from "escape-html";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {Action} from "../../../dispatcher/actions";
import sanitizeHtml from "sanitize-html";
import {UIFeature} from "../../../settings/UIFeature";
// This component does no cycle detection, simply because the only way to make such a cycle would be to
// craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would
@ -366,6 +367,7 @@ export default class ReplyThread extends React.Component {
isRedacted={ev.isRedacted()}
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
useIRCLayout={this.props.useIRCLayout}
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
/>
</blockquote>;
});

View file

@ -21,18 +21,19 @@ import classNames from "classnames";
type Data = Pick<IFieldState, "value" | "allowEmpty">;
interface IRule<T> {
interface IRule<T, D = void> {
key: string;
final?: boolean;
skip?(this: T, data: Data): boolean;
test(this: T, data: Data): boolean | Promise<boolean>;
valid?(this: T): string;
invalid?(this: T): string;
skip?(this: T, data: Data, derivedData: D): boolean;
test(this: T, data: Data, derivedData: D): boolean | Promise<boolean>;
valid?(this: T, derivedData: D): string;
invalid?(this: T, derivedData: D): string;
}
interface IArgs<T> {
rules: IRule<T>[];
description(this: T): React.ReactChild;
interface IArgs<T, D = void> {
rules: IRule<T, D>[];
description(this: T, derivedData: D): React.ReactChild;
deriveData?(data: Data): Promise<D>;
}
export interface IFieldState {
@ -53,6 +54,10 @@ export interface IValidationResult {
* @param {Function} description
* Function that returns a string summary of the kind of value that will
* meet the validation rules. Shown at the top of the validation feedback.
* @param {Function} deriveData
* Optional function that returns a Promise to an object of generic type D.
* The result of this Promise is passed to rule methods `skip`, `test`, `valid`, and `invalid`.
* Useful for doing calculations per-value update once rather than in each of the above rule methods.
* @param {Object} rules
* An array of rules describing how to check to input value. Each rule in an object
* and may have the following properties:
@ -66,7 +71,7 @@ export interface IValidationResult {
* A validation function that takes in the current input value and returns
* the overall validity and a feedback UI that can be rendered for more detail.
*/
export default function withValidation<T = undefined>({ description, rules }: IArgs<T>) {
export default function withValidation<T = undefined, D = void>({ description, deriveData, rules }: IArgs<T, D>) {
return async function onValidate({ value, focused, allowEmpty = true }: IFieldState): Promise<IValidationResult> {
if (!value && allowEmpty) {
return {
@ -75,6 +80,9 @@ export default function withValidation<T = undefined>({ description, rules }: IA
};
}
const data = { value, allowEmpty };
const derivedData = deriveData ? await deriveData(data) : undefined;
const results = [];
let valid = true;
if (rules && rules.length) {
@ -87,20 +95,18 @@ export default function withValidation<T = undefined>({ description, rules }: IA
continue;
}
const data = { value, allowEmpty };
if (rule.skip && rule.skip.call(this, data)) {
if (rule.skip && rule.skip.call(this, data, derivedData)) {
continue;
}
// We're setting `this` to whichever component holds the validation
// function. That allows rules to access the state of the component.
const ruleValid = await rule.test.call(this, data);
const ruleValid = await rule.test.call(this, data, derivedData);
valid = valid && ruleValid;
if (ruleValid && rule.valid) {
// If the rule's result is valid and has text to show for
// the valid state, show it.
const text = rule.valid.call(this);
const text = rule.valid.call(this, derivedData);
if (!text) {
continue;
}
@ -112,7 +118,7 @@ export default function withValidation<T = undefined>({ description, rules }: IA
} else if (!ruleValid && rule.invalid) {
// If the rule's result is invalid and has text to show for
// the invalid state, show it.
const text = rule.invalid.call(this);
const text = rule.invalid.call(this, derivedData);
if (!text) {
continue;
}
@ -153,7 +159,7 @@ export default function withValidation<T = undefined>({ description, rules }: IA
if (description) {
// We're setting `this` to whichever component holds the validation
// function. That allows rules to access the state of the component.
const content = description.call(this);
const content = description.call(this, derivedData);
summary = <div className="mx_Validation_description">{content}</div>;
}

View file

@ -99,7 +99,7 @@ const AppsSection: React.FC<IAppsSectionProps> = ({ room }) => {
}
};
return <Group className="mx_RoomSummaryCard_appsGroup" title={_t("Apps")}>
return <Group className="mx_RoomSummaryCard_appsGroup" title={_t("Widgets")}>
{ apps.map(app => {
const name = WidgetUtils.getWidgetName(app);
const dataTitle = WidgetUtils.getWidgetDataTitle(app);
@ -161,7 +161,7 @@ const AppsSection: React.FC<IAppsSectionProps> = ({ room }) => {
}) }
<AccessibleButton kind="link" onClick={onManageIntegrations}>
{ apps.length > 0 ? _t("Edit apps, bridges & bots") : _t("Add apps, bridges & bots") }
{ apps.length > 0 ? _t("Edit widgets, bridges & bots") : _t("Add widgets, bridges & bots") }
</AccessibleButton>
</Group>;
};

View file

@ -1296,7 +1296,8 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
const userTrust = cryptoEnabled && cli.checkUserTrust(member.userId);
const userVerified = cryptoEnabled && userTrust.isCrossSigningVerified();
const isMe = member.userId === cli.getUserId();
const canVerify = cryptoEnabled && homeserverSupportsCrossSigning && !userVerified && !isMe;
const canVerify = cryptoEnabled && homeserverSupportsCrossSigning && !userVerified && !isMe &&
devices && devices.length > 0;
const setUpdating = (updating) => {
setPendingUpdateCount(count => count + (updating ? 1 : -1));

View file

@ -152,7 +152,7 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
</AccessibleButton>;
} else {
pinButton = <AccessibleTooltipButton
title={_t("You can only pin 2 apps at a time")}
title={_t("You can only pin 2 widgets at a time")}
tooltipClassName="mx_WidgetCard_maxPinnedTooltip"
kind="secondary"
className={pinButtonClasses}

View file

@ -75,6 +75,15 @@ export default class RoomProfileSettings extends React.Component {
});
};
_clearProfile = async (e) => {
e.stopPropagation();
e.preventDefault();
if (!this.state.enableProfileSave) return;
this._removeAvatar();
this.setState({enableProfileSave: false, displayName: this.state.originalDisplayName});
};
_saveProfile = async (e) => {
e.stopPropagation();
e.preventDefault();
@ -150,7 +159,12 @@ export default class RoomProfileSettings extends React.Component {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const AvatarSetting = sdk.getComponent('settings.AvatarSetting');
return (
<form onSubmit={this._saveProfile} autoComplete="off" noValidate={true}>
<form
onSubmit={this._saveProfile}
autoComplete="off"
noValidate={true}
className="mx_ProfileSettings_profileForm"
>
<input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload"
onChange={this._onAvatarChanged} accept="image/*" />
<div className="mx_ProfileSettings_profile">
@ -169,10 +183,22 @@ export default class RoomProfileSettings extends React.Component {
uploadAvatar={this.state.canSetAvatar ? this._uploadAvatar : undefined}
removeAvatar={this.state.canSetAvatar ? this._removeAvatar : undefined} />
</div>
<AccessibleButton onClick={this._saveProfile} kind="primary"
disabled={!this.state.enableProfileSave}>
{_t("Save")}
</AccessibleButton>
<div className="mx_ProfileSettings_buttons">
<AccessibleButton
onClick={this._clearProfile}
kind="link"
disabled={!this.state.enableProfileSave}
>
{_t("Cancel")}
</AccessibleButton>
<AccessibleButton
onClick={this._saveProfile}
kind="primary"
disabled={!this.state.enableProfileSave}
>
{_t("Save")}
</AccessibleButton>
</div>
</form>
);
}

View file

@ -39,15 +39,9 @@ export default class AuxPanel extends React.Component {
showApps: PropTypes.bool, // Render apps
hideAppsDrawer: PropTypes.bool, // Do not display apps drawer and content (may still be rendered)
// Conference Handler implementation
conferenceHandler: PropTypes.object,
// set to true to show the file drop target
draggingFile: PropTypes.bool,
// set to true to show the 'active conf call' banner
displayConfCallNotification: PropTypes.bool,
// maxHeight attribute for the aux panel and the video
// therein
maxHeight: PropTypes.number,
@ -161,39 +155,9 @@ export default class AuxPanel extends React.Component {
);
}
let conferenceCallNotification = null;
if (this.props.displayConfCallNotification) {
let supportedText = '';
let joinNode;
if (!MatrixClientPeg.get().supportsVoip()) {
supportedText = _t(" (unsupported)");
} else {
joinNode = (<span>
{ _t(
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
{},
{
'voiceText': (sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice');}} href="#">{ sub }</a>,
'videoText': (sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video');}} href="#">{ sub }</a>,
},
) }
</span>);
}
// XXX: the translation here isn't great: appending ' (unsupported)' is likely to not make sense in many languages,
// but there are translations for this in the languages we do have so I'm leaving it for now.
conferenceCallNotification = (
<div className="mx_RoomView_ongoingConfCallNotification">
{ _t("Ongoing conference call%(supportedText)s.", {supportedText: supportedText}) }
&nbsp;
{ joinNode }
</div>
);
}
const callView = (
<CallView
room={this.props.room}
ConferenceHandler={this.props.conferenceHandler}
onResize={this.props.onResize}
maxVideoHeight={this.props.maxHeight}
/>
@ -276,7 +240,6 @@ export default class AuxPanel extends React.Component {
{ appsDrawer }
{ fileDropTarget }
{ callView }
{ conferenceCallNotification }
{ this.props.children }
</AutoHideScrollbar>
);

View file

@ -34,6 +34,7 @@ import * as ObjectUtils from "../../../ObjectUtils";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {E2E_STATE} from "./E2EIcon";
import {toRem} from "../../../utils/units";
import RoomAvatar from "../avatars/RoomAvatar";
const eventTileTypes = {
'm.room.message': 'messages.MessageEvent',
@ -147,6 +148,10 @@ export default class EventTile extends React.Component {
*/
last: PropTypes.bool,
// true if the event is the last event in a section (adds a css class for
// targeting)
lastInSection: PropTypes.bool,
/* true if this is search context (which has the effect of greying out
* the text
*/
@ -206,6 +211,9 @@ export default class EventTile extends React.Component {
// whether to use the irc layout
useIRCLayout: PropTypes.bool,
// whether or not to show flair at all
enableFlair: PropTypes.bool,
};
static defaultProps = {
@ -670,6 +678,7 @@ export default class EventTile extends React.Component {
mx_EventTile_selected: this.props.isSelectedEvent,
mx_EventTile_continuation: this.props.tileShape ? '' : this.props.continuation,
mx_EventTile_last: this.props.last,
mx_EventTile_lastInSection: this.props.lastInSection,
mx_EventTile_contextual: this.props.contextual,
mx_EventTile_actionBarFocused: this.state.actionBarFocused,
mx_EventTile_verified: !isBubbleMessage && this.state.verified === E2E_STATE.VERIFIED,
@ -736,10 +745,10 @@ export default class EventTile extends React.Component {
else if (msgtype === 'm.file') text = _td('%(senderName)s uploaded a file');
sender = <SenderProfile onClick={this.onSenderProfileClick}
mxEvent={this.props.mxEvent}
enableFlair={!text}
enableFlair={this.props.enableFlair && !text}
text={text} />;
} else {
sender = <SenderProfile mxEvent={this.props.mxEvent} enableFlair={true} />;
sender = <SenderProfile mxEvent={this.props.mxEvent} enableFlair={this.props.enableFlair} />;
}
}
@ -818,6 +827,7 @@ export default class EventTile extends React.Component {
return (
<div className={classes} aria-live={ariaLive} aria-atomic="true">
<div className="mx_EventTile_roomName">
<RoomAvatar room={room} width={28} height={28} />
<a href={permalink} onClick={this.onPermalinkClicked}>
{ room ? room.name : '' }
</a>

View file

@ -24,7 +24,6 @@ import {isValid3pidInvite} from "../../../RoomInvite";
import rate_limited_func from "../../../ratelimitedfunc";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import * as sdk from "../../../index";
import CallHandler from "../../../CallHandler";
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
import BaseCard from "../right_panel/BaseCard";
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
@ -233,15 +232,10 @@ export default class MemberList extends React.Component {
}
roomMembers() {
const ConferenceHandler = CallHandler.getConferenceHandler();
const allMembers = this.getMembersWithUser();
const filteredAndSortedMembers = allMembers.filter((m) => {
return (
m.membership === 'join' || m.membership === 'invite'
) && (
!ConferenceHandler ||
(ConferenceHandler && !ConferenceHandler.isConferenceUser(m.userId))
);
});
filteredAndSortedMembers.sort(this.memberSort);

View file

@ -99,7 +99,7 @@ function HangupButton(props) {
return;
}
const call = CallHandler.getCallForRoom(props.roomId);
const call = CallHandler.sharedInstance().getCallForRoom(props.roomId);
if (!call) {
return;
}

View file

@ -22,6 +22,7 @@ import RoomViewStore from '../../../stores/RoomViewStore';
import SettingsStore from "../../../settings/SettingsStore";
import PropTypes from "prop-types";
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
import {UIFeature} from "../../../settings/UIFeature";
function cancelQuoting() {
dis.dispatch({
@ -80,11 +81,14 @@ export default class ReplyPreview extends React.Component {
onClick={cancelQuoting} />
</div>
<div className="mx_ReplyPreview_clear" />
<EventTile last={true}
tileShape="reply_preview"
mxEvent={this.state.event}
permalinkCreator={this.props.permalinkCreator}
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} />
<EventTile
last={true}
tileShape="reply_preview"
mxEvent={this.state.event}
permalinkCreator={this.props.permalinkCreator}
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
/>
</div>
</div>;
}

View file

@ -1,170 +0,0 @@
/*
Copyright 2018, 2019 New Vector Ltd
Copyright 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 React from "react";
import PropTypes from "prop-types";
import * as sdk from "../../../index";
import { _t } from "../../../languageHandler";
import Modal from "../../../Modal";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import SettingsStore from "../../../settings/SettingsStore";
import {SettingLevel} from "../../../settings/SettingLevel";
export default class RoomRecoveryReminder extends React.PureComponent {
static propTypes = {
// called if the user sets the option to suppress this reminder in the future
onDontAskAgainSet: PropTypes.func,
}
static defaultProps = {
onDontAskAgainSet: function() {},
}
constructor(props) {
super(props);
this.state = {
loading: true,
error: null,
backupInfo: null,
notNowClicked: false,
};
}
componentDidMount() {
this._loadBackupStatus();
}
async _loadBackupStatus() {
try {
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
this.setState({
loading: false,
backupInfo,
});
} catch (e) {
console.log("Unable to fetch key backup status", e);
this.setState({
loading: false,
error: e,
});
}
}
showSetupDialog = () => {
if (this.state.backupInfo) {
// A key backup exists for this account, but the creating device is not
// verified, so restore the backup which will give us the keys from it and
// allow us to trust it (ie. upload keys to it)
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
Modal.createTrackedDialog(
'Restore Backup', '', RestoreKeyBackupDialog, null, null,
/* priority = */ false, /* static = */ true,
);
} else {
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"),
null, null, /* priority = */ false, /* static = */ true,
);
}
}
onOnNotNowClick = () => {
this.setState({notNowClicked: true});
}
onDontAskAgainClick = () => {
// When you choose "Don't ask again" from the room reminder, we show a
// dialog to confirm the choice.
Modal.createTrackedDialogAsync("Ignore Recovery Reminder", "Ignore Recovery Reminder",
import("../../../async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog"),
{
onDontAskAgain: async () => {
await SettingsStore.setValue(
"showRoomRecoveryReminder",
null,
SettingLevel.ACCOUNT,
false,
);
this.props.onDontAskAgainSet();
},
onSetup: () => {
this.showSetupDialog();
},
},
);
}
onSetupClick = () => {
this.showSetupDialog();
}
render() {
// If there was an error loading just don't display the banner: we'll try again
// next time the user switchs to the room.
if (this.state.error || this.state.loading || this.state.notNowClicked) {
return null;
}
const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
let setupCaption;
if (this.state.backupInfo) {
setupCaption = _t("Connect this session to Key Backup");
} else {
setupCaption = _t("Start using Key Backup");
}
return (
<div className="mx_RoomRecoveryReminder">
<div className="mx_RoomRecoveryReminder_header">{_t(
"Never lose encrypted messages",
)}</div>
<div className="mx_RoomRecoveryReminder_body">
<p>{_t(
"Messages in this room are secured with end-to-end " +
"encryption. Only you and the recipient(s) have the " +
"keys to read these messages.",
)}</p>
<p>{_t(
"Securely back up your keys to avoid losing them. " +
"<a>Learn more.</a>", {},
{
// TODO: We don't have this link yet: this will prevent the translators
// having to re-translate the string when we do.
a: sub => '',
},
)}</p>
</div>
<div className="mx_RoomRecoveryReminder_buttons">
<AccessibleButton kind="primary"
onClick={this.onSetupClick}>
{setupCaption}
</AccessibleButton>
<AccessibleButton className="mx_RoomRecoveryReminder_secondary mx_linkButton"
onClick={this.onOnNotNowClick}>
{ _t("Not now") }
</AccessibleButton>
<AccessibleButton className="mx_RoomRecoveryReminder_secondary mx_linkButton"
onClick={this.onDontAskAgainClick}>
{ _t("Don't ask me again") }
</AccessibleButton>
</div>
</div>
);
}
}

View file

@ -1,5 +1,5 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2018-2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -28,6 +28,11 @@ export default class RoomUpgradeWarningBar extends React.Component {
recommendation: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
const tombstone = this.props.room.currentState.getStateEvents("m.room.tombstone", "");
this.setState({upgraded: tombstone && tombstone.getContent().replacement_room});
@ -35,6 +40,13 @@ export default class RoomUpgradeWarningBar extends React.Component {
MatrixClientPeg.get().on("RoomState.events", this._onStateEvents);
}
componentWillUnmount() {
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener("RoomState.events", this._onStateEvents);
}
}
_onStateEvents = (event, state) => {
if (!this.props.room || event.getRoomId() !== this.props.room.roomId) {
return;

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 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.
@ -19,6 +20,7 @@ import AccessibleButton from "../elements/AccessibleButton";
import classNames from "classnames";
import { _t } from '../../../languageHandler';
import {Key} from "../../../Keyboard";
import DesktopBuildsNotice, {WarningKind} from "../elements/DesktopBuildsNotice";
export default class SearchBar extends React.Component {
constructor(props) {
@ -72,21 +74,24 @@ export default class SearchBar extends React.Component {
});
return (
<div className="mx_SearchBar">
<div className="mx_SearchBar_buttons" role="radiogroup">
<AccessibleButton className={ thisRoomClasses } onClick={this.onThisRoomClick} aria-checked={this.state.scope === 'Room'} role="radio">
{_t("This Room")}
</AccessibleButton>
<AccessibleButton className={ allRoomsClasses } onClick={this.onAllRoomsClick} aria-checked={this.state.scope === 'All'} role="radio">
{_t("All Rooms")}
</AccessibleButton>
<>
<div className="mx_SearchBar">
<div className="mx_SearchBar_buttons" role="radiogroup">
<AccessibleButton className={ thisRoomClasses } onClick={this.onThisRoomClick} aria-checked={this.state.scope === 'Room'} role="radio">
{_t("This Room")}
</AccessibleButton>
<AccessibleButton className={ allRoomsClasses } onClick={this.onAllRoomsClick} aria-checked={this.state.scope === 'All'} role="radio">
{_t("All Rooms")}
</AccessibleButton>
</div>
<div className="mx_SearchBar_input mx_textinput">
<input ref={this._search_term} type="text" autoFocus={true} placeholder={_t("Search…")} onKeyDown={this.onSearchChange} />
<AccessibleButton className={ searchButtonClasses } onClick={this.onSearch} />
</div>
<AccessibleButton className="mx_SearchBar_cancel" onClick={this.props.onCancelClick} />
</div>
<div className="mx_SearchBar_input mx_textinput">
<input ref={this._search_term} type="text" autoFocus={true} placeholder={_t("Search…")} onKeyDown={this.onSearchChange} />
<AccessibleButton className={ searchButtonClasses } onClick={this.onSearch} />
</div>
<AccessibleButton className="mx_SearchBar_cancel" onClick={this.props.onCancelClick} />
</div>
<DesktopBuildsNotice isRoomEncrypted={this.props.isRoomEncrypted} kind={WarningKind.Search} />
</>
);
}
}

View file

@ -19,6 +19,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import {haveTileForEvent} from "./EventTile";
import SettingsStore from "../../../settings/SettingsStore";
import {UIFeature} from "../../../settings/UIFeature";
export default class SearchResultTile extends React.Component {
static propTypes = {
@ -45,22 +47,31 @@ export default class SearchResultTile extends React.Component {
const ret = [<DateSeparator key={ts1 + "-search"} ts={ts1} />];
const timeline = result.context.getTimeline();
for (var j = 0; j < timeline.length; j++) {
for (let j = 0; j < timeline.length; j++) {
const ev = timeline[j];
var highlights;
let highlights;
const contextual = (j != result.context.getOurEventIndex());
if (!contextual) {
highlights = this.props.searchHighlights;
}
if (haveTileForEvent(ev)) {
ret.push(<EventTile key={eventId+"+"+j} mxEvent={ev} contextual={contextual} highlights={highlights}
permalinkCreator={this.props.permalinkCreator}
highlightLink={this.props.resultLink}
onHeightChanged={this.props.onHeightChanged} />);
ret.push((
<EventTile
key={`${eventId}+${j}`}
mxEvent={ev}
contextual={contextual}
highlights={highlights}
permalinkCreator={this.props.permalinkCreator}
highlightLink={this.props.resultLink}
onHeightChanged={this.props.onHeightChanged}
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
/>
));
}
}
return (
<li data-scroll-tokens={eventId+"+"+j}>
<li data-scroll-tokens={eventId}>
{ ret }
</li>);
}

View file

@ -14,25 +14,25 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useCallback} from "react";
import React, {useState} from "react";
import PropTypes from "prop-types";
import * as sdk from "../../../index";
import {_t} from "../../../languageHandler";
import Modal from "../../../Modal";
import AccessibleButton from "../elements/AccessibleButton";
import classNames from "classnames";
const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, removeAvatar}) => {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const [isHovering, setIsHovering] = useState(false);
const hoveringProps = {
onMouseEnter: () => setIsHovering(true),
onMouseLeave: () => setIsHovering(false),
};
const openImageView = useCallback(() => {
const ImageView = sdk.getComponent("elements.ImageView");
Modal.createDialog(ImageView, {
src: avatarUrl,
name: avatarName,
}, "mx_Dialog_lightbox");
}, [avatarUrl, avatarName]);
let avatarElement = <div className="mx_AvatarSetting_avatarPlaceholder" />;
let avatarElement = <AccessibleButton
element="div"
onClick={uploadAvatar}
className="mx_AvatarSetting_avatarPlaceholder"
{...hoveringProps}
/>;
if (avatarUrl) {
avatarElement = (
<AccessibleButton
@ -40,16 +40,20 @@ const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, remo
src={avatarUrl}
alt={avatarAltText}
aria-label={avatarAltText}
onClick={openImageView} />
onClick={uploadAvatar}
{...hoveringProps}
/>
);
}
let uploadAvatarBtn;
if (uploadAvatar) {
// insert an empty div to be the host for a css mask containing the upload.svg
uploadAvatarBtn = <AccessibleButton onClick={uploadAvatar} kind="primary">
{_t("Upload")}
</AccessibleButton>;
uploadAvatarBtn = <AccessibleButton
onClick={uploadAvatar}
className='mx_AvatarSetting_uploadButton'
{...hoveringProps}
/>;
}
let removeAvatarBtn;
@ -59,10 +63,18 @@ const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, remo
</AccessibleButton>;
}
return <div className="mx_AvatarSetting_avatar">
{ avatarElement }
{ uploadAvatarBtn }
{ removeAvatarBtn }
const avatarClasses = classNames({
"mx_AvatarSetting_avatar": true,
"mx_AvatarSetting_avatar_hovering": isHovering,
});
return <div className={avatarClasses}>
{avatarElement}
<div className="mx_AvatarSetting_hover">
<div className="mx_AvatarSetting_hoverBg" />
<span>{_t("Upload")}</span>
</div>
{uploadAvatarBtn}
{removeAvatarBtn}
</div>;
};

View file

@ -184,7 +184,7 @@ export default class ChangePassword extends React.Component {
_onExportE2eKeysClicked = () => {
Modal.createTrackedDialogAsync('Export E2E Keys', 'Change Password',
import('../../../async-components/views/dialogs/ExportE2eKeysDialog'),
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
{
matrixClient: MatrixClientPeg.get(),
},

View file

@ -22,6 +22,7 @@ import * as sdk from '../../../index';
import Modal from '../../../Modal';
import Spinner from '../elements/Spinner';
import InteractiveAuthDialog from '../dialogs/InteractiveAuthDialog';
import ConfirmDestroyCrossSigningDialog from '../dialogs/security/ConfirmDestroyCrossSigningDialog';
export default class CrossSigningPanel extends React.PureComponent {
constructor(props) {
@ -137,7 +138,6 @@ export default class CrossSigningPanel extends React.PureComponent {
}
_resetCrossSigning = () => {
const ConfirmDestroyCrossSigningDialog = sdk.getComponent("dialogs.ConfirmDestroyCrossSigningDialog");
Modal.createDialog(ConfirmDestroyCrossSigningDialog, {
onFinished: (act) => {
if (!act) return;
@ -187,37 +187,46 @@ export default class CrossSigningPanel extends React.PureComponent {
}
const keysExistAnywhere = (
crossSigningPublicKeysOnDevice ||
crossSigningPrivateKeysInStorage ||
crossSigningPublicKeysOnDevice
masterPrivateKeyCached ||
selfSigningPrivateKeyCached ||
userSigningPrivateKeyCached
);
const keysExistEverywhere = (
crossSigningPublicKeysOnDevice &&
crossSigningPrivateKeysInStorage &&
crossSigningPublicKeysOnDevice
masterPrivateKeyCached &&
selfSigningPrivateKeyCached &&
userSigningPrivateKeyCached
);
let resetButton;
if (keysExistAnywhere) {
resetButton = (
<div className="mx_CrossSigningPanel_buttonRow">
<AccessibleButton kind="danger" onClick={this._resetCrossSigning}>
{_t("Reset")}
</AccessibleButton>
</div>
const actions = [];
// 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}>
{_t("Set up")}
</AccessibleButton>,
);
}
// TODO: determine how better to expose this to users in addition to prompts at login/toast
let bootstrapButton;
if (!keysExistEverywhere && homeserverSupportsCrossSigning) {
bootstrapButton = (
<div className="mx_CrossSigningPanel_buttonRow">
<AccessibleButton kind="primary" onClick={this._onBootstrapClick}>
{_t("Set up")}
</AccessibleButton>
</div>
if (keysExistAnywhere) {
actions.push(
<AccessibleButton key="reset" kind="danger" onClick={this._resetCrossSigning}>
{_t("Reset")}
</AccessibleButton>,
);
}
let actionRow;
if (actions.length) {
actionRow = <div className="mx_CrossSigningPanel_buttonRow">
{actions}
</div>;
}
return (
<div>
{summarisedStatus}
@ -230,7 +239,7 @@ export default class CrossSigningPanel extends React.PureComponent {
</tr>
<tr>
<td>{_t("Cross-signing private keys:")}</td>
<td>{crossSigningPrivateKeysInStorage ? _t("in secret storage") : _t("not found")}</td>
<td>{crossSigningPrivateKeysInStorage ? _t("in secret storage") : _t("not found in storage")}</td>
</tr>
<tr>
<td>{_t("Master private key:")}</td>
@ -251,8 +260,7 @@ export default class CrossSigningPanel extends React.PureComponent {
</tbody></table>
</details>
{errorSection}
{bootstrapButton}
{resetButton}
{actionRow}
</div>
);
}

View file

@ -19,6 +19,7 @@ import React from 'react';
import * as sdk from '../../../index';
import {_t} from "../../../languageHandler";
import {SettingLevel} from "../../../settings/SettingLevel";
import SettingsStore from "../../../settings/SettingsStore";
const SETTING_MANUALLY_VERIFY_ALL_SESSIONS = "e2ee.manuallyVerifyAllSessions";
@ -37,3 +38,7 @@ const E2eAdvancedPanel = props => {
};
export default E2eAdvancedPanel;
export function isE2eAdvancedPanelPossible(): boolean {
return SettingsStore.isEnabled(SETTING_MANUALLY_VERIFY_ALL_SESSIONS);
}

View file

@ -65,6 +65,15 @@ export default class ProfileSettings extends React.Component {
});
};
_clearProfile = async (e) => {
e.stopPropagation();
e.preventDefault();
if (!this.state.enableProfileSave) return;
this._removeAvatar();
this.setState({enableProfileSave: false, displayName: this.state.originalDisplayName});
};
_saveProfile = async (e) => {
e.stopPropagation();
e.preventDefault();
@ -144,18 +153,27 @@ export default class ProfileSettings extends React.Component {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const AvatarSetting = sdk.getComponent('settings.AvatarSetting');
return (
<form onSubmit={this._saveProfile} autoComplete="off" noValidate={true}>
<form
onSubmit={this._saveProfile}
autoComplete="off"
noValidate={true}
className="mx_ProfileSettings_profileForm"
>
<input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload"
onChange={this._onAvatarChanged} accept="image/*" />
<div className="mx_ProfileSettings_profile">
<div className="mx_ProfileSettings_controls">
<span className="mx_SettingsTab_subheading">{_t("Profile")}</span>
<Field
label={_t("Display Name")}
type="text" value={this.state.displayName}
autoComplete="off"
onChange={this._onDisplayNameChanged}
/>
<p>
{this.state.userId}
{hostingSignup}
</p>
<Field label={_t("Display Name")}
type="text" value={this.state.displayName} autoComplete="off"
onChange={this._onDisplayNameChanged} />
</div>
<AvatarSetting
avatarUrl={this.state.avatarUrl}
@ -164,10 +182,22 @@ export default class ProfileSettings extends React.Component {
uploadAvatar={this._uploadAvatar}
removeAvatar={this._removeAvatar} />
</div>
<AccessibleButton onClick={this._saveProfile} kind="primary"
disabled={!this.state.enableProfileSave}>
{_t("Save")}
</AccessibleButton>
<div className="mx_ProfileSettings_buttons">
<AccessibleButton
onClick={this._clearProfile}
kind="link"
disabled={!this.state.enableProfileSave}
>
{_t("Cancel")}
</AccessibleButton>
<AccessibleButton
onClick={this._saveProfile}
kind="primary"
disabled={!this.state.enableProfileSave}
>
{_t("Save")}
</AccessibleButton>
</div>
</form>
);
}

View file

@ -24,7 +24,7 @@ import { isSecureBackupRequired } from '../../../utils/WellKnownUtils';
import Spinner from '../elements/Spinner';
import AccessibleButton from '../elements/AccessibleButton';
import QuestionDialog from '../dialogs/QuestionDialog';
import RestoreKeyBackupDialog from '../dialogs/keybackup/RestoreKeyBackupDialog';
import RestoreKeyBackupDialog from '../dialogs/security/RestoreKeyBackupDialog';
import { accessSecretStorage } from '../../../SecurityManager';
export default class SecureBackupPanel extends React.PureComponent {
@ -131,7 +131,7 @@ export default class SecureBackupPanel extends React.PureComponent {
const cli = MatrixClientPeg.get();
const secretStorage = cli._crypto._secretStorage;
const backupKeyStored = await cli.isKeyBackupKeyStored();
const backupKeyStored = !!(await cli.isKeyBackupKeyStored());
const backupKeyFromCache = await cli._crypto.getSessionBackupPrivateKey();
const backupKeyCached = !!(backupKeyFromCache);
const backupKeyWellFormed = backupKeyFromCache instanceof Uint8Array;
@ -150,7 +150,7 @@ export default class SecureBackupPanel extends React.PureComponent {
_startNewBackup = () => {
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
import('../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
import('../../../async-components/views/dialogs/security/CreateKeyBackupDialog'),
{
onFinished: () => {
this._loadBackupStatus();
@ -367,14 +367,14 @@ export default class SecureBackupPanel extends React.PureComponent {
</>;
actions.push(
<AccessibleButton kind="primary" onClick={this._restoreBackup}>
<AccessibleButton key="restore" kind="primary" onClick={this._restoreBackup}>
{restoreButtonCaption}
</AccessibleButton>,
);
if (!isSecureBackupRequired()) {
actions.push(
<AccessibleButton kind="danger" onClick={this._deleteBackup}>
<AccessibleButton key="delete" kind="danger" onClick={this._deleteBackup}>
{_t("Delete Backup")}
</AccessibleButton>,
);
@ -388,7 +388,7 @@ export default class SecureBackupPanel extends React.PureComponent {
<p>{_t("Back up your keys before signing out to avoid losing them.")}</p>
</>;
actions.push(
<AccessibleButton kind="primary" onClick={this._startNewBackup}>
<AccessibleButton key="setup" kind="primary" onClick={this._startNewBackup}>
{_t("Set up")}
</AccessibleButton>,
);
@ -396,7 +396,7 @@ export default class SecureBackupPanel extends React.PureComponent {
if (secretStorageKeyInAccount) {
actions.push(
<AccessibleButton kind="danger" onClick={this._resetSecretStorage}>
<AccessibleButton key="reset" kind="danger" onClick={this._resetSecretStorage}>
{_t("Reset")}
</AccessibleButton>,
);

View file

@ -73,6 +73,18 @@ export default class GeneralRoomSettingsTab extends React.Component {
urlPreviewSettings = null;
}
let flairSection;
if (SettingsStore.getValue(UIFeature.Flair)) {
flairSection = <>
<span className='mx_SettingsTab_subheading'>{_t("Flair")}</span>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<RelatedGroupSettings roomId={room.roomId}
canSetRelatedGroups={canChangeGroups}
relatedGroupsEvent={groupsEvent} />
</div>
</>;
}
return (
<div className="mx_SettingsTab mx_GeneralRoomSettingsTab">
<div className="mx_SettingsTab_heading">{_t("General")}</div>
@ -87,14 +99,8 @@ export default class GeneralRoomSettingsTab extends React.Component {
canonicalAliasEvent={canonicalAliasEv} aliasEvents={aliasEvents} />
</div>
<div className="mx_SettingsTab_heading">{_t("Other")}</div>
<span className='mx_SettingsTab_subheading'>{_t("Flair")}</span>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<RelatedGroupSettings roomId={room.roomId}
canSetRelatedGroups={canChangeGroups}
relatedGroupsEvent={groupsEvent} />
</div>
{urlPreviewSettings}
{ flairSection }
{ urlPreviewSettings }
<span className='mx_SettingsTab_subheading'>{_t("Leave room")}</span>
<div className='mx_SettingsTab_section'>

View file

@ -24,6 +24,7 @@ import Modal from "../../../../../Modal";
import QuestionDialog from "../../../dialogs/QuestionDialog";
import StyledRadioGroup from '../../../elements/StyledRadioGroup';
import {SettingLevel} from "../../../../../settings/SettingLevel";
import SettingsStore from "../../../../../settings/SettingsStore";
export default class SecurityRoomSettingsTab extends React.Component {
static propTypes = {
@ -340,10 +341,13 @@ export default class SecurityRoomSettingsTab extends React.Component {
const canEnableEncryption = !isEncrypted && hasEncryptionPermission;
let encryptionSettings = null;
if (isEncrypted) {
encryptionSettings = <SettingsFlag name="blacklistUnverifiedDevices" level={SettingLevel.ROOM_DEVICE}
onChange={this._updateBlacklistDevicesFlag}
roomId={this.props.roomId} />;
if (isEncrypted && SettingsStore.isEnabled("blacklistUnverifiedDevices")) {
encryptionSettings = <SettingsFlag
name="blacklistUnverifiedDevices"
level={SettingLevel.ROOM_DEVICE}
onChange={this._updateBlacklistDevicesFlag}
roomId={this.props.roomId}
/>;
}
return (

View file

@ -36,6 +36,7 @@ import EventTilePreview from '../../../elements/EventTilePreview';
import StyledRadioGroup from "../../../elements/StyledRadioGroup";
import classNames from 'classnames';
import { SettingLevel } from "../../../../../settings/SettingLevel";
import {UIFeature} from "../../../../../settings/UIFeature";
interface IProps {
}
@ -386,6 +387,8 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
};
private renderAdvancedSection() {
if (!SettingsStore.getValue(UIFeature.AdvancedSettings)) return null;
const brand = SdkConfig.get().brand;
const toggle = <div
className="mx_AppearanceUserSettingsTab_AdvancedToggle"

View file

@ -221,7 +221,6 @@ export default class GeneralUserSettingsTab extends React.Component {
_renderProfileSection() {
return (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Profile")}</span>
<ProfileSettings />
</div>
);
@ -248,7 +247,9 @@ export default class GeneralUserSettingsTab extends React.Component {
// validate 3PID ownership even if we're just adding to the homeserver only.
// For newer homeservers with separate 3PID add and bind methods (MSC2290),
// there is no such concern, so we can always show the HS account 3PIDs.
if (this.state.haveIdServer || this.state.serverSupportsSeparateAddAndBind === true) {
if (SettingsStore.getValue(UIFeature.ThirdPartyID) &&
(this.state.haveIdServer || this.state.serverSupportsSeparateAddAndBind === true)
) {
const emails = this.state.loading3pids
? <Spinner />
: <EmailAddresses
@ -386,17 +387,31 @@ export default class GeneralUserSettingsTab extends React.Component {
width="18" height="18" alt={_t("Warning")} />
: null;
let accountManagementSection;
if (SettingsStore.getValue(UIFeature.Deactivate)) {
accountManagementSection = <>
<div className="mx_SettingsTab_heading">{_t("Deactivate account")}</div>
{this._renderManagementSection()}
</>;
}
let discoverySection;
if (SettingsStore.getValue(UIFeature.IdentityServer)) {
discoverySection = <>
<div className="mx_SettingsTab_heading">{discoWarning} {_t("Discovery")}</div>
{this._renderDiscoverySection()}
</>;
}
return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{_t("General")}</div>
{this._renderProfileSection()}
{this._renderAccountSection()}
{this._renderLanguageSection()}
<div className="mx_SettingsTab_heading">{discoWarning} {_t("Discovery")}</div>
{this._renderDiscoverySection()}
{ discoverySection }
{this._renderIntegrationManagerSection() /* Has its own title */}
<div className="mx_SettingsTab_heading">{_t("Deactivate account")}</div>
{this._renderManagementSection()}
{ accountManagementSection }
</div>
);
}

View file

@ -23,7 +23,6 @@ import Field from "../../../elements/Field";
import * as sdk from "../../../../..";
import PlatformPeg from "../../../../../PlatformPeg";
import {SettingLevel} from "../../../../../settings/SettingLevel";
import {UIFeature} from "../../../../../settings/UIFeature";
export default class PreferencesUserSettingsTab extends React.Component {
static ROOM_LIST_SETTINGS = [
@ -50,10 +49,10 @@ export default class PreferencesUserSettingsTab extends React.Component {
'showAvatarChanges',
'showDisplaynameChanges',
'showImages',
'Pill.shouldShowPillAvatar',
];
static ADVANCED_SETTINGS = [
'Pill.shouldShowPillAvatar',
static GENERAL_SETTINGS = [
'TagPanel.enableTagPanel',
'promptBeforeInviteUnknownUsers',
// Start automatically after startup (electron-only)
@ -138,12 +137,10 @@ export default class PreferencesUserSettingsTab extends React.Component {
};
_renderGroup(settingIds) {
if (!SettingsStore.getValue(UIFeature.URLPreviews)) {
settingIds = settingIds.filter(i => i !== 'urlPreviewsEnabled');
}
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
return settingIds.map(i => <SettingsFlag key={i} name={i} level={SettingLevel.ACCOUNT} />);
return settingIds.filter(SettingsStore.isEnabled).map(i => {
return <SettingsFlag key={i} name={i} level={SettingLevel.ACCOUNT} />;
});
}
render() {
@ -191,8 +188,8 @@ export default class PreferencesUserSettingsTab extends React.Component {
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Advanced")}</span>
{this._renderGroup(PreferencesUserSettingsTab.ADVANCED_SETTINGS)}
<span className="mx_SettingsTab_subheading">{_t("General")}</span>
{this._renderGroup(PreferencesUserSettingsTab.GENERAL_SETTINGS)}
{minimizeToTrayOption}
{autoHideMenuOption}
{autoLaunchOption}

View file

@ -30,6 +30,9 @@ import dis from "../../../../../dispatcher/dispatcher";
import {privateShouldBeEncrypted} from "../../../../../createRoom";
import {SettingLevel} from "../../../../../settings/SettingLevel";
import SecureBackupPanel from "../../SecureBackupPanel";
import SettingsStore from "../../../../../settings/SettingsStore";
import {UIFeature} from "../../../../../settings/UIFeature";
import {isE2eAdvancedPanelPossible} from "../../E2eAdvancedPanel";
export class IgnoredUser extends React.Component {
static propTypes = {
@ -103,14 +106,14 @@ export default class SecurityUserSettingsTab extends React.Component {
_onExportE2eKeysClicked = () => {
Modal.createTrackedDialogAsync('Export E2E Keys', '',
import('../../../../../async-components/views/dialogs/ExportE2eKeysDialog'),
import('../../../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
{matrixClient: MatrixClientPeg.get()},
);
};
_onImportE2eKeysClicked = () => {
Modal.createTrackedDialogAsync('Import E2E Keys', '',
import('../../../../../async-components/views/dialogs/ImportE2eKeysDialog'),
import('../../../../../async-components/views/dialogs/security/ImportE2eKeysDialog'),
{matrixClient: MatrixClientPeg.get()},
);
};
@ -217,6 +220,15 @@ export default class SecurityUserSettingsTab extends React.Component {
);
}
let noSendUnverifiedSetting;
if (SettingsStore.isEnabled("blacklistUnverifiedDevices")) {
noSendUnverifiedSetting = <SettingsFlag
name='blacklistUnverifiedDevices'
level={SettingLevel.DEVICE}
onChange={this._updateBlacklistDevicesFlag}
/>;
}
return (
<div className='mx_SettingsTab_section'>
<span className='mx_SettingsTab_subheading'>{_t("Cryptography")}</span>
@ -231,8 +243,7 @@ export default class SecurityUserSettingsTab extends React.Component {
</li>
</ul>
{importExportButtons}
<SettingsFlag name='blacklistUnverifiedDevices' level={SettingLevel.DEVICE}
onChange={this._updateBlacklistDevicesFlag} />
{noSendUnverifiedSetting}
</div>
);
}
@ -311,15 +322,13 @@ export default class SecurityUserSettingsTab extends React.Component {
// can remove this.
const CrossSigningPanel = sdk.getComponent('views.settings.CrossSigningPanel');
const crossSigning = (
<div className='mx_SettingsTab_section'>
<span className="mx_SettingsTab_subheading">{_t("Cross-signing")}</span>
<div className='mx_SettingsTab_subsectionText'>
<CrossSigningPanel />
</div>
<div className='mx_SettingsTab_section'>
<span className="mx_SettingsTab_subheading">{_t("Cross-signing")}</span>
<div className='mx_SettingsTab_subsectionText'>
<CrossSigningPanel />
</div>
);
const E2eAdvancedPanel = sdk.getComponent('views.settings.E2eAdvancedPanel');
</div>
);
let warning;
if (!privateShouldBeEncrypted()) {
@ -352,6 +361,25 @@ export default class SecurityUserSettingsTab extends React.Component {
</React.Fragment>;
}
const E2eAdvancedPanel = sdk.getComponent('views.settings.E2eAdvancedPanel');
let advancedSection;
if (SettingsStore.getValue(UIFeature.AdvancedSettings)) {
const ignoreUsersPanel = this._renderIgnoredUsers();
const invitesPanel = this._renderManageInvites();
const e2ePanel = isE2eAdvancedPanelPossible() ? <E2eAdvancedPanel /> : null;
// only show the section if there's something to show
if (ignoreUsersPanel || invitesPanel || e2ePanel) {
advancedSection = <>
<div className="mx_SettingsTab_heading">{_t("Advanced")}</div>
<div className="mx_SettingsTab_section">
{ignoreUsersPanel}
{invitesPanel}
{e2ePanel}
</div>
</>;
}
}
return (
<div className="mx_SettingsTab mx_SecurityUserSettingsTab">
{warning}
@ -381,12 +409,7 @@ export default class SecurityUserSettingsTab extends React.Component {
{this._renderCurrentDeviceInfo()}
</div>
{ privacySection }
<div className="mx_SettingsTab_heading">{_t("Advanced")}</div>
<div className="mx_SettingsTab_section">
{this._renderIgnoredUsers()}
{this._renderManageInvites()}
<E2eAdvancedPanel />
</div>
{ advancedSection }
</div>
);
}

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
import IncomingCallBox from './IncomingCallBox';
import CallPreview from './CallPreview';
import * as VectorConferenceHandler from '../../../VectorConferenceHandler';
interface IProps {
@ -31,7 +30,7 @@ export default class CallContainer extends React.PureComponent<IProps, IState> {
public render() {
return <div className="mx_CallContainer">
<IncomingCallBox />
<CallPreview ConferenceHandler={VectorConferenceHandler} />
<CallPreview />
</div>;
}
}

View file

@ -26,10 +26,6 @@ import PersistentApp from "../elements/PersistentApp";
import SettingsStore from "../../../settings/SettingsStore";
interface IProps {
// A Conference Handler implementation
// Must have a function signature:
// getConferenceCallForRoom(roomId: string): MatrixCall
ConferenceHandler: any;
}
interface IState {
@ -47,7 +43,7 @@ export default class CallPreview extends React.Component<IProps, IState> {
this.state = {
roomId: RoomViewStore.getRoomId(),
activeCall: CallHandler.getAnyActiveCall(),
activeCall: CallHandler.sharedInstance().getAnyActiveCall(),
};
}
@ -77,14 +73,14 @@ export default class CallPreview extends React.Component<IProps, IState> {
// may hide the global CallView if the call it is tracking is dead
case 'call_state':
this.setState({
activeCall: CallHandler.getAnyActiveCall(),
activeCall: CallHandler.sharedInstance().getAnyActiveCall(),
});
break;
}
};
private onCallViewClick = () => {
const call = CallHandler.getAnyActiveCall();
const call = CallHandler.sharedInstance().getAnyActiveCall();
if (call) {
dis.dispatch({
action: 'view_room',
@ -94,7 +90,7 @@ export default class CallPreview extends React.Component<IProps, IState> {
};
public render() {
const callForRoom = CallHandler.getCallForRoom(this.state.roomId);
const callForRoom = CallHandler.sharedInstance().getCallForRoom(this.state.roomId);
const showCall = (
this.state.activeCall &&
this.state.activeCall.call_state === 'connected' &&
@ -106,7 +102,6 @@ export default class CallPreview extends React.Component<IProps, IState> {
<CallView
className="mx_CallPreview"
onClick={this.onCallViewClick}
ConferenceHandler={this.props.ConferenceHandler}
showHangup={true}
/>
);

View file

@ -31,11 +31,6 @@ interface IProps {
// room; if not, we will show any active call.
room?: Room;
// A Conference Handler implementation
// Must have a function signature:
// getConferenceCallForRoom(roomId: string): MatrixCall
ConferenceHandler?: any;
// maxHeight style attribute for the video panel
maxVideoHeight?: number;
@ -96,14 +91,13 @@ export default class CallView extends React.Component<IProps, IState> {
if (this.props.room) {
const roomId = this.props.room.roomId;
call = CallHandler.getCallForRoom(roomId) ||
(this.props.ConferenceHandler ? this.props.ConferenceHandler.getConferenceCallForRoom(roomId) : null);
call = CallHandler.sharedInstance().getCallForRoom(roomId);
if (this.call) {
this.setState({ call: call });
}
} else {
call = CallHandler.getAnyActiveCall();
call = CallHandler.sharedInstance().getAnyActiveCall();
// Ignore calls if we can't get the room associated with them.
// I think the underlying problem is that the js-sdk sends events
// for calls before it has made the rooms available in the store,
@ -115,20 +109,19 @@ export default class CallView extends React.Component<IProps, IState> {
}
if (call) {
call.setLocalVideoElement(this.getVideoView().getLocalVideoElement());
call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement());
// always use a separate element for audio stream playback.
// this is to let us move CallView around the DOM without interrupting remote audio
// during playback, by having the audio rendered by a top-level <audio/> element.
// rather than being rendered by the main remoteVideo <video/> element.
call.setRemoteAudioElement(this.getVideoView().getRemoteAudioElement());
if (this.getVideoView()) {
call.setLocalVideoElement(this.getVideoView().getLocalVideoElement());
call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement());
// always use a separate element for audio stream playback.
// this is to let us move CallView around the DOM without interrupting remote audio
// during playback, by having the audio rendered by a top-level <audio/> element.
// rather than being rendered by the main remoteVideo <video/> element.
call.setRemoteAudioElement(this.getVideoView().getRemoteAudioElement());
}
}
if (call && call.type === "video" && call.call_state !== "ended" && call.call_state !== "ringing") {
// if this call is a conf call, don't display local video as the
// conference will have us in it
this.getVideoView().getLocalVideoElement().style.display = (
call.confUserId ? "none" : "block"
);
this.getVideoView().getLocalVideoElement().style.display = "block";
this.getVideoView().getRemoteVideoElement().style.display = "block";
} else {
this.getVideoView().getLocalVideoElement().style.display = "none";

View file

@ -52,7 +52,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
private onAction = (payload: ActionPayload) => {
switch (payload.action) {
case 'call_state': {
const call = CallHandler.getCall(payload.room_id);
const call = CallHandler.sharedInstance().getCallForRoom(payload.room_id);
if (call && call.call_state === 'ringing') {
this.setState({
incomingCall: call,