Merge branch 'develop' of https://github.com/matrix-org/matrix-react-sdk into travis/download-logs
This commit is contained in:
commit
8cff59f123
71 changed files with 2869 additions and 951 deletions
|
@ -1003,9 +1003,10 @@ export default createReactClass({
|
|||
this.state.inviterProfile.avatarUrl, 36, 36,
|
||||
) : null;
|
||||
|
||||
let inviterName = group.inviter.userId;
|
||||
const inviter = group.inviter || {};
|
||||
let inviterName = inviter.userId;
|
||||
if (this.state.inviterProfile) {
|
||||
inviterName = this.state.inviterProfile.displayName || group.inviter.userId;
|
||||
inviterName = this.state.inviterProfile.displayName || inviter.userId;
|
||||
}
|
||||
return <div className="mx_GroupView_membershipSection mx_GroupView_membershipSection_invited">
|
||||
<div className="mx_GroupView_membershipSubSection">
|
||||
|
@ -1016,7 +1017,7 @@ export default createReactClass({
|
|||
height={36}
|
||||
/>
|
||||
{ _t("%(inviter)s has invited you to join this community", {
|
||||
inviter: inviterName,
|
||||
inviter: inviterName || _t("Someone"),
|
||||
}) }
|
||||
</div>
|
||||
<div className="mx_GroupView_membership_buttonContainer">
|
||||
|
|
|
@ -37,6 +37,7 @@ import IndicatorScrollbar from "../structures/IndicatorScrollbar";
|
|||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||
import { OwnProfileStore } from "../../stores/OwnProfileStore";
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import RoomListNumResults from "../views/rooms/RoomListNumResults";
|
||||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
|
@ -409,6 +410,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
{this.renderHeader()}
|
||||
{this.renderSearchExplore()}
|
||||
{this.renderBreadcrumbs()}
|
||||
<RoomListNumResults />
|
||||
<div className="mx_LeftPanel_roomListWrapper">
|
||||
<div
|
||||
className={roomListClasses}
|
||||
|
|
|
@ -126,7 +126,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
public render(): React.ReactNode {
|
||||
const classes = classNames({
|
||||
'mx_RoomSearch': true,
|
||||
'mx_RoomSearch_expanded': this.state.query || this.state.focused,
|
||||
'mx_RoomSearch_hasQuery': this.state.query,
|
||||
'mx_RoomSearch_focused': this.state.focused,
|
||||
'mx_RoomSearch_minimized': this.props.isMinimized,
|
||||
});
|
||||
|
||||
|
|
|
@ -1913,6 +1913,7 @@ export default createReactClass({
|
|||
disabled={this.props.disabled}
|
||||
showApps={this.state.showApps}
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)}
|
||||
/>;
|
||||
}
|
||||
|
|
|
@ -14,15 +14,22 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import { TagID } from '../../../stores/room-list/models';
|
||||
import RoomAvatar from "./RoomAvatar";
|
||||
import RoomTileIcon from "../rooms/RoomTileIcon";
|
||||
import NotificationBadge from '../rooms/NotificationBadge';
|
||||
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||
import { NotificationState } from "../../../stores/notifications/NotificationState";
|
||||
import {isPresenceEnabled} from "../../../utils/presence";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import TextWithTooltip from "../elements/TextWithTooltip";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -36,18 +43,134 @@ interface IProps {
|
|||
|
||||
interface IState {
|
||||
notificationState?: NotificationState;
|
||||
icon: Icon;
|
||||
}
|
||||
|
||||
enum Icon {
|
||||
// Note: the names here are used in CSS class names
|
||||
None = "NONE", // ... except this one
|
||||
Globe = "GLOBE",
|
||||
PresenceOnline = "ONLINE",
|
||||
PresenceAway = "AWAY",
|
||||
PresenceOffline = "OFFLINE",
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
export default class DecoratedRoomAvatar extends React.PureComponent<IProps, IState> {
|
||||
private _dmUser: User;
|
||||
private isUnmounted = false;
|
||||
private isWatchingTimeline = false;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
notificationState: RoomNotificationStateStore.instance.getRoomState(this.props.room),
|
||||
icon: this.calculateIcon(),
|
||||
};
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.isUnmounted = true;
|
||||
if (this.isWatchingTimeline) this.props.room.off('Room.timeline', this.onRoomTimeline);
|
||||
this.dmUser = null; // clear listeners, if any
|
||||
}
|
||||
|
||||
private get isPublicRoom(): boolean {
|
||||
const joinRules = this.props.room.currentState.getStateEvents("m.room.join_rules", "");
|
||||
const joinRule = joinRules && joinRules.getContent().join_rule;
|
||||
return joinRule === 'public';
|
||||
}
|
||||
|
||||
private get dmUser(): User {
|
||||
return this._dmUser;
|
||||
}
|
||||
|
||||
private set dmUser(val: User) {
|
||||
const oldUser = this._dmUser;
|
||||
this._dmUser = val;
|
||||
if (oldUser && oldUser !== this._dmUser) {
|
||||
oldUser.off('User.currentlyActive', this.onPresenceUpdate);
|
||||
oldUser.off('User.presence', this.onPresenceUpdate);
|
||||
}
|
||||
if (this._dmUser && oldUser !== this._dmUser) {
|
||||
this._dmUser.on('User.currentlyActive', this.onPresenceUpdate);
|
||||
this._dmUser.on('User.presence', this.onPresenceUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
private onRoomTimeline = (ev: MatrixEvent, room: Room) => {
|
||||
if (this.isUnmounted) return;
|
||||
|
||||
// apparently these can happen?
|
||||
if (!room) return;
|
||||
if (this.props.room.roomId !== room.roomId) return;
|
||||
|
||||
if (ev.getType() === 'm.room.join_rules' || ev.getType() === 'm.room.member') {
|
||||
this.setState({icon: this.calculateIcon()});
|
||||
}
|
||||
};
|
||||
|
||||
private onPresenceUpdate = () => {
|
||||
if (this.isUnmounted) return;
|
||||
|
||||
let newIcon = this.getPresenceIcon();
|
||||
if (newIcon !== this.state.icon) this.setState({icon: newIcon});
|
||||
};
|
||||
|
||||
private getPresenceIcon(): Icon {
|
||||
if (!this.dmUser) return Icon.None;
|
||||
|
||||
let icon = Icon.None;
|
||||
|
||||
const isOnline = this.dmUser.currentlyActive || this.dmUser.presence === 'online';
|
||||
if (isOnline) {
|
||||
icon = Icon.PresenceOnline;
|
||||
} else if (this.dmUser.presence === 'offline') {
|
||||
icon = Icon.PresenceOffline;
|
||||
} else if (this.dmUser.presence === 'unavailable') {
|
||||
icon = Icon.PresenceAway;
|
||||
}
|
||||
|
||||
return icon;
|
||||
}
|
||||
|
||||
private calculateIcon(): Icon {
|
||||
let icon = Icon.None;
|
||||
|
||||
// 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()) {
|
||||
if (otherUserId) {
|
||||
this.dmUser = MatrixClientPeg.get().getUser(otherUserId);
|
||||
icon = this.getPresenceIcon();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Track publicity
|
||||
icon = this.isPublicRoom ? Icon.Globe : Icon.None;
|
||||
if (!this.isWatchingTimeline) {
|
||||
this.props.room.on('Room.timeline', this.onRoomTimeline);
|
||||
this.isWatchingTimeline = true;
|
||||
}
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
let badge: React.ReactNode;
|
||||
if (this.props.displayBadge) {
|
||||
|
@ -58,7 +181,19 @@ export default class DecoratedRoomAvatar extends React.PureComponent<IProps, ISt
|
|||
/>;
|
||||
}
|
||||
|
||||
return <div className="mx_DecoratedRoomAvatar">
|
||||
let icon;
|
||||
if (this.state.icon !== Icon.None) {
|
||||
icon = <TextWithTooltip
|
||||
tooltip={tooltipText(this.state.icon)}
|
||||
class={`mx_DecoratedRoomAvatar_icon mx_DecoratedRoomAvatar_icon_${this.state.icon.toLowerCase()}`}
|
||||
/>;
|
||||
}
|
||||
|
||||
const classes = classNames("mx_DecoratedRoomAvatar", {
|
||||
mx_DecoratedRoomAvatar_cutout: icon,
|
||||
});
|
||||
|
||||
return <div className={classes}>
|
||||
<RoomAvatar
|
||||
room={this.props.room}
|
||||
width={this.props.avatarSize}
|
||||
|
@ -66,7 +201,7 @@ export default class DecoratedRoomAvatar extends React.PureComponent<IProps, ISt
|
|||
oobData={this.props.oobData}
|
||||
viewAvatarOnClick={this.props.viewAvatarOnClick}
|
||||
/>
|
||||
<RoomTileIcon room={this.props.room} />
|
||||
{icon}
|
||||
{badge}
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
|
|||
title: string;
|
||||
tooltip?: React.ReactNode;
|
||||
tooltipClassName?: string;
|
||||
forceHide?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -39,7 +40,16 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
|
|||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<ITooltipProps>) {
|
||||
if (!prevProps.forceHide && this.props.forceHide && this.state.hover) {
|
||||
this.setState({
|
||||
hover: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onMouseOver = () => {
|
||||
if (this.props.forceHide) return;
|
||||
this.setState({
|
||||
hover: true,
|
||||
});
|
||||
|
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
|||
|
||||
import React, {useEffect} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { EventStatus } from 'matrix-js-sdk';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
|
@ -114,13 +115,19 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
static contextType = RoomContext;
|
||||
|
||||
componentDidMount() {
|
||||
this.props.mxEvent.on("Event.decrypted", this.onDecrypted);
|
||||
if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) {
|
||||
this.props.mxEvent.on("Event.status", this.onSent);
|
||||
}
|
||||
if (this.props.mxEvent.isBeingDecrypted()) {
|
||||
this.props.mxEvent.once("Event.decrypted", this.onDecrypted);
|
||||
}
|
||||
this.props.mxEvent.on("Event.beforeRedaction", this.onBeforeRedaction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.mxEvent.removeListener("Event.decrypted", this.onDecrypted);
|
||||
this.props.mxEvent.removeListener("Event.beforeRedaction", this.onBeforeRedaction);
|
||||
this.props.mxEvent.off("Event.status", this.onSent);
|
||||
this.props.mxEvent.off("Event.decrypted", this.onDecrypted);
|
||||
this.props.mxEvent.off("Event.beforeRedaction", this.onBeforeRedaction);
|
||||
}
|
||||
|
||||
onDecrypted = () => {
|
||||
|
@ -134,6 +141,11 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
this.forceUpdate();
|
||||
};
|
||||
|
||||
onSent = () => {
|
||||
// When an event is sent and echoed the possible actions change.
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
onFocusChange = (focused) => {
|
||||
if (!this.props.onFocusChange) {
|
||||
return;
|
||||
|
|
|
@ -30,6 +30,8 @@ import E2EIcon from './E2EIcon';
|
|||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu} from "../../structures/ContextMenu";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import ReplyPreview from "./ReplyPreview";
|
||||
|
||||
function ComposerAvatar(props) {
|
||||
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
|
||||
return <div className="mx_MessageComposer_avatar">
|
||||
|
@ -223,7 +225,7 @@ export default class MessageComposer extends React.Component {
|
|||
this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this);
|
||||
this._onTombstoneClick = this._onTombstoneClick.bind(this);
|
||||
this.renderPlaceholderText = this.renderPlaceholderText.bind(this);
|
||||
|
||||
this._dispatcherRef = null;
|
||||
this.state = {
|
||||
isQuoting: Boolean(RoomViewStore.getQuotingEvent()),
|
||||
tombstone: this._getRoomTombstone(),
|
||||
|
@ -232,7 +234,20 @@ export default class MessageComposer extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
onAction = (payload) => {
|
||||
if (payload.action === 'reply_to_event') {
|
||||
// add a timeout for the reply preview to be rendered, so
|
||||
// that the ScrollPanel listening to the resizeNotifier can
|
||||
// correctly measure it's new height and scroll down to keep
|
||||
// at the bottom if it already is
|
||||
setTimeout(() => {
|
||||
this.props.resizeNotifier.notifyTimelineHeightChanged();
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
|
||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||
this._waitForOwnMember();
|
||||
|
@ -261,6 +276,7 @@ export default class MessageComposer extends React.Component {
|
|||
if (this._roomStoreToken) {
|
||||
this._roomStoreToken.remove();
|
||||
}
|
||||
dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
_onRoomStateEvents(ev, state) {
|
||||
|
@ -364,6 +380,7 @@ export default class MessageComposer extends React.Component {
|
|||
key="controls_input"
|
||||
room={this.props.room}
|
||||
placeholder={this.renderPlaceholderText()}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
permalinkCreator={this.props.permalinkCreator} />,
|
||||
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
|
||||
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
|
||||
|
@ -414,6 +431,7 @@ export default class MessageComposer extends React.Component {
|
|||
return (
|
||||
<div className="mx_MessageComposer mx_GroupLayout">
|
||||
<div className="mx_MessageComposer_wrapper">
|
||||
<ReplyPreview permalinkCreator={this.props.permalinkCreator} />
|
||||
<div className="mx_MessageComposer_row">
|
||||
{ controls }
|
||||
</div>
|
||||
|
|
|
@ -43,6 +43,8 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import CustomRoomTagStore from "../../../stores/CustomRoomTagStore";
|
||||
import { arrayFastClone, arrayHasDiff } from "../../../utils/arrays";
|
||||
import { objectShallowClone, objectWithOnly } from "../../../utils/objects";
|
||||
import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
|
||||
interface IProps {
|
||||
onKeyDown: (ev: React.KeyboardEvent) => void;
|
||||
|
@ -81,6 +83,7 @@ interface ITagAesthetics {
|
|||
sectionLabelRaw?: string;
|
||||
addRoomLabel?: string;
|
||||
onAddRoom?: (dispatcher?: Dispatcher<ActionPayload>) => void;
|
||||
addRoomContextMenu?: (onFinished: () => void) => React.ReactNode;
|
||||
isInvite: boolean;
|
||||
defaultHidden: boolean;
|
||||
}
|
||||
|
@ -112,9 +115,30 @@ const TAG_AESTHETICS: {
|
|||
sectionLabel: _td("Rooms"),
|
||||
isInvite: false,
|
||||
defaultHidden: false,
|
||||
addRoomLabel: _td("Create room"),
|
||||
onAddRoom: (dispatcher?: Dispatcher<ActionPayload>) => {
|
||||
(dispatcher || defaultDispatcher).dispatch({action: 'view_create_room'})
|
||||
addRoomLabel: _td("Add room"),
|
||||
addRoomContextMenu: (onFinished: () => void) => {
|
||||
return <IconizedContextMenuOptionList first>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Create new room")}
|
||||
iconClassName="mx_RoomList_iconPlus"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onFinished();
|
||||
defaultDispatcher.dispatch({action: "view_create_room"});
|
||||
}}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Explore public rooms")}
|
||||
iconClassName="mx_RoomList_iconExplore"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onFinished();
|
||||
defaultDispatcher.fire(Action.ViewRoomDirectory);
|
||||
}}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>;
|
||||
},
|
||||
},
|
||||
[DefaultTagID.LowPriority]: {
|
||||
|
@ -255,6 +279,10 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private onExplore = () => {
|
||||
dis.fire(Action.ViewRoomDirectory);
|
||||
};
|
||||
|
||||
private renderCommunityInvites(): TemporaryTile[] {
|
||||
// TODO: Put community invites in a more sensible place (not in the room list)
|
||||
// See https://github.com/vector-im/element-web/issues/14456
|
||||
|
@ -324,6 +352,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
label={aesthetics.sectionLabelRaw ? aesthetics.sectionLabelRaw : _t(aesthetics.sectionLabel)}
|
||||
onAddRoom={aesthetics.onAddRoom}
|
||||
addRoomLabel={aesthetics.addRoomLabel ? _t(aesthetics.addRoomLabel) : aesthetics.addRoomLabel}
|
||||
addRoomContextMenu={aesthetics.addRoomContextMenu}
|
||||
isMinimized={this.props.isMinimized}
|
||||
onResize={this.props.onResize}
|
||||
extraBadTilesThatShouldntExist={extraTiles}
|
||||
|
@ -335,6 +364,16 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
let explorePrompt: JSX.Element;
|
||||
if (RoomListStore.instance.getFirstNameFilterCondition()) {
|
||||
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
||||
<div>{_t("Can't see what you’re looking for?")}</div>
|
||||
<AccessibleButton kind="link" onClick={this.onExplore}>
|
||||
{_t("Explore all public rooms")}
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
}
|
||||
|
||||
const sublists = this.renderSublists();
|
||||
return (
|
||||
<RovingTabIndexProvider handleHomeEnd={true} onKeyDown={this.props.onKeyDown}>
|
||||
|
@ -346,7 +385,10 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
className="mx_RoomList"
|
||||
role="tree"
|
||||
aria-label={_t("Rooms")}
|
||||
>{sublists}</div>
|
||||
>
|
||||
{sublists}
|
||||
{explorePrompt}
|
||||
</div>
|
||||
)}
|
||||
</RovingTabIndexProvider>
|
||||
);
|
||||
|
|
41
src/components/views/rooms/RoomListNumResults.tsx
Normal file
41
src/components/views/rooms/RoomListNumResults.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
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, {useState} from "react";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
|
||||
const RoomListNumResults: React.FC = () => {
|
||||
const [count, setCount] = useState<number>(null);
|
||||
useEventEmitter(RoomListStore.instance, LISTS_UPDATE_EVENT, () => {
|
||||
if (RoomListStore.instance.getFirstNameFilterCondition()) {
|
||||
const numRooms = Object.values(RoomListStore.instance.orderedLists).flat(1).length;
|
||||
setCount(numRooms);
|
||||
} else {
|
||||
setCount(null);
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof count !== "number") return null;
|
||||
|
||||
return <div className="mx_LeftPanel_roomListFilterCount">
|
||||
{_t("%(count)s results", { count })}
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default RoomListNumResults;
|
|
@ -50,6 +50,7 @@ import { arrayFastClone, arrayHasOrderChange } from "../../../utils/arrays";
|
|||
import { objectExcluding, objectHasDiff } from "../../../utils/objects";
|
||||
import TemporaryTile from "./TemporaryTile";
|
||||
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
|
||||
import IconizedContextMenu from "../context_menus/IconizedContextMenu";
|
||||
|
||||
const SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS
|
||||
const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS
|
||||
|
@ -65,6 +66,7 @@ interface IProps {
|
|||
startAsHidden: boolean;
|
||||
label: string;
|
||||
onAddRoom?: () => void;
|
||||
addRoomContextMenu?: (onFinished: () => void) => React.ReactNode;
|
||||
addRoomLabel: string;
|
||||
isMinimized: boolean;
|
||||
tagId: TagID;
|
||||
|
@ -87,6 +89,7 @@ type PartialDOMRect = Pick<DOMRect, "left" | "top" | "height">;
|
|||
|
||||
interface IState {
|
||||
contextMenuPosition: PartialDOMRect;
|
||||
addRoomContextMenuPosition: PartialDOMRect;
|
||||
isResizing: boolean;
|
||||
isExpanded: boolean; // used for the for expand of the sublist when the room list is being filtered
|
||||
height: number;
|
||||
|
@ -112,6 +115,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
this.notificationState = RoomNotificationStateStore.instance.getListState(this.props.tagId);
|
||||
this.state = {
|
||||
contextMenuPosition: null,
|
||||
addRoomContextMenuPosition: null,
|
||||
isResizing: false,
|
||||
isExpanded: this.isBeingFiltered ? this.isBeingFiltered : !this.layout.isCollapsed,
|
||||
height: 0, // to be fixed in a moment, we need `rooms` to calculate this.
|
||||
|
@ -376,10 +380,21 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
private onAddRoomContextMenu = (ev: React.MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const target = ev.target as HTMLButtonElement;
|
||||
this.setState({addRoomContextMenuPosition: target.getBoundingClientRect()});
|
||||
};
|
||||
|
||||
private onCloseMenu = () => {
|
||||
this.setState({contextMenuPosition: null});
|
||||
};
|
||||
|
||||
private onCloseAddRoomMenu = () => {
|
||||
this.setState({addRoomContextMenuPosition: null});
|
||||
};
|
||||
|
||||
private onUnreadFirstChanged = async () => {
|
||||
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
|
||||
const newAlgorithm = isUnreadFirst ? ListAlgorithm.Natural : ListAlgorithm.Importance;
|
||||
|
@ -594,6 +609,18 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
</div>
|
||||
</ContextMenu>
|
||||
);
|
||||
} else if (this.state.addRoomContextMenuPosition) {
|
||||
contextMenu = (
|
||||
<IconizedContextMenu
|
||||
chevronFace={ChevronFace.None}
|
||||
left={this.state.addRoomContextMenuPosition.left - 7} // center align with the handle
|
||||
top={this.state.addRoomContextMenuPosition.top + this.state.addRoomContextMenuPosition.height}
|
||||
onFinished={this.onCloseAddRoomMenu}
|
||||
compact
|
||||
>
|
||||
{this.props.addRoomContextMenu(this.onCloseAddRoomMenu)}
|
||||
</IconizedContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -637,9 +664,21 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
tabIndex={tabIndex}
|
||||
onClick={this.onAddRoom}
|
||||
className="mx_RoomSublist_auxButton"
|
||||
tooltipClassName="mx_RoomSublist_addRoomTooltip"
|
||||
aria-label={this.props.addRoomLabel || _t("Add room")}
|
||||
title={this.props.addRoomLabel}
|
||||
tooltipClassName={"mx_RoomSublist_addRoomTooltip"}
|
||||
/>
|
||||
);
|
||||
} else if (this.props.addRoomContextMenu) {
|
||||
addRoomButton = (
|
||||
<ContextMenuTooltipButton
|
||||
tabIndex={tabIndex}
|
||||
onClick={this.onAddRoomContextMenu}
|
||||
className="mx_RoomSublist_auxButton"
|
||||
tooltipClassName="mx_RoomSublist_addRoomTooltip"
|
||||
aria-label={this.props.addRoomLabel || _t("Add room")}
|
||||
title={this.props.addRoomLabel}
|
||||
isExpanded={!!this.state.addRoomContextMenuPosition}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -113,7 +113,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
};
|
||||
|
||||
private get showContextMenu(): boolean {
|
||||
return !this.props.isMinimized && this.props.tag !== DefaultTagID.Invite;
|
||||
return this.props.tag !== DefaultTagID.Invite;
|
||||
}
|
||||
|
||||
private get showMessagePreview(): boolean {
|
||||
|
@ -304,7 +304,9 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
private onClickMute = ev => this.saveNotifState(ev, MUTE);
|
||||
|
||||
private renderNotificationsMenu(isActive: boolean): React.ReactElement {
|
||||
if (MatrixClientPeg.get().isGuest() || this.props.tag === DefaultTagID.Archived || !this.showContextMenu) {
|
||||
if (MatrixClientPeg.get().isGuest() || this.props.tag === DefaultTagID.Archived ||
|
||||
!this.showContextMenu || this.props.isMinimized
|
||||
) {
|
||||
// the menu makes no sense in these cases so do not show one
|
||||
return null;
|
||||
}
|
||||
|
@ -530,9 +532,13 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
ariaDescribedBy = messagePreviewId(this.props.room.roomId);
|
||||
}
|
||||
|
||||
const props: Partial<React.ComponentProps<typeof AccessibleTooltipButton>> = {};
|
||||
let Button: React.ComponentType<React.ComponentProps<typeof AccessibleButton>> = AccessibleButton;
|
||||
if (this.props.isMinimized) {
|
||||
Button = AccessibleTooltipButton;
|
||||
props.title = name;
|
||||
// force the tooltip to hide whilst we are showing the context menu
|
||||
props.forceHide = !!this.state.generalMenuPosition;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -540,6 +546,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
<RovingTabIndexWrapper inputRef={this.roomTileRef}>
|
||||
{({onFocus, isActive, ref}) =>
|
||||
<Button
|
||||
{...props}
|
||||
onFocus={onFocus}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
inputRef={ref}
|
||||
|
@ -550,7 +557,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
aria-label={ariaLabel}
|
||||
aria-selected={this.state.selected}
|
||||
aria-describedby={ariaDescribedBy}
|
||||
title={this.props.isMinimized ? name : undefined}
|
||||
>
|
||||
{roomAvatar}
|
||||
{nameContainer}
|
||||
|
|
|
@ -1,168 +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 { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { isPresenceEnabled } from "../../../utils/presence";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import TextWithTooltip from "../elements/TextWithTooltip";
|
||||
|
||||
enum Icon {
|
||||
// Note: the names here are used in CSS class names
|
||||
None = "NONE", // ... except this one
|
||||
Globe = "GLOBE",
|
||||
PresenceOnline = "ONLINE",
|
||||
PresenceAway = "AWAY",
|
||||
PresenceOffline = "OFFLINE",
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
icon: Icon;
|
||||
}
|
||||
|
||||
export default class RoomTileIcon extends React.Component<IProps, IState> {
|
||||
private _dmUser: User;
|
||||
private isUnmounted = false;
|
||||
private isWatchingTimeline = false;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
icon: this.calculateIcon(),
|
||||
};
|
||||
}
|
||||
|
||||
private get isPublicRoom(): boolean {
|
||||
const joinRules = this.props.room.currentState.getStateEvents("m.room.join_rules", "");
|
||||
const joinRule = joinRules && joinRules.getContent().join_rule;
|
||||
return joinRule === 'public';
|
||||
}
|
||||
|
||||
private get dmUser(): User {
|
||||
return this._dmUser;
|
||||
}
|
||||
|
||||
private set dmUser(val: User) {
|
||||
const oldUser = this._dmUser;
|
||||
this._dmUser = val;
|
||||
if (oldUser && oldUser !== this._dmUser) {
|
||||
oldUser.off('User.currentlyActive', this.onPresenceUpdate);
|
||||
oldUser.off('User.presence', this.onPresenceUpdate);
|
||||
}
|
||||
if (this._dmUser && oldUser !== this._dmUser) {
|
||||
this._dmUser.on('User.currentlyActive', this.onPresenceUpdate);
|
||||
this._dmUser.on('User.presence', this.onPresenceUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.isUnmounted = true;
|
||||
if (this.isWatchingTimeline) this.props.room.off('Room.timeline', this.onRoomTimeline);
|
||||
this.dmUser = null; // clear listeners, if any
|
||||
}
|
||||
|
||||
private onRoomTimeline = (ev: MatrixEvent, room: Room) => {
|
||||
if (this.isUnmounted) return;
|
||||
|
||||
// apparently these can happen?
|
||||
if (!room) return;
|
||||
if (this.props.room.roomId !== room.roomId) return;
|
||||
|
||||
if (ev.getType() === 'm.room.join_rules' || ev.getType() === 'm.room.member') {
|
||||
this.setState({icon: this.calculateIcon()});
|
||||
}
|
||||
};
|
||||
|
||||
private onPresenceUpdate = () => {
|
||||
if (this.isUnmounted) return;
|
||||
|
||||
let newIcon = this.getPresenceIcon();
|
||||
if (newIcon !== this.state.icon) this.setState({icon: newIcon});
|
||||
};
|
||||
|
||||
private getPresenceIcon(): Icon {
|
||||
if (!this.dmUser) return Icon.None;
|
||||
|
||||
let icon = Icon.None;
|
||||
|
||||
const isOnline = this.dmUser.currentlyActive || this.dmUser.presence === 'online';
|
||||
if (isOnline) {
|
||||
icon = Icon.PresenceOnline;
|
||||
} else if (this.dmUser.presence === 'offline') {
|
||||
icon = Icon.PresenceOffline;
|
||||
} else if (this.dmUser.presence === 'unavailable') {
|
||||
icon = Icon.PresenceAway;
|
||||
}
|
||||
|
||||
return icon;
|
||||
}
|
||||
|
||||
private calculateIcon(): Icon {
|
||||
let icon = Icon.None;
|
||||
|
||||
// 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()) {
|
||||
if (otherUserId) {
|
||||
this.dmUser = MatrixClientPeg.get().getUser(otherUserId);
|
||||
icon = this.getPresenceIcon();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Track publicity
|
||||
icon = this.isPublicRoom ? Icon.Globe : Icon.None;
|
||||
if (!this.isWatchingTimeline) {
|
||||
this.props.room.on('Room.timeline', this.onRoomTimeline);
|
||||
this.isWatchingTimeline = true;
|
||||
}
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
|
||||
public render(): React.ReactElement {
|
||||
if (this.state.icon === Icon.None) return null;
|
||||
|
||||
return <TextWithTooltip
|
||||
tooltip={tooltipText(this.state.icon)}
|
||||
class={`mx_RoomTileIcon mx_RoomTileIcon_${this.state.icon.toLowerCase()}`}
|
||||
/>;
|
||||
}
|
||||
}
|
|
@ -29,7 +29,6 @@ import {
|
|||
} from '../../../editor/serialize';
|
||||
import {CommandPartCreator} from '../../../editor/parts';
|
||||
import BasicMessageComposer from "./BasicMessageComposer";
|
||||
import ReplyPreview from "./ReplyPreview";
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import ReplyThread from "../elements/ReplyThread";
|
||||
import {parseEvent} from '../../../editor/deserialize';
|
||||
|
@ -444,9 +443,6 @@ export default class SendMessageComposer extends React.Component {
|
|||
render() {
|
||||
return (
|
||||
<div className="mx_SendMessageComposer" onClick={this.focusComposer} onKeyDown={this._onKeyDown}>
|
||||
<div className="mx_SendMessageComposer_overlayWrapper">
|
||||
<ReplyPreview permalinkCreator={this.props.permalinkCreator} />
|
||||
</div>
|
||||
<BasicMessageComposer
|
||||
ref={this._setEditorRef}
|
||||
model={this.model}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue