Remove core structures for the old room list

This commit is contained in:
Travis Ralston 2020-07-17 14:09:05 -06:00
parent 06336a88b3
commit 3c047cecfd
22 changed files with 9 additions and 3961 deletions

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

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

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

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

@ -1,838 +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 '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!
}
},
_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

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

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