Merge branch 'develop' into joriks/appearance-advanced

This commit is contained in:
Jorik Schellekens 2020-06-22 11:27:48 +01:00 committed by GitHub
commit 2294d23b32
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
75 changed files with 2203 additions and 710 deletions

View file

@ -108,6 +108,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
header.classList.add("mx_RoomSublist2_headerContainer_sticky");
header.classList.add("mx_RoomSublist2_headerContainer_stickyBottom");
header.style.width = `${headerStickyWidth}px`;
header.style.top = `unset`;
gotBottom = true;
} else if (slRect.top < top) {
header.classList.add("mx_RoomSublist2_headerContainer_sticky");
@ -119,6 +120,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
header.classList.remove("mx_RoomSublist2_headerContainer_stickyTop");
header.classList.remove("mx_RoomSublist2_headerContainer_stickyBottom");
header.style.width = `unset`;
header.style.top = `unset`;
}
}
};

View file

@ -151,9 +151,9 @@ interface IProps { // TODO type things better
// Represents the screen to display as a result of parsing the initial window.location
initialScreenAfterLogin?: IScreen;
// displayname, if any, to set on the device when logging in/registering.
defaultDeviceDisplayName?: string,
defaultDeviceDisplayName?: string;
// A function that makes a registration URL
makeRegistrationUrl: (object) => string,
makeRegistrationUrl: (object) => string;
}
interface IState {
@ -1870,42 +1870,35 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.accountPasswordTimer = null;
}, 60 * 5 * 1000);
// Wait for the client to be logged in (but not started)
// which is enough to ask the server about account data.
const loggedIn = new Promise(resolve => {
const actionHandlerRef = dis.register(payload => {
if (payload.action !== "on_logged_in") {
return;
}
dis.unregister(actionHandlerRef);
resolve();
});
});
// Create and start the client in the background
const setLoggedInPromise = Lifecycle.setLoggedIn(credentials);
await loggedIn;
// Create and start the client
await Lifecycle.setLoggedIn(credentials);
const cli = MatrixClientPeg.get();
// We're checking `isCryptoAvailable` here instead of `isCryptoEnabled`
// because the client hasn't been started yet.
const cryptoAvailable = isCryptoAvailable();
if (!cryptoAvailable) {
const cryptoEnabled = cli.isCryptoEnabled();
if (!cryptoEnabled) {
this.onLoggedIn();
}
this.setState({ pendingInitialSync: true });
await this.firstSyncPromise.promise;
if (!cryptoAvailable) {
this.setState({ pendingInitialSync: false });
return setLoggedInPromise;
const promisesList = [this.firstSyncPromise.promise];
if (cryptoEnabled) {
// wait for the client to finish downloading cross-signing keys for us so we
// know whether or not we have keys set up on this account
promisesList.push(cli.downloadKeys([cli.getUserId()]));
}
// Test for the master cross-signing key in SSSS as a quick proxy for
// whether cross-signing has been set up on the account.
const masterKeyInStorage = !!cli.getAccountData("m.cross_signing.master");
if (masterKeyInStorage) {
// Now update the state to say we're waiting for the first sync to complete rather
// than for the login to finish.
this.setState({ pendingInitialSync: true });
await Promise.all(promisesList);
if (!cryptoEnabled) {
this.setState({ pendingInitialSync: false });
return;
}
const crossSigningIsSetUp = cli.getStoredCrossSigningForUser(cli.getUserId());
if (crossSigningIsSetUp) {
this.setStateForNewView({ view: Views.COMPLETE_SECURITY });
} else if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) {
this.setStateForNewView({ view: Views.E2E_SETUP });
@ -1913,8 +1906,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.onLoggedIn();
}
this.setState({ pendingInitialSync: false });
return setLoggedInPromise;
};
// complete security / e2e setup has finished

View file

@ -39,7 +39,7 @@ import Tinter from '../../Tinter';
import rate_limited_func from '../../ratelimitedfunc';
import * as ObjectUtils from '../../ObjectUtils';
import * as Rooms from '../../Rooms';
import eventSearch from '../../Searching';
import eventSearch, {searchPagination} from '../../Searching';
import {isOnlyCtrlOrCmdIgnoreShiftKeyEvent, isOnlyCtrlOrCmdKeyEvent, Key} from '../../Keyboard';
@ -1036,8 +1036,7 @@ export default createReactClass({
if (this.state.searchResults.next_batch) {
debuglog("requesting more search results");
const searchPromise = this.context.backPaginateRoomEventsSearch(
this.state.searchResults);
const searchPromise = searchPagination(this.state.searchResults);
return this._handleSearchResult(searchPromise);
} else {
debuglog("no more search results");
@ -1314,6 +1313,14 @@ export default createReactClass({
const mxEv = result.context.getEvent();
const roomId = mxEv.getRoomId();
const room = this.context.getRoom(roomId);
if (!room) {
// if we do not have the room in js-sdk stores then hide it as we cannot easily show it
// As per the spec, an all rooms search can create this condition,
// it happens with Seshat but not Synapse.
// It will make the result count not match the displayed count.
console.log("Hiding search result from an unknown room", roomId);
continue;
}
if (!haveTileForEvent(mxEv)) {
// XXX: can this ever happen? It will make the result count
@ -1322,16 +1329,9 @@ export default createReactClass({
}
if (this.state.searchScope === 'All') {
if (roomId != lastRoomId) {
// XXX: if we've left the room, we might not know about
// it. We should tell the js sdk to go and find out about
// it. But that's not an issue currently, as synapse only
// returns results for rooms we're joined to.
const roomName = room ? room.name : _t("Unknown room %(roomId)s", { roomId: roomId });
if (roomId !== lastRoomId) {
ret.push(<li key={mxEv.getId() + "-room"}>
<h2>{ _t("Room") }: { roomName }</h2>
<h2>{ _t("Room") }: { room.name }</h2>
</li>);
lastRoomId = roomId;
}

View file

@ -291,6 +291,6 @@ export default class UserMenuButton extends React.Component<IProps, IState> {
</ContextMenuButton>
{contextMenu}
</React.Fragment>
)
);
}
}

View file

@ -21,7 +21,6 @@ import * as sdk from '../../../index';
import {
SetupEncryptionStore,
PHASE_INTRO,
PHASE_RECOVERY_KEY,
PHASE_BUSY,
PHASE_DONE,
PHASE_CONFIRM_SKIP,
@ -62,9 +61,6 @@ export default class CompleteSecurity extends React.Component {
if (phase === PHASE_INTRO) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
title = _t("Verify this login");
} else if (phase === PHASE_RECOVERY_KEY) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified" />;
title = _t("Recovery Key");
} else if (phase === PHASE_DONE) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified" />;
title = _t("Session verified");

View file

@ -378,7 +378,7 @@ export default createReactClass({
}
if (response.access_token) {
const cli = await this.props.onLoggedIn({
await this.props.onLoggedIn({
userId: response.user_id,
deviceId: response.device_id,
homeserverUrl: this.state.matrixClient.getHomeserverUrl(),
@ -386,7 +386,7 @@ export default createReactClass({
accessToken: response.access_token,
}, this.state.formVals.password);
this._setupPushers(cli);
this._setupPushers();
// we're still busy until we get unmounted: don't show the registration form again
newState.busy = true;
} else {
@ -397,10 +397,11 @@ export default createReactClass({
this.setState(newState);
},
_setupPushers: function(matrixClient) {
_setupPushers: function() {
if (!this.props.brand) {
return Promise.resolve();
}
const matrixClient = MatrixClientPeg.get();
return matrixClient.getPushers().then((resp)=>{
const pushers = resp.pushers;
for (let i = 0; i < pushers.length; ++i) {

View file

@ -19,12 +19,9 @@ import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import withValidation from '../../views/elements/Validation';
import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey';
import {
SetupEncryptionStore,
PHASE_INTRO,
PHASE_RECOVERY_KEY,
PHASE_BUSY,
PHASE_DONE,
PHASE_CONFIRM_SKIP,
@ -56,11 +53,6 @@ export default class SetupEncryptionBody extends React.Component {
// Because of the latter, it lives in the state.
verificationRequest: store.verificationRequest,
backupInfo: store.backupInfo,
recoveryKey: '',
// whether the recovery key is a valid recovery key
recoveryKeyValid: null,
// whether the recovery key is the correct key or not
recoveryKeyCorrect: null,
};
}
@ -83,19 +75,9 @@ export default class SetupEncryptionBody extends React.Component {
store.stop();
}
_onResetClick = () => {
_onUsePassphraseClick = async () => {
const store = SetupEncryptionStore.sharedInstance();
store.startKeyReset();
}
_onUseRecoveryKeyClick = async () => {
const store = SetupEncryptionStore.sharedInstance();
store.useRecoveryKey();
}
_onRecoveryKeyCancelClick() {
const store = SetupEncryptionStore.sharedInstance();
store.cancelUseRecoveryKey();
store.usePassPhrase();
}
onSkipClick = () => {
@ -118,66 +100,6 @@ export default class SetupEncryptionBody extends React.Component {
store.done();
}
_onUsePassphraseClick = () => {
const store = SetupEncryptionStore.sharedInstance();
store.usePassPhrase();
}
_onRecoveryKeyChange = (e) => {
this.setState({recoveryKey: e.target.value});
}
_onRecoveryKeyValidate = async (fieldState) => {
const result = await this._validateRecoveryKey(fieldState);
this.setState({recoveryKeyValid: result.valid});
return result;
}
_validateRecoveryKey = withValidation({
rules: [
{
key: "required",
test: async (state) => {
try {
const decodedKey = decodeRecoveryKey(state.value);
const correct = await MatrixClientPeg.get().checkSecretStorageKey(
decodedKey, SetupEncryptionStore.sharedInstance().keyInfo,
);
this.setState({
recoveryKeyValid: true,
recoveryKeyCorrect: correct,
});
return correct;
} catch (e) {
this.setState({
recoveryKeyValid: false,
recoveryKeyCorrect: false,
});
return false;
}
},
invalid: function() {
if (this.state.recoveryKeyValid) {
return _t("This isn't the recovery key for your account");
} else {
return _t("This isn't a valid recovery key");
}
},
valid: function() {
return _t("Looks good!");
},
},
],
})
_onRecoveryKeyFormSubmit = (e) => {
e.preventDefault();
if (!this.state.recoveryKeyCorrect) return;
const store = SetupEncryptionStore.sharedInstance();
store.setupWithRecoveryKey(decodeRecoveryKey(this.state.recoveryKey));
}
render() {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
@ -196,11 +118,19 @@ export default class SetupEncryptionBody extends React.Component {
} else if (phase === PHASE_INTRO) {
const store = SetupEncryptionStore.sharedInstance();
let recoveryKeyPrompt;
if (keyHasPassphrase(store.keyInfo)) {
if (store.keyInfo && keyHasPassphrase(store.keyInfo)) {
recoveryKeyPrompt = _t("Use Recovery Key or Passphrase");
} else {
} else if (store.keyInfo) {
recoveryKeyPrompt = _t("Use Recovery Key");
}
let useRecoveryKeyButton;
if (recoveryKeyPrompt) {
useRecoveryKeyButton = <AccessibleButton kind="link" onClick={this._onUsePassphraseClick}>
{recoveryKeyPrompt}
</AccessibleButton>;
}
return (
<div>
<p>{_t(
@ -224,67 +154,13 @@ export default class SetupEncryptionBody extends React.Component {
</div>
<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton kind="link" onClick={this._onUseRecoveryKeyClick}>
{recoveryKeyPrompt}
</AccessibleButton>
{useRecoveryKeyButton}
<AccessibleButton kind="danger" onClick={this.onSkipClick}>
{_t("Skip")}
</AccessibleButton>
</div>
<div className="mx_CompleteSecurity_resetText">{_t(
"If you've forgotten your recovery key you can " +
"<button>set up new recovery options</button>", {}, {
button: sub => <AccessibleButton
element="span" className="mx_linkButton" onClick={this._onResetClick}
>
{sub}
</AccessibleButton>,
},
)}</div>
</div>
);
} else if (phase === PHASE_RECOVERY_KEY) {
const store = SetupEncryptionStore.sharedInstance();
let keyPrompt;
if (keyHasPassphrase(store.keyInfo)) {
keyPrompt = _t(
"Enter your Recovery Key or enter a <a>Recovery Passphrase</a> to continue.", {},
{
a: sub => <AccessibleButton
element="span"
className="mx_linkButton"
onClick={this._onUsePassphraseClick}
>{sub}</AccessibleButton>,
},
);
} else {
keyPrompt = _t("Enter your Recovery Key to continue.");
}
const Field = sdk.getComponent('elements.Field');
return <form onSubmit={this._onRecoveryKeyFormSubmit}>
<p>{keyPrompt}</p>
<div className="mx_CompleteSecurity_recoveryKeyEntry">
<Field
type="text"
label={_t('Recovery Key')}
value={this.state.recoveryKey}
onChange={this._onRecoveryKeyChange}
onValidate={this._onRecoveryKeyValidate}
/>
</div>
<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton kind="secondary" onClick={this._onRecoveryKeyCancelClick}>
{_t("Cancel")}
</AccessibleButton>
<AccessibleButton kind="primary"
disabled={!this.state.recoveryKeyCorrect}
onClick={this._onRecoveryKeyFormSubmit}
>
{_t("Continue")}
</AccessibleButton>
</div>
</form>;
} else if (phase === PHASE_DONE) {
let message;
if (this.state.backupInfo) {

View file

@ -118,7 +118,7 @@ class PassphraseField extends PureComponent<IProps, IState> {
value={this.props.value}
onChange={this.props.onChange}
onValidate={this.onValidate}
/>
/>;
}
}

View file

@ -88,7 +88,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
_onResetRecoveryClick = () => {
this.props.onFinished(false);
accessSecretStorage(() => {}, {forceReset: true});
accessSecretStorage(() => {}, /* forceReset = */ true);
}
_onRecoveryKeyChange = (e) => {

View file

@ -32,9 +32,6 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
keyInfo: PropTypes.object.isRequired,
// Function from one of { passphrase, recoveryKey } -> boolean
checkPrivateKey: PropTypes.func.isRequired,
// If true, only prompt for a passphrase and do not offer to restore with
// a recovery key or reset keys.
passphraseOnly: PropTypes.bool,
}
constructor(props) {
@ -61,7 +58,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
_onResetRecoveryClick = () => {
// Re-enter the access flow, but resetting storage this time around.
this.props.onFinished(false);
accessSecretStorage(() => {}, {forceReset: true});
accessSecretStorage(() => {}, /* forceReset = */ true);
}
_onRecoveryKeyChange = (e) => {
@ -167,7 +164,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
primaryDisabled={this.state.passPhrase.length === 0}
/>
</form>
{this.props.passphraseOnly ? null : _t(
{_t(
"If you've forgotten your recovery passphrase you can "+
"<button1>use your recovery key</button1> or " +
"<button2>set up new recovery options</button2>."
@ -237,7 +234,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
primaryDisabled={!this.state.recoveryKeyValid}
/>
</form>
{this.props.passphraseOnly ? null : _t(
{_t(
"If you've forgotten your recovery key you can "+
"<button>set up new recovery options</button>."
, {}, {

View file

@ -19,7 +19,7 @@ import React from 'react';
import {Key} from '../../../Keyboard';
import classnames from 'classnames';
export type ButtonEvent = React.MouseEvent<Element> | React.KeyboardEvent<Element>
export type ButtonEvent = React.MouseEvent<Element> | React.KeyboardEvent<Element>;
/**
* children: React's magic prop. Represents all children given to the element.
@ -40,7 +40,7 @@ interface IProps extends React.InputHTMLAttributes<Element> {
disabled?: boolean;
className?: string;
onClick?(e?: ButtonEvent): void;
};
}
interface IAccessibleButtonProps extends React.InputHTMLAttributes<Element> {
ref?: React.Ref<Element>;

View file

@ -17,20 +17,20 @@ limitations under the License.
import React from 'react';
interface IProps {
className: string,
dragFunc: (currentLocation: ILocationState, event: MouseEvent) => ILocationState,
onMouseUp: (event: MouseEvent) => void,
className: string;
dragFunc: (currentLocation: ILocationState, event: MouseEvent) => ILocationState;
onMouseUp: (event: MouseEvent) => void;
}
interface IState {
onMouseMove: (event: MouseEvent) => void,
onMouseUp: (event: MouseEvent) => void,
location: ILocationState,
onMouseMove: (event: MouseEvent) => void;
onMouseUp: (event: MouseEvent) => void;
location: ILocationState;
}
export interface ILocationState {
currentX: number,
currentY: number,
currentX: number;
currentY: number;
}
export default class Draggable extends React.Component<IProps, IState> {
@ -58,13 +58,13 @@ export default class Draggable extends React.Component<IProps, IState> {
document.addEventListener("mousemove", this.state.onMouseMove);
document.addEventListener("mouseup", this.state.onMouseUp);
}
};
private onMouseUp = (event: MouseEvent): void => {
document.removeEventListener("mousemove", this.state.onMouseMove);
document.removeEventListener("mouseup", this.state.onMouseUp);
this.props.onMouseUp(event);
}
};
private onMouseMove(event: MouseEvent): void {
const newLocation = this.props.dragFunc(this.state.location, event);
@ -75,7 +75,7 @@ export default class Draggable extends React.Component<IProps, IState> {
}
render() {
return <div className={this.props.className} onMouseDown={this.onMouseDown.bind(this)} />
return <div className={this.props.className} onMouseDown={this.onMouseDown.bind(this)} />;
}
}

View file

@ -87,20 +87,20 @@ interface ITextareaProps extends IProps, TextareaHTMLAttributes<HTMLTextAreaElem
type PropShapes = IInputProps | ISelectProps | ITextareaProps;
interface IState {
valid: boolean,
feedback: React.ReactNode,
feedbackVisible: boolean,
focused: boolean,
valid: boolean;
feedback: React.ReactNode;
feedbackVisible: boolean;
focused: boolean;
}
export default class Field extends React.PureComponent<PropShapes, IState> {
private id: string;
private input: HTMLInputElement;
private static defaultProps = {
public static readonly defaultProps = {
element: "input",
type: "text",
}
};
/*
* This was changed from throttle to debounce: this is more traditional for

View file

@ -20,15 +20,15 @@ import Draggable, {ILocationState} from './Draggable';
interface IProps {
// Current room
roomId: string,
minWidth: number,
maxWidth: number,
};
roomId: string;
minWidth: number;
maxWidth: number;
}
interface IState {
width: number,
IRCLayoutRoot: HTMLElement,
};
width: number;
IRCLayoutRoot: HTMLElement;
}
export default class IRCTimelineProfileResizer extends React.Component<IProps, IState> {
constructor(props: IProps) {
@ -37,20 +37,19 @@ export default class IRCTimelineProfileResizer extends React.Component<IProps, I
this.state = {
width: SettingsStore.getValue("ircDisplayNameWidth", this.props.roomId),
IRCLayoutRoot: null,
}
};
};
}
componentDidMount() {
this.setState({
IRCLayoutRoot: document.querySelector(".mx_IRCLayout") as HTMLElement,
}, () => this.updateCSSWidth(this.state.width))
}, () => this.updateCSSWidth(this.state.width));
}
private dragFunc = (location: ILocationState, event: React.MouseEvent<Element, MouseEvent>): ILocationState => {
const offset = event.clientX - location.currentX;
const newWidth = this.state.width + offset;
console.log({offset})
// If we're trying to go smaller than min width, don't.
if (newWidth < this.props.minWidth) {
return location;
@ -69,8 +68,8 @@ export default class IRCTimelineProfileResizer extends React.Component<IProps, I
return {
currentX: event.clientX,
currentY: location.currentY,
}
}
};
};
private updateCSSWidth(newWidth: number) {
this.state.IRCLayoutRoot.style.setProperty("--name-width", newWidth + "px");
@ -83,6 +82,10 @@ export default class IRCTimelineProfileResizer extends React.Component<IProps, I
}
render() {
return <Draggable className="mx_ProfileResizer" dragFunc={this.dragFunc.bind(this)} onMouseUp={this.onMoueUp.bind(this)}/>
return <Draggable
className="mx_ProfileResizer"
dragFunc={this.dragFunc.bind(this)}
onMouseUp={this.onMoueUp.bind(this)}
/>;
}
};
}

View file

@ -48,18 +48,18 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
this.props.roomId,
this.props.isExplicit,
),
}
};
}
private onChange = (checked: boolean): void => {
this.save(checked);
this.setState({ value: checked });
if (this.props.onChange) this.props.onChange(checked);
}
};
private checkBoxOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.onChange(e.target.checked);
}
};
private save = (val?: boolean): void => {
return SettingsStore.setValue(
@ -68,7 +68,7 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
this.props.level,
val !== undefined ? val : this.state.value,
);
}
};
public render() {
const canChange = SettingsStore.canSetValue(this.props.name, this.props.roomId, this.props.level);

View file

@ -65,9 +65,9 @@ export default class Slider extends React.Component<IProps> {
const intervalWidth = 1 / (values.length - 1);
const linearInterpolation = (value - closestLessValue) / (closestGreaterValue - closestLessValue)
const linearInterpolation = (value - closestLessValue) / (closestGreaterValue - closestLessValue);
return 100 * (closest - 1 + linearInterpolation) * intervalWidth
return 100 * (closest - 1 + linearInterpolation) * intervalWidth;
}
@ -87,7 +87,7 @@ export default class Slider extends React.Component<IProps> {
selection = <div className="mx_Slider_selection">
<div className="mx_Slider_selectionDot" style={{left: "calc(-0.55em + " + offset + "%)"}} />
<hr style={{width: offset + "%"}} />
</div>
</div>;
}
return <div className="mx_Slider">
@ -115,13 +115,13 @@ export default class Slider extends React.Component<IProps> {
interface IDotProps {
// Callback for behavior onclick
onClick: () => void,
onClick: () => void;
// Whether the dot should appear active
active: boolean,
active: boolean;
// The label on the dot
label: string,
label: string;
// Whether the slider is disabled
disabled: boolean;
@ -129,7 +129,7 @@ interface IDotProps {
class Dot extends React.PureComponent<IDotProps> {
render(): React.ReactNode {
let className = "mx_Slider_dot"
let className = "mx_Slider_dot";
if (!this.props.disabled && this.props.active) {
className += " mx_Slider_dotActive";
}

View file

@ -30,7 +30,7 @@ export default class StyledCheckbox extends React.PureComponent<IProps, IState>
public static readonly defaultProps = {
className: "",
}
};
constructor(props: IProps) {
super(props);
@ -51,6 +51,6 @@ export default class StyledCheckbox extends React.PureComponent<IProps, IState>
{ this.props.children }
</div>
</label>
</span>
</span>;
}
}

View file

@ -26,7 +26,7 @@ interface IState {
export default class StyledRadioButton extends React.PureComponent<IProps, IState> {
public static readonly defaultProps = {
className: '',
}
};
public render() {
const { children, className, disabled, ...otherProps } = this.props;
@ -43,6 +43,6 @@ export default class StyledRadioButton extends React.PureComponent<IProps, IStat
<div><div></div></div>
<span>{children}</span>
<div className="mx_RadioButton_spacer" />
</label>
</label>;
}
}

View file

@ -28,7 +28,7 @@ interface IProps {
// Called when the checked state changes. First argument will be the new state.
onChange(checked: boolean): void;
};
}
// Controlled Toggle Switch element, written with Accessibility in mind
export default ({checked, disabled = false, onChange, ...props}: IProps) => {

View file

@ -29,15 +29,15 @@ const MIN_TOOLTIP_HEIGHT = 25;
interface IProps {
// Class applied to the element used to position the tooltip
className: string,
className: string;
// Class applied to the tooltip itself
tooltipClassName?: string,
tooltipClassName?: string;
// Whether the tooltip is visible or hidden.
// The hidden state allows animating the tooltip away via CSS.
// Defaults to visible if unset.
visible?: boolean,
visible?: boolean;
// the react element to put into the tooltip
label: React.ReactNode,
label: React.ReactNode;
}
export default class Tooltip extends React.Component<IProps> {
@ -126,7 +126,7 @@ export default class Tooltip extends React.Component<IProps> {
tooltip: this.tooltip,
parent: parent,
});
}
};
public render() {
// Render a placeholder

View file

@ -240,6 +240,7 @@ export class ListNotificationState extends EventEmitter implements IDestroyable
this.rooms = rooms;
for (const oldRoom of diff.removed) {
const state = this.states[oldRoom.roomId];
if (!state) continue; // We likely just didn't have a badge (race condition)
delete this.states[oldRoom.roomId];
state.off(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
state.destroy();

View file

@ -97,7 +97,7 @@ export default class RoomBreadcrumbs2 extends React.PureComponent<IProps, IState
>
<RoomAvatar room={r} width={32} height={32}/>
</AccessibleButton>
)
);
});
if (tiles.length > 0) {

View file

@ -41,6 +41,11 @@ import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorith
* warning disappears. *
*******************************************************************/
const SHOW_N_BUTTON_HEIGHT = 32; // As defined by CSS
const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS
const MAX_PADDING_HEIGHT = SHOW_N_BUTTON_HEIGHT + RESIZE_HANDLE_HEIGHT;
interface IProps {
forRooms: boolean;
rooms?: Room[];
@ -105,7 +110,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
};
private onShowAllClick = () => {
this.props.layout.visibleTiles = this.numTiles;
this.props.layout.visibleTiles = this.props.layout.tilesWithPadding(this.numTiles, MAX_PADDING_HEIGHT);
this.forceUpdate(); // because the layout doesn't trigger a re-render
};
@ -359,7 +364,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
{showMoreText}
</div>
);
} else if (tiles.length <= nVisible) {
} else if (tiles.length <= nVisible && tiles.length > this.props.layout.minVisibleTiles) {
// we have all tiles visible - add a button to show less
let showLessText = (
<span className='mx_RoomSublist2_showNButtonText'>
@ -393,18 +398,16 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
// goes backwards and can become wildly incorrect (visibleTiles says 18 when there's
// only mathematically 7 possible).
const showMoreHeight = 32; // As defined by CSS
const resizeHandleHeight = 4; // As defined by CSS
// The padding is variable though, so figure out what we need padding for.
let padding = 0;
if (showNButton) padding += showMoreHeight;
if (handles.length > 0) padding += resizeHandleHeight;
if (showNButton) padding += SHOW_N_BUTTON_HEIGHT;
padding += RESIZE_HANDLE_HEIGHT; // always append the handle height
const minTilesPx = layout.calculateTilesToPixelsMin(tiles.length, layout.minVisibleTiles, padding);
const relativeTiles = layout.tilesWithPadding(tiles.length, padding);
const minTilesPx = layout.calculateTilesToPixelsMin(relativeTiles, layout.minVisibleTiles, padding);
const maxTilesPx = layout.tilesToPixelsWithPadding(tiles.length, padding);
const tilesWithoutPadding = Math.min(tiles.length, layout.visibleTiles);
const tilesPx = layout.calculateTilesToPixelsMin(tiles.length, tilesWithoutPadding, padding);
const tilesWithoutPadding = Math.min(relativeTiles, layout.visibleTiles);
const tilesPx = layout.calculateTilesToPixelsMin(relativeTiles, tilesWithoutPadding, padding);
content = (
<ResizableBox
@ -420,7 +423,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
{visibleTiles}
{showNButton}
</ResizableBox>
)
);
}
// TODO: onKeyDown support

View file

@ -232,7 +232,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
/>
{contextMenu}
</React.Fragment>
)
);
}
public render(): React.ReactElement {

View file

@ -113,7 +113,7 @@ export default class CrossSigningPanel extends React.PureComponent {
_bootstrapSecureSecretStorage = async (forceReset=false) => {
this.setState({ error: null });
try {
await accessSecretStorage(() => undefined, {forceReset});
await accessSecretStorage(() => undefined, forceReset);
} catch (e) {
this.setState({ error: e });
console.error("Error bootstrapping secret storage", e);

View file

@ -34,14 +34,14 @@ interface IProps {
}
interface IThemeState {
theme: string,
useSystemTheme: boolean,
theme: string;
useSystemTheme: boolean;
}
export interface CustomThemeMessage {
isError: boolean,
text: string
};
isError: boolean;
text: string;
}
interface IState extends IThemeState {
// String displaying the current selected fontSize.
@ -164,7 +164,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
);
return {valid: true, feedback: _t('Use between %(min)s pt and %(max)s pt', {min, max})};
}
};
private onAddCustomTheme = async (): Promise<void> => {
let currentThemes: string[] = SettingsStore.getValue("custom_themes");

View file

@ -90,7 +90,7 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
} catch (err) {
console.error("Error while cancelling verification request", err);
}
}
};
accept = async () => {
ToastStore.sharedInstance().dismissToast(this.props.toastKey);