Merge remote-tracking branch 'upstream/develop' into compact-reply-rendering
This commit is contained in:
commit
987cdf1bf0
201 changed files with 6532 additions and 2503 deletions
|
@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {CSSProperties, useRef, useState} from "react";
|
||||
import React, {CSSProperties, RefObject, useRef, useState} from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import classNames from "classnames";
|
||||
|
||||
|
@ -416,8 +416,8 @@ export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None
|
|||
return menuOptions;
|
||||
};
|
||||
|
||||
export const useContextMenu = () => {
|
||||
const button = useRef(null);
|
||||
export const useContextMenu = (): [boolean, RefObject<HTMLElement>, () => void, () => void, (val: boolean) => void] => {
|
||||
const button = useRef<HTMLElement>(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const open = () => {
|
||||
setIsOpen(true);
|
||||
|
|
|
@ -23,6 +23,8 @@ import * as sdk from '../../index';
|
|||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import EventIndexPeg from "../../indexing/EventIndexPeg";
|
||||
import { _t } from '../../languageHandler';
|
||||
import BaseCard from "../views/right_panel/BaseCard";
|
||||
import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
|
||||
|
||||
/*
|
||||
* Component which shows the filtered file using a TimelinePanel
|
||||
|
@ -30,6 +32,7 @@ import { _t } from '../../languageHandler';
|
|||
class FilePanel extends React.Component {
|
||||
static propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
// This is used to track if a decrypted event was a live event and should be
|
||||
|
@ -188,18 +191,26 @@ class FilePanel extends React.Component {
|
|||
|
||||
render() {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
|
||||
return <BaseCard
|
||||
className="mx_FilePanel mx_RoomView_messageListWrapper"
|
||||
onClose={this.props.onClose}
|
||||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
>
|
||||
<div className="mx_RoomView_empty">
|
||||
{ _t("You must <a>register</a> to use this functionality",
|
||||
{},
|
||||
{ 'a': (sub) => <a href="#/register" key="sub">{ sub }</a> })
|
||||
}
|
||||
</div>
|
||||
</div>;
|
||||
</BaseCard>;
|
||||
} else if (this.noRoom) {
|
||||
return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
|
||||
return <BaseCard
|
||||
className="mx_FilePanel mx_RoomView_messageListWrapper"
|
||||
onClose={this.props.onClose}
|
||||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
>
|
||||
<div className="mx_RoomView_empty">{ _t("You must join the room to see its files") }</div>
|
||||
</div>;
|
||||
</BaseCard>;
|
||||
}
|
||||
|
||||
// wrap a TimelinePanel with the jump-to-event bits turned off.
|
||||
|
@ -215,7 +226,12 @@ class FilePanel extends React.Component {
|
|||
// console.log("rendering TimelinePanel for timelineSet " + this.state.timelineSet.room.roomId + " " +
|
||||
// "(" + this.state.timelineSet._timelines.join(", ") + ")" + " with key " + this.props.roomId);
|
||||
return (
|
||||
<div className="mx_FilePanel" role="tabpanel">
|
||||
<BaseCard
|
||||
className="mx_FilePanel"
|
||||
onClose={this.props.onClose}
|
||||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
withoutScrollContainer
|
||||
>
|
||||
<TimelinePanel
|
||||
manageReadReceipts={false}
|
||||
manageReadMarkers={false}
|
||||
|
@ -226,13 +242,17 @@ class FilePanel extends React.Component {
|
|||
resizeNotifier={this.props.resizeNotifier}
|
||||
empty={emptyState}
|
||||
/>
|
||||
</div>
|
||||
</BaseCard>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="mx_FilePanel" role="tabpanel">
|
||||
<BaseCard
|
||||
className="mx_FilePanel"
|
||||
onClose={this.props.onClose}
|
||||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
>
|
||||
<Loader />
|
||||
</div>
|
||||
</BaseCard>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1322,7 +1322,7 @@ export default class GroupView extends React.Component {
|
|||
</div>
|
||||
<GroupHeaderButtons />
|
||||
</div>
|
||||
<MainSplit panel={rightPanel}>
|
||||
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
|
||||
<AutoHideScrollbar className="mx_GroupView_body">
|
||||
{ this._getMembershipSection() }
|
||||
{ this._getGroupSection() }
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -56,6 +56,7 @@ import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPay
|
|||
import RoomListStore from "../../stores/room-list/RoomListStore";
|
||||
import NonUrgentToastContainer from "./NonUrgentToastContainer";
|
||||
import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload";
|
||||
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||
|
||||
// We need to fetch each pinned message individually (if we don't already have it)
|
||||
// so each pinned message may trigger a request. Limit the number per room for sanity.
|
||||
|
@ -76,13 +77,12 @@ interface IProps {
|
|||
hideToSRUsers: boolean;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
middleDisabled: boolean;
|
||||
initialEventPixelOffset: number;
|
||||
leftDisabled: boolean;
|
||||
rightDisabled: boolean;
|
||||
// eslint-disable-next-line camelcase
|
||||
page_type: string;
|
||||
autoJoin: boolean;
|
||||
thirdPartyInvite?: object;
|
||||
threepidInvite?: IThreepidInvite;
|
||||
roomOobData?: object;
|
||||
currentRoomId: string;
|
||||
ConferenceHandler?: object;
|
||||
|
@ -257,6 +257,12 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
window.localStorage.setItem("mx_lhs_size", '' + size);
|
||||
this.props.resizeNotifier.notifyLeftHandleResized();
|
||||
},
|
||||
onResizeStart: () => {
|
||||
this.props.resizeNotifier.startResizing();
|
||||
},
|
||||
onResizeStop: () => {
|
||||
this.props.resizeNotifier.stopResizing();
|
||||
},
|
||||
};
|
||||
const resizer = new Resizer(
|
||||
this._resizeContainer.current,
|
||||
|
@ -626,10 +632,9 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
ref={this._roomView}
|
||||
autoJoin={this.props.autoJoin}
|
||||
onRegistered={this.props.onRegistered}
|
||||
thirdPartyInvite={this.props.thirdPartyInvite}
|
||||
threepidInvite={this.props.threepidInvite}
|
||||
oobData={this.props.roomOobData}
|
||||
viaServers={this.props.viaServers}
|
||||
eventPixelOffset={this.props.initialEventPixelOffset}
|
||||
key={this.props.currentRoomId || 'roomview'}
|
||||
disabled={this.props.middleDisabled}
|
||||
ConferenceHandler={this.props.ConferenceHandler}
|
||||
|
@ -650,12 +655,13 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
break;
|
||||
|
||||
case PageTypes.UserView:
|
||||
pageElement = <UserView userId={this.props.currentUserId} />;
|
||||
pageElement = <UserView userId={this.props.currentUserId} resizeNotifier={this.props.resizeNotifier} />;
|
||||
break;
|
||||
case PageTypes.GroupView:
|
||||
pageElement = <GroupView
|
||||
groupId={this.props.currentGroupId}
|
||||
isNew={this.props.currentGroupIsNew}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
/>;
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -19,9 +19,18 @@ import React from 'react';
|
|||
import { Resizable } from 're-resizable';
|
||||
|
||||
export default class MainSplit extends React.Component {
|
||||
_onResized = (event, direction, refToElement, delta) => {
|
||||
_onResizeStart = () => {
|
||||
this.props.resizeNotifier.startResizing();
|
||||
};
|
||||
|
||||
_onResize = () => {
|
||||
this.props.resizeNotifier.notifyRightHandleResized();
|
||||
};
|
||||
|
||||
_onResizeStop = (event, direction, refToElement, delta) => {
|
||||
this.props.resizeNotifier.stopResizing();
|
||||
window.localStorage.setItem("mx_rhs_size", this._loadSidePanelSize().width + delta.width);
|
||||
}
|
||||
};
|
||||
|
||||
_loadSidePanelSize() {
|
||||
let rhsSize = parseInt(window.localStorage.getItem("mx_rhs_size"), 10);
|
||||
|
@ -58,7 +67,9 @@ export default class MainSplit extends React.Component {
|
|||
bottomLeft: false,
|
||||
topLeft: false,
|
||||
}}
|
||||
onResizeStop={this._onResized}
|
||||
onResizeStart={this._onResizeStart}
|
||||
onResize={this._onResize}
|
||||
onResizeStop={this._onResizeStop}
|
||||
className="mx_RightPanel_ResizeWrapper"
|
||||
handleClasses={{left: "mx_RightPanel_ResizeHandle"}}
|
||||
>
|
||||
|
|
|
@ -78,6 +78,8 @@ import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotif
|
|||
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";
|
||||
|
||||
/** constants for MatrixChat.state.view */
|
||||
export enum Views {
|
||||
|
@ -137,9 +139,9 @@ interface IRoomInfo {
|
|||
|
||||
auto_join?: boolean;
|
||||
highlighted?: boolean;
|
||||
third_party_invite?: object;
|
||||
oob_data?: object;
|
||||
via_servers?: string[];
|
||||
threepid_invite?: IThreepidInvite;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
|
@ -147,7 +149,7 @@ interface IProps { // TODO type things better
|
|||
config: Record<string, any>;
|
||||
serverConfig?: ValidatedServerConfig;
|
||||
ConferenceHandler?: any;
|
||||
onNewScreen: (string) => void;
|
||||
onNewScreen: (screen: string, replaceLast: boolean) => void;
|
||||
enableGuest?: boolean;
|
||||
// the queryParams extracted from the [real] query-string of the URI
|
||||
realQueryParams?: Record<string, string>;
|
||||
|
@ -196,7 +198,7 @@ interface IState {
|
|||
resizeNotifier: ResizeNotifier;
|
||||
serverConfig?: ValidatedServerConfig;
|
||||
ready: boolean;
|
||||
thirdPartyInvite?: object;
|
||||
threepidInvite?: IThreepidInvite,
|
||||
roomOobData?: object;
|
||||
viaServers?: string[];
|
||||
pendingInitialSync?: boolean;
|
||||
|
@ -260,6 +262,14 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
// outside this.state because updating it should never trigger a
|
||||
// rerender.
|
||||
this.screenAfterLogin = this.props.initialScreenAfterLogin;
|
||||
if (this.screenAfterLogin) {
|
||||
const params = this.screenAfterLogin.params || {};
|
||||
if (this.screenAfterLogin.screen.startsWith("room/") && params['signurl'] && params['email']) {
|
||||
// probably a threepid invite - try to store it
|
||||
const roomId = this.screenAfterLogin.screen.substring("room/".length);
|
||||
ThreepidInviteStore.instance.storeInvite(roomId, params as IThreepidInviteWireFormat);
|
||||
}
|
||||
}
|
||||
|
||||
this.windowWidth = 10000;
|
||||
this.handleResize();
|
||||
|
@ -404,8 +414,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
}).then((loadedSession) => {
|
||||
if (!loadedSession) {
|
||||
// fall back to showing the welcome screen
|
||||
dis.dispatch({action: "view_welcome_page"});
|
||||
// fall back to showing the welcome screen... unless we have a 3pid invite pending
|
||||
if (ThreepidInviteStore.instance.pickBestInvite()) {
|
||||
dis.dispatch({action: 'start_registration'});
|
||||
} else {
|
||||
dis.dispatch({action: "view_welcome_page"});
|
||||
}
|
||||
}
|
||||
});
|
||||
// Note we don't catch errors from this: we catch everything within
|
||||
|
@ -835,10 +849,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
// context of that particular event.
|
||||
// @param {boolean=} roomInfo.highlighted If true, add event_id to the hash of the URL
|
||||
// and alter the EventTile to appear highlighted.
|
||||
// @param {Object=} roomInfo.third_party_invite Object containing data about the third party
|
||||
// we received to join the room, if any.
|
||||
// @param {string=} roomInfo.third_party_invite.inviteSignUrl 3pid invite sign URL
|
||||
// @param {string=} roomInfo.third_party_invite.invitedEmail The email address the invite was sent to
|
||||
// @param {Object=} roomInfo.threepid_invite Object containing data about the third party
|
||||
// we received to join the room, if any.
|
||||
// @param {Object=} roomInfo.oob_data Object of additional data about the room
|
||||
// that has been passed out-of-band (eg.
|
||||
// room name and avatar from an invite email)
|
||||
|
@ -886,6 +898,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
// If we are redirecting to a Room Alias and it is for the room we already showing then replace history item
|
||||
const replaceLast = presentedId[0] === "#" && roomInfo.room_id === this.state.currentRoomId;
|
||||
|
||||
if (roomInfo.event_id && roomInfo.highlighted) {
|
||||
presentedId += "/" + roomInfo.event_id;
|
||||
}
|
||||
|
@ -893,12 +908,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
view: Views.LOGGED_IN,
|
||||
currentRoomId: roomInfo.room_id || null,
|
||||
page_type: PageTypes.RoomView,
|
||||
thirdPartyInvite: roomInfo.third_party_invite,
|
||||
threepidInvite: roomInfo.threepid_invite,
|
||||
roomOobData: roomInfo.oob_data,
|
||||
viaServers: roomInfo.via_servers,
|
||||
ready: true,
|
||||
}, () => {
|
||||
this.notifyNewScreen('room/' + presentedId);
|
||||
this.notifyNewScreen('room/' + presentedId, replaceLast);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -1200,6 +1215,14 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
// the homepage.
|
||||
dis.dispatch({action: 'view_home_page'});
|
||||
}
|
||||
} else if (ThreepidInviteStore.instance.pickBestInvite()) {
|
||||
// The user has a 3pid invite pending - show them that
|
||||
const threepidInvite = ThreepidInviteStore.instance.pickBestInvite();
|
||||
|
||||
// HACK: This is a pretty brutal way of threading the invite back through
|
||||
// our systems, but it's the safest we have for now.
|
||||
const params = ThreepidInviteStore.instance.translateToWireFormat(threepidInvite);
|
||||
this.showScreen(`room/${threepidInvite.roomId}`, params)
|
||||
} else {
|
||||
// The user has just logged in after registering,
|
||||
// so show the homepage.
|
||||
|
@ -1211,8 +1234,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
|
||||
StorageManager.tryPersistStorage();
|
||||
|
||||
if (SettingsStore.getValue("showCookieBar") && this.props.config.piwik && navigator.doNotTrack !== "1") {
|
||||
showAnalyticsToast(this.props.config.piwik && this.props.config.piwik.policyUrl);
|
||||
if (SettingsStore.getValue("showCookieBar") && Analytics.canEnable()) {
|
||||
showAnalyticsToast(this.props.config.piwik?.policyUrl);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1341,7 +1364,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
this.firstSyncComplete = true;
|
||||
this.firstSyncPromise.resolve();
|
||||
|
||||
if (Notifier.shouldShowToolbar()) {
|
||||
if (Notifier.shouldShowPrompt()) {
|
||||
showNotificationsToast();
|
||||
}
|
||||
|
||||
|
@ -1350,15 +1373,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;
|
||||
|
||||
|
@ -1474,12 +1501,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'),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -1636,16 +1663,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
|
||||
// TODO: Handle encoded room/event IDs: https://github.com/vector-im/element-web/issues/9149
|
||||
|
||||
// FIXME: sort_out caseConsistency
|
||||
const thirdPartyInvite = {
|
||||
inviteSignUrl: params.signurl,
|
||||
invitedEmail: params.email,
|
||||
};
|
||||
const oobData = {
|
||||
name: params.room_name,
|
||||
avatarUrl: params.room_avatar_url,
|
||||
inviterName: params.inviter_name,
|
||||
};
|
||||
let threepidInvite: IThreepidInvite;
|
||||
if (params.signurl && params.email) {
|
||||
threepidInvite = ThreepidInviteStore.instance
|
||||
.storeInvite(roomString, params as IThreepidInviteWireFormat);
|
||||
}
|
||||
|
||||
// on our URLs there might be a ?via=matrix.org or similar to help
|
||||
// joins to the room succeed. We'll pass these through as an array
|
||||
|
@ -1666,8 +1688,15 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
// it as highlighted, which will propagate to RoomView and highlight the
|
||||
// associated EventTile.
|
||||
highlighted: Boolean(eventId),
|
||||
third_party_invite: thirdPartyInvite,
|
||||
oob_data: oobData,
|
||||
threepid_invite: threepidInvite,
|
||||
// TODO: Replace oob_data with the threepidInvite (which has the same info).
|
||||
// This isn't done yet because it's threaded through so many more places.
|
||||
// See https://github.com/vector-im/element-web/issues/15157
|
||||
oob_data: {
|
||||
name: threepidInvite?.roomName,
|
||||
avatarUrl: threepidInvite?.roomAvatarUrl,
|
||||
inviterName: threepidInvite?.inviterName,
|
||||
},
|
||||
room_alias: undefined,
|
||||
room_id: undefined,
|
||||
};
|
||||
|
@ -1699,9 +1728,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
notifyNewScreen(screen: string) {
|
||||
notifyNewScreen(screen: string, replaceLast = false) {
|
||||
if (this.props.onNewScreen) {
|
||||
this.props.onNewScreen(screen);
|
||||
this.props.onNewScreen(screen, replaceLast);
|
||||
}
|
||||
this.setPageSubtitle();
|
||||
}
|
||||
|
@ -1852,6 +1881,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
|
||||
|
@ -1918,7 +1954,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');
|
||||
|
@ -1997,14 +2033,15 @@ 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 = (
|
||||
<Registration
|
||||
clientSecret={this.state.register_client_secret}
|
||||
sessionId={this.state.register_session_id}
|
||||
idSid={this.state.register_id_sid}
|
||||
email={this.props.startingFragmentQueryParams.email}
|
||||
email={email}
|
||||
brand={this.props.config.brand}
|
||||
makeRegistrationUrl={this.makeRegistrationUrl}
|
||||
onLoggedIn={this.onRegisterFlowComplete}
|
||||
|
@ -2014,7 +2051,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
|
||||
|
@ -2025,6 +2062,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
|
||||
|
@ -2033,7 +2071,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()}
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -17,14 +17,21 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { _t } from '../../languageHandler';
|
||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
||||
import * as sdk from "../../index";
|
||||
import BaseCard from "../views/right_panel/BaseCard";
|
||||
|
||||
/*
|
||||
* Component which shows the global notification list using a TimelinePanel
|
||||
*/
|
||||
class NotificationPanel extends React.Component {
|
||||
static propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
// wrap a TimelinePanel with the jump-to-event bits turned off.
|
||||
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
||||
|
@ -35,28 +42,27 @@ class NotificationPanel extends React.Component {
|
|||
<p>{_t('You have no visible notifications in this room.')}</p>
|
||||
</div>);
|
||||
|
||||
let content;
|
||||
const timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
|
||||
if (timelineSet) {
|
||||
return (
|
||||
<div className="mx_NotificationPanel" role="tabpanel">
|
||||
<TimelinePanel
|
||||
manageReadReceipts={false}
|
||||
manageReadMarkers={false}
|
||||
timelineSet={timelineSet}
|
||||
showUrlPreview={false}
|
||||
tileShape="notif"
|
||||
empty={emptyState}
|
||||
/>
|
||||
</div>
|
||||
content = (
|
||||
<TimelinePanel
|
||||
manageReadReceipts={false}
|
||||
manageReadMarkers={false}
|
||||
timelineSet={timelineSet}
|
||||
showUrlPreview={false}
|
||||
tileShape="notif"
|
||||
empty={emptyState}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
console.error("No notifTimelineSet available!");
|
||||
return (
|
||||
<div className="mx_NotificationPanel" role="tabpanel">
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
content = <Loader />;
|
||||
}
|
||||
|
||||
return <BaseCard className="mx_NotificationPanel" onClose={this.props.onClose} withoutScrollContainer>
|
||||
{ content }
|
||||
</BaseCard>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,8 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import * as sdk from '../../index';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import RateLimitedFunc from '../../ratelimitedfunc';
|
||||
|
@ -30,11 +32,14 @@ import {RightPanelPhases, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPa
|
|||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import {Action} from "../../dispatcher/actions";
|
||||
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
|
||||
import WidgetCard from "../views/right_panel/WidgetCard";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
|
||||
export default class RightPanel extends React.Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
roomId: PropTypes.string, // if showing panels for a given room, this is set
|
||||
room: PropTypes.instanceOf(Room), // if showing panels for a given room, this is set
|
||||
groupId: PropTypes.string, // if showing panels for a given group, this is set
|
||||
user: PropTypes.object, // used if we know the user ahead of opening the panel
|
||||
};
|
||||
|
@ -45,10 +50,10 @@ export default class RightPanel extends React.Component {
|
|||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
...RightPanelStore.getSharedInstance().roomPanelPhaseParams,
|
||||
phase: this._getPhaseFromProps(),
|
||||
isUserPrivilegedInGroup: null,
|
||||
member: this._getUserForPanel(),
|
||||
verificationRequest: RightPanelStore.getSharedInstance().roomPanelPhaseParams.verificationRequest,
|
||||
};
|
||||
this.onAction = this.onAction.bind(this);
|
||||
this.onRoomStateMember = this.onRoomStateMember.bind(this);
|
||||
|
@ -100,10 +105,6 @@ export default class RightPanel extends React.Component {
|
|||
}
|
||||
return RightPanelPhases.RoomMemberInfo;
|
||||
} else {
|
||||
if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.roomPanelPhase)) {
|
||||
dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.RoomMemberList});
|
||||
return RightPanelPhases.RoomMemberList;
|
||||
}
|
||||
return rps.roomPanelPhase;
|
||||
}
|
||||
}
|
||||
|
@ -161,13 +162,13 @@ export default class RightPanel extends React.Component {
|
|||
}
|
||||
|
||||
onRoomStateMember(ev, state, member) {
|
||||
if (member.roomId !== this.props.roomId) {
|
||||
if (member.roomId !== this.props.room.roomId) {
|
||||
return;
|
||||
}
|
||||
// redraw the badge on the membership list
|
||||
if (this.state.phase === RightPanelPhases.RoomMemberList && member.roomId === this.props.roomId) {
|
||||
if (this.state.phase === RightPanelPhases.RoomMemberList && member.roomId === this.props.room.roomId) {
|
||||
this._delayedUpdate();
|
||||
} else if (this.state.phase === RightPanelPhases.RoomMemberInfo && member.roomId === this.props.roomId &&
|
||||
} else if (this.state.phase === RightPanelPhases.RoomMemberInfo && member.roomId === this.props.room.roomId &&
|
||||
member.userId === this.state.member.userId) {
|
||||
// refresh the member info (e.g. new power level)
|
||||
this._delayedUpdate();
|
||||
|
@ -184,6 +185,7 @@ export default class RightPanel extends React.Component {
|
|||
event: payload.event,
|
||||
verificationRequest: payload.verificationRequest,
|
||||
verificationRequestPromise: payload.verificationRequestPromise,
|
||||
widgetId: payload.widgetId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -211,6 +213,14 @@ export default class RightPanel extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
onClose = () => {
|
||||
// the RightPanelStore has no way of knowing which mode room/group it is in, so we handle closing here
|
||||
defaultDispatcher.dispatch({
|
||||
action: Action.ToggleRightPanel,
|
||||
type: this.props.groupId ? "group" : "room",
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const MemberList = sdk.getComponent('rooms.MemberList');
|
||||
const UserInfo = sdk.getComponent('right_panel.UserInfo');
|
||||
|
@ -223,36 +233,42 @@ export default class RightPanel extends React.Component {
|
|||
const GroupRoomInfo = sdk.getComponent('groups.GroupRoomInfo');
|
||||
|
||||
let panel = <div />;
|
||||
const roomId = this.props.room ? this.props.room.roomId : undefined;
|
||||
|
||||
switch (this.state.phase) {
|
||||
case RightPanelPhases.RoomMemberList:
|
||||
if (this.props.roomId) {
|
||||
panel = <MemberList roomId={this.props.roomId} key={this.props.roomId} />;
|
||||
if (roomId) {
|
||||
panel = <MemberList roomId={roomId} key={roomId} onClose={this.onClose} />;
|
||||
}
|
||||
break;
|
||||
|
||||
case RightPanelPhases.GroupMemberList:
|
||||
if (this.props.groupId) {
|
||||
panel = <GroupMemberList groupId={this.props.groupId} key={this.props.groupId} />;
|
||||
}
|
||||
break;
|
||||
|
||||
case RightPanelPhases.GroupRoomList:
|
||||
panel = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />;
|
||||
break;
|
||||
|
||||
case RightPanelPhases.RoomMemberInfo:
|
||||
case RightPanelPhases.EncryptionPanel:
|
||||
panel = <UserInfo
|
||||
user={this.state.member}
|
||||
roomId={this.props.roomId}
|
||||
key={this.props.roomId || this.state.member.userId}
|
||||
room={this.props.room}
|
||||
key={roomId || this.state.member.userId}
|
||||
onClose={this.onCloseUserInfo}
|
||||
phase={this.state.phase}
|
||||
verificationRequest={this.state.verificationRequest}
|
||||
verificationRequestPromise={this.state.verificationRequestPromise}
|
||||
/>;
|
||||
break;
|
||||
|
||||
case RightPanelPhases.Room3pidMemberInfo:
|
||||
panel = <ThirdPartyMemberInfo event={this.state.event} key={this.props.roomId} />;
|
||||
panel = <ThirdPartyMemberInfo event={this.state.event} key={roomId} />;
|
||||
break;
|
||||
|
||||
case RightPanelPhases.GroupMemberInfo:
|
||||
panel = <UserInfo
|
||||
user={this.state.member}
|
||||
|
@ -260,17 +276,28 @@ export default class RightPanel extends React.Component {
|
|||
key={this.state.member.userId}
|
||||
onClose={this.onCloseUserInfo} />;
|
||||
break;
|
||||
|
||||
case RightPanelPhases.GroupRoomInfo:
|
||||
panel = <GroupRoomInfo
|
||||
groupRoomId={this.state.groupRoomId}
|
||||
groupId={this.props.groupId}
|
||||
key={this.state.groupRoomId} />;
|
||||
break;
|
||||
|
||||
case RightPanelPhases.NotificationPanel:
|
||||
panel = <NotificationPanel />;
|
||||
panel = <NotificationPanel onClose={this.onClose} />;
|
||||
break;
|
||||
|
||||
case RightPanelPhases.FilePanel:
|
||||
panel = <FilePanel roomId={this.props.roomId} resizeNotifier={this.props.resizeNotifier} />;
|
||||
panel = <FilePanel roomId={roomId} resizeNotifier={this.props.resizeNotifier} onClose={this.onClose} />;
|
||||
break;
|
||||
|
||||
case RightPanelPhases.RoomSummary:
|
||||
panel = <RoomSummaryCard room={this.props.room} onClose={this.onClose} />;
|
||||
break;
|
||||
|
||||
case RightPanelPhases.Widget:
|
||||
panel = <WidgetCard room={this.props.room} widgetId={this.state.widgetId} onClose={this.onClose} />;
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
@ -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],
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -94,7 +94,7 @@ export default class ScrollPanel extends React.Component {
|
|||
|
||||
/* startAtBottom: if set to true, the view is assumed to start
|
||||
* scrolled to the bottom.
|
||||
* XXX: It's likley this is unecessary and can be derived from
|
||||
* XXX: It's likely this is unnecessary and can be derived from
|
||||
* stickyBottom, but I'm adding an extra parameter to ensure
|
||||
* behaviour stays the same for other uses of ScrollPanel.
|
||||
* If so, let's remove this parameter down the line.
|
||||
|
@ -138,6 +138,7 @@ export default class ScrollPanel extends React.Component {
|
|||
/* style: styles to add to the top-level div
|
||||
*/
|
||||
style: PropTypes.object,
|
||||
|
||||
/* resizeNotifier: ResizeNotifier to know when middle column has changed size
|
||||
*/
|
||||
resizeNotifier: PropTypes.object,
|
||||
|
@ -162,7 +163,7 @@ export default class ScrollPanel extends React.Component {
|
|||
this._pendingFillRequests = {b: null, f: null};
|
||||
|
||||
if (this.props.resizeNotifier) {
|
||||
this.props.resizeNotifier.on("middlePanelResized", this.onResize);
|
||||
this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize);
|
||||
}
|
||||
|
||||
this.resetScrollState();
|
||||
|
@ -192,11 +193,13 @@ export default class ScrollPanel extends React.Component {
|
|||
this.unmounted = true;
|
||||
|
||||
if (this.props.resizeNotifier) {
|
||||
this.props.resizeNotifier.removeListener("middlePanelResized", this.onResize);
|
||||
this.props.resizeNotifier.removeListener("middlePanelResizedNoisy", this.onResize);
|
||||
}
|
||||
}
|
||||
|
||||
onScroll = ev => {
|
||||
// skip scroll events caused by resizing
|
||||
if (this.props.resizeNotifier && this.props.resizeNotifier.isResizing) return;
|
||||
debuglog("onScroll", this._getScrollNode().scrollTop);
|
||||
this._scrollTimeout.restart();
|
||||
this._saveScrollState();
|
||||
|
@ -206,6 +209,7 @@ export default class ScrollPanel extends React.Component {
|
|||
};
|
||||
|
||||
onResize = () => {
|
||||
debuglog("onResize");
|
||||
this.checkScroll();
|
||||
// update preventShrinkingState if present
|
||||
if (this.preventShrinkingState) {
|
||||
|
@ -235,7 +239,6 @@ export default class ScrollPanel extends React.Component {
|
|||
// when scrolled all the way down. E.g. Chrome 72 on debian.
|
||||
// so check difference <= 1;
|
||||
return Math.abs(sn.scrollHeight - (sn.scrollTop + sn.clientHeight)) <= 1;
|
||||
|
||||
};
|
||||
|
||||
// returns the vertical height in the given direction that can be removed from
|
||||
|
|
|
@ -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;
|
||||
|
@ -104,8 +105,8 @@ class TimelinePanel extends React.Component {
|
|||
// shape property to be passed to EventTiles
|
||||
tileShape: PropTypes.string,
|
||||
|
||||
// placeholder text to use if the timeline is empty
|
||||
empty: PropTypes.string,
|
||||
// placeholder to use if the timeline is empty
|
||||
empty: PropTypes.node,
|
||||
|
||||
// whether to show reactions for an event
|
||||
showReactions: PropTypes.bool,
|
||||
|
@ -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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ import dis from "../../dispatcher/dispatcher";
|
|||
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
||||
import ErrorDialog from "../views/dialogs/ErrorDialog";
|
||||
import EditCommunityPrototypeDialog from "../views/dialogs/EditCommunityPrototypeDialog";
|
||||
import {UIFeature} from "../../settings/UIFeature";
|
||||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
|
@ -285,6 +286,15 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
let feedbackButton;
|
||||
if (SettingsStore.getValue(UIFeature.Feedback)) {
|
||||
feedbackButton = <IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconMessage"
|
||||
label={_t("Feedback")}
|
||||
onClick={this.onProvideFeedback}
|
||||
/>;
|
||||
}
|
||||
|
||||
let primaryHeader = (
|
||||
<div className="mx_UserMenu_contextMenu_name">
|
||||
<span className="mx_UserMenu_contextMenu_displayName">
|
||||
|
@ -319,11 +329,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
label={_t("Archived rooms")}
|
||||
onClick={this.onShowArchived}
|
||||
/> */}
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconMessage"
|
||||
label={_t("Feedback")}
|
||||
onClick={this.onProvideFeedback}
|
||||
/>
|
||||
{ feedbackButton }
|
||||
</IconizedContextMenuOptionList>
|
||||
<IconizedContextMenuOptionList red>
|
||||
<IconizedContextMenuOption
|
||||
|
@ -384,11 +390,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
aria-label={_t("User settings")}
|
||||
onClick={(e) => this.onSettingsOpen(e, null)}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconMessage"
|
||||
label={_t("Feedback")}
|
||||
onClick={this.onProvideFeedback}
|
||||
/>
|
||||
{ feedbackButton }
|
||||
</IconizedContextMenuOptionList>
|
||||
<IconizedContextMenuOptionList red>
|
||||
<IconizedContextMenuOption
|
||||
|
|
|
@ -80,7 +80,9 @@ export default class UserView extends React.Component {
|
|||
const RightPanel = sdk.getComponent('structures.RightPanel');
|
||||
const MainSplit = sdk.getComponent('structures.MainSplit');
|
||||
const panel = <RightPanel user={this.state.member} />;
|
||||
return (<MainSplit panel={panel}><HomePage /></MainSplit>);
|
||||
return (<MainSplit panel={panel} resizeNotifier={this.props.resizeNotifier}>
|
||||
<HomePage />
|
||||
</MainSplit>);
|
||||
} else {
|
||||
return (<div />);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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]*$/;
|
||||
|
@ -124,7 +126,11 @@ export default class LoginComponent extends React.Component {
|
|||
'm.login.cas': () => this._renderSsoStep("cas"),
|
||||
'm.login.sso': () => this._renderSsoStep("sso"),
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillMount() {
|
||||
this._initLoginLogic();
|
||||
}
|
||||
|
||||
|
@ -675,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') }
|
||||
|
|
|
@ -25,6 +25,7 @@ import * as sdk from '../../../index';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Spinner from "../elements/Spinner";
|
||||
|
||||
/* This file contains a collection of components which are used by the
|
||||
* InteractiveAuth to prompt the user to enter the information needed
|
||||
|
@ -404,8 +405,12 @@ export class EmailIdentityAuthEntry extends React.Component {
|
|||
// the validation link, we won't know the email address, so if we don't have it,
|
||||
// assume that the link has been clicked and the server will realise when we poll.
|
||||
if (this.props.inputs.emailAddress === undefined) {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
return <Loader />;
|
||||
return <Spinner />;
|
||||
} else if (this.props.stageState?.emailSid) {
|
||||
// we only have a session ID if the user has clicked the link in their email,
|
||||
// so show a loading state instead of "an email has been sent to..." because
|
||||
// that's confusing when you've already read that email.
|
||||
return <Spinner />;
|
||||
} else {
|
||||
return (
|
||||
<div>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -16,23 +16,24 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
||||
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import BaseAvatar from "./BaseAvatar";
|
||||
|
||||
interface IProps {
|
||||
// TODO: replace with correct type
|
||||
member: any;
|
||||
fallbackUserId: string;
|
||||
member: RoomMember;
|
||||
fallbackUserId?: string;
|
||||
width: number;
|
||||
height: number;
|
||||
resizeMethod: string;
|
||||
resizeMethod?: string;
|
||||
// The onClick to give the avatar
|
||||
onClick: React.MouseEventHandler;
|
||||
onClick?: React.MouseEventHandler;
|
||||
// Whether the onClick of the avatar should be overriden to dispatch `Action.ViewUser`
|
||||
viewUserOnClick: boolean;
|
||||
title: string;
|
||||
viewUserOnClick?: boolean;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
|
|
@ -37,7 +37,7 @@ interface IOptionListProps {
|
|||
}
|
||||
|
||||
interface IOptionProps extends React.ComponentProps<typeof MenuItem> {
|
||||
iconClassName: string;
|
||||
iconClassName?: string;
|
||||
}
|
||||
|
||||
interface ICheckboxProps extends React.ComponentProps<typeof MenuItemCheckbox> {
|
||||
|
@ -92,7 +92,7 @@ export const IconizedContextMenuCheckbox: React.FC<ICheckboxProps> = ({
|
|||
|
||||
export const IconizedContextMenuOption: React.FC<IOptionProps> = ({label, iconClassName, ...props}) => {
|
||||
return <MenuItem {...props} label={label}>
|
||||
<span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} />
|
||||
{ iconClassName && <span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} /> }
|
||||
<span className="mx_IconizedContextMenu_label">{label}</span>
|
||||
</MenuItem>;
|
||||
};
|
||||
|
|
|
@ -26,6 +26,9 @@ export default class WidgetContextMenu extends React.Component {
|
|||
// Callback for when the revoke button is clicked. Required.
|
||||
onRevokeClicked: PropTypes.func.isRequired,
|
||||
|
||||
// Callback for when the unpin button is clicked. If absent, unpin will be hidden.
|
||||
onUnpinClicked: PropTypes.func,
|
||||
|
||||
// Callback for when the snapshot button is clicked. Button not shown
|
||||
// without a callback.
|
||||
onSnapshotClicked: PropTypes.func,
|
||||
|
@ -70,6 +73,8 @@ export default class WidgetContextMenu extends React.Component {
|
|||
this.proxyClick(this.props.onRevokeClicked);
|
||||
};
|
||||
|
||||
onUnpinClicked = () => this.proxyClick(this.props.onUnpinClicked);
|
||||
|
||||
render() {
|
||||
const options = [];
|
||||
|
||||
|
@ -81,6 +86,14 @@ export default class WidgetContextMenu extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
if (this.props.onUnpinClicked) {
|
||||
options.push(
|
||||
<MenuItem className="mx_WidgetContextMenu_option" onClick={this.onUnpinClicked} key="unpin">
|
||||
{_t("Unpin")}
|
||||
</MenuItem>,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.onReloadClicked) {
|
||||
options.push(
|
||||
<MenuItem className='mx_WidgetContextMenu_option' onClick={this.onReloadClicked} key='reload'>
|
||||
|
|
|
@ -34,7 +34,7 @@ export default class BugReportDialog extends React.Component {
|
|||
busy: false,
|
||||
err: null,
|
||||
issueUrl: "",
|
||||
text: "",
|
||||
text: props.initialText || "",
|
||||
progress: null,
|
||||
downloadBusy: false,
|
||||
downloadProgress: null,
|
||||
|
@ -255,4 +255,5 @@ export default class BugReportDialog extends React.Component {
|
|||
|
||||
BugReportDialog.propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
initialText: PropTypes.string,
|
||||
};
|
||||
|
|
|
@ -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 can’t disable this later. Bridges & most bots won’t work yet.");
|
||||
if (this.state.canChangeEncryption) {
|
||||
microcopy = _t("You can’t disable this later. Bridges & most bots won’t 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>;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ export default ({onFinished}) => {
|
|||
|
||||
const categories = {};
|
||||
Commands.forEach(cmd => {
|
||||
if (!cmd.isEnabled()) return;
|
||||
if (!categories[cmd.category]) {
|
||||
categories[cmd.category] = [];
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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 = {
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -21,7 +21,7 @@ import * as sdk from '../../../../index';
|
|||
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import { accessSecretStorage } from '../../../../CrossSigningManager';
|
||||
import { accessSecretStorage } from '../../../../SecurityManager';
|
||||
|
||||
const RESTORE_TYPE_PASSPHRASE = 0;
|
||||
const RESTORE_TYPE_RECOVERYKEY = 1;
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -42,6 +42,8 @@ import {WidgetType} from "../../../widgets/WidgetType";
|
|||
import {Capability} from "../../../widgets/WidgetApi";
|
||||
import {sleep} from "../../../utils/promise";
|
||||
import {SettingLevel} from "../../../settings/SettingLevel";
|
||||
import WidgetStore from "../../../stores/WidgetStore";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
|
||||
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
||||
const ENABLE_REACT_PERF = false;
|
||||
|
@ -100,6 +102,8 @@ export default class AppTile extends React.Component {
|
|||
_getNewState(newProps) {
|
||||
// This is a function to make the impact of calling SettingsStore slightly less
|
||||
const hasPermissionToLoad = () => {
|
||||
if (this._usingLocalWidget()) return true;
|
||||
|
||||
const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", newProps.room.roomId);
|
||||
return !!currentlyAllowedWidgets[newProps.app.eventId];
|
||||
};
|
||||
|
@ -310,35 +314,12 @@ export default class AppTile extends React.Component {
|
|||
if (this.props.onEditClick) {
|
||||
this.props.onEditClick();
|
||||
} else {
|
||||
// TODO: Open the right manager for the widget
|
||||
if (SettingsStore.getValue("feature_many_integration_managers")) {
|
||||
IntegrationManagers.sharedInstance().openAll(
|
||||
this.props.room,
|
||||
'type_' + this.props.app.type,
|
||||
this.props.app.id,
|
||||
);
|
||||
} else {
|
||||
IntegrationManagers.sharedInstance().getPrimaryManager().open(
|
||||
this.props.room,
|
||||
'type_' + this.props.app.type,
|
||||
this.props.app.id,
|
||||
);
|
||||
}
|
||||
WidgetUtils.editWidget(this.props.room, this.props.app);
|
||||
}
|
||||
}
|
||||
|
||||
_onSnapshotClick() {
|
||||
console.log("Requesting widget snapshot");
|
||||
ActiveWidgetStore.getWidgetMessaging(this.props.app.id).getScreenshot()
|
||||
.catch((err) => {
|
||||
console.error("Failed to get screenshot", err);
|
||||
})
|
||||
.then((screenshot) => {
|
||||
dis.dispatch({
|
||||
action: 'picture_snapshot',
|
||||
file: screenshot,
|
||||
}, true);
|
||||
});
|
||||
WidgetUtils.snapshotWidget(this.props.app);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -419,6 +400,10 @@ export default class AppTile extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_onUnpinClicked = () => {
|
||||
WidgetStore.instance.unpinWidget(this.props.app.id);
|
||||
}
|
||||
|
||||
_onRevokeClicked() {
|
||||
console.info("Revoke widget permissions - %s", this.props.app.id);
|
||||
this._revokeWidgetPermission();
|
||||
|
@ -490,12 +475,20 @@ export default class AppTile extends React.Component {
|
|||
if (payload.widgetId === this.props.app.id) {
|
||||
switch (payload.action) {
|
||||
case 'm.sticker':
|
||||
if (this._hasCapability('m.sticker')) {
|
||||
dis.dispatch({action: 'post_sticker_message', data: payload.data});
|
||||
} else {
|
||||
console.warn('Ignoring sticker message. Invalid capability');
|
||||
}
|
||||
break;
|
||||
if (this._hasCapability('m.sticker')) {
|
||||
dis.dispatch({action: 'post_sticker_message', data: payload.data});
|
||||
} else {
|
||||
console.warn('Ignoring sticker message. Invalid capability');
|
||||
}
|
||||
break;
|
||||
|
||||
case Action.AppTileDelete:
|
||||
this._onDeleteClick();
|
||||
break;
|
||||
|
||||
case Action.AppTileRevoke:
|
||||
this._onRevokeClicked();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -613,6 +606,15 @@ export default class AppTile extends React.Component {
|
|||
return uriFromTemplate(u, vars);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether we're using a local version of the widget rather than loading the
|
||||
* actual widget URL
|
||||
* @returns {bool} true If using a local version of the widget
|
||||
*/
|
||||
_usingLocalWidget() {
|
||||
return WidgetType.JITSI.matches(this.props.app.type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL used in the iframe
|
||||
* In cases where we supply our own UI for a widget, this is an internal
|
||||
|
@ -626,7 +628,10 @@ export default class AppTile extends React.Component {
|
|||
|
||||
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
||||
console.log("Replacing Jitsi widget URL with local wrapper");
|
||||
url = WidgetUtils.getLocalJitsiWrapperUrl({forLocalRender: true});
|
||||
url = WidgetUtils.getLocalJitsiWrapperUrl({
|
||||
forLocalRender: true,
|
||||
auth: this.props.app.data ? this.props.app.data.auth : null,
|
||||
});
|
||||
url = this._addWurlParams(url);
|
||||
} else {
|
||||
url = this._getSafeUrl(this.state.widgetUrl);
|
||||
|
@ -637,7 +642,10 @@ export default class AppTile extends React.Component {
|
|||
_getPopoutUrl() {
|
||||
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
||||
return this._templatedUrl(
|
||||
WidgetUtils.getLocalJitsiWrapperUrl({forLocalRender: false}),
|
||||
WidgetUtils.getLocalJitsiWrapperUrl({
|
||||
forLocalRender: false,
|
||||
auth: this.props.app.data ? this.props.app.data.auth : null,
|
||||
}),
|
||||
this.props.app.type,
|
||||
);
|
||||
} else {
|
||||
|
@ -804,14 +812,16 @@ export default class AppTile extends React.Component {
|
|||
const showMinimiseButton = this.props.showMinimise && this.props.show;
|
||||
const showMaximiseButton = this.props.showMinimise && !this.props.show;
|
||||
|
||||
let appTileClass;
|
||||
let appTileClasses;
|
||||
if (this.props.miniMode) {
|
||||
appTileClass = 'mx_AppTile_mini';
|
||||
appTileClasses = {mx_AppTile_mini: true};
|
||||
} else if (this.props.fullWidth) {
|
||||
appTileClass = 'mx_AppTileFullWidth';
|
||||
appTileClasses = {mx_AppTileFullWidth: true};
|
||||
} else {
|
||||
appTileClass = 'mx_AppTile';
|
||||
appTileClasses = {mx_AppTile: true};
|
||||
}
|
||||
appTileClasses.mx_AppTile_minimised = !this.props.show;
|
||||
appTileClasses = classNames(appTileClasses);
|
||||
|
||||
const menuBarClasses = classNames({
|
||||
mx_AppTileMenuBar: true,
|
||||
|
@ -831,6 +841,9 @@ export default class AppTile extends React.Component {
|
|||
contextMenu = (
|
||||
<ContextMenu {...aboveLeftOf(elementRect, null)} onFinished={this._closeContextMenu}>
|
||||
<WidgetContextMenu
|
||||
onUnpinClicked={
|
||||
ActiveWidgetStore.getWidgetPersistence(this.props.app.id) ? null : this._onUnpinClicked
|
||||
}
|
||||
onRevokeClicked={this._onRevokeClicked}
|
||||
onEditClicked={showEditButton ? this._onEditClick : undefined}
|
||||
onDeleteClicked={showDeleteButton ? this._onDeleteClick : undefined}
|
||||
|
@ -843,20 +856,20 @@ export default class AppTile extends React.Component {
|
|||
}
|
||||
|
||||
return <React.Fragment>
|
||||
<div className={appTileClass} id={this.props.app.id}>
|
||||
<div className={appTileClasses} id={this.props.app.id}>
|
||||
{ this.props.showMenubar &&
|
||||
<div ref={this._menu_bar} className={menuBarClasses} onClick={this.onClickMenuBar}>
|
||||
<span className="mx_AppTileMenuBarTitle" style={{pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false)}}>
|
||||
{ /* 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 */ }
|
||||
|
|
|
@ -20,6 +20,7 @@ import { _t } from '../../../languageHandler';
|
|||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import PlatformPeg from '../../../PlatformPeg';
|
||||
import Modal from '../../../Modal';
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
|
||||
/**
|
||||
* This error boundary component can be used to wrap large content areas and
|
||||
|
@ -73,9 +74,10 @@ export default class ErrorBoundary extends React.PureComponent {
|
|||
if (this.state.error) {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const newIssueUrl = "https://github.com/vector-im/element-web/issues/new";
|
||||
return <div className="mx_ErrorBoundary">
|
||||
<div className="mx_ErrorBoundary_body">
|
||||
<h1>{_t("Something went wrong!")}</h1>
|
||||
|
||||
let bugReportSection;
|
||||
if (SdkConfig.get().bug_report_endpoint_url) {
|
||||
bugReportSection = <React.Fragment>
|
||||
<p>{_t(
|
||||
"Please <newIssueLink>create a new issue</newIssueLink> " +
|
||||
"on GitHub so that we can investigate this bug.", {}, {
|
||||
|
@ -94,6 +96,13 @@ export default class ErrorBoundary extends React.PureComponent {
|
|||
<AccessibleButton onClick={this._onBugReport} kind='primary'>
|
||||
{_t("Submit debug logs")}
|
||||
</AccessibleButton>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
return <div className="mx_ErrorBoundary">
|
||||
<div className="mx_ErrorBoundary_body">
|
||||
<h1>{_t("Something went wrong!")}</h1>
|
||||
{ bugReportSection }
|
||||
<AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'>
|
||||
{_t("Clear cache and reload")}
|
||||
</AccessibleButton>
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2019 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 { _t } from '../../../languageHandler';
|
||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleTooltipButton from "./AccessibleTooltipButton";
|
||||
|
||||
export default class ManageIntegsButton extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
onManageIntegrations = (ev) => {
|
||||
ev.preventDefault();
|
||||
|
||||
const managers = IntegrationManagers.sharedInstance();
|
||||
if (!managers.hasManager()) {
|
||||
managers.openNoManagerDialog();
|
||||
} else {
|
||||
if (SettingsStore.getValue("feature_many_integration_managers")) {
|
||||
managers.openAll(this.props.room);
|
||||
} else {
|
||||
managers.getPrimaryManager().open(this.props.room);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
let integrationsButton = <div />;
|
||||
if (IntegrationManagers.sharedInstance().hasManager()) {
|
||||
integrationsButton = (
|
||||
<AccessibleTooltipButton
|
||||
className='mx_RoomHeader_button mx_RoomHeader_manageIntegsButton'
|
||||
title={_t("Manage Integrations")}
|
||||
onClick={this.onManageIntegrations}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return integrationsButton;
|
||||
}
|
||||
}
|
||||
|
||||
ManageIntegsButton.propTypes = {
|
||||
room: PropTypes.object.isRequired,
|
||||
};
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {throttle} from "lodash";
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
|
@ -156,7 +156,7 @@ export default class PersistedElement extends React.Component {
|
|||
child.style.display = visible ? 'block' : 'none';
|
||||
}
|
||||
|
||||
updateChildPosition(child, parent) {
|
||||
updateChildPosition = throttle((child, parent) => {
|
||||
if (!child || !parent) return;
|
||||
|
||||
const parentRect = parent.getBoundingClientRect();
|
||||
|
@ -167,9 +167,9 @@ export default class PersistedElement extends React.Component {
|
|||
width: parentRect.width + 'px',
|
||||
height: parentRect.height + 'px',
|
||||
});
|
||||
}
|
||||
}, 100, {trailing: true, leading: true});
|
||||
|
||||
render() {
|
||||
return <div ref={this.collectChildContainer}></div>;
|
||||
return <div ref={this.collectChildContainer} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -76,7 +76,7 @@ export default class PersistentApp extends React.Component {
|
|||
userId={MatrixClientPeg.get().credentials.userId}
|
||||
show={true}
|
||||
creatorUserId={app.creatorUserId}
|
||||
widgetPageTitle={(app.data && app.data.title) ? app.data.title : ''}
|
||||
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
|
||||
waitForIframeLoad={app.waitForIframeLoad}
|
||||
whitelistCapabilities={capWhitelist}
|
||||
showDelete={false}
|
||||
|
|
|
@ -29,7 +29,7 @@ import {Action} from "../../../dispatcher/actions";
|
|||
|
||||
// For URLs of matrix.to links in the timeline which have been reformatted by
|
||||
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
|
||||
const REGEX_LOCAL_PERMALINK = /^#\/(?:user|room|group)\/(([#!@+])[^/]*)$/;
|
||||
const REGEX_LOCAL_PERMALINK = /^#\/(?:user|room|group)\/(([#!@+]).*?)(?=\/|\?|$)/;
|
||||
|
||||
class Pill extends React.Component {
|
||||
static isPillUrl(url) {
|
||||
|
|
|
@ -57,11 +57,14 @@ export default class PowerSelector extends React.Component {
|
|||
customValue: this.props.value,
|
||||
selectValue: 0,
|
||||
};
|
||||
|
||||
this._initStateFromProps(this.props);
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillMount() {
|
||||
this._initStateFromProps(this.props);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
this._initStateFromProps(newProps);
|
||||
|
|
|
@ -28,6 +28,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
|||
import { getUserNameColorClass } from "../../../utils/FormattingUtils";
|
||||
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
|
||||
|
@ -361,6 +362,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>;
|
||||
});
|
||||
|
|
|
@ -19,6 +19,7 @@ import classNames from 'classnames';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
|
||||
export default class TileErrorBoundary extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -54,14 +55,20 @@ export default class TileErrorBoundary extends React.Component {
|
|||
mx_EventTile_content: true,
|
||||
mx_EventTile_tileError: true,
|
||||
};
|
||||
|
||||
let submitLogsButton;
|
||||
if (SdkConfig.get().bug_report_endpoint_url) {
|
||||
submitLogsButton = <a onClick={this._onBugReport} href="#">
|
||||
{_t("Submit logs")}
|
||||
</a>;
|
||||
}
|
||||
|
||||
return (<div className={classNames(classes)}>
|
||||
<div className="mx_EventTile_line">
|
||||
<span>
|
||||
{_t("Can't load this message")}
|
||||
{ mxEvent && ` (${mxEvent.getType()})` }
|
||||
<a onClick={this._onBugReport} href="#">
|
||||
{_t("Submit logs")}
|
||||
</a>
|
||||
{ submitLogsButton }
|
||||
</span>
|
||||
</div>
|
||||
</div>);
|
||||
|
|
93
src/components/views/right_panel/BaseCard.tsx
Normal file
93
src/components/views/right_panel/BaseCard.tsx
Normal file
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
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, {ReactNode} from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
|
||||
interface IProps {
|
||||
header?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
className?: string;
|
||||
withoutScrollContainer?: boolean;
|
||||
previousPhase?: RightPanelPhases;
|
||||
onClose?(): void;
|
||||
}
|
||||
|
||||
interface IGroupProps {
|
||||
className?: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const Group: React.FC<IGroupProps> = ({ className, title, children }) => {
|
||||
return <div className={classNames("mx_BaseCard_Group", className)}>
|
||||
<h1>{title}</h1>
|
||||
{children}
|
||||
</div>;
|
||||
};
|
||||
|
||||
const BaseCard: React.FC<IProps> = ({
|
||||
onClose,
|
||||
className,
|
||||
header,
|
||||
footer,
|
||||
withoutScrollContainer,
|
||||
previousPhase,
|
||||
children,
|
||||
}) => {
|
||||
let backButton;
|
||||
if (previousPhase) {
|
||||
const onBackClick = () => {
|
||||
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: previousPhase,
|
||||
});
|
||||
};
|
||||
backButton = <AccessibleButton className="mx_BaseCard_back" onClick={onBackClick} title={_t("Back")} />;
|
||||
}
|
||||
|
||||
let closeButton;
|
||||
if (onClose) {
|
||||
closeButton = <AccessibleButton className="mx_BaseCard_close" onClick={onClose} title={_t("Close")} />;
|
||||
}
|
||||
|
||||
if (!withoutScrollContainer) {
|
||||
children = <AutoHideScrollbar>
|
||||
{ children }
|
||||
</AutoHideScrollbar>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames("mx_BaseCard", className)}>
|
||||
<div className="mx_BaseCard_header">
|
||||
{ backButton }
|
||||
{ closeButton }
|
||||
{ header }
|
||||
</div>
|
||||
{ children }
|
||||
{ footer && <div className="mx_BaseCard_footer">{ footer }</div> }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BaseCard;
|
|
@ -41,13 +41,9 @@ interface IProps {}
|
|||
export default class GroupHeaderButtons extends HeaderButtons {
|
||||
constructor(props: IProps) {
|
||||
super(props, HeaderKind.Group);
|
||||
this.onMembersClicked = this.onMembersClicked.bind(this);
|
||||
this.onRoomsClicked = this.onRoomsClicked.bind(this);
|
||||
}
|
||||
|
||||
protected onAction(payload: ActionPayload) {
|
||||
super.onAction(payload);
|
||||
|
||||
if (payload.action === Action.ViewUser) {
|
||||
if ((payload as ViewUserPayload).member) {
|
||||
this.setPhase(RightPanelPhases.RoomMemberInfo, {member: payload.member});
|
||||
|
@ -70,7 +66,7 @@ export default class GroupHeaderButtons extends HeaderButtons {
|
|||
}
|
||||
}
|
||||
|
||||
private onMembersClicked() {
|
||||
private onMembersClicked = () => {
|
||||
if (this.state.phase === RightPanelPhases.GroupMemberInfo) {
|
||||
// send the active phase to trigger a toggle
|
||||
this.setPhase(RightPanelPhases.GroupMemberInfo);
|
||||
|
@ -78,12 +74,12 @@ export default class GroupHeaderButtons extends HeaderButtons {
|
|||
// This toggles for us, if needed
|
||||
this.setPhase(RightPanelPhases.GroupMemberList);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private onRoomsClicked() {
|
||||
private onRoomsClicked = () => {
|
||||
// This toggles for us, if needed
|
||||
this.setPhase(RightPanelPhases.GroupRoomList);
|
||||
}
|
||||
};
|
||||
|
||||
renderButtons() {
|
||||
return [
|
||||
|
|
|
@ -65,9 +65,7 @@ export default abstract class HeaderButtons extends React.Component<IProps, ISta
|
|||
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
protected onAction(payload) {
|
||||
// Ignore - intended to be overridden by subclasses
|
||||
}
|
||||
protected abstract onAction(payload);
|
||||
|
||||
public setPhase(phase: RightPanelPhases, extras?: Partial<SetRightPanelPhaseRefireParams>) {
|
||||
dis.dispatch<SetRightPanelPhasePayload>({
|
||||
|
@ -98,8 +96,7 @@ export default abstract class HeaderButtons extends React.Component<IProps, ISta
|
|||
public abstract renderButtons(): JSX.Element[];
|
||||
|
||||
public render() {
|
||||
// inline style as this will be swapped around in future commits
|
||||
return <div className="mx_HeaderButtons" role="tablist">
|
||||
return <div className="mx_HeaderButtons">
|
||||
{this.renderButtons()}
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -19,14 +19,18 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {_t} from '../../../languageHandler';
|
||||
import HeaderButton from './HeaderButton';
|
||||
import HeaderButtons, {HeaderKind} from './HeaderButtons';
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {ActionPayload} from "../../../dispatcher/payloads";
|
||||
import RightPanelStore from "../../../stores/RightPanelStore";
|
||||
|
||||
const MEMBER_PHASES = [
|
||||
const ROOM_INFO_PHASES = [
|
||||
RightPanelPhases.RoomSummary,
|
||||
RightPanelPhases.Widget,
|
||||
RightPanelPhases.FilePanel,
|
||||
RightPanelPhases.RoomMemberList,
|
||||
RightPanelPhases.RoomMemberInfo,
|
||||
RightPanelPhases.EncryptionPanel,
|
||||
|
@ -36,13 +40,9 @@ const MEMBER_PHASES = [
|
|||
export default class RoomHeaderButtons extends HeaderButtons {
|
||||
constructor(props) {
|
||||
super(props, HeaderKind.Room);
|
||||
this.onMembersClicked = this.onMembersClicked.bind(this);
|
||||
this.onFilesClicked = this.onFilesClicked.bind(this);
|
||||
this.onNotificationsClicked = this.onNotificationsClicked.bind(this);
|
||||
}
|
||||
|
||||
protected onAction(payload: ActionPayload) {
|
||||
super.onAction(payload);
|
||||
if (payload.action === Action.ViewUser) {
|
||||
if (payload.member) {
|
||||
this.setPhase(RightPanelPhases.RoomMemberInfo, {member: payload.member});
|
||||
|
@ -58,47 +58,44 @@ export default class RoomHeaderButtons extends HeaderButtons {
|
|||
}
|
||||
}
|
||||
|
||||
private onMembersClicked() {
|
||||
if (this.state.phase === RightPanelPhases.RoomMemberInfo) {
|
||||
// send the active phase to trigger a toggle
|
||||
// XXX: we should pass refireParams here but then it won't collapse as we desire it to
|
||||
this.setPhase(RightPanelPhases.RoomMemberInfo);
|
||||
private onRoomSummaryClicked = () => {
|
||||
// use roomPanelPhase rather than this.state.phase as it remembers the latest one if we close
|
||||
const lastPhase = RightPanelStore.getSharedInstance().roomPanelPhase;
|
||||
if (ROOM_INFO_PHASES.includes(lastPhase)) {
|
||||
if (this.state.phase === lastPhase) {
|
||||
this.setPhase(lastPhase);
|
||||
} else {
|
||||
this.setPhase(lastPhase, RightPanelStore.getSharedInstance().roomPanelPhaseParams);
|
||||
}
|
||||
} else {
|
||||
// This toggles for us, if needed
|
||||
this.setPhase(RightPanelPhases.RoomMemberList);
|
||||
this.setPhase(RightPanelPhases.RoomSummary);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private onFilesClicked() {
|
||||
// This toggles for us, if needed
|
||||
this.setPhase(RightPanelPhases.FilePanel);
|
||||
}
|
||||
|
||||
private onNotificationsClicked() {
|
||||
private onNotificationsClicked = () => {
|
||||
// This toggles for us, if needed
|
||||
this.setPhase(RightPanelPhases.NotificationPanel);
|
||||
}
|
||||
};
|
||||
|
||||
public renderButtons() {
|
||||
return [
|
||||
<HeaderButton key="membersButton" name="membersButton"
|
||||
title={_t('Members')}
|
||||
isHighlighted={this.isPhase(MEMBER_PHASES)}
|
||||
onClick={this.onMembersClicked}
|
||||
analytics={['Right Panel', 'Member List Button', 'click']}
|
||||
/>,
|
||||
<HeaderButton key="filesButton" name="filesButton"
|
||||
title={_t('Files')}
|
||||
isHighlighted={this.isPhase(RightPanelPhases.FilePanel)}
|
||||
onClick={this.onFilesClicked}
|
||||
analytics={['Right Panel', 'File List Button', 'click']}
|
||||
/>,
|
||||
<HeaderButton key="notifsButton" name="notifsButton"
|
||||
<HeaderButton
|
||||
key="notifsButton"
|
||||
name="notifsButton"
|
||||
title={_t('Notifications')}
|
||||
isHighlighted={this.isPhase(RightPanelPhases.NotificationPanel)}
|
||||
onClick={this.onNotificationsClicked}
|
||||
analytics={['Right Panel', 'Notification List Button', 'click']}
|
||||
/>,
|
||||
<HeaderButton
|
||||
key="roomSummaryButton"
|
||||
name="roomSummaryButton"
|
||||
title={_t('Room Info')}
|
||||
isHighlighted={this.isPhase(ROOM_INFO_PHASES)}
|
||||
onClick={this.onRoomSummaryClicked}
|
||||
analytics={['Right Panel', 'Room Summary Button', 'click']}
|
||||
/>,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
250
src/components/views/right_panel/RoomSummaryCard.tsx
Normal file
250
src/components/views/right_panel/RoomSummaryCard.tsx
Normal file
|
@ -0,0 +1,250 @@
|
|||
/*
|
||||
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, {useCallback, useState, useEffect, useContext} from "react";
|
||||
import classNames from "classnames";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
||||
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { useIsEncrypted } from '../../../hooks/useIsEncrypted';
|
||||
import BaseCard, { Group } from "./BaseCard";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
import Modal from "../../../Modal";
|
||||
import ShareDialog from '../dialogs/ShareDialog';
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
|
||||
import WidgetUtils from "../../../utils/WidgetUtils";
|
||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import TextWithTooltip from "../elements/TextWithTooltip";
|
||||
import BaseAvatar from "../avatars/BaseAvatar";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import WidgetStore, {IApp} from "../../../stores/WidgetStore";
|
||||
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
onClose(): void;
|
||||
}
|
||||
|
||||
interface IAppsSectionProps {
|
||||
room: Room;
|
||||
}
|
||||
|
||||
interface IButtonProps {
|
||||
className: string;
|
||||
onClick(): void;
|
||||
}
|
||||
|
||||
const Button: React.FC<IButtonProps> = ({ children, className, onClick }) => {
|
||||
return <AccessibleButton
|
||||
className={classNames("mx_BaseCard_Button mx_RoomSummaryCard_Button", className)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{ children }
|
||||
</AccessibleButton>;
|
||||
};
|
||||
|
||||
export const useWidgets = (room: Room) => {
|
||||
const [apps, setApps] = useState<IApp[]>(WidgetStore.instance.getApps(room));
|
||||
|
||||
const updateApps = useCallback(() => {
|
||||
// Copy the array so that we always trigger a re-render, as some updates mutate the array of apps/settings
|
||||
setApps([...WidgetStore.instance.getApps(room)]);
|
||||
}, [room]);
|
||||
|
||||
useEffect(updateApps, [room]);
|
||||
useEventEmitter(WidgetEchoStore, "update", updateApps);
|
||||
useEventEmitter(WidgetStore.instance, room.roomId, updateApps);
|
||||
|
||||
return apps;
|
||||
};
|
||||
|
||||
const AppsSection: React.FC<IAppsSectionProps> = ({ room }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const apps = useWidgets(room);
|
||||
|
||||
const onManageIntegrations = () => {
|
||||
const managers = IntegrationManagers.sharedInstance();
|
||||
if (!managers.hasManager()) {
|
||||
managers.openNoManagerDialog();
|
||||
} else {
|
||||
if (SettingsStore.getValue("feature_many_integration_managers")) {
|
||||
managers.openAll(room);
|
||||
} else {
|
||||
managers.getPrimaryManager().open(room);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return <Group className="mx_RoomSummaryCard_appsGroup" title={_t("Widgets")}>
|
||||
{ apps.map(app => {
|
||||
const name = WidgetUtils.getWidgetName(app);
|
||||
const dataTitle = WidgetUtils.getWidgetDataTitle(app);
|
||||
const subtitle = dataTitle && " - " + dataTitle;
|
||||
|
||||
let iconUrls = [require("../../../../res/img/element-icons/room/default_app.svg")];
|
||||
// heuristics for some better icons until Widgets support their own icons
|
||||
if (app.type.includes("meeting") || app.type.includes("calendar")) {
|
||||
iconUrls = [require("../../../../res/img/element-icons/room/default_cal.svg")];
|
||||
} else if (app.type.includes("pad") || app.type.includes("doc") || app.type.includes("calc")) {
|
||||
iconUrls = [require("../../../../res/img/element-icons/room/default_doc.svg")];
|
||||
} else if (app.type.includes("clock")) {
|
||||
iconUrls = [require("../../../../res/img/element-icons/room/default_clock.svg")];
|
||||
}
|
||||
|
||||
if (app.avatar_url) { // MSC2765
|
||||
iconUrls.unshift(getHttpUriForMxc(cli.getHomeserverUrl(), app.avatar_url, 20, 20, "crop"));
|
||||
}
|
||||
|
||||
const isPinned = WidgetStore.instance.isPinned(app.id);
|
||||
const classes = classNames("mx_RoomSummaryCard_icon_app", {
|
||||
mx_RoomSummaryCard_icon_app_pinned: isPinned,
|
||||
});
|
||||
|
||||
if (isPinned) {
|
||||
const onClick = () => {
|
||||
WidgetStore.instance.unpinWidget(app.id);
|
||||
};
|
||||
|
||||
return <AccessibleTooltipButton
|
||||
key={app.id}
|
||||
className={classNames("mx_BaseCard_Button mx_RoomSummaryCard_Button", classes)}
|
||||
onClick={onClick}
|
||||
title={_t("Unpin app")}
|
||||
>
|
||||
<BaseAvatar name={app.id} urls={iconUrls} width={20} height={20} />
|
||||
<span>{name}</span>
|
||||
{ subtitle }
|
||||
</AccessibleTooltipButton>
|
||||
}
|
||||
|
||||
const onOpenWidgetClick = () => {
|
||||
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: RightPanelPhases.Widget,
|
||||
refireParams: {
|
||||
widgetId: app.id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Button key={app.id} className={classes} onClick={onOpenWidgetClick}>
|
||||
<BaseAvatar name={app.id} urls={iconUrls} width={20} height={20} />
|
||||
<span>{name}</span>
|
||||
{ subtitle }
|
||||
</Button>
|
||||
);
|
||||
}) }
|
||||
|
||||
<AccessibleButton kind="link" onClick={onManageIntegrations}>
|
||||
{ apps.length > 0 ? _t("Edit widgets, bridges & bots") : _t("Add widgets, bridges & bots") }
|
||||
</AccessibleButton>
|
||||
</Group>;
|
||||
};
|
||||
|
||||
const onRoomMembersClick = () => {
|
||||
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: RightPanelPhases.RoomMemberList,
|
||||
});
|
||||
};
|
||||
|
||||
const onRoomFilesClick = () => {
|
||||
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: RightPanelPhases.FilePanel,
|
||||
});
|
||||
};
|
||||
|
||||
const onRoomSettingsClick = () => {
|
||||
defaultDispatcher.dispatch({ action: "open_room_settings" });
|
||||
};
|
||||
|
||||
const useMemberCount = (room: Room) => {
|
||||
const [count, setCount] = useState(room.getJoinedMembers().length);
|
||||
useEventEmitter(room.currentState, "RoomState.members", () => {
|
||||
setCount(room.getJoinedMembers().length);
|
||||
});
|
||||
return count;
|
||||
};
|
||||
|
||||
const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
const onShareRoomClick = () => {
|
||||
Modal.createTrackedDialog('share room dialog', '', ShareDialog, {
|
||||
target: room,
|
||||
});
|
||||
};
|
||||
|
||||
const isRoomEncrypted = useIsEncrypted(cli, room);
|
||||
const roomContext = useContext(RoomContext);
|
||||
const e2eStatus = roomContext.e2eStatus;
|
||||
|
||||
const alias = room.getCanonicalAlias() || room.getAltAliases()[0] || "";
|
||||
const header = <React.Fragment>
|
||||
<div className="mx_RoomSummaryCard_avatar" role="presentation">
|
||||
<RoomAvatar room={room} height={54} width={54} viewAvatarOnClick />
|
||||
<TextWithTooltip
|
||||
tooltip={isRoomEncrypted ? _t("Encrypted") : _t("Not encrypted")}
|
||||
class={classNames("mx_RoomSummaryCard_e2ee", {
|
||||
mx_RoomSummaryCard_e2ee_normal: isRoomEncrypted,
|
||||
mx_RoomSummaryCard_e2ee_warning: isRoomEncrypted && e2eStatus === E2EStatus.Warning,
|
||||
mx_RoomSummaryCard_e2ee_verified: isRoomEncrypted && e2eStatus === E2EStatus.Verified,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h2 title={room.name}>{ room.name }</h2>
|
||||
<div className="mx_RoomSummaryCard_alias" title={alias}>
|
||||
{ alias }
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
|
||||
const memberCount = useMemberCount(room);
|
||||
|
||||
return <BaseCard header={header} className="mx_RoomSummaryCard" onClose={onClose}>
|
||||
<Group title={_t("About")} className="mx_RoomSummaryCard_aboutGroup">
|
||||
<Button className="mx_RoomSummaryCard_icon_people" onClick={onRoomMembersClick}>
|
||||
{_t("%(count)s people", { count: memberCount })}
|
||||
</Button>
|
||||
<Button className="mx_RoomSummaryCard_icon_files" onClick={onRoomFilesClick}>
|
||||
{_t("Show files")}
|
||||
</Button>
|
||||
<Button className="mx_RoomSummaryCard_icon_share" onClick={onShareRoomClick}>
|
||||
{_t("Share room")}
|
||||
</Button>
|
||||
<Button className="mx_RoomSummaryCard_icon_settings" onClick={onRoomSettingsClick}>
|
||||
{_t("Room settings")}
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{ SettingsStore.getValue(UIFeature.Widgets) && <AppsSection room={room} /> }
|
||||
</BaseCard>;
|
||||
};
|
||||
|
||||
export default RoomSummaryCard;
|
|
@ -20,7 +20,7 @@ limitations under the License.
|
|||
import React, {useCallback, useMemo, useState, useEffect, useContext} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import {Group, RoomMember, User} from 'matrix-js-sdk';
|
||||
import {Group, RoomMember, User, Room} from 'matrix-js-sdk';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import Modal from '../../../Modal';
|
||||
import * as sdk from '../../../index';
|
||||
|
@ -31,7 +31,6 @@ import AccessibleButton from '../elements/AccessibleButton';
|
|||
import SdkConfig from '../../../SdkConfig';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {EventTimeline} from "matrix-js-sdk";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import RoomViewStore from "../../../stores/RoomViewStore";
|
||||
import MultiInviter from "../../../utils/MultiInviter";
|
||||
import GroupStore from "../../../stores/GroupStore";
|
||||
|
@ -45,6 +44,8 @@ import EncryptionPanel from "./EncryptionPanel";
|
|||
import { useAsyncMemo } from '../../../hooks/useAsyncMemo';
|
||||
import { verifyUser, legacyVerifyUser, verifyDevice } from '../../../verification';
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {useIsEncrypted} from "../../../hooks/useIsEncrypted";
|
||||
import BaseCard from "./BaseCard";
|
||||
|
||||
const _disambiguateDevices = (devices) => {
|
||||
const names = Object.create(null);
|
||||
|
@ -124,18 +125,6 @@ async function openDMForUser(matrixClient, userId) {
|
|||
createRoom(createRoomOptions);
|
||||
}
|
||||
|
||||
function useIsEncrypted(cli, room) {
|
||||
const [isEncrypted, setIsEncrypted] = useState(room ? cli.isRoomEncrypted(room.roomId) : undefined);
|
||||
|
||||
const update = useCallback((event) => {
|
||||
if (event.getType() === "m.room.encryption") {
|
||||
setIsEncrypted(cli.isRoomEncrypted(room.roomId));
|
||||
}
|
||||
}, [cli, room]);
|
||||
useEventEmitter(room ? room.currentState : undefined, "RoomState.events", update);
|
||||
return isEncrypted;
|
||||
}
|
||||
|
||||
function useHasCrossSigningKeys(cli, member, canVerify, setUpdating) {
|
||||
return useAsyncMemo(async () => {
|
||||
if (!canVerify) {
|
||||
|
@ -462,7 +451,7 @@ const _isMuted = (member, powerLevelContent) => {
|
|||
return member.powerLevel < levelToSend;
|
||||
};
|
||||
|
||||
const useRoomPowerLevels = (cli, room) => {
|
||||
export const useRoomPowerLevels = (cli, room) => {
|
||||
const [powerLevels, setPowerLevels] = useState({});
|
||||
|
||||
const update = useCallback(() => {
|
||||
|
@ -963,30 +952,26 @@ function useRoomPermissions(cli, room, user) {
|
|||
|
||||
const PowerLevelSection = ({user, room, roomPermissions, powerLevels}) => {
|
||||
const [isEditing, setEditing] = useState(false);
|
||||
if (room && user.roomId) { // is in room
|
||||
if (isEditing) {
|
||||
return (<PowerLevelEditor
|
||||
user={user} room={room} roomPermissions={roomPermissions}
|
||||
onFinished={() => setEditing(false)} />);
|
||||
} else {
|
||||
const IconButton = sdk.getComponent('elements.IconButton');
|
||||
const powerLevelUsersDefault = powerLevels.users_default || 0;
|
||||
const powerLevel = parseInt(user.powerLevel, 10);
|
||||
const modifyButton = roomPermissions.canEdit ?
|
||||
(<IconButton icon="edit" onClick={() => setEditing(true)} />) : null;
|
||||
const role = textualPowerLevel(powerLevel, powerLevelUsersDefault);
|
||||
const label = _t("<strong>%(role)s</strong> in %(roomName)s",
|
||||
{role, roomName: room.name},
|
||||
{strong: label => <strong>{label}</strong>},
|
||||
);
|
||||
return (
|
||||
<div className="mx_UserInfo_profileField">
|
||||
<div className="mx_UserInfo_roleDescription">{label}{modifyButton}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (isEditing) {
|
||||
return (<PowerLevelEditor
|
||||
user={user} room={room} roomPermissions={roomPermissions}
|
||||
onFinished={() => setEditing(false)} />);
|
||||
} else {
|
||||
return null;
|
||||
const IconButton = sdk.getComponent('elements.IconButton');
|
||||
const powerLevelUsersDefault = powerLevels.users_default || 0;
|
||||
const powerLevel = parseInt(user.powerLevel, 10);
|
||||
const modifyButton = roomPermissions.canEdit ?
|
||||
(<IconButton icon="edit" onClick={() => setEditing(true)} />) : null;
|
||||
const role = textualPowerLevel(powerLevel, powerLevelUsersDefault);
|
||||
const label = _t("<strong>%(role)s</strong> in %(roomName)s",
|
||||
{role, roomName: room.name},
|
||||
{strong: label => <strong>{label}</strong>},
|
||||
);
|
||||
return (
|
||||
<div className="mx_UserInfo_profileField">
|
||||
<div className="mx_UserInfo_roleDescription">{label}{modifyButton}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1279,14 +1264,15 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
|
|||
spinner = <Loader imgClassName="mx_ContextualMenu_spinner" />;
|
||||
}
|
||||
|
||||
const memberDetails = (
|
||||
<PowerLevelSection
|
||||
let memberDetails;
|
||||
if (room && member.roomId) {
|
||||
memberDetails = <PowerLevelSection
|
||||
powerLevels={powerLevels}
|
||||
user={member}
|
||||
room={room}
|
||||
roomPermissions={roomPermissions}
|
||||
/>
|
||||
);
|
||||
/>;
|
||||
}
|
||||
|
||||
// only display the devices list if our client supports E2E
|
||||
const cryptoEnabled = cli.isCryptoEnabled();
|
||||
|
@ -1375,16 +1361,9 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
|
|||
</React.Fragment>;
|
||||
};
|
||||
|
||||
const UserInfoHeader = ({onClose, member, e2eStatus}) => {
|
||||
const UserInfoHeader = ({member, e2eStatus}) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
let closeButton;
|
||||
if (onClose) {
|
||||
closeButton = <AccessibleButton className="mx_UserInfo_cancel" onClick={onClose} title={_t('Close')}>
|
||||
<div />
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
const onMemberAvatarClick = useCallback(() => {
|
||||
const avatarUrl = member.getMxcAvatarUrl ? member.getMxcAvatarUrl() : member.avatarUrl;
|
||||
if (!avatarUrl) return;
|
||||
|
@ -1459,7 +1438,6 @@ const UserInfoHeader = ({onClose, member, e2eStatus}) => {
|
|||
|
||||
const displayName = member.name || member.displayname;
|
||||
return <React.Fragment>
|
||||
{ closeButton }
|
||||
{ avatarElement }
|
||||
|
||||
<div className="mx_UserInfo_container mx_UserInfo_separator">
|
||||
|
@ -1482,11 +1460,9 @@ const UserInfoHeader = ({onClose, member, e2eStatus}) => {
|
|||
</React.Fragment>;
|
||||
};
|
||||
|
||||
const UserInfo = ({user, groupId, roomId, onClose, phase=RightPanelPhases.RoomMemberInfo, ...props}) => {
|
||||
const UserInfo = ({user, groupId, room, onClose, phase=RightPanelPhases.RoomMemberInfo, ...props}) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
// Load room if we are given a room id and memoize it
|
||||
const room = useMemo(() => roomId ? cli.getRoom(roomId) : null, [cli, roomId]);
|
||||
// fetch latest room member if we have a room, so we don't show historical information, falling back to user
|
||||
const member = useMemo(() => room ? (room.getMember(user.userId) || user) : user, [room, user]);
|
||||
|
||||
|
@ -1521,15 +1497,16 @@ const UserInfo = ({user, groupId, roomId, onClose, phase=RightPanelPhases.RoomMe
|
|||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.join(" ")} role="tabpanel">
|
||||
<AutoHideScrollbar className="mx_UserInfo_scrollContainer">
|
||||
<UserInfoHeader member={member} e2eStatus={e2eStatus} onClose={onClose} />
|
||||
let previousPhase: RightPanelPhases;
|
||||
// We have no previousPhase for when viewing a UserInfo from a Group or without a Room at this time
|
||||
if (room) {
|
||||
previousPhase = RightPanelPhases.RoomMemberList;
|
||||
}
|
||||
|
||||
{ content }
|
||||
</AutoHideScrollbar>
|
||||
</div>
|
||||
);
|
||||
const header = <UserInfoHeader member={member} e2eStatus={e2eStatus} onClose={onClose} />;
|
||||
return <BaseCard className={classes.join(" ")} header={header} onClose={onClose} previousPhase={previousPhase}>
|
||||
{ content }
|
||||
</BaseCard>;
|
||||
};
|
||||
|
||||
UserInfo.propTypes = {
|
||||
|
@ -1540,7 +1517,7 @@ UserInfo.propTypes = {
|
|||
]).isRequired,
|
||||
group: PropTypes.instanceOf(Group),
|
||||
groupId: PropTypes.string,
|
||||
roomId: PropTypes.string,
|
||||
room: PropTypes.instanceOf(Room),
|
||||
|
||||
onClose: PropTypes.func,
|
||||
};
|
||||
|
|
205
src/components/views/right_panel/WidgetCard.tsx
Normal file
205
src/components/views/right_panel/WidgetCard.tsx
Normal file
|
@ -0,0 +1,205 @@
|
|||
/*
|
||||
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, {useContext, useEffect} from "react";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import BaseCard from "./BaseCard";
|
||||
import WidgetUtils from "../../../utils/WidgetUtils";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import AppTile from "../elements/AppTile";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {useWidgets} from "./RoomSummaryCard";
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import WidgetStore from "../../../stores/WidgetStore";
|
||||
import ActiveWidgetStore from "../../../stores/ActiveWidgetStore";
|
||||
import {ChevronFace, ContextMenuButton, useContextMenu} from "../../structures/ContextMenu";
|
||||
import IconizedContextMenu, {
|
||||
IconizedContextMenuOption,
|
||||
IconizedContextMenuOptionList,
|
||||
} from "../context_menus/IconizedContextMenu";
|
||||
import {AppTileActionPayload} from "../../../dispatcher/payloads/AppTileActionPayload";
|
||||
import {Capability} from "../../../widgets/WidgetApi";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
widgetId: string;
|
||||
onClose(): void;
|
||||
}
|
||||
|
||||
const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
const apps = useWidgets(room);
|
||||
const app = apps.find(a => a.id === widgetId);
|
||||
const isPinned = app && WidgetStore.instance.isPinned(app.id);
|
||||
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
||||
|
||||
useEffect(() => {
|
||||
if (!app || isPinned) {
|
||||
// stop showing this card
|
||||
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: RightPanelPhases.RoomSummary,
|
||||
});
|
||||
}
|
||||
}, [app, isPinned]);
|
||||
|
||||
// Don't render anything as we are about to transition
|
||||
if (!app || isPinned) return null;
|
||||
|
||||
const header = <React.Fragment>
|
||||
<h2>{ WidgetUtils.getWidgetName(app) }</h2>
|
||||
</React.Fragment>;
|
||||
|
||||
const canModify = WidgetUtils.canUserModifyWidgets(room.roomId);
|
||||
|
||||
let contextMenu;
|
||||
if (menuDisplayed) {
|
||||
let snapshotButton;
|
||||
if (ActiveWidgetStore.widgetHasCapability(app.id, Capability.Screenshot)) {
|
||||
const onSnapshotClick = () => {
|
||||
WidgetUtils.snapshotWidget(app);
|
||||
closeMenu();
|
||||
};
|
||||
|
||||
snapshotButton = <IconizedContextMenuOption onClick={onSnapshotClick} label={_t("Take a picture")} />;
|
||||
}
|
||||
|
||||
let deleteButton;
|
||||
if (canModify) {
|
||||
const onDeleteClick = () => {
|
||||
defaultDispatcher.dispatch<AppTileActionPayload>({
|
||||
action: Action.AppTileDelete,
|
||||
widgetId: app.id,
|
||||
});
|
||||
closeMenu();
|
||||
};
|
||||
|
||||
deleteButton = <IconizedContextMenuOption onClick={onDeleteClick} label={_t("Remove for everyone")} />;
|
||||
}
|
||||
|
||||
const onRevokeClick = () => {
|
||||
defaultDispatcher.dispatch<AppTileActionPayload>({
|
||||
action: Action.AppTileRevoke,
|
||||
widgetId: app.id,
|
||||
});
|
||||
closeMenu();
|
||||
};
|
||||
|
||||
const rect = handle.current.getBoundingClientRect();
|
||||
contextMenu = (
|
||||
<IconizedContextMenu
|
||||
chevronFace={ChevronFace.None}
|
||||
right={window.innerWidth - rect.right}
|
||||
bottom={window.innerHeight - rect.top}
|
||||
onFinished={closeMenu}
|
||||
>
|
||||
<IconizedContextMenuOptionList>
|
||||
{ snapshotButton }
|
||||
{ deleteButton }
|
||||
<IconizedContextMenuOption onClick={onRevokeClick} label={_t("Remove for me")} />
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
const onPinClick = () => {
|
||||
WidgetStore.instance.pinWidget(app.id);
|
||||
};
|
||||
|
||||
const onEditClick = () => {
|
||||
WidgetUtils.editWidget(room, app);
|
||||
};
|
||||
|
||||
let editButton;
|
||||
if (canModify) {
|
||||
editButton = <AccessibleButton kind="secondary" onClick={onEditClick}>
|
||||
{ _t("Edit") }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
const pinButtonClasses = canModify ? "" : "mx_WidgetCard_widePinButton";
|
||||
|
||||
let pinButton;
|
||||
if (WidgetStore.instance.canPin(app.id)) {
|
||||
pinButton = <AccessibleButton
|
||||
kind="secondary"
|
||||
onClick={onPinClick}
|
||||
className={pinButtonClasses}
|
||||
>
|
||||
{ _t("Pin to room") }
|
||||
</AccessibleButton>;
|
||||
} else {
|
||||
pinButton = <AccessibleTooltipButton
|
||||
title={_t("You can only pin 2 widgets at a time")}
|
||||
tooltipClassName="mx_WidgetCard_maxPinnedTooltip"
|
||||
kind="secondary"
|
||||
className={pinButtonClasses}
|
||||
disabled
|
||||
>
|
||||
{ _t("Pin to room") }
|
||||
</AccessibleTooltipButton>;
|
||||
}
|
||||
|
||||
const footer = <React.Fragment>
|
||||
{ editButton }
|
||||
{ pinButton }
|
||||
<ContextMenuButton
|
||||
kind="secondary"
|
||||
className="mx_WidgetCard_optionsButton"
|
||||
inputRef={handle}
|
||||
onClick={openMenu}
|
||||
isExpanded={menuDisplayed}
|
||||
label={_t("Options")}
|
||||
/>
|
||||
|
||||
{ contextMenu }
|
||||
</React.Fragment>;
|
||||
|
||||
return <BaseCard
|
||||
header={header}
|
||||
footer={footer}
|
||||
className={classNames("mx_WidgetCard", {
|
||||
mx_WidgetCard_noEdit: !canModify,
|
||||
})}
|
||||
onClose={onClose}
|
||||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
withoutScrollContainer
|
||||
>
|
||||
<AppTile
|
||||
app={app}
|
||||
fullWidth
|
||||
show
|
||||
showMenubar={false}
|
||||
room={room}
|
||||
userId={cli.getUserId()}
|
||||
creatorUserId={app.creatorUserId}
|
||||
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
|
||||
waitForIframeLoad={app.waitForIframeLoad}
|
||||
whitelistCapabilities={WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, room.roomId)}
|
||||
/>
|
||||
</BaseCard>;
|
||||
};
|
||||
|
||||
export default WidgetCard;
|
|
@ -15,11 +15,12 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {useState} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import classNames from 'classnames';
|
||||
import {Resizable} from "re-resizable";
|
||||
|
||||
import AppTile from '../elements/AppTile';
|
||||
import Modal from '../../../Modal';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import * as sdk from '../../../index';
|
||||
import * as ScalarMessaging from '../../../ScalarMessaging';
|
||||
|
@ -29,14 +30,15 @@ import WidgetEchoStore from "../../../stores/WidgetEchoStore";
|
|||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
// The maximum number of widgets that can be added in a room
|
||||
const MAX_WIDGETS = 2;
|
||||
import {useLocalStorageState} from "../../../hooks/useLocalStorageState";
|
||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||
import WidgetStore from "../../../stores/WidgetStore";
|
||||
|
||||
export default class AppsDrawer extends React.Component {
|
||||
static propTypes = {
|
||||
userId: PropTypes.string.isRequired,
|
||||
room: PropTypes.object.isRequired,
|
||||
resizeNotifier: PropTypes.instanceOf(ResizeNotifier).isRequired,
|
||||
showApps: PropTypes.bool, // Should apps be rendered
|
||||
hide: PropTypes.bool, // If rendered, should apps drawer be visible
|
||||
};
|
||||
|
@ -56,17 +58,13 @@ export default class AppsDrawer extends React.Component {
|
|||
|
||||
componentDidMount() {
|
||||
ScalarMessaging.startListening();
|
||||
MatrixClientPeg.get().on('RoomState.events', this.onRoomStateEvents);
|
||||
WidgetEchoStore.on('update', this._updateApps);
|
||||
WidgetStore.instance.on(this.props.room.roomId, this._updateApps);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
ScalarMessaging.stopListening();
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents);
|
||||
}
|
||||
WidgetEchoStore.removeListener('update', this._updateApps);
|
||||
WidgetStore.instance.off(this.props.room.roomId, this._updateApps);
|
||||
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
|
@ -95,28 +93,11 @@ export default class AppsDrawer extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
onRoomStateEvents = (ev, state) => {
|
||||
if (ev.getRoomId() !== this.props.room.roomId || ev.getType() !== 'im.vector.modular.widgets') {
|
||||
return;
|
||||
}
|
||||
this._updateApps();
|
||||
};
|
||||
|
||||
_getApps() {
|
||||
const widgets = WidgetEchoStore.getEchoedRoomWidgets(
|
||||
this.props.room.roomId, WidgetUtils.getRoomWidgets(this.props.room),
|
||||
);
|
||||
return widgets.map((ev) => {
|
||||
return WidgetUtils.makeAppConfig(
|
||||
ev.getStateKey(), ev.getContent(), ev.getSender(), ev.getRoomId(), ev.getId(),
|
||||
);
|
||||
});
|
||||
}
|
||||
_getApps = () => WidgetStore.instance.getApps(this.props.room, true);
|
||||
|
||||
_updateApps = () => {
|
||||
const apps = this._getApps();
|
||||
this.setState({
|
||||
apps: apps,
|
||||
apps: this._getApps(),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -139,18 +120,6 @@ export default class AppsDrawer extends React.Component {
|
|||
|
||||
onClickAddWidget = (e) => {
|
||||
e.preventDefault();
|
||||
// Display a warning dialog if the max number of widgets have already been added to the room
|
||||
const apps = this._getApps();
|
||||
if (apps && apps.length >= MAX_WIDGETS) {
|
||||
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
||||
const errorMsg = `The maximum number of ${MAX_WIDGETS} widgets have already been added to this room.`;
|
||||
console.error(errorMsg);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t('Cannot add any more widgets'),
|
||||
description: _t('The maximum permitted number of widgets have already been added to this room.'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
this._launchManageIntegrations();
|
||||
};
|
||||
|
||||
|
@ -161,19 +130,19 @@ export default class AppsDrawer extends React.Component {
|
|||
return (<AppTile
|
||||
key={app.id}
|
||||
app={app}
|
||||
fullWidth={arr.length<2 ? true : false}
|
||||
fullWidth={arr.length < 2}
|
||||
room={this.props.room}
|
||||
userId={this.props.userId}
|
||||
show={this.props.showApps}
|
||||
creatorUserId={app.creatorUserId}
|
||||
widgetPageTitle={(app.data && app.data.title) ? app.data.title : ''}
|
||||
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
|
||||
waitForIframeLoad={app.waitForIframeLoad}
|
||||
whitelistCapabilities={capWhitelist}
|
||||
/>);
|
||||
});
|
||||
|
||||
if (apps.length == 0) {
|
||||
return <div></div>;
|
||||
if (apps.length === 0) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
let addWidget;
|
||||
|
@ -202,14 +171,68 @@ export default class AppsDrawer extends React.Component {
|
|||
spinner = <Loader />;
|
||||
}
|
||||
|
||||
const classes = classNames({
|
||||
"mx_AppsDrawer": true,
|
||||
"mx_AppsDrawer_hidden": this.props.hide,
|
||||
"mx_AppsDrawer_fullWidth": apps.length < 2,
|
||||
"mx_AppsDrawer_minimised": !this.props.showApps,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={'mx_AppsDrawer' + (this.props.hide ? ' mx_AppsDrawer_hidden' : '')}>
|
||||
<div id='apps' className='mx_AppsContainer'>
|
||||
<div className={classes}>
|
||||
<PersistentVResizer
|
||||
id={"apps-drawer_" + this.props.room.roomId}
|
||||
minHeight={100}
|
||||
maxHeight={this.props.maxHeight ? this.props.maxHeight - 50 : undefined}
|
||||
handleClass="mx_AppsContainer_resizerHandle"
|
||||
className="mx_AppsContainer"
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
>
|
||||
{ apps }
|
||||
{ spinner }
|
||||
</div>
|
||||
</PersistentVResizer>
|
||||
{ this._canUserModify() && addWidget }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const PersistentVResizer = ({
|
||||
id,
|
||||
minHeight,
|
||||
maxHeight,
|
||||
className,
|
||||
handleWrapperClass,
|
||||
handleClass,
|
||||
resizeNotifier,
|
||||
children,
|
||||
}) => {
|
||||
const [height, setHeight] = useLocalStorageState("pvr_" + id, 280); // old fixed height was 273px
|
||||
const [resizing, setResizing] = useState(false);
|
||||
|
||||
return <Resizable
|
||||
size={{height: Math.min(height, maxHeight)}}
|
||||
minHeight={minHeight}
|
||||
maxHeight={maxHeight}
|
||||
onResizeStart={() => {
|
||||
if (!resizing) setResizing(true);
|
||||
resizeNotifier.startResizing();
|
||||
}}
|
||||
onResize={() => {
|
||||
resizeNotifier.notifyTimelineHeightChanged();
|
||||
}}
|
||||
onResizeStop={(e, dir, ref, d) => {
|
||||
setHeight(height + d.height);
|
||||
if (resizing) setResizing(false);
|
||||
resizeNotifier.stopResizing();
|
||||
}}
|
||||
handleWrapperClass={handleWrapperClass}
|
||||
handleClasses={{bottom: handleClass}}
|
||||
className={classNames(className, {
|
||||
mx_AppsDrawer_resizing: resizing,
|
||||
})}
|
||||
enable={{bottom: true}}
|
||||
>
|
||||
{ children }
|
||||
</Resizable>;
|
||||
};
|
||||
|
|
|
@ -28,6 +28,7 @@ import RateLimitedFunc from '../../../ratelimitedfunc';
|
|||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import CallView from "../voip/CallView";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
|
||||
|
||||
export default class AuxPanel extends React.Component {
|
||||
|
@ -198,17 +199,21 @@ export default class AuxPanel extends React.Component {
|
|||
/>
|
||||
);
|
||||
|
||||
const appsDrawer = <AppsDrawer
|
||||
room={this.props.room}
|
||||
userId={this.props.userId}
|
||||
maxHeight={this.props.maxHeight}
|
||||
showApps={this.props.showApps}
|
||||
hide={this.props.hideAppsDrawer}
|
||||
/>;
|
||||
let appsDrawer;
|
||||
if (SettingsStore.getValue(UIFeature.Widgets)) {
|
||||
appsDrawer = <AppsDrawer
|
||||
room={this.props.room}
|
||||
userId={this.props.userId}
|
||||
maxHeight={this.props.maxHeight}
|
||||
showApps={this.props.showApps}
|
||||
hide={this.props.hideAppsDrawer}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
/>;
|
||||
}
|
||||
|
||||
let stateViews = null;
|
||||
if (this.state.counters && SettingsStore.getValue("feature_state_counters")) {
|
||||
let counters = [];
|
||||
const counters = [];
|
||||
|
||||
this.state.counters.forEach((counter, idx) => {
|
||||
const title = counter.title;
|
||||
|
@ -217,7 +222,7 @@ export default class AuxPanel extends React.Component {
|
|||
const severity = counter.severity;
|
||||
const stateKey = counter.stateKey;
|
||||
|
||||
let span = <span>{ title }: { value }</span>
|
||||
let span = <span>{ title }: { value }</span>;
|
||||
|
||||
if (link) {
|
||||
span = (
|
||||
|
|
|
@ -207,7 +207,8 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
// If the user is entering a command, only consider them typing if it is one which sends a message into the room
|
||||
if (isTyping && this.props.model.parts[0].type === "command") {
|
||||
const {cmd} = parseCommandString(this.props.model.parts[0].text);
|
||||
if (!CommandMap.has(cmd) || CommandMap.get(cmd).category !== CommandCategories.messages) {
|
||||
const command = CommandMap.get(cmd);
|
||||
if (!command || !command.isEnabled() || command.category !== CommandCategories.messages) {
|
||||
isTyping = false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
@ -1027,11 +1037,7 @@ class E2ePadlock extends React.Component {
|
|||
tooltip = <Tooltip className="mx_EventTile_e2eIcon_tooltip" label={this.props.title} dir="auto" />;
|
||||
}
|
||||
|
||||
let classes = `mx_EventTile_e2eIcon mx_EventTile_e2eIcon_${this.props.icon}`;
|
||||
if (!SettingsStore.getValue("alwaysShowEncryptionIcons")) {
|
||||
classes += ' mx_EventTile_e2eIcon_hidden';
|
||||
}
|
||||
|
||||
const classes = `mx_EventTile_e2eIcon mx_EventTile_e2eIcon_${this.props.icon}`;
|
||||
return (
|
||||
<div
|
||||
className={classes}
|
||||
|
|
|
@ -20,13 +20,14 @@ import React from 'react';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
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";
|
||||
|
||||
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
||||
const INITIAL_LOAD_NUM_INVITED = 5;
|
||||
|
@ -438,7 +439,13 @@ export default class MemberList extends React.Component {
|
|||
render() {
|
||||
if (this.state.loading) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return <div className="mx_MemberList"><Spinner /></div>;
|
||||
return <BaseCard
|
||||
className="mx_MemberList"
|
||||
onClose={this.props.onClose}
|
||||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
>
|
||||
<Spinner />
|
||||
</BaseCard>;
|
||||
}
|
||||
|
||||
const SearchBox = sdk.getComponent('structures.SearchBox');
|
||||
|
@ -485,25 +492,29 @@ export default class MemberList extends React.Component {
|
|||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_MemberList" role="tabpanel">
|
||||
{ inviteButton }
|
||||
<AutoHideScrollbar>
|
||||
<div className="mx_MemberList_wrapper">
|
||||
<TruncatedList className="mx_MemberList_section mx_MemberList_joined" truncateAt={this.state.truncateAtJoined}
|
||||
createOverflowElement={this._createOverflowTileJoined}
|
||||
getChildren={this._getChildrenJoined}
|
||||
getChildCount={this._getChildCountJoined} />
|
||||
{ invitedHeader }
|
||||
{ invitedSection }
|
||||
</div>
|
||||
</AutoHideScrollbar>
|
||||
|
||||
<SearchBox className="mx_MemberList_query mx_textinput_icon mx_textinput_search"
|
||||
placeholder={ _t('Filter room members') }
|
||||
onSearch={ this.onSearchQueryChanged } />
|
||||
</div>
|
||||
const footer = (
|
||||
<SearchBox
|
||||
className="mx_MemberList_query mx_textinput_icon mx_textinput_search"
|
||||
placeholder={ _t('Filter room members') }
|
||||
onSearch={ this.onSearchQueryChanged } />
|
||||
);
|
||||
|
||||
return <BaseCard
|
||||
className="mx_MemberList"
|
||||
header={inviteButton}
|
||||
footer={footer}
|
||||
onClose={this.props.onClose}
|
||||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
>
|
||||
<div className="mx_MemberList_wrapper">
|
||||
<TruncatedList className="mx_MemberList_section mx_MemberList_joined" truncateAt={this.state.truncateAtJoined}
|
||||
createOverflowElement={this._createOverflowTileJoined}
|
||||
getChildren={this._getChildrenJoined}
|
||||
getChildCount={this._getChildCountJoined} />
|
||||
{ invitedHeader }
|
||||
{ invitedSection }
|
||||
</div>
|
||||
</BaseCard>;
|
||||
}
|
||||
|
||||
onInviteButtonClick = () => {
|
||||
|
|
|
@ -31,6 +31,7 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import {aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu} from "../../structures/ContextMenu";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import ReplyPreview from "./ReplyPreview";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
|
||||
function ComposerAvatar(props) {
|
||||
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
|
||||
|
@ -384,9 +385,12 @@ export default class MessageComposer extends React.Component {
|
|||
permalinkCreator={this.props.permalinkCreator} />,
|
||||
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
|
||||
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
|
||||
<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />,
|
||||
);
|
||||
|
||||
if (SettingsStore.getValue(UIFeature.Widgets)) {
|
||||
controls.push(<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />);
|
||||
}
|
||||
|
||||
if (this.state.showCallButtons) {
|
||||
if (callInProgress) {
|
||||
controls.push(
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import { formatMinimalBadgeCount } from "../../../utils/FormattingUtils";
|
||||
import { formatCount } from "../../../utils/FormattingUtils";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { XOR } from "../../../@types/common";
|
||||
|
@ -109,7 +109,7 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
|
|||
if (!notification.hasUnreadCount) return null; // Can't render a badge
|
||||
}
|
||||
|
||||
let symbol = notification.symbol || formatMinimalBadgeCount(notification.count);
|
||||
let symbol = notification.symbol || formatCount(notification.count);
|
||||
if (isEmptyBadge) symbol = "";
|
||||
|
||||
const classes = classNames({
|
||||
|
|
|
@ -18,14 +18,11 @@ limitations under the License.
|
|||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import Modal from "../../../Modal";
|
||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||
|
||||
import { linkifyElement } from '../../../HtmlUtils';
|
||||
import ManageIntegsButton from '../elements/ManageIntegsButton';
|
||||
import {CancelButton} from './SimpleRoomHeader';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
|
||||
|
@ -114,13 +111,6 @@ export default class RoomHeader extends React.Component {
|
|||
this.forceUpdate();
|
||||
};
|
||||
|
||||
onShareRoomClick = (ev) => {
|
||||
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||
Modal.createTrackedDialog('share room dialog', '', ShareDialog, {
|
||||
target: this.props.room,
|
||||
});
|
||||
};
|
||||
|
||||
_hasUnreadPins() {
|
||||
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
|
||||
if (!currentPinEvent) return false;
|
||||
|
@ -150,7 +140,6 @@ export default class RoomHeader extends React.Component {
|
|||
render() {
|
||||
let searchStatus = null;
|
||||
let cancelButton = null;
|
||||
let settingsButton = null;
|
||||
let pinnedEventsButton = null;
|
||||
|
||||
if (this.props.onCancelClick) {
|
||||
|
@ -214,14 +203,6 @@ export default class RoomHeader extends React.Component {
|
|||
/>;
|
||||
}
|
||||
|
||||
if (this.props.onSettingsClick) {
|
||||
settingsButton =
|
||||
<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_settingsButton"
|
||||
onClick={this.props.onSettingsClick}
|
||||
title={_t("Settings")} />;
|
||||
}
|
||||
|
||||
if (this.props.onPinnedClick && SettingsStore.getValue('feature_pinning')) {
|
||||
let pinsIndicator = null;
|
||||
if (this._hasUnreadPins()) {
|
||||
|
@ -258,26 +239,9 @@ export default class RoomHeader extends React.Component {
|
|||
title={_t("Search")} />;
|
||||
}
|
||||
|
||||
let shareRoomButton;
|
||||
if (this.props.inRoom) {
|
||||
shareRoomButton =
|
||||
<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_shareButton"
|
||||
onClick={this.onShareRoomClick}
|
||||
title={_t('Share room')} />;
|
||||
}
|
||||
|
||||
let manageIntegsButton;
|
||||
if (this.props.room && this.props.room.roomId && this.props.inRoom) {
|
||||
manageIntegsButton = <ManageIntegsButton room={this.props.room} />;
|
||||
}
|
||||
|
||||
const rightRow =
|
||||
<div className="mx_RoomHeader_buttons">
|
||||
{ settingsButton }
|
||||
{ pinnedEventsButton }
|
||||
{ shareRoomButton }
|
||||
{ manageIntegsButton }
|
||||
{ forgetButton }
|
||||
{ searchButton }
|
||||
</div>;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>);
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
|
|
|
@ -19,8 +19,10 @@ import React from 'react';
|
|||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
import { accessSecretStorage } from '../../../CrossSigningManager';
|
||||
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) {
|
||||
|
@ -30,13 +32,13 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
|
||||
this.state = {
|
||||
error: null,
|
||||
crossSigningPublicKeysOnDevice: false,
|
||||
crossSigningPrivateKeysInStorage: false,
|
||||
masterPrivateKeyCached: false,
|
||||
selfSigningPrivateKeyCached: false,
|
||||
userSigningPrivateKeyCached: false,
|
||||
sessionBackupKeyCached: false,
|
||||
secretStorageKeyInAccount: false,
|
||||
crossSigningPublicKeysOnDevice: null,
|
||||
crossSigningPrivateKeysInStorage: null,
|
||||
masterPrivateKeyCached: null,
|
||||
selfSigningPrivateKeyCached: null,
|
||||
userSigningPrivateKeyCached: null,
|
||||
homeserverSupportsCrossSigning: null,
|
||||
crossSigningReady: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -65,7 +67,7 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
};
|
||||
|
||||
_onBootstrapClick = () => {
|
||||
this._bootstrapSecureSecretStorage(false);
|
||||
this._bootstrapCrossSigning({ forceReset: false });
|
||||
};
|
||||
|
||||
onStatusChanged = () => {
|
||||
|
@ -82,14 +84,9 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
const masterPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("master"));
|
||||
const selfSigningPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("self_signing"));
|
||||
const userSigningPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("user_signing"));
|
||||
const sessionBackupKeyFromCache = await cli._crypto.getSessionBackupPrivateKey();
|
||||
const sessionBackupKeyCached = !!(sessionBackupKeyFromCache);
|
||||
const sessionBackupKeyWellFormed = sessionBackupKeyFromCache instanceof Uint8Array;
|
||||
const secretStorageKeyInAccount = await secretStorage.hasKey();
|
||||
const homeserverSupportsCrossSigning =
|
||||
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
|
||||
const crossSigningReady = await cli.isCrossSigningReady();
|
||||
const secretStorageReady = await cli.isSecretStorageReady();
|
||||
|
||||
this.setState({
|
||||
crossSigningPublicKeysOnDevice,
|
||||
|
@ -97,45 +94,55 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
masterPrivateKeyCached,
|
||||
selfSigningPrivateKeyCached,
|
||||
userSigningPrivateKeyCached,
|
||||
sessionBackupKeyCached,
|
||||
sessionBackupKeyWellFormed,
|
||||
secretStorageKeyInAccount,
|
||||
homeserverSupportsCrossSigning,
|
||||
crossSigningReady,
|
||||
secretStorageReady,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrapping secret storage may take one of these paths:
|
||||
* 1. Create secret storage from a passphrase and store cross-signing keys
|
||||
* in secret storage.
|
||||
* Bootstrapping cross-signing take one of these paths:
|
||||
* 1. Create cross-signing keys locally and store in secret storage (if it
|
||||
* already exists on the account).
|
||||
* 2. Access existing secret storage by requesting passphrase and accessing
|
||||
* cross-signing keys as needed.
|
||||
* 3. All keys are loaded and there's nothing to do.
|
||||
* @param {bool} [forceReset] Bootstrap again even if keys already present
|
||||
*/
|
||||
_bootstrapSecureSecretStorage = async (forceReset=false) => {
|
||||
_bootstrapCrossSigning = async ({ forceReset = false }) => {
|
||||
this.setState({ error: null });
|
||||
try {
|
||||
await accessSecretStorage(() => undefined, forceReset);
|
||||
const cli = MatrixClientPeg.get();
|
||||
await cli.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async (makeRequest) => {
|
||||
const { finished } = Modal.createTrackedDialog(
|
||||
'Cross-signing keys dialog', '', InteractiveAuthDialog,
|
||||
{
|
||||
title: _t("Setting up keys"),
|
||||
matrixClient: cli,
|
||||
makeRequest,
|
||||
},
|
||||
);
|
||||
const [confirmed] = await finished;
|
||||
if (!confirmed) {
|
||||
throw new Error("Cross-signing key upload auth canceled");
|
||||
}
|
||||
},
|
||||
setupNewCrossSigning: forceReset,
|
||||
});
|
||||
} catch (e) {
|
||||
this.setState({ error: e });
|
||||
console.error("Error bootstrapping secret storage", e);
|
||||
console.error("Error bootstrapping cross-signing", e);
|
||||
}
|
||||
if (this._unmounted) return;
|
||||
this._getUpdatedStatus();
|
||||
}
|
||||
|
||||
onDestroyStorage = (act) => {
|
||||
if (!act) return;
|
||||
this._bootstrapSecureSecretStorage(true);
|
||||
}
|
||||
|
||||
_destroySecureSecretStorage = () => {
|
||||
const ConfirmDestroyCrossSigningDialog = sdk.getComponent("dialogs.ConfirmDestroyCrossSigningDialog");
|
||||
_resetCrossSigning = () => {
|
||||
Modal.createDialog(ConfirmDestroyCrossSigningDialog, {
|
||||
onFinished: this.onDestroyStorage,
|
||||
onFinished: (act) => {
|
||||
if (!act) return;
|
||||
this._bootstrapCrossSigning({ forceReset: true });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -148,12 +155,8 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
masterPrivateKeyCached,
|
||||
selfSigningPrivateKeyCached,
|
||||
userSigningPrivateKeyCached,
|
||||
sessionBackupKeyCached,
|
||||
sessionBackupKeyWellFormed,
|
||||
secretStorageKeyInAccount,
|
||||
homeserverSupportsCrossSigning,
|
||||
crossSigningReady,
|
||||
secretStorageReady,
|
||||
} = this.state;
|
||||
|
||||
let errorSection;
|
||||
|
@ -163,20 +166,14 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
|
||||
let summarisedStatus;
|
||||
if (homeserverSupportsCrossSigning === undefined) {
|
||||
const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner');
|
||||
summarisedStatus = <p><InlineSpinner /></p>;
|
||||
summarisedStatus = <Spinner />;
|
||||
} else if (!homeserverSupportsCrossSigning) {
|
||||
summarisedStatus = <p>{_t(
|
||||
"Your homeserver does not support cross-signing.",
|
||||
)}</p>;
|
||||
} else if (crossSigningReady && secretStorageReady) {
|
||||
} else if (crossSigningReady) {
|
||||
summarisedStatus = <p>✅ {_t(
|
||||
"Cross-signing and secret storage are ready for use.",
|
||||
)}</p>;
|
||||
} else if (crossSigningReady && !secretStorageReady) {
|
||||
summarisedStatus = <p>✅ {_t(
|
||||
"Cross-signing is ready for use, but secret storage is " +
|
||||
"currently not being used to backup your keys.",
|
||||
"Cross-signing is ready for use.",
|
||||
)}</p>;
|
||||
} else if (crossSigningPrivateKeysInStorage) {
|
||||
summarisedStatus = <p>{_t(
|
||||
|
@ -185,52 +182,49 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
)}</p>;
|
||||
} else {
|
||||
summarisedStatus = <p>{_t(
|
||||
"Cross-signing and secret storage are not yet set up.",
|
||||
"Cross-signing is not set up.",
|
||||
)}</p>;
|
||||
}
|
||||
|
||||
const keysExistAnywhere = (
|
||||
secretStorageKeyInAccount ||
|
||||
crossSigningPublicKeysOnDevice ||
|
||||
crossSigningPrivateKeysInStorage ||
|
||||
crossSigningPublicKeysOnDevice
|
||||
masterPrivateKeyCached ||
|
||||
selfSigningPrivateKeyCached ||
|
||||
userSigningPrivateKeyCached
|
||||
);
|
||||
const keysExistEverywhere = (
|
||||
secretStorageKeyInAccount &&
|
||||
crossSigningPublicKeysOnDevice &&
|
||||
crossSigningPrivateKeysInStorage &&
|
||||
crossSigningPublicKeysOnDevice
|
||||
masterPrivateKeyCached &&
|
||||
selfSigningPrivateKeyCached &&
|
||||
userSigningPrivateKeyCached
|
||||
);
|
||||
|
||||
let resetButton;
|
||||
if (keysExistAnywhere) {
|
||||
resetButton = (
|
||||
<div className="mx_CrossSigningPanel_buttonRow">
|
||||
<AccessibleButton kind="danger" onClick={this._destroySecureSecretStorage}>
|
||||
{_t("Reset cross-signing and secret storage")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const actions = [];
|
||||
|
||||
// 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("Bootstrap cross-signing and secret storage")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
actions.push(
|
||||
<AccessibleButton key="setup" kind="primary" onClick={this._onBootstrapClick}>
|
||||
{_t("Set up")}
|
||||
</AccessibleButton>,
|
||||
);
|
||||
}
|
||||
|
||||
let sessionBackupKeyWellFormedText = "";
|
||||
if (sessionBackupKeyCached) {
|
||||
sessionBackupKeyWellFormedText = ", ";
|
||||
if (sessionBackupKeyWellFormed) {
|
||||
sessionBackupKeyWellFormedText += _t("well formed");
|
||||
} else {
|
||||
sessionBackupKeyWellFormedText += _t("unexpected type");
|
||||
}
|
||||
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 (
|
||||
|
@ -245,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>
|
||||
|
@ -259,17 +253,6 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
<td>{_t("User signing private key:")}</td>
|
||||
<td>{userSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_t("Session backup key:")}</td>
|
||||
<td>
|
||||
{sessionBackupKeyCached ? _t("cached locally") : _t("not found locally")}
|
||||
{sessionBackupKeyWellFormedText}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_t("Secret storage public key:")}</td>
|
||||
<td>{secretStorageKeyInAccount ? _t("in account data") : _t("not found")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_t("Homeserver feature support:")}</td>
|
||||
<td>{homeserverSupportsCrossSigning ? _t("exists") : _t("not found")}</td>
|
||||
|
@ -277,8 +260,7 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
</tbody></table>
|
||||
</details>
|
||||
{errorSection}
|
||||
{bootstrapButton}
|
||||
{resetButton}
|
||||
{actionRow}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -19,13 +19,14 @@ 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";
|
||||
|
||||
const E2eAdvancedPanel = props => {
|
||||
const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag');
|
||||
return <div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Advanced")}</span>
|
||||
<span className="mx_SettingsTab_subheading">{_t("Encryption")}</span>
|
||||
|
||||
<SettingsFlag name={SETTING_MANUALLY_VERIFY_ALL_SESSIONS}
|
||||
level={SettingLevel.DEVICE}
|
||||
|
@ -37,3 +38,7 @@ const E2eAdvancedPanel = props => {
|
|||
};
|
||||
|
||||
export default E2eAdvancedPanel;
|
||||
|
||||
export function isE2eAdvancedPanelPossible(): boolean {
|
||||
return SettingsStore.isEnabled(SETTING_MANUALLY_VERIFY_ALL_SESSIONS);
|
||||
}
|
||||
|
|
|
@ -17,13 +17,17 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Modal from '../../../Modal';
|
||||
import { isSecureBackupRequired } from '../../../utils/WellKnownUtils';
|
||||
import Spinner from '../elements/Spinner';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import QuestionDialog from '../dialogs/QuestionDialog';
|
||||
import RestoreKeyBackupDialog from '../dialogs/security/RestoreKeyBackupDialog';
|
||||
import { accessSecretStorage } from '../../../SecurityManager';
|
||||
|
||||
export default class KeyBackupPanel extends React.PureComponent {
|
||||
export default class SecureBackupPanel extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
@ -31,9 +35,13 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
this.state = {
|
||||
loading: true,
|
||||
error: null,
|
||||
backupKeyStored: null,
|
||||
backupKeyCached: null,
|
||||
backupKeyWellFormed: null,
|
||||
secretStorageKeyInAccount: null,
|
||||
secretStorageReady: null,
|
||||
backupInfo: null,
|
||||
backupSigStatus: null,
|
||||
backupKeyStored: null,
|
||||
sessionsRemaining: 0,
|
||||
};
|
||||
}
|
||||
|
@ -73,59 +81,76 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
}
|
||||
|
||||
async _checkKeyBackupStatus() {
|
||||
this._getUpdatedDiagnostics();
|
||||
try {
|
||||
const {backupInfo, trustInfo} = await MatrixClientPeg.get().checkKeyBackup();
|
||||
const backupKeyStored = Boolean(await MatrixClientPeg.get().isKeyBackupKeyStored());
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: null,
|
||||
backupInfo,
|
||||
backupSigStatus: trustInfo,
|
||||
backupKeyStored,
|
||||
error: null,
|
||||
loading: false,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Unable to fetch check backup status", e);
|
||||
if (this._unmounted) return;
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: e,
|
||||
backupInfo: null,
|
||||
backupSigStatus: null,
|
||||
backupKeyStored: null,
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async _loadBackupStatus() {
|
||||
this.setState({loading: true});
|
||||
this.setState({ loading: true });
|
||||
this._getUpdatedDiagnostics();
|
||||
try {
|
||||
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||
const backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo);
|
||||
const backupKeyStored = await MatrixClientPeg.get().isKeyBackupKeyStored();
|
||||
if (this._unmounted) return;
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: null,
|
||||
backupInfo,
|
||||
backupSigStatus,
|
||||
backupKeyStored,
|
||||
loading: false,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Unable to fetch key backup status", e);
|
||||
if (this._unmounted) return;
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: e,
|
||||
backupInfo: null,
|
||||
backupSigStatus: null,
|
||||
backupKeyStored: null,
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async _getUpdatedDiagnostics() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const secretStorage = cli._crypto._secretStorage;
|
||||
|
||||
const backupKeyStored = !!(await cli.isKeyBackupKeyStored());
|
||||
const backupKeyFromCache = await cli._crypto.getSessionBackupPrivateKey();
|
||||
const backupKeyCached = !!(backupKeyFromCache);
|
||||
const backupKeyWellFormed = backupKeyFromCache instanceof Uint8Array;
|
||||
const secretStorageKeyInAccount = await secretStorage.hasKey();
|
||||
const secretStorageReady = await cli.isSecretStorageReady();
|
||||
|
||||
if (this._unmounted) return;
|
||||
this.setState({
|
||||
backupKeyStored,
|
||||
backupKeyCached,
|
||||
backupKeyWellFormed,
|
||||
secretStorageKeyInAccount,
|
||||
secretStorageReady,
|
||||
});
|
||||
}
|
||||
|
||||
_startNewBackup = () => {
|
||||
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
|
||||
import('../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
|
||||
import('../../../async-components/views/dialogs/security/CreateKeyBackupDialog'),
|
||||
{
|
||||
onFinished: () => {
|
||||
this._loadBackupStatus();
|
||||
|
@ -135,7 +160,6 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
}
|
||||
|
||||
_deleteBackup = () => {
|
||||
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||
Modal.createTrackedDialog('Delete Backup', '', QuestionDialog, {
|
||||
title: _t('Delete Backup'),
|
||||
description: _t(
|
||||
|
@ -155,41 +179,58 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
}
|
||||
|
||||
_restoreBackup = async () => {
|
||||
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
||||
Modal.createTrackedDialog(
|
||||
'Restore Backup', '', RestoreKeyBackupDialog, null, null,
|
||||
/* priority = */ false, /* static = */ true,
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
const encryptedMessageAreEncrypted = _t(
|
||||
"Encrypted messages are secured with end-to-end encryption. " +
|
||||
"Only you and the recipient(s) have the keys to read these messages.",
|
||||
);
|
||||
_resetSecretStorage = async () => {
|
||||
this.setState({ error: null });
|
||||
try {
|
||||
await accessSecretStorage(() => { }, /* forceReset = */ true);
|
||||
} catch (e) {
|
||||
console.error("Error resetting secret storage", e);
|
||||
if (this._unmounted) return;
|
||||
this.setState({ error: e });
|
||||
}
|
||||
if (this._unmounted) return;
|
||||
this._loadBackupStatus();
|
||||
}
|
||||
|
||||
if (this.state.error) {
|
||||
return (
|
||||
render() {
|
||||
const {
|
||||
loading,
|
||||
error,
|
||||
backupKeyStored,
|
||||
backupKeyCached,
|
||||
backupKeyWellFormed,
|
||||
secretStorageKeyInAccount,
|
||||
secretStorageReady,
|
||||
backupInfo,
|
||||
backupSigStatus,
|
||||
sessionsRemaining,
|
||||
} = this.state;
|
||||
|
||||
let statusDescription;
|
||||
let extraDetailsTableRows;
|
||||
let extraDetails;
|
||||
const actions = [];
|
||||
if (error) {
|
||||
statusDescription = (
|
||||
<div className="error">
|
||||
{_t("Unable to load key backup status")}
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.loading) {
|
||||
return <Spinner />;
|
||||
} else if (this.state.backupInfo) {
|
||||
let clientBackupStatus;
|
||||
} else if (loading) {
|
||||
statusDescription = <Spinner />;
|
||||
} else if (backupInfo) {
|
||||
let restoreButtonCaption = _t("Restore from Backup");
|
||||
|
||||
if (MatrixClientPeg.get().getKeyBackupEnabled()) {
|
||||
clientBackupStatus = <div>
|
||||
<p>{encryptedMessageAreEncrypted}</p>
|
||||
<p>✅ {_t("This session is backing up your keys. ")}</p>
|
||||
</div>;
|
||||
statusDescription = <p>✅ {_t("This session is backing up your keys. ")}</p>;
|
||||
} else {
|
||||
clientBackupStatus = <div>
|
||||
<p>{encryptedMessageAreEncrypted}</p>
|
||||
statusDescription = <>
|
||||
<p>{_t(
|
||||
"This session is <b>not backing up your keys</b>, " +
|
||||
"but you do have an existing backup you can restore from " +
|
||||
|
@ -200,19 +241,11 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
"Connect this session to key backup before signing out to avoid " +
|
||||
"losing any keys that may only be on this session.",
|
||||
)}</p>
|
||||
</div>;
|
||||
</>;
|
||||
restoreButtonCaption = _t("Connect this session to Key Backup");
|
||||
}
|
||||
|
||||
let keyStatus;
|
||||
if (this.state.backupKeyStored === true) {
|
||||
keyStatus = _t("in secret storage");
|
||||
} else {
|
||||
keyStatus = _t("not stored");
|
||||
}
|
||||
|
||||
let uploadStatus;
|
||||
const { sessionsRemaining } = this.state;
|
||||
if (!MatrixClientPeg.get().getKeyBackupEnabled()) {
|
||||
// No upload status to show when backup disabled.
|
||||
uploadStatus = "";
|
||||
|
@ -226,17 +259,17 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
</div>;
|
||||
}
|
||||
|
||||
let backupSigStatuses = this.state.backupSigStatus.sigs.map((sig, i) => {
|
||||
let backupSigStatuses = backupSigStatus.sigs.map((sig, i) => {
|
||||
const deviceName = sig.device ? (sig.device.getDisplayName() || sig.device.deviceId) : null;
|
||||
const validity = sub =>
|
||||
<span className={sig.valid ? 'mx_KeyBackupPanel_sigValid' : 'mx_KeyBackupPanel_sigInvalid'}>
|
||||
<span className={sig.valid ? 'mx_SecureBackupPanel_sigValid' : 'mx_SecureBackupPanel_sigInvalid'}>
|
||||
{sub}
|
||||
</span>;
|
||||
const verify = sub =>
|
||||
<span className={sig.device && sig.deviceTrust.isVerified() ? 'mx_KeyBackupPanel_deviceVerified' : 'mx_KeyBackupPanel_deviceNotVerified'}>
|
||||
<span className={sig.device && sig.deviceTrust.isVerified() ? 'mx_SecureBackupPanel_deviceVerified' : 'mx_SecureBackupPanel_deviceNotVerified'}>
|
||||
{sub}
|
||||
</span>;
|
||||
const device = sub => <span className="mx_KeyBackupPanel_deviceName">{deviceName}</span>;
|
||||
const device = sub => <span className="mx_SecureBackupPanel_deviceName">{deviceName}</span>;
|
||||
const fromThisDevice = (
|
||||
sig.device &&
|
||||
sig.device.getFingerprint() === MatrixClientPeg.get().getDeviceEd25519Key()
|
||||
|
@ -307,60 +340,123 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
{sigStatus}
|
||||
</div>;
|
||||
});
|
||||
if (this.state.backupSigStatus.sigs.length === 0) {
|
||||
if (backupSigStatus.sigs.length === 0) {
|
||||
backupSigStatuses = _t("Backup is not signed by any of your sessions");
|
||||
}
|
||||
|
||||
let trustedLocally;
|
||||
if (this.state.backupSigStatus.trusted_locally) {
|
||||
if (backupSigStatus.trusted_locally) {
|
||||
trustedLocally = _t("This backup is trusted because it has been restored on this session");
|
||||
}
|
||||
|
||||
let deleteBackupButton;
|
||||
if (!isSecureBackupRequired()) {
|
||||
deleteBackupButton = <AccessibleButton kind="danger" onClick={this._deleteBackup}>
|
||||
{_t("Delete Backup")}
|
||||
</AccessibleButton>;
|
||||
}
|
||||
extraDetailsTableRows = <>
|
||||
<tr>
|
||||
<td>{_t("Backup version:")}</td>
|
||||
<td>{backupInfo.version}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_t("Algorithm:")}</td>
|
||||
<td>{backupInfo.algorithm}</td>
|
||||
</tr>
|
||||
</>;
|
||||
|
||||
const buttonRow = (
|
||||
<div className="mx_KeyBackupPanel_buttonRow">
|
||||
<AccessibleButton kind="primary" onClick={this._restoreBackup}>
|
||||
{restoreButtonCaption}
|
||||
</AccessibleButton>
|
||||
{deleteBackupButton}
|
||||
</div>
|
||||
extraDetails = <>
|
||||
{uploadStatus}
|
||||
<div>{backupSigStatuses}</div>
|
||||
<div>{trustedLocally}</div>
|
||||
</>;
|
||||
|
||||
actions.push(
|
||||
<AccessibleButton key="restore" kind="primary" onClick={this._restoreBackup}>
|
||||
{restoreButtonCaption}
|
||||
</AccessibleButton>,
|
||||
);
|
||||
|
||||
return <div>
|
||||
<div>{clientBackupStatus}</div>
|
||||
<details>
|
||||
<summary>{_t("Advanced")}</summary>
|
||||
<div>{_t("Backup version: ")}{this.state.backupInfo.version}</div>
|
||||
<div>{_t("Algorithm: ")}{this.state.backupInfo.algorithm}</div>
|
||||
<div>{_t("Backup key stored: ")}{keyStatus}</div>
|
||||
{uploadStatus}
|
||||
<div>{backupSigStatuses}</div>
|
||||
<div>{trustedLocally}</div>
|
||||
</details>
|
||||
{buttonRow}
|
||||
</div>;
|
||||
if (!isSecureBackupRequired()) {
|
||||
actions.push(
|
||||
<AccessibleButton key="delete" kind="danger" onClick={this._deleteBackup}>
|
||||
{_t("Delete Backup")}
|
||||
</AccessibleButton>,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return <div>
|
||||
<div>
|
||||
<p>{_t(
|
||||
"Your keys are <b>not being backed up from this session</b>.", {},
|
||||
{b: sub => <b>{sub}</b>},
|
||||
)}</p>
|
||||
<p>{encryptedMessageAreEncrypted}</p>
|
||||
<p>{_t("Back up your keys before signing out to avoid losing them.")}</p>
|
||||
</div>
|
||||
<div className="mx_KeyBackupPanel_buttonRow">
|
||||
<AccessibleButton kind="primary" onClick={this._startNewBackup}>
|
||||
{_t("Start using Key Backup")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
statusDescription = <>
|
||||
<p>{_t(
|
||||
"Your keys are <b>not being backed up from this session</b>.", {},
|
||||
{b: sub => <b>{sub}</b>},
|
||||
)}</p>
|
||||
<p>{_t("Back up your keys before signing out to avoid losing them.")}</p>
|
||||
</>;
|
||||
actions.push(
|
||||
<AccessibleButton key="setup" kind="primary" onClick={this._startNewBackup}>
|
||||
{_t("Set up")}
|
||||
</AccessibleButton>,
|
||||
);
|
||||
}
|
||||
|
||||
if (secretStorageKeyInAccount) {
|
||||
actions.push(
|
||||
<AccessibleButton key="reset" kind="danger" onClick={this._resetSecretStorage}>
|
||||
{_t("Reset")}
|
||||
</AccessibleButton>,
|
||||
);
|
||||
}
|
||||
|
||||
let backupKeyWellFormedText = "";
|
||||
if (backupKeyCached) {
|
||||
backupKeyWellFormedText = ", ";
|
||||
if (backupKeyWellFormed) {
|
||||
backupKeyWellFormedText += _t("well formed");
|
||||
} else {
|
||||
backupKeyWellFormedText += _t("unexpected type");
|
||||
}
|
||||
}
|
||||
|
||||
let actionRow;
|
||||
if (actions.length) {
|
||||
actionRow = <div className="mx_SecureBackupPanel_buttonRow">
|
||||
{actions}
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{_t(
|
||||
"Back up your encryption keys with your account data in case you " +
|
||||
"lose access to your sessions. Your keys will be secured with a " +
|
||||
"unique Recovery Key.",
|
||||
)}</p>
|
||||
{statusDescription}
|
||||
<details>
|
||||
<summary>{_t("Advanced")}</summary>
|
||||
<table className="mx_SecureBackupPanel_statusList"><tbody>
|
||||
<tr>
|
||||
<td>{_t("Backup key stored:")}</td>
|
||||
<td>{
|
||||
backupKeyStored === true ? _t("in secret storage") : _t("not stored")
|
||||
}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_t("Backup key cached:")}</td>
|
||||
<td>
|
||||
{backupKeyCached ? _t("cached locally") : _t("not found locally")}
|
||||
{backupKeyWellFormedText}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_t("Secret storage public key:")}</td>
|
||||
<td>{secretStorageKeyInAccount ? _t("in account data") : _t("not found")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_t("Secret storage:")}</td>
|
||||
<td>{secretStorageReady ? _t("ready") : _t("not ready")}</td>
|
||||
</tr>
|
||||
{extraDetailsTableRows}
|
||||
</tbody></table>
|
||||
{extraDetails}
|
||||
</details>
|
||||
{actionRow}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -46,9 +46,10 @@ export default class BridgeSettingsTab extends React.Component<IProps> {
|
|||
const client = MatrixClientPeg.get();
|
||||
const roomState = client.getRoom(roomId).currentState;
|
||||
|
||||
return [].concat(...BRIDGE_EVENT_TYPES.map((typeName) =>
|
||||
Array.from(roomState.events.get(typeName).values()),
|
||||
));
|
||||
return BRIDGE_EVENT_TYPES.map(typeName => {
|
||||
const events = roomState.events.get(typeName);
|
||||
return events ? Array.from(events.values()) : [];
|
||||
}).flat(1);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -22,6 +22,8 @@ import * as sdk from "../../../../..";
|
|||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import dis from "../../../../../dispatcher/dispatcher";
|
||||
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import {UIFeature} from "../../../../../settings/UIFeature";
|
||||
|
||||
export default class GeneralRoomSettingsTab extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -61,6 +63,28 @@ export default class GeneralRoomSettingsTab extends React.Component {
|
|||
const canChangeGroups = room.currentState.mayClientSendStateEvent("m.room.related_groups", client);
|
||||
const groupsEvent = room.currentState.getStateEvents("m.room.related_groups", "");
|
||||
|
||||
let urlPreviewSettings = <>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("URL Previews")}</span>
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<UrlPreviewSettings room={room} />
|
||||
</div>
|
||||
</>;
|
||||
if (!SettingsStore.getValue(UIFeature.URLPreviews)) {
|
||||
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>
|
||||
|
@ -75,17 +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>
|
||||
|
||||
<span className='mx_SettingsTab_subheading'>{_t("URL Previews")}</span>
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<UrlPreviewSettings room={room} />
|
||||
</div>
|
||||
{ flairSection }
|
||||
{ urlPreviewSettings }
|
||||
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Leave room")}</span>
|
||||
<div className='mx_SettingsTab_section'>
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -37,6 +37,7 @@ import {abbreviateUrl} from "../../../../../utils/UrlUtils";
|
|||
import { getThreepidsWithBindStatus } from '../../../../../boundThreepids';
|
||||
import Spinner from "../../../elements/Spinner";
|
||||
import {SettingLevel} from "../../../../../settings/SettingLevel";
|
||||
import {UIFeature} from "../../../../../settings/UIFeature";
|
||||
|
||||
export default class GeneralUserSettingsTab extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -247,7 +248,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
|
||||
|
@ -366,6 +369,8 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
}
|
||||
|
||||
_renderIntegrationManagerSection() {
|
||||
if (!SettingsStore.getValue(UIFeature.Widgets)) return null;
|
||||
|
||||
const SetIntegrationManager = sdk.getComponent("views.settings.SetIntegrationManager");
|
||||
|
||||
return (
|
||||
|
@ -383,17 +388,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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -204,9 +204,9 @@ export default class HelpUserSettingsTab extends React.Component {
|
|||
updateButton = <UpdateCheckButton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_HelpUserSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Help & About")}</div>
|
||||
let bugReportingSection;
|
||||
if (SdkConfig.get().bug_report_endpoint_url) {
|
||||
bugReportingSection = (
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className='mx_SettingsTab_subheading'>{_t('Bug reporting')}</span>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
|
@ -223,22 +223,24 @@ export default class HelpUserSettingsTab extends React.Component {
|
|||
{_t("Submit debug logs")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div className='mx_HelpUserSettingsTab_debugButton'>
|
||||
<AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'>
|
||||
{_t("Clear cache and reload")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
{
|
||||
_t( "To report a Matrix-related security issue, please read the Matrix.org " +
|
||||
"<a>Security Disclosure Policy</a>.", {},
|
||||
{
|
||||
'a': (sub) =>
|
||||
<a href="https://matrix.org/security-disclosure-policy/"
|
||||
rel="noreferrer noopener" target="_blank">{sub}</a>,
|
||||
rel="noreferrer noopener" target="_blank">{sub}</a>,
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_HelpUserSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Help & About")}</div>
|
||||
{ bugReportingSection }
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("FAQ")}</span>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
|
@ -268,6 +270,11 @@ export default class HelpUserSettingsTab extends React.Component {
|
|||
data-spoiler={MatrixClientPeg.get().getAccessToken()}>
|
||||
<{ _t("click to reveal") }>
|
||||
</AccessibleButton>
|
||||
<div className='mx_HelpUserSettingsTab_debugButton'>
|
||||
<AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'>
|
||||
{_t("Clear cache and reload")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -49,11 +49,10 @@ export default class PreferencesUserSettingsTab extends React.Component {
|
|||
'showAvatarChanges',
|
||||
'showDisplaynameChanges',
|
||||
'showImages',
|
||||
'Pill.shouldShowPillAvatar',
|
||||
];
|
||||
|
||||
static ADVANCED_SETTINGS = [
|
||||
'alwaysShowEncryptionIcons',
|
||||
'Pill.shouldShowPillAvatar',
|
||||
static GENERAL_SETTINGS = [
|
||||
'TagPanel.enableTagPanel',
|
||||
'promptBeforeInviteUnknownUsers',
|
||||
// Start automatically after startup (electron-only)
|
||||
|
@ -139,7 +138,9 @@ export default class PreferencesUserSettingsTab extends React.Component {
|
|||
|
||||
_renderGroup(settingIds) {
|
||||
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() {
|
||||
|
@ -187,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}
|
||||
|
|
|
@ -29,6 +29,10 @@ import {sleep} from "../../../../../utils/promise";
|
|||
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 = {
|
||||
|
@ -102,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()},
|
||||
);
|
||||
};
|
||||
|
@ -216,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>
|
||||
|
@ -230,8 +243,7 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
</li>
|
||||
</ul>
|
||||
{importExportButtons}
|
||||
<SettingsFlag name='blacklistUnverifiedDevices' level={SettingLevel.DEVICE}
|
||||
onChange={this._updateBlacklistDevicesFlag} />
|
||||
{noSendUnverifiedSetting}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -288,12 +300,11 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag');
|
||||
const EventIndexPanel = sdk.getComponent('views.settings.EventIndexPanel');
|
||||
|
||||
const KeyBackupPanel = sdk.getComponent('views.settings.KeyBackupPanel');
|
||||
const keyBackup = (
|
||||
const secureBackup = (
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<span className="mx_SettingsTab_subheading">{_t("Key backup")}</span>
|
||||
<span className="mx_SettingsTab_subheading">{_t("Secure Backup")}</span>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
<KeyBackupPanel />
|
||||
<SecureBackupPanel />
|
||||
</div>
|
||||
</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()) {
|
||||
|
@ -329,12 +338,53 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
</div>;
|
||||
}
|
||||
|
||||
let privacySection;
|
||||
if (Analytics.canEnable()) {
|
||||
privacySection = <React.Fragment>
|
||||
<div className="mx_SettingsTab_heading">{_t("Privacy")}</div>
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Analytics")}</span>
|
||||
<div className="mx_SettingsTab_subsectionText">
|
||||
{_t(
|
||||
"%(brand)s collects anonymous analytics to allow us to improve the application.",
|
||||
{ brand },
|
||||
)}
|
||||
|
||||
{_t("Privacy is important to us, so we don't collect any personal or " +
|
||||
"identifiable data for our analytics.")}
|
||||
<AccessibleButton className="mx_SettingsTab_linkBtn" onClick={Analytics.showDetailsModal}>
|
||||
{_t("Learn more about how we use analytics.")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<SettingsFlag name="analyticsOptIn" level={SettingLevel.DEVICE} onChange={this._updateAnalytics} />
|
||||
</div>
|
||||
</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}
|
||||
<div className="mx_SettingsTab_heading">{_t("Security & Privacy")}</div>
|
||||
<div className="mx_SettingsTab_heading">{_t("Where you’re logged in")}</div>
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Where you’re logged in")}</span>
|
||||
<span>
|
||||
{_t(
|
||||
"Manage the names of and sign out of your sessions below or " +
|
||||
|
@ -351,30 +401,15 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
<DevicesPanel />
|
||||
</div>
|
||||
</div>
|
||||
{keyBackup}
|
||||
{eventIndex}
|
||||
{crossSigning}
|
||||
{this._renderCurrentDeviceInfo()}
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<span className="mx_SettingsTab_subheading">{_t("Analytics")}</span>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
{_t(
|
||||
"%(brand)s collects anonymous analytics to allow us to improve the application.",
|
||||
{ brand },
|
||||
)}
|
||||
|
||||
{_t("Privacy is important to us, so we don't collect any personal or " +
|
||||
"identifiable data for our analytics.")}
|
||||
<AccessibleButton className="mx_SettingsTab_linkBtn" onClick={Analytics.showDetailsModal}>
|
||||
{_t("Learn more about how we use analytics.")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<SettingsFlag name='analyticsOptIn' level={SettingLevel.DEVICE}
|
||||
onChange={this._updateAnalytics} />
|
||||
<div className="mx_SettingsTab_heading">{_t("Encryption")}</div>
|
||||
<div className="mx_SettingsTab_section">
|
||||
{secureBackup}
|
||||
{eventIndex}
|
||||
{crossSigning}
|
||||
{this._renderCurrentDeviceInfo()}
|
||||
</div>
|
||||
{this._renderIgnoredUsers()}
|
||||
{this._renderManageInvites()}
|
||||
<E2eAdvancedPanel />
|
||||
{ privacySection }
|
||||
{ advancedSection }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue