Abstract spotlight to allow non-room results too (#7804)
This commit is contained in:
parent
201e552b0c
commit
0a084601c4
2 changed files with 147 additions and 32 deletions
|
@ -146,6 +146,7 @@ limitations under the License.
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
.mx_SpotlightDialog_metaspaceResult,
|
||||||
.mx_DecoratedRoomAvatar {
|
.mx_DecoratedRoomAvatar {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
|
@ -249,6 +250,29 @@ limitations under the License.
|
||||||
margin: 0 4px 0 auto;
|
margin: 0 4px 0 auto;
|
||||||
display: none;
|
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 {
|
.mx_SpotlightDialog_footer {
|
||||||
|
|
|
@ -69,6 +69,9 @@ import { UserTab } from "./UserSettingsDialog";
|
||||||
import BetaFeedbackDialog from "./BetaFeedbackDialog";
|
import BetaFeedbackDialog from "./BetaFeedbackDialog";
|
||||||
import SdkConfig from "../../../SdkConfig";
|
import SdkConfig from "../../../SdkConfig";
|
||||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
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 MAX_RECENT_SEARCHES = 10;
|
||||||
const SECTION_LIMIT = 50; // only show 50 results per section for performance reasons
|
const SECTION_LIMIT = 50; // only show 50 results per section for performance reasons
|
||||||
|
@ -175,23 +178,96 @@ function refIsForRecentlyViewed(ref: RefObject<HTMLElement>): boolean {
|
||||||
return ref.current?.id.startsWith("mx_SpotlightDialog_button_recentlyViewed_");
|
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<IProps> = ({ initialText = "", onFinished }) => {
|
const SpotlightDialog: React.FC<IProps> = ({ initialText = "", onFinished }) => {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const rovingContext = useContext(RovingTabIndexContext);
|
const rovingContext = useContext(RovingTabIndexContext);
|
||||||
const [query, _setQuery] = useState(initialText);
|
const [query, _setQuery] = useState(initialText);
|
||||||
const [recentSearches, clearRecentSearches] = useRecentSearches();
|
const [recentSearches, clearRecentSearches] = useRecentSearches();
|
||||||
|
|
||||||
const results = useMemo<Room[] | null>(() => {
|
const possibleResults = useMemo<Result[]>(() => [
|
||||||
if (!query) return null;
|
...SpaceStore.instance.enabledMetaSpaces.map(spaceKey => ({
|
||||||
|
section: Section.Spaces,
|
||||||
|
avatar: (
|
||||||
|
<div className={`mx_SpotlightDialog_metaspaceResult mx_SpotlightDialog_metaspaceResult_${spaceKey}`} />
|
||||||
|
),
|
||||||
|
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 lcQuery = trimmedQuery.toLowerCase();
|
||||||
const normalizedQuery = normalize(trimmedQuery);
|
const normalizedQuery = normalize(trimmedQuery);
|
||||||
|
|
||||||
return cli.getVisibleRooms().filter(r => {
|
const results: [Result[], Result[], Result[]] = [[], [], []];
|
||||||
return r.getCanonicalAlias()?.includes(lcQuery) || r.normalizedName.includes(normalizedQuery);
|
|
||||||
|
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 activeSpace = SpaceStore.instance.activeSpaceRoom;
|
||||||
const [spaceResults, spaceResultsLoading] = useSpaceResults(activeSpace, query);
|
const [spaceResults, spaceResultsLoading] = useSpaceResults(activeSpace, query);
|
||||||
|
@ -240,29 +316,40 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", onFinished }) =>
|
||||||
};
|
};
|
||||||
|
|
||||||
let content: JSX.Element;
|
let content: JSX.Element;
|
||||||
if (results) {
|
if (trimmedQuery) {
|
||||||
const [people, rooms, spaces] = results.reduce((result, room: Room) => {
|
const resultMapper = (result: Result): JSX.Element => {
|
||||||
if (room.isSpaceRoom()) result[2].push(room);
|
if (isRoomResult(result)) {
|
||||||
else if (!DMRoomMap.shared().getUserIdForRoomId(room.roomId)) result[1].push(room);
|
return (
|
||||||
else result[0].push(room);
|
<Option
|
||||||
return result;
|
id={`mx_SpotlightDialog_button_result_${result.room.roomId}`}
|
||||||
}, [[], [], []] as [Room[], Room[], Room[]]);
|
key={result.room.roomId}
|
||||||
|
onClick={(ev) => {
|
||||||
|
viewRoom(result.room.roomId, true, ev.type !== "click");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DecoratedRoomAvatar room={result.room} avatarSize={20} tooltipProps={{ tabIndex: -1 }} />
|
||||||
|
{ result.room.name }
|
||||||
|
<NotificationBadge notification={RoomNotificationStateStore.instance.getRoomState(result.room)} />
|
||||||
|
<ResultDetails room={result.room} />
|
||||||
|
<div className="mx_SpotlightDialog_enterPrompt">↵</div>
|
||||||
|
</Option>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const resultMapper = (room: Room): JSX.Element => (
|
const otherResult = (result as IResult);
|
||||||
<Option
|
return (
|
||||||
id={`mx_SpotlightDialog_button_result_${room.roomId}`}
|
<Option
|
||||||
key={room.roomId}
|
id={`mx_SpotlightDialog_button_result_${otherResult.name}`}
|
||||||
onClick={(ev) => {
|
key={otherResult.name}
|
||||||
viewRoom(room.roomId, true, ev.type !== "click");
|
onClick={otherResult.onClick}
|
||||||
}}
|
>
|
||||||
>
|
{ otherResult.avatar }
|
||||||
<DecoratedRoomAvatar room={room} avatarSize={20} tooltipProps={{ tabIndex: -1 }} />
|
{ otherResult.name }
|
||||||
{ room.name }
|
{ otherResult.description }
|
||||||
<NotificationBadge notification={RoomNotificationStateStore.instance.getRoomState(room)} />
|
<div className="mx_SpotlightDialog_enterPrompt">↵</div>
|
||||||
<ResultDetails room={room} />
|
</Option>
|
||||||
<div className="mx_SpotlightDialog_enterPrompt">↵</div>
|
);
|
||||||
</Option>
|
};
|
||||||
);
|
|
||||||
|
|
||||||
let peopleSection: JSX.Element;
|
let peopleSection: JSX.Element;
|
||||||
if (people.length) {
|
if (people.length) {
|
||||||
|
@ -454,10 +541,14 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", onFinished }) =>
|
||||||
}
|
}
|
||||||
|
|
||||||
const onDialogKeyDown = (ev: KeyboardEvent) => {
|
const onDialogKeyDown = (ev: KeyboardEvent) => {
|
||||||
if (ev.key === Key.ESCAPE) {
|
const navAction = getKeyBindingsManager().getNavigationAction(ev);
|
||||||
ev.stopPropagation();
|
switch (navAction) {
|
||||||
ev.preventDefault();
|
case "KeyBinding.closeDialogOrContextMenu" as KeyBindingAction:
|
||||||
onFinished();
|
case KeyBindingAction.FilterRooms:
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
onFinished();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue