From 5c014d41247f7733d029a2b601eddc0fb8becf69 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 15 Feb 2022 11:36:03 +0000 Subject: [PATCH 1/3] Abstract spotlight to allow non-room results too --- res/css/views/dialogs/_SpotlightDialog.scss | 24 +++ .../views/dialogs/SpotlightDialog.tsx | 144 ++++++++++++++---- 2 files changed, 139 insertions(+), 29 deletions(-) diff --git a/res/css/views/dialogs/_SpotlightDialog.scss b/res/css/views/dialogs/_SpotlightDialog.scss index 0a999bce05b..dd752ecdef5 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 2d60d4fbf33..eacc4801f1e 100644 --- a/src/components/views/dialogs/SpotlightDialog.tsx +++ b/src/components/views/dialogs/SpotlightDialog.tsx @@ -69,6 +69,7 @@ import { UserTab } from "./UserSettingsDialog"; import BetaFeedbackDialog from "./BetaFeedbackDialog"; import SdkConfig from "../../../SdkConfig"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; +import { getMetaSpaceName } from "../../../stores/spaces"; const MAX_RECENT_SEARCHES = 10; const SECTION_LIMIT = 50; // only show 50 results per section for performance reasons @@ -175,23 +176,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 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 ((entry as IRoomResult).room) { + const roomResult = entry as IRoomResult; + if (!roomResult.room.normalizedName.includes(normalizedQuery) && + !roomResult.room.getCanonicalAlias()?.toLowerCase().includes(lcQuery) && + !roomResult.query?.some(q => q.includes(lcQuery)) + ) return; // bail, does not match query + } else { + const otherResult = entry as IResult; + if (!otherResult.name.toLowerCase().includes(lcQuery) && + !otherResult.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 +314,41 @@ 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[]]); - - const resultMapper = (room: Room): JSX.Element => ( - - ); + if (trimmedQuery) { + const resultMapper = (result: Result): JSX.Element => { + const room = (result as IRoomResult).room; + if (room) { + return ( + + ); + } + + const otherResult = (result as IResult); + return ( + + ); + }; let peopleSection: JSX.Element; if (people.length) { From a55be12e6ad4d1aeb918c4b1cdabd7c2abdea264 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 15 Feb 2022 11:45:34 +0000 Subject: [PATCH 2/3] Make cmd-k toggle the search dialog --- src/components/views/dialogs/SpotlightDialog.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/views/dialogs/SpotlightDialog.tsx b/src/components/views/dialogs/SpotlightDialog.tsx index eacc4801f1e..95a6c1ace08 100644 --- a/src/components/views/dialogs/SpotlightDialog.tsx +++ b/src/components/views/dialogs/SpotlightDialog.tsx @@ -70,6 +70,8 @@ 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 @@ -540,10 +542,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; } }; From 00d9568d4a2530d0352b8961586fb4e5fe1bfed8 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 15 Feb 2022 13:26:33 +0000 Subject: [PATCH 3/3] tidy --- .../views/dialogs/SpotlightDialog.tsx | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/components/views/dialogs/SpotlightDialog.tsx b/src/components/views/dialogs/SpotlightDialog.tsx index 95a6c1ace08..08cbdf43973 100644 --- a/src/components/views/dialogs/SpotlightDialog.tsx +++ b/src/components/views/dialogs/SpotlightDialog.tsx @@ -202,6 +202,8 @@ interface IResult extends IBaseResult { 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); @@ -250,16 +252,14 @@ const SpotlightDialog: React.FC = ({ initialText = "", onFinished }) => const results: [Result[], Result[], Result[]] = [[], [], []]; possibleResults.forEach(entry => { - if ((entry as IRoomResult).room) { - const roomResult = entry as IRoomResult; - if (!roomResult.room.normalizedName.includes(normalizedQuery) && - !roomResult.room.getCanonicalAlias()?.toLowerCase().includes(lcQuery) && - !roomResult.query?.some(q => q.includes(lcQuery)) + 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 { - const otherResult = entry as IResult; - if (!otherResult.name.toLowerCase().includes(lcQuery) && - !otherResult.query?.some(q => q.includes(lcQuery)) + if (!entry.name.toLowerCase().includes(lcQuery) && + !entry.query?.some(q => q.includes(lcQuery)) ) return; // bail, does not match query } @@ -318,20 +318,19 @@ const SpotlightDialog: React.FC = ({ initialText = "", onFinished }) => let content: JSX.Element; if (trimmedQuery) { const resultMapper = (result: Result): JSX.Element => { - const room = (result as IRoomResult).room; - if (room) { + if (isRoomResult(result)) { return ( );