Polish & delabs Exploring public spaces feature (#11423)

* Iterate search public spaces UX

* Tweak iconography in spotlight

* Delabs `Exploring public spaces`

* Tweak msc3827 v1.4 support discovery

* i18n

* Delete stale test

* Fix tests

* Iterate

* Iterate PR based on review

* Improve types

* Add shortcut to search for public spaces to create space menu

* Update import

* Add org.matrix.msc3827.stable filtering

* Fix tests

* silence some errors
This commit is contained in:
Michael Telatynski 2023-08-21 10:39:20 +01:00 committed by GitHub
parent d81f71f993
commit dd6097c568
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 180 additions and 107 deletions

View file

@ -140,12 +140,13 @@ import { SdkContextClass, SDKContext } from "../../contexts/SDKContext";
import { viewUserDeviceSettings } from "../../actions/handlers/viewUserDeviceSettings";
import { cleanUpBroadcasts, VoiceBroadcastResumer } from "../../voice-broadcast";
import GenericToast from "../views/toasts/GenericToast";
import RovingSpotlightDialog, { Filter } from "../views/dialogs/spotlight/SpotlightDialog";
import RovingSpotlightDialog from "../views/dialogs/spotlight/SpotlightDialog";
import { findDMForUser } from "../../utils/dm/findDMForUser";
import { Linkify } from "../../HtmlUtils";
import { NotificationColor } from "../../stores/notifications/NotificationColor";
import { UserTab } from "../views/dialogs/UserTab";
import { shouldSkipSetupEncryption } from "../../utils/crypto/shouldSkipSetupEncryption";
import { Filter } from "../views/dialogs/spotlight/Filter";
// legacy export
export { default as Views } from "../../Views";
@ -898,6 +899,18 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
break;
}
case Action.OpenSpotlight:
Modal.createDialog(
RovingSpotlightDialog,
{
initialText: payload.initialText,
initialFilter: payload.initialFilter,
},
"mx_SpotlightDialog_wrapper",
false,
true,
);
break;
}
};

View file

@ -22,9 +22,8 @@ import defaultDispatcher from "../../dispatcher/dispatcher";
import { ActionPayload } from "../../dispatcher/payloads";
import { IS_MAC, Key } from "../../Keyboard";
import { _t } from "../../languageHandler";
import Modal from "../../Modal";
import SpotlightDialog from "../views/dialogs/spotlight/SpotlightDialog";
import AccessibleButton from "../views/elements/AccessibleButton";
import { Action } from "../../dispatcher/actions";
interface IProps {
isMinimized: boolean;
@ -44,7 +43,7 @@ export default class RoomSearch extends React.PureComponent<IProps> {
}
private openSpotlight(): void {
Modal.createDialog(SpotlightDialog, {}, "mx_SpotlightDialog_wrapper", false, true);
defaultDispatcher.fire(Action.OpenSpotlight);
}
private onAction = (payload: ActionPayload): void => {

View file

@ -0,0 +1,21 @@
/*
Copyright 2021 - 2023 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.
*/
export enum Filter {
People,
PublicRooms,
PublicSpaces,
}

View file

@ -71,7 +71,6 @@ import DecoratedRoomAvatar from "../../avatars/DecoratedRoomAvatar";
import { SearchResultAvatar } from "../../avatars/SearchResultAvatar";
import { NetworkDropdown } from "../../directory/NetworkDropdown";
import AccessibleButton, { ButtonEvent } from "../../elements/AccessibleButton";
import LabelledCheckbox from "../../elements/LabelledCheckbox";
import Spinner from "../../elements/Spinner";
import NotificationBadge from "../../rooms/NotificationBadge";
import BaseDialog from "../BaseDialog";
@ -85,6 +84,7 @@ import RoomAvatar from "../../avatars/RoomAvatar";
import { useFeatureEnabled } from "../../../../hooks/useSettings";
import { filterBoolean } from "../../../../utils/arrays";
import { transformSearchTerm } from "../../../../utils/SearchInput";
import { Filter } from "./Filter";
const MAX_RECENT_SEARCHES = 10;
const SECTION_LIMIT = 50; // only show 50 results per section for performance reasons
@ -100,11 +100,11 @@ function refIsForRecentlyViewed(ref?: RefObject<HTMLElement>): boolean {
return ref?.current?.id?.startsWith("mx_SpotlightDialog_button_recentlyViewed_") === true;
}
function getRoomTypes(showRooms: boolean, showSpaces: boolean): Set<RoomType | null> {
function getRoomTypes(filter: Filter | null): Set<RoomType | null> {
const roomTypes = new Set<RoomType | null>();
if (showRooms) roomTypes.add(null);
if (showSpaces) roomTypes.add(RoomType.Space);
if (filter === Filter.PublicRooms) roomTypes.add(null);
if (filter === Filter.PublicSpaces) roomTypes.add(RoomType.Space);
return roomTypes;
}
@ -114,12 +114,7 @@ enum Section {
Rooms,
Spaces,
Suggestions,
PublicRooms,
}
export enum Filter {
People,
PublicRooms,
PublicRoomsAndSpaces,
}
function filterToLabel(filter: Filter): string {
@ -128,6 +123,8 @@ function filterToLabel(filter: Filter): string {
return _t("People");
case Filter.PublicRooms:
return _t("Public rooms");
case Filter.PublicSpaces:
return _t("Public spaces");
}
}
@ -168,8 +165,8 @@ const isMemberResult = (result: any): result is IMemberResult => !!result?.membe
const toPublicRoomResult = (publicRoom: IPublicRoomsChunkRoom): IPublicRoomResult => ({
publicRoom,
section: Section.PublicRooms,
filter: [Filter.PublicRooms],
section: Section.PublicRoomsAndSpaces,
filter: [Filter.PublicRooms, Filter.PublicSpaces],
query: filterBoolean([
publicRoom.room_id.toLowerCase(),
publicRoom.canonical_alias?.toLowerCase(),
@ -308,7 +305,16 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
const [inviteLinkCopied, setInviteLinkCopied] = useState<boolean>(false);
const trimmedQuery = useMemo(() => query.trim(), [query]);
const exploringPublicSpacesEnabled = useFeatureEnabled("feature_exploring_public_spaces");
const [supportsSpaceFiltering, setSupportsSpaceFiltering] = useState(true); // assume it does until we find out it doesn't
useEffect(() => {
cli.isVersionSupported("v1.4")
.then((supported) => {
return supported || cli.doesServerSupportUnstableFeature("org.matrix.msc3827.stable");
})
.then((supported) => {
setSupportsSpaceFiltering(supported);
});
}, [cli]);
const {
loading: publicRoomsLoading,
@ -319,21 +325,23 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
search: searchPublicRooms,
error: publicRoomsError,
} = usePublicRoomDirectory();
const [showRooms, setShowRooms] = useState(true);
const [showSpaces, setShowSpaces] = useState(false);
const { loading: peopleLoading, users: userDirectorySearchResults, search: searchPeople } = useUserDirectory();
const { loading: profileLoading, profile, search: searchProfileInfo } = useProfileInfo();
const searchParams: [IDirectoryOpts] = useMemo(
() => [
{
query: trimmedQuery,
roomTypes: getRoomTypes(showRooms, showSpaces),
roomTypes: getRoomTypes(filter),
limit: SECTION_LIMIT,
},
],
[trimmedQuery, showRooms, showSpaces],
[trimmedQuery, filter],
);
useDebouncedCallback(
filter === Filter.PublicRooms || filter === Filter.PublicSpaces,
searchPublicRooms,
searchParams,
);
useDebouncedCallback(filter === Filter.PublicRooms, searchPublicRooms, searchParams);
useDebouncedCallback(filter === Filter.People, searchPeople, searchParams);
useDebouncedCallback(filter === Filter.People, searchProfileInfo, searchParams);
@ -403,7 +411,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
[Section.Rooms]: [],
[Section.Spaces]: [],
[Section.Suggestions]: [],
[Section.PublicRooms]: [],
[Section.PublicRoomsAndSpaces]: [],
};
// Group results in their respective sections
@ -436,7 +444,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
results[entry.section].push(entry);
});
} else if (filter === Filter.PublicRooms) {
} else if (filter === Filter.PublicRooms || filter === Filter.PublicSpaces) {
// return all results for public rooms if no query is given
possibleResults.forEach((entry) => {
if (isPublicRoomResult(entry)) {
@ -549,6 +557,15 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
{trimmedQuery ? _t('Use "%(query)s" to search', { query }) : _t("Search for")}
</h4>
<div>
{filter !== Filter.PublicSpaces && supportsSpaceFiltering && (
<Option
id="mx_SpotlightDialog_button_explorePublicSpaces"
className="mx_SpotlightDialog_explorePublicSpaces"
onClick={() => setFilter(Filter.PublicSpaces)}
>
{filterToLabel(Filter.PublicSpaces)}
</Option>
)}
{filter !== Filter.PublicRooms && (
<Option
id="mx_SpotlightDialog_button_explorePublicRooms"
@ -769,22 +786,18 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
}
let publicRoomsSection: JSX.Element | undefined;
if (filter === Filter.PublicRooms) {
if (filter === Filter.PublicRooms || filter === Filter.PublicSpaces) {
let content: JSX.Element | JSX.Element[];
if (!showRooms && !showSpaces) {
if (publicRoomsError) {
content = (
<div className="mx_SpotlightDialog_otherSearches_messageSearchText">
{_t("You cannot search for rooms that are neither a room nor a space")}
</div>
);
} else if (publicRoomsError) {
content = (
<div className="mx_SpotlightDialog_otherSearches_messageSearchText">
{_t("Failed to query public rooms")}
{filter === Filter.PublicRooms
? _t("Failed to query public rooms")
: _t("Failed to query public spaces")}
</div>
);
} else {
content = results[Section.PublicRooms].slice(0, SECTION_LIMIT).map(resultMapper);
content = results[Section.PublicRoomsAndSpaces].slice(0, SECTION_LIMIT).map(resultMapper);
}
publicRoomsSection = (
@ -796,20 +809,6 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
<div className="mx_SpotlightDialog_sectionHeader">
<h4 id="mx_SpotlightDialog_section_publicRooms">{_t("Suggestions")}</h4>
<div className="mx_SpotlightDialog_options">
{exploringPublicSpacesEnabled && (
<>
<LabelledCheckbox
label={_t("Show rooms")}
value={showRooms}
onChange={setShowRooms}
/>
<LabelledCheckbox
label={_t("Show spaces")}
value={showSpaces}
onChange={setShowSpaces}
/>
</>
)}
<NetworkDropdown protocols={protocols} config={config ?? null} setConfig={setConfig} />
</div>
</div>
@ -919,7 +918,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
</TooltipOption>
</div>
);
} else if (trimmedQuery && filter === Filter.PublicRooms) {
} else if (trimmedQuery && (filter === Filter.PublicRooms || filter === Filter.PublicSpaces)) {
hiddenResultsSection = (
<div className="mx_SpotlightDialog_section mx_SpotlightDialog_hiddenResults" role="group">
<h4>{_t("Some results may be hidden")}</h4>
@ -1218,6 +1217,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
className={classNames("mx_SpotlightDialog_filter", {
mx_SpotlightDialog_filterPeople: filter === Filter.People,
mx_SpotlightDialog_filterPublicRooms: filter === Filter.PublicRooms,
mx_SpotlightDialog_filterPublicSpaces: filter === Filter.PublicSpaces,
})}
>
<span>{filterToLabel(filter)}</span>

View file

@ -24,6 +24,7 @@ import React, {
useState,
ChangeEvent,
ReactNode,
useEffect,
} from "react";
import classNames from "classnames";
import {
@ -48,6 +49,9 @@ import withValidation from "../elements/Validation";
import RoomAliasField from "../elements/RoomAliasField";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import { Filter } from "../dialogs/spotlight/Filter";
export const createSpace = async (
client: MatrixClient,
@ -225,6 +229,17 @@ const SpaceCreateMenu: React.FC<{
const [avatar, setAvatar] = useState<File | undefined>(undefined);
const [topic, setTopic] = useState<string>("");
const [supportsSpaceFiltering, setSupportsSpaceFiltering] = useState(true); // assume it does until we find out it doesn't
useEffect(() => {
cli.isVersionSupported("v1.4")
.then((supported) => {
return supported || cli.doesServerSupportUnstableFeature("org.matrix.msc3827.stable");
})
.then((supported) => {
setSupportsSpaceFiltering(supported);
});
}, [cli]);
const onSpaceCreateClick = async (e: ButtonEvent): Promise<void> => {
e.preventDefault();
if (busy) return;
@ -258,6 +273,13 @@ const SpaceCreateMenu: React.FC<{
}
};
const onSearchClick = (): void => {
defaultDispatcher.dispatch({
action: Action.OpenSpotlight,
initialFilter: Filter.PublicSpaces,
});
};
let body;
if (visibility === null) {
body = (
@ -283,7 +305,11 @@ const SpaceCreateMenu: React.FC<{
onClick={() => setVisibility(Visibility.Private)}
/>
<p>{_t("To join a space you'll need an invite.")}</p>
{supportsSpaceFiltering && (
<AccessibleButton kind="primary_outline" onClick={onSearchClick}>
{_t("Search for public spaces")}
</AccessibleButton>
)}
</React.Fragment>
);
} else {

View file

@ -40,7 +40,9 @@ const SpaceSettingsVisibilityTab: React.FC<IProps> = ({ matrixClient: cli, space
const [error, setError] = useState("");
const serverSupportsExploringSpaces = useAsyncMemo<boolean>(
async (): Promise<boolean> => {
return cli.doesServerSupportUnstableFeature("org.matrix.msc3827.stable");
return cli.isVersionSupported("v1.4").then((supported) => {
return supported || cli.doesServerSupportUnstableFeature("org.matrix.msc3827.stable");
});
},
[cli],
false,