Merge branch 'joriks/font-scaling-message-preview' into joriks/appearance-tab-layout-options
This commit is contained in:
commit
f2440388b1
102 changed files with 1705 additions and 748 deletions
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue