Merge branch 'joriks/font-scaling-message-preview' into joriks/appearance-tab-layout-options

This commit is contained in:
Jorik Schellekens 2020-06-18 15:37:01 +01:00
commit f2440388b1
102 changed files with 1705 additions and 748 deletions

View file

@ -86,6 +86,45 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
}
};
// TODO: Apply this on resize, init, etc for reliability
private onScroll = (ev: React.MouseEvent<HTMLDivElement>) => {
const list = ev.target as HTMLDivElement;
const rlRect = list.getBoundingClientRect();
const bottom = rlRect.bottom;
const top = rlRect.top;
const sublists = list.querySelectorAll<HTMLDivElement>(".mx_RoomSublist2");
const headerHeight = 32; // Note: must match the CSS!
const headerRightMargin = 24; // calculated from margins and widths to align with non-sticky tiles
const headerStickyWidth = rlRect.width - headerRightMargin;
let gotBottom = false;
for (const sublist of sublists) {
const slRect = sublist.getBoundingClientRect();
const header = sublist.querySelector<HTMLDivElement>(".mx_RoomSublist2_stickable");
if (slRect.top + headerHeight > bottom && !gotBottom) {
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");
header.classList.add("mx_RoomSublist2_headerContainer_stickyTop");
header.style.width = `${headerStickyWidth}px`;
header.style.top = `${rlRect.top}px`;
} else {
header.classList.remove("mx_RoomSublist2_headerContainer_sticky");
header.classList.remove("mx_RoomSublist2_headerContainer_stickyTop");
header.classList.remove("mx_RoomSublist2_headerContainer_stickyBottom");
header.style.width = `unset`;
header.style.top = `unset`;
}
}
};
private renderHeader(): React.ReactNode {
// TODO: Update when profile info changes
// TODO: Presence
@ -191,7 +230,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
<aside className="mx_LeftPanel2_roomListContainer">
{this.renderHeader()}
{this.renderSearchExplore()}
<div className="mx_LeftPanel2_actualRoomListContainer">
<div className="mx_LeftPanel2_actualRoomListContainer" onScroll={this.onScroll}>
{roomList}
</div>
</aside>

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 {

View file

@ -1977,8 +1977,9 @@ export default createReactClass({
searchResultsPanel = (<div className="mx_RoomView_messagePanel mx_RoomView_messagePanelSearchSpinner" />);
} else {
searchResultsPanel = (
<ScrollPanel ref={this._searchResultsPanel}
className="mx_RoomView_messagePanel mx_RoomView_searchResultsPanel"
<ScrollPanel
ref={this._searchResultsPanel}
className="mx_RoomView_messagePanel mx_RoomView_searchResultsPanel mx_GroupLayout"
onFillRequest={this.onSearchResultsFillRequest}
resizeNotifier={this.props.resizeNotifier}
>

View file

@ -32,6 +32,8 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
import {getCustomTheme} from "../../theme";
import {getHostingLink} from "../../utils/HostingLink";
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
import SdkConfig from "../../SdkConfig";
import {getHomePageUrl} from "../../utils/pages";
interface IProps {
}
@ -67,6 +69,10 @@ export default class UserMenuButton extends React.Component<IProps, IState> {
}
}
private get hasHomePage(): boolean {
return !!getHomePageUrl(SdkConfig.get());
}
public componentDidMount() {
this.dispatcherRef = defaultDispatcher.register(this.onAction);
this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged);
@ -147,6 +153,13 @@ export default class UserMenuButton extends React.Component<IProps, IState> {
this.setState({menuDisplayed: false}); // also close the menu
};
private onHomeClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
defaultDispatcher.dispatch({action: 'view_home_page'});
};
public render() {
let contextMenu;
if (this.state.menuDisplayed) {
@ -172,6 +185,18 @@ export default class UserMenuButton extends React.Component<IProps, IState> {
);
}
let homeButton = null;
if (this.hasHomePage) {
homeButton = (
<li>
<AccessibleButton onClick={this.onHomeClick}>
<img src={require("../../../res/img/feather-customised/home.svg")} width={16} />
<span>{_t("Home")}</span>
</AccessibleButton>
</li>
);
}
const elementRect = this.buttonRef.current.getBoundingClientRect();
contextMenu = (
<ContextMenu
@ -205,6 +230,7 @@ export default class UserMenuButton extends React.Component<IProps, IState> {
{hostingLink}
<div className="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst">
<ul>
{homeButton}
<li>
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}>
<img src={require("../../../res/img/feather-customised/notifications.svg")} width={16} />
@ -265,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

@ -19,26 +19,15 @@ 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,
PHASE_FINISHED,
} from '../../../stores/SetupEncryptionStore';
function keyHasPassphrase(keyInfo) {
return (
keyInfo.passphrase &&
keyInfo.passphrase.salt &&
keyInfo.passphrase.iterations
);
}
export default class SetupEncryptionBody extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
@ -56,11 +45,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 +67,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 +92,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");
@ -194,13 +108,6 @@ export default class SetupEncryptionBody extends React.Component {
member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)}
/>;
} else if (phase === PHASE_INTRO) {
const store = SetupEncryptionStore.sharedInstance();
let recoveryKeyPrompt;
if (keyHasPassphrase(store.keyInfo)) {
recoveryKeyPrompt = _t("Use Recovery Key or Passphrase");
} else {
recoveryKeyPrompt = _t("Use Recovery Key");
}
return (
<div>
<p>{_t(
@ -224,67 +131,15 @@ export default class SetupEncryptionBody extends React.Component {
</div>
<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton kind="link" onClick={this._onUseRecoveryKeyClick}>
{recoveryKeyPrompt}
<AccessibleButton kind="link" onClick={this._onUsePassphraseClick}>
{_t("Use Recovery Passphrase or Key")}
</AccessibleButton>
<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) {