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 12 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 @@ -154,6 +154,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
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 @@ -1310,6 +1311,8 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
const isMe = member.userId === cli.getUserId();
const canVerify = 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 @@ -1361,6 +1364,8 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
</div> }

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

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 onRoomTileClick(roomId) {
dis.dispatch({
action: 'view_room',
show_room_tile: true, // to make sure the room gets scrolled into view
room_id: roomId,
});
}

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}>
<RoomTile
Half-Shot marked this conversation as resolved.
Show resolved Hide resolved
onClick={this.onRoomTileClick.bind(undefined, [room.roomId])}
room={room}
collapsed={false}
unread={false}
highlight={false}
transparent={true}
isInvite={false}
incomingCall={false}
/>
</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>;
}
}
9 changes: 9 additions & 0 deletions src/components/views/rooms/RoomPreviewBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import dis from '../../../dispatcher/dispatcher';
import classNames from 'classnames';
import { _t } from '../../../languageHandler';
import IdentityAuthClient from '../../../IdentityAuthClient';
import SettingsStore from '../../../settings/SettingsStore';
import UserInfoSharedRooms from '../right_panel/UserInfoSharedRooms';

const MessageCase = Object.freeze({
NotLoggedIn: "NotLoggedIn",
Expand Down Expand Up @@ -294,6 +296,7 @@ export default createReactClass({
let secondaryActionHandler;
let secondaryActionLabel;
let footer;
let extraContext;
const extraComponents = [];

const messageCase = this._getMessageCase();
Expand Down Expand Up @@ -473,13 +476,18 @@ export default createReactClass({
secondaryActionLabel = _t("Reject");
secondaryActionHandler = this.props.onRejectClick;

if (SettingsStore.isFeatureEnabled("feature_show_shared_rooms")) {
Half-Shot marked this conversation as resolved.
Show resolved Hide resolved
extraContext = <UserInfoSharedRooms userId={inviteMember.userId} compact={true} />;
}

if (this.props.onRejectAndIgnoreClick) {
extraComponents.push(
<AccessibleButton kind="secondary" onClick={this.props.onRejectAndIgnoreClick} key="ignore">
{ _t("Reject & Ignore user") }
</AccessibleButton>,
);
}

break;
}
case MessageCase.ViewingRoom: {
Expand Down Expand Up @@ -560,6 +568,7 @@ export default createReactClass({
<div className="mx_RoomPreviewBar_message">
{ titleElement }
{ subTitleElements }
{ extraContext }
</div>
<div className="mx_RoomPreviewBar_actions">
{ secondaryButton }
Expand Down
6 changes: 4 additions & 2 deletions src/components/views/rooms/RoomTile.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default createReactClass({

propTypes: {
onClick: PropTypes.func,

refreshSubList: PropTypes.func,
room: PropTypes.object.isRequired,
collapsed: PropTypes.bool.isRequired,
unread: PropTypes.bool.isRequired,
Expand Down Expand Up @@ -385,7 +385,9 @@ export default createReactClass({
this.setState({
contextMenuPosition: null,
});
this.props.refreshSubList();
if (this.props.refreshSubList) {
this.props.refreshSubList();
}
},

render: function() {
Expand Down
6 changes: 6 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,7 @@
"Use the improved room list (will refresh to apply changes)": "Use the improved room list (will refresh to apply changes)",
"Support adding custom themes": "Support adding custom themes",
"Enable IRC layout option in the appearance tab": "Enable IRC layout option in the appearance tab",
"Show rooms in common with another user in the member info panel": "Show rooms in common with another user in the member info panel",
"Show info about bridges in room settings": "Show info about bridges in room settings",
"Font size": "Font size",
"Use custom size": "Use custom size",
Expand Down Expand Up @@ -1359,6 +1360,11 @@
"Failed to deactivate user": "Failed to deactivate user",
"This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.",
"Security": "Security",
"You share no rooms in common with this user.": "You share no rooms in common with this user.",
"There was an error fetching shared rooms with this user.": "There was an error fetching shared rooms with this user.",
"You are both participating in <rooms></rooms>": "You are both participating in <rooms></rooms>",
"You are both participating in <rooms></rooms> and %(hidden)s more": "You are both participating in <rooms></rooms> and %(hidden)s more",
"Shared Rooms": "Shared Rooms",
"The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what Riot supports. Try with a different client.": "The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what Riot supports. Try with a different client.",
"Verify by scanning": "Verify by scanning",
"Ask %(displayName)s to scan your code:": "Ask %(displayName)s to scan your code:",
Expand Down
6 changes: 6 additions & 0 deletions src/settings/Settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,12 @@ export const SETTINGS = {
default: false,
isFeature: true,
},
"feature_show_shared_rooms": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Show rooms in common with another user in the member info panel'),
default: false,
isFeature: true,
},
"mjolnirRooms": {
supportedLevels: ['account'],
default: [],
Expand Down