Merge branch 'develop' into dehydration

This commit is contained in:
Hubert Chathi 2020-09-18 18:00:41 -04:00
commit add36ca4ce
384 changed files with 14620 additions and 9242 deletions

View file

@ -27,10 +27,14 @@ import {ModalManager} from "../Modal";
import SettingsStore from "../settings/SettingsStore";
import {ActiveRoomObserver} from "../ActiveRoomObserver";
import {Notifier} from "../Notifier";
import type {Renderer} from "react-dom";
import RightPanelStore from "../stores/RightPanelStore";
import WidgetStore from "../stores/WidgetStore";
declare global {
interface Window {
Modernizr: ModernizrStatic;
matrixChat: ReturnType<Renderer>;
mxMatrixClientPeg: IMatrixClientPeg;
Olm: {
init: () => Promise<void>;
@ -47,6 +51,8 @@ declare global {
singletonModalManager: ModalManager;
mxSettingsStore: SettingsStore;
mxNotifier: typeof Notifier;
mxRightPanelStore: RightPanelStore;
mxWidgetStore: WidgetStore;
}
interface Document {

View file

@ -170,15 +170,19 @@ class Analytics {
return !this.baseUrl;
}
canEnable() {
const config = SdkConfig.get();
return navigator.doNotTrack !== "1" && config && config.piwik && config.piwik.url && config.piwik.siteId;
}
/**
* Enable Analytics if initialized but disabled
* otherwise try and initalize, no-op if piwik config missing
*/
async enable() {
if (!this.disabled) return;
if (!this.canEnable()) return;
const config = SdkConfig.get();
if (!config || !config.piwik || !config.piwik.url || !config.piwik.siteId) return;
this.baseUrl = new URL("piwik.php", config.piwik.url);
// set constants

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import createReactClass from 'create-react-class';
import React from "react";
import * as sdk from './index';
import PropTypes from 'prop-types';
import { _t } from './languageHandler';
@ -24,21 +24,19 @@ import { _t } from './languageHandler';
* Wrap an asynchronous loader function with a react component which shows a
* spinner until the real component loads.
*/
export default createReactClass({
propTypes: {
export default class AsyncWrapper extends React.Component {
static propTypes = {
/** A promise which resolves with the real component
*/
prom: PropTypes.object.isRequired,
},
};
getInitialState: function() {
return {
component: null,
error: null,
};
},
state = {
component: null,
error: null,
};
componentDidMount: function() {
componentDidMount() {
this._unmounted = false;
// XXX: temporary logging to try to diagnose
// https://github.com/vector-im/element-web/issues/3148
@ -56,17 +54,17 @@ export default createReactClass({
console.warn('AsyncWrapper promise failed', e);
this.setState({error: e});
});
},
}
componentWillUnmount: function() {
componentWillUnmount() {
this._unmounted = true;
},
}
_onWrapperCancelClick: function() {
_onWrapperCancelClick = () => {
this.props.onFinished(false);
},
};
render: function() {
render() {
if (this.state.component) {
const Component = this.state.component;
return <Component {...this.props} />;
@ -87,6 +85,6 @@ export default createReactClass({
const Spinner = sdk.getComponent("elements.Spinner");
return <Spinner />;
}
},
});
}
}

View file

@ -1,7 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017, 2018 New Vector Ltd
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.
@ -56,7 +56,6 @@ limitations under the License.
import {MatrixClientPeg} from './MatrixClientPeg';
import PlatformPeg from './PlatformPeg';
import Modal from './Modal';
import * as sdk from './index';
import { _t } from './languageHandler';
import Matrix from 'matrix-js-sdk';
import dis from './dispatcher/dispatcher';
@ -67,6 +66,10 @@ import {generateHumanReadableId} from "./utils/NamingUtils";
import {Jitsi} from "./widgets/Jitsi";
import {WidgetType} from "./widgets/WidgetType";
import {SettingLevel} from "./settings/SettingLevel";
import {base32} from "rfc4648";
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
global.mxCalls = {
//room_id: MatrixCall
@ -130,7 +133,6 @@ function _setCallListeners(call) {
return;
}
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Call Failed', '', ErrorDialog, {
title: _t('Call Failed'),
description: err.message,
@ -159,7 +161,6 @@ function _setCallListeners(call) {
_setCallState(call, call.roomId, "busy");
pause("ringbackAudio");
play("busyAudio");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Call Handler', 'Call Timeout', ErrorDialog, {
title: _t('Call Timeout'),
description: _t('The remote side failed to pick up') + '.',
@ -201,7 +202,6 @@ function _setCallState(call, roomId, status) {
function _showICEFallbackPrompt() {
const cli = MatrixClientPeg.get();
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const code = sub => <code>{sub}</code>;
Modal.createTrackedDialog('No TURN servers', '', QuestionDialog, {
title: _t("Call failed due to misconfigured server"),
@ -244,7 +244,6 @@ function _onAction(payload) {
if (screenCapErrorString) {
_setCallState(undefined, newCall.roomId, "ended");
console.log("Can't capture screen: " + screenCapErrorString);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Call Handler', 'Unable to capture screen', ErrorDialog, {
title: _t('Unable to capture screen'),
description: screenCapErrorString,
@ -264,7 +263,6 @@ function _onAction(payload) {
case 'place_call':
{
if (callHandler.getAnyActiveCall()) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, {
title: _t('Existing Call'),
description: _t('You are already in a call.'),
@ -274,7 +272,6 @@ function _onAction(payload) {
// if the runtime env doesn't do VoIP, whine.
if (!MatrixClientPeg.get().supportsVoip()) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
title: _t('VoIP is unsupported'),
description: _t('You cannot place VoIP calls in this browser.'),
@ -290,7 +287,6 @@ function _onAction(payload) {
const members = room.getJoinedMembers();
if (members.length <= 1) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Call Handler', 'Cannot place call with self', ErrorDialog, {
description: _t('You cannot place a call with yourself.'),
});
@ -365,8 +361,6 @@ async function _startCallApp(roomId, type) {
const currentJitsiWidgets = WidgetUtils.getRoomWidgetsOfType(room, WidgetType.JITSI);
if (WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI)) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Call already in progress', '', ErrorDialog, {
title: _t('Call in Progress'),
description: _t('A call is currently being placed!'),
@ -379,19 +373,43 @@ async function _startCallApp(roomId, type) {
"Refusing to start conference call widget in " + roomId +
" a conference call widget is already present",
);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Already have Jitsi Widget', '', ErrorDialog, {
title: _t('Call in Progress'),
description: _t('A call is already in progress!'),
});
if (WidgetUtils.canUserModifyWidgets(roomId)) {
Modal.createTrackedDialog('Already have Jitsi Widget', '', QuestionDialog, {
title: _t('End Call'),
description: _t('Remove the group call from the room?'),
button: _t('End Call'),
cancelButton: _t('Cancel'),
onFinished: (endCall) => {
if (endCall) {
WidgetUtils.setRoomWidget(roomId, currentJitsiWidgets[0].getContent()['id']);
}
},
});
} else {
Modal.createTrackedDialog('Already have Jitsi Widget', '', ErrorDialog, {
title: _t('Call in Progress'),
description: _t("You don't have permission to remove the call from the room"),
});
}
return;
}
const confId = `JitsiConference${generateHumanReadableId()}`;
const jitsiDomain = Jitsi.getInstance().preferredDomain;
const jitsiAuth = await Jitsi.getInstance().getJitsiAuth();
let confId;
if (jitsiAuth === 'openidtoken-jwt') {
// Create conference ID from room ID
// For compatibility with Jitsi, use base32 without padding.
// More details here:
// https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification
confId = base32.stringify(Buffer.from(roomId), { pad: false });
} else {
// Create a random human readable conference ID
confId = `JitsiConference${generateHumanReadableId()}`;
}
let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl();
let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl({auth: jitsiAuth});
// TODO: Remove URL hacks when the mobile clients eventually support v2 widgets
const parsedUrl = new URL(widgetUrl);
@ -403,6 +421,7 @@ async function _startCallApp(roomId, type) {
conferenceId: confId,
isAudioOnly: type === 'voice',
domain: jitsiDomain,
auth: jitsiAuth,
};
const widgetId = (
@ -416,8 +435,6 @@ async function _startCallApp(roomId, type) {
console.log('Jitsi widget added');
}).catch((e) => {
if (e.errcode === 'M_FORBIDDEN') {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Call Failed', '', ErrorDialog, {
title: _t('Permission Required'),
description: _t("You do not have permission to start a conference call in this room"),

View file

@ -70,6 +70,7 @@ interface IContent {
interface IThumbnail {
info: {
// eslint-disable-next-line camelcase
thumbnail_info: {
w: number;
h: number;
@ -104,7 +105,12 @@ interface IAbortablePromise<T> extends Promise<T> {
* @return {Promise} A promise that resolves with an object with an info key
* and a thumbnail key.
*/
function createThumbnail(element: ThumbnailableElement, inputWidth: number, inputHeight: number, mimeType: string): Promise<IThumbnail> {
function createThumbnail(
element: ThumbnailableElement,
inputWidth: number,
inputHeight: number,
mimeType: string,
): Promise<IThumbnail> {
return new Promise((resolve) => {
let targetWidth = inputWidth;
let targetHeight = inputHeight;
@ -437,11 +443,13 @@ export default class ContentMessages {
for (let i = 0; i < okFiles.length; ++i) {
const file = okFiles[i];
if (!uploadAll) {
const {finished} = Modal.createTrackedDialog<[boolean, boolean]>('Upload Files confirmation', '', UploadConfirmDialog, {
file,
currentIndex: i,
totalFiles: okFiles.length,
});
const {finished} = Modal.createTrackedDialog<[boolean, boolean]>('Upload Files confirmation',
'', UploadConfirmDialog, {
file,
currentIndex: i,
totalFiles: okFiles.length,
},
);
const [shouldContinue, shouldUploadAll] = await finished;
if (!shouldContinue) break;
if (shouldUploadAll) {

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import {MatrixClientPeg} from './MatrixClientPeg';
import dis from "./dispatcher/dispatcher";
import {
hideToast as hideBulkUnverifiedSessionsToast,
showToast as showBulkUnverifiedSessionsToast,
@ -28,11 +29,15 @@ import {
hideToast as hideUnverifiedSessionsToast,
showToast as showUnverifiedSessionsToast,
} from "./toasts/UnverifiedSessionToast";
import {privateShouldBeEncrypted} from "./createRoom";
import { isSecretStorageBeingAccessed, accessSecretStorage } from "./SecurityManager";
import { isSecureBackupRequired } from './utils/WellKnownUtils';
import { isLoggedIn } from './components/structures/MatrixChat';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
export default class DeviceListener {
private dispatcherRef: string;
// device IDs for which the user has dismissed the verify toast ('Later')
private dismissed = new Set<string>();
// has the user dismissed any of the various nag toasts to setup encryption on this device?
@ -60,6 +65,8 @@ export default class DeviceListener {
MatrixClientPeg.get().on('crossSigning.keysChanged', this._onCrossSingingKeysChanged);
MatrixClientPeg.get().on('accountData', this._onAccountData);
MatrixClientPeg.get().on('sync', this._onSync);
MatrixClientPeg.get().on('RoomState.events', this._onRoomStateEvents);
this.dispatcherRef = dis.register(this._onAction);
this._recheck();
}
@ -72,6 +79,11 @@ export default class DeviceListener {
MatrixClientPeg.get().removeListener('crossSigning.keysChanged', this._onCrossSingingKeysChanged);
MatrixClientPeg.get().removeListener('accountData', this._onAccountData);
MatrixClientPeg.get().removeListener('sync', this._onSync);
MatrixClientPeg.get().removeListener('RoomState.events', this._onRoomStateEvents);
}
if (this.dispatcherRef) {
dis.unregister(this.dispatcherRef);
this.dispatcherRef = null;
}
this.dismissed.clear();
this.dismissedThisDeviceToast = false;
@ -158,6 +170,21 @@ export default class DeviceListener {
if (state === 'PREPARED' && prevState === null) this._recheck();
};
_onRoomStateEvents = (ev: MatrixEvent) => {
if (ev.getType() !== "m.room.encryption") {
return;
}
// If a room changes to encrypted, re-check as it may be our first
// encrypted room. This also catches encrypted room creation as well.
this._recheck();
};
_onAction = ({ action }) => {
if (action !== "on_logged_in") return;
this._recheck();
};
// The server doesn't tell us when key backup is set up, so we poll
// & cache the result
async _getKeyBackupInfo() {
@ -170,9 +197,10 @@ export default class DeviceListener {
}
private shouldShowSetupEncryptionToast() {
// In a default configuration, show the toasts. If the well-known config causes e2ee default to be false
// then do not show the toasts until user is in at least one encrypted room.
if (privateShouldBeEncrypted()) return true;
// If we're in the middle of a secret storage operation, we're likely
// modifying the state involved here, so don't add new toasts to setup.
if (isSecretStorageBeingAccessed()) return false;
// Show setup toasts once the user is in at least one encrypted room.
const cli = MatrixClientPeg.get();
return cli && cli.getRooms().some(r => cli.isRoomEncrypted(r.roomId));
}
@ -189,15 +217,20 @@ export default class DeviceListener {
if (!cli.isInitialSyncComplete()) return;
const crossSigningReady = await cli.isCrossSigningReady();
const secretStorageReady = await cli.isSecretStorageReady();
const allSystemsReady = crossSigningReady && secretStorageReady;
if (this.dismissedThisDeviceToast || crossSigningReady) {
if (this.dismissedThisDeviceToast || allSystemsReady) {
hideSetupEncryptionToast();
} else if (this.shouldShowSetupEncryptionToast()) {
// make sure our keys are finished downloading
await cli.downloadKeys([cli.getUserId()]);
// cross signing isn't enabled - nag to enable it
// There are 3 different toasts for:
if (cli.getStoredCrossSigningForUser(cli.getUserId())) {
if (
!cli.getCrossSigningId() &&
cli.getStoredCrossSigningForUser(cli.getUserId())
) {
// Cross-signing on account but this device doesn't trust the master key (verify this session)
showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION);
} else {
@ -207,7 +240,15 @@ export default class DeviceListener {
showSetupEncryptionToast(SetupKind.UPGRADE_ENCRYPTION);
} else {
// No cross-signing or key backup on account (set up encryption)
showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION);
await cli.waitForClientWellKnown();
if (isSecureBackupRequired() && isLoggedIn()) {
// If we're meant to set up, and Secure Backup is required,
// trigger the flow directly without a toast once logged in.
hideSetupEncryptionToast();
accessSecretStorage();
} else {
showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION);
}
}
}
}

View file

@ -339,33 +339,9 @@ class HtmlHighlighter extends BaseHighlighter<string> {
}
}
class TextHighlighter extends BaseHighlighter<React.ReactNode> {
private key = 0;
/* create a <span> node to hold the given content
*
* snippet: content of the span
* highlight: true to highlight as a search match
*
* returns a React node
*/
protected processSnippet(snippet: string, highlight: boolean): React.ReactNode {
const key = this.key++;
let node = <span key={key} className={highlight ? this.highlightClass : null}>
{ snippet }
</span>;
if (highlight && this.highlightLink) {
node = <a key={key} href={this.highlightLink}>{ node }</a>;
}
return node;
}
}
interface IContent {
format?: string;
// eslint-disable-next-line camelcase
formatted_body?: string;
body: string;
}
@ -474,8 +450,13 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
});
return isDisplayedWithHtml ?
<span key="body" ref={opts.ref} className={className} dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" /> :
<span key="body" ref={opts.ref} className={className} dir="auto">{ strippedBody }</span>;
<span
key="body"
ref={opts.ref}
className={className}
dangerouslySetInnerHTML={{ __html: safeBody }}
dir="auto"
/> : <span key="body" ref={opts.ref} className={className} dir="auto">{ strippedBody }</span>;
}
/**

View file

@ -43,6 +43,7 @@ import DeviceListener from "./DeviceListener";
import {Jitsi} from "./widgets/Jitsi";
import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "./BasePlatform";
import {decodeBase64, encodeBase64} from "matrix-js-sdk/src/crypto/olmlib";
import ThreepidInviteStore from "./stores/ThreepidInviteStore";
const HOMESERVER_URL_KEY = "mx_hs_url";
const ID_SERVER_URL_KEY = "mx_is_url";
@ -708,17 +709,30 @@ export async function onLoggedOut() {
// that can occur when components try to use a null client.
dis.dispatch({action: 'on_logged_out'}, true);
stopMatrixClient();
await _clearStorage();
await _clearStorage({deleteEverything: true});
}
/**
* @param {object} opts Options for how to clear storage.
* @returns {Promise} promise which resolves once the stores have been cleared
*/
async function _clearStorage() {
async function _clearStorage(opts: {deleteEverything: boolean}) {
Analytics.disable();
if (window.localStorage) {
// try to save any 3pid invites from being obliterated
const pendingInvites = ThreepidInviteStore.instance.getWireInvites();
window.localStorage.clear();
// now restore those invites
if (!opts?.deleteEverything) {
pendingInvites.forEach(i => {
const roomId = i.roomId;
delete i.roomId; // delete to avoid confusing the store
ThreepidInviteStore.instance.storeInvite(roomId, i);
});
}
}
if (window.sessionStorage) {

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import commonmark from 'commonmark';
import escape from 'lodash/escape';
import {escape} from "lodash";
const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];

View file

@ -31,7 +31,7 @@ import {verificationMethods} from 'matrix-js-sdk/src/crypto';
import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler";
import * as StorageManager from './utils/StorageManager';
import IdentityAuthClient from './IdentityAuthClient';
import { cacheDehydrationKey, crossSigningCallbacks } from './CrossSigningManager';
import { cacheDehydrationKey, crossSigningCallbacks } from './SecurityManager';
import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
export interface IMatrixClientCreds {

View file

@ -151,7 +151,7 @@ export class ModalManager {
prom: Promise<React.ComponentType>,
props?: IProps<T>,
className?: string,
options?: IOptions<T>
options?: IOptions<T>,
) {
const modal: IModal<T> = {
onFinished: props ? props.onFinished : null,
@ -182,7 +182,7 @@ export class ModalManager {
private getCloseFn<T extends any[]>(
modal: IModal<T>,
props: IProps<T>
props: IProps<T>,
): [IHandle<T>["close"], IHandle<T>["finished"]] {
const deferred = defer<T>();
return [async (...args: T) => {
@ -264,7 +264,7 @@ export class ModalManager {
className?: string,
isPriorityModal = false,
isStaticModal = false,
options: IOptions<T> = {}
options: IOptions<T> = {},
): IHandle<T> {
const {modal, closeDialog, onFinishedProm} = this.buildModal<T>(prom, props, className, options);
if (isPriorityModal) {
@ -287,7 +287,7 @@ export class ModalManager {
private appendDialogAsync<T extends any[]>(
prom: Promise<React.ComponentType>,
props?: IProps<T>,
className?: string
className?: string,
): IHandle<T> {
const {modal, closeDialog, onFinishedProm} = this.buildModal<T>(prom, props, className, {});

View file

@ -33,6 +33,7 @@ import Modal from './Modal';
import SettingsStore from "./settings/SettingsStore";
import { hideToast as hideNotificationsToast } from "./toasts/DesktopNotificationsToast";
import {SettingLevel} from "./settings/SettingLevel";
import {isPushNotifyDisabled} from "./settings/controllers/NotificationControllers";
/*
* Dispatches:
@ -258,7 +259,7 @@ export const Notifier = {
}
// set the notifications_hidden flag, as the user has knowingly interacted
// with the setting we shouldn't nag them any further
this.setToolbarHidden(true);
this.setPromptHidden(true);
},
isEnabled: function() {
@ -283,7 +284,7 @@ export const Notifier = {
return SettingsStore.getValue("audioNotificationsEnabled");
},
setToolbarHidden: function(hidden: boolean, persistent = true) {
setPromptHidden: function(hidden: boolean, persistent = true) {
this.toolbarHidden = hidden;
Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', hidden);
@ -296,17 +297,17 @@ export const Notifier = {
}
},
shouldShowToolbar: function() {
shouldShowPrompt: function() {
const client = MatrixClientPeg.get();
if (!client) {
return false;
}
const isGuest = client.isGuest();
return !isGuest && this.supportsDesktopNotifications() &&
!this.isEnabled() && !this._isToolbarHidden();
return !isGuest && this.supportsDesktopNotifications() && !isPushNotifyDisabled() &&
!this.isEnabled() && !this._isPromptHidden();
},
_isToolbarHidden: function() {
_isPromptHidden: function() {
// Check localStorage for any such meta data
if (global.localStorage) {
return global.localStorage.getItem("notifications_hidden") === "true";

View file

@ -23,6 +23,8 @@ import Modal from './Modal';
import * as sdk from './';
import { _t } from './languageHandler';
import {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog";
import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog";
import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore";
/**
* Invites multiple addresses to a room
@ -56,6 +58,23 @@ export function showRoomInviteDialog(roomId) {
);
}
export function showCommunityRoomInviteDialog(roomId, communityName) {
Modal.createTrackedDialog(
'Invite Users to Community', '', CommunityPrototypeInviteDialog, {communityName, roomId},
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
);
}
export function showCommunityInviteDialog(communityId) {
const chat = CommunityPrototypeStore.instance.getGeneralChat(communityId);
if (chat) {
const name = CommunityPrototypeStore.instance.getCommunityName(communityId);
showCommunityRoomInviteDialog(chat.roomId, name);
} else {
throw new Error("Failed to locate appropriate room to start an invite in");
}
}
/**
* Checks if the given MatrixEvent is a valid 3rd party user invite.
* @param {MatrixEvent} event The event to check
@ -77,7 +96,7 @@ export function isValid3pidInvite(event) {
export function inviteUsersToRoom(roomId, userIds) {
return inviteMultipleToRoom(roomId, userIds).then((result) => {
const room = MatrixClientPeg.get().getRoom(roomId);
return _showAnyInviteErrors(result.states, room, result.inviter);
showAnyInviteErrors(result.states, room, result.inviter);
}).catch((err) => {
console.error(err.stack);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -88,7 +107,7 @@ export function inviteUsersToRoom(roomId, userIds) {
});
}
function _showAnyInviteErrors(addrs, room, inviter) {
export function showAnyInviteErrors(addrs, room, inviter) {
// Show user any errors
const failedUsers = Object.keys(addrs).filter(a => addrs[a] === 'error');
if (failedUsers.length === 1 && inviter.fatal) {
@ -100,6 +119,7 @@ function _showAnyInviteErrors(addrs, room, inviter) {
title: _t("Failed to invite users to the room:", {roomName: room.name}),
description: inviter.getErrorText(failedUsers[0]),
});
return false;
} else {
const errorList = [];
for (const addr of failedUsers) {
@ -118,8 +138,9 @@ function _showAnyInviteErrors(addrs, room, inviter) {
title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}),
description,
});
return false;
}
}
return addrs;
return true;
}

View file

@ -1,5 +1,5 @@
/*
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.
@ -21,6 +21,9 @@ import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase';
import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey';
import { _t } from './languageHandler';
import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib";
import { isSecureBackupRequired } from './utils/WellKnownUtils';
import AccessSecretStorageDialog from './components/views/dialogs/security/AccessSecretStorageDialog';
import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreKeyBackupDialog';
// This stores the secret storage private keys in memory for the JS SDK. This is
// only meant to act as a cache to avoid prompting the user multiple times
@ -44,6 +47,17 @@ function isCachingAllowed() {
return secretStorageBeingAccessed;
}
/**
* This can be used by other components to check if secret storage access is in
* progress, so that we can e.g. avoid intermittently showing toasts during
* secret storage setup.
*
* @returns {bool}
*/
export function isSecretStorageBeingAccessed() {
return secretStorageBeingAccessed;
}
export class AccessCancelledError extends Error {
constructor() {
super("Secret storage access canceled");
@ -67,11 +81,11 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
if (keyInfoEntries.length > 1) {
throw new Error("Multiple storage key requests not implemented");
}
const [name, info] = keyInfoEntries[0];
const [keyId, keyInfo] = keyInfoEntries[0];
// Check the in-memory cache
if (isCachingAllowed() && secretStorageKeys[name]) {
return [name, secretStorageKeys[name]];
if (isCachingAllowed() && secretStorageKeys[keyId]) {
return [keyId, secretStorageKeys[keyId]];
}
// if we dehydrated a device, see if that key works for SSSS
@ -94,23 +108,21 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
if (passphrase) {
return deriveKey(
passphrase,
info.passphrase.salt,
info.passphrase.iterations,
keyInfo.passphrase.salt,
keyInfo.passphrase.iterations,
);
} else {
return decodeRecoveryKey(recoveryKey);
}
};
const AccessSecretStorageDialog =
sdk.getComponent("dialogs.secretstorage.AccessSecretStorageDialog");
const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "",
AccessSecretStorageDialog,
/* props= */
{
keyInfo: info,
keyInfo,
checkPrivateKey: async (input) => {
const key = await inputToKey(input);
return await MatrixClientPeg.get().checkSecretStorageKey(key, info);
return await MatrixClientPeg.get().checkSecretStorageKey(key, keyInfo);
},
},
/* className= */ null,
@ -132,11 +144,15 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
const key = await inputToKey(input);
// Save to cache to avoid future prompts in the current session
if (isCachingAllowed()) {
secretStorageKeys[name] = key;
}
cacheSecretStorageKey(keyId, key);
return [name, key];
return [keyId, key];
}
function cacheSecretStorageKey(keyId, key) {
if (isCachingAllowed()) {
secretStorageKeys[keyId] = key;
}
}
const onSecretRequested = async function({
@ -152,7 +168,7 @@ const onSecretRequested = async function({
return;
}
if (!deviceTrust || !deviceTrust.isVerified()) {
console.log(`CrossSigningManager: Ignoring request from untrusted device ${deviceId}`);
console.log(`Ignoring secret request from untrusted device ${deviceId}`);
return;
}
if (
@ -184,13 +200,13 @@ const onSecretRequested = async function({
export const crossSigningCallbacks = {
getSecretStorageKey,
cacheSecretStorageKey,
onSecretRequested,
};
export async function promptForBackupPassphrase() {
let key;
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
const { finished } = Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {
showSummary: false, keyCallback: k => key = k,
}, null, /* priority = */ false, /* static = */ true);
@ -230,11 +246,22 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
// This dialog calls bootstrap itself after guiding the user through
// passphrase creation.
const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '',
import("./async-components/views/dialogs/secretstorage/CreateSecretStorageDialog"),
import("./async-components/views/dialogs/security/CreateSecretStorageDialog"),
{
force: forceReset,
forceReset,
},
null,
/* priority = */ false,
/* static = */ true,
/* options = */ {
onBeforeClose(reason) {
// If Secure Backup is required, you cannot leave the modal.
if (reason === "backgroundClick") {
return !isSecureBackupRequired();
}
return true;
},
},
null, /* priority = */ false, /* static = */ true,
);
const [confirmed] = await finished;
if (!confirmed) {
@ -242,13 +269,13 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
}
} else {
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
await cli.bootstrapSecretStorage({
await cli.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async (makeRequest) => {
const { finished } = Modal.createTrackedDialog(
'Cross-signing keys dialog', '', InteractiveAuthDialog,
{
title: _t("Setting up keys"),
matrixClient: MatrixClientPeg.get(),
matrixClient: cli,
makeRequest,
},
);
@ -257,7 +284,9 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
throw new Error("Cross-signing key upload auth canceled");
}
},
getBackupPassphrase: promptForBackupPassphrase,
});
await cli.bootstrapSecretStorage({
getKeyBackupPassphrase: promptForBackupPassphrase,
});
}

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import _clamp from 'lodash/clamp';
import {clamp} from "lodash";
export default class SendHistoryManager {
history: Array<HistoryItem> = [];
@ -54,7 +54,7 @@ export default class SendHistoryManager {
}
getItem(offset: number): ?HistoryItem {
this.currentIndex = _clamp(this.currentIndex + offset, 0, this.history.length - 1);
this.currentIndex = clamp(this.currentIndex + offset, 0, this.history.length - 1);
return this.history[this.currentIndex];
}
}

View file

@ -38,12 +38,14 @@ import {inviteUsersToRoom} from "./RoomInvite";
import { WidgetType } from "./widgets/WidgetType";
import { Jitsi } from "./widgets/Jitsi";
import { parseFragment as parseHtml } from "parse5";
import sendBugReport from "./rageshake/submit-rageshake";
import SdkConfig from "./SdkConfig";
import BugReportDialog from "./components/views/dialogs/BugReportDialog";
import { ensureDMExists } from "./createRoom";
import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload";
import { Action } from "./dispatcher/actions";
import { EffectiveMembership, getEffectiveMembership } from "./utils/membership";
import { EffectiveMembership, getEffectiveMembership, leaveRoomBehaviour } from "./utils/membership";
import SdkConfig from "./SdkConfig";
import SettingsStore from "./settings/SettingsStore";
import {UIFeature} from "./settings/UIFeature";
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
interface HTMLInputEvent extends Event {
@ -88,6 +90,7 @@ interface ICommandOpts {
runFn?: RunFn;
category: string;
hideCompletionAfterSpace?: boolean;
isEnabled?(): boolean;
}
export class Command {
@ -98,6 +101,7 @@ export class Command {
runFn: undefined | RunFn;
category: string;
hideCompletionAfterSpace: boolean;
_isEnabled?: () => boolean;
constructor(opts: ICommandOpts) {
this.command = opts.command;
@ -107,6 +111,7 @@ export class Command {
this.runFn = opts.runFn;
this.category = opts.category || CommandCategories.other;
this.hideCompletionAfterSpace = opts.hideCompletionAfterSpace || false;
this._isEnabled = opts.isEnabled;
}
getCommand() {
@ -126,6 +131,10 @@ export class Command {
getUsage() {
return _t('Usage') + ': ' + this.getCommandWithArgs();
}
isEnabled() {
return this._isEnabled ? this._isEnabled() : true;
}
}
function reject(error) {
@ -154,6 +163,19 @@ export const Commands = [
},
category: CommandCategories.messages,
}),
new Command({
command: 'lenny',
args: '<message>',
description: _td('Prepends ( ͡° ͜ʖ ͡°) to a plain-text message'),
runFn: function(roomId, args) {
let message = '( ͡° ͜ʖ ͡°)';
if (args) {
message = message + ' ' + args;
}
return success(MatrixClientPeg.get().sendTextMessage(roomId, message));
},
category: CommandCategories.messages,
}),
new Command({
command: 'plain',
args: '<message>',
@ -601,11 +623,7 @@ export const Commands = [
}
if (!targetRoomId) targetRoomId = roomId;
return success(
cli.leaveRoomChain(targetRoomId).then(function() {
dis.dispatch({action: 'view_next_room'});
}),
);
return success(leaveRoomBehaviour(targetRoomId));
},
category: CommandCategories.actions,
}),
@ -781,6 +799,7 @@ export const Commands = [
command: 'addwidget',
args: '<url | embed code | Jitsi url>',
description: _td('Adds a custom widget by URL to the room'),
isEnabled: () => SettingsStore.getValue(UIFeature.Widgets),
runFn: function(roomId, widgetUrl) {
if (!widgetUrl) {
return reject(_t("Please supply a widget URL or embed code"));
@ -864,12 +883,12 @@ export const Commands = [
_t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session' +
' %(deviceId)s is "%(fprint)s" which does not match the provided key ' +
'"%(fingerprint)s". This could mean your communications are being intercepted!',
{
fprint,
userId,
deviceId,
fingerprint,
}));
{
fprint,
userId,
deviceId,
fingerprint,
}));
}
await cli.setDeviceVerified(userId, deviceId, true);
@ -883,7 +902,7 @@ export const Commands = [
{
_t('The signing key you provided matches the signing key you received ' +
'from %(userId)s\'s session %(deviceId)s. Session marked as verified.',
{userId, deviceId})
{userId, deviceId})
}
</p>
</div>,
@ -963,19 +982,13 @@ export const Commands = [
command: "rageshake",
aliases: ["bugreport"],
description: _td("Send a bug report with logs"),
isEnabled: () => !!SdkConfig.get().bug_report_endpoint_url,
args: "<description>",
runFn: function(roomId, args) {
return success(
sendBugReport(SdkConfig.get().bug_report_endpoint_url, {
userText: args,
sendLogs: true,
}).then(() => {
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
Modal.createTrackedDialog('Slash Commands', 'Rageshake sent', InfoDialog, {
title: _t('Logs sent'),
description: _t('Thank you!'),
});
}),
Modal.createTrackedDialog('Slash Commands', 'Bug Report Dialog', BugReportDialog, {
initialText: args,
}).finished,
);
},
category: CommandCategories.advanced,
@ -1047,7 +1060,7 @@ Commands.forEach(cmd => {
});
});
export function parseCommandString(input) {
export function parseCommandString(input: string) {
// trim any trailing whitespace, as it can confuse the parser for
// IRC-style commands
input = input.replace(/\s+$/, '');
@ -1074,10 +1087,10 @@ export function parseCommandString(input) {
* processing the command, or 'promise' if a request was sent out.
* Returns null if the input didn't match a command.
*/
export function getCommand(roomId, input) {
export function getCommand(roomId: string, input: string) {
const {cmd, args} = parseCommandString(input);
if (CommandMap.has(cmd)) {
if (CommandMap.has(cmd) && CommandMap.get(cmd).isEnabled()) {
return () => CommandMap.get(cmd).run(roomId, args, cmd);
}
}

View file

@ -19,6 +19,7 @@ import { _t } from './languageHandler';
import * as Roles from './Roles';
import {isValid3pidInvite} from "./RoomInvite";
import SettingsStore from "./settings/SettingsStore";
import {WidgetType} from "./widgets/WidgetType";
import {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList";
function textForMemberEvent(ev) {
@ -475,6 +476,10 @@ function textForWidgetEvent(event) {
const {name: prevName, type: prevType, url: prevUrl} = event.getPrevContent();
const {name, type, url} = event.getContent() || {};
if (WidgetType.JITSI.matches(type) || WidgetType.JITSI.matches(prevType)) {
return textForJitsiWidgetEvent(event, senderName, url, prevUrl);
}
let widgetName = name || prevName || type || prevType || '';
// Apply sentence case to widget name
if (widgetName && widgetName.length > 0) {
@ -500,6 +505,24 @@ function textForWidgetEvent(event) {
}
}
function textForJitsiWidgetEvent(event, senderName, url, prevUrl) {
if (url) {
if (prevUrl) {
return _t('Group call modified by %(senderName)s', {
senderName,
});
} else {
return _t('Group call started by %(senderName)s', {
senderName,
});
}
} else {
return _t('Group call ended by %(senderName)s', {
senderName,
});
}
}
function textForMjolnirEvent(event) {
const senderName = event.getSender();
const {entity: prevEntity} = event.getPrevContent();

View file

@ -186,7 +186,14 @@ export default class WidgetMessaging {
isUserWidget: this.isUserWidget,
onFinished: async (confirm) => {
const responseBody = {success: confirm};
const responseBody = {
// Legacy (early draft) fields
success: confirm,
// New style MSC1960 fields
state: confirm ? "allowed" : "blocked",
original_request_id: ev.requestId, // eslint-disable-line camelcase
};
if (confirm) {
const credentials = await MatrixClientPeg.get().getOpenIdToken();
Object.assign(responseBody, credentials);

View file

@ -168,7 +168,7 @@ const shortcuts: Record<Categories, IShortcut[]> = {
key: Key.U,
}],
description: _td("Upload a file"),
}
},
],
[Categories.ROOM_LIST]: [

View file

@ -190,7 +190,7 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({children, handleHomeEn
ev.preventDefault();
ev.stopPropagation();
} else if (onKeyDown) {
return onKeyDown(ev, state);
return onKeyDown(ev, context.state);
}
}, [context.state, onKeyDown, handleHomeEnd]);

View file

@ -30,6 +30,7 @@ const Toolbar: React.FC<IProps> = ({children, ...props}) => {
const target = ev.target as HTMLElement;
let handled = true;
// HOME and END are handled by RovingTabIndexProvider
switch (ev.key) {
case Key.ARROW_UP:
case Key.ARROW_DOWN:
@ -47,8 +48,6 @@ const Toolbar: React.FC<IProps> = ({children, ...props}) => {
}
break;
// HOME and END are handled by RovingTabIndexProvider
default:
handled = false;
}

View file

@ -20,7 +20,7 @@ import React from "react";
import AccessibleTooltipButton from "../../components/views/elements/AccessibleTooltipButton";
interface IProps extends React.ComponentProps<typeof AccessibleTooltipButton> {
interface IProps extends React.ComponentProps<typeof AccessibleTooltipButton> {
// whether or not the context menu is currently open
isExpanded: boolean;
}

View file

@ -26,8 +26,9 @@ interface IProps extends React.ComponentProps<typeof AccessibleButton> {
// Semantic component for representing a role=menuitem
export const MenuItem: React.FC<IProps> = ({children, label, ...props}) => {
const ariaLabel = props["aria-label"] || label;
return (
<AccessibleButton {...props} role="menuitem" tabIndex={-1} aria-label={label}>
<AccessibleButton {...props} role="menuitem" tabIndex={-1} aria-label={ariaLabel}>
{ children }
</AccessibleButton>
);

View file

@ -20,7 +20,8 @@ import AccessibleTooltipButton from "../../components/views/elements/AccessibleT
import {useRovingTabIndex} from "../RovingTabIndex";
import {Ref} from "./types";
interface IProps extends Omit<React.ComponentProps<typeof AccessibleTooltipButton>, "onFocus" | "inputRef" | "tabIndex"> {
type ATBProps = React.ComponentProps<typeof AccessibleTooltipButton>;
interface IProps extends Omit<ATBProps, "onFocus" | "inputRef" | "tabIndex"> {
inputRef?: Ref;
}

View file

@ -16,7 +16,6 @@ limitations under the License.
import React from "react";
import AccessibleButton from "../../components/views/elements/AccessibleButton";
import {useRovingTabIndex} from "../RovingTabIndex";
import {FocusHandler, Ref} from "./types";

View file

@ -1,70 +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 * as sdk from "../../../../index";
import { _t } from "../../../../languageHandler";
export default class IgnoreRecoveryReminderDialog extends React.PureComponent {
static propTypes = {
onDontAskAgain: PropTypes.func.isRequired,
onFinished: PropTypes.func.isRequired,
onSetup: PropTypes.func.isRequired,
}
onDontAskAgainClick = () => {
this.props.onFinished();
this.props.onDontAskAgain();
}
onSetupClick = () => {
this.props.onFinished();
this.props.onSetup();
}
render() {
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
return (
<BaseDialog className="mx_IgnoreRecoveryReminderDialog"
onFinished={this.props.onFinished}
title={_t("Are you sure?")}
>
<div>
<p>{_t(
"Without setting up Secure Message Recovery, " +
"you'll lose your secure message history when you " +
"log out.",
)}</p>
<p>{_t(
"If you don't want to set this up now, you can later " +
"in Settings.",
)}</p>
<div className="mx_Dialog_buttons">
<DialogButtons
primaryButton={_t("Set up")}
onPrimaryButtonClick={this.onSetupClick}
cancelButton={_t("Don't ask again")}
onCancel={this.onDontAskAgainClick}
/>
</div>
</div>
</BaseDialog>
);
}
}

View file

@ -21,7 +21,7 @@ import * as sdk from '../../../../index';
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
import PropTypes from 'prop-types';
import {_t, _td} from '../../../../languageHandler';
import { accessSecretStorage } from '../../../../CrossSigningManager';
import { accessSecretStorage } from '../../../../SecurityManager';
import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
import {copyNode} from "../../../../utils/strings";
import PassphraseField from "../../../../components/views/auth/PassphraseField";

View file

@ -22,7 +22,7 @@ import {MatrixClientPeg} from '../../../../MatrixClientPeg';
import FileSaver from 'file-saver';
import {_t, _td} from '../../../../languageHandler';
import Modal from '../../../../Modal';
import { promptForBackupPassphrase } from '../../../../CrossSigningManager';
import { promptForBackupPassphrase } from '../../../../SecurityManager';
import {copyNode} from "../../../../utils/strings";
import {SSOAuthEntry} from "../../../../components/views/auth/InteractiveAuthEntryComponents";
import PassphraseField from "../../../../components/views/auth/PassphraseField";
@ -30,6 +30,8 @@ import StyledRadioButton from '../../../../components/views/elements/StyledRadio
import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
import DialogButtons from "../../../../components/views/elements/DialogButtons";
import InlineSpinner from "../../../../components/views/elements/InlineSpinner";
import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog";
import { isSecureBackupRequired } from '../../../../utils/WellKnownUtils';
const PHASE_LOADING = 0;
const PHASE_LOADERROR = 1;
@ -55,12 +57,12 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
static propTypes = {
hasCancel: PropTypes.bool,
accountPassword: PropTypes.string,
force: PropTypes.bool,
forceReset: PropTypes.bool,
};
static defaultProps = {
hasCancel: true,
force: false,
forceReset: false,
};
constructor(props) {
@ -85,8 +87,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
canUploadKeysWithPasswordOnly: null,
accountPassword: props.accountPassword || "",
accountPasswordCorrect: null,
passPhraseKeySelected: CREATE_STORAGE_OPTION_KEY,
canSkip: !isSecureBackupRequired(),
};
this._passphraseField = createRef();
@ -117,8 +119,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
MatrixClientPeg.get().isCryptoEnabled() && await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo)
);
const { force } = this.props;
const phase = (backupInfo && !force) ? PHASE_MIGRATE : PHASE_CHOOSE_KEY_PASSPHRASE;
const { forceReset } = this.props;
const phase = (backupInfo && !forceReset) ? PHASE_MIGRATE : PHASE_CHOOSE_KEY_PASSPHRASE;
this.setState({
phase,
@ -276,20 +278,28 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
const cli = MatrixClientPeg.get();
const { force } = this.props;
const { forceReset } = this.props;
try {
if (force) {
console.log("Forcing secret storage reset"); // log something so we can debug this later
if (forceReset) {
console.log("Forcing secret storage reset");
await cli.bootstrapSecretStorage({
authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
createSecretStorageKey: async () => this._recoveryKey,
setupNewKeyBackup: true,
setupNewSecretStorage: true,
});
} else {
await cli.bootstrapSecretStorage({
// For password authentication users after 2020-09, this cross-signing
// step will be a no-op since it is now setup during registration or login
// when needed. We should keep this here to cover other cases such as:
// * Users with existing sessions prior to 2020-09 changes
// * SSO authentication users which require interactive auth to upload
// keys (and also happen to skip all post-authentication flows at the
// moment via token login)
await cli.bootstrapCrossSigning({
authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
});
await cli.bootstrapSecretStorage({
createSecretStorageKey: async () => this._recoveryKey,
keyBackupInfo: this.state.backupInfo,
setupNewKeyBackup: !this.state.backupInfo,
@ -337,7 +347,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
// so let's stash it here, rather than prompting for it twice.
const keyCallback = k => this._backupKey = k;
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
const { finished } = Modal.createTrackedDialog(
'Restore Backup', '', RestoreKeyBackupDialog,
{
@ -475,7 +484,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
primaryButton={_t("Continue")}
onPrimaryButtonClick={this._onChooseKeyPassphraseFormSubmit}
onCancel={this._onCancelClick}
hasCancel={true}
hasCancel={this.state.canSkip}
/>
</form>;
}
@ -692,7 +701,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
<div className="mx_Dialog_buttons">
<DialogButtons primaryButton={_t('Retry')}
onPrimaryButtonClick={this._onLoadRetryClick}
hasCancel={true}
hasCancel={this.state.canSkip}
onCancel={this._onCancel}
/>
</div>
@ -719,7 +728,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
_titleForPhase(phase) {
switch (phase) {
case PHASE_CHOOSE_KEY_PASSPHRASE:
return _t('Set up Secure backup');
return _t('Set up Secure Backup');
case PHASE_MIGRATE:
return _t('Upgrade your encryption');
case PHASE_PASSPHRASE:
@ -747,7 +756,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
<div className="mx_Dialog_buttons">
<DialogButtons primaryButton={_t('Retry')}
onPrimaryButtonClick={this._bootstrapSecretStorage}
hasCancel={true}
hasCancel={this.state.canSkip}
onCancel={this._onCancel}
/>
</div>

View file

@ -17,44 +17,40 @@ limitations under the License.
import FileSaver from 'file-saver';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
import { _t } from '../../../../languageHandler';
import { MatrixClient } from 'matrix-js-sdk';
import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption';
import * as sdk from '../../../index';
import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption';
import * as sdk from '../../../../index';
const PHASE_EDIT = 1;
const PHASE_EXPORTING = 2;
export default createReactClass({
displayName: 'ExportE2eKeysDialog',
propTypes: {
export default class ExportE2eKeysDialog extends React.Component {
static propTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
onFinished: PropTypes.func.isRequired,
},
};
getInitialState: function() {
return {
phase: PHASE_EDIT,
errStr: null,
};
},
constructor(props) {
super(props);
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._unmounted = false;
this._passphrase1 = createRef();
this._passphrase2 = createRef();
},
componentWillUnmount: function() {
this.state = {
phase: PHASE_EDIT,
errStr: null,
};
}
componentWillUnmount() {
this._unmounted = true;
},
}
_onPassphraseFormSubmit: function(ev) {
_onPassphraseFormSubmit = (ev) => {
ev.preventDefault();
const passphrase = this._passphrase1.current.value;
@ -69,9 +65,9 @@ export default createReactClass({
this._startExport(passphrase);
return false;
},
};
_startExport: function(passphrase) {
_startExport(passphrase) {
// extra Promise.resolve() to turn synchronous exceptions into
// asynchronous ones.
Promise.resolve().then(() => {
@ -102,15 +98,15 @@ export default createReactClass({
errStr: null,
phase: PHASE_EXPORTING,
});
},
}
_onCancelClick: function(ev) {
_onCancelClick = (ev) => {
ev.preventDefault();
this.props.onFinished(false);
return false;
},
};
render: function() {
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const disableForm = (this.state.phase === PHASE_EXPORTING);
@ -184,5 +180,5 @@ export default createReactClass({
</form>
</BaseDialog>
);
},
});
}
}

View file

@ -16,12 +16,11 @@ limitations under the License.
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { MatrixClient } from 'matrix-js-sdk';
import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption';
import * as sdk from '../../../../index';
import { _t } from '../../../../languageHandler';
function readFileAsArrayBuffer(file) {
return new Promise((resolve, reject) => {
@ -38,48 +37,45 @@ function readFileAsArrayBuffer(file) {
const PHASE_EDIT = 1;
const PHASE_IMPORTING = 2;
export default createReactClass({
displayName: 'ImportE2eKeysDialog',
propTypes: {
export default class ImportE2eKeysDialog extends React.Component {
static propTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
onFinished: PropTypes.func.isRequired,
},
};
getInitialState: function() {
return {
enableSubmit: false,
phase: PHASE_EDIT,
errStr: null,
};
},
constructor(props) {
super(props);
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._unmounted = false;
this._file = createRef();
this._passphrase = createRef();
},
componentWillUnmount: function() {
this.state = {
enableSubmit: false,
phase: PHASE_EDIT,
errStr: null,
};
}
componentWillUnmount() {
this._unmounted = true;
},
}
_onFormChange: function(ev) {
_onFormChange = (ev) => {
const files = this._file.current.files || [];
this.setState({
enableSubmit: (this._passphrase.current.value !== "" && files.length > 0),
});
},
};
_onFormSubmit: function(ev) {
_onFormSubmit = (ev) => {
ev.preventDefault();
this._startImport(this._file.current.files[0], this._passphrase.current.value);
return false;
},
};
_startImport: function(file, passphrase) {
_startImport(file, passphrase) {
this.setState({
errStr: null,
phase: PHASE_IMPORTING,
@ -105,15 +101,15 @@ export default createReactClass({
phase: PHASE_EDIT,
});
});
},
}
_onCancelClick: function(ev) {
_onCancelClick = (ev) => {
ev.preventDefault();
this.props.onFinished(false);
return false;
},
};
render: function() {
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const disableForm = (this.state.phase !== PHASE_EDIT);
@ -188,5 +184,5 @@ export default createReactClass({
</form>
</BaseDialog>
);
},
});
}
}

View file

@ -22,6 +22,7 @@ import {MatrixClientPeg} from '../../../../MatrixClientPeg';
import dis from "../../../../dispatcher/dispatcher";
import { _t } from "../../../../languageHandler";
import Modal from "../../../../Modal";
import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog";
import {Action} from "../../../../dispatcher/actions";
export default class NewRecoveryMethodDialog extends React.PureComponent {
@ -41,7 +42,6 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
}
onSetupClick = async () => {
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
Modal.createTrackedDialog(
'Restore Backup', '', RestoreKeyBackupDialog, {
onFinished: this.props.onFinished,

View file

@ -47,7 +47,7 @@ export default class CommandProvider extends AutocompleteProvider {
if (command[0] !== command[1]) {
// The input looks like a command with arguments, perform exact match
const name = command[1].substr(1); // strip leading `/`
if (CommandMap.has(name)) {
if (CommandMap.has(name) && CommandMap.get(name).isEnabled()) {
// some commands, namely `me` and `ddg` don't suit having the usage shown whilst typing their arguments
if (CommandMap.get(name).hideCompletionAfterSpace) return [];
matches = [CommandMap.get(name)];
@ -63,7 +63,7 @@ export default class CommandProvider extends AutocompleteProvider {
}
return matches.map((result) => {
return matches.filter(cmd => cmd.isEnabled()).map((result) => {
let completion = result.getCommand() + ' ';
const usedAlias = result.aliases.find(alias => `/${alias}` === command[1]);
// If the command (or an alias) is the same as the one they entered, we don't want to discard their arguments
@ -89,7 +89,11 @@ export default class CommandProvider extends AutocompleteProvider {
renderCompletions(completions: React.ReactNode[]): React.ReactNode {
return (
<div className="mx_Autocomplete_Completion_container_block" role="listbox" aria-label={_t("Command Autocomplete")}>
<div
className="mx_Autocomplete_Completion_container_block"
role="listbox"
aria-label={_t("Command Autocomplete")}
>
{ completions }
</div>
);

View file

@ -23,7 +23,7 @@ import {MatrixClientPeg} from '../MatrixClientPeg';
import QueryMatcher from './QueryMatcher';
import {PillCompletion} from './Components';
import * as sdk from '../index';
import _sortBy from 'lodash/sortBy';
import {sortBy} from "lodash";
import {makeGroupPermalink} from "../utils/permalinks/Permalinks";
import {ICompletion, ISelectionRange} from "./Autocompleter";
import FlairStore from "../stores/FlairStore";
@ -81,7 +81,7 @@ export default class CommunityProvider extends AutocompleteProvider {
const matchedString = command[0];
completions = this.matcher.match(matchedString);
completions = _sortBy(completions, [
completions = sortBy(completions, [
(c) => score(matchedString, c.groupId),
(c) => c.groupId.length,
]).map(({avatarUrl, groupId, name}) => ({
@ -91,15 +91,15 @@ export default class CommunityProvider extends AutocompleteProvider {
href: makeGroupPermalink(groupId),
component: (
<PillCompletion title={name} description={groupId}>
<BaseAvatar name={name || groupId}
width={24}
height={24}
url={avatarUrl ? cli.mxcUrlToHttp(avatarUrl, 24, 24) : null} />
<BaseAvatar
name={name || groupId}
width={24}
height={24}
url={avatarUrl ? cli.mxcUrlToHttp(avatarUrl, 24, 24) : null} />
</PillCompletion>
),
range,
}))
.slice(0, 4);
})).slice(0, 4);
}
return completions;
}

View file

@ -34,9 +34,9 @@ export const TextualCompletion = forwardRef<ITextualCompletionProps, any>((props
const {title, subtitle, description, className, ...restProps} = props;
return (
<div {...restProps}
className={classNames('mx_Autocomplete_Completion_block', className)}
role="option"
ref={ref}
className={classNames('mx_Autocomplete_Completion_block', className)}
role="option"
ref={ref}
>
<span className="mx_Autocomplete_Completion_title">{ title }</span>
<span className="mx_Autocomplete_Completion_subtitle">{ subtitle }</span>
@ -53,9 +53,9 @@ export const PillCompletion = forwardRef<IPillCompletionProps, any>((props, ref)
const {title, subtitle, description, className, children, ...restProps} = props;
return (
<div {...restProps}
className={classNames('mx_Autocomplete_Completion_pill', className)}
role="option"
ref={ref}
className={classNames('mx_Autocomplete_Completion_pill', className)}
role="option"
ref={ref}
>
{ children }
<span className="mx_Autocomplete_Completion_title">{ title }</span>

View file

@ -23,8 +23,7 @@ import AutocompleteProvider from './AutocompleteProvider';
import QueryMatcher from './QueryMatcher';
import {PillCompletion} from './Components';
import {ICompletion, ISelectionRange} from './Autocompleter';
import _uniq from 'lodash/uniq';
import _sortBy from 'lodash/sortBy';
import {uniq, sortBy} from 'lodash';
import SettingsStore from "../settings/SettingsStore";
import { shortcodeToUnicode } from '../HtmlUtils';
import { EMOJI, IEmoji } from '../emoji';
@ -115,7 +114,7 @@ export default class EmojiProvider extends AutocompleteProvider {
}
// Finally, sort by original ordering
sorters.push((c) => c._orderBy);
completions = _sortBy(_uniq(completions), sorters);
completions = sortBy(uniq(completions), sorters);
completions = completions.map(({shortname}) => {
const unicode = shortcodeToUnicode(shortname);
@ -139,7 +138,11 @@ export default class EmojiProvider extends AutocompleteProvider {
renderCompletions(completions: React.ReactNode[]): React.ReactNode {
return (
<div className="mx_Autocomplete_Completion_container_pill" role="listbox" aria-label={_t("Emoji Autocomplete")}>
<div
className="mx_Autocomplete_Completion_container_pill"
role="listbox"
aria-label={_t("Emoji Autocomplete")}
>
{ completions }
</div>
);

View file

@ -16,8 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import _at from 'lodash/at';
import _uniq from 'lodash/uniq';
import {at, uniq} from 'lodash';
import {removeHiddenChars} from "matrix-js-sdk/src/utils";
interface IOptions<T extends {}> {
@ -73,7 +72,7 @@ export default class QueryMatcher<T extends Object> {
// type for their values. We assume that those values who's keys have
// been specified will be string. Also, we cannot infer all the
// types of the keys of the objects at compile.
const keyValues = _at<string>(<any>object, this._options.keys);
const keyValues = at<string>(<any>object, this._options.keys);
if (this._options.funcs) {
for (const f of this._options.funcs) {
@ -137,7 +136,7 @@ export default class QueryMatcher<T extends Object> {
});
// Now map the keys to the result objects. Also remove any duplicates.
return _uniq(matches.map((match) => match.object));
return uniq(matches.map((match) => match.object));
}
private processQuery(query: string): string {

View file

@ -27,7 +27,7 @@ import {PillCompletion} from './Components';
import * as sdk from '../index';
import {makeRoomPermalink} from "../utils/permalinks/Permalinks";
import {ICompletion, ISelectionRange} from "./Autocompleter";
import { uniqBy, sortBy } from 'lodash';
import {uniqBy, sortBy} from "lodash";
const ROOM_REGEX = /\B#\S*/g;
@ -110,9 +110,7 @@ export default class RoomProvider extends AutocompleteProvider {
),
range,
};
})
.filter((completion) => !!completion.completion && completion.completion.length > 0)
.slice(0, 4);
}).filter((completion) => !!completion.completion && completion.completion.length > 0).slice(0, 4);
}
return completions;
}

View file

@ -23,7 +23,7 @@ import AutocompleteProvider from './AutocompleteProvider';
import {PillCompletion} from './Components';
import * as sdk from '../index';
import QueryMatcher from './QueryMatcher';
import _sortBy from 'lodash/sortBy';
import {sortBy} from 'lodash';
import {MatrixClientPeg} from '../MatrixClientPeg';
import MatrixEvent from "matrix-js-sdk/src/models/event";
@ -71,8 +71,13 @@ export default class UserProvider extends AutocompleteProvider {
}
}
private onRoomTimeline = (ev: MatrixEvent, room: Room, toStartOfTimeline: boolean, removed: boolean,
data: IRoomTimelineData) => {
private onRoomTimeline = (
ev: MatrixEvent,
room: Room,
toStartOfTimeline: boolean,
removed: boolean,
data: IRoomTimelineData,
) => {
if (!room) return;
if (removed) return;
if (room.roomId !== this.room.roomId) return;
@ -151,7 +156,7 @@ export default class UserProvider extends AutocompleteProvider {
const currentUserId = MatrixClientPeg.get().credentials.userId;
this.users = this.room.getJoinedMembers().filter(({userId}) => userId !== currentUserId);
this.users = _sortBy(this.users, (member) => 1E20 - lastSpoken[member.userId] || 1E20);
this.users = sortBy(this.users, (member) => 1E20 - lastSpoken[member.userId] || 1E20);
this.matcher.setObjects(this.users);
}
@ -171,7 +176,11 @@ export default class UserProvider extends AutocompleteProvider {
renderCompletions(completions: React.ReactNode[]): React.ReactNode {
return (
<div className="mx_Autocomplete_Completion_container_pill" role="listbox" aria-label={_t("User Autocomplete")}>
<div
className="mx_Autocomplete_Completion_container_pill"
role="listbox"
aria-label={_t("User Autocomplete")}
>
{ completions }
</div>
);

View file

@ -1,90 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { _t } from '../../languageHandler';
import SdkConfig from '../../SdkConfig';
export default createReactClass({
displayName: 'CompatibilityPage',
propTypes: {
onAccept: PropTypes.func,
},
getDefaultProps: function() {
return {
onAccept: function() {}, // NOP
};
},
onAccept: function() {
this.props.onAccept();
},
render: function() {
const brand = SdkConfig.get().brand;
return (
<div className="mx_CompatibilityPage">
<div className="mx_CompatibilityPage_box">
<p>{_t(
"Sorry, your browser is <b>not</b> able to run %(brand)s.",
{
brand,
},
{
'b': (sub) => <b>{sub}</b>,
})
}</p>
<p>
{ _t(
"%(brand)s uses many advanced browser features, some of which are not available " +
"or experimental in your current browser.",
{ brand },
) }
</p>
<p>
{ _t(
'Please install <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, ' +
'or <safariLink>Safari</safariLink> for the best experience.',
{},
{
'chromeLink': (sub) => <a href="https://www.google.com/chrome">{sub}</a>,
'firefoxLink': (sub) => <a href="https://firefox.com">{sub}</a>,
'safariLink': (sub) => <a href="https://apple.com/safari">{sub}</a>,
},
)}
</p>
<p>
{ _t(
"With your current browser, the look and feel of the application may be " +
"completely incorrect, and some or all features may not function. " +
"If you want to try it anyway you can continue, but you are on your own in terms " +
"of any issues you may encounter!",
) }
</p>
<button onClick={this.onAccept}>
{ _t("I understand the risks and wish to continue") }
</button>
</div>
</div>
);
},
});

View file

@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {CSSProperties, useRef, useState} from "react";
import React, {CSSProperties, RefObject, useRef, useState} from "react";
import ReactDOM from "react-dom";
import classNames from "classnames";
@ -233,8 +233,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
switch (ev.key) {
case Key.TAB:
case Key.ESCAPE:
// close on left and right arrows too for when it is a context menu on a <Toolbar />
case Key.ARROW_LEFT:
case Key.ARROW_LEFT: // close on left and right arrows too for when it is a context menu on a <Toolbar />
case Key.ARROW_RIGHT:
this.props.onFinished();
break;
@ -417,8 +416,8 @@ export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None
return menuOptions;
};
export const useContextMenu = () => {
const button = useRef(null);
export const useContextMenu = (): [boolean, RefObject<HTMLElement>, () => void, () => void, (val: boolean) => void] => {
const button = useRef<HTMLElement>(null);
const [isOpen, setIsOpen] = useState(false);
const open = () => {
setIsOpen(true);

View file

@ -43,8 +43,8 @@ export default class EmbeddedPage extends React.PureComponent {
static contextType = MatrixClientContext;
constructor(props) {
super(props);
constructor(props, context) {
super(props, context);
this._dispatcherRef = null;

View file

@ -16,7 +16,6 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import {Filter} from 'matrix-js-sdk';
@ -24,27 +23,27 @@ import * as sdk from '../../index';
import {MatrixClientPeg} from '../../MatrixClientPeg';
import EventIndexPeg from "../../indexing/EventIndexPeg";
import { _t } from '../../languageHandler';
import BaseCard from "../views/right_panel/BaseCard";
import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
/*
* Component which shows the filtered file using a TimelinePanel
*/
const FilePanel = createReactClass({
displayName: 'FilePanel',
class FilePanel extends React.Component {
static propTypes = {
roomId: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
};
// This is used to track if a decrypted event was a live event and should be
// added to the timeline.
decryptingEvents: new Set(),
decryptingEvents = new Set();
propTypes: {
roomId: PropTypes.string.isRequired,
},
state = {
timelineSet: null,
};
getInitialState: function() {
return {
timelineSet: null,
};
},
onRoomTimeline(ev, room, toStartOfTimeline, removed, data) {
onRoomTimeline = (ev, room, toStartOfTimeline, removed, data) => {
if (room.roomId !== this.props.roomId) return;
if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return;
@ -53,9 +52,9 @@ const FilePanel = createReactClass({
} else {
this.addEncryptedLiveEvent(ev);
}
},
};
onEventDecrypted(ev, err) {
onEventDecrypted = (ev, err) => {
if (ev.getRoomId() !== this.props.roomId) return;
const eventId = ev.getId();
@ -63,7 +62,7 @@ const FilePanel = createReactClass({
if (err) return;
this.addEncryptedLiveEvent(ev);
},
};
addEncryptedLiveEvent(ev, toStartOfTimeline) {
if (!this.state.timelineSet) return;
@ -77,7 +76,7 @@ const FilePanel = createReactClass({
if (!this.state.timelineSet.eventIdToTimeline(ev.getId())) {
this.state.timelineSet.addEventToTimeline(ev, timeline, false);
}
},
}
async componentDidMount() {
const client = MatrixClientPeg.get();
@ -98,7 +97,7 @@ const FilePanel = createReactClass({
client.on('Room.timeline', this.onRoomTimeline);
client.on('Event.decrypted', this.onEventDecrypted);
}
},
}
componentWillUnmount() {
const client = MatrixClientPeg.get();
@ -110,7 +109,7 @@ const FilePanel = createReactClass({
client.removeListener('Room.timeline', this.onRoomTimeline);
client.removeListener('Event.decrypted', this.onEventDecrypted);
}
},
}
async fetchFileEventsServer(room) {
const client = MatrixClientPeg.get();
@ -134,9 +133,9 @@ const FilePanel = createReactClass({
const timelineSet = room.getOrCreateFilteredTimelineSet(filter);
return timelineSet;
},
}
onPaginationRequest(timelineWindow, direction, limit) {
onPaginationRequest = (timelineWindow, direction, limit) => {
const client = MatrixClientPeg.get();
const eventIndex = EventIndexPeg.get();
const roomId = this.props.roomId;
@ -152,7 +151,7 @@ const FilePanel = createReactClass({
} else {
return timelineWindow.paginate(direction, limit);
}
},
};
async updateTimelineSet(roomId: string) {
const client = MatrixClientPeg.get();
@ -188,22 +187,30 @@ const FilePanel = createReactClass({
} else {
console.error("Failed to add filtered timelineSet for FilePanel as no room!");
}
},
}
render: function() {
render() {
if (MatrixClientPeg.get().isGuest()) {
return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
return <BaseCard
className="mx_FilePanel mx_RoomView_messageListWrapper"
onClose={this.props.onClose}
previousPhase={RightPanelPhases.RoomSummary}
>
<div className="mx_RoomView_empty">
{ _t("You must <a>register</a> to use this functionality",
{},
{ 'a': (sub) => <a href="#/register" key="sub">{ sub }</a> })
}
</div>
</div>;
</BaseCard>;
} else if (this.noRoom) {
return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
return <BaseCard
className="mx_FilePanel mx_RoomView_messageListWrapper"
onClose={this.props.onClose}
previousPhase={RightPanelPhases.RoomSummary}
>
<div className="mx_RoomView_empty">{ _t("You must join the room to see its files") }</div>
</div>;
</BaseCard>;
}
// wrap a TimelinePanel with the jump-to-event bits turned off.
@ -219,8 +226,13 @@ const FilePanel = createReactClass({
// console.log("rendering TimelinePanel for timelineSet " + this.state.timelineSet.room.roomId + " " +
// "(" + this.state.timelineSet._timelines.join(", ") + ")" + " with key " + this.props.roomId);
return (
<div className="mx_FilePanel" role="tabpanel">
<TimelinePanel key={"filepanel_" + this.props.roomId}
<BaseCard
className="mx_FilePanel"
onClose={this.props.onClose}
previousPhase={RightPanelPhases.RoomSummary}
withoutScrollContainer
>
<TimelinePanel
manageReadReceipts={false}
manageReadMarkers={false}
timelineSet={this.state.timelineSet}
@ -230,16 +242,20 @@ const FilePanel = createReactClass({
resizeNotifier={this.props.resizeNotifier}
empty={emptyState}
/>
</div>
</BaseCard>
);
} else {
return (
<div className="mx_FilePanel" role="tabpanel">
<BaseCard
className="mx_FilePanel"
onClose={this.props.onClose}
previousPhase={RightPanelPhases.RoomSummary}
>
<Loader />
</div>
</BaseCard>
);
}
},
});
}
}
export default FilePanel;

View file

@ -17,7 +17,6 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import {MatrixClientPeg} from '../../MatrixClientPeg';
import * as sdk from '../../index';
@ -70,10 +69,8 @@ const UserSummaryType = PropTypes.shape({
}).isRequired,
});
const CategoryRoomList = createReactClass({
displayName: 'CategoryRoomList',
props: {
class CategoryRoomList extends React.Component {
static propTypes = {
rooms: PropTypes.arrayOf(RoomSummaryType).isRequired,
category: PropTypes.shape({
profile: PropTypes.shape({
@ -84,9 +81,9 @@ const CategoryRoomList = createReactClass({
// Whether the list should be editable
editing: PropTypes.bool.isRequired,
},
};
onAddRoomsToSummaryClicked: function(ev) {
onAddRoomsToSummaryClicked = (ev) => {
ev.preventDefault();
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
Modal.createTrackedDialog('Add Rooms to Group Summary', '', AddressPickerDialog, {
@ -122,9 +119,9 @@ const CategoryRoomList = createReactClass({
});
},
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
},
};
render: function() {
render() {
const TintableSvg = sdk.getComponent("elements.TintableSvg");
const addButton = this.props.editing ?
(<AccessibleButton className="mx_GroupView_featuredThings_addButton"
@ -155,19 +152,17 @@ const CategoryRoomList = createReactClass({
{ roomNodes }
{ addButton }
</div>;
},
});
}
}
const FeaturedRoom = createReactClass({
displayName: 'FeaturedRoom',
props: {
class FeaturedRoom extends React.Component {
static propTypes = {
summaryInfo: RoomSummaryType.isRequired,
editing: PropTypes.bool.isRequired,
groupId: PropTypes.string.isRequired,
},
};
onClick: function(e) {
onClick = (e) => {
e.preventDefault();
e.stopPropagation();
@ -176,9 +171,9 @@ const FeaturedRoom = createReactClass({
room_alias: this.props.summaryInfo.profile.canonical_alias,
room_id: this.props.summaryInfo.room_id,
});
},
};
onDeleteClicked: function(e) {
onDeleteClicked = (e) => {
e.preventDefault();
e.stopPropagation();
GroupStore.removeRoomFromGroupSummary(
@ -201,9 +196,9 @@ const FeaturedRoom = createReactClass({
description: _t("The room '%(roomName)s' could not be removed from the summary.", {roomName}),
});
});
},
};
render: function() {
render() {
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
const roomName = this.props.summaryInfo.profile.name ||
@ -243,13 +238,11 @@ const FeaturedRoom = createReactClass({
<div className="mx_GroupView_featuredThing_name">{ roomNameNode }</div>
{ deleteButton }
</AccessibleButton>;
},
});
}
}
const RoleUserList = createReactClass({
displayName: 'RoleUserList',
props: {
class RoleUserList extends React.Component {
static propTypes = {
users: PropTypes.arrayOf(UserSummaryType).isRequired,
role: PropTypes.shape({
profile: PropTypes.shape({
@ -260,9 +253,9 @@ const RoleUserList = createReactClass({
// Whether the list should be editable
editing: PropTypes.bool.isRequired,
},
};
onAddUsersClicked: function(ev) {
onAddUsersClicked = (ev) => {
ev.preventDefault();
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
Modal.createTrackedDialog('Add Users to Group Summary', '', AddressPickerDialog, {
@ -298,9 +291,9 @@ const RoleUserList = createReactClass({
});
},
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
},
};
render: function() {
render() {
const TintableSvg = sdk.getComponent("elements.TintableSvg");
const addButton = this.props.editing ?
(<AccessibleButton className="mx_GroupView_featuredThings_addButton" onClick={this.onAddUsersClicked}>
@ -325,19 +318,17 @@ const RoleUserList = createReactClass({
{ userNodes }
{ addButton }
</div>;
},
});
}
}
const FeaturedUser = createReactClass({
displayName: 'FeaturedUser',
props: {
class FeaturedUser extends React.Component {
static propTypes = {
summaryInfo: UserSummaryType.isRequired,
editing: PropTypes.bool.isRequired,
groupId: PropTypes.string.isRequired,
},
};
onClick: function(e) {
onClick = (e) => {
e.preventDefault();
e.stopPropagation();
@ -345,9 +336,9 @@ const FeaturedUser = createReactClass({
action: 'view_start_chat_or_reuse',
user_id: this.props.summaryInfo.user_id,
});
},
};
onDeleteClicked: function(e) {
onDeleteClicked = (e) => {
e.preventDefault();
e.stopPropagation();
GroupStore.removeUserFromGroupSummary(
@ -368,9 +359,9 @@ const FeaturedUser = createReactClass({
description: _t("The user '%(displayName)s' could not be removed from the summary.", {displayName}),
});
});
},
};
render: function() {
render() {
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
const name = this.props.summaryInfo.displayname || this.props.summaryInfo.user_id;
@ -394,41 +385,37 @@ const FeaturedUser = createReactClass({
<div className="mx_GroupView_featuredThing_name">{ userNameNode }</div>
{ deleteButton }
</AccessibleButton>;
},
});
}
}
const GROUP_JOINPOLICY_OPEN = "open";
const GROUP_JOINPOLICY_INVITE = "invite";
export default createReactClass({
displayName: 'GroupView',
propTypes: {
export default class GroupView extends React.Component {
static propTypes = {
groupId: PropTypes.string.isRequired,
// Whether this is the first time the group admin is viewing the group
groupIsNew: PropTypes.bool,
},
};
getInitialState: function() {
return {
summary: null,
isGroupPublicised: null,
isUserPrivileged: null,
groupRooms: null,
groupRoomsLoading: null,
error: null,
editing: false,
saving: false,
uploadingAvatar: false,
avatarChanged: false,
membershipBusy: false,
publicityBusy: false,
inviterProfile: null,
showRightPanel: RightPanelStore.getSharedInstance().isOpenForGroup,
};
},
state = {
summary: null,
isGroupPublicised: null,
isUserPrivileged: null,
groupRooms: null,
groupRoomsLoading: null,
error: null,
editing: false,
saving: false,
uploadingAvatar: false,
avatarChanged: false,
membershipBusy: false,
publicityBusy: false,
inviterProfile: null,
showRightPanel: RightPanelStore.getSharedInstance().isOpenForGroup,
};
componentDidMount: function() {
componentDidMount() {
this._unmounted = false;
this._matrixClient = MatrixClientPeg.get();
this._matrixClient.on("Group.myMembership", this._onGroupMyMembership);
@ -437,9 +424,9 @@ export default createReactClass({
this._dispatcherRef = dis.register(this._onAction);
this._rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this._onRightPanelStoreUpdate);
},
}
componentWillUnmount: function() {
componentWillUnmount() {
this._unmounted = true;
this._matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
dis.unregister(this._dispatcherRef);
@ -448,10 +435,11 @@ export default createReactClass({
if (this._rightPanelStoreToken) {
this._rightPanelStoreToken.remove();
}
},
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps: function(newProps) {
// eslint-disable-next-line camelcase
UNSAFE_componentWillReceiveProps(newProps) {
if (this.props.groupId !== newProps.groupId) {
this.setState({
summary: null,
@ -460,24 +448,24 @@ export default createReactClass({
this._initGroupStore(newProps.groupId);
});
}
},
}
_onRightPanelStoreUpdate: function() {
_onRightPanelStoreUpdate = () => {
this.setState({
showRightPanel: RightPanelStore.getSharedInstance().isOpenForGroup,
});
},
};
_onGroupMyMembership: function(group) {
_onGroupMyMembership = (group) => {
if (this._unmounted || group.groupId !== this.props.groupId) return;
if (group.myMembership === 'leave') {
// Leave settings - the user might have clicked the "Leave" button
this._closeSettings();
}
this.setState({membershipBusy: false});
},
};
_initGroupStore: function(groupId, firstInit) {
_initGroupStore(groupId, firstInit) {
const group = this._matrixClient.getGroup(groupId);
if (group && group.inviter && group.inviter.userId) {
this._fetchInviterProfile(group.inviter.userId);
@ -506,9 +494,9 @@ export default createReactClass({
});
}
});
},
}
onGroupStoreUpdated(firstInit) {
onGroupStoreUpdated = (firstInit) => {
if (this._unmounted) return;
const summary = GroupStore.getSummary(this.props.groupId);
if (summary.profile) {
@ -533,7 +521,7 @@ export default createReactClass({
if (this.props.groupIsNew && firstInit) {
this._onEditClick();
}
},
};
_fetchInviterProfile(userId) {
this.setState({
@ -555,9 +543,9 @@ export default createReactClass({
inviterProfileBusy: false,
});
});
},
}
_onEditClick: function() {
_onEditClick = () => {
this.setState({
editing: true,
profileForm: Object.assign({}, this.state.summary.profile),
@ -568,20 +556,20 @@ export default createReactClass({
GROUP_JOINPOLICY_INVITE,
},
});
},
};
_onShareClick: function() {
_onShareClick = () => {
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
Modal.createTrackedDialog('share community dialog', '', ShareDialog, {
target: this._matrixClient.getGroup(this.props.groupId) || new Group(this.props.groupId),
});
},
};
_onCancelClick: function() {
_onCancelClick = () => {
this._closeSettings();
},
};
_onAction(payload) {
_onAction = (payload) => {
switch (payload.action) {
// NOTE: close_settings is an app-wide dispatch; as it is dispatched from MatrixChat
case 'close_settings':
@ -593,34 +581,34 @@ export default createReactClass({
default:
break;
}
},
};
_closeSettings() {
_closeSettings = () => {
dis.dispatch({action: 'close_settings'});
},
};
_onNameChange: function(value) {
_onNameChange = (value) => {
const newProfileForm = Object.assign(this.state.profileForm, { name: value });
this.setState({
profileForm: newProfileForm,
});
},
};
_onShortDescChange: function(value) {
_onShortDescChange = (value) => {
const newProfileForm = Object.assign(this.state.profileForm, { short_description: value });
this.setState({
profileForm: newProfileForm,
});
},
};
_onLongDescChange: function(e) {
_onLongDescChange = (e) => {
const newProfileForm = Object.assign(this.state.profileForm, { long_description: e.target.value });
this.setState({
profileForm: newProfileForm,
});
},
};
_onAvatarSelected: function(ev) {
_onAvatarSelected = ev => {
const file = ev.target.files[0];
if (!file) return;
@ -644,15 +632,15 @@ export default createReactClass({
description: _t('Failed to upload image'),
});
});
},
};
_onJoinableChange: function(ev) {
_onJoinableChange = ev => {
this.setState({
joinableForm: { policyType: ev.target.value },
});
},
};
_onSaveClick: function() {
_onSaveClick = () => {
this.setState({saving: true});
const savePromise = this.state.isUserPrivileged ? this._saveGroup() : Promise.resolve();
savePromise.then((result) => {
@ -683,16 +671,16 @@ export default createReactClass({
avatarChanged: false,
});
});
},
};
_saveGroup: async function() {
async _saveGroup() {
await this._matrixClient.setGroupProfile(this.props.groupId, this.state.profileForm);
await this._matrixClient.setGroupJoinPolicy(this.props.groupId, {
type: this.state.joinableForm.policyType,
});
},
}
_onAcceptInviteClick: async function() {
_onAcceptInviteClick = async () => {
this.setState({membershipBusy: true});
// Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
@ -709,9 +697,9 @@ export default createReactClass({
description: _t("Unable to accept invite"),
});
});
},
};
_onRejectInviteClick: async function() {
_onRejectInviteClick = async () => {
this.setState({membershipBusy: true});
// Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
@ -728,9 +716,9 @@ export default createReactClass({
description: _t("Unable to reject invite"),
});
});
},
};
_onJoinClick: async function() {
_onJoinClick = async () => {
if (this._matrixClient.isGuest()) {
dis.dispatch({action: 'require_registration', screen_after: {screen: `group/${this.props.groupId}`}});
return;
@ -752,9 +740,9 @@ export default createReactClass({
description: _t("Unable to join community"),
});
});
},
};
_leaveGroupWarnings: function() {
_leaveGroupWarnings() {
const warnings = [];
if (this.state.isUserPrivileged) {
@ -768,10 +756,9 @@ export default createReactClass({
}
return warnings;
},
}
_onLeaveClick: function() {
_onLeaveClick = () => {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const warnings = this._leaveGroupWarnings();
@ -806,13 +793,13 @@ export default createReactClass({
});
},
});
},
};
_onAddRoomsClick: function() {
_onAddRoomsClick = () => {
showGroupAddRoomDialog(this.props.groupId);
},
};
_getGroupSection: function() {
_getGroupSection() {
const groupSettingsSectionClasses = classnames({
"mx_GroupView_group": this.state.editing,
"mx_GroupView_group_disabled": this.state.editing && !this.state.isUserPrivileged,
@ -856,9 +843,9 @@ export default createReactClass({
{ this._getLongDescriptionNode() }
{ this._getRoomsNode() }
</div>;
},
}
_getRoomsNode: function() {
_getRoomsNode() {
const RoomDetailList = sdk.getComponent('rooms.RoomDetailList');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const TintableSvg = sdk.getComponent('elements.TintableSvg');
@ -902,9 +889,9 @@ export default createReactClass({
className={roomDetailListClassName} />
}
</div>;
},
}
_getFeaturedRoomsNode: function() {
_getFeaturedRoomsNode() {
const summary = this.state.summary;
const defaultCategoryRooms = [];
@ -943,9 +930,9 @@ export default createReactClass({
{ defaultCategoryNode }
{ categoryRoomNodes }
</div>;
},
}
_getFeaturedUsersNode: function() {
_getFeaturedUsersNode() {
const summary = this.state.summary;
const noRoleUsers = [];
@ -984,9 +971,9 @@ export default createReactClass({
{ noRoleNode }
{ roleUserNodes }
</div>;
},
}
_getMembershipSection: function() {
_getMembershipSection() {
const Spinner = sdk.getComponent("elements.Spinner");
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
@ -1100,9 +1087,9 @@ export default createReactClass({
</div>
</div>
</div>;
},
}
_getJoinableNode: function() {
_getJoinableNode() {
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
return this.state.editing ? <div>
<h3>
@ -1136,9 +1123,9 @@ export default createReactClass({
</label>
</div>
</div> : null;
},
}
_getLongDescriptionNode: function() {
_getLongDescriptionNode() {
const summary = this.state.summary;
let description = null;
if (summary.profile && summary.profile.long_description) {
@ -1175,9 +1162,9 @@ export default createReactClass({
<div className="mx_GroupView_groupDesc">
{ description }
</div>;
},
}
render: function() {
render() {
const GroupAvatar = sdk.getComponent("avatars.GroupAvatar");
const Spinner = sdk.getComponent("elements.Spinner");
@ -1335,7 +1322,7 @@ export default createReactClass({
</div>
<GroupHeaderButtons />
</div>
<MainSplit panel={rightPanel}>
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
<AutoHideScrollbar className="mx_GroupView_body">
{ this._getMembershipSection() }
{ this._getGroupSection() }
@ -1366,5 +1353,5 @@ export default createReactClass({
console.error("Invalid state for GroupView");
return <div />;
}
},
});
}
}

View file

@ -17,7 +17,6 @@ limitations under the License.
import {InteractiveAuth} from "matrix-js-sdk";
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import getEntryComponentForLoginType from '../views/auth/InteractiveAuthEntryComponents';
@ -26,10 +25,8 @@ import * as sdk from '../../index';
export const ERROR_USER_CANCELLED = new Error("User cancelled auth session");
export default createReactClass({
displayName: 'InteractiveAuth',
propTypes: {
export default class InteractiveAuthComponent extends React.Component {
static propTypes = {
// matrix client to use for UI auth requests
matrixClient: PropTypes.object.isRequired,
@ -86,20 +83,19 @@ export default createReactClass({
// continueText and continueKind are passed straight through to the AuthEntryComponent.
continueText: PropTypes.string,
continueKind: PropTypes.string,
},
};
getInitialState: function() {
return {
constructor(props) {
super(props);
this.state = {
authStage: null,
busy: false,
errorText: null,
stageErrorText: null,
submitButtonEnabled: false,
};
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._unmounted = false;
this._authLogic = new InteractiveAuth({
authData: this.props.authData,
@ -114,6 +110,18 @@ export default createReactClass({
requestEmailToken: this._requestEmailToken,
});
this._intervalId = null;
if (this.props.poll) {
this._intervalId = setInterval(() => {
this._authLogic.poll();
}, 2000);
}
this._stageComponent = createRef();
}
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount() { // eslint-disable-line camelcase
this._authLogic.attemptAuth().then((result) => {
const extra = {
emailSid: this._authLogic.getEmailSid(),
@ -132,26 +140,17 @@ export default createReactClass({
errorText: msg,
});
});
}
this._intervalId = null;
if (this.props.poll) {
this._intervalId = setInterval(() => {
this._authLogic.poll();
}, 2000);
}
this._stageComponent = createRef();
},
componentWillUnmount: function() {
componentWillUnmount() {
this._unmounted = true;
if (this._intervalId !== null) {
clearInterval(this._intervalId);
}
},
}
_requestEmailToken: async function(...args) {
_requestEmailToken = async (...args) => {
this.setState({
busy: true,
});
@ -162,15 +161,15 @@ export default createReactClass({
busy: false,
});
}
},
};
tryContinue: function() {
tryContinue = () => {
if (this._stageComponent.current && this._stageComponent.current.tryContinue) {
this._stageComponent.current.tryContinue();
}
},
};
_authStateUpdated: function(stageType, stageState) {
_authStateUpdated = (stageType, stageState) => {
const oldStage = this.state.authStage;
this.setState({
busy: false,
@ -180,16 +179,16 @@ export default createReactClass({
}, () => {
if (oldStage != stageType) this._setFocus();
});
},
};
_requestCallback: function(auth) {
_requestCallback = (auth) => {
// This wrapper just exists because the js-sdk passes a second
// 'busy' param for backwards compat. This throws the tests off
// so discard it here.
return this.props.makeRequest(auth);
},
};
_onBusyChanged: function(busy) {
_onBusyChanged = (busy) => {
// if we've started doing stuff, reset the error messages
if (busy) {
this.setState({
@ -204,29 +203,29 @@ export default createReactClass({
// there's a new screen to show the user. This is implemented by setting
// `busy: false` in `_authStateUpdated`.
// See also https://github.com/vector-im/element-web/issues/12546
},
};
_setFocus: function() {
_setFocus() {
if (this._stageComponent.current && this._stageComponent.current.focus) {
this._stageComponent.current.focus();
}
},
}
_submitAuthDict: function(authData) {
_submitAuthDict = authData => {
this._authLogic.submitAuthDict(authData);
},
};
_onPhaseChange: function(newPhase) {
_onPhaseChange = newPhase => {
if (this.props.onStagePhaseChange) {
this.props.onStagePhaseChange(this.state.authStage, newPhase || 0);
}
},
};
_onStageCancel: function() {
_onStageCancel = () => {
this.props.onAuthFinished(false, ERROR_USER_CANCELLED);
},
};
_renderCurrentStage: function() {
_renderCurrentStage() {
const stage = this.state.authStage;
if (!stage) {
if (this.state.busy) {
@ -260,16 +259,17 @@ export default createReactClass({
onCancel={this._onStageCancel}
/>
);
},
}
_onAuthStageFailed: function(e) {
_onAuthStageFailed = e => {
this.props.onAuthFinished(false, e);
},
_setEmailSid: function(sid) {
this._authLogic.setEmailSid(sid);
},
};
render: function() {
_setEmailSid = sid => {
this._authLogic.setEmailSid(sid);
};
render() {
let error = null;
if (this.state.errorText) {
error = (
@ -287,5 +287,5 @@ export default createReactClass({
</div>
</div>
);
},
});
}
}

View file

@ -52,7 +52,7 @@ interface IState {
// List of CSS classes which should be included in keyboard navigation within the room list
const cssClasses = [
"mx_RoomSearch_input",
"mx_RoomSearch_icon", // minimized <RoomSearch />
"mx_RoomSearch_minimizedHandle", // minimized <RoomSearch />
"mx_RoomSublist_headerText",
"mx_RoomTile",
"mx_RoomSublist_showNButton",
@ -377,7 +377,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
public render(): React.ReactNode {
const tagPanel = !this.state.showTagPanel ? null : (
<div className="mx_LeftPanel_tagPanelContainer">
<TagPanel/>
<TagPanel />
{SettingsStore.getValue("feature_custom_tags") ? <CustomRoomTagPanel /> : null}
</div>
);

View file

@ -43,11 +43,11 @@ import PlatformPeg from "../../PlatformPeg";
import { DefaultTagID } from "../../stores/room-list/models";
import {
showToast as showSetPasswordToast,
hideToast as hideSetPasswordToast
hideToast as hideSetPasswordToast,
} from "../../toasts/SetPasswordToast";
import {
showToast as showServerLimitToast,
hideToast as hideServerLimitToast
hideToast as hideServerLimitToast,
} from "../../toasts/ServerLimitToast";
import { Action } from "../../dispatcher/actions";
import LeftPanel from "./LeftPanel";
@ -56,6 +56,7 @@ import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPay
import RoomListStore from "../../stores/room-list/RoomListStore";
import NonUrgentToastContainer from "./NonUrgentToastContainer";
import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload";
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
// We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity.
@ -76,12 +77,12 @@ interface IProps {
hideToSRUsers: boolean;
resizeNotifier: ResizeNotifier;
middleDisabled: boolean;
initialEventPixelOffset: number;
leftDisabled: boolean;
rightDisabled: boolean;
// eslint-disable-next-line camelcase
page_type: string;
autoJoin: boolean;
thirdPartyInvite?: object;
threepidInvite?: IThreepidInvite;
roomOobData?: object;
currentRoomId: string;
ConferenceHandler?: object;
@ -98,7 +99,9 @@ interface IProps {
}
interface IUsageLimit {
// eslint-disable-next-line camelcase
limit_type: "monthly_active_user" | string;
// eslint-disable-next-line camelcase
admin_contact?: string;
}
@ -254,6 +257,12 @@ class LoggedInView extends React.Component<IProps, IState> {
window.localStorage.setItem("mx_lhs_size", '' + size);
this.props.resizeNotifier.notifyLeftHandleResized();
},
onResizeStart: () => {
this.props.resizeNotifier.startResizing();
},
onResizeStop: () => {
this.props.resizeNotifier.stopResizing();
},
};
const resizer = new Resizer(
this._resizeContainer.current,
@ -316,10 +325,10 @@ class LoggedInView extends React.Component<IProps, IState> {
}
};
_calculateServerLimitToast(syncErrorData: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) {
const error = syncErrorData && syncErrorData.error && syncErrorData.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED";
_calculateServerLimitToast(syncError: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) {
const error = syncError && syncError.error && syncError.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED";
if (error) {
usageLimitEventContent = syncErrorData.error.data;
usageLimitEventContent = syncError.error.data;
}
if (usageLimitEventContent) {
@ -620,18 +629,17 @@ class LoggedInView extends React.Component<IProps, IState> {
switch (this.props.page_type) {
case PageTypes.RoomView:
pageElement = <RoomView
ref={this._roomView}
autoJoin={this.props.autoJoin}
onRegistered={this.props.onRegistered}
thirdPartyInvite={this.props.thirdPartyInvite}
oobData={this.props.roomOobData}
viaServers={this.props.viaServers}
eventPixelOffset={this.props.initialEventPixelOffset}
key={this.props.currentRoomId || 'roomview'}
disabled={this.props.middleDisabled}
ConferenceHandler={this.props.ConferenceHandler}
resizeNotifier={this.props.resizeNotifier}
/>;
ref={this._roomView}
autoJoin={this.props.autoJoin}
onRegistered={this.props.onRegistered}
threepidInvite={this.props.threepidInvite}
oobData={this.props.roomOobData}
viaServers={this.props.viaServers}
key={this.props.currentRoomId || 'roomview'}
disabled={this.props.middleDisabled}
ConferenceHandler={this.props.ConferenceHandler}
resizeNotifier={this.props.resizeNotifier}
/>;
break;
case PageTypes.MyGroups:
@ -647,12 +655,13 @@ class LoggedInView extends React.Component<IProps, IState> {
break;
case PageTypes.UserView:
pageElement = <UserView userId={this.props.currentUserId} />;
pageElement = <UserView userId={this.props.currentUserId} resizeNotifier={this.props.resizeNotifier} />;
break;
case PageTypes.GroupView:
pageElement = <GroupView
groupId={this.props.currentGroupId}
isNew={this.props.currentGroupIsNew}
resizeNotifier={this.props.resizeNotifier}
/>;
break;
}

View file

@ -19,9 +19,18 @@ import React from 'react';
import { Resizable } from 're-resizable';
export default class MainSplit extends React.Component {
_onResized = (event, direction, refToElement, delta) => {
_onResizeStart = () => {
this.props.resizeNotifier.startResizing();
};
_onResize = () => {
this.props.resizeNotifier.notifyRightHandleResized();
};
_onResizeStop = (event, direction, refToElement, delta) => {
this.props.resizeNotifier.stopResizing();
window.localStorage.setItem("mx_rhs_size", this._loadSidePanelSize().width + delta.width);
}
};
_loadSidePanelSize() {
let rhsSize = parseInt(window.localStorage.getItem("mx_rhs_size"), 10);
@ -58,7 +67,9 @@ export default class MainSplit extends React.Component {
bottomLeft: false,
topLeft: false,
}}
onResizeStop={this._onResized}
onResizeStart={this._onResizeStart}
onResize={this._onResize}
onResizeStop={this._onResizeStop}
className="mx_RightPanel_ResizeWrapper"
handleClasses={{left: "mx_RightPanel_ResizeHandle"}}
>

View file

@ -69,13 +69,17 @@ import { ViewUserPayload } from "../../dispatcher/payloads/ViewUserPayload";
import { Action } from "../../dispatcher/actions";
import {
showToast as showAnalyticsToast,
hideToast as hideAnalyticsToast
hideToast as hideAnalyticsToast,
} from "../../toasts/AnalyticsToast";
import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast";
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
import ErrorDialog from "../views/dialogs/ErrorDialog";
import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
import { SettingLevel } from "../../settings/SettingLevel";
import { leaveRoomBehaviour } from "../../utils/membership";
import CreateCommunityPrototypeDialog from "../views/dialogs/CreateCommunityPrototypeDialog";
import ThreepidInviteStore, { IThreepidInvite, IThreepidInviteWireFormat } from "../../stores/ThreepidInviteStore";
import {UIFeature} from "../../settings/UIFeature";
/** constants for MatrixChat.state.view */
export enum Views {
@ -127,6 +131,7 @@ interface IScreen {
params?: object;
}
/* eslint-disable camelcase */
interface IRoomInfo {
room_id?: string;
room_alias?: string;
@ -134,16 +139,17 @@ interface IRoomInfo {
auto_join?: boolean;
highlighted?: boolean;
third_party_invite?: object;
oob_data?: object;
via_servers?: string[];
threepid_invite?: IThreepidInvite;
}
/* eslint-enable camelcase */
interface IProps { // TODO type things better
config: Record<string, any>;
serverConfig?: ValidatedServerConfig;
ConferenceHandler?: any;
onNewScreen: (string) => void;
onNewScreen: (screen: string, replaceLast: boolean) => void;
enableGuest?: boolean;
// the queryParams extracted from the [real] query-string of the URI
realQueryParams?: Record<string, string>;
@ -163,6 +169,7 @@ interface IState {
// the master view we are showing.
view: Views;
// What the LoggedInView would be showing if visible
// eslint-disable-next-line camelcase
page_type?: PageTypes;
// The ID of the room we're viewing. This is either populated directly
// in the case where we view a room by ID or by RoomView when it resolves
@ -178,8 +185,11 @@ interface IState {
middleDisabled: boolean;
// the right panel's disabled state is tracked in its store.
// Parameters used in the registration dance with the IS
// eslint-disable-next-line camelcase
register_client_secret?: string;
// eslint-disable-next-line camelcase
register_session_id?: string;
// eslint-disable-next-line camelcase
register_id_sid?: string;
// When showing Modal dialogs we need to set aria-hidden on the root app element
// and disable it when there are no dialogs
@ -188,7 +198,7 @@ interface IState {
resizeNotifier: ResizeNotifier;
serverConfig?: ValidatedServerConfig;
ready: boolean;
thirdPartyInvite?: object;
threepidInvite?: IThreepidInvite,
roomOobData?: object;
viaServers?: string[];
pendingInitialSync?: boolean;
@ -252,6 +262,14 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// outside this.state because updating it should never trigger a
// rerender.
this.screenAfterLogin = this.props.initialScreenAfterLogin;
if (this.screenAfterLogin) {
const params = this.screenAfterLogin.params || {};
if (this.screenAfterLogin.screen.startsWith("room/") && params['signurl'] && params['email']) {
// probably a threepid invite - try to store it
const roomId = this.screenAfterLogin.screen.substring("room/".length);
ThreepidInviteStore.instance.storeInvite(roomId, params as IThreepidInviteWireFormat);
}
}
this.windowWidth = 10000;
this.handleResize();
@ -339,6 +357,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle stage
// eslint-disable-next-line camelcase
UNSAFE_componentWillUpdate(props, state) {
if (this.shouldTrackPageChange(this.state, state)) {
this.startPageChangeTimer();
@ -395,8 +414,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
});
}).then((loadedSession) => {
if (!loadedSession) {
// fall back to showing the welcome screen
dis.dispatch({action: "view_welcome_page"});
// fall back to showing the welcome screen... unless we have a 3pid invite pending
if (ThreepidInviteStore.instance.pickBestInvite()) {
dis.dispatch({action: 'start_registration'});
} else {
dis.dispatch({action: "view_welcome_page"});
}
}
});
// Note we don't catch errors from this: we catch everything within
@ -608,8 +631,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog");
Modal.createTrackedDialog('User settings', '', UserSettingsDialog,
{initialTabId: tabPayload.initialTabId},
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true
);
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
// View the welcome or home page if we need something to look at
this.viewSomethingBehindModal();
@ -619,7 +641,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.createRoom(payload.public);
break;
case 'view_create_group': {
const CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog");
let CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog")
if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
CreateGroupDialog = CreateCommunityPrototypeDialog;
}
Modal.createTrackedDialog('Create Community', '', CreateGroupDialog);
break;
}
@ -824,10 +849,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// context of that particular event.
// @param {boolean=} roomInfo.highlighted If true, add event_id to the hash of the URL
// and alter the EventTile to appear highlighted.
// @param {Object=} roomInfo.third_party_invite Object containing data about the third party
// we received to join the room, if any.
// @param {string=} roomInfo.third_party_invite.inviteSignUrl 3pid invite sign URL
// @param {string=} roomInfo.third_party_invite.invitedEmail The email address the invite was sent to
// @param {Object=} roomInfo.threepid_invite Object containing data about the third party
// we received to join the room, if any.
// @param {Object=} roomInfo.oob_data Object of additional data about the room
// that has been passed out-of-band (eg.
// room name and avatar from an invite email)
@ -875,6 +898,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
}
// If we are redirecting to a Room Alias and it is for the room we already showing then replace history item
const replaceLast = presentedId[0] === "#" && roomInfo.room_id === this.state.currentRoomId;
if (roomInfo.event_id && roomInfo.highlighted) {
presentedId += "/" + roomInfo.event_id;
}
@ -882,12 +908,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
view: Views.LOGGED_IN,
currentRoomId: roomInfo.room_id || null,
page_type: PageTypes.RoomView,
thirdPartyInvite: roomInfo.third_party_invite,
threepidInvite: roomInfo.threepid_invite,
roomOobData: roomInfo.oob_data,
viaServers: roomInfo.via_servers,
ready: true,
}, () => {
this.notifyNewScreen('room/' + presentedId);
this.notifyNewScreen('room/' + presentedId, replaceLast);
});
});
}
@ -1075,57 +1101,20 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
title: _t("Leave room"),
description: (
<span>
{ _t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name}) }
{ _t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name}) }
{ warnings }
</span>
),
button: _t("Leave"),
onFinished: (shouldLeave) => {
if (shouldLeave) {
const d = MatrixClientPeg.get().leaveRoomChain(roomId);
const d = leaveRoomBehaviour(roomId);
// FIXME: controller shouldn't be loading a view :(
const Loader = sdk.getComponent("elements.Spinner");
const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
d.then((errors) => {
modal.close();
for (const leftRoomId of Object.keys(errors)) {
const err = errors[leftRoomId];
if (!err) continue;
console.error("Failed to leave room " + leftRoomId + " " + err);
let title = _t("Failed to leave room");
let message = _t("Server may be unavailable, overloaded, or you hit a bug.");
if (err.errcode === 'M_CANNOT_LEAVE_SERVER_NOTICE_ROOM') {
title = _t("Can't leave Server Notices room");
message = _t(
"This room is used for important messages from the Homeserver, " +
"so you cannot leave it.",
);
} else if (err && err.message) {
message = err.message;
}
Modal.createTrackedDialog('Failed to leave room', '', ErrorDialog, {
title: title,
description: message,
});
return;
}
if (this.state.currentRoomId === roomId) {
dis.dispatch({action: 'view_next_room'});
}
}, (err) => {
// This should only happen if something went seriously wrong with leaving the chain.
modal.close();
console.error("Failed to leave room " + roomId + " " + err);
Modal.createTrackedDialog('Failed to leave room', '', ErrorDialog, {
title: _t("Failed to leave room"),
description: _t("Unknown error"),
});
});
d.finally(() => modal.close());
}
},
});
@ -1226,6 +1215,14 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// the homepage.
dis.dispatch({action: 'view_home_page'});
}
} else if (ThreepidInviteStore.instance.pickBestInvite()) {
// The user has a 3pid invite pending - show them that
const threepidInvite = ThreepidInviteStore.instance.pickBestInvite();
// HACK: This is a pretty brutal way of threading the invite back through
// our systems, but it's the safest we have for now.
const params = ThreepidInviteStore.instance.translateToWireFormat(threepidInvite);
this.showScreen(`room/${threepidInvite.roomId}`, params)
} else {
// The user has just logged in after registering,
// so show the homepage.
@ -1237,8 +1234,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
StorageManager.tryPersistStorage();
if (SettingsStore.getValue("showCookieBar") && this.props.config.piwik && navigator.doNotTrack !== "1") {
showAnalyticsToast(this.props.config.piwik && this.props.config.piwik.policyUrl);
if (SettingsStore.getValue("showCookieBar") && Analytics.canEnable()) {
showAnalyticsToast(this.props.config.piwik?.policyUrl);
}
}
@ -1367,7 +1364,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.firstSyncComplete = true;
this.firstSyncPromise.resolve();
if (Notifier.shouldShowToolbar()) {
if (Notifier.shouldShowPrompt()) {
showNotificationsToast();
}
@ -1376,15 +1373,19 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
ready: true,
});
});
cli.on('Call.incoming', function(call) {
// we dispatch this synchronously to make sure that the event
// handlers on the call are set up immediately (so that if
// we get an immediate hangup, we don't get a stuck call)
dis.dispatch({
action: 'incoming_call',
call: call,
}, true);
});
if (SettingsStore.getValue(UIFeature.Voip)) {
cli.on('Call.incoming', function(call) {
// we dispatch this synchronously to make sure that the event
// handlers on the call are set up immediately (so that if
// we get an immediate hangup, we don't get a stuck call)
dis.dispatch({
action: 'incoming_call',
call: call,
}, true);
});
}
cli.on('Session.logged_out', function(errObj) {
if (Lifecycle.isLoggingOut()) return;
@ -1465,7 +1466,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
cli.on("crypto.warning", (type) => {
switch (type) {
case 'CRYPTO_WARNING_OLD_VERSION_DETECTED':
const brand = SdkConfig.get().brand;
Modal.createTrackedDialog('Crypto migrated', '', ErrorDialog, {
title: _t('Old cryptography data detected'),
description: _t(
@ -1476,7 +1476,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
"in this version. This may also cause messages exchanged with this " +
"version to fail. If you experience problems, log out and back in " +
"again. To retain message history, export and re-import your keys.",
{ brand },
{ brand: SdkConfig.get().brand },
),
});
break;
@ -1501,12 +1501,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
if (haveNewVersion) {
Modal.createTrackedDialogAsync('New Recovery Method', 'New Recovery Method',
import('../../async-components/views/dialogs/keybackup/NewRecoveryMethodDialog'),
import('../../async-components/views/dialogs/security/NewRecoveryMethodDialog'),
{ newVersionInfo },
);
} else {
Modal.createTrackedDialogAsync('Recovery Method Removed', 'Recovery Method Removed',
import('../../async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog'),
import('../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog'),
);
}
});
@ -1663,16 +1663,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// TODO: Handle encoded room/event IDs: https://github.com/vector-im/element-web/issues/9149
// FIXME: sort_out caseConsistency
const thirdPartyInvite = {
inviteSignUrl: params.signurl,
invitedEmail: params.email,
};
const oobData = {
name: params.room_name,
avatarUrl: params.room_avatar_url,
inviterName: params.inviter_name,
};
let threepidInvite: IThreepidInvite;
if (params.signurl && params.email) {
threepidInvite = ThreepidInviteStore.instance
.storeInvite(roomString, params as IThreepidInviteWireFormat);
}
// on our URLs there might be a ?via=matrix.org or similar to help
// joins to the room succeed. We'll pass these through as an array
@ -1693,8 +1688,15 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// it as highlighted, which will propagate to RoomView and highlight the
// associated EventTile.
highlighted: Boolean(eventId),
third_party_invite: thirdPartyInvite,
oob_data: oobData,
threepid_invite: threepidInvite,
// TODO: Replace oob_data with the threepidInvite (which has the same info).
// This isn't done yet because it's threaded through so many more places.
// See https://github.com/vector-im/element-web/issues/15157
oob_data: {
name: threepidInvite?.roomName,
avatarUrl: threepidInvite?.roomAvatarUrl,
inviterName: threepidInvite?.inviterName,
},
room_alias: undefined,
room_id: undefined,
};
@ -1726,9 +1728,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
}
notifyNewScreen(screen: string) {
notifyNewScreen(screen: string, replaceLast = false) {
if (this.props.onNewScreen) {
this.props.onNewScreen(screen);
this.props.onNewScreen(screen, replaceLast);
}
this.setPageSubtitle();
}
@ -1879,6 +1881,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
return this.props.makeRegistrationUrl(params);
};
/**
* After registration or login, we run various post-auth steps before entering the app
* proper, such setting up cross-signing or verifying the new session.
*
* Note: SSO users (and any others using token login) currently do not pass through
* this, as they instead jump straight into the app after `attemptTokenLogin`.
*/
onUserCompletedLoginFlow = async (credentials: object, password: string) => {
this.accountPassword = password;
// self-destruct the password after 5mins
@ -1945,7 +1954,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
render() {
const fragmentAfterLogin = this.getFragmentAfterLogin();
let view;
let view = null;
if (this.state.view === Views.LOADING) {
const Spinner = sdk.getComponent('elements.Spinner');
@ -2024,14 +2033,15 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} else if (this.state.view === Views.WELCOME) {
const Welcome = sdk.getComponent('auth.Welcome');
view = <Welcome />;
} else if (this.state.view === Views.REGISTER) {
} else if (this.state.view === Views.REGISTER && SettingsStore.getValue(UIFeature.Registration)) {
const Registration = sdk.getComponent('structures.auth.Registration');
const email = ThreepidInviteStore.instance.pickBestInvite()?.toEmail;
view = (
<Registration
clientSecret={this.state.register_client_secret}
sessionId={this.state.register_session_id}
idSid={this.state.register_id_sid}
email={this.props.startingFragmentQueryParams.email}
email={email}
brand={this.props.config.brand}
makeRegistrationUrl={this.makeRegistrationUrl}
onLoggedIn={this.onRegisterFlowComplete}
@ -2041,7 +2051,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
{...this.getServerProperties()}
/>
);
} else if (this.state.view === Views.FORGOT_PASSWORD) {
} else if (this.state.view === Views.FORGOT_PASSWORD && SettingsStore.getValue(UIFeature.PasswordReset)) {
const ForgotPassword = sdk.getComponent('structures.auth.ForgotPassword');
view = (
<ForgotPassword
@ -2052,6 +2062,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
/>
);
} else if (this.state.view === Views.LOGIN) {
const showPasswordReset = SettingsStore.getValue(UIFeature.PasswordReset);
const Login = sdk.getComponent('structures.auth.Login');
view = (
<Login
@ -2060,7 +2071,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
onRegisterClick={this.onRegisterClick}
fallbackHsUrl={this.getFallbackHsUrl()}
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
onForgotPasswordClick={this.onForgotPasswordClick}
onForgotPasswordClick={showPasswordReset ? this.onForgotPasswordClick : undefined}
onServerConfigChange={this.onServerConfigChange}
fragmentAfterLogin={fragmentAfterLogin}
{...this.getServerProperties()}
@ -2085,3 +2096,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
</ErrorBoundary>;
}
}
export function isLoggedIn(): boolean {
// JRS: Maybe we should move the step that writes this to the window out of
// `element-web` and into this file? Better yet, we should probably create a
// store to hold this state.
// See also https://github.com/vector-im/element-web/issues/15034.
const app = window.matrixChat;
return app && (app as MatrixChat).state.view === Views.LOGGED_IN;
}

View file

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

View file

@ -17,7 +17,6 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import * as sdk from '../../index';
import { _t } from '../../languageHandler';
import SdkConfig from '../../SdkConfig';
@ -26,29 +25,23 @@ import AccessibleButton from '../views/elements/AccessibleButton';
import MatrixClientContext from "../../contexts/MatrixClientContext";
import AutoHideScrollbar from "./AutoHideScrollbar";
export default createReactClass({
displayName: 'MyGroups',
export default class MyGroups extends React.Component {
static contextType = MatrixClientContext;
getInitialState: function() {
return {
groups: null,
error: null,
};
},
state = {
groups: null,
error: null,
};
statics: {
contextType: MatrixClientContext,
},
componentDidMount: function() {
componentDidMount() {
this._fetch();
},
}
_onCreateGroupClick: function() {
_onCreateGroupClick = () => {
dis.dispatch({action: 'view_create_group'});
},
};
_fetch: function() {
_fetch() {
this.context.getJoinedGroups().then((result) => {
this.setState({groups: result.groups, error: null});
}, (err) => {
@ -59,9 +52,9 @@ export default createReactClass({
}
this.setState({groups: null, error: err});
});
},
}
render: function() {
render() {
const brand = SdkConfig.get().brand;
const Loader = sdk.getComponent("elements.Spinner");
const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
@ -149,5 +142,5 @@ export default createReactClass({
{ content }
</div>
</div>;
},
});
}
}

View file

@ -17,21 +17,22 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from "prop-types";
import { _t } from '../../languageHandler';
import {MatrixClientPeg} from "../../MatrixClientPeg";
import * as sdk from "../../index";
import BaseCard from "../views/right_panel/BaseCard";
/*
* Component which shows the global notification list using a TimelinePanel
*/
const NotificationPanel = createReactClass({
displayName: 'NotificationPanel',
class NotificationPanel extends React.Component {
static propTypes = {
onClose: PropTypes.func.isRequired,
};
propTypes: {
},
render: function() {
render() {
// wrap a TimelinePanel with the jump-to-event bits turned off.
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
const Loader = sdk.getComponent("elements.Spinner");
@ -41,29 +42,28 @@ const NotificationPanel = createReactClass({
<p>{_t('You have no visible notifications in this room.')}</p>
</div>);
let content;
const timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
if (timelineSet) {
return (
<div className="mx_NotificationPanel" role="tabpanel">
<TimelinePanel key={"NotificationPanel_" + this.props.roomId}
manageReadReceipts={false}
manageReadMarkers={false}
timelineSet={timelineSet}
showUrlPreview={false}
tileShape="notif"
empty={emptyState}
/>
</div>
content = (
<TimelinePanel
manageReadReceipts={false}
manageReadMarkers={false}
timelineSet={timelineSet}
showUrlPreview={false}
tileShape="notif"
empty={emptyState}
/>
);
} else {
console.error("No notifTimelineSet available!");
return (
<div className="mx_NotificationPanel" role="tabpanel">
<Loader />
</div>
);
content = <Loader />;
}
},
});
return <BaseCard className="mx_NotificationPanel" onClose={this.props.onClose} withoutScrollContainer>
{ content }
</BaseCard>;
}
}
export default NotificationPanel;

View file

@ -21,6 +21,8 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import {Room} from "matrix-js-sdk/src/models/room";
import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher';
import RateLimitedFunc from '../../ratelimitedfunc';
@ -30,11 +32,14 @@ import {RightPanelPhases, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPa
import RightPanelStore from "../../stores/RightPanelStore";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import {Action} from "../../dispatcher/actions";
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
import WidgetCard from "../views/right_panel/WidgetCard";
import defaultDispatcher from "../../dispatcher/dispatcher";
export default class RightPanel extends React.Component {
static get propTypes() {
return {
roomId: PropTypes.string, // if showing panels for a given room, this is set
room: PropTypes.instanceOf(Room), // if showing panels for a given room, this is set
groupId: PropTypes.string, // if showing panels for a given group, this is set
user: PropTypes.object, // used if we know the user ahead of opening the panel
};
@ -42,13 +47,13 @@ export default class RightPanel extends React.Component {
static contextType = MatrixClientContext;
constructor(props) {
super(props);
constructor(props, context) {
super(props, context);
this.state = {
...RightPanelStore.getSharedInstance().roomPanelPhaseParams,
phase: this._getPhaseFromProps(),
isUserPrivilegedInGroup: null,
member: this._getUserForPanel(),
verificationRequest: RightPanelStore.getSharedInstance().roomPanelPhaseParams.verificationRequest,
};
this.onAction = this.onAction.bind(this);
this.onRoomStateMember = this.onRoomStateMember.bind(this);
@ -100,10 +105,6 @@ export default class RightPanel extends React.Component {
}
return RightPanelPhases.RoomMemberInfo;
} else {
if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.roomPanelPhase)) {
dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.RoomMemberList});
return RightPanelPhases.RoomMemberList;
}
return rps.roomPanelPhase;
}
}
@ -161,13 +162,13 @@ export default class RightPanel extends React.Component {
}
onRoomStateMember(ev, state, member) {
if (member.roomId !== this.props.roomId) {
if (member.roomId !== this.props.room.roomId) {
return;
}
// redraw the badge on the membership list
if (this.state.phase === RightPanelPhases.RoomMemberList && member.roomId === this.props.roomId) {
if (this.state.phase === RightPanelPhases.RoomMemberList && member.roomId === this.props.room.roomId) {
this._delayedUpdate();
} else if (this.state.phase === RightPanelPhases.RoomMemberInfo && member.roomId === this.props.roomId &&
} else if (this.state.phase === RightPanelPhases.RoomMemberInfo && member.roomId === this.props.room.roomId &&
member.userId === this.state.member.userId) {
// refresh the member info (e.g. new power level)
this._delayedUpdate();
@ -184,6 +185,7 @@ export default class RightPanel extends React.Component {
event: payload.event,
verificationRequest: payload.verificationRequest,
verificationRequestPromise: payload.verificationRequestPromise,
widgetId: payload.widgetId,
});
}
}
@ -211,6 +213,14 @@ export default class RightPanel extends React.Component {
}
};
onClose = () => {
// the RightPanelStore has no way of knowing which mode room/group it is in, so we handle closing here
defaultDispatcher.dispatch({
action: Action.ToggleRightPanel,
type: this.props.groupId ? "group" : "room",
});
};
render() {
const MemberList = sdk.getComponent('rooms.MemberList');
const UserInfo = sdk.getComponent('right_panel.UserInfo');
@ -223,36 +233,42 @@ export default class RightPanel extends React.Component {
const GroupRoomInfo = sdk.getComponent('groups.GroupRoomInfo');
let panel = <div />;
const roomId = this.props.room ? this.props.room.roomId : undefined;
switch (this.state.phase) {
case RightPanelPhases.RoomMemberList:
if (this.props.roomId) {
panel = <MemberList roomId={this.props.roomId} key={this.props.roomId} />;
if (roomId) {
panel = <MemberList roomId={roomId} key={roomId} onClose={this.onClose} />;
}
break;
case RightPanelPhases.GroupMemberList:
if (this.props.groupId) {
panel = <GroupMemberList groupId={this.props.groupId} key={this.props.groupId} />;
}
break;
case RightPanelPhases.GroupRoomList:
panel = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />;
break;
case RightPanelPhases.RoomMemberInfo:
case RightPanelPhases.EncryptionPanel:
panel = <UserInfo
user={this.state.member}
roomId={this.props.roomId}
key={this.props.roomId || this.state.member.userId}
room={this.props.room}
key={roomId || this.state.member.userId}
onClose={this.onCloseUserInfo}
phase={this.state.phase}
verificationRequest={this.state.verificationRequest}
verificationRequestPromise={this.state.verificationRequestPromise}
/>;
break;
case RightPanelPhases.Room3pidMemberInfo:
panel = <ThirdPartyMemberInfo event={this.state.event} key={this.props.roomId} />;
panel = <ThirdPartyMemberInfo event={this.state.event} key={roomId} />;
break;
case RightPanelPhases.GroupMemberInfo:
panel = <UserInfo
user={this.state.member}
@ -260,17 +276,28 @@ export default class RightPanel extends React.Component {
key={this.state.member.userId}
onClose={this.onCloseUserInfo} />;
break;
case RightPanelPhases.GroupRoomInfo:
panel = <GroupRoomInfo
groupRoomId={this.state.groupRoomId}
groupId={this.props.groupId}
key={this.state.groupRoomId} />;
break;
case RightPanelPhases.NotificationPanel:
panel = <NotificationPanel />;
panel = <NotificationPanel onClose={this.onClose} />;
break;
case RightPanelPhases.FilePanel:
panel = <FilePanel roomId={this.props.roomId} resizeNotifier={this.props.resizeNotifier} />;
panel = <FilePanel roomId={roomId} resizeNotifier={this.props.resizeNotifier} onClose={this.onClose} />;
break;
case RightPanelPhases.RoomSummary:
panel = <RoomSummaryCard room={this.props.room} onClose={this.onClose} />;
break;
case RightPanelPhases.Widget:
panel = <WidgetCard room={this.props.room} widgetId={this.state.widgetId} onClose={this.onClose} />;
break;
}

View file

@ -17,7 +17,6 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import {MatrixClientPeg} from "../../MatrixClientPeg";
import * as sdk from "../../index";
import dis from "../../dispatcher/dispatcher";
@ -30,6 +29,10 @@ import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/Di
import Analytics from '../../Analytics';
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
import {ALL_ROOMS} from "../views/directory/NetworkDropdown";
import SettingsStore from "../../settings/SettingsStore";
import TagOrderStore from "../../stores/TagOrderStore";
import GroupStore from "../../stores/GroupStore";
import FlairStore from "../../stores/FlairStore";
const MAX_NAME_LENGTH = 80;
const MAX_TOPIC_LENGTH = 160;
@ -38,15 +41,16 @@ function track(action) {
Analytics.trackEvent('RoomDirectory', action);
}
export default createReactClass({
displayName: 'RoomDirectory',
propTypes: {
export default class RoomDirectory extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
},
};
getInitialState: function() {
return {
constructor(props) {
super(props);
const selectedCommunityId = TagOrderStore.getSelectedTags()[0];
this.state = {
publicRooms: [],
loading: true,
protocolsLoading: true,
@ -54,66 +58,108 @@ export default createReactClass({
instanceId: undefined,
roomServer: MatrixClientPeg.getHomeserverName(),
filterString: null,
selectedCommunityId: SettingsStore.getValue("feature_communities_v2_prototypes")
? selectedCommunityId
: null,
communityName: null,
};
},
// TODO: [REACT-WARNING] Move this to constructor
UNSAFE_componentWillMount: function() {
this._unmounted = false;
this.nextBatch = null;
this.filterTimeout = null;
this.scrollPanel = null;
this.protocols = null;
this.setState({protocolsLoading: true});
this.state.protocolsLoading = true;
if (!MatrixClientPeg.get()) {
// We may not have a client yet when invoked from welcome page
this.setState({protocolsLoading: false});
this.state.protocolsLoading = false;
return;
}
MatrixClientPeg.get().getThirdpartyProtocols().then((response) => {
this.protocols = response;
this.setState({protocolsLoading: false});
}, (err) => {
console.warn(`error loading third party protocols: ${err}`);
this.setState({protocolsLoading: false});
if (MatrixClientPeg.get().isGuest()) {
// Guests currently aren't allowed to use this API, so
// ignore this as otherwise this error is literally the
// thing you see when loading the client!
return;
}
track('Failed to get protocol list from homeserver');
const brand = SdkConfig.get().brand;
this.setState({
error: _t(
'%(brand)s failed to get the protocol list from the homeserver. ' +
'The homeserver may be too old to support third party networks.',
{ brand },
),
if (!this.state.selectedCommunityId) {
MatrixClientPeg.get().getThirdpartyProtocols().then((response) => {
this.protocols = response;
this.setState({protocolsLoading: false});
}, (err) => {
console.warn(`error loading third party protocols: ${err}`);
this.setState({protocolsLoading: false});
if (MatrixClientPeg.get().isGuest()) {
// Guests currently aren't allowed to use this API, so
// ignore this as otherwise this error is literally the
// thing you see when loading the client!
return;
}
track('Failed to get protocol list from homeserver');
const brand = SdkConfig.get().brand;
this.setState({
error: _t(
'%(brand)s failed to get the protocol list from the homeserver. ' +
'The homeserver may be too old to support third party networks.',
{brand},
),
});
});
});
} else {
// We don't use the protocols in the communities v2 prototype experience
this.state.protocolsLoading = false;
// Grab the profile info async
FlairStore.getGroupProfileCached(MatrixClientPeg.get(), this.state.selectedCommunityId).then(profile => {
this.setState({communityName: profile.name});
});
}
}
componentDidMount() {
this.refreshRoomList();
},
}
componentWillUnmount: function() {
componentWillUnmount() {
if (this.filterTimeout) {
clearTimeout(this.filterTimeout);
}
this._unmounted = true;
},
}
refreshRoomList = () => {
if (this.state.selectedCommunityId) {
this.setState({
publicRooms: GroupStore.getGroupRooms(this.state.selectedCommunityId).map(r => {
return {
// Translate all the group properties to the directory format
room_id: r.roomId,
name: r.name,
topic: r.topic,
canonical_alias: r.canonicalAlias,
num_joined_members: r.numJoinedMembers,
avatarUrl: r.avatarUrl,
world_readable: r.worldReadable,
guest_can_join: r.guestsCanJoin,
};
}).filter(r => {
const filterString = this.state.filterString;
if (filterString) {
const containedIn = (s: string) => (s || "").toLowerCase().includes(filterString.toLowerCase());
return containedIn(r.name) || containedIn(r.topic) || containedIn(r.canonical_alias);
}
return true;
}),
loading: false,
});
return;
}
refreshRoomList: function() {
this.nextBatch = null;
this.setState({
publicRooms: [],
loading: true,
});
this.getMoreRooms();
},
};
getMoreRooms: function() {
getMoreRooms() {
if (this.state.selectedCommunityId) return Promise.resolve(); // no more rooms
if (!MatrixClientPeg.get()) return Promise.resolve();
this.setState({
@ -185,7 +231,7 @@ export default createReactClass({
),
});
});
},
}
/**
* A limited interface for removing rooms from the directory.
@ -194,7 +240,7 @@ export default createReactClass({
* HS admins to do this through the RoomSettings interface, but
* this needs SPEC-417.
*/
removeFromDirectory: function(room) {
removeFromDirectory(room) {
const alias = get_display_alias_for_room(room);
const name = room.name || alias || _t('Unnamed room');
@ -236,18 +282,18 @@ export default createReactClass({
});
},
});
},
}
onRoomClicked: function(room, ev) {
if (ev.shiftKey) {
onRoomClicked = (room, ev) => {
if (ev.shiftKey && !this.state.selectedCommunityId) {
ev.preventDefault();
this.removeFromDirectory(room);
} else {
this.showRoom(room);
}
},
};
onOptionChange: function(server, instanceId) {
onOptionChange = (server, instanceId) => {
// clear next batch so we don't try to load more rooms
this.nextBatch = null;
this.setState({
@ -265,15 +311,15 @@ export default createReactClass({
// find the five gitter ones, at which point we do not want
// to render all those rooms when switching back to 'all networks'.
// Easiest to just blow away the state & re-fetch.
},
};
onFillRequest: function(backwards) {
onFillRequest = (backwards) => {
if (backwards || !this.nextBatch) return Promise.resolve(false);
return this.getMoreRooms();
},
};
onFilterChange: function(alias) {
onFilterChange = (alias) => {
this.setState({
filterString: alias || null,
});
@ -289,9 +335,9 @@ export default createReactClass({
this.filterTimeout = null;
this.refreshRoomList();
}, 700);
},
};
onFilterClear: function() {
onFilterClear = () => {
// update immediately
this.setState({
filterString: null,
@ -300,9 +346,9 @@ export default createReactClass({
if (this.filterTimeout) {
clearTimeout(this.filterTimeout);
}
},
};
onJoinFromSearchClick: function(alias) {
onJoinFromSearchClick = (alias) => {
// If we don't have a particular instance id selected, just show that rooms alias
if (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) {
// If the user specified an alias without a domain, add on whichever server is selected
@ -343,50 +389,41 @@ export default createReactClass({
});
});
}
},
};
onPreviewClick: function(ev, room) {
this.props.onFinished();
dis.dispatch({
action: 'view_room',
room_id: room.room_id,
should_peek: true,
});
onPreviewClick = (ev, room) => {
this.showRoom(room, null, false, true);
ev.stopPropagation();
},
};
onViewClick: function(ev, room) {
this.props.onFinished();
dis.dispatch({
action: 'view_room',
room_id: room.room_id,
should_peek: false,
});
onViewClick = (ev, room) => {
this.showRoom(room);
ev.stopPropagation();
},
};
onJoinClick: function(ev, room) {
onJoinClick = (ev, room) => {
this.showRoom(room, null, true);
ev.stopPropagation();
},
};
onCreateRoomClick: function(room) {
onCreateRoomClick = room => {
this.props.onFinished();
dis.dispatch({
action: 'view_create_room',
public: true,
});
},
};
showRoomAlias: function(alias, autoJoin=false) {
showRoomAlias(alias, autoJoin=false) {
this.showRoom(null, alias, autoJoin);
},
}
showRoom: function(room, room_alias, autoJoin=false) {
showRoom(room, room_alias, autoJoin = false, shouldPeek = false) {
this.props.onFinished();
const payload = {
action: 'view_room',
auto_join: autoJoin,
should_peek: shouldPeek,
};
if (room) {
// Don't let the user view a room they won't be able to either
@ -411,6 +448,7 @@ export default createReactClass({
};
if (this.state.roomServer) {
payload.via_servers = [this.state.roomServer];
payload.opts = {
viaServers: [this.state.roomServer],
};
@ -426,7 +464,7 @@ export default createReactClass({
payload.room_id = room.room_id;
}
dis.dispatch(payload);
},
}
getRow(room) {
const client = MatrixClientPeg.get();
@ -492,22 +530,22 @@ export default createReactClass({
<td className="mx_RoomDirectory_join">{joinOrViewButton}</td>
</tr>
);
},
}
collectScrollPanel: function(element) {
collectScrollPanel = (element) => {
this.scrollPanel = element;
},
};
_stringLooksLikeId: function(s, field_type) {
_stringLooksLikeId(s, field_type) {
let pat = /^#[^\s]+:[^\s]/;
if (field_type && field_type.regexp) {
pat = new RegExp(field_type.regexp);
}
return pat.test(s);
},
}
_getFieldsForThirdPartyLocation: function(userInput, protocol, instance) {
_getFieldsForThirdPartyLocation(userInput, protocol, instance) {
// make an object with the fields specified by that protocol. We
// require that the values of all but the last field come from the
// instance. The last is the user input.
@ -521,20 +559,20 @@ export default createReactClass({
}
fields[requiredFields[requiredFields.length - 1]] = userInput;
return fields;
},
}
/**
* called by the parent component when PageUp/Down/etc is pressed.
*
* We pass it down to the scroll panel.
*/
handleScrollKey: function(ev) {
handleScrollKey = ev => {
if (this.scrollPanel) {
this.scrollPanel.handleScrollKey(ev);
}
},
};
render: function() {
render() {
const Loader = sdk.getComponent("elements.Spinner");
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
@ -610,6 +648,18 @@ export default createReactClass({
}
}
let dropdown = (
<NetworkDropdown
protocols={this.protocols}
onOptionChange={this.onOptionChange}
selectedServerName={this.state.roomServer}
selectedInstanceId={this.state.instanceId}
/>
);
if (this.state.selectedCommunityId) {
dropdown = null;
}
listHeader = <div className="mx_RoomDirectory_listheader">
<DirectorySearchBox
className="mx_RoomDirectory_searchbox"
@ -619,12 +669,7 @@ export default createReactClass({
placeholder={placeholder}
showJoinButton={showJoinButton}
/>
<NetworkDropdown
protocols={this.protocols}
onOptionChange={this.onOptionChange}
selectedServerName={this.state.roomServer}
selectedInstanceId={this.state.instanceId}
/>
{dropdown}
</div>;
}
const explanation =
@ -637,12 +682,16 @@ export default createReactClass({
}},
);
const title = this.state.selectedCommunityId
? _t("Explore rooms in %(communityName)s", {
communityName: this.state.communityName || this.state.selectedCommunityId,
}) : _t("Explore rooms");
return (
<BaseDialog
className={'mx_RoomDirectory_dialog'}
hasCancel={true}
onFinished={this.props.onFinished}
title={_t("Explore rooms")}
title={title}
>
<div className="mx_RoomDirectory">
{explanation}
@ -653,8 +702,8 @@ export default createReactClass({
</div>
</BaseDialog>
);
},
});
}
}
// Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom
// but works with the objects we get from the public room list

View file

@ -20,7 +20,6 @@ import classNames from "classnames";
import defaultDispatcher from "../../dispatcher/dispatcher";
import { _t } from "../../languageHandler";
import { ActionPayload } from "../../dispatcher/payloads";
import { throttle } from 'lodash';
import { Key } from "../../Keyboard";
import AccessibleButton from "../views/elements/AccessibleButton";
import { Action } from "../../dispatcher/actions";
@ -137,7 +136,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
});
let icon = (
<div className='mx_RoomSearch_icon'/>
<div className='mx_RoomSearch_icon' />
);
let input = (
<input
@ -166,7 +165,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
icon = (
<AccessibleButton
title={_t("Search rooms")}
className="mx_RoomSearch_icon"
className="mx_RoomSearch_icon mx_RoomSearch_minimizedHandle"
onClick={this.openSearch}
/>
);

View file

@ -17,7 +17,6 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import Matrix from 'matrix-js-sdk';
import { _t, _td } from '../../languageHandler';
@ -39,10 +38,8 @@ function getUnsentMessages(room) {
});
}
export default createReactClass({
displayName: 'RoomStatusBar',
propTypes: {
export default class RoomStatusBar extends React.Component {
static propTypes = {
// the room this statusbar is representing.
room: PropTypes.object.isRequired,
// This is true when the user is alone in the room, but has also sent a message.
@ -86,37 +83,35 @@ export default createReactClass({
// callback for when the status bar is displaying something and should
// be visible
onVisible: PropTypes.func,
},
};
getInitialState: function() {
return {
syncState: MatrixClientPeg.get().getSyncState(),
syncStateData: MatrixClientPeg.get().getSyncStateData(),
unsentMessages: getUnsentMessages(this.props.room),
};
},
state = {
syncState: MatrixClientPeg.get().getSyncState(),
syncStateData: MatrixClientPeg.get().getSyncStateData(),
unsentMessages: getUnsentMessages(this.props.room),
};
componentDidMount: function() {
componentDidMount() {
MatrixClientPeg.get().on("sync", this.onSyncStateChange);
MatrixClientPeg.get().on("Room.localEchoUpdated", this._onRoomLocalEchoUpdated);
this._checkSize();
},
}
componentDidUpdate: function() {
componentDidUpdate() {
this._checkSize();
},
}
componentWillUnmount: function() {
componentWillUnmount() {
// we may have entirely lost our client as we're logging out before clicking login on the guest bar...
const client = MatrixClientPeg.get();
if (client) {
client.removeListener("sync", this.onSyncStateChange);
client.removeListener("Room.localEchoUpdated", this._onRoomLocalEchoUpdated);
}
},
}
onSyncStateChange: function(state, prevState, data) {
onSyncStateChange = (state, prevState, data) => {
if (state === "SYNCING" && prevState === "SYNCING") {
return;
}
@ -124,39 +119,39 @@ export default createReactClass({
syncState: state,
syncStateData: data,
});
},
};
_onResendAllClick: function() {
_onResendAllClick = () => {
Resend.resendUnsentEvents(this.props.room);
dis.fire(Action.FocusComposer);
},
};
_onCancelAllClick: function() {
_onCancelAllClick = () => {
Resend.cancelUnsentEvents(this.props.room);
dis.fire(Action.FocusComposer);
},
};
_onRoomLocalEchoUpdated: function(event, room, oldEventId, oldStatus) {
_onRoomLocalEchoUpdated = (event, room, oldEventId, oldStatus) => {
if (room.roomId !== this.props.room.roomId) return;
this.setState({
unsentMessages: getUnsentMessages(this.props.room),
});
},
};
// Check whether current size is greater than 0, if yes call props.onVisible
_checkSize: function() {
_checkSize() {
if (this._getSize()) {
if (this.props.onVisible) this.props.onVisible();
} else {
if (this.props.onHidden) this.props.onHidden();
}
},
}
// We don't need the actual height - just whether it is likely to have
// changed - so we use '0' to indicate normal size, and other values to
// indicate other sizes.
_getSize: function() {
_getSize() {
if (this._shouldShowConnectionError() ||
this.props.hasActiveCall ||
this.props.sentMessageAndIsAlone
@ -166,10 +161,10 @@ export default createReactClass({
return STATUS_BAR_EXPANDED_LARGE;
}
return STATUS_BAR_HIDDEN;
},
}
// return suitable content for the image on the left of the status bar.
_getIndicator: function() {
_getIndicator() {
if (this.props.hasActiveCall) {
const TintableSvg = sdk.getComponent("elements.TintableSvg");
return (
@ -182,9 +177,9 @@ export default createReactClass({
}
return null;
},
}
_shouldShowConnectionError: function() {
_shouldShowConnectionError() {
// no conn bar trumps the "some not sent" msg since you can't resend without
// a connection!
// There's one situation in which we don't show this 'no connection' bar, and that's
@ -195,9 +190,9 @@ export default createReactClass({
this.state.syncStateData.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED',
);
return this.state.syncState === "ERROR" && !errorIsMauError;
},
}
_getUnsentMessageContent: function() {
_getUnsentMessageContent() {
const unsentMessages = this.state.unsentMessages;
if (!unsentMessages.length) return null;
@ -272,10 +267,10 @@ export default createReactClass({
</div>
</div>
</div>;
},
}
// return suitable content for the main (text) part of the status bar.
_getContent: function() {
_getContent() {
if (this._shouldShowConnectionError()) {
return (
<div className="mx_RoomStatusBar_connectionLostBar">
@ -323,9 +318,9 @@ export default createReactClass({
}
return null;
},
}
render: function() {
render() {
const content = this._getContent();
const indicator = this._getIndicator();
@ -339,5 +334,5 @@ export default createReactClass({
</div>
</div>
);
},
});
}
}

View file

@ -15,7 +15,6 @@ limitations under the License.
*/
import React, {createRef} from "react";
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { Key } from '../../Keyboard';
import Timer from '../../utils/Timer';
@ -84,10 +83,8 @@ if (DEBUG_SCROLL) {
* offset as normal.
*/
export default createReactClass({
displayName: 'ScrollPanel',
propTypes: {
export default class ScrollPanel extends React.Component {
static propTypes = {
/* stickyBottom: if set to true, then once the user hits the bottom of
* the list, any new children added to the list will cause the list to
* scroll down to show the new element, rather than preserving the
@ -97,7 +94,7 @@ export default createReactClass({
/* startAtBottom: if set to true, the view is assumed to start
* scrolled to the bottom.
* XXX: It's likley this is unecessary and can be derived from
* XXX: It's likely this is unnecessary and can be derived from
* stickyBottom, but I'm adding an extra parameter to ensure
* behaviour stays the same for other uses of ScrollPanel.
* If so, let's remove this parameter down the line.
@ -141,6 +138,7 @@ export default createReactClass({
/* style: styles to add to the top-level div
*/
style: PropTypes.object,
/* resizeNotifier: ResizeNotifier to know when middle column has changed size
*/
resizeNotifier: PropTypes.object,
@ -149,36 +147,35 @@ export default createReactClass({
* of the wrapper
*/
fixedChildren: PropTypes.node,
},
};
getDefaultProps: function() {
return {
stickyBottom: true,
startAtBottom: true,
onFillRequest: function(backwards) { return Promise.resolve(false); },
onUnfillRequest: function(backwards, scrollToken) {},
onScroll: function() {},
};
},
static defaultProps = {
stickyBottom: true,
startAtBottom: true,
onFillRequest: function(backwards) { return Promise.resolve(false); },
onUnfillRequest: function(backwards, scrollToken) {},
onScroll: function() {},
};
constructor(props) {
super(props);
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._pendingFillRequests = {b: null, f: null};
if (this.props.resizeNotifier) {
this.props.resizeNotifier.on("middlePanelResized", this.onResize);
this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize);
}
this.resetScrollState();
this._itemlist = createRef();
},
}
componentDidMount: function() {
componentDidMount() {
this.checkScroll();
},
}
componentDidUpdate: function() {
componentDidUpdate() {
// after adding event tiles, we may need to tweak the scroll (either to
// keep at the bottom of the timeline, or to maintain the view after
// adding events to the top).
@ -186,9 +183,9 @@ export default createReactClass({
// This will also re-check the fill state, in case the paginate was inadequate
this.checkScroll();
this.updatePreventShrinking();
},
}
componentWillUnmount: function() {
componentWillUnmount() {
// set a boolean to say we've been unmounted, which any pending
// promises can use to throw away their results.
//
@ -196,51 +193,53 @@ export default createReactClass({
this.unmounted = true;
if (this.props.resizeNotifier) {
this.props.resizeNotifier.removeListener("middlePanelResized", this.onResize);
this.props.resizeNotifier.removeListener("middlePanelResizedNoisy", this.onResize);
}
},
}
onScroll: function(ev) {
onScroll = ev => {
// skip scroll events caused by resizing
if (this.props.resizeNotifier && this.props.resizeNotifier.isResizing) return;
debuglog("onScroll", this._getScrollNode().scrollTop);
this._scrollTimeout.restart();
this._saveScrollState();
this.updatePreventShrinking();
this.props.onScroll(ev);
this.checkFillState();
},
};
onResize: function() {
onResize = () => {
debuglog("onResize");
this.checkScroll();
// update preventShrinkingState if present
if (this.preventShrinkingState) {
this.preventShrinking();
}
},
};
// after an update to the contents of the panel, check that the scroll is
// where it ought to be, and set off pagination requests if necessary.
checkScroll: function() {
checkScroll = () => {
if (this.unmounted) {
return;
}
this._restoreSavedScrollState();
this.checkFillState();
},
};
// return true if the content is fully scrolled down right now; else false.
//
// note that this is independent of the 'stuckAtBottom' state - it is simply
// about whether the content is scrolled down right now, irrespective of
// whether it will stay that way when the children update.
isAtBottom: function() {
isAtBottom = () => {
const sn = this._getScrollNode();
// fractional values (both too big and too small)
// for scrollTop happen on certain browsers/platforms
// when scrolled all the way down. E.g. Chrome 72 on debian.
// so check difference <= 1;
return Math.abs(sn.scrollHeight - (sn.scrollTop + sn.clientHeight)) <= 1;
},
};
// returns the vertical height in the given direction that can be removed from
// the content box (which has a height of scrollHeight, see checkFillState) without
@ -273,7 +272,7 @@ export default createReactClass({
// |#########| - |
// |#########| |
// `---------' -
_getExcessHeight: function(backwards) {
_getExcessHeight(backwards) {
const sn = this._getScrollNode();
const contentHeight = this._getMessagesHeight();
const listHeight = this._getListHeight();
@ -285,10 +284,10 @@ export default createReactClass({
} else {
return contentHeight - (unclippedScrollTop + 2*sn.clientHeight) - UNPAGINATION_PADDING;
}
},
}
// check the scroll state and send out backfill requests if necessary.
checkFillState: async function(depth=0) {
checkFillState = async (depth=0) => {
if (this.unmounted) {
return;
}
@ -368,10 +367,10 @@ export default createReactClass({
this._fillRequestWhileRunning = false;
this.checkFillState();
}
},
};
// check if unfilling is possible and send an unfill request if necessary
_checkUnfillState: function(backwards) {
_checkUnfillState(backwards) {
let excessHeight = this._getExcessHeight(backwards);
if (excessHeight <= 0) {
return;
@ -417,10 +416,10 @@ export default createReactClass({
this.props.onUnfillRequest(backwards, markerScrollToken);
}, UNFILL_REQUEST_DEBOUNCE_MS);
}
},
}
// check if there is already a pending fill request. If not, set one off.
_maybeFill: function(depth, backwards) {
_maybeFill(depth, backwards) {
const dir = backwards ? 'b' : 'f';
if (this._pendingFillRequests[dir]) {
debuglog("Already a "+dir+" fill in progress - not starting another");
@ -456,7 +455,7 @@ export default createReactClass({
return this.checkFillState(depth + 1);
}
});
},
}
/* get the current scroll state. This returns an object with the following
* properties:
@ -472,9 +471,7 @@ export default createReactClass({
* the number of pixels the bottom of the tracked child is above the
* bottom of the scroll panel.
*/
getScrollState: function() {
return this.scrollState;
},
getScrollState = () => this.scrollState;
/* reset the saved scroll state.
*
@ -488,7 +485,7 @@ export default createReactClass({
* no use if no children exist yet, or if you are about to replace the
* child list.)
*/
resetScrollState: function() {
resetScrollState = () => {
this.scrollState = {
stuckAtBottom: this.props.startAtBottom,
};
@ -496,20 +493,20 @@ export default createReactClass({
this._pages = 0;
this._scrollTimeout = new Timer(100);
this._heightUpdateInProgress = false;
},
};
/**
* jump to the top of the content.
*/
scrollToTop: function() {
scrollToTop = () => {
this._getScrollNode().scrollTop = 0;
this._saveScrollState();
},
};
/**
* jump to the bottom of the content.
*/
scrollToBottom: function() {
scrollToBottom = () => {
// the easiest way to make sure that the scroll state is correctly
// saved is to do the scroll, then save the updated state. (Calculating
// it ourselves is hard, and we can't rely on an onScroll callback
@ -517,25 +514,25 @@ export default createReactClass({
const sn = this._getScrollNode();
sn.scrollTop = sn.scrollHeight;
this._saveScrollState();
},
};
/**
* Page up/down.
*
* @param {number} mult: -1 to page up, +1 to page down
*/
scrollRelative: function(mult) {
scrollRelative = mult => {
const scrollNode = this._getScrollNode();
const delta = mult * scrollNode.clientHeight * 0.5;
scrollNode.scrollBy(0, delta);
this._saveScrollState();
},
};
/**
* Scroll up/down in response to a scroll key
* @param {object} ev the keyboard event
*/
handleScrollKey: function(ev) {
handleScrollKey = ev => {
switch (ev.key) {
case Key.PAGE_UP:
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
@ -561,7 +558,7 @@ export default createReactClass({
}
break;
}
},
};
/* Scroll the panel to bring the DOM node with the scroll token
* `scrollToken` into view.
@ -574,7 +571,7 @@ export default createReactClass({
* node (specifically, the bottom of it) will be positioned. If omitted, it
* defaults to 0.
*/
scrollToToken: function(scrollToken, pixelOffset, offsetBase) {
scrollToToken = (scrollToken, pixelOffset, offsetBase) => {
pixelOffset = pixelOffset || 0;
offsetBase = offsetBase || 0;
@ -596,9 +593,9 @@ export default createReactClass({
scrollNode.scrollTop = (trackedNode.offsetTop - (scrollNode.clientHeight * offsetBase)) + pixelOffset;
this._saveScrollState();
}
},
};
_saveScrollState: function() {
_saveScrollState() {
if (this.props.stickyBottom && this.isAtBottom()) {
this.scrollState = { stuckAtBottom: true };
debuglog("saved stuckAtBottom state");
@ -641,9 +638,9 @@ export default createReactClass({
bottomOffset: bottomOffset,
pixelOffset: bottomOffset - viewportBottom, //needed for restoring the scroll position when coming back to the room
};
},
}
_restoreSavedScrollState: async function() {
async _restoreSavedScrollState() {
const scrollState = this.scrollState;
if (scrollState.stuckAtBottom) {
@ -676,7 +673,8 @@ export default createReactClass({
} else {
debuglog("not updating height because request already in progress");
}
},
}
// need a better name that also indicates this will change scrollTop? Rebalance height? Reveal content?
async _updateHeight() {
// wait until user has stopped scrolling
@ -731,7 +729,7 @@ export default createReactClass({
debuglog("updateHeight to", {newHeight, topDiff});
}
}
},
}
_getTrackedNode() {
const scrollState = this.scrollState;
@ -764,11 +762,11 @@ export default createReactClass({
}
return scrollState.trackedNode;
},
}
_getListHeight() {
return this._bottomGrowth + (this._pages * PAGE_SIZE);
},
}
_getMessagesHeight() {
const itemlist = this._itemlist.current;
@ -777,17 +775,17 @@ export default createReactClass({
const firstNodeTop = itemlist.firstElementChild ? itemlist.firstElementChild.offsetTop : 0;
// 18 is itemlist padding
return lastNodeBottom - firstNodeTop + (18 * 2);
},
}
_topFromBottom(node) {
// current capped height - distance from top = distance from bottom of container to top of tracked element
return this._itemlist.current.clientHeight - node.offsetTop;
},
}
/* get the DOM node which has the scrollTop property we care about for our
* message panel.
*/
_getScrollNode: function() {
_getScrollNode() {
if (this.unmounted) {
// this shouldn't happen, but when it does, turn the NPE into
// something more meaningful.
@ -801,18 +799,18 @@ export default createReactClass({
}
return this._divScroll;
},
}
_collectScroll: function(divScroll) {
_collectScroll = divScroll => {
this._divScroll = divScroll;
},
};
/**
Mark the bottom offset of the last tile so we can balance it out when
anything below it changes, by calling updatePreventShrinking, to keep
the same minimum bottom offset, effectively preventing the timeline to shrink.
*/
preventShrinking: function() {
preventShrinking = () => {
const messageList = this._itemlist.current;
const tiles = messageList && messageList.children;
if (!messageList) {
@ -836,16 +834,16 @@ export default createReactClass({
offsetNode: lastTileNode,
};
debuglog("prevent shrinking, last tile ", offsetFromBottom, "px from bottom");
},
};
/** Clear shrinking prevention. Used internally, and when the timeline is reloaded. */
clearPreventShrinking: function() {
clearPreventShrinking = () => {
const messageList = this._itemlist.current;
const balanceElement = messageList && messageList.parentElement;
if (balanceElement) balanceElement.style.paddingBottom = null;
this.preventShrinkingState = null;
debuglog("prevent shrinking cleared");
},
};
/**
update the container padding to balance
@ -855,7 +853,7 @@ export default createReactClass({
from the bottom of the marked tile grows larger than
what it was when marking.
*/
updatePreventShrinking: function() {
updatePreventShrinking = () => {
if (this.preventShrinkingState) {
const sn = this._getScrollNode();
const scrollState = this.scrollState;
@ -885,9 +883,9 @@ export default createReactClass({
this.clearPreventShrinking();
}
}
},
};
render: function() {
render() {
// TODO: the classnames on the div and ol could do with being updated to
// reflect the fact that we don't necessarily contain a list of messages.
// it's not obvious why we have a separate div and ol anyway.
@ -905,5 +903,5 @@ export default createReactClass({
</div>
</AutoHideScrollbar>
);
},
});
}
}

View file

@ -16,18 +16,15 @@ limitations under the License.
*/
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { Key } from '../../Keyboard';
import dis from '../../dispatcher/dispatcher';
import { throttle } from 'lodash';
import {throttle} from 'lodash';
import AccessibleButton from '../../components/views/elements/AccessibleButton';
import classNames from 'classnames';
export default createReactClass({
displayName: 'SearchBox',
propTypes: {
export default class SearchBox extends React.Component {
static propTypes = {
onSearch: PropTypes.func,
onCleared: PropTypes.func,
onKeyDown: PropTypes.func,
@ -38,35 +35,32 @@ export default createReactClass({
// on room search focus action (it would be nicer to take
// this functionality out, but not obvious how that would work)
enableRoomSearchFocus: PropTypes.bool,
},
};
getDefaultProps: function() {
return {
enableRoomSearchFocus: false,
};
},
static defaultProps = {
enableRoomSearchFocus: false,
};
getInitialState: function() {
return {
constructor(props) {
super(props);
this._search = createRef();
this.state = {
searchTerm: "",
blurred: true,
};
},
}
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._search = createRef();
},
componentDidMount: function() {
componentDidMount() {
this.dispatcherRef = dis.register(this.onAction);
},
}
componentWillUnmount: function() {
componentWillUnmount() {
dis.unregister(this.dispatcherRef);
},
}
onAction: function(payload) {
onAction = payload => {
if (!this.props.enableRoomSearchFocus) return;
switch (payload.action) {
@ -81,51 +75,51 @@ export default createReactClass({
}
break;
}
},
};
onChange: function() {
onChange = () => {
if (!this._search.current) return;
this.setState({ searchTerm: this._search.current.value });
this.onSearch();
},
};
onSearch: throttle(function() {
onSearch = throttle(() => {
this.props.onSearch(this._search.current.value);
}, 200, {trailing: true, leading: true}),
}, 200, {trailing: true, leading: true});
_onKeyDown: function(ev) {
_onKeyDown = ev => {
switch (ev.key) {
case Key.ESCAPE:
this._clearSearch("keyboard");
break;
}
if (this.props.onKeyDown) this.props.onKeyDown(ev);
},
};
_onFocus: function(ev) {
_onFocus = ev => {
this.setState({blurred: false});
ev.target.select();
if (this.props.onFocus) {
this.props.onFocus(ev);
}
},
};
_onBlur: function(ev) {
_onBlur = ev => {
this.setState({blurred: true});
if (this.props.onBlur) {
this.props.onBlur(ev);
}
},
};
_clearSearch: function(source) {
_clearSearch(source) {
this._search.current.value = "";
this.onChange();
if (this.props.onCleared) {
this.props.onCleared(source);
}
},
}
render: function() {
render() {
// check for collapsed here and
// not at parent so we keep
// searchTerm in our state
@ -166,5 +160,5 @@ export default createReactClass({
{ clearButton }
</div>
);
},
});
}
}

View file

@ -18,7 +18,6 @@ limitations under the License.
import * as React from "react";
import {_t} from '../../languageHandler';
import * as PropTypes from "prop-types";
import * as sdk from "../../index";
import AutoHideScrollbar from './AutoHideScrollbar';
import { ReactNode } from "react";

View file

@ -16,7 +16,6 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import TagOrderStore from '../../stores/TagOrderStore';
import GroupActions from '../../actions/GroupActions';
@ -29,22 +28,18 @@ import { Droppable } from 'react-beautiful-dnd';
import classNames from 'classnames';
import MatrixClientContext from "../../contexts/MatrixClientContext";
import AutoHideScrollbar from "./AutoHideScrollbar";
import SettingsStore from "../../settings/SettingsStore";
import UserTagTile from "../views/elements/UserTagTile";
const TagPanel = createReactClass({
displayName: 'TagPanel',
class TagPanel extends React.Component {
static contextType = MatrixClientContext;
statics: {
contextType: MatrixClientContext,
},
state = {
orderedTags: [],
selectedTags: [],
};
getInitialState() {
return {
orderedTags: [],
selectedTags: [],
};
},
componentDidMount: function() {
componentDidMount() {
this.unmounted = false;
this.context.on("Group.myMembership", this._onGroupMyMembership);
this.context.on("sync", this._onClientSync);
@ -60,7 +55,7 @@ const TagPanel = createReactClass({
});
// This could be done by anything with a matrix client
dis.dispatch(GroupActions.fetchJoinedGroups(this.context));
},
}
componentWillUnmount() {
this.unmounted = true;
@ -69,14 +64,14 @@ const TagPanel = createReactClass({
if (this._tagOrderStoreToken) {
this._tagOrderStoreToken.remove();
}
},
}
_onGroupMyMembership() {
_onGroupMyMembership = () => {
if (this.unmounted) return;
dis.dispatch(GroupActions.fetchJoinedGroups(this.context));
},
};
_onClientSync(syncState, prevState) {
_onClientSync = (syncState, prevState) => {
// Consider the client reconnected if there is no error with syncing.
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
const reconnected = syncState !== "ERROR" && prevState !== syncState;
@ -84,29 +79,33 @@ const TagPanel = createReactClass({
// Load joined groups
dis.dispatch(GroupActions.fetchJoinedGroups(this.context));
}
},
};
onMouseDown(e) {
onMouseDown = e => {
// only dispatch if its not a no-op
if (this.state.selectedTags.length > 0) {
dis.dispatch({action: 'deselect_tags'});
}
},
};
onCreateGroupClick(ev) {
ev.stopPropagation();
dis.dispatch({action: 'view_create_group'});
},
onClearFilterClick(ev) {
onClearFilterClick = ev => {
dis.dispatch({action: 'deselect_tags'});
},
};
renderGlobalIcon() {
if (!SettingsStore.getValue("feature_communities_v2_prototypes")) return null;
return (
<div>
<UserTagTile />
<hr className="mx_TagPanel_divider" />
</div>
);
}
render() {
const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const ActionButton = sdk.getComponent('elements.ActionButton');
const TintableSvg = sdk.getComponent('elements.TintableSvg');
const tags = this.state.orderedTags.map((tag, index) => {
return <DNDTagTile
@ -118,26 +117,29 @@ const TagPanel = createReactClass({
});
const itemsSelected = this.state.selectedTags.length > 0;
let clearButton;
if (itemsSelected) {
clearButton = <AccessibleButton className="mx_TagPanel_clearButton" onClick={this.onClearFilterClick}>
<TintableSvg src={require("../../../res/img/icons-close.svg")} width="24" height="24"
alt={_t("Clear filter")}
title={_t("Clear filter")}
/>
</AccessibleButton>;
}
const classes = classNames('mx_TagPanel', {
mx_TagPanel_items_selected: itemsSelected,
});
return <div className={classes}>
<div className="mx_TagPanel_clearButton_container">
{ clearButton }
</div>
<div className="mx_TagPanel_divider" />
let createButton = (
<ActionButton
tooltip
label={_t("Communities")}
action="toggle_my_groups"
className="mx_TagTile mx_TagTile_plus" />
);
if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
createButton = (
<ActionButton
tooltip
label={_t("Create community")}
action="view_create_group"
className="mx_TagTile mx_TagTile_plus" />
);
}
return <div className={classes} onClick={this.onClearFilterClick}>
<AutoHideScrollbar
className="mx_TagPanel_scroller"
// XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
@ -153,13 +155,10 @@ const TagPanel = createReactClass({
className="mx_TagPanel_tagTileContainer"
ref={provided.innerRef}
>
{ this.renderGlobalIcon() }
{ tags }
<div>
<ActionButton
tooltip
label={_t("Communities")}
action="toggle_my_groups"
className="mx_TagTile mx_TagTile_plus" />
{createButton}
</div>
{ provided.placeholder }
</div>
@ -167,6 +166,6 @@ const TagPanel = createReactClass({
</Droppable>
</AutoHideScrollbar>
</div>;
},
});
}
}
export default TagPanel;

View file

@ -19,7 +19,6 @@ limitations under the License.
import SettingsStore from "../../settings/SettingsStore";
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import ReactDOM from "react-dom";
import PropTypes from 'prop-types';
import {EventTimeline} from "matrix-js-sdk";
@ -36,6 +35,7 @@ import Timer from '../../utils/Timer';
import shouldHideEvent from '../../shouldHideEvent';
import EditorStateTransfer from '../../utils/EditorStateTransfer';
import {haveTileForEvent} from "../views/rooms/EventTile";
import {UIFeature} from "../../settings/UIFeature";
const PAGINATE_SIZE = 20;
const INITIAL_SIZE = 20;
@ -54,10 +54,8 @@ if (DEBUG) {
*
* Also responsible for handling and sending read receipts.
*/
const TimelinePanel = createReactClass({
displayName: 'TimelinePanel',
propTypes: {
class TimelinePanel extends React.Component {
static propTypes = {
// The js-sdk EventTimelineSet object for the timeline sequence we are
// representing. This may or may not have a room, depending on what it's
// a timeline representing. If it has a room, we maintain RRs etc for
@ -107,31 +105,36 @@ const TimelinePanel = createReactClass({
// shape property to be passed to EventTiles
tileShape: PropTypes.string,
// placeholder text to use if the timeline is empty
empty: PropTypes.string,
// placeholder to use if the timeline is empty
empty: PropTypes.node,
// whether to show reactions for an event
showReactions: PropTypes.bool,
// whether to use the irc layout
useIRCLayout: PropTypes.bool,
},
}
statics: {
// a map from room id to read marker event timestamp
roomReadMarkerTsMap: {},
},
// a map from room id to read marker event timestamp
static roomReadMarkerTsMap = {};
getDefaultProps: function() {
return {
// By default, disable the timelineCap in favour of unpaginating based on
// event tile heights. (See _unpaginateEvents)
timelineCap: Number.MAX_VALUE,
className: 'mx_RoomView_messagePanel',
};
},
static defaultProps = {
// By default, disable the timelineCap in favour of unpaginating based on
// event tile heights. (See _unpaginateEvents)
timelineCap: Number.MAX_VALUE,
className: 'mx_RoomView_messagePanel',
};
constructor(props) {
super(props);
debuglog("TimelinePanel: mounting");
this.lastRRSentEventId = undefined;
this.lastRMSentEventId = undefined;
this._messagePanel = createRef();
getInitialState: function() {
// XXX: we could track RM per TimelineSet rather than per Room.
// but for now we just do it per room for simplicity.
let initialReadMarker = null;
@ -144,7 +147,7 @@ const TimelinePanel = createReactClass({
}
}
return {
this.state = {
events: [],
liveEvents: [],
timelineLoading: true, // track whether our room timeline is loading
@ -203,24 +206,6 @@ const TimelinePanel = createReactClass({
// how long to show the RM for when it's scrolled off-screen
readMarkerOutOfViewThresholdMs: SettingsStore.getValue("readMarkerOutOfViewThresholdMs"),
};
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
debuglog("TimelinePanel: mounting");
this.lastRRSentEventId = undefined;
this.lastRMSentEventId = undefined;
this._messagePanel = createRef();
if (this.props.manageReadReceipts) {
this.updateReadReceiptOnUserActivity();
}
if (this.props.manageReadMarkers) {
this.updateReadMarkerOnUserActivity();
}
this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
@ -234,12 +219,24 @@ const TimelinePanel = createReactClass({
MatrixClientPeg.get().on("Event.decrypted", this.onEventDecrypted);
MatrixClientPeg.get().on("Event.replaced", this.onEventReplaced);
MatrixClientPeg.get().on("sync", this.onSync);
}
// TODO: [REACT-WARNING] Move into constructor
// eslint-disable-next-line camelcase
UNSAFE_componentWillMount() {
if (this.props.manageReadReceipts) {
this.updateReadReceiptOnUserActivity();
}
if (this.props.manageReadMarkers) {
this.updateReadMarkerOnUserActivity();
}
this._initTimeline(this.props);
},
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps: function(newProps) {
// eslint-disable-next-line camelcase
UNSAFE_componentWillReceiveProps(newProps) {
if (newProps.timelineSet !== this.props.timelineSet) {
// throw new Error("changing timelineSet on a TimelinePanel is not supported");
@ -260,9 +257,9 @@ const TimelinePanel = createReactClass({
" (was " + this.props.eventId + ")");
return this._initTimeline(newProps);
}
},
}
shouldComponentUpdate: function(nextProps, nextState) {
shouldComponentUpdate(nextProps, nextState) {
if (!ObjectUtils.shallowEqual(this.props, nextProps)) {
if (DEBUG) {
console.group("Timeline.shouldComponentUpdate: props change");
@ -284,9 +281,9 @@ const TimelinePanel = createReactClass({
}
return false;
},
}
componentWillUnmount: function() {
componentWillUnmount() {
// set a boolean to say we've been unmounted, which any pending
// promises can use to throw away their results.
//
@ -316,9 +313,9 @@ const TimelinePanel = createReactClass({
client.removeListener("Event.replaced", this.onEventReplaced);
client.removeListener("sync", this.onSync);
}
},
}
onMessageListUnfillRequest: function(backwards, scrollToken) {
onMessageListUnfillRequest = (backwards, scrollToken) => {
// If backwards, unpaginate from the back (i.e. the start of the timeline)
const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
debuglog("TimelinePanel: unpaginating events in direction", dir);
@ -349,18 +346,18 @@ const TimelinePanel = createReactClass({
firstVisibleEventIndex,
});
}
},
};
onPaginationRequest(timelineWindow, direction, size) {
onPaginationRequest = (timelineWindow, direction, size) => {
if (this.props.onPaginationRequest) {
return this.props.onPaginationRequest(timelineWindow, direction, size);
} else {
return timelineWindow.paginate(direction, size);
}
},
};
// set off a pagination request.
onMessageListFillRequest: function(backwards) {
onMessageListFillRequest = backwards => {
if (!this._shouldPaginate()) return Promise.resolve(false);
const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
@ -425,9 +422,9 @@ const TimelinePanel = createReactClass({
});
});
});
},
};
onMessageListScroll: function(e) {
onMessageListScroll = e => {
if (this.props.onScroll) {
this.props.onScroll(e);
}
@ -447,9 +444,9 @@ const TimelinePanel = createReactClass({
// NO-OP when timeout already has set to the given value
this._readMarkerActivityTimer.changeTimeout(timeout);
}
},
};
onAction: function(payload) {
onAction = payload => {
if (payload.action === 'ignore_state_changed') {
this.forceUpdate();
}
@ -463,9 +460,9 @@ const TimelinePanel = createReactClass({
}
});
}
},
};
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
onRoomTimeline = (ev, room, toStartOfTimeline, removed, data) => {
// ignore events for other timeline sets
if (data.timeline.getTimelineSet() !== this.props.timelineSet) return;
@ -537,21 +534,19 @@ const TimelinePanel = createReactClass({
}
});
});
},
};
onRoomTimelineReset: function(room, timelineSet) {
onRoomTimelineReset = (room, timelineSet) => {
if (timelineSet !== this.props.timelineSet) return;
if (this._messagePanel.current && this._messagePanel.current.isAtBottom()) {
this._loadTimeline();
}
},
};
canResetTimeline: function() {
return this._messagePanel.current && this._messagePanel.current.isAtBottom();
},
canResetTimeline = () => this._messagePanel.current && this._messagePanel.current.isAtBottom();
onRoomRedaction: function(ev, room) {
onRoomRedaction = (ev, room) => {
if (this.unmounted) return;
// ignore events for other rooms
@ -560,9 +555,9 @@ const TimelinePanel = createReactClass({
// we could skip an update if the event isn't in our timeline,
// but that's probably an early optimisation.
this.forceUpdate();
},
};
onEventReplaced: function(replacedEvent, room) {
onEventReplaced = (replacedEvent, room) => {
if (this.unmounted) return;
// ignore events for other rooms
@ -571,27 +566,27 @@ const TimelinePanel = createReactClass({
// we could skip an update if the event isn't in our timeline,
// but that's probably an early optimisation.
this.forceUpdate();
},
};
onRoomReceipt: function(ev, room) {
onRoomReceipt = (ev, room) => {
if (this.unmounted) return;
// ignore events for other rooms
if (room !== this.props.timelineSet.room) return;
this.forceUpdate();
},
};
onLocalEchoUpdated: function(ev, room, oldEventId) {
onLocalEchoUpdated = (ev, room, oldEventId) => {
if (this.unmounted) return;
// ignore events for other rooms
if (room !== this.props.timelineSet.room) return;
this._reloadEvents();
},
};
onAccountData: function(ev, room) {
onAccountData = (ev, room) => {
if (this.unmounted) return;
// ignore events for other rooms
@ -605,9 +600,9 @@ const TimelinePanel = createReactClass({
this.setState({
readMarkerEventId: ev.getContent().event_id,
}, this.props.onReadMarkerUpdated);
},
};
onEventDecrypted: function(ev) {
onEventDecrypted = ev => {
// Can be null for the notification timeline, etc.
if (!this.props.timelineSet.room) return;
@ -620,19 +615,19 @@ const TimelinePanel = createReactClass({
if (ev.getRoomId() === this.props.timelineSet.room.roomId) {
this.forceUpdate();
}
},
};
onSync: function(state, prevState, data) {
onSync = (state, prevState, data) => {
this.setState({clientSyncState: state});
},
};
_readMarkerTimeout(readMarkerPosition) {
return readMarkerPosition === 0 ?
this.state.readMarkerInViewThresholdMs :
this.state.readMarkerOutOfViewThresholdMs;
},
}
updateReadMarkerOnUserActivity: async function() {
async updateReadMarkerOnUserActivity() {
const initialTimeout = this._readMarkerTimeout(this.getReadMarkerPosition());
this._readMarkerActivityTimer = new Timer(initialTimeout);
@ -644,9 +639,9 @@ const TimelinePanel = createReactClass({
// outside of try/catch to not swallow errors
this.updateReadMarker();
}
},
}
updateReadReceiptOnUserActivity: async function() {
async updateReadReceiptOnUserActivity() {
this._readReceiptActivityTimer = new Timer(READ_RECEIPT_INTERVAL_MS);
while (this._readReceiptActivityTimer) { //unset on unmount
UserActivity.sharedInstance().timeWhileActiveNow(this._readReceiptActivityTimer);
@ -656,9 +651,9 @@ const TimelinePanel = createReactClass({
// outside of try/catch to not swallow errors
this.sendReadReceipt();
}
},
}
sendReadReceipt: function() {
sendReadReceipt = () => {
if (SettingsStore.getValue("lowBandwidth")) return;
if (!this._messagePanel.current) return;
@ -766,11 +761,11 @@ const TimelinePanel = createReactClass({
});
}
}
},
};
// if the read marker is on the screen, we can now assume we've caught up to the end
// of the screen, so move the marker down to the bottom of the screen.
updateReadMarker: function() {
updateReadMarker = () => {
if (!this.props.manageReadMarkers) return;
if (this.getReadMarkerPosition() === 1) {
// the read marker is at an event below the viewport,
@ -801,11 +796,11 @@ const TimelinePanel = createReactClass({
// Send the updated read marker (along with read receipt) to the server
this.sendReadReceipt();
},
};
// advance the read marker past any events we sent ourselves.
_advanceReadMarkerPastMyEvents: function() {
_advanceReadMarkerPastMyEvents() {
if (!this.props.manageReadMarkers) return;
// we call `_timelineWindow.getEvents()` rather than using
@ -837,11 +832,11 @@ const TimelinePanel = createReactClass({
const ev = events[i];
this._setReadMarker(ev.getId(), ev.getTs());
},
}
/* jump down to the bottom of this room, where new events are arriving
*/
jumpToLiveTimeline: function() {
jumpToLiveTimeline = () => {
// if we can't forward-paginate the existing timeline, then there
// is no point reloading it - just jump straight to the bottom.
//
@ -854,12 +849,12 @@ const TimelinePanel = createReactClass({
this._messagePanel.current.scrollToBottom();
}
}
},
};
/* scroll to show the read-up-to marker. We put it 1/3 of the way down
* the container.
*/
jumpToReadMarker: function() {
jumpToReadMarker = () => {
if (!this.props.manageReadMarkers) return;
if (!this._messagePanel.current) return;
if (!this.state.readMarkerEventId) return;
@ -883,11 +878,11 @@ const TimelinePanel = createReactClass({
// As with jumpToLiveTimeline, we want to reload the timeline around the
// read-marker.
this._loadTimeline(this.state.readMarkerEventId, 0, 1/3);
},
};
/* update the read-up-to marker to match the read receipt
*/
forgetReadMarker: function() {
forgetReadMarker = () => {
if (!this.props.manageReadMarkers) return;
const rmId = this._getCurrentReadReceipt();
@ -903,17 +898,17 @@ const TimelinePanel = createReactClass({
}
this._setReadMarker(rmId, rmTs);
},
};
/* return true if the content is fully scrolled down and we are
* at the end of the live timeline.
*/
isAtEndOfLiveTimeline: function() {
isAtEndOfLiveTimeline = () => {
return this._messagePanel.current
&& this._messagePanel.current.isAtBottom()
&& this._timelineWindow
&& !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
},
}
/* get the current scroll state. See ScrollPanel.getScrollState for
@ -921,10 +916,10 @@ const TimelinePanel = createReactClass({
*
* returns null if we are not mounted.
*/
getScrollState: function() {
getScrollState = () => {
if (!this._messagePanel.current) { return null; }
return this._messagePanel.current.getScrollState();
},
};
// returns one of:
//
@ -932,7 +927,7 @@ const TimelinePanel = createReactClass({
// -1: read marker is above the window
// 0: read marker is visible
// +1: read marker is below the window
getReadMarkerPosition: function() {
getReadMarkerPosition = () => {
if (!this.props.manageReadMarkers) return null;
if (!this._messagePanel.current) return null;
@ -953,9 +948,9 @@ const TimelinePanel = createReactClass({
}
return null;
},
};
canJumpToReadMarker: function() {
canJumpToReadMarker = () => {
// 1. Do not show jump bar if neither the RM nor the RR are set.
// 3. We want to show the bar if the read-marker is off the top of the screen.
// 4. Also, if pos === null, the event might not be paginated - show the unread bar
@ -963,14 +958,14 @@ const TimelinePanel = createReactClass({
const ret = this.state.readMarkerEventId !== null && // 1.
(pos < 0 || pos === null); // 3., 4.
return ret;
},
};
/*
* called by the parent component when PageUp/Down/etc is pressed.
*
* We pass it down to the scroll panel.
*/
handleScrollKey: function(ev) {
handleScrollKey = ev => {
if (!this._messagePanel.current) { return; }
// jump to the live timeline on ctrl-end, rather than the end of the
@ -980,9 +975,9 @@ const TimelinePanel = createReactClass({
} else {
this._messagePanel.current.handleScrollKey(ev);
}
},
};
_initTimeline: function(props) {
_initTimeline(props) {
const initialEvent = props.eventId;
const pixelOffset = props.eventPixelOffset;
@ -994,7 +989,7 @@ const TimelinePanel = createReactClass({
}
return this._loadTimeline(initialEvent, pixelOffset, offsetBase);
},
}
/**
* (re)-load the event timeline, and initialise the scroll state, centered
@ -1012,7 +1007,7 @@ const TimelinePanel = createReactClass({
*
* returns a promise which will resolve when the load completes.
*/
_loadTimeline: function(eventId, pixelOffset, offsetBase) {
_loadTimeline(eventId, pixelOffset, offsetBase) {
this._timelineWindow = new Matrix.TimelineWindow(
MatrixClientPeg.get(), this.props.timelineSet,
{windowLimit: this.props.timelineCap});
@ -1122,21 +1117,21 @@ const TimelinePanel = createReactClass({
});
prom.then(onLoaded, onError);
}
},
}
// handle the completion of a timeline load or localEchoUpdate, by
// reloading the events from the timelinewindow and pending event list into
// the state.
_reloadEvents: function() {
_reloadEvents() {
// we might have switched rooms since the load started - just bin
// the results if so.
if (this.unmounted) return;
this.setState(this._getEvents());
},
}
// get the list of events from the timeline window and the pending event list
_getEvents: function() {
_getEvents() {
const events = this._timelineWindow.getEvents();
const firstVisibleEventIndex = this._checkForPreJoinUISI(events);
@ -1154,7 +1149,7 @@ const TimelinePanel = createReactClass({
liveEvents,
firstVisibleEventIndex,
};
},
}
/**
* Check for undecryptable messages that were sent while the user was not in
@ -1166,7 +1161,7 @@ const TimelinePanel = createReactClass({
* undecryptable event that was sent while the user was not in the room. If no
* such events were found, then it returns 0.
*/
_checkForPreJoinUISI: function(events) {
_checkForPreJoinUISI(events) {
const room = this.props.timelineSet.room;
if (events.length === 0 || !room ||
@ -1228,18 +1223,18 @@ const TimelinePanel = createReactClass({
}
}
return 0;
},
}
_indexForEventId: function(evId) {
_indexForEventId(evId) {
for (let i = 0; i < this.state.events.length; ++i) {
if (evId == this.state.events[i].getId()) {
return i;
}
}
return null;
},
}
_getLastDisplayedEventIndex: function(opts) {
_getLastDisplayedEventIndex(opts) {
opts = opts || {};
const ignoreOwn = opts.ignoreOwn || false;
const allowPartial = opts.allowPartial || false;
@ -1313,7 +1308,7 @@ const TimelinePanel = createReactClass({
}
return null;
},
}
/**
* Get the id of the event corresponding to our user's latest read-receipt.
@ -1324,7 +1319,7 @@ const TimelinePanel = createReactClass({
* SDK.
* @return {String} the event ID
*/
_getCurrentReadReceipt: function(ignoreSynthesized) {
_getCurrentReadReceipt(ignoreSynthesized) {
const client = MatrixClientPeg.get();
// the client can be null on logout
if (client == null) {
@ -1333,9 +1328,9 @@ const TimelinePanel = createReactClass({
const myUserId = client.credentials.userId;
return this.props.timelineSet.room.getEventReadUpTo(myUserId, ignoreSynthesized);
},
}
_setReadMarker: function(eventId, eventTs, inhibitSetState) {
_setReadMarker(eventId, eventTs, inhibitSetState) {
const roomId = this.props.timelineSet.room.roomId;
// don't update the state (and cause a re-render) if there is
@ -1358,9 +1353,9 @@ const TimelinePanel = createReactClass({
this.setState({
readMarkerEventId: eventId,
}, this.props.onReadMarkerUpdated);
},
}
_shouldPaginate: function() {
_shouldPaginate() {
// don't try to paginate while events in the timeline are
// still being decrypted. We don't render events while they're
// being decrypted, so they don't take up space in the timeline.
@ -1369,13 +1364,11 @@ const TimelinePanel = createReactClass({
return !this.state.events.some((e) => {
return e.isBeingDecrypted();
});
},
}
getRelationsForEvent(...args) {
return this.props.timelineSet.getRelationsForEvent(...args);
},
getRelationsForEvent = (...args) => this.props.timelineSet.getRelationsForEvent(...args);
render: function() {
render() {
const MessagePanel = sdk.getComponent("structures.MessagePanel");
const Loader = sdk.getComponent("elements.Spinner");
@ -1454,9 +1447,10 @@ const TimelinePanel = createReactClass({
editState={this.state.editState}
showReactions={this.props.showReactions}
useIRCLayout={this.props.useIRCLayout}
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
/>
);
},
});
}
}
export default TimelinePanel;

View file

@ -16,30 +16,28 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import ContentMessages from '../../ContentMessages';
import dis from "../../dispatcher/dispatcher";
import filesize from "filesize";
import { _t } from '../../languageHandler';
export default createReactClass({
displayName: 'UploadBar',
propTypes: {
export default class UploadBar extends React.Component {
static propTypes = {
room: PropTypes.object,
},
};
componentDidMount: function() {
componentDidMount() {
this.dispatcherRef = dis.register(this.onAction);
this.mounted = true;
},
}
componentWillUnmount: function() {
componentWillUnmount() {
this.mounted = false;
dis.unregister(this.dispatcherRef);
},
}
onAction: function(payload) {
onAction = payload => {
switch (payload.action) {
case 'upload_progress':
case 'upload_finished':
@ -48,9 +46,9 @@ export default createReactClass({
if (this.mounted) this.forceUpdate();
break;
}
},
};
render: function() {
render() {
const uploads = ContentMessages.sharedInstance().getCurrentUploads();
// for testing UI... - also fix up the ContentMessages.getCurrentUploads().length
@ -105,5 +103,5 @@ export default createReactClass({
<div className="mx_UploadBar_uploadFilename">{ uploadText }</div>
</div>
);
},
});
}
}

View file

@ -40,8 +40,17 @@ import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import { SettingLevel } from "../../settings/SettingLevel";
import IconizedContextMenu, {
IconizedContextMenuOption,
IconizedContextMenuOptionList
IconizedContextMenuOptionList,
} from "../views/context_menus/IconizedContextMenu";
import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
import * as fbEmitter from "fbemitter";
import TagOrderStore from "../../stores/TagOrderStore";
import { showCommunityInviteDialog } from "../../RoomInvite";
import dis from "../../dispatcher/dispatcher";
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
import ErrorDialog from "../views/dialogs/ErrorDialog";
import EditCommunityPrototypeDialog from "../views/dialogs/EditCommunityPrototypeDialog";
import {UIFeature} from "../../settings/UIFeature";
interface IProps {
isMinimized: boolean;
@ -58,6 +67,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
private dispatcherRef: string;
private themeWatcherRef: string;
private buttonRef: React.RefObject<HTMLButtonElement> = createRef();
private tagStoreRef: fbEmitter.EventSubscription;
constructor(props: IProps) {
super(props);
@ -77,14 +87,20 @@ export default class UserMenu extends React.Component<IProps, IState> {
public componentDidMount() {
this.dispatcherRef = defaultDispatcher.register(this.onAction);
this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged);
this.tagStoreRef = TagOrderStore.addListener(this.onTagStoreUpdate);
}
public componentWillUnmount() {
if (this.themeWatcherRef) SettingsStore.unwatchSetting(this.themeWatcherRef);
if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef);
OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
this.tagStoreRef.remove();
}
private onTagStoreUpdate = () => {
this.forceUpdate(); // we don't have anything useful in state to update
};
private isUserOnDarkTheme(): boolean {
const theme = SettingsStore.getValue("theme");
if (theme.startsWith("custom-")) {
@ -189,9 +205,54 @@ export default class UserMenu extends React.Component<IProps, IState> {
defaultDispatcher.dispatch({action: 'view_home_page'});
};
private onCommunitySettingsClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
Modal.createTrackedDialog('Edit Community', '', EditCommunityPrototypeDialog, {
communityId: CommunityPrototypeStore.instance.getSelectedCommunityId(),
});
this.setState({contextMenuPosition: null}); // also close the menu
};
private onCommunityMembersClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
// We'd ideally just pop open a right panel with the member list, but the current
// way the right panel is structured makes this exceedingly difficult. Instead, we'll
// switch to the general room and open the member list there as it should be in sync
// anyways.
const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat();
if (chat) {
dis.dispatch({
action: 'view_room',
room_id: chat.roomId,
}, true);
dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.RoomMemberList});
} else {
// "This should never happen" clauses go here for the prototype.
Modal.createTrackedDialog('Failed to find general chat', '', ErrorDialog, {
title: _t('Failed to find the general chat for this community'),
description: _t("Failed to find the general chat for this community"),
});
}
this.setState({contextMenuPosition: null}); // also close the menu
};
private onCommunityInviteClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId());
this.setState({contextMenuPosition: null}); // also close the menu
};
private renderContextMenu = (): React.ReactNode => {
if (!this.state.contextMenuPosition) return null;
const prototypeCommunityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
let hostingLink;
const signupLink = getHostingLink("user-context-menu");
if (signupLink) {
@ -225,22 +286,138 @@ export default class UserMenu extends React.Component<IProps, IState> {
);
}
let feedbackButton;
if (SettingsStore.getValue(UIFeature.Feedback)) {
feedbackButton = <IconizedContextMenuOption
iconClassName="mx_UserMenu_iconMessage"
label={_t("Feedback")}
onClick={this.onProvideFeedback}
/>;
}
let primaryHeader = (
<div className="mx_UserMenu_contextMenu_name">
<span className="mx_UserMenu_contextMenu_displayName">
{OwnProfileStore.instance.displayName}
</span>
<span className="mx_UserMenu_contextMenu_userId">
{MatrixClientPeg.get().getUserId()}
</span>
</div>
);
let primaryOptionList = (
<React.Fragment>
<IconizedContextMenuOptionList>
{homeButton}
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconBell"
label={_t("Notification settings")}
onClick={(e) => this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}
/>
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconLock"
label={_t("Security & privacy")}
onClick={(e) => this.onSettingsOpen(e, USER_SECURITY_TAB)}
/>
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconSettings"
label={_t("All settings")}
onClick={(e) => this.onSettingsOpen(e, null)}
/>
{/* <IconizedContextMenuOption
iconClassName="mx_UserMenu_iconArchive"
label={_t("Archived rooms")}
onClick={this.onShowArchived}
/> */}
{ feedbackButton }
</IconizedContextMenuOptionList>
<IconizedContextMenuOptionList red>
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconSignOut"
label={_t("Sign out")}
onClick={this.onSignOutClick}
/>
</IconizedContextMenuOptionList>
</React.Fragment>
);
let secondarySection = null;
if (prototypeCommunityName) {
primaryHeader = (
<div className="mx_UserMenu_contextMenu_name">
<span className="mx_UserMenu_contextMenu_displayName">
{prototypeCommunityName}
</span>
</div>
);
primaryOptionList = (
<IconizedContextMenuOptionList>
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconSettings"
label={_t("Settings")}
aria-label={_t("Community settings")}
onClick={this.onCommunitySettingsClick}
/>
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconMembers"
label={_t("Members")}
onClick={this.onCommunityMembersClick}
/>
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconInvite"
label={_t("Invite")}
onClick={this.onCommunityInviteClick}
/>
</IconizedContextMenuOptionList>
);
secondarySection = (
<React.Fragment>
<hr />
<div className="mx_UserMenu_contextMenu_header">
<div className="mx_UserMenu_contextMenu_name">
<span className="mx_UserMenu_contextMenu_displayName">
{OwnProfileStore.instance.displayName}
</span>
<span className="mx_UserMenu_contextMenu_userId">
{MatrixClientPeg.get().getUserId()}
</span>
</div>
</div>
<IconizedContextMenuOptionList>
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconSettings"
label={_t("Settings")}
aria-label={_t("User settings")}
onClick={(e) => this.onSettingsOpen(e, null)}
/>
{ feedbackButton }
</IconizedContextMenuOptionList>
<IconizedContextMenuOptionList red>
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconSignOut"
label={_t("Sign out")}
onClick={this.onSignOutClick}
/>
</IconizedContextMenuOptionList>
</React.Fragment>
)
}
const classes = classNames({
"mx_UserMenu_contextMenu": true,
"mx_UserMenu_contextMenu_prototype": !!prototypeCommunityName,
});
return <IconizedContextMenu
// -20 to overlap the context menu by just over the width of the `...` icon and make it look connected
left={this.state.contextMenuPosition.width + this.state.contextMenuPosition.left - 20}
top={this.state.contextMenuPosition.top + this.state.contextMenuPosition.height}
// numerical adjustments to overlap the context menu by just over the width of the
// menu icon and make it look connected
left={this.state.contextMenuPosition.width + this.state.contextMenuPosition.left - 10}
top={this.state.contextMenuPosition.top + this.state.contextMenuPosition.height + 8}
onFinished={this.onCloseMenu}
className="mx_UserMenu_contextMenu"
className={classes}
>
<div className="mx_UserMenu_contextMenu_header">
<div className="mx_UserMenu_contextMenu_name">
<span className="mx_UserMenu_contextMenu_displayName">
{OwnProfileStore.instance.displayName}
</span>
<span className="mx_UserMenu_contextMenu_userId">
{MatrixClientPeg.get().getUserId()}
</span>
</div>
{primaryHeader}
<AccessibleTooltipButton
className="mx_UserMenu_contextMenu_themeButton"
onClick={this.onSwitchThemeClick}
@ -254,53 +431,45 @@ export default class UserMenu extends React.Component<IProps, IState> {
</AccessibleTooltipButton>
</div>
{hostingLink}
<IconizedContextMenuOptionList>
{homeButton}
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconBell"
label={_t("Notification settings")}
onClick={(e) => this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}
/>
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconLock"
label={_t("Security & privacy")}
onClick={(e) => this.onSettingsOpen(e, USER_SECURITY_TAB)}
/>
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconSettings"
label={_t("All settings")}
onClick={(e) => this.onSettingsOpen(e, null)}
/>
{/* <IconizedContextMenuOption
iconClassName="mx_UserMenu_iconArchive"
label={_t("Archived rooms")}
onClick={this.onShowArchived}
/> */}
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconMessage"
label={_t("Feedback")}
onClick={this.onProvideFeedback}
/>
</IconizedContextMenuOptionList>
<IconizedContextMenuOptionList red>
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconSignOut"
label={_t("Sign out")}
onClick={this.onSignOutClick}
/>
</IconizedContextMenuOptionList>
{primaryOptionList}
{secondarySection}
</IconizedContextMenu>;
};
public render() {
const avatarSize = 32; // should match border-radius of the avatar
let name = <span className="mx_UserMenu_userName">{OwnProfileStore.instance.displayName}</span>;
const displayName = OwnProfileStore.instance.displayName || MatrixClientPeg.get().getUserId();
const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize);
const prototypeCommunityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
let isPrototype = false;
let menuName = _t("User menu");
let name = <span className="mx_UserMenu_userName">{displayName}</span>;
let buttons = (
<span className="mx_UserMenu_headerButtons">
{/* masked image in CSS */}
</span>
);
if (prototypeCommunityName) {
name = (
<div className="mx_UserMenu_doubleName">
<span className="mx_UserMenu_userName">{prototypeCommunityName}</span>
<span className="mx_UserMenu_subUserName">{displayName}</span>
</div>
);
menuName = _t("Community and user menu");
isPrototype = true;
} else if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
name = (
<div className="mx_UserMenu_doubleName">
<span className="mx_UserMenu_userName">{_t("Home")}</span>
<span className="mx_UserMenu_subUserName">{displayName}</span>
</div>
);
isPrototype = true;
}
if (this.props.isMinimized) {
name = null;
buttons = null;
@ -309,6 +478,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
const classes = classNames({
'mx_UserMenu': true,
'mx_UserMenu_minimized': this.props.isMinimized,
'mx_UserMenu_prototype': isPrototype,
});
return (
@ -317,16 +487,16 @@ export default class UserMenu extends React.Component<IProps, IState> {
className={classes}
onClick={this.onOpenMenuClick}
inputRef={this.buttonRef}
label={_t("User menu")}
label={menuName}
isExpanded={!!this.state.contextMenuPosition}
onContextMenu={this.onContextMenu}
>
<div className="mx_UserMenu_row">
<span className="mx_UserMenu_userAvatarContainer">
<BaseAvatar
idName={MatrixClientPeg.get().getUserId()}
name={OwnProfileStore.instance.displayName || MatrixClientPeg.get().getUserId()}
url={OwnProfileStore.instance.getHttpAvatarUrl(avatarSize)}
idName={displayName}
name={displayName}
url={avatarUrl}
width={avatarSize}
height={avatarSize}
resizeMethod="crop"

View file

@ -80,7 +80,9 @@ export default class UserView extends React.Component {
const RightPanel = sdk.getComponent('structures.RightPanel');
const MainSplit = sdk.getComponent('structures.MainSplit');
const panel = <RightPanel user={this.state.member} />;
return (<MainSplit panel={panel}><HomePage /></MainSplit>);
return (<MainSplit panel={panel} resizeNotifier={this.props.resizeNotifier}>
<HomePage />
</MainSplit>);
} else {
return (<div />);
}

View file

@ -17,24 +17,21 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import SyntaxHighlight from '../views/elements/SyntaxHighlight';
import {_t} from "../../languageHandler";
import * as sdk from "../../index";
export default createReactClass({
displayName: 'ViewSource',
propTypes: {
export default class ViewSource extends React.Component {
static propTypes = {
content: PropTypes.object.isRequired,
onFinished: PropTypes.func.isRequired,
roomId: PropTypes.string.isRequired,
eventId: PropTypes.string.isRequired,
},
};
render: function() {
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<BaseDialog className="mx_ViewSource" onFinished={this.props.onFinished} title={_t('View Source')}>
@ -49,5 +46,5 @@ export default createReactClass({
</div>
</BaseDialog>
);
},
});
}
}

View file

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

View file

@ -17,7 +17,6 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
@ -40,50 +39,47 @@ const PHASE_EMAIL_SENT = 3;
// User has clicked the link in email and completed reset
const PHASE_DONE = 4;
export default createReactClass({
displayName: 'ForgotPassword',
propTypes: {
export default class ForgotPassword extends React.Component {
static propTypes = {
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
onServerConfigChange: PropTypes.func.isRequired,
onLoginClick: PropTypes.func,
onComplete: PropTypes.func.isRequired,
},
};
getInitialState: function() {
return {
phase: PHASE_FORGOT,
email: "",
password: "",
password2: "",
errorText: null,
state = {
phase: PHASE_FORGOT,
email: "",
password: "",
password2: "",
errorText: null,
// We perform liveliness checks later, but for now suppress the errors.
// We also track the server dead errors independently of the regular errors so
// that we can render it differently, and override any other error the user may
// be seeing.
serverIsAlive: true,
serverErrorIsFatal: false,
serverDeadError: "",
serverRequiresIdServer: null,
};
},
// We perform liveliness checks later, but for now suppress the errors.
// We also track the server dead errors independently of the regular errors so
// that we can render it differently, and override any other error the user may
// be seeing.
serverIsAlive: true,
serverErrorIsFatal: false,
serverDeadError: "",
serverRequiresIdServer: null,
};
componentDidMount: function() {
componentDidMount() {
this.reset = null;
this._checkServerLiveliness(this.props.serverConfig);
},
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps: function(newProps) {
// eslint-disable-next-line camelcase
UNSAFE_componentWillReceiveProps(newProps) {
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
// Do a liveliness check on the new URLs
this._checkServerLiveliness(newProps.serverConfig);
},
}
_checkServerLiveliness: async function(serverConfig) {
async _checkServerLiveliness(serverConfig) {
try {
await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(
serverConfig.hsUrl,
@ -100,9 +96,9 @@ export default createReactClass({
} catch (e) {
this.setState(AutoDiscoveryUtils.authComponentStateForError(e, "forgot_password"));
}
},
}
submitPasswordReset: function(email, password) {
submitPasswordReset(email, password) {
this.setState({
phase: PHASE_SENDING_EMAIL,
});
@ -117,9 +113,9 @@ export default createReactClass({
phase: PHASE_FORGOT,
});
});
},
}
onVerify: async function(ev) {
onVerify = async ev => {
ev.preventDefault();
if (!this.reset) {
console.error("onVerify called before submitPasswordReset!");
@ -131,9 +127,9 @@ export default createReactClass({
} catch (err) {
this.showErrorDialog(err.message);
}
},
};
onSubmitForm: async function(ev) {
onSubmitForm = async ev => {
ev.preventDefault();
// refresh the server errors, just in case the server came back online
@ -166,41 +162,41 @@ export default createReactClass({
},
});
}
},
};
onInputChanged: function(stateKey, ev) {
onInputChanged = (stateKey, ev) => {
this.setState({
[stateKey]: ev.target.value,
});
},
};
async onServerDetailsNextPhaseClick() {
onServerDetailsNextPhaseClick = async () => {
this.setState({
phase: PHASE_FORGOT,
});
},
};
onEditServerDetailsClick(ev) {
onEditServerDetailsClick = ev => {
ev.preventDefault();
ev.stopPropagation();
this.setState({
phase: PHASE_SERVER_DETAILS,
});
},
};
onLoginClick: function(ev) {
onLoginClick = ev => {
ev.preventDefault();
ev.stopPropagation();
this.props.onLoginClick();
},
};
showErrorDialog: function(body, title) {
showErrorDialog(body, title) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Forgot Password Error', '', ErrorDialog, {
title: title,
description: body,
});
},
}
renderServerDetails() {
const ServerConfig = sdk.getComponent("auth.ServerConfig");
@ -218,7 +214,7 @@ export default createReactClass({
submitText={_t("Next")}
submitClass="mx_Login_submit"
/>;
},
}
renderForgot() {
const Field = sdk.getComponent('elements.Field');
@ -335,12 +331,12 @@ export default createReactClass({
{_t('Sign in instead')}
</a>
</div>;
},
}
renderSendingEmail() {
const Spinner = sdk.getComponent("elements.Spinner");
return <Spinner />;
},
}
renderEmailSent() {
return <div>
@ -350,7 +346,7 @@ export default createReactClass({
<input className="mx_Login_submit" type="button" onClick={this.onVerify}
value={_t('I have verified my email address')} />
</div>;
},
}
renderDone() {
return <div>
@ -363,9 +359,9 @@ export default createReactClass({
<input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
value={_t('Return to login screen')} />
</div>;
},
}
render: function() {
render() {
const AuthHeader = sdk.getComponent("auth.AuthHeader");
const AuthBody = sdk.getComponent("auth.AuthBody");
@ -397,5 +393,5 @@ export default createReactClass({
</AuthBody>
</AuthPage>
);
},
});
}
}

View file

@ -17,7 +17,6 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import {_t, _td} from '../../../languageHandler';
import * as sdk from '../../../index';
@ -29,6 +28,8 @@ import classNames from "classnames";
import AuthPage from "../../views/auth/AuthPage";
import SSOButton from "../../views/elements/SSOButton";
import PlatformPeg from '../../../PlatformPeg';
import SettingsStore from "../../../settings/SettingsStore";
import {UIFeature} from "../../../settings/UIFeature";
// For validating phone numbers without country codes
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
@ -53,13 +54,11 @@ _td("Invalid base_url for m.identity_server");
_td("Identity server URL does not appear to be a valid identity server");
_td("General failure");
/**
/*
* A wire component which glues together login UI components and Login logic
*/
export default createReactClass({
displayName: 'Login',
propTypes: {
export default class LoginComponent extends React.Component {
static propTypes = {
// Called when the user has logged in. Params:
// - The object returned by the login API
// - The user's password, if applicable, (may be cached in memory for a
@ -85,10 +84,14 @@ export default createReactClass({
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
isSyncing: PropTypes.bool,
},
};
getInitialState: function() {
return {
constructor(props) {
super(props);
this._unmounted = false;
this.state = {
busy: false,
busyLoggingIn: null,
errorText: null,
@ -113,11 +116,6 @@ export default createReactClass({
serverErrorIsFatal: false,
serverDeadError: "",
};
},
// TODO: [REACT-WARNING] Move this to constructor
UNSAFE_componentWillMount: function() {
this._unmounted = false;
// map from login step type to a function which will render a control
// letting you do that login type
@ -128,35 +126,38 @@ export default createReactClass({
'm.login.cas': () => this._renderSsoStep("cas"),
'm.login.sso': () => this._renderSsoStep("sso"),
};
this._initLoginLogic();
},
componentWillUnmount: function() {
this._unmounted = true;
},
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
// eslint-disable-next-line camelcase
UNSAFE_componentWillMount() {
this._initLoginLogic();
}
componentWillUnmount() {
this._unmounted = true;
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
// eslint-disable-next-line camelcase
UNSAFE_componentWillReceiveProps(newProps) {
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
// Ensure that we end up actually logging in to the right place
this._initLoginLogic(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl);
},
}
onPasswordLoginError: function(errorText) {
onPasswordLoginError = errorText => {
this.setState({
errorText,
loginIncorrect: Boolean(errorText),
});
},
};
isBusy: function() {
return this.state.busy || this.props.busy;
},
isBusy = () => this.state.busy || this.props.busy;
onPasswordLogin: async function(username, phoneCountry, phoneNumber, password) {
onPasswordLogin = async (username, phoneCountry, phoneNumber, password) => {
if (!this.state.serverIsAlive) {
this.setState({busy: true});
// Do a quick liveliness check on the URLs
@ -263,13 +264,13 @@ export default createReactClass({
loginIncorrect: error.httpStatus === 401 || error.httpStatus === 403,
});
});
},
};
onUsernameChanged: function(username) {
onUsernameChanged = username => {
this.setState({ username: username });
},
};
onUsernameBlur: async function(username) {
onUsernameBlur = async username => {
const doWellknownLookup = username[0] === "@";
this.setState({
username: username,
@ -314,19 +315,19 @@ export default createReactClass({
});
}
}
},
};
onPhoneCountryChanged: function(phoneCountry) {
onPhoneCountryChanged = phoneCountry => {
this.setState({ phoneCountry: phoneCountry });
},
};
onPhoneNumberChanged: function(phoneNumber) {
onPhoneNumberChanged = phoneNumber => {
this.setState({
phoneNumber: phoneNumber,
});
},
};
onPhoneNumberBlur: function(phoneNumber) {
onPhoneNumberBlur = phoneNumber => {
// Validate the phone number entered
if (!PHONE_NUMBER_REGEX.test(phoneNumber)) {
this.setState({
@ -339,15 +340,15 @@ export default createReactClass({
canTryLogin: true,
});
}
},
};
onRegisterClick: function(ev) {
onRegisterClick = ev => {
ev.preventDefault();
ev.stopPropagation();
this.props.onRegisterClick();
},
};
onTryRegisterClick: function(ev) {
onTryRegisterClick = ev => {
const step = this._getCurrentFlowStep();
if (step === 'm.login.sso' || step === 'm.login.cas') {
// If we're showing SSO it means that registration is also probably disabled,
@ -361,23 +362,23 @@ export default createReactClass({
// Don't intercept - just go through to the register page
this.onRegisterClick(ev);
}
},
};
async onServerDetailsNextPhaseClick() {
onServerDetailsNextPhaseClick = () => {
this.setState({
phase: PHASE_LOGIN,
});
},
};
onEditServerDetailsClick(ev) {
onEditServerDetailsClick = ev => {
ev.preventDefault();
ev.stopPropagation();
this.setState({
phase: PHASE_SERVER_DETAILS,
});
},
};
_initLoginLogic: async function(hsUrl, isUrl) {
async _initLoginLogic(hsUrl, isUrl) {
hsUrl = hsUrl || this.props.serverConfig.hsUrl;
isUrl = isUrl || this.props.serverConfig.isUrl;
@ -465,9 +466,9 @@ export default createReactClass({
busy: false,
});
});
},
}
_isSupportedFlow: function(flow) {
_isSupportedFlow(flow) {
// technically the flow can have multiple steps, but no one does this
// for login and loginLogic doesn't support it so we can ignore it.
if (!this._stepRendererMap[flow.type]) {
@ -475,11 +476,11 @@ export default createReactClass({
return false;
}
return true;
},
}
_getCurrentFlowStep: function() {
_getCurrentFlowStep() {
return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null;
},
}
_errorTextFromError(err) {
let errCode = err.errcode;
@ -526,7 +527,7 @@ export default createReactClass({
}
return errorText;
},
}
renderServerComponent() {
const ServerConfig = sdk.getComponent("auth.ServerConfig");
@ -552,7 +553,7 @@ export default createReactClass({
delayTimeMs={250}
{...serverDetailsProps}
/>;
},
}
renderLoginComponentForStep() {
if (PHASES_ENABLED && this.state.phase !== PHASE_LOGIN) {
@ -572,9 +573,9 @@ export default createReactClass({
}
return null;
},
}
_renderPasswordStep: function() {
_renderPasswordStep = () => {
const PasswordLogin = sdk.getComponent('auth.PasswordLogin');
let onEditServerDetailsClick = null;
@ -603,9 +604,9 @@ export default createReactClass({
busy={this.props.isSyncing || this.state.busyLoggingIn}
/>
);
},
};
_renderSsoStep: function(loginType) {
_renderSsoStep = loginType => {
const SignInToText = sdk.getComponent('views.auth.SignInToText');
let onEditServerDetailsClick = null;
@ -634,9 +635,9 @@ export default createReactClass({
/>
</div>
);
},
};
render: function() {
render() {
const Loader = sdk.getComponent("elements.Spinner");
const InlineSpinner = sdk.getComponent("elements.InlineSpinner");
const AuthHeader = sdk.getComponent("auth.AuthHeader");
@ -680,7 +681,7 @@ export default createReactClass({
{_t("If you've joined lots of rooms, this might take a while")}
</div> }
</div>;
} else {
} else if (SettingsStore.getValue(UIFeature.Registration)) {
footer = (
<a className="mx_AuthBody_changeFlow" onClick={this.onTryRegisterClick} href="#">
{ _t('Create account') }
@ -704,5 +705,5 @@ export default createReactClass({
</AuthBody>
</AuthPage>
);
},
});
}
}

View file

@ -15,29 +15,24 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import AuthPage from "../../views/auth/AuthPage";
export default createReactClass({
displayName: 'PostRegistration',
propTypes: {
export default class PostRegistration extends React.Component {
static propTypes = {
onComplete: PropTypes.func.isRequired,
},
};
getInitialState: function() {
return {
avatarUrl: null,
errorString: null,
busy: false,
};
},
state = {
avatarUrl: null,
errorString: null,
busy: false,
};
componentDidMount: function() {
componentDidMount() {
// There is some assymetry between ChangeDisplayName and ChangeAvatar,
// as ChangeDisplayName will auto-get the name but ChangeAvatar expects
// the URL to be passed to you (because it's also used for room avatars).
@ -55,9 +50,9 @@ export default createReactClass({
busy: false,
});
});
},
}
render: function() {
render() {
const ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName');
const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
const AuthHeader = sdk.getComponent('auth.AuthHeader');
@ -78,5 +73,5 @@ export default createReactClass({
</AuthBody>
</AuthPage>
);
},
});
}
}

View file

@ -19,7 +19,6 @@ limitations under the License.
import Matrix from 'matrix-js-sdk';
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';
@ -43,10 +42,8 @@ const PHASE_REGISTRATION = 1;
// Enable phases for registration
const PHASES_ENABLED = true;
export default createReactClass({
displayName: 'Registration',
propTypes: {
export default class Registration extends React.Component {
static propTypes = {
// Called when the user has logged in. Params:
// - object with userId, deviceId, homeserverUrl, identityServerUrl, accessToken
// - The user's password, if available and applicable (may be cached in memory
@ -65,12 +62,13 @@ export default createReactClass({
onLoginClick: PropTypes.func.isRequired,
onServerConfigChange: PropTypes.func.isRequired,
defaultDeviceDisplayName: PropTypes.string,
},
};
constructor(props) {
super(props);
getInitialState: function() {
const serverType = ServerType.getTypeFromServerConfig(this.props.serverConfig);
return {
this.state = {
busy: false,
errorText: null,
// We remember the values entered by the user because
@ -118,14 +116,15 @@ export default createReactClass({
// this is the user ID that's logged in.
differentLoggedInUserId: null,
};
},
}
componentDidMount: function() {
componentDidMount() {
this._unmounted = false;
this._replaceClient();
},
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
// eslint-disable-next-line camelcase
UNSAFE_componentWillReceiveProps(newProps) {
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
@ -142,7 +141,7 @@ export default createReactClass({
phase: this.getDefaultPhaseForServerType(serverType),
});
}
},
}
getDefaultPhaseForServerType(type) {
switch (type) {
@ -155,9 +154,9 @@ export default createReactClass({
case ServerType.ADVANCED:
return PHASE_SERVER_DETAILS;
}
},
}
onServerTypeChange(type) {
onServerTypeChange = type => {
this.setState({
serverType: type,
});
@ -184,9 +183,9 @@ export default createReactClass({
this.setState({
phase: this.getDefaultPhaseForServerType(type),
});
},
};
_replaceClient: async function(serverConfig) {
async _replaceClient(serverConfig) {
this.setState({
errorText: null,
serverDeadError: null,
@ -286,18 +285,18 @@ export default createReactClass({
showGenericError(e);
}
}
},
}
onFormSubmit: function(formVals) {
onFormSubmit = formVals => {
this.setState({
errorText: "",
busy: true,
formVals: formVals,
doingUIAuth: true,
});
},
};
_requestEmailToken: function(emailAddress, clientSecret, sendAttempt, sessionId) {
_requestEmailToken = (emailAddress, clientSecret, sendAttempt, sessionId) => {
return this.state.matrixClient.requestRegisterEmailToken(
emailAddress,
clientSecret,
@ -309,9 +308,9 @@ export default createReactClass({
session_id: sessionId,
}),
);
},
}
_onUIAuthFinished: async function(success, response, extra) {
_onUIAuthFinished = async (success, response, extra) => {
if (!success) {
let msg = response.message || response.toString();
// can we give a better error message?
@ -395,9 +394,9 @@ export default createReactClass({
}
this.setState(newState);
},
};
_setupPushers: function() {
_setupPushers() {
if (!this.props.brand) {
return Promise.resolve();
}
@ -418,15 +417,15 @@ export default createReactClass({
}, (error) => {
console.error("Couldn't get pushers: " + error);
});
},
}
onLoginClick: function(ev) {
onLoginClick = ev => {
ev.preventDefault();
ev.stopPropagation();
this.props.onLoginClick();
},
};
onGoToFormClicked(ev) {
onGoToFormClicked = ev => {
ev.preventDefault();
ev.stopPropagation();
this._replaceClient();
@ -435,23 +434,23 @@ export default createReactClass({
doingUIAuth: false,
phase: PHASE_REGISTRATION,
});
},
};
async onServerDetailsNextPhaseClick() {
onServerDetailsNextPhaseClick = async () => {
this.setState({
phase: PHASE_REGISTRATION,
});
},
};
onEditServerDetailsClick(ev) {
onEditServerDetailsClick = ev => {
ev.preventDefault();
ev.stopPropagation();
this.setState({
phase: PHASE_SERVER_DETAILS,
});
},
};
_makeRegisterRequest: function(auth) {
_makeRegisterRequest = auth => {
// We inhibit login if we're trying to register with an email address: this
// avoids a lot of complex race conditions that can occur if we try to log
// the user in one one or both of the tabs they might end up with after
@ -471,20 +470,20 @@ export default createReactClass({
if (auth) registerParams.auth = auth;
if (inhibitLogin !== undefined && inhibitLogin !== null) registerParams.inhibit_login = inhibitLogin;
return this.state.matrixClient.registerRequest(registerParams);
},
};
_getUIAuthInputs: function() {
_getUIAuthInputs() {
return {
emailAddress: this.state.formVals.email,
phoneCountry: this.state.formVals.phoneCountry,
phoneNumber: this.state.formVals.phoneNumber,
};
},
}
// Links to the login page shown after registration is completed are routed through this
// which checks the user hasn't already logged in somewhere else (perhaps we should do
// this more generally?)
_onLoginClickWithCheck: async function(ev) {
_onLoginClickWithCheck = async ev => {
ev.preventDefault();
const sessionLoaded = await Lifecycle.loadSession({ignoreGuest: true});
@ -492,7 +491,7 @@ export default createReactClass({
// ok fine, there's still no session: really go to the login page
this.props.onLoginClick();
}
},
};
renderServerComponent() {
const ServerTypeSelector = sdk.getComponent("auth.ServerTypeSelector");
@ -553,7 +552,7 @@ export default createReactClass({
/>
{serverDetails}
</div>;
},
}
renderRegisterComponent() {
if (PHASES_ENABLED && this.state.phase !== PHASE_REGISTRATION) {
@ -608,9 +607,9 @@ export default createReactClass({
serverRequiresIdServer={this.state.serverRequiresIdServer}
/>;
}
},
}
render: function() {
render() {
const AuthHeader = sdk.getComponent('auth.AuthHeader');
const AuthBody = sdk.getComponent("auth.AuthBody");
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
@ -706,5 +705,5 @@ export default createReactClass({
</AuthBody>
</AuthPage>
);
},
});
}
}

View file

@ -18,16 +18,13 @@ limitations under the License.
import { _t } from '../../../languageHandler';
import React from 'react';
import createReactClass from 'create-react-class';
export default createReactClass({
displayName: 'AuthFooter',
render: function() {
export default class AuthFooter extends React.Component {
render() {
return (
<div className="mx_AuthFooter">
<a href="https://matrix.org" target="_blank" rel="noreferrer noopener">{ _t("powered by Matrix") }</a>
</div>
);
},
});
}
}

View file

@ -17,17 +17,14 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import * as sdk from '../../../index';
export default createReactClass({
displayName: 'AuthHeader',
propTypes: {
export default class AuthHeader extends React.Component {
static propTypes = {
disableLanguageSelector: PropTypes.bool,
},
};
render: function() {
render() {
const AuthHeaderLogo = sdk.getComponent('auth.AuthHeaderLogo');
const LanguageSelector = sdk.getComponent('views.auth.LanguageSelector');
@ -37,5 +34,5 @@ export default createReactClass({
<LanguageSelector disabled={this.props.disableLanguageSelector} />
</div>
);
},
});
}
}

View file

@ -15,7 +15,6 @@ limitations under the License.
*/
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
@ -24,36 +23,31 @@ const DIV_ID = 'mx_recaptcha';
/**
* A pure UI component which displays a captcha form.
*/
export default createReactClass({
displayName: 'CaptchaForm',
propTypes: {
export default class CaptchaForm extends React.Component {
static propTypes = {
sitePublicKey: PropTypes.string,
// called with the captcha response
onCaptchaResponse: PropTypes.func,
},
};
getDefaultProps: function() {
return {
onCaptchaResponse: () => {},
};
},
static defaultProps = {
onCaptchaResponse: () => {},
};
getInitialState: function() {
return {
constructor(props) {
super(props);
this.state = {
errorText: null,
};
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._captchaWidgetId = null;
this._recaptchaContainer = createRef();
},
}
componentDidMount: function() {
componentDidMount() {
// Just putting a script tag into the returned jsx doesn't work, annoyingly,
// so we do this instead.
if (global.grecaptcha) {
@ -68,13 +62,13 @@ export default createReactClass({
);
this._recaptchaContainer.current.appendChild(scriptTag);
}
},
}
componentWillUnmount: function() {
componentWillUnmount() {
this._resetRecaptcha();
},
}
_renderRecaptcha: function(divId) {
_renderRecaptcha(divId) {
if (!global.grecaptcha) {
console.error("grecaptcha not loaded!");
throw new Error("Recaptcha did not load successfully");
@ -93,15 +87,15 @@ export default createReactClass({
sitekey: publicKey,
callback: this.props.onCaptchaResponse,
});
},
}
_resetRecaptcha: function() {
_resetRecaptcha() {
if (this._captchaWidgetId !== null) {
global.grecaptcha.reset(this._captchaWidgetId);
}
},
}
_onCaptchaLoaded: function() {
_onCaptchaLoaded() {
console.log("Loaded recaptcha script.");
try {
this._renderRecaptcha(DIV_ID);
@ -110,9 +104,9 @@ export default createReactClass({
errorText: e.toString(),
});
}
},
}
render: function() {
render() {
let error = null;
if (this.state.errorText) {
error = (
@ -131,5 +125,5 @@ export default createReactClass({
{ error }
</div>
);
},
});
}
}

View file

@ -16,14 +16,11 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
export default createReactClass({
displayName: 'CustomServerDialog',
render: function() {
export default class CustomServerDialog extends React.Component {
render() {
const brand = SdkConfig.get().brand;
return (
<div className="mx_ErrorDialog">
@ -46,5 +43,5 @@ export default createReactClass({
</div>
</div>
);
},
});
}
}

View file

@ -17,7 +17,6 @@ limitations under the License.
*/
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import url from 'url';
import classnames from 'classnames';
@ -26,6 +25,7 @@ import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from "../elements/AccessibleButton";
import Spinner from "../elements/Spinner";
/* This file contains a collection of components which are used by the
* InteractiveAuth to prompt the user to enter the information needed
@ -75,14 +75,10 @@ import AccessibleButton from "../elements/AccessibleButton";
export const DEFAULT_PHASE = 0;
export const PasswordAuthEntry = createReactClass({
displayName: 'PasswordAuthEntry',
export class PasswordAuthEntry extends React.Component {
static LOGIN_TYPE = "m.login.password";
statics: {
LOGIN_TYPE: "m.login.password",
},
propTypes: {
static propTypes = {
matrixClient: PropTypes.object.isRequired,
submitAuthDict: PropTypes.func.isRequired,
errorText: PropTypes.string,
@ -90,19 +86,17 @@ export const PasswordAuthEntry = createReactClass({
// happen?
busy: PropTypes.bool,
onPhaseChange: PropTypes.func.isRequired,
},
};
componentDidMount: function() {
componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE);
},
}
getInitialState: function() {
return {
password: "",
};
},
state = {
password: "",
};
_onSubmit: function(e) {
_onSubmit = e => {
e.preventDefault();
if (this.props.busy) return;
@ -117,16 +111,16 @@ export const PasswordAuthEntry = createReactClass({
},
password: this.state.password,
});
},
};
_onPasswordFieldChange: function(ev) {
_onPasswordFieldChange = ev => {
// enable the submit button iff the password is non-empty
this.setState({
password: ev.target.value,
});
},
};
render: function() {
render() {
const passwordBoxClass = classnames({
"error": this.props.errorText,
});
@ -176,36 +170,32 @@ export const PasswordAuthEntry = createReactClass({
{ errorSection }
</div>
);
},
});
}
}
export const RecaptchaAuthEntry = createReactClass({
displayName: 'RecaptchaAuthEntry',
export class RecaptchaAuthEntry extends React.Component {
static LOGIN_TYPE = "m.login.recaptcha";
statics: {
LOGIN_TYPE: "m.login.recaptcha",
},
propTypes: {
static propTypes = {
submitAuthDict: PropTypes.func.isRequired,
stageParams: PropTypes.object.isRequired,
errorText: PropTypes.string,
busy: PropTypes.bool,
onPhaseChange: PropTypes.func.isRequired,
},
};
componentDidMount: function() {
componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE);
},
}
_onCaptchaResponse: function(response) {
_onCaptchaResponse = response => {
this.props.submitAuthDict({
type: RecaptchaAuthEntry.LOGIN_TYPE,
response: response,
});
},
};
render: function() {
render() {
if (this.props.busy) {
const Loader = sdk.getComponent("elements.Spinner");
return <Loader />;
@ -241,31 +231,24 @@ export const RecaptchaAuthEntry = createReactClass({
{ errorSection }
</div>
);
},
});
}
}
export const TermsAuthEntry = createReactClass({
displayName: 'TermsAuthEntry',
export class TermsAuthEntry extends React.Component {
static LOGIN_TYPE = "m.login.terms";
statics: {
LOGIN_TYPE: "m.login.terms",
},
propTypes: {
static propTypes = {
submitAuthDict: PropTypes.func.isRequired,
stageParams: PropTypes.object.isRequired,
errorText: PropTypes.string,
busy: PropTypes.bool,
showContinue: PropTypes.bool,
onPhaseChange: PropTypes.func.isRequired,
},
};
componentDidMount: function() {
this.props.onPhaseChange(DEFAULT_PHASE);
},
constructor(props) {
super(props);
// TODO: [REACT-WARNING] Move this to constructor
componentWillMount: function() {
// example stageParams:
//
// {
@ -310,17 +293,22 @@ export const TermsAuthEntry = createReactClass({
pickedPolicies.push(langPolicy);
}
this.setState({
"toggledPolicies": initToggles,
"policies": pickedPolicies,
});
},
this.state = {
toggledPolicies: initToggles,
policies: pickedPolicies,
};
}
tryContinue: function() {
componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE);
}
tryContinue = () => {
this._trySubmit();
},
};
_togglePolicy: function(policyId) {
_togglePolicy(policyId) {
const newToggles = {};
for (const policy of this.state.policies) {
let checked = this.state.toggledPolicies[policy.id];
@ -329,9 +317,9 @@ export const TermsAuthEntry = createReactClass({
newToggles[policy.id] = checked;
}
this.setState({"toggledPolicies": newToggles});
},
}
_trySubmit: function() {
_trySubmit = () => {
let allChecked = true;
for (const policy of this.state.policies) {
const checked = this.state.toggledPolicies[policy.id];
@ -340,9 +328,9 @@ export const TermsAuthEntry = createReactClass({
if (allChecked) this.props.submitAuthDict({type: TermsAuthEntry.LOGIN_TYPE});
else this.setState({errorText: _t("Please review and accept all of the homeserver's policies")});
},
};
render: function() {
render() {
if (this.props.busy) {
const Loader = sdk.getComponent("elements.Spinner");
return <Loader />;
@ -387,17 +375,13 @@ export const TermsAuthEntry = createReactClass({
{ submitButton }
</div>
);
},
});
}
}
export const EmailIdentityAuthEntry = createReactClass({
displayName: 'EmailIdentityAuthEntry',
export class EmailIdentityAuthEntry extends React.Component {
static LOGIN_TYPE = "m.login.email.identity";
statics: {
LOGIN_TYPE: "m.login.email.identity",
},
propTypes: {
static propTypes = {
matrixClient: PropTypes.object.isRequired,
submitAuthDict: PropTypes.func.isRequired,
authSessionId: PropTypes.string.isRequired,
@ -407,13 +391,13 @@ export const EmailIdentityAuthEntry = createReactClass({
fail: PropTypes.func.isRequired,
setEmailSid: PropTypes.func.isRequired,
onPhaseChange: PropTypes.func.isRequired,
},
};
componentDidMount: function() {
componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE);
},
}
render: function() {
render() {
// This component is now only displayed once the token has been requested,
// so we know the email has been sent. It can also get loaded after the user
// has clicked the validation link if the server takes a while to propagate
@ -421,8 +405,12 @@ export const EmailIdentityAuthEntry = createReactClass({
// the validation link, we won't know the email address, so if we don't have it,
// assume that the link has been clicked and the server will realise when we poll.
if (this.props.inputs.emailAddress === undefined) {
const Loader = sdk.getComponent("elements.Spinner");
return <Loader />;
return <Spinner />;
} else if (this.props.stageState?.emailSid) {
// we only have a session ID if the user has clicked the link in their email,
// so show a loading state instead of "an email has been sent to..." because
// that's confusing when you've already read that email.
return <Spinner />;
} else {
return (
<div>
@ -434,17 +422,13 @@ export const EmailIdentityAuthEntry = createReactClass({
</div>
);
}
},
});
}
}
export const MsisdnAuthEntry = createReactClass({
displayName: 'MsisdnAuthEntry',
export class MsisdnAuthEntry extends React.Component {
static LOGIN_TYPE = "m.login.msisdn";
statics: {
LOGIN_TYPE: "m.login.msisdn",
},
propTypes: {
static propTypes = {
inputs: PropTypes.shape({
phoneCountry: PropTypes.string,
phoneNumber: PropTypes.string,
@ -454,16 +438,14 @@ export const MsisdnAuthEntry = createReactClass({
submitAuthDict: PropTypes.func.isRequired,
matrixClient: PropTypes.object,
onPhaseChange: PropTypes.func.isRequired,
},
};
getInitialState: function() {
return {
token: '',
requestingToken: false,
};
},
state = {
token: '',
requestingToken: false,
};
componentDidMount: function() {
componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE);
this._submitUrl = null;
@ -477,12 +459,12 @@ export const MsisdnAuthEntry = createReactClass({
}).finally(() => {
this.setState({requestingToken: false});
});
},
}
/*
* Requests a verification token by SMS.
*/
_requestMsisdnToken: function() {
_requestMsisdnToken() {
return this.props.matrixClient.requestRegisterMsisdnToken(
this.props.inputs.phoneCountry,
this.props.inputs.phoneNumber,
@ -493,15 +475,15 @@ export const MsisdnAuthEntry = createReactClass({
this._sid = result.sid;
this._msisdn = result.msisdn;
});
},
}
_onTokenChange: function(e) {
_onTokenChange = e => {
this.setState({
token: e.target.value,
});
},
};
_onFormSubmit: async function(e) {
_onFormSubmit = async e => {
e.preventDefault();
if (this.state.token == '') return;
@ -552,9 +534,9 @@ export const MsisdnAuthEntry = createReactClass({
this.props.fail(e);
console.log("Failed to submit msisdn token");
}
},
};
render: function() {
render() {
if (this.state.requestingToken) {
const Loader = sdk.getComponent("elements.Spinner");
return <Loader />;
@ -598,8 +580,8 @@ export const MsisdnAuthEntry = createReactClass({
</div>
);
}
},
});
}
}
export class SSOAuthEntry extends React.Component {
static propTypes = {
@ -686,46 +668,46 @@ export class SSOAuthEntry extends React.Component {
}
}
export const FallbackAuthEntry = createReactClass({
displayName: 'FallbackAuthEntry',
propTypes: {
export class FallbackAuthEntry extends React.Component {
static propTypes = {
matrixClient: PropTypes.object.isRequired,
authSessionId: PropTypes.string.isRequired,
loginType: PropTypes.string.isRequired,
submitAuthDict: PropTypes.func.isRequired,
errorText: PropTypes.string,
onPhaseChange: PropTypes.func.isRequired,
},
};
componentDidMount: function() {
this.props.onPhaseChange(DEFAULT_PHASE);
},
constructor(props) {
super(props);
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
// we have to make the user click a button, as browsers will block
// the popup if we open it immediately.
this._popupWindow = null;
window.addEventListener("message", this._onReceiveMessage);
this._fallbackButton = createRef();
},
}
componentWillUnmount: function() {
componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE);
}
componentWillUnmount() {
window.removeEventListener("message", this._onReceiveMessage);
if (this._popupWindow) {
this._popupWindow.close();
}
},
}
focus: function() {
focus = () => {
if (this._fallbackButton.current) {
this._fallbackButton.current.focus();
}
},
};
_onShowFallbackClick: function(e) {
_onShowFallbackClick = e => {
e.preventDefault();
e.stopPropagation();
@ -735,18 +717,18 @@ export const FallbackAuthEntry = createReactClass({
);
this._popupWindow = window.open(url);
this._popupWindow.opener = null;
},
};
_onReceiveMessage: function(event) {
_onReceiveMessage = event => {
if (
event.data === "authDone" &&
event.origin === this.props.matrixClient.getHomeserverUrl()
) {
this.props.submitAuthDict({});
}
},
};
render: function() {
render() {
let errorSection;
if (this.props.errorText) {
errorSection = (
@ -761,8 +743,8 @@ export const FallbackAuthEntry = createReactClass({
{errorSection}
</div>
);
},
});
}
}
const AuthEntryComponents = [
PasswordAuthEntry,

View file

@ -18,7 +18,6 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import * as Email from '../../../email';
@ -39,13 +38,11 @@ const FIELD_PASSWORD_CONFIRM = 'field_password_confirm';
const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from offline slow-hash scenario.
/**
/*
* A pure UI component which displays a registration form.
*/
export default createReactClass({
displayName: 'RegistrationForm',
propTypes: {
export default class RegistrationForm extends React.Component {
static propTypes = {
// Values pre-filled in the input boxes when the component loads
defaultEmail: PropTypes.string,
defaultPhoneCountry: PropTypes.string,
@ -58,17 +55,17 @@ export default createReactClass({
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
canSubmit: PropTypes.bool,
serverRequiresIdServer: PropTypes.bool,
},
};
getDefaultProps: function() {
return {
onValidationChange: console.error,
canSubmit: true,
};
},
static defaultProps = {
onValidationChange: console.error,
canSubmit: true,
};
getInitialState: function() {
return {
constructor(props) {
super(props);
this.state = {
// Field error codes by field ID
fieldValid: {},
// The ISO2 country code selected in the phone number entry
@ -80,9 +77,9 @@ export default createReactClass({
passwordConfirm: this.props.defaultPassword || "",
passwordComplexity: null,
};
},
}
onSubmit: async function(ev) {
onSubmit = async ev => {
ev.preventDefault();
if (!this.props.canSubmit) return;
@ -118,7 +115,7 @@ export default createReactClass({
title: _t("Warning!"),
description: desc,
button: _t("Continue"),
onFinished: function(confirmed) {
onFinished(confirmed) {
if (confirmed) {
self._doSubmit(ev);
}
@ -127,9 +124,9 @@ export default createReactClass({
} else {
self._doSubmit(ev);
}
},
};
_doSubmit: function(ev) {
_doSubmit(ev) {
const email = this.state.email.trim();
const promise = this.props.onRegisterClick({
username: this.state.username.trim(),
@ -145,7 +142,7 @@ export default createReactClass({
ev.target.disabled = false;
});
}
},
}
async verifyFieldsBeforeSubmit() {
// Blur the active element if any, so we first run its blur validation,
@ -196,12 +193,12 @@ export default createReactClass({
invalidField.focus();
invalidField.validate({ allowEmpty: false, focused: true });
return false;
},
}
/**
* @returns {boolean} true if all fields were valid last time they were validated.
*/
allFieldsValid: function() {
allFieldsValid() {
const keys = Object.keys(this.state.fieldValid);
for (let i = 0; i < keys.length; ++i) {
if (!this.state.fieldValid[keys[i]]) {
@ -209,7 +206,7 @@ export default createReactClass({
}
}
return true;
},
}
findFirstInvalidField(fieldIDs) {
for (const fieldID of fieldIDs) {
@ -218,34 +215,34 @@ export default createReactClass({
}
}
return null;
},
}
markFieldValid: function(fieldID, valid) {
markFieldValid(fieldID, valid) {
const { fieldValid } = this.state;
fieldValid[fieldID] = valid;
this.setState({
fieldValid,
});
},
}
onEmailChange(ev) {
onEmailChange = ev => {
this.setState({
email: ev.target.value,
});
},
};
async onEmailValidate(fieldState) {
onEmailValidate = async fieldState => {
const result = await this.validateEmailRules(fieldState);
this.markFieldValid(FIELD_EMAIL, result.valid);
return result;
},
};
validateEmailRules: withValidation({
validateEmailRules = withValidation({
description: () => _t("Use an email address to recover your account"),
rules: [
{
key: "required",
test: function({ value, allowEmpty }) {
test({ value, allowEmpty }) {
return allowEmpty || !this._authStepIsRequired('m.login.email.identity') || !!value;
},
invalid: () => _t("Enter email address (required on this homeserver)"),
@ -256,31 +253,31 @@ export default createReactClass({
invalid: () => _t("Doesn't look like a valid email address"),
},
],
}),
});
onPasswordChange(ev) {
onPasswordChange = ev => {
this.setState({
password: ev.target.value,
});
},
};
onPasswordValidate(result) {
onPasswordValidate = result => {
this.markFieldValid(FIELD_PASSWORD, result.valid);
},
};
onPasswordConfirmChange(ev) {
onPasswordConfirmChange = ev => {
this.setState({
passwordConfirm: ev.target.value,
});
},
};
async onPasswordConfirmValidate(fieldState) {
onPasswordConfirmValidate = async fieldState => {
const result = await this.validatePasswordConfirmRules(fieldState);
this.markFieldValid(FIELD_PASSWORD_CONFIRM, result.valid);
return result;
},
};
validatePasswordConfirmRules: withValidation({
validatePasswordConfirmRules = withValidation({
rules: [
{
key: "required",
@ -289,39 +286,39 @@ export default createReactClass({
},
{
key: "match",
test: function({ value }) {
test({ value }) {
return !value || value === this.state.password;
},
invalid: () => _t("Passwords don't match"),
},
],
}),
});
onPhoneCountryChange(newVal) {
onPhoneCountryChange = newVal => {
this.setState({
phoneCountry: newVal.iso2,
phonePrefix: newVal.prefix,
});
},
};
onPhoneNumberChange(ev) {
onPhoneNumberChange = ev => {
this.setState({
phoneNumber: ev.target.value,
});
},
};
async onPhoneNumberValidate(fieldState) {
onPhoneNumberValidate = async fieldState => {
const result = await this.validatePhoneNumberRules(fieldState);
this.markFieldValid(FIELD_PHONE_NUMBER, result.valid);
return result;
},
};
validatePhoneNumberRules: withValidation({
validatePhoneNumberRules = withValidation({
description: () => _t("Other users can invite you to rooms using your contact details"),
rules: [
{
key: "required",
test: function({ value, allowEmpty }) {
test({ value, allowEmpty }) {
return allowEmpty || !this._authStepIsRequired('m.login.msisdn') || !!value;
},
invalid: () => _t("Enter phone number (required on this homeserver)"),
@ -332,21 +329,21 @@ export default createReactClass({
invalid: () => _t("Doesn't look like a valid phone number"),
},
],
}),
});
onUsernameChange(ev) {
onUsernameChange = ev => {
this.setState({
username: ev.target.value,
});
},
};
async onUsernameValidate(fieldState) {
onUsernameValidate = async fieldState => {
const result = await this.validateUsernameRules(fieldState);
this.markFieldValid(FIELD_USERNAME, result.valid);
return result;
},
};
validateUsernameRules: withValidation({
validateUsernameRules = withValidation({
description: () => _t("Use lowercase letters, numbers, dashes and underscores only"),
rules: [
{
@ -360,7 +357,7 @@ export default createReactClass({
invalid: () => _t("Some characters not allowed"),
},
],
}),
});
/**
* A step is required if all flows include that step.
@ -372,7 +369,7 @@ export default createReactClass({
return this.props.flows.every((flow) => {
return flow.stages.includes(step);
});
},
}
/**
* A step is used if any flows include that step.
@ -384,7 +381,7 @@ export default createReactClass({
return this.props.flows.some((flow) => {
return flow.stages.includes(step);
});
},
}
_showEmail() {
const haveIs = Boolean(this.props.serverConfig.isUrl);
@ -395,7 +392,7 @@ export default createReactClass({
return false;
}
return true;
},
}
_showPhoneNumber() {
const threePidLogin = !SdkConfig.get().disable_3pid_login;
@ -408,7 +405,7 @@ export default createReactClass({
return false;
}
return true;
},
}
renderEmail() {
if (!this._showEmail()) {
@ -426,7 +423,7 @@ export default createReactClass({
onChange={this.onEmailChange}
onValidate={this.onEmailValidate}
/>;
},
}
renderPassword() {
return <PassphraseField
@ -437,7 +434,7 @@ export default createReactClass({
onChange={this.onPasswordChange}
onValidate={this.onPasswordValidate}
/>;
},
}
renderPasswordConfirm() {
const Field = sdk.getComponent('elements.Field');
@ -451,7 +448,7 @@ export default createReactClass({
onChange={this.onPasswordConfirmChange}
onValidate={this.onPasswordConfirmValidate}
/>;
},
}
renderPhoneNumber() {
if (!this._showPhoneNumber()) {
@ -477,7 +474,7 @@ export default createReactClass({
onChange={this.onPhoneNumberChange}
onValidate={this.onPhoneNumberValidate}
/>;
},
}
renderUsername() {
const Field = sdk.getComponent('elements.Field');
@ -491,9 +488,9 @@ export default createReactClass({
onChange={this.onUsernameChange}
onValidate={this.onUsernameValidate}
/>;
},
}
render: function() {
render() {
let yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
serverName: this.props.serverConfig.hsName,
});
@ -578,5 +575,5 @@ export default createReactClass({
</form>
</div>
);
},
});
}
}

View file

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

View file

@ -17,7 +17,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
import React, {useCallback, useContext, useEffect, useState} from 'react';
import classNames from 'classnames';
import * as AvatarLogic from '../../../Avatar';
import SettingsStore from "../../../settings/SettingsStore";
@ -42,34 +42,35 @@ interface IProps {
className?: string;
}
const calculateUrls = (url, urls) => {
// work out the full set of urls to try to load. This is formed like so:
// imageUrls: [ props.url, ...props.urls ]
let _urls = [];
if (!SettingsStore.getValue("lowBandwidth")) {
_urls = urls || [];
if (url) {
_urls.unshift(url); // put in urls[0]
}
}
// deduplicate URLs
return Array.from(new Set(_urls));
};
const useImageUrl = ({url, urls}): [string, () => void] => {
const [imageUrls, setUrls] = useState<string[]>([]);
const [urlsIndex, setIndex] = useState<number>();
const [imageUrls, setUrls] = useState<string[]>(calculateUrls(url, urls));
const [urlsIndex, setIndex] = useState<number>(0);
const onError = useCallback(() => {
setIndex(i => i + 1); // try the next one
}, []);
const memoizedUrls = useMemo(() => urls, [JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
// work out the full set of urls to try to load. This is formed like so:
// imageUrls: [ props.url, ...props.urls ]
let _urls = [];
if (!SettingsStore.getValue("lowBandwidth")) {
_urls = memoizedUrls || [];
if (url) {
_urls.unshift(url); // put in urls[0]
}
}
// deduplicate URLs
_urls = Array.from(new Set(_urls));
setUrls(calculateUrls(url, urls));
setIndex(0);
setUrls(_urls);
}, [url, memoizedUrls]); // eslint-disable-line react-hooks/exhaustive-deps
}, [url, JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps
const cli = useContext(MatrixClientContext);
const onClientSync = useCallback((syncState, prevState) => {
@ -95,7 +96,7 @@ const BaseAvatar = (props: IProps) => {
urls,
width = 40,
height = 40,
resizeMethod = "crop", // eslint-disable-line no-unused-vars
resizeMethod = "crop", // eslint-disable-line @typescript-eslint/no-unused-vars
defaultToInitialLetter = true,
onClick,
inputRef,

View file

@ -126,7 +126,7 @@ export default class DecoratedRoomAvatar extends React.PureComponent<IProps, ISt
private onPresenceUpdate = () => {
if (this.isUnmounted) return;
let newIcon = this.getPresenceIcon();
const newIcon = this.getPresenceIcon();
if (newIcon !== this.state.icon) this.setState({icon: newIcon});
};

View file

@ -47,7 +47,7 @@ export default class GroupAvatar extends React.Component<IProps> {
render() {
// extract the props we use from props so we can pass any others through
// should consider adding this as a global rule in js-sdk?
/*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
const {groupId, groupAvatarUrl, groupName, ...otherProps} = this.props;
return (

View file

@ -16,23 +16,24 @@ limitations under the License.
*/
import React from 'react';
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
import dis from "../../../dispatcher/dispatcher";
import {Action} from "../../../dispatcher/actions";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import BaseAvatar from "./BaseAvatar";
interface IProps {
// TODO: replace with correct type
member: any;
fallbackUserId: string;
member: RoomMember;
fallbackUserId?: string;
width: number;
height: number;
resizeMethod: string;
resizeMethod?: string;
// The onClick to give the avatar
onClick: React.MouseEventHandler;
onClick?: React.MouseEventHandler;
// Whether the onClick of the avatar should be overriden to dispatch `Action.ViewUser`
viewUserOnClick: boolean;
title: string;
viewUserOnClick?: boolean;
title?: string;
}
interface IState {

View file

@ -25,4 +25,4 @@ const PulsedAvatar: React.FC<IProps> = (props) => {
</div>;
};
export default PulsedAvatar;
export default PulsedAvatar;

View file

@ -114,9 +114,12 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
}
private onRoomAvatarClick = () => {
const avatarUrl = this.props.room.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
null, null, null, false);
const avatarUrl = Avatar.avatarUrlForRoom(
this.props.room,
null,
null,
null,
);
const params = {
src: avatarUrl,
name: this.props.room.name,

View file

@ -37,7 +37,7 @@ interface IOptionListProps {
}
interface IOptionProps extends React.ComponentProps<typeof MenuItem> {
iconClassName: string;
iconClassName?: string;
}
interface ICheckboxProps extends React.ComponentProps<typeof MenuItemCheckbox> {
@ -92,7 +92,7 @@ export const IconizedContextMenuCheckbox: React.FC<ICheckboxProps> = ({
export const IconizedContextMenuOption: React.FC<IOptionProps> = ({label, iconClassName, ...props}) => {
return <MenuItem {...props} label={label}>
<span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} />
{ iconClassName && <span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} /> }
<span className="mx_IconizedContextMenu_label">{label}</span>
</MenuItem>;
};

View file

@ -19,7 +19,6 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import {EventStatus} from 'matrix-js-sdk';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
@ -37,10 +36,8 @@ function canCancel(eventStatus) {
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
}
export default createReactClass({
displayName: 'MessageContextMenu',
propTypes: {
export default class MessageContextMenu extends React.Component {
static propTypes = {
/* the MatrixEvent associated with the context menu */
mxEvent: PropTypes.object.isRequired,
@ -52,28 +49,26 @@ export default createReactClass({
/* callback called when the menu is dismissed */
onFinished: PropTypes.func,
},
};
getInitialState: function() {
return {
canRedact: false,
canPin: false,
};
},
state = {
canRedact: false,
canPin: false,
};
componentDidMount: function() {
componentDidMount() {
MatrixClientPeg.get().on('RoomMember.powerLevel', this._checkPermissions);
this._checkPermissions();
},
}
componentWillUnmount: function() {
componentWillUnmount() {
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener('RoomMember.powerLevel', this._checkPermissions);
}
},
}
_checkPermissions: function() {
_checkPermissions = () => {
const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.mxEvent.getRoomId());
@ -84,47 +79,47 @@ export default createReactClass({
if (!SettingsStore.getValue("feature_pinning")) canPin = false;
this.setState({canRedact, canPin});
},
};
_isPinned: function() {
_isPinned() {
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
const pinnedEvent = room.currentState.getStateEvents('m.room.pinned_events', '');
if (!pinnedEvent) return false;
const content = pinnedEvent.getContent();
return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId());
},
}
onResendClick: function() {
onResendClick = () => {
Resend.resend(this.props.mxEvent);
this.closeMenu();
},
};
onResendEditClick: function() {
onResendEditClick = () => {
Resend.resend(this.props.mxEvent.replacingEvent());
this.closeMenu();
},
};
onResendRedactionClick: function() {
onResendRedactionClick = () => {
Resend.resend(this.props.mxEvent.localRedactionEvent());
this.closeMenu();
},
};
onResendReactionsClick: function() {
onResendReactionsClick = () => {
for (const reaction of this._getUnsentReactions()) {
Resend.resend(reaction);
}
this.closeMenu();
},
};
onReportEventClick: function() {
onReportEventClick = () => {
const ReportEventDialog = sdk.getComponent("dialogs.ReportEventDialog");
Modal.createTrackedDialog('Report Event', '', ReportEventDialog, {
mxEvent: this.props.mxEvent,
}, 'mx_Dialog_reportEvent');
this.closeMenu();
},
};
onViewSourceClick: function() {
onViewSourceClick = () => {
const ev = this.props.mxEvent.replacingEvent() || this.props.mxEvent;
const ViewSource = sdk.getComponent('structures.ViewSource');
Modal.createTrackedDialog('View Event Source', '', ViewSource, {
@ -133,9 +128,9 @@ export default createReactClass({
content: ev.event,
}, 'mx_Dialog_viewsource');
this.closeMenu();
},
};
onViewClearSourceClick: function() {
onViewClearSourceClick = () => {
const ev = this.props.mxEvent.replacingEvent() || this.props.mxEvent;
const ViewSource = sdk.getComponent('structures.ViewSource');
Modal.createTrackedDialog('View Clear Event Source', '', ViewSource, {
@ -145,9 +140,9 @@ export default createReactClass({
content: ev._clearEvent,
}, 'mx_Dialog_viewsource');
this.closeMenu();
},
};
onRedactClick: function() {
onRedactClick = () => {
const ConfirmRedactDialog = sdk.getComponent("dialogs.ConfirmRedactDialog");
Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, {
onFinished: async (proceed) => {
@ -176,9 +171,9 @@ export default createReactClass({
},
}, 'mx_Dialog_confirmredact');
this.closeMenu();
},
};
onCancelSendClick: function() {
onCancelSendClick = () => {
const mxEvent = this.props.mxEvent;
const editEvent = mxEvent.replacingEvent();
const redactEvent = mxEvent.localRedactionEvent();
@ -199,17 +194,17 @@ export default createReactClass({
Resend.removeFromQueue(this.props.mxEvent);
}
this.closeMenu();
},
};
onForwardClick: function() {
onForwardClick = () => {
dis.dispatch({
action: 'forward_event',
event: this.props.mxEvent,
});
this.closeMenu();
},
};
onPinClick: function() {
onPinClick = () => {
MatrixClientPeg.get().getStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', '')
.catch((e) => {
// Intercept the Event Not Found error and fall through the promise chain with no event.
@ -230,28 +225,28 @@ export default createReactClass({
cli.sendStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', {pinned: eventIds}, '');
});
this.closeMenu();
},
};
closeMenu: function() {
closeMenu = () => {
if (this.props.onFinished) this.props.onFinished();
},
};
onUnhidePreviewClick: function() {
onUnhidePreviewClick = () => {
if (this.props.eventTileOps) {
this.props.eventTileOps.unhideWidget();
}
this.closeMenu();
},
};
onQuoteClick: function() {
onQuoteClick = () => {
dis.dispatch({
action: 'quote',
event: this.props.mxEvent,
});
this.closeMenu();
},
};
onPermalinkClick: function(e: Event) {
onPermalinkClick = (e: Event) => {
e.preventDefault();
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
Modal.createTrackedDialog('share room message dialog', '', ShareDialog, {
@ -259,12 +254,12 @@ export default createReactClass({
permalinkCreator: this.props.permalinkCreator,
});
this.closeMenu();
},
};
onCollapseReplyThreadClick: function() {
onCollapseReplyThreadClick = () => {
this.props.collapseReplyThread();
this.closeMenu();
},
};
_getReactions(filter) {
const cli = MatrixClientPeg.get();
@ -277,17 +272,17 @@ export default createReactClass({
relation.event_id === eventId &&
filter(e);
});
},
}
_getPendingReactions() {
return this._getReactions(e => canCancel(e.status));
},
}
_getUnsentReactions() {
return this._getReactions(e => e.status === EventStatus.NOT_SENT);
},
}
render: function() {
render() {
const cli = MatrixClientPeg.get();
const me = cli.getUserId();
const mxEvent = this.props.mxEvent;
@ -489,5 +484,5 @@ export default createReactClass({
{ reportEventButton }
</div>
);
},
});
}
}

View file

@ -1,404 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import classNames from 'classnames';
import * as sdk from '../../../index';
import { _t, _td } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher';
import DMRoomMap from '../../../utils/DMRoomMap';
import * as Rooms from '../../../Rooms';
import * as RoomNotifs from '../../../RoomNotifs';
import Modal from '../../../Modal';
import RoomListActions from '../../../actions/RoomListActions';
import RoomViewStore from '../../../stores/RoomViewStore';
import {sleep} from "../../../utils/promise";
import {MenuItem, MenuItemCheckbox, MenuItemRadio} from "../../structures/ContextMenu";
const RoomTagOption = ({active, onClick, src, srcSet, label}) => {
const classes = classNames('mx_RoomTileContextMenu_tag_field', {
'mx_RoomTileContextMenu_tag_fieldSet': active,
'mx_RoomTileContextMenu_tag_fieldDisabled': false,
});
return (
<MenuItemCheckbox className={classes} onClick={onClick} active={active} label={label}>
<img className="mx_RoomTileContextMenu_tag_icon" src={src} width="15" height="15" alt="" />
<img className="mx_RoomTileContextMenu_tag_icon_set" src={srcSet} width="15" height="15" alt="" />
{ label }
</MenuItemCheckbox>
);
};
const NotifOption = ({active, onClick, src, label}) => {
const classes = classNames('mx_RoomTileContextMenu_notif_field', {
'mx_RoomTileContextMenu_notif_fieldSet': active,
});
return (
<MenuItemRadio className={classes} onClick={onClick} active={active} label={label}>
<img className="mx_RoomTileContextMenu_notif_activeIcon" src={require("../../../../res/img/notif-active.svg")} width="12" height="12" alt="" />
<img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src={src} width="16" height="12" alt="" />
{ label }
</MenuItemRadio>
);
};
export default createReactClass({
displayName: 'RoomTileContextMenu',
propTypes: {
room: PropTypes.object.isRequired,
/* callback called when the menu is dismissed */
onFinished: PropTypes.func,
},
getInitialState() {
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
return {
roomNotifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
isFavourite: this.props.room.tags.hasOwnProperty("m.favourite"),
isLowPriority: this.props.room.tags.hasOwnProperty("m.lowpriority"),
isDirectMessage: Boolean(dmRoomMap.getUserIdForRoomId(this.props.room.roomId)),
};
},
componentDidMount: function() {
this._unmounted = false;
},
componentWillUnmount: function() {
this._unmounted = true;
},
_toggleTag: function(tagNameOn, tagNameOff) {
if (!MatrixClientPeg.get().isGuest()) {
sleep(500).then(() => {
dis.dispatch(RoomListActions.tagRoom(
MatrixClientPeg.get(),
this.props.room,
tagNameOff, tagNameOn,
undefined, 0,
), true);
this.props.onFinished();
});
}
},
_onClickFavourite: function() {
// Tag room as 'Favourite'
if (!this.state.isFavourite && this.state.isLowPriority) {
this.setState({
isFavourite: true,
isLowPriority: false,
});
this._toggleTag("m.favourite", "m.lowpriority");
} else if (this.state.isFavourite) {
this.setState({isFavourite: false});
this._toggleTag(null, "m.favourite");
} else if (!this.state.isFavourite) {
this.setState({isFavourite: true});
this._toggleTag("m.favourite");
}
},
_onClickLowPriority: function() {
// Tag room as 'Low Priority'
if (!this.state.isLowPriority && this.state.isFavourite) {
this.setState({
isFavourite: false,
isLowPriority: true,
});
this._toggleTag("m.lowpriority", "m.favourite");
} else if (this.state.isLowPriority) {
this.setState({isLowPriority: false});
this._toggleTag(null, "m.lowpriority");
} else if (!this.state.isLowPriority) {
this.setState({isLowPriority: true});
this._toggleTag("m.lowpriority");
}
},
_onClickDM: function() {
if (MatrixClientPeg.get().isGuest()) return;
const newIsDirectMessage = !this.state.isDirectMessage;
this.setState({
isDirectMessage: newIsDirectMessage,
});
Rooms.guessAndSetDMRoom(
this.props.room, newIsDirectMessage,
).then(sleep(500)).finally(() => {
// Close the context menu
if (this.props.onFinished) {
this.props.onFinished();
}
}, (err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to set Direct Message status of room', '', ErrorDialog, {
title: _t('Failed to set Direct Message status of room'),
description: ((err && err.message) ? err.message : _t('Operation failed')),
});
});
},
_onClickLeave: function() {
// Leave room
dis.dispatch({
action: 'leave_room',
room_id: this.props.room.roomId,
});
// Close the context menu
if (this.props.onFinished) {
this.props.onFinished();
}
},
_onClickReject: function() {
dis.dispatch({
action: 'reject_invite',
room_id: this.props.room.roomId,
});
// Close the context menu
if (this.props.onFinished) {
this.props.onFinished();
}
},
_onClickForget: function() {
// FIXME: duplicated with RoomSettings (and dead code in RoomView)
MatrixClientPeg.get().forget(this.props.room.roomId).then(() => {
// Switch to another room view if we're currently viewing the
// historical room
if (RoomViewStore.getRoomId() === this.props.room.roomId) {
dis.dispatch({ action: 'view_next_room' });
}
}, function(err) {
const errCode = err.errcode || _td("unknown error code");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to forget room', '', ErrorDialog, {
title: _t('Failed to forget room %(errCode)s', {errCode: errCode}),
description: ((err && err.message) ? err.message : _t('Operation failed')),
});
});
// Close the context menu
if (this.props.onFinished) {
this.props.onFinished();
}
},
_saveNotifState: function(newState) {
if (MatrixClientPeg.get().isGuest()) return;
const oldState = this.state.roomNotifState;
const roomId = this.props.room.roomId;
this.setState({
roomNotifState: newState,
});
RoomNotifs.setRoomNotifsState(roomId, newState).then(() => {
// delay slightly so that the user can see their state change
// before closing the menu
return sleep(500).then(() => {
if (this._unmounted) return;
// Close the context menu
if (this.props.onFinished) {
this.props.onFinished();
}
});
}, (error) => {
// TODO: some form of error notification to the user
// to inform them that their state change failed.
// For now we at least set the state back
if (this._unmounted) return;
this.setState({
roomNotifState: oldState,
});
});
},
_onClickAlertMe: function() {
this._saveNotifState(RoomNotifs.ALL_MESSAGES_LOUD);
},
_onClickAllNotifs: function() {
this._saveNotifState(RoomNotifs.ALL_MESSAGES);
},
_onClickMentions: function() {
this._saveNotifState(RoomNotifs.MENTIONS_ONLY);
},
_onClickMute: function() {
this._saveNotifState(RoomNotifs.MUTE);
},
_renderNotifMenu: function() {
return (
<div className="mx_RoomTileContextMenu" role="group" aria-label={_t("Notification settings")}>
<div className="mx_RoomTileContextMenu_notif_picker" role="presentation">
<img src={require("../../../../res/img/notif-slider.svg")} width="20" height="107" alt="" />
</div>
<NotifOption
active={this.state.roomNotifState === RoomNotifs.ALL_MESSAGES_LOUD}
label={_t('All messages (noisy)')}
onClick={this._onClickAlertMe}
src={require("../../../../res/img/icon-context-mute-off-copy.svg")}
/>
<NotifOption
active={this.state.roomNotifState === RoomNotifs.ALL_MESSAGES}
label={_t('All messages')}
onClick={this._onClickAllNotifs}
src={require("../../../../res/img/icon-context-mute-off.svg")}
/>
<NotifOption
active={this.state.roomNotifState === RoomNotifs.MENTIONS_ONLY}
label={_t('Mentions only')}
onClick={this._onClickMentions}
src={require("../../../../res/img/icon-context-mute-mentions.svg")}
/>
<NotifOption
active={this.state.roomNotifState === RoomNotifs.MUTE}
label={_t('Mute')}
onClick={this._onClickMute}
src={require("../../../../res/img/icon-context-mute.svg")}
/>
</div>
);
},
_onClickSettings: function() {
dis.dispatch({
action: 'open_room_settings',
room_id: this.props.room.roomId,
});
if (this.props.onFinished) {
this.props.onFinished();
}
},
_renderSettingsMenu: function() {
return (
<div>
<MenuItem className="mx_RoomTileContextMenu_tag_field" onClick={this._onClickSettings}>
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/feather-customised/settings.svg")} width="15" height="15" alt="" />
{ _t('Settings') }
</MenuItem>
</div>
);
},
_renderLeaveMenu: function(membership) {
if (!membership) {
return null;
}
let leaveClickHandler = null;
let leaveText = null;
switch (membership) {
case "join":
leaveClickHandler = this._onClickLeave;
leaveText = _t('Leave');
break;
case "leave":
case "ban":
leaveClickHandler = this._onClickForget;
leaveText = _t('Forget');
break;
case "invite":
leaveClickHandler = this._onClickReject;
leaveText = _t('Reject');
break;
}
return (
<div>
<MenuItem className="mx_RoomTileContextMenu_leave" onClick={leaveClickHandler}>
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_delete.svg")} width="15" height="15" alt="" />
{ leaveText }
</MenuItem>
</div>
);
},
_renderRoomTagMenu: function() {
return (
<div>
<RoomTagOption
active={this.state.isFavourite}
label={_t('Favourite')}
onClick={this._onClickFavourite}
src={require("../../../../res/img/icon_context_fave.svg")}
srcSet={require("../../../../res/img/icon_context_fave_on.svg")}
/>
<RoomTagOption
active={this.state.isLowPriority}
label={_t('Low Priority')}
onClick={this._onClickLowPriority}
src={require("../../../../res/img/icon_context_low.svg")}
srcSet={require("../../../../res/img/icon_context_low_on.svg")}
/>
<RoomTagOption
active={this.state.isDirectMessage}
label={_t('Direct Chat')}
onClick={this._onClickDM}
src={require("../../../../res/img/icon_context_person.svg")}
srcSet={require("../../../../res/img/icon_context_person_on.svg")}
/>
</div>
);
},
render: function() {
const myMembership = this.props.room.getMyMembership();
switch (myMembership) {
case 'join':
return <div>
{ this._renderNotifMenu() }
<hr className="mx_RoomTileContextMenu_separator" role="separator" />
{ this._renderLeaveMenu(myMembership) }
<hr className="mx_RoomTileContextMenu_separator" role="separator" />
{ this._renderRoomTagMenu() }
<hr className="mx_RoomTileContextMenu_separator" role="separator" />
{ this._renderSettingsMenu() }
</div>;
case 'invite':
return <div>
{ this._renderLeaveMenu(myMembership) }
</div>;
default:
return <div>
{ this._renderLeaveMenu(myMembership) }
<hr className="mx_RoomTileContextMenu_separator" role="separator" />
{ this._renderSettingsMenu() }
</div>;
}
},
});

View file

@ -1,155 +0,0 @@
/*
Copyright 2018, 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import LogoutDialog from "../dialogs/LogoutDialog";
import Modal from "../../../Modal";
import SdkConfig from '../../../SdkConfig';
import { getHostingLink } from '../../../utils/HostingLink';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {MenuItem} from "../../structures/ContextMenu";
import * as sdk from "../../../index";
import {getHomePageUrl} from "../../../utils/pages";
import {Action} from "../../../dispatcher/actions";
export default class TopLeftMenu extends React.Component {
static propTypes = {
displayName: PropTypes.string.isRequired,
userId: PropTypes.string.isRequired,
onFinished: PropTypes.func,
// Optional function to collect a reference to the container
// of this component directly.
containerRef: PropTypes.func,
};
constructor() {
super();
this.viewHomePage = this.viewHomePage.bind(this);
this.openSettings = this.openSettings.bind(this);
this.signIn = this.signIn.bind(this);
this.signOut = this.signOut.bind(this);
}
hasHomePage() {
return !!getHomePageUrl(SdkConfig.get());
}
render() {
const isGuest = MatrixClientPeg.get().isGuest();
const hostingSignupLink = getHostingLink('user-context-menu');
let hostingSignup = null;
if (hostingSignupLink) {
hostingSignup = <div className="mx_TopLeftMenu_upgradeLink">
{_t(
"<a>Upgrade</a> to your own domain", {},
{
a: sub =>
<a href={hostingSignupLink} target="_blank" rel="noreferrer noopener" tabIndex={-1}>{sub}</a>,
},
)}
<a href={hostingSignupLink} target="_blank" rel="noreferrer noopener" role="presentation" aria-hidden={true} tabIndex={-1}>
<img src={require("../../../../res/img/external-link.svg")} width="11" height="10" alt='' />
</a>
</div>;
}
let homePageItem = null;
if (this.hasHomePage()) {
homePageItem = (
<MenuItem className="mx_TopLeftMenu_icon_home" onClick={this.viewHomePage}>
{_t("Home")}
</MenuItem>
);
}
let signInOutItem;
if (isGuest) {
signInOutItem = (
<MenuItem className="mx_TopLeftMenu_icon_signin" onClick={this.signIn}>
{_t("Sign in")}
</MenuItem>
);
} else {
signInOutItem = (
<MenuItem className="mx_TopLeftMenu_icon_signout" onClick={this.signOut}>
{_t("Sign out")}
</MenuItem>
);
}
const helpItem = (
<MenuItem className="mx_TopLeftMenu_icon_help" onClick={this.openHelp}>
{_t("Help")}
</MenuItem>
);
const settingsItem = (
<MenuItem className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings}>
{_t("Settings")}
</MenuItem>
);
return <div className="mx_TopLeftMenu" ref={this.props.containerRef} role="menu">
<div className="mx_TopLeftMenu_section_noIcon" aria-readonly={true} tabIndex={-1}>
<div>{this.props.displayName}</div>
<div className="mx_TopLeftMenu_greyedText" aria-hidden={true}>{this.props.userId}</div>
{hostingSignup}
</div>
<ul className="mx_TopLeftMenu_section_withIcon" role="none">
{homePageItem}
{settingsItem}
{helpItem}
{signInOutItem}
</ul>
</div>;
}
openHelp = () => {
this.closeMenu();
const RedesignFeedbackDialog = sdk.getComponent("views.dialogs.RedesignFeedbackDialog");
Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog);
};
viewHomePage() {
dis.dispatch({action: 'view_home_page'});
this.closeMenu();
}
openSettings() {
dis.fire(Action.ViewUserSettings);
this.closeMenu();
}
signIn() {
dis.dispatch({action: 'start_login'});
this.closeMenu();
}
signOut() {
Modal.createTrackedDialog('Logout E2E Export', '', LogoutDialog);
this.closeMenu();
}
closeMenu() {
if (this.props.onFinished) this.props.onFinished();
}
}

View file

@ -26,6 +26,9 @@ export default class WidgetContextMenu extends React.Component {
// Callback for when the revoke button is clicked. Required.
onRevokeClicked: PropTypes.func.isRequired,
// Callback for when the unpin button is clicked. If absent, unpin will be hidden.
onUnpinClicked: PropTypes.func,
// Callback for when the snapshot button is clicked. Button not shown
// without a callback.
onSnapshotClicked: PropTypes.func,
@ -70,6 +73,8 @@ export default class WidgetContextMenu extends React.Component {
this.proxyClick(this.props.onRevokeClicked);
};
onUnpinClicked = () => this.proxyClick(this.props.onUnpinClicked);
render() {
const options = [];
@ -81,6 +86,14 @@ export default class WidgetContextMenu extends React.Component {
);
}
if (this.props.onUnpinClicked) {
options.push(
<MenuItem className="mx_WidgetContextMenu_option" onClick={this.onUnpinClicked} key="unpin">
{_t("Unpin")}
</MenuItem>,
);
}
if (this.props.onReloadClicked) {
options.push(
<MenuItem className='mx_WidgetContextMenu_option' onClick={this.onReloadClicked} key='reload'>

View file

@ -1,57 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
const Presets = {
PrivateChat: "private_chat",
PublicChat: "public_chat",
Custom: "custom",
};
export default createReactClass({
displayName: 'CreateRoomPresets',
propTypes: {
onChange: PropTypes.func,
preset: PropTypes.string,
},
Presets: Presets,
getDefaultProps: function() {
return {
onChange: function() {},
};
},
onValueChanged: function(ev) {
this.props.onChange(ev.target.value);
},
render: function() {
return (
<select className="mx_Presets" onChange={this.onValueChanged} value={this.props.preset}>
<option value={this.Presets.PrivateChat}>{ _t("Private Chat") }</option>
<option value={this.Presets.PublicChat}>{ _t("Public Chat") }</option>
<option value={this.Presets.Custom}>{ _t("Custom") }</option>
</select>
);
},
});

View file

@ -1,106 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
export default createReactClass({
displayName: 'RoomAlias',
propTypes: {
// Specifying a homeserver will make magical things happen when you,
// e.g. start typing in the room alias box.
homeserver: PropTypes.string,
alias: PropTypes.string,
onChange: PropTypes.func,
},
getDefaultProps: function() {
return {
onChange: function() {},
alias: '',
};
},
getAliasLocalpart: function() {
let room_alias = this.props.alias;
if (room_alias && this.props.homeserver) {
const suffix = ":" + this.props.homeserver;
if (room_alias.startsWith("#") && room_alias.endsWith(suffix)) {
room_alias = room_alias.slice(1, -suffix.length);
}
}
return room_alias;
},
onValueChanged: function(ev) {
this.props.onChange(ev.target.value);
},
onFocus: function(ev) {
const target = ev.target;
const curr_val = ev.target.value;
if (this.props.homeserver) {
if (curr_val == "") {
const self = this;
setTimeout(function() {
target.value = "#:" + self.props.homeserver;
target.setSelectionRange(1, 1);
}, 0);
} else {
const suffix = ":" + this.props.homeserver;
setTimeout(function() {
target.setSelectionRange(
curr_val.startsWith("#") ? 1 : 0,
curr_val.endsWith(suffix) ? (target.value.length - suffix.length) : target.value.length,
);
}, 0);
}
}
},
onBlur: function(ev) {
const curr_val = ev.target.value;
if (this.props.homeserver) {
if (curr_val == "#:" + this.props.homeserver) {
ev.target.value = "";
return;
}
if (curr_val != "") {
let new_val = ev.target.value;
const suffix = ":" + this.props.homeserver;
if (!curr_val.startsWith("#")) new_val = "#" + new_val;
if (!curr_val.endsWith(suffix)) new_val = new_val + suffix;
ev.target.value = new_val;
}
}
},
render: function() {
return (
<input type="text" className="mx_RoomAlias" placeholder={_t("Address (optional)")}
onChange={this.onValueChanged} onFocus={this.onFocus} onBlur={this.onBlur}
value={this.props.alias} />
);
},
});

View file

@ -19,7 +19,6 @@ limitations under the License.
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { _t, _td } from '../../../languageHandler';
import * as sdk from '../../../index';
@ -45,10 +44,8 @@ const addressTypeName = {
};
export default createReactClass({
displayName: "AddressPickerDialog",
propTypes: {
export default class AddressPickerDialog extends React.Component {
static propTypes = {
title: PropTypes.string.isRequired,
description: PropTypes.node,
// Extra node inserted after picker input, dropdown and errors
@ -66,26 +63,28 @@ export default createReactClass({
// Whether the current user should be included in the addresses returned. Only
// applicable when pickerType is `user`. Default: false.
includeSelf: PropTypes.bool,
},
};
getDefaultProps: function() {
return {
value: "",
focus: true,
validAddressTypes: addressTypes,
pickerType: 'user',
includeSelf: false,
};
},
static defaultProps = {
value: "",
focus: true,
validAddressTypes: addressTypes,
pickerType: 'user',
includeSelf: false,
};
constructor(props) {
super(props);
this._textinput = createRef();
getInitialState: function() {
let validAddressTypes = this.props.validAddressTypes;
// Remove email from validAddressTypes if no IS is configured. It may be added at a later stage by the user
if (!MatrixClientPeg.get().getIdentityServerUrl() && validAddressTypes.includes("email")) {
validAddressTypes = validAddressTypes.filter(type => type !== "email");
}
return {
this.state = {
// Whether to show an error message because of an invalid address
invalidAddressError: false,
// List of UserAddressType objects representing
@ -106,19 +105,14 @@ export default createReactClass({
// dialog is open and represents the supported list of address types at this time.
validAddressTypes,
};
},
}
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._textinput = createRef();
},
componentDidMount: function() {
componentDidMount() {
if (this.props.focus) {
// Set the cursor at the end of the text input
this._textinput.current.value = this.props.value;
}
},
}
getPlaceholder() {
const { placeholder } = this.props;
@ -127,9 +121,9 @@ export default createReactClass({
}
// Otherwise it's a function, as checked by prop types.
return placeholder(this.state.validAddressTypes);
},
}
onButtonClick: function() {
onButtonClick = () => {
let selectedList = this.state.selectedList.slice();
// Check the text input field to see if user has an unconverted address
// If there is and it's valid add it to the local selectedList
@ -138,13 +132,13 @@ export default createReactClass({
if (selectedList === null) return;
}
this.props.onFinished(true, selectedList);
},
};
onCancel: function() {
onCancel = () => {
this.props.onFinished(false);
},
};
onKeyDown: function(e) {
onKeyDown = e => {
const textInput = this._textinput.current ? this._textinput.current.value : undefined;
if (e.key === Key.ESCAPE) {
@ -181,9 +175,9 @@ export default createReactClass({
e.preventDefault();
this._addAddressesToList([textInput]);
}
},
};
onQueryChanged: function(ev) {
onQueryChanged = ev => {
const query = ev.target.value;
if (this.queryChangedDebouncer) {
clearTimeout(this.queryChangedDebouncer);
@ -216,28 +210,24 @@ export default createReactClass({
searchError: null,
});
}
},
};
onDismissed: function(index) {
return () => {
const selectedList = this.state.selectedList.slice();
selectedList.splice(index, 1);
this.setState({
selectedList,
suggestedList: [],
query: "",
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
};
},
onDismissed = index => () => {
const selectedList = this.state.selectedList.slice();
selectedList.splice(index, 1);
this.setState({
selectedList,
suggestedList: [],
query: "",
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
};
onClick: function(index) {
return () => {
this.onSelected(index);
};
},
onClick = index => () => {
this.onSelected(index);
};
onSelected: function(index) {
onSelected = index => {
const selectedList = this.state.selectedList.slice();
selectedList.push(this._getFilteredSuggestions()[index]);
this.setState({
@ -246,9 +236,9 @@ export default createReactClass({
query: "",
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
},
};
_doNaiveGroupSearch: function(query) {
_doNaiveGroupSearch(query) {
const lowerCaseQuery = query.toLowerCase();
this.setState({
busy: true,
@ -280,9 +270,9 @@ export default createReactClass({
busy: false,
});
});
},
}
_doNaiveGroupRoomSearch: function(query) {
_doNaiveGroupRoomSearch(query) {
const lowerCaseQuery = query.toLowerCase();
const results = [];
GroupStore.getGroupRooms(this.props.groupId).forEach((r) => {
@ -302,9 +292,9 @@ export default createReactClass({
this.setState({
busy: false,
});
},
}
_doRoomSearch: function(query) {
_doRoomSearch(query) {
const lowerCaseQuery = query.toLowerCase();
const rooms = MatrixClientPeg.get().getRooms();
const results = [];
@ -359,9 +349,9 @@ export default createReactClass({
this.setState({
busy: false,
});
},
}
_doUserDirectorySearch: function(query) {
_doUserDirectorySearch(query) {
this.setState({
busy: true,
query,
@ -393,9 +383,9 @@ export default createReactClass({
busy: false,
});
});
},
}
_doLocalSearch: function(query) {
_doLocalSearch(query) {
this.setState({
query,
searchError: null,
@ -417,9 +407,9 @@ export default createReactClass({
});
});
this._processResults(results, query);
},
}
_processResults: function(results, query) {
_processResults(results, query) {
const suggestedList = [];
results.forEach((result) => {
if (result.room_id) {
@ -485,9 +475,9 @@ export default createReactClass({
}, () => {
if (this.addressSelector) this.addressSelector.moveSelectionTop();
});
},
}
_addAddressesToList: function(addressTexts) {
_addAddressesToList(addressTexts) {
const selectedList = this.state.selectedList.slice();
let hasError = false;
@ -529,9 +519,9 @@ export default createReactClass({
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
return hasError ? null : selectedList;
},
}
_lookupThreepid: async function(medium, address) {
async _lookupThreepid(medium, address) {
let cancelled = false;
// Note that we can't safely remove this after we're done
// because we don't know that it's the same one, so we just
@ -577,9 +567,9 @@ export default createReactClass({
searchError: _t('Something went wrong!'),
});
}
},
}
_getFilteredSuggestions: function() {
_getFilteredSuggestions() {
// map addressType => set of addresses to avoid O(n*m) operation
const selectedAddresses = {};
this.state.selectedList.forEach(({address, addressType}) => {
@ -591,17 +581,17 @@ export default createReactClass({
return this.state.suggestedList.filter(({address, addressType}) => {
return !(selectedAddresses[addressType] && selectedAddresses[addressType].has(address));
});
},
}
_onPaste: function(e) {
_onPaste = e => {
// Prevent the text being pasted into the textarea
e.preventDefault();
const text = e.clipboardData.getData("text");
// Process it as a list of addresses to add instead
this._addAddressesToList(text.split(/[\s,]+/));
},
};
onUseDefaultIdentityServerClick(e) {
onUseDefaultIdentityServerClick = e => {
e.preventDefault();
// Update the IS in account data. Actually using it may trigger terms.
@ -612,15 +602,15 @@ export default createReactClass({
const { validAddressTypes } = this.state;
validAddressTypes.push('email');
this.setState({ validAddressTypes });
},
};
onManageSettingsClick(e) {
onManageSettingsClick = e => {
e.preventDefault();
dis.fire(Action.ViewUserSettings);
this.onCancel();
},
};
render: function() {
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AddressSelector = sdk.getComponent("elements.AddressSelector");
@ -738,5 +728,5 @@ export default createReactClass({
onCancel={this.onCancel} />
</BaseDialog>
);
},
});
}
}

View file

@ -16,37 +16,36 @@ 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 { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
import {SettingLevel} from "../../../settings/SettingLevel";
export default createReactClass({
propTypes: {
export default class AskInviteAnywayDialog extends React.Component {
static propTypes = {
unknownProfileUsers: PropTypes.array.isRequired, // [ {userId, errorText}... ]
onInviteAnyways: PropTypes.func.isRequired,
onGiveUp: PropTypes.func.isRequired,
onFinished: PropTypes.func.isRequired,
},
};
_onInviteClicked: function() {
_onInviteClicked = () => {
this.props.onInviteAnyways();
this.props.onFinished(true);
},
};
_onInviteNeverWarnClicked: function() {
_onInviteNeverWarnClicked = () => {
SettingsStore.setValue("promptBeforeInviteUnknownUsers", null, SettingLevel.ACCOUNT, false);
this.props.onInviteAnyways();
this.props.onFinished(true);
},
};
_onGiveUpClicked: function() {
_onGiveUpClicked = () => {
this.props.onGiveUp();
this.props.onFinished(false);
},
};
render: function() {
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const errorList = this.props.unknownProfileUsers
@ -78,5 +77,5 @@ export default createReactClass({
</div>
</BaseDialog>
);
},
});
}
}

View file

@ -17,7 +17,6 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import FocusLock from 'react-focus-lock';
import PropTypes from 'prop-types';
import classNames from 'classnames';
@ -28,16 +27,14 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { _t } from "../../../languageHandler";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
/**
/*
* Basic container for modal dialogs.
*
* Includes a div for the title, and a keypress handler which cancels the
* dialog on escape.
*/
export default createReactClass({
displayName: 'BaseDialog',
propTypes: {
export default class BaseDialog extends React.Component {
static propTypes = {
// onFinished callback to call when Escape is pressed
// Take a boolean which is true if the dialog was dismissed
// with a positive / confirm action or false if it was
@ -81,21 +78,20 @@ export default createReactClass({
PropTypes.object,
PropTypes.arrayOf(PropTypes.string),
]),
},
};
getDefaultProps: function() {
return {
hasCancel: true,
fixedWidth: true,
};
},
static defaultProps = {
hasCancel: true,
fixedWidth: true,
};
constructor(props) {
super(props);
// TODO: [REACT-WARNING] Move this to constructor
UNSAFE_componentWillMount() {
this._matrixClient = MatrixClientPeg.get();
},
}
_onKeyDown: function(e) {
_onKeyDown = (e) => {
if (this.props.onKeyDown) {
this.props.onKeyDown(e);
}
@ -104,13 +100,13 @@ export default createReactClass({
e.preventDefault();
this.props.onFinished(false);
}
},
};
_onCancelClick: function(e) {
_onCancelClick = (e) => {
this.props.onFinished(false);
},
};
render: function() {
render() {
let cancelButton;
if (this.props.hasCancel) {
cancelButton = (
@ -161,5 +157,5 @@ export default createReactClass({
</FocusLock>
</MatrixClientContext.Provider>
);
},
});
}
}

View file

@ -34,7 +34,7 @@ export default class BugReportDialog extends React.Component {
busy: false,
err: null,
issueUrl: "",
text: "",
text: props.initialText || "",
progress: null,
downloadBusy: false,
downloadProgress: null,
@ -255,4 +255,5 @@ export default class BugReportDialog extends React.Component {
BugReportDialog.propTypes = {
onFinished: PropTypes.func.isRequired,
initialText: PropTypes.string,
};

View file

@ -0,0 +1,248 @@
/*
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, { ChangeEvent, FormEvent } from 'react';
import BaseDialog from "./BaseDialog";
import { _t } from "../../../languageHandler";
import { IDialogProps } from "./IDialogProps";
import Field from "../elements/Field";
import AccessibleButton from "../elements/AccessibleButton";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { arrayFastClone } from "../../../utils/arrays";
import SdkConfig from "../../../SdkConfig";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import InviteDialog from "./InviteDialog";
import BaseAvatar from "../avatars/BaseAvatar";
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
import {inviteMultipleToRoom, showAnyInviteErrors} from "../../../RoomInvite";
import StyledCheckbox from "../elements/StyledCheckbox";
import Modal from "../../../Modal";
import ErrorDialog from "./ErrorDialog";
interface IProps extends IDialogProps {
roomId: string;
communityName: string;
}
interface IPerson {
userId: string;
user: RoomMember;
lastActive: number;
}
interface IState {
emailTargets: string[];
userTargets: string[];
showPeople: boolean;
people: IPerson[];
numPeople: number;
busy: boolean;
}
export default class CommunityPrototypeInviteDialog extends React.PureComponent<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
emailTargets: [],
userTargets: [],
showPeople: false,
people: this.buildSuggestions(),
numPeople: 5, // arbitrary default
busy: false,
};
}
private buildSuggestions(): IPerson[] {
const alreadyInvited = new Set([MatrixClientPeg.get().getUserId(), SdkConfig.get()['welcomeUserId']]);
if (this.props.roomId) {
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
if (!room) throw new Error("Room ID given to InviteDialog does not look like a room");
room.getMembersWithMembership('invite').forEach(m => alreadyInvited.add(m.userId));
room.getMembersWithMembership('join').forEach(m => alreadyInvited.add(m.userId));
// add banned users, so we don't try to invite them
room.getMembersWithMembership('ban').forEach(m => alreadyInvited.add(m.userId));
}
return InviteDialog.buildRecents(alreadyInvited);
}
private onSubmit = async (ev: FormEvent) => {
ev.preventDefault();
ev.stopPropagation();
this.setState({busy: true});
try {
const targets = [...this.state.emailTargets, ...this.state.userTargets];
const result = await inviteMultipleToRoom(this.props.roomId, targets);
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
const success = showAnyInviteErrors(result.states, room, result.inviter);
if (success) {
this.props.onFinished(true);
} else {
this.setState({busy: false});
}
} catch (e) {
this.setState({busy: false});
console.error(e);
Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, {
title: _t("Failed to invite"),
description: ((e && e.message) ? e.message : _t("Operation failed")),
});
}
};
private onAddressChange = (ev: ChangeEvent<HTMLInputElement>, index: number) => {
const targets = arrayFastClone(this.state.emailTargets);
if (index >= targets.length) {
targets.push(ev.target.value);
} else {
targets[index] = ev.target.value;
}
this.setState({emailTargets: targets});
};
private onAddressBlur = (index: number) => {
const targets = arrayFastClone(this.state.emailTargets);
if (index >= targets.length) return; // not important
if (targets[index].trim() === "") {
targets.splice(index, 1);
this.setState({emailTargets: targets});
}
};
private onShowPeopleClick = () => {
this.setState({showPeople: !this.state.showPeople});
};
private setPersonToggle = (person: IPerson, selected: boolean) => {
const targets = arrayFastClone(this.state.userTargets);
if (selected && !targets.includes(person.userId)) {
targets.push(person.userId);
} else if (!selected && targets.includes(person.userId)) {
targets.splice(targets.indexOf(person.userId), 1);
}
this.setState({userTargets: targets});
};
private renderPerson(person: IPerson, key: any) {
const avatarSize = 36;
return (
<div className="mx_CommunityPrototypeInviteDialog_person" key={key}>
<BaseAvatar
url={getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(), person.user.getMxcAvatarUrl(),
avatarSize, avatarSize, "crop")}
name={person.user.name}
idName={person.user.userId}
width={avatarSize}
height={avatarSize}
/>
<div className="mx_CommunityPrototypeInviteDialog_personIdentifiers">
<span className="mx_CommunityPrototypeInviteDialog_personName">{person.user.name}</span>
<span className="mx_CommunityPrototypeInviteDialog_personId">{person.userId}</span>
</div>
<StyledCheckbox onChange={(e) => this.setPersonToggle(person, e.target.checked)} />
</div>
);
}
private onShowMorePeople = () => {
this.setState({numPeople: this.state.numPeople + 5}); // arbitrary increase
};
public render() {
const emailAddresses = [];
this.state.emailTargets.forEach((address, i) => {
emailAddresses.push((
<Field
key={i}
value={address}
onChange={(e) => this.onAddressChange(e, i)}
label={_t("Email address")}
placeholder={_t("Email address")}
onBlur={() => this.onAddressBlur(i)}
/>
));
});
// Push a clean input
emailAddresses.push((
<Field
key={emailAddresses.length}
value={""}
onChange={(e) => this.onAddressChange(e, emailAddresses.length)}
label={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")}
placeholder={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")}
/>
));
let peopleIntro = null;
const people = [];
if (this.state.showPeople) {
const humansToPresent = this.state.people.slice(0, this.state.numPeople);
humansToPresent.forEach((person, i) => {
people.push(this.renderPerson(person, i));
});
if (humansToPresent.length < this.state.people.length) {
people.push((
<AccessibleButton
onClick={this.onShowMorePeople}
kind="link" key="more"
className="mx_CommunityPrototypeInviteDialog_morePeople"
>{_t("Show more")}</AccessibleButton>
));
}
}
if (this.state.people.length > 0) {
peopleIntro = (
<div className="mx_CommunityPrototypeInviteDialog_people">
<span>{_t("People you know on %(brand)s", {brand: SdkConfig.get().brand})}</span>
<AccessibleButton onClick={this.onShowPeopleClick}>
{this.state.showPeople ? _t("Hide") : _t("Show")}
</AccessibleButton>
</div>
);
}
let buttonText = _t("Skip");
const targetCount = this.state.userTargets.length + this.state.emailTargets.length;
if (targetCount > 0) {
buttonText = _t("Send %(count)s invites", {count: targetCount});
}
return (
<BaseDialog
className="mx_CommunityPrototypeInviteDialog"
onFinished={this.props.onFinished}
title={_t("Invite people to join %(communityName)s", {communityName: this.props.communityName})}
>
<form onSubmit={this.onSubmit}>
<div className="mx_Dialog_content">
{emailAddresses}
{peopleIntro}
{people}
<AccessibleButton
kind="primary" onClick={this.onSubmit}
disabled={this.state.busy}
className="mx_CommunityPrototypeInviteDialog_primaryButton"
>{buttonText}</AccessibleButton>
</div>
</form>
</BaseDialog>
);
}
}

View file

@ -15,17 +15,14 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
/*
* A dialog for confirming a redaction.
*/
export default createReactClass({
displayName: 'ConfirmRedactDialog',
render: function() {
export default class ConfirmRedactDialog extends React.Component {
render() {
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
return (
<QuestionDialog onFinished={this.props.onFinished}
@ -36,5 +33,5 @@ export default createReactClass({
button={_t("Remove")}>
</QuestionDialog>
);
},
});
}
}

View file

@ -15,7 +15,6 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
import * as sdk from '../../../index';
@ -30,9 +29,8 @@ import { GroupMemberType } from '../../../groups';
* to make it obvious what is going to happen.
* Also tweaks the style for 'dangerous' actions (albeit only with colour)
*/
export default createReactClass({
displayName: 'ConfirmUserActionDialog',
propTypes: {
export default class ConfirmUserActionDialog extends React.Component {
static propTypes = {
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
member: PropTypes.object,
// group member object. Supply either this or 'member'
@ -48,35 +46,36 @@ export default createReactClass({
askReason: PropTypes.bool,
danger: PropTypes.bool,
onFinished: PropTypes.func.isRequired,
},
};
getDefaultProps: () => ({
static defaultProps = {
danger: false,
askReason: false,
}),
};
constructor(props) {
super(props);
// TODO: [REACT-WARNING] Move this to constructor
UNSAFE_componentWillMount: function() {
this._reasonField = null;
},
}
onOk: function() {
onOk = () => {
let reason;
if (this._reasonField) {
reason = this._reasonField.value;
}
this.props.onFinished(true, reason);
},
};
onCancel: function() {
onCancel = () => {
this.props.onFinished(false);
},
};
_collectReasonField: function(e) {
_collectReasonField = e => {
this._reasonField = e;
},
};
render: function() {
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
@ -134,5 +133,5 @@ export default createReactClass({
onCancel={this.onCancel} />
</BaseDialog>
);
},
});
}
}

View file

@ -0,0 +1,227 @@
/*
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, { ChangeEvent } from 'react';
import BaseDialog from "./BaseDialog";
import { _t } from "../../../languageHandler";
import { IDialogProps } from "./IDialogProps";
import Field from "../elements/Field";
import AccessibleButton from "../elements/AccessibleButton";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import InfoTooltip from "../elements/InfoTooltip";
import dis from "../../../dispatcher/dispatcher";
import {showCommunityRoomInviteDialog} from "../../../RoomInvite";
import GroupStore from "../../../stores/GroupStore";
interface IProps extends IDialogProps {
}
interface IState {
name: string;
localpart: string;
error: string;
busy: boolean;
avatarFile: File;
avatarPreview: string;
}
export default class CreateCommunityPrototypeDialog extends React.PureComponent<IProps, IState> {
private avatarUploadRef: React.RefObject<HTMLInputElement> = React.createRef();
constructor(props: IProps) {
super(props);
this.state = {
name: "",
localpart: "",
error: null,
busy: false,
avatarFile: null,
avatarPreview: null,
};
}
private onNameChange = (ev: ChangeEvent<HTMLInputElement>) => {
const localpart = (ev.target.value || "").toLowerCase().replace(/[^a-z0-9.\-_]/g, '-');
this.setState({name: ev.target.value, localpart});
};
private onSubmit = async (ev) => {
ev.preventDefault();
ev.stopPropagation();
if (this.state.busy) return;
// We'll create the community now to see if it's taken, leaving it active in
// the background for the user to look at while they invite people.
this.setState({busy: true});
try {
let avatarUrl = ''; // must be a string for synapse to accept it
if (this.state.avatarFile) {
avatarUrl = await MatrixClientPeg.get().uploadContent(this.state.avatarFile);
}
const result = await MatrixClientPeg.get().createGroup({
localpart: this.state.localpart,
profile: {
name: this.state.name,
avatar_url: avatarUrl,
},
});
// Ensure the tag gets selected now that we've created it
dis.dispatch({action: 'deselect_tags'}, true);
dis.dispatch({
action: 'select_tag',
tag: result.group_id,
});
// Close our own dialog before moving much further
this.props.onFinished(true);
if (result.room_id) {
// Force the group store to update as it might have missed the general chat
await GroupStore.refreshGroupRooms(result.group_id);
dis.dispatch({
action: 'view_room',
room_id: result.room_id,
});
showCommunityRoomInviteDialog(result.room_id, this.state.name);
} else {
dis.dispatch({
action: 'view_group',
group_id: result.group_id,
group_is_new: true,
});
}
} catch (e) {
console.error(e);
this.setState({
busy: false,
error: _t(
"There was an error creating your community. The name may be taken or the " +
"server is unable to process your request.",
),
});
}
};
private onAvatarChanged = (e: ChangeEvent<HTMLInputElement>) => {
if (!e.target.files || !e.target.files.length) {
this.setState({avatarFile: null});
} else {
this.setState({busy: true});
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = (ev: ProgressEvent<FileReader>) => {
this.setState({avatarFile: file, busy: false, avatarPreview: ev.target.result as string});
};
reader.readAsDataURL(file);
}
};
private onChangeAvatar = () => {
if (this.avatarUploadRef.current) this.avatarUploadRef.current.click();
};
public render() {
let communityId = null;
if (this.state.localpart) {
communityId = (
<span className="mx_CreateCommunityPrototypeDialog_communityId">
{_t("Community ID: +<localpart />:%(domain)s", {
domain: MatrixClientPeg.getHomeserverName(),
}, {
localpart: () => <u>{this.state.localpart}</u>,
})}
<InfoTooltip
tooltip={_t(
"Use this when referencing your community to others. The community ID " +
"cannot be changed.",
)}
/>
</span>
);
}
let helpText = (
<span className="mx_CreateCommunityPrototypeDialog_subtext">
{_t("You can change this later if needed.")}
</span>
);
if (this.state.error) {
const classes = "mx_CreateCommunityPrototypeDialog_subtext mx_CreateCommunityPrototypeDialog_subtext_error";
helpText = (
<span className={classes}>
{this.state.error}
</span>
);
}
let preview = <img src={this.state.avatarPreview} className="mx_CreateCommunityPrototypeDialog_avatar" />;
if (!this.state.avatarPreview) {
preview = <div className="mx_CreateCommunityPrototypeDialog_placeholderAvatar" />
}
return (
<BaseDialog
className="mx_CreateCommunityPrototypeDialog"
onFinished={this.props.onFinished}
title={_t("What's the name of your community or team?")}
>
<form onSubmit={this.onSubmit}>
<div className="mx_Dialog_content">
<div className="mx_CreateCommunityPrototypeDialog_colName">
<Field
value={this.state.name}
onChange={this.onNameChange}
placeholder={_t("Enter name")}
label={_t("Enter name")}
/>
{helpText}
<span className="mx_CreateCommunityPrototypeDialog_subtext">
{/*nbsp is to reserve the height of this element when there's nothing*/}
&nbsp;{communityId}
</span>
<AccessibleButton kind="primary" onClick={this.onSubmit} disabled={this.state.busy}>
{_t("Create")}
</AccessibleButton>
</div>
<div className="mx_CreateCommunityPrototypeDialog_colAvatar">
<input
type="file" style={{display: "none"}}
ref={this.avatarUploadRef} accept="image/*"
onChange={this.onAvatarChanged}
/>
<AccessibleButton
onClick={this.onChangeAvatar}
className="mx_CreateCommunityPrototypeDialog_avatarContainer"
>
{preview}
</AccessibleButton>
<div className="mx_CreateCommunityPrototypeDialog_tip">
<b>{_t("Add image (optional)")}</b>
<span>
{_t("An image will help people identify your community.")}
</span>
</div>
</div>
</div>
</form>
</BaseDialog>
);
}
}

View file

@ -15,46 +15,42 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
export default createReactClass({
displayName: 'CreateGroupDialog',
propTypes: {
export default class CreateGroupDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
},
};
getInitialState: function() {
return {
groupName: '',
groupId: '',
groupError: null,
creating: false,
createError: null,
};
},
state = {
groupName: '',
groupId: '',
groupError: null,
creating: false,
createError: null,
};
_onGroupNameChange: function(e) {
_onGroupNameChange = e => {
this.setState({
groupName: e.target.value,
});
},
};
_onGroupIdChange: function(e) {
_onGroupIdChange = e => {
this.setState({
groupId: e.target.value,
});
},
};
_onGroupIdBlur: function(e) {
_onGroupIdBlur = e => {
this._checkGroupId();
},
};
_checkGroupId: function(e) {
_checkGroupId(e) {
let error = null;
if (!this.state.groupId) {
error = _t("Community IDs cannot be empty.");
@ -67,9 +63,9 @@ export default createReactClass({
createError: null,
});
return error;
},
}
_onFormSubmit: function(e) {
_onFormSubmit = e => {
e.preventDefault();
if (this._checkGroupId()) return;
@ -94,13 +90,13 @@ export default createReactClass({
}).finally(() => {
this.setState({creating: false});
});
},
};
_onCancel: function() {
_onCancel = () => {
this.props.onFinished(false);
},
};
render: function() {
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Spinner = sdk.getComponent('elements.Spinner');
@ -171,5 +167,5 @@ export default createReactClass({
</form>
</BaseDialog>
);
},
});
}
}

Some files were not shown because too many files have changed in this diff Show more