Merge pull request #4874 from matrix-org/travis/room-list/community-invites
Wedge community invites into the new room listpull/21833/head
						commit
						eac4eb7ed9
					
				|  | @ -25,10 +25,15 @@ 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 defaultDispatcher from "../../../dispatcher/dispatcher"; | ||||
| import RoomSublist2 from "./RoomSublist2"; | ||||
| import { ActionPayload } from "../../../dispatcher/payloads"; | ||||
| import { NameFilterCondition } from "../../../stores/room-list/filters/NameFilterCondition"; | ||||
| 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: 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[] { | ||||
|         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`); | ||||
| 
 | ||||
|             const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null; | ||||
|             const extraTiles = orderedTagId === DefaultTagID.Invite ? this.renderCommunityInvites() : null; | ||||
|             components.push( | ||||
|                 <RoomSublist2 | ||||
|                     key={`sublist-${orderedTagId}`} | ||||
|  | @ -208,6 +248,7 @@ export default class RoomList2 extends React.Component<IProps, IState> { | |||
|                     isInvite={aesthetics.isInvite} | ||||
|                     layout={this.state.layouts.get(orderedTagId)} | ||||
|                     isMinimized={this.props.isMinimized} | ||||
|                     extraBadTilesThatShouldntExist={extraTiles} | ||||
|                 /> | ||||
|             ); | ||||
|         } | ||||
|  |  | |||
|  | @ -62,6 +62,10 @@ interface IProps { | |||
|     isMinimized: boolean; | ||||
|     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
 | ||||
| } | ||||
| 
 | ||||
|  | @ -87,8 +91,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> { | |||
|     } | ||||
| 
 | ||||
|     private get numTiles(): number { | ||||
|         // TODO: Account for group invites: https://github.com/vector-im/riot-web/issues/14179
 | ||||
|         return (this.props.rooms || []).length; | ||||
|         return (this.props.rooms || []).length + (this.props.extraBadTilesThatShouldntExist || []).length; | ||||
|     } | ||||
| 
 | ||||
|     private get numVisibleTiles(): number { | ||||
|  | @ -187,6 +190,10 @@ export default class RoomSublist2 extends React.Component<IProps, IState> { | |||
| 
 | ||||
|         const tiles: React.ReactElement[] = []; | ||||
| 
 | ||||
|         if (this.props.extraBadTilesThatShouldntExist) { | ||||
|             tiles.push(...this.props.extraBadTilesThatShouldntExist); | ||||
|         } | ||||
| 
 | ||||
|         if (this.props.rooms) { | ||||
|             const visibleRooms = this.props.rooms.slice(0, this.numVisibleTiles); | ||||
|             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; | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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
 | ||||
| 
 | ||||
|         return this.matches(room.name); | ||||
|     } | ||||
| 
 | ||||
|     public matches(val: string): boolean { | ||||
|         // 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).
 | ||||
|         // We also doubly convert to lowercase to work around oddities of the library.
 | ||||
|         const noSecretsFilter = removeHiddenChars(lcFilter).toLowerCase(); | ||||
|         const noSecretsName = removeHiddenChars(room.name.toLowerCase()).toLowerCase(); | ||||
|         const noSecretsFilter = removeHiddenChars(this.search.toLowerCase()).toLowerCase(); | ||||
|         const noSecretsName = removeHiddenChars(val.toLowerCase()).toLowerCase(); | ||||
|         return noSecretsName.includes(noSecretsFilter); | ||||
|     } | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Travis Ralston
						Travis Ralston