Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Show rooms in common with another user #4897

Closed
wants to merge 28 commits into from
Closed
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
354089b
Show rooms in common with another user
Half-Shot Jul 5, 2020
e901aae
Fix lint
Half-Shot Jul 5, 2020
d06b90b
Add strings
Half-Shot Jul 5, 2020
f6d2e49
Add "compact" view for SharedRooms view
Half-Shot Jul 5, 2020
9aed1c0
Show shared rooms to users when being invited
Half-Shot Jul 5, 2020
db2b642
Lint
Half-Shot Jul 5, 2020
e6d2c89
Typescriptify RoomListSorter
Half-Shot Jul 6, 2020
29ce381
Order by activity
Half-Shot Jul 6, 2020
23fe7d0
Limit to 3 rooms, but allow expansion
Half-Shot Jul 6, 2020
a617eb5
Fix lint
Half-Shot Jul 6, 2020
a3a9f19
Show count more
Half-Shot Jul 6, 2020
81c68aa
Update if a new user is selected
Half-Shot Jul 6, 2020
b3a7215
Merge remote-tracking branch 'origin/develop' into hs/shared-rooms
Half-Shot Aug 18, 2020
169a66c
Add UserInfoRoomTile
Half-Shot Aug 18, 2020
f9cfa60
Refactor to work with new room tiles
Half-Shot Aug 18, 2020
5473f63
Fix builds
Half-Shot Aug 18, 2020
f73f937
Fix feature code
Half-Shot Aug 20, 2020
76ab806
Merge branch 'develop' into hs/shared-rooms
Half-Shot Sep 7, 2020
30384ac
Apply suggestions from code review
Half-Shot Nov 20, 2020
7c828f4
Merge remote-tracking branch 'origin/develop' into hs/shared-rooms
Half-Shot Dec 14, 2020
f57dca7
Drop roomIds state
Half-Shot Dec 14, 2020
ba7ccbf
Tweaks
Half-Shot Dec 14, 2020
51a4371
Linting
Half-Shot Dec 14, 2020
b2a188f
Merge remote-tracking branch 'origin/develop' into hs/shared-rooms
Half-Shot Feb 16, 2021
deafdb8
Initial stab at rendering shared rooms in the side panel
Half-Shot Feb 16, 2021
fbcaaab
Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into…
t3chguy Jul 6, 2021
ec90183
Iterate PR
t3chguy Jul 6, 2021
135c2f7
Iterate PR some more
t3chguy Jul 6, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions res/css/_components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@
@import "./views/messages/_common_CryptoEvent.scss";
@import "./views/right_panel/_EncryptionInfo.scss";
@import "./views/right_panel/_UserInfo.scss";
@import "./views/right_panel/_UserInfoSharedRooms.scss";
@import "./views/right_panel/_VerificationPanel.scss";
@import "./views/room_settings/_AliasSettings.scss";
@import "./views/room_settings/_ColorSettings.scss";
Expand Down
7 changes: 7 additions & 0 deletions res/css/views/right_panel/_UserInfoSharedRooms.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.mx_UserInfoSharedRooms ul {
padding-left: 0px;
> li {
padding-left: 0px;
list-style: none;
}
}
6 changes: 2 additions & 4 deletions src/RoomListSorter.js → src/RoomListSorter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

'use strict';

function tsOfNewestEvent(room) {
function tsOfNewestEvent(room: any) {
if (room.timeline.length) {
return room.timeline[room.timeline.length - 1].getTs();
} else {
return Number.MAX_SAFE_INTEGER;
}
}

export function mostRecentActivityFirst(roomList) {
export function mostRecentActivityFirst(roomList: any[]) {
return roomList.sort(function(a, b) {
return tsOfNewestEvent(b) - tsOfNewestEvent(a);
});
Expand Down
118 changes: 118 additions & 0 deletions src/components/views/elements/UserInfoRoomTile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
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 from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import classNames from "classnames";
import AccessibleButton from "../../views/elements/AccessibleButton";
import ActiveRoomObserver from "../../../ActiveRoomObserver";
import { _t } from "../../../languageHandler";
import { TagID, DefaultTagID } from "../../../stores/room-list/models";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import dis from '../../../dispatcher/dispatcher';
import { Key } from "../../../Keyboard";

interface IProps {
room: Room;
}

type PartialDOMRect = Pick<DOMRect, "left" | "bottom">;

interface IState {
selected: boolean;
notificationsMenuPosition: PartialDOMRect;
generalMenuPosition: PartialDOMRect;
messagePreview?: string;
}

export default class UserInfoRoomTile extends React.PureComponent<IProps, IState> {
constructor(props: IProps) {
super(props);

this.state = {
selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId,
notificationsMenuPosition: null,
generalMenuPosition: null,
};
}

private onTileClick = (ev: React.KeyboardEvent) => {
ev.preventDefault();
ev.stopPropagation();
dis.dispatch({
action: 'view_room',
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 {
const classes = classNames({
'mx_RoomTile': true,
'mx_RoomTile_selected': this.state.selected,
});

const roomAvatar = <DecoratedRoomAvatar
room={this.props.room}
avatarSize={32}
tag={DefaultTagID.Untagged}
displayBadge={false}
/>;

let badge: React.ReactNode;

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

let nameContainer = (
<div className="mx_RoomTile_nameContainer">
<div title={name} className={"mx_RoomTile_name"} tabIndex={-1} dir="auto">
{name}
</div>
</div>
);

let ariaLabel = name;
let ariaDescribedBy: string;

const props: Partial<React.ComponentProps<typeof AccessibleTooltipButton>> = {};
let Button: React.ComponentType<React.ComponentProps<typeof AccessibleButton>> = AccessibleButton;

return (
<React.Fragment>
Half-Shot marked this conversation as resolved.
Show resolved Hide resolved
<Button
{...props}
className={classes}
onClick={this.onTileClick}
role="treeitem"
aria-label={ariaLabel}
aria-selected={this.state.selected}
aria-describedby={ariaDescribedBy}
>
{roomAvatar}
{nameContainer}
{badge}
</Button>
</React.Fragment>
Half-Shot marked this conversation as resolved.
Show resolved Hide resolved
);
}
}
5 changes: 5 additions & 0 deletions src/components/views/right_panel/UserInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import EncryptionPanel from "./EncryptionPanel";
import { useAsyncMemo } from '../../../hooks/useAsyncMemo';
import { verifyUser, legacyVerifyUser, verifyDevice } from '../../../verification';
import {Action} from "../../../dispatcher/actions";
import UserInfoSharedRooms from "./UserInfoSharedRooms";

const _disambiguateDevices = (devices) => {
const names = Object.create(null);
Expand Down Expand Up @@ -1312,6 +1313,8 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
const isMe = member.userId === cli.getUserId();
const canVerify = cryptoEnabled && homeserverSupportsCrossSigning && !userVerified && !isMe;

const isSharedRoomsFeatureEnabled = SettingsStore.isFeatureEnabled("feature_show_shared_rooms");
Half-Shot marked this conversation as resolved.
Show resolved Hide resolved

const setUpdating = (updating) => {
setPendingUpdateCount(count => count + (updating ? 1 : -1));
};
Expand Down Expand Up @@ -1363,6 +1366,8 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
</div> }

{ securitySection }
{ isSharedRoomsFeatureEnabled &&!isMe && <UserInfoSharedRooms
userId={member.userId} /> }
<UserOptionsSection
devices={devices}
canInvite={roomPermissions.canInvite}
Expand Down
177 changes: 177 additions & 0 deletions src/components/views/right_panel/UserInfoSharedRooms.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/*
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 { MatrixClientPeg } from '../../../MatrixClientPeg';
import Spinner from "../elements/Spinner";
import { _t } from '../../../languageHandler';
import Pill from '../../views/elements/Pill';
import AccessibleButton from '../../views/elements/AccessibleButton';
import SpecPermalinkConstructor from '../../../utils/permalinks/SpecPermalinkConstructor';
import { mostRecentActivityFirst } from '../../../RoomListSorter';
import UserInfoRoomTile from "../elements/UserInfoRoomTile";

interface IProps {
userId: string;
compact: boolean;
}

interface IState {
roomIds?: [];
error: boolean;
showAll: boolean;
}

const LIMITED_VIEW_SHOW_COUNT = 3;

export default class UserInfoSharedRooms extends React.PureComponent<IProps, IState> {

constructor(props: IProps) {
super(props);

this.state = {
error: false,
showAll: false,
};
}

componentDidMount() {
return this.componentDidUpdate();
}

async componentDidUpdate(prevProps?: IProps) {
if (prevProps && prevProps.userId === this.props.userId) {
// Nothing to update.
return;
}

// Reset because this is a new user
this.setState({
error: false,
showAll: false,
roomIds: undefined,
});

try {
const roomIds = await MatrixClientPeg.get()._unstable_getSharedRooms(this.props.userId);
this.setState({
roomIds,
});
} catch (ex) {
console.log(`Failed to get shared rooms for ${this.props.userId}`, ex);
this.setState({ error: true });
}
}

private onShowMoreClick() {
console.log("Showing more");
this.setState({
showAll: true,
});
}

private renderRoomTile(room) {
// If the room cannot be found, hide it.
if (!room) {
return null;
}

// If the room has been upgraded, hide it.
const tombstone = room.currentState.getStateEvents("m.room.tombstone", "");
if (tombstone) {
return null;
}

if (this.props.compact) {
// XXX: This is inefficent as we only render COMPACT_VIEW_SHOW_COUNT rooms at a time, the other pills are wasted.
const alias = room.getCanonicalAlias();
if (!alias) {
// Without an alias we get ugly room_ids, hide it.
return null;
}
return <a href={`#/room/${alias}`}><Pill
key={room.roomId}
type={Pill.TYPE_ROOM_MENTION}
room={room}
url={new SpecPermalinkConstructor().forRoom(alias)}
inMessage={false}
shouldShowPillAvatar={true}
isSelected={false}
/></a>;
}

return <li key={room.roomId}>
<UserInfoRoomTile room={room}/>
</li>;
}

private renderRoomTiles() {
const peg = MatrixClientPeg.get();
const orderedActiveRooms = mostRecentActivityFirst(this.state.roomIds.map(
(roomId) => peg.getRoom(roomId)
));

// We must remove the null values in order for the slice to work in render()
return orderedActiveRooms.map((room) => this.renderRoomTile(room)).filter((tile => tile !== null));
}

render(): React.ReactNode {
let content;
let realCount = 0;

if (this.state.roomIds && this.state.roomIds.length > 0) {
content = this.renderRoomTiles();
realCount = content.length;
if (!this.state.showAll) {
content = content.slice(0, LIMITED_VIEW_SHOW_COUNT);
}
} else if (this.state.roomIds) {
content = <p> {_t("You share no rooms in common with this user.")} </p>;
} else if (this.state.error) {
content = <p> {_t("There was an error fetching shared rooms with this user.")} </p>;
} else {
// We're still loading
content = <Spinner/>;
}

// Compact view: Show as a single line.
if (this.props.compact && content.length) {
if (realCount <= content.length) {
return <p> {_t("You are both participating in <rooms></rooms>", {}, {rooms: content})} </p>;
} else {
return <p> {_t("You are both participating in <rooms></rooms> and %(hidden)s more", {
hidden: realCount - content.length,
}, {
rooms: content
})}</p>;
}
} else if (this.props.compact) {
return content;
}

const canShowMore = !this.state.showAll && realCount > LIMITED_VIEW_SHOW_COUNT;
// Normal view: Show as a list with a header
return <div className="mx_UserInfoSharedRooms mx_UserInfo_container">
<h3>{ _t("Shared Rooms") }</h3>
<ul>
{content}
</ul>
{ canShowMore && <AccessibleButton className="mx_UserInfo_field" onClick={() => this.onShowMoreClick()}>
{ _t("Show %(count)s more", { count: realCount - content.length}) }
</AccessibleButton> }
</div>;
}
}
Loading