Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into joriks/style-fighting

This commit is contained in:
Jorik Schellekens 2020-08-04 15:04:56 +01:00
commit 271eeeabee
315 changed files with 8146 additions and 2874 deletions

View file

@ -210,6 +210,11 @@ const FilePanel = createReactClass({
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
const Loader = sdk.getComponent("elements.Spinner");
const emptyState = (<div className="mx_RightPanel_empty mx_FilePanel_empty">
<h2>{_t('No files visible in this room')}</h2>
<p>{_t('Attach files from chat or just drag and drop them anywhere in a room.')}</p>
</div>);
if (this.state.timelineSet) {
// console.log("rendering TimelinePanel for timelineSet " + this.state.timelineSet.room.roomId + " " +
// "(" + this.state.timelineSet._timelines.join(", ") + ")" + " with key " + this.props.roomId);
@ -223,7 +228,7 @@ const FilePanel = createReactClass({
onPaginationRequest={this.onPaginationRequest}
tileShape="file_grid"
resizeNotifier={this.props.resizeNotifier}
empty={_t('There are no visible files in this room')}
empty={emptyState}
/>
</div>
);

View file

@ -44,7 +44,6 @@ interface IProps {
}
interface IState {
searchFilter: string;
showBreadcrumbs: boolean;
showTagPanel: boolean;
}
@ -69,7 +68,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
super(props);
this.state = {
searchFilter: "",
showBreadcrumbs: BreadcrumbsStore.instance.visible,
showTagPanel: SettingsStore.getValue('TagPanel.enableTagPanel'),
};
@ -97,10 +95,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize);
}
private onSearch = (term: string): void => {
this.setState({searchFilter: term});
};
private onExplore = () => {
dis.fire(Action.ViewRoomDirectory);
};
@ -366,7 +360,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
onKeyDown={this.onKeyDown}
>
<RoomSearch
onQueryUpdate={this.onSearch}
isMinimized={this.props.isMinimized}
onVerticalArrow={this.onKeyDown}
onEnter={this.onEnter}
@ -392,7 +385,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
onKeyDown={this.onKeyDown}
resizeNotifier={null}
collapsed={false}
searchFilter={this.state.searchFilter}
onFocus={this.onFocus}
onBlur={this.onBlur}
isMinimized={this.props.isMinimized}

View file

@ -54,6 +54,8 @@ import LeftPanel from "./LeftPanel";
import CallContainer from '../views/voip/CallContainer';
import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload";
import RoomListStore from "../../stores/room-list/RoomListStore";
import NonUrgentToastContainer from "./NonUrgentToastContainer";
import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload";
// We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity.
@ -472,8 +474,8 @@ class LoggedInView extends React.Component<IProps, IState> {
case Key.PERIOD:
if (ctrlCmdOnly && (this.props.page_type === "room_view" || this.props.page_type === "group_view")) {
dis.dispatch({
action: 'toggle_right_panel',
dis.dispatch<ToggleRightPanelPayload>({
action: Action.ToggleRightPanel,
type: this.props.page_type === "room_view" ? "room" : "group",
});
handled = true;
@ -687,6 +689,7 @@ class LoggedInView extends React.Component<IProps, IState> {
</DragDropContext>
</div>
<CallContainer />
<NonUrgentToastContainer />
</MatrixClientContext.Provider>
);
}

View file

@ -16,77 +16,24 @@ limitations under the License.
*/
import React from 'react';
import ResizeHandle from '../views/elements/ResizeHandle';
import {Resizer, FixedDistributor} from '../../resizer';
import { Resizable } from 're-resizable';
export default class MainSplit extends React.Component {
constructor(props) {
super(props);
this._setResizeContainerRef = this._setResizeContainerRef.bind(this);
this._onResized = this._onResized.bind(this);
_onResized = (event, direction, refToElement, delta) => {
window.localStorage.setItem("mx_rhs_size", this._loadSidePanelSize().width + delta.width);
}
_onResized(size) {
window.localStorage.setItem("mx_rhs_size", size);
if (this.props.resizeNotifier) {
this.props.resizeNotifier.notifyRightHandleResized();
}
}
_loadSidePanelSize() {
let rhsSize = parseInt(window.localStorage.getItem("mx_rhs_size"), 10);
_createResizer() {
const classNames = {
handle: "mx_ResizeHandle",
vertical: "mx_ResizeHandle_vertical",
reverse: "mx_ResizeHandle_reverse",
};
const resizer = new Resizer(
this.resizeContainer,
FixedDistributor,
{onResized: this._onResized},
);
resizer.setClassNames(classNames);
let rhsSize = window.localStorage.getItem("mx_rhs_size");
if (rhsSize !== null) {
rhsSize = parseInt(rhsSize, 10);
} else {
if (isNaN(rhsSize)) {
rhsSize = 350;
}
resizer.forHandleAt(0).resize(rhsSize);
resizer.attach();
this.resizer = resizer;
}
_setResizeContainerRef(div) {
this.resizeContainer = div;
}
componentDidMount() {
if (this.props.panel) {
this._createResizer();
}
}
componentWillUnmount() {
if (this.resizer) {
this.resizer.detach();
this.resizer = null;
}
}
componentDidUpdate(prevProps) {
const wasPanelSet = this.props.panel && !prevProps.panel;
const wasPanelCleared = !this.props.panel && prevProps.panel;
if (this.resizeContainer && wasPanelSet) {
// The resizer can only be created when **both** expanded and the panel is
// set. Once both are true, the container ref will mount, which is required
// for the resizer to work.
this._createResizer();
} else if (this.resizer && wasPanelCleared) {
this.resizer.detach();
this.resizer = null;
}
return {
height: "100%",
width: rhsSize,
};
}
render() {
@ -97,13 +44,29 @@ export default class MainSplit extends React.Component {
let children;
if (hasResizer) {
children = <React.Fragment>
<ResizeHandle reverse={true} />
children = <Resizable
defaultSize={this._loadSidePanelSize()}
minWidth={264}
maxWidth="50%"
enable={{
top: false,
right: false,
bottom: false,
left: true,
topRight: false,
bottomRight: false,
bottomLeft: false,
topLeft: false,
}}
onResizeStop={this._onResized}
className="mx_RightPanel_ResizeWrapper"
handleClasses={{left: "mx_RightPanel_ResizeHandle"}}
>
{ panelView }
</React.Fragment>;
</Resizable>;
}
return <div className="mx_MainSplit" ref={hasResizer ? this._setResizeContainerRef : undefined}>
return <div className="mx_MainSplit">
{ bodyView }
{ children }
</div>;

View file

@ -51,7 +51,7 @@ import { getHomePageUrl } from '../../utils/pages';
import createRoom from "../../createRoom";
import {_t, _td, getCurrentLanguage} from '../../languageHandler';
import SettingsStore, { SettingLevel } from "../../settings/SettingsStore";
import SettingsStore from "../../settings/SettingsStore";
import ThemeController from "../../settings/controllers/ThemeController";
import { startAnyRegistrationFlow } from "../../Registration.js";
import { messageForSyncError } from '../../utils/ErrorUtils';
@ -75,6 +75,7 @@ import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificat
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
import ErrorDialog from "../views/dialogs/ErrorDialog";
import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
import { SettingLevel } from "../../settings/SettingLevel";
/** constants for MatrixChat.state.view */
export enum Views {

View file

@ -346,9 +346,9 @@ export default class MessagePanel extends React.Component {
}
}
_isUnmounting() {
_isUnmounting = () => {
return !this._isMounted;
}
};
// TODO: Implement granular (per-room) hide options
_shouldShowEvent(mxEv) {
@ -571,12 +571,10 @@ export default class MessagePanel extends React.Component {
const readReceipts = this._readReceiptsByEvent[eventId];
// Dev note: `this._isUnmounting.bind(this)` is important - it ensures that
// the function is run in the context of this class and not EventTile, therefore
// ensuring the right `this._mounted` variable is used by read receipts (which
// don't update their position if we, the MessagePanel, is unmounting).
// use txnId as key if available so that we don't remount during sending
ret.push(
<li key={eventId}
<li
key={mxEv.getTxnId() || eventId}
ref={this._collectEventNode.bind(this, eventId)}
data-scroll-tokens={scrollToken}
>
@ -590,7 +588,7 @@ export default class MessagePanel extends React.Component {
readReceipts={readReceipts}
readReceiptMap={this._readReceiptMap}
showUrlPreview={this.props.showUrlPreview}
checkUnmounting={this._isUnmounting.bind(this)}
checkUnmounting={this._isUnmounting}
eventSendStatus={mxEv.getAssociatedStatus()}
tileShape={this.props.tileShape}
isTwelveHour={this.props.isTwelveHour}

View file

@ -0,0 +1,63 @@
/*
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 * as React from "react";
import { ComponentClass } from "../../@types/common";
import NonUrgentToastStore from "../../stores/NonUrgentToastStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore";
interface IProps {
}
interface IState {
toasts: ComponentClass[],
}
export default class NonUrgentToastContainer extends React.PureComponent<IProps, IState> {
public constructor(props, context) {
super(props, context);
this.state = {
toasts: NonUrgentToastStore.instance.components,
};
NonUrgentToastStore.instance.on(UPDATE_EVENT, this.onUpdateToasts);
}
public componentWillUnmount() {
NonUrgentToastStore.instance.off(UPDATE_EVENT, this.onUpdateToasts);
}
private onUpdateToasts = () => {
this.setState({toasts: NonUrgentToastStore.instance.components});
};
public render() {
const toasts = this.state.toasts.map((t, i) => {
return (
<div className="mx_NonUrgentToastContainer_toast" key={`toast-${i}`}>
{React.createElement(t, {})}
</div>
);
});
return (
<div className="mx_NonUrgentToastContainer" role="alert">
{toasts}
</div>
);
}
}

View file

@ -36,6 +36,11 @@ const NotificationPanel = createReactClass({
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
const Loader = sdk.getComponent("elements.Spinner");
const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
<h2>{_t('Youre all caught up')}</h2>
<p>{_t('You have no visible notifications in this room.')}</p>
</div>);
const timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
if (timelineSet) {
return (
@ -46,7 +51,7 @@ const NotificationPanel = createReactClass({
timelineSet={timelineSet}
showUrlPreview={false}
tileShape="notif"
empty={_t('You have no visible notifications')}
empty={emptyState}
/>
</div>
);

View file

@ -26,7 +26,7 @@ import dis from '../../dispatcher/dispatcher';
import RateLimitedFunc from '../../ratelimitedfunc';
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
import GroupStore from '../../stores/GroupStore';
import {RIGHT_PANEL_PHASES, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPanelStorePhases";
import {RightPanelPhases, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPanelStorePhases";
import RightPanelStore from "../../stores/RightPanelStore";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import {Action} from "../../dispatcher/actions";
@ -75,8 +75,8 @@ export default class RightPanel extends React.Component {
const userForPanel = this._getUserForPanel();
if (this.props.groupId) {
if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.groupPanelPhase)) {
dis.dispatch({action: "set_right_panel_phase", phase: RIGHT_PANEL_PHASES.GroupMemberList});
return RIGHT_PANEL_PHASES.GroupMemberList;
dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.GroupMemberList});
return RightPanelPhases.GroupMemberList;
}
return rps.groupPanelPhase;
} else if (userForPanel) {
@ -98,11 +98,11 @@ export default class RightPanel extends React.Component {
) {
return rps.roomPanelPhase;
}
return RIGHT_PANEL_PHASES.RoomMemberInfo;
return RightPanelPhases.RoomMemberInfo;
} else {
if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.roomPanelPhase)) {
dis.dispatch({action: "set_right_panel_phase", phase: RIGHT_PANEL_PHASES.RoomMemberList});
return RIGHT_PANEL_PHASES.RoomMemberList;
dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.RoomMemberList});
return RightPanelPhases.RoomMemberList;
}
return rps.roomPanelPhase;
}
@ -149,7 +149,7 @@ export default class RightPanel extends React.Component {
onInviteToGroupButtonClick() {
showGroupInviteDialog(this.props.groupId).then(() => {
this.setState({
phase: RIGHT_PANEL_PHASES.GroupMemberList,
phase: RightPanelPhases.GroupMemberList,
});
});
}
@ -165,9 +165,9 @@ export default class RightPanel extends React.Component {
return;
}
// redraw the badge on the membership list
if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberList && member.roomId === this.props.roomId) {
if (this.state.phase === RightPanelPhases.RoomMemberList && member.roomId === this.props.roomId) {
this._delayedUpdate();
} else if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberInfo && member.roomId === this.props.roomId &&
} else if (this.state.phase === RightPanelPhases.RoomMemberInfo && member.roomId === this.props.roomId &&
member.userId === this.state.member.userId) {
// refresh the member info (e.g. new power level)
this._delayedUpdate();
@ -175,7 +175,7 @@ export default class RightPanel extends React.Component {
}
onAction(payload) {
if (payload.action === "after_right_panel_phase_change") {
if (payload.action === Action.AfterRightPanelPhaseChange) {
this.setState({
phase: payload.phase,
groupRoomId: payload.groupRoomId,
@ -206,7 +206,7 @@ export default class RightPanel extends React.Component {
// or the member list if we were in the member panel... phew.
dis.dispatch({
action: Action.ViewUser,
member: this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel ? this.state.member : null,
member: this.state.phase === RightPanelPhases.EncryptionPanel ? this.state.member : null,
});
}
};
@ -225,21 +225,21 @@ export default class RightPanel extends React.Component {
let panel = <div />;
switch (this.state.phase) {
case RIGHT_PANEL_PHASES.RoomMemberList:
case RightPanelPhases.RoomMemberList:
if (this.props.roomId) {
panel = <MemberList roomId={this.props.roomId} key={this.props.roomId} />;
}
break;
case RIGHT_PANEL_PHASES.GroupMemberList:
case RightPanelPhases.GroupMemberList:
if (this.props.groupId) {
panel = <GroupMemberList groupId={this.props.groupId} key={this.props.groupId} />;
}
break;
case RIGHT_PANEL_PHASES.GroupRoomList:
case RightPanelPhases.GroupRoomList:
panel = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />;
break;
case RIGHT_PANEL_PHASES.RoomMemberInfo:
case RIGHT_PANEL_PHASES.EncryptionPanel:
case RightPanelPhases.RoomMemberInfo:
case RightPanelPhases.EncryptionPanel:
panel = <UserInfo
user={this.state.member}
roomId={this.props.roomId}
@ -250,26 +250,26 @@ export default class RightPanel extends React.Component {
verificationRequestPromise={this.state.verificationRequestPromise}
/>;
break;
case RIGHT_PANEL_PHASES.Room3pidMemberInfo:
case RightPanelPhases.Room3pidMemberInfo:
panel = <ThirdPartyMemberInfo event={this.state.event} key={this.props.roomId} />;
break;
case RIGHT_PANEL_PHASES.GroupMemberInfo:
case RightPanelPhases.GroupMemberInfo:
panel = <UserInfo
user={this.state.member}
groupId={this.props.groupId}
key={this.state.member.userId}
onClose={this.onCloseUserInfo} />;
break;
case RIGHT_PANEL_PHASES.GroupRoomInfo:
case RightPanelPhases.GroupRoomInfo:
panel = <GroupRoomInfo
groupRoomId={this.state.groupRoomId}
groupId={this.props.groupId}
key={this.state.groupRoomId} />;
break;
case RIGHT_PANEL_PHASES.NotificationPanel:
case RightPanelPhases.NotificationPanel:
panel = <NotificationPanel />;
break;
case RIGHT_PANEL_PHASES.FilePanel:
case RightPanelPhases.FilePanel:
panel = <FilePanel roomId={this.props.roomId} resizeNotifier={this.props.resizeNotifier} />;
break;
}

View file

@ -24,9 +24,10 @@ import { throttle } from 'lodash';
import { Key } from "../../Keyboard";
import AccessibleButton from "../views/elements/AccessibleButton";
import { Action } from "../../dispatcher/actions";
import RoomListStore from "../../stores/room-list/RoomListStore";
import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition";
interface IProps {
onQueryUpdate: (newQuery: string) => void;
isMinimized: boolean;
onVerticalArrow(ev: React.KeyboardEvent): void;
onEnter(ev: React.KeyboardEvent): boolean;
@ -40,6 +41,7 @@ interface IState {
export default class RoomSearch extends React.PureComponent<IProps, IState> {
private dispatcherRef: string;
private inputRef: React.RefObject<HTMLInputElement> = createRef();
private searchFilter: NameFilterCondition = new NameFilterCondition();
constructor(props: IProps) {
super(props);
@ -52,6 +54,21 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
this.dispatcherRef = defaultDispatcher.register(this.onAction);
}
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>): void {
if (prevState.query !== this.state.query) {
const hadSearch = !!this.searchFilter.search.trim();
const haveSearch = !!this.state.query.trim();
this.searchFilter.search = this.state.query;
if (!hadSearch && haveSearch) {
// started a new filter - add the condition
RoomListStore.instance.addFilter(this.searchFilter);
} else if (hadSearch && !haveSearch) {
// cleared a filter - remove the condition
RoomListStore.instance.removeFilter(this.searchFilter);
} // else the filter hasn't changed enough for us to care here
}
}
public componentWillUnmount() {
defaultDispatcher.unregister(this.dispatcherRef);
}
@ -78,19 +95,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
private onChange = () => {
if (!this.inputRef.current) return;
this.setState({query: this.inputRef.current.value});
this.onSearchUpdated();
};
// it wants this at the top of the file, but we know better
// tslint:disable-next-line
private onSearchUpdated = throttle(
() => {
// We can't use the state variable because it can lag behind the input.
// The lag is most obvious when deleting/clearing text with the keyboard.
this.props.onQueryUpdate(this.inputRef.current.value);
}, 200, {trailing: true, leading: true},
);
private onFocus = (ev: React.FocusEvent<HTMLInputElement>) => {
this.setState({focused: true});
ev.target.select();

View file

@ -48,7 +48,7 @@ import RightPanel from './RightPanel';
import RoomViewStore from '../../stores/RoomViewStore';
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
import WidgetEchoStore from '../../stores/WidgetEchoStore';
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
import SettingsStore from "../../settings/SettingsStore";
import AccessibleButton from "../views/elements/AccessibleButton";
import RightPanelStore from "../../stores/RightPanelStore";
import {haveTileForEvent} from "../views/rooms/EventTile";
@ -56,6 +56,7 @@ import RoomContext from "../../contexts/RoomContext";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import { shieldStatusForRoom } from '../../utils/ShieldUtils';
import {Action} from "../../dispatcher/actions";
import {SettingLevel} from "../../settings/SettingLevel";
const DEBUG = false;
let debuglog = function() {};

View file

@ -26,7 +26,7 @@ import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
import RedesignFeedbackDialog from "../views/dialogs/RedesignFeedbackDialog";
import Modal from "../../Modal";
import LogoutDialog from "../views/dialogs/LogoutDialog";
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
import SettingsStore from "../../settings/SettingsStore";
import {getCustomTheme} from "../../theme";
import {getHostingLink} from "../../utils/HostingLink";
import {ButtonEvent} from "../views/elements/AccessibleButton";
@ -37,6 +37,7 @@ import { UPDATE_EVENT } from "../../stores/AsyncStore";
import BaseAvatar from '../views/avatars/BaseAvatar';
import classNames from "classnames";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import { SettingLevel } from "../../settings/SettingLevel";
interface IProps {
isMinimized: boolean;

View file

@ -72,7 +72,7 @@ export default class SoftLogout extends React.Component {
this._initLogin();
MatrixClientPeg.get().flagAllGroupSessionsForBackup().then(remaining => {
MatrixClientPeg.get().countSessionsNeedingBackup().then(remaining => {
this.setState({keyBackupNeeded: remaining > 0});
});
}

View file

@ -16,10 +16,11 @@ limitations under the License.
import SdkConfig from "../../../SdkConfig";
import {getCurrentLanguage} from "../../../languageHandler";
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import SettingsStore from "../../../settings/SettingsStore";
import PlatformPeg from "../../../PlatformPeg";
import * as sdk from '../../../index';
import React from 'react';
import {SettingLevel} from "../../../settings/SettingLevel";
function onChange(newLang) {
if (getCurrentLanguage() !== newLang) {

View file

@ -19,8 +19,8 @@ import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {SettingLevel} from "../../../settings/SettingsStore";
import SettingsStore from "../../../settings/SettingsStore";
import {SettingLevel} from "../../../settings/SettingLevel";
export default createReactClass({
propTypes: {

View file

@ -349,7 +349,7 @@ export default class InviteDialog extends React.PureComponent {
// Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the
// room list doesn't tag the room for the DMRoomMap, but does for the room list.
const dmTaggedRooms = RoomListStore.instance.orderedLists[DefaultTagID.DM];
const dmTaggedRooms = RoomListStore.instance.orderedLists[DefaultTagID.DM] || [];
const myUserId = MatrixClientPeg.get().getUserId();
for (const dmRoom of dmTaggedRooms) {
const otherMembers = dmRoom.getJoinedMembers().filter(u => u.userId !== myUserId);

View file

@ -0,0 +1,124 @@
/*
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 * as React from 'react';
import BaseDialog from './BaseDialog';
import { _t } from '../../../languageHandler';
import { EchoStore } from "../../../stores/local-echo/EchoStore";
import { formatTime } from "../../../DateUtils";
import SettingsStore from "../../../settings/SettingsStore";
import { RoomEchoContext } from "../../../stores/local-echo/RoomEchoContext";
import RoomAvatar from "../avatars/RoomAvatar";
import { TransactionStatus } from "../../../stores/local-echo/EchoTransaction";
import Spinner from "../elements/Spinner";
import AccessibleButton from "../elements/AccessibleButton";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
interface IProps {
onFinished: (bool) => void;
}
export default class ServerOfflineDialog extends React.PureComponent<IProps> {
public componentDidMount() {
EchoStore.instance.on(UPDATE_EVENT, this.onEchosUpdated);
}
public componentWillUnmount() {
EchoStore.instance.off(UPDATE_EVENT, this.onEchosUpdated);
}
private onEchosUpdated = () => {
this.forceUpdate(); // no state to worry about
};
private renderTimeline(): React.ReactElement[] {
return EchoStore.instance.contexts.map((c, i) => {
if (!c.firstFailedTime) return null; // not useful
if (!(c instanceof RoomEchoContext)) throw new Error("Cannot render unknown context: " + c);
const header = (
<div className="mx_ServerOfflineDialog_content_context_timeline_header">
<RoomAvatar width={24} height={24} room={c.room} />
<span>{c.room.name}</span>
</div>
);
const entries = c.transactions
.filter(t => t.status === TransactionStatus.Error || t.didPreviouslyFail)
.map((t, j) => {
let button = <Spinner w={19} h={19} />;
if (t.status === TransactionStatus.Error) {
button = (
<AccessibleButton kind="link" onClick={() => t.run()}>{_t("Resend")}</AccessibleButton>
);
}
return (
<div className="mx_ServerOfflineDialog_content_context_txn" key={`txn-${j}`}>
<span className="mx_ServerOfflineDialog_content_context_txn_desc">
{t.auditName}
</span>
{button}
</div>
);
});
return (
<div className="mx_ServerOfflineDialog_content_context" key={`context-${i}`}>
<div className="mx_ServerOfflineDialog_content_context_timestamp">
{formatTime(c.firstFailedTime, SettingsStore.getValue("showTwelveHourTimestamps"))}
</div>
<div className="mx_ServerOfflineDialog_content_context_timeline">
{header}
{entries}
</div>
</div>
)
});
}
public render() {
let timeline = this.renderTimeline().filter(c => !!c); // remove nulls for next check
if (timeline.length === 0) {
timeline = [<div key={1}>{_t("You're all caught up.")}</div>];
}
const serverName = MatrixClientPeg.getHomeserverName();
return <BaseDialog title={_t("Server isn't responding")}
className='mx_ServerOfflineDialog'
contentId='mx_Dialog_content'
onFinished={this.props.onFinished}
hasCancel={true}
>
<div className="mx_ServerOfflineDialog_content">
<p>{_t(
"Your server isn't responding to some of your requests. " +
"Below are some of the most likely reasons.",
)}</p>
<ul>
<li>{_t("The server (%(serverName)s) took too long to respond.", {serverName})}</li>
<li>{_t("Your firewall or anti-virus is blocking the request.")}</li>
<li>{_t("A browser extension is preventing the request.")}</li>
<li>{_t("The server is offline.")}</li>
<li>{_t("The server has denied your request.")}</li>
<li>{_t("Your area is experiencing difficulties connecting to the internet.")}</li>
<li>{_t("A connection error occurred while trying to contact the server.")}</li>
<li>{_t("The server is not configured to indicate what the problem is (CORS).")}</li>
</ul>
<hr />
<h2>{_t("Recent changes that have not yet been received")}</h2>
{timeline}
</div>
</BaseDialog>;
}
}

View file

@ -17,10 +17,11 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import SettingsStore from "../../../settings/SettingsStore";
import * as sdk from "../../../index";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import WidgetUtils from "../../../utils/WidgetUtils";
import {SettingLevel} from "../../../settings/SettingLevel";
export default class WidgetOpenIDPermissionsDialog extends React.Component {
static propTypes = {

View file

@ -35,12 +35,13 @@ import dis from '../../../dispatcher/dispatcher';
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
import classNames from 'classnames';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import SettingsStore from "../../../settings/SettingsStore";
import {aboveLeftOf, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
import PersistedElement from "./PersistedElement";
import {WidgetType} from "../../../widgets/WidgetType";
import {Capability} from "../../../widgets/WidgetApi";
import {sleep} from "../../../utils/promise";
import {SettingLevel} from "../../../settings/SettingLevel";
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
const ENABLE_REACT_PERF = false;

View file

@ -15,8 +15,9 @@ limitations under the License.
*/
import React from 'react';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import SettingsStore from "../../../settings/SettingsStore";
import Draggable, {ILocationState} from './Draggable';
import { SettingLevel } from "../../../settings/SettingLevel";
interface IProps {
// Current room

View file

@ -20,11 +20,12 @@ import SettingsStore from "../../../settings/SettingsStore";
import { _t } from '../../../languageHandler';
import ToggleSwitch from "./ToggleSwitch";
import StyledCheckbox from "./StyledCheckbox";
import { SettingLevel } from "../../../settings/SettingLevel";
interface IProps {
// The setting must be a boolean
name: string;
level: string;
level: SettingLevel;
roomId?: string; // for per-room settings
label?: string; // untranslated
isExplicit?: boolean;
@ -52,8 +53,8 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
};
}
private onChange = (checked: boolean): void => {
this.save(checked);
private onChange = async (checked: boolean) => {
await this.save(checked);
this.setState({ value: checked });
if (this.props.onChange) this.props.onChange(checked);
};
@ -62,8 +63,8 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
this.onChange(e.target.checked);
};
private save = (val?: boolean): void => {
return SettingsStore.setValue(
private save = async (val?: boolean) => {
await SettingsStore.setValue(
this.props.name,
this.props.roomId,
this.props.level,

View file

@ -47,11 +47,10 @@ export default class TextWithTooltip extends React.Component {
return (
<span onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className={this.props.class}>
{this.props.children}
<Tooltip
{this.state.hover && <Tooltip
label={this.props.tooltip}
visible={this.state.hover}
tooltipClassName={this.props.tooltipClass}
className={"mx_TextWithTooltip_tooltip"} />
className={"mx_TextWithTooltip_tooltip"} /> }
</span>
);
}

View file

@ -24,8 +24,9 @@ import GroupStore from '../../../stores/GroupStore';
import PropTypes from 'prop-types';
import { showGroupInviteDialog } from '../../../GroupAddressPicker';
import AccessibleButton from '../elements/AccessibleButton';
import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import {Action} from "../../../dispatcher/actions";
const INITIAL_LOAD_NUM_MEMBERS = 30;
@ -164,9 +165,9 @@ export default createReactClass({
onInviteToGroupButtonClick() {
showGroupInviteDialog(this.props.groupId).then(() => {
dis.dispatch({
action: 'set_right_panel_phase',
phase: RIGHT_PANEL_PHASES.GroupMemberList,
groupId: this.props.groupId,
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.GroupMemberList,
refireParams: { groupId: this.props.groupId },
});
});
},

View file

@ -22,7 +22,8 @@ import { _t } from '../../../languageHandler';
import {getNameForEventRoom, userLabelForEventRoom}
from '../../../utils/KeyVerificationStateObserver';
import dis from "../../../dispatcher/dispatcher";
import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import {Action} from "../../../dispatcher/actions";
export default class MKeyVerificationRequest extends React.Component {
constructor(props) {
@ -48,8 +49,8 @@ export default class MKeyVerificationRequest extends React.Component {
const {verificationRequest} = this.props.mxEvent;
const member = MatrixClientPeg.get().getUser(verificationRequest.otherUserId);
dis.dispatch({
action: "set_right_panel_phase",
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.EncryptionPanel,
refireParams: {verificationRequest, member},
});
};

View file

@ -15,10 +15,10 @@ limitations under the License.
*/
import React from "react";
import PropTypes from "prop-types";
import * as sdk from "../../../index";
import {_t} from "../../../languageHandler";
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
export const PendingActionSpinner = ({text}) => {
const Spinner = sdk.getComponent('elements.Spinner');
@ -28,7 +28,17 @@ export const PendingActionSpinner = ({text}) => {
</div>;
};
const EncryptionInfo = ({
interface IProps {
waitingForOtherParty: boolean;
waitingForNetwork: boolean;
member: RoomMember;
onStartVerification: () => Promise<void>;
isRoomEncrypted: boolean;
inDialog: boolean;
isSelfVerification: boolean;
}
const EncryptionInfo: React.FC<IProps> = ({
waitingForOtherParty,
waitingForNetwork,
member,
@ -36,10 +46,10 @@ const EncryptionInfo = ({
isRoomEncrypted,
inDialog,
isSelfVerification,
}) => {
let content;
}: IProps) => {
let content: JSX.Element;
if (waitingForOtherParty || waitingForNetwork) {
let text;
let text: string;
if (waitingForOtherParty) {
if (isSelfVerification) {
text = _t("Waiting for you to accept on your other session…");
@ -61,7 +71,7 @@ const EncryptionInfo = ({
);
}
let description;
let description: JSX.Element;
if (isRoomEncrypted) {
description = (
<div>
@ -97,10 +107,5 @@ const EncryptionInfo = ({
</div>
</React.Fragment>;
};
EncryptionInfo.propTypes = {
member: PropTypes.object.isRequired,
onStartVerification: PropTypes.func.isRequired,
request: PropTypes.object,
};
export default EncryptionInfo;

View file

@ -15,7 +15,6 @@ limitations under the License.
*/
import React, {useCallback, useEffect, useState} from "react";
import PropTypes from "prop-types";
import EncryptionInfo from "./EncryptionInfo";
import VerificationPanel from "./VerificationPanel";
@ -26,11 +25,23 @@ import Modal from "../../../Modal";
import {PHASE_REQUESTED, PHASE_UNSENT} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import * as sdk from "../../../index";
import {_t} from "../../../languageHandler";
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
// cancellation codes which constitute a key mismatch
const MISMATCHES = ["m.key_mismatch", "m.user_error", "m.mismatched_sas"];
const EncryptionPanel = (props) => {
interface IProps {
member: RoomMember;
onClose: () => void;
verificationRequest: VerificationRequest;
verificationRequestPromise: Promise<VerificationRequest>;
layout: string;
inDialog: boolean;
isRoomEncrypted: boolean;
}
const EncryptionPanel: React.FC<IProps> = (props: IProps) => {
const {verificationRequest, verificationRequestPromise, member, onClose, layout, isRoomEncrypted} = props;
const [request, setRequest] = useState(verificationRequest);
// state to show a spinner immediately after clicking "start verification",
@ -48,10 +59,10 @@ const EncryptionPanel = (props) => {
useEffect(() => {
async function awaitPromise() {
setRequesting(true);
const request = await verificationRequestPromise;
const requestFromPromise = await verificationRequestPromise;
setRequesting(false);
setRequest(request);
setPhase(request.phase);
setRequest(requestFromPromise);
setPhase(requestFromPromise.phase);
}
if (verificationRequestPromise) {
awaitPromise();
@ -90,7 +101,7 @@ const EncryptionPanel = (props) => {
}
}, [request]);
let cancelButton;
let cancelButton: JSX.Element;
if (layout !== "dialog" && request && request.pending) {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
cancelButton = (<AccessibleButton
@ -104,9 +115,9 @@ const EncryptionPanel = (props) => {
setRequesting(true);
const cli = MatrixClientPeg.get();
const roomId = await ensureDMExists(cli, member.userId);
const verificationRequest = await cli.requestVerificationDM(member.userId, roomId);
setRequest(verificationRequest);
setPhase(verificationRequest.phase);
const verificationRequest_ = await cli.requestVerificationDM(member.userId, roomId);
setRequest(verificationRequest_);
setPhase(verificationRequest_.phase);
}, [member.userId]);
const requested =
@ -144,12 +155,5 @@ const EncryptionPanel = (props) => {
</React.Fragment>);
}
};
EncryptionPanel.propTypes = {
member: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired,
verificationRequest: PropTypes.object,
layout: PropTypes.string,
inDialog: PropTypes.bool,
};
export default EncryptionPanel;

View file

@ -21,65 +21,68 @@ limitations under the License.
import React from 'react';
import { _t } from '../../../languageHandler';
import HeaderButton from './HeaderButton';
import HeaderButtons, {HEADER_KIND_GROUP} from './HeaderButtons';
import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
import HeaderButtons, {HeaderKind} from './HeaderButtons';
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import {Action} from "../../../dispatcher/actions";
import {ActionPayload} from "../../../dispatcher/payloads";
import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload";
const GROUP_PHASES = [
RIGHT_PANEL_PHASES.GroupMemberInfo,
RIGHT_PANEL_PHASES.GroupMemberList,
RightPanelPhases.GroupMemberInfo,
RightPanelPhases.GroupMemberList,
];
const ROOM_PHASES = [
RIGHT_PANEL_PHASES.GroupRoomList,
RIGHT_PANEL_PHASES.GroupRoomInfo,
RightPanelPhases.GroupRoomList,
RightPanelPhases.GroupRoomInfo,
];
interface IProps {}
export default class GroupHeaderButtons extends HeaderButtons {
constructor(props) {
super(props, HEADER_KIND_GROUP);
this._onMembersClicked = this._onMembersClicked.bind(this);
this._onRoomsClicked = this._onRoomsClicked.bind(this);
constructor(props: IProps) {
super(props, HeaderKind.Group);
this.onMembersClicked = this.onMembersClicked.bind(this);
this.onRoomsClicked = this.onRoomsClicked.bind(this);
}
onAction(payload: ActionPayload) {
protected onAction(payload: ActionPayload) {
super.onAction(payload);
if (payload.action === Action.ViewUser) {
if (payload.member) {
this.setPhase(RIGHT_PANEL_PHASES.RoomMemberInfo, {member: payload.member});
if ((payload as ViewUserPayload).member) {
this.setPhase(RightPanelPhases.RoomMemberInfo, {member: payload.member});
} else {
this.setPhase(RIGHT_PANEL_PHASES.GroupMemberList);
this.setPhase(RightPanelPhases.GroupMemberList);
}
} else if (payload.action === "view_group") {
this.setPhase(RIGHT_PANEL_PHASES.GroupMemberList);
this.setPhase(RightPanelPhases.GroupMemberList);
} else if (payload.action === "view_group_room") {
this.setPhase(
RIGHT_PANEL_PHASES.GroupRoomInfo,
RightPanelPhases.GroupRoomInfo,
{groupRoomId: payload.groupRoomId, groupId: payload.groupId},
);
} else if (payload.action === "view_group_room_list") {
this.setPhase(RIGHT_PANEL_PHASES.GroupRoomList);
this.setPhase(RightPanelPhases.GroupRoomList);
} else if (payload.action === "view_group_member_list") {
this.setPhase(RIGHT_PANEL_PHASES.GroupMemberList);
this.setPhase(RightPanelPhases.GroupMemberList);
} else if (payload.action === "view_group_user") {
this.setPhase(RIGHT_PANEL_PHASES.GroupMemberInfo, {member: payload.member});
this.setPhase(RightPanelPhases.GroupMemberInfo, {member: payload.member});
}
}
_onMembersClicked() {
if (this.state.phase === RIGHT_PANEL_PHASES.GroupMemberInfo) {
private onMembersClicked() {
if (this.state.phase === RightPanelPhases.GroupMemberInfo) {
// send the active phase to trigger a toggle
this.setPhase(RIGHT_PANEL_PHASES.GroupMemberInfo);
this.setPhase(RightPanelPhases.GroupMemberInfo);
} else {
// This toggles for us, if needed
this.setPhase(RIGHT_PANEL_PHASES.GroupMemberList);
this.setPhase(RightPanelPhases.GroupMemberList);
}
}
_onRoomsClicked() {
private onRoomsClicked() {
// This toggles for us, if needed
this.setPhase(RIGHT_PANEL_PHASES.GroupRoomList);
this.setPhase(RightPanelPhases.GroupRoomList);
}
renderButtons() {
@ -87,13 +90,13 @@ export default class GroupHeaderButtons extends HeaderButtons {
<HeaderButton key="groupMembersButton" name="groupMembersButton"
title={_t('Members')}
isHighlighted={this.isPhase(GROUP_PHASES)}
onClick={this._onMembersClicked}
onClick={this.onMembersClicked}
analytics={['Right Panel', 'Group Member List Button', 'click']}
/>,
<HeaderButton key="roomsButton" name="roomsButton"
title={_t('Rooms')}
isHighlighted={this.isPhase(ROOM_PHASES)}
onClick={this._onRoomsClicked}
onClick={this.onRoomsClicked}
analytics={['Right Panel', 'Group Room List Button', 'click']}
/>,
];

View file

@ -19,25 +19,40 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Analytics from '../../../Analytics';
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
interface IProps {
// Whether this button is highlighted
isHighlighted: boolean;
// click handler
onClick: () => void;
// The badge to display above the icon
badge?: React.ReactNode;
// The parameters to track the click event
analytics: string[];
// Button name
name: string;
// Button title
title: string;
}
// TODO: replace this, the composer buttons and the right panel buttons with a unified
// representation
export default class HeaderButton extends React.Component {
constructor() {
super();
export default class HeaderButton extends React.Component<IProps> {
constructor(props: IProps) {
super(props);
this.onClick = this.onClick.bind(this);
}
onClick(ev) {
private onClick() {
Analytics.trackEvent(...this.props.analytics);
this.props.onClick();
}
render() {
public render() {
const classes = classNames({
mx_RightPanel_headerButton: true,
mx_RightPanel_headerButton_highlight: this.props.isHighlighted,
@ -53,19 +68,3 @@ export default class HeaderButton extends React.Component {
/>;
}
}
HeaderButton.propTypes = {
// Whether this button is highlighted
isHighlighted: PropTypes.bool.isRequired,
// click handler
onClick: PropTypes.func.isRequired,
// The badge to display above the icon
badge: PropTypes.node,
// The parameters to track the click event
analytics: PropTypes.arrayOf(PropTypes.string).isRequired,
// Button name
name: PropTypes.string.isRequired,
// Button title
title: PropTypes.string.isRequired,
};

View file

@ -1,88 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 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 dis from '../../../dispatcher/dispatcher';
import RightPanelStore from "../../../stores/RightPanelStore";
export const HEADER_KIND_ROOM = "room";
export const HEADER_KIND_GROUP = "group";
const HEADER_KINDS = [HEADER_KIND_GROUP, HEADER_KIND_ROOM];
export default class HeaderButtons extends React.Component {
constructor(props, kind) {
super(props);
if (!HEADER_KINDS.includes(kind)) throw new Error(`Invalid header kind: ${kind}`);
const rps = RightPanelStore.getSharedInstance();
this.state = {
headerKind: kind,
phase: kind === HEADER_KIND_ROOM ? rps.visibleRoomPanelPhase : rps.visibleGroupPanelPhase,
};
}
componentDidMount() {
this._storeToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelUpdate.bind(this));
this._dispatcherRef = dis.register(this.onAction.bind(this)); // used by subclasses
}
componentWillUnmount() {
if (this._storeToken) this._storeToken.remove();
if (this._dispatcherRef) dis.unregister(this._dispatcherRef);
}
onAction(payload) {
// Ignore - intended to be overridden by subclasses
}
setPhase(phase, extras) {
dis.dispatch({
action: 'set_right_panel_phase',
phase: phase,
refireParams: extras,
});
}
isPhase(phases: string | string[]) {
if (Array.isArray(phases)) {
return phases.includes(this.state.phase);
} else {
return phases === this.state.phase;
}
}
onRightPanelUpdate() {
const rps = RightPanelStore.getSharedInstance();
if (this.state.headerKind === HEADER_KIND_ROOM) {
this.setState({phase: rps.visibleRoomPanelPhase});
} else if (this.state.headerKind === HEADER_KIND_GROUP) {
this.setState({phase: rps.visibleGroupPanelPhase});
}
}
render() {
// inline style as this will be swapped around in future commits
return <div className="mx_HeaderButtons" role="tablist">
{this.renderButtons()}
</div>;
}
}

View file

@ -0,0 +1,110 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 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 dis from '../../../dispatcher/dispatcher';
import RightPanelStore from "../../../stores/RightPanelStore";
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import {Action} from '../../../dispatcher/actions';
import {SetRightPanelPhasePayload, SetRightPanelPhaseRefireParams} from '../../../dispatcher/payloads/SetRightPanelPhasePayload';
import {EventSubscription} from "fbemitter";
export enum HeaderKind {
Room = "room",
Group = "group",
}
interface IState {
headerKind: HeaderKind;
phase: RightPanelPhases;
}
interface IProps {}
export default class HeaderButtons extends React.Component<IProps, IState> {
private storeToken: EventSubscription;
private dispatcherRef: string;
constructor(props: IProps, kind: HeaderKind) {
super(props);
const rps = RightPanelStore.getSharedInstance();
this.state = {
headerKind: kind,
phase: kind === HeaderKind.Room ? rps.visibleRoomPanelPhase : rps.visibleGroupPanelPhase,
};
}
public componentDidMount() {
this.storeToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelUpdate.bind(this));
this.dispatcherRef = dis.register(this.onAction.bind(this)); // used by subclasses
}
public componentWillUnmount() {
if (this.storeToken) this.storeToken.remove();
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
}
protected onAction(payload) {
// Ignore - intended to be overridden by subclasses
}
public setPhase(phase: RightPanelPhases, extras?: Partial<SetRightPanelPhaseRefireParams>) {
dis.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: phase,
refireParams: extras,
});
}
public isPhase(phases: string | string[]) {
if (Array.isArray(phases)) {
return phases.includes(this.state.phase);
} else {
return phases === this.state.phase;
}
}
private onRightPanelUpdate() {
const rps = RightPanelStore.getSharedInstance();
if (this.state.headerKind === HeaderKind.Room) {
this.setState({phase: rps.visibleRoomPanelPhase});
} else if (this.state.headerKind === HeaderKind.Group) {
this.setState({phase: rps.visibleGroupPanelPhase});
}
}
// XXX: Make renderButtons a prop
public renderButtons(): JSX.Element[] {
// Ignore - intended to be overridden by subclasses
// Return empty fragment to satisfy the type
return [
<React.Fragment>
</React.Fragment>
];
}
public render() {
// inline style as this will be swapped around in future commits
return <div className="mx_HeaderButtons" role="tablist">
{this.renderButtons()}
</div>;
}
}

View file

@ -21,82 +21,82 @@ limitations under the License.
import React from 'react';
import { _t } from '../../../languageHandler';
import HeaderButton from './HeaderButton';
import HeaderButtons, {HEADER_KIND_ROOM} from './HeaderButtons';
import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
import HeaderButtons, {HeaderKind} from './HeaderButtons';
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import {Action} from "../../../dispatcher/actions";
import {ActionPayload} from "../../../dispatcher/payloads";
const MEMBER_PHASES = [
RIGHT_PANEL_PHASES.RoomMemberList,
RIGHT_PANEL_PHASES.RoomMemberInfo,
RIGHT_PANEL_PHASES.EncryptionPanel,
RIGHT_PANEL_PHASES.Room3pidMemberInfo,
RightPanelPhases.RoomMemberList,
RightPanelPhases.RoomMemberInfo,
RightPanelPhases.EncryptionPanel,
RightPanelPhases.Room3pidMemberInfo,
];
export default class RoomHeaderButtons extends HeaderButtons {
constructor(props) {
super(props, HEADER_KIND_ROOM);
this._onMembersClicked = this._onMembersClicked.bind(this);
this._onFilesClicked = this._onFilesClicked.bind(this);
this._onNotificationsClicked = this._onNotificationsClicked.bind(this);
super(props, HeaderKind.Room);
this.onMembersClicked = this.onMembersClicked.bind(this);
this.onFilesClicked = this.onFilesClicked.bind(this);
this.onNotificationsClicked = this.onNotificationsClicked.bind(this);
}
onAction(payload: ActionPayload) {
protected onAction(payload: ActionPayload) {
super.onAction(payload);
if (payload.action === Action.ViewUser) {
if (payload.member) {
this.setPhase(RIGHT_PANEL_PHASES.RoomMemberInfo, {member: payload.member});
this.setPhase(RightPanelPhases.RoomMemberInfo, {member: payload.member});
} else {
this.setPhase(RIGHT_PANEL_PHASES.RoomMemberList);
this.setPhase(RightPanelPhases.RoomMemberList);
}
} else if (payload.action === "view_3pid_invite") {
if (payload.event) {
this.setPhase(RIGHT_PANEL_PHASES.Room3pidMemberInfo, {event: payload.event});
this.setPhase(RightPanelPhases.Room3pidMemberInfo, {event: payload.event});
} else {
this.setPhase(RIGHT_PANEL_PHASES.RoomMemberList);
this.setPhase(RightPanelPhases.RoomMemberList);
}
}
}
_onMembersClicked() {
if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberInfo) {
private onMembersClicked() {
if (this.state.phase === RightPanelPhases.RoomMemberInfo) {
// send the active phase to trigger a toggle
// XXX: we should pass refireParams here but then it won't collapse as we desire it to
this.setPhase(RIGHT_PANEL_PHASES.RoomMemberInfo);
this.setPhase(RightPanelPhases.RoomMemberInfo);
} else {
// This toggles for us, if needed
this.setPhase(RIGHT_PANEL_PHASES.RoomMemberList);
this.setPhase(RightPanelPhases.RoomMemberList);
}
}
_onFilesClicked() {
private onFilesClicked() {
// This toggles for us, if needed
this.setPhase(RIGHT_PANEL_PHASES.FilePanel);
this.setPhase(RightPanelPhases.FilePanel);
}
_onNotificationsClicked() {
private onNotificationsClicked() {
// This toggles for us, if needed
this.setPhase(RIGHT_PANEL_PHASES.NotificationPanel);
this.setPhase(RightPanelPhases.NotificationPanel);
}
renderButtons() {
public renderButtons() {
return [
<HeaderButton key="membersButton" name="membersButton"
title={_t('Members')}
isHighlighted={this.isPhase(MEMBER_PHASES)}
onClick={this._onMembersClicked}
onClick={this.onMembersClicked}
analytics={['Right Panel', 'Member List Button', 'click']}
/>,
<HeaderButton key="filesButton" name="filesButton"
title={_t('Files')}
isHighlighted={this.isPhase(RIGHT_PANEL_PHASES.FilePanel)}
onClick={this._onFilesClicked}
isHighlighted={this.isPhase(RightPanelPhases.FilePanel)}
onClick={this.onFilesClicked}
analytics={['Right Panel', 'File List Button', 'click']}
/>,
<HeaderButton key="notifsButton" name="notifsButton"
title={_t('Notifications')}
isHighlighted={this.isPhase(RIGHT_PANEL_PHASES.NotificationPanel)}
onClick={this._onNotificationsClicked}
isHighlighted={this.isPhase(RightPanelPhases.NotificationPanel)}
onClick={this.onNotificationsClicked}
analytics={['Right Panel', 'Notification List Button', 'click']}
/>,
];

View file

@ -40,7 +40,7 @@ import E2EIcon from "../rooms/E2EIcon";
import {useEventEmitter} from "../../../hooks/useEventEmitter";
import {textualPowerLevel} from '../../../Roles';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import EncryptionPanel from "./EncryptionPanel";
import { useAsyncMemo } from '../../../hooks/useAsyncMemo';
import { verifyUser, legacyVerifyUser, verifyDevice } from '../../../verification';
@ -550,7 +550,9 @@ const RedactMessagesButton = ({member}) => {
let eventsToRedact = [];
while (timeline) {
eventsToRedact = timeline.getEvents().reduce((events, event) => {
if (event.getSender() === userId && !event.isRedacted() && !event.isRedaction()) {
if (event.getSender() === userId && !event.isRedacted() && !event.isRedaction() &&
event.getType() !== "m.room.create"
) {
return events.concat(event);
} else {
return events;
@ -1480,7 +1482,7 @@ const UserInfoHeader = ({onClose, member, e2eStatus}) => {
</React.Fragment>;
};
const UserInfo = ({user, groupId, roomId, onClose, phase=RIGHT_PANEL_PHASES.RoomMemberInfo, ...props}) => {
const UserInfo = ({user, groupId, roomId, onClose, phase=RightPanelPhases.RoomMemberInfo, ...props}) => {
const cli = useContext(MatrixClientContext);
// Load room if we are given a room id and memoize it
@ -1500,8 +1502,8 @@ const UserInfo = ({user, groupId, roomId, onClose, phase=RIGHT_PANEL_PHASES.Room
let content;
switch (phase) {
case RIGHT_PANEL_PHASES.RoomMemberInfo:
case RIGHT_PANEL_PHASES.GroupMemberInfo:
case RightPanelPhases.RoomMemberInfo:
case RightPanelPhases.GroupMemberInfo:
content = (
<BasicUserInfo
room={room}
@ -1511,7 +1513,7 @@ const UserInfo = ({user, groupId, roomId, onClose, phase=RIGHT_PANEL_PHASES.Room
isRoomEncrypted={isRoomEncrypted} />
);
break;
case RIGHT_PANEL_PHASES.EncryptionPanel:
case RightPanelPhases.EncryptionPanel:
classes.push("mx_UserInfo_smallAvatar");
content = (
<EncryptionPanel {...props} member={member} onClose={onClose} isRoomEncrypted={isRoomEncrypted} />

View file

@ -15,12 +15,15 @@ limitations under the License.
*/
import React from "react";
import PropTypes from "prop-types";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import * as sdk from '../../../index';
import {verificationMethods} from 'matrix-js-sdk/src/crypto';
import {SCAN_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
import {ReciprocateQRCode} from "matrix-js-sdk/src/crypto/verification/QRCode";
import {SAS} from "matrix-js-sdk/src/crypto/verification/SAS";
import VerificationQRCode from "../elements/crypto/VerificationQRCode";
import {_t} from "../../../languageHandler";
@ -36,37 +39,51 @@ import {
} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import Spinner from "../elements/Spinner";
export default class VerificationPanel extends React.PureComponent {
static propTypes = {
layout: PropTypes.string,
request: PropTypes.object.isRequired,
member: PropTypes.object.isRequired,
phase: PropTypes.oneOf([
PHASE_UNSENT,
PHASE_REQUESTED,
PHASE_READY,
PHASE_STARTED,
PHASE_CANCELLED,
PHASE_DONE,
]).isRequired,
onClose: PropTypes.func.isRequired,
isRoomEncrypted: PropTypes.bool,
};
// XXX: Should be defined in matrix-js-sdk
enum VerificationPhase {
PHASE_UNSENT,
PHASE_REQUESTED,
PHASE_READY,
PHASE_DONE,
PHASE_STARTED,
PHASE_CANCELLED,
}
constructor(props) {
interface IProps {
layout: string;
request: VerificationRequest;
member: RoomMember;
phase: VerificationPhase;
onClose: () => void;
isRoomEncrypted: boolean;
inDialog: boolean;
key: number;
}
interface IState {
sasEvent?: SAS;
emojiButtonClicked?: boolean;
reciprocateButtonClicked?: boolean;
reciprocateQREvent?: ReciprocateQRCode;
}
export default class VerificationPanel extends React.PureComponent<IProps, IState> {
private hasVerifier: boolean;
constructor(props: IProps) {
super(props);
this.state = {};
this._hasVerifier = false;
this.hasVerifier = false;
}
renderQRPhase() {
private renderQRPhase() {
const {member, request} = this.props;
const showSAS = request.otherPartySupportsMethod(verificationMethods.SAS);
const showQR = request.otherPartySupportsMethod(SCAN_QR_CODE_METHOD);
const showSAS: boolean = request.otherPartySupportsMethod(verificationMethods.SAS);
const showQR: boolean = request.otherPartySupportsMethod(SCAN_QR_CODE_METHOD);
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const brand = SdkConfig.get().brand;
const noCommonMethodError = !showSAS && !showQR ?
const noCommonMethodError: JSX.Element = !showSAS && !showQR ?
<p>{_t(
"The session you are trying to verify doesn't support scanning a " +
"QR code or emoji verification, which is what %(brand)s supports. Try " +
@ -77,41 +94,41 @@ export default class VerificationPanel extends React.PureComponent {
if (this.props.layout === 'dialog') {
// HACK: This is a terrible idea.
let qrBlock;
let sasBlock;
let qrBlockDialog: JSX.Element;
let sasBlockDialog: JSX.Element;
if (showQR) {
qrBlock =
qrBlockDialog =
<div className='mx_VerificationPanel_QRPhase_startOption'>
<p>{_t("Scan this unique code")}</p>
<VerificationQRCode qrCodeData={request.qrCodeData} />
</div>;
}
if (showSAS) {
sasBlock =
sasBlockDialog =
<div className='mx_VerificationPanel_QRPhase_startOption'>
<p>{_t("Compare unique emoji")}</p>
<span className='mx_VerificationPanel_QRPhase_helpText'>{_t("Compare a unique set of emoji if you don't have a camera on either device")}</span>
<AccessibleButton disabled={this.state.emojiButtonClicked} onClick={this._startSAS} kind='primary'>
<AccessibleButton disabled={this.state.emojiButtonClicked} onClick={this.startSAS} kind='primary'>
{_t("Start")}
</AccessibleButton>
</div>;
}
const or = qrBlock && sasBlock ?
const or = qrBlockDialog && sasBlockDialog ?
<div className='mx_VerificationPanel_QRPhase_betweenText'>{_t("or")}</div> : null;
return (
<div>
{_t("Verify this session by completing one of the following:")}
<div className='mx_VerificationPanel_QRPhase_startOptions'>
{qrBlock}
{qrBlockDialog}
{or}
{sasBlock}
{sasBlockDialog}
{noCommonMethodError}
</div>
</div>
);
}
let qrBlock;
let qrBlock: JSX.Element;
if (showQR) {
qrBlock = <div className="mx_UserInfo_container">
<h3>{_t("Verify by scanning")}</h3>
@ -125,7 +142,7 @@ export default class VerificationPanel extends React.PureComponent {
</div>;
}
let sasBlock;
let sasBlock: JSX.Element;
if (showSAS) {
const disabled = this.state.emojiButtonClicked;
const sasLabel = showQR ?
@ -140,7 +157,7 @@ export default class VerificationPanel extends React.PureComponent {
disabled={disabled}
kind="primary"
className="mx_UserInfo_wideButton mx_VerificationPanel_verifyByEmojiButton"
onClick={this._startSAS}
onClick={this.startSAS}
>
{_t("Verify by emoji")}
</AccessibleButton>
@ -159,22 +176,22 @@ export default class VerificationPanel extends React.PureComponent {
</React.Fragment>;
}
_onReciprocateYesClick = () => {
private onReciprocateYesClick = () => {
this.setState({reciprocateButtonClicked: true});
this.state.reciprocateQREvent.confirm();
};
_onReciprocateNoClick = () => {
private onReciprocateNoClick = () => {
this.setState({reciprocateButtonClicked: true});
this.state.reciprocateQREvent.cancel();
};
_getDevice() {
private getDevice() {
const deviceId = this.props.request && this.props.request.channel.deviceId;
return MatrixClientPeg.get().getStoredDevice(MatrixClientPeg.get().getUserId(), deviceId);
}
renderQRReciprocatePhase() {
private renderQRReciprocatePhase() {
const {member, request} = this.props;
let Button;
// a bit of a hack, but the FormButton should only be used in the right panel
@ -189,7 +206,7 @@ export default class VerificationPanel extends React.PureComponent {
_t("Almost there! Is %(displayName)s showing the same shield?", {
displayName: member.displayName || member.name || member.userId,
});
let body;
let body: JSX.Element;
if (this.state.reciprocateQREvent) {
// riot web doesn't support scanning yet, so assume here we're the client being scanned.
//
@ -202,11 +219,11 @@ export default class VerificationPanel extends React.PureComponent {
<Button
label={_t("No")} kind="danger"
disabled={this.state.reciprocateButtonClicked}
onClick={this._onReciprocateNoClick}>{_t("No")}</Button>
onClick={this.onReciprocateNoClick}>{_t("No")}</Button>
<Button
label={_t("Yes")} kind="primary"
disabled={this.state.reciprocateButtonClicked}
onClick={this._onReciprocateYesClick}>{_t("Yes")}</Button>
onClick={this.onReciprocateYesClick}>{_t("Yes")}</Button>
</div>
</React.Fragment>;
} else {
@ -218,10 +235,10 @@ export default class VerificationPanel extends React.PureComponent {
</div>;
}
renderVerifiedPhase() {
private renderVerifiedPhase() {
const {member, request} = this.props;
let text;
let text: string;
if (!request.isSelfVerification) {
if (this.props.isRoomEncrypted) {
text = _t("Verify all users in a room to ensure it's secure.");
@ -230,9 +247,9 @@ export default class VerificationPanel extends React.PureComponent {
}
}
let description;
let description: string;
if (request.isSelfVerification) {
const device = this._getDevice();
const device = this.getDevice();
if (!device) {
// This can happen if the device is logged out while we're still showing verification
// UI for it.
@ -264,19 +281,19 @@ export default class VerificationPanel extends React.PureComponent {
);
}
renderCancelledPhase() {
private renderCancelledPhase() {
const {member, request} = this.props;
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let startAgainInstruction;
let startAgainInstruction: string;
if (request.isSelfVerification) {
startAgainInstruction = _t("Start verification again from the notification.");
} else {
startAgainInstruction = _t("Start verification again from their profile.");
}
let text;
let text: string;
if (request.cancellationCode === "m.timeout") {
text = _t("Verification timed out.") + ` ${startAgainInstruction}`;
} else if (request.cancellingUserId === request.otherUserId) {
@ -304,7 +321,7 @@ export default class VerificationPanel extends React.PureComponent {
);
}
render() {
public render() {
const {member, phase, request} = this.props;
const displayName = member.displayName || member.name || member.userId;
@ -321,10 +338,10 @@ export default class VerificationPanel extends React.PureComponent {
const emojis = this.state.sasEvent ?
<VerificationShowSas
displayName={displayName}
device={this._getDevice()}
device={this.getDevice()}
sas={this.state.sasEvent.sas}
onCancel={this._onSasMismatchesClick}
onDone={this._onSasMatchesClick}
onCancel={this.onSasMismatchesClick}
onDone={this.onSasMatchesClick}
inDialog={this.props.inDialog}
isSelf={request.isSelfVerification}
/> : <Spinner />;
@ -345,7 +362,7 @@ export default class VerificationPanel extends React.PureComponent {
return null;
}
_startSAS = async () => {
private startSAS = async () => {
this.setState({emojiButtonClicked: true});
const verifier = this.props.request.beginKeyVerification(verificationMethods.SAS);
try {
@ -355,31 +372,31 @@ export default class VerificationPanel extends React.PureComponent {
}
};
_onSasMatchesClick = () => {
private onSasMatchesClick = () => {
this.state.sasEvent.confirm();
};
_onSasMismatchesClick = () => {
private onSasMismatchesClick = () => {
this.state.sasEvent.mismatch();
};
_updateVerifierState = () => {
private updateVerifierState = () => {
const {request} = this.props;
const {sasEvent, reciprocateQREvent} = request.verifier;
request.verifier.off('show_sas', this._updateVerifierState);
request.verifier.off('show_reciprocate_qr', this._updateVerifierState);
request.verifier.off('show_sas', this.updateVerifierState);
request.verifier.off('show_reciprocate_qr', this.updateVerifierState);
this.setState({sasEvent, reciprocateQREvent});
};
_onRequestChange = async () => {
private onRequestChange = async () => {
const {request} = this.props;
const hadVerifier = this._hasVerifier;
this._hasVerifier = !!request.verifier;
if (!hadVerifier && this._hasVerifier) {
request.verifier.on('show_sas', this._updateVerifierState);
request.verifier.on('show_reciprocate_qr', this._updateVerifierState);
const hadVerifier = this.hasVerifier;
this.hasVerifier = !!request.verifier;
if (!hadVerifier && this.hasVerifier) {
request.verifier.on('show_sas', this.updateVerifierState);
request.verifier.on('show_reciprocate_qr', this.updateVerifierState);
try {
// on the requester side, this is also awaited in _startSAS,
// on the requester side, this is also awaited in startSAS,
// but that's ok as verify should return the same promise.
await request.verifier.verify();
} catch (err) {
@ -388,23 +405,22 @@ export default class VerificationPanel extends React.PureComponent {
}
};
componentDidMount() {
public componentDidMount() {
const {request} = this.props;
request.on("change", this._onRequestChange);
request.on("change", this.onRequestChange);
if (request.verifier) {
const {request} = this.props;
const {sasEvent, reciprocateQREvent} = request.verifier;
this.setState({sasEvent, reciprocateQREvent});
}
this._onRequestChange();
this.onRequestChange();
}
componentWillUnmount() {
public componentWillUnmount() {
const {request} = this.props;
if (request.verifier) {
request.verifier.off('show_sas', this._updateVerifierState);
request.verifier.off('show_reciprocate_qr', this._updateVerifierState);
request.verifier.off('show_sas', this.updateVerifierState);
request.verifier.off('show_reciprocate_qr', this.updateVerifierState);
}
request.off("change", this._onRequestChange);
request.off("change", this.onRequestChange);
}
}

View file

@ -20,7 +20,8 @@ import createReactClass from 'create-react-class';
import Tinter from '../../../Tinter';
import dis from '../../../dispatcher/dispatcher';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import SettingsStore from "../../../settings/SettingsStore";
import {SettingLevel} from "../../../settings/SettingLevel";
const ROOM_COLORS = [
// magic room default values courtesy of Ribot

View file

@ -22,10 +22,11 @@ import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import * as sdk from "../../../index";
import { _t, _td } from '../../../languageHandler';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import SettingsStore from "../../../settings/SettingsStore";
import dis from "../../../dispatcher/dispatcher";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {Action} from "../../../dispatcher/actions";
import {SettingLevel} from "../../../settings/SettingLevel";
export default createReactClass({

View file

@ -685,6 +685,9 @@ export default createReactClass({
mx_EventTile_emote: msgtype === 'm.emote',
});
// If the tile is in the Sending state, don't speak the message.
const ariaLive = (this.props.eventSendStatus !== null) ? 'off' : undefined;
let permalink = "#";
if (this.props.permalinkCreator) {
permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
@ -819,7 +822,7 @@ export default createReactClass({
case 'notif': {
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
return (
<div className={classes}>
<div className={classes} aria-live={ariaLive} aria-atomic="true">
<div className="mx_EventTile_roomName">
<a href={permalink} onClick={this.onPermalinkClicked}>
{ room ? room.name : '' }
@ -845,7 +848,7 @@ export default createReactClass({
}
case 'file_grid': {
return (
<div className={classes}>
<div className={classes} aria-live={ariaLive} aria-atomic="true">
<div className="mx_EventTile_line">
<EventTileType ref={this._tile}
mxEvent={this.props.mxEvent}
@ -881,7 +884,7 @@ export default createReactClass({
);
}
return (
<div className={classes}>
<div className={classes} aria-live={ariaLive} aria-atomic="true">
{ ircTimestamp }
{ avatar }
{ sender }
@ -911,7 +914,7 @@ export default createReactClass({
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
return (
<div className={classes} tabIndex={-1}>
<div className={classes} tabIndex={-1} aria-live={ariaLive} aria-atomic="true">
{ ircTimestamp }
<div className="mx_EventTile_msgOption">
{ readAvatars }

View file

@ -72,6 +72,7 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
public componentWillUnmount() {
SettingsStore.unwatchSetting(this.countWatcherRef);
this.props.notification.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
}
public componentDidUpdate(prevProps: Readonly<IProps>) {

View file

@ -73,7 +73,7 @@ export default class ReplyPreview extends React.Component {
return <div className="mx_ReplyPreview">
<div className="mx_ReplyPreview_section">
<div className="mx_ReplyPreview_header mx_ReplyPreview_title">
{ '💬 ' + _t('Replying') }
{ _t('Replying') }
</div>
<div className="mx_ReplyPreview_header mx_ReplyPreview_cancel">
<img className="mx_filterFlipColor" src={require("../../../../res/img/cancel.svg")} width="18" height="18"

View file

@ -31,7 +31,6 @@ import dis from "../../../dispatcher/dispatcher";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import RoomSublist from "./RoomSublist";
import { ActionPayload } from "../../../dispatcher/payloads";
import { NameFilterCondition } from "../../../stores/room-list/filters/NameFilterCondition";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import GroupAvatar from "../avatars/GroupAvatar";
import TemporaryTile from "./TemporaryTile";
@ -42,6 +41,8 @@ import { ViewRoomDeltaPayload } from "../../../dispatcher/payloads/ViewRoomDelta
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import SettingsStore from "../../../settings/SettingsStore";
import CustomRoomTagStore from "../../../stores/CustomRoomTagStore";
import { arrayFastClone, arrayHasDiff } from "../../../utils/arrays";
import { objectShallowClone, objectWithOnly } from "../../../utils/objects";
interface IProps {
onKeyDown: (ev: React.KeyboardEvent) => void;
@ -50,7 +51,6 @@ interface IProps {
onResize: () => void;
resizeNotifier: ResizeNotifier;
collapsed: boolean;
searchFilter: string;
isMinimized: boolean;
}
@ -80,7 +80,7 @@ interface ITagAesthetics {
sectionLabel: string;
sectionLabelRaw?: string;
addRoomLabel?: string;
onAddRoom?: (dispatcher: Dispatcher<ActionPayload>) => void;
onAddRoom?: (dispatcher?: Dispatcher<ActionPayload>) => void;
isInvite: boolean;
defaultHidden: boolean;
}
@ -104,14 +104,18 @@ const TAG_AESTHETICS: {
isInvite: false,
defaultHidden: false,
addRoomLabel: _td("Start chat"),
onAddRoom: (dispatcher: Dispatcher<ActionPayload>) => dispatcher.dispatch({action: 'view_create_chat'}),
onAddRoom: (dispatcher?: Dispatcher<ActionPayload>) => {
(dispatcher || defaultDispatcher).dispatch({action: 'view_create_chat'});
},
},
[DefaultTagID.Untagged]: {
sectionLabel: _td("Rooms"),
isInvite: false,
defaultHidden: false,
addRoomLabel: _td("Create room"),
onAddRoom: (dispatcher: Dispatcher<ActionPayload>) => dispatcher.dispatch({action: 'view_create_room'}),
onAddRoom: (dispatcher?: Dispatcher<ActionPayload>) => {
(dispatcher || defaultDispatcher).dispatch({action: 'view_create_room'})
},
},
[DefaultTagID.LowPriority]: {
sectionLabel: _td("Low priority"),
@ -144,8 +148,7 @@ function customTagAesthetics(tagId: TagID): ITagAesthetics {
};
}
export default class RoomList extends React.Component<IProps, IState> {
private searchFilter: NameFilterCondition = new NameFilterCondition();
export default class RoomList extends React.PureComponent<IProps, IState> {
private dispatcherRef;
private customTagStoreRef;
@ -159,21 +162,6 @@ export default class RoomList extends React.Component<IProps, IState> {
this.dispatcherRef = defaultDispatcher.register(this.onAction);
}
public componentDidUpdate(prevProps: Readonly<IProps>): void {
if (prevProps.searchFilter !== this.props.searchFilter) {
const hadSearch = !!this.searchFilter.search.trim();
const haveSearch = !!this.props.searchFilter.trim();
this.searchFilter.search = this.props.searchFilter;
if (!hadSearch && haveSearch) {
// started a new filter - add the condition
RoomListStore.instance.addFilter(this.searchFilter);
} else if (hadSearch && !haveSearch) {
// cleared a filter - remove the condition
RoomListStore.instance.removeFilter(this.searchFilter);
} // else the filter hasn't changed enough for us to care here
}
}
public componentDidMount(): void {
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists);
this.customTagStoreRef = CustomRoomTagStore.addListener(this.updateLists);
@ -231,17 +219,46 @@ export default class RoomList extends React.Component<IProps, IState> {
console.log("new lists", newLists);
}
this.setState({sublists: newLists}, () => {
this.props.onResize();
const previousListIds = Object.keys(this.state.sublists);
const newListIds = Object.keys(newLists).filter(t => {
if (!isCustomTag(t)) return true; // always include non-custom tags
// if the tag is custom though, only include it if it is enabled
return CustomRoomTagStore.getTags()[t];
});
let doUpdate = arrayHasDiff(previousListIds, newListIds);
if (!doUpdate) {
// so we didn't have the visible sublists change, but did the contents of those
// sublists change significantly enough to break the sticky headers? Probably, so
// let's check the length of each.
for (const tagId of newListIds) {
const oldRooms = this.state.sublists[tagId];
const newRooms = newLists[tagId];
if (oldRooms.length !== newRooms.length) {
doUpdate = true;
break;
}
}
}
if (doUpdate) {
// We have to break our reference to the room list store if we want to be able to
// diff the object for changes, so do that.
const newSublists = objectWithOnly(newLists, newListIds);
const sublists = objectShallowClone(newSublists, (k, v) => arrayFastClone(v));
this.setState({sublists}, () => {
this.props.onResize();
});
}
};
private renderCommunityInvites(): React.ReactElement[] {
private renderCommunityInvites(): TemporaryTile[] {
// TODO: Put community invites in a more sensible place (not in the room list)
// See https://github.com/vector-im/riot-web/issues/14456
return MatrixClientPeg.get().getGroups().filter(g => {
if (g.myMembership !== 'invite') return false;
return !this.searchFilter || this.searchFilter.matches(g.name || "");
return g.myMembership === 'invite';
}).map(g => {
const avatar = (
<GroupAvatar
@ -277,8 +294,7 @@ export default class RoomList extends React.Component<IProps, IState> {
const tagOrder = TAG_ORDER.reduce((p, c) => {
if (c === CUSTOM_TAGS_BEFORE_TAG) {
const customTags = Object.keys(this.state.sublists)
.filter(t => isCustomTag(t))
.filter(t => CustomRoomTagStore.getTags()[t]); // isSelected
.filter(t => isCustomTag(t));
p.push(...customTags);
}
p.push(c);
@ -298,21 +314,18 @@ export default class RoomList extends React.Component<IProps, IState> {
: TAG_AESTHETICS[orderedTagId];
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null;
components.push(
<RoomSublist
key={`sublist-${orderedTagId}`}
tagId={orderedTagId}
forRooms={true}
rooms={orderedRooms}
startAsHidden={aesthetics.defaultHidden}
label={aesthetics.sectionLabelRaw ? aesthetics.sectionLabelRaw : _t(aesthetics.sectionLabel)}
onAddRoom={onAddRoomFn}
onAddRoom={aesthetics.onAddRoom}
addRoomLabel={aesthetics.addRoomLabel ? _t(aesthetics.addRoomLabel) : aesthetics.addRoomLabel}
isMinimized={this.props.isMinimized}
onResize={this.props.onResize}
extraBadTilesThatShouldntExist={extraTiles}
isFiltered={!!this.searchFilter.search}
/>
);
}

View file

@ -288,7 +288,6 @@ export default createReactClass({
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let showSpinner = false;
let darkStyle = false;
let title;
let subTitle;
let primaryActionHandler;
@ -316,7 +315,6 @@ export default createReactClass({
break;
}
case MessageCase.NotLoggedIn: {
darkStyle = true;
title = _t("Join the conversation with an account");
primaryActionLabel = _t("Sign Up");
primaryActionHandler = this.onRegisterClick;
@ -557,7 +555,6 @@ export default createReactClass({
const classes = classNames("mx_RoomPreviewBar", "dark-panel", `mx_RoomPreviewBar_${messageCase}`, {
"mx_RoomPreviewBar_panel": this.props.canPreview,
"mx_RoomPreviewBar_dialog": !this.props.canPreview,
"mx_RoomPreviewBar_dark": darkStyle,
});
return (

View file

@ -21,7 +21,8 @@ import * as sdk from "../../../index";
import { _t } from "../../../languageHandler";
import Modal from "../../../Modal";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import SettingsStore from "../../../settings/SettingsStore";
import {SettingLevel} from "../../../settings/SettingLevel";
export default class RoomRecoveryReminder extends React.PureComponent {
static propTypes = {

View file

@ -32,13 +32,12 @@ import {
StyledMenuItemCheckbox,
StyledMenuItemRadio,
} from "../../structures/ContextMenu";
import RoomListStore from "../../../stores/room-list/RoomListStore";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorithms/models";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import dis from "../../../dispatcher/dispatcher";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import NotificationBadge from "./NotificationBadge";
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { Key } from "../../../Keyboard";
import { ActionPayload } from "../../../dispatcher/payloads";
@ -47,6 +46,10 @@ import { Direction } from "re-resizable/lib/resizer";
import { polyfillTouchEvent } from "../../../@types/polyfill";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore";
import { arrayFastClone, arrayHasOrderChange } from "../../../utils/arrays";
import { objectExcluding, objectHasDiff } from "../../../utils/objects";
import TemporaryTile from "./TemporaryTile";
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
const SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS
const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS
@ -59,7 +62,6 @@ polyfillTouchEvent();
interface IProps {
forRooms: boolean;
rooms?: Room[];
startAsHidden: boolean;
label: string;
onAddRoom?: () => void;
@ -67,11 +69,10 @@ interface IProps {
isMinimized: boolean;
tagId: TagID;
onResize: () => void;
isFiltered: boolean;
// TODO: Don't use this. It's for community invites, and community invites shouldn't be here.
// You should feel bad if you use this.
extraBadTilesThatShouldntExist?: React.ReactElement[];
extraBadTilesThatShouldntExist?: TemporaryTile[];
// TODO: Account for https://github.com/vector-im/riot-web/issues/14179
}
@ -85,11 +86,12 @@ interface ResizeDelta {
type PartialDOMRect = Pick<DOMRect, "left" | "top" | "height">;
interface IState {
notificationState: ListNotificationState;
contextMenuPosition: PartialDOMRect;
isResizing: boolean;
isExpanded: boolean; // used for the for expand of the sublist when the room list is being filtered
height: number;
rooms: Room[];
filteredExtraTiles?: TemporaryTile[];
}
export default class RoomSublist extends React.Component<IProps, IState> {
@ -98,22 +100,27 @@ export default class RoomSublist extends React.Component<IProps, IState> {
private dispatcherRef: string;
private layout: ListLayout;
private heightAtStart: number;
private isBeingFiltered: boolean;
private notificationState: ListNotificationState;
constructor(props: IProps) {
super(props);
this.layout = RoomListLayoutStore.instance.getLayoutFor(this.props.tagId);
this.heightAtStart = 0;
const height = this.calculateInitialHeight();
this.isBeingFiltered = !!RoomListStore.instance.getFirstNameFilterCondition();
this.notificationState = RoomNotificationStateStore.instance.getListState(this.props.tagId);
this.state = {
notificationState: RoomNotificationStateStore.instance.getListState(this.props.tagId),
contextMenuPosition: null,
isResizing: false,
isExpanded: this.props.isFiltered ? this.props.isFiltered : !this.layout.isCollapsed,
height,
isExpanded: this.isBeingFiltered ? this.isBeingFiltered : !this.layout.isCollapsed,
height: 0, // to be fixed in a moment, we need `rooms` to calculate this.
rooms: arrayFastClone(RoomListStore.instance.orderedLists[this.props.tagId] || []),
};
this.state.notificationState.setRooms(this.props.rooms);
// Why Object.assign() and not this.state.height? Because TypeScript says no.
this.state = Object.assign(this.state, {height: this.calculateInitialHeight()});
this.dispatcherRef = defaultDispatcher.register(this.onAction);
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onListsUpdated);
}
private calculateInitialHeight() {
@ -141,12 +148,22 @@ export default class RoomSublist extends React.Component<IProps, IState> {
return padding;
}
private get numTiles(): number {
return RoomSublist.calcNumTiles(this.props);
private get extraTiles(): TemporaryTile[] | null {
if (this.state.filteredExtraTiles) {
return this.state.filteredExtraTiles;
}
if (this.props.extraBadTilesThatShouldntExist) {
return this.props.extraBadTilesThatShouldntExist;
}
return null;
}
private static calcNumTiles(props) {
return (props.rooms || []).length + (props.extraBadTilesThatShouldntExist || []).length;
private get numTiles(): number {
return RoomSublist.calcNumTiles(this.state.rooms, this.extraTiles);
}
private static calcNumTiles(rooms: Room[], extraTiles: any[]) {
return (rooms || []).length + (extraTiles || []).length;
}
private get numVisibleTiles(): number {
@ -154,33 +171,116 @@ export default class RoomSublist extends React.Component<IProps, IState> {
return Math.min(nVisible, this.numTiles);
}
public componentDidUpdate(prevProps: Readonly<IProps>) {
this.state.notificationState.setRooms(this.props.rooms);
if (prevProps.isFiltered !== this.props.isFiltered) {
if (this.props.isFiltered) {
this.setState({isExpanded: true});
} else {
this.setState({isExpanded: !this.layout.isCollapsed});
}
}
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>) {
const prevExtraTiles = prevState.filteredExtraTiles || prevProps.extraBadTilesThatShouldntExist;
// as the rooms can come in one by one we need to reevaluate
// the amount of available rooms to cap the amount of requested visible rooms by the layout
if (RoomSublist.calcNumTiles(prevProps) !== this.numTiles) {
if (RoomSublist.calcNumTiles(prevState.rooms, prevExtraTiles) !== this.numTiles) {
this.setState({height: this.calculateInitialHeight()});
}
}
public componentWillUnmount() {
this.state.notificationState.destroy();
defaultDispatcher.unregister(this.dispatcherRef);
public shouldComponentUpdate(nextProps: Readonly<IProps>, nextState: Readonly<IState>): boolean {
if (objectHasDiff(this.props, nextProps)) {
// Something we don't care to optimize has updated, so update.
return true;
}
// Do the same check used on props for state, without the rooms we're going to no-op
const prevStateNoRooms = objectExcluding(this.state, ['rooms']);
const nextStateNoRooms = objectExcluding(nextState, ['rooms']);
if (objectHasDiff(prevStateNoRooms, nextStateNoRooms)) {
return true;
}
// If we're supposed to handle extra tiles, take the performance hit and re-render all the
// time so we don't have to consider them as part of the visible room optimization.
const prevExtraTiles = this.props.extraBadTilesThatShouldntExist || [];
const nextExtraTiles = (nextState.filteredExtraTiles || nextProps.extraBadTilesThatShouldntExist) || [];
if (prevExtraTiles.length > 0 || nextExtraTiles.length > 0) {
return true;
}
// If we're about to update the height of the list, we don't really care about which rooms
// are visible or not for no-op purposes, so ensure that the height calculation runs through.
if (RoomSublist.calcNumTiles(nextState.rooms, nextExtraTiles) !== this.numTiles) {
return true;
}
// Before we go analyzing the rooms, we can see if we're collapsed. If we're collapsed, we don't need
// to render anything. We do this after the height check though to ensure that the height gets appropriately
// calculated for when/if we become uncollapsed.
if (!nextState.isExpanded) {
return false;
}
// Quickly double check we're not about to break something due to the number of rooms changing.
if (this.state.rooms.length !== nextState.rooms.length) {
return true;
}
// Finally, determine if the room update (as presumably that's all that's left) is within
// our visible range. If it is, then do a render. If the update is outside our visible range
// then we can skip the update.
//
// We also optimize for order changing here: if the update did happen in our visible range
// but doesn't result in the list re-sorting itself then there's no reason for us to update
// on our own.
const prevSlicedRooms = this.state.rooms.slice(0, this.numVisibleTiles);
const nextSlicedRooms = nextState.rooms.slice(0, this.numVisibleTiles);
if (arrayHasOrderChange(prevSlicedRooms, nextSlicedRooms)) {
return true;
}
// Finally, nothing happened so no-op the update
return false;
}
public componentWillUnmount() {
defaultDispatcher.unregister(this.dispatcherRef);
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onListsUpdated);
}
private onListsUpdated = () => {
const stateUpdates: IState & any = {}; // &any is to avoid a cast on the initializer
if (this.props.extraBadTilesThatShouldntExist) {
const nameCondition = RoomListStore.instance.getFirstNameFilterCondition();
if (nameCondition) {
stateUpdates.filteredExtraTiles = this.props.extraBadTilesThatShouldntExist
.filter(t => nameCondition.matches(t.props.displayName || ""));
} else if (this.state.filteredExtraTiles) {
stateUpdates.filteredExtraTiles = null;
}
}
const currentRooms = this.state.rooms;
const newRooms = arrayFastClone(RoomListStore.instance.orderedLists[this.props.tagId] || []);
if (arrayHasOrderChange(currentRooms, newRooms)) {
stateUpdates.rooms = newRooms;
}
const isStillBeingFiltered = !!RoomListStore.instance.getFirstNameFilterCondition();
if (isStillBeingFiltered !== this.isBeingFiltered) {
this.isBeingFiltered = isStillBeingFiltered;
if (isStillBeingFiltered) {
stateUpdates.isExpanded = true;
} else {
stateUpdates.isExpanded = !this.layout.isCollapsed;
}
}
if (Object.keys(stateUpdates).length > 0) {
this.setState(stateUpdates);
}
};
private onAction = (payload: ActionPayload) => {
if (payload.action === "view_room" && payload.show_room_tile && this.props.rooms) {
if (payload.action === "view_room" && payload.show_room_tile && this.state.rooms) {
// XXX: we have to do this a tick later because we have incorrect intermediate props during a room change
// where we lose the room we are changing from temporarily and then it comes back in an update right after.
setImmediate(() => {
const roomIndex = this.props.rooms.findIndex((r) => r.roomId === payload.room_id);
const roomIndex = this.state.rooms.findIndex((r) => r.roomId === payload.room_id);
if (!this.state.isExpanded && roomIndex > -1) {
this.toggleCollapsed();
@ -302,12 +402,12 @@ export default class RoomSublist extends React.Component<IProps, IState> {
let room;
if (this.props.tagId === DefaultTagID.Invite) {
// switch to first room as that'll be the top of the list for the user
room = this.props.rooms && this.props.rooms[0];
room = this.state.rooms && this.state.rooms[0];
} else {
// find the first room with a count of the same colour as the badge count
room = this.props.rooms.find((r: Room) => {
const notifState = this.state.notificationState.getForRoom(r);
return notifState.count > 0 && notifState.color === this.state.notificationState.color;
room = this.state.rooms.find((r: Room) => {
const notifState = this.notificationState.getForRoom(r);
return notifState.count > 0 && notifState.color === this.notificationState.color;
});
}
@ -399,8 +499,8 @@ export default class RoomSublist extends React.Component<IProps, IState> {
const tiles: React.ReactElement[] = [];
if (this.props.rooms) {
const visibleRooms = this.props.rooms.slice(0, this.numVisibleTiles);
if (this.state.rooms) {
const visibleRooms = this.state.rooms.slice(0, this.numVisibleTiles);
for (const room of visibleRooms) {
tiles.push(
<RoomTile
@ -414,8 +514,9 @@ export default class RoomSublist extends React.Component<IProps, IState> {
}
}
if (this.props.extraBadTilesThatShouldntExist) {
tiles.push(...this.props.extraBadTilesThatShouldntExist);
if (this.extraTiles) {
// HACK: We break typing here, but this 'extra tiles' property shouldn't exist.
(tiles as any[]).push(...this.extraTiles);
}
// We only have to do this because of the extra tiles. We do it conditionally
@ -522,7 +623,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
const badge = (
<NotificationBadge
forceCount={true}
notification={this.state.notificationState}
notification={this.notificationState}
onClick={this.onBadgeClick}
tabIndex={tabIndex}
aria-label={ariaLabel}

View file

@ -17,12 +17,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {createRef} from "react";
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 dis from '../../../dispatcher/dispatcher';
import defaultDispatcher from '../../../dispatcher/dispatcher';
import { Key } from "../../../Keyboard";
import ActiveRoomObserver from "../../../ActiveRoomObserver";
import { _t } from "../../../languageHandler";
@ -30,49 +31,41 @@ import {
ChevronFace,
ContextMenu,
ContextMenuTooltipButton,
MenuItemRadio,
MenuItemCheckbox,
MenuItem,
MenuItemCheckbox,
MenuItemRadio,
} from "../../structures/ContextMenu";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
import { MessagePreviewStore, ROOM_PREVIEW_CHANGED } from "../../../stores/room-list/MessagePreviewStore";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import {
getRoomNotifsState,
setRoomNotifsState,
ALL_MESSAGES,
ALL_MESSAGES_LOUD,
MENTIONS_ONLY,
MUTE,
} from "../../../RoomNotifs";
import { ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE, } from "../../../RoomNotifs";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import NotificationBadge from "./NotificationBadge";
import { Volume } from "../../../RoomNotifsTypes";
import RoomListStore from "../../../stores/room-list/RoomListStore";
import RoomListActions from "../../../actions/RoomListActions";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import {ActionPayload} from "../../../dispatcher/payloads";
import { ActionPayload } from "../../../dispatcher/payloads";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import { NotificationState } from "../../../stores/notifications/NotificationState";
import { NOTIFICATION_STATE_UPDATE, NotificationState } from "../../../stores/notifications/NotificationState";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { EchoChamber } from "../../../stores/local-echo/EchoChamber";
import { CachedRoomKey, RoomEchoChamber } from "../../../stores/local-echo/RoomEchoChamber";
import { PROPERTY_UPDATED } from "../../../stores/local-echo/GenericEchoChamber";
interface IProps {
room: Room;
showMessagePreview: boolean;
isMinimized: boolean;
tag: TagID;
// TODO: Incoming call boxes: https://github.com/vector-im/riot-web/issues/14177
}
type PartialDOMRect = Pick<DOMRect, "left" | "bottom">;
interface IState {
hover: boolean;
notificationState: NotificationState;
selected: boolean;
notificationsMenuPosition: PartialDOMRect;
generalMenuPosition: PartialDOMRect;
messagePreview?: string;
}
const messagePreviewId = (roomId: string) => `mx_RoomTile_messagePreview_${roomId}`;
@ -111,25 +104,42 @@ const NotifOption: React.FC<INotifOptionProps> = ({active, onClick, iconClassNam
);
};
export default class RoomTile extends React.Component<IProps, IState> {
export default class RoomTile extends React.PureComponent<IProps, IState> {
private dispatcherRef: string;
private roomTileRef = createRef<HTMLDivElement>();
private notificationState: NotificationState;
private roomProps: RoomEchoChamber;
constructor(props: IProps) {
super(props);
this.state = {
hover: false,
notificationState: RoomNotificationStateStore.instance.getRoomState(this.props.room),
selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId,
notificationsMenuPosition: null,
generalMenuPosition: null,
// generatePreview() will return nothing if the user has previews disabled
messagePreview: this.generatePreview(),
};
ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate);
this.dispatcherRef = defaultDispatcher.register(this.onAction);
MessagePreviewStore.instance.on(ROOM_PREVIEW_CHANGED, this.onRoomPreviewChanged);
this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room);
this.notificationState.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
this.roomProps = EchoChamber.forRoom(this.props.room);
this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
}
private onNotificationUpdate = () => {
this.forceUpdate(); // notification state changed - update
};
private onRoomPropertyUpdate = (property: CachedRoomKey) => {
if (property === CachedRoomKey.NotificationVolume) this.onNotificationUpdate();
// else ignore - not important for this tile
};
private get showContextMenu(): boolean {
return !this.props.isMinimized && this.props.tag !== DefaultTagID.Invite;
}
@ -150,6 +160,8 @@ export default class RoomTile extends React.Component<IProps, IState> {
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
}
defaultDispatcher.unregister(this.dispatcherRef);
MessagePreviewStore.instance.off(ROOM_PREVIEW_CHANGED, this.onRoomPreviewChanged);
this.notificationState.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
}
private onAction = (payload: ActionPayload) => {
@ -160,6 +172,21 @@ export default class RoomTile extends React.Component<IProps, IState> {
}
};
private onRoomPreviewChanged = (room: Room) => {
if (this.props.room && room.roomId === this.props.room.roomId) {
// generatePreview() will return nothing if the user has previews disabled
this.setState({messagePreview: this.generatePreview()});
}
};
private generatePreview(): string | null {
if (!this.showMessagePreview) {
return null;
}
return MessagePreviewStore.instance.getPreviewForRoom(this.props.room, this.props.tag);
}
private scrollIntoView = () => {
if (!this.roomTileRef.current) return;
this.roomTileRef.current.scrollIntoView({
@ -168,14 +195,6 @@ export default class RoomTile extends React.Component<IProps, IState> {
});
};
private onTileMouseEnter = () => {
this.setState({hover: true});
};
private onTileMouseLeave = () => {
this.setState({hover: false});
};
private onTileClick = (ev: React.KeyboardEvent) => {
ev.preventDefault();
ev.stopPropagation();
@ -292,17 +311,9 @@ export default class RoomTile extends React.Component<IProps, IState> {
ev.stopPropagation();
if (MatrixClientPeg.get().isGuest()) return;
// get key before we go async and React discards the nativeEvent
const key = (ev as React.KeyboardEvent).key;
try {
// TODO add local echo - https://github.com/vector-im/riot-web/issues/14280
await setRoomNotifsState(this.props.room.roomId, newState);
} catch (error) {
// TODO: some form of error notification to the user to inform them that their state change failed.
// See https://github.com/vector-im/riot-web/issues/14281
console.error(error);
}
this.roomProps.notificationVolume = newState;
const key = (ev as React.KeyboardEvent).key;
if (key === Key.ENTER) {
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
this.setState({notificationsMenuPosition: null}); // hide the menu
@ -320,7 +331,7 @@ export default class RoomTile extends React.Component<IProps, IState> {
return null;
}
const state = getRoomNotifsState(this.props.room.roomId);
const state = this.roomProps.notificationVolume;
let contextMenu = null;
if (this.state.notificationsMenuPosition) {
@ -482,7 +493,7 @@ export default class RoomTile extends React.Component<IProps, IState> {
badge = (
<div className="mx_RoomTile_badgeContainer" aria-hidden="true">
<NotificationBadge
notification={this.state.notificationState}
notification={this.notificationState}
forceCount={false}
roomId={this.props.room.roomId}
/>
@ -495,24 +506,18 @@ export default class RoomTile extends React.Component<IProps, IState> {
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
let messagePreview = null;
if (this.showMessagePreview) {
// The preview store heavily caches this info, so should be safe to hammer.
const text = MessagePreviewStore.instance.getPreviewForRoom(this.props.room, this.props.tag);
// Only show the preview if there is one to show.
if (text) {
messagePreview = (
<div className="mx_RoomTile_messagePreview" id={messagePreviewId(this.props.room.roomId)}>
{text}
</div>
);
}
if (this.showMessagePreview && this.state.messagePreview) {
messagePreview = (
<div className="mx_RoomTile_messagePreview" id={messagePreviewId(this.props.room.roomId)}>
{this.state.messagePreview}
</div>
);
}
const nameClasses = classNames({
"mx_RoomTile_name": true,
"mx_RoomTile_nameWithPreview": !!messagePreview,
"mx_RoomTile_nameHasUnreadEvents": this.state.notificationState.isUnread,
"mx_RoomTile_nameHasUnreadEvents": this.notificationState.isUnread,
});
let nameContainer = (
@ -529,15 +534,15 @@ export default class RoomTile extends React.Component<IProps, IState> {
// The following labels are written in such a fashion to increase screen reader efficiency (speed).
if (this.props.tag === DefaultTagID.Invite) {
// append nothing
} else if (this.state.notificationState.hasMentions) {
} else if (this.notificationState.hasMentions) {
ariaLabel += " " + _t("%(count)s unread messages including mentions.", {
count: this.state.notificationState.count,
count: this.notificationState.count,
});
} else if (this.state.notificationState.hasUnreadCount) {
} else if (this.notificationState.hasUnreadCount) {
ariaLabel += " " + _t("%(count)s unread messages.", {
count: this.state.notificationState.count,
count: this.notificationState.count,
});
} else if (this.state.notificationState.isUnread) {
} else if (this.notificationState.isUnread) {
ariaLabel += " " + _t("Unread messages.");
}
@ -560,8 +565,6 @@ export default class RoomTile extends React.Component<IProps, IState> {
tabIndex={isActive ? 0 : -1}
inputRef={ref}
className={classes}
onMouseEnter={this.onTileMouseEnter}
onMouseLeave={this.onTileMouseLeave}
onClick={this.onTileClick}
onContextMenu={this.onContextMenu}
role="treeitem"

View file

@ -29,6 +29,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import {ContextMenu} from "../../structures/ContextMenu";
import {WidgetType} from "../../../widgets/WidgetType";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import {Action} from "../../../dispatcher/actions";
// This should be below the dialog level (4000), but above the rest of the UI (1000-2000).
// We sit in a context menu, so this should be given to the context menu.
@ -182,7 +183,7 @@ export default class Stickerpicker extends React.Component {
case "stickerpicker_close":
this.setState({showStickers: false});
break;
case "after_right_panel_phase_change":
case Action.AfterRightPanelPhaseChange:
case "show_left_panel":
case "hide_left_panel":
this.setState({showStickers: false});

View file

@ -18,7 +18,7 @@ import React from 'react';
import * as sdk from '../../../index';
import {_t} from "../../../languageHandler";
import {SettingLevel} from "../../../settings/SettingsStore";
import {SettingLevel} from "../../../settings/SettingLevel";
const SETTING_MANUALLY_VERIFY_ALL_SESSIONS = "e2ee.manuallyVerifyAllSessions";

View file

@ -20,10 +20,11 @@ import { _t } from '../../../languageHandler';
import SdkConfig from "../../../SdkConfig";
import * as sdk from '../../../index';
import Modal from '../../../Modal';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from "../elements/AccessibleButton";
import {formatBytes, formatCountLong} from "../../../utils/FormattingUtils";
import EventIndexPeg from "../../../indexing/EventIndexPeg";
import {SettingLevel} from "../../../settings/SettingLevel";
export default class EventIndexPanel extends React.Component {
constructor() {

View file

@ -20,7 +20,7 @@ import createReactClass from 'create-react-class';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import SettingsStore, {SettingLevel} from '../../../settings/SettingsStore';
import SettingsStore from '../../../settings/SettingsStore';
import Modal from '../../../Modal';
import {
NotificationUtils,
@ -31,6 +31,7 @@ import {
import SdkConfig from "../../../SdkConfig";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import AccessibleButton from "../elements/AccessibleButton";
import {SettingLevel} from "../../../settings/SettingLevel";
// TODO: this "view" component still has far too much application logic in it,
// which should be factored out to other files.

View file

@ -18,7 +18,8 @@ import React from 'react';
import {_t} from "../../../languageHandler";
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import * as sdk from '../../../index';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import SettingsStore from "../../../settings/SettingsStore";
import {SettingLevel} from "../../../settings/SettingLevel";
export default class SetIntegrationManager extends React.Component {
constructor() {

View file

@ -21,7 +21,7 @@ import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import AccessibleButton from "../../../elements/AccessibleButton";
import Notifier from "../../../../../Notifier";
import SettingsStore from '../../../../../settings/SettingsStore';
import { SettingLevel } from '../../../../../settings/SettingsStore';
import {SettingLevel} from "../../../../../settings/SettingLevel";
export default class NotificationsSettingsTab extends React.Component {
static propTypes = {

View file

@ -20,10 +20,10 @@ import {_t} from "../../../../../languageHandler";
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import * as sdk from "../../../../..";
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
import {SettingLevel} from "../../../../../settings/SettingsStore";
import Modal from "../../../../../Modal";
import QuestionDialog from "../../../dialogs/QuestionDialog";
import StyledRadioGroup from '../../../elements/StyledRadioGroup';
import {SettingLevel} from "../../../../../settings/SettingLevel";
export default class SecurityRoomSettingsTab extends React.Component {
static propTypes = {

View file

@ -18,7 +18,7 @@ limitations under the License.
import React from 'react';
import {_t} from "../../../../../languageHandler";
import SdkConfig from "../../../../../SdkConfig";
import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore";
import SettingsStore from "../../../../../settings/SettingsStore";
import { enumerateThemes } from "../../../../../theme";
import ThemeWatcher from "../../../../../settings/watchers/ThemeWatcher";
import Slider from "../../../elements/Slider";
@ -35,6 +35,7 @@ import Field from '../../../elements/Field';
import EventTilePreview from '../../../elements/EventTilePreview';
import StyledRadioGroup from "../../../elements/StyledRadioGroup";
import classNames from 'classnames';
import { SettingLevel } from "../../../../../settings/SettingLevel";
interface IProps {
}

View file

@ -20,7 +20,6 @@ import React from 'react';
import {_t} from "../../../../../languageHandler";
import ProfileSettings from "../../ProfileSettings";
import * as languageHandler from "../../../../../languageHandler";
import {SettingLevel} from "../../../../../settings/SettingsStore";
import SettingsStore from "../../../../../settings/SettingsStore";
import LanguageDropdown from "../../../elements/LanguageDropdown";
import AccessibleButton from "../../../elements/AccessibleButton";
@ -37,6 +36,7 @@ import IdentityAuthClient from "../../../../../IdentityAuthClient";
import {abbreviateUrl} from "../../../../../utils/UrlUtils";
import { getThreepidsWithBindStatus } from '../../../../../boundThreepids';
import Spinner from "../../../elements/Spinner";
import {SettingLevel} from "../../../../../settings/SettingLevel";
export default class GeneralUserSettingsTab extends React.Component {
static propTypes = {

View file

@ -17,9 +17,10 @@ limitations under the License.
import React from 'react';
import {_t} from "../../../../../languageHandler";
import PropTypes from "prop-types";
import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore";
import SettingsStore from "../../../../../settings/SettingsStore";
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
import * as sdk from "../../../../../index";
import {SettingLevel} from "../../../../../settings/SettingLevel";
export class LabsSettingToggle extends React.Component {
static propTypes = {

View file

@ -17,12 +17,12 @@ limitations under the License.
import React from 'react';
import {_t} from "../../../../../languageHandler";
import {SettingLevel} from "../../../../../settings/SettingsStore";
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
import SettingsStore from "../../../../../settings/SettingsStore";
import Field from "../../../elements/Field";
import * as sdk from "../../../../..";
import PlatformPeg from "../../../../../PlatformPeg";
import {SettingLevel} from "../../../../../settings/SettingLevel";
export default class PreferencesUserSettingsTab extends React.Component {
static ROOM_LIST_SETTINGS = [

View file

@ -19,7 +19,6 @@ import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../../../languageHandler";
import SdkConfig from "../../../../../SdkConfig";
import {SettingLevel} from "../../../../../settings/SettingsStore";
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import * as FormattingUtils from "../../../../../utils/FormattingUtils";
import AccessibleButton from "../../../elements/AccessibleButton";
@ -29,6 +28,7 @@ import * as sdk from "../../../../..";
import {sleep} from "../../../../../utils/promise";
import dis from "../../../../../dispatcher/dispatcher";
import {privateShouldBeEncrypted} from "../../../../../createRoom";
import {SettingLevel} from "../../../../../settings/SettingLevel";
export class IgnoredUser extends React.Component {
static propTypes = {

View file

@ -21,10 +21,10 @@ import SdkConfig from "../../../../../SdkConfig";
import CallMediaHandler from "../../../../../CallMediaHandler";
import Field from "../../../elements/Field";
import AccessibleButton from "../../../elements/AccessibleButton";
import {SettingLevel} from "../../../../../settings/SettingsStore";
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import * as sdk from "../../../../../index";
import Modal from "../../../../../Modal";
import {SettingLevel} from "../../../../../settings/SettingLevel";
export default class VoiceUserSettingsTab extends React.Component {
constructor() {

View file

@ -0,0 +1,40 @@
/*
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 { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
import Modal from "../../../Modal";
import ServerOfflineDialog from "../dialogs/ServerOfflineDialog";
export default class NonUrgentEchoFailureToast extends React.PureComponent {
private openDialog = () => {
Modal.createTrackedDialog('Local Echo Server Error', '', ServerOfflineDialog, {});
};
public render() {
return (
<div className="mx_NonUrgentEchoFailureToast">
<span className="mx_NonUrgentEchoFailureToast_icon" />
{_t("Your server isn't responding to some <a>requests</a>.", {}, {
'a': (sub) => (
<AccessibleButton kind="link" onClick={this.openDialog}>{sub}</AccessibleButton>
),
})}
</div>
)
}
}

View file

@ -19,7 +19,8 @@ import React from "react";
import * as sdk from "../../../index";
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload"
import {userLabelForEventRoom} from "../../../utils/KeyVerificationStateObserver";
import dis from "../../../dispatcher/dispatcher";
import ToastStore from "../../../stores/ToastStore";
@ -27,6 +28,7 @@ import Modal from "../../../Modal";
import GenericToast from "./GenericToast";
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import {DeviceInfo} from "matrix-js-sdk/src/crypto/deviceinfo";
import {Action} from "../../../dispatcher/actions";
interface IProps {
toastKey: string;
@ -104,9 +106,9 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
room_id: request.channel.roomId,
should_peek: false,
});
dis.dispatch({
action: "set_right_panel_phase",
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
dis.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.EncryptionPanel,
refireParams: {
verificationRequest: request,
member: cli.getUser(request.otherUserId),