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:
parent
d81f71f993
commit
dd6097c568
17 changed files with 180 additions and 107 deletions
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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 => {
|
||||
|
|
21
src/components/views/dialogs/spotlight/Filter.ts
Normal file
21
src/components/views/dialogs/spotlight/Filter.ts
Normal 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,
|
||||
}
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue