Merge branch 'develop' into joriks/fix-filepanel-regression
This commit is contained in:
commit
b61f1704d3
80 changed files with 2126 additions and 2464 deletions
|
@ -43,6 +43,15 @@ import ResizeNotifier from "../../utils/ResizeNotifier";
|
|||
import PlatformPeg from "../../PlatformPeg";
|
||||
import { RoomListStoreTempProxy } from "../../stores/room-list/RoomListStoreTempProxy";
|
||||
import { DefaultTagID } from "../../stores/room-list/models";
|
||||
import {
|
||||
showToast as showSetPasswordToast,
|
||||
hideToast as hideSetPasswordToast
|
||||
} from "../../toasts/SetPasswordToast";
|
||||
import {
|
||||
showToast as showServerLimitToast,
|
||||
hideToast as hideServerLimitToast
|
||||
} from "../../toasts/ServerLimitToast";
|
||||
|
||||
// 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.
|
||||
// NB. this is just for server notices rather than pinned messages in general.
|
||||
|
@ -65,10 +74,6 @@ interface IProps {
|
|||
initialEventPixelOffset: number;
|
||||
leftDisabled: boolean;
|
||||
rightDisabled: boolean;
|
||||
showCookieBar: boolean;
|
||||
hasNewVersion: boolean;
|
||||
userHasGeneratedPassword: boolean;
|
||||
showNotifierToolbar: boolean;
|
||||
page_type: string;
|
||||
autoJoin: boolean;
|
||||
thirdPartyInvite?: object;
|
||||
|
@ -86,10 +91,8 @@ interface IProps {
|
|||
currentUserId?: string;
|
||||
currentGroupId?: string;
|
||||
currentGroupIsNew?: boolean;
|
||||
version?: string;
|
||||
newVersion?: string;
|
||||
newVersionReleaseNotes?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
mouseDown?: {
|
||||
x: number;
|
||||
|
@ -97,8 +100,6 @@ interface IState {
|
|||
};
|
||||
syncErrorData: any;
|
||||
useCompactLayout: boolean;
|
||||
serverNoticeEvents: MatrixEvent[];
|
||||
userHasGeneratedPassword: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -141,11 +142,8 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
this.state = {
|
||||
mouseDown: undefined,
|
||||
syncErrorData: undefined,
|
||||
userHasGeneratedPassword: false,
|
||||
// use compact timeline view
|
||||
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
|
||||
// any currently active server notice events
|
||||
serverNoticeEvents: [],
|
||||
};
|
||||
|
||||
// stash the MatrixClient in case we log out before we are unmounted
|
||||
|
@ -182,10 +180,7 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
componentDidUpdate(prevProps, prevState) {
|
||||
// attempt to guess when a banner was opened or closed
|
||||
if (
|
||||
(prevProps.showCookieBar !== this.props.showCookieBar) ||
|
||||
(prevProps.hasNewVersion !== this.props.hasNewVersion) ||
|
||||
(prevState.userHasGeneratedPassword !== this.state.userHasGeneratedPassword) ||
|
||||
(prevProps.showNotifierToolbar !== this.props.showNotifierToolbar)
|
||||
(prevProps.checkingForUpdate !== this.props.checkingForUpdate)
|
||||
) {
|
||||
this.props.resizeNotifier.notifyBannersChanged();
|
||||
}
|
||||
|
@ -220,9 +215,11 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
};
|
||||
|
||||
_setStateFromSessionStore = () => {
|
||||
this.setState({
|
||||
userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()),
|
||||
});
|
||||
if (this._sessionStore.getCachedPassword()) {
|
||||
showSetPasswordToast();
|
||||
} else {
|
||||
hideSetPasswordToast();
|
||||
}
|
||||
};
|
||||
|
||||
_createResizer() {
|
||||
|
@ -294,6 +291,8 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
|
||||
if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') {
|
||||
this._updateServerNoticeEvents();
|
||||
} else {
|
||||
this._calculateServerLimitToast(data);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -304,11 +303,24 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
_calculateServerLimitToast(syncErrorData, usageLimitEventContent?) {
|
||||
const error = syncErrorData && syncErrorData.error && syncErrorData.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED";
|
||||
if (error) {
|
||||
usageLimitEventContent = syncErrorData.error.data;
|
||||
}
|
||||
|
||||
if (usageLimitEventContent) {
|
||||
showServerLimitToast(usageLimitEventContent.limit_type, usageLimitEventContent.admin_contact, error);
|
||||
} else {
|
||||
hideServerLimitToast();
|
||||
}
|
||||
}
|
||||
|
||||
_updateServerNoticeEvents = async () => {
|
||||
const roomLists = RoomListStoreTempProxy.getRoomLists();
|
||||
if (!roomLists[DefaultTagID.ServerNotice]) return [];
|
||||
|
||||
const pinnedEvents = [];
|
||||
const events = [];
|
||||
for (const room of roomLists[DefaultTagID.ServerNotice]) {
|
||||
const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", "");
|
||||
|
||||
|
@ -318,12 +330,18 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
for (const eventId of pinnedEventIds) {
|
||||
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0);
|
||||
const event = timeline.getEvents().find(ev => ev.getId() === eventId);
|
||||
if (event) pinnedEvents.push(event);
|
||||
if (event) events.push(event);
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
serverNoticeEvents: pinnedEvents,
|
||||
|
||||
const usageLimitEvent = events.find((e) => {
|
||||
return (
|
||||
e && e.getType() === 'm.room.message' &&
|
||||
e.getContent()['server_notice_type'] === 'm.server_notice.usage_limit_reached'
|
||||
);
|
||||
});
|
||||
|
||||
this._calculateServerLimitToast(this.state.syncErrorData, usageLimitEvent && usageLimitEvent.getContent());
|
||||
};
|
||||
|
||||
_onPaste = (ev) => {
|
||||
|
@ -599,12 +617,7 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
const GroupView = sdk.getComponent('structures.GroupView');
|
||||
const MyGroups = sdk.getComponent('structures.MyGroups');
|
||||
const ToastContainer = sdk.getComponent('structures.ToastContainer');
|
||||
const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
|
||||
const CookieBar = sdk.getComponent('globals.CookieBar');
|
||||
const NewVersionBar = sdk.getComponent('globals.NewVersionBar');
|
||||
const UpdateCheckBar = sdk.getComponent('globals.UpdateCheckBar');
|
||||
const PasswordNagBar = sdk.getComponent('globals.PasswordNagBar');
|
||||
const ServerLimitBar = sdk.getComponent('globals.ServerLimitBar');
|
||||
|
||||
let pageElement;
|
||||
|
||||
|
@ -648,40 +661,9 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
break;
|
||||
}
|
||||
|
||||
const usageLimitEvent = this.state.serverNoticeEvents.find((e) => {
|
||||
return (
|
||||
e && e.getType() === 'm.room.message' &&
|
||||
e.getContent()['server_notice_type'] === 'm.server_notice.usage_limit_reached'
|
||||
);
|
||||
});
|
||||
|
||||
let topBar;
|
||||
if (this.state.syncErrorData && this.state.syncErrorData.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
topBar = <ServerLimitBar kind='hard'
|
||||
adminContact={this.state.syncErrorData.error.data.admin_contact}
|
||||
limitType={this.state.syncErrorData.error.data.limit_type}
|
||||
/>;
|
||||
} else if (usageLimitEvent) {
|
||||
topBar = <ServerLimitBar kind='soft'
|
||||
adminContact={usageLimitEvent.getContent().admin_contact}
|
||||
limitType={usageLimitEvent.getContent().limit_type}
|
||||
/>;
|
||||
} else if (this.props.showCookieBar &&
|
||||
this.props.config.piwik &&
|
||||
navigator.doNotTrack !== "1"
|
||||
) {
|
||||
const policyUrl = this.props.config.piwik.policyUrl || null;
|
||||
topBar = <CookieBar policyUrl={policyUrl} />;
|
||||
} else if (this.props.hasNewVersion) {
|
||||
topBar = <NewVersionBar version={this.props.version} newVersion={this.props.newVersion}
|
||||
releaseNotes={this.props.newVersionReleaseNotes}
|
||||
/>;
|
||||
} else if (this.props.checkingForUpdate) {
|
||||
if (this.props.checkingForUpdate) {
|
||||
topBar = <UpdateCheckBar {...this.props.checkingForUpdate} />;
|
||||
} else if (this.state.userHasGeneratedPassword) {
|
||||
topBar = <PasswordNagBar />;
|
||||
} else if (this.props.showNotifierToolbar) {
|
||||
topBar = <MatrixToolbar />;
|
||||
}
|
||||
|
||||
let bodyClasses = 'mx_MatrixChat';
|
||||
|
|
|
@ -49,7 +49,6 @@ import PageTypes from '../../PageTypes';
|
|||
import { getHomePageUrl } from '../../utils/pages';
|
||||
|
||||
import createRoom from "../../createRoom";
|
||||
import KeyRequestHandler from '../../KeyRequestHandler';
|
||||
import { _t, getCurrentLanguage } from '../../languageHandler';
|
||||
import SettingsStore, { SettingLevel } from "../../settings/SettingsStore";
|
||||
import ThemeController from "../../settings/controllers/ThemeController";
|
||||
|
@ -59,8 +58,8 @@ import ResizeNotifier from "../../utils/ResizeNotifier";
|
|||
import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
|
||||
import DMRoomMap from '../../utils/DMRoomMap';
|
||||
import { countRoomsWithNotif } from '../../RoomNotifs';
|
||||
import { ThemeWatcher } from "../../theme";
|
||||
import { FontWatcher } from '../../FontWatcher';
|
||||
import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
|
||||
import { FontWatcher } from '../../settings/watchers/FontWatcher';
|
||||
import { storeRoomAliasInCache } from '../../RoomAliasCache';
|
||||
import { defer, IDeferred } from "../../utils/promise";
|
||||
import ToastStore from "../../stores/ToastStore";
|
||||
|
@ -68,6 +67,11 @@ import * as StorageManager from "../../utils/StorageManager";
|
|||
import type LoggedInViewType from "./LoggedInView";
|
||||
import { ViewUserPayload } from "../../dispatcher/payloads/ViewUserPayload";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import {
|
||||
showToast as showAnalyticsToast,
|
||||
hideToast as hideAnalyticsToast
|
||||
} from "../../toasts/AnalyticsToast";
|
||||
import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast";
|
||||
|
||||
/** constants for MatrixChat.state.view */
|
||||
export enum Views {
|
||||
|
@ -169,12 +173,7 @@ interface IState {
|
|||
leftDisabled: boolean;
|
||||
middleDisabled: boolean;
|
||||
// the right panel's disabled state is tracked in its store.
|
||||
version?: string;
|
||||
newVersion?: string;
|
||||
hasNewVersion: boolean;
|
||||
newVersionReleaseNotes?: string;
|
||||
checkingForUpdate?: string; // updateCheckStatusEnum
|
||||
showCookieBar: boolean;
|
||||
// Parameters used in the registration dance with the IS
|
||||
register_client_secret?: string;
|
||||
register_session_id?: string;
|
||||
|
@ -184,7 +183,6 @@ interface IState {
|
|||
hideToSRUsers: boolean;
|
||||
syncError?: Error;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
showNotifierToolbar: boolean;
|
||||
serverConfig?: ValidatedServerConfig;
|
||||
ready: boolean;
|
||||
thirdPartyInvite?: object;
|
||||
|
@ -228,17 +226,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
leftDisabled: false,
|
||||
middleDisabled: false,
|
||||
|
||||
hasNewVersion: false,
|
||||
newVersionReleaseNotes: null,
|
||||
checkingForUpdate: null,
|
||||
|
||||
showCookieBar: false,
|
||||
|
||||
hideToSRUsers: false,
|
||||
|
||||
syncError: null, // If the current syncing status is ERROR, the error object, otherwise null.
|
||||
resizeNotifier: new ResizeNotifier(),
|
||||
showNotifierToolbar: false,
|
||||
ready: false,
|
||||
};
|
||||
|
||||
|
@ -339,12 +332,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
}
|
||||
|
||||
if (SettingsStore.getValue("showCookieBar")) {
|
||||
this.setState({
|
||||
showCookieBar: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (SettingsStore.getValue("analyticsOptIn")) {
|
||||
Analytics.enable();
|
||||
}
|
||||
|
@ -686,9 +673,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
dis.dispatch({action: 'view_my_groups'});
|
||||
}
|
||||
break;
|
||||
case 'notifier_enabled':
|
||||
this.setState({showNotifierToolbar: Notifier.shouldShowToolbar()});
|
||||
break;
|
||||
case 'hide_left_panel':
|
||||
this.setState({
|
||||
collapseLhs: true,
|
||||
|
@ -736,12 +720,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
case 'client_started':
|
||||
this.onClientStarted();
|
||||
break;
|
||||
case 'new_version':
|
||||
this.onVersion(
|
||||
payload.currentVersion, payload.newVersion,
|
||||
payload.releaseNotes,
|
||||
);
|
||||
break;
|
||||
case 'check_updates':
|
||||
this.setState({ checkingForUpdate: payload.value });
|
||||
break;
|
||||
|
@ -761,19 +739,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
case 'accept_cookies':
|
||||
SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, true);
|
||||
SettingsStore.setValue("showCookieBar", null, SettingLevel.DEVICE, false);
|
||||
|
||||
this.setState({
|
||||
showCookieBar: false,
|
||||
});
|
||||
hideAnalyticsToast();
|
||||
Analytics.enable();
|
||||
break;
|
||||
case 'reject_cookies':
|
||||
SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, false);
|
||||
SettingsStore.setValue("showCookieBar", null, SettingLevel.DEVICE, false);
|
||||
|
||||
this.setState({
|
||||
showCookieBar: false,
|
||||
});
|
||||
hideAnalyticsToast();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
@ -1262,6 +1234,10 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
private showScreenAfterLogin() {
|
||||
|
@ -1389,10 +1365,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
this.firstSyncComplete = true;
|
||||
this.firstSyncPromise.resolve();
|
||||
|
||||
if (Notifier.shouldShowToolbar()) {
|
||||
showNotificationsToast();
|
||||
}
|
||||
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
this.setState({
|
||||
ready: true,
|
||||
showNotifierToolbar: Notifier.shouldShowToolbar(),
|
||||
});
|
||||
});
|
||||
cli.on('Call.incoming', function(call) {
|
||||
|
@ -1471,16 +1450,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
cli.on("Session.logged_out", () => dft.stop());
|
||||
cli.on("Event.decrypted", (e, err) => dft.eventDecrypted(e, err));
|
||||
|
||||
// TODO: We can remove this once cross-signing is the only way.
|
||||
// https://github.com/vector-im/riot-web/issues/11908
|
||||
const krh = new KeyRequestHandler(cli);
|
||||
cli.on("crypto.roomKeyRequest", (req) => {
|
||||
krh.handleKeyRequest(req);
|
||||
});
|
||||
cli.on("crypto.roomKeyRequestCancellation", (req) => {
|
||||
krh.handleKeyRequestCancellation(req);
|
||||
});
|
||||
|
||||
cli.on("Room", (room) => {
|
||||
if (MatrixClientPeg.get().isCryptoEnabled()) {
|
||||
const blacklistEnabled = SettingsStore.getValueAt(
|
||||
|
@ -1570,7 +1539,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
icon: "verification",
|
||||
props: {request},
|
||||
component: sdk.getComponent("toasts.VerificationRequestToast"),
|
||||
priority: ToastStore.PRIORITY_REALTIME,
|
||||
priority: 90,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -1844,16 +1813,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
this.showScreen("settings");
|
||||
};
|
||||
|
||||
onVersion(current: string, latest: string, releaseNotes?: string) {
|
||||
this.setState({
|
||||
version: current,
|
||||
newVersion: latest,
|
||||
hasNewVersion: current !== latest,
|
||||
newVersionReleaseNotes: releaseNotes,
|
||||
checkingForUpdate: null,
|
||||
});
|
||||
}
|
||||
|
||||
onSendEvent(roomId: string, event: MatrixEvent) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (!cli) {
|
||||
|
@ -2048,7 +2007,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
onCloseAllSettings={this.onCloseAllSettings}
|
||||
onRegistered={this.onRegistered}
|
||||
currentRoomId={this.state.currentRoomId}
|
||||
showCookieBar={this.state.showCookieBar}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
|
|
|
@ -164,7 +164,10 @@ export default createReactClass({
|
|||
|
||||
canReact: false,
|
||||
canReply: false,
|
||||
|
||||
useIRCLayout: SettingsStore.getValue("feature_irc_ui"),
|
||||
|
||||
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -235,7 +238,8 @@ export default createReactClass({
|
|||
initialEventId: RoomViewStore.getInitialEventId(),
|
||||
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
|
||||
forwardingEvent: RoomViewStore.getForwardingEvent(),
|
||||
shouldPeek: RoomViewStore.shouldPeek(),
|
||||
// we should only peek once we have a ready client
|
||||
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
|
||||
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId),
|
||||
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
|
||||
};
|
||||
|
@ -692,6 +696,16 @@ export default createReactClass({
|
|||
});
|
||||
}
|
||||
break;
|
||||
case 'sync_state':
|
||||
if (!this.state.matrixClientIsReady) {
|
||||
this.setState({
|
||||
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
|
||||
}, () => {
|
||||
// send another "initial" RVS update to trigger peeking if needed
|
||||
this._onRoomViewStoreUpdate(true);
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -1674,14 +1688,16 @@ export default createReactClass({
|
|||
const ErrorBoundary = sdk.getComponent("elements.ErrorBoundary");
|
||||
|
||||
if (!this.state.room) {
|
||||
const loading = this.state.roomLoading || this.state.peekLoading;
|
||||
const loading = !this.state.matrixClientIsReady || this.state.roomLoading || this.state.peekLoading;
|
||||
if (loading) {
|
||||
// Assume preview loading if we don't have a ready client or a room ID (still resolving the alias)
|
||||
const previewLoading = !this.state.matrixClientIsReady || !this.state.roomId || this.state.peekLoading;
|
||||
return (
|
||||
<div className="mx_RoomView">
|
||||
<ErrorBoundary>
|
||||
<RoomPreviewBar
|
||||
canPreview={false}
|
||||
previewLoading={this.state.peekLoading}
|
||||
previewLoading={previewLoading && !this.state.roomLoadError}
|
||||
error={this.state.roomLoadError}
|
||||
loading={loading}
|
||||
joining={this.state.joining}
|
||||
|
@ -1706,7 +1722,8 @@ export default createReactClass({
|
|||
return (
|
||||
<div className="mx_RoomView">
|
||||
<ErrorBoundary>
|
||||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
|
||||
<RoomPreviewBar
|
||||
onJoinClick={this.onJoinButtonClicked}
|
||||
onForgetClick={this.onForgetClick}
|
||||
onRejectClick={this.onRejectThreepidInviteButtonClicked}
|
||||
canPreview={false} error={this.state.roomLoadError}
|
||||
|
|
|
@ -15,13 +15,21 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import ToastStore from "../../stores/ToastStore";
|
||||
import ToastStore, {IToast} from "../../stores/ToastStore";
|
||||
import classNames from "classnames";
|
||||
|
||||
export default class ToastContainer extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {toasts: ToastStore.sharedInstance().getToasts()};
|
||||
interface IState {
|
||||
toasts: IToast<any>[];
|
||||
countSeen: number;
|
||||
}
|
||||
|
||||
export default class ToastContainer extends React.Component<{}, IState> {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
toasts: ToastStore.sharedInstance().getToasts(),
|
||||
countSeen: ToastStore.sharedInstance().getCountSeen(),
|
||||
};
|
||||
|
||||
// Start listening here rather than in componentDidMount because
|
||||
// toasts may dismiss themselves in their didMount if they find
|
||||
|
@ -35,7 +43,10 @@ export default class ToastContainer extends React.Component {
|
|||
}
|
||||
|
||||
_onToastStoreUpdate = () => {
|
||||
this.setState({toasts: ToastStore.sharedInstance().getToasts()});
|
||||
this.setState({
|
||||
toasts: ToastStore.sharedInstance().getToasts(),
|
||||
countSeen: ToastStore.sharedInstance().getCountSeen(),
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -51,8 +62,8 @@ export default class ToastContainer extends React.Component {
|
|||
});
|
||||
|
||||
let countIndicator;
|
||||
if (isStacked) {
|
||||
countIndicator = `(1/${totalCount})`;
|
||||
if (isStacked || this.state.countSeen > 0) {
|
||||
countIndicator = ` (${this.state.countSeen + 1}/${this.state.countSeen + totalCount})`;
|
||||
}
|
||||
|
||||
const toastProps = Object.assign({}, props, {
|
|
@ -538,6 +538,7 @@ export const MsisdnAuthEntry = createReactClass({
|
|||
type: MsisdnAuthEntry.LOGIN_TYPE,
|
||||
// TODO: Remove `threepid_creds` once servers support proper UIA
|
||||
// See https://github.com/vector-im/riot-web/issues/10312
|
||||
// See https://github.com/matrix-org/matrix-doc/issues/2220
|
||||
threepid_creds: creds,
|
||||
threepidCreds: creds,
|
||||
});
|
||||
|
|
|
@ -36,7 +36,7 @@ interface IProps {
|
|||
labelStrongPassword?: string;
|
||||
labelAllowedButUnsafe?: string;
|
||||
|
||||
onChange(ev: KeyboardEvent);
|
||||
onChange(ev: React.FormEvent<HTMLElement>);
|
||||
onValidate(result: IValidationResult);
|
||||
}
|
||||
|
||||
|
|
|
@ -238,7 +238,7 @@ export default class PasswordLogin extends React.Component {
|
|||
type="text"
|
||||
label={_t("Phone")}
|
||||
value={this.state.phoneNumber}
|
||||
prefix={phoneCountry}
|
||||
prefixComponent={phoneCountry}
|
||||
onChange={this.onPhoneNumberChanged}
|
||||
onBlur={this.onPhoneNumberBlur}
|
||||
disabled={this.props.disableSubmit}
|
||||
|
|
|
@ -473,7 +473,7 @@ export default createReactClass({
|
|||
type="text"
|
||||
label={phoneLabel}
|
||||
value={this.state.phoneNumber}
|
||||
prefix={phoneCountry}
|
||||
prefixComponent={phoneCountry}
|
||||
onChange={this.onPhoneNumberChange}
|
||||
onValidate={this.onPhoneNumberValidate}
|
||||
/>;
|
||||
|
|
|
@ -18,10 +18,10 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import * as Avatar from '../../../Avatar';
|
||||
import * as sdk from "../../../index";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'MemberAvatar',
|
||||
|
@ -62,10 +62,14 @@ export default createReactClass({
|
|||
return {
|
||||
name: props.member.name,
|
||||
title: props.title || props.member.userId,
|
||||
imageUrl: Avatar.avatarUrlForMember(props.member,
|
||||
props.width,
|
||||
props.height,
|
||||
props.resizeMethod),
|
||||
imageUrl: props.member.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
};
|
||||
} else if (props.fallbackUserId) {
|
||||
return {
|
||||
|
|
|
@ -116,11 +116,6 @@ export default createReactClass({
|
|||
this.closeMenu();
|
||||
},
|
||||
|
||||
e2eInfoClicked: function() {
|
||||
this.props.e2eInfoCallback();
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
onReportEventClick: function() {
|
||||
const ReportEventDialog = sdk.getComponent("dialogs.ReportEventDialog");
|
||||
Modal.createTrackedDialog('Report Event', '', ReportEventDialog, {
|
||||
|
@ -465,15 +460,6 @@ export default createReactClass({
|
|||
);
|
||||
}
|
||||
|
||||
let e2eInfo;
|
||||
if (this.props.e2eInfoCallback) {
|
||||
e2eInfo = (
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.e2eInfoClicked}>
|
||||
{ _t('End-to-end encryption information') }
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
let reportEventButton;
|
||||
if (mxEvent.getSender() !== me) {
|
||||
reportEventButton = (
|
||||
|
@ -500,7 +486,6 @@ export default createReactClass({
|
|||
{ quoteButton }
|
||||
{ externalURLButton }
|
||||
{ collapseReplyThread }
|
||||
{ e2eInfo }
|
||||
{ reportEventButton }
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -42,11 +42,9 @@ export default (props) => {
|
|||
};
|
||||
|
||||
const description =
|
||||
_t("You've previously used a newer version of Riot on %(host)s. " +
|
||||
_t("You've previously used a newer version of Riot with this session. " +
|
||||
"To use this version again with end to end encryption, you will " +
|
||||
"need to sign out and back in again. ",
|
||||
{host: props.host},
|
||||
);
|
||||
"need to sign out and back in again.");
|
||||
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
|
|
@ -1,178 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Modal from '../../../Modal';
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
|
||||
// TODO: We can remove this once cross-signing is the only way.
|
||||
// https://github.com/vector-im/riot-web/issues/11908
|
||||
|
||||
/**
|
||||
* Dialog which asks the user whether they want to share their keys with
|
||||
* an unverified device.
|
||||
*
|
||||
* onFinished is called with `true` if the key should be shared, `false` if it
|
||||
* should not, and `undefined` if the dialog is cancelled. (In other words:
|
||||
* truthy: do the key share. falsy: don't share the keys).
|
||||
*/
|
||||
export default createReactClass({
|
||||
propTypes: {
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
deviceId: PropTypes.string.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
deviceInfo: null,
|
||||
wasNewDevice: false,
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this._unmounted = false;
|
||||
const userId = this.props.userId;
|
||||
const deviceId = this.props.deviceId;
|
||||
|
||||
// give the client a chance to refresh the device list
|
||||
this.props.matrixClient.downloadKeys([userId], false).then((r) => {
|
||||
if (this._unmounted) { return; }
|
||||
|
||||
const deviceInfo = r[userId][deviceId];
|
||||
|
||||
if (!deviceInfo) {
|
||||
console.warn(`No details found for session ${userId}:${deviceId}`);
|
||||
|
||||
this.props.onFinished(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const wasNewDevice = !deviceInfo.isKnown();
|
||||
|
||||
this.setState({
|
||||
deviceInfo: deviceInfo,
|
||||
wasNewDevice: wasNewDevice,
|
||||
});
|
||||
|
||||
// if the device was new before, it's not any more.
|
||||
if (wasNewDevice) {
|
||||
this.props.matrixClient.setDeviceKnown(
|
||||
userId,
|
||||
deviceId,
|
||||
true,
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._unmounted = true;
|
||||
},
|
||||
|
||||
|
||||
_onVerifyClicked: function() {
|
||||
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
|
||||
|
||||
console.log("KeyShareDialog: Starting verify dialog");
|
||||
Modal.createTrackedDialog('Key Share', 'Starting dialog', DeviceVerifyDialog, {
|
||||
userId: this.props.userId,
|
||||
device: this.state.deviceInfo,
|
||||
onFinished: (verified) => {
|
||||
if (verified) {
|
||||
// can automatically share the keys now.
|
||||
this.props.onFinished(true);
|
||||
}
|
||||
},
|
||||
}, null, /* priority = */ false, /* static = */ true);
|
||||
},
|
||||
|
||||
_onShareClicked: function() {
|
||||
console.log("KeyShareDialog: User clicked 'share'");
|
||||
this.props.onFinished(true);
|
||||
},
|
||||
|
||||
_onIgnoreClicked: function() {
|
||||
console.log("KeyShareDialog: User clicked 'ignore'");
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
_renderContent: function() {
|
||||
const displayName = this.state.deviceInfo.getDisplayName() ||
|
||||
this.state.deviceInfo.deviceId;
|
||||
|
||||
let text;
|
||||
if (this.state.wasNewDevice) {
|
||||
text = _td("You added a new session '%(displayName)s', which is"
|
||||
+ " requesting encryption keys.");
|
||||
} else {
|
||||
text = _td("Your unverified session '%(displayName)s' is requesting"
|
||||
+ " encryption keys.");
|
||||
}
|
||||
text = _t(text, {displayName: displayName});
|
||||
|
||||
return (
|
||||
<div id='mx_Dialog_content'>
|
||||
<p>{ text }</p>
|
||||
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this._onVerifyClicked} autoFocus="true">
|
||||
{ _t('Start verification') }
|
||||
</button>
|
||||
<button onClick={this._onShareClicked}>
|
||||
{ _t('Share without verifying') }
|
||||
</button>
|
||||
<button onClick={this._onIgnoreClicked}>
|
||||
{ _t('Ignore request') }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
||||
|
||||
let content;
|
||||
|
||||
if (this.state.deviceInfo) {
|
||||
content = this._renderContent();
|
||||
} else {
|
||||
content = (
|
||||
<div id='mx_Dialog_content'>
|
||||
<p>{ _t('Loading session info...') }</p>
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className='mx_KeyShareRequestDialog'
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t('Encryption key request')}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
{ content }
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -15,10 +15,10 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import * as sdk from '../../../index';
|
||||
import { debounce } from 'lodash';
|
||||
import {IFieldState, IValidationResult} from "../elements/Validation";
|
||||
|
||||
// Invoke validation from user input (when typing, etc.) at most once every N ms.
|
||||
const VALIDATION_THROTTLE_MS = 200;
|
||||
|
@ -29,58 +29,93 @@ function getId() {
|
|||
return `${BASE_ID}_${count++}`;
|
||||
}
|
||||
|
||||
export default class Field extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// The field's ID, which binds the input and label together. Immutable.
|
||||
id: PropTypes.string,
|
||||
// The element to create. Defaults to "input".
|
||||
// To define options for a select, use <Field><option ... /></Field>
|
||||
element: PropTypes.oneOf(["input", "select", "textarea"]),
|
||||
// The field's type (when used as an <input>). Defaults to "text".
|
||||
type: PropTypes.string,
|
||||
// id of a <datalist> element for suggestions
|
||||
list: PropTypes.string,
|
||||
// The field's label string.
|
||||
label: PropTypes.string,
|
||||
// The field's placeholder string. Defaults to the label.
|
||||
placeholder: PropTypes.string,
|
||||
// The field's value.
|
||||
// This is a controlled component, so the value is required.
|
||||
value: PropTypes.string.isRequired,
|
||||
// Optional component to include inside the field before the input.
|
||||
prefix: PropTypes.node,
|
||||
// Optional component to include inside the field after the input.
|
||||
postfix: PropTypes.node,
|
||||
// The callback called whenever the contents of the field
|
||||
// changes. Returns an object with `valid` boolean field
|
||||
// and a `feedback` react component field to provide feedback
|
||||
// to the user.
|
||||
onValidate: PropTypes.func,
|
||||
// If specified, overrides the value returned by onValidate.
|
||||
flagInvalid: PropTypes.bool,
|
||||
// If specified, contents will appear as a tooltip on the element and
|
||||
// validation feedback tooltips will be suppressed.
|
||||
tooltipContent: PropTypes.node,
|
||||
// If specified alongside tooltipContent, the class name to apply to the
|
||||
// tooltip itself.
|
||||
tooltipClassName: PropTypes.string,
|
||||
// If specified, an additional class name to apply to the field container
|
||||
className: PropTypes.string,
|
||||
// All other props pass through to the <input>.
|
||||
};
|
||||
interface IProps extends React.InputHTMLAttributes<HTMLSelectElement | HTMLInputElement> {
|
||||
// The field's ID, which binds the input and label together. Immutable.
|
||||
id?: string,
|
||||
// The element to create. Defaults to "input".
|
||||
// To define options for a select, use <Field><option ... /></Field>
|
||||
element?: "input" | "select" | "textarea",
|
||||
// The field's type (when used as an <input>). Defaults to "text".
|
||||
type?: string,
|
||||
// id of a <datalist> element for suggestions
|
||||
list?: string,
|
||||
// The field's label string.
|
||||
label?: string,
|
||||
// The field's placeholder string. Defaults to the label.
|
||||
placeholder?: string,
|
||||
// The field's value.
|
||||
// This is a controlled component, so the value is required.
|
||||
value: string,
|
||||
// Optional component to include inside the field before the input.
|
||||
prefixComponent?: React.ReactNode,
|
||||
// Optional component to include inside the field after the input.
|
||||
postfixComponent?: React.ReactNode,
|
||||
// The callback called whenever the contents of the field
|
||||
// changes. Returns an object with `valid` boolean field
|
||||
// and a `feedback` react component field to provide feedback
|
||||
// to the user.
|
||||
onValidate?: (input: IFieldState) => Promise<IValidationResult>,
|
||||
// If specified, overrides the value returned by onValidate.
|
||||
flagInvalid?: boolean,
|
||||
// If specified, contents will appear as a tooltip on the element and
|
||||
// validation feedback tooltips will be suppressed.
|
||||
tooltipContent?: React.ReactNode,
|
||||
// If specified alongside tooltipContent, the class name to apply to the
|
||||
// tooltip itself.
|
||||
tooltipClassName?: string,
|
||||
// If specified, an additional class name to apply to the field container
|
||||
className?: string,
|
||||
// All other props pass through to the <input>.
|
||||
}
|
||||
|
||||
interface IState {
|
||||
valid: boolean,
|
||||
feedback: React.ReactNode,
|
||||
feedbackVisible: boolean,
|
||||
focused: boolean,
|
||||
}
|
||||
|
||||
export default class Field extends React.PureComponent<IProps, IState> {
|
||||
private id: string;
|
||||
private input: HTMLInputElement;
|
||||
|
||||
private static defaultProps = {
|
||||
element: "input",
|
||||
type: "text",
|
||||
}
|
||||
|
||||
/*
|
||||
* This was changed from throttle to debounce: this is more traditional for
|
||||
* form validation since it means that the validation doesn't happen at all
|
||||
* until the user stops typing for a bit (debounce defaults to not running on
|
||||
* the leading edge). If we're doing an HTTP hit on each validation, we have more
|
||||
* incentive to prevent validating input that's very unlikely to be valid.
|
||||
* We may find that we actually want different behaviour for registration
|
||||
* fields, in which case we can add some options to control it.
|
||||
*/
|
||||
private validateOnChange = debounce(() => {
|
||||
this.validate({
|
||||
focused: true,
|
||||
});
|
||||
}, VALIDATION_THROTTLE_MS);
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
valid: undefined,
|
||||
feedback: undefined,
|
||||
feedbackVisible: false,
|
||||
focused: false,
|
||||
};
|
||||
|
||||
this.id = this.props.id || getId();
|
||||
}
|
||||
|
||||
onFocus = (ev) => {
|
||||
public focus() {
|
||||
this.input.focus();
|
||||
}
|
||||
|
||||
private onFocus = (ev) => {
|
||||
this.setState({
|
||||
focused: true,
|
||||
});
|
||||
|
@ -93,7 +128,7 @@ export default class Field extends React.PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
onChange = (ev) => {
|
||||
private onChange = (ev) => {
|
||||
this.validateOnChange();
|
||||
// Parent component may have supplied its own `onChange` as well
|
||||
if (this.props.onChange) {
|
||||
|
@ -101,7 +136,7 @@ export default class Field extends React.PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
onBlur = (ev) => {
|
||||
private onBlur = (ev) => {
|
||||
this.setState({
|
||||
focused: false,
|
||||
});
|
||||
|
@ -114,11 +149,7 @@ export default class Field extends React.PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
focus() {
|
||||
this.input.focus();
|
||||
}
|
||||
|
||||
async validate({ focused, allowEmpty = true }) {
|
||||
private async validate({ focused, allowEmpty = true }: {focused: boolean, allowEmpty?: boolean}) {
|
||||
if (!this.props.onValidate) {
|
||||
return;
|
||||
}
|
||||
|
@ -149,56 +180,42 @@ export default class Field extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* This was changed from throttle to debounce: this is more traditional for
|
||||
* form validation since it means that the validation doesn't happen at all
|
||||
* until the user stops typing for a bit (debounce defaults to not running on
|
||||
* the leading edge). If we're doing an HTTP hit on each validation, we have more
|
||||
* incentive to prevent validating input that's very unlikely to be valid.
|
||||
* We may find that we actually want different behaviour for registration
|
||||
* fields, in which case we can add some options to control it.
|
||||
*/
|
||||
validateOnChange = debounce(() => {
|
||||
this.validate({
|
||||
focused: true,
|
||||
});
|
||||
}, VALIDATION_THROTTLE_MS);
|
||||
|
||||
render() {
|
||||
|
||||
public render() {
|
||||
const {
|
||||
element, prefix, postfix, className, onValidate, children,
|
||||
element, prefixComponent, postfixComponent, className, onValidate, children,
|
||||
tooltipContent, flagInvalid, tooltipClassName, list, ...inputProps} = this.props;
|
||||
|
||||
const inputElement = element || "input";
|
||||
|
||||
// Set some defaults for the <input> element
|
||||
inputProps.type = inputProps.type || "text";
|
||||
inputProps.ref = input => this.input = input;
|
||||
const ref = input => this.input = input;
|
||||
inputProps.placeholder = inputProps.placeholder || inputProps.label;
|
||||
inputProps.id = this.id; // this overwrites the id from props
|
||||
|
||||
inputProps.onFocus = this.onFocus;
|
||||
inputProps.onChange = this.onChange;
|
||||
inputProps.onBlur = this.onBlur;
|
||||
inputProps.list = list;
|
||||
|
||||
const fieldInput = React.createElement(inputElement, inputProps, children);
|
||||
// Appease typescript's inference
|
||||
const inputProps_ = {...inputProps, ref, list};
|
||||
|
||||
const fieldInput = React.createElement(this.props.element, inputProps_, children);
|
||||
|
||||
let prefixContainer = null;
|
||||
if (prefix) {
|
||||
prefixContainer = <span className="mx_Field_prefix">{prefix}</span>;
|
||||
if (prefixComponent) {
|
||||
prefixContainer = <span className="mx_Field_prefix">{prefixComponent}</span>;
|
||||
}
|
||||
let postfixContainer = null;
|
||||
if (postfix) {
|
||||
postfixContainer = <span className="mx_Field_postfix">{postfix}</span>;
|
||||
if (postfixComponent) {
|
||||
postfixContainer = <span className="mx_Field_postfix">{postfixComponent}</span>;
|
||||
}
|
||||
|
||||
const hasValidationFlag = flagInvalid !== null && flagInvalid !== undefined;
|
||||
const fieldClasses = classNames("mx_Field", `mx_Field_${inputElement}`, className, {
|
||||
const fieldClasses = classNames("mx_Field", `mx_Field_${this.props.element}`, className, {
|
||||
// If we have a prefix element, leave the label always at the top left and
|
||||
// don't animate it, as it looks a bit clunky and would add complexity to do
|
||||
// properly.
|
||||
mx_Field_labelAlwaysTopLeft: prefix,
|
||||
mx_Field_labelAlwaysTopLeft: prefixComponent,
|
||||
mx_Field_valid: onValidate && this.state.valid === true,
|
||||
mx_Field_invalid: hasValidationFlag
|
||||
? flagInvalid
|
|
@ -47,8 +47,8 @@ export default class RoomAliasField extends React.PureComponent {
|
|||
<Field
|
||||
label={_t("Room address")}
|
||||
className="mx_RoomAliasField"
|
||||
prefix={poundSign}
|
||||
postfix={domain}
|
||||
prefixComponent={poundSign}
|
||||
postfixComponent={domain}
|
||||
ref={ref => this._fieldRef = ref}
|
||||
onValidate={this._onValidate}
|
||||
placeholder={_t("e.g. my-room")}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
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.
|
||||
|
@ -18,67 +18,68 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
|
||||
import React from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import classNames from 'classnames';
|
||||
import { ViewTooltipPayload } from '../../../dispatcher/payloads/ViewTooltipPayload';
|
||||
import { Action } from '../../../dispatcher/actions';
|
||||
|
||||
const MIN_TOOLTIP_HEIGHT = 25;
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'Tooltip',
|
||||
|
||||
propTypes: {
|
||||
interface IProps {
|
||||
// Class applied to the element used to position the tooltip
|
||||
className: PropTypes.string,
|
||||
className: string,
|
||||
// Class applied to the tooltip itself
|
||||
tooltipClassName: PropTypes.string,
|
||||
tooltipClassName?: string,
|
||||
// Whether the tooltip is visible or hidden.
|
||||
// The hidden state allows animating the tooltip away via CSS.
|
||||
// Defaults to visible if unset.
|
||||
visible: PropTypes.bool,
|
||||
visible?: boolean,
|
||||
// the react element to put into the tooltip
|
||||
label: PropTypes.node,
|
||||
},
|
||||
label: React.ReactNode,
|
||||
}
|
||||
|
||||
getDefaultProps() {
|
||||
return {
|
||||
visible: true,
|
||||
};
|
||||
},
|
||||
export default class Tooltip extends React.Component<IProps> {
|
||||
private tooltipContainer: HTMLElement;
|
||||
private tooltip: void | Element | Component<Element, any, any>;
|
||||
private parent: Element;
|
||||
|
||||
|
||||
public static readonly defaultProps = {
|
||||
visible: true,
|
||||
};
|
||||
|
||||
// Create a wrapper for the tooltip outside the parent and attach it to the body element
|
||||
componentDidMount: function() {
|
||||
public componentDidMount() {
|
||||
this.tooltipContainer = document.createElement("div");
|
||||
this.tooltipContainer.className = "mx_Tooltip_wrapper";
|
||||
document.body.appendChild(this.tooltipContainer);
|
||||
window.addEventListener('scroll', this._renderTooltip, true);
|
||||
window.addEventListener('scroll', this.renderTooltip, true);
|
||||
|
||||
this.parent = ReactDOM.findDOMNode(this).parentNode;
|
||||
this.parent = ReactDOM.findDOMNode(this).parentNode as Element;
|
||||
|
||||
this._renderTooltip();
|
||||
},
|
||||
this.renderTooltip();
|
||||
}
|
||||
|
||||
componentDidUpdate: function() {
|
||||
this._renderTooltip();
|
||||
},
|
||||
public componentDidUpdate() {
|
||||
this.renderTooltip();
|
||||
}
|
||||
|
||||
// Remove the wrapper element, as the tooltip has finished using it
|
||||
componentWillUnmount: function() {
|
||||
dis.dispatch({
|
||||
action: 'view_tooltip',
|
||||
public componentWillUnmount() {
|
||||
dis.dispatch<ViewTooltipPayload>({
|
||||
action: Action.ViewTooltip,
|
||||
tooltip: null,
|
||||
parent: null,
|
||||
});
|
||||
|
||||
ReactDOM.unmountComponentAtNode(this.tooltipContainer);
|
||||
document.body.removeChild(this.tooltipContainer);
|
||||
window.removeEventListener('scroll', this._renderTooltip, true);
|
||||
},
|
||||
window.removeEventListener('scroll', this.renderTooltip, true);
|
||||
}
|
||||
|
||||
_updatePosition(style) {
|
||||
private updatePosition(style: {[key: string]: any}) {
|
||||
const parentBox = this.parent.getBoundingClientRect();
|
||||
let offset = 0;
|
||||
if (parentBox.height > MIN_TOOLTIP_HEIGHT) {
|
||||
|
@ -91,16 +92,15 @@ export default createReactClass({
|
|||
style.top = (parentBox.top - 2) + window.pageYOffset + offset;
|
||||
style.left = 6 + parentBox.right + window.pageXOffset;
|
||||
return style;
|
||||
},
|
||||
}
|
||||
|
||||
_renderTooltip: function() {
|
||||
private renderTooltip() {
|
||||
// Add the parent's position to the tooltips, so it's correctly
|
||||
// positioned, also taking into account any window zoom
|
||||
// NOTE: The additional 6 pixels for the left position, is to take account of the
|
||||
// tooltips chevron
|
||||
const parent = ReactDOM.findDOMNode(this).parentNode;
|
||||
let style = {};
|
||||
style = this._updatePosition(style);
|
||||
const parent = ReactDOM.findDOMNode(this).parentNode as Element;
|
||||
const style = this.updatePosition({});
|
||||
// Hide the entire container when not visible. This prevents flashing of the tooltip
|
||||
// if it is not meant to be visible on first mount.
|
||||
style.display = this.props.visible ? "block" : "none";
|
||||
|
@ -118,21 +118,21 @@ export default createReactClass({
|
|||
);
|
||||
|
||||
// Render the tooltip manually, as we wish it not to be rendered within the parent
|
||||
this.tooltip = ReactDOM.render(tooltip, this.tooltipContainer);
|
||||
this.tooltip = ReactDOM.render<Element>(tooltip, this.tooltipContainer);
|
||||
|
||||
// Tell the roomlist about us so it can manipulate us if it wishes
|
||||
dis.dispatch({
|
||||
action: 'view_tooltip',
|
||||
dis.dispatch<ViewTooltipPayload>({
|
||||
action: Action.ViewTooltip,
|
||||
tooltip: this.tooltip,
|
||||
parent: parent,
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
render: function() {
|
||||
public render() {
|
||||
// Render a placeholder
|
||||
return (
|
||||
<div className={this.props.className} >
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
import Analytics from '../../../Analytics';
|
||||
|
||||
export default class CookieBar extends React.Component {
|
||||
static propTypes = {
|
||||
policyUrl: PropTypes.string,
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
onUsageDataClicked(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
Analytics.showDetailsModal();
|
||||
}
|
||||
|
||||
onAccept() {
|
||||
dis.dispatch({
|
||||
action: 'accept_cookies',
|
||||
});
|
||||
}
|
||||
|
||||
onReject() {
|
||||
dis.dispatch({
|
||||
action: 'reject_cookies',
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const toolbarClasses = "mx_MatrixToolbar";
|
||||
return (
|
||||
<div className={toolbarClasses}>
|
||||
<img className="mx_MatrixToolbar_warning" src={require("../../../../res/img/warning.svg")} width="24" height="23" alt="" />
|
||||
<div className="mx_MatrixToolbar_content">
|
||||
{ this.props.policyUrl ? _t(
|
||||
"Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. " +
|
||||
"This will use a cookie " +
|
||||
"(please see our <PolicyLink>Cookie Policy</PolicyLink>).",
|
||||
{},
|
||||
{
|
||||
'UsageDataLink': (sub) => <a
|
||||
className="mx_MatrixToolbar_link"
|
||||
onClick={this.onUsageDataClicked}
|
||||
>
|
||||
{ sub }
|
||||
</a>,
|
||||
// XXX: We need to link to the page that explains our cookies
|
||||
'PolicyLink': (sub) => <a
|
||||
className="mx_MatrixToolbar_link"
|
||||
target="_blank"
|
||||
href={this.props.policyUrl}
|
||||
>
|
||||
{ sub }
|
||||
</a>
|
||||
,
|
||||
},
|
||||
) : _t(
|
||||
"Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. " +
|
||||
"This will use a cookie.",
|
||||
{},
|
||||
{
|
||||
'UsageDataLink': (sub) => <a
|
||||
className="mx_MatrixToolbar_link"
|
||||
onClick={this.onUsageDataClicked}
|
||||
>
|
||||
{ sub }
|
||||
</a>,
|
||||
},
|
||||
) }
|
||||
</div>
|
||||
<AccessibleButton element='button' className="mx_MatrixToolbar_action" onClick={this.onAccept}>
|
||||
{ _t("Yes, I want to help!") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className="mx_MatrixToolbar_close" onClick={this.onReject}>
|
||||
<img src={require("../../../../res/img/cancel.svg")} width="18" height="18" alt={_t('Close')} />
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Notifier from '../../../Notifier';
|
||||
import AccessibleButton from '../../../components/views/elements/AccessibleButton';
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'MatrixToolbar',
|
||||
|
||||
hideToolbar: function() {
|
||||
Notifier.setToolbarHidden(true);
|
||||
},
|
||||
|
||||
onClick: function() {
|
||||
Notifier.setEnabled(true);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div className="mx_MatrixToolbar">
|
||||
<img className="mx_MatrixToolbar_warning" src={require("../../../../res/img/warning.svg")} width="24" height="23" alt="" />
|
||||
<div className="mx_MatrixToolbar_content">
|
||||
{ _t('You are not receiving desktop notifications') } <a className="mx_MatrixToolbar_link" onClick={ this.onClick }> { _t('Enable them now') }</a>
|
||||
</div>
|
||||
<AccessibleButton className="mx_MatrixToolbar_close" onClick={ this.hideToolbar } ><img src={require("../../../../res/img/cancel.svg")} width="18" height="18" alt={_t('Close')} /></AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -1,108 +0,0 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
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 createReactClass from 'create-react-class';
|
||||
import * as sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
import PlatformPeg from '../../../PlatformPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
/**
|
||||
* Check a version string is compatible with the Changelog
|
||||
* dialog ([vectorversion]-react-[react-sdk-version]-js-[js-sdk-version])
|
||||
*/
|
||||
function checkVersion(ver) {
|
||||
const parts = ver.split('-');
|
||||
return parts.length == 5 && parts[1] == 'react' && parts[3] == 'js';
|
||||
}
|
||||
|
||||
export default createReactClass({
|
||||
propTypes: {
|
||||
version: PropTypes.string.isRequired,
|
||||
newVersion: PropTypes.string.isRequired,
|
||||
releaseNotes: PropTypes.string,
|
||||
},
|
||||
|
||||
displayReleaseNotes: function(releaseNotes) {
|
||||
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||
Modal.createTrackedDialog('Display release notes', '', QuestionDialog, {
|
||||
title: _t("What's New"),
|
||||
description: <div className="mx_MatrixToolbar_changelog">{releaseNotes}</div>,
|
||||
button: _t("Update"),
|
||||
onFinished: (update) => {
|
||||
if (update && PlatformPeg.get()) {
|
||||
PlatformPeg.get().installUpdate();
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
displayChangelog: function() {
|
||||
const ChangelogDialog = sdk.getComponent('dialogs.ChangelogDialog');
|
||||
Modal.createTrackedDialog('Display Changelog', '', ChangelogDialog, {
|
||||
version: this.props.version,
|
||||
newVersion: this.props.newVersion,
|
||||
onFinished: (update) => {
|
||||
if (update && PlatformPeg.get()) {
|
||||
PlatformPeg.get().installUpdate();
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
onUpdateClicked: function() {
|
||||
PlatformPeg.get().installUpdate();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
let action_button;
|
||||
// If we have release notes to display, we display them. Otherwise,
|
||||
// we display the Changelog Dialog which takes two versions and
|
||||
// automatically tells you what's changed (provided the versions
|
||||
// are in the right format)
|
||||
if (this.props.releaseNotes) {
|
||||
action_button = (
|
||||
<button className="mx_MatrixToolbar_action" onClick={this.displayReleaseNotes}>
|
||||
{ _t("What's new?") }
|
||||
</button>
|
||||
);
|
||||
} else if (checkVersion(this.props.version) && checkVersion(this.props.newVersion)) {
|
||||
action_button = (
|
||||
<button className="mx_MatrixToolbar_action" onClick={this.displayChangelog}>
|
||||
{ _t("What's new?") }
|
||||
</button>
|
||||
);
|
||||
} else if (PlatformPeg.get()) {
|
||||
action_button = (
|
||||
<button className="mx_MatrixToolbar_action" onClick={this.onUpdateClicked}>
|
||||
{ _t("Update") }
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="mx_MatrixToolbar">
|
||||
<img className="mx_MatrixToolbar_warning" src={require("../../../../res/img/warning.svg")} width="24" height="23" alt="" />
|
||||
<div className="mx_MatrixToolbar_content">
|
||||
{_t("A new version of Riot is available.")}
|
||||
</div>
|
||||
{action_button}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -1,53 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import * as sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default createReactClass({
|
||||
onUpdateClicked: function() {
|
||||
const SetPasswordDialog = sdk.getComponent('dialogs.SetPasswordDialog');
|
||||
Modal.createTrackedDialog('Set Password Dialog', 'Password Nag Bar', SetPasswordDialog);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const toolbarClasses = "mx_MatrixToolbar mx_MatrixToolbar_clickable";
|
||||
return (
|
||||
<div className={toolbarClasses} onClick={this.onUpdateClicked}>
|
||||
<img className="mx_MatrixToolbar_warning"
|
||||
src={require("../../../../res/img/warning.svg")}
|
||||
width="24"
|
||||
height="23"
|
||||
alt=""
|
||||
/>
|
||||
<div className="mx_MatrixToolbar_content">
|
||||
{ _t(
|
||||
"To return to your account in future you need to <u>set a password</u>",
|
||||
{},
|
||||
{ 'u': (sub) => <u>{ sub }</u> },
|
||||
) }
|
||||
</div>
|
||||
<button className="mx_MatrixToolbar_action">
|
||||
{ _t("Set Password") }
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -1,99 +0,0 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import classNames from 'classnames';
|
||||
import { _td } from '../../../languageHandler';
|
||||
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
||||
|
||||
export default createReactClass({
|
||||
propTypes: {
|
||||
// 'hard' if the logged in user has been locked out, 'soft' if they haven't
|
||||
kind: PropTypes.string,
|
||||
adminContact: PropTypes.string,
|
||||
// The type of limit that has been hit.
|
||||
limitType: PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
kind: 'hard',
|
||||
};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const toolbarClasses = {
|
||||
'mx_MatrixToolbar': true,
|
||||
};
|
||||
|
||||
let adminContact;
|
||||
let limitError;
|
||||
if (this.props.kind === 'hard') {
|
||||
toolbarClasses['mx_MatrixToolbar_error'] = true;
|
||||
|
||||
adminContact = messageForResourceLimitError(
|
||||
this.props.limitType,
|
||||
this.props.adminContact,
|
||||
{
|
||||
'': _td("Please <a>contact your service administrator</a> to continue using the service."),
|
||||
},
|
||||
);
|
||||
limitError = messageForResourceLimitError(
|
||||
this.props.limitType,
|
||||
this.props.adminContact,
|
||||
{
|
||||
'monthly_active_user': _td("This homeserver has hit its Monthly Active User limit."),
|
||||
'': _td("This homeserver has exceeded one of its resource limits."),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
toolbarClasses['mx_MatrixToolbar_info'] = true;
|
||||
adminContact = messageForResourceLimitError(
|
||||
this.props.limitType,
|
||||
this.props.adminContact,
|
||||
{
|
||||
'': _td("Please <a>contact your service administrator</a> to get this limit increased."),
|
||||
},
|
||||
);
|
||||
limitError = messageForResourceLimitError(
|
||||
this.props.limitType,
|
||||
this.props.adminContact,
|
||||
{
|
||||
'monthly_active_user': _td(
|
||||
"This homeserver has hit its Monthly Active User limit so " +
|
||||
"<b>some users will not be able to log in</b>.",
|
||||
),
|
||||
'': _td(
|
||||
"This homeserver has exceeded one of its resource limits so " +
|
||||
"<b>some users will not be able to log in</b>.",
|
||||
),
|
||||
},
|
||||
{'b': sub => <b>{sub}</b>},
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={classNames(toolbarClasses)}>
|
||||
<div className="mx_MatrixToolbar_content">
|
||||
{limitError}
|
||||
{' '}
|
||||
{adminContact}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
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.
|
||||
|
@ -22,11 +22,9 @@ import PropTypes from 'prop-types';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import Modal from '../../../Modal';
|
||||
import {aboveLeftOf, ContextMenu, ContextMenuButton, useContextMenu} from '../../structures/ContextMenu';
|
||||
import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
|
||||
const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => {
|
||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
||||
|
@ -41,18 +39,6 @@ const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFo
|
|||
const tile = getTile && getTile();
|
||||
const replyThread = getReplyThread && getReplyThread();
|
||||
|
||||
const onCryptoClick = () => {
|
||||
Modal.createTrackedDialogAsync('Encrypted Event Dialog', '',
|
||||
import('../../../async-components/views/dialogs/EncryptedEventDialog'),
|
||||
{event: mxEvent},
|
||||
);
|
||||
};
|
||||
|
||||
let e2eInfoCallback = null;
|
||||
if (mxEvent.isEncrypted() && !SettingsStore.getValue("feature_cross_signing")) {
|
||||
e2eInfoCallback = onCryptoClick;
|
||||
}
|
||||
|
||||
const buttonRect = button.current.getBoundingClientRect();
|
||||
contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu}>
|
||||
<MessageContextMenu
|
||||
|
@ -60,7 +46,6 @@ const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFo
|
|||
permalinkCreator={permalinkCreator}
|
||||
eventTileOps={tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined}
|
||||
collapseReplyThread={replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined}
|
||||
e2eInfoCallback={e2eInfoCallback}
|
||||
onFinished={closeMenu}
|
||||
/>
|
||||
</ContextMenu>;
|
||||
|
|
|
@ -802,6 +802,8 @@ export default createReactClass({
|
|||
|
||||
const groupTimestamp = !this.props.useIRCLayout ? linkedTimestamp : null;
|
||||
const ircTimestamp = this.props.useIRCLayout ? linkedTimestamp : null;
|
||||
const groupPadlock = !this.props.useIRCLayout && !isBubbleMessage && this._renderE2EPadlock();
|
||||
const ircPadlock = this.props.useIRCLayout && !isBubbleMessage && this._renderE2EPadlock();
|
||||
|
||||
switch (this.props.tileShape) {
|
||||
case 'notif': {
|
||||
|
@ -873,9 +875,10 @@ export default createReactClass({
|
|||
{ ircTimestamp }
|
||||
{ avatar }
|
||||
{ sender }
|
||||
{ ircPadlock }
|
||||
<div className="mx_EventTile_reply">
|
||||
{ groupTimestamp }
|
||||
{ !isBubbleMessage && this._renderE2EPadlock() }
|
||||
{ groupPadlock }
|
||||
{ thread }
|
||||
<EventTileType ref={this._tile}
|
||||
mxEvent={this.props.mxEvent}
|
||||
|
@ -904,9 +907,10 @@ export default createReactClass({
|
|||
{ readAvatars }
|
||||
</div>
|
||||
{ sender }
|
||||
{ ircPadlock }
|
||||
<div className="mx_EventTile_line">
|
||||
{ groupTimestamp }
|
||||
{ !isBubbleMessage && this._renderE2EPadlock() }
|
||||
{ groupPadlock }
|
||||
{ thread }
|
||||
<EventTileType ref={this._tile}
|
||||
mxEvent={this.props.mxEvent}
|
||||
|
|
|
@ -30,6 +30,8 @@ import * as RoomNotifs from '../../../RoomNotifs';
|
|||
import { EffectiveMembership, getEffectiveMembership } from "../../../stores/room-list/membership";
|
||||
import * as Unread from '../../../Unread';
|
||||
import * as FormattingUtils from "../../../utils/FormattingUtils";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
/*******************************************************************
|
||||
* CAUTION *
|
||||
|
@ -86,10 +88,22 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
hover: false,
|
||||
notificationState: this.getNotificationState(),
|
||||
};
|
||||
|
||||
this.props.room.on("Room.receipt", this.handleRoomEventUpdate);
|
||||
this.props.room.on("Room.timeline", this.handleRoomEventUpdate);
|
||||
this.props.room.on("Room.redaction", this.handleRoomEventUpdate);
|
||||
MatrixClientPeg.get().on("Event.decrypted", this.handleRoomEventUpdate);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
// TODO: Listen for changes to the badge count and update as needed
|
||||
if (this.props.room) {
|
||||
this.props.room.removeListener("Room.receipt", this.handleRoomEventUpdate);
|
||||
this.props.room.removeListener("Room.timeline", this.handleRoomEventUpdate);
|
||||
this.props.room.removeListener("Room.redaction", this.handleRoomEventUpdate);
|
||||
}
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("Event.decrypted", this.handleRoomEventUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: This is a bit of an awful-looking hack. We should probably be using state for
|
||||
|
@ -99,7 +113,15 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
return getEffectiveMembership(this.props.room.getMyMembership()) === EffectiveMembership.Invite;
|
||||
}
|
||||
|
||||
// TODO: Make use of this function when the notification state needs updating.
|
||||
private handleRoomEventUpdate = (event: MatrixEvent) => {
|
||||
const roomId = event.getRoomId();
|
||||
|
||||
// Sanity check: should never happen
|
||||
if (roomId !== this.props.room.roomId) return;
|
||||
|
||||
this.updateNotificationState();
|
||||
};
|
||||
|
||||
private updateNotificationState() {
|
||||
this.setState({notificationState: this.getNotificationState()});
|
||||
}
|
||||
|
@ -214,7 +236,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
let tooltip = null;
|
||||
if (false) { // isCollapsed
|
||||
if (this.state.hover) {
|
||||
tooltip = <Tooltip className="mx_RoomTile_tooltip" label={this.props.room.name} dir="auto"/>
|
||||
tooltip = <Tooltip className="mx_RoomTile_tooltip" label={this.props.room.name} />
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -141,6 +141,12 @@ export default createReactClass({
|
|||
_changePassword: function(cli, oldPassword, newPassword) {
|
||||
const authDict = {
|
||||
type: 'm.login.password',
|
||||
identifier: {
|
||||
type: 'm.id.user',
|
||||
user: cli.credentials.userId,
|
||||
},
|
||||
// TODO: Remove `user` once servers support proper UIA
|
||||
// See https://github.com/matrix-org/synapse/issues/5665
|
||||
user: cli.credentials.userId,
|
||||
password: oldPassword,
|
||||
};
|
||||
|
|
|
@ -1,75 +0,0 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import createReactClass from 'create-react-class';
|
||||
import Notifier from "../../../Notifier";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'EnableNotificationsButton',
|
||||
|
||||
componentDidMount: function() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
if (payload.action !== "notifier_enabled") {
|
||||
return;
|
||||
}
|
||||
this.forceUpdate();
|
||||
},
|
||||
|
||||
enabled: function() {
|
||||
return Notifier.isEnabled();
|
||||
},
|
||||
|
||||
onClick: function() {
|
||||
const self = this;
|
||||
if (!Notifier.supportsDesktopNotifications()) {
|
||||
return;
|
||||
}
|
||||
if (!Notifier.isEnabled()) {
|
||||
Notifier.setEnabled(true, function() {
|
||||
self.forceUpdate();
|
||||
});
|
||||
} else {
|
||||
Notifier.setEnabled(false);
|
||||
}
|
||||
this.forceUpdate();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.enabled()) {
|
||||
return (
|
||||
<button className="mx_EnableNotificationsButton" onClick={this.onClick}>
|
||||
{ _t("Disable Notifications") }
|
||||
</button>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<button className="mx_EnableNotificationsButton" onClick={this.onClick}>
|
||||
{ _t("Enable Notifications") }
|
||||
</button>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
|
@ -267,7 +267,7 @@ export default class PhoneNumbers extends React.Component {
|
|||
label={_t("Phone Number")}
|
||||
autoComplete="off"
|
||||
disabled={this.state.verifying}
|
||||
prefix={phoneCountry}
|
||||
prefixComponent={phoneCountry}
|
||||
value={this.state.newPhoneNumber}
|
||||
onChange={this._onChangeNewPhoneNumber}
|
||||
/>
|
||||
|
|
|
@ -20,34 +20,64 @@ import React from 'react';
|
|||
import {_t} from "../../../../../languageHandler";
|
||||
import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore";
|
||||
import * as sdk from "../../../../../index";
|
||||
import {enumerateThemes, ThemeWatcher} from "../../../../../theme";
|
||||
import { enumerateThemes } from "../../../../../theme";
|
||||
import ThemeWatcher from "../../../../../settings/watchers/ThemeWatcher";
|
||||
import Field from "../../../elements/Field";
|
||||
import Slider from "../../../elements/Slider";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import dis from "../../../../../dispatcher/dispatcher";
|
||||
import { FontWatcher } from "../../../../../FontWatcher";
|
||||
import { FontWatcher } from "../../../../../settings/watchers/FontWatcher";
|
||||
import { RecheckThemePayload } from '../../../../../dispatcher/payloads/RecheckThemePayload';
|
||||
import { Action } from '../../../../../dispatcher/actions';
|
||||
import { IValidationResult, IFieldState } from '../../../elements/Validation';
|
||||
|
||||
export default class AppearanceUserSettingsTab extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
interface IProps {
|
||||
}
|
||||
|
||||
interface IThemeState {
|
||||
theme: string,
|
||||
useSystemTheme: boolean,
|
||||
}
|
||||
|
||||
export interface CustomThemeMessage {
|
||||
isError: boolean,
|
||||
text: string
|
||||
};
|
||||
|
||||
interface IState extends IThemeState {
|
||||
// String displaying the current selected fontSize.
|
||||
// Needs to be string for things like '17.' without
|
||||
// trailing 0s.
|
||||
fontSize: string,
|
||||
customThemeUrl: string,
|
||||
customThemeMessage: CustomThemeMessage,
|
||||
useCustomFontSize: boolean,
|
||||
}
|
||||
|
||||
export default class AppearanceUserSettingsTab extends React.Component<IProps, IState> {
|
||||
|
||||
private themeTimer: NodeJS.Timeout;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
fontSize: SettingsStore.getValue("fontSize", null),
|
||||
...this._calculateThemeState(),
|
||||
fontSize: SettingsStore.getValue("fontSize", null).toString(),
|
||||
...this.calculateThemeState(),
|
||||
customThemeUrl: "",
|
||||
customThemeMessage: {isError: false, text: ""},
|
||||
useCustomFontSize: SettingsStore.getValue("useCustomFontSize"),
|
||||
};
|
||||
}
|
||||
|
||||
_calculateThemeState() {
|
||||
private calculateThemeState(): IThemeState {
|
||||
// We have to mirror the logic from ThemeWatcher.getEffectiveTheme so we
|
||||
// show the right values for things.
|
||||
|
||||
const themeChoice = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme");
|
||||
const systemThemeExplicit = SettingsStore.getValueAt(
|
||||
const themeChoice: string = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme");
|
||||
const systemThemeExplicit: boolean = SettingsStore.getValueAt(
|
||||
SettingLevel.DEVICE, "use_system_theme", null, false, true);
|
||||
const themeExplicit = SettingsStore.getValueAt(
|
||||
const themeExplicit: string = SettingsStore.getValueAt(
|
||||
SettingLevel.DEVICE, "theme", null, false, true);
|
||||
|
||||
// If the user has enabled system theme matching, use that.
|
||||
|
@ -73,15 +103,15 @@ export default class AppearanceUserSettingsTab extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
_onThemeChange = (e) => {
|
||||
private onThemeChange(e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>): void {
|
||||
const newTheme = e.target.value;
|
||||
if (this.state.theme === newTheme) return;
|
||||
|
||||
// doing getValue in the .catch will still return the value we failed to set,
|
||||
// so remember what the value was before we tried to set it so we can revert
|
||||
const oldTheme = SettingsStore.getValue('theme');
|
||||
const oldTheme: string = SettingsStore.getValue('theme');
|
||||
SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme).catch(() => {
|
||||
dis.dispatch({action: 'recheck_theme'});
|
||||
dis.dispatch<RecheckThemePayload>({action: Action.RecheckTheme});
|
||||
this.setState({theme: oldTheme});
|
||||
});
|
||||
this.setState({theme: newTheme});
|
||||
|
@ -91,23 +121,21 @@ export default class AppearanceUserSettingsTab extends React.Component {
|
|||
// XXX: The local echoed value appears to be unreliable, in particular
|
||||
// when settings custom themes(!) so adding forceTheme to override
|
||||
// the value from settings.
|
||||
dis.dispatch({action: 'recheck_theme', forceTheme: newTheme});
|
||||
dis.dispatch<RecheckThemePayload>({action: Action.RecheckTheme, forceTheme: newTheme});
|
||||
};
|
||||
|
||||
_onUseSystemThemeChanged = (checked) => {
|
||||
private onUseSystemThemeChanged(checked: boolean) {
|
||||
this.setState({useSystemTheme: checked});
|
||||
SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, checked);
|
||||
dis.dispatch({action: 'recheck_theme'});
|
||||
dis.dispatch<RecheckThemePayload>({action: Action.RecheckTheme});
|
||||
};
|
||||
|
||||
_onFontSizeChanged = (size) => {
|
||||
this.setState({fontSize: size});
|
||||
private onFontSizeChanged(size: number) {
|
||||
this.setState({fontSize: size.toString()});
|
||||
SettingsStore.setValue("fontSize", null, SettingLevel.DEVICE, size);
|
||||
};
|
||||
|
||||
_onValidateFontSize = ({value}) => {
|
||||
console.log({value});
|
||||
|
||||
private async onValidateFontSize({value}: Pick<IFieldState, "value">): Promise<IValidationResult> {
|
||||
const parsedSize = parseFloat(value);
|
||||
const min = FontWatcher.MIN_SIZE;
|
||||
const max = FontWatcher.MAX_SIZE;
|
||||
|
@ -127,17 +155,18 @@ export default class AppearanceUserSettingsTab extends React.Component {
|
|||
return {valid: true, feedback: _t('Use between %(min)s pt and %(max)s pt', {min, max})};
|
||||
}
|
||||
|
||||
_onAddCustomTheme = async () => {
|
||||
let currentThemes = SettingsStore.getValue("custom_themes");
|
||||
private async onAddCustomTheme() {
|
||||
let currentThemes: string[] = SettingsStore.getValue("custom_themes");
|
||||
if (!currentThemes) currentThemes = [];
|
||||
currentThemes = currentThemes.map(c => c); // cheap clone
|
||||
|
||||
if (this._themeTimer) {
|
||||
clearTimeout(this._themeTimer);
|
||||
if (this.themeTimer) {
|
||||
clearTimeout(this.themeTimer);
|
||||
}
|
||||
|
||||
try {
|
||||
const r = await fetch(this.state.customThemeUrl);
|
||||
// XXX: need some schema for this
|
||||
const themeInfo = await r.json();
|
||||
if (!themeInfo || typeof(themeInfo['name']) !== 'string' || typeof(themeInfo['colors']) !== 'object') {
|
||||
this.setState({customThemeMessage: {text: _t("Invalid theme schema."), isError: true}});
|
||||
|
@ -153,42 +182,32 @@ export default class AppearanceUserSettingsTab extends React.Component {
|
|||
await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, currentThemes);
|
||||
this.setState({customThemeUrl: "", customThemeMessage: {text: _t("Theme added!"), isError: false}});
|
||||
|
||||
this._themeTimer = setTimeout(() => {
|
||||
this.themeTimer = setTimeout(() => {
|
||||
this.setState({customThemeMessage: {text: "", isError: false}});
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
_onCustomThemeChange = (e) => {
|
||||
private onCustomThemeChange(e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>) {
|
||||
this.setState({customThemeUrl: e.target.value});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="mx_SettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Appearance")}</div>
|
||||
{this._renderThemeSection()}
|
||||
{SettingsStore.isFeatureEnabled("feature_font_scaling") ? this._renderFontSection() : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_renderThemeSection() {
|
||||
private renderThemeSection() {
|
||||
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
|
||||
const LabelledToggleSwitch = sdk.getComponent("views.elements.LabelledToggleSwitch");
|
||||
|
||||
const themeWatcher = new ThemeWatcher();
|
||||
let systemThemeSection;
|
||||
let systemThemeSection: JSX.Element;
|
||||
if (themeWatcher.isSystemThemeSupported()) {
|
||||
systemThemeSection = <div>
|
||||
<LabelledToggleSwitch
|
||||
value={this.state.useSystemTheme}
|
||||
label={SettingsStore.getDisplayName("use_system_theme")}
|
||||
onChange={this._onUseSystemThemeChanged}
|
||||
onChange={this.onUseSystemThemeChanged}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
let customThemeForm;
|
||||
let customThemeForm: JSX.Element;
|
||||
if (SettingsStore.isFeatureEnabled("feature_custom_themes")) {
|
||||
let messageElement = null;
|
||||
if (this.state.customThemeMessage.text) {
|
||||
|
@ -200,17 +219,17 @@ export default class AppearanceUserSettingsTab extends React.Component {
|
|||
}
|
||||
customThemeForm = (
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<form onSubmit={this._onAddCustomTheme}>
|
||||
<form onSubmit={this.onAddCustomTheme}>
|
||||
<Field
|
||||
label={_t("Custom theme URL")}
|
||||
type='text'
|
||||
id='mx_GeneralUserSettingsTab_customThemeInput'
|
||||
autoComplete="off"
|
||||
onChange={this._onCustomThemeChange}
|
||||
onChange={this.onCustomThemeChange}
|
||||
value={this.state.customThemeUrl}
|
||||
/>
|
||||
<AccessibleButton
|
||||
onClick={this._onAddCustomTheme}
|
||||
onClick={this.onAddCustomTheme}
|
||||
type="submit" kind="primary_sm"
|
||||
disabled={!this.state.customThemeUrl.trim()}
|
||||
>{_t("Add theme")}</AccessibleButton>
|
||||
|
@ -220,7 +239,8 @@ export default class AppearanceUserSettingsTab extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
const themes = Object.entries(enumerateThemes())
|
||||
// XXX: replace any type here
|
||||
const themes = Object.entries<any>(enumerateThemes())
|
||||
.map(p => ({id: p[0], name: p[1]})); // convert pairs to objects for code readability
|
||||
const builtInThemes = themes.filter(p => !p.id.startsWith("custom-"));
|
||||
const customThemes = themes.filter(p => !builtInThemes.includes(p))
|
||||
|
@ -232,7 +252,7 @@ export default class AppearanceUserSettingsTab extends React.Component {
|
|||
{systemThemeSection}
|
||||
<Field
|
||||
id="theme" label={_t("Theme")} element="select"
|
||||
value={this.state.theme} onChange={this._onThemeChange}
|
||||
value={this.state.theme} onChange={this.onThemeChange}
|
||||
disabled={this.state.useSystemTheme}
|
||||
>
|
||||
{orderedThemes.map(theme => {
|
||||
|
@ -245,7 +265,7 @@ export default class AppearanceUserSettingsTab extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
_renderFontSection() {
|
||||
private renderFontSection() {
|
||||
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
|
||||
return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_fontScaling">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Font size")}</span>
|
||||
|
@ -253,9 +273,9 @@ export default class AppearanceUserSettingsTab extends React.Component {
|
|||
<div className="mx_AppearanceUserSettingsTab_fontSlider_smallText">Aa</div>
|
||||
<Slider
|
||||
values={[13, 15, 16, 18, 20]}
|
||||
value={this.state.fontSize}
|
||||
onSelectionChange={this._onFontSizeChanged}
|
||||
displayFunc={value => {}}
|
||||
value={parseInt(this.state.fontSize, 10)}
|
||||
onSelectionChange={this.onFontSizeChanged}
|
||||
displayFunc={value => ""}
|
||||
disabled={this.state.useCustomFontSize}
|
||||
/>
|
||||
<div className="mx_AppearanceUserSettingsTab_fontSlider_largeText">Aa</div>
|
||||
|
@ -263,7 +283,7 @@ export default class AppearanceUserSettingsTab extends React.Component {
|
|||
<SettingsFlag
|
||||
name="useCustomFontSize"
|
||||
level={SettingLevel.ACCOUNT}
|
||||
onChange={(checked)=> this.setState({useCustomFontSize: checked})}
|
||||
onChange={(checked) => this.setState({useCustomFontSize: checked})}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
|
@ -272,10 +292,20 @@ export default class AppearanceUserSettingsTab extends React.Component {
|
|||
placeholder={this.state.fontSize.toString()}
|
||||
value={this.state.fontSize.toString()}
|
||||
id="font_size_field"
|
||||
onValidate={this._onValidateFontSize}
|
||||
onValidate={this.onValidateFontSize}
|
||||
onChange={(value) => this.setState({fontSize: value.target.value})}
|
||||
disabled={!this.state.useCustomFontSize}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="mx_SettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Appearance")}</div>
|
||||
{this.renderThemeSection()}
|
||||
{SettingsStore.isFeatureEnabled("feature_font_scaling") ? this.renderFontSection() : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
/*
|
||||
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 { _t } from '../../../languageHandler';
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import DeviceListener from '../../../DeviceListener';
|
||||
import FormButton from '../elements/FormButton';
|
||||
import { replaceableComponent } from '../../../utils/replaceableComponent';
|
||||
|
||||
@replaceableComponent("views.toasts.BulkUnverifiedSessionsToast")
|
||||
export default class BulkUnverifiedSessionsToast extends React.PureComponent {
|
||||
static propTypes = {
|
||||
deviceIds: PropTypes.array,
|
||||
}
|
||||
|
||||
_onLaterClick = () => {
|
||||
DeviceListener.sharedInstance().dismissUnverifiedSessions(this.props.deviceIds);
|
||||
};
|
||||
|
||||
_onReviewClick = async () => {
|
||||
DeviceListener.sharedInstance().dismissUnverifiedSessions(this.props.deviceIds);
|
||||
|
||||
dis.dispatch({
|
||||
action: 'view_user_info',
|
||||
userId: MatrixClientPeg.get().getUserId(),
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (<div>
|
||||
<div className="mx_Toast_description">
|
||||
{_t("Verify all your sessions to ensure your account & messages are safe")}
|
||||
</div>
|
||||
<div className="mx_Toast_buttons" aria-live="off">
|
||||
<FormButton label={_t("Later")} kind="danger" onClick={this._onLaterClick} />
|
||||
<FormButton label={_t("Review")} onClick={this._onReviewClick} />
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
}
|
42
src/components/views/toasts/GenericToast.tsx
Normal file
42
src/components/views/toasts/GenericToast.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
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, {ReactChild} from "react";
|
||||
|
||||
import FormButton from "../elements/FormButton";
|
||||
|
||||
interface IProps {
|
||||
description: ReactChild;
|
||||
acceptLabel: string;
|
||||
rejectLabel?: string;
|
||||
|
||||
onAccept();
|
||||
onReject?();
|
||||
}
|
||||
|
||||
const GenericToast: React.FC<IProps> = ({description, acceptLabel, rejectLabel, onAccept, onReject}) => {
|
||||
return <div>
|
||||
<div className="mx_Toast_description">
|
||||
{ description }
|
||||
</div>
|
||||
<div className="mx_Toast_buttons" aria-live="off">
|
||||
{onReject && rejectLabel && <FormButton label={rejectLabel} kind="danger" onClick={onReject} /> }
|
||||
<FormButton label={acceptLabel} onClick={onAccept} />
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default GenericToast;
|
|
@ -1,88 +0,0 @@
|
|||
/*
|
||||
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 Modal from '../../../Modal';
|
||||
import * as sdk from "../../../index";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import DeviceListener from '../../../DeviceListener';
|
||||
import SetupEncryptionDialog from "../dialogs/SetupEncryptionDialog";
|
||||
import { accessSecretStorage } from '../../../CrossSigningManager';
|
||||
|
||||
export default class SetupEncryptionToast extends React.PureComponent {
|
||||
static propTypes = {
|
||||
toastKey: PropTypes.string.isRequired,
|
||||
kind: PropTypes.oneOf([
|
||||
'set_up_encryption',
|
||||
'verify_this_session',
|
||||
'upgrade_encryption',
|
||||
]).isRequired,
|
||||
};
|
||||
|
||||
_onLaterClick = () => {
|
||||
DeviceListener.sharedInstance().dismissEncryptionSetup();
|
||||
};
|
||||
|
||||
_onSetupClick = async () => {
|
||||
if (this.props.kind === "verify_this_session") {
|
||||
Modal.createTrackedDialog('Verify session', 'Verify session', SetupEncryptionDialog,
|
||||
{}, null, /* priority = */ false, /* static = */ true);
|
||||
} else {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
const modal = Modal.createDialog(
|
||||
Spinner, null, 'mx_Dialog_spinner', /* priority */ false, /* static */ true,
|
||||
);
|
||||
try {
|
||||
await accessSecretStorage();
|
||||
} finally {
|
||||
modal.close();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
getDescription() {
|
||||
switch (this.props.kind) {
|
||||
case 'set_up_encryption':
|
||||
case 'upgrade_encryption':
|
||||
return _t('Verify yourself & others to keep your chats safe');
|
||||
case 'verify_this_session':
|
||||
return _t('Other users may not trust it');
|
||||
}
|
||||
}
|
||||
|
||||
getSetupCaption() {
|
||||
switch (this.props.kind) {
|
||||
case 'set_up_encryption':
|
||||
return _t('Set up');
|
||||
case 'upgrade_encryption':
|
||||
return _t('Upgrade');
|
||||
case 'verify_this_session':
|
||||
return _t('Verify');
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const FormButton = sdk.getComponent("elements.FormButton");
|
||||
return (<div>
|
||||
<div className="mx_Toast_description">{this.getDescription()}</div>
|
||||
<div className="mx_Toast_buttons" aria-live="off">
|
||||
<FormButton label={_t("Later")} kind="danger" onClick={this._onLaterClick} />
|
||||
<FormButton label={this.getSetupCaption()} onClick={this._onSetupClick} />
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
/*
|
||||
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 { _t } from '../../../languageHandler';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
import DeviceListener from '../../../DeviceListener';
|
||||
import NewSessionReviewDialog from '../dialogs/NewSessionReviewDialog';
|
||||
import FormButton from '../elements/FormButton';
|
||||
import { replaceableComponent } from '../../../utils/replaceableComponent';
|
||||
|
||||
@replaceableComponent("views.toasts.UnverifiedSessionToast")
|
||||
export default class UnverifiedSessionToast extends React.PureComponent {
|
||||
static propTypes = {
|
||||
deviceId: PropTypes.string,
|
||||
}
|
||||
|
||||
_onLaterClick = () => {
|
||||
DeviceListener.sharedInstance().dismissUnverifiedSessions([this.props.deviceId]);
|
||||
};
|
||||
|
||||
_onReviewClick = async () => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
Modal.createTrackedDialog('New Session Review', 'Starting dialog', NewSessionReviewDialog, {
|
||||
userId: cli.getUserId(),
|
||||
device: cli.getStoredDevice(cli.getUserId(), this.props.deviceId),
|
||||
onFinished: (r) => {
|
||||
if (!r) {
|
||||
/* This'll come back false if the user clicks "this wasn't me" and saw a warning dialog */
|
||||
DeviceListener.sharedInstance().dismissUnverifiedSessions([this.props.deviceId]);
|
||||
}
|
||||
},
|
||||
}, null, /* priority = */ false, /* static = */ true);
|
||||
};
|
||||
|
||||
render() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const device = cli.getStoredDevice(cli.getUserId(), this.props.deviceId);
|
||||
|
||||
return (<div>
|
||||
<div className="mx_Toast_description">
|
||||
{_t(
|
||||
"Verify the new login accessing your account: %(name)s", { name: device.getDisplayName()})}
|
||||
</div>
|
||||
<div className="mx_Toast_buttons" aria-live="off">
|
||||
<FormButton label={_t("Later")} kind="danger" onClick={this._onLaterClick} />
|
||||
<FormButton label={_t("Verify")} onClick={this._onReviewClick} />
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
}
|
|
@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from "react";
|
||||
|
||||
import * as sdk from "../../../index";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
|
@ -24,8 +24,23 @@ import {userLabelForEventRoom} from "../../../utils/KeyVerificationStateObserver
|
|||
import dis from "../../../dispatcher/dispatcher";
|
||||
import ToastStore from "../../../stores/ToastStore";
|
||||
import Modal from "../../../Modal";
|
||||
import GenericToast from "./GenericToast";
|
||||
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import {DeviceInfo} from "matrix-js-sdk/src/crypto/deviceinfo";
|
||||
|
||||
interface IProps {
|
||||
toastKey: string;
|
||||
request: VerificationRequest;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
counter: number;
|
||||
device?: DeviceInfo;
|
||||
}
|
||||
|
||||
export default class VerificationRequestToast extends React.PureComponent<IProps, IState> {
|
||||
private intervalHandle: NodeJS.Timeout;
|
||||
|
||||
export default class VerificationRequestToast extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {counter: Math.ceil(props.request.timeout / 1000)};
|
||||
|
@ -34,7 +49,7 @@ export default class VerificationRequestToast extends React.PureComponent {
|
|||
async componentDidMount() {
|
||||
const {request} = this.props;
|
||||
if (request.timeout && request.timeout > 0) {
|
||||
this._intervalHandle = setInterval(() => {
|
||||
this.intervalHandle = setInterval(() => {
|
||||
let {counter} = this.state;
|
||||
counter = Math.max(0, counter - 1);
|
||||
this.setState({counter});
|
||||
|
@ -56,7 +71,7 @@ export default class VerificationRequestToast extends React.PureComponent {
|
|||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this._intervalHandle);
|
||||
clearInterval(this.intervalHandle);
|
||||
const {request} = this.props;
|
||||
request.off("change", this._checkRequestIsPending);
|
||||
}
|
||||
|
@ -110,7 +125,6 @@ export default class VerificationRequestToast extends React.PureComponent {
|
|||
};
|
||||
|
||||
render() {
|
||||
const FormButton = sdk.getComponent("elements.FormButton");
|
||||
const {request} = this.props;
|
||||
let nameLabel;
|
||||
if (request.isSelfVerification) {
|
||||
|
@ -133,20 +147,16 @@ export default class VerificationRequestToast extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
}
|
||||
const declineLabel = this.state.counter == 0 ?
|
||||
const declineLabel = this.state.counter === 0 ?
|
||||
_t("Decline") :
|
||||
_t("Decline (%(counter)s)", {counter: this.state.counter});
|
||||
return (<div>
|
||||
<div className="mx_Toast_description">{nameLabel}</div>
|
||||
<div className="mx_Toast_buttons" aria-live="off">
|
||||
<FormButton label={declineLabel} kind="danger" onClick={this.cancel} />
|
||||
<FormButton label={_t("Accept")} onClick={this.accept} />
|
||||
</div>
|
||||
</div>);
|
||||
|
||||
return <GenericToast
|
||||
description={nameLabel}
|
||||
acceptLabel={_t("Accept")}
|
||||
onAccept={this.accept}
|
||||
rejectLabel={declineLabel}
|
||||
onReject={this.cancel}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
VerificationRequestToast.propTypes = {
|
||||
request: PropTypes.object.isRequired,
|
||||
toastKey: PropTypes.string.isRequired,
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue