Merge pull request #5139 from matrix-org/travis/communities/room-behaviour

Communities v2 prototype: Explore rooms, global state, and default room
This commit is contained in:
Travis Ralston 2020-08-24 07:43:01 -06:00 committed by GitHub
commit 84d782022f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 230 additions and 38 deletions

View file

@ -51,9 +51,9 @@ limitations under the License.
.mx_TagPanel .mx_TagPanel_divider { .mx_TagPanel .mx_TagPanel_divider {
height: 0px; height: 0px;
width: 34px; width: 90%;
border-bottom: 1px solid $panel-divider-color; border: none;
display: none; border-bottom: 1px solid $tagpanel-divider-color;
} }
.mx_TagPanel .mx_TagPanel_scroller { .mx_TagPanel .mx_TagPanel_scroller {
@ -116,6 +116,10 @@ limitations under the License.
border-radius: 0 3px 3px 0; border-radius: 0 3px 3px 0;
} }
.mx_TagPanel .mx_TagTile.mx_TagTile_large.mx_TagTile_selected::before {
left: -10px;
}
.mx_TagPanel .mx_TagTile.mx_AccessibleButton:focus { .mx_TagPanel .mx_TagTile.mx_AccessibleButton:focus {
filter: none; filter: none;
} }

View file

@ -119,6 +119,8 @@ $roomlist-bg-color: rgba(33, 38, 44, 0.90);
$roomlist-header-color: $tertiary-fg-color; $roomlist-header-color: $tertiary-fg-color;
$roomsublist-divider-color: $primary-fg-color; $roomsublist-divider-color: $primary-fg-color;
$tagpanel-divider-color: $roomlist-header-color;
$roomtile-preview-color: $secondary-fg-color; $roomtile-preview-color: $secondary-fg-color;
$roomtile-default-badge-bg-color: #61708b; $roomtile-default-badge-bg-color: #61708b;
$roomtile-selected-bg-color: rgba(141, 151, 165, 0.2); $roomtile-selected-bg-color: rgba(141, 151, 165, 0.2);

View file

@ -116,6 +116,8 @@ $roomlist-bg-color: $header-panel-bg-color;
$roomsublist-divider-color: $primary-fg-color; $roomsublist-divider-color: $primary-fg-color;
$tagpanel-divider-color: $roomlist-header-color;
$roomtile-preview-color: #9e9e9e; $roomtile-preview-color: #9e9e9e;
$roomtile-default-badge-bg-color: #61708b; $roomtile-default-badge-bg-color: #61708b;
$roomtile-selected-bg-color: #1A1D23; $roomtile-selected-bg-color: #1A1D23;

View file

@ -183,6 +183,8 @@ $roomlist-bg-color: $header-panel-bg-color;
$roomlist-header-color: $primary-fg-color; $roomlist-header-color: $primary-fg-color;
$roomsublist-divider-color: $primary-fg-color; $roomsublist-divider-color: $primary-fg-color;
$tagpanel-divider-color: $roomlist-header-color;
$roomtile-preview-color: #9e9e9e; $roomtile-preview-color: #9e9e9e;
$roomtile-default-badge-bg-color: #61708b; $roomtile-default-badge-bg-color: #61708b;
$roomtile-selected-bg-color: #fff; $roomtile-selected-bg-color: #fff;

View file

@ -177,6 +177,8 @@ $roomlist-bg-color: rgba(245, 245, 245, 0.90);
$roomlist-header-color: $tertiary-fg-color; $roomlist-header-color: $tertiary-fg-color;
$roomsublist-divider-color: $primary-fg-color; $roomsublist-divider-color: $primary-fg-color;
$tagpanel-divider-color: $roomlist-header-color;
$roomtile-preview-color: $secondary-fg-color; $roomtile-preview-color: $secondary-fg-color;
$roomtile-default-badge-bg-color: #61708b; $roomtile-default-badge-bg-color: #61708b;
$roomtile-selected-bg-color: #FFF; $roomtile-selected-bg-color: #FFF;

View file

@ -30,6 +30,10 @@ import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/Di
import Analytics from '../../Analytics'; import Analytics from '../../Analytics';
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
import {ALL_ROOMS} from "../views/directory/NetworkDropdown"; import {ALL_ROOMS} from "../views/directory/NetworkDropdown";
import SettingsStore from "../../settings/SettingsStore";
import TagOrderStore from "../../stores/TagOrderStore";
import GroupStore from "../../stores/GroupStore";
import FlairStore from "../../stores/FlairStore";
const MAX_NAME_LENGTH = 80; const MAX_NAME_LENGTH = 80;
const MAX_TOPIC_LENGTH = 160; const MAX_TOPIC_LENGTH = 160;
@ -46,6 +50,7 @@ export default createReactClass({
}, },
getInitialState: function() { getInitialState: function() {
const selectedCommunityId = TagOrderStore.getSelectedTags()[0];
return { return {
publicRooms: [], publicRooms: [],
loading: true, loading: true,
@ -54,6 +59,10 @@ export default createReactClass({
instanceId: undefined, instanceId: undefined,
roomServer: MatrixClientPeg.getHomeserverName(), roomServer: MatrixClientPeg.getHomeserverName(),
filterString: null, filterString: null,
selectedCommunityId: SettingsStore.getValue("feature_communities_v2_prototypes")
? selectedCommunityId
: null,
communityName: null,
}; };
}, },
@ -71,28 +80,39 @@ export default createReactClass({
this.setState({protocolsLoading: false}); this.setState({protocolsLoading: false});
return; return;
} }
MatrixClientPeg.get().getThirdpartyProtocols().then((response) => {
this.protocols = response; if (!this.state.selectedCommunityId) {
this.setState({protocolsLoading: false}); MatrixClientPeg.get().getThirdpartyProtocols().then((response) => {
}, (err) => { this.protocols = response;
console.warn(`error loading third party protocols: ${err}`); this.setState({protocolsLoading: false});
this.setState({protocolsLoading: false}); }, (err) => {
if (MatrixClientPeg.get().isGuest()) { console.warn(`error loading third party protocols: ${err}`);
// Guests currently aren't allowed to use this API, so this.setState({protocolsLoading: false});
// ignore this as otherwise this error is literally the if (MatrixClientPeg.get().isGuest()) {
// thing you see when loading the client! // Guests currently aren't allowed to use this API, so
return; // ignore this as otherwise this error is literally the
} // thing you see when loading the client!
track('Failed to get protocol list from homeserver'); return;
const brand = SdkConfig.get().brand; }
this.setState({ track('Failed to get protocol list from homeserver');
error: _t( const brand = SdkConfig.get().brand;
'%(brand)s failed to get the protocol list from the homeserver. ' + this.setState({
'The homeserver may be too old to support third party networks.', error: _t(
{ brand }, '%(brand)s failed to get the protocol list from the homeserver. ' +
), 'The homeserver may be too old to support third party networks.',
{brand},
),
});
}); });
}); } else {
// We don't use the protocols in the communities v2 prototype experience
this.setState({protocolsLoading: false});
// Grab the profile info async
FlairStore.getGroupProfileCached(MatrixClientPeg.get(), this.state.selectedCommunityId).then(profile => {
this.setState({communityName: profile.name});
});
}
this.refreshRoomList(); this.refreshRoomList();
}, },
@ -105,6 +125,33 @@ export default createReactClass({
}, },
refreshRoomList: function() { refreshRoomList: function() {
if (this.state.selectedCommunityId) {
this.setState({
publicRooms: GroupStore.getGroupRooms(this.state.selectedCommunityId).map(r => {
return {
// Translate all the group properties to the directory format
room_id: r.roomId,
name: r.name,
topic: r.topic,
canonical_alias: r.canonicalAlias,
num_joined_members: r.numJoinedMembers,
avatarUrl: r.avatarUrl,
world_readable: r.worldReadable,
guest_can_join: r.guestsCanJoin,
};
}).filter(r => {
const filterString = this.state.filterString;
if (filterString) {
const containedIn = (s: string) => (s || "").toLowerCase().includes(filterString.toLowerCase());
return containedIn(r.name) || containedIn(r.topic) || containedIn(r.canonical_alias);
}
return true;
}),
loading: false,
});
return;
}
this.nextBatch = null; this.nextBatch = null;
this.setState({ this.setState({
publicRooms: [], publicRooms: [],
@ -114,6 +161,7 @@ export default createReactClass({
}, },
getMoreRooms: function() { getMoreRooms: function() {
if (this.state.selectedCommunityId) return Promise.resolve(); // no more rooms
if (!MatrixClientPeg.get()) return Promise.resolve(); if (!MatrixClientPeg.get()) return Promise.resolve();
this.setState({ this.setState({
@ -239,7 +287,7 @@ export default createReactClass({
}, },
onRoomClicked: function(room, ev) { onRoomClicked: function(room, ev) {
if (ev.shiftKey) { if (ev.shiftKey && !this.state.selectedCommunityId) {
ev.preventDefault(); ev.preventDefault();
this.removeFromDirectory(room); this.removeFromDirectory(room);
} else { } else {
@ -610,6 +658,18 @@ export default createReactClass({
} }
} }
let dropdown = (
<NetworkDropdown
protocols={this.protocols}
onOptionChange={this.onOptionChange}
selectedServerName={this.state.roomServer}
selectedInstanceId={this.state.instanceId}
/>
);
if (this.state.selectedCommunityId) {
dropdown = null;
}
listHeader = <div className="mx_RoomDirectory_listheader"> listHeader = <div className="mx_RoomDirectory_listheader">
<DirectorySearchBox <DirectorySearchBox
className="mx_RoomDirectory_searchbox" className="mx_RoomDirectory_searchbox"
@ -619,12 +679,7 @@ export default createReactClass({
placeholder={placeholder} placeholder={placeholder}
showJoinButton={showJoinButton} showJoinButton={showJoinButton}
/> />
<NetworkDropdown {dropdown}
protocols={this.protocols}
onOptionChange={this.onOptionChange}
selectedServerName={this.state.roomServer}
selectedInstanceId={this.state.instanceId}
/>
</div>; </div>;
} }
const explanation = const explanation =
@ -637,12 +692,16 @@ export default createReactClass({
}}, }},
); );
const title = this.state.selectedCommunityId
? _t("Explore rooms in %(communityName)s", {
communityName: this.state.communityName || this.state.selectedCommunityId,
}) : _t("Explore rooms");
return ( return (
<BaseDialog <BaseDialog
className={'mx_RoomDirectory_dialog'} className={'mx_RoomDirectory_dialog'}
hasCancel={true} hasCancel={true}
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
title={_t("Explore rooms")} title={title}
> >
<div className="mx_RoomDirectory"> <div className="mx_RoomDirectory">
{explanation} {explanation}

View file

@ -29,6 +29,8 @@ import { Droppable } from 'react-beautiful-dnd';
import classNames from 'classnames'; import classNames from 'classnames';
import MatrixClientContext from "../../contexts/MatrixClientContext"; import MatrixClientContext from "../../contexts/MatrixClientContext";
import AutoHideScrollbar from "./AutoHideScrollbar"; import AutoHideScrollbar from "./AutoHideScrollbar";
import SettingsStore from "../../settings/SettingsStore";
import UserTagTile from "../views/elements/UserTagTile";
const TagPanel = createReactClass({ const TagPanel = createReactClass({
displayName: 'TagPanel', displayName: 'TagPanel',
@ -102,6 +104,17 @@ const TagPanel = createReactClass({
dis.dispatch({action: 'deselect_tags'}); dis.dispatch({action: 'deselect_tags'});
}, },
renderGlobalIcon() {
if (!SettingsStore.getValue("feature_communities_v2_prototypes")) return null;
return (
<div>
<UserTagTile />
<hr className="mx_TagPanel_divider" />
</div>
);
},
render() { render() {
const DNDTagTile = sdk.getComponent('elements.DNDTagTile'); const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
@ -137,7 +150,6 @@ const TagPanel = createReactClass({
<div className="mx_TagPanel_clearButton_container"> <div className="mx_TagPanel_clearButton_container">
{ clearButton } { clearButton }
</div> </div>
<div className="mx_TagPanel_divider" />
<AutoHideScrollbar <AutoHideScrollbar
className="mx_TagPanel_scroller" className="mx_TagPanel_scroller"
// XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273 // XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
@ -153,6 +165,7 @@ const TagPanel = createReactClass({
className="mx_TagPanel_tagTileContainer" className="mx_TagPanel_tagTileContainer"
ref={provided.innerRef} ref={provided.innerRef}
> >
{ this.renderGlobalIcon() }
{ tags } { tags }
<div> <div>
<ActionButton <ActionButton

View file

@ -83,11 +83,18 @@ export default createReactClass({
localpart: this.state.groupId, localpart: this.state.groupId,
profile: profile, profile: profile,
}).then((result) => { }).then((result) => {
dis.dispatch({ if (result.room_id) {
action: 'view_group', dis.dispatch({
group_id: result.group_id, action: 'view_room',
group_is_new: true, room_id: result.room_id,
}); });
} else {
dis.dispatch({
action: 'view_group',
group_id: result.group_id,
group_is_new: true,
});
}
this.props.onFinished(true); this.props.onFinished(true);
}).catch((e) => { }).catch((e) => {
this.setState({createError: e}); this.setState({createError: e});

View file

@ -47,6 +47,7 @@ export default createReactClass({
contextMenuButtonRef: PropTypes.object, contextMenuButtonRef: PropTypes.object,
openMenu: PropTypes.func, openMenu: PropTypes.func,
menuDisplayed: PropTypes.bool, menuDisplayed: PropTypes.bool,
selected: PropTypes.bool,
}, },
statics: { statics: {

View file

@ -0,0 +1,99 @@
/*
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 defaultDispatcher from "../../../dispatcher/dispatcher";
import { OwnProfileStore } from "../../../stores/OwnProfileStore";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import * as fbEmitter from "fbemitter";
import TagOrderStore from "../../../stores/TagOrderStore";
import AccessibleTooltipButton from "./AccessibleTooltipButton";
import BaseAvatar from "../avatars/BaseAvatar";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import classNames from "classnames";
interface IProps{}
interface IState {
selected: boolean;
}
export default class UserTagTile extends React.PureComponent<IProps, IState> {
private tagStoreRef: fbEmitter.EventSubscription;
constructor(props: IProps) {
super(props);
this.state = {
selected: TagOrderStore.getSelectedTags().length === 0,
};
}
public componentDidMount() {
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
this.tagStoreRef = TagOrderStore.addListener(this.onTagStoreUpdate);
}
public componentWillUnmount() {
OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
}
private onProfileUpdate = () => {
this.forceUpdate();
};
private onTagStoreUpdate = () => {
const selected = TagOrderStore.getSelectedTags().length === 0;
this.setState({selected});
};
private onTileClick = (ev) => {
ev.preventDefault();
ev.stopPropagation();
// Deselect all tags
defaultDispatcher.dispatch({action: "deselect_tags"});
};
public render() {
// XXX: We reuse TagTile classes for ease of demonstration - we should probably generify
// TagTile instead if we continue to use this component.
const avatarHeight = 36;
const name = OwnProfileStore.instance.displayName || MatrixClientPeg.get().getUserId();
const className = classNames({
mx_TagTile: true,
mx_TagTile_selected: this.state.selected,
mx_TagTile_large: true,
});
return (
<AccessibleTooltipButton
className={className}
onClick={this.onTileClick}
title={name}
>
<div className="mx_TagTile_avatar">
<BaseAvatar
name={name}
idName={MatrixClientPeg.get().getUserId()}
url={OwnProfileStore.instance.getHttpAvatarUrl(avatarHeight)}
width={avatarHeight}
height={avatarHeight}
/>
</div>
</AccessibleTooltipButton>
);
}
}

View file

@ -2064,6 +2064,7 @@
"Find a room…": "Find a room…", "Find a room…": "Find a room…",
"Find a room… (e.g. %(exampleRoom)s)": "Find a room… (e.g. %(exampleRoom)s)", "Find a room… (e.g. %(exampleRoom)s)": "Find a room… (e.g. %(exampleRoom)s)",
"If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.", "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.",
"Explore rooms in %(communityName)s": "Explore rooms in %(communityName)s",
"Clear filter": "Clear filter", "Clear filter": "Clear filter",
"Search rooms": "Search rooms", "Search rooms": "Search rooms",
"You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.": "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.", "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.": "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.",