Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/notifications0
This commit is contained in:
commit
edd09f66d1
142 changed files with 4631 additions and 1005 deletions
|
@ -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)} />;
|
||||
}
|
||||
|
||||
}
|
129
src/components/views/elements/EventTilePreview.tsx
Normal file
129
src/components/views/elements/EventTilePreview.tsx
Normal file
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
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 classnames from 'classnames';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
|
||||
import * as Avatar from '../../../Avatar';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import EventTile from '../rooms/EventTile';
|
||||
|
||||
interface IProps {
|
||||
/**
|
||||
* The text to be displayed in the message preview
|
||||
*/
|
||||
message: string;
|
||||
|
||||
/**
|
||||
* Whether to use the irc layout or not
|
||||
*/
|
||||
useIRCLayout: boolean;
|
||||
|
||||
/**
|
||||
* classnames to apply to the wrapper of the preview
|
||||
*/
|
||||
className: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
userId: string;
|
||||
displayname: string;
|
||||
avatar_url: string;
|
||||
}
|
||||
|
||||
const AVATAR_SIZE = 32;
|
||||
|
||||
export default class EventTilePreview extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
userId: "@erim:fink.fink",
|
||||
displayname: "Erimayas Fink",
|
||||
avatar_url: null,
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
// Fetch current user data
|
||||
const client = MatrixClientPeg.get();
|
||||
const userId = client.getUserId();
|
||||
const profileInfo = await client.getProfileInfo(userId);
|
||||
const avatar_url = Avatar.avatarUrlForUser(
|
||||
{avatarUrl: profileInfo.avatar_url},
|
||||
AVATAR_SIZE, AVATAR_SIZE, "crop");
|
||||
|
||||
this.setState({
|
||||
userId,
|
||||
displayname: profileInfo.displayname,
|
||||
avatar_url,
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private fakeEvent({userId, displayname, avatar_url}: IState) {
|
||||
// Fake it till we make it
|
||||
const event = new MatrixEvent(JSON.parse(`{
|
||||
"type": "m.room.message",
|
||||
"sender": "${userId}",
|
||||
"content": {
|
||||
"m.new_content": {
|
||||
"msgtype": "m.text",
|
||||
"body": "${this.props.message}",
|
||||
"displayname": "${displayname}",
|
||||
"avatar_url": "${avatar_url}"
|
||||
},
|
||||
"msgtype": "m.text",
|
||||
"body": "${this.props.message}",
|
||||
"displayname": "${displayname}",
|
||||
"avatar_url": "${avatar_url}"
|
||||
},
|
||||
"unsigned": {
|
||||
"age": 97
|
||||
},
|
||||
"event_id": "$9999999999999999999999999999999999999999999",
|
||||
"room_id": "!999999999999999999:matrix.org"
|
||||
}`));
|
||||
|
||||
// Fake it more
|
||||
event.sender = {
|
||||
name: displayname,
|
||||
userId: userId,
|
||||
getAvatarUrl: (..._) => {
|
||||
return avatar_url;
|
||||
},
|
||||
};
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const event = this.fakeEvent(this.state);
|
||||
|
||||
let className = classnames(
|
||||
this.props.className,
|
||||
{
|
||||
"mx_IRCLayout": this.props.useIRCLayout,
|
||||
"mx_GroupLayout": !this.props.useIRCLayout,
|
||||
}
|
||||
);
|
||||
|
||||
return <div className={className}>
|
||||
<EventTile mxEvent={event} useIRCLayout={this.props.useIRCLayout} />
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -54,6 +54,8 @@ interface IProps {
|
|||
// If specified, contents will appear as a tooltip on the element and
|
||||
// validation feedback tooltips will be suppressed.
|
||||
tooltipContent?: React.ReactNode;
|
||||
// If specified the tooltip will be shown regardless of feedback
|
||||
forceTooltipVisible?: boolean;
|
||||
// If specified alongside tooltipContent, the class name to apply to the
|
||||
// tooltip itself.
|
||||
tooltipClassName?: string;
|
||||
|
@ -85,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
|
||||
|
@ -242,10 +244,9 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
|
|||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||
let fieldTooltip;
|
||||
if (tooltipContent || this.state.feedback) {
|
||||
const addlClassName = tooltipClassName ? tooltipClassName : '';
|
||||
fieldTooltip = <Tooltip
|
||||
tooltipClassName={`mx_Field_tooltip ${addlClassName}`}
|
||||
visible={this.state.feedbackVisible}
|
||||
tooltipClassName={classNames("mx_Field_tooltip", tooltipClassName)}
|
||||
visible={(this.state.focused && this.props.forceTooltipVisible) || this.state.feedbackVisible}
|
||||
label={tooltipContent || this.state.feedback}
|
||||
/>;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
@ -36,6 +36,7 @@ export default class StyledRadioButton extends React.PureComponent<IProps, IStat
|
|||
{
|
||||
"mx_RadioButton_disabled": disabled,
|
||||
"mx_RadioButton_enabled": !disabled,
|
||||
"mx_RadioButton_checked": this.props.checked,
|
||||
});
|
||||
return <label className={_className}>
|
||||
<input type='radio' disabled={disabled} {...otherProps} />
|
||||
|
@ -43,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
|
||||
|
|
|
@ -28,6 +28,7 @@ export const E2E_STATE = {
|
|||
WARNING: "warning",
|
||||
UNKNOWN: "unknown",
|
||||
NORMAL: "normal",
|
||||
UNAUTHENTICATED: "unauthenticated",
|
||||
};
|
||||
|
||||
const crossSigningUserTitles = {
|
||||
|
|
|
@ -313,35 +313,52 @@ export default createReactClass({
|
|||
return;
|
||||
}
|
||||
|
||||
// If we directly trust the device, short-circuit here
|
||||
const verified = await this.context.isEventSenderVerified(mxEvent);
|
||||
if (verified) {
|
||||
const encryptionInfo = this.context.getEventEncryptionInfo(mxEvent);
|
||||
const senderId = mxEvent.getSender();
|
||||
const userTrust = this.context.checkUserTrust(senderId);
|
||||
|
||||
if (encryptionInfo.mismatchedSender) {
|
||||
// something definitely wrong is going on here
|
||||
this.setState({
|
||||
verified: E2E_STATE.VERIFIED,
|
||||
}, () => {
|
||||
// Decryption may have caused a change in size
|
||||
this.props.onHeightChanged();
|
||||
});
|
||||
verified: E2E_STATE.WARNING,
|
||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.context.checkUserTrust(mxEvent.getSender()).isCrossSigningVerified()) {
|
||||
if (!userTrust.isCrossSigningVerified()) {
|
||||
// user is not verified, so default to everything is normal
|
||||
this.setState({
|
||||
verified: E2E_STATE.NORMAL,
|
||||
}, this.props.onHeightChanged);
|
||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||
return;
|
||||
}
|
||||
|
||||
const eventSenderTrust = await this.context.checkEventSenderTrust(mxEvent);
|
||||
const eventSenderTrust = this.context.checkDeviceTrust(
|
||||
senderId, encryptionInfo.sender.deviceId,
|
||||
);
|
||||
if (!eventSenderTrust) {
|
||||
this.setState({
|
||||
verified: E2E_STATE.UNKNOWN,
|
||||
}, this.props.onHeightChanged); // Decryption may have cause a change in size
|
||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||
return;
|
||||
}
|
||||
|
||||
if (!eventSenderTrust.isVerified()) {
|
||||
this.setState({
|
||||
verified: E2E_STATE.WARNING,
|
||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||
return;
|
||||
}
|
||||
|
||||
if (!encryptionInfo.authenticated) {
|
||||
this.setState({
|
||||
verified: E2E_STATE.UNAUTHENTICATED,
|
||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
verified: eventSenderTrust.isVerified() ? E2E_STATE.VERIFIED : E2E_STATE.WARNING,
|
||||
verified: E2E_STATE.VERIFIED,
|
||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||
},
|
||||
|
||||
|
@ -526,6 +543,8 @@ export default createReactClass({
|
|||
return; // no icon if we've not even cross-signed the user
|
||||
} else if (this.state.verified === E2E_STATE.VERIFIED) {
|
||||
return; // no icon for verified
|
||||
} else if (this.state.verified === E2E_STATE.UNAUTHENTICATED) {
|
||||
return (<E2ePadlockUnauthenticated />);
|
||||
} else if (this.state.verified === E2E_STATE.UNKNOWN) {
|
||||
return (<E2ePadlockUnknown />);
|
||||
} else {
|
||||
|
@ -976,6 +995,12 @@ function E2ePadlockUnknown(props) {
|
|||
);
|
||||
}
|
||||
|
||||
function E2ePadlockUnauthenticated(props) {
|
||||
return (
|
||||
<E2ePadlock title={_t("The authenticity of this encrypted message can't be guaranteed on this device.")} icon="unauthenticated" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
class E2ePadlock extends React.Component {
|
||||
static propTypes = {
|
||||
icon: PropTypes.string.isRequired,
|
||||
|
|
|
@ -18,27 +18,24 @@ import React from "react";
|
|||
import classNames from "classnames";
|
||||
import { formatMinimalBadgeCount } from "../../../utils/FormattingUtils";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
||||
import AccessibleButton from "../../views/elements/AccessibleButton";
|
||||
import RoomAvatar from "../../views/avatars/RoomAvatar";
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { Key } from "../../../Keyboard";
|
||||
import * as RoomNotifs from '../../../RoomNotifs';
|
||||
import { EffectiveMembership, getEffectiveMembership } from "../../../stores/room-list/membership";
|
||||
import * as Unread from '../../../Unread';
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import ActiveRoomObserver from "../../../ActiveRoomObserver";
|
||||
import { EventEmitter } from "events";
|
||||
import { arrayDiff } from "../../../utils/arrays";
|
||||
import { IDestroyable } from "../../../utils/IDestroyable";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||
import { readReceiptChangeIsFor } from "../../../utils/read-receipts";
|
||||
|
||||
export const NOTIFICATION_STATE_UPDATE = "update";
|
||||
|
||||
export enum NotificationColor {
|
||||
// Inverted (None -> Red) because we do integer comparisons on this
|
||||
None, // nothing special
|
||||
Bold, // no badge, show as unread
|
||||
Bold, // no badge, show as unread // TODO: This goes away with new notification structures
|
||||
Grey, // unread notified messages
|
||||
Red, // unread pings
|
||||
}
|
||||
|
@ -53,18 +50,45 @@ interface IProps {
|
|||
notification: INotificationState;
|
||||
|
||||
/**
|
||||
* If true, the badge will conditionally display a badge without count for the user.
|
||||
* If true, the badge will show a count if at all possible. This is typically
|
||||
* used to override the user's preference for things like room sublists.
|
||||
*/
|
||||
allowNoCount: boolean;
|
||||
forceCount: boolean;
|
||||
|
||||
/**
|
||||
* The room ID, if any, the badge represents.
|
||||
*/
|
||||
roomId?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
showCounts: boolean; // whether or not to show counts. Independent of props.forceCount
|
||||
}
|
||||
|
||||
export default class NotificationBadge extends React.PureComponent<IProps, IState> {
|
||||
private countWatcherRef: string;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.props.notification.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
|
||||
|
||||
this.state = {
|
||||
showCounts: SettingsStore.getValue("Notifications.alwaysShowBadgeCounts", this.roomId),
|
||||
};
|
||||
|
||||
this.countWatcherRef = SettingsStore.watchSetting(
|
||||
"Notifications.alwaysShowBadgeCounts", this.roomId,
|
||||
this.countPreferenceChanged,
|
||||
);
|
||||
}
|
||||
|
||||
private get roomId(): string {
|
||||
// We should convert this to null for safety with the SettingsStore
|
||||
return this.props.roomId || null;
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
SettingsStore.unwatchSetting(this.countWatcherRef);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Readonly<IProps>) {
|
||||
|
@ -75,24 +99,34 @@ export default class NotificationBadge extends React.PureComponent<IProps, IStat
|
|||
this.props.notification.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
|
||||
}
|
||||
|
||||
private countPreferenceChanged = () => {
|
||||
this.setState({showCounts: SettingsStore.getValue("Notifications.alwaysShowBadgeCounts", this.roomId)});
|
||||
};
|
||||
|
||||
private onNotificationUpdate = () => {
|
||||
this.forceUpdate(); // notification state changed - update
|
||||
};
|
||||
|
||||
public render(): React.ReactElement {
|
||||
// Don't show a badge if we don't need to
|
||||
if (this.props.notification.color <= NotificationColor.Bold) return null;
|
||||
if (this.props.notification.color <= NotificationColor.None) return null;
|
||||
|
||||
const hasNotif = this.props.notification.color >= NotificationColor.Red;
|
||||
const hasCount = this.props.notification.color >= NotificationColor.Grey;
|
||||
const isEmptyBadge = this.props.allowNoCount && !localStorage.getItem("mx_rl_rt_badgeCount");
|
||||
const hasUnread = this.props.notification.color >= NotificationColor.Bold;
|
||||
const couldBeEmpty = (!this.state.showCounts || hasUnread) && !hasNotif;
|
||||
let isEmptyBadge = couldBeEmpty && (!this.state.showCounts || !hasCount);
|
||||
if (this.props.forceCount) {
|
||||
isEmptyBadge = false;
|
||||
if (!hasCount) return null; // Can't render a badge
|
||||
}
|
||||
|
||||
let symbol = this.props.notification.symbol || formatMinimalBadgeCount(this.props.notification.count);
|
||||
if (isEmptyBadge) symbol = "";
|
||||
|
||||
const classes = classNames({
|
||||
'mx_NotificationBadge': true,
|
||||
'mx_NotificationBadge_visible': hasCount,
|
||||
'mx_NotificationBadge_visible': isEmptyBadge ? true : hasCount,
|
||||
'mx_NotificationBadge_highlighted': hasNotif,
|
||||
'mx_NotificationBadge_dot': isEmptyBadge,
|
||||
'mx_NotificationBadge_2char': symbol.length > 0 && symbol.length < 3,
|
||||
|
@ -107,14 +141,14 @@ export default class NotificationBadge extends React.PureComponent<IProps, IStat
|
|||
}
|
||||
}
|
||||
|
||||
export class RoomNotificationState extends EventEmitter implements IDestroyable {
|
||||
export class RoomNotificationState extends EventEmitter implements IDestroyable, INotificationState {
|
||||
private _symbol: string;
|
||||
private _count: number;
|
||||
private _color: NotificationColor;
|
||||
|
||||
constructor(private room: Room) {
|
||||
super();
|
||||
this.room.on("Room.receipt", this.handleRoomEventUpdate);
|
||||
this.room.on("Room.receipt", this.handleReadReceipt);
|
||||
this.room.on("Room.timeline", this.handleRoomEventUpdate);
|
||||
this.room.on("Room.redaction", this.handleRoomEventUpdate);
|
||||
MatrixClientPeg.get().on("Event.decrypted", this.handleRoomEventUpdate);
|
||||
|
@ -138,7 +172,7 @@ export class RoomNotificationState extends EventEmitter implements IDestroyable
|
|||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.room.removeListener("Room.receipt", this.handleRoomEventUpdate);
|
||||
this.room.removeListener("Room.receipt", this.handleReadReceipt);
|
||||
this.room.removeListener("Room.timeline", this.handleRoomEventUpdate);
|
||||
this.room.removeListener("Room.redaction", this.handleRoomEventUpdate);
|
||||
if (MatrixClientPeg.get()) {
|
||||
|
@ -146,6 +180,12 @@ export class RoomNotificationState extends EventEmitter implements IDestroyable
|
|||
}
|
||||
}
|
||||
|
||||
private handleReadReceipt = (event: MatrixEvent, room: Room) => {
|
||||
if (!readReceiptChangeIsFor(event, MatrixClientPeg.get())) return; // not our own - ignore
|
||||
if (room.roomId !== this.room.roomId) return; // not for us - ignore
|
||||
this.updateNotificationState();
|
||||
};
|
||||
|
||||
private handleRoomEventUpdate = (event: MatrixEvent) => {
|
||||
const roomId = event.getRoomId();
|
||||
|
||||
|
@ -205,13 +245,38 @@ export class RoomNotificationState extends EventEmitter implements IDestroyable
|
|||
}
|
||||
}
|
||||
|
||||
export class ListNotificationState extends EventEmitter implements IDestroyable {
|
||||
export class TagSpecificNotificationState extends RoomNotificationState {
|
||||
private static TAG_TO_COLOR: {
|
||||
// @ts-ignore - TS wants this to be a string key, but we know better
|
||||
[tagId: TagID]: NotificationColor,
|
||||
} = {
|
||||
[DefaultTagID.DM]: NotificationColor.Red,
|
||||
};
|
||||
|
||||
private readonly colorWhenNotIdle?: NotificationColor;
|
||||
|
||||
constructor(room: Room, tagId: TagID) {
|
||||
super(room);
|
||||
|
||||
const specificColor = TagSpecificNotificationState.TAG_TO_COLOR[tagId];
|
||||
if (specificColor) this.colorWhenNotIdle = specificColor;
|
||||
}
|
||||
|
||||
public get color(): NotificationColor {
|
||||
if (!this.colorWhenNotIdle) return super.color;
|
||||
|
||||
if (super.color !== NotificationColor.None) return this.colorWhenNotIdle;
|
||||
return super.color;
|
||||
}
|
||||
}
|
||||
|
||||
export class ListNotificationState extends EventEmitter implements IDestroyable, INotificationState {
|
||||
private _count: number;
|
||||
private _color: NotificationColor;
|
||||
private rooms: Room[] = [];
|
||||
private states: { [roomId: string]: RoomNotificationState } = {};
|
||||
|
||||
constructor(private byTileCount = false) {
|
||||
constructor(private byTileCount = false, private tagId: TagID) {
|
||||
super();
|
||||
}
|
||||
|
||||
|
@ -240,12 +305,13 @@ 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();
|
||||
}
|
||||
for (const newRoom of diff.added) {
|
||||
const state = new RoomNotificationState(newRoom);
|
||||
const state = new TagSpecificNotificationState(newRoom, this.tagId);
|
||||
state.on(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
|
||||
if (this.states[newRoom.roomId]) {
|
||||
// "Should never happen" disclaimer.
|
||||
|
@ -258,6 +324,12 @@ export class ListNotificationState extends EventEmitter implements IDestroyable
|
|||
this.calculateTotalState();
|
||||
}
|
||||
|
||||
public getForRoom(room: Room) {
|
||||
const state = this.states[room.roomId];
|
||||
if (!state) throw new Error("Unknown room for notification state");
|
||||
return state;
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
for (const state of Object.values(this.states)) {
|
||||
state.destroy();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -193,6 +193,7 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
|||
components.push(
|
||||
<RoomSublist2
|
||||
key={`sublist-${orderedTagId}`}
|
||||
tagId={orderedTagId}
|
||||
forRooms={true}
|
||||
rooms={orderedRooms}
|
||||
startAsHidden={aesthetics.defaultHidden}
|
||||
|
|
|
@ -32,6 +32,7 @@ import StyledCheckbox from "../elements/StyledCheckbox";
|
|||
import StyledRadioButton from "../elements/StyledRadioButton";
|
||||
import RoomListStore from "../../../stores/room-list/RoomListStore2";
|
||||
import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorithms/models";
|
||||
import { TagID } from "../../../stores/room-list/models";
|
||||
|
||||
/*******************************************************************
|
||||
* CAUTION *
|
||||
|
@ -41,6 +42,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[];
|
||||
|
@ -51,6 +57,7 @@ interface IProps {
|
|||
isInvite: boolean;
|
||||
layout: ListLayout;
|
||||
isMinimized: boolean;
|
||||
tagId: TagID;
|
||||
|
||||
// TODO: Collapsed state
|
||||
// TODO: Group invites
|
||||
|
@ -73,7 +80,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
super(props);
|
||||
|
||||
this.state = {
|
||||
notificationState: new ListNotificationState(this.props.isInvite),
|
||||
notificationState: new ListNotificationState(this.props.isInvite, this.props.tagId),
|
||||
menuDisplayed: false,
|
||||
};
|
||||
this.state.notificationState.setRooms(this.props.rooms);
|
||||
|
@ -105,7 +112,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
|
||||
};
|
||||
|
||||
|
@ -120,13 +132,13 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private onUnreadFirstChanged = async () => {
|
||||
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.layout.tagId) === ListAlgorithm.Importance;
|
||||
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
|
||||
const newAlgorithm = isUnreadFirst ? ListAlgorithm.Natural : ListAlgorithm.Importance;
|
||||
await RoomListStore.instance.setListOrder(this.props.layout.tagId, newAlgorithm);
|
||||
await RoomListStore.instance.setListOrder(this.props.tagId, newAlgorithm);
|
||||
};
|
||||
|
||||
private onTagSortChanged = async (sort: SortAlgorithm) => {
|
||||
await RoomListStore.instance.setTagSorting(this.props.layout.tagId, sort);
|
||||
await RoomListStore.instance.setTagSorting(this.props.tagId, sort);
|
||||
};
|
||||
|
||||
private onMessagePreviewChanged = () => {
|
||||
|
@ -134,7 +146,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 +178,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.tagId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -157,8 +191,8 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
let contextMenu = null;
|
||||
if (this.state.menuDisplayed) {
|
||||
const elementRect = this.menuButtonRef.current.getBoundingClientRect();
|
||||
const isAlphabetical = RoomListStore.instance.getTagSorting(this.props.layout.tagId) === SortAlgorithm.Alphabetic;
|
||||
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.layout.tagId) === ListAlgorithm.Importance;
|
||||
const isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic;
|
||||
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
|
||||
contextMenu = (
|
||||
<ContextMenu
|
||||
chevronFace="none"
|
||||
|
@ -172,14 +206,14 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
<StyledRadioButton
|
||||
onChange={() => this.onTagSortChanged(SortAlgorithm.Recent)}
|
||||
checked={!isAlphabetical}
|
||||
name={`mx_${this.props.layout.tagId}_sortBy`}
|
||||
name={`mx_${this.props.tagId}_sortBy`}
|
||||
>
|
||||
{_t("Activity")}
|
||||
</StyledRadioButton>
|
||||
<StyledRadioButton
|
||||
onChange={() => this.onTagSortChanged(SortAlgorithm.Alphabetic)}
|
||||
checked={isAlphabetical}
|
||||
name={`mx_${this.props.layout.tagId}_sortBy`}
|
||||
name={`mx_${this.props.tagId}_sortBy`}
|
||||
>
|
||||
{_t("A-Z")}
|
||||
</StyledRadioButton>
|
||||
|
@ -235,7 +269,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
|
||||
// TODO: Collapsed state
|
||||
|
||||
const badge = <NotificationBadge allowNoCount={false} notification={this.state.notificationState}/>;
|
||||
const badge = <NotificationBadge forceCount={true} notification={this.state.notificationState}/>;
|
||||
|
||||
let addRoomButton = null;
|
||||
if (!!this.props.onAddRoom) {
|
||||
|
@ -249,28 +283,48 @@ 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,
|
||||
});
|
||||
|
||||
const badgeContainer = (
|
||||
<div className="mx_RoomSublist2_badgeContainer">
|
||||
{badge}
|
||||
</div>
|
||||
);
|
||||
|
||||
// TODO: a11y (see old component)
|
||||
// Note: the addRoomButton conditionally gets moved around
|
||||
// the DOM depending on whether or not the list is minimized.
|
||||
// If we're minimized, we want it below the header so it
|
||||
// doesn't become sticky.
|
||||
// The same applies to the notification badge.
|
||||
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()}
|
||||
{this.props.isMinimized ? null : addRoomButton}
|
||||
{this.props.isMinimized ? null : badgeContainer}
|
||||
</div>
|
||||
{this.props.isMinimized ? badgeContainer : null}
|
||||
{this.props.isMinimized ? addRoomButton : null}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
|
@ -303,25 +357,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 +411,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 +434,9 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
className="mx_RoomSublist2_resizeBox"
|
||||
>
|
||||
{visibleTiles}
|
||||
{showMoreButton}
|
||||
{showNButton}
|
||||
</ResizableBox>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: onKeyDown support
|
||||
|
|
|
@ -21,16 +21,21 @@ 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";
|
||||
import ActiveRoomObserver from "../../../ActiveRoomObserver";
|
||||
import NotificationBadge, { INotificationState, NotificationColor, RoomNotificationState } from "./NotificationBadge";
|
||||
import NotificationBadge, {
|
||||
INotificationState,
|
||||
NotificationColor,
|
||||
TagSpecificNotificationState
|
||||
} from "./NotificationBadge";
|
||||
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 +49,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?
|
||||
|
@ -77,7 +83,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
|
||||
this.state = {
|
||||
hover: false,
|
||||
notificationState: new RoomNotificationState(this.props.room),
|
||||
notificationState: new TagSpecificNotificationState(this.props.room, this.props.tag),
|
||||
selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId,
|
||||
generalMenuDisplayed: false,
|
||||
};
|
||||
|
@ -230,7 +236,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
/>
|
||||
{contextMenu}
|
||||
</React.Fragment>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public render(): React.ReactElement {
|
||||
|
@ -246,7 +252,13 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
'mx_RoomTile2_minimized': this.props.isMinimized,
|
||||
});
|
||||
|
||||
const badge = <NotificationBadge notification={this.state.notificationState} allowNoCount={true} />;
|
||||
const badge = (
|
||||
<NotificationBadge
|
||||
notification={this.state.notificationState}
|
||||
forceCount={false}
|
||||
roomId={this.props.room.roomId}
|
||||
/>
|
||||
);
|
||||
|
||||
// TODO: the original RoomTile uses state for the room name. Do we need to?
|
||||
let name = this.props.room.name;
|
||||
|
@ -303,7 +315,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);
|
||||
|
@ -154,13 +154,6 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
errorSection = <div className="error">{error.toString()}</div>;
|
||||
}
|
||||
|
||||
// Whether the various keys exist on your account (but not necessarily
|
||||
// on this device).
|
||||
const enabledForAccount = (
|
||||
crossSigningPrivateKeysInStorage &&
|
||||
secretStorageKeyInAccount
|
||||
);
|
||||
|
||||
let summarisedStatus;
|
||||
if (homeserverSupportsCrossSigning === undefined) {
|
||||
const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner');
|
||||
|
@ -184,8 +177,19 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
)}</p>;
|
||||
}
|
||||
|
||||
const keysExistAnywhere = (
|
||||
secretStorageKeyInAccount ||
|
||||
crossSigningPrivateKeysInStorage ||
|
||||
crossSigningPublicKeysOnDevice
|
||||
);
|
||||
const keysExistEverywhere = (
|
||||
secretStorageKeyInAccount &&
|
||||
crossSigningPrivateKeysInStorage &&
|
||||
crossSigningPublicKeysOnDevice
|
||||
);
|
||||
|
||||
let resetButton;
|
||||
if (enabledForAccount) {
|
||||
if (keysExistAnywhere) {
|
||||
resetButton = (
|
||||
<div className="mx_CrossSigningPanel_buttonRow">
|
||||
<AccessibleButton kind="danger" onClick={this._destroySecureSecretStorage}>
|
||||
|
@ -197,10 +201,7 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
|
||||
// TODO: determine how better to expose this to users in addition to prompts at login/toast
|
||||
let bootstrapButton;
|
||||
if (
|
||||
(!enabledForAccount || !crossSigningPublicKeysOnDevice) &&
|
||||
homeserverSupportsCrossSigning
|
||||
) {
|
||||
if (!keysExistEverywhere && homeserverSupportsCrossSigning) {
|
||||
bootstrapButton = (
|
||||
<div className="mx_CrossSigningPanel_buttonRow">
|
||||
<AccessibleButton kind="primary" onClick={this._onBootstrapClick}>
|
||||
|
|
|
@ -19,10 +19,8 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import {_t} from "../../../../../languageHandler";
|
||||
import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore";
|
||||
import * as sdk from "../../../../../index";
|
||||
import { enumerateThemes } from "../../../../../theme";
|
||||
import ThemeWatcher from "../../../../../settings/watchers/ThemeWatcher";
|
||||
import Field from "../../../elements/Field";
|
||||
import Slider from "../../../elements/Slider";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import dis from "../../../../../dispatcher/dispatcher";
|
||||
|
@ -30,31 +28,42 @@ import { FontWatcher } from "../../../../../settings/watchers/FontWatcher";
|
|||
import { RecheckThemePayload } from '../../../../../dispatcher/payloads/RecheckThemePayload';
|
||||
import { Action } from '../../../../../dispatcher/actions';
|
||||
import { IValidationResult, IFieldState } from '../../../elements/Validation';
|
||||
import StyledRadioButton from '../../../elements/StyledRadioButton';
|
||||
import StyledCheckbox from '../../../elements/StyledCheckbox';
|
||||
import SettingsFlag from '../../../elements/SettingsFlag';
|
||||
import Field from '../../../elements/Field';
|
||||
import EventTilePreview from '../../../elements/EventTilePreview';
|
||||
|
||||
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.
|
||||
// Needs to be string for things like '17.' without
|
||||
// trailing 0s.
|
||||
fontSize: string,
|
||||
customThemeUrl: string,
|
||||
customThemeMessage: CustomThemeMessage,
|
||||
useCustomFontSize: boolean,
|
||||
fontSize: string;
|
||||
customThemeUrl: string;
|
||||
customThemeMessage: CustomThemeMessage;
|
||||
useCustomFontSize: boolean;
|
||||
useSystemFont: boolean;
|
||||
systemFont: string;
|
||||
showAdvanced: boolean;
|
||||
useIRCLayout: boolean;
|
||||
}
|
||||
|
||||
|
||||
export default class AppearanceUserSettingsTab extends React.Component<IProps, IState> {
|
||||
private readonly MESSAGE_PREVIEW_TEXT = _t("Hey you. You're the best!");
|
||||
|
||||
private themeTimer: NodeJS.Timeout;
|
||||
|
||||
|
@ -67,6 +76,10 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
customThemeUrl: "",
|
||||
customThemeMessage: {isError: false, text: ""},
|
||||
useCustomFontSize: SettingsStore.getValue("useCustomFontSize"),
|
||||
useSystemFont: SettingsStore.getValue("useSystemFont"),
|
||||
systemFont: SettingsStore.getValue("systemFont"),
|
||||
showAdvanced: false,
|
||||
useIRCLayout: SettingsStore.getValue("useIRCLayout"),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -159,7 +172,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");
|
||||
|
@ -197,11 +210,17 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
this.setState({customThemeUrl: e.target.value});
|
||||
};
|
||||
|
||||
private renderThemeSection() {
|
||||
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
|
||||
const StyledCheckbox = sdk.getComponent("views.elements.StyledCheckbox");
|
||||
const StyledRadioButton = sdk.getComponent("views.elements.StyledRadioButton");
|
||||
private onLayoutChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const val = e.target.value === "true";
|
||||
|
||||
this.setState({
|
||||
useIRCLayout: val,
|
||||
});
|
||||
|
||||
SettingsStore.setValue("useIRCLayout", null, SettingLevel.DEVICE, val);
|
||||
};
|
||||
|
||||
private renderThemeSection() {
|
||||
const themeWatcher = new ThemeWatcher();
|
||||
let systemThemeSection: JSX.Element;
|
||||
if (themeWatcher.isSystemThemeSupported()) {
|
||||
|
@ -263,7 +282,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
return <StyledRadioButton
|
||||
key={theme.id}
|
||||
value={theme.id}
|
||||
name={"theme"}
|
||||
name="theme"
|
||||
disabled={this.state.useSystemTheme}
|
||||
checked={!this.state.useSystemTheme && theme.id === this.state.theme}
|
||||
className={"mx_ThemeSelector_" + theme.id}
|
||||
|
@ -279,9 +298,14 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
}
|
||||
|
||||
private renderFontSection() {
|
||||
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
|
||||
return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_fontScaling">
|
||||
|
||||
<span className="mx_SettingsTab_subheading">{_t("Font size")}</span>
|
||||
<EventTilePreview
|
||||
className="mx_AppearanceUserSettingsTab_fontSlider_preview"
|
||||
message={this.MESSAGE_PREVIEW_TEXT}
|
||||
useIRCLayout={this.state.useIRCLayout}
|
||||
/>
|
||||
<div className="mx_AppearanceUserSettingsTab_fontSlider">
|
||||
<div className="mx_AppearanceUserSettingsTab_fontSlider_smallText">Aa</div>
|
||||
<Slider
|
||||
|
@ -293,12 +317,14 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
/>
|
||||
<div className="mx_AppearanceUserSettingsTab_fontSlider_largeText">Aa</div>
|
||||
</div>
|
||||
|
||||
<SettingsFlag
|
||||
name="useCustomFontSize"
|
||||
level={SettingLevel.ACCOUNT}
|
||||
onChange={(checked) => this.setState({useCustomFontSize: checked})}
|
||||
useCheckbox={true}
|
||||
/>
|
||||
|
||||
<Field
|
||||
type="number"
|
||||
label={_t("Font size")}
|
||||
|
@ -314,6 +340,87 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
</div>;
|
||||
}
|
||||
|
||||
private renderLayoutSection = () => {
|
||||
return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_Layout">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Message layout")}</span>
|
||||
|
||||
<div className="mx_AppearanceUserSettingsTab_Layout_RadioButtons" >
|
||||
<div className="mx_AppearanceUserSettingsTab_Layout_RadioButton">
|
||||
<EventTilePreview
|
||||
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
|
||||
message={this.MESSAGE_PREVIEW_TEXT}
|
||||
useIRCLayout={true}
|
||||
/>
|
||||
<StyledRadioButton
|
||||
name="layout"
|
||||
value="true"
|
||||
checked={this.state.useIRCLayout}
|
||||
onChange={this.onLayoutChange}
|
||||
>
|
||||
{_t("Compact")}
|
||||
</StyledRadioButton>
|
||||
</div>
|
||||
<div className="mx_AppearanceUserSettingsTab_spacer" />
|
||||
<div className="mx_AppearanceUserSettingsTab_Layout_RadioButton">
|
||||
<EventTilePreview
|
||||
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
|
||||
message={this.MESSAGE_PREVIEW_TEXT}
|
||||
useIRCLayout={false}
|
||||
/>
|
||||
<StyledRadioButton
|
||||
name="layout"
|
||||
value="false"
|
||||
checked={!this.state.useIRCLayout}
|
||||
onChange={this.onLayoutChange}
|
||||
>
|
||||
{_t("Modern")}
|
||||
</StyledRadioButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
private renderAdvancedSection() {
|
||||
const toggle = <div
|
||||
className="mx_AppearanceUserSettingsTab_AdvancedToggle"
|
||||
onClick={() => this.setState({showAdvanced: !this.state.showAdvanced})}
|
||||
>
|
||||
{this.state.showAdvanced ? "Hide advanced" : "Show advanced"}
|
||||
</div>;
|
||||
|
||||
let advanced: React.ReactNode;
|
||||
|
||||
if (this.state.showAdvanced) {
|
||||
advanced = <div>
|
||||
<SettingsFlag
|
||||
name="useSystemFont"
|
||||
level={SettingLevel.DEVICE}
|
||||
useCheckbox={true}
|
||||
onChange={(checked) => this.setState({useSystemFont: checked})}
|
||||
/>
|
||||
<Field
|
||||
className="mx_AppearanceUserSettingsTab_systemFont"
|
||||
label={SettingsStore.getDisplayName("systemFont")}
|
||||
onChange={(value) => {
|
||||
this.setState({
|
||||
systemFont: value.target.value,
|
||||
});
|
||||
|
||||
SettingsStore.setValue("systemFont", null, SettingLevel.DEVICE, value.target.value);
|
||||
}}
|
||||
tooltipContent="Set the name of a font installed on your system & Riot will attempt to use it."
|
||||
forceTooltipVisible={true}
|
||||
disabled={!this.state.useSystemFont}
|
||||
value={this.state.systemFont}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_Advanced">
|
||||
{toggle}
|
||||
{advanced}
|
||||
</div>;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_AppearanceUserSettingsTab">
|
||||
|
@ -323,6 +430,8 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
</div>
|
||||
{this.renderThemeSection()}
|
||||
{SettingsStore.isFeatureEnabled("feature_font_scaling") ? this.renderFontSection() : null}
|
||||
{SettingsStore.isFeatureEnabled("feature_irc_ui") ? this.renderLayoutSection() : null}
|
||||
{this.renderAdvancedSection()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -18,6 +18,7 @@ import React from "react";
|
|||
import PropTypes from "prop-types";
|
||||
import {_t, pickBestLanguage} from "../../../languageHandler";
|
||||
import * as sdk from "../../..";
|
||||
import {objectClone} from "../../../utils/objects";
|
||||
|
||||
export default class InlineTermsAgreement extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -56,7 +57,7 @@ export default class InlineTermsAgreement extends React.Component {
|
|||
}
|
||||
|
||||
_togglePolicy = (index) => {
|
||||
const policies = JSON.parse(JSON.stringify(this.state.policies)); // deep & cheap clone
|
||||
const policies = objectClone(this.state.policies);
|
||||
policies[index].checked = !policies[index].checked;
|
||||
this.setState({policies});
|
||||
};
|
||||
|
|
|
@ -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