Merge branch 'develop' into joriks/field-ts
This commit is contained in:
commit
b4624baf2f
40 changed files with 2537 additions and 269 deletions
|
@ -26,6 +26,7 @@ import * as VectorConferenceHandler from '../../VectorConferenceHandler';
|
|||
import SettingsStore from '../../settings/SettingsStore';
|
||||
import {_t} from "../../languageHandler";
|
||||
import Analytics from "../../Analytics";
|
||||
import RoomList2 from "../views/rooms/RoomList2";
|
||||
|
||||
|
||||
const LeftPanel = createReactClass({
|
||||
|
@ -273,6 +274,29 @@ const LeftPanel = createReactClass({
|
|||
breadcrumbs = (<RoomBreadcrumbs collapsed={this.props.collapsed} />);
|
||||
}
|
||||
|
||||
let roomList = null;
|
||||
if (SettingsStore.isFeatureEnabled("feature_new_room_list")) {
|
||||
roomList = <RoomList2
|
||||
onKeyDown={this._onKeyDown}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
collapsed={this.props.collapsed}
|
||||
searchFilter={this.state.searchFilter}
|
||||
ref={this.collectRoomList}
|
||||
onFocus={this._onFocus}
|
||||
onBlur={this._onBlur}
|
||||
/>;
|
||||
} else {
|
||||
roomList = <RoomList
|
||||
onKeyDown={this._onKeyDown}
|
||||
onFocus={this._onFocus}
|
||||
onBlur={this._onBlur}
|
||||
ref={this.collectRoomList}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
collapsed={this.props.collapsed}
|
||||
searchFilter={this.state.searchFilter}
|
||||
ConferenceHandler={VectorConferenceHandler} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={containerClasses}>
|
||||
{ tagPanelContainer }
|
||||
|
@ -284,15 +308,7 @@ const LeftPanel = createReactClass({
|
|||
{ exploreButton }
|
||||
{ searchBox }
|
||||
</div>
|
||||
<RoomList
|
||||
onKeyDown={this._onKeyDown}
|
||||
onFocus={this._onFocus}
|
||||
onBlur={this._onBlur}
|
||||
ref={this.collectRoomList}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
collapsed={this.props.collapsed}
|
||||
searchFilter={this.state.searchFilter}
|
||||
ConferenceHandler={VectorConferenceHandler} />
|
||||
{roomList}
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -31,7 +31,6 @@ import dis from '../../dispatcher/dispatcher';
|
|||
import sessionStore from '../../stores/SessionStore';
|
||||
import {MatrixClientPeg, MatrixClientCreds} from '../../MatrixClientPeg';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import RoomListStore from "../../stores/RoomListStore";
|
||||
|
||||
import TagOrderActions from '../../actions/TagOrderActions';
|
||||
import RoomListActions from '../../actions/RoomListActions';
|
||||
|
@ -42,6 +41,8 @@ import * as KeyboardShortcuts from "../../accessibility/KeyboardShortcuts";
|
|||
import HomePage from "./HomePage";
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import PlatformPeg from "../../PlatformPeg";
|
||||
import { RoomListStoreTempProxy } from "../../stores/room-list/RoomListStoreTempProxy";
|
||||
import { DefaultTagID } from "../../stores/room-list/models";
|
||||
// We need to fetch each pinned message individually (if we don't already have it)
|
||||
// so each pinned message may trigger a request. Limit the number per room for sanity.
|
||||
// NB. this is just for server notices rather than pinned messages in general.
|
||||
|
@ -297,18 +298,18 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
};
|
||||
|
||||
onRoomStateEvents = (ev, state) => {
|
||||
const roomLists = RoomListStore.getRoomLists();
|
||||
if (roomLists['m.server_notice'] && roomLists['m.server_notice'].some(r => r.roomId === ev.getRoomId())) {
|
||||
const roomLists = RoomListStoreTempProxy.getRoomLists();
|
||||
if (roomLists[DefaultTagID.ServerNotice] && roomLists[DefaultTagID.ServerNotice].some(r => r.roomId === ev.getRoomId())) {
|
||||
this._updateServerNoticeEvents();
|
||||
}
|
||||
};
|
||||
|
||||
_updateServerNoticeEvents = async () => {
|
||||
const roomLists = RoomListStore.getRoomLists();
|
||||
if (!roomLists['m.server_notice']) return [];
|
||||
const roomLists = RoomListStoreTempProxy.getRoomLists();
|
||||
if (!roomLists[DefaultTagID.ServerNotice]) return [];
|
||||
|
||||
const pinnedEvents = [];
|
||||
for (const room of roomLists['m.server_notice']) {
|
||||
for (const room of roomLists[DefaultTagID.ServerNotice]) {
|
||||
const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", "");
|
||||
|
||||
if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -17,220 +17,192 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import * as AvatarLogic from '../../../Avatar';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import {toPx} from "../../../utils/units";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'BaseAvatar',
|
||||
const useImageUrl = ({url, urls, idName, name, defaultToInitialLetter}) => {
|
||||
const [imageUrls, setUrls] = useState([]);
|
||||
const [urlsIndex, setIndex] = useState();
|
||||
|
||||
propTypes: {
|
||||
name: PropTypes.string.isRequired, // The name (first initial used as default)
|
||||
idName: PropTypes.string, // ID for generating hash colours
|
||||
title: PropTypes.string, // onHover title text
|
||||
url: PropTypes.string, // highest priority of them all, shortcut to set in urls[0]
|
||||
urls: PropTypes.array, // [highest_priority, ... , lowest_priority]
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
// XXX resizeMethod not actually used.
|
||||
resizeMethod: PropTypes.string,
|
||||
defaultToInitialLetter: PropTypes.bool, // true to add default url
|
||||
inputRef: PropTypes.oneOfType([
|
||||
// Either a function
|
||||
PropTypes.func,
|
||||
// Or the instance of a DOM native element
|
||||
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
|
||||
]),
|
||||
},
|
||||
|
||||
statics: {
|
||||
contextType: MatrixClientContext,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
width: 40,
|
||||
height: 40,
|
||||
resizeMethod: 'crop',
|
||||
defaultToInitialLetter: true,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return this._getState(this.props);
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
this.unmounted = false;
|
||||
this.context.on('sync', this.onClientSync);
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
this.context.removeListener('sync', this.onClientSync);
|
||||
},
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps: function(nextProps) {
|
||||
// work out if we need to call setState (if the image URLs array has changed)
|
||||
const newState = this._getState(nextProps);
|
||||
const newImageUrls = newState.imageUrls;
|
||||
const oldImageUrls = this.state.imageUrls;
|
||||
if (newImageUrls.length !== oldImageUrls.length) {
|
||||
this.setState(newState); // detected a new entry
|
||||
} else {
|
||||
// check each one to see if they are the same
|
||||
for (let i = 0; i < newImageUrls.length; i++) {
|
||||
if (oldImageUrls[i] !== newImageUrls[i]) {
|
||||
this.setState(newState); // detected a diff
|
||||
break;
|
||||
}
|
||||
}
|
||||
const onError = () => {
|
||||
const nextIndex = urlsIndex + 1;
|
||||
if (nextIndex < imageUrls.length) {
|
||||
// try the next one
|
||||
setIndex(nextIndex);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
onClientSync: function(syncState, prevState) {
|
||||
if (this.unmounted) return;
|
||||
const defaultImageUrl = useMemo(() => AvatarLogic.defaultAvatarUrlForString(idName || name), [idName, name]);
|
||||
|
||||
// Consider the client reconnected if there is no error with syncing.
|
||||
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
|
||||
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
||||
if (reconnected &&
|
||||
// Did we fall back?
|
||||
this.state.urlsIndex > 0
|
||||
) {
|
||||
// Start from the highest priority URL again
|
||||
this.setState({
|
||||
urlsIndex: 0,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_getState: function(props) {
|
||||
useEffect(() => {
|
||||
// work out the full set of urls to try to load. This is formed like so:
|
||||
// imageUrls: [ props.url, props.urls, default image ]
|
||||
// imageUrls: [ props.url, ...props.urls, default image ]
|
||||
|
||||
let urls = [];
|
||||
let _urls = [];
|
||||
if (!SettingsStore.getValue("lowBandwidth")) {
|
||||
urls = props.urls || [];
|
||||
_urls = urls || [];
|
||||
|
||||
if (props.url) {
|
||||
urls.unshift(props.url); // put in urls[0]
|
||||
if (url) {
|
||||
_urls.unshift(url); // put in urls[0]
|
||||
}
|
||||
}
|
||||
|
||||
let defaultImageUrl = null;
|
||||
if (props.defaultToInitialLetter) {
|
||||
defaultImageUrl = AvatarLogic.defaultAvatarUrlForString(
|
||||
props.idName || props.name,
|
||||
);
|
||||
urls.push(defaultImageUrl); // lowest priority
|
||||
if (defaultToInitialLetter) {
|
||||
_urls.push(defaultImageUrl); // lowest priority
|
||||
}
|
||||
|
||||
// deduplicate URLs
|
||||
urls = Array.from(new Set(urls));
|
||||
_urls = Array.from(new Set(_urls));
|
||||
|
||||
return {
|
||||
imageUrls: urls,
|
||||
defaultImageUrl: defaultImageUrl,
|
||||
urlsIndex: 0,
|
||||
};
|
||||
},
|
||||
setIndex(0);
|
||||
setUrls(_urls);
|
||||
}, [url, ...(urls || [])]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
onError: function(ev) {
|
||||
const nextIndex = this.state.urlsIndex + 1;
|
||||
if (nextIndex < this.state.imageUrls.length) {
|
||||
// try the next one
|
||||
this.setState({
|
||||
urlsIndex: nextIndex,
|
||||
});
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const onClientSync = useCallback((syncState, prevState) => {
|
||||
// Consider the client reconnected if there is no error with syncing.
|
||||
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
|
||||
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
||||
if (reconnected && urlsIndex > 0 ) { // Did we fall back?
|
||||
// Start from the highest priority URL again
|
||||
setIndex(0);
|
||||
}
|
||||
},
|
||||
}, [urlsIndex]);
|
||||
useEventEmitter(cli, "sync", onClientSync);
|
||||
|
||||
render: function() {
|
||||
const imageUrl = this.state.imageUrls[this.state.urlsIndex];
|
||||
const imageUrl = imageUrls[urlsIndex];
|
||||
return [imageUrl, imageUrl === defaultImageUrl, onError];
|
||||
};
|
||||
|
||||
const {
|
||||
name, idName, title, url, urls, width, height, resizeMethod,
|
||||
defaultToInitialLetter, onClick, inputRef,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
const BaseAvatar = (props) => {
|
||||
const {
|
||||
name,
|
||||
idName,
|
||||
title,
|
||||
url,
|
||||
urls,
|
||||
width=40,
|
||||
height=40,
|
||||
resizeMethod="crop", // eslint-disable-line no-unused-vars
|
||||
defaultToInitialLetter=true,
|
||||
onClick,
|
||||
inputRef,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const [imageUrl, isDefault, onError] = useImageUrl({url, urls, idName, name, defaultToInitialLetter});
|
||||
|
||||
if (isDefault) {
|
||||
const initialLetter = AvatarLogic.getInitialLetter(name);
|
||||
const textNode = (
|
||||
<span
|
||||
className="mx_BaseAvatar_initial"
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
fontSize: toPx(width * 0.65),
|
||||
width: toPx(width),
|
||||
lineHeight: toPx(height),
|
||||
}}
|
||||
>
|
||||
{ initialLetter }
|
||||
</span>
|
||||
);
|
||||
const imgNode = (
|
||||
<img
|
||||
className="mx_BaseAvatar_image"
|
||||
src={imageUrl}
|
||||
alt=""
|
||||
title={title}
|
||||
onError={onError}
|
||||
style={{
|
||||
width: toPx(width),
|
||||
height: toPx(height),
|
||||
}}
|
||||
aria-hidden="true" />
|
||||
);
|
||||
|
||||
if (imageUrl === this.state.defaultImageUrl) {
|
||||
const initialLetter = AvatarLogic.getInitialLetter(name);
|
||||
const textNode = (
|
||||
<span className="mx_BaseAvatar_initial" aria-hidden="true"
|
||||
style={{
|
||||
fontSize: toPx(width * 0.65),
|
||||
width: toPx(width),
|
||||
lineHeight: toPx(height),
|
||||
}}
|
||||
>
|
||||
{ initialLetter }
|
||||
</span>
|
||||
);
|
||||
const imgNode = (
|
||||
<img className="mx_BaseAvatar_image" src={imageUrl}
|
||||
alt="" title={title} onError={this.onError}
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
width: toPx(width),
|
||||
height: toPx(height)
|
||||
}} />
|
||||
);
|
||||
if (onClick != null) {
|
||||
return (
|
||||
<AccessibleButton element='span' className="mx_BaseAvatar"
|
||||
onClick={onClick} inputRef={inputRef} {...otherProps}
|
||||
>
|
||||
{ textNode }
|
||||
{ imgNode }
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<span className="mx_BaseAvatar" ref={inputRef} {...otherProps}>
|
||||
{ textNode }
|
||||
{ imgNode }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
if (onClick != null) {
|
||||
return (
|
||||
<AccessibleButton
|
||||
className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||
element='img'
|
||||
src={imageUrl}
|
||||
{...otherProps}
|
||||
element="span"
|
||||
className="mx_BaseAvatar"
|
||||
onClick={onClick}
|
||||
onError={this.onError}
|
||||
style={{
|
||||
width: toPx(width),
|
||||
height: toPx(height),
|
||||
}}
|
||||
title={title} alt=""
|
||||
inputRef={inputRef}
|
||||
{...otherProps} />
|
||||
>
|
||||
{ textNode }
|
||||
{ imgNode }
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<img
|
||||
className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||
src={imageUrl}
|
||||
onError={this.onError}
|
||||
style={{
|
||||
width: toPx(width),
|
||||
height: toPx(height),
|
||||
}}
|
||||
title={title} alt=""
|
||||
ref={inputRef}
|
||||
{...otherProps} />
|
||||
<span className="mx_BaseAvatar" ref={inputRef} {...otherProps}>
|
||||
{ textNode }
|
||||
{ imgNode }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (onClick != null) {
|
||||
return (
|
||||
<AccessibleButton
|
||||
className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||
element='img'
|
||||
src={imageUrl}
|
||||
onClick={onClick}
|
||||
onError={onError}
|
||||
style={{
|
||||
width: toPx(width),
|
||||
height: toPx(height),
|
||||
}}
|
||||
title={title} alt=""
|
||||
inputRef={inputRef}
|
||||
{...otherProps} />
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<img
|
||||
className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||
src={imageUrl}
|
||||
onError={onError}
|
||||
style={{
|
||||
width: toPx(width),
|
||||
height: toPx(height),
|
||||
}}
|
||||
title={title} alt=""
|
||||
ref={inputRef}
|
||||
{...otherProps} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
BaseAvatar.displayName = "BaseAvatar";
|
||||
|
||||
BaseAvatar.propTypes = {
|
||||
name: PropTypes.string.isRequired, // The name (first initial used as default)
|
||||
idName: PropTypes.string, // ID for generating hash colours
|
||||
title: PropTypes.string, // onHover title text
|
||||
url: PropTypes.string, // highest priority of them all, shortcut to set in urls[0]
|
||||
urls: PropTypes.array, // [highest_priority, ... , lowest_priority]
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
// XXX resizeMethod not actually used.
|
||||
resizeMethod: PropTypes.string,
|
||||
defaultToInitialLetter: PropTypes.bool, // true to add default url
|
||||
onClick: PropTypes.func,
|
||||
inputRef: PropTypes.oneOfType([
|
||||
// Either a function
|
||||
PropTypes.func,
|
||||
// Or the instance of a DOM native element
|
||||
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
|
||||
]),
|
||||
};
|
||||
|
||||
export default BaseAvatar;
|
||||
|
|
|
@ -34,9 +34,10 @@ import {humanizeTime} from "../../../utils/humanize";
|
|||
import createRoom, {canEncryptToAllUsers} from "../../../createRoom";
|
||||
import {inviteMultipleToRoom} from "../../../RoomInvite";
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import RoomListStore, {TAG_DM} from "../../../stores/RoomListStore";
|
||||
import {Key} from "../../../Keyboard";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {RoomListStoreTempProxy} from "../../../stores/room-list/RoomListStoreTempProxy";
|
||||
import {DefaultTagID} from "../../../stores/room-list/models";
|
||||
|
||||
export const KIND_DM = "dm";
|
||||
export const KIND_INVITE = "invite";
|
||||
|
@ -344,10 +345,10 @@ export default class InviteDialog extends React.PureComponent {
|
|||
_buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number} {
|
||||
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
|
||||
|
||||
// Also pull in all the rooms tagged as TAG_DM so we don't miss anything. Sometimes the
|
||||
// Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the
|
||||
// room list doesn't tag the room for the DMRoomMap, but does for the room list.
|
||||
const taggedRooms = RoomListStore.getRoomLists();
|
||||
const dmTaggedRooms = taggedRooms[TAG_DM];
|
||||
const taggedRooms = RoomListStoreTempProxy.getRoomLists();
|
||||
const dmTaggedRooms = taggedRooms[DefaultTagID.DM];
|
||||
const myUserId = MatrixClientPeg.get().getUserId();
|
||||
for (const dmRoom of dmTaggedRooms) {
|
||||
const otherMembers = dmRoom.getJoinedMembers().filter(u => u.userId !== myUserId);
|
||||
|
|
|
@ -29,7 +29,6 @@ import rate_limited_func from "../../../ratelimitedfunc";
|
|||
import * as Rooms from '../../../Rooms';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import TagOrderStore from '../../../stores/TagOrderStore';
|
||||
import RoomListStore, {TAG_DM} from '../../../stores/RoomListStore';
|
||||
import CustomRoomTagStore from '../../../stores/CustomRoomTagStore';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import RoomSubList from '../../structures/RoomSubList';
|
||||
|
@ -41,8 +40,11 @@ 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))$/;
|
||||
|
@ -161,7 +163,7 @@ export default createReactClass({
|
|||
this.updateVisibleRooms();
|
||||
});
|
||||
|
||||
this._roomListStoreToken = RoomListStore.addListener(() => {
|
||||
this._roomListStoreToken = RoomListStoreTempProxy.addListener(() => {
|
||||
this._delayedRefreshRoomList();
|
||||
});
|
||||
|
||||
|
@ -521,7 +523,7 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
getTagNameForRoomId: function(roomId) {
|
||||
const lists = RoomListStore.getRoomLists();
|
||||
const lists = RoomListStoreTempProxy.getRoomLists();
|
||||
for (const tagName of Object.keys(lists)) {
|
||||
for (const room of lists[tagName]) {
|
||||
// Should be impossible, but guard anyways.
|
||||
|
@ -541,7 +543,7 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
getRoomLists: function() {
|
||||
const lists = RoomListStore.getRoomLists();
|
||||
const lists = RoomListStoreTempProxy.getRoomLists();
|
||||
|
||||
const filteredLists = {};
|
||||
|
||||
|
@ -773,10 +775,10 @@ export default createReactClass({
|
|||
incomingCall: incomingCallIfTaggedAs('m.favourite'),
|
||||
},
|
||||
{
|
||||
list: this.state.lists[TAG_DM],
|
||||
list: this.state.lists[DefaultTagID.DM],
|
||||
label: _t('Direct Messages'),
|
||||
tagName: TAG_DM,
|
||||
incomingCall: incomingCallIfTaggedAs(TAG_DM),
|
||||
tagName: DefaultTagID.DM,
|
||||
incomingCall: incomingCallIfTaggedAs(DefaultTagID.DM),
|
||||
onAddRoom: () => {dis.dispatch({action: 'view_create_chat'});},
|
||||
addRoomLabel: _t("Start chat"),
|
||||
},
|
||||
|
|
246
src/components/views/rooms/RoomList2.tsx
Normal file
246
src/components/views/rooms/RoomList2.tsx
Normal file
|
@ -0,0 +1,246 @@
|
|||
/*
|
||||
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 * as React from "react";
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
import { Layout } from '../../../resizer/distributors/roomsublist2';
|
||||
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
|
||||
import { ResizeNotifier } from "../../../utils/ResizeNotifier";
|
||||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore2";
|
||||
import { ITagMap } from "../../../stores/room-list/algorithms/models";
|
||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||
import { Dispatcher } from "flux";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import RoomSublist2 from "./RoomSublist2";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
|
||||
/*******************************************************************
|
||||
* CAUTION *
|
||||
*******************************************************************
|
||||
* This is a work in progress implementation and isn't complete or *
|
||||
* even useful as a component. Please avoid using it until this *
|
||||
* warning disappears. *
|
||||
*******************************************************************/
|
||||
|
||||
interface IProps {
|
||||
onKeyDown: (ev: React.KeyboardEvent) => void;
|
||||
onFocus: (ev: React.FocusEvent) => void;
|
||||
onBlur: (ev: React.FocusEvent) => void;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
collapsed: boolean;
|
||||
searchFilter: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
sublists: ITagMap;
|
||||
}
|
||||
|
||||
const TAG_ORDER: TagID[] = [
|
||||
// -- Community Invites Placeholder --
|
||||
|
||||
DefaultTagID.Invite,
|
||||
DefaultTagID.Favourite,
|
||||
DefaultTagID.DM,
|
||||
DefaultTagID.Untagged,
|
||||
|
||||
// -- Custom Tags Placeholder --
|
||||
|
||||
DefaultTagID.LowPriority,
|
||||
DefaultTagID.ServerNotice,
|
||||
DefaultTagID.Archived,
|
||||
];
|
||||
const COMMUNITY_TAGS_BEFORE_TAG = DefaultTagID.Invite;
|
||||
const CUSTOM_TAGS_BEFORE_TAG = DefaultTagID.LowPriority;
|
||||
const ALWAYS_VISIBLE_TAGS: TagID[] = [
|
||||
DefaultTagID.DM,
|
||||
DefaultTagID.Untagged,
|
||||
];
|
||||
|
||||
interface ITagAesthetics {
|
||||
sectionLabel: string;
|
||||
addRoomLabel?: string;
|
||||
onAddRoom?: (dispatcher: Dispatcher<ActionPayload>) => void;
|
||||
isInvite: boolean;
|
||||
defaultHidden: boolean;
|
||||
}
|
||||
|
||||
const TAG_AESTHETICS: {
|
||||
// @ts-ignore - TS wants this to be a string but we know better
|
||||
[tagId: TagID]: ITagAesthetics;
|
||||
} = {
|
||||
[DefaultTagID.Invite]: {
|
||||
sectionLabel: _td("Invites"),
|
||||
isInvite: true,
|
||||
defaultHidden: false,
|
||||
},
|
||||
[DefaultTagID.Favourite]: {
|
||||
sectionLabel: _td("Favourites"),
|
||||
isInvite: false,
|
||||
defaultHidden: false,
|
||||
},
|
||||
[DefaultTagID.DM]: {
|
||||
sectionLabel: _td("Direct Messages"),
|
||||
isInvite: false,
|
||||
defaultHidden: false,
|
||||
addRoomLabel: _td("Start chat"),
|
||||
onAddRoom: (dispatcher: Dispatcher<ActionPayload>) => dispatcher.dispatch({action: 'view_create_chat'}),
|
||||
},
|
||||
[DefaultTagID.Untagged]: {
|
||||
sectionLabel: _td("Rooms"),
|
||||
isInvite: false,
|
||||
defaultHidden: false,
|
||||
addRoomLabel: _td("Create room"),
|
||||
onAddRoom: (dispatcher: Dispatcher<ActionPayload>) => dispatcher.dispatch({action: 'view_create_room'}),
|
||||
},
|
||||
[DefaultTagID.LowPriority]: {
|
||||
sectionLabel: _td("Low priority"),
|
||||
isInvite: false,
|
||||
defaultHidden: false,
|
||||
},
|
||||
[DefaultTagID.ServerNotice]: {
|
||||
sectionLabel: _td("System Alerts"),
|
||||
isInvite: false,
|
||||
defaultHidden: false,
|
||||
},
|
||||
[DefaultTagID.Archived]: {
|
||||
sectionLabel: _td("Historical"),
|
||||
isInvite: false,
|
||||
defaultHidden: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default class RoomList2 extends React.Component<IProps, IState> {
|
||||
private sublistRefs: { [tagId: string]: React.RefObject<RoomSublist2> } = {};
|
||||
private sublistSizes: { [tagId: string]: number } = {};
|
||||
private sublistCollapseStates: { [tagId: string]: boolean } = {};
|
||||
private unfilteredLayout: Layout;
|
||||
private filteredLayout: Layout;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {sublists: {}};
|
||||
this.loadSublistSizes();
|
||||
this.prepareLayouts();
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
RoomListStore.instance.on(LISTS_UPDATE_EVENT, (store) => {
|
||||
console.log("new lists", store.orderedLists);
|
||||
this.setState({sublists: store.orderedLists});
|
||||
});
|
||||
}
|
||||
|
||||
private loadSublistSizes() {
|
||||
const sizesJson = window.localStorage.getItem("mx_roomlist_sizes");
|
||||
if (sizesJson) this.sublistSizes = JSON.parse(sizesJson);
|
||||
|
||||
const collapsedJson = window.localStorage.getItem("mx_roomlist_collapsed");
|
||||
if (collapsedJson) this.sublistCollapseStates = JSON.parse(collapsedJson);
|
||||
}
|
||||
|
||||
private saveSublistSizes() {
|
||||
window.localStorage.setItem("mx_roomlist_sizes", JSON.stringify(this.sublistSizes));
|
||||
window.localStorage.setItem("mx_roomlist_collapsed", JSON.stringify(this.sublistCollapseStates));
|
||||
}
|
||||
|
||||
private prepareLayouts() {
|
||||
// TODO: Change layout engine for FTUE support
|
||||
this.unfilteredLayout = new Layout((tagId: string, height: number) => {
|
||||
const sublist = this.sublistRefs[tagId];
|
||||
if (sublist) sublist.current.setHeight(height);
|
||||
|
||||
// TODO: Check overflow (see old impl)
|
||||
|
||||
// Don't store a height for collapsed sublists
|
||||
if (!this.sublistCollapseStates[tagId]) {
|
||||
this.sublistSizes[tagId] = height;
|
||||
this.saveSublistSizes();
|
||||
}
|
||||
}, this.sublistSizes, this.sublistCollapseStates, {
|
||||
allowWhitespace: false,
|
||||
handleHeight: 1,
|
||||
});
|
||||
|
||||
this.filteredLayout = new Layout((tagId: string, height: number) => {
|
||||
const sublist = this.sublistRefs[tagId];
|
||||
if (sublist) sublist.current.setHeight(height);
|
||||
}, null, null, {
|
||||
allowWhitespace: false,
|
||||
handleHeight: 0,
|
||||
});
|
||||
}
|
||||
|
||||
private renderSublists(): React.ReactElement[] {
|
||||
const components: React.ReactElement[] = [];
|
||||
|
||||
for (const orderedTagId of TAG_ORDER) {
|
||||
if (COMMUNITY_TAGS_BEFORE_TAG === orderedTagId) {
|
||||
// Populate community invites if we have the chance
|
||||
// TODO
|
||||
}
|
||||
if (CUSTOM_TAGS_BEFORE_TAG === orderedTagId) {
|
||||
// Populate custom tags if needed
|
||||
// TODO
|
||||
}
|
||||
|
||||
const orderedRooms = this.state.sublists[orderedTagId] || [];
|
||||
if (orderedRooms.length === 0 && !ALWAYS_VISIBLE_TAGS.includes(orderedTagId)) {
|
||||
continue; // skip tag - not needed
|
||||
}
|
||||
|
||||
const aesthetics: ITagAesthetics = TAG_AESTHETICS[orderedTagId];
|
||||
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
|
||||
|
||||
const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null;
|
||||
components.push(<RoomSublist2
|
||||
key={`sublist-${orderedTagId}`}
|
||||
forRooms={true}
|
||||
rooms={orderedRooms}
|
||||
startAsHidden={aesthetics.defaultHidden}
|
||||
label={_t(aesthetics.sectionLabel)}
|
||||
onAddRoom={onAddRoomFn}
|
||||
addRoomLabel={aesthetics.addRoomLabel}
|
||||
isInvite={aesthetics.isInvite}
|
||||
/>);
|
||||
}
|
||||
|
||||
return components;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const sublists = this.renderSublists();
|
||||
return (
|
||||
<RovingTabIndexProvider handleHomeEnd={true} onKeyDown={this.props.onKeyDown}>
|
||||
{({onKeyDownHandler}) => (
|
||||
<div
|
||||
onFocus={this.props.onFocus}
|
||||
onBlur={this.props.onBlur}
|
||||
onKeyDown={onKeyDownHandler}
|
||||
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}
|
||||
>{sublists}</div>
|
||||
)}
|
||||
</RovingTabIndexProvider>
|
||||
);
|
||||
}
|
||||
}
|
226
src/components/views/rooms/RoomSublist2.tsx
Normal file
226
src/components/views/rooms/RoomSublist2.tsx
Normal file
|
@ -0,0 +1,226 @@
|
|||
/*
|
||||
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 * as React from "react";
|
||||
import { createRef } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import classNames from 'classnames';
|
||||
import IndicatorScrollbar from "../../structures/IndicatorScrollbar";
|
||||
import * as RoomNotifs from '../../../RoomNotifs';
|
||||
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import AccessibleButton from "../../views/elements/AccessibleButton";
|
||||
import AccessibleTooltipButton from "../../views/elements/AccessibleTooltipButton";
|
||||
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
||||
import RoomTile2 from "./RoomTile2";
|
||||
|
||||
/*******************************************************************
|
||||
* CAUTION *
|
||||
*******************************************************************
|
||||
* This is a work in progress implementation and isn't complete or *
|
||||
* even useful as a component. Please avoid using it until this *
|
||||
* warning disappears. *
|
||||
*******************************************************************/
|
||||
|
||||
interface IProps {
|
||||
forRooms: boolean;
|
||||
rooms?: Room[];
|
||||
startAsHidden: boolean;
|
||||
label: string;
|
||||
onAddRoom?: () => void;
|
||||
addRoomLabel: string;
|
||||
isInvite: boolean;
|
||||
|
||||
// TODO: Collapsed state
|
||||
// TODO: Height
|
||||
// TODO: Group invites
|
||||
// TODO: Calls
|
||||
// TODO: forceExpand?
|
||||
// TODO: Header clicking
|
||||
// TODO: Spinner support for historical
|
||||
}
|
||||
|
||||
interface IState {
|
||||
}
|
||||
|
||||
export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||
private headerButton = createRef();
|
||||
|
||||
public setHeight(size: number) {
|
||||
// TODO: Do a thing (maybe - height changes are different in FTUE)
|
||||
}
|
||||
|
||||
private hasTiles(): boolean {
|
||||
return this.numTiles > 0;
|
||||
}
|
||||
|
||||
private get numTiles(): number {
|
||||
// TODO: Account for group invites
|
||||
return (this.props.rooms || []).length;
|
||||
}
|
||||
|
||||
private onAddRoom = (e) => {
|
||||
e.stopPropagation();
|
||||
if (this.props.onAddRoom) this.props.onAddRoom();
|
||||
};
|
||||
|
||||
private renderTiles(): React.ReactElement[] {
|
||||
const tiles: React.ReactElement[] = [];
|
||||
|
||||
if (this.props.rooms) {
|
||||
for (const room of this.props.rooms) {
|
||||
tiles.push(<RoomTile2 room={room} key={`room-${room.roomId}`}/>);
|
||||
}
|
||||
}
|
||||
|
||||
return tiles;
|
||||
}
|
||||
|
||||
private renderHeader(): React.ReactElement {
|
||||
const notifications = !this.props.isInvite
|
||||
? RoomNotifs.aggregateNotificationCount(this.props.rooms)
|
||||
: {count: 0, highlight: true};
|
||||
const notifCount = notifications.count;
|
||||
const notifHighlight = notifications.highlight;
|
||||
|
||||
// TODO: Title on collapsed
|
||||
// TODO: Incoming call box
|
||||
|
||||
let chevron = null;
|
||||
if (this.hasTiles()) {
|
||||
const chevronClasses = classNames({
|
||||
'mx_RoomSubList_chevron': true,
|
||||
'mx_RoomSubList_chevronRight': false, // isCollapsed
|
||||
'mx_RoomSubList_chevronDown': true, // !isCollapsed
|
||||
});
|
||||
chevron = (<div className={chevronClasses}/>);
|
||||
}
|
||||
|
||||
return (
|
||||
<RovingTabIndexWrapper inputRef={this.headerButton}>
|
||||
{({onFocus, isActive, ref}) => {
|
||||
// TODO: Use onFocus
|
||||
const tabIndex = isActive ? 0 : -1;
|
||||
|
||||
// TODO: Collapsed state
|
||||
let badge;
|
||||
if (true) { // !isCollapsed
|
||||
const badgeClasses = classNames({
|
||||
'mx_RoomSubList_badge': true,
|
||||
'mx_RoomSubList_badgeHighlight': notifHighlight,
|
||||
});
|
||||
// Wrap the contents in a div and apply styles to the child div so that the browser default outline works
|
||||
if (notifCount > 0) {
|
||||
badge = (
|
||||
<AccessibleButton
|
||||
tabIndex={tabIndex}
|
||||
className={badgeClasses}
|
||||
aria-label={_t("Jump to first unread room.")}
|
||||
>
|
||||
<div>
|
||||
{FormattingUtils.formatCount(notifCount)}
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else if (this.props.isInvite && this.hasTiles()) {
|
||||
// Render the `!` badge for invites
|
||||
badge = (
|
||||
<AccessibleButton
|
||||
tabIndex={tabIndex}
|
||||
className={badgeClasses}
|
||||
aria-label={_t("Jump to first invite.")}
|
||||
>
|
||||
<div>
|
||||
{FormattingUtils.formatCount(this.numTiles)}
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let addRoomButton = null;
|
||||
if (!!this.props.onAddRoom) {
|
||||
addRoomButton = (
|
||||
<AccessibleTooltipButton
|
||||
tabIndex={tabIndex}
|
||||
onClick={this.onAddRoom}
|
||||
className="mx_RoomSubList_addRoom"
|
||||
title={this.props.addRoomLabel || _t("Add room")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: a11y (see old component)
|
||||
return (
|
||||
<div className={"mx_RoomSubList_labelContainer"}>
|
||||
<AccessibleButton
|
||||
inputRef={ref}
|
||||
tabIndex={tabIndex}
|
||||
className={"mx_RoomSubList_label"}
|
||||
role="treeitem"
|
||||
aria-level="1"
|
||||
>
|
||||
{chevron}
|
||||
<span>{this.props.label}</span>
|
||||
</AccessibleButton>
|
||||
{badge}
|
||||
{addRoomButton}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</RovingTabIndexWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
public render(): React.ReactElement {
|
||||
// TODO: Proper rendering
|
||||
// TODO: Error boundary
|
||||
|
||||
const tiles = this.renderTiles();
|
||||
|
||||
const classes = classNames({
|
||||
// TODO: Proper collapse support
|
||||
'mx_RoomSubList': true,
|
||||
'mx_RoomSubList_hidden': false, // len && isCollapsed
|
||||
'mx_RoomSubList_nonEmpty': this.hasTiles(), // len && !isCollapsed
|
||||
});
|
||||
|
||||
let content = null;
|
||||
if (tiles.length > 0) {
|
||||
// TODO: Lazy list rendering
|
||||
// TODO: Whatever scrolling magic needs to happen here
|
||||
content = (
|
||||
<IndicatorScrollbar className='mx_RoomSubList_scroll'>
|
||||
{tiles}
|
||||
</IndicatorScrollbar>
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: onKeyDown support
|
||||
return (
|
||||
<div
|
||||
className={classes}
|
||||
role="group"
|
||||
aria-label={this.props.label}
|
||||
>
|
||||
{this.renderHeader()}
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
255
src/components/views/rooms/RoomTile2.tsx
Normal file
255
src/components/views/rooms/RoomTile2.tsx
Normal file
|
@ -0,0 +1,255 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
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 { Room } from "matrix-js-sdk/src/models/room";
|
||||
import classNames from "classnames";
|
||||
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
||||
import AccessibleButton from "../../views/elements/AccessibleButton";
|
||||
import RoomAvatar from "../../views/avatars/RoomAvatar";
|
||||
import Tooltip from "../../views/elements/Tooltip";
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { Key } from "../../../Keyboard";
|
||||
import * as RoomNotifs from '../../../RoomNotifs';
|
||||
import { EffectiveMembership, getEffectiveMembership } from "../../../stores/room-list/membership";
|
||||
import * as Unread from '../../../Unread';
|
||||
import * as FormattingUtils from "../../../utils/FormattingUtils";
|
||||
|
||||
/*******************************************************************
|
||||
* CAUTION *
|
||||
*******************************************************************
|
||||
* This is a work in progress implementation and isn't complete or *
|
||||
* even useful as a component. Please avoid using it until this *
|
||||
* warning disappears. *
|
||||
*******************************************************************/
|
||||
|
||||
enum NotificationColor {
|
||||
// Inverted (None -> Red) because we do integer comparisons on this
|
||||
None, // nothing special
|
||||
Bold, // no badge, show as unread
|
||||
Grey, // unread notified messages
|
||||
Red, // unread pings
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
||||
// TODO: Allow falsifying counts (for invites and stuff)
|
||||
// TODO: Transparency? Was this ever used?
|
||||
// TODO: Incoming call boxes?
|
||||
}
|
||||
|
||||
interface INotificationState {
|
||||
symbol: string;
|
||||
color: NotificationColor;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
hover: boolean;
|
||||
notificationState: INotificationState;
|
||||
}
|
||||
|
||||
export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||
private roomTile = createRef();
|
||||
|
||||
// TODO: Custom status
|
||||
// TODO: Lock icon
|
||||
// TODO: Presence indicator
|
||||
// TODO: e2e shields
|
||||
// TODO: Handle changes to room aesthetics (name, join rules, etc)
|
||||
// TODO: scrollIntoView?
|
||||
// TODO: hover, badge, etc
|
||||
// TODO: isSelected for hover effects
|
||||
// TODO: Context menu
|
||||
// TODO: a11y
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hover: false,
|
||||
notificationState: this.getNotificationState(),
|
||||
};
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
// TODO: Listen for changes to the badge count and update as needed
|
||||
}
|
||||
|
||||
// XXX: This is a bit of an awful-looking hack. We should probably be using state for
|
||||
// this, but instead we're kinda forced to either duplicate the code or thread a variable
|
||||
// through the code paths. This feels like the least evil option.
|
||||
private get roomIsInvite(): boolean {
|
||||
return getEffectiveMembership(this.props.room.getMyMembership()) === EffectiveMembership.Invite;
|
||||
}
|
||||
|
||||
// TODO: Make use of this function when the notification state needs updating.
|
||||
private updateNotificationState() {
|
||||
this.setState({notificationState: this.getNotificationState()});
|
||||
}
|
||||
|
||||
private getNotificationState(): INotificationState {
|
||||
const state: INotificationState = {
|
||||
color: NotificationColor.None,
|
||||
symbol: null,
|
||||
};
|
||||
|
||||
if (this.roomIsInvite) {
|
||||
state.color = NotificationColor.Red;
|
||||
state.symbol = "!";
|
||||
} else {
|
||||
const redNotifs = RoomNotifs.getUnreadNotificationCount(this.props.room, 'highlight');
|
||||
const greyNotifs = RoomNotifs.getUnreadNotificationCount(this.props.room, 'total');
|
||||
|
||||
// For a 'true count' we pick the grey notifications first because they include the
|
||||
// red notifications. If we don't have a grey count for some reason we use the red
|
||||
// count. If that count is broken for some reason, assume zero. This avoids us showing
|
||||
// a badge for 'NaN' (which formats as 'NaNB' for NaN Billion).
|
||||
const trueCount = greyNotifs ? greyNotifs : (redNotifs ? redNotifs : 0);
|
||||
|
||||
// Note: we only set the symbol if we have an actual count. We don't want to show
|
||||
// zero on badges.
|
||||
|
||||
if (redNotifs > 0) {
|
||||
state.color = NotificationColor.Red;
|
||||
state.symbol = FormattingUtils.formatCount(trueCount);
|
||||
} else if (greyNotifs > 0) {
|
||||
state.color = NotificationColor.Grey;
|
||||
state.symbol = FormattingUtils.formatCount(trueCount);
|
||||
} else {
|
||||
// We don't have any notified messages, but we might have unread messages. Let's
|
||||
// find out.
|
||||
const hasUnread = Unread.doesRoomHaveUnreadMessages(this.props.room);
|
||||
if (hasUnread) {
|
||||
state.color = NotificationColor.Bold;
|
||||
// no symbol for this state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
private onTileMouseEnter = () => {
|
||||
this.setState({hover: true});
|
||||
};
|
||||
|
||||
private onTileMouseLeave = () => {
|
||||
this.setState({hover: false});
|
||||
};
|
||||
|
||||
private onTileClick = (ev: React.KeyboardEvent) => {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
// TODO: Support show_room_tile in new room list
|
||||
show_room_tile: true, // make sure the room is visible in the list
|
||||
room_id: this.props.room.roomId,
|
||||
clear_search: (ev && (ev.key === Key.ENTER || ev.key === Key.SPACE)),
|
||||
});
|
||||
};
|
||||
|
||||
public render(): React.ReactElement {
|
||||
// TODO: Collapsed state
|
||||
// TODO: Invites
|
||||
// TODO: a11y proper
|
||||
// TODO: Render more than bare minimum
|
||||
|
||||
const hasBadge = this.state.notificationState.color > NotificationColor.Bold;
|
||||
const isUnread = this.state.notificationState.color > NotificationColor.None;
|
||||
const classes = classNames({
|
||||
'mx_RoomTile': true,
|
||||
// 'mx_RoomTile_selected': this.state.selected,
|
||||
'mx_RoomTile_unread': isUnread,
|
||||
'mx_RoomTile_unreadNotify': this.state.notificationState.color >= NotificationColor.Grey,
|
||||
'mx_RoomTile_highlight': this.state.notificationState.color >= NotificationColor.Red,
|
||||
'mx_RoomTile_invited': this.roomIsInvite,
|
||||
// 'mx_RoomTile_menuDisplayed': isMenuDisplayed,
|
||||
'mx_RoomTile_noBadges': !hasBadge,
|
||||
// 'mx_RoomTile_transparent': this.props.transparent,
|
||||
// 'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed,
|
||||
});
|
||||
|
||||
const avatarClasses = classNames({
|
||||
'mx_RoomTile_avatar': true,
|
||||
});
|
||||
|
||||
|
||||
let badge;
|
||||
if (hasBadge) {
|
||||
const badgeClasses = classNames({
|
||||
'mx_RoomTile_badge': true,
|
||||
'mx_RoomTile_badgeButton': false, // this.state.badgeHover || isMenuDisplayed
|
||||
});
|
||||
badge = <div className={badgeClasses}>{this.state.notificationState.symbol}</div>;
|
||||
}
|
||||
|
||||
// TODO: the original RoomTile uses state for the room name. Do we need to?
|
||||
let name = this.props.room.name;
|
||||
if (typeof name !== 'string') name = '';
|
||||
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
|
||||
|
||||
const nameClasses = classNames({
|
||||
'mx_RoomTile_name': true,
|
||||
'mx_RoomTile_invite': this.roomIsInvite,
|
||||
'mx_RoomTile_badgeShown': hasBadge,
|
||||
});
|
||||
|
||||
// TODO: Support collapsed state properly
|
||||
let tooltip = null;
|
||||
if (false) { // isCollapsed
|
||||
if (this.state.hover) {
|
||||
tooltip = <Tooltip className="mx_RoomTile_tooltip" label={this.props.room.name} dir="auto"/>
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<RovingTabIndexWrapper inputRef={this.roomTile}>
|
||||
{({onFocus, isActive, ref}) =>
|
||||
<AccessibleButton
|
||||
onFocus={onFocus}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
inputRef={ref}
|
||||
className={classes}
|
||||
onMouseEnter={this.onTileMouseEnter}
|
||||
onMouseLeave={this.onTileMouseLeave}
|
||||
onClick={this.onTileClick}
|
||||
role="treeitem"
|
||||
>
|
||||
<div className={avatarClasses}>
|
||||
<div className="mx_RoomTile_avatar_container">
|
||||
<RoomAvatar room={this.props.room} width={24} height={24}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_RoomTile_nameContainer">
|
||||
<div className="mx_RoomTile_labelContainer">
|
||||
<div title={name} className={nameClasses} tabIndex={-1} dir="auto">
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
{badge}
|
||||
</div>
|
||||
{tooltip}
|
||||
</AccessibleButton>
|
||||
}
|
||||
</RovingTabIndexWrapper>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue