Merge remote-tracking branch 'origin' into joriks/eslint-config

This commit is contained in:
Jorik Schellekens 2020-06-29 16:49:33 +01:00
commit b110639c76
103 changed files with 3118 additions and 1035 deletions

View file

@ -18,9 +18,7 @@ import React from 'react';
import * as sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
import AuthPage from "./AuthPage";
import * as Matrix from "matrix-js-sdk";
import {_td} from "../../../languageHandler";
import PlatformPeg from "../../../PlatformPeg";
// translatable strings for Welcome pages
_td("Sign in with SSO");
@ -39,15 +37,6 @@ export default class Welcome extends React.PureComponent {
pageUrl = 'welcome.html';
}
const {hsUrl, isUrl} = this.props.serverConfig;
const tmpClient = Matrix.createClient({
baseUrl: hsUrl,
idBaseUrl: isUrl,
});
const plaf = PlatformPeg.get();
const callbackUrl = plaf.getSSOCallbackUrl(tmpClient.getHomeserverUrl(), tmpClient.getIdentityServerUrl(),
this.props.fragmentAfterLogin);
return (
<AuthPage>
<div className="mx_Welcome">
@ -55,8 +44,8 @@ export default class Welcome extends React.PureComponent {
className="mx_WelcomePage"
url={pageUrl}
replaceMap={{
"$riot:ssoUrl": tmpClient.getSsoLoginUrl(callbackUrl.toString(), "sso"),
"$riot:casUrl": tmpClient.getSsoLoginUrl(callbackUrl.toString(), "cas"),
"$riot:ssoUrl": "#/start_sso",
"$riot:casUrl": "#/start_cas",
}}
/>
<LanguageSelector />

View file

@ -29,7 +29,7 @@ import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import AppPermission from './AppPermission';
import AppWarning from './AppWarning';
import MessageSpinner from './MessageSpinner';
import Spinner from './Spinner';
import WidgetUtils from '../../../utils/WidgetUtils';
import dis from '../../../dispatcher/dispatcher';
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
@ -741,7 +741,7 @@ export default class AppTile extends React.Component {
if (this.props.show) {
const loadingElement = (
<div className="mx_AppLoading_spinner_fadeIn">
<MessageSpinner msg='Loading...' />
<Spinner message={_t("Loading...")} />
</div>
);
if (!this.state.hasPermissionToLoad) {

View file

@ -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;
@ -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}
/>;
}

View file

@ -16,6 +16,8 @@ limitations under the License.
import React from "react";
import createReactClass from 'create-react-class';
import {_t} from "../../../languageHandler";
import SettingsStore from "../../../settings/SettingsStore";
export default createReactClass({
displayName: 'InlineSpinner',
@ -25,9 +27,25 @@ export default createReactClass({
const h = this.props.h || 16;
const imgClass = this.props.imgClassName || "";
let divClass;
let imageSource;
if (SettingsStore.isFeatureEnabled('feature_new_spinner')) {
divClass = "mx_InlineSpinner mx_Spinner_spin";
imageSource = require("../../../../res/img/spinner.svg");
} else {
divClass = "mx_InlineSpinner";
imageSource = require("../../../../res/img/spinner.gif");
}
return (
<div className="mx_InlineSpinner">
<img src={require("../../../../res/img/spinner.gif")} width={w} height={h} className={imgClass} />
<div className={divClass}>
<img
src={imageSource}
width={w}
height={h}
className={imgClass}
aria-label={_t("Loading...")}
/>
</div>
);
},

View file

@ -1,35 +0,0 @@
/*
Copyright 2017 Vector Creations Ltd
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 createReactClass from 'create-react-class';
export default createReactClass({
displayName: 'MessageSpinner',
render: function() {
const w = this.props.w || 32;
const h = this.props.h || 32;
const imgClass = this.props.imgClassName || "";
const msg = this.props.msg || "Loading...";
return (
<div className="mx_Spinner">
<div className="mx_Spinner_Msg">{ msg }</div>&nbsp;
<img src={require("../../../../res/img/spinner.gif")} width={w} height={h} className={imgClass} />
</div>
);
},
});

View file

@ -30,6 +30,7 @@ interface IProps {
isExplicit?: boolean;
// XXX: once design replaces all toggles make this the default
useCheckbox?: boolean;
disabled?: boolean;
onChange?(checked: boolean): void;
}
@ -78,14 +79,23 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
else label = _t(label);
if (this.props.useCheckbox) {
return <StyledCheckbox checked={this.state.value} onChange={this.checkBoxOnChange} disabled={!canChange} >
return <StyledCheckbox
checked={this.state.value}
onChange={this.checkBoxOnChange}
disabled={this.props.disabled || !canChange}
>
{label}
</StyledCheckbox>;
} else {
return (
<div className="mx_SettingsFlag">
<span className="mx_SettingsFlag_label">{label}</span>
<ToggleSwitch checked={this.state.value} onChange={this.onChange} disabled={!canChange} aria-label={label} />
<ToggleSwitch
checked={this.state.value}
onChange={this.onChange}
disabled={this.props.disabled || !canChange}
aria-label={label}
/>
</div>
);
}

View file

@ -16,19 +16,39 @@ limitations under the License.
*/
import React from "react";
import createReactClass from 'create-react-class';
import PropTypes from "prop-types";
import {_t} from "../../../languageHandler";
import SettingsStore from "../../../settings/SettingsStore";
export default createReactClass({
displayName: 'Spinner',
const Spinner = ({w = 32, h = 32, imgClassName, message}) => {
let divClass;
let imageSource;
if (SettingsStore.isFeatureEnabled('feature_new_spinner')) {
divClass = "mx_Spinner mx_Spinner_spin";
imageSource = require("../../../../res/img/spinner.svg");
} else {
divClass = "mx_Spinner";
imageSource = require("../../../../res/img/spinner.gif");
}
render: function() {
const w = this.props.w || 32;
const h = this.props.h || 32;
const imgClass = this.props.imgClassName || "";
return (
<div className="mx_Spinner">
<img src={require("../../../../res/img/spinner.gif")} width={w} height={h} className={imgClass} />
</div>
);
},
});
return (
<div className={divClass}>
{ message && <React.Fragment><div className="mx_Spinner_Msg">{ message}</div>&nbsp;</React.Fragment> }
<img
src={imageSource}
width={w}
height={h}
className={imgClassName}
aria-label={_t("Loading...")}
/>
</div>
);
};
Spinner.propTypes = {
w: PropTypes.number,
h: PropTypes.number,
imgClassName: PropTypes.string,
message: PropTypes.node,
};
export default Spinner;

View file

@ -0,0 +1,61 @@
/*
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 StyledRadioButton from "./StyledRadioButton";
interface IDefinition<T extends string> {
value: T;
className?: string;
disabled?: boolean;
label: React.ReactChild;
description?: React.ReactChild;
}
interface IProps<T extends string> {
name: string;
className?: string;
definitions: IDefinition<T>[];
value?: T; // if not provided no options will be selected
onChange(newValue: T);
}
function StyledRadioGroup<T extends string>({name, definitions, value, className, onChange}: IProps<T>) {
const _onChange = e => {
onChange(e.target.value);
};
return <React.Fragment>
{definitions.map(d => <React.Fragment>
<StyledRadioButton
key={d.value}
className={classNames(className, d.className)}
onChange={_onChange}
checked={d.value === value}
name={name}
value={d.value}
disabled={d.disabled}
>
{d.label}
</StyledRadioButton>
{d.description}
</React.Fragment>)}
</React.Fragment>;
}
export default StyledRadioGroup;

View file

@ -22,6 +22,7 @@ import MFileBody from './MFileBody';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { decryptFile } from '../../../utils/DecryptFile';
import { _t } from '../../../languageHandler';
import InlineSpinner from '../elements/InlineSpinner';
export default class MAudioBody extends React.Component {
constructor(props) {
@ -94,7 +95,7 @@ export default class MAudioBody extends React.Component {
// Not sure how tall the audio player is so not sure how tall it should actually be.
return (
<span className="mx_MAudioBody">
<img src={require("../../../../res/img/spinner.gif")} alt={content.body} width="16" height="16" />
<InlineSpinner />
</span>
);
}

View file

@ -26,6 +26,7 @@ import { decryptFile } from '../../../utils/DecryptFile';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import InlineSpinner from '../elements/InlineSpinner';
export default class MImageBody extends React.Component {
static propTypes = {
@ -365,12 +366,7 @@ export default class MImageBody extends React.Component {
// e2e image hasn't been decrypted yet
if (content.file !== undefined && this.state.decryptedUrl === null) {
placeholder = <img
src={require("../../../../res/img/spinner.gif")}
alt={content.body}
width="32"
height="32"
/>;
placeholder = <InlineSpinner w={32} h={32} />;
} else if (!this.state.imgLoaded) {
// Deliberately, getSpinner is left unimplemented here, MStickerBody overides
placeholder = this.getPlaceholder();

View file

@ -23,6 +23,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { decryptFile } from '../../../utils/DecryptFile';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
import InlineSpinner from '../elements/InlineSpinner';
export default createReactClass({
displayName: 'MVideoBody',
@ -147,7 +148,7 @@ export default createReactClass({
return (
<span className="mx_MVideoBody">
<div className="mx_MImageBody_thumbnail mx_MImageBody_thumbnail_spinner">
<img src={require("../../../../res/img/spinner.gif")} alt={content.body} width="16" height="16" />
<InlineSpinner />
</div>
</span>
);

View file

@ -19,6 +19,8 @@ import {MatrixClient} from "matrix-js-sdk/src/client";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import { _t } from "../../../languageHandler";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {formatFullDate} from "../../../DateUtils";
import SettingsStore from "../../../settings/SettingsStore";
interface IProps {
mxEvent: MatrixEvent;
@ -36,8 +38,12 @@ const RedactedBody = React.forwardRef<any, IProps>(({mxEvent}, ref) => {
text = _t("Message deleted by %(name)s", { name: sender ? sender.name : redactedBecauseUserId });
}
const showTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps");
const fullDate = formatFullDate(new Date(unsigned.redacted_because.origin_server_ts), showTwelveHour);
const titleText = _t("Message deleted on %(date)s", { date: fullDate });
return (
<span className="mx_RedactedBody" ref={ref}>
<span className="mx_RedactedBody" ref={ref} title={titleText}>
{ text }
</span>
);

View file

@ -28,6 +28,7 @@ export const E2E_STATE = {
WARNING: "warning",
UNKNOWN: "unknown",
NORMAL: "normal",
UNAUTHENTICATED: "unauthenticated",
};
const crossSigningUserTitles = {

View file

@ -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,

View file

@ -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,28 @@ export default class NotificationBadge extends React.PureComponent<IProps, IStat
}
}
export class RoomNotificationState extends EventEmitter implements IDestroyable {
export class StaticNotificationState extends EventEmitter implements INotificationState {
constructor(public symbol: string, public count: number, public color: NotificationColor) {
super();
}
public static forCount(count: number, color: NotificationColor): StaticNotificationState {
return new StaticNotificationState(null, count, color);
}
public static forSymbol(symbol: string, color: NotificationColor): StaticNotificationState {
return new StaticNotificationState(symbol, 0, color);
}
}
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 +186,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 +194,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 +259,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();
}
@ -246,7 +325,7 @@ export class ListNotificationState extends EventEmitter implements IDestroyable
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.
@ -259,6 +338,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();

View file

@ -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}

View file

@ -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 *
@ -56,6 +57,7 @@ interface IProps {
isInvite: boolean;
layout: ListLayout;
isMinimized: boolean;
tagId: TagID;
// TODO: Collapsed state
// TODO: Group invites
@ -68,6 +70,7 @@ interface IProps {
interface IState {
notificationState: ListNotificationState;
menuDisplayed: boolean;
isResizing: boolean;
}
export default class RoomSublist2 extends React.Component<IProps, IState> {
@ -78,8 +81,9 @@ 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,
isResizing: false,
};
this.state.notificationState.setRooms(this.props.rooms);
}
@ -109,13 +113,21 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
this.forceUpdate(); // because the layout doesn't trigger a re-render
};
private onResizeStart = () => {
this.setState({isResizing: true});
};
private onResizeStop = () => {
this.setState({isResizing: false});
};
private onShowAllClick = () => {
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.props.layout.visibleTiles = this.props.layout.defaultVisibleTiles;
this.forceUpdate(); // because the layout doesn't trigger a re-render
};
@ -130,13 +142,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 = () => {
@ -176,7 +188,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}
tag={this.props.tagId}
/>
);
}
@ -189,8 +201,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"
@ -204,14 +216,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>
@ -267,7 +279,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) {
@ -291,7 +303,18 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
'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}>
<div className='mx_RoomSublist2_stickable'>
@ -307,11 +330,11 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
<span>{this.props.label}</span>
</AccessibleButton>
{this.renderMenu()}
{addRoomButton}
<div className="mx_RoomSublist2_badgeContainer">
{badge}
</div>
{this.props.isMinimized ? null : badgeContainer}
{this.props.isMinimized ? null : addRoomButton}
</div>
{this.props.isMinimized ? badgeContainer : null}
{this.props.isMinimized ? addRoomButton : null}
</div>
);
}}
@ -343,6 +366,12 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
const nVisible = Math.floor(layout.visibleTiles);
const visibleTiles = tiles.slice(0, nVisible);
const maxTilesFactored = layout.tilesWithResizerBoxFactor(tiles.length);
const showMoreBtnClasses = classNames({
'mx_RoomSublist2_showNButton': true,
'mx_RoomSublist2_isCutting': this.state.isResizing && layout.visibleTiles < maxTilesFactored,
});
// 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. If the user has all
// tiles visible, it becomes 'show less'.
@ -357,14 +386,14 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
);
if (this.props.isMinimized) showMoreText = null;
showNButton = (
<div onClick={this.onShowAllClick} className='mx_RoomSublist2_showNButton'>
<div onClick={this.onShowAllClick} className={showMoreBtnClasses}>
<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) {
} else if (tiles.length <= nVisible && tiles.length > this.props.layout.defaultVisibleTiles) {
// we have all tiles visible - add a button to show less
let showLessText = (
<span className='mx_RoomSublist2_showNButtonText'>
@ -373,7 +402,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
);
if (this.props.isMinimized) showLessText = null;
showNButton = (
<div onClick={this.onShowLessClick} className='mx_RoomSublist2_showNButton'>
<div onClick={this.onShowLessClick} className={showMoreBtnClasses}>
<span className='mx_RoomSublist2_showLessButtonChevron mx_RoomSublist2_showNButtonChevron'>
{/* set by CSS masking */}
</span>
@ -419,6 +448,8 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
resizeHandles={handles}
onResize={this.onResize}
className="mx_RoomSublist2_resizeBox"
onResizeStart={this.onResizeStart}
onResizeStop={this.onResizeStop}
>
{visibleTiles}
{showNButton}

View file

@ -26,11 +26,15 @@ 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 { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
import RoomTileIcon from "./RoomTileIcon";
/*******************************************************************
@ -79,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,
};
@ -131,11 +135,8 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
ev.preventDefault();
ev.stopPropagation();
if (tagId === DefaultTagID.DM) {
// TODO: DM Flagging
} else {
// TODO: XOR favourites and low priority
}
// TODO: Support tagging: https://github.com/vector-im/riot-web/issues/14211
// TODO: XOR favourites and low priority: https://github.com/vector-im/riot-web/issues/14210
};
private onLeaveRoomClick = (ev: ButtonEvent) => {
@ -192,12 +193,6 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
<span>{_t("Low Priority")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={(e) => this.onTagRoom(e, DefaultTagID.DM)}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconUser" />
<span>{_t("Direct Chat")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={this.onOpenRoomSettings}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSettings" />
@ -248,7 +243,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;
@ -261,7 +262,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
let messagePreview = null;
if (this.props.showMessagePreview && !this.props.isMinimized) {
// The preview store heavily caches this info, so should be safe to hammer.
const text = MessagePreviewStore.instance.getPreviewForRoom(this.props.room);
const text = MessagePreviewStore.instance.getPreviewForRoom(this.props.room, this.props.tag);
// Only show the preview if there is one to show.
if (text) {

View file

@ -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}>

View file

@ -39,12 +39,11 @@ export default class NotificationsSettingsTab extends React.Component {
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount() { // eslint-disable-line camelcase
Notifier.getSoundForRoom(this.props.roomId).then((soundData) => {
if (!soundData) {
return;
}
this.setState({currentSound: soundData.name || soundData.url});
});
const soundData = Notifier.getSoundForRoom(this.props.roomId);
if (!soundData) {
return;
}
this.setState({currentSound: soundData.name || soundData.url});
this._soundUpload = createRef();
}

View file

@ -21,7 +21,6 @@ import {_t} from "../../../../../languageHandler";
import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore";
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";
@ -32,7 +31,9 @@ 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';
import StyledRadioGroup from "../../../elements/StyledRadioGroup";
interface IProps {
}
@ -55,6 +56,9 @@ interface IState extends IThemeState {
customThemeUrl: string;
customThemeMessage: CustomThemeMessage;
useCustomFontSize: boolean;
useSystemFont: boolean;
systemFont: string;
showAdvanced: boolean;
useIRCLayout: boolean;
}
@ -73,6 +77,9 @@ 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"),
};
}
@ -110,8 +117,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
};
}
private onThemeChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
const newTheme = e.target.value;
private onThemeChange = (newTheme: string): void => {
if (this.state.theme === newTheme) return;
// doing getValue in the .catch will still return the value we failed to set,
@ -271,19 +277,18 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
<div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_themeSection">
<span className="mx_SettingsTab_subheading">{_t("Theme")}</span>
{systemThemeSection}
<div className="mx_ThemeSelectors" onChange={this.onThemeChange}>
{orderedThemes.map(theme => {
return <StyledRadioButton
key={theme.id}
value={theme.id}
name="theme"
disabled={this.state.useSystemTheme}
checked={!this.state.useSystemTheme && theme.id === this.state.theme}
className={"mx_ThemeSelector_" + theme.id}
>
{theme.name}
</StyledRadioButton>;
})}
<div className="mx_ThemeSelectors">
<StyledRadioGroup
name="theme"
definitions={orderedThemes.map(t => ({
value: t.id,
label: t.name,
disabled: this.state.useSystemTheme,
className: "mx_ThemeSelector_" + t.id,
}))}
onChange={this.onThemeChange}
value={this.state.useSystemTheme ? undefined : this.state.theme}
/>
</div>
{customThemeForm}
<SettingsFlag name="useCompactLayout" level={SettingLevel.ACCOUNT} useCheckbox={true} />
@ -374,6 +379,53 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
</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 = <>
<SettingsFlag
name="useCompactLayout"
level={SettingLevel.DEVICE}
useCheckbox={true}
disabled={this.state.useIRCLayout}
/>
<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}
/>
</>;
}
return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_Advanced">
{toggle}
{advanced}
</div>;
}
render() {
return (
<div className="mx_SettingsTab mx_AppearanceUserSettingsTab">
@ -384,6 +436,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
{this.renderThemeSection()}
{SettingsStore.isFeatureEnabled("feature_font_scaling") ? this.renderFontSection() : null}
{SettingsStore.isFeatureEnabled("feature_irc_ui") ? this.renderLayoutSection() : null}
{this.renderAdvancedSection()}
</div>
);
}

View file

@ -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});
};