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) {
|
||||
|
|
|
@ -118,7 +118,7 @@ class PassphraseField extends PureComponent<IProps, IState> {
|
|||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
onValidate={this.onValidate}
|
||||
/>
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -88,7 +88,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
|
||||
_onResetRecoveryClick = () => {
|
||||
this.props.onFinished(false);
|
||||
accessSecretStorage(() => {}, {forceReset: true});
|
||||
accessSecretStorage(() => {}, /* forceReset = */ true);
|
||||
}
|
||||
|
||||
_onRecoveryKeyChange = (e) => {
|
||||
|
|
|
@ -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>."
|
||||
, {}, {
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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)} />;
|
||||
}
|
||||
|
||||
}
|
|
@ -47,7 +47,7 @@ interface IState {
|
|||
|
||||
const AVATAR_SIZE = 32;
|
||||
|
||||
export default class MessagePreview extends React.Component<IProps, IState> {
|
||||
export default class EventTilePreview extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
|
@ -55,14 +55,13 @@ export default class MessagePreview extends React.Component<IProps, IState> {
|
|||
userId: "@erim:fink.fink",
|
||||
displayname: "Erimayas Fink",
|
||||
avatar_url: null,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
// Fetch current user data
|
||||
const client = MatrixClientPeg.get()
|
||||
const client = MatrixClientPeg.get();
|
||||
const userId = client.getUserId();
|
||||
console.log({userId})
|
||||
const profileInfo = await client.getProfileInfo(userId);
|
||||
const avatar_url = Avatar.avatarUrlForUser(
|
||||
{avatarUrl: profileInfo.avatar_url},
|
||||
|
@ -98,7 +97,7 @@ export default class MessagePreview extends React.Component<IProps, IState> {
|
|||
},
|
||||
"event_id": "$9999999999999999999999999999999999999999999",
|
||||
"room_id": "!999999999999999999:matrix.org"
|
||||
}`))
|
||||
}`));
|
||||
|
||||
// Fake it more
|
||||
event.sender = {
|
||||
|
@ -107,7 +106,7 @@ export default class MessagePreview extends React.Component<IProps, IState> {
|
|||
getAvatarUrl: (..._) => {
|
||||
return avatar_url;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
|
@ -125,6 +124,6 @@ export default class MessagePreview extends React.Component<IProps, IState> {
|
|||
|
||||
return <div className={className}>
|
||||
<EventTile mxEvent={event} useIRCLayout={this.props.useIRCLayout} />
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {InputHTMLAttributes, SelectHTMLAttributes, TextareaHTMLAttributes} from 'react';
|
||||
import classNames from 'classnames';
|
||||
import * as sdk from '../../../index';
|
||||
import { debounce } from 'lodash';
|
||||
import {IFieldState, IValidationResult} from "../elements/Validation";
|
||||
import {IFieldState, IValidationResult} from "./Validation";
|
||||
|
||||
// Invoke validation from user input (when typing, etc.) at most once every N ms.
|
||||
const VALIDATION_THROTTLE_MS = 200;
|
||||
|
@ -29,60 +29,76 @@ function getId() {
|
|||
return `${BASE_ID}_${count++}`;
|
||||
}
|
||||
|
||||
interface IProps extends React.InputHTMLAttributes<HTMLSelectElement | HTMLInputElement> {
|
||||
interface IProps {
|
||||
// The field's ID, which binds the input and label together. Immutable.
|
||||
id?: string,
|
||||
// The element to create. Defaults to "input".
|
||||
// To define options for a select, use <Field><option ... /></Field>
|
||||
element?: "input" | "select" | "textarea",
|
||||
id?: string;
|
||||
// The field's type (when used as an <input>). Defaults to "text".
|
||||
type?: string,
|
||||
type?: string;
|
||||
// id of a <datalist> element for suggestions
|
||||
list?: string,
|
||||
list?: string;
|
||||
// The field's label string.
|
||||
label?: string,
|
||||
label?: string;
|
||||
// The field's placeholder string. Defaults to the label.
|
||||
placeholder?: string,
|
||||
// The field's value.
|
||||
// This is a controlled component, so the value is required.
|
||||
value: string,
|
||||
placeholder?: string;
|
||||
// Optional component to include inside the field before the input.
|
||||
prefixComponent?: React.ReactNode,
|
||||
prefixComponent?: React.ReactNode;
|
||||
// Optional component to include inside the field after the input.
|
||||
postfixComponent?: React.ReactNode,
|
||||
postfixComponent?: React.ReactNode;
|
||||
// The callback called whenever the contents of the field
|
||||
// changes. Returns an object with `valid` boolean field
|
||||
// and a `feedback` react component field to provide feedback
|
||||
// to the user.
|
||||
onValidate?: (input: IFieldState) => Promise<IValidationResult>,
|
||||
onValidate?: (input: IFieldState) => Promise<IValidationResult>;
|
||||
// If specified, overrides the value returned by onValidate.
|
||||
flagInvalid?: boolean,
|
||||
flagInvalid?: boolean;
|
||||
// If specified, contents will appear as a tooltip on the element and
|
||||
// validation feedback tooltips will be suppressed.
|
||||
tooltipContent?: React.ReactNode,
|
||||
tooltipContent?: React.ReactNode;
|
||||
// If specified alongside tooltipContent, the class name to apply to the
|
||||
// tooltip itself.
|
||||
tooltipClassName?: string,
|
||||
tooltipClassName?: string;
|
||||
// If specified, an additional class name to apply to the field container
|
||||
className?: string,
|
||||
className?: string;
|
||||
// All other props pass through to the <input>.
|
||||
}
|
||||
|
||||
interface IState {
|
||||
valid: boolean,
|
||||
feedback: React.ReactNode,
|
||||
feedbackVisible: boolean,
|
||||
focused: boolean,
|
||||
interface IInputProps extends IProps, InputHTMLAttributes<HTMLInputElement> {
|
||||
// The element to create. Defaults to "input".
|
||||
element?: "input";
|
||||
// The input's value. This is a controlled component, so the value is required.
|
||||
value: string;
|
||||
}
|
||||
|
||||
export default class Field extends React.PureComponent<IProps, IState> {
|
||||
interface ISelectProps extends IProps, SelectHTMLAttributes<HTMLSelectElement> {
|
||||
// To define options for a select, use <Field><option ... /></Field>
|
||||
element: "select";
|
||||
// The select's value. This is a controlled component, so the value is required.
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface ITextareaProps extends IProps, TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
element: "textarea";
|
||||
// The textarea's value. This is a controlled component, so the value is required.
|
||||
value: string;
|
||||
}
|
||||
|
||||
type PropShapes = IInputProps | ISelectProps | ITextareaProps;
|
||||
|
||||
interface IState {
|
||||
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
|
||||
|
|
|
@ -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)}
|
||||
/>;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
@ -44,6 +44,6 @@ export default class StyledRadioButton extends React.PureComponent<IProps, IStat
|
|||
<div><div></div></div>
|
||||
<span>{children}</span>
|
||||
<div className="mx_RadioButton_spacer" />
|
||||
</label>
|
||||
</label>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -748,19 +748,26 @@ const RoomAdminToolsContainer = ({room, children, member, startUpdating, stopUpd
|
|||
powerLevels.state_default
|
||||
);
|
||||
|
||||
// if these do not exist in the event then they should default to 50 as per the spec
|
||||
const {
|
||||
ban: banPowerLevel = 50,
|
||||
kick: kickPowerLevel = 50,
|
||||
redact: redactPowerLevel = 50,
|
||||
} = powerLevels;
|
||||
|
||||
const me = room.getMember(cli.getUserId());
|
||||
const isMe = me.userId === member.userId;
|
||||
const canAffectUser = member.powerLevel < me.powerLevel || isMe;
|
||||
|
||||
if (canAffectUser && me.powerLevel >= powerLevels.kick) {
|
||||
if (canAffectUser && me.powerLevel >= kickPowerLevel) {
|
||||
kickButton = <RoomKickButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
|
||||
}
|
||||
if (me.powerLevel >= powerLevels.redact) {
|
||||
if (me.powerLevel >= redactPowerLevel) {
|
||||
redactButton = (
|
||||
<RedactMessagesButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />
|
||||
);
|
||||
}
|
||||
if (canAffectUser && me.powerLevel >= powerLevels.ban) {
|
||||
if (canAffectUser && me.powerLevel >= banPowerLevel) {
|
||||
banButton = <BanToggleButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
|
||||
}
|
||||
if (canAffectUser && me.powerLevel >= editPowerLevel) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,12 @@ 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
|
||||
};
|
||||
|
||||
private onShowLessClick = () => {
|
||||
this.props.layout.visibleTiles = this.props.layout.minVisibleTiles;
|
||||
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
||||
};
|
||||
|
||||
|
@ -134,7 +144,28 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
||||
};
|
||||
|
||||
private onHeaderClick = (ev: React.MouseEvent<HTMLDivElement>) => {
|
||||
let target = ev.target as HTMLDivElement;
|
||||
if (!target.classList.contains('mx_RoomSublist2_headerText')) {
|
||||
// If we don't have the headerText class, the user clicked the span in the headerText.
|
||||
target = target.parentElement as HTMLDivElement;
|
||||
}
|
||||
|
||||
const possibleSticky = target.parentElement;
|
||||
const sublist = possibleSticky.parentElement.parentElement;
|
||||
if (possibleSticky.classList.contains('mx_RoomSublist2_headerContainer_sticky')) {
|
||||
// is sticky - jump to list
|
||||
sublist.scrollIntoView({behavior: 'smooth'});
|
||||
} else {
|
||||
// on screen - toggle collapse
|
||||
this.props.layout.isCollapsed = !this.props.layout.isCollapsed;
|
||||
this.forceUpdate(); // because the layout doesn't trigger an update
|
||||
}
|
||||
};
|
||||
|
||||
private renderTiles(): React.ReactElement[] {
|
||||
if (this.props.layout && this.props.layout.isCollapsed) return []; // don't waste time on rendering
|
||||
|
||||
const tiles: React.ReactElement[] = [];
|
||||
|
||||
if (this.props.rooms) {
|
||||
|
@ -145,6 +176,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
key={`room-${room.roomId}`}
|
||||
showMessagePreview={this.props.layout.showPreviews}
|
||||
isMinimized={this.props.isMinimized}
|
||||
tag={this.props.layout.tagId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -249,6 +281,11 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
const collapseClasses = classNames({
|
||||
'mx_RoomSublist2_collapseBtn': true,
|
||||
'mx_RoomSublist2_collapseBtn_collapsed': this.props.layout && this.props.layout.isCollapsed,
|
||||
});
|
||||
|
||||
const classes = classNames({
|
||||
'mx_RoomSublist2_headerContainer': true,
|
||||
'mx_RoomSublist2_headerContainer_withAux': !!addRoomButton,
|
||||
|
@ -257,19 +294,23 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
// TODO: a11y (see old component)
|
||||
return (
|
||||
<div className={classes}>
|
||||
<AccessibleButton
|
||||
inputRef={ref}
|
||||
tabIndex={tabIndex}
|
||||
className={"mx_RoomSublist2_headerText"}
|
||||
role="treeitem"
|
||||
aria-level={1}
|
||||
>
|
||||
<span>{this.props.label}</span>
|
||||
</AccessibleButton>
|
||||
{this.renderMenu()}
|
||||
{addRoomButton}
|
||||
<div className="mx_RoomSublist2_badgeContainer">
|
||||
{badge}
|
||||
<div className='mx_RoomSublist2_stickable'>
|
||||
<AccessibleButton
|
||||
inputRef={ref}
|
||||
tabIndex={tabIndex}
|
||||
className={"mx_RoomSublist2_headerText"}
|
||||
role="treeitem"
|
||||
aria-level={1}
|
||||
onClick={this.onHeaderClick}
|
||||
>
|
||||
<span className={collapseClasses} />
|
||||
<span>{this.props.label}</span>
|
||||
</AccessibleButton>
|
||||
{this.renderMenu()}
|
||||
{addRoomButton}
|
||||
<div className="mx_RoomSublist2_badgeContainer">
|
||||
{badge}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -303,25 +344,42 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
const visibleTiles = tiles.slice(0, nVisible);
|
||||
|
||||
// If we're hiding rooms, show a 'show more' button to the user. This button
|
||||
// floats above the resize handle, if we have one present
|
||||
let showMoreButton = null;
|
||||
// floats above the resize handle, if we have one present. If the user has all
|
||||
// tiles visible, it becomes 'show less'.
|
||||
let showNButton = null;
|
||||
if (tiles.length > nVisible) {
|
||||
// we have a cutoff condition - add the button to show all
|
||||
const numMissing = tiles.length - visibleTiles.length;
|
||||
let showMoreText = (
|
||||
<span className='mx_RoomSublist2_showMoreButtonText'>
|
||||
<span className='mx_RoomSublist2_showNButtonText'>
|
||||
{_t("Show %(count)s more", {count: numMissing})}
|
||||
</span>
|
||||
);
|
||||
if (this.props.isMinimized) showMoreText = null;
|
||||
showMoreButton = (
|
||||
<div onClick={this.onShowAllClick} className='mx_RoomSublist2_showMoreButton'>
|
||||
<span className='mx_RoomSublist2_showMoreButtonChevron'>
|
||||
showNButton = (
|
||||
<div onClick={this.onShowAllClick} className='mx_RoomSublist2_showNButton'>
|
||||
<span className='mx_RoomSublist2_showMoreButtonChevron mx_RoomSublist2_showNButtonChevron'>
|
||||
{/* set by CSS masking */}
|
||||
</span>
|
||||
{showMoreText}
|
||||
</div>
|
||||
);
|
||||
} 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'>
|
||||
{_t("Show less")}
|
||||
</span>
|
||||
);
|
||||
if (this.props.isMinimized) showLessText = null;
|
||||
showNButton = (
|
||||
<div onClick={this.onShowLessClick} className='mx_RoomSublist2_showNButton'>
|
||||
<span className='mx_RoomSublist2_showLessButtonChevron mx_RoomSublist2_showNButtonChevron'>
|
||||
{/* set by CSS masking */}
|
||||
</span>
|
||||
{showLessText}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Figure out if we need a handle
|
||||
|
@ -340,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 (showMoreButton) 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
|
||||
|
@ -365,9 +421,9 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
className="mx_RoomSublist2_resizeBox"
|
||||
>
|
||||
{visibleTiles}
|
||||
{showMoreButton}
|
||||
{showNButton}
|
||||
</ResizableBox>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: onKeyDown support
|
||||
|
|
|
@ -21,7 +21,7 @@ import React, { createRef } from "react";
|
|||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import classNames from "classnames";
|
||||
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
||||
import AccessibleButton, {ButtonEvent} from "../../views/elements/AccessibleButton";
|
||||
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
|
||||
import RoomAvatar from "../../views/avatars/RoomAvatar";
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { Key } from "../../../Keyboard";
|
||||
|
@ -31,6 +31,7 @@ import { _t } from "../../../languageHandler";
|
|||
import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
|
||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||
import { MessagePreviewStore } from "../../../stores/MessagePreviewStore";
|
||||
import RoomTileIcon from "./RoomTileIcon";
|
||||
|
||||
/*******************************************************************
|
||||
* CAUTION *
|
||||
|
@ -44,6 +45,7 @@ interface IProps {
|
|||
room: Room;
|
||||
showMessagePreview: boolean;
|
||||
isMinimized: boolean;
|
||||
tag: TagID;
|
||||
|
||||
// TODO: Allow falsifying counts (for invites and stuff)
|
||||
// TODO: Transparency? Was this ever used?
|
||||
|
@ -230,7 +232,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
/>
|
||||
{contextMenu}
|
||||
</React.Fragment>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public render(): React.ReactElement {
|
||||
|
@ -303,7 +305,8 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
role="treeitem"
|
||||
>
|
||||
<div className="mx_RoomTile2_avatarContainer">
|
||||
<RoomAvatar room={this.props.room} width={avatarSize} height={avatarSize}/>
|
||||
<RoomAvatar room={this.props.room} width={avatarSize} height={avatarSize} />
|
||||
<RoomTileIcon room={this.props.room} tag={this.props.tag} />
|
||||
</div>
|
||||
{nameContainer}
|
||||
<div className="mx_RoomTile2_badgeContainer">
|
||||
|
|
150
src/components/views/rooms/RoomTileIcon.tsx
Normal file
150
src/components/views/rooms/RoomTileIcon.tsx
Normal file
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { isPresenceEnabled } from "../../../utils/presence";
|
||||
|
||||
enum Icon {
|
||||
// Note: the names here are used in CSS class names
|
||||
None = "NONE", // ... except this one
|
||||
Globe = "GLOBE",
|
||||
PresenceOnline = "ONLINE",
|
||||
PresenceAway = "AWAY",
|
||||
PresenceOffline = "OFFLINE",
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
tag: TagID;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
icon: Icon;
|
||||
}
|
||||
|
||||
export default class RoomTileIcon extends React.Component<IProps, IState> {
|
||||
private _dmUser: User;
|
||||
private isUnmounted = false;
|
||||
private isWatchingTimeline = false;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
icon: this.calculateIcon(),
|
||||
};
|
||||
}
|
||||
|
||||
private get isPublicRoom(): boolean {
|
||||
const joinRules = this.props.room.currentState.getStateEvents("m.room.join_rules", "");
|
||||
const joinRule = joinRules && joinRules.getContent().join_rule;
|
||||
return joinRule === 'public';
|
||||
}
|
||||
|
||||
private get dmUser(): User {
|
||||
return this._dmUser;
|
||||
}
|
||||
|
||||
private set dmUser(val: User) {
|
||||
const oldUser = this._dmUser;
|
||||
this._dmUser = val;
|
||||
if (oldUser && oldUser !== this._dmUser) {
|
||||
oldUser.off('User.currentlyActive', this.onPresenceUpdate);
|
||||
oldUser.off('User.presence', this.onPresenceUpdate);
|
||||
}
|
||||
if (this._dmUser && oldUser !== this._dmUser) {
|
||||
this._dmUser.on('User.currentlyActive', this.onPresenceUpdate);
|
||||
this._dmUser.on('User.presence', this.onPresenceUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.isUnmounted = true;
|
||||
if (this.isWatchingTimeline) this.props.room.off('Room.timeline', this.onRoomTimeline);
|
||||
this.dmUser = null; // clear listeners, if any
|
||||
}
|
||||
|
||||
private onRoomTimeline = (ev: MatrixEvent, room: Room) => {
|
||||
if (this.isUnmounted) return;
|
||||
|
||||
// apparently these can happen?
|
||||
if (!room) return;
|
||||
if (this.props.room.roomId !== room.roomId) return;
|
||||
|
||||
if (ev.getType() === 'm.room.join_rules' || ev.getType() === 'm.room.member') {
|
||||
this.setState({icon: this.calculateIcon()});
|
||||
}
|
||||
};
|
||||
|
||||
private onPresenceUpdate = () => {
|
||||
if (this.isUnmounted) return;
|
||||
|
||||
let newIcon = this.getPresenceIcon();
|
||||
if (newIcon !== this.state.icon) this.setState({icon: newIcon});
|
||||
};
|
||||
|
||||
private getPresenceIcon(): Icon {
|
||||
if (!this.dmUser) return Icon.None;
|
||||
|
||||
let icon = Icon.None;
|
||||
|
||||
const isOnline = this.dmUser.currentlyActive || this.dmUser.presence === 'online';
|
||||
if (isOnline) {
|
||||
icon = Icon.PresenceOnline;
|
||||
} else if (this.dmUser.presence === 'offline') {
|
||||
icon = Icon.PresenceOffline;
|
||||
} else if (this.dmUser.presence === 'unavailable') {
|
||||
icon = Icon.PresenceAway;
|
||||
}
|
||||
|
||||
return icon;
|
||||
}
|
||||
|
||||
private calculateIcon(): Icon {
|
||||
let icon = Icon.None;
|
||||
|
||||
if (this.props.tag === DefaultTagID.DM && this.props.room.getJoinedMemberCount() === 2) {
|
||||
// Track presence, if available
|
||||
if (isPresenceEnabled()) {
|
||||
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId);
|
||||
if (otherUserId) {
|
||||
this.dmUser = MatrixClientPeg.get().getUser(otherUserId);
|
||||
icon = this.getPresenceIcon();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Track publicity
|
||||
icon = this.isPublicRoom ? Icon.Globe : Icon.None;
|
||||
if (!this.isWatchingTimeline) {
|
||||
this.props.room.on('Room.timeline', this.onRoomTimeline);
|
||||
this.isWatchingTimeline = true;
|
||||
}
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
|
||||
public render(): React.ReactElement {
|
||||
if (this.state.icon === Icon.None) return null;
|
||||
|
||||
return <span className={`mx_RoomTileIcon mx_RoomTileIcon_${this.state.icon.toLowerCase()}`} />;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -32,7 +32,7 @@ import { IValidationResult, IFieldState } from '../../../elements/Validation';
|
|||
import StyledRadioButton from '../../../elements/StyledRadioButton';
|
||||
import StyledCheckbox from '../../../elements/StyledCheckbox';
|
||||
import SettingsFlag from '../../../elements/SettingsFlag';
|
||||
import MessagePreview from '../../../elements/MessagePreview';
|
||||
import EventTilePreview from '../../../elements/EventTilePreview';
|
||||
|
||||
interface IProps {
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ interface IThemeState {
|
|||
export interface CustomThemeMessage {
|
||||
isError: boolean;
|
||||
text: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface IState extends IThemeState {
|
||||
// String displaying the current selected fontSize.
|
||||
|
@ -166,7 +166,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");
|
||||
|
@ -295,7 +295,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_fontScaling">
|
||||
|
||||
<span className="mx_SettingsTab_subheading">{_t("Font size")}</span>
|
||||
<MessagePreview
|
||||
<EventTilePreview
|
||||
className="mx_AppearanceUserSettingsTab_fontSlider_preview"
|
||||
message={MESSAGE_PREVIEW_TEXT}
|
||||
useIRCLayout={this.state.useIRCLayout}
|
||||
|
|
|
@ -66,7 +66,6 @@ export default class LabsUserSettingsTab extends React.Component {
|
|||
<SettingsFlag name={"showHiddenEventsInTimeline"} level={SettingLevel.DEVICE} />
|
||||
<SettingsFlag name={"lowBandwidth"} level={SettingLevel.DEVICE} />
|
||||
<SettingsFlag name={"sendReadReceipts"} level={SettingLevel.ACCOUNT} />
|
||||
<SettingsFlag name={"keepSecretStoragePassphraseForSession"} level={SettingLevel.DEVICE} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue