From 0a084601c405c27de832a728c9282e55753b8e94 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 15 Feb 2022 13:36:32 +0000 Subject: [PATCH] Abstract spotlight to allow non-room results too (#7804) --- res/css/views/dialogs/_SpotlightDialog.scss | 24 +++ .../views/dialogs/SpotlightDialog.tsx | 155 ++++++++++++++---- 2 files changed, 147 insertions(+), 32 deletions(-) diff --git a/res/css/views/dialogs/_SpotlightDialog.scss b/res/css/views/dialogs/_SpotlightDialog.scss index 0a999bce05..dd752ecdef 100644 --- a/res/css/views/dialogs/_SpotlightDialog.scss +++ b/res/css/views/dialogs/_SpotlightDialog.scss @@ -146,6 +146,7 @@ limitations under the License. text-overflow: ellipsis; overflow: hidden; + .mx_SpotlightDialog_metaspaceResult, .mx_DecoratedRoomAvatar { margin-right: 8px; width: 20px; @@ -249,6 +250,29 @@ limitations under the License. margin: 0 4px 0 auto; display: none; } + + .mx_SpotlightDialog_metaspaceResult { + background-color: $secondary-content; + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + + &.mx_SpotlightDialog_metaspaceResult_home-space { + mask-image: url('$(res)/img/element-icons/home.svg'); + } + + &.mx_SpotlightDialog_metaspaceResult_favourites-space { + mask-image: url('$(res)/img/element-icons/roomlist/favorite.svg'); + } + + &.mx_SpotlightDialog_metaspaceResult_people-space { + mask-image: url('$(res)/img/element-icons/room/members.svg'); + } + + &.mx_SpotlightDialog_metaspaceResult_orphans-space { + mask-image: url('$(res)/img/element-icons/roomlist/hash-circle.svg'); + } + } } .mx_SpotlightDialog_footer { diff --git a/src/components/views/dialogs/SpotlightDialog.tsx b/src/components/views/dialogs/SpotlightDialog.tsx index 2d60d4fbf3..08cbdf4397 100644 --- a/src/components/views/dialogs/SpotlightDialog.tsx +++ b/src/components/views/dialogs/SpotlightDialog.tsx @@ -69,6 +69,9 @@ import { UserTab } from "./UserSettingsDialog"; import BetaFeedbackDialog from "./BetaFeedbackDialog"; import SdkConfig from "../../../SdkConfig"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; +import { getMetaSpaceName } from "../../../stores/spaces"; +import { getKeyBindingsManager } from "../../../KeyBindingsManager"; +import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; const MAX_RECENT_SEARCHES = 10; const SECTION_LIMIT = 50; // only show 50 results per section for performance reasons @@ -175,23 +178,96 @@ function refIsForRecentlyViewed(ref: RefObject): boolean { return ref.current?.id.startsWith("mx_SpotlightDialog_button_recentlyViewed_"); } +enum Section { + People, + Rooms, + Spaces, +} + +interface IBaseResult { + section: Section; + query?: string[]; // extra fields to query match, stored as lowercase +} + +interface IRoomResult extends IBaseResult { + room: Room; +} + +interface IResult extends IBaseResult { + avatar: JSX.Element; + name: string; + description?: string; + onClick?(): void; +} + +type Result = IRoomResult | IResult; + +const isRoomResult = (result: any): result is IRoomResult => !!result?.room; + const SpotlightDialog: React.FC = ({ initialText = "", onFinished }) => { const cli = MatrixClientPeg.get(); const rovingContext = useContext(RovingTabIndexContext); const [query, _setQuery] = useState(initialText); const [recentSearches, clearRecentSearches] = useRecentSearches(); - const results = useMemo(() => { - if (!query) return null; + const possibleResults = useMemo(() => [ + ...SpaceStore.instance.enabledMetaSpaces.map(spaceKey => ({ + section: Section.Spaces, + avatar: ( +
+ ), + name: getMetaSpaceName(spaceKey, SpaceStore.instance.allRoomsInHome), + onClick() { + SpaceStore.instance.setActiveSpace(spaceKey); + }, + })), + ...cli.getVisibleRooms().map(room => { + let section: Section; + let query: string[]; + + const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId); + if (otherUserId) { + section = Section.People; + query = [ + otherUserId.toLowerCase(), + room.getMember(otherUserId)?.name.toLowerCase(), + ].filter(Boolean); + } else if (room.isSpaceRoom()) { + section = Section.Spaces; + } else { + section = Section.Rooms; + } + + return { room, section, query }; + }), + ], [cli]); + + const trimmedQuery = query.trim(); + const [people, rooms, spaces] = useMemo<[Result[], Result[], Result[]] | []>(() => { + if (!trimmedQuery) return []; - const trimmedQuery = query.trim(); const lcQuery = trimmedQuery.toLowerCase(); const normalizedQuery = normalize(trimmedQuery); - return cli.getVisibleRooms().filter(r => { - return r.getCanonicalAlias()?.includes(lcQuery) || r.normalizedName.includes(normalizedQuery); + const results: [Result[], Result[], Result[]] = [[], [], []]; + + possibleResults.forEach(entry => { + if (isRoomResult(entry)) { + if (!entry.room.normalizedName.includes(normalizedQuery) && + !entry.room.getCanonicalAlias()?.toLowerCase().includes(lcQuery) && + !entry.query?.some(q => q.includes(lcQuery)) + ) return; // bail, does not match query + } else { + if (!entry.name.toLowerCase().includes(lcQuery) && + !entry.query?.some(q => q.includes(lcQuery)) + ) return; // bail, does not match query + } + + results[entry.section].push(entry); }); - }, [cli, query]); + + return results; + }, [possibleResults, trimmedQuery]); const activeSpace = SpaceStore.instance.activeSpaceRoom; const [spaceResults, spaceResultsLoading] = useSpaceResults(activeSpace, query); @@ -240,29 +316,40 @@ const SpotlightDialog: React.FC = ({ initialText = "", onFinished }) => }; let content: JSX.Element; - if (results) { - const [people, rooms, spaces] = results.reduce((result, room: Room) => { - if (room.isSpaceRoom()) result[2].push(room); - else if (!DMRoomMap.shared().getUserIdForRoomId(room.roomId)) result[1].push(room); - else result[0].push(room); - return result; - }, [[], [], []] as [Room[], Room[], Room[]]); + if (trimmedQuery) { + const resultMapper = (result: Result): JSX.Element => { + if (isRoomResult(result)) { + return ( + + ); + } - const resultMapper = (room: Room): JSX.Element => ( - - ); + const otherResult = (result as IResult); + return ( + + ); + }; let peopleSection: JSX.Element; if (people.length) { @@ -454,10 +541,14 @@ const SpotlightDialog: React.FC = ({ initialText = "", onFinished }) => } const onDialogKeyDown = (ev: KeyboardEvent) => { - if (ev.key === Key.ESCAPE) { - ev.stopPropagation(); - ev.preventDefault(); - onFinished(); + const navAction = getKeyBindingsManager().getNavigationAction(ev); + switch (navAction) { + case "KeyBinding.closeDialogOrContextMenu" as KeyBindingAction: + case KeyBindingAction.FilterRooms: + ev.stopPropagation(); + ev.preventDefault(); + onFinished(); + break; } };