Merge branch 'develop' into hs/bridge-info

This commit is contained in:
Will Hunt 2019-12-30 17:09:05 +01:00 committed by GitHub
commit 0a8cc416bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
203 changed files with 5331 additions and 2496 deletions

View file

@ -422,6 +422,9 @@ export default class ContentMessages {
const UploadConfirmDialog = sdk.getComponent("dialogs.UploadConfirmDialog");
let uploadAll = false;
// Promise to complete before sending next file into room, used for synchronisation of file-sending
// to match the order the files were specified in
let promBefore = Promise.resolve();
for (let i = 0; i < okFiles.length; ++i) {
const file = okFiles[i];
if (!uploadAll) {
@ -440,11 +443,11 @@ export default class ContentMessages {
});
if (!shouldContinue) break;
}
this._sendContentToRoom(file, roomId, matrixClient);
promBefore = this._sendContentToRoom(file, roomId, matrixClient, promBefore);
}
}
_sendContentToRoom(file, roomId, matrixClient) {
_sendContentToRoom(file, roomId, matrixClient, promBefore) {
const content = {
body: file.name || 'Attachment',
info: {
@ -517,7 +520,10 @@ export default class ContentMessages {
content.file = result.file;
content.url = result.url;
});
}).then(function(url) {
}).then((url) => {
// Await previous message being sent into the room
return promBefore;
}).then(function() {
return matrixClient.sendMessage(roomId, content);
}, function(err) {
error = err;

149
src/CrossSigningManager.js Normal file
View file

@ -0,0 +1,149 @@
/*
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 Modal from './Modal';
import sdk from './index';
import MatrixClientPeg from './MatrixClientPeg';
import { deriveKey } from 'matrix-js-sdk/lib/crypto/key_passphrase';
import { decodeRecoveryKey } from 'matrix-js-sdk/lib/crypto/recoverykey';
import { _t } from './languageHandler';
// 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
// during the same single operation. Use `accessSecretStorage` below to scope a
// single secret storage operation, as it will clear the cached keys once the
// operation ends.
let secretStorageKeys = {};
let cachingAllowed = false;
async function getSecretStorageKey({ keys: keyInfos }) {
const keyInfoEntries = Object.entries(keyInfos);
if (keyInfoEntries.length > 1) {
throw new Error("Multiple storage key requests not implemented");
}
const [name, info] = keyInfoEntries[0];
// Check the in-memory cache
if (cachingAllowed && secretStorageKeys[name]) {
return [name, secretStorageKeys[name]];
}
const inputToKey = async ({ passphrase, recoveryKey }) => {
if (passphrase) {
return deriveKey(
passphrase,
info.passphrase.salt,
info.passphrase.iterations,
);
} else {
return decodeRecoveryKey(recoveryKey);
}
};
const AccessSecretStorageDialog =
sdk.getComponent("dialogs.secretstorage.AccessSecretStorageDialog");
const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "",
AccessSecretStorageDialog,
{
keyInfo: info,
checkPrivateKey: async (input) => {
const key = await inputToKey(input);
return MatrixClientPeg.get().checkSecretStoragePrivateKey(key, info.pubkey);
},
},
);
const [input] = await finished;
if (!input) {
throw new Error("Secret storage access canceled");
}
const key = await inputToKey(input);
// Save to cache to avoid future prompts in the current session
if (cachingAllowed) {
secretStorageKeys[name] = key;
}
return [name, key];
}
export const crossSigningCallbacks = {
getSecretStorageKey,
};
/**
* This helper should be used whenever you need to access secret storage. It
* ensures that secret storage (and also cross-signing since they each depend on
* each other in a cycle of sorts) have been bootstrapped before running the
* provided function.
*
* Bootstrapping secret storage may take one of these paths:
* 1. Create secret storage from a passphrase and store cross-signing keys
* in secret storage.
* 2. Access existing secret storage by requesting passphrase and accessing
* cross-signing keys as needed.
* 3. All keys are loaded and there's nothing to do.
*
* Additionally, the secret storage keys are cached during the scope of this function
* to ensure the user is prompted only once for their secret storage
* passphrase. The cache is then
*
* @param {Function} [func] An operation to perform once secret storage has been
* bootstrapped. Optional.
*/
export async function accessSecretStorage(func = async () => { }) {
const cli = MatrixClientPeg.get();
cachingAllowed = true;
try {
if (!cli.hasSecretStorageKey()) {
// 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"),
null, null, /* priority = */ false, /* static = */ true,
);
const [confirmed] = await finished;
if (!confirmed) {
throw new Error("Secret storage creation canceled");
}
} else {
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
await cli.bootstrapSecretStorage({
authUploadDeviceSigningKeys: async (makeRequest) => {
const { finished } = Modal.createTrackedDialog(
'Cross-signing keys dialog', '', InteractiveAuthDialog,
{
title: _t("Send cross-signing keys to homeserver"),
matrixClient: MatrixClientPeg.get(),
makeRequest,
},
);
const [confirmed] = await finished;
if (!confirmed) {
throw new Error("Cross-signing key upload auth canceled");
}
},
});
}
// `return await` needed here to ensure `finally` block runs after the
// inner operation completes.
return await func();
} finally {
// Clear secret storage key cache now that work is complete
cachingAllowed = false;
secretStorageKeys = {};
}
}

View file

@ -32,9 +32,9 @@ import classNames from 'classnames';
import MatrixClientPeg from './MatrixClientPeg';
import url from 'url';
import EMOJIBASE from 'emojibase-data/en/compact.json';
import EMOJIBASE_REGEX from 'emojibase-regex';
import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji";
linkifyMatrix(linkify);
@ -58,8 +58,6 @@ const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet'];
const VARIATION_SELECTOR = String.fromCharCode(0xFE0F);
/*
* Return true if the given string contains emoji
* Uses a much, much simpler regex than emojibase's so will give false
@ -71,21 +69,6 @@ function mightContainEmoji(str) {
return SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(str);
}
/**
* Find emoji data in emojibase by character.
*
* @param {String} char The emoji character
* @return {Object} The emoji data
*/
export function findEmojiData(char) {
// Check against both the char and the char with an empty variation selector
// appended because that's how emojibase stores its base emojis which have
// variations.
// See also https://github.com/vector-im/riot-web/issues/9785.
const emptyVariation = char + VARIATION_SELECTOR;
return EMOJIBASE.find(e => e.unicode === char || e.unicode === emptyVariation);
}
/**
* Returns the shortcode for an emoji character.
*
@ -93,7 +76,7 @@ export function findEmojiData(char) {
* @return {String} The shortcode (such as :thumbup:)
*/
export function unicodeToShortcode(char) {
const data = findEmojiData(char);
const data = getEmojiFromUnicode(char);
return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : '');
}
@ -105,7 +88,7 @@ export function unicodeToShortcode(char) {
*/
export function shortcodeToUnicode(shortcode) {
shortcode = shortcode.slice(1, shortcode.length - 1);
const data = EMOJIBASE.find(e => e.shortcodes && e.shortcodes.includes(shortcode));
const data = SHORTCODE_TO_EMOJI.get(shortcode);
return data ? data.unicode : null;
}

View file

@ -1,6 +1,7 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -15,52 +16,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/* a selection of key codes, as used in KeyboardEvent.keyCode */
export const KeyCode = {
BACKSPACE: 8,
TAB: 9,
ENTER: 13,
SHIFT: 16,
ESCAPE: 27,
SPACE: 32,
PAGE_UP: 33,
PAGE_DOWN: 34,
END: 35,
HOME: 36,
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40,
DELETE: 46,
KEY_A: 65,
KEY_B: 66,
KEY_C: 67,
KEY_D: 68,
KEY_E: 69,
KEY_F: 70,
KEY_G: 71,
KEY_H: 72,
KEY_I: 73,
KEY_J: 74,
KEY_K: 75,
KEY_L: 76,
KEY_M: 77,
KEY_N: 78,
KEY_O: 79,
KEY_P: 80,
KEY_Q: 81,
KEY_R: 82,
KEY_S: 83,
KEY_T: 84,
KEY_U: 85,
KEY_V: 86,
KEY_W: 87,
KEY_X: 88,
KEY_Y: 89,
KEY_Z: 90,
KEY_BACKTICK: 223, // DO NOT USE THIS: browsers disagree on backtick 192 vs 223
};
export const Key = {
HOME: "Home",
END: "End",
@ -80,13 +35,35 @@ export const Key = {
SHIFT: "Shift",
CONTEXT_MENU: "ContextMenu",
COMMA: ",",
LESS_THAN: "<",
GREATER_THAN: ">",
BACKTICK: "`",
SPACE: " ",
A: "a",
B: "b",
C: "c",
D: "d",
E: "e",
F: "f",
G: "g",
H: "h",
I: "i",
J: "j",
K: "k",
L: "l",
M: "m",
N: "n",
O: "o",
P: "p",
Q: "q",
R: "r",
S: "s",
T: "t",
U: "u",
V: "v",
W: "w",
X: "x",
Y: "y",
Z: "z",
};

View file

@ -1,7 +1,8 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd.
Copyright 2017 New Vector Ltd
Copyright 2017, 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.
@ -30,6 +31,7 @@ import {verificationMethods} from 'matrix-js-sdk/lib/crypto';
import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler";
import * as StorageManager from './utils/StorageManager';
import IdentityAuthClient from './IdentityAuthClient';
import { crossSigningCallbacks } from './CrossSigningManager';
interface MatrixClientCreds {
homeserverUrl: string,
@ -220,14 +222,9 @@ class MatrixClientPeg {
identityServer: new IdentityAuthClient(),
};
opts.cryptoCallbacks = {};
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
// TODO: Cross-signing keys are temporarily in memory only. A
// separate task in the cross-signing project will build from here.
const keys = [];
opts.cryptoCallbacks = {
getCrossSigningKey: k => keys[k],
saveCrossSigningKeys: newKeys => Object.assign(keys, newKeys),
};
Object.assign(opts.cryptoCallbacks, crossSigningCallbacks);
}
this.matrixClient = createMatrixClient(opts);

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 {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList";
function textForMemberEvent(ev) {
// XXX: SYJS-16 "sender is sometimes null for join messages"
@ -506,6 +507,87 @@ function textForWidgetEvent(event) {
}
}
function textForMjolnirEvent(event) {
const senderName = event.getSender();
const {entity: prevEntity} = event.getPrevContent();
const {entity, recommendation, reason} = event.getContent();
// Rule removed
if (!entity) {
if (USER_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s removed the rule banning users matching %(glob)s",
{senderName, glob: prevEntity});
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s removed the rule banning rooms matching %(glob)s",
{senderName, glob: prevEntity});
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s removed the rule banning servers matching %(glob)s",
{senderName, glob: prevEntity});
}
// Unknown type. We'll say something, but we shouldn't end up here.
return _t("%(senderName)s removed a ban rule matching %(glob)s", {senderName, glob: prevEntity});
}
// Invalid rule
if (!recommendation || !reason) return _t(`%(senderName)s updated an invalid ban rule`, {senderName});
// Rule updated
if (entity === prevEntity) {
if (USER_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason});
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason});
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason});
}
// Unknown type. We'll say something but we shouldn't end up here.
return _t("%(senderName)s updated a ban rule matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason});
}
// New rule
if (!prevEntity) {
if (USER_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s created a rule banning users matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason});
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason});
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason});
}
// Unknown type. We'll say something but we shouldn't end up here.
return _t("%(senderName)s created a ban rule matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason});
}
// else the entity !== prevEntity - count as a removal & add
if (USER_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " +
"%(newGlob)s for %(reason)s",
{senderName, oldGlob: prevEntity, newGlob: entity, reason});
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " +
"%(newGlob)s for %(reason)s",
{senderName, oldGlob: prevEntity, newGlob: entity, reason});
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " +
"%(newGlob)s for %(reason)s",
{senderName, oldGlob: prevEntity, newGlob: entity, reason});
}
// Unknown type. We'll say something but we shouldn't end up here.
return _t("%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s " +
"for %(reason)s", {senderName, oldGlob: prevEntity, newGlob: entity, reason});
}
const handlers = {
'm.room.message': textForMessageEvent,
'm.call.invite': textForCallInviteEvent,
@ -533,6 +615,11 @@ const stateHandlers = {
'im.vector.modular.widgets': textForWidgetEvent,
};
// Add all the Mjolnir stuff to the renderer
for (const evType of ALL_RULE_TYPES) {
stateHandlers[evType] = textForMjolnirEvent;
}
module.exports = {
textForEvent: function(ev) {
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];

View file

@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {Key} from "../../../Keyboard";
const React = require("react");
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
@ -83,7 +85,7 @@ module.exports = createReactClass({
},
onKeyDown: function(e) {
if (e.keyCode === 27) { // escape
if (e.key === Key.ESCAPE) {
e.stopPropagation();
e.preventDefault();
this.props.onFinished(false);

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import FileSaver from 'file-saver';
import React from 'react';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
@ -44,6 +44,9 @@ export default createReactClass({
componentWillMount: function() {
this._unmounted = false;
this._passphrase1 = createRef();
this._passphrase2 = createRef();
},
componentWillUnmount: function() {
@ -53,8 +56,8 @@ export default createReactClass({
_onPassphraseFormSubmit: function(ev) {
ev.preventDefault();
const passphrase = this.refs.passphrase1.value;
if (passphrase !== this.refs.passphrase2.value) {
const passphrase = this._passphrase1.current.value;
if (passphrase !== this._passphrase2.current.value) {
this.setState({errStr: _t('Passphrases must match')});
return false;
}
@ -148,7 +151,7 @@ export default createReactClass({
</label>
</div>
<div className='mx_E2eKeysDialog_inputCell'>
<input ref='passphrase1' id='passphrase1'
<input ref={this._passphrase1} id='passphrase1'
autoFocus={true} size='64' type='password'
disabled={disableForm}
/>
@ -161,7 +164,7 @@ export default createReactClass({
</label>
</div>
<div className='mx_E2eKeysDialog_inputCell'>
<input ref='passphrase2' id='passphrase2'
<input ref={this._passphrase2} id='passphrase2'
size='64' type='password'
disabled={disableForm}
/>

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
@ -56,6 +56,9 @@ export default createReactClass({
componentWillMount: function() {
this._unmounted = false;
this._file = createRef();
this._passphrase = createRef();
},
componentWillUnmount: function() {
@ -63,15 +66,15 @@ export default createReactClass({
},
_onFormChange: function(ev) {
const files = this.refs.file.files || [];
const files = this._file.current.files || [];
this.setState({
enableSubmit: (this.refs.passphrase.value !== "" && files.length > 0),
enableSubmit: (this._passphrase.current.value !== "" && files.length > 0),
});
},
_onFormSubmit: function(ev) {
ev.preventDefault();
this._startImport(this.refs.file.files[0], this.refs.passphrase.value);
this._startImport(this._file.current.files[0], this._passphrase.current.value);
return false;
},
@ -146,7 +149,10 @@ export default createReactClass({
</label>
</div>
<div className='mx_E2eKeysDialog_inputCell'>
<input ref='file' id='importFile' type='file'
<input
ref={this._file}
id='importFile'
type='file'
autoFocus={true}
onChange={this._onFormChange}
disabled={disableForm} />
@ -159,8 +165,11 @@ export default createReactClass({
</label>
</div>
<div className='mx_E2eKeysDialog_inputCell'>
<input ref='passphrase' id='passphrase'
size='64' type='password'
<input
ref={this._passphrase}
id='passphrase'
size='64'
type='password'
onChange={this._onFormChange}
disabled={disableForm} />
</div>

View file

@ -1,5 +1,6 @@
/*
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.
@ -15,13 +16,11 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import FileSaver from 'file-saver';
import sdk from '../../../../index';
import MatrixClientPeg from '../../../../MatrixClientPeg';
import { scorePassword } from '../../../../utils/PasswordScorer';
import FileSaver from 'file-saver';
import { _t } from '../../../../languageHandler';
const PHASE_PASSPHRASE = 0;
@ -45,13 +44,15 @@ function selectText(target) {
selection.addRange(range);
}
/**
/*
* Walks the user through the process of creating an e2e key backup
* on the server.
*/
export default createReactClass({
getInitialState: function() {
return {
export default class CreateKeyBackupDialog extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
phase: PHASE_PASSPHRASE,
passPhrase: '',
passPhraseConfirm: '',
@ -60,25 +61,25 @@ export default createReactClass({
zxcvbnResult: null,
setPassPhrase: false,
};
},
}
componentWillMount: function() {
componentWillMount() {
this._recoveryKeyNode = null;
this._keyBackupInfo = null;
this._setZxcvbnResultTimeout = null;
},
}
componentWillUnmount: function() {
componentWillUnmount() {
if (this._setZxcvbnResultTimeout !== null) {
clearTimeout(this._setZxcvbnResultTimeout);
}
},
}
_collectRecoveryKeyNode: function(n) {
_collectRecoveryKeyNode = (n) => {
this._recoveryKeyNode = n;
},
}
_onCopyClick: function() {
_onCopyClick = () => {
selectText(this._recoveryKeyNode);
const successful = document.execCommand('copy');
if (successful) {
@ -87,9 +88,9 @@ export default createReactClass({
phase: PHASE_KEEPITSAFE,
});
}
},
}
_onDownloadClick: function() {
_onDownloadClick = () => {
const blob = new Blob([this._keyBackupInfo.recovery_key], {
type: 'text/plain;charset=us-ascii',
});
@ -99,9 +100,9 @@ export default createReactClass({
downloaded: true,
phase: PHASE_KEEPITSAFE,
});
},
}
_createBackup: async function() {
_createBackup = async () => {
this.setState({
phase: PHASE_BACKINGUP,
error: null,
@ -116,7 +117,7 @@ export default createReactClass({
phase: PHASE_DONE,
});
} catch (e) {
console.log("Error creating key backup", e);
console.error("Error creating key backup", e);
// TODO: If creating a version succeeds, but backup fails, should we
// delete the version, disable backup, or do nothing? If we just
// disable without deleting, we'll enable on next app reload since
@ -128,38 +129,38 @@ export default createReactClass({
error: e,
});
}
},
}
_onCancel: function() {
_onCancel = () => {
this.props.onFinished(false);
},
}
_onDone: function() {
_onDone = () => {
this.props.onFinished(true);
},
}
_onOptOutClick: function() {
_onOptOutClick = () => {
this.setState({phase: PHASE_OPTOUT_CONFIRM});
},
}
_onSetUpClick: function() {
_onSetUpClick = () => {
this.setState({phase: PHASE_PASSPHRASE});
},
}
_onSkipPassPhraseClick: async function() {
_onSkipPassPhraseClick = async () => {
this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion();
this.setState({
copied: false,
downloaded: false,
phase: PHASE_SHOWKEY,
});
},
}
_onPassPhraseNextClick: function() {
_onPassPhraseNextClick = () => {
this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
},
}
_onPassPhraseKeyPress: async function(e) {
_onPassPhraseKeyPress = async (e) => {
if (e.key === 'Enter') {
// If we're waiting for the timeout before updating the result at this point,
// skip ahead and do it now, otherwise we'll deny the attempt to proceed
@ -177,9 +178,9 @@ export default createReactClass({
this._onPassPhraseNextClick();
}
}
},
}
_onPassPhraseConfirmNextClick: async function() {
_onPassPhraseConfirmNextClick = async () => {
this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(this.state.passPhrase);
this.setState({
setPassPhrase: true,
@ -187,30 +188,30 @@ export default createReactClass({
downloaded: false,
phase: PHASE_SHOWKEY,
});
},
}
_onPassPhraseConfirmKeyPress: function(e) {
_onPassPhraseConfirmKeyPress = (e) => {
if (e.key === 'Enter' && this.state.passPhrase === this.state.passPhraseConfirm) {
this._onPassPhraseConfirmNextClick();
}
},
}
_onSetAgainClick: function() {
_onSetAgainClick = () => {
this.setState({
passPhrase: '',
passPhraseConfirm: '',
phase: PHASE_PASSPHRASE,
zxcvbnResult: null,
});
},
}
_onKeepItSafeBackClick: function() {
_onKeepItSafeBackClick = () => {
this.setState({
phase: PHASE_SHOWKEY,
});
},
}
_onPassPhraseChange: function(e) {
_onPassPhraseChange = (e) => {
this.setState({
passPhrase: e.target.value,
});
@ -227,19 +228,19 @@ export default createReactClass({
zxcvbnResult: scorePassword(this.state.passPhrase),
});
}, PASSPHRASE_FEEDBACK_DELAY);
},
}
_onPassPhraseConfirmChange: function(e) {
_onPassPhraseConfirmChange = (e) => {
this.setState({
passPhraseConfirm: e.target.value,
});
},
}
_passPhraseIsValid: function() {
_passPhraseIsValid() {
return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE;
},
}
_renderPhasePassPhrase: function() {
_renderPhasePassPhrase() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let strengthMeter;
@ -266,7 +267,7 @@ export default createReactClass({
return <div>
<p>{_t(
"<b>Warning</b>: you should only set up key backup from a trusted computer.", {},
"<b>Warning</b>: You should only set up key backup from a trusted computer.", {},
{ b: sub => <b>{sub}</b> },
)}</p>
<p>{_t(
@ -305,9 +306,9 @@ export default createReactClass({
</button></p>
</details>
</div>;
},
}
_renderPhasePassPhraseConfirm: function() {
_renderPhasePassPhraseConfirm() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let matchText;
@ -361,9 +362,9 @@ export default createReactClass({
disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
/>
</div>;
},
}
_renderPhaseShowKey: function() {
_renderPhaseShowKey() {
let bodyText;
if (this.state.setPassPhrase) {
bodyText = _t(
@ -380,7 +381,7 @@ export default createReactClass({
"access to your encrypted messages if you forget your passphrase.",
)}</p>
<p>{_t(
"Keep your recovery key somewhere very secure, like a password manager (or a safe)",
"Keep your recovery key somewhere very secure, like a password manager (or a safe).",
)}</p>
<p>{bodyText}</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer">
@ -402,18 +403,18 @@ export default createReactClass({
</div>
</div>
</div>;
},
}
_renderPhaseKeepItSafe: function() {
_renderPhaseKeepItSafe() {
let introText;
if (this.state.copied) {
introText = _t(
"Your Recovery Key has been <b>copied to your clipboard</b>, paste it to:",
"Your recovery key has been <b>copied to your clipboard</b>, paste it to:",
{}, {b: s => <b>{s}</b>},
);
} else if (this.state.downloaded) {
introText = _t(
"Your Recovery Key is in your <b>Downloads</b> folder.",
"Your recovery key is in your <b>Downloads</b> folder.",
{}, {b: s => <b>{s}</b>},
);
}
@ -431,16 +432,16 @@ export default createReactClass({
<button onClick={this._onKeepItSafeBackClick}>{_t("Back")}</button>
</DialogButtons>
</div>;
},
}
_renderBusyPhase: function(text) {
_renderBusyPhase(text) {
const Spinner = sdk.getComponent('views.elements.Spinner');
return <div>
<Spinner />
</div>;
},
}
_renderPhaseDone: function() {
_renderPhaseDone() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
<p>{_t(
@ -451,9 +452,9 @@ export default createReactClass({
hasCancel={false}
/>
</div>;
},
}
_renderPhaseOptOutConfirm: function() {
_renderPhaseOptOutConfirm() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
{_t(
@ -467,9 +468,9 @@ export default createReactClass({
<button onClick={this._onCancel}>I understand, continue without</button>
</DialogButtons>
</div>;
},
}
_titleForPhase: function(phase) {
_titleForPhase(phase) {
switch (phase) {
case PHASE_PASSPHRASE:
return _t('Secure your backup with a passphrase');
@ -488,9 +489,9 @@ export default createReactClass({
default:
return _t("Create Key Backup");
}
},
}
render: function() {
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
let content;
@ -543,5 +544,5 @@ export default createReactClass({
</div>
</BaseDialog>
);
},
});
}
}

View file

@ -0,0 +1,611 @@
/*
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 sdk from '../../../../index';
import MatrixClientPeg from '../../../../MatrixClientPeg';
import { scorePassword } from '../../../../utils/PasswordScorer';
import FileSaver from 'file-saver';
import { _t } from '../../../../languageHandler';
import Modal from '../../../../Modal';
const PHASE_LOADING = 0;
const PHASE_MIGRATE = 1;
const PHASE_PASSPHRASE = 2;
const PHASE_PASSPHRASE_CONFIRM = 3;
const PHASE_SHOWKEY = 4;
const PHASE_KEEPITSAFE = 5;
const PHASE_STORING = 6;
const PHASE_DONE = 7;
const PHASE_OPTOUT_CONFIRM = 8;
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms.
// XXX: copied from ShareDialog: factor out into utils
function selectText(target) {
const range = document.createRange();
range.selectNodeContents(target);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
/*
* Walks the user through the process of creating a passphrase to guard Secure
* Secret Storage in account data.
*/
export default class CreateSecretStorageDialog extends React.PureComponent {
constructor(props) {
super(props);
this._keyInfo = null;
this._encodedRecoveryKey = null;
this._recoveryKeyNode = null;
this._setZxcvbnResultTimeout = null;
this.state = {
phase: PHASE_LOADING,
passPhrase: '',
passPhraseConfirm: '',
copied: false,
downloaded: false,
zxcvbnResult: null,
setPassPhrase: false,
};
this._fetchBackupInfo();
}
componentWillUnmount() {
if (this._setZxcvbnResultTimeout !== null) {
clearTimeout(this._setZxcvbnResultTimeout);
}
}
async _fetchBackupInfo() {
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
this.setState({
phase: backupInfo ? PHASE_MIGRATE: PHASE_PASSPHRASE,
backupInfo,
});
}
_collectRecoveryKeyNode = (n) => {
this._recoveryKeyNode = n;
}
_onMigrateNextClick = () => {
this._bootstrapSecretStorage();
}
_onCopyClick = () => {
selectText(this._recoveryKeyNode);
const successful = document.execCommand('copy');
if (successful) {
this.setState({
copied: true,
phase: PHASE_KEEPITSAFE,
});
}
}
_onDownloadClick = () => {
const blob = new Blob([this._encodedRecoveryKey], {
type: 'text/plain;charset=us-ascii',
});
FileSaver.saveAs(blob, 'recovery-key.txt');
this.setState({
downloaded: true,
phase: PHASE_KEEPITSAFE,
});
}
_bootstrapSecretStorage = async () => {
this.setState({
phase: PHASE_STORING,
error: null,
});
const cli = MatrixClientPeg.get();
try {
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
await cli.bootstrapSecretStorage({
authUploadDeviceSigningKeys: async (makeRequest) => {
const { finished } = Modal.createTrackedDialog(
'Cross-signing keys dialog', '', InteractiveAuthDialog,
{
title: _t("Send cross-signing keys to homeserver"),
matrixClient: MatrixClientPeg.get(),
makeRequest,
},
);
const [confirmed] = await finished;
if (!confirmed) {
throw new Error("Cross-signing key upload auth canceled");
}
},
createSecretStorageKey: async () => this._keyInfo,
keyBackupInfo: this.state.backupInfo,
});
this.setState({
phase: PHASE_DONE,
});
} catch (e) {
this.setState({ error: e });
console.error("Error bootstrapping secret storage", e);
}
}
_onCancel = () => {
this.props.onFinished(false);
}
_onDone = () => {
this.props.onFinished(true);
}
_onOptOutClick = () => {
this.setState({phase: PHASE_OPTOUT_CONFIRM});
}
_onSetUpClick = () => {
this.setState({phase: PHASE_PASSPHRASE});
}
_onSkipPassPhraseClick = async () => {
const [keyInfo, encodedRecoveryKey] =
await MatrixClientPeg.get().createRecoveryKeyFromPassphrase();
this._keyInfo = keyInfo;
this._encodedRecoveryKey = encodedRecoveryKey;
this.setState({
copied: false,
downloaded: false,
phase: PHASE_SHOWKEY,
});
}
_onPassPhraseNextClick = () => {
this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
}
_onPassPhraseKeyPress = async (e) => {
if (e.key === 'Enter') {
// If we're waiting for the timeout before updating the result at this point,
// skip ahead and do it now, otherwise we'll deny the attempt to proceed
// even if the user entered a valid passphrase
if (this._setZxcvbnResultTimeout !== null) {
clearTimeout(this._setZxcvbnResultTimeout);
this._setZxcvbnResultTimeout = null;
await new Promise((resolve) => {
this.setState({
zxcvbnResult: scorePassword(this.state.passPhrase),
}, resolve);
});
}
if (this._passPhraseIsValid()) {
this._onPassPhraseNextClick();
}
}
}
_onPassPhraseConfirmNextClick = async () => {
const [keyInfo, encodedRecoveryKey] =
await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(this.state.passPhrase);
this._keyInfo = keyInfo;
this._encodedRecoveryKey = encodedRecoveryKey;
this.setState({
setPassPhrase: true,
copied: false,
downloaded: false,
phase: PHASE_SHOWKEY,
});
}
_onPassPhraseConfirmKeyPress = (e) => {
if (e.key === 'Enter' && this.state.passPhrase === this.state.passPhraseConfirm) {
this._onPassPhraseConfirmNextClick();
}
}
_onSetAgainClick = () => {
this.setState({
passPhrase: '',
passPhraseConfirm: '',
phase: PHASE_PASSPHRASE,
zxcvbnResult: null,
});
}
_onKeepItSafeBackClick = () => {
this.setState({
phase: PHASE_SHOWKEY,
});
}
_onPassPhraseChange = (e) => {
this.setState({
passPhrase: e.target.value,
});
if (this._setZxcvbnResultTimeout !== null) {
clearTimeout(this._setZxcvbnResultTimeout);
}
this._setZxcvbnResultTimeout = setTimeout(() => {
this._setZxcvbnResultTimeout = null;
this.setState({
// precompute this and keep it in state: zxcvbn is fast but
// we use it in a couple of different places so no point recomputing
// it unnecessarily.
zxcvbnResult: scorePassword(this.state.passPhrase),
});
}, PASSPHRASE_FEEDBACK_DELAY);
}
_onPassPhraseConfirmChange = (e) => {
this.setState({
passPhraseConfirm: e.target.value,
});
}
_passPhraseIsValid() {
return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE;
}
_renderPhaseMigrate() {
// TODO: This is a temporary screen so people who have the labs flag turned on and
// click the button are aware they're making a change to their account.
// Once we're confident enough in this (and it's supported enough) we can do
// it automatically.
// https://github.com/vector-im/riot-web/issues/11696
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
<p>{_t(
"Secret Storage will be set up using your existing key backup details." +
"Your secret storage passphrase and recovery key will be the same as " +
" they were for your key backup",
)}</p>
<DialogButtons primaryButton={_t('Next')}
onPrimaryButtonClick={this._onMigrateNextClick}
hasCancel={true}
onCancel={this._onCancel}
/>
</div>;
}
_renderPhasePassPhrase() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let strengthMeter;
let helpText;
if (this.state.zxcvbnResult) {
if (this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE) {
helpText = _t("Great! This passphrase looks strong enough.");
} else {
const suggestions = [];
for (let i = 0; i < this.state.zxcvbnResult.feedback.suggestions.length; ++i) {
suggestions.push(<div key={i}>{this.state.zxcvbnResult.feedback.suggestions[i]}</div>);
}
const suggestionBlock = <div>{suggestions.length > 0 ? suggestions : _t("Keep going...")}</div>;
helpText = <div>
{this.state.zxcvbnResult.feedback.warning}
{suggestionBlock}
</div>;
}
strengthMeter = <div>
<progress max={PASSWORD_MIN_SCORE} value={this.state.zxcvbnResult.score} />
</div>;
}
return <div>
<p>{_t(
"<b>Warning</b>: You should only set up secret storage from a trusted computer.", {},
{ b: sub => <b>{sub}</b> },
)}</p>
<p>{_t(
"We'll use secret storage to optionally store an encrypted copy of " +
"your cross-signing identity for verifying other devices and message " +
"keys on our server. Protect your access to encrypted messages with a " +
"passphrase to keep it secure.",
)}</p>
<p>{_t("For maximum security, this should be different from your account password.")}</p>
<div className="mx_CreateSecretStorageDialog_primaryContainer">
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
<input type="password"
onChange={this._onPassPhraseChange}
onKeyPress={this._onPassPhraseKeyPress}
value={this.state.passPhrase}
className="mx_CreateSecretStorageDialog_passPhraseInput"
placeholder={_t("Enter a passphrase...")}
autoFocus={true}
/>
<div className="mx_CreateSecretStorageDialog_passPhraseHelp">
{strengthMeter}
{helpText}
</div>
</div>
</div>
<DialogButtons primaryButton={_t('Next')}
onPrimaryButtonClick={this._onPassPhraseNextClick}
hasCancel={false}
disabled={!this._passPhraseIsValid()}
/>
<details>
<summary>{_t("Advanced")}</summary>
<p><button onClick={this._onSkipPassPhraseClick} >
{_t("Set up with a recovery key")}
</button></p>
</details>
</div>;
}
_renderPhasePassPhraseConfirm() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let matchText;
if (this.state.passPhraseConfirm === this.state.passPhrase) {
matchText = _t("That matches!");
} else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) {
// only tell them they're wrong if they've actually gone wrong.
// Security concious readers will note that if you left riot-web unattended
// on this screen, this would make it easy for a malicious person to guess
// your passphrase one letter at a time, but they could get this faster by
// just opening the browser's developer tools and reading it.
// Note that not having typed anything at all will not hit this clause and
// fall through so empty box === no hint.
matchText = _t("That doesn't match.");
}
let passPhraseMatch = null;
if (matchText) {
passPhraseMatch = <div className="mx_CreateSecretStorageDialog_passPhraseMatch">
<div>{matchText}</div>
<div>
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
{_t("Go back to set it again.")}
</AccessibleButton>
</div>
</div>;
}
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
<p>{_t(
"Please enter your passphrase a second time to confirm.",
)}</p>
<div className="mx_CreateSecretStorageDialog_primaryContainer">
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
<div>
<input type="password"
onChange={this._onPassPhraseConfirmChange}
onKeyPress={this._onPassPhraseConfirmKeyPress}
value={this.state.passPhraseConfirm}
className="mx_CreateSecretStorageDialog_passPhraseInput"
placeholder={_t("Repeat your passphrase...")}
autoFocus={true}
/>
</div>
{passPhraseMatch}
</div>
</div>
<DialogButtons primaryButton={_t('Next')}
onPrimaryButtonClick={this._onPassPhraseConfirmNextClick}
hasCancel={false}
disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
/>
</div>;
}
_renderPhaseShowKey() {
let bodyText;
if (this.state.setPassPhrase) {
bodyText = _t(
"As a safety net, you can use it to restore your access to encrypted " +
"messages if you forget your passphrase.",
);
} else {
bodyText = _t(
"As a safety net, you can use it to restore your access to encrypted " +
"messages.",
);
}
return <div>
<p>{_t(
"Your recovery key is a safety net - you can use it to restore " +
"access to your encrypted messages if you forget your passphrase.",
)}</p>
<p>{_t(
"Keep your recovery key somewhere very secure, like a password manager (or a safe).",
)}</p>
<p>{bodyText}</p>
<div className="mx_CreateSecretStorageDialog_primaryContainer">
<div className="mx_CreateSecretStorageDialog_recoveryKeyHeader">
{_t("Your Recovery Key")}
</div>
<div className="mx_CreateSecretStorageDialog_recoveryKeyContainer">
<div className="mx_CreateSecretStorageDialog_recoveryKey">
<code ref={this._collectRecoveryKeyNode}>{this._encodedRecoveryKey}</code>
</div>
<div className="mx_CreateSecretStorageDialog_recoveryKeyButtons">
<button className="mx_Dialog_primary" onClick={this._onCopyClick}>
{_t("Copy to clipboard")}
</button>
<button className="mx_Dialog_primary" onClick={this._onDownloadClick}>
{_t("Download")}
</button>
</div>
</div>
</div>
</div>;
}
_renderPhaseKeepItSafe() {
let introText;
if (this.state.copied) {
introText = _t(
"Your recovery key has been <b>copied to your clipboard</b>, paste it to:",
{}, {b: s => <b>{s}</b>},
);
} else if (this.state.downloaded) {
introText = _t(
"Your recovery key is in your <b>Downloads</b> folder.",
{}, {b: s => <b>{s}</b>},
);
}
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
{introText}
<ul>
<li>{_t("<b>Print it</b> and store it somewhere safe", {}, {b: s => <b>{s}</b>})}</li>
<li>{_t("<b>Save it</b> on a USB key or backup drive", {}, {b: s => <b>{s}</b>})}</li>
<li>{_t("<b>Copy it</b> to your personal cloud storage", {}, {b: s => <b>{s}</b>})}</li>
</ul>
<DialogButtons primaryButton={_t("OK")}
onPrimaryButtonClick={this._bootstrapSecretStorage}
hasCancel={false}>
<button onClick={this._onKeepItSafeBackClick}>{_t("Back")}</button>
</DialogButtons>
</div>;
}
_renderBusyPhase() {
const Spinner = sdk.getComponent('views.elements.Spinner');
return <div>
<Spinner />
</div>;
}
_renderPhaseDone() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
<p>{_t(
"Your access to encrypted messages is now protected.",
)}</p>
<DialogButtons primaryButton={_t('OK')}
onPrimaryButtonClick={this._onDone}
hasCancel={false}
/>
</div>;
}
_renderPhaseOptOutConfirm() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
{_t(
"Without setting up secret storage, you won't be able to restore your " +
"access to encrypted messages or your cross-signing identity for " +
"verifying other devices if you log out or use another device.",
)}
<DialogButtons primaryButton={_t('Set up secret storage')}
onPrimaryButtonClick={this._onSetUpClick}
hasCancel={false}
>
<button onClick={this._onCancel}>I understand, continue without</button>
</DialogButtons>
</div>;
}
_titleForPhase(phase) {
switch (phase) {
case PHASE_MIGRATE:
return _t('Migrate from Key Backup');
case PHASE_PASSPHRASE:
return _t('Secure your encrypted messages with a passphrase');
case PHASE_PASSPHRASE_CONFIRM:
return _t('Confirm your passphrase');
case PHASE_OPTOUT_CONFIRM:
return _t('Warning!');
case PHASE_SHOWKEY:
return _t('Recovery key');
case PHASE_KEEPITSAFE:
return _t('Keep it safe');
case PHASE_STORING:
return _t('Storing secrets...');
case PHASE_DONE:
return _t('Success!');
default:
return null;
}
}
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
let content;
if (this.state.error) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
content = <div>
<p>{_t("Unable to set up secret storage")}</p>
<div className="mx_Dialog_buttons">
<DialogButtons primaryButton={_t('Retry')}
onPrimaryButtonClick={this._bootstrapSecretStorage}
hasCancel={true}
onCancel={this._onCancel}
/>
</div>
</div>;
} else {
switch (this.state.phase) {
case PHASE_LOADING:
content = this._renderBusyPhase();
break;
case PHASE_MIGRATE:
content = this._renderPhaseMigrate();
break;
case PHASE_PASSPHRASE:
content = this._renderPhasePassPhrase();
break;
case PHASE_PASSPHRASE_CONFIRM:
content = this._renderPhasePassPhraseConfirm();
break;
case PHASE_SHOWKEY:
content = this._renderPhaseShowKey();
break;
case PHASE_KEEPITSAFE:
content = this._renderPhaseKeepItSafe();
break;
case PHASE_STORING:
content = this._renderBusyPhase();
break;
case PHASE_DONE:
content = this._renderPhaseDone();
break;
case PHASE_OPTOUT_CONFIRM:
content = this._renderPhaseOptOutConfirm();
break;
}
}
return (
<BaseDialog className='mx_CreateSecretStorageDialog'
onFinished={this.props.onFinished}
title={this._titleForPhase(this.state.phase)}
hasCancel={[PHASE_PASSPHRASE, PHASE_DONE].includes(this.state.phase)}
>
<div>
{content}
</div>
</BaseDialog>
);
}
}

View file

@ -2,6 +2,7 @@
Copyright 2016 Aviral Dasgupta
Copyright 2017 Vector Creations Ltd
Copyright 2017, 2018 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.
@ -28,7 +29,7 @@ import SettingsStore from "../settings/SettingsStore";
import { shortcodeToUnicode } from '../HtmlUtils';
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
import EmojiData from '../stripped-emoji.json';
import EMOJIBASE from 'emojibase-data/en/compact.json';
const LIMIT = 20;
@ -38,19 +39,15 @@ const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|:[+-\\w]*:?)$', '
// XXX: it's very unclear why we bother with this generated emojidata file.
// all it means is that we end up bloating the bundle with precomputed stuff
// which would be trivial to calculate and cache on demand.
const EMOJI_SHORTNAMES = Object.keys(EmojiData).map((key) => EmojiData[key]).sort(
(a, b) => {
if (a.category === b.category) {
return a.emoji_order - b.emoji_order;
}
return a.category - b.category;
},
).map((a, index) => {
const EMOJI_SHORTNAMES = EMOJIBASE.sort((a, b) => {
if (a.group === b.group) {
return a.order - b.order;
}
return a.group - b.group;
}).map((emoji, index) => {
return {
name: a.name,
shortname: a.shortname,
aliases: a.aliases ? a.aliases.join(' ') : '',
aliases_ascii: a.aliases_ascii ? a.aliases_ascii.join(' ') : '',
emoji,
shortname: `:${emoji.shortcodes[0]}:`,
// Include the index so that we can preserve the original order
_orderBy: index,
};
@ -69,12 +66,15 @@ export default class EmojiProvider extends AutocompleteProvider {
constructor() {
super(EMOJI_REGEX);
this.matcher = new QueryMatcher(EMOJI_SHORTNAMES, {
keys: ['aliases_ascii', 'shortname', 'aliases'],
keys: ['emoji.emoticon', 'shortname'],
funcs: [
(o) => o.emoji.shortcodes.length > 1 ? o.emoji.shortcodes.slice(1).map(s => `:${s}:`).join(" ") : "", // aliases
],
// For matching against ascii equivalents
shouldMatchWordsOnly: false,
});
this.nameMatcher = new QueryMatcher(EMOJI_SHORTNAMES, {
keys: ['name'],
keys: ['emoji.annotation'],
// For removing punctuation
shouldMatchWordsOnly: true,
});
@ -96,7 +96,7 @@ export default class EmojiProvider extends AutocompleteProvider {
const sorters = [];
// make sure that emoticons come first
sorters.push((c) => score(matchedString, c.aliases_ascii));
sorters.push((c) => score(matchedString, c.emoji.emoticon || ""));
// then sort by score (Infinity if matchedString not in shortname)
sorters.push((c) => score(matchedString, c.shortname));
@ -110,8 +110,7 @@ export default class EmojiProvider extends AutocompleteProvider {
sorters.push((c) => c._orderBy);
completions = _sortBy(_uniq(completions), sorters);
completions = completions.map((result) => {
const { shortname } = result;
completions = completions.map(({shortname}) => {
const unicode = shortcodeToUnicode(shortname);
return {
completion: unicode,

View file

@ -71,6 +71,7 @@ export default class QueryMatcher {
}
for (const keyValue of keyValues) {
if (!keyValue) continue; // skip falsy keyValues
const key = stripDiacritics(keyValue).toLowerCase();
if (!this._items.has(key)) {
this._items.set(key, []);

View file

@ -70,10 +70,13 @@ export class ContextMenu extends React.Component {
// on resize callback
windowResize: PropTypes.func,
managed: PropTypes.bool, // whether this context menu should be focus managed. If false it must handle itself
};
static defaultProps = {
hasBackground: true,
managed: true,
};
constructor() {
@ -183,6 +186,15 @@ export class ContextMenu extends React.Component {
};
_onKeyDown = (ev) => {
if (!this.props.managed) {
if (ev.key === Key.ESCAPE) {
this.props.onFinished();
ev.stopPropagation();
ev.preventDefault();
}
return;
}
let handled = true;
switch (ev.key) {
@ -313,7 +325,7 @@ export class ContextMenu extends React.Component {
return (
<div className="mx_ContextualMenu_wrapper" style={{...position, ...wrapperStyle}} onKeyDown={this._onKeyDown}>
<div className={menuClasses} style={menuStyle} ref={this.collectContextMenuRect} role="menu">
<div className={menuClasses} style={menuStyle} ref={this.collectContextMenuRect} role={this.props.managed ? "menu" : undefined}>
{ chevron }
{ props.children }
</div>
@ -411,7 +423,7 @@ export const toRightOf = (elementRect, chevronOffset=12) => {
const left = elementRect.right + window.pageXOffset + 3;
let top = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
top -= chevronOffset + 8; // where 8 is half the height of the chevron
return {left, top};
return {left, top, chevronOffset};
};
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect

View file

@ -61,30 +61,13 @@ class CustomRoomTagPanel extends React.Component {
}
class CustomRoomTagTile extends React.Component {
constructor(props) {
super(props);
this.state = {hover: false};
this.onClick = this.onClick.bind(this);
this.onMouseOut = this.onMouseOut.bind(this);
this.onMouseOver = this.onMouseOver.bind(this);
}
onMouseOver() {
this.setState({hover: true});
}
onMouseOut() {
this.setState({hover: false});
}
onClick() {
onClick = () => {
dis.dispatch({action: 'select_custom_room_tag', tag: this.props.tag.name});
}
};
render() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const Tooltip = sdk.getComponent('elements.Tooltip');
const AccessibleTooltipButton = sdk.getComponent('elements.AccessibleTooltipButton');
const tag = this.props.tag;
const avatarHeight = 40;
@ -102,12 +85,9 @@ class CustomRoomTagTile extends React.Component {
badgeElement = (<div className={badgeClasses}>{FormattingUtils.formatCount(badge.count)}</div>);
}
const tip = (this.state.hover ?
<Tooltip className="mx_TagTile_tooltip" label={name} /> :
<div />);
return (
<AccessibleButton className={className} onClick={this.onClick}>
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
<AccessibleTooltipButton className={className} onClick={this.onClick} title={name}>
<div className="mx_TagTile_avatar">
<BaseAvatar
name={tag.avatarLetter}
idName={name}
@ -115,9 +95,8 @@ class CustomRoomTagTile extends React.Component {
height={avatarHeight}
/>
{ badgeElement }
{ tip }
</div>
</AccessibleButton>
</AccessibleTooltipButton>
);
}
}

View file

@ -26,8 +26,8 @@ import sanitizeHtml from 'sanitize-html';
import sdk from '../../index';
import dis from '../../dispatcher';
import MatrixClientPeg from '../../MatrixClientPeg';
import { MatrixClient } from 'matrix-js-sdk';
import classnames from 'classnames';
import MatrixClientContext from "../../contexts/MatrixClientContext";
export default class EmbeddedPage extends React.PureComponent {
static propTypes = {
@ -39,9 +39,7 @@ export default class EmbeddedPage extends React.PureComponent {
scrollbar: PropTypes.bool,
};
static contextTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient),
};
static contextType = MatrixClientContext;
constructor(props) {
super(props);
@ -104,7 +102,7 @@ export default class EmbeddedPage extends React.PureComponent {
render() {
// HACK: Workaround for the context's MatrixClient not updating.
const client = this.context.matrixClient || MatrixClientPeg.get();
const client = this.context || MatrixClientPeg.get();
const isGuest = client ? client.isGuest() : true;
const className = this.props.className;
const classes = classnames({

View file

@ -38,6 +38,7 @@ import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks";
import {Group} from "matrix-js-sdk";
import {allSettled, sleep} from "../../utils/promise";
import RightPanelStore from "../../stores/RightPanelStore";
const LONG_DESC_PLACEHOLDER = _td(
`<h1>HTML for your community's page</h1>
@ -542,10 +543,6 @@ export default createReactClass({
});
},
_onShowRhsClick: function(ev) {
dis.dispatch({ action: 'show_right_panel' });
},
_onEditClick: function() {
this.setState({
editing: true,
@ -583,6 +580,10 @@ export default createReactClass({
profileForm: null,
});
break;
case 'after_right_panel_phase_change':
// We don't keep state on the right panel, so just re-render to update
this.forceUpdate();
break;
default:
break;
}
@ -1214,25 +1215,25 @@ export default createReactClass({
const EditableText = sdk.getComponent("elements.EditableText");
nameNode = <EditableText ref="nameEditor"
className="mx_GroupView_editable"
placeholderClassName="mx_GroupView_placeholder"
placeholder={_t('Community Name')}
blurToCancel={false}
initialValue={this.state.profileForm.name}
onValueChanged={this._onNameChange}
tabIndex="0"
dir="auto" />;
nameNode = <EditableText
className="mx_GroupView_editable"
placeholderClassName="mx_GroupView_placeholder"
placeholder={_t('Community Name')}
blurToCancel={false}
initialValue={this.state.profileForm.name}
onValueChanged={this._onNameChange}
tabIndex="0"
dir="auto" />;
shortDescNode = <EditableText ref="descriptionEditor"
className="mx_GroupView_editable"
placeholderClassName="mx_GroupView_placeholder"
placeholder={_t("Description")}
blurToCancel={false}
initialValue={this.state.profileForm.short_description}
onValueChanged={this._onShortDescChange}
tabIndex="0"
dir="auto" />;
shortDescNode = <EditableText
className="mx_GroupView_editable"
placeholderClassName="mx_GroupView_placeholder"
placeholder={_t("Description")}
blurToCancel={false}
initialValue={this.state.profileForm.short_description}
onValueChanged={this._onShortDescChange}
tabIndex="0"
dir="auto" />;
} else {
const onGroupHeaderItemClick = this.state.isUserMember ? this._onEditClick : null;
const groupAvatarUrl = summary.profile ? summary.profile.avatar_url : null;
@ -1298,7 +1299,9 @@ export default createReactClass({
);
}
const rightPanel = !this.props.collapsedRhs ? <RightPanel groupId={this.props.groupId} /> : undefined;
const rightPanel = !RightPanelStore.getSharedInstance().isOpenForGroup
? <RightPanel groupId={this.props.groupId} />
: undefined;
const headerClasses = {
"mx_GroupView_header": true,
@ -1326,9 +1329,9 @@ export default createReactClass({
<div className="mx_GroupView_header_rightCol">
{ rightButtons }
</div>
<GroupHeaderButtons collapsedRhs={this.props.collapsedRhs} />
<GroupHeaderButtons />
</div>
<MainSplit collapsedRhs={this.props.collapsedRhs} panel={rightPanel}>
<MainSplit panel={rightPanel}>
<GeminiScrollbarWrapper className="mx_GroupView_body">
{ this._getMembershipSection() }
{ this._getGroupSection() }

View file

@ -18,7 +18,7 @@ limitations under the License.
import Matrix from 'matrix-js-sdk';
const InteractiveAuth = Matrix.InteractiveAuth;
import React from 'react';
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
@ -129,6 +129,8 @@ export default createReactClass({
this._authLogic.poll();
}, 2000);
}
this._stageComponent = createRef();
},
componentWillUnmount: function() {
@ -153,8 +155,8 @@ export default createReactClass({
},
tryContinue: function() {
if (this.refs.stageComponent && this.refs.stageComponent.tryContinue) {
this.refs.stageComponent.tryContinue();
if (this._stageComponent.current && this._stageComponent.current.tryContinue) {
this._stageComponent.current.tryContinue();
}
},
@ -192,8 +194,8 @@ export default createReactClass({
},
_setFocus: function() {
if (this.refs.stageComponent && this.refs.stageComponent.focus) {
this.refs.stageComponent.focus();
if (this._stageComponent.current && this._stageComponent.current.focus) {
this._stageComponent.current.focus();
}
},
@ -214,7 +216,8 @@ export default createReactClass({
const StageComponent = getEntryComponentForLoginType(stage);
return (
<StageComponent ref="stageComponent"
<StageComponent
ref={this._stageComponent}
loginType={stage}
matrixClient={this.props.matrixClient}
authSessionId={this._authLogic.getSessionId()}

View file

@ -19,12 +19,10 @@ import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { MatrixClient } from 'matrix-js-sdk';
import { Key } from '../../Keyboard';
import sdk from '../../index';
import dis from '../../dispatcher';
import VectorConferenceHandler from '../../VectorConferenceHandler';
import TagPanelButtons from './TagPanelButtons';
import SettingsStore from '../../settings/SettingsStore';
import {_t} from "../../languageHandler";
import Analytics from "../../Analytics";
@ -39,10 +37,6 @@ const LeftPanel = createReactClass({
collapsed: PropTypes.bool.isRequired,
},
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
},
getInitialState: function() {
return {
searchFilter: '',
@ -243,7 +237,6 @@ const LeftPanel = createReactClass({
tagPanelContainer = (<div className="mx_LeftPanel_tagPanelContainer">
<TagPanel />
{ isCustomTagsEnabled ? <CustomRoomTagPanel /> : undefined }
<TagPanelButtons />
</div>);
}

View file

@ -17,7 +17,7 @@ limitations under the License.
*/
import { MatrixClient } from 'matrix-js-sdk';
import React from 'react';
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { DragDropContext } from 'react-beautiful-dnd';
@ -38,6 +38,7 @@ import TagOrderActions from '../../actions/TagOrderActions';
import RoomListActions from '../../actions/RoomListActions';
import ResizeHandle from '../views/elements/ResizeHandle';
import {Resizer, CollapseDistributor} from '../../resizer';
import MatrixClientContext from "../../contexts/MatrixClientContext";
// We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity.
// NB. this is just for server notices rather than pinned messages in general.
@ -70,7 +71,6 @@ const LoggedInView = createReactClass({
// Called with the credentials of a registered user (if they were a ROU that
// transitioned to PWLU)
onRegistered: PropTypes.func,
collapsedRhs: PropTypes.bool,
// Used by the RoomView to handle joining rooms
viaServers: PropTypes.arrayOf(PropTypes.string),
@ -78,21 +78,6 @@ const LoggedInView = createReactClass({
// and lots and lots of other stuff.
},
childContextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
authCache: PropTypes.object,
},
getChildContext: function() {
return {
matrixClient: this._matrixClient,
authCache: {
auth: {},
lastUpdate: 0,
},
};
},
getInitialState: function() {
return {
// use compact timeline view
@ -129,6 +114,8 @@ const LoggedInView = createReactClass({
this._matrixClient.on("RoomState.events", this.onRoomStateEvents);
fixupColorFonts();
this._roomView = createRef();
},
componentDidUpdate(prevProps) {
@ -165,10 +152,10 @@ const LoggedInView = createReactClass({
},
canResetTimelineInRoom: function(roomId) {
if (!this.refs.roomView) {
if (!this._roomView.current) {
return true;
}
return this.refs.roomView.canResetTimeline();
return this._roomView.current.canResetTimeline();
},
_setStateFromSessionStore() {
@ -428,8 +415,8 @@ const LoggedInView = createReactClass({
* @param {Object} ev The key event
*/
_onScrollKeyPressed: function(ev) {
if (this.refs.roomView) {
this.refs.roomView.handleScrollKey(ev);
if (this._roomView.current) {
this._roomView.current.handleScrollKey(ev);
}
},
@ -543,7 +530,7 @@ const LoggedInView = createReactClass({
switch (this.props.page_type) {
case PageTypes.RoomView:
pageElement = <RoomView
ref='roomView'
ref={this._roomView}
autoJoin={this.props.autoJoin}
onRegistered={this.props.onRegistered}
thirdPartyInvite={this.props.thirdPartyInvite}
@ -552,7 +539,6 @@ const LoggedInView = createReactClass({
eventPixelOffset={this.props.initialEventPixelOffset}
key={this.props.currentRoomId || 'roomview'}
disabled={this.props.middleDisabled}
collapsedRhs={this.props.collapsedRhs}
ConferenceHandler={this.props.ConferenceHandler}
resizeNotifier={this.props.resizeNotifier}
/>;
@ -583,7 +569,6 @@ const LoggedInView = createReactClass({
pageElement = <GroupView
groupId={this.props.currentGroupId}
isNew={this.props.currentGroupIsNew}
collapsedRhs={this.props.collapsedRhs}
/>;
break;
}
@ -632,21 +617,30 @@ const LoggedInView = createReactClass({
}
return (
<div onPaste={this._onPaste} onKeyDown={this._onReactKeyDown} className='mx_MatrixChat_wrapper' aria-hidden={this.props.hideToSRUsers} onMouseDown={this._onMouseDown} onMouseUp={this._onMouseUp}>
{ topBar }
<ToastContainer />
<DragDropContext onDragEnd={this._onDragEnd}>
<div ref={this._setResizeContainerRef} className={bodyClasses}>
<LeftPanel
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapseLhs || false}
disabled={this.props.leftDisabled}
/>
<ResizeHandle />
{ pageElement }
</div>
</DragDropContext>
</div>
<MatrixClientContext.Provider value={this._matrixClient}>
<div
onPaste={this._onPaste}
onKeyDown={this._onReactKeyDown}
className='mx_MatrixChat_wrapper'
aria-hidden={this.props.hideToSRUsers}
onMouseDown={this._onMouseDown}
onMouseUp={this._onMouseUp}
>
{ topBar }
<ToastContainer />
<DragDropContext onDragEnd={this._onDragEnd}>
<div ref={this._setResizeContainerRef} className={bodyClasses}>
<LeftPanel
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapseLhs || false}
disabled={this.props.leftDisabled}
/>
<ResizeHandle />
{ pageElement }
</div>
</DragDropContext>
</div>
</MatrixClientContext.Provider>
);
},
});

View file

@ -62,7 +62,7 @@ export default class MainSplit extends React.Component {
}
componentDidMount() {
if (this.props.panel && !this.props.collapsedRhs) {
if (this.props.panel) {
this._createResizer();
}
}
@ -75,17 +75,15 @@ export default class MainSplit extends React.Component {
}
componentDidUpdate(prevProps) {
const wasExpanded = !this.props.collapsedRhs && prevProps.collapsedRhs;
const wasCollapsed = this.props.collapsedRhs && !prevProps.collapsedRhs;
const wasPanelSet = this.props.panel && !prevProps.panel;
const wasPanelCleared = !this.props.panel && prevProps.panel;
if (this.resizeContainer && (wasExpanded || wasPanelSet)) {
if (this.resizeContainer && wasPanelSet) {
// The resizer can only be created when **both** expanded and the panel is
// set. Once both are true, the container ref will mount, which is required
// for the resizer to work.
this._createResizer();
} else if (this.resizer && (wasCollapsed || wasPanelCleared)) {
} else if (this.resizer && wasPanelCleared) {
this.resizer.detach();
this.resizer = null;
}

View file

@ -24,6 +24,8 @@ import Matrix from "matrix-js-sdk";
// focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss
import 'focus-visible';
// what-input helps improve keyboard accessibility
import 'what-input';
import Analytics from "../../Analytics";
import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
@ -148,16 +150,6 @@ export default createReactClass({
makeRegistrationUrl: PropTypes.func.isRequired,
},
childContextTypes: {
appConfig: PropTypes.object,
},
getChildContext: function() {
return {
appConfig: this.props.config,
};
},
getInitialState: function() {
const s = {
// the master view we are showing.
@ -175,10 +167,9 @@ export default createReactClass({
viewUserId: null,
// this is persisted as mx_lhs_size, loaded in LoggedInView
collapseLhs: false,
collapsedRhs: window.localStorage.getItem("mx_rhs_collapsed") === "true",
leftDisabled: false,
middleDisabled: false,
rightDisabled: false,
// the right panel's disabled state is tracked in its store.
version: null,
newVersion: null,
@ -657,23 +648,11 @@ export default createReactClass({
collapseLhs: false,
});
break;
case 'hide_right_panel':
window.localStorage.setItem("mx_rhs_collapsed", true);
this.setState({
collapsedRhs: true,
});
break;
case 'show_right_panel':
window.localStorage.setItem("mx_rhs_collapsed", false);
this.setState({
collapsedRhs: false,
});
break;
case 'panel_disable': {
this.setState({
leftDisabled: payload.leftDisabled || payload.sideDisabled || false,
middleDisabled: payload.middleDisabled || false,
rightDisabled: payload.rightDisabled || payload.sideDisabled || false,
// We don't track the right panel being disabled here - it's tracked in the store.
});
break;
}
@ -1245,7 +1224,6 @@ export default createReactClass({
view: VIEWS.LOGIN,
ready: false,
collapseLhs: false,
collapsedRhs: false,
currentRoomId: null,
});
this.subTitleStatus = '';
@ -1261,7 +1239,6 @@ export default createReactClass({
view: VIEWS.SOFT_LOGOUT,
ready: false,
collapseLhs: false,
collapsedRhs: false,
currentRoomId: null,
});
this.subTitleStatus = '';
@ -1479,7 +1456,7 @@ export default createReactClass({
}
});
if (SettingsStore.isFeatureEnabled("feature_dm_verification")) {
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
cli.on("crypto.verification.request", request => {
let requestObserver;
if (request.event.getRoomId()) {
@ -1505,7 +1482,7 @@ export default createReactClass({
const IncomingSasDialog = sdk.getComponent("views.dialogs.IncomingSasDialog");
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {
verifier,
});
}, null, /* priority = */ false, /* static = */ true);
});
}
// Fire the tinter right on startup to ensure the default theme is applied
@ -1705,8 +1682,6 @@ export default createReactClass({
handleResize: function(e) {
const hideLhsThreshold = 1000;
const showLhsThreshold = 1000;
const hideRhsThreshold = 820;
const showRhsThreshold = 820;
if (this._windowWidth > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) {
dis.dispatch({ action: 'hide_left_panel' });
@ -1714,12 +1689,6 @@ export default createReactClass({
if (this._windowWidth <= showLhsThreshold && window.innerWidth > showLhsThreshold) {
dis.dispatch({ action: 'show_left_panel' });
}
if (this._windowWidth > hideRhsThreshold && window.innerWidth <= hideRhsThreshold) {
dis.dispatch({ action: 'hide_right_panel' });
}
if (this._windowWidth <= showRhsThreshold && window.innerWidth > showRhsThreshold) {
dis.dispatch({ action: 'show_right_panel' });
}
this.state.resizeNotifier.notifyWindowResized();
this._windowWidth = window.innerWidth;

View file

@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import classNames from 'classnames';
@ -159,6 +159,10 @@ export default class MessagePanel extends React.Component {
SettingsStore.getValue("showHiddenEventsInTimeline");
this._isMounted = false;
this._readMarkerNode = createRef();
this._whoIsTyping = createRef();
this._scrollPanel = createRef();
}
componentDidMount() {
@ -191,8 +195,7 @@ export default class MessagePanel extends React.Component {
/* return true if the content is fully scrolled down right now; else false.
*/
isAtBottom() {
return this.refs.scrollPanel
&& this.refs.scrollPanel.isAtBottom();
return this._scrollPanel.current && this._scrollPanel.current.isAtBottom();
}
/* get the current scroll state. See ScrollPanel.getScrollState for
@ -201,8 +204,7 @@ export default class MessagePanel extends React.Component {
* returns null if we are not mounted.
*/
getScrollState() {
if (!this.refs.scrollPanel) { return null; }
return this.refs.scrollPanel.getScrollState();
return this._scrollPanel.current ? this._scrollPanel.current.getScrollState() : null;
}
// returns one of:
@ -212,8 +214,8 @@ export default class MessagePanel extends React.Component {
// 0: read marker is within the window
// +1: read marker is below the window
getReadMarkerPosition() {
const readMarker = this.refs.readMarkerNode;
const messageWrapper = this.refs.scrollPanel;
const readMarker = this._readMarkerNode.current;
const messageWrapper = this._scrollPanel.current;
if (!readMarker || !messageWrapper) {
return null;
@ -236,16 +238,16 @@ export default class MessagePanel extends React.Component {
/* jump to the top of the content.
*/
scrollToTop() {
if (this.refs.scrollPanel) {
this.refs.scrollPanel.scrollToTop();
if (this._scrollPanel.current) {
this._scrollPanel.current.scrollToTop();
}
}
/* jump to the bottom of the content.
*/
scrollToBottom() {
if (this.refs.scrollPanel) {
this.refs.scrollPanel.scrollToBottom();
if (this._scrollPanel.current) {
this._scrollPanel.current.scrollToBottom();
}
}
@ -255,8 +257,8 @@ export default class MessagePanel extends React.Component {
* @param {number} mult: -1 to page up, +1 to page down
*/
scrollRelative(mult) {
if (this.refs.scrollPanel) {
this.refs.scrollPanel.scrollRelative(mult);
if (this._scrollPanel.current) {
this._scrollPanel.current.scrollRelative(mult);
}
}
@ -266,8 +268,8 @@ export default class MessagePanel extends React.Component {
* @param {KeyboardEvent} ev: the keyboard event to handle
*/
handleScrollKey(ev) {
if (this.refs.scrollPanel) {
this.refs.scrollPanel.handleScrollKey(ev);
if (this._scrollPanel.current) {
this._scrollPanel.current.handleScrollKey(ev);
}
}
@ -282,8 +284,8 @@ export default class MessagePanel extends React.Component {
* defaults to 0.
*/
scrollToEvent(eventId, pixelOffset, offsetBase) {
if (this.refs.scrollPanel) {
this.refs.scrollPanel.scrollToToken(eventId, pixelOffset, offsetBase);
if (this._scrollPanel.current) {
this._scrollPanel.current.scrollToToken(eventId, pixelOffset, offsetBase);
}
}
@ -297,8 +299,8 @@ export default class MessagePanel extends React.Component {
/* check the scroll state and send out pagination requests if necessary.
*/
checkFillState() {
if (this.refs.scrollPanel) {
this.refs.scrollPanel.checkFillState();
if (this._scrollPanel.current) {
this._scrollPanel.current.checkFillState();
}
}
@ -345,7 +347,7 @@ export default class MessagePanel extends React.Component {
}
return (
<li key={"readMarker_"+eventId} ref="readMarkerNode"
<li key={"readMarker_"+eventId} ref={this._readMarkerNode}
className="mx_RoomView_myReadMarker_container">
{ hr }
</li>
@ -829,14 +831,14 @@ export default class MessagePanel extends React.Component {
// once dynamic content in the events load, make the scrollPanel check the
// scroll offsets.
_onHeightChanged = () => {
const scrollPanel = this.refs.scrollPanel;
const scrollPanel = this._scrollPanel.current;
if (scrollPanel) {
scrollPanel.checkScroll();
}
};
_onTypingShown = () => {
const scrollPanel = this.refs.scrollPanel;
const scrollPanel = this._scrollPanel.current;
// this will make the timeline grow, so checkScroll
scrollPanel.checkScroll();
if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) {
@ -845,7 +847,7 @@ export default class MessagePanel extends React.Component {
};
_onTypingHidden = () => {
const scrollPanel = this.refs.scrollPanel;
const scrollPanel = this._scrollPanel.current;
if (scrollPanel) {
// as hiding the typing notifications doesn't
// update the scrollPanel, we tell it to apply
@ -858,11 +860,11 @@ export default class MessagePanel extends React.Component {
};
updateTimelineMinHeight() {
const scrollPanel = this.refs.scrollPanel;
const scrollPanel = this._scrollPanel.current;
if (scrollPanel) {
const isAtBottom = scrollPanel.isAtBottom();
const whoIsTyping = this.refs.whoIsTyping;
const whoIsTyping = this._whoIsTyping.current;
const isTypingVisible = whoIsTyping && whoIsTyping.isVisible();
// when messages get added to the timeline,
// but somebody else is still typing,
@ -875,7 +877,7 @@ export default class MessagePanel extends React.Component {
}
onTimelineReset() {
const scrollPanel = this.refs.scrollPanel;
const scrollPanel = this._scrollPanel.current;
if (scrollPanel) {
scrollPanel.clearPreventShrinking();
}
@ -909,19 +911,22 @@ export default class MessagePanel extends React.Component {
room={this.props.room}
onShown={this._onTypingShown}
onHidden={this._onTypingHidden}
ref="whoIsTyping" />
ref={this._whoIsTyping} />
);
}
return (
<ScrollPanel ref="scrollPanel" className={className}
onScroll={this.props.onScroll}
onResize={this.onResize}
onFillRequest={this.props.onFillRequest}
onUnfillRequest={this.props.onUnfillRequest}
style={style}
stickyBottom={this.props.stickyBottom}
resizeNotifier={this.props.resizeNotifier}>
<ScrollPanel
ref={this._scrollPanel}
className={className}
onScroll={this.props.onScroll}
onResize={this.onResize}
onFillRequest={this.props.onFillRequest}
onUnfillRequest={this.props.onUnfillRequest}
style={style}
stickyBottom={this.props.stickyBottom}
resizeNotifier={this.props.resizeNotifier}
>
{ topSpinner }
{ this._getEventTiles() }
{ whoIsTyping }

View file

@ -17,12 +17,11 @@ 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 sdk from '../../index';
import { _t } from '../../languageHandler';
import dis from '../../dispatcher';
import AccessibleButton from '../views/elements/AccessibleButton';
import MatrixClientContext from "../../contexts/MatrixClientContext";
export default createReactClass({
displayName: 'MyGroups',
@ -34,8 +33,8 @@ export default createReactClass({
};
},
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
statics: {
contextType: MatrixClientContext,
},
componentWillMount: function() {
@ -47,7 +46,7 @@ export default createReactClass({
},
_fetch: function() {
this.context.matrixClient.getJoinedGroups().then((result) => {
this.context.getJoinedGroups().then((result) => {
this.setState({groups: result.groups, error: null});
}, (err) => {
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') {

View file

@ -1,9 +1,9 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Copyright 2018 New Vector Ltd
Copyright 2017, 2018 New Vector 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.
@ -23,44 +23,31 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import sdk from '../../index';
import dis from '../../dispatcher';
import { MatrixClient } from 'matrix-js-sdk';
import RateLimitedFunc from '../../ratelimitedfunc';
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
import GroupStore from '../../stores/GroupStore';
import SettingsStore from "../../settings/SettingsStore";
import {RIGHT_PANEL_PHASES, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPanelStorePhases";
import RightPanelStore from "../../stores/RightPanelStore";
import MatrixClientContext from "../../contexts/MatrixClientContext";
export default class RightPanel extends React.Component {
static get propTypes() {
return {
roomId: PropTypes.string, // 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,
user: PropTypes.object, // used if we know the user ahead of opening the panel
};
}
static get contextTypes() {
return {
matrixClient: PropTypes.instanceOf(MatrixClient),
};
}
static contextType = MatrixClientContext;
static Phase = Object.freeze({
RoomMemberList: 'RoomMemberList',
GroupMemberList: 'GroupMemberList',
GroupRoomList: 'GroupRoomList',
GroupRoomInfo: 'GroupRoomInfo',
FilePanel: 'FilePanel',
NotificationPanel: 'NotificationPanel',
RoomMemberInfo: 'RoomMemberInfo',
Room3pidMemberInfo: 'Room3pidMemberInfo',
GroupMemberInfo: 'GroupMemberInfo',
});
constructor(props, context) {
super(props, context);
constructor(props) {
super(props);
this.state = {
phase: this._getPhaseFromProps(),
isUserPrivilegedInGroup: null,
member: this._getUserForPanel(),
};
this.onAction = this.onAction.bind(this);
this.onRoomStateMember = this.onRoomStateMember.bind(this);
@ -73,30 +60,44 @@ export default class RightPanel extends React.Component {
}, 500);
}
// Helper function to split out the logic for _getPhaseFromProps() and the constructor
// as both are called at the same time in the constructor.
_getUserForPanel() {
if (this.state && this.state.member) return this.state.member;
const lastParams = RightPanelStore.getSharedInstance().roomPanelPhaseParams;
return this.props.user || lastParams['member'];
}
_getPhaseFromProps() {
const rps = RightPanelStore.getSharedInstance();
if (this.props.groupId) {
return RightPanel.Phase.GroupMemberList;
} else if (this.props.user) {
return RightPanel.Phase.RoomMemberInfo;
if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.groupPanelPhase)) {
dis.dispatch({action: "set_right_panel_phase", phase: RIGHT_PANEL_PHASES.GroupMemberList});
return RIGHT_PANEL_PHASES.GroupMemberList;
}
return rps.groupPanelPhase;
} else if (this._getUserForPanel()) {
return RIGHT_PANEL_PHASES.RoomMemberInfo;
} else {
return RightPanel.Phase.RoomMemberList;
if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.roomPanelPhase)) {
dis.dispatch({action: "set_right_panel_phase", phase: RIGHT_PANEL_PHASES.RoomMemberList});
return RIGHT_PANEL_PHASES.RoomMemberList;
}
return rps.roomPanelPhase;
}
}
componentWillMount() {
this.dispatcherRef = dis.register(this.onAction);
const cli = this.context.matrixClient;
const cli = this.context;
cli.on("RoomState.members", this.onRoomStateMember);
this._initGroupStore(this.props.groupId);
if (this.props.user) {
this.setState({member: this.props.user});
}
}
componentWillUnmount() {
dis.unregister(this.dispatcherRef);
if (this.context.matrixClient) {
this.context.matrixClient.removeListener("RoomState.members", this.onRoomStateMember);
if (this.context) {
this.context.removeListener("RoomState.members", this.onRoomStateMember);
}
this._unregisterGroupStore(this.props.groupId);
}
@ -126,7 +127,7 @@ export default class RightPanel extends React.Component {
onInviteToGroupButtonClick() {
showGroupInviteDialog(this.props.groupId).then(() => {
this.setState({
phase: RightPanel.Phase.GroupMemberList,
phase: RIGHT_PANEL_PHASES.GroupMemberList,
});
});
}
@ -142,9 +143,9 @@ export default class RightPanel extends React.Component {
return;
}
// redraw the badge on the membership list
if (this.state.phase === RightPanel.Phase.RoomMemberList && member.roomId === this.props.roomId) {
if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberList && member.roomId === this.props.roomId) {
this._delayedUpdate();
} else if (this.state.phase === RightPanel.Phase.RoomMemberInfo && member.roomId === this.props.roomId &&
} else if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberInfo && member.roomId === this.props.roomId &&
member.userId === this.state.member.userId) {
// refresh the member info (e.g. new power level)
this._delayedUpdate();
@ -152,7 +153,7 @@ export default class RightPanel extends React.Component {
}
onAction(payload) {
if (payload.action === "view_right_panel_phase") {
if (payload.action === "after_right_panel_phase_change") {
this.setState({
phase: payload.phase,
groupRoomId: payload.groupRoomId,
@ -178,14 +179,14 @@ export default class RightPanel extends React.Component {
let panel = <div />;
if (this.props.roomId && this.state.phase === RightPanel.Phase.RoomMemberList) {
if (this.props.roomId && this.state.phase === RIGHT_PANEL_PHASES.RoomMemberList) {
panel = <MemberList roomId={this.props.roomId} key={this.props.roomId} />;
} else if (this.props.groupId && this.state.phase === RightPanel.Phase.GroupMemberList) {
} else if (this.props.groupId && this.state.phase === RIGHT_PANEL_PHASES.GroupMemberList) {
panel = <GroupMemberList groupId={this.props.groupId} key={this.props.groupId} />;
} else if (this.state.phase === RightPanel.Phase.GroupRoomList) {
} else if (this.state.phase === RIGHT_PANEL_PHASES.GroupRoomList) {
panel = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />;
} else if (this.state.phase === RightPanel.Phase.RoomMemberInfo) {
if (SettingsStore.isFeatureEnabled("feature_dm_verification")) {
} else if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberInfo) {
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
const onClose = () => {
dis.dispatch({
action: "view_user",
@ -201,10 +202,10 @@ export default class RightPanel extends React.Component {
} else {
panel = <MemberInfo member={this.state.member} key={this.props.roomId || this.state.member.userId} />;
}
} else if (this.state.phase === RightPanel.Phase.Room3pidMemberInfo) {
} else if (this.state.phase === RIGHT_PANEL_PHASES.Room3pidMemberInfo) {
panel = <ThirdPartyMemberInfo event={this.state.event} key={this.props.roomId} />;
} else if (this.state.phase === RightPanel.Phase.GroupMemberInfo) {
if (SettingsStore.isFeatureEnabled("feature_dm_verification")) {
} else if (this.state.phase === RIGHT_PANEL_PHASES.GroupMemberInfo) {
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
const onClose = () => {
dis.dispatch({
action: "view_user",
@ -225,14 +226,14 @@ export default class RightPanel extends React.Component {
/>
);
}
} else if (this.state.phase === RightPanel.Phase.GroupRoomInfo) {
} else if (this.state.phase === RIGHT_PANEL_PHASES.GroupRoomInfo) {
panel = <GroupRoomInfo
groupRoomId={this.state.groupRoomId}
groupId={this.props.groupId}
key={this.state.groupRoomId} />;
} else if (this.state.phase === RightPanel.Phase.NotificationPanel) {
} else if (this.state.phase === RIGHT_PANEL_PHASES.NotificationPanel) {
panel = <NotificationPanel />;
} else if (this.state.phase === RightPanel.Phase.FilePanel) {
} else if (this.state.phase === RIGHT_PANEL_PHASES.FilePanel) {
panel = <FilePanel roomId={this.props.roomId} resizeNotifier={this.props.resizeNotifier} />;
}

View file

@ -30,6 +30,7 @@ import PropTypes from 'prop-types';
import { _t } from '../../languageHandler';
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
import Analytics from '../../Analytics';
import MatrixClientContext from "../../contexts/MatrixClientContext";
const MAX_NAME_LENGTH = 80;
const MAX_TOPIC_LENGTH = 160;
@ -65,16 +66,6 @@ module.exports = createReactClass({
};
},
childContextTypes: {
matrixClient: PropTypes.object,
},
getChildContext: function() {
return {
matrixClient: MatrixClientPeg.get(),
};
},
componentWillMount: function() {
this._unmounted = false;
this.nextBatch = null;
@ -108,20 +99,9 @@ module.exports = createReactClass({
),
});
});
// dis.dispatch({
// action: 'panel_disable',
// sideDisabled: true,
// middleDisabled: true,
// });
},
componentWillUnmount: function() {
// dis.dispatch({
// action: 'panel_disable',
// sideDisabled: false,
// middleDisabled: false,
// });
if (this.filterTimeout) {
clearTimeout(this.filterTimeout);
}
@ -281,6 +261,7 @@ module.exports = createReactClass({
roomServer: server,
instanceId: instanceId,
includeAll: includeAll,
error: null,
}, this.refreshRoomList);
// We also refresh the room list each time even though this
// filtering is client-side. It hopefully won't be client side
@ -572,7 +553,7 @@ module.exports = createReactClass({
if (rows.length === 0 && !this.state.loading) {
scrollpanel_content = <i>{ _t('No rooms to show') }</i>;
} else {
scrollpanel_content = <table ref="directory_table" className="mx_RoomDirectory_table">
scrollpanel_content = <table className="mx_RoomDirectory_table">
<tbody>
{ rows }
</tbody>

View file

@ -25,7 +25,7 @@ import MatrixClientPeg from '../../MatrixClientPeg';
import Resend from '../../Resend';
import * as cryptodevices from '../../cryptodevices';
import dis from '../../dispatcher';
import { messageForResourceLimitError } from '../../utils/ErrorUtils';
import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils';
const STATUS_BAR_HIDDEN = 0;
const STATUS_BAR_EXPANDED = 1;
@ -272,7 +272,7 @@ module.exports = createReactClass({
unsentMessages[0].error.data &&
unsentMessages[0].error.data.error
) {
title = unsentMessages[0].error.data.error;
title = messageForSendError(unsentMessages[0].error.data) || unsentMessages[0].error.data.error;
} else {
title = _t('%(count)s of your messages have not been sent.', { count: unsentMessages.length });
}

View file

@ -18,7 +18,6 @@ limitations under the License.
*/
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import classNames from 'classnames';
import sdk from '../../index';
import dis from '../../dispatcher';
@ -26,7 +25,7 @@ import Unread from '../../Unread';
import * as RoomNotifs from '../../RoomNotifs';
import * as FormattingUtils from '../../utils/FormattingUtils';
import IndicatorScrollbar from './IndicatorScrollbar';
import {Key, KeyCode} from '../../Keyboard';
import {Key} from '../../Keyboard';
import { Group } from 'matrix-js-sdk';
import PropTypes from 'prop-types';
import RoomTile from "../views/rooms/RoomTile";
@ -36,12 +35,11 @@ import {_t} from "../../languageHandler";
// turn this on for drop & drag console debugging galore
const debug = false;
const RoomSubList = createReactClass({
displayName: 'RoomSubList',
export default class RoomSubList extends React.PureComponent {
static displayName = 'RoomSubList';
static debug = debug;
debug: debug,
propTypes: {
static propTypes = {
list: PropTypes.arrayOf(PropTypes.object).isRequired,
label: PropTypes.string.isRequired,
tagName: PropTypes.string,
@ -59,10 +57,26 @@ const RoomSubList = createReactClass({
incomingCall: PropTypes.object,
extraTiles: PropTypes.arrayOf(PropTypes.node), // extra elements added beneath tiles
forceExpand: PropTypes.bool,
},
};
getInitialState: function() {
static defaultProps = {
onHeaderClick: function() {
}, // NOP
extraTiles: [],
isInvite: false,
};
static getDerivedStateFromProps(props, state) {
return {
listLength: props.list.length,
scrollTop: props.list.length === state.listLength ? state.scrollTop : 0,
};
}
constructor(props) {
super(props);
this.state = {
hidden: this.props.startAsHidden || false,
// some values to get LazyRenderList starting
scrollerHeight: 800,
@ -71,47 +85,33 @@ const RoomSubList = createReactClass({
// we have to store the length of the list here so we can see if it's changed or not...
listLength: null,
};
},
getDefaultProps: function() {
return {
onHeaderClick: function() {
}, // NOP
extraTiles: [],
isInvite: false,
};
},
componentDidMount: function() {
this._header = createRef();
this._subList = createRef();
this._scroller = createRef();
this._headerButton = createRef();
}
componentDidMount() {
this.dispatcherRef = dis.register(this.onAction);
},
}
statics: {
getDerivedStateFromProps: function(props, state) {
return {
listLength: props.list.length,
scrollTop: props.list.length === state.listLength ? state.scrollTop : 0,
};
},
},
componentWillUnmount: function() {
componentWillUnmount() {
dis.unregister(this.dispatcherRef);
},
}
// The header is collapsible if it is hidden or not stuck
// The dataset elements are added in the RoomList _initAndPositionStickyHeaders method
isCollapsibleOnClick: function() {
const stuck = this.refs.header.dataset.stuck;
isCollapsibleOnClick() {
const stuck = this._header.current.dataset.stuck;
if (!this.props.forceExpand && (this.state.hidden || stuck === undefined || stuck === "none")) {
return true;
} else {
return false;
}
},
}
onAction: function(payload) {
onAction = (payload) => {
// XXX: Previously RoomList would forceUpdate whenever on_room_read is dispatched,
// but this is no longer true, so we must do it here (and can apply the small
// optimisation of checking that we care about the room being read).
@ -124,9 +124,9 @@ const RoomSubList = createReactClass({
) {
this.forceUpdate();
}
},
};
onClick: function(ev) {
onClick = (ev) => {
if (this.isCollapsibleOnClick()) {
// The header isCollapsible, so the click is to be interpreted as collapse and truncation logic
const isHidden = !this.state.hidden;
@ -135,11 +135,11 @@ const RoomSubList = createReactClass({
});
} else {
// The header is stuck, so the click is to be interpreted as a scroll to the header
this.props.onHeaderClick(this.state.hidden, this.refs.header.dataset.originalPosition);
this.props.onHeaderClick(this.state.hidden, this._header.current.dataset.originalPosition);
}
},
};
onHeaderKeyDown: function(ev) {
onHeaderKeyDown = (ev) => {
switch (ev.key) {
case Key.TAB:
// Prevent LeftPanel handling Tab if focus is on the sublist header itself
@ -159,7 +159,7 @@ const RoomSubList = createReactClass({
this.onClick();
} else if (!this.props.forceExpand) {
// sublist is expanded, go to first room
const element = this.refs.subList && this.refs.subList.querySelector(".mx_RoomTile");
const element = this._subList.current && this._subList.current.querySelector(".mx_RoomTile");
if (element) {
element.focus();
}
@ -167,9 +167,9 @@ const RoomSubList = createReactClass({
break;
}
}
},
};
onKeyDown: function(ev) {
onKeyDown = (ev) => {
switch (ev.key) {
// On ARROW_LEFT go to the sublist header
case Key.ARROW_LEFT:
@ -180,24 +180,24 @@ const RoomSubList = createReactClass({
case Key.ARROW_RIGHT:
ev.stopPropagation();
}
},
};
onRoomTileClick(roomId, ev) {
onRoomTileClick = (roomId, ev) => {
dis.dispatch({
action: 'view_room',
room_id: roomId,
clear_search: (ev && (ev.keyCode === KeyCode.ENTER || ev.keyCode === KeyCode.SPACE)),
clear_search: (ev && (ev.key === Key.ENTER || ev.key === Key.SPACE)),
});
},
};
_updateSubListCount: function() {
_updateSubListCount = () => {
// Force an update by setting the state to the current state
// Doing it this way rather than using forceUpdate(), so that the shouldComponentUpdate()
// method is honoured
this.setState(this.state);
},
};
makeRoomTile: function(room) {
makeRoomTile = (room) => {
return <RoomTile
room={room}
roomSubList={this}
@ -212,9 +212,9 @@ const RoomSubList = createReactClass({
incomingCall={null}
onClick={this.onRoomTileClick}
/>;
},
};
_onNotifBadgeClick: function(e) {
_onNotifBadgeClick = (e) => {
// prevent the roomsublist collapsing
e.preventDefault();
e.stopPropagation();
@ -225,9 +225,9 @@ const RoomSubList = createReactClass({
room_id: room.roomId,
});
}
},
};
_onInviteBadgeClick: function(e) {
_onInviteBadgeClick = (e) => {
// prevent the roomsublist collapsing
e.preventDefault();
e.stopPropagation();
@ -247,14 +247,14 @@ const RoomSubList = createReactClass({
});
}
}
},
};
onAddRoom: function(e) {
onAddRoom = (e) => {
e.stopPropagation();
if (this.props.onAddRoom) this.props.onAddRoom();
},
};
_getHeaderJsx: function(isCollapsed) {
_getHeaderJsx(isCollapsed) {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const AccessibleTooltipButton = sdk.getComponent('elements.AccessibleTooltipButton');
const subListNotifications = !this.props.isInvite ?
@ -328,7 +328,7 @@ const RoomSubList = createReactClass({
}
return (
<div className="mx_RoomSubList_labelContainer" title={title} ref="header" onKeyDown={this.onHeaderKeyDown}>
<div className="mx_RoomSubList_labelContainer" title={title} ref={this._header} onKeyDown={this.onHeaderKeyDown}>
<AccessibleButton
onClick={this.onClick}
className="mx_RoomSubList_label"
@ -346,36 +346,36 @@ const RoomSubList = createReactClass({
{ addRoomButton }
</div>
);
},
}
checkOverflow: function() {
if (this.refs.scroller) {
this.refs.scroller.checkOverflow();
checkOverflow = () => {
if (this._scroller.current) {
this._scroller.current.checkOverflow();
}
},
};
setHeight: function(height) {
if (this.refs.subList) {
this.refs.subList.style.height = `${height}px`;
setHeight = (height) => {
if (this._subList.current) {
this._subList.current.style.height = `${height}px`;
}
this._updateLazyRenderHeight(height);
},
};
_updateLazyRenderHeight: function(height) {
_updateLazyRenderHeight(height) {
this.setState({scrollerHeight: height});
},
}
_onScroll: function() {
this.setState({scrollTop: this.refs.scroller.getScrollTop()});
},
_onScroll = () => {
this.setState({scrollTop: this._scroller.current.getScrollTop()});
};
_canUseLazyListRendering() {
// for now disable lazy rendering as they are already rendered tiles
// not rooms like props.list we pass to LazyRenderList
return !this.props.extraTiles || !this.props.extraTiles.length;
},
}
render: function() {
render() {
const len = this.props.list.length + this.props.extraTiles.length;
const isCollapsed = this.state.hidden && !this.props.forceExpand;
@ -391,7 +391,7 @@ const RoomSubList = createReactClass({
// no body
} else if (this._canUseLazyListRendering()) {
content = (
<IndicatorScrollbar ref="scroller" className="mx_RoomSubList_scroll" onScroll={this._onScroll}>
<IndicatorScrollbar ref={this._scroller} className="mx_RoomSubList_scroll" onScroll={this._onScroll}>
<LazyRenderList
scrollTop={this.state.scrollTop }
height={ this.state.scrollerHeight }
@ -404,7 +404,7 @@ const RoomSubList = createReactClass({
const roomTiles = this.props.list.map(r => this.makeRoomTile(r));
const tiles = roomTiles.concat(this.props.extraTiles);
content = (
<IndicatorScrollbar ref="scroller" className="mx_RoomSubList_scroll" onScroll={this._onScroll}>
<IndicatorScrollbar ref={this._scroller} className="mx_RoomSubList_scroll" onScroll={this._onScroll}>
{ tiles }
</IndicatorScrollbar>
);
@ -418,7 +418,7 @@ const RoomSubList = createReactClass({
return (
<div
ref="subList"
ref={this._subList}
className={subListClasses}
role="group"
aria-label={this.props.label}
@ -428,7 +428,5 @@ const RoomSubList = createReactClass({
{ content }
</div>
);
},
});
module.exports = RoomSubList;
}
}

View file

@ -23,12 +23,10 @@ limitations under the License.
import shouldHideEvent from '../../shouldHideEvent';
import React from 'react';
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import {Room} from "matrix-js-sdk";
import { _t } from '../../languageHandler';
import {RoomPermalinkCreator} from '../../utils/permalinks/Permalinks';
@ -44,7 +42,7 @@ import ObjectUtils from '../../ObjectUtils';
import * as Rooms from '../../Rooms';
import eventSearch from '../../Searching';
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
import {isOnlyCtrlOrCmdKeyEvent, Key} from '../../Keyboard';
import MainSplit from './MainSplit';
import RightPanel from './RightPanel';
@ -54,6 +52,8 @@ import WidgetEchoStore from '../../stores/WidgetEchoStore';
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
import WidgetUtils from '../../utils/WidgetUtils';
import AccessibleButton from "../views/elements/AccessibleButton";
import RightPanelStore from "../../stores/RightPanelStore";
import RoomContext from "../../contexts/RoomContext";
const DEBUG = false;
let debuglog = function() {};
@ -65,12 +65,6 @@ if (DEBUG) {
debuglog = console.log.bind(console);
}
const RoomContext = PropTypes.shape({
canReact: PropTypes.bool.isRequired,
canReply: PropTypes.bool.isRequired,
room: PropTypes.instanceOf(Room),
});
module.exports = createReactClass({
displayName: 'RoomView',
propTypes: {
@ -98,9 +92,6 @@ module.exports = createReactClass({
// * invited us to the room
oobData: PropTypes.object,
// is the RightPanel collapsed?
collapsedRhs: PropTypes.bool,
// Servers the RoomView can use to try and assist joins
viaServers: PropTypes.arrayOf(PropTypes.string),
},
@ -171,21 +162,6 @@ module.exports = createReactClass({
};
},
childContextTypes: {
room: RoomContext,
},
getChildContext: function() {
const {canReact, canReply, room} = this.state;
return {
room: {
canReact,
canReply,
room,
},
};
},
componentWillMount: function() {
this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on("Room", this.onRoom);
@ -207,6 +183,9 @@ module.exports = createReactClass({
this._onCiderUpdated();
this._ciderWatcherRef = SettingsStore.watchSetting(
"useCiderComposer", null, this._onCiderUpdated);
this._roomView = createRef();
this._searchResultsPanel = createRef();
},
_onCiderUpdated: function() {
@ -459,8 +438,8 @@ module.exports = createReactClass({
},
componentDidUpdate: function() {
if (this.refs.roomView) {
const roomView = ReactDOM.findDOMNode(this.refs.roomView);
if (this._roomView.current) {
const roomView = this._roomView.current;
if (!roomView.ondrop) {
roomView.addEventListener('drop', this.onDrop);
roomView.addEventListener('dragover', this.onDragOver);
@ -474,10 +453,10 @@ module.exports = createReactClass({
// in render() prevents the ref from being set on first mount, so we try and
// catch the messagePanel when it does mount. Because we only want the ref once,
// we use a boolean flag to avoid duplicate work.
if (this.refs.messagePanel && !this.state.atEndOfLiveTimelineInit) {
if (this._messagePanel && !this.state.atEndOfLiveTimelineInit) {
this.setState({
atEndOfLiveTimelineInit: true,
atEndOfLiveTimeline: this.refs.messagePanel.isAtEndOfLiveTimeline(),
atEndOfLiveTimeline: this._messagePanel.isAtEndOfLiveTimeline(),
});
}
},
@ -499,12 +478,12 @@ module.exports = createReactClass({
// stop tracking room changes to format permalinks
this._stopAllPermalinkCreators();
if (this.refs.roomView) {
if (this._roomView.current) {
// disconnect the D&D event listeners from the room view. This
// is really just for hygiene - we're going to be
// deleted anyway, so it doesn't matter if the event listeners
// don't get cleaned up.
const roomView = ReactDOM.findDOMNode(this.refs.roomView);
const roomView = this._roomView.current;
roomView.removeEventListener('drop', this.onDrop);
roomView.removeEventListener('dragover', this.onDragOver);
roomView.removeEventListener('dragleave', this.onDragLeaveOrEnd);
@ -560,15 +539,15 @@ module.exports = createReactClass({
let handled = false;
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
switch (ev.keyCode) {
case KeyCode.KEY_D:
switch (ev.key) {
case Key.D:
if (ctrlCmdOnly) {
this.onMuteAudioClick();
handled = true;
}
break;
case KeyCode.KEY_E:
case Key.E:
if (ctrlCmdOnly) {
this.onMuteVideoClick();
handled = true;
@ -584,6 +563,10 @@ module.exports = createReactClass({
onAction: function(payload) {
switch (payload.action) {
case 'after_right_panel_phase_change':
// We don't keep state on the right panel, so just re-render to update
this.forceUpdate();
break;
case 'message_send_failed':
case 'message_sent':
this._checkIfAlone(this.state.room);
@ -701,10 +684,10 @@ module.exports = createReactClass({
},
canResetTimeline: function() {
if (!this.refs.messagePanel) {
if (!this._messagePanel) {
return true;
}
return this.refs.messagePanel.canResetTimeline();
return this._messagePanel.canResetTimeline();
},
// called when state.room is first initialised (either at initial load,
@ -787,11 +770,12 @@ module.exports = createReactClass({
this._updateE2EStatus(room);
},
_updateE2EStatus: function(room) {
if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) {
_updateE2EStatus: async function(room) {
const cli = MatrixClientPeg.get();
if (!cli.isRoomEncrypted(room.roomId)) {
return;
}
if (!MatrixClientPeg.get().isCryptoEnabled()) {
if (!cli.isCryptoEnabled()) {
// If crypto is not currently enabled, we aren't tracking devices at all,
// so we don't know what the answer is. Let's error on the safe side and show
// a warning for this case.
@ -800,10 +784,38 @@ module.exports = createReactClass({
});
return;
}
room.hasUnverifiedDevices().then((hasUnverifiedDevices) => {
this.setState({
e2eStatus: hasUnverifiedDevices ? "warning" : "verified",
if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) {
room.hasUnverifiedDevices().then((hasUnverifiedDevices) => {
this.setState({
e2eStatus: hasUnverifiedDevices ? "warning" : "verified",
});
});
return;
}
const e2eMembers = await room.getEncryptionTargetMembers();
for (const member of e2eMembers) {
const { userId } = member;
const userVerified = cli.checkUserTrust(userId).isCrossSigningVerified();
if (!userVerified) {
this.setState({
e2eStatus: "warning",
});
return;
}
const devices = await cli.getStoredDevicesForUser(userId);
const allDevicesVerified = devices.every(device => {
const { deviceId } = device;
return cli.checkDeviceTrust(userId, deviceId).isCrossSigningVerified();
});
if (!allDevicesVerified) {
this.setState({
e2eStatus: "warning",
});
return;
}
}
this.setState({
e2eStatus: "verified",
});
},
@ -1046,7 +1058,7 @@ module.exports = createReactClass({
},
onMessageListScroll: function(ev) {
if (this.refs.messagePanel.isAtEndOfLiveTimeline()) {
if (this._messagePanel.isAtEndOfLiveTimeline()) {
this.setState({
numUnreadMessages: 0,
atEndOfLiveTimeline: true,
@ -1119,8 +1131,8 @@ module.exports = createReactClass({
// if we already have a search panel, we need to tell it to forget
// about its scroll state.
if (this.refs.searchResultsPanel) {
this.refs.searchResultsPanel.resetScrollState();
if (this._searchResultsPanel.current) {
this._searchResultsPanel.current.resetScrollState();
}
// make sure that we don't end up showing results from
@ -1225,7 +1237,7 @@ module.exports = createReactClass({
// once dynamic content in the search results load, make the scrollPanel check
// the scroll offsets.
const onHeightChanged = () => {
const scrollPanel = this.refs.searchResultsPanel;
const scrollPanel = this._searchResultsPanel.current;
if (scrollPanel) {
scrollPanel.checkScroll();
}
@ -1370,28 +1382,28 @@ module.exports = createReactClass({
// jump down to the bottom of this room, where new events are arriving
jumpToLiveTimeline: function() {
this.refs.messagePanel.jumpToLiveTimeline();
this._messagePanel.jumpToLiveTimeline();
dis.dispatch({action: 'focus_composer'});
},
// jump up to wherever our read marker is
jumpToReadMarker: function() {
this.refs.messagePanel.jumpToReadMarker();
this._messagePanel.jumpToReadMarker();
},
// update the read marker to match the read-receipt
forgetReadMarker: function(ev) {
ev.stopPropagation();
this.refs.messagePanel.forgetReadMarker();
this._messagePanel.forgetReadMarker();
},
// decide whether or not the top 'unread messages' bar should be shown
_updateTopUnreadMessagesBar: function() {
if (!this.refs.messagePanel) {
if (!this._messagePanel) {
return;
}
const showBar = this.refs.messagePanel.canJumpToReadMarker();
const showBar = this._messagePanel.canJumpToReadMarker();
if (this.state.showTopUnreadMessagesBar != showBar) {
this.setState({showTopUnreadMessagesBar: showBar});
}
@ -1401,7 +1413,7 @@ module.exports = createReactClass({
// restored when we switch back to it.
//
_getScrollState: function() {
const messagePanel = this.refs.messagePanel;
const messagePanel = this._messagePanel;
if (!messagePanel) return null;
// if we're following the live timeline, we want to return null; that
@ -1506,10 +1518,10 @@ module.exports = createReactClass({
*/
handleScrollKey: function(ev) {
let panel;
if (this.refs.searchResultsPanel) {
panel = this.refs.searchResultsPanel;
} else if (this.refs.messagePanel) {
panel = this.refs.messagePanel;
if (this._searchResultsPanel.current) {
panel = this._searchResultsPanel.current;
} else if (this._messagePanel) {
panel = this._messagePanel;
}
if (panel) {
@ -1530,7 +1542,7 @@ module.exports = createReactClass({
// this has to be a proper method rather than an unnamed function,
// otherwise react calls it with null on each update.
_gatherTimelinePanelRef: function(r) {
this.refs.messagePanel = r;
this._messagePanel = r;
if (r) {
console.log("updateTint from RoomView._gatherTimelinePanelRef");
this.updateTint();
@ -1714,12 +1726,12 @@ module.exports = createReactClass({
let aux = null;
let previewBar;
let hideCancel = false;
let hideRightPanel = false;
let forceHideRightPanel = false;
if (this.state.forwardingEvent !== null) {
aux = <ForwardMessage onCancelClick={this.onCancelClick} />;
} else if (this.state.searching) {
hideCancel = true; // has own cancel
aux = <SearchBar ref="search_bar" searchInProgress={this.state.searchInProgress} onCancelClick={this.onCancelSearchClick} onSearch={this.onSearch} />;
aux = <SearchBar searchInProgress={this.state.searchInProgress} onCancelClick={this.onCancelSearchClick} onSearch={this.onSearch} />;
} else if (showRoomUpgradeBar) {
aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />;
hideCancel = true;
@ -1760,7 +1772,7 @@ module.exports = createReactClass({
</div>
);
} else {
hideRightPanel = true;
forceHideRightPanel = true;
}
} else if (hiddenHighlightCount > 0) {
aux = (
@ -1775,7 +1787,7 @@ module.exports = createReactClass({
}
const auxPanel = (
<AuxPanel ref="auxPanel" room={this.state.room}
<AuxPanel room={this.state.room}
fullHeight={false}
userId={MatrixClientPeg.get().credentials.userId}
conferenceHandler={this.props.ConferenceHandler}
@ -1875,7 +1887,7 @@ module.exports = createReactClass({
searchResultsPanel = (<div className="mx_RoomView_messagePanel mx_RoomView_messagePanelSearchSpinner" />);
} else {
searchResultsPanel = (
<ScrollPanel ref="searchResultsPanel"
<ScrollPanel ref={this._searchResultsPanel}
className="mx_RoomView_messagePanel mx_RoomView_searchResultsPanel"
onFillRequest={this.onSearchResultsFillRequest}
resizeNotifier={this.props.resizeNotifier}
@ -1898,7 +1910,8 @@ module.exports = createReactClass({
// console.info("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
const messagePanel = (
<TimelinePanel ref={this._gatherTimelinePanelRef}
<TimelinePanel
ref={this._gatherTimelinePanelRef}
timelineSet={this.state.room.getUnfilteredTimelineSet()}
showReadReceipts={SettingsStore.getValue('showReadReceipts')}
manageReadReceipts={!this.state.isPeeking}
@ -1918,7 +1931,8 @@ module.exports = createReactClass({
/>);
let topUnreadMessagesBar = null;
if (this.state.showTopUnreadMessagesBar) {
// Do not show TopUnreadMessagesBar if we have search results showing, it makes no sense
if (this.state.showTopUnreadMessagesBar && !this.state.searchResults) {
const TopUnreadMessagesBar = sdk.getComponent('rooms.TopUnreadMessagesBar');
topUnreadMessagesBar = (<TopUnreadMessagesBar
onScrollUpClick={this.jumpToReadMarker}
@ -1926,7 +1940,8 @@ module.exports = createReactClass({
/>);
}
let jumpToBottom;
if (!this.state.atEndOfLiveTimeline) {
// Do not show JumpToBottomButton if we have search results showing, it makes no sense
if (!this.state.atEndOfLiveTimeline && !this.state.searchResults) {
const JumpToBottomButton = sdk.getComponent('rooms.JumpToBottomButton');
jumpToBottom = (<JumpToBottomButton
numUnreadMessages={this.state.numUnreadMessages}
@ -1947,50 +1962,54 @@ module.exports = createReactClass({
},
);
const rightPanel = !hideRightPanel && this.state.room &&
<RightPanel roomId={this.state.room.roomId} resizeNotifier={this.props.resizeNotifier} />;
const collapsedRhs = hideRightPanel || this.props.collapsedRhs;
const showRightPanel = !forceHideRightPanel && this.state.room
&& RightPanelStore.getSharedInstance().isOpenForRoom;
const rightPanel = showRightPanel
? <RightPanel roomId={this.state.room.roomId} resizeNotifier={this.props.resizeNotifier} />
: null;
return (
<main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref="roomView">
<ErrorBoundary>
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
oobData={this.props.oobData}
inRoom={myMembership === 'join'}
collapsedRhs={collapsedRhs}
onSearchClick={this.onSearchClick}
onSettingsClick={this.onSettingsClick}
onPinnedClick={this.onPinnedClick}
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
e2eStatus={this.state.e2eStatus}
/>
<MainSplit
panel={rightPanel}
collapsedRhs={collapsedRhs}
resizeNotifier={this.props.resizeNotifier}
>
<div className={fadableSectionClasses}>
{auxPanel}
<div className="mx_RoomView_timeline">
{topUnreadMessagesBar}
{jumpToBottom}
{messagePanel}
{searchResultsPanel}
</div>
<div className={statusBarAreaClass}>
<div className="mx_RoomView_statusAreaBox">
<div className="mx_RoomView_statusAreaBox_line"></div>
{statusBar}
<RoomContext.Provider value={this.state}>
<main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref={this._roomView}>
<ErrorBoundary>
<RoomHeader
room={this.state.room}
searchInfo={searchInfo}
oobData={this.props.oobData}
inRoom={myMembership === 'join'}
onSearchClick={this.onSearchClick}
onSettingsClick={this.onSettingsClick}
onPinnedClick={this.onPinnedClick}
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
e2eStatus={this.state.e2eStatus}
/>
<MainSplit
panel={rightPanel}
resizeNotifier={this.props.resizeNotifier}
>
<div className={fadableSectionClasses}>
{auxPanel}
<div className="mx_RoomView_timeline">
{topUnreadMessagesBar}
{jumpToBottom}
{messagePanel}
{searchResultsPanel}
</div>
<div className={statusBarAreaClass}>
<div className="mx_RoomView_statusAreaBox">
<div className="mx_RoomView_statusAreaBox_line" />
{statusBar}
</div>
</div>
{previewBar}
{messageComposer}
</div>
{previewBar}
{messageComposer}
</div>
</MainSplit>
</ErrorBoundary>
</main>
</MainSplit>
</ErrorBoundary>
</main>
</RoomContext.Provider>
);
},
});

View file

@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import React, {createRef} from "react";
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { KeyCode } from '../../Keyboard';
import { Key } from '../../Keyboard';
import Timer from '../../utils/Timer';
import AutoHideScrollbar from "./AutoHideScrollbar";
@ -166,6 +166,8 @@ module.exports = createReactClass({
}
this.resetScrollState();
this._itemlist = createRef();
},
componentDidMount: function() {
@ -328,7 +330,7 @@ module.exports = createReactClass({
this._isFilling = true;
}
const itemlist = this.refs.itemlist;
const itemlist = this._itemlist.current;
const firstTile = itemlist && itemlist.firstElementChild;
const contentTop = firstTile && firstTile.offsetTop;
const fillPromises = [];
@ -373,7 +375,7 @@ module.exports = createReactClass({
const origExcessHeight = excessHeight;
const tiles = this.refs.itemlist.children;
const tiles = this._itemlist.current.children;
// The scroll token of the first/last tile to be unpaginated
let markerScrollToken = null;
@ -530,26 +532,26 @@ module.exports = createReactClass({
* @param {object} ev the keyboard event
*/
handleScrollKey: function(ev) {
switch (ev.keyCode) {
case KeyCode.PAGE_UP:
switch (ev.key) {
case Key.PAGE_UP:
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
this.scrollRelative(-1);
}
break;
case KeyCode.PAGE_DOWN:
case Key.PAGE_DOWN:
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
this.scrollRelative(1);
}
break;
case KeyCode.HOME:
case Key.HOME:
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
this.scrollToTop();
}
break;
case KeyCode.END:
case Key.END:
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
this.scrollToBottom();
}
@ -602,7 +604,7 @@ module.exports = createReactClass({
const scrollNode = this._getScrollNode();
const viewportBottom = scrollNode.scrollHeight - (scrollNode.scrollTop + scrollNode.clientHeight);
const itemlist = this.refs.itemlist;
const itemlist = this._itemlist.current;
const messages = itemlist.children;
let node = null;
@ -644,7 +646,7 @@ module.exports = createReactClass({
const sn = this._getScrollNode();
sn.scrollTop = sn.scrollHeight;
} else if (scrollState.trackedScrollToken) {
const itemlist = this.refs.itemlist;
const itemlist = this._itemlist.current;
const trackedNode = this._getTrackedNode();
if (trackedNode) {
const newBottomOffset = this._topFromBottom(trackedNode);
@ -682,7 +684,7 @@ module.exports = createReactClass({
}
const sn = this._getScrollNode();
const itemlist = this.refs.itemlist;
const itemlist = this._itemlist.current;
const contentHeight = this._getMessagesHeight();
const minHeight = sn.clientHeight;
const height = Math.max(minHeight, contentHeight);
@ -724,7 +726,7 @@ module.exports = createReactClass({
if (!trackedNode || !trackedNode.parentElement) {
let node;
const messages = this.refs.itemlist.children;
const messages = this._itemlist.current.children;
const scrollToken = scrollState.trackedScrollToken;
for (let i = messages.length-1; i >= 0; --i) {
@ -756,7 +758,7 @@ module.exports = createReactClass({
},
_getMessagesHeight() {
const itemlist = this.refs.itemlist;
const itemlist = this._itemlist.current;
const lastNode = itemlist.lastElementChild;
const lastNodeBottom = lastNode ? lastNode.offsetTop + lastNode.clientHeight : 0;
const firstNodeTop = itemlist.firstElementChild ? itemlist.firstElementChild.offsetTop : 0;
@ -765,7 +767,7 @@ module.exports = createReactClass({
},
_topFromBottom(node) {
return this.refs.itemlist.clientHeight - node.offsetTop;
return this._itemlist.current.clientHeight - node.offsetTop;
},
/* get the DOM node which has the scrollTop property we care about for our
@ -797,7 +799,7 @@ module.exports = createReactClass({
the same minimum bottom offset, effectively preventing the timeline to shrink.
*/
preventShrinking: function() {
const messageList = this.refs.itemlist;
const messageList = this._itemlist.current;
const tiles = messageList && messageList.children;
if (!messageList) {
return;
@ -824,7 +826,7 @@ module.exports = createReactClass({
/** Clear shrinking prevention. Used internally, and when the timeline is reloaded. */
clearPreventShrinking: function() {
const messageList = this.refs.itemlist;
const messageList = this._itemlist.current;
const balanceElement = messageList && messageList.parentElement;
if (balanceElement) balanceElement.style.paddingBottom = null;
this.preventShrinkingState = null;
@ -843,7 +845,7 @@ module.exports = createReactClass({
if (this.preventShrinkingState) {
const sn = this._getScrollNode();
const scrollState = this.scrollState;
const messageList = this.refs.itemlist;
const messageList = this._itemlist.current;
const {offsetNode, offsetFromBottom} = this.preventShrinkingState;
// element used to set paddingBottom to balance the typing notifs disappearing
const balanceElement = messageList.parentElement;
@ -879,7 +881,7 @@ module.exports = createReactClass({
onScroll={this.onScroll}
className={`mx_ScrollPanel ${this.props.className}`} style={this.props.style}>
<div className="mx_RoomView_messageListWrapper">
<ol ref="itemlist" className="mx_RoomView_MessageList" aria-live="polite">
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite">
{ this.props.children }
</ol>
</div>

View file

@ -15,10 +15,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { KeyCode } from '../../Keyboard';
import { Key } from '../../Keyboard';
import dis from '../../dispatcher';
import { throttle } from 'lodash';
import AccessibleButton from '../../components/views/elements/AccessibleButton';
@ -53,6 +53,10 @@ module.exports = createReactClass({
};
},
UNSAFE_componentWillMount: function() {
this._search = createRef();
},
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
},
@ -66,31 +70,31 @@ module.exports = createReactClass({
switch (payload.action) {
case 'view_room':
if (this.refs.search && payload.clear_search) {
if (this._search.current && payload.clear_search) {
this._clearSearch();
}
break;
case 'focus_room_filter':
if (this.refs.search) {
this.refs.search.focus();
if (this._search.current) {
this._search.current.focus();
}
break;
}
},
onChange: function() {
if (!this.refs.search) return;
this.setState({ searchTerm: this.refs.search.value });
if (!this._search.current) return;
this.setState({ searchTerm: this._search.current.value });
this.onSearch();
},
onSearch: throttle(function() {
this.props.onSearch(this.refs.search.value);
this.props.onSearch(this._search.current.value);
}, 200, {trailing: true, leading: true}),
_onKeyDown: function(ev) {
switch (ev.keyCode) {
case KeyCode.ESCAPE:
switch (ev.key) {
case Key.ESCAPE:
this._clearSearch("keyboard");
break;
}
@ -113,7 +117,7 @@ module.exports = createReactClass({
},
_clearSearch: function(source) {
this.refs.search.value = "";
this._search.current.value = "";
this.onChange();
if (this.props.onCleared) {
this.props.onCleared(source);
@ -146,7 +150,7 @@ module.exports = createReactClass({
<input
key="searchfield"
type="text"
ref="search"
ref={this._search}
className={"mx_textinput_icon mx_textinput_search " + className}
value={ this.state.searchTerm }
onFocus={ this._onFocus }

View file

@ -16,8 +16,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 TagOrderStore from '../../stores/TagOrderStore';
import GroupActions from '../../actions/GroupActions';
@ -28,12 +26,13 @@ import { _t } from '../../languageHandler';
import { Droppable } from 'react-beautiful-dnd';
import classNames from 'classnames';
import MatrixClientContext from "../../contexts/MatrixClientContext";
const TagPanel = createReactClass({
displayName: 'TagPanel',
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
statics: {
contextType: MatrixClientContext,
},
getInitialState() {
@ -45,8 +44,8 @@ const TagPanel = createReactClass({
componentWillMount: function() {
this.unmounted = false;
this.context.matrixClient.on("Group.myMembership", this._onGroupMyMembership);
this.context.matrixClient.on("sync", this._onClientSync);
this.context.on("Group.myMembership", this._onGroupMyMembership);
this.context.on("sync", this._onClientSync);
this._tagOrderStoreToken = TagOrderStore.addListener(() => {
if (this.unmounted) {
@ -58,13 +57,13 @@ const TagPanel = createReactClass({
});
});
// This could be done by anything with a matrix client
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
dis.dispatch(GroupActions.fetchJoinedGroups(this.context));
},
componentWillUnmount() {
this.unmounted = true;
this.context.matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
this.context.matrixClient.removeListener("sync", this._onClientSync);
this.context.removeListener("Group.myMembership", this._onGroupMyMembership);
this.context.removeListener("sync", this._onClientSync);
if (this._filterStoreToken) {
this._filterStoreToken.remove();
}
@ -72,7 +71,7 @@ const TagPanel = createReactClass({
_onGroupMyMembership() {
if (this.unmounted) return;
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
dis.dispatch(GroupActions.fetchJoinedGroups(this.context));
},
_onClientSync(syncState, prevState) {
@ -81,7 +80,7 @@ const TagPanel = createReactClass({
const reconnected = syncState !== "ERROR" && prevState !== syncState;
if (reconnected) {
// Load joined groups
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
dis.dispatch(GroupActions.fetchJoinedGroups(this.context));
}
},
@ -104,6 +103,7 @@ const TagPanel = createReactClass({
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 GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
@ -154,6 +154,13 @@ const TagPanel = createReactClass({
ref={provided.innerRef}
>
{ tags }
<div>
<ActionButton
tooltip
label={_t("Communities")}
action="toggle_my_groups"
className="mx_TagTile mx_TagTile_plus" />
</div>
{ provided.placeholder }
</div>
) }

View file

@ -1,59 +0,0 @@
/*
Copyright 2019 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import sdk from '../../index';
import dis from '../../dispatcher';
import Modal from '../../Modal';
import { _t } from '../../languageHandler';
const TagPanelButtons = createReactClass({
displayName: 'TagPanelButtons',
componentDidMount: function() {
this._dispatcherRef = dis.register(this._onAction);
},
componentWillUnmount() {
if (this._dispatcherRef) {
dis.unregister(this._dispatcherRef);
this._dispatcherRef = null;
}
},
_onAction(payload) {
if (payload.action === "show_redesign_feedback_dialog") {
const RedesignFeedbackDialog =
sdk.getComponent("views.dialogs.RedesignFeedbackDialog");
Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog);
}
},
render() {
const GroupsButton = sdk.getComponent('elements.GroupsButton');
const ActionButton = sdk.getComponent("elements.ActionButton");
return (<div className="mx_TagPanelButtons">
<GroupsButton />
<ActionButton
className="mx_TagPanelButtons_report" action="show_redesign_feedback_dialog"
label={_t("Report bugs & give feedback")} tooltip={true} />
</div>);
},
});
export default TagPanelButtons;

View file

@ -19,7 +19,7 @@ limitations under the License.
import SettingsStore from "../../settings/SettingsStore";
import React from 'react';
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import ReactDOM from "react-dom";
import PropTypes from 'prop-types';
@ -34,7 +34,7 @@ const dis = require("../../dispatcher");
const ObjectUtils = require('../../ObjectUtils');
const Modal = require("../../Modal");
const UserActivity = require("../../UserActivity");
import { KeyCode } from '../../Keyboard';
import {Key} from '../../Keyboard';
import Timer from '../../utils/Timer';
import shouldHideEvent from '../../shouldHideEvent';
import EditorStateTransfer from '../../utils/EditorStateTransfer';
@ -203,6 +203,8 @@ const TimelinePanel = createReactClass({
this.lastRRSentEventId = undefined;
this.lastRMSentEventId = undefined;
this._messagePanel = createRef();
if (this.props.manageReadReceipts) {
this.updateReadReceiptOnUserActivity();
}
@ -425,8 +427,8 @@ const TimelinePanel = createReactClass({
if (payload.action === "edit_event") {
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
this.setState({editState}, () => {
if (payload.event && this.refs.messagePanel) {
this.refs.messagePanel.scrollToEventIfNeeded(
if (payload.event && this._messagePanel.current) {
this._messagePanel.current.scrollToEventIfNeeded(
payload.event.getId(),
);
}
@ -442,9 +444,9 @@ const TimelinePanel = createReactClass({
// updates from pagination will happen when the paginate completes.
if (toStartOfTimeline || !data || !data.liveEvent) return;
if (!this.refs.messagePanel) return;
if (!this._messagePanel.current) return;
if (!this.refs.messagePanel.getScrollState().stuckAtBottom) {
if (!this._messagePanel.current.getScrollState().stuckAtBottom) {
// we won't load this event now, because we don't want to push any
// events off the other end of the timeline. But we need to note
// that we can now paginate.
@ -499,7 +501,7 @@ const TimelinePanel = createReactClass({
}
this.setState(updatedState, () => {
this.refs.messagePanel.updateTimelineMinHeight();
this._messagePanel.current.updateTimelineMinHeight();
if (callRMUpdated) {
this.props.onReadMarkerUpdated();
}
@ -510,13 +512,13 @@ const TimelinePanel = createReactClass({
onRoomTimelineReset: function(room, timelineSet) {
if (timelineSet !== this.props.timelineSet) return;
if (this.refs.messagePanel && this.refs.messagePanel.isAtBottom()) {
if (this._messagePanel.current && this._messagePanel.current.isAtBottom()) {
this._loadTimeline();
}
},
canResetTimeline: function() {
return this.refs.messagePanel && this.refs.messagePanel.isAtBottom();
return this._messagePanel.current && this._messagePanel.current.isAtBottom();
},
onRoomRedaction: function(ev, room) {
@ -629,7 +631,7 @@ const TimelinePanel = createReactClass({
sendReadReceipt: function() {
if (SettingsStore.getValue("lowBandwidth")) return;
if (!this.refs.messagePanel) return;
if (!this._messagePanel.current) return;
if (!this.props.manageReadReceipts) return;
// This happens on user_activity_end which is delayed, and it's
// very possible have logged out within that timeframe, so check
@ -815,8 +817,8 @@ const TimelinePanel = createReactClass({
if (this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
this._loadTimeline();
} else {
if (this.refs.messagePanel) {
this.refs.messagePanel.scrollToBottom();
if (this._messagePanel.current) {
this._messagePanel.current.scrollToBottom();
}
}
},
@ -826,7 +828,7 @@ const TimelinePanel = createReactClass({
*/
jumpToReadMarker: function() {
if (!this.props.manageReadMarkers) return;
if (!this.refs.messagePanel) return;
if (!this._messagePanel.current) return;
if (!this.state.readMarkerEventId) return;
// we may not have loaded the event corresponding to the read-marker
@ -835,11 +837,11 @@ const TimelinePanel = createReactClass({
//
// a quick way to figure out if we've loaded the relevant event is
// simply to check if the messagepanel knows where the read-marker is.
const ret = this.refs.messagePanel.getReadMarkerPosition();
const ret = this._messagePanel.current.getReadMarkerPosition();
if (ret !== null) {
// The messagepanel knows where the RM is, so we must have loaded
// the relevant event.
this.refs.messagePanel.scrollToEvent(this.state.readMarkerEventId,
this._messagePanel.current.scrollToEvent(this.state.readMarkerEventId,
0, 1/3);
return;
}
@ -874,8 +876,8 @@ const TimelinePanel = createReactClass({
* at the end of the live timeline.
*/
isAtEndOfLiveTimeline: function() {
return this.refs.messagePanel
&& this.refs.messagePanel.isAtBottom()
return this._messagePanel.current
&& this._messagePanel.current.isAtBottom()
&& this._timelineWindow
&& !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
},
@ -887,8 +889,8 @@ const TimelinePanel = createReactClass({
* returns null if we are not mounted.
*/
getScrollState: function() {
if (!this.refs.messagePanel) { return null; }
return this.refs.messagePanel.getScrollState();
if (!this._messagePanel.current) { return null; }
return this._messagePanel.current.getScrollState();
},
// returns one of:
@ -899,9 +901,9 @@ const TimelinePanel = createReactClass({
// +1: read marker is below the window
getReadMarkerPosition: function() {
if (!this.props.manageReadMarkers) return null;
if (!this.refs.messagePanel) return null;
if (!this._messagePanel.current) return null;
const ret = this.refs.messagePanel.getReadMarkerPosition();
const ret = this._messagePanel.current.getReadMarkerPosition();
if (ret !== null) {
return ret;
}
@ -936,15 +938,14 @@ const TimelinePanel = createReactClass({
* We pass it down to the scroll panel.
*/
handleScrollKey: function(ev) {
if (!this.refs.messagePanel) { return; }
if (!this._messagePanel.current) { return; }
// jump to the live timeline on ctrl-end, rather than the end of the
// timeline window.
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey &&
ev.keyCode == KeyCode.END) {
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey && ev.key === Key.END) {
this.jumpToLiveTimeline();
} else {
this.refs.messagePanel.handleScrollKey(ev);
this._messagePanel.current.handleScrollKey(ev);
}
},
@ -986,8 +987,8 @@ const TimelinePanel = createReactClass({
const onLoaded = () => {
// clear the timeline min-height when
// (re)loading the timeline
if (this.refs.messagePanel) {
this.refs.messagePanel.onTimelineReset();
if (this._messagePanel.current) {
this._messagePanel.current.onTimelineReset();
}
this._reloadEvents();
@ -1002,7 +1003,7 @@ const TimelinePanel = createReactClass({
timelineLoading: false,
}, () => {
// initialise the scroll state of the message panel
if (!this.refs.messagePanel) {
if (!this._messagePanel.current) {
// this shouldn't happen - we know we're mounted because
// we're in a setState callback, and we know
// timelineLoading is now false, so render() should have
@ -1012,10 +1013,10 @@ const TimelinePanel = createReactClass({
return;
}
if (eventId) {
this.refs.messagePanel.scrollToEvent(eventId, pixelOffset,
this._messagePanel.current.scrollToEvent(eventId, pixelOffset,
offsetBase);
} else {
this.refs.messagePanel.scrollToBottom();
this._messagePanel.current.scrollToBottom();
}
this.sendReadReceipt();
@ -1134,7 +1135,7 @@ const TimelinePanel = createReactClass({
const ignoreOwn = opts.ignoreOwn || false;
const allowPartial = opts.allowPartial || false;
const messagePanel = this.refs.messagePanel;
const messagePanel = this._messagePanel.current;
if (messagePanel === undefined) return null;
const EventTile = sdk.getComponent('rooms.EventTile');
@ -1313,7 +1314,8 @@ const TimelinePanel = createReactClass({
['PREPARED', 'CATCHUP'].includes(this.state.clientSyncState)
);
return (
<MessagePanel ref="messagePanel"
<MessagePanel
ref={this._messagePanel}
room={this.props.timelineSet.room}
permalinkCreator={this.props.permalinkCreator}
hidden={this.props.hidden}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
@ -48,6 +48,8 @@ module.exports = createReactClass({
componentWillMount: function() {
this._captchaWidgetId = null;
this._recaptchaContainer = createRef();
},
componentDidMount: function() {
@ -67,7 +69,7 @@ module.exports = createReactClass({
scriptTag.setAttribute(
'src', `${protocol}//www.recaptcha.net/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit`,
);
this.refs.recaptchaContainer.appendChild(scriptTag);
this._recaptchaContainer.current.appendChild(scriptTag);
}
},
@ -124,11 +126,11 @@ module.exports = createReactClass({
}
return (
<div ref="recaptchaContainer">
<div ref={this._recaptchaContainer}>
<p>{_t(
"This homeserver would like to make sure you are not a robot.",
)}</p>
<div id={DIV_ID}></div>
<div id={DIV_ID} />
{ error }
</div>
);

View file

@ -21,6 +21,7 @@ import sdk from '../../../index';
import { COUNTRIES } from '../../../phonenumber';
import SdkConfig from "../../../SdkConfig";
import { _t } from "../../../languageHandler";
const COUNTRIES_BY_ISO2 = {};
for (const c of COUNTRIES) {
@ -130,10 +131,17 @@ export default class CountryDropdown extends React.Component {
// values between mounting and the initial value propgating
const value = this.props.value || this.state.defaultCountry.iso2;
return <Dropdown className={this.props.className + " mx_CountryDropdown"}
onOptionChange={this._onOptionChange} onSearchChange={this._onSearchChange}
menuWidth={298} getShortOption={this._getShortOption}
value={value} searchEnabled={true} disabled={this.props.disabled}
return <Dropdown
id="mx_CountryDropdown"
className={this.props.className + " mx_CountryDropdown"}
onOptionChange={this._onOptionChange}
onSearchChange={this._onSearchChange}
menuWidth={298}
getShortOption={this._getShortOption}
value={value}
searchEnabled={true}
disabled={this.props.disabled}
label={_t("Country Dropdown")}
>
{ options }
</Dropdown>;

View file

@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import url from 'url';
@ -581,6 +581,8 @@ export const FallbackAuthEntry = createReactClass({
// the popup if we open it immediately.
this._popupWindow = null;
window.addEventListener("message", this._onReceiveMessage);
this._fallbackButton = createRef();
},
componentWillUnmount: function() {
@ -591,8 +593,8 @@ export const FallbackAuthEntry = createReactClass({
},
focus: function() {
if (this.refs.fallbackButton) {
this.refs.fallbackButton.focus();
if (this._fallbackButton.current) {
this._fallbackButton.current.focus();
}
},
@ -624,7 +626,7 @@ export const FallbackAuthEntry = createReactClass({
}
return (
<div>
<a ref="fallbackButton" onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
<a ref={this._fallbackButton} onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
{errorSection}
</div>
);

View file

@ -19,10 +19,10 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { MatrixClient } from 'matrix-js-sdk';
import AvatarLogic from '../../../Avatar';
import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from '../elements/AccessibleButton';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
module.exports = createReactClass({
displayName: 'BaseAvatar',
@ -38,10 +38,16 @@ module.exports = createReactClass({
// XXX resizeMethod not actually used.
resizeMethod: PropTypes.string,
defaultToInitialLetter: PropTypes.bool, // true to add default url
inputRef: PropTypes.oneOfType([
// Either a function
PropTypes.func,
// Or the instance of a DOM native element
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
]),
},
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
statics: {
contextType: MatrixClientContext,
},
getDefaultProps: function() {
@ -59,12 +65,12 @@ module.exports = createReactClass({
componentDidMount() {
this.unmounted = false;
this.context.matrixClient.on('sync', this.onClientSync);
this.context.on('sync', this.onClientSync);
},
componentWillUnmount() {
this.unmounted = true;
this.context.matrixClient.removeListener('sync', this.onClientSync);
this.context.removeListener('sync', this.onClientSync);
},
componentWillReceiveProps: function(nextProps) {
@ -148,7 +154,7 @@ module.exports = createReactClass({
const {
name, idName, title, url, urls, width, height, resizeMethod,
defaultToInitialLetter, onClick,
defaultToInitialLetter, onClick, inputRef,
...otherProps
} = this.props;
@ -171,7 +177,7 @@ module.exports = createReactClass({
if (onClick != null) {
return (
<AccessibleButton element='span' className="mx_BaseAvatar"
onClick={onClick} {...otherProps}
onClick={onClick} inputRef={inputRef} {...otherProps}
>
{ textNode }
{ imgNode }
@ -179,7 +185,7 @@ module.exports = createReactClass({
);
} else {
return (
<span className="mx_BaseAvatar" {...otherProps}>
<span className="mx_BaseAvatar" ref={inputRef} {...otherProps}>
{ textNode }
{ imgNode }
</span>
@ -188,21 +194,26 @@ module.exports = createReactClass({
}
if (onClick != null) {
return (
<AccessibleButton className="mx_BaseAvatar mx_BaseAvatar_image"
<AccessibleButton
className="mx_BaseAvatar mx_BaseAvatar_image"
element='img'
src={imageUrl}
onClick={onClick}
onError={this.onError}
width={width} height={height}
title={title} alt=""
inputRef={inputRef}
{...otherProps} />
);
} else {
return (
<img className="mx_BaseAvatar mx_BaseAvatar_image" src={imageUrl}
<img
className="mx_BaseAvatar mx_BaseAvatar_image"
src={imageUrl}
onError={this.onError}
width={width} height={height}
title={title} alt=""
ref={inputRef}
{...otherProps} />
);
}

View file

@ -38,8 +38,8 @@ export default class MemberStatusMessageAvatar extends React.Component {
resizeMethod: 'crop',
};
constructor(props, context) {
super(props, context);
constructor(props) {
super(props);
this.state = {
hasStatus: this.hasStatus,

View file

@ -31,8 +31,8 @@ export default class GroupInviteTileContextMenu extends React.Component {
onFinished: PropTypes.func,
};
constructor(props, context) {
super(props, context);
constructor(props) {
super(props);
this._onClickReject = this._onClickReject.bind(this);
}

View file

@ -27,8 +27,8 @@ export default class StatusMessageContextMenu extends React.Component {
user: PropTypes.object,
};
constructor(props, context) {
super(props, context);
constructor(props) {
super(props);
this.state = {
message: this.comittedStatusMessage,

View file

@ -17,12 +17,12 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher';
import TagOrderActions from '../../../actions/TagOrderActions';
import sdk from '../../../index';
import {MenuItem} from "../../structures/ContextMenu";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
export default class TagTileContextMenu extends React.Component {
static propTypes = {
@ -31,9 +31,7 @@ export default class TagTileContextMenu extends React.Component {
onFinished: PropTypes.func.isRequired,
};
static contextTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient),
};
static contextType = MatrixClientContext;
constructor() {
super();
@ -51,7 +49,7 @@ export default class TagTileContextMenu extends React.Component {
}
_onRemoveClick() {
dis.dispatch(TagOrderActions.removeTag(this.context.matrixClient, this.props.tag));
dis.dispatch(TagOrderActions.removeTag(this.context, this.props.tag));
this.props.onFinished();
}

View file

@ -25,6 +25,7 @@ import SdkConfig from '../../../SdkConfig';
import { getHostingLink } from '../../../utils/HostingLink';
import MatrixClientPeg from '../../../MatrixClientPeg';
import {MenuItem} from "../../structures/ContextMenu";
import sdk from "../../../index";
export class TopLeftMenu extends React.Component {
static propTypes = {
@ -100,6 +101,12 @@ export class TopLeftMenu extends React.Component {
);
}
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")}
@ -115,11 +122,18 @@ export class TopLeftMenu extends React.Component {
<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();

View file

@ -17,7 +17,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
@ -32,6 +32,7 @@ import IdentityAuthClient from '../../../IdentityAuthClient';
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../utils/IdentityServerUtils';
import { abbreviateUrl } from '../../../utils/UrlUtils';
import {sleep} from "../../../utils/promise";
import {Key} from "../../../Keyboard";
const TRUNCATE_QUERY_LIST = 40;
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
@ -106,10 +107,14 @@ module.exports = createReactClass({
};
},
UNSAFE_componentWillMount: function() {
this._textinput = createRef();
},
componentDidMount: function() {
if (this.props.focus) {
// Set the cursor at the end of the text input
this.refs.textinput.value = this.props.value;
this._textinput.current.value = this.props.value;
}
},
@ -126,8 +131,8 @@ module.exports = createReactClass({
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
if (this.refs.textinput.value !== '') {
selectedList = this._addAddressesToList([this.refs.textinput.value]);
if (this._textinput.current.value !== '') {
selectedList = this._addAddressesToList([this._textinput.current.value]);
if (selectedList === null) return;
}
this.props.onFinished(true, selectedList);
@ -138,39 +143,41 @@ module.exports = createReactClass({
},
onKeyDown: function(e) {
if (e.keyCode === 27) { // escape
const textInput = this._textinput.current ? this._textinput.current.value : undefined;
if (e.key === Key.ESCAPE) {
e.stopPropagation();
e.preventDefault();
this.props.onFinished(false);
} else if (e.keyCode === 38) { // up arrow
} else if (e.key === Key.ARROW_UP) {
e.stopPropagation();
e.preventDefault();
if (this.addressSelector) this.addressSelector.moveSelectionUp();
} else if (e.keyCode === 40) { // down arrow
} else if (e.key === Key.ARROW_DOWN) {
e.stopPropagation();
e.preventDefault();
if (this.addressSelector) this.addressSelector.moveSelectionDown();
} else if (this.state.suggestedList.length > 0 && (e.keyCode === 188 || e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab
} else if (this.state.suggestedList.length > 0 && [Key.COMMA, Key.ENTER, Key.TAB].includes(e.key)) {
e.stopPropagation();
e.preventDefault();
if (this.addressSelector) this.addressSelector.chooseSelection();
} else if (this.refs.textinput.value.length === 0 && this.state.selectedList.length && e.keyCode === 8) { // backspace
} else if (textInput.length === 0 && this.state.selectedList.length && e.key === Key.BACKSPACE) {
e.stopPropagation();
e.preventDefault();
this.onDismissed(this.state.selectedList.length - 1)();
} else if (e.keyCode === 13) { // enter
} else if (e.key === Key.ENTER) {
e.stopPropagation();
e.preventDefault();
if (this.refs.textinput.value === '') {
if (textInput === '') {
// if there's nothing in the input box, submit the form
this.onButtonClick();
} else {
this._addAddressesToList([this.refs.textinput.value]);
this._addAddressesToList([textInput]);
}
} else if (e.keyCode === 188 || e.keyCode === 9) { // comma or tab
} else if (textInput && (e.key === Key.COMMA || e.key === Key.TAB)) {
e.stopPropagation();
e.preventDefault();
this._addAddressesToList([this.refs.textinput.value]);
this._addAddressesToList([textInput]);
}
},
@ -647,7 +654,7 @@ module.exports = createReactClass({
onPaste={this._onPaste}
rows="1"
id="textinput"
ref="textinput"
ref={this._textinput}
className="mx_AddressPickerDialog_input"
onChange={this.onQueryChanged}
placeholder={this.getPlaceholder()}

View file

@ -1,6 +1,7 @@
/*
Copyright 2017 Vector Creations Ltd
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.
@ -17,16 +18,15 @@ limitations under the License.
import React from 'react';
import createReactClass from 'create-react-class';
import FocusTrap from 'focus-trap-react';
import FocusLock from 'react-focus-lock';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { MatrixClient } from 'matrix-js-sdk';
import { KeyCode } from '../../../Keyboard';
import { Key } from '../../../Keyboard';
import AccessibleButton from '../elements/AccessibleButton';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { _t } from "../../../languageHandler";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
/**
* Basic container for modal dialogs.
@ -83,16 +83,6 @@ export default createReactClass({
};
},
childContextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
},
getChildContext: function() {
return {
matrixClient: this._matrixClient,
};
},
componentWillMount() {
this._matrixClient = MatrixClientPeg.get();
},
@ -101,7 +91,7 @@ export default createReactClass({
if (this.props.onKeyDown) {
this.props.onKeyDown(e);
}
if (this.props.hasCancel && e.keyCode === KeyCode.ESCAPE) {
if (this.props.hasCancel && e.key === Key.ESCAPE) {
e.stopPropagation();
e.preventDefault();
this.props.onFinished(false);
@ -121,32 +111,38 @@ export default createReactClass({
}
return (
<FocusTrap onKeyDown={this._onKeyDown}
className={classNames({
[this.props.className]: true,
'mx_Dialog_fixedWidth': this.props.fixedWidth,
})}
role="dialog"
aria-labelledby='mx_BaseDialog_title'
// This should point to a node describing the dialog.
// If we were about to completely follow this recommendation we'd need to
// make all the components relying on BaseDialog to be aware of it.
// So instead we will use the whole content as the description.
// Description comes first and if the content contains more text,
// AT users can skip its presentation.
aria-describedby={this.props.contentId}
>
<div className={classNames('mx_Dialog_header', {
'mx_Dialog_headerWithButton': !!this.props.headerButton,
})}>
<div className={classNames('mx_Dialog_title', this.props.titleClass)} id='mx_BaseDialog_title'>
{ this.props.title }
<MatrixClientContext.Provider value={this._matrixClient}>
<FocusLock
returnFocus={true}
lockProps={{
onKeyDown: this._onKeyDown,
role: "dialog",
["aria-labelledby"]: "mx_BaseDialog_title",
// This should point to a node describing the dialog.
// If we were about to completely follow this recommendation we'd need to
// make all the components relying on BaseDialog to be aware of it.
// So instead we will use the whole content as the description.
// Description comes first and if the content contains more text,
// AT users can skip its presentation.
["aria-describedby"]: this.props.contentId,
}}
className={classNames({
[this.props.className]: true,
'mx_Dialog_fixedWidth': this.props.fixedWidth,
})}
>
<div className={classNames('mx_Dialog_header', {
'mx_Dialog_headerWithButton': !!this.props.headerButton,
})}>
<div className={classNames('mx_Dialog_title', this.props.titleClass)} id='mx_BaseDialog_title'>
{ this.props.title }
</div>
{ this.props.headerButton }
{ cancelButton }
</div>
{ this.props.headerButton }
{ cancelButton }
</div>
{ this.props.children }
</FocusTrap>
{ this.props.children }
</FocusLock>
</MatrixClientContext.Provider>
);
},
});

View file

@ -25,8 +25,8 @@ import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
export default class BugReportDialog extends React.Component {
constructor(props, context) {
super(props, context);
constructor(props) {
super(props);
this.state = {
sendLogs: true,
busy: false,

View file

@ -173,7 +173,7 @@ export default createReactClass({
const domain = MatrixClientPeg.get().getDomain();
aliasField = (
<div className="mx_CreateRoomDialog_aliasContainer">
<RoomAliasField id="alias" ref={ref => this._aliasFieldRef = ref} onChange={this.onAliasChange} domain={domain} />
<RoomAliasField id="alias" ref={ref => this._aliasFieldRef = ref} onChange={this.onAliasChange} domain={domain} value={this.state.alias} />
</div>
);
} else {

View file

@ -25,8 +25,8 @@ import * as Lifecycle from '../../../Lifecycle';
import { _t } from '../../../languageHandler';
export default class DeactivateAccountDialog extends React.Component {
constructor(props, context) {
super(props, context);
constructor(props) {
super(props);
this._onOk = this._onOk.bind(this);
this._onCancel = this._onCancel.bind(this);

View file

@ -97,7 +97,7 @@ export default class DeviceVerifyDialog extends React.Component {
const client = MatrixClientPeg.get();
const verifyingOwnDevice = this.props.userId === client.getUserId();
try {
if (!verifyingOwnDevice && SettingsStore.getValue("feature_dm_verification")) {
if (!verifyingOwnDevice && SettingsStore.getValue("feature_cross_signing")) {
const roomId = await ensureDMExistsAndOpen(this.props.userId);
// throws upon cancellation before having started
this._verifier = await client.requestVerificationDM(

View file

@ -16,23 +16,19 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import { Room } from "matrix-js-sdk";
import sdk from '../../../index';
import SyntaxHighlight from '../elements/SyntaxHighlight';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import Field from "../elements/Field";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
class DevtoolsComponent extends React.Component {
static contextTypes = {
roomId: PropTypes.string.isRequired,
};
}
class GenericEditor extends DevtoolsComponent {
class GenericEditor extends React.PureComponent {
// static propTypes = {onBack: PropTypes.func.isRequired};
constructor(props, context) {
super(props, context);
constructor(props) {
super(props);
this._onChange = this._onChange.bind(this);
this.onBack = this.onBack.bind(this);
}
@ -67,12 +63,15 @@ class SendCustomEvent extends GenericEditor {
static propTypes = {
onBack: PropTypes.func.isRequired,
room: PropTypes.instanceOf(Room).isRequired,
forceStateEvent: PropTypes.bool,
inputs: PropTypes.object,
};
constructor(props, context) {
super(props, context);
static contextType = MatrixClientContext;
constructor(props) {
super(props);
this._send = this._send.bind(this);
const {eventType, stateKey, evContent} = Object.assign({
@ -91,11 +90,11 @@ class SendCustomEvent extends GenericEditor {
}
send(content) {
const cli = MatrixClientPeg.get();
const cli = this.context;
if (this.state.isStateEvent) {
return cli.sendStateEvent(this.context.roomId, this.state.eventType, content, this.state.stateKey);
return cli.sendStateEvent(this.props.room.roomId, this.state.eventType, content, this.state.stateKey);
} else {
return cli.sendEvent(this.context.roomId, this.state.eventType, content);
return cli.sendEvent(this.props.room.roomId, this.state.eventType, content);
}
}
@ -154,13 +153,16 @@ class SendAccountData extends GenericEditor {
static getLabel() { return _t('Send Account Data'); }
static propTypes = {
room: PropTypes.instanceOf(Room).isRequired,
isRoomAccountData: PropTypes.bool,
forceMode: PropTypes.bool,
inputs: PropTypes.object,
};
constructor(props, context) {
super(props, context);
static contextType = MatrixClientContext;
constructor(props) {
super(props);
this._send = this._send.bind(this);
const {eventType, evContent} = Object.assign({
@ -177,9 +179,9 @@ class SendAccountData extends GenericEditor {
}
send(content) {
const cli = MatrixClientPeg.get();
const cli = this.context;
if (this.state.isRoomAccountData) {
return cli.setRoomAccountData(this.context.roomId, this.state.eventType, content);
return cli.setRoomAccountData(this.props.room.roomId, this.state.eventType, content);
}
return cli.setAccountData(this.state.eventType, content);
}
@ -234,7 +236,7 @@ class SendAccountData extends GenericEditor {
const INITIAL_LOAD_TILES = 20;
const LOAD_TILES_STEP_SIZE = 50;
class FilteredList extends React.Component {
class FilteredList extends React.PureComponent {
static propTypes = {
children: PropTypes.any,
query: PropTypes.string,
@ -247,8 +249,8 @@ class FilteredList extends React.Component {
return children.filter((child) => child.key.toLowerCase().includes(lcQuery));
}
constructor(props, context) {
super(props, context);
constructor(props) {
super(props);
this.state = {
filteredChildren: FilteredList.filterChildren(this.props.children, this.props.query),
@ -305,19 +307,20 @@ class FilteredList extends React.Component {
}
}
class RoomStateExplorer extends DevtoolsComponent {
class RoomStateExplorer extends React.PureComponent {
static getLabel() { return _t('Explore Room State'); }
static propTypes = {
onBack: PropTypes.func.isRequired,
room: PropTypes.instanceOf(Room).isRequired,
};
constructor(props, context) {
super(props, context);
static contextType = MatrixClientContext;
const room = MatrixClientPeg.get().getRoom(this.context.roomId);
this.roomStateEvents = room.currentState.events;
constructor(props) {
super(props);
this.roomStateEvents = this.props.room.currentState.events;
this.onBack = this.onBack.bind(this);
this.editEv = this.editEv.bind(this);
@ -373,7 +376,7 @@ class RoomStateExplorer extends DevtoolsComponent {
render() {
if (this.state.event) {
if (this.state.editing) {
return <SendCustomEvent forceStateEvent={true} onBack={this.onBack} inputs={{
return <SendCustomEvent room={this.props.room} forceStateEvent={true} onBack={this.onBack} inputs={{
eventType: this.state.event.getType(),
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
stateKey: this.state.event.getStateKey(),
@ -442,15 +445,18 @@ class RoomStateExplorer extends DevtoolsComponent {
}
}
class AccountDataExplorer extends DevtoolsComponent {
class AccountDataExplorer extends React.PureComponent {
static getLabel() { return _t('Explore Account Data'); }
static propTypes = {
onBack: PropTypes.func.isRequired,
room: PropTypes.instanceOf(Room).isRequired,
};
constructor(props, context) {
super(props, context);
static contextType = MatrixClientContext;
constructor(props) {
super(props);
this.onBack = this.onBack.bind(this);
this.editEv = this.editEv.bind(this);
@ -467,11 +473,10 @@ class AccountDataExplorer extends DevtoolsComponent {
}
getData() {
const cli = MatrixClientPeg.get();
if (this.state.isRoomAccountData) {
return cli.getRoom(this.context.roomId).accountData;
return this.props.room.accountData;
}
return cli.store.accountData;
return this.context.store.accountData;
}
onViewSourceClick(event) {
@ -505,10 +510,14 @@ class AccountDataExplorer extends DevtoolsComponent {
render() {
if (this.state.event) {
if (this.state.editing) {
return <SendAccountData isRoomAccountData={this.state.isRoomAccountData} onBack={this.onBack} inputs={{
eventType: this.state.event.getType(),
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
}} forceMode={true} />;
return <SendAccountData
room={this.props.room}
isRoomAccountData={this.state.isRoomAccountData}
onBack={this.onBack}
inputs={{
eventType: this.state.event.getType(),
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
}} forceMode={true} />;
}
return <div className="mx_ViewSource">
@ -553,17 +562,20 @@ class AccountDataExplorer extends DevtoolsComponent {
}
}
class ServersInRoomList extends DevtoolsComponent {
class ServersInRoomList extends React.PureComponent {
static getLabel() { return _t('View Servers in Room'); }
static propTypes = {
onBack: PropTypes.func.isRequired,
room: PropTypes.instanceOf(Room).isRequired,
};
constructor(props, context) {
super(props, context);
static contextType = MatrixClientContext;
const room = MatrixClientPeg.get().getRoom(this.context.roomId);
constructor(props) {
super(props);
const room = this.props.room;
const servers = new Set();
room.currentState.getStateEvents("m.room.member").forEach(ev => servers.add(ev.getSender().split(":")[1]));
this.servers = Array.from(servers).map(s =>
@ -602,19 +614,14 @@ const Entries = [
ServersInRoomList,
];
export default class DevtoolsDialog extends React.Component {
static childContextTypes = {
roomId: PropTypes.string.isRequired,
// client: PropTypes.instanceOf(MatixClient),
};
export default class DevtoolsDialog extends React.PureComponent {
static propTypes = {
roomId: PropTypes.string.isRequired,
onFinished: PropTypes.func.isRequired,
};
constructor(props, context) {
super(props, context);
constructor(props) {
super(props);
this.onBack = this.onBack.bind(this);
this.onCancel = this.onCancel.bind(this);
@ -627,10 +634,6 @@ export default class DevtoolsDialog extends React.Component {
this._unmounted = true;
}
getChildContext() {
return { roomId: this.props.roomId };
}
_setMode(mode) {
return () => {
this.setState({ mode });
@ -654,15 +657,17 @@ export default class DevtoolsDialog extends React.Component {
let body;
if (this.state.mode) {
body = <div>
<div className="mx_DevTools_label_left">{ this.state.mode.getLabel() }</div>
<div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div>
<div className="mx_DevTools_label_bottom" />
<this.state.mode onBack={this.onBack} />
</div>;
body = <MatrixClientContext.Consumer>
{(cli) => <React.Fragment>
<div className="mx_DevTools_label_left">{ this.state.mode.getLabel() }</div>
<div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div>
<div className="mx_DevTools_label_bottom" />
<this.state.mode onBack={this.onBack} room={cli.getRoom(this.props.roomId)} />
</React.Fragment>}
</MatrixClientContext.Consumer>;
} else {
const classes = "mx_DevTools_RoomStateExplorer_button";
body = <div>
body = <React.Fragment>
<div>
<div className="mx_DevTools_label_left">{ _t('Toolbox') }</div>
<div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div>
@ -679,7 +684,7 @@ export default class DevtoolsDialog extends React.Component {
<div className="mx_Dialog_buttons">
<button onClick={this.onCancel}>{ _t('Cancel') }</button>
</div>
</div>;
</React.Fragment>;
}
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');

View file

@ -99,7 +99,7 @@ export default createReactClass({
this.props.onFinished(true);
}
},
});
}, null, /* priority = */ false, /* static = */ true);
},
_onShareClicked: function() {

View file

@ -30,8 +30,8 @@ export default class ReportEventDialog extends PureComponent {
onFinished: PropTypes.func.isRequired,
};
constructor(props, context) {
super(props, context);
constructor(props) {
super(props);
this.state = {
reason: "",

View file

@ -15,13 +15,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import classnames from 'classnames';
import { KeyCode } from '../../../Keyboard';
import { Key } from '../../../Keyboard';
import { _t } from '../../../languageHandler';
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
@ -62,8 +62,13 @@ export default createReactClass({
};
},
UNSAFE_componentWillMount: function() {
this._input_value = createRef();
this._uiAuth = createRef();
},
componentDidMount: function() {
this.refs.input_value.select();
this._input_value.current.select();
this._matrixClient = MatrixClientPeg.get();
},
@ -96,14 +101,14 @@ export default createReactClass({
},
onKeyUp: function(ev) {
if (ev.keyCode === KeyCode.ENTER) {
if (ev.key === Key.ENTER) {
this.onSubmit();
}
},
onSubmit: function(ev) {
if (this.refs.uiAuth) {
this.refs.uiAuth.tryContinue();
if (this._uiAuth.current) {
this._uiAuth.current.tryContinue();
}
this.setState({
doingUIAuth: true,
@ -215,7 +220,7 @@ export default createReactClass({
onAuthFinished={this._onUIAuthFinished}
inputs={{}}
poll={true}
ref="uiAuth"
ref={this._uiAuth}
continueIsManaged={true}
/>;
}
@ -257,7 +262,7 @@ export default createReactClass({
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>
<div className="mx_SetMxIdDialog_input_group">
<input type="text" ref="input_value" value={this.state.username}
<input type="text" ref={this._input_value} value={this.state.username}
autoFocus={true}
onChange={this.onValueChange}
onKeyUp={this.onKeyUp}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import {Room, User, Group, RoomMember, MatrixEvent} from 'matrix-js-sdk';
import sdk from '../../../index';
@ -74,6 +74,8 @@ export default class ShareDialog extends React.Component {
// MatrixEvent defaults to share linkSpecificEvent
linkSpecificEvent: this.props.target instanceof MatrixEvent,
};
this._link = createRef();
}
static _selectText(target) {
@ -94,7 +96,7 @@ export default class ShareDialog extends React.Component {
onCopyClick(e) {
e.preventDefault();
ShareDialog._selectText(this.refs.link);
ShareDialog._selectText(this._link.current);
let successful;
try {
@ -106,7 +108,7 @@ export default class ShareDialog extends React.Component {
const buttonRect = e.target.getBoundingClientRect();
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
...toRightOf(buttonRect, 11),
...toRightOf(buttonRect, 2),
message: successful ? _t('Copied!') : _t('Failed to copy'),
});
// Drop a reference to this close handler for componentWillUnmount
@ -195,7 +197,7 @@ export default class ShareDialog extends React.Component {
>
<div className="mx_ShareDialog_content">
<div className="mx_ShareDialog_matrixto">
<a ref="link"
<a ref={this._link}
href={matrixToUrl}
onClick={ShareDialog.onLinkClick}
className="mx_ShareDialog_matrixto_link"

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import sdk from '../../../index';
@ -42,15 +42,19 @@ export default createReactClass({
};
},
UNSAFE_componentWillMount: function() {
this._textinput = createRef();
},
componentDidMount: function() {
if (this.props.focus) {
// Set the cursor at the end of the text input
this.refs.textinput.value = this.props.value;
this._textinput.current.value = this.props.value;
}
},
onOk: function() {
this.props.onFinished(true, this.refs.textinput.value);
this.props.onFinished(true, this._textinput.current.value);
},
onCancel: function() {
@ -70,7 +74,13 @@ export default createReactClass({
<label htmlFor="textinput"> { this.props.description } </label>
</div>
<div>
<input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" />
<input
id="textinput"
ref={this._textinput}
className="mx_TextInputDialog_input"
defaultValue={this.props.value}
autoFocus={this.props.focus}
size="64" />
</div>
</div>
</form>

View file

@ -15,7 +15,6 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import sdk from '../../../../index';
import MatrixClientPeg from '../../../../MatrixClientPeg';
import Modal from '../../../../Modal';
@ -28,12 +27,13 @@ import {Key} from "../../../../Keyboard";
const RESTORE_TYPE_PASSPHRASE = 0;
const RESTORE_TYPE_RECOVERYKEY = 1;
/**
/*
* Dialog for restoring e2e keys from a backup and the user's recovery key
*/
export default createReactClass({
getInitialState: function() {
return {
export default class RestoreKeyBackupDialog extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
backupInfo: null,
loading: false,
loadError: null,
@ -45,27 +45,27 @@ export default createReactClass({
passPhrase: '',
restoreType: null,
};
},
}
componentWillMount: function() {
componentDidMount() {
this._loadBackupStatus();
},
}
_onCancel: function() {
_onCancel = () => {
this.props.onFinished(false);
},
}
_onDone: function() {
_onDone = () => {
this.props.onFinished(true);
},
}
_onUseRecoveryKeyClick: function() {
_onUseRecoveryKeyClick = () => {
this.setState({
forceRecoveryKey: true,
});
},
}
_onResetRecoveryClick: function() {
_onResetRecoveryClick = () => {
this.props.onFinished(false);
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
import('../../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
@ -75,16 +75,16 @@ export default createReactClass({
},
},
);
},
}
_onRecoveryKeyChange: function(e) {
_onRecoveryKeyChange = (e) => {
this.setState({
recoveryKey: e.target.value,
recoveryKeyValid: MatrixClientPeg.get().isValidRecoveryKey(e.target.value),
});
},
}
_onPassPhraseNext: async function() {
_onPassPhraseNext = async () => {
this.setState({
loading: true,
restoreError: null,
@ -105,9 +105,9 @@ export default createReactClass({
restoreError: e,
});
}
},
}
_onRecoveryKeyNext: async function() {
_onRecoveryKeyNext = async () => {
this.setState({
loading: true,
restoreError: null,
@ -128,27 +128,27 @@ export default createReactClass({
restoreError: e,
});
}
},
}
_onPassPhraseChange: function(e) {
_onPassPhraseChange = (e) => {
this.setState({
passPhrase: e.target.value,
});
},
}
_onPassPhraseKeyPress: function(e) {
_onPassPhraseKeyPress = (e) => {
if (e.key === Key.ENTER) {
this._onPassPhraseNext();
}
},
}
_onRecoveryKeyKeyPress: function(e) {
_onRecoveryKeyKeyPress = (e) => {
if (e.key === Key.ENTER && this.state.recoveryKeyValid) {
this._onRecoveryKeyNext();
}
},
}
_loadBackupStatus: async function() {
async _loadBackupStatus() {
this.setState({
loading: true,
loadError: null,
@ -167,9 +167,9 @@ export default createReactClass({
loading: false,
});
}
},
}
render: function() {
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Spinner = sdk.getComponent("elements.Spinner");
@ -296,7 +296,7 @@ export default createReactClass({
content = <div>
<p>{_t(
"<b>Warning</b>: you should only set up key backup " +
"<b>Warning</b>: You should only set up key backup " +
"from a trusted computer.", {},
{ b: sub => <b>{sub}</b> },
)}</p>
@ -322,7 +322,7 @@ export default createReactClass({
/>
</div>
{_t(
"If you've forgotten your recovery passphrase you can "+
"If you've forgotten your recovery key you can "+
"<button>set up new recovery options</button>"
, {}, {
button: s => <AccessibleButton className="mx_linkButton"
@ -345,5 +345,5 @@ export default createReactClass({
</div>
</BaseDialog>
);
},
});
}
}

View file

@ -0,0 +1,265 @@
/*
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 sdk from '../../../../index';
import MatrixClientPeg from '../../../../MatrixClientPeg';
import { _t } from '../../../../languageHandler';
import { Key } from "../../../../Keyboard";
/*
* Access Secure Secret Storage by requesting the user's passphrase.
*/
export default class AccessSecretStorageDialog extends React.PureComponent {
static propTypes = {
// { passphrase, pubkey }
keyInfo: PropTypes.object.isRequired,
// Function from one of { passphrase, recoveryKey } -> boolean
checkPrivateKey: PropTypes.func.isRequired,
}
constructor(props) {
super(props);
this.state = {
recoveryKey: "",
recoveryKeyValid: false,
forceRecoveryKey: false,
passPhrase: '',
keyMatches: null,
};
}
_onCancel = () => {
this.props.onFinished(false);
}
_onUseRecoveryKeyClick = () => {
this.setState({
forceRecoveryKey: true,
});
}
_onResetRecoveryClick = () => {
this.props.onFinished(false);
throw new Error("Resetting secret storage unimplemented");
}
_onRecoveryKeyChange = (e) => {
this.setState({
recoveryKey: e.target.value,
recoveryKeyValid: MatrixClientPeg.get().isValidRecoveryKey(e.target.value),
keyMatches: null,
});
}
_onPassPhraseNext = async () => {
this.setState({ keyMatches: null });
const input = { passphrase: this.state.passPhrase };
const keyMatches = await this.props.checkPrivateKey(input);
if (keyMatches) {
this.props.onFinished(input);
} else {
this.setState({ keyMatches });
}
}
_onRecoveryKeyNext = async () => {
this.setState({ keyMatches: null });
const input = { recoveryKey: this.state.recoveryKey };
const keyMatches = await this.props.checkPrivateKey(input);
if (keyMatches) {
this.props.onFinished(input);
} else {
this.setState({ keyMatches });
}
}
_onPassPhraseChange = (e) => {
this.setState({
passPhrase: e.target.value,
keyMatches: null,
});
}
_onPassPhraseKeyPress = (e) => {
if (e.key === Key.ENTER && this.state.passPhrase.length > 0) {
this._onPassPhraseNext();
}
}
_onRecoveryKeyKeyPress = (e) => {
if (e.key === Key.ENTER && this.state.recoveryKeyValid) {
this._onRecoveryKeyNext();
}
}
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const hasPassphrase = (
this.props.keyInfo &&
this.props.keyInfo.passphrase &&
this.props.keyInfo.passphrase.salt &&
this.props.keyInfo.passphrase.iterations
);
let content;
let title;
if (hasPassphrase && !this.state.forceRecoveryKey) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
title = _t("Enter secret storage passphrase");
let keyStatus;
if (this.state.keyMatches === false) {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
{"\uD83D\uDC4E "}{_t(
"Unable to access secret storage. Please verify that you " +
"entered the correct passphrase.",
)}
</div>;
} else {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus"></div>;
}
content = <div>
<p>{_t(
"<b>Warning</b>: You should only access secret storage " +
"from a trusted computer.", {},
{ b: sub => <b>{sub}</b> },
)}</p>
<p>{_t(
"Access your secure message history and your cross-signing " +
"identity for verifying other devices by entering your passphrase.",
)}</p>
<div className="mx_AccessSecretStorageDialog_primaryContainer">
<input type="password"
className="mx_AccessSecretStorageDialog_passPhraseInput"
onChange={this._onPassPhraseChange}
onKeyPress={this._onPassPhraseKeyPress}
value={this.state.passPhrase}
autoFocus={true}
/>
{keyStatus}
<DialogButtons primaryButton={_t('Next')}
onPrimaryButtonClick={this._onPassPhraseNext}
hasCancel={true}
onCancel={this._onCancel}
focus={false}
primaryDisabled={this.state.passPhrase.length === 0}
/>
</div>
{_t(
"If you've forgotten your passphrase you can "+
"<button1>use your recovery key</button1> or " +
"<button2>set up new recovery options</button2>."
, {}, {
button1: s => <AccessibleButton className="mx_linkButton"
element="span"
onClick={this._onUseRecoveryKeyClick}
>
{s}
</AccessibleButton>,
button2: s => <AccessibleButton className="mx_linkButton"
element="span"
onClick={this._onResetRecoveryClick}
>
{s}
</AccessibleButton>,
})}
</div>;
} else {
title = _t("Enter secret storage recovery key");
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let keyStatus;
if (this.state.recoveryKey.length === 0) {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus"></div>;
} else if (this.state.recoveryKeyValid) {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
{"\uD83D\uDC4D "}{_t("This looks like a valid recovery key!")}
</div>;
} else if (this.state.keyMatches === false) {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
{"\uD83D\uDC4E "}{_t(
"Unable to access secret storage. Please verify that you " +
"entered the correct recovery key.",
)}
</div>;
} else {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
{"\uD83D\uDC4E "}{_t("Not a valid recovery key")}
</div>;
}
content = <div>
<p>{_t(
"<b>Warning</b>: You should only access secret storage " +
"from a trusted computer.", {},
{ b: sub => <b>{sub}</b> },
)}</p>
<p>{_t(
"Access your secure message history and your cross-signing " +
"identity for verifying other devices by entering your recovery key.",
)}</p>
<div className="mx_AccessSecretStorageDialog_primaryContainer">
<input className="mx_AccessSecretStorageDialog_recoveryKeyInput"
onChange={this._onRecoveryKeyChange}
onKeyPress={this._onRecoveryKeyKeyPress}
value={this.state.recoveryKey}
autoFocus={true}
/>
{keyStatus}
<DialogButtons primaryButton={_t('Next')}
onPrimaryButtonClick={this._onRecoveryKeyNext}
hasCancel={true}
onCancel={this._onCancel}
focus={false}
primaryDisabled={!this.state.recoveryKeyValid}
/>
</div>
{_t(
"If you've forgotten your recovery key you can "+
"<button>set up new recovery options</button>."
, {}, {
button: s => <AccessibleButton className="mx_linkButton"
element="span"
onClick={this._onResetRecoveryClick}
>
{s}
</AccessibleButton>,
})}
</div>;
}
return (
<BaseDialog className='mx_AccessSecretStorageDialog'
onFinished={this.props.onFinished}
title={title}
>
<div>
{content}
</div>
</BaseDialog>
);
}
}

View file

@ -17,7 +17,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { KeyCode } from '../../../Keyboard';
import {Key} from '../../../Keyboard';
/**
* AccessibleButton is a generic wrapper for any element that should be treated
@ -40,23 +40,23 @@ export default function AccessibleButton(props) {
// Browsers handle space and enter keypresses differently and we are only adjusting to the
// inconsistencies here
restProps.onKeyDown = function(e) {
if (e.keyCode === KeyCode.ENTER) {
if (e.key === Key.ENTER) {
e.stopPropagation();
e.preventDefault();
return onClick(e);
}
if (e.keyCode === KeyCode.SPACE) {
if (e.key === Key.SPACE) {
e.stopPropagation();
e.preventDefault();
}
};
restProps.onKeyUp = function(e) {
if (e.keyCode === KeyCode.SPACE) {
if (e.key === Key.SPACE) {
e.stopPropagation();
e.preventDefault();
return onClick(e);
}
if (e.keyCode === KeyCode.ENTER) {
if (e.key === Key.ENTER) {
e.stopPropagation();
e.preventDefault();
}

View file

@ -48,7 +48,7 @@ export default class AccessibleTooltipButton extends React.PureComponent {
const Tooltip = sdk.getComponent("elements.Tooltip");
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
const {title, ...props} = this.props;
const {title, children, ...props} = this.props;
const tip = this.state.hover ? <Tooltip
className="mx_AccessibleTooltipButton_container"
@ -57,6 +57,7 @@ export default class AccessibleTooltipButton extends React.PureComponent {
/> : <div />;
return (
<AccessibleButton {...props} onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} aria-label={title}>
{ children }
{ tip }
</AccessibleButton>
);

View file

@ -64,6 +64,8 @@ export default class AppTile extends React.Component {
this._onReloadWidgetClick = this._onReloadWidgetClick.bind(this);
this._contextMenuButton = createRef();
this._appFrame = createRef();
this._menu_bar = createRef();
}
/**
@ -337,14 +339,14 @@ export default class AppTile extends React.Component {
// HACK: This is a really dirty way to ensure that Jitsi cleans up
// its hold on the webcam. Without this, the widget holds a media
// stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351
if (this.refs.appFrame) {
if (this._appFrame.current) {
// In practice we could just do `+= ''` to trick the browser
// into thinking the URL changed, however I can foresee this
// being optimized out by a browser. Instead, we'll just point
// the iframe at a page that is reasonably safe to use in the
// event the iframe doesn't wink away.
// This is relative to where the Riot instance is located.
this.refs.appFrame.src = 'about:blank';
this._appFrame.current.src = 'about:blank';
}
WidgetUtils.setRoomWidget(
@ -389,7 +391,7 @@ export default class AppTile extends React.Component {
// FIXME: There's probably no reason to do this here: it should probably be done entirely
// in ActiveWidgetStore.
const widgetMessaging = new WidgetMessaging(
this.props.id, this.props.url, this.props.userWidget, this.refs.appFrame.contentWindow);
this.props.id, this.props.url, this.props.userWidget, this._appFrame.current.contentWindow);
ActiveWidgetStore.setWidgetMessaging(this.props.id, widgetMessaging);
widgetMessaging.getCapabilities().then((requestedCapabilities) => {
console.log(`Widget ${this.props.id} requested capabilities: ` + requestedCapabilities);
@ -496,7 +498,7 @@ export default class AppTile extends React.Component {
ev.preventDefault();
// Ignore clicks on menu bar children
if (ev.target !== this.refs.menu_bar) {
if (ev.target !== this._menu_bar.current) {
return;
}
@ -555,7 +557,7 @@ export default class AppTile extends React.Component {
_onReloadWidgetClick() {
// Reload iframe in this way to avoid cross-origin restrictions
this.refs.appFrame.src = this.refs.appFrame.src;
this._appFrame.current.src = this._appFrame.current.src;
}
_onContextMenuClick = () => {
@ -626,7 +628,7 @@ export default class AppTile extends React.Component {
{ this.state.loading && loadingElement }
<iframe
allow={iframeFeatures}
ref="appFrame"
ref={this._appFrame}
src={this._getSafeUrl()}
allowFullScreen={true}
sandbox={sandboxFlags}
@ -694,7 +696,7 @@ export default class AppTile extends React.Component {
return <React.Fragment>
<div className={appTileClass} id={this.props.id}>
{ this.props.showMenubar &&
<div ref="menu_bar" className={menuBarClasses} onClick={this.onClickMenuBar}>
<div ref={this._menu_bar} className={menuBarClasses} onClick={this.onClickMenuBar}>
<span className="mx_AppTileMenuBarTitle" style={{pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false)}}>
{ /* Minimise widget */ }
{ showMinimiseButton && <AccessibleButton

View file

@ -59,7 +59,7 @@ export default createReactClass({
Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
userId: this.props.userId,
device: this.state.device,
});
}, null, /* priority = */ false, /* static = */ true);
},
onUnverifyClick: function() {

View file

@ -1,6 +1,7 @@
/*
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.
@ -15,11 +16,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import AccessibleButton from './AccessibleButton';
import { _t } from '../../../languageHandler';
import {Key} from "../../../Keyboard";
class MenuOption extends React.Component {
constructor(props) {
@ -48,9 +50,14 @@ class MenuOption extends React.Component {
mx_Dropdown_option_highlight: this.props.highlighted,
});
return <div className={optClasses}
return <div
id={this.props.id}
className={optClasses}
onClick={this._onClick}
onMouseEnter={this._onMouseEnter}
role="option"
aria-selected={this.props.highlighted}
ref={this.props.inputRef}
>
{ this.props.children }
</div>;
@ -66,6 +73,7 @@ MenuOption.propTypes = {
dropdownKey: PropTypes.string,
onClick: PropTypes.func.isRequired,
onMouseEnter: PropTypes.func.isRequired,
inputRef: PropTypes.any,
};
/*
@ -86,8 +94,6 @@ export default class Dropdown extends React.Component {
this._onRootClick = this._onRootClick.bind(this);
this._onDocumentClick = this._onDocumentClick.bind(this);
this._onMenuOptionClick = this._onMenuOptionClick.bind(this);
this._onInputKeyPress = this._onInputKeyPress.bind(this);
this._onInputKeyUp = this._onInputKeyUp.bind(this);
this._onInputChange = this._onInputChange.bind(this);
this._collectRoot = this._collectRoot.bind(this);
this._collectInputTextBox = this._collectInputTextBox.bind(this);
@ -111,6 +117,7 @@ export default class Dropdown extends React.Component {
}
componentWillMount() {
this._button = createRef();
// Listen for all clicks on the document so we can close the
// menu when the user clicks somewhere else
document.addEventListener('click', this._onDocumentClick, false);
@ -169,40 +176,49 @@ export default class Dropdown extends React.Component {
}
}
_onMenuOptionClick(dropdownKey) {
_close() {
this.setState({
expanded: false,
});
this.props.onOptionChange(dropdownKey);
}
_onInputKeyPress(e) {
// This needs to be on the keypress event because otherwise
// it can't cancel the form submission
if (e.key == 'Enter') {
this.setState({
expanded: false,
});
this.props.onOptionChange(this.state.highlightedOption);
e.preventDefault();
// their focus was on the input, its getting unmounted, move it to the button
if (this._button.current) {
this._button.current.focus();
}
}
_onInputKeyUp(e) {
// These keys don't generate keypress events and so needs to
// be on keyup
if (e.key == 'Escape') {
this.setState({
expanded: false,
});
} else if (e.key == 'ArrowDown') {
this.setState({
highlightedOption: this._nextOption(this.state.highlightedOption),
});
} else if (e.key == 'ArrowUp') {
this.setState({
highlightedOption: this._prevOption(this.state.highlightedOption),
});
_onMenuOptionClick(dropdownKey) {
this._close();
this.props.onOptionChange(dropdownKey);
}
_onInputKeyDown = (e) => {
let handled = true;
// These keys don't generate keypress events and so needs to be on keyup
switch (e.key) {
case Key.ENTER:
this.props.onOptionChange(this.state.highlightedOption);
// fallthrough
case Key.ESCAPE:
this._close();
break;
case Key.ARROW_DOWN:
this.setState({
highlightedOption: this._nextOption(this.state.highlightedOption),
});
break;
case Key.ARROW_UP:
this.setState({
highlightedOption: this._prevOption(this.state.highlightedOption),
});
break;
default:
handled = false;
}
if (handled) {
e.preventDefault();
e.stopPropagation();
}
}
@ -250,20 +266,34 @@ export default class Dropdown extends React.Component {
return keys[(index - 1) % keys.length];
}
_scrollIntoView(node) {
if (node) {
node.scrollIntoView({
block: "nearest",
behavior: "auto",
});
}
}
_getMenuOptions() {
const options = React.Children.map(this.props.children, (child) => {
const highlighted = this.state.highlightedOption === child.key;
return (
<MenuOption key={child.key} dropdownKey={child.key}
highlighted={this.state.highlightedOption == child.key}
<MenuOption
id={`${this.props.id}__${child.key}`}
key={child.key}
dropdownKey={child.key}
highlighted={highlighted}
onMouseEnter={this._setHighlightedOption}
onClick={this._onMenuOptionClick}
inputRef={highlighted ? this._scrollIntoView : undefined}
>
{ child }
</MenuOption>
);
});
if (options.length === 0) {
return [<div key="0" className="mx_Dropdown_option">
return [<div key="0" className="mx_Dropdown_option" role="option">
{ _t("No results") }
</div>];
}
@ -279,23 +309,35 @@ export default class Dropdown extends React.Component {
let menu;
if (this.state.expanded) {
if (this.props.searchEnabled) {
currentValue = <input type="text" className="mx_Dropdown_option"
ref={this._collectInputTextBox} onKeyPress={this._onInputKeyPress}
onKeyUp={this._onInputKeyUp}
onChange={this._onInputChange}
value={this.state.searchQuery}
/>;
currentValue = (
<input
type="text"
className="mx_Dropdown_option"
ref={this._collectInputTextBox}
onKeyDown={this._onInputKeyDown}
onChange={this._onInputChange}
value={this.state.searchQuery}
role="combobox"
aria-autocomplete="list"
aria-activedescendant={`${this.props.id}__${this.state.highlightedOption}`}
aria-owns={`${this.props.id}_listbox`}
aria-disabled={this.props.disabled}
aria-label={this.props.label}
/>
);
}
menu = <div className="mx_Dropdown_menu" style={menuStyle}>
{ this._getMenuOptions() }
</div>;
menu = (
<div className="mx_Dropdown_menu" style={menuStyle} role="listbox" id={`${this.props.id}_listbox`}>
{ this._getMenuOptions() }
</div>
);
}
if (!currentValue) {
const selectedChild = this.props.getShortOption ?
this.props.getShortOption(this.props.value) :
this.childrenByKey[this.props.value];
currentValue = <div className="mx_Dropdown_option">
currentValue = <div className="mx_Dropdown_option" id={`${this.props.id}_value`}>
{ selectedChild }
</div>;
}
@ -311,9 +353,18 @@ export default class Dropdown extends React.Component {
// Note the menu sits inside the AccessibleButton div so it's anchored
// to the input, but overflows below it. The root contains both.
return <div className={classnames(dropdownClasses)} ref={this._collectRoot}>
<AccessibleButton className="mx_Dropdown_input mx_no_textinput" onClick={this._onInputClick}>
<AccessibleButton
className="mx_Dropdown_input mx_no_textinput"
onClick={this._onInputClick}
aria-haspopup="listbox"
aria-expanded={this.state.expanded}
disabled={this.props.disabled}
inputRef={this._button}
aria-label={this.props.label}
aria-describedby={`${this.props.id}_value`}
>
{ currentValue }
<span className="mx_Dropdown_arrow"></span>
<span className="mx_Dropdown_arrow" />
{ menu }
</AccessibleButton>
</div>;
@ -321,6 +372,7 @@ export default class Dropdown extends React.Component {
}
Dropdown.propTypes = {
id: PropTypes.string.isRequired,
// The width that the dropdown should be. If specified,
// the dropped-down part of the menu will be set to this
// width.
@ -340,4 +392,6 @@ Dropdown.propTypes = {
value: PropTypes.string,
// negative for consistency with HTML
disabled: PropTypes.bool,
// ARIA label
label: PropTypes.string.isRequired,
};

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import {Key} from "../../../Keyboard";
@ -65,7 +65,7 @@ module.exports = createReactClass({
componentWillReceiveProps: function(nextProps) {
if (nextProps.initialValue !== this.props.initialValue) {
this.value = nextProps.initialValue;
if (this.refs.editable_div) {
if (this._editable_div.current) {
this.showPlaceholder(!this.value);
}
}
@ -76,24 +76,27 @@ module.exports = createReactClass({
// as React doesn't play nice with contentEditable.
this.value = '';
this.placeholder = false;
this._editable_div = createRef();
},
componentDidMount: function() {
this.value = this.props.initialValue;
if (this.refs.editable_div) {
if (this._editable_div.current) {
this.showPlaceholder(!this.value);
}
},
showPlaceholder: function(show) {
if (show) {
this.refs.editable_div.textContent = this.props.placeholder;
this.refs.editable_div.setAttribute("class", this.props.className + " " + this.props.placeholderClassName);
this._editable_div.current.textContent = this.props.placeholder;
this._editable_div.current.setAttribute("class", this.props.className
+ " " + this.props.placeholderClassName);
this.placeholder = true;
this.value = '';
} else {
this.refs.editable_div.textContent = this.value;
this.refs.editable_div.setAttribute("class", this.props.className);
this._editable_div.current.textContent = this.value;
this._editable_div.current.setAttribute("class", this.props.className);
this.placeholder = false;
}
},
@ -120,7 +123,7 @@ module.exports = createReactClass({
this.value = this.props.initialValue;
this.showPlaceholder(!this.value);
this.onValueChanged(false);
this.refs.editable_div.blur();
this._editable_div.current.blur();
},
onValueChanged: function(shouldSubmit) {
@ -219,7 +222,7 @@ module.exports = createReactClass({
</div>;
} else {
// show the content editable div, but manually manage its contents as react and contentEditable don't play nice together
editableEl = <div ref="editable_div"
editableEl = <div ref={this._editable_div}
contentEditable={true}
className={className}
onKeyDown={this.onKeyDown}

View file

@ -25,13 +25,13 @@ import sdk from '../../../index';
* Parent components should supply an 'onSubmit' callback which returns a
* promise; a spinner is shown until the promise resolves.
*
* The parent can also supply a 'getIntialValue' callback, which works in a
* The parent can also supply a 'getInitialValue' callback, which works in a
* similarly asynchronous way. If this is not provided, the initial value is
* taken from the 'initialValue' property.
*/
export default class EditableTextContainer extends React.Component {
constructor(props, context) {
super(props, context);
constructor(props) {
super(props);
this._unmounted = false;
this.state = {

View file

@ -66,10 +66,14 @@ export default class Field extends React.PureComponent {
this.state = {
valid: undefined,
feedback: undefined,
focused: false,
};
}
onFocus = (ev) => {
this.setState({
focused: true,
});
this.validate({
focused: true,
});
@ -88,6 +92,9 @@ export default class Field extends React.PureComponent {
};
onBlur = (ev) => {
this.setState({
focused: false,
});
this.validate({
focused: false,
});
@ -112,7 +119,9 @@ export default class Field extends React.PureComponent {
allowEmpty,
});
if (feedback) {
// this method is async and so we may have been blurred since the method was called
// if we have then hide the feedback as withValidation does
if (this.state.focused && feedback) {
this.setState({
valid,
feedback,

View file

@ -18,9 +18,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import {MatrixClient} from 'matrix-js-sdk';
import FlairStore from '../../../stores/FlairStore';
import dis from '../../../dispatcher';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
class FlairAvatar extends React.Component {
@ -40,7 +40,7 @@ class FlairAvatar extends React.Component {
}
render() {
const httpUrl = this.context.matrixClient.mxcUrlToHttp(
const httpUrl = this.context.mxcUrlToHttp(
this.props.groupProfile.avatarUrl, 16, 16, 'scale', false);
const tooltip = this.props.groupProfile.name ?
`${this.props.groupProfile.name} (${this.props.groupProfile.groupId})`:
@ -62,9 +62,7 @@ FlairAvatar.propTypes = {
}),
};
FlairAvatar.contextTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
};
FlairAvatar.contextType = MatrixClientContext;
export default class Flair extends React.Component {
constructor() {
@ -92,7 +90,7 @@ export default class Flair extends React.Component {
for (const groupId of groups) {
let groupProfile = null;
try {
groupProfile = await FlairStore.getGroupProfileCached(this.context.matrixClient, groupId);
groupProfile = await FlairStore.getGroupProfileCached(this.context, groupId);
} catch (err) {
console.error('Could not get profile for group', groupId, err);
}
@ -134,6 +132,4 @@ Flair.propTypes = {
groups: PropTypes.arrayOf(PropTypes.string),
};
Flair.contextTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
};
Flair.contextType = MatrixClientContext;

View file

@ -1,37 +0,0 @@
/*
Copyright 2017 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 sdk from '../../../index';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
const GroupsButton = function(props) {
const ActionButton = sdk.getComponent('elements.ActionButton');
return (
<ActionButton className="mx_GroupsButton" action="toggle_my_groups"
label={_t("Communities")}
size={props.size}
tooltip={true}
/>
);
};
GroupsButton.propTypes = {
size: PropTypes.string,
};
export default GroupsButton;

View file

@ -28,6 +28,7 @@ const AccessibleButton = require('../../../components/views/elements/AccessibleB
const Modal = require('../../../Modal');
const sdk = require('../../../index');
import { _t } from '../../../languageHandler';
import {Key} from "../../../Keyboard";
export default class ImageView extends React.Component {
static propTypes = {
@ -62,7 +63,7 @@ export default class ImageView extends React.Component {
}
onKeyDown = (ev) => {
if (ev.keyCode === 27) { // escape
if (ev.key === Key.ESCAPE) {
ev.stopPropagation();
ev.preventDefault();
this.props.onFinished();

View file

@ -21,6 +21,7 @@ import PropTypes from 'prop-types';
import sdk from '../../../index';
import * as languageHandler from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
import { _t } from "../../../languageHandler";
function languageMatchesSearchQuery(query, language) {
if (language.label.toUpperCase().indexOf(query.toUpperCase()) == 0) return true;
@ -105,9 +106,14 @@ export default class LanguageDropdown extends React.Component {
value = this.props.value || language;
}
return <Dropdown className={this.props.className}
onOptionChange={this.props.onOptionChange} onSearchChange={this._onSearchChange}
searchEnabled={true} value={value}
return <Dropdown
id="mx_LanguageDropdown"
className={this.props.className}
onOptionChange={this.props.onOptionChange}
onSearchChange={this._onSearchChange}
searchEnabled={true}
value={value}
label={_t("Language Dropdown")}
>
{ options }
</Dropdown>;

View file

@ -20,12 +20,13 @@ import createReactClass from 'create-react-class';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import classNames from 'classnames';
import { Room, RoomMember, MatrixClient } from 'matrix-js-sdk';
import { Room, RoomMember } from 'matrix-js-sdk';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { getDisplayAliasForRoom } from '../../../Rooms';
import FlairStore from "../../../stores/FlairStore";
import {getPrimaryPermalinkEntity} from "../../../utils/permalinks/Permalinks";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
// For URLs of matrix.to links in the timeline which have been reformatted by
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
@ -66,17 +67,6 @@ const Pill = createReactClass({
isSelected: PropTypes.bool,
},
childContextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
},
getChildContext() {
return {
matrixClient: this._matrixClient,
};
},
getInitialState() {
return {
// ID/alias of the room/user
@ -127,7 +117,7 @@ const Pill = createReactClass({
}
break;
case Pill.TYPE_USER_MENTION: {
const localMember = nextProps.room.getMember(resourceId);
const localMember = nextProps.room ? nextProps.room.getMember(resourceId) : undefined;
member = localMember;
if (!localMember) {
member = new RoomMember(null, resourceId);
@ -276,15 +266,17 @@ const Pill = createReactClass({
});
if (this.state.pillType) {
return this.props.inMessage ?
<a className={classes} href={href} onClick={onClick} title={resource} data-offset-key={this.props.offsetKey}>
{ avatar }
{ linkText }
</a> :
<span className={classes} title={resource} data-offset-key={this.props.offsetKey}>
{ avatar }
{ linkText }
</span>;
return <MatrixClientContext.Provider value={this._matrixClient}>
{ this.props.inMessage ?
<a className={classes} href={href} onClick={onClick} title={resource} data-offset-key={this.props.offsetKey}>
{ avatar }
{ linkText }
</a> :
<span className={classes} title={resource} data-offset-key={this.props.offsetKey}>
{ avatar }
{ linkText }
</span> }
</MatrixClientContext.Provider>;
} else {
// Deliberately render nothing if the URL isn't recognised
return null;

View file

@ -21,10 +21,11 @@ import {_t} from '../../../languageHandler';
import PropTypes from 'prop-types';
import dis from '../../../dispatcher';
import {wantsDateSeparator} from '../../../DateUtils';
import {MatrixEvent, MatrixClient} from 'matrix-js-sdk';
import {MatrixEvent} from 'matrix-js-sdk';
import {makeUserPermalink, RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
import SettingsStore from "../../../settings/SettingsStore";
import escapeHtml from "escape-html";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
// This component does no cycle detection, simply because the only way to make such a cycle would be to
// craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would
@ -38,12 +39,10 @@ export default class ReplyThread extends React.Component {
permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
};
static contextTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
};
static contextType = MatrixClientContext;
constructor(props, context) {
super(props, context);
constructor(props) {
super(props);
this.state = {
// The loaded events to be rendered as linear-replies
@ -187,7 +186,7 @@ export default class ReplyThread extends React.Component {
componentWillMount() {
this.unmounted = false;
this.room = this.context.matrixClient.getRoom(this.props.parentEv.getRoomId());
this.room = this.context.getRoom(this.props.parentEv.getRoomId());
this.room.on("Room.redaction", this.onRoomRedaction);
// same event handler as Room.redaction as for both we just do forceUpdate
this.room.on("Room.redactionCancelled", this.onRoomRedaction);
@ -259,7 +258,7 @@ export default class ReplyThread extends React.Component {
try {
// ask the client to fetch the event we want using the context API, only interface to do so is to ask
// for a timeline with that event, but once it is loaded we can use findEventById to look up the ev map
await this.context.matrixClient.getEventTimeline(this.room.getUnfilteredTimelineSet(), eventId);
await this.context.getEventTimeline(this.room.getUnfilteredTimelineSet(), eventId);
} catch (e) {
// if it fails catch the error and return early, there's no point trying to find the event in this case.
// Return null as it is falsey and thus should be treated as an error (as the event cannot be resolved).
@ -300,7 +299,7 @@ export default class ReplyThread extends React.Component {
} else if (this.state.loadedEv) {
const ev = this.state.loadedEv;
const Pill = sdk.getComponent('elements.Pill');
const room = this.context.matrixClient.getRoom(ev.getRoomId());
const room = this.context.getRoom(ev.getRoomId());
header = <blockquote className="mx_ReplyThread">
{
_t('<a>In reply to</a> <pill>', {}, {

View file

@ -20,11 +20,13 @@ import sdk from '../../../index';
import withValidation from './Validation';
import MatrixClientPeg from '../../../MatrixClientPeg';
// Controlled form component wrapping Field for inputting a room alias scoped to a given domain
export default class RoomAliasField extends React.PureComponent {
static propTypes = {
id: PropTypes.string.isRequired,
domain: PropTypes.string.isRequired,
onChange: PropTypes.func,
value: PropTypes.string.isRequired,
};
constructor(props) {
@ -53,6 +55,7 @@ export default class RoomAliasField extends React.PureComponent {
onValidate={this._onValidate}
placeholder={_t("e.g. my-room")}
onChange={this._onChange}
value={this.props.value.substring(1, this.props.value.length - this.props.domain.length - 1)}
maxLength={maxlength} />
);
}
@ -61,7 +64,7 @@ export default class RoomAliasField extends React.PureComponent {
if (this.props.onChange) {
this.props.onChange(this._asFullAlias(ev.target.value));
}
}
};
_onValidate = async (fieldState) => {
const result = await this._validationRules(fieldState);

View file

@ -24,8 +24,8 @@ export default class SyntaxHighlight extends React.Component {
children: PropTypes.node,
};
constructor(props, context) {
super(props, context);
constructor(props) {
super(props);
this._ref = this._ref.bind(this);
}

View file

@ -20,17 +20,16 @@ import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import classNames from 'classnames';
import { MatrixClient } from 'matrix-js-sdk';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import {_t} from '../../../languageHandler';
import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent } from '../../../Keyboard';
import * as FormattingUtils from '../../../utils/FormattingUtils';
import FlairStore from '../../../stores/FlairStore';
import GroupStore from '../../../stores/GroupStore';
import TagOrderStore from '../../../stores/TagOrderStore';
import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/ContextMenu";
import {ContextMenu, toRightOf} from "../../structures/ContextMenu";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
// A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents
// a thing to click on for the user to filter the visible rooms in the RoomList to:
@ -46,8 +45,8 @@ export default createReactClass({
tag: PropTypes.string,
},
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
statics: {
contextType: MatrixClientContext,
},
getInitialState() {
@ -81,7 +80,7 @@ export default createReactClass({
_onFlairStoreUpdated() {
if (this.unmounted) return;
FlairStore.getGroupProfileCached(
this.context.matrixClient,
this.context,
this.props.tag,
).then((profile) => {
if (this.unmounted) return;
@ -112,12 +111,10 @@ export default createReactClass({
},
onMouseOver: function() {
console.log("DEBUG onMouseOver");
this.setState({hover: true});
},
onMouseOut: function() {
console.log("DEBUG onMouseOut");
this.setState({hover: false});
},
@ -140,12 +137,11 @@ export default createReactClass({
render: function() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const Tooltip = sdk.getComponent('elements.Tooltip');
const profile = this.state.profile || {};
const name = profile.name || this.props.tag;
const avatarHeight = 40;
const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp(
const httpUrl = profile.avatarUrl ? this.context.mxcUrlToHttp(
profile.avatarUrl, avatarHeight, avatarHeight, "crop",
) : null;
@ -164,9 +160,6 @@ export default createReactClass({
badgeElement = (<div className={badgeClasses}>{FormattingUtils.formatCount(badge.count)}</div>);
}
const tip = this.state.hover ?
<Tooltip className="mx_TagTile_tooltip" label={name} /> :
<div />;
// FIXME: this ought to use AccessibleButton for a11y but that causes onMouseOut/onMouseOver to fire too much
const contextButton = this.state.hover || this.state.menuDisplayed ?
<div className="mx_TagTile_context_button" onClick={this.openMenu} ref={this._contextMenuButton}>
@ -184,14 +177,9 @@ export default createReactClass({
);
}
const AccessibleTooltipButton = sdk.getComponent("elements.AccessibleTooltipButton");
return <React.Fragment>
<ContextMenuButton
className={className}
onClick={this.onClick}
onContextMenu={this.openMenu}
label={_t("Options")}
isExpanded={this.state.menuDisplayed}
>
<AccessibleTooltipButton className={className} onClick={this.onClick} onContextMenu={this.openMenu} title={name}>
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
<BaseAvatar
name={name}
@ -200,11 +188,10 @@ export default createReactClass({
width={avatarHeight}
height={avatarHeight}
/>
{ tip }
{ contextButton }
{ badgeElement }
</div>
</ContextMenuButton>
</AccessibleTooltipButton>
{ contextMenu }
</React.Fragment>;

View file

@ -19,46 +19,32 @@ import React from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
import {KeyCode} from "../../../Keyboard";
import sdk from "../../../index";
// Controlled Toggle Switch element
// Controlled Toggle Switch element, written with Accessibility in mind
const ToggleSwitch = ({checked, disabled=false, onChange, ...props}) => {
const _onClick = (e) => {
e.stopPropagation();
e.preventDefault();
if (disabled) return;
onChange(!checked);
};
const _onKeyDown = (e) => {
e.stopPropagation();
e.preventDefault();
if (disabled) return;
if (e.keyCode === KeyCode.ENTER || e.keyCode === KeyCode.SPACE) {
onChange(!checked);
}
};
const classes = classNames({
"mx_ToggleSwitch": true,
"mx_ToggleSwitch_on": checked,
"mx_ToggleSwitch_enabled": !disabled,
});
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
return (
<div {...props}
<AccessibleButton {...props}
className={classes}
onClick={_onClick}
onKeyDown={_onKeyDown}
role="checkbox"
role="switch"
aria-checked={checked}
aria-disabled={disabled}
tabIndex={0}
>
<div className="mx_ToggleSwitch_ball" />
</div>
</AccessibleButton>
);
};

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
@ -34,6 +34,10 @@ module.exports = createReactClass({
};
},
UNSAFE_componentWillMount: function() {
this._user_id_input = createRef();
},
addUser: function(user_id) {
if (this.props.selected_users.indexOf(user_id == -1)) {
this.props.onChange(this.props.selected_users.concat([user_id]));
@ -47,20 +51,20 @@ module.exports = createReactClass({
},
onAddUserId: function() {
this.addUser(this.refs.user_id_input.value);
this.refs.user_id_input.value = "";
this.addUser(this._user_id_input.current.value);
this._user_id_input.current.value = "";
},
render: function() {
const self = this;
return (
<div>
<ul className="mx_UserSelector_UserIdList" ref="list">
<ul className="mx_UserSelector_UserIdList">
{ this.props.selected_users.map(function(user_id, i) {
return <li key={user_id}>{ user_id } - <span onClick={function() {self.removeUser(user_id);}}>X</span></li>;
}) }
</ul>
<input type="text" ref="user_id_input" defaultValue="" className="mx_UserSelector_userIdInput" placeholder={_t("ex. @bob:example.com")} />
<input type="text" ref={this._user_id_input} defaultValue="" className="mx_UserSelector_userIdInput" placeholder={_t("ex. @bob:example.com")} />
<button onClick={this.onAddUserId} className="mx_UserSelector_AddUserId">
{ _t("Add User") }
</button>

View file

@ -16,54 +16,12 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import EMOJIBASE from 'emojibase-data/en/compact.json';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import * as recent from './recent';
const EMOJIBASE_CATEGORY_IDS = [
"people", // smileys
"people", // actually people
"control", // modifiers and such, not displayed in picker
"nature",
"foods",
"places",
"activity",
"objects",
"symbols",
"flags",
];
const DATA_BY_CATEGORY = {
"people": [],
"nature": [],
"foods": [],
"places": [],
"activity": [],
"objects": [],
"symbols": [],
"flags": [],
};
const DATA_BY_EMOJI = {};
const VARIATION_SELECTOR = String.fromCharCode(0xFE0F);
EMOJIBASE.forEach(emoji => {
if (emoji.unicode.includes(VARIATION_SELECTOR)) {
// Clone data into variation-less version
emoji = Object.assign({}, emoji, {
unicode: emoji.unicode.replace(VARIATION_SELECTOR, ""),
});
}
DATA_BY_EMOJI[emoji.unicode] = emoji;
const categoryId = EMOJIBASE_CATEGORY_IDS[emoji.group];
if (DATA_BY_CATEGORY.hasOwnProperty(categoryId)) {
DATA_BY_CATEGORY[categoryId].push(emoji);
}
// This is used as the string to match the query against when filtering emojis.
emoji.filterString = `${emoji.annotation}\n${emoji.shortcodes.join('\n')}}\n${emoji.emoticon || ''}`.toLowerCase();
});
import {DATA_BY_CATEGORY, getEmojiFromUnicode} from "../../../emoji";
export const CATEGORY_HEADER_HEIGHT = 22;
export const EMOJI_HEIGHT = 37;
@ -91,7 +49,7 @@ class EmojiPicker extends React.Component {
// Convert recent emoji characters to emoji data, removing unknowns.
this.recentlyUsed = recent.get()
.map(unicode => DATA_BY_EMOJI[unicode])
.map(unicode => getEmojiFromUnicode(unicode))
.filter(data => !!data);
this.memoizedDataByCategory = {
recent: this.recentlyUsed,

View file

@ -19,15 +19,15 @@ import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import { findEmojiData } from '../../../HtmlUtils';
import {getEmojiFromUnicode} from "../../../emoji";
// We use the variation-selector Heart in Quick Reactions for some reason
const QUICK_REACTIONS = ["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀"].map(emoji => {
const data = findEmojiData(emoji);
const data = getEmojiFromUnicode(emoji);
if (!data) {
throw new Error(`Emoji ${emoji} doesn't exist in emojibase`);
}
// Prefer our unicode value for quick reactions (which does not have
// variation selectors).
// Prefer our unicode value for quick reactions as we sometimes use variation selectors.
return Object.assign({}, data, { unicode: emoji });
});

View file

@ -34,7 +34,7 @@ module.exports = createReactClass({
render: function() {
return (
<div className="mx_MatrixToolbar">
<img className="mx_MatrixToolbar_warning" src={require("../../../../res/img/warning.svg")} width="24" height="23" />
<img className="mx_MatrixToolbar_warning" src={require("../../../../res/img/warning.svg")} width="24" height="23" alt="" />
<div className="mx_MatrixToolbar_content">
{ _t('You are not receiving desktop notifications') } <a className="mx_MatrixToolbar_link" onClick={ this.onClick }> { _t('Enable them now') }</a>
</div>

View file

@ -97,7 +97,7 @@ export default createReactClass({
}
return (
<div className="mx_MatrixToolbar">
<img className="mx_MatrixToolbar_warning" src={require("../../../../res/img/warning.svg")} width="24" height="23" />
<img className="mx_MatrixToolbar_warning" src={require("../../../../res/img/warning.svg")} width="24" height="23" alt="" />
<div className="mx_MatrixToolbar_content">
{_t("A new version of Riot is available.")}
</div>

View file

@ -16,17 +16,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {createRef} from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { MatrixClient } from 'matrix-js-sdk';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import {_t} from '../../../languageHandler';
import classNames from 'classnames';
import MatrixClientPeg from "../../../MatrixClientPeg";
import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/ContextMenu";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
// XXX this class copies a lot from RoomTile.js
export default createReactClass({
displayName: 'GroupInviteTile',
@ -34,8 +35,8 @@ export default createReactClass({
group: PropTypes.object.isRequired,
},
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
statics: {
contextType: MatrixClientContext,
},
getInitialState: function() {
@ -47,10 +48,6 @@ export default createReactClass({
});
},
componentDidMount: function() {
this._contextMenuButton = createRef();
},
onClick: function(e) {
dis.dispatch({
action: 'view_group',
@ -61,7 +58,7 @@ export default createReactClass({
onMouseEnter: function() {
const state = {hover: true};
// Only allow non-guests to access the context menu
if (!this.context.matrixClient.isGuest()) {
if (!this.context.isGuest()) {
state.badgeHover = true;
}
this.setState(state);
@ -74,16 +71,12 @@ export default createReactClass({
});
},
openMenu: function(e) {
_showContextMenu: function(boundingClientRect) {
// Only allow non-guests to access the context menu
if (MatrixClientPeg.get().isGuest()) return;
// Prevent the GroupInviteTile onClick event firing as well
e.stopPropagation();
e.preventDefault();
const state = {
menuDisplayed: true,
contextMenuPosition: boundingClientRect,
};
// If the badge is clicked, then no longer show tooltip
@ -94,9 +87,28 @@ export default createReactClass({
this.setState(state);
},
onContextMenuButtonClick: function(e) {
// Prevent the RoomTile onClick event firing as well
e.stopPropagation();
e.preventDefault();
this._showContextMenu(e.target.getBoundingClientRect());
},
onContextMenu: function(e) {
// Prevent the native context menu
e.preventDefault();
this._showContextMenu({
right: e.clientX,
top: e.clientY,
height: 0,
});
},
closeMenu: function() {
this.setState({
menuDisplayed: false,
contextMenuPosition: null,
});
},
@ -106,19 +118,20 @@ export default createReactClass({
const groupName = this.props.group.name || this.props.group.groupId;
const httpAvatarUrl = this.props.group.avatarUrl ?
this.context.matrixClient.mxcUrlToHttp(this.props.group.avatarUrl, 24, 24) : null;
this.context.mxcUrlToHttp(this.props.group.avatarUrl, 24, 24) : null;
const av = <BaseAvatar name={groupName} width={24} height={24} url={httpAvatarUrl} />;
const isMenuDisplayed = Boolean(this.state.contextMenuPosition);
const nameClasses = classNames('mx_RoomTile_name mx_RoomTile_invite mx_RoomTile_badgeShown', {
'mx_RoomTile_badgeShown': this.state.badgeHover || this.state.menuDisplayed,
'mx_RoomTile_badgeShown': this.state.badgeHover || isMenuDisplayed,
});
const label = <div title={this.props.group.groupId} className={nameClasses} dir="auto">
{ groupName }
</div>;
const badgeEllipsis = this.state.badgeHover || this.state.menuDisplayed;
const badgeEllipsis = this.state.badgeHover || isMenuDisplayed;
const badgeClasses = classNames('mx_RoomTile_badge mx_RoomTile_highlight', {
'mx_RoomTile_badgeButton': badgeEllipsis,
});
@ -127,10 +140,9 @@ export default createReactClass({
const badge = (
<ContextMenuButton
className={badgeClasses}
inputRef={this._contextMenuButton}
onClick={this.openMenu}
onClick={this.onContextMenuButtonClick}
label={_t("Options")}
isExpanded={this.state.menuDisplayed}
isExpanded={isMenuDisplayed}
>
{ badgeContent }
</ContextMenuButton>
@ -143,17 +155,16 @@ export default createReactClass({
}
const classes = classNames('mx_RoomTile mx_RoomTile_highlight', {
'mx_RoomTile_menuDisplayed': this.state.menuDisplayed,
'mx_RoomTile_menuDisplayed': isMenuDisplayed,
'mx_RoomTile_selected': this.state.selected,
'mx_GroupInviteTile': true,
});
let contextMenu;
if (this.state.menuDisplayed) {
const elementRect = this._contextMenuButton.current.getBoundingClientRect();
if (isMenuDisplayed) {
const GroupInviteTileContextMenu = sdk.getComponent('context_menus.GroupInviteTileContextMenu');
contextMenu = (
<ContextMenu {...toRightOf(elementRect)} onFinished={this.closeMenu}>
<ContextMenu {...toRightOf(this.state.contextMenuPosition)} onFinished={this.closeMenu}>
<GroupInviteTileContextMenu group={this.props.group} onFinished={this.closeMenu} />
</ContextMenu>
);

View file

@ -18,7 +18,6 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { MatrixClient } from 'matrix-js-sdk';
import dis from '../../../dispatcher';
import Modal from '../../../Modal';
import sdk from '../../../index';
@ -26,12 +25,13 @@ import { _t } from '../../../languageHandler';
import { GroupMemberType } from '../../../groups';
import GroupStore from '../../../stores/GroupStore';
import AccessibleButton from '../elements/AccessibleButton';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
module.exports = createReactClass({
displayName: 'GroupMemberInfo',
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
statics: {
contextType: MatrixClientContext,
},
propTypes: {
@ -85,7 +85,7 @@ module.exports = createReactClass({
_onKick: function() {
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
Modal.createDialog(ConfirmUserActionDialog, {
matrixClient: this.context.matrixClient,
matrixClient: this.context,
groupMember: this.props.groupMember,
action: this.state.isUserInvited ? _t('Disinvite') : _t('Remove from community'),
title: this.state.isUserInvited ? _t('Disinvite this user from community?')
@ -95,7 +95,7 @@ module.exports = createReactClass({
if (!proceed) return;
this.setState({removingUser: true});
this.context.matrixClient.removeUserFromGroup(
this.context.removeUserFromGroup(
this.props.groupId, this.props.groupMember.userId,
).then(() => {
// return to the user list
@ -171,7 +171,7 @@ module.exports = createReactClass({
const avatarUrl = this.props.groupMember.avatarUrl;
let avatarElement;
if (avatarUrl) {
const httpUrl = this.context.matrixClient.mxcUrlToHttp(avatarUrl, 800, 800);
const httpUrl = this.context.mxcUrlToHttp(avatarUrl, 800, 800);
avatarElement = (<div className="mx_MemberInfo_avatar">
<img src={httpUrl} />
</div>);

View file

@ -1,6 +1,7 @@
/*
Copyright 2017 Vector Creations Ltd.
Copyright 2017 New Vector Ltd.
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -24,7 +25,7 @@ import PropTypes from 'prop-types';
import { showGroupInviteDialog } from '../../../GroupAddressPicker';
import AccessibleButton from '../elements/AccessibleButton';
import TintableSvg from '../elements/TintableSvg';
import RightPanel from '../../structures/RightPanel';
import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
const INITIAL_LOAD_NUM_MEMBERS = 30;
@ -163,8 +164,8 @@ export default createReactClass({
onInviteToGroupButtonClick() {
showGroupInviteDialog(this.props.groupId).then(() => {
dis.dispatch({
action: 'view_right_panel_phase',
phase: RightPanel.Phase.GroupMemberList,
action: 'set_right_panel_phase',
phase: RIGHT_PANEL_PHASES.GroupMemberList,
groupId: this.props.groupId,
});
});

View file

@ -19,10 +19,10 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { MatrixClient } from 'matrix-js-sdk';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import { GroupMemberType } from '../../../groups';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
export default createReactClass({
displayName: 'GroupMemberTile',
@ -36,8 +36,8 @@ export default createReactClass({
return {};
},
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
statics: {
contextType: MatrixClientContext,
},
onClick: function(e) {
@ -53,7 +53,7 @@ export default createReactClass({
const EntityTile = sdk.getComponent('rooms.EntityTile');
const name = this.props.member.displayname || this.props.member.userId;
const avatarUrl = this.context.matrixClient.mxcUrlToHttp(
const avatarUrl = this.context.mxcUrlToHttp(
this.props.member.avatarUrl,
36, 36, 'crop',
);

View file

@ -17,18 +17,18 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { MatrixClient } from 'matrix-js-sdk';
import dis from '../../../dispatcher';
import Modal from '../../../Modal';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import GroupStore from '../../../stores/GroupStore';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
module.exports = createReactClass({
displayName: 'GroupRoomInfo',
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
statics: {
contextType: MatrixClientContext,
},
propTypes: {
@ -206,7 +206,7 @@ module.exports = createReactClass({
const avatarUrl = this.state.groupRoom.avatarUrl;
let avatarElement;
if (avatarUrl) {
const httpUrl = this.context.matrixClient.mxcUrlToHttp(avatarUrl, 800, 800);
const httpUrl = this.context.mxcUrlToHttp(avatarUrl, 800, 800);
avatarElement = (<div className="mx_MemberInfo_avatar">
<img src={httpUrl} />
</div>);

View file

@ -17,10 +17,10 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import {MatrixClient} from 'matrix-js-sdk';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import { GroupRoomType } from '../../../groups';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
const GroupRoomTile = createReactClass({
displayName: 'GroupRoomTile',
@ -41,7 +41,7 @@ const GroupRoomTile = createReactClass({
render: function() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const avatarUrl = this.context.matrixClient.mxcUrlToHttp(
const avatarUrl = this.context.mxcUrlToHttp(
this.props.groupRoom.avatarUrl,
36, 36, 'crop',
);
@ -66,9 +66,7 @@ const GroupRoomTile = createReactClass({
},
});
GroupRoomTile.contextTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
};
GroupRoomTile.contextType = MatrixClientContext;
export default GroupRoomTile;

View file

@ -17,11 +17,11 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import {MatrixClient} from 'matrix-js-sdk';
import { Draggable, Droppable } from 'react-beautiful-dnd';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import FlairStore from '../../../stores/FlairStore';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
function nop() {}
@ -37,8 +37,8 @@ const GroupTile = createReactClass({
draggable: PropTypes.bool,
},
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
statics: {
contextType: MatrixClientContext,
},
getInitialState() {
@ -56,7 +56,7 @@ const GroupTile = createReactClass({
},
componentWillMount: function() {
FlairStore.getGroupProfileCached(this.context.matrixClient, this.props.groupId).then((profile) => {
FlairStore.getGroupProfileCached(this.context, this.props.groupId).then((profile) => {
this.setState({profile});
}).catch((err) => {
console.error('Error whilst getting cached profile for GroupTile', err);
@ -80,7 +80,7 @@ const GroupTile = createReactClass({
const descElement = this.props.showDescription ?
<div className="mx_GroupTile_desc">{ profile.shortDescription }</div> :
<div />;
const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp(
const httpUrl = profile.avatarUrl ? this.context.mxcUrlToHttp(
profile.avatarUrl, avatarHeight, avatarHeight, "crop") : null;
let avatarElement = (

View file

@ -15,17 +15,16 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import sdk from '../../../index';
import { MatrixClient } from 'matrix-js-sdk';
import { _t } from '../../../languageHandler';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
export default createReactClass({
displayName: 'GroupUserSettings',
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
statics: {
contextType: MatrixClientContext,
},
getInitialState() {
@ -36,7 +35,7 @@ export default createReactClass({
},
componentWillMount: function() {
this.context.matrixClient.getJoinedGroups().then((result) => {
this.context.getJoinedGroups().then((result) => {
this.setState({groups: result.groups || [], error: null});
}, (err) => {
console.error(err);

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import * as HtmlUtils from '../../../HtmlUtils';
import { editBodyDiffToHtml } from '../../../utils/MessageDiffUtils';
@ -51,6 +51,8 @@ export default class EditHistoryMessage extends React.PureComponent {
}
const canRedact = room.currentState.maySendRedactionForEvent(event, userId);
this.state = {canRedact, sendStatus: event.getAssociatedStatus()};
this._content = createRef();
}
_onAssociatedStatusChanged = () => {
@ -78,8 +80,8 @@ export default class EditHistoryMessage extends React.PureComponent {
pillifyLinks() {
// not present for redacted events
if (this.refs.content) {
pillifyLinks(this.refs.content.children, this.props.mxEvent);
if (this._content.current) {
pillifyLinks(this._content.current.children, this.props.mxEvent);
}
}
@ -140,13 +142,13 @@ export default class EditHistoryMessage extends React.PureComponent {
if (mxEvent.getContent().msgtype === "m.emote") {
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
contentContainer = (
<div className="mx_EventTile_content" ref="content">*&nbsp;
<div className="mx_EventTile_content" ref={this._content}>*&nbsp;
<span className="mx_MEmoteBody_sender">{ name }</span>
&nbsp;{contentElements}
</div>
);
} else {
contentContainer = <div className="mx_EventTile_content" ref="content">{contentElements}</div>;
contentContainer = <div className="mx_EventTile_content" ref={this._content}>{contentElements}</div>;
}
}

View file

@ -80,7 +80,7 @@ export default class MAudioBody extends React.Component {
if (this.state.error !== null) {
return (
<span className="mx_MAudioBody" ref="body">
<span className="mx_MAudioBody">
<img src={require("../../../../res/img/warning.svg")} width="16" height="16" />
{ _t("Error decrypting audio") }
</span>

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import filesize from 'filesize';
@ -26,6 +26,7 @@ import {decryptFile} from '../../../utils/DecryptFile';
import Tinter from '../../../Tinter';
import request from 'browser-request';
import Modal from '../../../Modal';
import SdkConfig from "../../../SdkConfig";
// A cached tinted copy of require("../../../../res/img/download.svg")
@ -214,10 +215,6 @@ module.exports = createReactClass({
tileShape: PropTypes.string,
},
contextTypes: {
appConfig: PropTypes.object,
},
/**
* Extracts a human readable label for the file attachment to use as
* link text.
@ -251,6 +248,12 @@ module.exports = createReactClass({
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
},
UNSAFE_componentWillMount: function() {
this._iframe = createRef();
this._dummyLink = createRef();
this._downloadImage = createRef();
},
componentDidMount: function() {
// Add this to the list of mounted components to receive notifications
// when the tint changes.
@ -272,17 +275,17 @@ module.exports = createReactClass({
tint: function() {
// Update our tinted copy of require("../../../../res/img/download.svg")
if (this.refs.downloadImage) {
this.refs.downloadImage.src = tintedDownloadImageURL;
if (this._downloadImage.current) {
this._downloadImage.current.src = tintedDownloadImageURL;
}
if (this.refs.iframe) {
if (this._iframe.current) {
// If the attachment is encrypted then the download image
// will be inside the iframe so we wont be able to update
// it directly.
this.refs.iframe.contentWindow.postMessage({
this._iframe.current.contentWindow.postMessage({
code: remoteSetTint.toString(),
imgSrc: tintedDownloadImageURL,
style: computedStyle(this.refs.dummyLink),
style: computedStyle(this._dummyLink.current),
}, "*");
}
},
@ -325,7 +328,7 @@ module.exports = createReactClass({
};
return (
<span className="mx_MFileBody" ref="body">
<span className="mx_MFileBody">
<div className="mx_MFileBody_download">
<a href="javascript:void(0)" onClick={decrypt}>
{ _t("Decrypt %(text)s", { text: text }) }
@ -340,7 +343,7 @@ module.exports = createReactClass({
ev.target.contentWindow.postMessage({
code: remoteRender.toString(),
imgSrc: tintedDownloadImageURL,
style: computedStyle(this.refs.dummyLink),
style: computedStyle(this._dummyLink.current),
blob: this.state.decryptedBlob,
// Set a download attribute for encrypted files so that the file
// will have the correct name when the user tries to download it.
@ -354,8 +357,9 @@ module.exports = createReactClass({
// If the attachment is encryped then put the link inside an iframe.
let renderer_url = DEFAULT_CROSS_ORIGIN_RENDERER;
if (this.context.appConfig && this.context.appConfig.cross_origin_renderer_url) {
renderer_url = this.context.appConfig.cross_origin_renderer_url;
const appConfig = SdkConfig.get();
if (appConfig && appConfig.cross_origin_renderer_url) {
renderer_url = appConfig.cross_origin_renderer_url;
}
renderer_url += "?origin=" + encodeURIComponent(window.location.origin);
return (
@ -367,9 +371,9 @@ module.exports = createReactClass({
* We'll use it to learn how the download link
* would have been styled if it was rendered inline.
*/ }
<a ref="dummyLink" />
<a ref={this._dummyLink} />
</div>
<iframe src={renderer_url} onLoad={onIframeLoad} ref="iframe" />
<iframe src={renderer_url} onLoad={onIframeLoad} ref={this._iframe} />
</div>
</span>
);
@ -439,7 +443,7 @@ module.exports = createReactClass({
<span className="mx_MFileBody">
<div className="mx_MFileBody_download">
<a {...downloadProps}>
<img src={tintedDownloadImageURL} width="12" height="14" ref="downloadImage" />
<img src={tintedDownloadImageURL} width="12" height="14" ref={this._downloadImage} />
{ _t("Download %(text)s", { text: text }) }
</a>
</div>

View file

@ -16,9 +16,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
import MFileBody from './MFileBody';
import Modal from '../../../Modal';
@ -26,6 +25,7 @@ import sdk from '../../../index';
import { decryptFile } from '../../../utils/DecryptFile';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
export default class MImageBody extends React.Component {
static propTypes = {
@ -39,9 +39,7 @@ export default class MImageBody extends React.Component {
maxImageHeight: PropTypes.number,
};
static contextTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient),
};
static contextType = MatrixClientContext;
constructor(props) {
super(props);
@ -65,11 +63,13 @@ export default class MImageBody extends React.Component {
hover: false,
showImage: SettingsStore.getValue("showImages"),
};
this._image = createRef();
}
componentWillMount() {
this.unmounted = false;
this.context.matrixClient.on('sync', this.onClientSync);
this.context.on('sync', this.onClientSync);
}
// FIXME: factor this out and aplpy it to MVideoBody and MAudioBody too!
@ -158,8 +158,8 @@ export default class MImageBody extends React.Component {
let loadedImageDimensions;
if (this.refs.image) {
const { naturalWidth, naturalHeight } = this.refs.image;
if (this._image.current) {
const { naturalWidth, naturalHeight } = this._image.current;
// this is only used as a fallback in case content.info.w/h is missing
loadedImageDimensions = { naturalWidth, naturalHeight };
}
@ -172,7 +172,7 @@ export default class MImageBody extends React.Component {
if (content.file !== undefined) {
return this.state.decryptedUrl;
} else {
return this.context.matrixClient.mxcUrlToHttp(content.url);
return this.context.mxcUrlToHttp(content.url);
}
}
@ -196,7 +196,7 @@ export default class MImageBody extends React.Component {
// special case to return clientside sender-generated thumbnails for SVGs, if any,
// given we deliberately don't thumbnail them serverside to prevent
// billion lol attacks and similar
return this.context.matrixClient.mxcUrlToHttp(
return this.context.mxcUrlToHttp(
content.info.thumbnail_url,
thumbWidth,
thumbHeight,
@ -219,7 +219,7 @@ export default class MImageBody extends React.Component {
pixelRatio === 1.0 ||
(!info || !info.w || !info.h || !info.size)
) {
return this.context.matrixClient.mxcUrlToHttp(content.url, thumbWidth, thumbHeight);
return this.context.mxcUrlToHttp(content.url, thumbWidth, thumbHeight);
} else {
// we should only request thumbnails if the image is bigger than 800x600
// (or 1600x1200 on retina) otherwise the image in the timeline will just
@ -240,7 +240,7 @@ export default class MImageBody extends React.Component {
// image is too large physically and bytewise to clutter our timeline so
// we ask for a thumbnail, despite knowing that it will be max 800x600
// despite us being retina (as synapse doesn't do 1600x1200 thumbs yet).
return this.context.matrixClient.mxcUrlToHttp(
return this.context.mxcUrlToHttp(
content.url,
thumbWidth,
thumbHeight,
@ -249,7 +249,7 @@ export default class MImageBody extends React.Component {
// download the original image otherwise, so we can scale it client side
// to take pixelRatio into account.
// ( no width/height means we want the original image)
return this.context.matrixClient.mxcUrlToHttp(
return this.context.mxcUrlToHttp(
content.url,
);
}
@ -306,7 +306,7 @@ export default class MImageBody extends React.Component {
componentWillUnmount() {
this.unmounted = true;
this.context.matrixClient.removeListener('sync', this.onClientSync);
this.context.removeListener('sync', this.onClientSync);
this._afterComponentWillUnmount();
if (this.state.decryptedUrl) {
@ -342,7 +342,7 @@ export default class MImageBody extends React.Component {
imageElement = <HiddenImagePlaceholder />;
} else {
imageElement = (
<img style={{display: 'none'}} src={thumbUrl} ref="image"
<img style={{display: 'none'}} src={thumbUrl} ref={this._image}
alt={content.body}
onError={this.onImageError}
onLoad={this.onImageLoad}
@ -385,7 +385,7 @@ export default class MImageBody extends React.Component {
// which has the same width as the timeline
// mx_MImageBody_thumbnail resizes img to exactly container size
img = (
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref={this._image}
style={{ maxWidth: maxWidth + "px" }}
alt={content.body}
onError={this.onImageError}
@ -459,7 +459,7 @@ export default class MImageBody extends React.Component {
if (this.state.error !== null) {
return (
<span className="mx_MImageBody" ref="body">
<span className="mx_MImageBody">
<img src={require("../../../../res/img/warning.svg")} width="16" height="16" />
{ _t("Error decrypting image") }
</span>
@ -477,7 +477,7 @@ export default class MImageBody extends React.Component {
const thumbnail = this._messageContent(contentUrl, thumbUrl, content);
const fileBody = this.getFileBody();
return <span className="mx_MImageBody" ref="body">
return <span className="mx_MImageBody">
{ thumbnail }
{ fileBody }
</span>;

View file

@ -52,7 +52,7 @@ export default class MKeyVerificationRequest extends React.Component {
const verifier = MatrixClientPeg.get().acceptVerificationDM(this.props.mxEvent, verificationMethods.SAS);
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {
verifier,
});
}, null, /* priority = */ false, /* static = */ true);
};
_onRejectClicked = () => {

View file

@ -132,7 +132,7 @@ module.exports = createReactClass({
if (this.state.error !== null) {
return (
<span className="mx_MVideoBody" ref="body">
<span className="mx_MVideoBody">
<img src={require("../../../../res/img/warning.svg")} width="16" height="16" />
{ _t("Error decrypting video") }
</span>
@ -144,8 +144,8 @@ module.exports = createReactClass({
// The attachment is decrypted in componentDidMount.
// For now add an img tag with a spinner.
return (
<span className="mx_MVideoBody" ref="body">
<div className="mx_MImageBody_thumbnail mx_MImageBody_thumbnail_spinner" ref="image">
<span className="mx_MVideoBody">
<div className="mx_MImageBody_thumbnail mx_MImageBody_thumbnail_spinner">
<img src={require("../../../../res/img/spinner.gif")} alt={content.body} width="16" height="16" />
</div>
</span>

View file

@ -25,7 +25,7 @@ import dis from '../../../dispatcher';
import Modal from '../../../Modal';
import {aboveLeftOf, ContextMenu, ContextMenuButton, useContextMenu} from '../../structures/ContextMenu';
import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
import {RoomContext} from "../../structures/RoomView";
import RoomContext from "../../../contexts/RoomContext";
const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
@ -78,14 +78,17 @@ const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFo
</React.Fragment>;
};
const ReactButton = ({mxEvent, reactions}) => {
const ReactButton = ({mxEvent, reactions, onFocusChange}) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
useEffect(() => {
onFocusChange(menuDisplayed);
}, [onFocusChange, menuDisplayed]);
let contextMenu;
if (menuDisplayed) {
const buttonRect = button.current.getBoundingClientRect();
const ReactionPicker = sdk.getComponent('emojipicker.ReactionPicker');
contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu}>
contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} managed={false}>
<ReactionPicker mxEvent={mxEvent} reactions={reactions} onFinished={closeMenu} />
</ContextMenu>;
}
@ -114,9 +117,7 @@ export default class MessageActionBar extends React.PureComponent {
onFocusChange: PropTypes.func,
};
static contextTypes = {
room: RoomContext,
};
static contextType = RoomContext;
componentDidMount() {
this.props.mxEvent.on("Event.decrypted", this.onDecrypted);
@ -161,10 +162,12 @@ export default class MessageActionBar extends React.PureComponent {
let editButton;
if (isContentActionable(this.props.mxEvent)) {
if (this.context.room.canReact) {
reactButton = <ReactButton mxEvent={this.props.mxEvent} reactions={this.props.reactions} />;
if (this.context.canReact) {
reactButton = (
<ReactButton mxEvent={this.props.mxEvent} reactions={this.props.reactions} onFocusChange={this.onFocusChange} />
);
}
if (this.context.room.canReply) {
if (this.context.canReply) {
replyButton = <AccessibleButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"
title={_t("Reply")}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import sdk from '../../../index';
@ -47,8 +47,12 @@ module.exports = createReactClass({
maxImageHeight: PropTypes.number,
},
UNSAFE_componentWillMount: function() {
this._body = createRef();
},
getEventTileOps: function() {
return this.refs.body && this.refs.body.getEventTileOps ? this.refs.body.getEventTileOps() : null;
return this._body.current && this._body.current.getEventTileOps ? this._body.current.getEventTileOps() : null;
},
onTileUpdate: function() {
@ -103,7 +107,8 @@ module.exports = createReactClass({
}
return <BodyType
ref="body" mxEvent={this.props.mxEvent}
ref={this._body}
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}

View file

@ -126,10 +126,9 @@ export default class ReactionsRowButton extends React.PureComponent {
);
}
return <span className={classes}
role="button"
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return <AccessibleButton className={classes}
aria-label={label}
tabindex="0"
onClick={this.onClick}
onMouseOver={this.onMouseOver}
onMouseOut={this.onMouseOut}
@ -141,6 +140,6 @@ export default class ReactionsRowButton extends React.PureComponent {
{count}
</span>
{tooltip}
</span>;
</AccessibleButton>;
}
}

View file

@ -17,11 +17,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import {MatrixClient} from 'matrix-js-sdk';
import Flair from '../elements/Flair.js';
import FlairStore from '../../../stores/FlairStore';
import { _t } from '../../../languageHandler';
import {getUserNameColorClass} from '../../../utils/FormattingUtils';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
export default createReactClass({
displayName: 'SenderProfile',
@ -31,8 +31,8 @@ export default createReactClass({
onClick: PropTypes.func,
},
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
statics: {
contextType: MatrixClientContext,
},
getInitialState() {
@ -47,18 +47,18 @@ export default createReactClass({
this._updateRelatedGroups();
FlairStore.getPublicisedGroupsCached(
this.context.matrixClient, this.props.mxEvent.getSender(),
this.context, this.props.mxEvent.getSender(),
).then((userGroups) => {
if (this.unmounted) return;
this.setState({userGroups});
});
this.context.matrixClient.on('RoomState.events', this.onRoomStateEvents);
this.context.on('RoomState.events', this.onRoomStateEvents);
},
componentWillUnmount() {
this.unmounted = true;
this.context.matrixClient.removeListener('RoomState.events', this.onRoomStateEvents);
this.context.removeListener('RoomState.events', this.onRoomStateEvents);
},
onRoomStateEvents(event) {
@ -71,7 +71,7 @@ export default createReactClass({
_updateRelatedGroups() {
if (this.unmounted) return;
const room = this.context.matrixClient.getRoom(this.props.mxEvent.getRoomId());
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
if (!room) return;
const relatedGroupsEvent = room.currentState.getStateEvents('m.room.related_groups', '');

View file

@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
@ -86,6 +86,10 @@ module.exports = createReactClass({
return successful;
},
UNSAFE_componentWillMount: function() {
this._content = createRef();
},
componentDidMount: function() {
this._unmounted = false;
if (!this.props.editState) {
@ -94,13 +98,13 @@ module.exports = createReactClass({
},
_applyFormatting() {
this.activateSpoilers(this.refs.content.children);
this.activateSpoilers(this._content.current.children);
// pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer
// are still sent as plaintext URLs. If these are ever pillified in the composer,
// we should be pillify them here by doing the linkifying BEFORE the pillifying.
pillifyLinks(this.refs.content.children, this.props.mxEvent);
HtmlUtils.linkifyElement(this.refs.content);
pillifyLinks(this._content.current.children, this.props.mxEvent);
HtmlUtils.linkifyElement(this._content.current);
this.calculateUrlPreview();
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") {
@ -163,7 +167,7 @@ module.exports = createReactClass({
//console.info("calculateUrlPreview: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
if (this.props.showUrlPreview) {
let links = this.findLinks(this.refs.content.children);
let links = this.findLinks(this._content.current.children);
if (links.length) {
// de-dup the links (but preserve ordering)
const seen = new Set();
@ -276,7 +280,7 @@ module.exports = createReactClass({
const buttonRect = e.target.getBoundingClientRect();
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
...toRightOf(buttonRect, 11),
...toRightOf(buttonRect, 2),
message: successful ? _t('Copied!') : _t('Failed to copy'),
});
e.target.onmouseleave = close;
@ -327,7 +331,7 @@ module.exports = createReactClass({
},
getInnerText: () => {
return this.refs.content.innerText;
return this._content.current.innerText;
},
};
},
@ -401,13 +405,18 @@ module.exports = createReactClass({
label={_t("Edited at %(date)s. Click to view edits.", {date: dateString})}
/>;
}
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (
<div
key="editedMarker" className="mx_EventTile_edited"
<AccessibleButton
key="editedMarker"
className="mx_EventTile_edited"
onClick={this._openHistoryDialog}
onMouseEnter={this._onMouseEnterEditedMarker}
onMouseLeave={this._onMouseLeaveEditedMarker}
>{editedTooltip}<span>{`(${_t("edited")})`}</span></div>
>
{ editedTooltip }<span>{`(${_t("edited")})`}</span>
</AccessibleButton>
);
},
@ -452,7 +461,7 @@ module.exports = createReactClass({
case "m.emote":
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
return (
<span ref="content" className="mx_MEmoteBody mx_EventTile_content">
<span ref={this._content} className="mx_MEmoteBody mx_EventTile_content">
*&nbsp;
<span
className="mx_MEmoteBody_sender"
@ -467,14 +476,14 @@ module.exports = createReactClass({
);
case "m.notice":
return (
<span ref="content" className="mx_MNoticeBody mx_EventTile_content">
<span ref={this._content} className="mx_MNoticeBody mx_EventTile_content">
{ body }
{ widgets }
</span>
);
default: // including "m.text"
return (
<span ref="content" className="mx_MTextBody mx_EventTile_content">
<span ref={this._content} className="mx_MTextBody mx_EventTile_content">
{ body }
{ widgets }
</span>

View file

@ -3,6 +3,7 @@ Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Copyright 2018 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.
@ -20,21 +21,21 @@ limitations under the License.
import React from 'react';
import { _t } from '../../../languageHandler';
import HeaderButton from './HeaderButton';
import HeaderButtons from './HeaderButtons';
import RightPanel from '../../structures/RightPanel';
import HeaderButtons, {HEADER_KIND_GROUP} from './HeaderButtons';
import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
const GROUP_PHASES = [
RightPanel.Phase.GroupMemberInfo,
RightPanel.Phase.GroupMemberList,
RIGHT_PANEL_PHASES.GroupMemberInfo,
RIGHT_PANEL_PHASES.GroupMemberList,
];
const ROOM_PHASES = [
RightPanel.Phase.GroupRoomList,
RightPanel.Phase.GroupRoomInfo,
RIGHT_PANEL_PHASES.GroupRoomList,
RIGHT_PANEL_PHASES.GroupRoomInfo,
];
export default class GroupHeaderButtons extends HeaderButtons {
constructor(props) {
super(props, RightPanel.Phase.GroupMemberList);
super(props, HEADER_KIND_GROUP);
this._onMembersClicked = this._onMembersClicked.bind(this);
this._onRoomsClicked = this._onRoomsClicked.bind(this);
}
@ -44,29 +45,34 @@ export default class GroupHeaderButtons extends HeaderButtons {
if (payload.action === "view_user") {
if (payload.member) {
this.setPhase(RightPanel.Phase.RoomMemberInfo, {member: payload.member});
this.setPhase(RIGHT_PANEL_PHASES.RoomMemberInfo, {member: payload.member});
} else {
this.setPhase(RightPanel.Phase.GroupMemberList);
this.setPhase(RIGHT_PANEL_PHASES.GroupMemberList);
}
} else if (payload.action === "view_group") {
this.setPhase(RightPanel.Phase.GroupMemberList);
this.setPhase(RIGHT_PANEL_PHASES.GroupMemberList);
} else if (payload.action === "view_group_room") {
this.setPhase(RightPanel.Phase.GroupRoomInfo, {groupRoomId: payload.groupRoomId, groupId: payload.groupId});
this.setPhase(
RIGHT_PANEL_PHASES.GroupRoomInfo,
{groupRoomId: payload.groupRoomId, groupId: payload.groupId},
);
} else if (payload.action === "view_group_room_list") {
this.setPhase(RightPanel.Phase.GroupRoomList);
this.setPhase(RIGHT_PANEL_PHASES.GroupRoomList);
} else if (payload.action === "view_group_member_list") {
this.setPhase(RightPanel.Phase.GroupMemberList);
this.setPhase(RIGHT_PANEL_PHASES.GroupMemberList);
} else if (payload.action === "view_group_user") {
this.setPhase(RightPanel.Phase.GroupMemberInfo, {member: payload.member});
this.setPhase(RIGHT_PANEL_PHASES.GroupMemberInfo, {member: payload.member});
}
}
_onMembersClicked() {
this.togglePhase(RightPanel.Phase.GroupMemberList, GROUP_PHASES);
// This toggles for us, if needed
this.setPhase(RIGHT_PANEL_PHASES.GroupMemberList);
}
_onRoomsClicked() {
this.togglePhase(RightPanel.Phase.GroupRoomList, ROOM_PHASES);
// This toggles for us, if needed
this.setPhase(RIGHT_PANEL_PHASES.GroupRoomList);
}
renderButtons() {

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