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 09:39:20 +00:00
committed by GitHub
parent d81f71f993
commit dd6097c568
17 changed files with 180 additions and 107 deletions
+14 -1
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;
}
};
+2 -3
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 => {
@@ -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,
}
@@ -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>
@@ -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 {
@@ -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,
+5
View File
@@ -366,4 +366,9 @@ export enum Action {
* Fired when requesting to cancel an ask to join a room. Use with a CancelAskToJoinPayload.
*/
CancelAskToJoin = "cancel_ask_to_join",
/**
* Fired when we want to open spotlight search. Use with a OpenSpotlightPayload.
*/
OpenSpotlight = "open_spotlight",
}
@@ -0,0 +1,26 @@
/*
Copyright 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.
*/
import { Action } from "../actions";
import { ActionPayload } from "../payloads";
import { Filter } from "../../components/views/dialogs/spotlight/Filter";
export interface OpenSpotlightPayload extends ActionPayload {
action: Action.OpenSpotlight;
initialFilter?: Filter;
initialText?: string;
}
+1 -1
View File
@@ -20,7 +20,7 @@ const DEBOUNCE_TIMEOUT = 100;
export function useDebouncedCallback<T extends any[]>(
enabled: boolean,
callback: (...params: T) => void,
callback: (...params: T) => unknown,
params: T,
): void {
useEffect(() => {
+3 -6
View File
@@ -992,8 +992,6 @@
"New Notification Settings": "New Notification Settings",
"Notification Settings": "Notification Settings",
"Introducing a simpler way to change your notification settings. Customize your %(brand)s, just the way you like.": "Introducing a simpler way to change your notification settings. Customize your %(brand)s, just the way you like.",
"Explore public spaces in the new search dialog": "Explore public spaces in the new search dialog",
"Requires your server to support the stable version of MSC3827": "Requires your server to support the stable version of MSC3827",
"Let moderators hide messages pending moderation.": "Let moderators hide messages pending moderation.",
"Report to moderators": "Report to moderators",
"In rooms that support moderation, the “Report” button will let you report abuse to room moderators.": "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.",
@@ -1269,7 +1267,7 @@
"Open space for anyone, best for communities": "Open space for anyone, best for communities",
"Private": "Private",
"Invite only, best for yourself or teams": "Invite only, best for yourself or teams",
"To join a space you'll need an invite.": "To join a space you'll need an invite.",
"Search for public spaces": "Search for public spaces",
"Go back": "Go back",
"Your public space": "Your public space",
"Your private space": "Your private space",
@@ -3254,14 +3252,13 @@
"one": "%(count)s Member"
},
"Public rooms": "Public rooms",
"Public spaces": "Public spaces",
"Use \"%(query)s\" to search": "Use \"%(query)s\" to search",
"Search for": "Search for",
"View": "View",
"Spaces you're in": "Spaces you're in",
"You cannot search for rooms that are neither a room nor a space": "You cannot search for rooms that are neither a room nor a space",
"Failed to query public rooms": "Failed to query public rooms",
"Show rooms": "Show rooms",
"Show spaces": "Show spaces",
"Failed to query public spaces": "Failed to query public spaces",
"Other rooms in %(spaceName)s": "Other rooms in %(spaceName)s",
"Join %(roomAddress)s": "Join %(roomAddress)s",
"Some results may be hidden for privacy": "Some results may be hidden for privacy",
-14
View File
@@ -252,20 +252,6 @@ export const SETTINGS: { [setting: string]: ISetting } = {
),
},
},
"feature_exploring_public_spaces": {
isFeature: true,
labsGroup: LabGroup.Spaces,
displayName: _td("Explore public spaces in the new search dialog"),
supportedLevels: LEVELS_FEATURE,
default: false,
controller: new ServerSupportUnstableFeatureController(
"feature_exploring_public_spaces",
defaultWatchManager,
[["org.matrix.msc3827.stable"]],
"v1.4",
_td("Requires your server to support the stable version of MSC3827"),
),
},
"feature_msc3531_hide_messages_pending_moderation": {
isFeature: true,
labsGroup: LabGroup.Moderation,