Merge pull request #4874 from matrix-org/travis/room-list/community-invites
Wedge community invites into the new room list
This commit is contained in:
commit
eac4eb7ed9
4 changed files with 178 additions and 4 deletions
|
@ -25,10 +25,15 @@ import { ITagMap } from "../../../stores/room-list/algorithms/models";
|
||||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||||
import { Dispatcher } from "flux";
|
import { Dispatcher } from "flux";
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
|
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||||
import RoomSublist2 from "./RoomSublist2";
|
import RoomSublist2 from "./RoomSublist2";
|
||||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||||
import { NameFilterCondition } from "../../../stores/room-list/filters/NameFilterCondition";
|
import { NameFilterCondition } from "../../../stores/room-list/filters/NameFilterCondition";
|
||||||
import { ListLayout } from "../../../stores/room-list/ListLayout";
|
import { ListLayout } from "../../../stores/room-list/ListLayout";
|
||||||
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
|
import GroupAvatar from "../avatars/GroupAvatar";
|
||||||
|
import TemporaryTile from "./TemporaryTile";
|
||||||
|
import { NotificationColor, StaticNotificationState } from "./NotificationBadge";
|
||||||
|
|
||||||
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
|
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
|
@ -173,6 +178,40 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderCommunityInvites(): React.ReactElement[] {
|
||||||
|
// TODO: Put community invites in a more sensible place (not in the room list)
|
||||||
|
return MatrixClientPeg.get().getGroups().filter(g => {
|
||||||
|
if (g.myMembership !== 'invite') return false;
|
||||||
|
return !this.searchFilter || this.searchFilter.matches(g.name);
|
||||||
|
}).map(g => {
|
||||||
|
const avatar = (
|
||||||
|
<GroupAvatar
|
||||||
|
groupId={g.groupId}
|
||||||
|
groupName={g.name}
|
||||||
|
groupAvatarUrl={g.avatarUrl}
|
||||||
|
width={32} height={32} resizeMethod='crop'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const openGroup = () => {
|
||||||
|
defaultDispatcher.dispatch({
|
||||||
|
action: 'view_group',
|
||||||
|
group_id: g.groupId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<TemporaryTile
|
||||||
|
isMinimized={this.props.isMinimized}
|
||||||
|
isSelected={false}
|
||||||
|
displayName={g.name}
|
||||||
|
avatar={avatar}
|
||||||
|
notificationState={StaticNotificationState.forSymbol("!", NotificationColor.Red)}
|
||||||
|
onClick={openGroup}
|
||||||
|
key={`temporaryGroupTile_${g.groupId}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private renderSublists(): React.ReactElement[] {
|
private renderSublists(): React.ReactElement[] {
|
||||||
const components: React.ReactElement[] = [];
|
const components: React.ReactElement[] = [];
|
||||||
|
|
||||||
|
@ -195,6 +234,7 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
||||||
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
|
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
|
||||||
|
|
||||||
const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null;
|
const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null;
|
||||||
|
const extraTiles = orderedTagId === DefaultTagID.Invite ? this.renderCommunityInvites() : null;
|
||||||
components.push(
|
components.push(
|
||||||
<RoomSublist2
|
<RoomSublist2
|
||||||
key={`sublist-${orderedTagId}`}
|
key={`sublist-${orderedTagId}`}
|
||||||
|
@ -208,6 +248,7 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
||||||
isInvite={aesthetics.isInvite}
|
isInvite={aesthetics.isInvite}
|
||||||
layout={this.state.layouts.get(orderedTagId)}
|
layout={this.state.layouts.get(orderedTagId)}
|
||||||
isMinimized={this.props.isMinimized}
|
isMinimized={this.props.isMinimized}
|
||||||
|
extraBadTilesThatShouldntExist={extraTiles}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,6 +62,10 @@ interface IProps {
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
tagId: TagID;
|
tagId: TagID;
|
||||||
|
|
||||||
|
// TODO: Don't use this. It's for community invites, and community invites shouldn't be here.
|
||||||
|
// You should feel bad if you use this.
|
||||||
|
extraBadTilesThatShouldntExist?: React.ReactElement[];
|
||||||
|
|
||||||
// TODO: Account for https://github.com/vector-im/riot-web/issues/14179
|
// TODO: Account for https://github.com/vector-im/riot-web/issues/14179
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,8 +91,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private get numTiles(): number {
|
private get numTiles(): number {
|
||||||
// TODO: Account for group invites: https://github.com/vector-im/riot-web/issues/14179
|
return (this.props.rooms || []).length + (this.props.extraBadTilesThatShouldntExist || []).length;
|
||||||
return (this.props.rooms || []).length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private get numVisibleTiles(): number {
|
private get numVisibleTiles(): number {
|
||||||
|
@ -187,6 +190,10 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
const tiles: React.ReactElement[] = [];
|
const tiles: React.ReactElement[] = [];
|
||||||
|
|
||||||
|
if (this.props.extraBadTilesThatShouldntExist) {
|
||||||
|
tiles.push(...this.props.extraBadTilesThatShouldntExist);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.props.rooms) {
|
if (this.props.rooms) {
|
||||||
const visibleRooms = this.props.rooms.slice(0, this.numVisibleTiles);
|
const visibleRooms = this.props.rooms.slice(0, this.numVisibleTiles);
|
||||||
for (const room of visibleRooms) {
|
for (const room of visibleRooms) {
|
||||||
|
@ -202,6 +209,14 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We only have to do this because of the extra tiles. We do it conditionally
|
||||||
|
// to avoid spending cycles on slicing. It's generally fine to do this though
|
||||||
|
// as users are unlikely to have more than a handful of tiles when the extra
|
||||||
|
// tiles are used.
|
||||||
|
if (tiles.length > this.numVisibleTiles) {
|
||||||
|
return tiles.slice(0, this.numVisibleTiles);
|
||||||
|
}
|
||||||
|
|
||||||
return tiles;
|
return tiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
114
src/components/views/rooms/TemporaryTile.tsx
Normal file
114
src/components/views/rooms/TemporaryTile.tsx
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
/*
|
||||||
|
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 classNames from "classnames";
|
||||||
|
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
||||||
|
import AccessibleButton from "../../views/elements/AccessibleButton";
|
||||||
|
import NotificationBadge, { INotificationState, NotificationColor } from "./NotificationBadge";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
isMinimized: boolean;
|
||||||
|
isSelected: boolean;
|
||||||
|
displayName: string;
|
||||||
|
avatar: React.ReactElement;
|
||||||
|
notificationState: INotificationState;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
hover: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class TemporaryTile extends React.Component<IProps, IState> {
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
hover: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private onTileMouseEnter = () => {
|
||||||
|
this.setState({hover: true});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onTileMouseLeave = () => {
|
||||||
|
this.setState({hover: false});
|
||||||
|
};
|
||||||
|
|
||||||
|
public render(): React.ReactElement {
|
||||||
|
// XXX: We copy classes because it's easier
|
||||||
|
const classes = classNames({
|
||||||
|
'mx_RoomTile2': true,
|
||||||
|
'mx_RoomTile2_selected': this.props.isSelected,
|
||||||
|
'mx_RoomTile2_minimized': this.props.isMinimized,
|
||||||
|
});
|
||||||
|
|
||||||
|
const badge = (
|
||||||
|
<NotificationBadge
|
||||||
|
notification={this.props.notificationState}
|
||||||
|
forceCount={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
let name = this.props.displayName;
|
||||||
|
if (typeof name !== 'string') name = '';
|
||||||
|
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
|
||||||
|
|
||||||
|
const nameClasses = classNames({
|
||||||
|
"mx_RoomTile2_name": true,
|
||||||
|
"mx_RoomTile2_nameHasUnreadEvents": this.props.notificationState.color >= NotificationColor.Bold,
|
||||||
|
});
|
||||||
|
|
||||||
|
let nameContainer = (
|
||||||
|
<div className="mx_RoomTile2_nameContainer">
|
||||||
|
<div title={name} className={nameClasses} tabIndex={-1} dir="auto">
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
if (this.props.isMinimized) nameContainer = null;
|
||||||
|
|
||||||
|
const avatarSize = 32;
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<RovingTabIndexWrapper>
|
||||||
|
{({onFocus, isActive, ref}) =>
|
||||||
|
<AccessibleButton
|
||||||
|
onFocus={onFocus}
|
||||||
|
tabIndex={isActive ? 0 : -1}
|
||||||
|
inputRef={ref}
|
||||||
|
className={classes}
|
||||||
|
onMouseEnter={this.onTileMouseEnter}
|
||||||
|
onMouseLeave={this.onTileMouseLeave}
|
||||||
|
onClick={this.props.onClick}
|
||||||
|
role="treeitem"
|
||||||
|
>
|
||||||
|
<div className="mx_RoomTile2_avatarContainer">
|
||||||
|
{this.props.avatar}
|
||||||
|
</div>
|
||||||
|
{nameContainer}
|
||||||
|
<div className="mx_RoomTile2_badgeContainer">
|
||||||
|
{badge}
|
||||||
|
</div>
|
||||||
|
</AccessibleButton>
|
||||||
|
}
|
||||||
|
</RovingTabIndexWrapper>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -60,11 +60,15 @@ export class NameFilterCondition extends EventEmitter implements IFilterConditio
|
||||||
|
|
||||||
if (!room.name) return false; // should realistically not happen: the js-sdk always calculates a name
|
if (!room.name) return false; // should realistically not happen: the js-sdk always calculates a name
|
||||||
|
|
||||||
|
return this.matches(room.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public matches(val: string): boolean {
|
||||||
// Note: we have to match the filter with the removeHiddenChars() room name because the
|
// Note: we have to match the filter with the removeHiddenChars() room name because the
|
||||||
// function strips spaces and other characters (M becomes RN for example, in lowercase).
|
// function strips spaces and other characters (M becomes RN for example, in lowercase).
|
||||||
// We also doubly convert to lowercase to work around oddities of the library.
|
// We also doubly convert to lowercase to work around oddities of the library.
|
||||||
const noSecretsFilter = removeHiddenChars(lcFilter).toLowerCase();
|
const noSecretsFilter = removeHiddenChars(this.search.toLowerCase()).toLowerCase();
|
||||||
const noSecretsName = removeHiddenChars(room.name.toLowerCase()).toLowerCase();
|
const noSecretsName = removeHiddenChars(val.toLowerCase()).toLowerCase();
|
||||||
return noSecretsName.includes(noSecretsFilter);
|
return noSecretsName.includes(noSecretsFilter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue