Merge branches 'develop' and 't3chguy/ts/2' of github.com:matrix-org/matrix-react-sdk into t3chguy/ts/2

This commit is contained in:
Michael Telatynski 2020-07-20 17:07:18 +01:00
commit 9fca422de7
381 changed files with 5751 additions and 15130 deletions

View file

@ -38,12 +38,13 @@ export default class AutoHideScrollbar extends React.Component {
render() {
return (<div
ref={this._collectContainerRef}
style={this.props.style}
className={["mx_AutoHideScrollbar", this.props.className].join(" ")}
onScroll={this.props.onScroll}
onWheel={this.props.onWheel}
>
ref={this._collectContainerRef}
style={this.props.style}
className={["mx_AutoHideScrollbar", this.props.className].join(" ")}
onScroll={this.props.onScroll}
onWheel={this.props.onWheel}
tabIndex={this.props.tabIndex}
>
{ this.props.children }
</div>);
}

View file

@ -1,7 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 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.
@ -20,6 +20,7 @@ import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { _t } from '../../languageHandler';
import SdkConfig from '../../SdkConfig';
export default createReactClass({
displayName: 'CompatibilityPage',
@ -38,14 +39,25 @@ export default createReactClass({
},
render: function() {
const brand = SdkConfig.get().brand;
return (
<div className="mx_CompatibilityPage">
<div className="mx_CompatibilityPage_box">
<p>{ _t("Sorry, your browser is <b>not</b> able to run Riot.", {}, { 'b': (sub) => <b>{sub}</b> }) } </p>
<p>{_t(
"Sorry, your browser is <b>not</b> able to run %(brand)s.",
{
brand,
},
{
'b': (sub) => <b>{sub}</b>,
})
}</p>
<p>
{ _t(
"Riot uses many advanced browser features, some of which are not available " +
"%(brand)s uses many advanced browser features, some of which are not available " +
"or experimental in your current browser.",
{ brand },
) }
</p>
<p>

View file

@ -233,6 +233,9 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
switch (ev.key) {
case Key.TAB:
case Key.ESCAPE:
// close on left and right arrows too for when it is a context menu on a <Toolbar />
case Key.ARROW_LEFT:
case Key.ARROW_RIGHT:
this.props.onFinished();
break;
case Key.ARROW_UP:
@ -458,6 +461,7 @@ export function createMenu(ElementClass, props) {
// re-export the semantic helper components for simplicity
export {ContextMenuButton} from "../../accessibility/context_menu/ContextMenuButton";
export {ContextMenuTooltipButton} from "../../accessibility/context_menu/ContextMenuTooltipButton";
export {MenuGroup} from "../../accessibility/context_menu/MenuGroup";
export {MenuItem} from "../../accessibility/context_menu/MenuItem";
export {MenuItemCheckbox} from "../../accessibility/context_menu/MenuItemCheckbox";

View file

@ -38,7 +38,7 @@ const HomePage = () => {
}
const brandingConfig = config.branding;
let logoUrl = "themes/riot/img/logos/riot-logo.svg";
let logoUrl = "themes/element/img/logos/element-logo.svg";
if (brandingConfig && brandingConfig.authHeaderLogoUrl) {
logoUrl = brandingConfig.authHeaderLogoUrl;
}
@ -46,7 +46,7 @@ const HomePage = () => {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
return <AutoHideScrollbar className="mx_HomePage mx_HomePage_default">
<div className="mx_HomePage_default_wrapper">
<img src={logoUrl} alt="Riot" />
<img src={logoUrl} alt={config.brand || "Riot"} />
<h1>{ _t("Welcome to %(appName)s", { appName: config.brand || "Riot" }) }</h1>
<h4>{ _t("Liberate your communication") }</h4>
<div className="mx_HomePage_default_buttons">

View file

@ -192,7 +192,7 @@ export default class IndicatorScrollbar extends React.Component {
ref={this._collectScrollerComponent}
wrappedRef={this._collectScroller}
onWheel={this.onMouseWheel}
{... this.props}
{...this.props}
>
{ leftOverflowIndicator }
{ this.props.children }

View file

@ -1,305 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket 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 createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Key } from '../../Keyboard';
import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher';
import * as VectorConferenceHandler from '../../VectorConferenceHandler';
import SettingsStore from '../../settings/SettingsStore';
import {_t} from "../../languageHandler";
import Analytics from "../../Analytics";
import {Action} from "../../dispatcher/actions";
const LeftPanel = createReactClass({
displayName: 'LeftPanel',
// NB. If you add props, don't forget to update
// shouldComponentUpdate!
propTypes: {
collapsed: PropTypes.bool.isRequired,
},
getInitialState: function() {
return {
searchFilter: '',
breadcrumbs: false,
};
},
// TODO: [REACT-WARNING] Move this to constructor
UNSAFE_componentWillMount: function() {
this.focusedElement = null;
this._breadcrumbsWatcherRef = SettingsStore.watchSetting(
"breadcrumbs", null, this._onBreadcrumbsChanged);
this._tagPanelWatcherRef = SettingsStore.watchSetting(
"TagPanel.enableTagPanel", null, () => this.forceUpdate());
const useBreadcrumbs = !!SettingsStore.getValue("breadcrumbs");
Analytics.setBreadcrumbs(useBreadcrumbs);
this.setState({breadcrumbs: useBreadcrumbs});
},
componentWillUnmount: function() {
SettingsStore.unwatchSetting(this._breadcrumbsWatcherRef);
SettingsStore.unwatchSetting(this._tagPanelWatcherRef);
},
shouldComponentUpdate: function(nextProps, nextState) {
// MatrixChat will update whenever the user switches
// rooms, but propagating this change all the way down
// the react tree is quite slow, so we cut this off
// here. The RoomTiles listen for the room change
// events themselves to know when to update.
// We just need to update if any of these things change.
if (
this.props.collapsed !== nextProps.collapsed ||
this.props.disabled !== nextProps.disabled
) {
return true;
}
if (this.state.searchFilter !== nextState.searchFilter) {
return true;
}
if (this.state.searchExpanded !== nextState.searchExpanded) {
return true;
}
return false;
},
componentDidUpdate(prevProps, prevState) {
if (prevState.breadcrumbs !== this.state.breadcrumbs) {
Analytics.setBreadcrumbs(this.state.breadcrumbs);
}
},
_onBreadcrumbsChanged: function(settingName, roomId, level, valueAtLevel, value) {
// Features are only possible at a single level, so we can get away with using valueAtLevel.
// The SettingsStore runs on the same tick as the update, so `value` will be wrong.
this.setState({breadcrumbs: valueAtLevel});
// For some reason the setState doesn't trigger a render of the component, so force one.
// Probably has to do with the change happening outside of a change detector cycle.
this.forceUpdate();
},
_onFocus: function(ev) {
this.focusedElement = ev.target;
},
_onBlur: function(ev) {
this.focusedElement = null;
},
_onFilterKeyDown: function(ev) {
if (!this.focusedElement) return;
switch (ev.key) {
// On enter of rooms filter select and activate first room if such one exists
case Key.ENTER: {
const firstRoom = ev.target.closest(".mx_LeftPanel").querySelector(".mx_RoomTile");
if (firstRoom) {
firstRoom.click();
}
break;
}
}
},
_onKeyDown: function(ev) {
if (!this.focusedElement) return;
switch (ev.key) {
case Key.ARROW_UP:
this._onMoveFocus(ev, true, true);
break;
case Key.ARROW_DOWN:
this._onMoveFocus(ev, false, true);
break;
}
},
_onMoveFocus: function(ev, up, trap) {
let element = this.focusedElement;
// unclear why this isn't needed
// var descending = (up == this.focusDirection) ? this.focusDescending : !this.focusDescending;
// this.focusDirection = up;
let descending = false; // are we currently descending or ascending through the DOM tree?
let classes;
do {
const child = up ? element.lastElementChild : element.firstElementChild;
const sibling = up ? element.previousElementSibling : element.nextElementSibling;
if (descending) {
if (child) {
element = child;
} else if (sibling) {
element = sibling;
} else {
descending = false;
element = element.parentElement;
}
} else {
if (sibling) {
element = sibling;
descending = true;
} else {
element = element.parentElement;
}
}
if (element) {
classes = element.classList;
}
} while (element && !(
classes.contains("mx_RoomTile") ||
classes.contains("mx_RoomSubList_label") ||
classes.contains("mx_LeftPanel_filterRooms")));
if (element) {
ev.stopPropagation();
ev.preventDefault();
element.focus();
this.focusedElement = element;
} else if (trap) {
// if navigation is via up/down arrow-keys, trap in the widget so it doesn't send to composer
ev.stopPropagation();
ev.preventDefault();
}
},
onSearch: function(term) {
this.setState({ searchFilter: term });
},
onSearchCleared: function(source) {
if (source === "keyboard") {
dis.fire(Action.FocusComposer);
}
this.setState({searchExpanded: false});
},
collectRoomList: function(ref) {
this._roomList = ref;
},
_onSearchFocus: function() {
this.setState({searchExpanded: true});
},
_onSearchBlur: function(event) {
if (event.target.value.length === 0) {
this.setState({searchExpanded: false});
}
},
render: function() {
const RoomList = sdk.getComponent('rooms.RoomList');
const RoomBreadcrumbs = sdk.getComponent('rooms.RoomBreadcrumbs');
const TagPanel = sdk.getComponent('structures.TagPanel');
const CustomRoomTagPanel = sdk.getComponent('structures.CustomRoomTagPanel');
const TopLeftMenuButton = sdk.getComponent('structures.TopLeftMenuButton');
const SearchBox = sdk.getComponent('structures.SearchBox');
const CallPreview = sdk.getComponent('voip.CallPreview');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const tagPanelEnabled = SettingsStore.getValue("TagPanel.enableTagPanel");
let tagPanelContainer;
const isCustomTagsEnabled = SettingsStore.isFeatureEnabled("feature_custom_tags");
if (tagPanelEnabled) {
tagPanelContainer = (<div className="mx_LeftPanel_tagPanelContainer">
<TagPanel />
{ isCustomTagsEnabled ? <CustomRoomTagPanel /> : undefined }
</div>);
}
const containerClasses = classNames(
"mx_LeftPanel_container", "mx_fadable",
{
"collapsed": this.props.collapsed,
"mx_LeftPanel_container_hasTagPanel": tagPanelEnabled,
"mx_fadable_faded": this.props.disabled,
},
);
let exploreButton;
if (!this.props.collapsed) {
exploreButton = (
<div className={classNames("mx_LeftPanel_explore", {"mx_LeftPanel_explore_hidden": this.state.searchExpanded})}>
<AccessibleButton onClick={() => dis.fire(Action.ViewRoomDirectory)}>{_t("Explore")}</AccessibleButton>
</div>
);
}
const searchBox = (<SearchBox
className="mx_LeftPanel_filterRooms"
enableRoomSearchFocus={true}
blurredPlaceholder={ _t('Filter') }
placeholder={ _t('Filter rooms…') }
onKeyDown={this._onFilterKeyDown}
onSearch={ this.onSearch }
onCleared={ this.onSearchCleared }
onFocus={this._onSearchFocus}
onBlur={this._onSearchBlur}
collapsed={this.props.collapsed} />);
let breadcrumbs;
if (this.state.breadcrumbs) {
breadcrumbs = (<RoomBreadcrumbs collapsed={this.props.collapsed} />);
}
const roomList = <RoomList
onKeyDown={this._onKeyDown}
onFocus={this._onFocus}
onBlur={this._onBlur}
ref={this.collectRoomList}
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapsed}
searchFilter={this.state.searchFilter}
ConferenceHandler={VectorConferenceHandler} />;
return (
<div className={containerClasses}>
{ tagPanelContainer }
<aside className="mx_LeftPanel dark-panel">
<TopLeftMenuButton collapsed={this.props.collapsed} />
{ breadcrumbs }
<CallPreview ConferenceHandler={VectorConferenceHandler} />
<div className="mx_LeftPanel_exploreAndFilterRow" onKeyDown={this._onKeyDown} onFocus={this._onFocus} onBlur={this._onBlur}>
{ exploreButton }
{ searchBox }
</div>
{roomList}
</aside>
</div>
);
},
});
export default LeftPanel;

View file

@ -20,22 +20,20 @@ import TagPanel from "./TagPanel";
import classNames from "classnames";
import dis from "../../dispatcher/dispatcher";
import { _t } from "../../languageHandler";
import RoomList2 from "../views/rooms/RoomList2";
import { HEADER_HEIGHT } from "../views/rooms/RoomSublist2";
import RoomList from "../views/rooms/RoomList";
import { HEADER_HEIGHT } from "../views/rooms/RoomSublist";
import { Action } from "../../dispatcher/actions";
import UserMenu from "./UserMenu";
import RoomSearch from "./RoomSearch";
import AccessibleButton from "../views/elements/AccessibleButton";
import RoomBreadcrumbs2 from "../views/rooms/RoomBreadcrumbs2";
import RoomBreadcrumbs from "../views/rooms/RoomBreadcrumbs";
import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore";
import ResizeNotifier from "../../utils/ResizeNotifier";
import SettingsStore from "../../settings/SettingsStore";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore2";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore";
import {Key} from "../../Keyboard";
import IndicatorScrollbar from "../structures/IndicatorScrollbar";
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
interface IProps {
isMinimized: boolean;
@ -52,12 +50,12 @@ interface IState {
const cssClasses = [
"mx_RoomSearch_input",
"mx_RoomSearch_icon", // minimized <RoomSearch />
"mx_RoomSublist2_headerText",
"mx_RoomTile2",
"mx_RoomSublist2_showNButton",
"mx_RoomSublist_headerText",
"mx_RoomTile",
"mx_RoomSublist_showNButton",
];
export default class LeftPanel2 extends React.Component<IProps, IState> {
export default class LeftPanel extends React.Component<IProps, IState> {
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
private tagPanelWatcherRef: string;
private focusedElement = null;
@ -121,7 +119,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
private doStickyHeaders(list: HTMLDivElement) {
const topEdge = list.scrollTop;
const bottomEdge = list.offsetHeight + list.scrollTop;
const sublists = list.querySelectorAll<HTMLDivElement>(".mx_RoomSublist2");
const sublists = list.querySelectorAll<HTMLDivElement>(".mx_RoomSublist");
const headerRightMargin = 16; // calculated from margins and widths to align with non-sticky tiles
const headerStickyWidth = list.clientWidth - headerRightMargin;
@ -137,7 +135,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
let lastTopHeader;
let firstBottomHeader;
for (const sublist of sublists) {
const header = sublist.querySelector<HTMLDivElement>(".mx_RoomSublist2_stickable");
const header = sublist.querySelector<HTMLDivElement>(".mx_RoomSublist_stickable");
header.style.removeProperty("display"); // always clear display:none first
// When an element is <=40% off screen, make it take over
@ -173,8 +171,8 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
}
if (style.stickyTop) {
if (!header.classList.contains("mx_RoomSublist2_headerContainer_stickyTop")) {
header.classList.add("mx_RoomSublist2_headerContainer_stickyTop");
if (!header.classList.contains("mx_RoomSublist_headerContainer_stickyTop")) {
header.classList.add("mx_RoomSublist_headerContainer_stickyTop");
}
const newTop = `${list.parentElement.offsetTop}px`;
@ -182,8 +180,8 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
header.style.top = newTop;
}
} else {
if (header.classList.contains("mx_RoomSublist2_headerContainer_stickyTop")) {
header.classList.remove("mx_RoomSublist2_headerContainer_stickyTop");
if (header.classList.contains("mx_RoomSublist_headerContainer_stickyTop")) {
header.classList.remove("mx_RoomSublist_headerContainer_stickyTop");
}
if (header.style.top) {
header.style.removeProperty('top');
@ -191,18 +189,18 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
}
if (style.stickyBottom) {
if (!header.classList.contains("mx_RoomSublist2_headerContainer_stickyBottom")) {
header.classList.add("mx_RoomSublist2_headerContainer_stickyBottom");
if (!header.classList.contains("mx_RoomSublist_headerContainer_stickyBottom")) {
header.classList.add("mx_RoomSublist_headerContainer_stickyBottom");
}
} else {
if (header.classList.contains("mx_RoomSublist2_headerContainer_stickyBottom")) {
header.classList.remove("mx_RoomSublist2_headerContainer_stickyBottom");
if (header.classList.contains("mx_RoomSublist_headerContainer_stickyBottom")) {
header.classList.remove("mx_RoomSublist_headerContainer_stickyBottom");
}
}
if (style.stickyTop || style.stickyBottom) {
if (!header.classList.contains("mx_RoomSublist2_headerContainer_sticky")) {
header.classList.add("mx_RoomSublist2_headerContainer_sticky");
if (!header.classList.contains("mx_RoomSublist_headerContainer_sticky")) {
header.classList.add("mx_RoomSublist_headerContainer_sticky");
}
const newWidth = `${headerStickyWidth}px`;
@ -210,8 +208,8 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
header.style.width = newWidth;
}
} else if (!style.stickyTop && !style.stickyBottom) {
if (header.classList.contains("mx_RoomSublist2_headerContainer_sticky")) {
header.classList.remove("mx_RoomSublist2_headerContainer_sticky");
if (header.classList.contains("mx_RoomSublist_headerContainer_sticky")) {
header.classList.remove("mx_RoomSublist_headerContainer_sticky");
}
if (header.style.width) {
header.style.removeProperty('width');
@ -221,16 +219,16 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
// add appropriate sticky classes to wrapper so it has
// the necessary top/bottom padding to put the sticky header in
const listWrapper = list.parentElement; // .mx_LeftPanel2_roomListWrapper
const listWrapper = list.parentElement; // .mx_LeftPanel_roomListWrapper
if (lastTopHeader) {
listWrapper.classList.add("mx_LeftPanel2_roomListWrapper_stickyTop");
listWrapper.classList.add("mx_LeftPanel_roomListWrapper_stickyTop");
} else {
listWrapper.classList.remove("mx_LeftPanel2_roomListWrapper_stickyTop");
listWrapper.classList.remove("mx_LeftPanel_roomListWrapper_stickyTop");
}
if (firstBottomHeader) {
listWrapper.classList.add("mx_LeftPanel2_roomListWrapper_stickyBottom");
listWrapper.classList.add("mx_LeftPanel_roomListWrapper_stickyBottom");
} else {
listWrapper.classList.remove("mx_LeftPanel2_roomListWrapper_stickyBottom");
listWrapper.classList.remove("mx_LeftPanel_roomListWrapper_stickyBottom");
}
}
@ -266,10 +264,10 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
};
private onEnter = () => {
const firstRoom = this.listContainerRef.current.querySelector<HTMLDivElement>(".mx_RoomTile2");
const firstRoom = this.listContainerRef.current.querySelector<HTMLDivElement>(".mx_RoomTile");
if (firstRoom) {
firstRoom.click();
this.onSearch(""); // clear the search field
return true; // to get the field to clear
}
};
@ -313,30 +311,33 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
};
private renderHeader(): React.ReactNode {
let breadcrumbs;
return (
<div className="mx_LeftPanel_userHeader">
<UserMenu isMinimized={this.props.isMinimized} />
</div>
);
}
private renderBreadcrumbs(): React.ReactNode {
if (this.state.showBreadcrumbs && !this.props.isMinimized) {
breadcrumbs = (
return (
<IndicatorScrollbar
className="mx_LeftPanel2_headerRow mx_LeftPanel2_breadcrumbsContainer mx_AutoHideScrollbar"
className="mx_LeftPanel_breadcrumbsContainer mx_AutoHideScrollbar"
verticalScrollsHorizontally={true}
// Firefox sometimes makes this element focusable due to
// overflow:scroll;, so force it out of tab order.
tabIndex={-1}
>
<RoomBreadcrumbs2 />
<RoomBreadcrumbs />
</IndicatorScrollbar>
);
}
return (
<div className="mx_LeftPanel2_userHeader">
<UserMenu isMinimized={this.props.isMinimized} />
{breadcrumbs}
</div>
);
}
private renderSearchExplore(): React.ReactNode {
return (
<div
className="mx_LeftPanel2_filterContainer"
className="mx_LeftPanel_filterContainer"
onFocus={this.onFocus}
onBlur={this.onBlur}
onKeyDown={this.onKeyDown}
@ -347,8 +348,8 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
onVerticalArrow={this.onKeyDown}
onEnter={this.onEnter}
/>
<AccessibleButton
className="mx_LeftPanel2_exploreButton"
<AccessibleTooltipButton
className="mx_LeftPanel_exploreButton"
onClick={this.onExplore}
title={_t("Explore rooms")}
/>
@ -358,12 +359,12 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
public render(): React.ReactNode {
const tagPanel = !this.state.showTagPanel ? null : (
<div className="mx_LeftPanel2_tagPanelContainer">
<div className="mx_LeftPanel_tagPanelContainer">
<TagPanel/>
</div>
);
const roomList = <RoomList2
const roomList = <RoomList
onKeyDown={this.onKeyDown}
resizeNotifier={null}
collapsed={false}
@ -375,23 +376,24 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
/>;
const containerClasses = classNames({
"mx_LeftPanel2": true,
"mx_LeftPanel2_hasTagPanel": !!tagPanel,
"mx_LeftPanel2_minimized": this.props.isMinimized,
"mx_LeftPanel": true,
"mx_LeftPanel_hasTagPanel": !!tagPanel,
"mx_LeftPanel_minimized": this.props.isMinimized,
});
const roomListClasses = classNames(
"mx_LeftPanel2_actualRoomListContainer",
"mx_LeftPanel_actualRoomListContainer",
"mx_AutoHideScrollbar",
);
return (
<div className={containerClasses}>
{tagPanel}
<aside className="mx_LeftPanel2_roomListContainer">
<aside className="mx_LeftPanel_roomListContainer">
{this.renderHeader()}
{this.renderSearchExplore()}
<div className="mx_LeftPanel2_roomListWrapper">
{this.renderBreadcrumbs()}
<div className="mx_LeftPanel_roomListWrapper">
<div
className={roomListClasses}
onScroll={this.onScroll}

View file

@ -40,7 +40,6 @@ import * as KeyboardShortcuts from "../../accessibility/KeyboardShortcuts";
import HomePage from "./HomePage";
import ResizeNotifier from "../../utils/ResizeNotifier";
import PlatformPeg from "../../PlatformPeg";
import { RoomListStoreTempProxy } from "../../stores/room-list/RoomListStoreTempProxy";
import { DefaultTagID } from "../../stores/room-list/models";
import {
showToast as showSetPasswordToast,
@ -51,9 +50,10 @@ import {
hideToast as hideServerLimitToast
} from "../../toasts/ServerLimitToast";
import { Action } from "../../dispatcher/actions";
import LeftPanel2 from "./LeftPanel2";
import LeftPanel from "./LeftPanel";
import CallContainer from '../views/voip/CallContainer';
import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload";
import RoomListStore from "../../stores/room-list/RoomListStore";
// 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.
@ -308,8 +308,8 @@ class LoggedInView extends React.Component<IProps, IState> {
};
onRoomStateEvents = (ev, state) => {
const roomLists = RoomListStoreTempProxy.getRoomLists();
if (roomLists[DefaultTagID.ServerNotice] && roomLists[DefaultTagID.ServerNotice].some(r => r.roomId === ev.getRoomId())) {
const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice];
if (serverNoticeList && serverNoticeList.some(r => r.roomId === ev.getRoomId())) {
this._updateServerNoticeEvents();
}
};
@ -328,11 +328,11 @@ class LoggedInView extends React.Component<IProps, IState> {
}
_updateServerNoticeEvents = async () => {
const roomLists = RoomListStoreTempProxy.getRoomLists();
if (!roomLists[DefaultTagID.ServerNotice]) return [];
const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice];
if (!serverNoticeList) return [];
const events = [];
for (const room of roomLists[DefaultTagID.ServerNotice]) {
for (const room of serverNoticeList) {
const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue;
@ -607,7 +607,6 @@ class LoggedInView extends React.Component<IProps, IState> {
};
render() {
const LeftPanel = sdk.getComponent('structures.LeftPanel');
const RoomView = sdk.getComponent('structures.RoomView');
const UserView = sdk.getComponent('structures.UserView');
const GroupView = sdk.getComponent('structures.GroupView');
@ -661,21 +660,12 @@ class LoggedInView extends React.Component<IProps, IState> {
bodyClasses += ' mx_MatrixChat_useCompactLayout';
}
let leftPanel = (
const leftPanel = (
<LeftPanel
isMinimized={this.props.collapseLhs || false}
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapseLhs || false}
disabled={this.props.leftDisabled}
/>
);
if (SettingsStore.getValue("feature_new_room_list")) {
leftPanel = (
<LeftPanel2
isMinimized={this.props.collapseLhs || false}
resizeNotifier={this.props.resizeNotifier}
/>
);
}
return (
<MatrixClientContext.Provider value={this._matrixClient}>

View file

@ -675,12 +675,16 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
case 'hide_left_panel':
this.setState({
collapseLhs: true,
}, () => {
this.state.resizeNotifier.notifyLeftHandleResized();
});
break;
case 'focus_room_filter': // for CtrlOrCmd+K to work by expanding the left panel first
case 'show_left_panel':
this.setState({
collapseLhs: false,
}, () => {
this.state.resizeNotifier.notifyLeftHandleResized();
});
break;
case 'panel_disable': {
@ -1460,16 +1464,18 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
cli.on("crypto.warning", (type) => {
switch (type) {
case 'CRYPTO_WARNING_OLD_VERSION_DETECTED':
const brand = SdkConfig.get().brand;
Modal.createTrackedDialog('Crypto migrated', '', ErrorDialog, {
title: _t('Old cryptography data detected'),
description: _t(
"Data from an older version of Riot has been detected. " +
"Data from an older version of %(brand)s has been detected. " +
"This will have caused end-to-end cryptography to malfunction " +
"in the older version. End-to-end encrypted messages exchanged " +
"recently whilst using the older version may not be decryptable " +
"in this version. This may also cause messages exchanged with this " +
"version to fail. If you experience problems, log out and back in " +
"again. To retain message history, export and re-import your keys.",
{ brand },
),
});
break;
@ -1834,7 +1840,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} else {
subtitle = `${this.subTitleStatus} ${subtitle}`;
}
document.title = `${SdkConfig.get().brand || 'Riot'} ${subtitle}`;
document.title = `${SdkConfig.get().brand} ${subtitle}`;
}
updateStatusIndicator(state: string, prevState: string) {

View file

@ -1,6 +1,7 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
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.
@ -19,6 +20,7 @@ import React from 'react';
import createReactClass from 'create-react-class';
import * as sdk from '../../index';
import { _t } from '../../languageHandler';
import SdkConfig from '../../SdkConfig';
import dis from '../../dispatcher/dispatcher';
import AccessibleButton from '../views/elements/AccessibleButton';
import MatrixClientContext from "../../contexts/MatrixClientContext";
@ -60,6 +62,7 @@ export default createReactClass({
},
render: function() {
const brand = SdkConfig.get().brand;
const Loader = sdk.getComponent("elements.Spinner");
const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
const GroupTile = sdk.getComponent("groups.GroupTile");
@ -77,7 +80,8 @@ export default createReactClass({
<div className="mx_MyGroups_microcopy">
<p>
{ _t(
"Did you know: you can use communities to filter your Riot.im experience!",
"Did you know: you can use communities to filter your %(brand)s experience!",
{ brand },
) }
</p>
<p>

View file

@ -1,7 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 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.
@ -25,6 +25,7 @@ import Modal from "../../Modal";
import { linkifyAndSanitizeHtml } from '../../HtmlUtils';
import PropTypes from 'prop-types';
import { _t } from '../../languageHandler';
import SdkConfig from '../../SdkConfig';
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
import Analytics from '../../Analytics';
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
@ -74,7 +75,7 @@ export default createReactClass({
this.protocols = response;
this.setState({protocolsLoading: false});
}, (err) => {
console.warn(`error loading thirdparty protocols: ${err}`);
console.warn(`error loading third party protocols: ${err}`);
this.setState({protocolsLoading: false});
if (MatrixClientPeg.get().isGuest()) {
// Guests currently aren't allowed to use this API, so
@ -83,10 +84,12 @@ export default createReactClass({
return;
}
track('Failed to get protocol list from homeserver');
const brand = SdkConfig.get().brand;
this.setState({
error: _t(
'Riot failed to get the protocol list from the homeserver. ' +
'%(brand)s failed to get the protocol list from the homeserver. ' +
'The homeserver may be too old to support third party networks.',
{ brand },
),
});
});
@ -173,12 +176,13 @@ export default createReactClass({
console.error("Failed to get publicRooms: %s", JSON.stringify(err));
track('Failed to get public room list');
const brand = SdkConfig.get().brand;
this.setState({
loading: false,
error:
`${_t('Riot failed to get the public room list.')} ` +
`${(err && err.message) ? err.message : _t('The homeserver may be unavailable or overloaded.')}`
,
error: (
_t('%(brand)s failed to get the public room list.', { brand }) +
(err && err.message) ? err.message : _t('The homeserver may be unavailable or overloaded.')
),
});
});
},
@ -314,9 +318,10 @@ export default createReactClass({
const fields = protocolName ? this._getFieldsForThirdPartyLocation(alias, this.protocols[protocolName], instance) : null;
if (!fields) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const brand = SdkConfig.get().brand;
Modal.createTrackedDialog('Unable to join network', '', ErrorDialog, {
title: _t('Unable to join network'),
description: _t('Riot does not know how to join a room on this network'),
description: _t('%(brand)s does not know how to join a room on this network', { brand }),
});
return;
}

View file

@ -28,8 +28,8 @@ import { Action } from "../../dispatcher/actions";
interface IProps {
onQueryUpdate: (newQuery: string) => void;
isMinimized: boolean;
onVerticalArrow(ev: React.KeyboardEvent);
onEnter(ev: React.KeyboardEvent);
onVerticalArrow(ev: React.KeyboardEvent): void;
onEnter(ev: React.KeyboardEvent): boolean;
}
interface IState {
@ -107,7 +107,13 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
} else if (ev.key === Key.ARROW_UP || ev.key === Key.ARROW_DOWN) {
this.props.onVerticalArrow(ev);
} else if (ev.key === Key.ENTER) {
this.props.onEnter(ev);
const shouldClear = this.props.onEnter(ev);
if (shouldClear) {
// wrap in set immediate to delay it so that we don't clear the filter & then change room
setImmediate(() => {
this.clearInput();
});
}
}
};

View file

@ -173,7 +173,7 @@ export default createReactClass({
if (this.props.hasActiveCall) {
const TintableSvg = sdk.getComponent("elements.TintableSvg");
return (
<TintableSvg src={require("../../../res/img/sound-indicator.svg")} width="23" height="20" />
<TintableSvg src={require("../../../res/img/element-icons/room/in-call.svg")} width="23" height="20" />
);
}

View file

@ -1,496 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018, 2019 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, {createRef} from 'react';
import classNames from 'classnames';
import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher';
import * as Unread from '../../Unread';
import * as RoomNotifs from '../../RoomNotifs';
import * as FormattingUtils from '../../utils/FormattingUtils';
import IndicatorScrollbar from './IndicatorScrollbar';
import {Key} from '../../Keyboard';
import { Group } from 'matrix-js-sdk';
import PropTypes from 'prop-types';
import RoomTile from "../views/rooms/RoomTile";
import LazyRenderList from "../views/elements/LazyRenderList";
import {_t} from "../../languageHandler";
import {RovingTabIndexWrapper} from "../../accessibility/RovingTabIndex";
import {toPx} from "../../utils/units";
// turn this on for drop & drag console debugging galore
const debug = false;
class RoomTileErrorBoundary extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
error: null,
};
}
static getDerivedStateFromError(error) {
// Side effects are not permitted here, so we only update the state so
// that the next render shows an error message.
return { error };
}
componentDidCatch(error, { componentStack }) {
// Browser consoles are better at formatting output when native errors are passed
// in their own `console.error` invocation.
console.error(error);
console.error(
"The above error occured while React was rendering the following components:",
componentStack,
);
}
render() {
if (this.state.error) {
return (<div className="mx_RoomTile mx_RoomTileError">
{this.props.roomId}
</div>);
} else {
return this.props.children;
}
}
}
export default class RoomSubList extends React.PureComponent {
static displayName = 'RoomSubList';
static debug = debug;
static propTypes = {
list: PropTypes.arrayOf(PropTypes.object).isRequired,
label: PropTypes.string.isRequired,
tagName: PropTypes.string,
addRoomLabel: PropTypes.string,
// passed through to RoomTile and used to highlight room with `!` regardless of notifications count
isInvite: PropTypes.bool,
startAsHidden: PropTypes.bool,
showSpinner: PropTypes.bool, // true to show a spinner if 0 elements when expanded
collapsed: PropTypes.bool.isRequired, // is LeftPanel collapsed?
onHeaderClick: PropTypes.func,
incomingCall: PropTypes.object,
extraTiles: PropTypes.arrayOf(PropTypes.node), // extra elements added beneath tiles
forceExpand: PropTypes.bool,
};
static defaultProps = {
onHeaderClick: function() {
}, // NOP
extraTiles: [],
isInvite: false,
};
static getDerivedStateFromProps(props, state) {
return {
listLength: props.list.length,
scrollTop: props.list.length === state.listLength ? state.scrollTop : 0,
};
}
constructor(props) {
super(props);
this.state = {
hidden: this.props.startAsHidden || false,
// some values to get LazyRenderList starting
scrollerHeight: 800,
scrollTop: 0,
// React 16's getDerivedStateFromProps(props, state) doesn't give the previous props so
// we have to store the length of the list here so we can see if it's changed or not...
listLength: null,
};
this._header = createRef();
this._subList = createRef();
this._scroller = createRef();
this._headerButton = createRef();
}
componentDidMount() {
this.dispatcherRef = dis.register(this.onAction);
}
componentWillUnmount() {
dis.unregister(this.dispatcherRef);
}
// The header is collapsible if it is hidden or not stuck
// The dataset elements are added in the RoomList _initAndPositionStickyHeaders method
isCollapsibleOnClick() {
const stuck = this._header.current.dataset.stuck;
if (!this.props.forceExpand && (this.state.hidden || stuck === undefined || stuck === "none")) {
return true;
} else {
return false;
}
}
onAction = (payload) => {
switch (payload.action) {
case 'on_room_read':
// XXX: Previously RoomList would forceUpdate whenever on_room_read is dispatched,
// but this is no longer true, so we must do it here (and can apply the small
// optimisation of checking that we care about the room being read).
//
// Ultimately we need to transition to a state pushing flow where something
// explicitly notifies the components concerned that the notif count for a room
// has change (e.g. a Flux store).
if (this.props.list.some((r) => r.roomId === payload.roomId)) {
this.forceUpdate();
}
break;
case 'view_room':
if (this.state.hidden && !this.props.forceExpand && payload.show_room_tile &&
this.props.list.some((r) => r.roomId === payload.room_id)
) {
this.toggle();
}
}
};
toggle = () => {
if (this.isCollapsibleOnClick()) {
// The header isCollapsible, so the click is to be interpreted as collapse and truncation logic
const isHidden = !this.state.hidden;
this.setState({hidden: isHidden}, () => {
this.props.onHeaderClick(isHidden);
});
} else {
// The header is stuck, so the click is to be interpreted as a scroll to the header
this.props.onHeaderClick(this.state.hidden, this._header.current.dataset.originalPosition);
}
};
onClick = (ev) => {
this.toggle();
};
onHeaderKeyDown = (ev) => {
switch (ev.key) {
case Key.ARROW_LEFT:
// On ARROW_LEFT collapse the room sublist
if (!this.state.hidden && !this.props.forceExpand) {
this.onClick();
}
ev.stopPropagation();
break;
case Key.ARROW_RIGHT: {
ev.stopPropagation();
if (this.state.hidden && !this.props.forceExpand) {
// sublist is collapsed, expand it
this.onClick();
} else if (!this.props.forceExpand) {
// sublist is expanded, go to first room
const element = this._subList.current && this._subList.current.querySelector(".mx_RoomTile");
if (element) {
element.focus();
}
}
break;
}
}
};
onKeyDown = (ev) => {
switch (ev.key) {
// On ARROW_LEFT go to the sublist header
case Key.ARROW_LEFT:
ev.stopPropagation();
this._headerButton.current.focus();
break;
// Consume ARROW_RIGHT so it doesn't cause focus to get sent to composer
case Key.ARROW_RIGHT:
ev.stopPropagation();
}
};
onRoomTileClick = (roomId, ev) => {
dis.dispatch({
action: 'view_room',
show_room_tile: true, // to make sure the room gets scrolled into view
room_id: roomId,
clear_search: (ev && (ev.key === Key.ENTER || ev.key === Key.SPACE)),
});
};
_updateSubListCount = () => {
// Force an update by setting the state to the current state
// Doing it this way rather than using forceUpdate(), so that the shouldComponentUpdate()
// method is honoured
this.setState(this.state);
};
makeRoomTile = (room) => {
return <RoomTileErrorBoundary roomId={room.roomId}><RoomTile
room={room}
roomSubList={this}
tagName={this.props.tagName}
key={room.roomId}
collapsed={this.props.collapsed || false}
unread={Unread.doesRoomHaveUnreadMessages(room)}
highlight={this.props.isInvite || RoomNotifs.getUnreadNotificationCount(room, 'highlight') > 0}
notificationCount={RoomNotifs.getUnreadNotificationCount(room)}
isInvite={this.props.isInvite}
refreshSubList={this._updateSubListCount}
incomingCall={null}
onClick={this.onRoomTileClick}
/></RoomTileErrorBoundary>;
};
_onNotifBadgeClick = (e) => {
// prevent the roomsublist collapsing
e.preventDefault();
e.stopPropagation();
const room = this.props.list.find(room => RoomNotifs.getRoomHasBadge(room));
if (room) {
dis.dispatch({
action: 'view_room',
room_id: room.roomId,
});
}
};
_onInviteBadgeClick = (e) => {
// prevent the roomsublist collapsing
e.preventDefault();
e.stopPropagation();
// switch to first room in sortedList as that'll be the top of the list for the user
if (this.props.list && this.props.list.length > 0) {
dis.dispatch({
action: 'view_room',
room_id: this.props.list[0].roomId,
});
} else if (this.props.extraTiles && this.props.extraTiles.length > 0) {
// Group Invites are different in that they are all extra tiles and not rooms
// XXX: this is a horrible special case because Group Invite sublist is a hack
if (this.props.extraTiles[0].props && this.props.extraTiles[0].props.group instanceof Group) {
dis.dispatch({
action: 'view_group',
group_id: this.props.extraTiles[0].props.group.groupId,
});
}
}
};
onAddRoom = (e) => {
e.stopPropagation();
if (this.props.onAddRoom) this.props.onAddRoom();
};
_getHeaderJsx(isCollapsed) {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const AccessibleTooltipButton = sdk.getComponent('elements.AccessibleTooltipButton');
const subListNotifications = !this.props.isInvite ?
RoomNotifs.aggregateNotificationCount(this.props.list) :
{count: 0, highlight: true};
const subListNotifCount = subListNotifications.count;
const subListNotifHighlight = subListNotifications.highlight;
// When collapsed, allow a long hover on the header to show user
// the full tag name and room count
let title;
if (this.props.collapsed) {
title = this.props.label;
}
let incomingCall;
if (this.props.incomingCall) {
// We can assume that if we have an incoming call then it is for this list
const IncomingCallBox = sdk.getComponent("voip.IncomingCallBox");
incomingCall =
<IncomingCallBox className="mx_RoomSubList_incomingCall" incomingCall={this.props.incomingCall} />;
}
const len = this.props.list.length + this.props.extraTiles.length;
let chevron;
if (len) {
const chevronClasses = classNames({
'mx_RoomSubList_chevron': true,
'mx_RoomSubList_chevronRight': isCollapsed,
'mx_RoomSubList_chevronDown': !isCollapsed,
});
chevron = (<div className={chevronClasses} />);
}
return <RovingTabIndexWrapper inputRef={this._headerButton}>
{({onFocus, isActive, ref}) => {
const tabIndex = isActive ? 0 : -1;
let badge;
if (!this.props.collapsed) {
const badgeClasses = classNames({
'mx_RoomSubList_badge': true,
'mx_RoomSubList_badgeHighlight': subListNotifHighlight,
});
// Wrap the contents in a div and apply styles to the child div so that the browser default outline works
if (subListNotifCount > 0) {
badge = (
<AccessibleButton
tabIndex={tabIndex}
className={badgeClasses}
onClick={this._onNotifBadgeClick}
aria-label={_t("Jump to first unread room.")}
>
<div>
{ FormattingUtils.formatCount(subListNotifCount) }
</div>
</AccessibleButton>
);
} else if (this.props.isInvite && this.props.list.length) {
// no notifications but highlight anyway because this is an invite badge
badge = (
<AccessibleButton
tabIndex={tabIndex}
className={badgeClasses}
onClick={this._onInviteBadgeClick}
aria-label={_t("Jump to first invite.")}
>
<div>
{ this.props.list.length }
</div>
</AccessibleButton>
);
}
}
let addRoomButton;
if (this.props.onAddRoom) {
addRoomButton = (
<AccessibleTooltipButton
tabIndex={tabIndex}
onClick={this.onAddRoom}
className="mx_RoomSubList_addRoom"
title={this.props.addRoomLabel || _t("Add room")}
/>
);
}
return (
<div className="mx_RoomSubList_labelContainer" title={title} ref={this._header} onKeyDown={this.onHeaderKeyDown}>
<AccessibleButton
onFocus={onFocus}
tabIndex={tabIndex}
inputRef={ref}
onClick={this.onClick}
className="mx_RoomSubList_label"
aria-expanded={!isCollapsed}
role="treeitem"
aria-level="1"
>
{ chevron }
<span>{this.props.label}</span>
{ incomingCall }
</AccessibleButton>
{ badge }
{ addRoomButton }
</div>
);
} }
</RovingTabIndexWrapper>;
}
checkOverflow = () => {
if (this._scroller.current) {
this._scroller.current.checkOverflow();
}
};
setHeight = (height) => {
if (this._subList.current) {
this._subList.current.style.height = toPx(height);
}
this._updateLazyRenderHeight(height);
};
_updateLazyRenderHeight(height) {
this.setState({scrollerHeight: height});
}
_onScroll = () => {
this.setState({scrollTop: this._scroller.current.getScrollTop()});
};
_canUseLazyListRendering() {
// for now disable lazy rendering as they are already rendered tiles
// not rooms like props.list we pass to LazyRenderList
return !this.props.extraTiles || !this.props.extraTiles.length;
}
render() {
const len = this.props.list.length + this.props.extraTiles.length;
const isCollapsed = this.state.hidden && !this.props.forceExpand;
const subListClasses = classNames({
"mx_RoomSubList": true,
"mx_RoomSubList_hidden": len && isCollapsed,
"mx_RoomSubList_nonEmpty": len && !isCollapsed,
});
let content;
if (len) {
if (isCollapsed) {
// no body
} else if (this._canUseLazyListRendering()) {
content = (
<IndicatorScrollbar ref={this._scroller} className="mx_RoomSubList_scroll" onScroll={this._onScroll}>
<LazyRenderList
scrollTop={this.state.scrollTop }
height={ this.state.scrollerHeight }
renderItem={ this.makeRoomTile }
itemHeight={34}
items={ this.props.list } />
</IndicatorScrollbar>
);
} else {
const roomTiles = this.props.list.map(r => this.makeRoomTile(r));
const tiles = roomTiles.concat(this.props.extraTiles);
content = (
<IndicatorScrollbar ref={this._scroller} className="mx_RoomSubList_scroll" onScroll={this._onScroll}>
{ tiles }
</IndicatorScrollbar>
);
}
} else {
if (this.props.showSpinner && !isCollapsed) {
const Loader = sdk.getComponent("elements.Spinner");
content = <Loader />;
}
}
return (
<div
ref={this._subList}
className={subListClasses}
role="group"
aria-label={this.props.label}
onKeyDown={this.onKeyDown}
>
{ this._getHeaderJsx(isCollapsed) }
{ content }
</div>
);
}
}

View file

@ -1927,25 +1927,29 @@ export default createReactClass({
}
if (inCall) {
let zoomButton; let voiceMuteButton; let videoMuteButton;
let zoomButton; let videoMuteButton;
if (call.type === "video") {
zoomButton = (
<div className="mx_RoomView_voipButton" onClick={this.onFullscreenClick} title={_t("Fill screen")}>
<TintableSvg src={require("../../../res/img/fullscreen.svg")} width="29" height="22" style={{ marginTop: 1, marginRight: 4 }} />
<TintableSvg src={require("../../../res/img/element-icons/call/fullscreen.svg")} width="29" height="22" style={{ marginTop: 1, marginRight: 4 }} />
</div>
);
videoMuteButton =
<div className="mx_RoomView_voipButton" onClick={this.onMuteVideoClick}>
<TintableSvg src={call.isLocalVideoMuted() ? require("../../../res/img/video-unmute.svg") : require("../../../res/img/video-mute.svg")}
<TintableSvg src={call.isLocalVideoMuted() ?
require("../../../res/img/element-icons/call/video-muted.svg") :
require("../../../res/img/element-icons/call/video-call.svg")}
alt={call.isLocalVideoMuted() ? _t("Click to unmute video") : _t("Click to mute video")}
width="31" height="27" />
width="" height="27" />
</div>;
}
voiceMuteButton =
const voiceMuteButton =
<div className="mx_RoomView_voipButton" onClick={this.onMuteAudioClick}>
<TintableSvg src={call.isMicrophoneMuted() ? require("../../../res/img/voice-unmute.svg") : require("../../../res/img/voice-mute.svg")}
<TintableSvg src={call.isMicrophoneMuted() ?
require("../../../res/img/element-icons/call/voice-muted.svg") :
require("../../../res/img/element-icons/call/voice-unmuted.svg")}
alt={call.isMicrophoneMuted() ? _t("Click to unmute audio") : _t("Click to mute audio")}
width="21" height="26" />
</div>;
@ -1957,7 +1961,6 @@ export default createReactClass({
{ videoMuteButton }
{ zoomButton }
{ statusBar }
<TintableSvg className="mx_RoomView_voipChevron" src={require("../../../res/img/voip-chevron.svg")} width="22" height="17" />
</div>;
}

View file

@ -1,158 +0,0 @@
/*
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 PropTypes from 'prop-types';
import TopLeftMenu from '../views/context_menus/TopLeftMenu';
import BaseAvatar from '../views/avatars/BaseAvatar';
import {MatrixClientPeg} from '../../MatrixClientPeg';
import * as Avatar from '../../Avatar';
import { _t } from '../../languageHandler';
import dis from "../../dispatcher/dispatcher";
import {ContextMenu, ContextMenuButton} from "./ContextMenu";
import {Action} from "../../dispatcher/actions";
const AVATAR_SIZE = 28;
export default class TopLeftMenuButton extends React.Component {
static propTypes = {
collapsed: PropTypes.bool.isRequired,
};
static displayName = 'TopLeftMenuButton';
constructor() {
super();
this.state = {
menuDisplayed: false,
profileInfo: null,
};
}
async _getProfileInfo() {
const cli = MatrixClientPeg.get();
const userId = cli.getUserId();
const profileInfo = await cli.getProfileInfo(userId);
const avatarUrl = Avatar.avatarUrlForUser(
{avatarUrl: profileInfo.avatar_url},
AVATAR_SIZE, AVATAR_SIZE, "crop");
return {
userId,
name: profileInfo.displayname,
avatarUrl,
};
}
async componentDidMount() {
this._dispatcherRef = dis.register(this.onAction);
try {
const profileInfo = await this._getProfileInfo();
this.setState({profileInfo});
} catch (ex) {
console.log("could not fetch profile");
console.error(ex);
}
}
componentWillUnmount() {
dis.unregister(this._dispatcherRef);
}
onAction = (payload) => {
// For accessibility
if (payload.action === Action.ToggleUserMenu) {
if (this._buttonRef) this._buttonRef.click();
}
};
_getDisplayName() {
if (MatrixClientPeg.get().isGuest()) {
return _t("Guest");
} else if (this.state.profileInfo) {
return this.state.profileInfo.name;
} else {
return MatrixClientPeg.get().getUserId();
}
}
openMenu = (e) => {
e.preventDefault();
e.stopPropagation();
this.setState({ menuDisplayed: true });
};
closeMenu = () => {
this.setState({
menuDisplayed: false,
});
};
render() {
const cli = MatrixClientPeg.get().getUserId();
const name = this._getDisplayName();
let nameElement;
let chevronElement;
if (!this.props.collapsed) {
nameElement = <div className="mx_TopLeftMenuButton_name">
{ name }
</div>;
chevronElement = <span className="mx_TopLeftMenuButton_chevron" />;
}
let contextMenu;
if (this.state.menuDisplayed) {
const elementRect = this._buttonRef.getBoundingClientRect();
contextMenu = (
<ContextMenu
chevronFace="none"
left={elementRect.left}
top={elementRect.top + elementRect.height}
onFinished={this.closeMenu}
>
<TopLeftMenu displayName={name} userId={cli} onFinished={this.closeMenu} />
</ContextMenu>
);
}
return <React.Fragment>
<ContextMenuButton
className="mx_TopLeftMenuButton"
onClick={this.openMenu}
inputRef={(r) => this._buttonRef = r}
label={_t("Your profile")}
isExpanded={this.state.menuDisplayed}
>
<BaseAvatar
idName={MatrixClientPeg.get().getUserId()}
name={name}
url={this.state.profileInfo && this.state.profileInfo.avatarUrl}
width={AVATAR_SIZE}
height={AVATAR_SIZE}
resizeMethod="crop"
/>
{ nameElement }
{ chevronElement }
</ContextMenuButton>
{ contextMenu }
</React.Fragment>;
}
}

View file

@ -257,7 +257,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
title={this.state.isDarkTheme ? _t("Switch to light mode") : _t("Switch to dark mode")}
>
<img
src={require("../../../res/img/feather-customised/sun.svg")}
src={require("../../../res/img/element-icons/roomlist/dark-light-mode.svg")}
alt={_t("Switch theme")}
width={16}
/>
@ -306,6 +306,9 @@ export default class UserMenu extends React.Component<IProps, IState> {
public render() {
const avatarSize = 32; // should match border-radius of the avatar
const {body} = document;
const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize);
body.style.setProperty("--avatar-url", `url('${avatarUrl}')`);
let name = <span className="mx_UserMenu_userName">{OwnProfileStore.instance.displayName}</span>;
let buttons = (

View file

@ -17,6 +17,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import {
@ -131,6 +132,8 @@ export default class SetupEncryptionBody extends React.Component {
</AccessibleButton>;
}
const brand = SdkConfig.get().brand;
return (
<div>
<p>{_t(
@ -138,17 +141,18 @@ export default class SetupEncryptionBody extends React.Component {
"granting it access to encrypted messages.",
)}</p>
<p>{_t(
"This requires the latest Riot on your other devices:",
"This requires the latest %(brand)s on your other devices:",
{ brand },
)}</p>
<div className="mx_CompleteSecurity_clients">
<div className="mx_CompleteSecurity_clients_desktop">
<div>Riot Web</div>
<div>Riot Desktop</div>
<div>{_t("%(brand)s Web", { brand })}</div>
<div>{_t("%(brand)s Desktop", { brand })}</div>
</div>
<div className="mx_CompleteSecurity_clients_mobile">
<div>Riot iOS</div>
<div>Riot X for Android</div>
<div>{_t("%(brand)s iOS", { brand })}</div>
<div>{_t("%(brand)s X for Android", { brand })}</div>
</div>
<p>{_t("or another cross-signing capable Matrix client")}</p>
</div>

View file

@ -1,6 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 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.
@ -18,11 +18,13 @@ limitations under the License.
import React from 'react';
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
export default createReactClass({
displayName: 'CustomServerDialog',
render: function() {
const brand = SdkConfig.get().brand;
return (
<div className="mx_ErrorDialog">
<div className="mx_Dialog_title">
@ -32,8 +34,9 @@ export default createReactClass({
<p>{_t(
"You can use the custom server options to sign into other " +
"Matrix servers by specifying a different homeserver URL. This " +
"allows you to use this app with an existing Matrix account on a " +
"allows you to use %(brand)s with an existing Matrix account on a " +
"different homeserver.",
{ brand },
)}</p>
</div>
<div className="mx_Dialog_buttons">

View file

@ -23,8 +23,8 @@ import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
import * as ServerType from '../../views/auth/ServerTypeSelector';
import ServerConfig from "./ServerConfig";
const MODULAR_URL = 'https://modular.im/services/matrix-hosting-riot' +
'?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
const MODULAR_URL = 'https://element.io/matrix-services' +
'?utm_source=element-web&utm_medium=web&utm_campaign=element-web-authentication';
// TODO: TravisR - Can this extend ServerConfig for most things?
@ -95,10 +95,10 @@ export default class ModularServerConfig extends ServerConfig {
return (
<div className="mx_ServerConfig">
<h3>{_t("Your Modular server")}</h3>
<h3>{_t("Your server")}</h3>
{_t(
"Enter the location of your Modular homeserver. It may use your own " +
"domain name or be a subdomain of <a>modular.im</a>.",
"Enter the location of your Element Matrix Services homeserver. It may use your own " +
"domain name or be a subdomain of <a>element.io</a>.",
{}, {
a: sub => <a href={MODULAR_URL} target="_blank" rel="noreferrer noopener">
{sub}

View file

@ -22,8 +22,8 @@ import classnames from 'classnames';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import {makeType} from "../../../utils/TypeUtils";
const MODULAR_URL = 'https://modular.im/services/matrix-hosting-riot' +
'?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
const MODULAR_URL = 'https://element.io/matrix-services' +
'?utm_source=element-web&utm_medium=web&utm_campaign=element-web-authentication';
export const FREE = 'Free';
export const PREMIUM = 'Premium';
@ -45,7 +45,7 @@ export const TYPES = {
PREMIUM: {
id: PREMIUM,
label: () => _t('Premium'),
logo: () => <img src={require('../../../../res/img/modular-bw-logo.svg')} />,
logo: () => <img src={require('../../../../res/img/ems-logo.svg')} height={16} />,
description: () => _t('Premium hosting for organisations <a>Learn more</a>', {}, {
a: sub => <a href={MODULAR_URL} target="_blank" rel="noreferrer noopener">
{sub}

View file

@ -30,6 +30,8 @@ interface IProps {
tag: TagID;
displayBadge?: boolean;
forceCount?: boolean;
oobData?: object;
viewAvatarOnClick?: boolean;
}
interface IState {
@ -57,8 +59,14 @@ export default class DecoratedRoomAvatar extends React.PureComponent<IProps, ISt
}
return <div className="mx_DecoratedRoomAvatar">
<RoomAvatar room={this.props.room} width={this.props.avatarSize} height={this.props.avatarSize} />
<RoomTileIcon room={this.props.room} tag={this.props.tag} />
<RoomAvatar
room={this.props.room}
width={this.props.avatarSize}
height={this.props.avatarSize}
oobData={this.props.oobData}
viewAvatarOnClick={this.props.viewAvatarOnClick}
/>
<RoomTileIcon room={this.props.room} />
{badge}
</div>;
}

View file

@ -20,7 +20,6 @@ import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher/dispatcher';
import TagOrderActions from '../../../actions/TagOrderActions';
import * as sdk from '../../../index';
import {MenuItem} from "../../structures/ContextMenu";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
@ -54,21 +53,12 @@ export default class TagTileContextMenu extends React.Component {
}
render() {
const TintableSvg = sdk.getComponent("elements.TintableSvg");
return <div>
<MenuItem className="mx_TagTileContextMenu_item" onClick={this._onViewCommunityClick}>
<TintableSvg
className="mx_TagTileContextMenu_item_icon"
src={require("../../../../res/img/icons-groups.svg")}
width="15"
height="15"
/>
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_viewCommunity" onClick={this._onViewCommunityClick}>
{ _t('View Community') }
</MenuItem>
<hr className="mx_TagTileContextMenu_separator" role="separator" />
<MenuItem className="mx_TagTileContextMenu_item" onClick={this._onRemoveClick}>
<img className="mx_TagTileContextMenu_item_icon" src={require("../../../../res/img/icon_context_delete.svg")} width="15" height="15" alt="" />
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_hideCommunity" onClick={this._onRemoveClick}>
{ _t('Hide') }
</MenuItem>
</div>;

View file

@ -1,44 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket 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 PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
export default createReactClass({
displayName: 'CreateRoomButton',
propTypes: {
onCreateRoom: PropTypes.func,
},
getDefaultProps: function() {
return {
onCreateRoom: function() {},
};
},
onClick: function() {
this.props.onCreateRoom();
},
render: function() {
return (
<button className="mx_CreateRoomButton" onClick={this.onClick}>{ _t("Create Room") }</button>
);
},
});

View file

@ -1,5 +1,6 @@
/*
Copyright 2018 New Vector Ltd
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.
@ -18,9 +19,12 @@ import React from 'react';
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal';
export default (props) => {
const brand = SdkConfig.get().brand;
const _onLogoutClicked = () => {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Logout e2e db too new', '', QuestionDialog, {
@ -28,7 +32,8 @@ export default (props) => {
description: _t(
"To avoid losing your chat history, you must export your room keys " +
"before logging out. You will need to go back to the newer version of " +
"Riot to do this",
"%(brand)s to do this",
{ brand },
),
button: _t("Sign out"),
focus: false,
@ -42,9 +47,12 @@ export default (props) => {
};
const description =
_t("You've previously used a newer version of Riot with this session. " +
_t(
"You've previously used a newer version of %(brand)s with this session. " +
"To use this version again with end to end encryption, you will " +
"need to sign out and back in again.");
"need to sign out and back in again.",
{ brand },
);
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');

View file

@ -19,7 +19,7 @@ import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import SyntaxHighlight from '../elements/SyntaxHighlight';
import { _t } from '../../../languageHandler';
import { Room } from "matrix-js-sdk";
import { Room, MatrixEvent } from "matrix-js-sdk";
import Field from "../elements/Field";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {useEventEmitter} from "../../../hooks/useEventEmitter";
@ -327,6 +327,8 @@ class RoomStateExplorer extends React.PureComponent {
static contextType = MatrixClientContext;
roomStateEvents: Map<string, Map<string, MatrixEvent>>;
constructor(props) {
super(props);
@ -412,30 +414,26 @@ class RoomStateExplorer extends React.PureComponent {
if (this.state.eventType === null) {
list = <FilteredList query={this.state.queryEventType} onChange={this.onQueryEventType}>
{
Object.keys(this.roomStateEvents).map((evType) => {
const stateGroup = this.roomStateEvents[evType];
const stateKeys = Object.keys(stateGroup);
Array.from(this.roomStateEvents.entries()).map(([eventType, allStateKeys]) => {
let onClickFn;
if (stateKeys.length === 1 && stateKeys[0] === '') {
onClickFn = this.onViewSourceClick(stateGroup[stateKeys[0]]);
if (allStateKeys.size === 1 && allStateKeys.has("")) {
onClickFn = this.onViewSourceClick(allStateKeys.get(""));
} else {
onClickFn = this.browseEventType(evType);
onClickFn = this.browseEventType(eventType);
}
return <button className={classes} key={evType} onClick={onClickFn}>
{ evType }
return <button className={classes} key={eventType} onClick={onClickFn}>
{eventType}
</button>;
})
}
</FilteredList>;
} else {
const stateGroup = this.roomStateEvents[this.state.eventType];
const stateGroup = this.roomStateEvents.get(this.state.eventType);
list = <FilteredList query={this.state.queryStateKey} onChange={this.onQueryStateKey}>
{
Object.keys(stateGroup).map((stateKey) => {
const ev = stateGroup[stateKey];
Array.from(stateGroup.entries()).map(([stateKey, ev]) => {
return <button className={classes} key={stateKey} onClick={this.onViewSourceClick(ev)}>
{ stateKey }
</button>;

View file

@ -1,5 +1,5 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 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.
@ -17,6 +17,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
import SdkConfig from "../../../SdkConfig";
import * as sdk from "../../../index";
export default class IntegrationsImpossibleDialog extends React.Component {
@ -29,6 +30,7 @@ export default class IntegrationsImpossibleDialog extends React.Component {
};
render() {
const brand = SdkConfig.get().brand;
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
@ -39,8 +41,9 @@ export default class IntegrationsImpossibleDialog extends React.Component {
<div className='mx_IntegrationsImpossibleDialog_content'>
<p>
{_t(
"Your Riot doesn't allow you to use an Integration Manager to do this. " +
"Your %(brand)s doesn't allow you to use an Integration Manager to do this. " +
"Please contact an admin.",
{ brand },
)}
</p>
</div>

View file

@ -35,8 +35,8 @@ import createRoom, {canEncryptToAllUsers, privateShouldBeEncrypted} from "../../
import {inviteMultipleToRoom} from "../../../RoomInvite";
import {Key} from "../../../Keyboard";
import {Action} from "../../../dispatcher/actions";
import {RoomListStoreTempProxy} from "../../../stores/room-list/RoomListStoreTempProxy";
import {DefaultTagID} from "../../../stores/room-list/models";
import RoomListStore from "../../../stores/room-list/RoomListStore";
export const KIND_DM = "dm";
export const KIND_INVITE = "invite";
@ -346,8 +346,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 taggedRooms = RoomListStoreTempProxy.getRoomLists();
const dmTaggedRooms = taggedRooms[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

@ -17,6 +17,7 @@ limitations under the License.
import React, {useState, useCallback, useRef} from 'react';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
export default function KeySignatureUploadFailedDialog({
failures,
@ -65,9 +66,10 @@ export default function KeySignatureUploadFailedDialog({
let body;
if (!success && !cancelled && continuation && retry > 0) {
const reason = causes.get(source) || defaultCause;
const brand = SdkConfig.get().brand;
body = (<div>
<p>{_t("Riot encountered an error during upload of:")}</p>
<p>{_t("%(brand)s encountered an error during upload of:", { brand })}</p>
<p>{reason}</p>
{retrying && <Spinner />}
<pre>{JSON.stringify(failures, null, 2)}</pre>

View file

@ -1,5 +1,6 @@
/*
Copyright 2018 New Vector Ltd
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.
@ -17,17 +18,28 @@ limitations under the License.
import React from 'react';
import QuestionDialog from './QuestionDialog';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
export default (props) => {
const description1 =
_t("You've previously used Riot on %(host)s with lazy loading of members enabled. " +
"In this version lazy loading is disabled. " +
"As the local cache is not compatible between these two settings, " +
"Riot needs to resync your account.",
{host: props.host});
const description2 = _t("If the other version of Riot is still open in another tab, " +
"please close it as using Riot on the same host with both " +
"lazy loading enabled and disabled simultaneously will cause issues.");
const brand = SdkConfig.get().brand;
const description1 = _t(
"You've previously used %(brand)s on %(host)s with lazy loading of members enabled. " +
"In this version lazy loading is disabled. " +
"As the local cache is not compatible between these two settings, " +
"%(brand)s needs to resync your account.",
{
brand,
host: props.host,
},
);
const description2 = _t(
"If the other version of %(brand)s is still open in another tab, " +
"please close it as using %(brand)s on the same host with both " +
"lazy loading enabled and disabled simultaneously will cause issues.",
{
brand,
},
);
return (<QuestionDialog
hasCancelButton={false}

View file

@ -1,5 +1,6 @@
/*
Copyright 2018 New Vector Ltd
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.
@ -17,15 +18,21 @@ limitations under the License.
import React from 'react';
import QuestionDialog from './QuestionDialog';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
export default (props) => {
const brand = SdkConfig.get().brand;
const description =
_t("Riot now uses 3-5x less memory, by only loading information about other users"
+ " when needed. Please wait whilst we resynchronise with the server!");
_t(
"%(brand)s now uses 3-5x less memory, by only loading information " +
"about other users when needed. Please wait whilst we resynchronise " +
"with the server!",
{ brand },
);
return (<QuestionDialog
hasCancelButton={false}
title={_t("Updating Riot")}
title={_t("Updating %(brand)s", { brand })}
description={<div>{description}</div>}
button={_t("OK")}
onFinished={props.onFinished}

View file

@ -0,0 +1,116 @@
/*
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 * as PropTypes from 'prop-types';
import BaseDialog from './BaseDialog';
import { _t } from '../../../languageHandler';
import DialogButtons from '../elements/DialogButtons';
export enum RebrandDialogKind {
NAG,
ONE_TIME,
}
interface IProps {
onFinished: (bool) => void;
kind: RebrandDialogKind;
targetUrl?: string;
}
export default class RebrandDialog extends React.PureComponent<IProps> {
private onDoneClick = () => {
this.props.onFinished(true);
};
private onGoToElementClick = () => {
this.props.onFinished(true);
};
private onRemindMeLaterClick = () => {
this.props.onFinished(false);
};
private getPrettyTargetUrl() {
const u = new URL(this.props.targetUrl);
let ret = u.host;
if (u.pathname !== '/') ret += u.pathname;
return ret;
}
getBodyText() {
if (this.props.kind === RebrandDialogKind.NAG) {
return _t(
"Use your account to sign in to the latest version of the app at <a />", {},
{
a: sub => <a href={this.props.targetUrl} rel="noopener noreferrer" target="_blank">{this.getPrettyTargetUrl()}</a>,
},
);
} else {
return _t(
"Youre already signed in and good to go here, but you can also grab the latest " +
"versions of the app on all platforms at <a>element.io/get-started</a>.", {},
{
a: sub => <a href="https://element.io/get-started" rel="noopener noreferrer" target="_blank">{sub}</a>,
},
);
}
}
getDialogButtons() {
if (this.props.kind === RebrandDialogKind.NAG) {
return <DialogButtons primaryButton={_t("Go to Element")}
primaryButtonClass='primary'
onPrimaryButtonClick={this.onGoToElementClick}
hasCancel={true}
cancelButton={"Remind me later"}
onCancel={this.onRemindMeLaterClick}
focus={true}
/>;
} else {
return <DialogButtons primaryButton={_t("Done")}
primaryButtonClass='primary'
hasCancel={false}
onPrimaryButtonClick={this.onDoneClick}
focus={true}
/>;
}
}
render() {
return <BaseDialog title={_t("Were excited to announce Riot is now Element!")}
className='mx_RebrandDialog'
contentId='mx_Dialog_content'
onFinished={this.props.onFinished}
hasCancel={false}
>
<div className="mx_RebrandDialog_body">{this.getBodyText()}</div>
<div className="mx_RebrandDialog_logoContainer">
<img className="mx_RebrandDialog_logo" src={require("../../../../res/img/riot-logo.svg")} alt="Riot Logo" />
<span className="mx_RebrandDialog_chevron" />
<img className="mx_RebrandDialog_logo" src={require("../../../../res/img/element-logo.svg")} alt="Element Logo" />
</div>
<div>
{_t(
"Learn more at <a>element.io/previously-riot</a>", {}, {
a: sub => <a href="https://element.io/previously-riot" rel="noopener noreferrer" target="_blank">{sub}</a>,
}
)}
</div>
{this.getDialogButtons()}
</BaseDialog>;
}
}

View file

@ -1,5 +1,5 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 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.
@ -17,6 +17,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
import SdkConfig from "../../../SdkConfig";
import * as sdk from "../../../index";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
@ -63,6 +64,7 @@ export default class RoomUpgradeWarningDialog extends React.Component {
};
render() {
const brand = SdkConfig.get().brand;
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
@ -96,8 +98,11 @@ export default class RoomUpgradeWarningDialog extends React.Component {
<p>
{_t(
"This usually only affects how the room is processed on the server. If you're " +
"having problems with your Riot, please <a>report a bug</a>.",
{}, {
"having problems with your %(brand)s, please <a>report a bug</a>.",
{
brand,
},
{
"a": (sub) => {
return <a href='#' onClick={this._openBugReportDialog}>{sub}</a>;
},

View file

@ -1,6 +1,7 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
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.
@ -56,6 +57,7 @@ export default createReactClass({
},
render: function() {
const brand = SdkConfig.get().brand;
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
@ -94,9 +96,10 @@ export default createReactClass({
<p>{ _t("We encountered an error trying to restore your previous session.") }</p>
<p>{ _t(
"If you have previously used a more recent version of Riot, your session " +
"If you have previously used a more recent version of %(brand)s, your session " +
"may be incompatible with this version. Close this window and return " +
"to the more recent version.",
{ brand },
) }</p>
<p>{ _t(

View file

@ -27,7 +27,7 @@ export type ButtonEvent = React.MouseEvent<Element> | React.KeyboardEvent<Elemen
* onClick: (required) Event handler for button activation. Should be
* implemented exactly like a normal onClick handler.
*/
export interface IProps extends React.InputHTMLAttributes<Element> {
interface IProps extends React.InputHTMLAttributes<Element> {
inputRef?: React.Ref<Element>;
element?: string;
// The kind of button, similar to how Bootstrap works.
@ -118,7 +118,7 @@ export default function AccessibleButton({
AccessibleButton.defaultProps = {
element: 'div',
role: 'button',
tabIndex: "0",
tabIndex: 0,
};
AccessibleButton.displayName = "AccessibleButton";

View file

@ -16,14 +16,14 @@ limitations under the License.
*/
import React from 'react';
import classnames from 'classnames';
import classNames from 'classnames';
import AccessibleButton from "./AccessibleButton";
import {IProps} from "./AccessibleButton";
import Tooltip from './Tooltip';
interface ITooltipProps extends IProps {
interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
title: string;
tooltip?: React.ReactNode;
tooltipClassName?: string;
}
@ -45,26 +45,27 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
});
};
onMouseOut = () => {
onMouseLeave = () => {
this.setState({
hover: false,
});
};
render() {
const {title, children, ...props} = this.props;
const tooltipClassName = classnames(
"mx_AccessibleTooltipButton_tooltip",
this.props.tooltipClassName,
);
const {title, tooltip, children, tooltipClassName, ...props} = this.props;
const tip = this.state.hover ? <Tooltip
className="mx_AccessibleTooltipButton_container"
tooltipClassName={tooltipClassName}
label={title}
tooltipClassName={classNames("mx_AccessibleTooltipButton_tooltip", tooltipClassName)}
label={tooltip || title}
/> : <div />;
return (
<AccessibleButton {...props} onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} aria-label={title}>
<AccessibleButton
{...props}
onMouseOver={this.onMouseOver}
onMouseLeave={this.onMouseLeave}
aria-label={title}
>
{ children }
{ tip }
</AccessibleButton>

View file

@ -58,18 +58,6 @@ export default createReactClass({
imgUrls.push(require("../../../../res/img/icon-email-user.svg"));
}
// Removing networks for now as they're not really supported
/*
var network;
if (this.props.networkUrl !== "") {
network = (
<div className="mx_AddressTile_network">
<BaseAvatar width={25} height={25} name={this.props.networkName} title="Riot" url={this.props.networkUrl} />
</div>
);
}
*/
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const TintableSvg = sdk.getComponent("elements.TintableSvg");

View file

@ -1,7 +1,7 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018, 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 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.
@ -21,6 +21,7 @@ import PropTypes from 'prop-types';
import url from 'url';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import WidgetUtils from "../../../utils/WidgetUtils";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
@ -76,6 +77,7 @@ export default class AppPermission extends React.Component {
}
render() {
const brand = SdkConfig.get().brand;
const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
@ -96,7 +98,7 @@ export default class AppPermission extends React.Component {
<li>{_t("Your avatar URL")}</li>
<li>{_t("Your user ID")}</li>
<li>{_t("Your theme")}</li>
<li>{_t("Riot URL")}</li>
<li>{_t("%(brand)s URL", { brand })}</li>
<li>{_t("Room ID")}</li>
<li>{_t("Widget ID")}</li>
</ul>

View file

@ -1,40 +0,0 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import * as sdk from '../../../index';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
const CreateRoomButton = function(props) {
const ActionButton = sdk.getComponent('elements.ActionButton');
return (
<ActionButton action="view_create_room"
mouseOverAction={props.callout ? "callout_create_room" : null}
label={_t("Create new room")}
iconPath={require("../../../../res/img/icons-create-room.svg")}
size={props.size}
tooltip={props.tooltip}
/>
);
};
CreateRoomButton.propTypes = {
size: PropTypes.string,
tooltip: PropTypes.bool,
};
export default CreateRoomButton;

View file

@ -248,6 +248,7 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
tooltipClassName={classNames("mx_Field_tooltip", tooltipClassName)}
visible={(this.state.focused && this.props.forceTooltipVisible) || this.state.feedbackVisible}
label={tooltipContent || this.state.feedback}
forceOnRight
/>;
}

View file

@ -27,18 +27,15 @@ export default createReactClass({
const h = this.props.h || 16;
const imgClass = this.props.imgClassName || "";
let divClass;
let imageSource;
if (SettingsStore.isFeatureEnabled('feature_new_spinner')) {
divClass = "mx_InlineSpinner mx_Spinner_spin";
imageSource = require("../../../../res/img/spinner.svg");
} else {
divClass = "mx_InlineSpinner";
imageSource = require("../../../../res/img/spinner.gif");
}
return (
<div className={divClass}>
<div className="mx_InlineSpinner">
<img
src={imageSource}
width={w}

View file

@ -1,336 +0,0 @@
/*
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 ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import classNames from 'classnames';
const InteractiveTooltipContainerId = "mx_InteractiveTooltip_Container";
// If the distance from tooltip to window edge is below this value, the tooltip
// will flip around to the other side of the target.
const MIN_SAFE_DISTANCE_TO_WINDOW_EDGE = 20;
function getOrCreateContainer() {
let container = document.getElementById(InteractiveTooltipContainerId);
if (!container) {
container = document.createElement("div");
container.id = InteractiveTooltipContainerId;
document.body.appendChild(container);
}
return container;
}
function isInRect(x, y, rect) {
const { top, right, bottom, left } = rect;
return x >= left && x <= right && y >= top && y <= bottom;
}
/**
* Returns the positive slope of the diagonal of the rect.
*
* @param {DOMRect} rect
* @return {integer}
*/
function getDiagonalSlope(rect) {
const { top, right, bottom, left } = rect;
return (bottom - top) / (right - left);
}
function isInUpperLeftHalf(x, y, rect) {
const { bottom, left } = rect;
// Negative slope because Y values grow downwards and for this case, the
// diagonal goes from larger to smaller Y values.
const diagonalSlope = getDiagonalSlope(rect) * -1;
return isInRect(x, y, rect) && (y <= bottom + diagonalSlope * (x - left));
}
function isInLowerRightHalf(x, y, rect) {
const { bottom, left } = rect;
// Negative slope because Y values grow downwards and for this case, the
// diagonal goes from larger to smaller Y values.
const diagonalSlope = getDiagonalSlope(rect) * -1;
return isInRect(x, y, rect) && (y >= bottom + diagonalSlope * (x - left));
}
function isInUpperRightHalf(x, y, rect) {
const { top, left } = rect;
// Positive slope because Y values grow downwards and for this case, the
// diagonal goes from smaller to larger Y values.
const diagonalSlope = getDiagonalSlope(rect) * 1;
return isInRect(x, y, rect) && (y <= top + diagonalSlope * (x - left));
}
function isInLowerLeftHalf(x, y, rect) {
const { top, left } = rect;
// Positive slope because Y values grow downwards and for this case, the
// diagonal goes from smaller to larger Y values.
const diagonalSlope = getDiagonalSlope(rect) * 1;
return isInRect(x, y, rect) && (y >= top + diagonalSlope * (x - left));
}
/*
* This style of tooltip takes a "target" element as its child and centers the
* tooltip along one edge of the target.
*/
export default class InteractiveTooltip extends React.Component {
static propTypes = {
// Content to show in the tooltip
content: PropTypes.node.isRequired,
// Function to call when visibility of the tooltip changes
onVisibilityChange: PropTypes.func,
// flag to forcefully hide this tooltip
forceHidden: PropTypes.bool,
};
constructor() {
super();
this.state = {
contentRect: null,
visible: false,
};
}
componentDidUpdate() {
// Whenever this passthrough component updates, also render the tooltip
// in a separate DOM tree. This allows the tooltip content to participate
// the normal React rendering cycle: when this component re-renders, the
// tooltip content re-renders.
// Once we upgrade to React 16, this could be done a bit more naturally
// using the portals feature instead.
this.renderTooltip();
}
componentWillUnmount() {
document.removeEventListener("mousemove", this.onMouseMove);
}
collectContentRect = (element) => {
// We don't need to clean up when unmounting, so ignore
if (!element) return;
this.setState({
contentRect: element.getBoundingClientRect(),
});
}
collectTarget = (element) => {
this.target = element;
}
canTooltipFitAboveTarget() {
const { contentRect } = this.state;
const targetRect = this.target.getBoundingClientRect();
const targetTop = targetRect.top + window.pageYOffset;
return (
!contentRect ||
(targetTop - contentRect.height > MIN_SAFE_DISTANCE_TO_WINDOW_EDGE)
);
}
onMouseMove = (ev) => {
const { clientX: x, clientY: y } = ev;
const { contentRect } = this.state;
const targetRect = this.target.getBoundingClientRect();
// When moving the mouse from the target to the tooltip, we create a
// safe area that includes the tooltip, the target, and the trapezoid
// ABCD between them:
// ┌───────────┐
// │ │
// │ │
// A └───E───F───┘ B
// V
// ┌─┐
// │ │
// C└─┘D
//
// As long as the mouse remains inside the safe area, the tooltip will
// stay open.
const buffer = 50;
if (isInRect(x, y, targetRect)) {
return;
}
if (this.canTooltipFitAboveTarget()) {
const contentRectWithBuffer = {
top: contentRect.top - buffer,
right: contentRect.right + buffer,
bottom: contentRect.bottom,
left: contentRect.left - buffer,
};
const trapezoidLeft = {
top: contentRect.bottom,
right: targetRect.left,
bottom: targetRect.bottom,
left: contentRect.left - buffer,
};
const trapezoidCenter = {
top: contentRect.bottom,
right: targetRect.right,
bottom: targetRect.bottom,
left: targetRect.left,
};
const trapezoidRight = {
top: contentRect.bottom,
right: contentRect.right + buffer,
bottom: targetRect.bottom,
left: targetRect.right,
};
if (
isInRect(x, y, contentRectWithBuffer) ||
isInUpperRightHalf(x, y, trapezoidLeft) ||
isInRect(x, y, trapezoidCenter) ||
isInUpperLeftHalf(x, y, trapezoidRight)
) {
return;
}
} else {
const contentRectWithBuffer = {
top: contentRect.top,
right: contentRect.right + buffer,
bottom: contentRect.bottom + buffer,
left: contentRect.left - buffer,
};
const trapezoidLeft = {
top: targetRect.top,
right: targetRect.left,
bottom: contentRect.top,
left: contentRect.left - buffer,
};
const trapezoidCenter = {
top: targetRect.top,
right: targetRect.right,
bottom: contentRect.top,
left: targetRect.left,
};
const trapezoidRight = {
top: targetRect.top,
right: contentRect.right + buffer,
bottom: contentRect.top,
left: targetRect.right,
};
if (
isInRect(x, y, contentRectWithBuffer) ||
isInLowerRightHalf(x, y, trapezoidLeft) ||
isInRect(x, y, trapezoidCenter) ||
isInLowerLeftHalf(x, y, trapezoidRight)
) {
return;
}
}
this.hideTooltip();
}
onTargetMouseOver = (ev) => {
this.showTooltip();
}
showTooltip() {
// Don't enter visible state if we haven't collected the target yet
if (!this.target) {
return;
}
this.setState({
visible: true,
});
if (this.props.onVisibilityChange) {
this.props.onVisibilityChange(true);
}
document.addEventListener("mousemove", this.onMouseMove);
}
hideTooltip() {
this.setState({
visible: false,
});
if (this.props.onVisibilityChange) {
this.props.onVisibilityChange(false);
}
document.removeEventListener("mousemove", this.onMouseMove);
}
renderTooltip() {
const { contentRect, visible } = this.state;
if (this.props.forceHidden === true || !visible) {
ReactDOM.render(null, getOrCreateContainer());
return null;
}
const targetRect = this.target.getBoundingClientRect();
// The window X and Y offsets are to adjust position when zoomed in to page
const targetLeft = targetRect.left + window.pageXOffset;
const targetBottom = targetRect.bottom + window.pageYOffset;
const targetTop = targetRect.top + window.pageYOffset;
// Place the tooltip above the target by default. If we find that the
// tooltip content would extend past the safe area towards the window
// edge, flip around to below the target.
const position = {};
let chevronFace = null;
if (this.canTooltipFitAboveTarget()) {
position.bottom = window.innerHeight - targetTop;
chevronFace = "bottom";
} else {
position.top = targetBottom;
chevronFace = "top";
}
// Center the tooltip horizontally with the target's center.
position.left = targetLeft + targetRect.width / 2;
const chevron = <div className={"mx_InteractiveTooltip_chevron_" + chevronFace} />;
const menuClasses = classNames({
'mx_InteractiveTooltip': true,
'mx_InteractiveTooltip_withChevron_top': chevronFace === 'top',
'mx_InteractiveTooltip_withChevron_bottom': chevronFace === 'bottom',
});
const menuStyle = {};
if (contentRect) {
menuStyle.left = `-${contentRect.width / 2}px`;
}
const tooltip = <div className="mx_InteractiveTooltip_wrapper" style={{...position}}>
<div className={menuClasses}
style={menuStyle}
ref={this.collectContentRect}
>
{chevron}
{this.props.content}
</div>
</div>;
ReactDOM.render(tooltip, getOrCreateContainer());
}
render() {
// We use `cloneElement` here to append some props to the child content
// without using a wrapper element which could disrupt layout.
return React.cloneElement(this.props.children, {
ref: this.collectTarget,
onMouseOver: this.onTargetMouseOver,
});
}
}

View file

@ -17,10 +17,10 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";
import AccessibleTooltipButton from "./AccessibleTooltipButton";
export default class ManageIntegsButton extends React.Component {
constructor(props) {
@ -45,9 +45,8 @@ export default class ManageIntegsButton extends React.Component {
render() {
let integrationsButton = <div />;
if (IntegrationManagers.sharedInstance().hasManager()) {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
integrationsButton = (
<AccessibleButton
<AccessibleTooltipButton
className='mx_RoomHeader_button mx_RoomHeader_manageIntegsButton'
title={_t("Manage Integrations")}
onClick={this.onManageIntegrations}

View file

@ -27,6 +27,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import escapeHtml from "escape-html";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {Action} from "../../../dispatcher/actions";
import sanitizeHtml from "sanitize-html";
// This component does no cycle detection, simply because the only way to make such a cycle would be to
// craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would
@ -92,7 +93,21 @@ export default class ReplyThread extends React.Component {
// Part of Replies fallback support
static stripHTMLReply(html) {
return html.replace(/^<mx-reply>[\s\S]+?<\/mx-reply>/, '');
// Sanitize the original HTML for inclusion in <mx-reply>. We allow
// any HTML, since the original sender could use special tags that we
// don't recognize, but want to pass along to any recipients who do
// recognize them -- recipients should be sanitizing before displaying
// anyways. However, we sanitize to 1) remove any mx-reply, so that we
// don't generate a nested mx-reply, and 2) make sure that the HTML is
// properly formatted (e.g. tags are closed where necessary)
return sanitizeHtml(
html,
{
allowedTags: false, // false means allow everything
allowedAttributes: false,
exclusiveFilter: (frame) => frame.tag === "mx-reply",
},
);
}
// Part of Replies fallback support
@ -102,15 +117,19 @@ export default class ReplyThread extends React.Component {
let {body, formatted_body: html} = ev.getContent();
if (this.getParentEventId(ev)) {
if (body) body = this.stripPlainReply(body);
if (html) html = this.stripHTMLReply(html);
}
if (!body) body = ""; // Always ensure we have a body, for reasons.
// Escape the body to use as HTML below.
// We also run a nl2br over the result to fix the fallback representation. We do this
// after converting the text to safe HTML to avoid user-provided BR's from being converted.
if (!html) html = escapeHtml(body).replace(/\n/g, '<br/>');
if (html) {
// sanitize the HTML before we put it in an <mx-reply>
html = this.stripHTMLReply(html);
} else {
// Escape the body to use as HTML below.
// We also run a nl2br over the result to fix the fallback representation. We do this
// after converting the text to safe HTML to avoid user-provided BR's from being converted.
html = escapeHtml(body).replace(/\n/g, '<br/>');
}
// dev note: do not rely on `body` being safe for HTML usage below.

View file

@ -21,18 +21,15 @@ import {_t} from "../../../languageHandler";
import SettingsStore from "../../../settings/SettingsStore";
const Spinner = ({w = 32, h = 32, imgClassName, message}) => {
let divClass;
let imageSource;
if (SettingsStore.isFeatureEnabled('feature_new_spinner')) {
divClass = "mx_Spinner mx_Spinner_spin";
imageSource = require("../../../../res/img/spinner.svg");
} else {
divClass = "mx_Spinner";
imageSource = require("../../../../res/img/spinner.gif");
}
return (
<div className={divClass}>
<div className="mx_Spinner">
{ message && <React.Fragment><div className="mx_Spinner_Msg">{ message}</div>&nbsp;</React.Fragment> }
<img
src={imageSource}

View file

@ -29,6 +29,7 @@ import FlairStore from '../../../stores/FlairStore';
import GroupStore from '../../../stores/GroupStore';
import TagOrderStore from '../../../stores/TagOrderStore';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import AccessibleButton from "./AccessibleButton";
// A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents
// a thing to click on for the user to filter the visible rooms in the RoomList to:
@ -114,7 +115,7 @@ export default createReactClass({
this.setState({ hover: true });
},
onMouseOut: function() {
onMouseLeave: function() {
this.setState({ hover: false });
},
@ -130,7 +131,7 @@ export default createReactClass({
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const profile = this.state.profile || {};
const name = profile.name || this.props.tag;
const avatarHeight = 40;
const avatarHeight = 32;
const httpUrl = profile.avatarUrl ? this.context.mxcUrlToHttp(
profile.avatarUrl, avatarHeight, avatarHeight, "crop",
@ -151,11 +152,14 @@ export default createReactClass({
badgeElement = (<div className={badgeClasses}>{FormattingUtils.formatCount(badge.count)}</div>);
}
// FIXME: this ought to use AccessibleButton for a11y but that causes onMouseOut/onMouseOver to fire too much
const contextButton = this.state.hover || this.props.menuDisplayed ?
<div className="mx_TagTile_context_button" onClick={this.openMenu} ref={this.props.contextMenuButtonRef}>
<AccessibleButton
className="mx_TagTile_context_button"
onClick={this.openMenu}
ref={this.props.contextMenuButtonRef}
>
{"\u00B7\u00B7\u00B7"}
</div> : <div ref={this.props.contextMenuButtonRef} />;
</AccessibleButton> : <div ref={this.props.contextMenuButtonRef} />;
const AccessibleTooltipButton = sdk.getComponent("elements.AccessibleTooltipButton");
@ -168,7 +172,7 @@ export default createReactClass({
<div
className="mx_TagTile_avatar"
onMouseOver={this.onMouseOver}
onMouseOut={this.onMouseOut}
onMouseLeave={this.onMouseLeave}
>
<BaseAvatar
name={name}

View file

@ -37,7 +37,7 @@ export default class TextWithTooltip extends React.Component {
this.setState({hover: true});
};
onMouseOut = () => {
onMouseLeave = () => {
this.setState({hover: false});
};
@ -45,7 +45,7 @@ export default class TextWithTooltip extends React.Component {
const Tooltip = sdk.getComponent("elements.Tooltip");
return (
<span onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} className={this.props.class}>
<span onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className={this.props.class}>
{this.props.children}
<Tooltip
label={this.props.tooltip}

View file

@ -18,18 +18,15 @@ limitations under the License.
*/
import React, { Component } from 'react';
import React, {Component, CSSProperties} from 'react';
import ReactDOM from 'react-dom';
import dis from '../../../dispatcher/dispatcher';
import classNames from 'classnames';
import { ViewTooltipPayload } from '../../../dispatcher/payloads/ViewTooltipPayload';
import { Action } from '../../../dispatcher/actions';
const MIN_TOOLTIP_HEIGHT = 25;
interface IProps {
// Class applied to the element used to position the tooltip
className: string;
className?: string;
// Class applied to the tooltip itself
tooltipClassName?: string;
// Whether the tooltip is visible or hidden.
@ -38,6 +35,7 @@ interface IProps {
visible?: boolean;
// the react element to put into the tooltip
label: React.ReactNode;
forceOnRight?: boolean;
}
export default class Tooltip extends React.Component<IProps> {
@ -68,18 +66,12 @@ export default class Tooltip extends React.Component<IProps> {
// Remove the wrapper element, as the tooltip has finished using it
public componentWillUnmount() {
dis.dispatch<ViewTooltipPayload>({
action: Action.ViewTooltip,
tooltip: null,
parent: null,
});
ReactDOM.unmountComponentAtNode(this.tooltipContainer);
document.body.removeChild(this.tooltipContainer);
window.removeEventListener('scroll', this.renderTooltip, true);
}
private updatePosition(style: {[key: string]: any}) {
private updatePosition(style: CSSProperties) {
const parentBox = this.parent.getBoundingClientRect();
let offset = 0;
if (parentBox.height > MIN_TOOLTIP_HEIGHT) {
@ -89,8 +81,14 @@ export default class Tooltip extends React.Component<IProps> {
// we need so that we're still centered.
offset = Math.floor(parentBox.height - MIN_TOOLTIP_HEIGHT);
}
style.top = (parentBox.top - 2) + window.pageYOffset + offset;
style.left = 6 + parentBox.right + window.pageXOffset;
if (!this.props.forceOnRight && parentBox.right > window.innerWidth / 2) {
style.right = window.innerWidth - parentBox.right - window.pageXOffset - 8;
} else {
style.left = parentBox.right + window.pageXOffset + 6;
}
return style;
}
@ -99,7 +97,6 @@ export default class Tooltip extends React.Component<IProps> {
// positioned, also taking into account any window zoom
// NOTE: The additional 6 pixels for the left position, is to take account of the
// tooltips chevron
const parent = ReactDOM.findDOMNode(this).parentNode as Element;
const style = this.updatePosition({});
// Hide the entire container when not visible. This prevents flashing of the tooltip
// if it is not meant to be visible on first mount.
@ -119,19 +116,12 @@ export default class Tooltip extends React.Component<IProps> {
// Render the tooltip manually, as we wish it not to be rendered within the parent
this.tooltip = ReactDOM.render<Element>(tooltip, this.tooltipContainer);
// Tell the roomlist about us so it can manipulate us if it wishes
dis.dispatch<ViewTooltipPayload>({
action: Action.ViewTooltip,
tooltip: this.tooltip,
parent: parent,
});
};
public render() {
// Render a placeholder
return (
<div className={this.props.className} >
<div className={this.props.className}>
</div>
);
}

View file

@ -34,7 +34,7 @@ export default createReactClass({
});
},
onMouseOut: function() {
onMouseLeave: function() {
this.setState({
hover: false,
});
@ -48,7 +48,7 @@ export default createReactClass({
label={this.props.helpText}
/> : <div />;
return (
<div className="mx_TooltipButton" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} >
<div className="mx_TooltipButton" onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave}>
?
{ tip }
</div>

View file

@ -24,7 +24,6 @@ import GroupStore from '../../../stores/GroupStore';
import PropTypes from 'prop-types';
import { showGroupInviteDialog } from '../../../GroupAddressPicker';
import AccessibleButton from '../elements/AccessibleButton';
import TintableSvg from '../elements/TintableSvg';
import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
@ -211,15 +210,13 @@ export default createReactClass({
let inviteButton;
if (GroupStore.isUserPrivileged(this.props.groupId)) {
inviteButton = (
<AccessibleButton
className="mx_RightPanel_invite"
onClick={this.onInviteToGroupButtonClick}
>
<div className="mx_RightPanel_icon" >
<TintableSvg src={require("../../../../res/img/icon-invite-people.svg")} width="18" height="14" />
</div>
<div className="mx_RightPanel_message">{ _t('Invite to this community') }</div>
</AccessibleButton>);
<AccessibleButton
className="mx_MemberList_invite mx_MemberList_inviteCommunity"
onClick={this.onInviteToGroupButtonClick}
>
<span>{ _t('Invite to this community') }</span>
</AccessibleButton>
);
}
return (

View file

@ -21,7 +21,6 @@ import GroupStore from '../../../stores/GroupStore';
import PropTypes from 'prop-types';
import { showGroupAddRoomDialog } from '../../../GroupAddressPicker';
import AccessibleButton from '../elements/AccessibleButton';
import TintableSvg from '../elements/TintableSvg';
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
const INITIAL_LOAD_NUM_ROOMS = 30;
@ -135,13 +134,10 @@ export default createReactClass({
if (GroupStore.isUserPrivileged(this.props.groupId)) {
inviteButton = (
<AccessibleButton
className="mx_RightPanel_invite"
className="mx_MemberList_invite mx_MemberList_addRoomToCommunity"
onClick={this.onAddRoomToGroupButtonClick}
>
<div className="mx_RightPanel_icon" >
<TintableSvg src={require("../../../../res/img/icons-room-add.svg")} width="18" height="14" />
</div>
<div className="mx_RightPanel_message">{ _t('Add rooms to this community') }</div>
<span>{ _t('Add rooms to this community') }</span>
</AccessibleButton>
);
}

View file

@ -22,12 +22,15 @@ import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import {aboveLeftOf, ContextMenu, ContextMenuButton, useContextMenu} from '../../structures/ContextMenu';
import {aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu} from '../../structures/ContextMenu';
import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
import RoomContext from "../../../contexts/RoomContext";
import Toolbar from "../../../accessibility/Toolbar";
import {RovingAccessibleTooltipButton, useRovingTabIndex} from "../../../accessibility/RovingTabIndex";
const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
const [onFocus, isActive, ref] = useRovingTabIndex(button);
useEffect(() => {
onFocusChange(menuDisplayed);
}, [onFocusChange, menuDisplayed]);
@ -52,12 +55,14 @@ const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFo
}
return <React.Fragment>
<ContextMenuButton
<ContextMenuTooltipButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_optionsButton"
label={_t("Options")}
title={_t("Options")}
onClick={openMenu}
isExpanded={menuDisplayed}
inputRef={button}
inputRef={ref}
onFocus={onFocus}
tabIndex={isActive ? 0 : -1}
/>
{ contextMenu }
@ -66,6 +71,7 @@ const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFo
const ReactButton = ({mxEvent, reactions, onFocusChange}) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
const [onFocus, isActive, ref] = useRovingTabIndex(button);
useEffect(() => {
onFocusChange(menuDisplayed);
}, [onFocusChange, menuDisplayed]);
@ -80,12 +86,14 @@ const ReactButton = ({mxEvent, reactions, onFocusChange}) => {
}
return <React.Fragment>
<ContextMenuButton
<ContextMenuTooltipButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_reactButton"
label={_t("React")}
title={_t("React")}
onClick={openMenu}
isExpanded={menuDisplayed}
inputRef={button}
inputRef={ref}
onFocus={onFocus}
tabIndex={isActive ? 0 : -1}
/>
{ contextMenu }
@ -148,8 +156,6 @@ export default class MessageActionBar extends React.PureComponent {
};
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let reactButton;
let replyButton;
let editButton;
@ -161,7 +167,7 @@ export default class MessageActionBar extends React.PureComponent {
);
}
if (this.context.canReply) {
replyButton = <AccessibleButton
replyButton = <RovingAccessibleTooltipButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"
title={_t("Reply")}
onClick={this.onReplyClick}
@ -169,7 +175,7 @@ export default class MessageActionBar extends React.PureComponent {
}
}
if (canEditContent(this.props.mxEvent)) {
editButton = <AccessibleButton
editButton = <RovingAccessibleTooltipButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_editButton"
title={_t("Edit")}
onClick={this.onEditClick}
@ -177,7 +183,7 @@ export default class MessageActionBar extends React.PureComponent {
}
// aria-live=off to not have this read out automatically as navigating around timeline, gets repetitive.
return <div className="mx_MessageActionBar" role="toolbar" aria-label={_t("Message Actions")} aria-live="off">
return <Toolbar className="mx_MessageActionBar" aria-label={_t("Message Actions")} aria-live="off">
{reactButton}
{replyButton}
{editButton}
@ -188,6 +194,6 @@ export default class MessageActionBar extends React.PureComponent {
permalinkCreator={this.props.permalinkCreator}
onFocusChange={this.onFocusChange}
/>
</div>;
</Toolbar>;
}
}

View file

@ -74,7 +74,7 @@ export default class ReactionsRowButton extends React.PureComponent {
});
}
onMouseOut = () => {
onMouseLeave = () => {
this.setState({
tooltipVisible: false,
});
@ -129,11 +129,12 @@ export default class ReactionsRowButton extends React.PureComponent {
}
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return <AccessibleButton className={classes}
return <AccessibleButton
className={classes}
aria-label={label}
onClick={this.onClick}
onMouseOver={this.onMouseOver}
onMouseOut={this.onMouseOut}
onMouseLeave={this.onMouseLeave}
>
<span className="mx_ReactionsRowButton_content" aria-hidden="true">
{content}

View file

@ -55,7 +55,7 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent {
},
{
reactors: () => {
return <div className="mx_ReactionsRowButtonTooltip_senders">
return <div className="mx_Tooltip_title">
{formatCommaSeparatedList(senders, 6)}
</div>;
},
@ -63,7 +63,7 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent {
if (!shortName) {
return null;
}
return <div className="mx_ReactionsRowButtonTooltip_reactedWith">
return <div className="mx_Tooltip_sub">
{sub}
</div>;
},
@ -73,11 +73,7 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent {
let tooltip;
if (tooltipLabel) {
tooltip = <Tooltip
tooltipClassName="mx_Tooltip_timeline"
visible={visible}
label={tooltipLabel}
/>;
tooltip = <Tooltip visible={visible} label={tooltipLabel} />;
}
return tooltip;

View file

@ -125,8 +125,10 @@ export default createReactClass({
</span>;
const content = this.props.text ?
<span className="mx_SenderProfile_aux">
{ _t(this.props.text, { senderName: () => nameElem }) }
<span>
<span className="mx_SenderProfile_aux">
{ _t(this.props.text, { senderName: () => nameElem }) }
</span>
</span> : nameFlair;
return (

View file

@ -35,6 +35,7 @@ import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import {isPermalinkHost} from "../../../utils/permalinks/Permalinks";
import {toRightOf} from "../../structures/ContextMenu";
import {copyPlaintext} from "../../../utils/strings";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
export default createReactClass({
displayName: 'TextualBody',
@ -146,7 +147,6 @@ export default createReactClass({
nextProps.showUrlPreview !== this.props.showUrlPreview ||
nextProps.editState !== this.props.editState ||
nextState.links !== this.state.links ||
nextState.editedMarkerHovered !== this.state.editedMarkerHovered ||
nextState.widgetHidden !== this.state.widgetHidden);
},
@ -367,42 +367,33 @@ export default createReactClass({
});
},
_onMouseEnterEditedMarker: function() {
this.setState({editedMarkerHovered: true});
},
_onMouseLeaveEditedMarker: function() {
this.setState({editedMarkerHovered: false});
},
_openHistoryDialog: async function() {
const MessageEditHistoryDialog = sdk.getComponent("views.dialogs.MessageEditHistoryDialog");
Modal.createDialog(MessageEditHistoryDialog, {mxEvent: this.props.mxEvent});
},
_renderEditedMarker: function() {
let editedTooltip;
if (this.state.editedMarkerHovered) {
const Tooltip = sdk.getComponent('elements.Tooltip');
const date = this.props.mxEvent.replacingEventDate();
const dateString = date && formatDate(date);
editedTooltip = <Tooltip
tooltipClassName="mx_Tooltip_timeline"
label={_t("Edited at %(date)s. Click to view edits.", {date: dateString})}
/>;
}
const date = this.props.mxEvent.replacingEventDate();
const dateString = date && formatDate(date);
const tooltip = <div>
<div className="mx_Tooltip_title">
{_t("Edited at %(date)s", {date: dateString})}
</div>
<div className="mx_Tooltip_sub">
{_t("Click to view edits")}
</div>
</div>;
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (
<AccessibleButton
key="editedMarker"
<AccessibleTooltipButton
className="mx_EventTile_edited"
onClick={this._openHistoryDialog}
onMouseEnter={this._onMouseEnterEditedMarker}
onMouseLeave={this._onMouseLeaveEditedMarker}
title={_t("Edited at %(date)s. Click to view edits.", {date: dateString})}
tooltip={tooltip}
>
{ editedTooltip }<span>{`(${_t("edited")})`}</span>
</AccessibleButton>
<span>{`(${_t("edited")})`}</span>
</AccessibleTooltipButton>
);
},

View file

@ -22,7 +22,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Analytics from '../../../Analytics';
import AccessibleButton from '../elements/AccessibleButton';
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
export default class HeaderButton extends React.Component {
constructor() {
@ -42,13 +42,13 @@ export default class HeaderButton extends React.Component {
[`mx_RightPanel_${this.props.name}`]: true,
});
return <AccessibleButton
return <AccessibleTooltipButton
aria-selected={this.props.isHighlighted}
role="tab"
title={this.props.title}
className={classes}
onClick={this.onClick}>
</AccessibleButton>;
onClick={this.onClick}
/>;
}
}

View file

@ -1287,11 +1287,11 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
);
// only display the devices list if our client supports E2E
const _enableDevices = cli.isCryptoEnabled();
const cryptoEnabled = cli.isCryptoEnabled();
let text;
if (!isRoomEncrypted) {
if (!_enableDevices) {
if (!cryptoEnabled) {
text = _t("This client does not support end-to-end encryption.");
} else if (room) {
text = _t("Messages in this room are not end-to-end encrypted.");
@ -1305,10 +1305,10 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
let verifyButton;
const homeserverSupportsCrossSigning = useHomeserverSupportsCrossSigning(cli);
const userTrust = cli.checkUserTrust(member.userId);
const userVerified = userTrust.isCrossSigningVerified();
const userTrust = cryptoEnabled && cli.checkUserTrust(member.userId);
const userVerified = cryptoEnabled && userTrust.isCrossSigningVerified();
const isMe = member.userId === cli.getUserId();
const canVerify = homeserverSupportsCrossSigning && !userVerified && !isMe;
const canVerify = cryptoEnabled && homeserverSupportsCrossSigning && !userVerified && !isMe;
const setUpdating = (updating) => {
setPendingUpdateCount(count => count + (updating ? 1 : -1));
@ -1345,10 +1345,10 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
<h3>{ _t("Security") }</h3>
<p>{ text }</p>
{ verifyButton }
<DevicesSection
{ cryptoEnabled && <DevicesSection
loading={showDeviceListSpinner}
devices={devices}
userId={member.userId} />
userId={member.userId} /> }
</div>
);

View file

@ -24,6 +24,7 @@ import {SCAN_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode"
import VerificationQRCode from "../elements/crypto/VerificationQRCode";
import {_t} from "../../../languageHandler";
import SdkConfig from "../../../SdkConfig";
import E2EIcon from "../rooms/E2EIcon";
import {
PHASE_UNSENT,
@ -63,9 +64,15 @@ export default class VerificationPanel extends React.PureComponent {
const showSAS = request.otherPartySupportsMethod(verificationMethods.SAS);
const showQR = request.otherPartySupportsMethod(SCAN_QR_CODE_METHOD);
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const brand = SdkConfig.get().brand;
const noCommonMethodError = !showSAS && !showQR ?
<p>{_t("The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what Riot supports. Try with a different client.")}</p> :
<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 " +
"with a different client.",
{ brand },
)}</p> :
null;
if (this.props.layout === 'dialog') {

View file

@ -28,6 +28,7 @@ import classNames from 'classnames';
import RateLimitedFunc from '../../../ratelimitedfunc';
import SettingsStore from "../../../settings/SettingsStore";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import CallView from "../voip/CallView";
export default createReactClass({
@ -142,7 +143,6 @@ export default createReactClass({
},
render: function() {
const CallView = sdk.getComponent("voip.CallView");
const TintableSvg = sdk.getComponent("elements.TintableSvg");
let fileDropTarget = null;

View file

@ -42,11 +42,12 @@ const crossSigningRoomTitles = {
[E2E_STATE.VERIFIED]: _td("Everyone in this room is verified"),
};
const E2EIcon = ({isUser, status, className, size, onClick, hideTooltip}) => {
const E2EIcon = ({isUser, status, className, size, onClick, hideTooltip, bordered}) => {
const [hover, setHover] = useState(false);
const classes = classNames({
mx_E2EIcon: true,
mx_E2EIcon_bordered: bordered,
mx_E2EIcon_warning: status === E2E_STATE.WARNING,
mx_E2EIcon_normal: status === E2E_STATE.NORMAL,
mx_E2EIcon_verified: status === E2E_STATE.VERIFIED,
@ -65,7 +66,7 @@ const E2EIcon = ({isUser, status, className, size, onClick, hideTooltip}) => {
}
const onMouseOver = () => setHover(true);
const onMouseOut = () => setHover(false);
const onMouseLeave = () => setHover(false);
let tip;
if (hover && !hideTooltip) {
@ -77,7 +78,7 @@ const E2EIcon = ({isUser, status, className, size, onClick, hideTooltip}) => {
<AccessibleButton
onClick={onClick}
onMouseOver={onMouseOver}
onMouseOut={onMouseOut}
onMouseLeave={onMouseLeave}
className={classes}
style={style}
>
@ -86,7 +87,7 @@ const E2EIcon = ({isUser, status, className, size, onClick, hideTooltip}) => {
);
}
return <div onMouseOver={onMouseOver} onMouseOut={onMouseOut} className={classes} style={style}>
return <div onMouseOver={onMouseOver} onMouseLeave={onMouseLeave} className={classes} style={style}>
{ tip }
</div>;
};

View file

@ -170,7 +170,7 @@ const EntityTile = createReactClass({
let e2eIcon;
const { e2eStatus } = this.props;
if (e2eStatus) {
e2eIcon = <E2EIcon status={e2eStatus} isUser={true} />;
e2eIcon = <E2EIcon status={e2eStatus} isUser={true} bordered={true} />;
}
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');

View file

@ -333,7 +333,7 @@ export default createReactClass({
return;
}
const eventSenderTrust = this.context.checkDeviceTrust(
const eventSenderTrust = encryptionInfo.sender && this.context.checkDeviceTrust(
senderId, encryptionInfo.sender.deviceId,
);
if (!eventSenderTrust) {

View file

@ -1,53 +0,0 @@
/*
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 * as sdk from '../../../index';
export default class InviteOnlyIcon extends React.Component {
constructor() {
super();
this.state = {
hover: false,
};
}
onHoverStart = () => {
this.setState({hover: true});
};
onHoverEnd = () => {
this.setState({hover: false});
};
render() {
const classes = this.props.collapsedPanel ? "mx_InviteOnlyIcon_small": "mx_InviteOnlyIcon_large";
const Tooltip = sdk.getComponent("elements.Tooltip");
let tooltip;
if (this.state.hover) {
tooltip = <Tooltip className="mx_InviteOnlyIcon_tooltip" label={_t("Invite only")} dir="auto" />;
}
return (<div className={classes}
onMouseEnter={this.onHoverStart}
onMouseLeave={this.onHoverEnd}
>
{ tooltip }
</div>);
}
}

View file

@ -27,7 +27,8 @@ import { makeRoomPermalink } from '../../../utils/permalinks/Permalinks';
import ContentMessages from '../../../ContentMessages';
import E2EIcon from './E2EIcon';
import SettingsStore from "../../../settings/SettingsStore";
import {aboveLeftOf, ContextMenu, ContextMenuButton, useContextMenu} from "../../structures/ContextMenu";
import {aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu} from "../../structures/ContextMenu";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
function ComposerAvatar(props) {
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
@ -41,7 +42,6 @@ ComposerAvatar.propTypes = {
};
function CallButton(props) {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const onVoiceCallClick = (ev) => {
dis.dispatch({
action: 'place_call',
@ -50,10 +50,11 @@ function CallButton(props) {
});
};
return (<AccessibleButton className="mx_MessageComposer_button mx_MessageComposer_voicecall"
onClick={onVoiceCallClick}
title={_t('Voice call')}
/>);
return (<AccessibleTooltipButton
className="mx_MessageComposer_button mx_MessageComposer_voicecall"
onClick={onVoiceCallClick}
title={_t('Voice call')}
/>);
}
CallButton.propTypes = {
@ -61,7 +62,6 @@ CallButton.propTypes = {
};
function VideoCallButton(props) {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const onCallClick = (ev) => {
dis.dispatch({
action: 'place_call',
@ -70,7 +70,8 @@ function VideoCallButton(props) {
});
};
return <AccessibleButton className="mx_MessageComposer_button mx_MessageComposer_videocall"
return <AccessibleTooltipButton
className="mx_MessageComposer_button mx_MessageComposer_videocall"
onClick={onCallClick}
title={_t('Video call')}
/>;
@ -117,14 +118,15 @@ const EmojiButton = ({addEmoji}) => {
}
return <React.Fragment>
<ContextMenuButton className="mx_MessageComposer_button mx_MessageComposer_emoji"
onClick={openMenu}
isExpanded={menuDisplayed}
label={_t('Emoji picker')}
inputRef={button}
<ContextMenuTooltipButton
className="mx_MessageComposer_button mx_MessageComposer_emoji"
onClick={openMenu}
isExpanded={menuDisplayed}
title={_t('Emoji picker')}
inputRef={button}
>
</ContextMenuButton>
</ContextMenuTooltipButton>
{ contextMenu }
</React.Fragment>;
@ -185,9 +187,9 @@ class UploadButton extends React.Component {
render() {
const uploadInputStyle = {display: 'none'};
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (
<AccessibleButton className="mx_MessageComposer_button mx_MessageComposer_upload"
<AccessibleTooltipButton
className="mx_MessageComposer_button mx_MessageComposer_upload"
onClick={this.onUploadClick}
title={_t('Upload file')}
>
@ -198,7 +200,7 @@ class UploadButton extends React.Component {
multiple
onChange={this.onUploadFileInputChange}
/>
</AccessibleButton>
</AccessibleTooltipButton>
);
}
}

View file

@ -17,9 +17,8 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import classNames from 'classnames';
import AccessibleButton from "../elements/AccessibleButton";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
export default class MessageComposerFormatBar extends React.PureComponent {
static propTypes = {
@ -68,28 +67,28 @@ class FormatButton extends React.PureComponent {
};
render() {
const InteractiveTooltip = sdk.getComponent('elements.InteractiveTooltip');
const className = `mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIcon${this.props.icon}`;
let shortcut;
if (this.props.shortcut) {
shortcut = <div className="mx_MessageComposerFormatBar_tooltipShortcut">{this.props.shortcut}</div>;
}
const tooltipContent = (
<div className="mx_MessageComposerFormatBar_buttonTooltip">
<div>{this.props.label}</div>
const tooltip = <div>
<div className="mx_Tooltip_title">
{this.props.label}
</div>
<div className="mx_Tooltip_sub">
{shortcut}
</div>
);
</div>;
return (
<InteractiveTooltip content={tooltipContent} forceHidden={!this.props.visible}>
<AccessibleButton
as="span"
role="button"
onClick={this.props.onClick}
aria-label={this.props.label}
className={className} />
</InteractiveTooltip>
<AccessibleTooltipButton
as="span"
role="button"
onClick={this.props.onClick}
title={this.props.label}
tooltip={tooltip}
className={className} />
);
}
}

View file

@ -1,394 +0,0 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {createRef} from "react";
import dis from "../../../dispatcher/dispatcher";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import AccessibleButton from '../elements/AccessibleButton';
import RoomAvatar from '../avatars/RoomAvatar';
import classNames from 'classnames';
import * as sdk from "../../../index";
import Analytics from "../../../Analytics";
import * as RoomNotifs from '../../../RoomNotifs';
import * as FormattingUtils from "../../../utils/FormattingUtils";
import DMRoomMap from "../../../utils/DMRoomMap";
import {_t} from "../../../languageHandler";
const MAX_ROOMS = 20;
const MIN_ROOMS_BEFORE_ENABLED = 10;
// The threshold time in milliseconds to wait for an autojoined room to show up.
const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90 seconds
export default class RoomBreadcrumbs extends React.Component {
constructor(props) {
super(props);
this.state = {rooms: [], enabled: false};
this.onAction = this.onAction.bind(this);
this._dispatcherRef = null;
// The room IDs we're waiting to come down the Room handler and when we
// started waiting for them. Used to track a room over an upgrade/autojoin.
this._waitingRoomQueue = [/* { roomId, addedTs } */];
this._scroller = createRef();
}
// TODO: [REACT-WARNING] Move this to constructor
UNSAFE_componentWillMount() { // eslint-disable-line camelcase
this._dispatcherRef = dis.register(this.onAction);
const storedRooms = SettingsStore.getValue("breadcrumb_rooms");
this._loadRoomIds(storedRooms || []);
this._settingWatchRef = SettingsStore.watchSetting("breadcrumb_rooms", null, this.onBreadcrumbsChanged);
this.setState({enabled: this._shouldEnable()});
MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership);
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
MatrixClientPeg.get().on("Event.decrypted", this.onEventDecrypted);
MatrixClientPeg.get().on("Room", this.onRoom);
}
componentWillUnmount() {
dis.unregister(this._dispatcherRef);
SettingsStore.unwatchSetting(this._settingWatchRef);
const client = MatrixClientPeg.get();
if (client) {
client.removeListener("Room.myMembership", this.onMyMembership);
client.removeListener("Room.receipt", this.onRoomReceipt);
client.removeListener("Room.timeline", this.onRoomTimeline);
client.removeListener("Event.decrypted", this.onEventDecrypted);
client.removeListener("Room", this.onRoom);
}
}
componentDidUpdate() {
const rooms = this.state.rooms.slice();
if (rooms.length) {
const roomModel = rooms[0];
if (!roomModel.animated) {
roomModel.animated = true;
setTimeout(() => this.setState({rooms}), 0);
}
}
}
onAction(payload) {
switch (payload.action) {
case 'view_room':
if (payload.auto_join && !MatrixClientPeg.get().getRoom(payload.room_id)) {
// Queue the room instead of pushing it immediately - we're probably just waiting
// for a join to complete (ie: joining the upgraded room).
this._waitingRoomQueue.push({roomId: payload.room_id, addedTs: (new Date).getTime()});
break;
}
this._appendRoomId(payload.room_id);
break;
// XXX: slight hack in order to zero the notification count when a room
// is read. Copied from RoomTile
case 'on_room_read': {
const room = MatrixClientPeg.get().getRoom(payload.roomId);
this._calculateRoomBadges(room, /*zero=*/true);
break;
}
}
}
onMyMembership = (room, membership) => {
if (membership === "leave" || membership === "ban") {
const rooms = this.state.rooms.slice();
const roomState = rooms.find((r) => r.room.roomId === room.roomId);
if (roomState) {
roomState.left = true;
this.setState({rooms});
}
}
this.onRoomMembershipChanged();
};
onRoomReceipt = (event, room) => {
if (this.state.rooms.map(r => r.room.roomId).includes(room.roomId)) {
this._calculateRoomBadges(room);
}
};
onRoomTimeline = (event, room) => {
if (!room) return; // Can be null for the notification timeline, etc.
if (this.state.rooms.map(r => r.room.roomId).includes(room.roomId)) {
this._calculateRoomBadges(room);
}
};
onEventDecrypted = (event) => {
if (this.state.rooms.map(r => r.room.roomId).includes(event.getRoomId())) {
this._calculateRoomBadges(MatrixClientPeg.get().getRoom(event.getRoomId()));
}
};
onBreadcrumbsChanged = (settingName, roomId, level, valueAtLevel, value) => {
if (!value) return;
const currentState = this.state.rooms.map((r) => r.room.roomId);
if (currentState.length === value.length) {
let changed = false;
for (let i = 0; i < currentState.length; i++) {
if (currentState[i] !== value[i]) {
changed = true;
break;
}
}
if (!changed) return;
}
this._loadRoomIds(value);
};
onRoomMembershipChanged = () => {
if (!this.state.enabled && this._shouldEnable()) {
this.setState({enabled: true});
}
};
onRoom = (room) => {
// Always check for membership changes when we see new rooms
this.onRoomMembershipChanged();
const waitingRoom = this._waitingRoomQueue.find(r => r.roomId === room.roomId);
if (!waitingRoom) return;
this._waitingRoomQueue.splice(this._waitingRoomQueue.indexOf(waitingRoom), 1);
const now = (new Date()).getTime();
if ((now - waitingRoom.addedTs) > AUTOJOIN_WAIT_THRESHOLD_MS) return; // Too long ago.
this._appendRoomId(room.roomId); // add the room we've been waiting for
};
_shouldEnable() {
const client = MatrixClientPeg.get();
const joinedRoomCount = client.getRooms().reduce((count, r) => {
return count + (r.getMyMembership() === "join" ? 1 : 0);
}, 0);
return joinedRoomCount >= MIN_ROOMS_BEFORE_ENABLED;
}
_loadRoomIds(roomIds) {
if (!roomIds || roomIds.length <= 0) return; // Skip updates with no rooms
// If we're here, the list changed.
const rooms = roomIds.map((r) => MatrixClientPeg.get().getRoom(r)).filter((r) => r).map((r) => {
const badges = this._calculateBadgesForRoom(r) || {};
return {
room: r,
animated: false,
...badges,
};
});
this.setState({
rooms: rooms,
});
}
_calculateBadgesForRoom(room, zero=false) {
if (!room) return null;
// Reset the notification variables for simplicity
const roomModel = {
redBadge: false,
formattedCount: "0",
showCount: false,
};
if (zero) return roomModel;
const notifState = RoomNotifs.getRoomNotifsState(room.roomId);
if (RoomNotifs.MENTION_BADGE_STATES.includes(notifState)) {
const highlightNotifs = RoomNotifs.getUnreadNotificationCount(room, 'highlight');
const unreadNotifs = RoomNotifs.getUnreadNotificationCount(room);
const redBadge = highlightNotifs > 0;
const greyBadge = redBadge || (unreadNotifs > 0 && RoomNotifs.BADGE_STATES.includes(notifState));
if (redBadge || greyBadge) {
const notifCount = redBadge ? highlightNotifs : unreadNotifs;
const limitedCount = FormattingUtils.formatCount(notifCount);
roomModel.redBadge = redBadge;
roomModel.formattedCount = limitedCount;
roomModel.showCount = true;
}
}
return roomModel;
}
_calculateRoomBadges(room, zero=false) {
if (!room) return;
const rooms = this.state.rooms.slice();
const roomModel = rooms.find((r) => r.room.roomId === room.roomId);
if (!roomModel) return; // No applicable room, so don't do math on it
const badges = this._calculateBadgesForRoom(room, zero);
if (!badges) return; // No badges for some reason
Object.assign(roomModel, badges);
this.setState({rooms});
}
_appendRoomId(roomId) {
let room = MatrixClientPeg.get().getRoom(roomId);
if (!room) return;
const rooms = this.state.rooms.slice();
// If the room is upgraded, use that room instead. We'll also splice out
// any children of the room.
const history = MatrixClientPeg.get().getRoomUpgradeHistory(roomId);
if (history.length > 1) {
room = history[history.length - 1]; // Last room is most recent
// Take out any room that isn't the most recent room
for (let i = 0; i < history.length - 1; i++) {
const idx = rooms.findIndex((r) => r.room.roomId === history[i].roomId);
if (idx !== -1) rooms.splice(idx, 1);
}
}
const existingIdx = rooms.findIndex((r) => r.room.roomId === room.roomId);
if (existingIdx !== -1) {
rooms.splice(existingIdx, 1);
}
rooms.splice(0, 0, {room, animated: false});
if (rooms.length > MAX_ROOMS) {
rooms.splice(MAX_ROOMS, rooms.length - MAX_ROOMS);
}
this.setState({rooms});
if (this._scroller.current) {
this._scroller.current.moveToOrigin();
}
// We don't track room aesthetics (badges, membership, etc) over the wire so we
// don't need to do this elsewhere in the file. Just where we alter the room IDs
// and their order.
const roomIds = rooms.map((r) => r.room.roomId);
if (roomIds.length > 0) {
SettingsStore.setValue("breadcrumb_rooms", null, SettingLevel.ACCOUNT, roomIds);
}
}
_viewRoom(room, index) {
Analytics.trackEvent("Breadcrumbs", "click_node", index);
dis.dispatch({action: "view_room", room_id: room.roomId});
}
_onMouseEnter(room) {
this._onHover(room);
}
_onMouseLeave(room) {
this._onHover(null); // clear hover states
}
_onHover(room) {
const rooms = this.state.rooms.slice();
for (const r of rooms) {
r.hover = room && r.room.roomId === room.roomId;
}
this.setState({rooms});
}
_isDmRoom(room) {
const dmRooms = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
return Boolean(dmRooms);
}
render() {
const Tooltip = sdk.getComponent('elements.Tooltip');
const IndicatorScrollbar = sdk.getComponent('structures.IndicatorScrollbar');
// check for collapsed here and not at parent so we keep rooms in our state
// when collapsing and expanding
if (this.props.collapsed || !this.state.enabled) {
return null;
}
const rooms = this.state.rooms;
const avatars = rooms.map((r, i) => {
const isFirst = i === 0;
const classes = classNames({
"mx_RoomBreadcrumbs_crumb": true,
"mx_RoomBreadcrumbs_preAnimate": isFirst && !r.animated,
"mx_RoomBreadcrumbs_animate": isFirst,
"mx_RoomBreadcrumbs_left": r.left,
});
let tooltip = null;
if (r.hover) {
tooltip = <Tooltip label={r.room.name} />;
}
let badge;
if (r.showCount) {
const badgeClasses = classNames({
'mx_RoomTile_badge': true,
'mx_RoomTile_badgeButton': true,
'mx_RoomTile_badgeRed': r.redBadge,
'mx_RoomTile_badgeUnread': !r.redBadge,
});
badge = <div className={badgeClasses}>{r.formattedCount}</div>;
}
return (
<AccessibleButton
className={classes}
key={r.room.roomId}
onClick={() => this._viewRoom(r.room, i)}
onMouseEnter={() => this._onMouseEnter(r.room)}
onMouseLeave={() => this._onMouseLeave(r.room)}
aria-label={_t("Room %(name)s", {name: r.room.name})}
>
<RoomAvatar room={r.room} width={32} height={32} />
{badge}
{tooltip}
</AccessibleButton>
);
});
return (
<div role="toolbar" aria-label={_t("Recent rooms")}>
<IndicatorScrollbar
ref={this._scroller}
className="mx_RoomBreadcrumbs"
trackHorizontalOverflow={true}
verticalScrollsHorizontally={true}
>
{ avatars }
</IndicatorScrollbar>
</div>
);
}
}

View file

@ -23,11 +23,10 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
import Analytics from "../../../Analytics";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { CSSTransition } from "react-transition-group";
import RoomListStore from "../../../stores/room-list/RoomListStore2";
import RoomListStore from "../../../stores/room-list/RoomListStore";
import { DefaultTagID } from "../../../stores/room-list/models";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex";
import Toolbar from "../../../accessibility/Toolbar";
interface IProps {
}
@ -43,7 +42,7 @@ interface IState {
skipFirst: boolean;
}
export default class RoomBreadcrumbs2 extends React.PureComponent<IProps, IState> {
export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState> {
private isMounted = true;
constructor(props: IProps) {
@ -86,13 +85,13 @@ export default class RoomBreadcrumbs2 extends React.PureComponent<IProps, IState
const roomTags = RoomListStore.instance.getTagsForRoom(r);
const roomTag = roomTags.includes(DefaultTagID.DM) ? DefaultTagID.DM : roomTags[0];
return (
<AccessibleTooltipButton
className="mx_RoomBreadcrumbs2_crumb"
<RovingAccessibleTooltipButton
className="mx_RoomBreadcrumbs_crumb"
key={r.roomId}
onClick={() => this.viewRoom(r, i)}
aria-label={_t("Room %(name)s", {name: r.name})}
title={r.name}
tooltipClassName={"mx_RoomBreadcrumbs2_Tooltip"}
tooltipClassName="mx_RoomBreadcrumbs_Tooltip"
>
<DecoratedRoomAvatar
room={r}
@ -101,7 +100,7 @@ export default class RoomBreadcrumbs2 extends React.PureComponent<IProps, IState
displayBadge={true}
forceCount={true}
/>
</AccessibleTooltipButton>
</RovingAccessibleTooltipButton>
);
});
@ -110,17 +109,17 @@ export default class RoomBreadcrumbs2 extends React.PureComponent<IProps, IState
return (
<CSSTransition
appear={true} in={this.state.doAnimation} timeout={640}
classNames='mx_RoomBreadcrumbs2'
classNames='mx_RoomBreadcrumbs'
>
<div className='mx_RoomBreadcrumbs2'>
<Toolbar className='mx_RoomBreadcrumbs'>
{tiles.slice(this.state.skipFirst ? 1 : 0)}
</div>
</Toolbar>
</CSSTransition>
);
} else {
return (
<div className='mx_RoomBreadcrumbs2'>
<div className="mx_RoomBreadcrumbs2_placeholder">
<div className='mx_RoomBreadcrumbs'>
<div className="mx_RoomBreadcrumbs_placeholder">
{_t("No recently visited rooms")}
</div>
</div>

View file

@ -1,35 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket 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 createReactClass from 'create-react-class';
export default createReactClass({
displayName: 'RoomDropTarget',
render: function() {
return (
<div className="mx_RoomDropTarget_container">
<div className="mx_RoomDropTarget">
<div className="mx_RoomDropTarget_label">
{ this.props.label }
</div>
</div>
</div>
);
},
});

View file

@ -26,14 +26,14 @@ import Modal from "../../../Modal";
import RateLimitedFunc from '../../../ratelimitedfunc';
import { linkifyElement } from '../../../HtmlUtils';
import AccessibleButton from '../elements/AccessibleButton';
import ManageIntegsButton from '../elements/ManageIntegsButton';
import {CancelButton} from './SimpleRoomHeader';
import SettingsStore from "../../../settings/SettingsStore";
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
import DMRoomMap from '../../../utils/DMRoomMap';
import E2EIcon from './E2EIcon';
import InviteOnlyIcon from './InviteOnlyIcon';
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import {DefaultTagID} from "../../../stores/room-list/models";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
export default createReactClass({
displayName: 'RoomHeader',
@ -152,26 +152,11 @@ export default createReactClass({
},
render: function() {
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
let searchStatus = null;
let cancelButton = null;
let settingsButton = null;
let pinnedEventsButton = null;
const e2eIcon = this.props.e2eStatus ?
<E2EIcon status={this.props.e2eStatus} /> :
undefined;
const dmUserId = DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId);
const joinRules = this.props.room && this.props.room.currentState.getStateEvents("m.room.join_rules", "");
const joinRule = joinRules && joinRules.getContent().join_rule;
let privateIcon;
// Don't show an invite-only icon for DMs. Users know they're invite-only.
if (!dmUserId && joinRule === "invite") {
privateIcon = <InviteOnlyIcon />;
}
if (this.props.onCancelClick) {
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
}
@ -221,24 +206,24 @@ export default createReactClass({
}
const topicElement =
<div className="mx_RoomHeader_topic" ref={this._topic} title={topic} dir="auto">{ topic }</div>;
const avatarSize = 28;
let roomAvatar;
if (this.props.room) {
roomAvatar = (<RoomAvatar
roomAvatar = <DecoratedRoomAvatar
room={this.props.room}
width={avatarSize}
height={avatarSize}
avatarSize={32}
tag={DefaultTagID.Untagged} // to apply room publicity badging
oobData={this.props.oobData}
viewAvatarOnClick={true} />);
viewAvatarOnClick={true}
/>;
}
if (this.props.onSettingsClick) {
settingsButton =
<AccessibleButton className="mx_RoomHeader_button mx_RoomHeader_settingsButton"
<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_settingsButton"
onClick={this.props.onSettingsClick}
title={_t("Settings")}
>
</AccessibleButton>;
title={_t("Settings")} />;
}
if (this.props.onPinnedClick && SettingsStore.isFeatureEnabled('feature_pinning')) {
@ -250,55 +235,45 @@ export default createReactClass({
}
pinnedEventsButton =
<AccessibleButton className="mx_RoomHeader_button mx_RoomHeader_pinnedButton"
onClick={this.props.onPinnedClick} title={_t("Pinned Messages")}>
<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_pinnedButton"
onClick={this.props.onPinnedClick}
title={_t("Pinned Messages")}
>
{ pinsIndicator }
</AccessibleButton>;
</AccessibleTooltipButton>;
}
// var leave_button;
// if (this.props.onLeaveClick) {
// leave_button =
// <div className="mx_RoomHeader_button" onClick={this.props.onLeaveClick} title="Leave room">
// <TintableSvg src={require("../../../../res/img/leave.svg")} width="26" height="20"/>
// </div>;
// }
let forgetButton;
if (this.props.onForgetClick) {
forgetButton =
<AccessibleButton className="mx_RoomHeader_button mx_RoomHeader_forgetButton"
<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_forgetButton"
onClick={this.props.onForgetClick}
title={_t("Forget room")}
>
</AccessibleButton>;
title={_t("Forget room")} />;
}
let searchButton;
if (this.props.onSearchClick && this.props.inRoom) {
searchButton =
<AccessibleButton className="mx_RoomHeader_button mx_RoomHeader_searchButton"
<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_searchButton"
onClick={this.props.onSearchClick}
title={_t("Search")}
>
</AccessibleButton>;
title={_t("Search")} />;
}
let shareRoomButton;
if (this.props.inRoom) {
shareRoomButton =
<AccessibleButton className="mx_RoomHeader_button mx_RoomHeader_shareButton"
<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_shareButton"
onClick={this.onShareRoomClick}
title={_t('Share room')}
>
</AccessibleButton>;
title={_t('Share room')} />;
}
let manageIntegsButton;
if (this.props.room && this.props.room.roomId && this.props.inRoom) {
manageIntegsButton = <ManageIntegsButton
room={this.props.room}
/>;
manageIntegsButton = <ManageIntegsButton room={this.props.room} />;
}
const rightRow =
@ -311,11 +286,13 @@ export default createReactClass({
{ searchButton }
</div>;
const e2eIcon = this.props.e2eStatus ? <E2EIcon status={this.props.e2eStatus} /> : undefined;
return (
<div className="mx_RoomHeader light-panel">
<div className="mx_RoomHeader_wrapper" aria-owns="mx_RightPanel">
<div className="mx_RoomHeader_avatar">{ roomAvatar }{ e2eIcon }</div>
{ privateIcon }
<div className="mx_RoomHeader_avatar">{ roomAvatar }</div>
<div className="mx_RoomHeader_e2eIcon">{ e2eIcon }</div>
{ name }
{ topicElement }
{ cancelButton }

View file

@ -1,853 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017, 2018 Vector Creations Ltd
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 SettingsStore from "../../../settings/SettingsStore";
import Timer from "../../../utils/Timer";
import React from "react";
import ReactDOM from "react-dom";
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import * as utils from "matrix-js-sdk/src/utils";
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import rate_limited_func from "../../../ratelimitedfunc";
import * as Rooms from '../../../Rooms';
import DMRoomMap from '../../../utils/DMRoomMap';
import TagOrderStore from '../../../stores/TagOrderStore';
import CustomRoomTagStore from '../../../stores/CustomRoomTagStore';
import GroupStore from '../../../stores/GroupStore';
import RoomSubList from '../../structures/RoomSubList';
import ResizeHandle from '../elements/ResizeHandle';
import CallHandler from "../../../CallHandler";
import dis from "../../../dispatcher/dispatcher";
import * as sdk from "../../../index";
import * as Receipt from "../../../utils/Receipt";
import {Resizer} from '../../../resizer';
import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2';
import {RovingTabIndexProvider} from "../../../accessibility/RovingTabIndex";
import {RoomListStoreTempProxy} from "../../../stores/room-list/RoomListStoreTempProxy";
import {DefaultTagID} from "../../../stores/room-list/models";
import * as Unread from "../../../Unread";
import RoomViewStore from "../../../stores/RoomViewStore";
import {TAG_DM} from "../../../stores/RoomListStore";
const HIDE_CONFERENCE_CHANS = true;
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
const HOVER_MOVE_TIMEOUT = 1000;
function labelForTagName(tagName) {
if (tagName.startsWith('u.')) return tagName.slice(2);
return tagName;
}
export default createReactClass({
displayName: 'RoomList',
propTypes: {
ConferenceHandler: PropTypes.any,
collapsed: PropTypes.bool.isRequired,
searchFilter: PropTypes.string,
},
getInitialState: function() {
this._hoverClearTimer = null;
this._subListRefs = {
// key => RoomSubList ref
};
const sizesJson = window.localStorage.getItem("mx_roomlist_sizes");
const collapsedJson = window.localStorage.getItem("mx_roomlist_collapsed");
this.subListSizes = sizesJson ? JSON.parse(sizesJson) : {};
this.collapsedState = collapsedJson ? JSON.parse(collapsedJson) : {};
this._layoutSections = [];
const unfilteredOptions = {
allowWhitespace: false,
handleHeight: 1,
};
this._unfilteredlayout = new Layout((key, size) => {
const subList = this._subListRefs[key];
if (subList) {
subList.setHeight(size);
}
// update overflow indicators
this._checkSubListsOverflow();
// don't store height for collapsed sublists
if (!this.collapsedState[key]) {
this.subListSizes[key] = size;
window.localStorage.setItem("mx_roomlist_sizes",
JSON.stringify(this.subListSizes));
}
}, this.subListSizes, this.collapsedState, unfilteredOptions);
this._filteredLayout = new Layout((key, size) => {
const subList = this._subListRefs[key];
if (subList) {
subList.setHeight(size);
}
}, null, null, {
allowWhitespace: false,
handleHeight: 0,
});
this._layout = this._unfilteredlayout;
return {
isLoadingLeftRooms: false,
totalRoomCount: null,
lists: {},
incomingCallTag: null,
incomingCall: null,
selectedTags: [],
hover: false,
customTags: CustomRoomTagStore.getTags(),
};
},
// TODO: [REACT-WARNING] Replace component with real class, put this in the constructor.
UNSAFE_componentWillMount: function() {
this.mounted = false;
const cli = MatrixClientPeg.get();
cli.on("Room", this.onRoom);
cli.on("deleteRoom", this.onDeleteRoom);
cli.on("Room.receipt", this.onRoomReceipt);
cli.on("RoomMember.name", this.onRoomMemberName);
cli.on("Event.decrypted", this.onEventDecrypted);
cli.on("accountData", this.onAccountData);
cli.on("Group.myMembership", this._onGroupMyMembership);
cli.on("RoomState.events", this.onRoomStateEvents);
const dmRoomMap = DMRoomMap.shared();
// A map between tags which are group IDs and the room IDs of rooms that should be kept
// in the room list when filtering by that tag.
this._visibleRoomsForGroup = {
// $groupId: [$roomId1, $roomId2, ...],
};
// All rooms that should be kept in the room list when filtering.
// By default, show all rooms.
this._visibleRooms = MatrixClientPeg.get().getVisibleRooms();
// Listen to updates to group data. RoomList cares about members and rooms in order
// to filter the room list when group tags are selected.
this._groupStoreToken = GroupStore.registerListener(null, () => {
(TagOrderStore.getOrderedTags() || []).forEach((tag) => {
if (tag[0] !== '+') {
return;
}
// This group's rooms or members may have updated, update rooms for its tag
this.updateVisibleRoomsForTag(dmRoomMap, tag);
this.updateVisibleRooms();
});
});
this._tagStoreToken = TagOrderStore.addListener(() => {
// Filters themselves have changed
this.updateVisibleRooms();
});
this._roomListStoreToken = RoomListStoreTempProxy.addListener(() => {
this._delayedRefreshRoomList();
});
if (SettingsStore.isFeatureEnabled("feature_custom_tags")) {
this._customTagStoreToken = CustomRoomTagStore.addListener(() => {
this.setState({
customTags: CustomRoomTagStore.getTags(),
});
});
}
this.refreshRoomList();
// order of the sublists
//this.listOrder = [];
// loop count to stop a stack overflow if the user keeps waggling the
// mouse for >30s in a row, or if running under mocha
this._delayedRefreshRoomListLoopCount = 0;
},
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
const cfg = {
getLayout: () => this._layout,
};
this.resizer = new Resizer(this.resizeContainer, Distributor, cfg);
this.resizer.setClassNames({
handle: "mx_ResizeHandle",
vertical: "mx_ResizeHandle_vertical",
reverse: "mx_ResizeHandle_reverse",
});
this._layout.update(
this._layoutSections,
this.resizeContainer && this.resizeContainer.offsetHeight,
);
this._checkSubListsOverflow();
this.resizer.attach();
if (this.props.resizeNotifier) {
this.props.resizeNotifier.on("leftPanelResized", this.onResize);
}
this.mounted = true;
},
componentDidUpdate: function(prevProps) {
let forceLayoutUpdate = false;
this._repositionIncomingCallBox(undefined, false);
if (!this.props.searchFilter && prevProps.searchFilter) {
this._layout = this._unfilteredlayout;
forceLayoutUpdate = true;
} else if (this.props.searchFilter && !prevProps.searchFilter) {
this._layout = this._filteredLayout;
forceLayoutUpdate = true;
}
this._layout.update(
this._layoutSections,
this.resizeContainer && this.resizeContainer.clientHeight,
forceLayoutUpdate,
);
this._checkSubListsOverflow();
},
onAction: function(payload) {
switch (payload.action) {
case 'view_tooltip':
this.tooltip = payload.tooltip;
break;
case 'call_state':
var call = CallHandler.getCall(payload.room_id);
if (call && call.call_state === 'ringing') {
this.setState({
incomingCall: call,
incomingCallTag: this.getTagNameForRoomId(payload.room_id),
});
this._repositionIncomingCallBox(undefined, true);
} else {
this.setState({
incomingCall: null,
incomingCallTag: null,
});
}
break;
case 'view_room_delta': {
const currentRoomId = RoomViewStore.getRoomId();
const {
"im.vector.fake.invite": inviteRooms,
"m.favourite": favouriteRooms,
[TAG_DM]: dmRooms,
"im.vector.fake.recent": recentRooms,
"m.lowpriority": lowPriorityRooms,
"im.vector.fake.archived": historicalRooms,
"m.server_notice": serverNoticeRooms,
...tags
} = this.state.lists;
const shownCustomTagRooms = Object.keys(tags).filter(tagName => {
return (!this.state.customTags || this.state.customTags[tagName]) &&
!tagName.match(STANDARD_TAGS_REGEX);
}).map(tagName => tags[tagName]);
// this order matches the one when generating the room sublists below.
let rooms = this._applySearchFilter([
...inviteRooms,
...favouriteRooms,
...dmRooms,
...recentRooms,
...[].concat.apply([], shownCustomTagRooms), // eslint-disable-line prefer-spread
...lowPriorityRooms,
...historicalRooms,
...serverNoticeRooms,
], this.props.searchFilter);
if (payload.unread) {
// filter to only notification rooms (and our current active room so we can index properly)
rooms = rooms.filter(room => {
return room.roomId === currentRoomId || Unread.doesRoomHaveUnreadMessages(room);
});
}
const currentIndex = rooms.findIndex(room => room.roomId === currentRoomId);
// use slice to account for looping around the start
const [room] = rooms.slice((currentIndex + payload.delta) % rooms.length);
if (room) {
dis.dispatch({
action: 'view_room',
room_id: room.roomId,
show_room_tile: true, // to make sure the room gets scrolled into view
});
}
break;
}
}
},
componentWillUnmount: function() {
this.mounted = false;
dis.unregister(this.dispatcherRef);
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Room", this.onRoom);
MatrixClientPeg.get().removeListener("deleteRoom", this.onDeleteRoom);
MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt);
MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName);
MatrixClientPeg.get().removeListener("Event.decrypted", this.onEventDecrypted);
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership);
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
}
if (this.props.resizeNotifier) {
this.props.resizeNotifier.removeListener("leftPanelResized", this.onResize);
}
if (this._tagStoreToken) {
this._tagStoreToken.remove();
}
if (this._roomListStoreToken) {
this._roomListStoreToken.remove();
}
if (this._customTagStoreToken) {
this._customTagStoreToken.remove();
}
// NB: GroupStore is not a Flux.Store
if (this._groupStoreToken) {
this._groupStoreToken.unregister();
}
// cancel any pending calls to the rate_limited_funcs
this._delayedRefreshRoomList.cancelPendingCall();
},
onResize: function() {
if (this.mounted && this._layout && this.resizeContainer &&
Array.isArray(this._layoutSections)
) {
this._layout.update(
this._layoutSections,
this.resizeContainer.offsetHeight,
);
}
},
onRoom: function(room) {
this.updateVisibleRooms();
},
onRoomStateEvents: function(ev, state) {
if (ev.getType() === "m.room.create" || ev.getType() === "m.room.tombstone") {
this.updateVisibleRooms();
}
},
onDeleteRoom: function(roomId) {
this.updateVisibleRooms();
},
onArchivedHeaderClick: function(isHidden, scrollToPosition) {
if (!isHidden) {
const self = this;
this.setState({ isLoadingLeftRooms: true });
// we don't care about the response since it comes down via "Room"
// events.
MatrixClientPeg.get().syncLeftRooms().catch(function(err) {
console.error("Failed to sync left rooms: %s", err);
console.error(err);
}).finally(function() {
self.setState({ isLoadingLeftRooms: false });
});
}
},
onRoomReceipt: function(receiptEvent, room) {
// because if we read a notification, it will affect notification count
// only bother updating if there's a receipt from us
if (Receipt.findReadReceiptFromUserId(receiptEvent, MatrixClientPeg.get().credentials.userId)) {
this._delayedRefreshRoomList();
}
},
onRoomMemberName: function(ev, member) {
this._delayedRefreshRoomList();
},
onEventDecrypted: function(ev) {
// An event being decrypted may mean we need to re-order the room list
this._delayedRefreshRoomList();
},
onAccountData: function(ev) {
if (ev.getType() == 'm.direct') {
this._delayedRefreshRoomList();
}
},
_onGroupMyMembership: function(group) {
this.forceUpdate();
},
onMouseMove: async function(ev) {
if (!this._hoverClearTimer) {
this.setState({hover: true});
this._hoverClearTimer = new Timer(HOVER_MOVE_TIMEOUT);
this._hoverClearTimer.start();
let finished = true;
try {
await this._hoverClearTimer.finished();
} catch (err) {
finished = false;
}
this._hoverClearTimer = null;
if (finished) {
this.setState({hover: false});
this._delayedRefreshRoomList();
}
} else {
this._hoverClearTimer.restart();
}
},
onMouseLeave: function(ev) {
if (this._hoverClearTimer) {
this._hoverClearTimer.abort();
this._hoverClearTimer = null;
}
this.setState({hover: false});
// Refresh the room list just in case the user missed something.
this._delayedRefreshRoomList();
},
_delayedRefreshRoomList: rate_limited_func(function() {
this.refreshRoomList();
}, 500),
// Update which rooms and users should appear in RoomList for a given group tag
updateVisibleRoomsForTag: function(dmRoomMap, tag) {
if (!this.mounted) return;
// For now, only handle group tags
if (tag[0] !== '+') return;
this._visibleRoomsForGroup[tag] = [];
GroupStore.getGroupRooms(tag).forEach((room) => this._visibleRoomsForGroup[tag].push(room.roomId));
GroupStore.getGroupMembers(tag).forEach((member) => {
if (member.userId === MatrixClientPeg.get().credentials.userId) return;
dmRoomMap.getDMRoomsForUserId(member.userId).forEach(
(roomId) => this._visibleRoomsForGroup[tag].push(roomId),
);
});
// TODO: Check if room has been tagged to the group by the user
},
// Update which rooms and users should appear according to which tags are selected
updateVisibleRooms: function() {
const selectedTags = TagOrderStore.getSelectedTags();
const visibleGroupRooms = [];
selectedTags.forEach((tag) => {
(this._visibleRoomsForGroup[tag] || []).forEach(
(roomId) => visibleGroupRooms.push(roomId),
);
});
// If there are any tags selected, constrain the rooms listed to the
// visible rooms as determined by visibleGroupRooms. Here, we
// de-duplicate and filter out rooms that the client doesn't know
// about (hence the Set and the null-guard on `room`).
if (selectedTags.length > 0) {
const roomSet = new Set();
visibleGroupRooms.forEach((roomId) => {
const room = MatrixClientPeg.get().getRoom(roomId);
if (room) {
roomSet.add(room);
}
});
this._visibleRooms = Array.from(roomSet);
} else {
// Show all rooms
this._visibleRooms = MatrixClientPeg.get().getVisibleRooms();
}
this._delayedRefreshRoomList();
},
refreshRoomList: function() {
if (this.state.hover) {
// Don't re-sort the list if we're hovering over the list
return;
}
// TODO: ideally we'd calculate this once at start, and then maintain
// any changes to it incrementally, updating the appropriate sublists
// as needed.
// Alternatively we'd do something magical with Immutable.js or similar.
const lists = this.getRoomLists();
let totalRooms = 0;
for (const l of Object.values(lists)) {
totalRooms += l.length;
}
this.setState({
lists,
totalRoomCount: totalRooms,
// Do this here so as to not render every time the selected tags
// themselves change.
selectedTags: TagOrderStore.getSelectedTags(),
}, () => {
// we don't need to restore any size here, do we?
// i guess we could have triggered a new group to appear
// that already an explicit size the last time it appeared ...
this._checkSubListsOverflow();
});
// this._lastRefreshRoomListTs = Date.now();
},
getTagNameForRoomId: function(roomId) {
const lists = RoomListStoreTempProxy.getRoomLists();
for (const tagName of Object.keys(lists)) {
for (const room of lists[tagName]) {
// Should be impossible, but guard anyways.
if (!room) {
continue;
}
const myUserId = MatrixClientPeg.get().getUserId();
if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, myUserId, this.props.ConferenceHandler)) {
continue;
}
if (room.roomId === roomId) return tagName;
}
}
return null;
},
getRoomLists: function() {
const lists = RoomListStoreTempProxy.getRoomLists();
const filteredLists = {};
const isRoomVisible = {
// $roomId: true,
};
this._visibleRooms.forEach((r) => {
isRoomVisible[r.roomId] = true;
});
Object.keys(lists).forEach((tagName) => {
const filteredRooms = lists[tagName].filter((taggedRoom) => {
// Somewhat impossible, but guard against it anyway
if (!taggedRoom) {
return;
}
const myUserId = MatrixClientPeg.get().getUserId();
if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(taggedRoom, myUserId, this.props.ConferenceHandler)) {
return;
}
return Boolean(isRoomVisible[taggedRoom.roomId]);
});
if (filteredRooms.length > 0 || tagName.match(STANDARD_TAGS_REGEX)) {
filteredLists[tagName] = filteredRooms;
}
});
return filteredLists;
},
_getScrollNode: function() {
if (!this.mounted) return null;
const panel = ReactDOM.findDOMNode(this);
if (!panel) return null;
if (panel.classList.contains('gm-prevented')) {
return panel;
} else {
return panel.children[2]; // XXX: Fragile!
}
},
_whenScrolling: function(e) {
this._hideTooltip(e);
this._repositionIncomingCallBox(e, false);
},
_hideTooltip: function(e) {
// Hide tooltip when scrolling, as we'll no longer be over the one we were on
if (this.tooltip && this.tooltip.style.display !== "none") {
this.tooltip.style.display = "none";
}
},
_repositionIncomingCallBox: function(e, firstTime) {
const incomingCallBox = document.getElementById("incomingCallBox");
if (incomingCallBox && incomingCallBox.parentElement) {
const scrollArea = this._getScrollNode();
if (!scrollArea) return;
// Use the offset of the top of the scroll area from the window
// as this is used to calculate the CSS fixed top position for the stickies
const scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset;
// Use the offset of the top of the component from the window
// as this is used to calculate the CSS fixed top position for the stickies
const scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height;
let top = (incomingCallBox.parentElement.getBoundingClientRect().top + window.pageYOffset);
// Make sure we don't go too far up, if the headers aren't sticky
top = (top < scrollAreaOffset) ? scrollAreaOffset : top;
// make sure we don't go too far down, if the headers aren't sticky
const bottomMargin = scrollAreaOffset + (scrollAreaHeight - 45);
top = (top > bottomMargin) ? bottomMargin : top;
incomingCallBox.style.top = top + "px";
incomingCallBox.style.left = scrollArea.offsetLeft + scrollArea.offsetWidth + 12 + "px";
}
},
_makeGroupInviteTiles(filter) {
const ret = [];
const lcFilter = filter && filter.toLowerCase();
const GroupInviteTile = sdk.getComponent('groups.GroupInviteTile');
for (const group of MatrixClientPeg.get().getGroups()) {
const {groupId, name, myMembership} = group;
// filter to only groups in invite state and group_id starts with filter or group name includes it
if (myMembership !== 'invite') continue;
if (lcFilter && !groupId.toLowerCase().startsWith(lcFilter) &&
!(name && name.toLowerCase().includes(lcFilter))) continue;
ret.push(<GroupInviteTile key={groupId} group={group} collapsed={this.props.collapsed} />);
}
return ret;
},
_applySearchFilter: function(list, filter) {
if (filter === "") return list;
const lcFilter = filter.toLowerCase();
// apply toLowerCase before and after removeHiddenChars because different rules get applied
// e.g M -> M but m -> n, yet some unicode homoglyphs come out as uppercase, e.g 𝚮 -> H
const fuzzyFilter = utils.removeHiddenChars(lcFilter).toLowerCase();
// case insensitive if room name includes filter,
// or if starts with `#` and one of room's aliases starts with filter
return list.filter((room) => {
if (filter[0] === "#") {
if (room.getCanonicalAlias() && room.getCanonicalAlias().toLowerCase().startsWith(lcFilter)) {
return true;
}
if (room.getAltAliases().some((alias) => alias.toLowerCase().startsWith(lcFilter))) {
return true;
}
}
return room.name && utils.removeHiddenChars(room.name.toLowerCase()).toLowerCase().includes(fuzzyFilter);
});
},
_handleCollapsedState: function(key, collapsed) {
// persist collapsed state
this.collapsedState[key] = collapsed;
window.localStorage.setItem("mx_roomlist_collapsed", JSON.stringify(this.collapsedState));
// load the persisted size configuration of the expanded sub list
if (collapsed) {
this._layout.collapseSection(key);
} else {
this._layout.expandSection(key, this.subListSizes[key]);
}
// check overflow, as sub lists sizes have changed
// important this happens after calling resize above
this._checkSubListsOverflow();
},
// check overflow for scroll indicator gradient
_checkSubListsOverflow() {
Object.values(this._subListRefs).forEach(l => l.checkOverflow());
},
_subListRef: function(key, ref) {
if (!ref) {
delete this._subListRefs[key];
} else {
this._subListRefs[key] = ref;
}
},
_mapSubListProps: function(subListsProps) {
this._layoutSections = [];
const defaultProps = {
collapsed: this.props.collapsed,
isFiltered: !!this.props.searchFilter,
};
subListsProps.forEach((p) => {
p.list = this._applySearchFilter(p.list, this.props.searchFilter);
});
subListsProps = subListsProps.filter((props => {
const len = props.list.length + (props.extraTiles ? props.extraTiles.length : 0);
return len !== 0 || props.onAddRoom;
}));
return subListsProps.reduce((components, props, i) => {
props = {...defaultProps, ...props};
const isLast = i === subListsProps.length - 1;
const len = props.list.length + (props.extraTiles ? props.extraTiles.length : 0);
const {key, label, onHeaderClick, ...otherProps} = props;
const chosenKey = key || label;
const onSubListHeaderClick = (collapsed) => {
this._handleCollapsedState(chosenKey, collapsed);
if (onHeaderClick) {
onHeaderClick(collapsed);
}
};
const startAsHidden = props.startAsHidden || this.collapsedState[chosenKey];
this._layoutSections.push({
id: chosenKey,
count: len,
});
const subList = (<RoomSubList
ref={this._subListRef.bind(this, chosenKey)}
startAsHidden={startAsHidden}
forceExpand={!!this.props.searchFilter}
onHeaderClick={onSubListHeaderClick}
key={chosenKey}
label={label}
{...otherProps} />);
if (!isLast) {
return components.concat(
subList,
<ResizeHandle key={chosenKey+"-resizer"} vertical={true} id={chosenKey} />
);
} else {
return components.concat(subList);
}
}, []);
},
_collectResizeContainer: function(el) {
this.resizeContainer = el;
},
render: function() {
const incomingCallIfTaggedAs = (tagName) => {
if (!this.state.incomingCall) return null;
if (this.state.incomingCallTag !== tagName) return null;
return this.state.incomingCall;
};
let subLists = [
{
list: [],
extraTiles: this._makeGroupInviteTiles(this.props.searchFilter),
label: _t('Community Invites'),
isInvite: true,
},
{
list: this.state.lists['im.vector.fake.invite'],
label: _t('Invites'),
incomingCall: incomingCallIfTaggedAs('im.vector.fake.invite'),
isInvite: true,
},
{
list: this.state.lists['m.favourite'],
label: _t('Favourites'),
tagName: "m.favourite",
incomingCall: incomingCallIfTaggedAs('m.favourite'),
},
{
list: this.state.lists[DefaultTagID.DM],
label: _t('Direct Messages'),
tagName: DefaultTagID.DM,
incomingCall: incomingCallIfTaggedAs(DefaultTagID.DM),
onAddRoom: () => {dis.dispatch({action: 'view_create_chat'});},
addRoomLabel: _t("Start chat"),
},
{
list: this.state.lists['im.vector.fake.recent'],
label: _t('Rooms'),
incomingCall: incomingCallIfTaggedAs('im.vector.fake.recent'),
onAddRoom: () => {dis.dispatch({action: 'view_create_room'});},
addRoomLabel: _t("Create room"),
},
];
const tagSubLists = Object.keys(this.state.lists)
.filter((tagName) => {
return (!this.state.customTags || this.state.customTags[tagName]) &&
!tagName.match(STANDARD_TAGS_REGEX);
}).map((tagName) => {
return {
list: this.state.lists[tagName],
key: tagName,
label: labelForTagName(tagName),
tagName: tagName,
incomingCall: incomingCallIfTaggedAs(tagName),
};
});
subLists = subLists.concat(tagSubLists);
subLists = subLists.concat([
{
list: this.state.lists['m.lowpriority'],
label: _t('Low priority'),
tagName: "m.lowpriority",
incomingCall: incomingCallIfTaggedAs('m.lowpriority'),
},
{
list: this.state.lists['im.vector.fake.archived'],
label: _t('Historical'),
incomingCall: incomingCallIfTaggedAs('im.vector.fake.archived'),
startAsHidden: true,
showSpinner: this.state.isLoadingLeftRooms,
onHeaderClick: this.onArchivedHeaderClick,
},
{
list: this.state.lists['m.server_notice'],
label: _t('System Alerts'),
tagName: "m.lowpriority",
incomingCall: incomingCallIfTaggedAs('m.server_notice'),
},
]);
const subListComponents = this._mapSubListProps(subLists);
const {resizeNotifier, collapsed, searchFilter, ConferenceHandler, onKeyDown, ...props} = this.props; // eslint-disable-line
return (
<RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}>
{({onKeyDownHandler}) => <div
{...props}
onKeyDown={onKeyDownHandler}
ref={this._collectResizeContainer}
className="mx_RoomList"
role="tree"
aria-label={_t("Rooms")}
// Firefox sometimes makes this element focusable due to
// overflow:scroll;, so force it out of tab order.
tabIndex="-1"
onMouseMove={this.onMouseMove}
onMouseLeave={this.onMouseLeave}
>
{ subListComponents }
</div> }
</RovingTabIndexProvider>
);
},
});

View file

@ -23,13 +23,13 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { _t, _td } from "../../../languageHandler";
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
import { ResizeNotifier } from "../../../utils/ResizeNotifier";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore2";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import RoomViewStore from "../../../stores/RoomViewStore";
import { ITagMap } 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 RoomSublist2 from "./RoomSublist2";
import RoomSublist from "./RoomSublist";
import { ActionPayload } from "../../../dispatcher/payloads";
import { NameFilterCondition } from "../../../stores/room-list/filters/NameFilterCondition";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
@ -40,8 +40,7 @@ import { NotificationColor } from "../../../stores/notifications/NotificationCol
import { Action } from "../../../dispatcher/actions";
import { ViewRoomDeltaPayload } from "../../../dispatcher/payloads/ViewRoomDeltaPayload";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
import SettingsStore from "../../../settings/SettingsStore";
interface IProps {
onKeyDown: (ev: React.KeyboardEvent) => void;
@ -131,7 +130,7 @@ const TAG_AESTHETICS: {
},
};
export default class RoomList2 extends React.Component<IProps, IState> {
export default class RoomList extends React.Component<IProps, IState> {
private searchFilter: NameFilterCondition = new NameFilterCondition();
private dispatcherRef;
@ -209,7 +208,13 @@ export default class RoomList2 extends React.Component<IProps, IState> {
};
private updateLists = () => {
this.setState({sublists: RoomListStore.instance.orderedLists}, () => {
const newLists = RoomListStore.instance.orderedLists;
if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log("new lists", newLists);
}
this.setState({sublists: newLists}, () => {
this.props.onResize();
});
};
@ -270,7 +275,7 @@ export default class RoomList2 extends React.Component<IProps, IState> {
const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null;
components.push(
<RoomSublist2
<RoomSublist
key={`sublist-${orderedTagId}`}
tagId={orderedTagId}
forRooms={true}
@ -299,7 +304,7 @@ export default class RoomList2 extends React.Component<IProps, IState> {
onFocus={this.props.onFocus}
onBlur={this.props.onBlur}
onKeyDown={onKeyDownHandler}
className="mx_RoomList2"
className="mx_RoomList"
role="tree"
aria-label={_t("Rooms")}
>{sublists}</div>

View file

@ -1,7 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 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.
@ -24,6 +24,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher';
import classNames from 'classnames';
import { _t } from '../../../languageHandler';
import SdkConfig from "../../../SdkConfig";
import IdentityAuthClient from '../../../IdentityAuthClient';
const MessageCase = Object.freeze({
@ -282,6 +283,7 @@ export default createReactClass({
},
render: function() {
const brand = SdkConfig.get().brand;
const Spinner = sdk.getComponent('elements.Spinner');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
@ -398,7 +400,8 @@ export default createReactClass({
);
subTitle = _t(
"Link this email with your account in Settings to receive invites " +
"directly in Riot.",
"directly in %(brand)s.",
{ brand },
);
primaryActionLabel = _t("Join the discussion");
primaryActionHandler = this.props.onJoinClick;
@ -413,7 +416,8 @@ export default createReactClass({
},
);
subTitle = _t(
"Use an identity server in Settings to receive invites directly in Riot.",
"Use an identity server in Settings to receive invites directly in %(brand)s.",
{ brand },
);
primaryActionLabel = _t("Join the discussion");
primaryActionHandler = this.props.onJoinClick;
@ -428,7 +432,8 @@ export default createReactClass({
},
);
subTitle = _t(
"Share this email in Settings to receive invites directly in Riot.",
"Share this email in Settings to receive invites directly in %(brand)s.",
{ brand },
);
primaryActionLabel = _t("Join the discussion");
primaryActionHandler = this.props.onJoinClick;

View file

@ -23,16 +23,16 @@ import classNames from 'classnames';
import { RovingAccessibleButton, RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
import { _t } from "../../../languageHandler";
import AccessibleButton from "../../views/elements/AccessibleButton";
import RoomTile2 from "./RoomTile2";
import RoomTile from "./RoomTile";
import { ListLayout } from "../../../stores/room-list/ListLayout";
import {
ChevronFace,
ContextMenu,
ContextMenuButton,
ContextMenuTooltipButton,
StyledMenuItemCheckbox,
StyledMenuItemRadio,
} from "../../structures/ContextMenu";
import RoomListStore from "../../../stores/room-list/RoomListStore2";
import RoomListStore 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";
@ -48,8 +48,6 @@ import { polyfillTouchEvent } from "../../../@types/polyfill";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore";
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
const SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS
const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS
export const HEADER_HEIGHT = 32; // As defined by CSS
@ -94,7 +92,7 @@ interface IState {
height: number;
}
export default class RoomSublist2 extends React.Component<IProps, IState> {
export default class RoomSublist extends React.Component<IProps, IState> {
private headerButton = createRef<HTMLDivElement>();
private sublistRef = createRef<HTMLDivElement>();
private dispatcherRef: string;
@ -127,18 +125,24 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
private get padding() {
let padding = RESIZE_HANDLE_HEIGHT;
// this is used for calculating the max height of the whole container,
// and takes into account whether there should be room reserved for the show less button
// when fully expanded. We cannot check against the layout's defaultVisible tile count
// and takes into account whether there should be room reserved for the show more/less button
// when fully expanded. We can't rely purely on the layout's defaultVisible tile count
// because there are conditions in which we need to know that the 'show more' button
// is present while well under the default tile limit.
if (this.numTiles > this.numVisibleTiles) {
const needsShowMore = this.numTiles > this.numVisibleTiles;
// ...but also check this or we'll miss if the section is expanded and we need a
// 'show less'
const needsShowLess = this.numTiles > this.layout.defaultVisibleTiles;
if (needsShowMore || needsShowLess) {
padding += SHOW_N_BUTTON_HEIGHT;
}
return padding;
}
private get numTiles(): number {
return RoomSublist2.calcNumTiles(this.props);
return RoomSublist.calcNumTiles(this.props);
}
private static calcNumTiles(props) {
@ -161,7 +165,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
}
// 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 (RoomSublist2.calcNumTiles(prevProps) !== this.numTiles) {
if (RoomSublist.calcNumTiles(prevProps) !== this.numTiles) {
this.setState({height: this.calculateInitialHeight()});
}
}
@ -246,7 +250,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
private focusRoomTile = (index: number) => {
if (!this.sublistRef.current) return;
const elements = this.sublistRef.current.querySelectorAll<HTMLDivElement>(".mx_RoomTile2");
const elements = this.sublistRef.current.querySelectorAll<HTMLDivElement>(".mx_RoomTile");
const element = elements && elements[index];
if (element) {
element.focus();
@ -320,11 +324,11 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
const possibleSticky = this.headerButton.current.parentElement;
const sublist = possibleSticky.parentElement.parentElement;
const list = sublist.parentElement.parentElement;
// the scrollTop is capped at the height of the header in LeftPanel2, the top header is always sticky
// the scrollTop is capped at the height of the header in LeftPanel, the top header is always sticky
const isAtTop = list.scrollTop <= HEADER_HEIGHT;
const isAtBottom = list.scrollTop >= list.scrollHeight - list.offsetHeight;
const isStickyTop = possibleSticky.classList.contains('mx_RoomSublist2_headerContainer_stickyTop');
const isStickyBottom = possibleSticky.classList.contains('mx_RoomSublist2_headerContainer_stickyBottom');
const isStickyTop = possibleSticky.classList.contains('mx_RoomSublist_headerContainer_stickyTop');
const isStickyBottom = possibleSticky.classList.contains('mx_RoomSublist_headerContainer_stickyBottom');
if ((isStickyBottom && !isAtBottom) || (isStickyTop && !isAtTop)) {
// is sticky - jump to list
@ -364,7 +368,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
this.toggleCollapsed();
} else if (this.sublistRef.current) {
// otherwise focus the first room
const element = this.sublistRef.current.querySelector(".mx_RoomTile2") as HTMLDivElement;
const element = this.sublistRef.current.querySelector(".mx_RoomTile") as HTMLDivElement;
if (element) {
element.focus();
}
@ -399,7 +403,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
const visibleRooms = this.props.rooms.slice(0, this.numVisibleTiles);
for (const room of visibleRooms) {
tiles.push(
<RoomTile2
<RoomTile
room={room}
key={`room-${room.roomId}`}
showMessagePreview={this.layout.showPreviews}
@ -438,24 +442,20 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
<React.Fragment>
<hr />
<div>
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Unread rooms")}</div>
<div className='mx_RoomSublist_contextMenu_title'>{_t("Appearance")}</div>
<StyledMenuItemCheckbox
onClose={this.onCloseMenu}
onChange={this.onUnreadFirstChanged}
checked={isUnreadFirst}
>
{_t("Always show first")}
{_t("Show rooms with unread messages first")}
</StyledMenuItemCheckbox>
</div>
<hr />
<div>
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Show")}</div>
<StyledMenuItemCheckbox
onClose={this.onCloseMenu}
onChange={this.onMessagePreviewChanged}
checked={this.layout.showPreviews}
>
{_t("Message preview")}
{_t("Show previews of messages")}
</StyledMenuItemCheckbox>
</div>
</React.Fragment>
@ -469,9 +469,9 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
top={this.state.contextMenuPosition.top + this.state.contextMenuPosition.height}
onFinished={this.onCloseMenu}
>
<div className="mx_RoomSublist2_contextMenu">
<div className="mx_RoomSublist_contextMenu">
<div>
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Sort by")}</div>
<div className='mx_RoomSublist_contextMenu_title'>{_t("Sort by")}</div>
<StyledMenuItemRadio
onClose={this.onCloseMenu}
onChange={() => this.onTagSortChanged(SortAlgorithm.Recent)}
@ -497,10 +497,10 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
return (
<React.Fragment>
<ContextMenuButton
className="mx_RoomSublist2_menuButton"
<ContextMenuTooltipButton
className="mx_RoomSublist_menuButton"
onClick={this.onOpenMenuClick}
label={_t("List options")}
title={_t("List options")}
isExpanded={!!this.state.contextMenuPosition}
/>
{contextMenu}
@ -535,30 +535,35 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
<AccessibleTooltipButton
tabIndex={tabIndex}
onClick={this.onAddRoom}
className="mx_RoomSublist2_auxButton"
className="mx_RoomSublist_auxButton"
aria-label={this.props.addRoomLabel || _t("Add room")}
title={this.props.addRoomLabel}
tooltipClassName={"mx_RoomSublist2_addRoomTooltip"}
tooltipClassName={"mx_RoomSublist_addRoomTooltip"}
/>
);
}
const collapseClasses = classNames({
'mx_RoomSublist2_collapseBtn': true,
'mx_RoomSublist2_collapseBtn_collapsed': !this.state.isExpanded,
'mx_RoomSublist_collapseBtn': true,
'mx_RoomSublist_collapseBtn_collapsed': !this.state.isExpanded,
});
const classes = classNames({
'mx_RoomSublist2_headerContainer': true,
'mx_RoomSublist2_headerContainer_withAux': !!addRoomButton,
'mx_RoomSublist_headerContainer': true,
'mx_RoomSublist_headerContainer_withAux': !!addRoomButton,
});
const badgeContainer = (
<div className="mx_RoomSublist2_badgeContainer">
<div className="mx_RoomSublist_badgeContainer">
{badge}
</div>
);
let Button: React.ComponentType<React.ComponentProps<typeof AccessibleButton>> = AccessibleButton;
if (this.props.isMinimized) {
Button = AccessibleTooltipButton;
}
// Note: the addRoomButton conditionally gets moved around
// the DOM depending on whether or not the list is minimized.
// If we're minimized, we want it below the header so it
@ -566,21 +571,22 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
// The same applies to the notification badge.
return (
<div className={classes} onKeyDown={this.onHeaderKeyDown} onFocus={onFocus} aria-label={this.props.label}>
<div className="mx_RoomSublist2_stickable">
<AccessibleButton
<div className="mx_RoomSublist_stickable">
<Button
onFocus={onFocus}
inputRef={ref}
tabIndex={tabIndex}
className="mx_RoomSublist2_headerText"
className="mx_RoomSublist_headerText"
role="treeitem"
aria-expanded={this.state.isExpanded}
aria-level={1}
onClick={this.onHeaderClick}
onContextMenu={this.onContextMenu}
title={this.props.isMinimized ? this.props.label : undefined}
>
<span className={collapseClasses} />
<span>{this.props.label}</span>
</AccessibleButton>
</Button>
{this.renderMenu()}
{this.props.isMinimized ? null : badgeContainer}
{this.props.isMinimized ? null : addRoomButton}
@ -603,9 +609,9 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
public render(): React.ReactElement {
const visibleTiles = this.renderVisibleTiles();
const classes = classNames({
'mx_RoomSublist2': true,
'mx_RoomSublist2_hasMenuOpen': !!this.state.contextMenuPosition,
'mx_RoomSublist2_minimized': this.props.isMinimized,
'mx_RoomSublist': true,
'mx_RoomSublist_hasMenuOpen': !!this.state.contextMenuPosition,
'mx_RoomSublist_minimized': this.props.isMinimized,
});
let content = null;
@ -618,31 +624,28 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
const minTilesPx = layout.tilesToPixelsWithPadding(minTiles, minHeightPadding);
let maxTilesPx = layout.tilesToPixelsWithPadding(this.numTiles, this.padding);
const showMoreBtnClasses = classNames({
'mx_RoomSublist2_showNButton': true,
'mx_RoomSublist_showNButton': true,
});
if (this.numTiles > this.layout.defaultVisibleTiles) {
maxTilesPx += SHOW_N_BUTTON_HEIGHT;
}
// If we're hiding rooms, show a 'show more' button to the user. This button
// floats above the resize handle, if we have one present. If the user has all
// tiles visible, it becomes 'show less'.
let showNButton = null;
if (maxTilesPx > this.state.height) {
// the height of all the tiles is greater than the section height: we need a 'show more' button
const nonPaddedHeight = this.state.height - RESIZE_HANDLE_HEIGHT - SHOW_N_BUTTON_HEIGHT;
const amountFullyShown = Math.floor(nonPaddedHeight / this.layout.tileHeight);
const numMissing = this.numTiles - amountFullyShown;
let showMoreText = (
<span className='mx_RoomSublist2_showNButtonText'>
<span className='mx_RoomSublist_showNButtonText'>
{_t("Show %(count)s more", {count: numMissing})}
</span>
);
if (this.props.isMinimized) showMoreText = null;
showNButton = (
<RovingAccessibleButton onClick={this.onShowAllClick} className={showMoreBtnClasses}>
<span className='mx_RoomSublist2_showMoreButtonChevron mx_RoomSublist2_showNButtonChevron'>
<span className='mx_RoomSublist_showMoreButtonChevron mx_RoomSublist_showNButtonChevron'>
{/* set by CSS masking */}
</span>
{showMoreText}
@ -651,14 +654,14 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
} else if (this.numTiles > this.layout.defaultVisibleTiles) {
// we have all tiles visible - add a button to show less
let showLessText = (
<span className='mx_RoomSublist2_showNButtonText'>
<span className='mx_RoomSublist_showNButtonText'>
{_t("Show less")}
</span>
);
if (this.props.isMinimized) showLessText = null;
showNButton = (
<RovingAccessibleButton onClick={this.onShowLessClick} className={showMoreBtnClasses}>
<span className='mx_RoomSublist2_showLessButtonChevron mx_RoomSublist2_showNButtonChevron'>
<span className='mx_RoomSublist_showLessButtonChevron mx_RoomSublist_showNButtonChevron'>
{/* set by CSS masking */}
</span>
{showLessText}
@ -693,8 +696,8 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
// only mathematically 7 possible).
const handleWrapperClasses = classNames({
'mx_RoomSublist2_resizerHandles': true,
'mx_RoomSublist2_resizerHandles_showNButton': !!showNButton,
'mx_RoomSublist_resizerHandles': true,
'mx_RoomSublist_resizerHandles_showNButton': !!showNButton,
});
content = (
@ -707,11 +710,11 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
onResizeStop={this.onResizeStop}
onResize={this.onResize}
handleWrapperClass={handleWrapperClasses}
handleClasses={{bottom: "mx_RoomSublist2_resizerHandle"}}
className="mx_RoomSublist2_resizeBox"
handleClasses={{bottom: "mx_RoomSublist_resizerHandle"}}
className="mx_RoomSublist_resizeBox"
enable={handles}
>
<div className="mx_RoomSublist2_tiles" onScroll={this.onScrollPrevent}>
<div className="mx_RoomSublist_tiles" onScroll={this.onScrollPrevent}>
{visibleTiles}
</div>
{showNButton}

View file

@ -1,565 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
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, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import classNames from 'classnames';
import dis from '../../../dispatcher/dispatcher';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import DMRoomMap from '../../../utils/DMRoomMap';
import * as sdk from '../../../index';
import {ContextMenu, ContextMenuButton, toRightOf} from '../../structures/ContextMenu';
import * as RoomNotifs from '../../../RoomNotifs';
import * as FormattingUtils from '../../../utils/FormattingUtils';
import ActiveRoomObserver from '../../../ActiveRoomObserver';
import RoomViewStore from '../../../stores/RoomViewStore';
import SettingsStore from "../../../settings/SettingsStore";
import {_t} from "../../../languageHandler";
import {RovingTabIndexWrapper} from "../../../accessibility/RovingTabIndex";
import E2EIcon from './E2EIcon';
import InviteOnlyIcon from './InviteOnlyIcon';
// eslint-disable-next-line camelcase
import rate_limited_func from '../../../ratelimitedfunc';
import { shieldStatusForRoom } from '../../../utils/ShieldUtils';
export default createReactClass({
displayName: 'RoomTile',
propTypes: {
onClick: PropTypes.func,
room: PropTypes.object.isRequired,
collapsed: PropTypes.bool.isRequired,
unread: PropTypes.bool.isRequired,
highlight: PropTypes.bool.isRequired,
// If true, apply mx_RoomTile_transparent class
transparent: PropTypes.bool,
isInvite: PropTypes.bool.isRequired,
incomingCall: PropTypes.object,
},
getDefaultProps: function() {
return {
isDragging: false,
};
},
getInitialState: function() {
const joinRules = this.props.room.currentState.getStateEvents("m.room.join_rules", "");
const joinRule = joinRules && joinRules.getContent().join_rule;
return ({
joinRule,
hover: false,
badgeHover: false,
contextMenuPosition: null, // DOM bounding box, null if non-shown
roomName: this.props.room.name,
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
notificationCount: this.props.room.getUnreadNotificationCount(),
selected: this.props.room.roomId === RoomViewStore.getRoomId(),
statusMessage: this._getStatusMessage(),
e2eStatus: null,
});
},
_shouldShowStatusMessage() {
if (!SettingsStore.isFeatureEnabled("feature_custom_status")) {
return false;
}
const isInvite = this.props.room.getMyMembership() === "invite";
const isJoined = this.props.room.getMyMembership() === "join";
const looksLikeDm = this.props.room.getInvitedAndJoinedMemberCount() === 2;
return !isInvite && isJoined && looksLikeDm;
},
_getStatusMessageUser() {
if (!MatrixClientPeg.get()) return null; // We've probably been logged out
const selfId = MatrixClientPeg.get().getUserId();
const otherMember = this.props.room.currentState.getMembersExcept([selfId])[0];
if (!otherMember) {
return null;
}
return otherMember.user;
},
_getStatusMessage() {
const statusUser = this._getStatusMessageUser();
if (!statusUser) {
return "";
}
return statusUser._unstable_statusMessage;
},
onRoomStateMember: function(ev, state, member) {
// we only care about leaving users
// because trust state will change if someone joins a megolm session anyway
if (member.membership !== "leave") {
return;
}
// ignore members in other rooms
if (member.roomId !== this.props.room.roomId) {
return;
}
this._updateE2eStatus();
},
onUserVerificationChanged: function(userId, _trustStatus) {
if (!this.props.room.getMember(userId)) {
// Not in this room
return;
}
this._updateE2eStatus();
},
onCrossSigningKeysChanged: function() {
this._updateE2eStatus();
},
onRoomTimeline: function(ev, room) {
if (!room) return;
if (room.roomId != this.props.room.roomId) return;
if (ev.getType() !== "m.room.encryption") return;
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
this.onFindingRoomToBeEncrypted();
},
onFindingRoomToBeEncrypted: function() {
const cli = MatrixClientPeg.get();
cli.on("RoomState.members", this.onRoomStateMember);
cli.on("userTrustStatusChanged", this.onUserVerificationChanged);
cli.on("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
this._updateE2eStatus();
},
_updateE2eStatus: async function() {
const cli = MatrixClientPeg.get();
if (!cli.isRoomEncrypted(this.props.room.roomId)) {
return;
}
/* At this point, the user has encryption on and cross-signing on */
this.setState({
e2eStatus: await shieldStatusForRoom(cli, this.props.room),
});
},
onRoomName: function(room) {
if (room !== this.props.room) return;
this.setState({
roomName: this.props.room.name,
});
},
onJoinRule: function(ev) {
if (ev.getType() !== "m.room.join_rules") return;
if (ev.getRoomId() !== this.props.room.roomId) return;
this.setState({ joinRule: ev.getContent().join_rule });
},
onAccountData: function(accountDataEvent) {
if (accountDataEvent.getType() === 'm.push_rules') {
this.setState({
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
});
}
},
onAction: function(payload) {
switch (payload.action) {
// XXX: slight hack in order to zero the notification count when a room
// is read. Ideally this state would be given to this via props (as we
// do with `unread`). This is still better than forceUpdating the entire
// RoomList when a room is read.
case 'on_room_read':
if (payload.roomId !== this.props.room.roomId) break;
this.setState({
notificationCount: this.props.room.getUnreadNotificationCount(),
});
break;
// RoomTiles are one of the few components that may show custom status and
// also remain on screen while in Settings toggling the feature. This ensures
// you can clearly see the status hide and show when toggling the feature.
case 'feature_custom_status_changed':
this.forceUpdate();
break;
case 'view_room':
// when the room is selected make sure its tile is visible, for breadcrumbs/keyboard shortcut access
if (payload.room_id === this.props.room.roomId && payload.show_room_tile) {
this._scrollIntoView();
}
break;
}
},
_scrollIntoView: function() {
if (!this._roomTile.current) return;
this._roomTile.current.scrollIntoView({
block: "nearest",
behavior: "auto",
});
},
_onActiveRoomChange: function() {
this.setState({
selected: this.props.room.roomId === RoomViewStore.getRoomId(),
});
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._roomTile = createRef();
},
componentDidMount: function() {
/* We bind here rather than in the definition because otherwise we wind up with the
method only being callable once every 500ms across all instances, which would be wrong */
this._updateE2eStatus = rate_limited_func(this._updateE2eStatus, 500);
const cli = MatrixClientPeg.get();
cli.on("accountData", this.onAccountData);
cli.on("Room.name", this.onRoomName);
cli.on("RoomState.events", this.onJoinRule);
if (cli.isRoomEncrypted(this.props.room.roomId)) {
this.onFindingRoomToBeEncrypted();
} else {
cli.on("Room.timeline", this.onRoomTimeline);
}
ActiveRoomObserver.addListener(this.props.room.roomId, this._onActiveRoomChange);
this.dispatcherRef = dis.register(this.onAction);
if (this._shouldShowStatusMessage()) {
const statusUser = this._getStatusMessageUser();
if (statusUser) {
statusUser.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
}
}
// when we're first rendered (or our sublist is expanded) make sure we are visible if we're active
if (this.state.selected) {
this._scrollIntoView();
}
},
componentWillUnmount: function() {
const cli = MatrixClientPeg.get();
if (cli) {
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
cli.removeListener("RoomState.events", this.onJoinRule);
cli.removeListener("RoomState.members", this.onRoomStateMember);
cli.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
cli.removeListener("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
cli.removeListener("Room.timeline", this.onRoomTimeline);
}
ActiveRoomObserver.removeListener(this.props.room.roomId, this._onActiveRoomChange);
dis.unregister(this.dispatcherRef);
if (this._shouldShowStatusMessage()) {
const statusUser = this._getStatusMessageUser();
if (statusUser) {
statusUser.removeListener(
"User._unstable_statusMessage",
this._onStatusMessageCommitted,
);
}
}
},
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps: function(props) {
// XXX: This could be a lot better - this makes the assumption that
// the notification count may have changed when the properties of
// the room tile change.
this.setState({
notificationCount: this.props.room.getUnreadNotificationCount(),
});
},
// Do a simple shallow comparison of props and state to avoid unnecessary
// renders. The assumption made here is that only state and props are used
// in rendering this component and children.
//
// RoomList is frequently made to forceUpdate, so this decreases number of
// RoomTile renderings.
shouldComponentUpdate: function(newProps, newState) {
if (Object.keys(newProps).some((k) => newProps[k] !== this.props[k])) {
return true;
}
if (Object.keys(newState).some((k) => newState[k] !== this.state[k])) {
return true;
}
return false;
},
_onStatusMessageCommitted() {
// The status message `User` object has observed a message change.
this.setState({
statusMessage: this._getStatusMessage(),
});
},
onClick: function(ev) {
if (this.props.onClick) {
this.props.onClick(this.props.room.roomId, ev);
}
},
onMouseEnter: function() {
this.setState( { hover: true });
this.badgeOnMouseEnter();
},
onMouseLeave: function() {
this.setState( { hover: false });
this.badgeOnMouseLeave();
},
badgeOnMouseEnter: function() {
// Only allow non-guests to access the context menu
// and only change it if it needs to change
if (!MatrixClientPeg.get().isGuest() && !this.state.badgeHover) {
this.setState( { badgeHover: true } );
}
},
badgeOnMouseLeave: function() {
this.setState( { badgeHover: false } );
},
_showContextMenu: function(boundingClientRect) {
// Only allow non-guests to access the context menu
if (MatrixClientPeg.get().isGuest()) return;
const state = {
contextMenuPosition: boundingClientRect,
};
// If the badge is clicked, then no longer show tooltip
if (this.props.collapsed) {
state.hover = false;
}
this.setState(state);
},
onContextMenuButtonClick: function(e) {
// Prevent the RoomTile onClick event firing as well
e.stopPropagation();
e.preventDefault();
this._showContextMenu(e.target.getBoundingClientRect());
},
onContextMenu: function(e) {
// Prevent the native context menu
e.preventDefault();
this._showContextMenu({
right: e.clientX,
top: e.clientY,
height: 0,
});
},
closeMenu: function() {
this.setState({
contextMenuPosition: null,
});
this.props.refreshSubList();
},
render: function() {
const isInvite = this.props.room.getMyMembership() === "invite";
const notificationCount = this.props.notificationCount;
// var highlightCount = this.props.room.getUnreadNotificationCount("highlight");
const notifBadges = notificationCount > 0 && RoomNotifs.shouldShowNotifBadge(this.state.notifState);
const mentionBadges = this.props.highlight && RoomNotifs.shouldShowMentionBadge(this.state.notifState);
const badges = notifBadges || mentionBadges;
let subtext = null;
if (this._shouldShowStatusMessage()) {
subtext = this.state.statusMessage;
}
const isMenuDisplayed = Boolean(this.state.contextMenuPosition);
const dmUserId = DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId);
const classes = classNames({
'mx_RoomTile': true,
'mx_RoomTile_selected': this.state.selected,
'mx_RoomTile_unread': this.props.unread,
'mx_RoomTile_unreadNotify': notifBadges,
'mx_RoomTile_highlight': mentionBadges,
'mx_RoomTile_invited': isInvite,
'mx_RoomTile_menuDisplayed': isMenuDisplayed,
'mx_RoomTile_noBadges': !badges,
'mx_RoomTile_transparent': this.props.transparent,
'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed,
});
const avatarClasses = classNames({
'mx_RoomTile_avatar': true,
});
const badgeClasses = classNames({
'mx_RoomTile_badge': true,
'mx_RoomTile_badgeButton': this.state.badgeHover || isMenuDisplayed,
});
let name = this.state.roomName;
if (typeof name !== 'string') name = '';
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
let badge;
if (badges) {
const limitedCount = FormattingUtils.formatCount(notificationCount);
const badgeContent = notificationCount ? limitedCount : '!';
badge = <div className={badgeClasses}>{ badgeContent }</div>;
}
let label;
let subtextLabel;
let tooltip;
if (!this.props.collapsed) {
const nameClasses = classNames({
'mx_RoomTile_name': true,
'mx_RoomTile_invite': this.props.isInvite,
'mx_RoomTile_badgeShown': badges || this.state.badgeHover || isMenuDisplayed,
});
subtextLabel = subtext ? <span className="mx_RoomTile_subtext">{ subtext }</span> : null;
// XXX: this is a workaround for Firefox giving this div a tabstop :( [tabIndex]
label = <div title={name} className={nameClasses} tabIndex={-1} dir="auto">{ name }</div>;
} else if (this.state.hover) {
const Tooltip = sdk.getComponent("elements.Tooltip");
tooltip = <Tooltip className="mx_RoomTile_tooltip" label={this.props.room.name} dir="auto" />;
}
//var incomingCallBox;
//if (this.props.incomingCall) {
// var IncomingCallBox = sdk.getComponent("voip.IncomingCallBox");
// incomingCallBox = <IncomingCallBox incomingCall={ this.props.incomingCall }/>;
//}
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let contextMenuButton;
if (!MatrixClientPeg.get().isGuest()) {
contextMenuButton = (
<ContextMenuButton
className="mx_RoomTile_menuButton"
label={_t("Options")}
isExpanded={isMenuDisplayed}
onClick={this.onContextMenuButtonClick} />
);
}
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
let ariaLabel = name;
let dmOnline;
const { room } = this.props;
const member = room.getMember(dmUserId);
if (member && member.membership === "join" && room.getJoinedMemberCount() === 2) {
const UserOnlineDot = sdk.getComponent('rooms.UserOnlineDot');
dmOnline = <UserOnlineDot userId={dmUserId} />;
}
// The following labels are written in such a fashion to increase screen reader efficiency (speed).
if (notifBadges && mentionBadges && !isInvite) {
ariaLabel += " " + _t("%(count)s unread messages including mentions.", {
count: notificationCount,
});
} else if (notifBadges) {
ariaLabel += " " + _t("%(count)s unread messages.", { count: notificationCount });
} else if (mentionBadges && !isInvite) {
ariaLabel += " " + _t("Unread mentions.");
} else if (this.props.unread) {
ariaLabel += " " + _t("Unread messages.");
}
let contextMenu;
if (isMenuDisplayed) {
const RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu');
contextMenu = (
<ContextMenu {...toRightOf(this.state.contextMenuPosition)} onFinished={this.closeMenu}>
<RoomTileContextMenu room={this.props.room} onFinished={this.closeMenu} />
</ContextMenu>
);
}
let privateIcon = null;
if (this.state.joinRule === "invite" && !dmUserId) {
privateIcon = <InviteOnlyIcon collapsedPanel={this.props.collapsed} />;
}
let e2eIcon = null;
if (this.state.e2eStatus) {
e2eIcon = <E2EIcon status={this.state.e2eStatus} className="mx_RoomTile_e2eIcon" />;
}
return <React.Fragment>
<RovingTabIndexWrapper inputRef={this._roomTile}>
{({onFocus, isActive, ref}) =>
<AccessibleButton
onFocus={onFocus}
tabIndex={isActive ? 0 : -1}
inputRef={ref}
className={classes}
onClick={this.onClick}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
onContextMenu={this.onContextMenu}
aria-label={ariaLabel}
aria-selected={this.state.selected}
role="treeitem"
>
<div className={avatarClasses}>
<div className="mx_RoomTile_avatar_container">
<RoomAvatar room={this.props.room} width={24} height={24} />
{ e2eIcon }
</div>
</div>
{ privateIcon }
<div className="mx_RoomTile_nameContainer">
<div className="mx_RoomTile_labelContainer">
{ label }
{ subtextLabel }
</div>
{ dmOnline }
{ contextMenuButton }
{ badge }
</div>
{ /* { incomingCallBox } */ }
{ tooltip }
</AccessibleButton>
}
</RovingTabIndexWrapper>
{ contextMenu }
</React.Fragment>;
},
});

View file

@ -29,7 +29,7 @@ import { _t } from "../../../languageHandler";
import {
ChevronFace,
ContextMenu,
ContextMenuButton,
ContextMenuTooltipButton,
MenuItemRadio,
MenuItemCheckbox,
MenuItem,
@ -48,14 +48,13 @@ import {
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import NotificationBadge from "./NotificationBadge";
import { Volume } from "../../../RoomNotifsTypes";
import RoomListStore from "../../../stores/room-list/RoomListStore2";
import RoomListStore from "../../../stores/room-list/RoomListStore";
import RoomListActions from "../../../actions/RoomListActions";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import {ActionPayload} from "../../../dispatcher/payloads";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import { NotificationState } from "../../../stores/notifications/NotificationState";
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
interface IProps {
room: Room;
@ -76,7 +75,7 @@ interface IState {
generalMenuPosition: PartialDOMRect;
}
const messagePreviewId = (roomId: string) => `mx_RoomTile2_messagePreview_${roomId}`;
const messagePreviewId = (roomId: string) => `mx_RoomTile_messagePreview_${roomId}`;
const contextMenuBelow = (elementRect: PartialDOMRect) => {
// align the context menu's icons with the icon which opened the context menu
@ -95,12 +94,12 @@ interface INotifOptionProps {
const NotifOption: React.FC<INotifOptionProps> = ({active, onClick, iconClassName, label}) => {
const classes = classNames({
mx_RoomTile2_contextMenu_activeRow: active,
mx_RoomTile_contextMenu_activeRow: active,
});
let activeIcon;
if (active) {
activeIcon = <span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconCheck" />;
activeIcon = <span className="mx_IconizedContextMenu_icon mx_RoomTile_iconCheck" />;
}
return (
@ -112,7 +111,7 @@ const NotifOption: React.FC<INotifOptionProps> = ({active, onClick, iconClassNam
);
};
export default class RoomTile2 extends React.Component<IProps, IState> {
export default class RoomTile extends React.Component<IProps, IState> {
private dispatcherRef: string;
private roomTileRef = createRef<HTMLDivElement>();
@ -327,30 +326,30 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
if (this.state.notificationsMenuPosition) {
contextMenu = (
<ContextMenu {...contextMenuBelow(this.state.notificationsMenuPosition)} onFinished={this.onCloseNotificationsMenu}>
<div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile2_contextMenu">
<div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile_contextMenu">
<div className="mx_IconizedContextMenu_optionList">
<NotifOption
label={_t("Use default")}
active={state === ALL_MESSAGES}
iconClassName="mx_RoomTile2_iconBell"
iconClassName="mx_RoomTile_iconBell"
onClick={this.onClickAllNotifs}
/>
<NotifOption
label={_t("All messages")}
active={state === ALL_MESSAGES_LOUD}
iconClassName="mx_RoomTile2_iconBellDot"
iconClassName="mx_RoomTile_iconBellDot"
onClick={this.onClickAlertMe}
/>
<NotifOption
label={_t("Mentions & Keywords")}
active={state === MENTIONS_ONLY}
iconClassName="mx_RoomTile2_iconBellMentions"
iconClassName="mx_RoomTile_iconBellMentions"
onClick={this.onClickMentions}
/>
<NotifOption
label={_t("None")}
active={state === MUTE}
iconClassName="mx_RoomTile2_iconBellCrossed"
iconClassName="mx_RoomTile_iconBellCrossed"
onClick={this.onClickMute}
/>
</div>
@ -359,24 +358,24 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
);
}
const classes = classNames("mx_RoomTile2_notificationsButton", {
const classes = classNames("mx_RoomTile_notificationsButton", {
// Show bell icon for the default case too.
mx_RoomTile2_iconBell: state === ALL_MESSAGES,
mx_RoomTile2_iconBellDot: state === ALL_MESSAGES_LOUD,
mx_RoomTile2_iconBellMentions: state === MENTIONS_ONLY,
mx_RoomTile2_iconBellCrossed: state === MUTE,
mx_RoomTile_iconBell: state === ALL_MESSAGES,
mx_RoomTile_iconBellDot: state === ALL_MESSAGES_LOUD,
mx_RoomTile_iconBellMentions: state === MENTIONS_ONLY,
mx_RoomTile_iconBellCrossed: state === MUTE,
// Only show the icon by default if the room is overridden to muted.
// TODO: [FTUE Notifications] Probably need to detect global mute state
mx_RoomTile2_notificationsButton_show: state === MUTE,
mx_RoomTile_notificationsButton_show: state === MUTE,
});
return (
<React.Fragment>
<ContextMenuButton
<ContextMenuTooltipButton
className={classes}
onClick={this.onNotificationsMenuOpenClick}
label={_t("Notification options")}
title={_t("Notification options")}
isExpanded={!!this.state.notificationsMenuPosition}
tabIndex={isActive ? 0 : -1}
/>
@ -391,18 +390,18 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
const roomTags = RoomListStore.instance.getTagsForRoom(this.props.room);
const isFavorite = roomTags.includes(DefaultTagID.Favourite);
const favouriteIconClassName = isFavorite ? "mx_RoomTile2_iconFavorite" : "mx_RoomTile2_iconStar";
const favouriteLabelClassName = isFavorite ? "mx_RoomTile2_contextMenu_activeRow" : "";
const favouriteIconClassName = isFavorite ? "mx_RoomTile_iconFavorite" : "mx_RoomTile_iconStar";
const favouriteLabelClassName = isFavorite ? "mx_RoomTile_contextMenu_activeRow" : "";
const favouriteLabel = isFavorite ? _t("Favourited") : _t("Favourite");
let contextMenu = null;
if (this.state.generalMenuPosition && this.props.tag === DefaultTagID.Archived) {
contextMenu = (
<ContextMenu {...contextMenuBelow(this.state.generalMenuPosition)} onFinished={this.onCloseGeneralMenu}>
<div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile2_contextMenu">
<div className="mx_IconizedContextMenu_optionList mx_RoomTile2_contextMenu_redRow">
<div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile_contextMenu">
<div className="mx_IconizedContextMenu_optionList mx_RoomTile_contextMenu_redRow">
<MenuItem onClick={this.onForgetRoomClick} label={_t("Leave Room")}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSignOut" />
<span className="mx_IconizedContextMenu_icon mx_RoomTile_iconSignOut" />
<span className="mx_IconizedContextMenu_label">{_t("Forget Room")}</span>
</MenuItem>
</div>
@ -412,7 +411,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
} else if (this.state.generalMenuPosition) {
contextMenu = (
<ContextMenu {...contextMenuBelow(this.state.generalMenuPosition)} onFinished={this.onCloseGeneralMenu}>
<div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile2_contextMenu">
<div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile_contextMenu">
<div className="mx_IconizedContextMenu_optionList">
<MenuItemCheckbox
className={favouriteLabelClassName}
@ -424,13 +423,13 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
<span className="mx_IconizedContextMenu_label">{favouriteLabel}</span>
</MenuItemCheckbox>
<MenuItem onClick={this.onOpenRoomSettings} label={_t("Settings")}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSettings" />
<span className="mx_IconizedContextMenu_icon mx_RoomTile_iconSettings" />
<span className="mx_IconizedContextMenu_label">{_t("Settings")}</span>
</MenuItem>
</div>
<div className="mx_IconizedContextMenu_optionList mx_RoomTile2_contextMenu_redRow">
<div className="mx_IconizedContextMenu_optionList mx_RoomTile_contextMenu_redRow">
<MenuItem onClick={this.onLeaveRoomClick} label={_t("Leave Room")}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSignOut" />
<span className="mx_IconizedContextMenu_icon mx_RoomTile_iconSignOut" />
<span className="mx_IconizedContextMenu_label">{_t("Leave Room")}</span>
</MenuItem>
</div>
@ -441,10 +440,10 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
return (
<React.Fragment>
<ContextMenuButton
className="mx_RoomTile2_menuButton"
<ContextMenuTooltipButton
className="mx_RoomTile_menuButton"
onClick={this.onGeneralMenuOpenClick}
label={_t("Room options")}
title={_t("Room options")}
isExpanded={!!this.state.generalMenuPosition}
/>
{contextMenu}
@ -454,10 +453,10 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
public render(): React.ReactElement {
const classes = classNames({
'mx_RoomTile2': true,
'mx_RoomTile2_selected': this.state.selected,
'mx_RoomTile2_hasMenuOpen': !!(this.state.generalMenuPosition || this.state.notificationsMenuPosition),
'mx_RoomTile2_minimized': this.props.isMinimized,
'mx_RoomTile': true,
'mx_RoomTile_selected': this.state.selected,
'mx_RoomTile_hasMenuOpen': !!(this.state.generalMenuPosition || this.state.notificationsMenuPosition),
'mx_RoomTile_minimized': this.props.isMinimized,
});
const roomAvatar = <DecoratedRoomAvatar
@ -471,7 +470,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
if (!this.props.isMinimized) {
// aria-hidden because we summarise the unread count/highlight status in a manual aria-label below
badge = (
<div className="mx_RoomTile2_badgeContainer" aria-hidden="true">
<div className="mx_RoomTile_badgeContainer" aria-hidden="true">
<NotificationBadge
notification={this.state.notificationState}
forceCount={false}
@ -493,7 +492,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
// Only show the preview if there is one to show.
if (text) {
messagePreview = (
<div className="mx_RoomTile2_messagePreview" id={messagePreviewId(this.props.room.roomId)}>
<div className="mx_RoomTile_messagePreview" id={messagePreviewId(this.props.room.roomId)}>
{text}
</div>
);
@ -501,13 +500,13 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
}
const nameClasses = classNames({
"mx_RoomTile2_name": true,
"mx_RoomTile2_nameWithPreview": !!messagePreview,
"mx_RoomTile2_nameHasUnreadEvents": this.state.notificationState.isUnread,
"mx_RoomTile_name": true,
"mx_RoomTile_nameWithPreview": !!messagePreview,
"mx_RoomTile_nameHasUnreadEvents": this.state.notificationState.isUnread,
});
let nameContainer = (
<div className="mx_RoomTile2_nameContainer">
<div className="mx_RoomTile_nameContainer">
<div title={name} className={nameClasses} tabIndex={-1} dir="auto">
{name}
</div>
@ -537,11 +536,16 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
ariaDescribedBy = messagePreviewId(this.props.room.roomId);
}
let Button: React.ComponentType<React.ComponentProps<typeof AccessibleButton>> = AccessibleButton;
if (this.props.isMinimized) {
Button = AccessibleTooltipButton;
}
return (
<React.Fragment>
<RovingTabIndexWrapper inputRef={this.roomTileRef}>
{({onFocus, isActive, ref}) =>
<AccessibleButton
<Button
onFocus={onFocus}
tabIndex={isActive ? 0 : -1}
inputRef={ref}
@ -554,13 +558,14 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
aria-label={ariaLabel}
aria-selected={this.state.selected}
aria-describedby={ariaDescribedBy}
title={this.props.isMinimized ? name : undefined}
>
{roomAvatar}
{nameContainer}
{badge}
{this.renderGeneralMenu()}
{this.renderNotificationsMenu(isActive)}
</AccessibleButton>
</Button>
}
</RovingTabIndexWrapper>
</React.Fragment>

View file

@ -22,6 +22,8 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import DMRoomMap from "../../../utils/DMRoomMap";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { isPresenceEnabled } from "../../../utils/presence";
import { _t } from "../../../languageHandler";
import TextWithTooltip from "../elements/TextWithTooltip";
enum Icon {
// Note: the names here are used in CSS class names
@ -32,9 +34,21 @@ enum Icon {
PresenceOffline = "OFFLINE",
}
function tooltipText(variant: Icon) {
switch (variant) {
case Icon.Globe:
return _t("This room is public");
case Icon.PresenceOnline:
return _t("Online");
case Icon.PresenceAway:
return _t("Away");
case Icon.PresenceOffline:
return _t("Offline");
}
}
interface IProps {
room: Room;
tag: TagID;
}
interface IState {
@ -122,10 +136,11 @@ export default class RoomTileIcon extends React.Component<IProps, IState> {
private calculateIcon(): Icon {
let icon = Icon.None;
if (this.props.tag === DefaultTagID.DM && this.props.room.getJoinedMemberCount() === 2) {
// We look at the DMRoomMap and not the tag here so that we don't exclude DMs in Favourites
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId);
if (otherUserId && this.props.room.getJoinedMemberCount() === 2) {
// Track presence, if available
if (isPresenceEnabled()) {
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId);
if (otherUserId) {
this.dmUser = MatrixClientPeg.get().getUser(otherUserId);
icon = this.getPresenceIcon();
@ -145,6 +160,9 @@ export default class RoomTileIcon extends React.Component<IProps, IState> {
public render(): React.ReactElement {
if (this.state.icon === Icon.None) return null;
return <span className={`mx_RoomTileIcon mx_RoomTileIcon_${this.state.icon.toLowerCase()}`} />;
return <TextWithTooltip
tooltip={tooltipText(this.state.icon)}
class={`mx_RoomTileIcon mx_RoomTileIcon_${this.state.icon.toLowerCase()}`}
/>;
}
}

View file

@ -27,6 +27,7 @@ import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";
import {ContextMenu} from "../../structures/ContextMenu";
import {WidgetType} from "../../../widgets/WidgetType";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
// 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.
@ -409,14 +410,14 @@ export default class Stickerpicker extends React.Component {
} else {
// Show show-stickers button
stickersButton =
<AccessibleButton
<AccessibleTooltipButton
id='stickersButton'
key="controls_show_stickers"
className="mx_MessageComposer_button mx_MessageComposer_stickers"
onClick={this._onShowStickersClick}
title={_t("Show Stickers")}
>
</AccessibleButton>;
</AccessibleTooltipButton>;
}
return <React.Fragment>
{ stickersButton }

View file

@ -16,7 +16,11 @@ limitations under the License.
import React from "react";
import classNames from "classnames";
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
import {
RovingAccessibleButton,
RovingAccessibleTooltipButton,
RovingTabIndexWrapper
} from "../../../accessibility/RovingTabIndex";
import AccessibleButton from "../../views/elements/AccessibleButton";
import NotificationBadge from "./NotificationBadge";
import { NotificationState } from "../../../stores/notifications/NotificationState";
@ -55,10 +59,10 @@ export default class TemporaryTile extends React.Component<IProps, IState> {
public render(): React.ReactElement {
// XXX: We copy classes because it's easier
const classes = classNames({
'mx_RoomTile2': true,
'mx_RoomTile': true,
'mx_TemporaryTile': true,
'mx_RoomTile2_selected': this.props.isSelected,
'mx_RoomTile2_minimized': this.props.isMinimized,
'mx_RoomTile_selected': this.props.isSelected,
'mx_RoomTile_minimized': this.props.isMinimized,
});
const badge = (
@ -73,12 +77,12 @@ export default class TemporaryTile extends React.Component<IProps, IState> {
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
const nameClasses = classNames({
"mx_RoomTile2_name": true,
"mx_RoomTile2_nameHasUnreadEvents": this.props.notificationState.isUnread,
"mx_RoomTile_name": true,
"mx_RoomTile_nameHasUnreadEvents": this.props.notificationState.isUnread,
});
let nameContainer = (
<div className="mx_RoomTile2_nameContainer">
<div className="mx_RoomTile_nameContainer">
<div title={name} className={nameClasses} tabIndex={-1} dir="auto">
{name}
</div>
@ -86,30 +90,29 @@ export default class TemporaryTile extends React.Component<IProps, IState> {
);
if (this.props.isMinimized) nameContainer = null;
let Button = RovingAccessibleButton;
if (this.props.isMinimized) {
Button = RovingAccessibleTooltipButton;
}
return (
<React.Fragment>
<RovingTabIndexWrapper>
{({onFocus, isActive, ref}) =>
<AccessibleButton
onFocus={onFocus}
tabIndex={isActive ? 0 : -1}
inputRef={ref}
className={classes}
onMouseEnter={this.onTileMouseEnter}
onMouseLeave={this.onTileMouseLeave}
onClick={this.props.onClick}
role="treeitem"
>
<div className="mx_RoomTile2_avatarContainer">
{this.props.avatar}
</div>
{nameContainer}
<div className="mx_RoomTile2_badgeContainer">
{badge}
</div>
</AccessibleButton>
}
</RovingTabIndexWrapper>
<Button
className={classes}
onMouseEnter={this.onTileMouseEnter}
onMouseLeave={this.onTileMouseLeave}
onClick={this.props.onClick}
role="treeitem"
title={this.props.isMinimized ? name : undefined}
>
<div className="mx_RoomTile_avatarContainer">
{this.props.avatar}
</div>
{nameContainer}
<div className="mx_RoomTile_badgeContainer">
{badge}
</div>
</Button>
</React.Fragment>
);
}

View file

@ -1,48 +0,0 @@
/*
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, {useContext, useEffect, useMemo, useState, useCallback} from "react";
import PropTypes from "prop-types";
import {useEventEmitter} from "../../../hooks/useEventEmitter";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
const UserOnlineDot = ({userId}) => {
const cli = useContext(MatrixClientContext);
const user = useMemo(() => cli.getUser(userId), [cli, userId]);
const [isOnline, setIsOnline] = useState(false);
// Recheck if the user or client changes
useEffect(() => {
setIsOnline(user && (user.currentlyActive || user.presence === "online"));
}, [cli, user]);
// Recheck also if we receive a User.currentlyActive event
const currentlyActiveHandler = useCallback((ev) => {
const content = ev.getContent();
setIsOnline(content.currently_active || content.presence === "online");
}, []);
useEventEmitter(user, "User.currentlyActive", currentlyActiveHandler);
useEventEmitter(user, "User.presence", currentlyActiveHandler);
return isOnline ? <span className="mx_UserOnlineDot" /> : null;
};
UserOnlineDot.propTypes = {
userId: PropTypes.string.isRequired,
};
export default UserOnlineDot;

View file

@ -48,7 +48,6 @@ const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, remo
if (uploadAvatar) {
// insert an empty div to be the host for a css mask containing the upload.svg
uploadAvatarBtn = <AccessibleButton onClick={uploadAvatar} kind="primary">
<div>&nbsp;</div>
{_t("Upload")}
</AccessibleButton>;
}

View file

@ -17,6 +17,7 @@ limitations under the License.
import React from 'react';
import { _t } from '../../../languageHandler';
import SdkConfig from "../../../SdkConfig";
import * as sdk from '../../../index';
import Modal from '../../../Modal';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
@ -121,6 +122,7 @@ export default class EventIndexPanel extends React.Component {
render() {
let eventIndexingSettings = null;
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
const brand = SdkConfig.get().brand;
if (EventIndexPeg.get() !== null) {
eventIndexingSettings = (
@ -165,11 +167,13 @@ export default class EventIndexPanel extends React.Component {
eventIndexingSettings = (
<div className='mx_SettingsTab_subsectionText'>
{
_t( "Riot is missing some components required for securely " +
_t( "%(brand)s is missing some components required for securely " +
"caching encrypted messages locally. If you'd like to " +
"experiment with this feature, build a custom Riot Desktop " +
"experiment with this feature, build a custom %(brand)s Desktop " +
"with <nativeLink>search components added</nativeLink>.",
{},
{
brand,
},
{
'nativeLink': (sub) => <a href={nativeLink} target="_blank"
rel="noreferrer noopener">{sub}</a>,
@ -182,12 +186,14 @@ export default class EventIndexPanel extends React.Component {
eventIndexingSettings = (
<div className='mx_SettingsTab_subsectionText'>
{
_t( "Riot can't securely cache encrypted messages locally " +
"while running in a web browser. Use <riotLink>Riot Desktop</riotLink> " +
_t( "%(brand)s can't securely cache encrypted messages locally " +
"while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> " +
"for encrypted messages to appear in search results.",
{},
{
'riotLink': (sub) => <a href="https://riot.im/download/desktop"
brand,
},
{
'desktopLink': (sub) => <a href="https://riot.im/download/desktop"
target="_blank" rel="noreferrer noopener">{sub}</a>,
},
)

View file

@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
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.
@ -154,7 +155,7 @@ export default createReactClass({
let emailPusherPromise;
if (checked) {
const data = {};
data['brand'] = SdkConfig.get().brand || 'Riot';
data['brand'] = SdkConfig.get().brand;
emailPusherPromise = MatrixClientPeg.get().setPusher({
kind: 'email',
app_id: 'm.email',
@ -841,11 +842,16 @@ export default createReactClass({
let advancedSettings;
if (externalRules.length) {
const brand = SdkConfig.get().brand;
advancedSettings = (
<div>
<h3>{ _t('Advanced notification settings') }</h3>
{ _t('There are advanced notifications which are not shown here') }.<br />
{ _t('You might have configured them in a client other than Riot. You cannot tune them in Riot but they still apply') }.
{ _t('There are advanced notifications which are not shown here.') }<br />
{_t(
'You might have configured them in a client other than %(brand)s. ' +
'You cannot tune them in %(brand)s but they still apply.',
{ brand },
)}
<ul>
{ externalRules }
</ul>

View file

@ -22,7 +22,7 @@ import * as sdk from "../../../../..";
import AccessibleButton from "../../../elements/AccessibleButton";
import Modal from "../../../../../Modal";
import dis from "../../../../../dispatcher/dispatcher";
import RoomListStore from "../../../../../stores/room-list/RoomListStore2";
import RoomListStore from "../../../../../stores/room-list/RoomListStore";
import RoomListActions from "../../../../../actions/RoomListActions";
import { DefaultTagID } from '../../../../../stores/room-list/models';
import LabelledToggleSwitch from '../../../elements/LabelledToggleSwitch';

View file

@ -2,7 +2,6 @@
Copyright 2019 New Vector Ltd
Copyright 2019, 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
@ -18,6 +17,7 @@ limitations under the License.
import React from 'react';
import {_t} from "../../../../../languageHandler";
import SdkConfig from "../../../../../SdkConfig";
import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore";
import { enumerateThemes } from "../../../../../theme";
import ThemeWatcher from "../../../../../settings/watchers/ThemeWatcher";
@ -89,7 +89,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
// We have to mirror the logic from ThemeWatcher.getEffectiveTheme so we
// show the right values for things.
const themeChoice: string = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme");
const themeChoice: string = SettingsStore.getValue("theme");
const systemThemeExplicit: boolean = SettingsStore.getValueAt(
SettingLevel.DEVICE, "use_system_theme", null, false, true);
const themeExplicit: string = SettingsStore.getValueAt(
@ -385,6 +385,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
};
private renderAdvancedSection() {
const brand = SdkConfig.get().brand;
const toggle = <div
className="mx_AppearanceUserSettingsTab_AdvancedToggle"
onClick={() => this.setState({showAdvanced: !this.state.showAdvanced})}
@ -395,6 +396,10 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
let advanced: React.ReactNode;
if (this.state.showAdvanced) {
const tooltipContent = _t(
"Set the name of a font installed on your system & %(brand)s will attempt to use it.",
{ brand },
);
advanced = <>
<SettingsFlag
name="useCompactLayout"
@ -424,7 +429,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
SettingsStore.setValue("systemFont", null, SettingLevel.DEVICE, value.target.value);
}}
tooltipContent="Set the name of a font installed on your system & Riot will attempt to use it."
tooltipContent={tooltipContent}
forceTooltipVisible={true}
disabled={!this.state.useSystemFont}
value={this.state.systemFont}
@ -438,11 +443,13 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
}
render() {
const brand = SdkConfig.get().brand;
return (
<div className="mx_SettingsTab mx_AppearanceUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Customise your appearance")}</div>
<div className="mx_SettingsTab_SubHeading">
{_t("Appearance Settings only affect this Riot session.")}
{_t("Appearance Settings only affect this %(brand)s session.", { brand })}
</div>
{this.renderThemeSection()}
{this.renderFontSection()}

View file

@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector Ltd
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.
@ -36,13 +37,13 @@ export default class HelpUserSettingsTab extends React.Component {
super();
this.state = {
vectorVersion: null,
appVersion: null,
canUpdate: false,
};
}
componentDidMount(): void {
PlatformPeg.get().getAppVersion().then((ver) => this.setState({vectorVersion: ver})).catch((e) => {
PlatformPeg.get().getAppVersion().then((ver) => this.setState({appVersion: ver})).catch((e) => {
console.error("Error getting vector version: ", e);
});
PlatformPeg.get().canSelfUpdate().then((v) => this.setState({canUpdate: v})).catch((e) => {
@ -119,7 +120,7 @@ export default class HelpUserSettingsTab extends React.Component {
<span className='mx_SettingsTab_subheading'>{_t("Credits")}</span>
<ul>
<li>
The <a href="themes/riot/img/backgrounds/valley.jpg" rel="noreferrer noopener" target="_blank">
The <a href="themes/element/img/backgrounds/lake.jpg" rel="noreferrer noopener" target="_blank">
default cover photo</a> is ©&nbsp;
<a href="https://www.flickr.com/golan" rel="noreferrer noopener" target="_blank">Jesús Roncero</a>{' '}
used under the terms of&nbsp;
@ -148,30 +149,52 @@ export default class HelpUserSettingsTab extends React.Component {
}
render() {
let faqText = _t('For help with using Riot, click <a>here</a>.', {}, {
'a': (sub) =>
<a href="https://about.riot.im/need-help/" rel="noreferrer noopener" target="_blank">{sub}</a>,
});
const brand = SdkConfig.get().brand;
let faqText = _t(
'For help with using %(brand)s, click <a>here</a>.',
{
brand,
},
{
'a': (sub) => <a
href="https://element.io/help"
rel="noreferrer noopener"
target="_blank"
>
{sub}
</a>,
},
);
if (SdkConfig.get().welcomeUserId && getCurrentLanguage().startsWith('en')) {
faqText = (
<div>
{
_t('For help with using Riot, click <a>here</a> or start a chat with our ' +
'bot using the button below.', {}, {
'a': (sub) => <a href="https://about.riot.im/need-help/" rel='noreferrer noopener'
target='_blank'>{sub}</a>,
})
}
{_t(
'For help with using %(brand)s, click <a>here</a> or start a chat with our ' +
'bot using the button below.',
{
brand,
},
{
'a': (sub) => <a
href="https://element.io/help"
rel='noreferrer noopener'
target='_blank'
>
{sub}
</a>,
},
)}
<div>
<AccessibleButton onClick={this._onStartBotChat} kind='primary'>
{_t("Chat with Riot Bot")}
{_t("Chat with %(brand)s Bot", { brand })}
</AccessibleButton>
</div>
</div>
);
}
const vectorVersion = this.state.vectorVersion || 'unknown';
const appVersion = this.state.appVersion || 'unknown';
let olmVersion = MatrixClientPeg.get().olmVersion;
olmVersion = olmVersion ? `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}` : '<not-enabled>';
@ -228,7 +251,7 @@ export default class HelpUserSettingsTab extends React.Component {
<div className='mx_SettingsTab_section mx_HelpUserSettingsTab_versions'>
<span className='mx_SettingsTab_subheading'>{_t("Versions")}</span>
<div className='mx_SettingsTab_subsectionText'>
{_t("riot-web version:")} {vectorVersion}<br />
{_t("%(brand)s version:", { brand })} {appVersion}<br />
{_t("olm version:")} {olmVersion}<br />
{updateButton}
</div>

View file

@ -66,6 +66,7 @@ export default class LabsUserSettingsTab extends React.Component {
<SettingsFlag name={"showHiddenEventsInTimeline"} level={SettingLevel.DEVICE} />
<SettingsFlag name={"lowBandwidth"} level={SettingLevel.DEVICE} />
<SettingsFlag name={"sendReadReceipts"} level={SettingLevel.ACCOUNT} />
<SettingsFlag name={"advancedRoomListLogging"} level={SettingLevel.DEVICE} />
</div>
</div>
);

View file

@ -1,5 +1,5 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 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.
@ -16,6 +16,7 @@ limitations under the License.
import React from 'react';
import {_t} from "../../../../../languageHandler";
import SdkConfig from "../../../../../SdkConfig";
import {Mjolnir} from "../../../../../mjolnir/Mjolnir";
import {ListRule} from "../../../../../mjolnir/ListRule";
import {BanList, RULE_SERVER, RULE_USER} from "../../../../../mjolnir/BanList";
@ -234,6 +235,7 @@ export default class MjolnirUserSettingsTab extends React.Component {
render() {
const Field = sdk.getComponent('elements.Field');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const brand = SdkConfig.get().brand;
return (
<div className="mx_SettingsTab mx_MjolnirUserSettingsTab">
@ -244,9 +246,9 @@ export default class MjolnirUserSettingsTab extends React.Component {
<br />
{_t(
"Add users and servers you want to ignore here. Use asterisks " +
"to have Riot match any characters. For example, <code>@bot:*</code> " +
"to have %(brand)s match any characters. For example, <code>@bot:*</code> " +
"would ignore all users that have the name 'bot' on any server.",
{}, {code: (s) => <code>{s}</code>},
{ brand }, {code: (s) => <code>{s}</code>},
)}<br />
<br />
{_t(

View file

@ -23,28 +23,12 @@ import SettingsStore from "../../../../../settings/SettingsStore";
import Field from "../../../elements/Field";
import * as sdk from "../../../../..";
import PlatformPeg from "../../../../../PlatformPeg";
import {RoomListStoreTempProxy} from "../../../../../stores/room-list/RoomListStoreTempProxy";
export default class PreferencesUserSettingsTab extends React.Component {
static ROOM_LIST_SETTINGS = [
'RoomList.orderAlphabetically',
'RoomList.orderByImportance',
'breadcrumbs',
];
// TODO: Remove temp structures: https://github.com/vector-im/riot-web/issues/14367
static ROOM_LIST_2_SETTINGS = [
'breadcrumbs',
];
// TODO: Remove temp structures: https://github.com/vector-im/riot-web/issues/14367
static eligibleRoomListSettings = () => {
if (RoomListStoreTempProxy.isUsingNewStore()) {
return PreferencesUserSettingsTab.ROOM_LIST_2_SETTINGS;
}
return PreferencesUserSettingsTab.ROOM_LIST_SETTINGS;
};
static COMPOSER_SETTINGS = [
'MessageComposerInput.autoReplaceEmoji',
'MessageComposerInput.suggestEmoji',
@ -189,7 +173,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Room list")}</span>
{this._renderGroup(PreferencesUserSettingsTab.eligibleRoomListSettings())}
{this._renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS)}
</div>
<div className="mx_SettingsTab_section">

View file

@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector Ltd
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.
@ -17,6 +18,7 @@ limitations under the License.
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";
@ -281,6 +283,7 @@ export default class SecurityUserSettingsTab extends React.Component {
}
render() {
const brand = SdkConfig.get().brand;
const DevicesPanel = sdk.getComponent('views.settings.DevicesPanel');
const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag');
const EventIndexPanel = sdk.getComponent('views.settings.EventIndexPanel');
@ -355,7 +358,10 @@ export default class SecurityUserSettingsTab extends React.Component {
<div className='mx_SettingsTab_section'>
<span className="mx_SettingsTab_subheading">{_t("Analytics")}</span>
<div className='mx_SettingsTab_subsectionText'>
{_t("Riot collects anonymous analytics to allow us to improve the application.")}
{_t(
"%(brand)s collects anonymous analytics to allow us to improve the application.",
{ brand },
)}
&nbsp;
{_t("Privacy is important to us, so we don't collect any personal or " +
"identifiable data for our analytics.")}

View file

@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector Ltd
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.
@ -16,6 +17,7 @@ limitations under the License.
import React from 'react';
import {_t} from "../../../../../languageHandler";
import SdkConfig from "../../../../../SdkConfig";
import CallMediaHandler from "../../../../../CallMediaHandler";
import Field from "../../../elements/Field";
import AccessibleButton from "../../../elements/AccessibleButton";
@ -80,10 +82,14 @@ export default class VoiceUserSettingsTab extends React.Component {
}
}
if (error) {
const brand = SdkConfig.get().brand;
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
Modal.createTrackedDialog('No media permissions', '', ErrorDialog, {
title: _t('No media permissions'),
description: _t('You may need to manually permit Riot to access your microphone/webcam'),
description: _t(
'You may need to manually permit %(brand)s to access your microphone/webcam',
{ brand },
),
});
} else {
this._refreshMediaDevices(stream);

View file

@ -19,6 +19,7 @@ import PropTypes from "prop-types";
import {_t, pickBestLanguage} from "../../../languageHandler";
import * as sdk from "../../..";
import {objectClone} from "../../../utils/objects";
import StyledCheckbox from "../elements/StyledCheckbox";
export default class InlineTermsAgreement extends React.Component {
static propTypes = {
@ -90,8 +91,9 @@ export default class InlineTermsAgreement extends React.Component {
<div key={i} className='mx_InlineTermsAgreement_cbContainer'>
<div>{introText}</div>
<div className='mx_InlineTermsAgreement_checkbox'>
<input type='checkbox' onChange={() => this._togglePolicy(i)} checked={policy.checked} />
{_t("Accept")}
<StyledCheckbox onChange={() => this._togglePolicy(i)} checked={policy.checked}>
{_t("Accept")}
</StyledCheckbox>
</div>
</div>,
);

View file

@ -15,8 +15,8 @@ limitations under the License.
*/
import React from 'react';
import IncomingCallBox2 from './IncomingCallBox2';
import CallPreview from './CallPreview2';
import IncomingCallBox from './IncomingCallBox';
import CallPreview from './CallPreview';
import * as VectorConferenceHandler from '../../../VectorConferenceHandler';
interface IProps {
@ -30,8 +30,8 @@ interface IState {
export default class CallContainer extends React.PureComponent<IProps, IState> {
public render() {
return <div className="mx_CallContainer">
<IncomingCallBox2 />
<IncomingCallBox />
<CallPreview ConferenceHandler={VectorConferenceHandler} />
</div>;
}
}
}

View file

@ -1,101 +0,0 @@
/*
Copyright 2017, 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 PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import RoomViewStore from '../../../stores/RoomViewStore';
import CallHandler from '../../../CallHandler';
import dis from '../../../dispatcher/dispatcher';
import * as sdk from '../../../index';
export default createReactClass({
displayName: 'CallPreview',
propTypes: {
// A Conference Handler implementation
// Must have a function signature:
// getConferenceCallForRoom(roomId: string): MatrixCall
ConferenceHandler: PropTypes.object,
},
getInitialState: function() {
return {
roomId: RoomViewStore.getRoomId(),
activeCall: CallHandler.getAnyActiveCall(),
};
},
componentDidMount: function() {
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
this.dispatcherRef = dis.register(this._onAction);
},
componentWillUnmount: function() {
if (this._roomStoreToken) {
this._roomStoreToken.remove();
}
dis.unregister(this.dispatcherRef);
},
_onRoomViewStoreUpdate: function(payload) {
if (RoomViewStore.getRoomId() === this.state.roomId) return;
this.setState({
roomId: RoomViewStore.getRoomId(),
});
},
_onAction: function(payload) {
switch (payload.action) {
// listen for call state changes to prod the render method, which
// may hide the global CallView if the call it is tracking is dead
case 'call_state':
this.setState({
activeCall: CallHandler.getAnyActiveCall(),
});
break;
}
},
_onCallViewClick: function() {
const call = CallHandler.getAnyActiveCall();
if (call) {
dis.dispatch({
action: 'view_room',
room_id: call.groupRoomId || call.roomId,
});
}
},
render: function() {
const callForRoom = CallHandler.getCallForRoom(this.state.roomId);
const showCall = (this.state.activeCall && this.state.activeCall.call_state === 'connected' && !callForRoom);
if (showCall) {
const CallView = sdk.getComponent('voip.CallView');
return (
<CallView
className="mx_LeftPanel_callView" showVoice={true} onClick={this._onCallViewClick}
ConferenceHandler={this.props.ConferenceHandler}
/>
);
}
const PersistentApp = sdk.getComponent('elements.PersistentApp');
return <PersistentApp />;
},
});

View file

@ -15,11 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
import React from 'react';
import CallView from "./CallView2";
import CallView from "./CallView";
import RoomViewStore from '../../../stores/RoomViewStore';
import CallHandler from '../../../CallHandler';
import dis from '../../../dispatcher/dispatcher';
@ -37,7 +35,6 @@ interface IProps {
interface IState {
roomId: string;
activeCall: any;
newRoomListActive: boolean;
}
export default class CallPreview extends React.Component<IProps, IState> {
@ -51,12 +48,7 @@ export default class CallPreview extends React.Component<IProps, IState> {
this.state = {
roomId: RoomViewStore.getRoomId(),
activeCall: CallHandler.getAnyActiveCall(),
newRoomListActive: SettingsStore.getValue("feature_new_room_list"),
};
this.settingsWatcherRef = SettingsStore.watchSetting("feature_new_room_list", null, (name, roomId, level, valAtLevel, newVal) => this.setState({
newRoomListActive: newVal,
}));
}
public componentDidMount() {
@ -102,28 +94,25 @@ export default class CallPreview extends React.Component<IProps, IState> {
};
public render() {
if (this.state.newRoomListActive) {
const callForRoom = CallHandler.getCallForRoom(this.state.roomId);
const showCall = (
this.state.activeCall &&
this.state.activeCall.call_state === 'connected' &&
!callForRoom
const callForRoom = CallHandler.getCallForRoom(this.state.roomId);
const showCall = (
this.state.activeCall &&
this.state.activeCall.call_state === 'connected' &&
!callForRoom
);
if (showCall) {
return (
<CallView
className="mx_CallPreview"
onClick={this.onCallViewClick}
ConferenceHandler={this.props.ConferenceHandler}
showHangup={true}
/>
);
if (showCall) {
return (
<CallView
className="mx_CallPreview" onClick={this.onCallViewClick}
ConferenceHandler={this.props.ConferenceHandler}
showHangup={true}
/>
);
}
return <PersistentApp />;
}
return null;
return <PersistentApp />;
}
}

View file

@ -1,167 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket 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, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import dis from '../../../dispatcher/dispatcher';
import CallHandler from '../../../CallHandler';
import * as sdk from '../../../index';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
export default createReactClass({
displayName: 'CallView',
propTypes: {
// js-sdk room object. If set, we will only show calls for the given
// room; if not, we will show any active call.
room: PropTypes.object,
// A Conference Handler implementation
// Must have a function signature:
// getConferenceCallForRoom(roomId: string): MatrixCall
ConferenceHandler: PropTypes.object,
// maxHeight style attribute for the video panel
maxVideoHeight: PropTypes.number,
// a callback which is called when the user clicks on the video div
onClick: PropTypes.func,
// a callback which is called when the content in the callview changes
// in a way that is likely to cause a resize.
onResize: PropTypes.func,
// render ongoing audio call details - useful when in LeftPanel
showVoice: PropTypes.bool,
},
getInitialState: function() {
return {
// the call this view is displaying (if any)
call: null,
};
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._video = createRef();
},
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
this.showCall();
},
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
},
onAction: function(payload) {
// don't filter out payloads for room IDs other than props.room because
// we may be interested in the conf 1:1 room
if (payload.action !== 'call_state') {
return;
}
this.showCall();
},
showCall: function() {
let call;
if (this.props.room) {
const roomId = this.props.room.roomId;
call = CallHandler.getCallForRoom(roomId) ||
(this.props.ConferenceHandler ?
this.props.ConferenceHandler.getConferenceCallForRoom(roomId) :
null
);
if (this.call) {
this.setState({ call: call });
}
} else {
call = CallHandler.getAnyActiveCall();
// Ignore calls if we can't get the room associated with them.
// I think the underlying problem is that the js-sdk sends events
// for calls before it has made the rooms available in the store,
// although this isn't confirmed.
if (MatrixClientPeg.get().getRoom(call.roomId) === null) {
call = null;
}
this.setState({ call: call });
}
if (call) {
call.setLocalVideoElement(this.getVideoView().getLocalVideoElement());
call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement());
// always use a separate element for audio stream playback.
// this is to let us move CallView around the DOM without interrupting remote audio
// during playback, by having the audio rendered by a top-level <audio/> element.
// rather than being rendered by the main remoteVideo <video/> element.
call.setRemoteAudioElement(this.getVideoView().getRemoteAudioElement());
}
if (call && call.type === "video" && call.call_state !== "ended" && call.call_state !== "ringing") {
// if this call is a conf call, don't display local video as the
// conference will have us in it
this.getVideoView().getLocalVideoElement().style.display = (
call.confUserId ? "none" : "block"
);
this.getVideoView().getRemoteVideoElement().style.display = "block";
} else {
this.getVideoView().getLocalVideoElement().style.display = "none";
this.getVideoView().getRemoteVideoElement().style.display = "none";
dis.dispatch({action: 'video_fullscreen', fullscreen: false});
}
if (this.props.onResize) {
this.props.onResize();
}
},
getVideoView: function() {
return this._video.current;
},
render: function() {
const VideoView = sdk.getComponent('voip.VideoView');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let voice;
if (this.state.call && this.state.call.type === "voice" && this.props.showVoice) {
const callRoom = MatrixClientPeg.get().getRoom(this.state.call.roomId);
voice = (
<AccessibleButton className="mx_CallView_voice" onClick={this.props.onClick}>
{ _t("Active call (%(roomName)s)", {roomName: callRoom.name}) }
</AccessibleButton>
);
}
return (
<div>
<VideoView
ref={this._video}
onClick={this.props.onClick}
onResize={this.props.onResize}
maxHeight={this.props.maxVideoHeight}
/>
{ voice }
</div>
);
},
});

View file

@ -15,8 +15,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
import React, {createRef} from 'react';
import Room from 'matrix-js-sdk/src/models/room';
import dis from '../../../dispatcher/dispatcher';
@ -156,7 +154,7 @@ export default class CallView extends React.Component<IProps, IState> {
const client = MatrixClientPeg.get();
const callRoom = client.getRoom(this.state.call.roomId);
view = <AccessibleButton className="mx_CallView2_voice" onClick={this.props.onClick}>
view = <AccessibleButton className="mx_CallView_voice" onClick={this.props.onClick}>
<PulsedAvatar>
<RoomAvatar
room={callRoom}
@ -181,7 +179,7 @@ export default class CallView extends React.Component<IProps, IState> {
let hangup: React.ReactNode;
if (this.props.showHangup) {
hangup = <div
className="mx_CallView2_hangup"
className="mx_CallView_hangup"
onClick={() => {
dis.dispatch({
action: 'hangup',

View file

@ -1,91 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket 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 PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
export default createReactClass({
displayName: 'IncomingCallBox',
propTypes: {
incomingCall: PropTypes.object,
},
onAnswerClick: function(e) {
e.stopPropagation();
dis.dispatch({
action: 'answer',
room_id: this.props.incomingCall.roomId,
});
},
onRejectClick: function(e) {
e.stopPropagation();
dis.dispatch({
action: 'hangup',
room_id: this.props.incomingCall.roomId,
});
},
render: function() {
let room = null;
if (this.props.incomingCall) {
room = MatrixClientPeg.get().getRoom(this.props.incomingCall.roomId);
}
const caller = room ? room.name : _t("unknown caller");
let incomingCallText = null;
if (this.props.incomingCall) {
if (this.props.incomingCall.type === "voice") {
incomingCallText = _t("Incoming voice call from %(name)s", {name: caller});
} else if (this.props.incomingCall.type === "video") {
incomingCallText = _t("Incoming video call from %(name)s", {name: caller});
} else {
incomingCallText = _t("Incoming call from %(name)s", {name: caller});
}
}
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (
<div className="mx_IncomingCallBox" id="incomingCallBox">
<img className="mx_IncomingCallBox_chevron" src={require("../../../../res/img/chevron-left.png")} width="9" height="16" />
<div className="mx_IncomingCallBox_title">
{ incomingCallText }
</div>
<div className="mx_IncomingCallBox_buttons">
<div className="mx_IncomingCallBox_buttons_cell">
<AccessibleButton className="mx_IncomingCallBox_buttons_decline" onClick={this.onRejectClick}>
{ _t("Decline") }
</AccessibleButton>
</div>
<div className="mx_IncomingCallBox_buttons_cell">
<AccessibleButton className="mx_IncomingCallBox_buttons_accept" onClick={this.onAnswerClick}>
{ _t("Accept") }
</AccessibleButton>
</div>
</div>
</div>
);
},
});

View file

@ -16,8 +16,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
import React from 'react';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher';
@ -35,7 +33,7 @@ interface IState {
incomingCall: any;
}
export default class IncomingCallBox2 extends React.Component<IProps, IState> {
export default class IncomingCallBox extends React.Component<IProps, IState> {
private dispatcherRef: string;
constructor(props: IProps) {
@ -106,8 +104,8 @@ export default class IncomingCallBox2 extends React.Component<IProps, IState> {
}
}
return <div className="mx_IncomingCallBox2">
<div className="mx_IncomingCallBox2_CallerInfo">
return <div className="mx_IncomingCallBox">
<div className="mx_IncomingCallBox_CallerInfo">
<PulsedAvatar>
<RoomAvatar
room={room}
@ -120,16 +118,16 @@ export default class IncomingCallBox2 extends React.Component<IProps, IState> {
<p>{incomingCallText}</p>
</div>
</div>
<div className="mx_IncomingCallBox2_buttons">
<div className="mx_IncomingCallBox_buttons">
<FormButton
className={"mx_IncomingCallBox2_decline"}
className={"mx_IncomingCallBox_decline"}
onClick={this.onRejectClick}
kind="danger"
label={_t("Decline")}
/>
<div className="mx_IncomingCallBox2_spacer" />
<div className="mx_IncomingCallBox_spacer" />
<FormButton
className={"mx_IncomingCallBox2_accept"}
className={"mx_IncomingCallBox_accept"}
onClick={this.onAnswerClick}
kind="primary"
label={_t("Accept")}