Spotlight search labs (#7116)
This commit is contained in:
parent
c56833816a
commit
914b61239c
12 changed files with 907 additions and 54 deletions
|
@ -113,6 +113,7 @@
|
||||||
@import "./views/dialogs/_ShareDialog.scss";
|
@import "./views/dialogs/_ShareDialog.scss";
|
||||||
@import "./views/dialogs/_SlashCommandHelpDialog.scss";
|
@import "./views/dialogs/_SlashCommandHelpDialog.scss";
|
||||||
@import "./views/dialogs/_SpaceSettingsDialog.scss";
|
@import "./views/dialogs/_SpaceSettingsDialog.scss";
|
||||||
|
@import "./views/dialogs/_SpotlightDialog.scss";
|
||||||
@import "./views/dialogs/_TabbedIntegrationManagerDialog.scss";
|
@import "./views/dialogs/_TabbedIntegrationManagerDialog.scss";
|
||||||
@import "./views/dialogs/_TermsDialog.scss";
|
@import "./views/dialogs/_TermsDialog.scss";
|
||||||
@import "./views/dialogs/_UntrustedDeviceDialog.scss";
|
@import "./views/dialogs/_UntrustedDeviceDialog.scss";
|
||||||
|
|
|
@ -39,6 +39,7 @@ limitations under the License.
|
||||||
content: '';
|
content: '';
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
|
right: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
286
res/css/views/dialogs/_SpotlightDialog.scss
Normal file
286
res/css/views/dialogs/_SpotlightDialog.scss
Normal file
|
@ -0,0 +1,286 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_SpotlightDialog_wrapper .mx_Dialog {
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow-y: initial;
|
||||||
|
position: relative;
|
||||||
|
height: 60%;
|
||||||
|
padding: 0;
|
||||||
|
contain: unset; // needed for .mx_SpotlightDialog_keyboardPrompt to not be culled
|
||||||
|
|
||||||
|
.mx_SpotlightDialog_keyboardPrompt {
|
||||||
|
position: absolute;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: $background;
|
||||||
|
top: -60px; // relative to the top of the modal
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
color: $secondary-content;
|
||||||
|
|
||||||
|
> span > div {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 4px;
|
||||||
|
margin: 0 4px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: $quinary-content;
|
||||||
|
vertical-align: middle;
|
||||||
|
color: $tertiary-content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpotlightDialog {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.mx_Dialog_header {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpotlightDialog_searchBox {
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid $system;
|
||||||
|
|
||||||
|
> input {
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: transparent;
|
||||||
|
width: 100%;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
color: $tertiary-content;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#mx_SpotlightDialog_content {
|
||||||
|
margin: 16px;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.mx_SpotlightDialog_section {
|
||||||
|
> h4 {
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
color: $secondary-content;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& + .mx_SpotlightDialog_section {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpotlightDialog_recentlyViewed {
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton {
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 4px;
|
||||||
|
color: $primary-content;
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
display: inline-block;
|
||||||
|
width: 50px;
|
||||||
|
min-width: 50px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
.mx_DecoratedRoomAvatar {
|
||||||
|
margin: 0 5px 4px; // maintain centering
|
||||||
|
}
|
||||||
|
|
||||||
|
& + .mx_AccessibleButton {
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &[aria-selected=true] {
|
||||||
|
background-color: $quinary-content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpotlightDialog_results,
|
||||||
|
.mx_SpotlightDialog_recentSearches,
|
||||||
|
.mx_SpotlightDialog_otherSearches {
|
||||||
|
.mx_AccessibleButton {
|
||||||
|
padding: 6px 4px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
color: $primary-content;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.mx_BaseAvatar {
|
||||||
|
margin-right: 8px;
|
||||||
|
display: inline-block;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &[aria-selected=true] {
|
||||||
|
background-color: $system;
|
||||||
|
|
||||||
|
.mx_SpotlightDialog_enterPrompt {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpotlightDialog_otherSearches {
|
||||||
|
.mx_SpotlightDialog_startChat,
|
||||||
|
.mx_SpotlightDialog_explorePublicRooms {
|
||||||
|
padding-left: 32px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: $secondary-content;
|
||||||
|
content: "";
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: contain;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
position: absolute;
|
||||||
|
left: 4px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpotlightDialog_startChat::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/members.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpotlightDialog_explorePublicRooms::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpotlightDialog_otherSearches_messageSearchText {
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpotlightDialog_otherSearches_messageSearchIcon {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 8px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background-color: $secondary-content;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/search-inset.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpotlightDialog_result_details {
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-right: 8px;
|
||||||
|
color: $tertiary-content;
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpotlightDialog_recentSearches {
|
||||||
|
overflow-y: hidden;
|
||||||
|
height: calc(100% - 190px);
|
||||||
|
|
||||||
|
> h4 > .mx_AccessibleButton_kind_link {
|
||||||
|
padding: 0;
|
||||||
|
float: right;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
color: $secondary-content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpotlightDialog_enterPrompt {
|
||||||
|
padding: 2px 4px;
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
color: $tertiary-content;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: $quinary-content;
|
||||||
|
margin: 0 4px 0 auto;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpotlightDialog_footer {
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
color: $secondary-content;
|
||||||
|
padding: 16px 16px 20px;
|
||||||
|
display: flex;
|
||||||
|
border-top: 1px solid $quinary-content;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 20px;
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: $secondary-content;
|
||||||
|
content: "";
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton_kind_primary_outline {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-color: $secondary-content;
|
||||||
|
color: $secondary-content;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -162,7 +162,7 @@ export function roomContextDetailsText(room: Room): string {
|
||||||
|
|
||||||
const dmPartner = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
const dmPartner = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
||||||
if (dmPartner) {
|
if (dmPartner) {
|
||||||
return room.getMember(dmPartner)?.rawDisplayName;
|
return dmPartner;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [parent, ...otherParents] = SpaceStore.instance.getKnownParents(room.roomId);
|
const [parent, ...otherParents] = SpaceStore.instance.getKnownParents(room.roomId);
|
||||||
|
|
|
@ -43,8 +43,6 @@ import { FocusHandler, Ref } from "./roving/types";
|
||||||
* https://developer.mozilla.org/en-US/docs/Web/Accessibility/Keyboard-navigable_JavaScript_widgets#Technique_1_Roving_tabindex
|
* https://developer.mozilla.org/en-US/docs/Web/Accessibility/Keyboard-navigable_JavaScript_widgets#Technique_1_Roving_tabindex
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const DOCUMENT_POSITION_PRECEDING = 2;
|
|
||||||
|
|
||||||
export interface IState {
|
export interface IState {
|
||||||
activeRef: Ref;
|
activeRef: Ref;
|
||||||
refs: Ref[];
|
refs: Ref[];
|
||||||
|
@ -55,7 +53,7 @@ interface IContext {
|
||||||
dispatch: Dispatch<IAction>;
|
dispatch: Dispatch<IAction>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RovingTabIndexContext = createContext<IContext>({
|
export const RovingTabIndexContext = createContext<IContext>({
|
||||||
state: {
|
state: {
|
||||||
activeRef: null,
|
activeRef: null,
|
||||||
refs: [], // list of refs in DOM order
|
refs: [], // list of refs in DOM order
|
||||||
|
@ -80,37 +78,29 @@ interface IAction {
|
||||||
export const reducer = (state: IState, action: IAction) => {
|
export const reducer = (state: IState, action: IAction) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case Type.Register: {
|
case Type.Register: {
|
||||||
let left = 0;
|
|
||||||
let right = state.refs.length - 1;
|
|
||||||
let index = state.refs.length; // by default append to the end
|
|
||||||
|
|
||||||
// do a binary search to find the right slot
|
|
||||||
while (left <= right) {
|
|
||||||
index = Math.floor((left + right) / 2);
|
|
||||||
const ref = state.refs[index];
|
|
||||||
|
|
||||||
if (ref === action.payload.ref) {
|
|
||||||
return state; // already in refs, this should not happen
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action.payload.ref.current.compareDocumentPosition(ref.current) & DOCUMENT_POSITION_PRECEDING) {
|
|
||||||
left = ++index;
|
|
||||||
} else {
|
|
||||||
right = index - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state.activeRef) {
|
if (!state.activeRef) {
|
||||||
// Our list of refs was empty, set activeRef to this first item
|
// Our list of refs was empty, set activeRef to this first item
|
||||||
state.activeRef = action.payload.ref;
|
state.activeRef = action.payload.ref;
|
||||||
}
|
}
|
||||||
|
|
||||||
// update the refs list
|
// Sadly due to the potential of DOM elements swapping order we can't do anything fancy like a binary insert
|
||||||
if (index < state.refs.length) {
|
state.refs.push(action.payload.ref);
|
||||||
state.refs.splice(index, 0, action.payload.ref);
|
state.refs.sort((a, b) => {
|
||||||
} else {
|
if (a === b) {
|
||||||
state.refs.push(action.payload.ref);
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const position = a.current.compareDocumentPosition(b.current);
|
||||||
|
|
||||||
|
if (position & Node.DOCUMENT_POSITION_FOLLOWING || position & Node.DOCUMENT_POSITION_CONTAINED_BY) {
|
||||||
|
return -1;
|
||||||
|
} else if (position & Node.DOCUMENT_POSITION_PRECEDING || position & Node.DOCUMENT_POSITION_CONTAINS) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return { ...state };
|
return { ...state };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,9 @@ import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
import SpaceStore from "../../stores/spaces/SpaceStore";
|
import SpaceStore from "../../stores/spaces/SpaceStore";
|
||||||
import { UPDATE_SELECTED_SPACE } from "../../stores/spaces";
|
import { UPDATE_SELECTED_SPACE } from "../../stores/spaces";
|
||||||
import { isMac } from "../../Keyboard";
|
import { isMac } from "../../Keyboard";
|
||||||
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
|
import Modal from "../../Modal";
|
||||||
|
import SpotlightDialog from "../views/dialogs/SpotlightDialog";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
|
@ -83,11 +86,19 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||||
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.clearInput);
|
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.clearInput);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private openSpotlight() {
|
||||||
|
Modal.createTrackedDialog("Spotlight", "", SpotlightDialog, {}, "mx_SpotlightDialog_wrapper", false, true);
|
||||||
|
}
|
||||||
|
|
||||||
private onAction = (payload: ActionPayload) => {
|
private onAction = (payload: ActionPayload) => {
|
||||||
if (payload.action === Action.ViewRoom && payload.clear_search) {
|
if (payload.action === Action.ViewRoom && payload.clear_search) {
|
||||||
this.clearInput();
|
this.clearInput();
|
||||||
} else if (payload.action === 'focus_room_filter' && this.inputRef.current) {
|
} else if (payload.action === 'focus_room_filter' && this.inputRef.current) {
|
||||||
this.inputRef.current.focus();
|
if (SettingsStore.getValue("feature_spotlight")) {
|
||||||
|
this.openSpotlight();
|
||||||
|
} else {
|
||||||
|
this.inputRef.current.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -107,6 +118,14 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||||
this.setState({ query: this.inputRef.current.value });
|
this.setState({ query: this.inputRef.current.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onMouseDown = (ev: React.MouseEvent<HTMLInputElement>) => {
|
||||||
|
if (SettingsStore.getValue("feature_spotlight")) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
this.openSpotlight();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private onFocus = (ev: React.FocusEvent<HTMLInputElement>) => {
|
private onFocus = (ev: React.FocusEvent<HTMLInputElement>) => {
|
||||||
this.setState({ focused: true });
|
this.setState({ focused: true });
|
||||||
ev.target.select();
|
ev.target.select();
|
||||||
|
@ -162,11 +181,12 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||||
ref={this.inputRef}
|
ref={this.inputRef}
|
||||||
className={inputClasses}
|
className={inputClasses}
|
||||||
value={this.state.query}
|
value={this.state.query}
|
||||||
|
onMouseDown={this.onMouseDown}
|
||||||
onFocus={this.onFocus}
|
onFocus={this.onFocus}
|
||||||
onBlur={this.onBlur}
|
onBlur={this.onBlur}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
placeholder={_t("Filter")}
|
placeholder={SettingsStore.getValue("feature_spotlight") ? _t("Search") : _t("Filter")}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef } from 'react';
|
import React, { createRef, HTMLProps } from 'react';
|
||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ import AccessibleButton from '../../components/views/elements/AccessibleButton';
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
import { Action } from '../../dispatcher/actions';
|
import { Action } from '../../dispatcher/actions';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps extends HTMLProps<HTMLInputElement> {
|
||||||
onSearch?: (query: string) => void;
|
onSearch?: (query: string) => void;
|
||||||
onCleared?: (source?: string) => void;
|
onCleared?: (source?: string) => void;
|
||||||
onKeyDown?: (ev: React.KeyboardEvent) => void;
|
onKeyDown?: (ev: React.KeyboardEvent) => void;
|
||||||
|
@ -135,11 +135,15 @@ export default class SearchBox extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
|
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
|
||||||
|
const { onSearch, onCleared, onKeyDown, onFocus, onBlur, className = "", placeholder, blurredPlaceholder,
|
||||||
|
autoFocus, initialValue, collapsed, enableRoomSearchFocus, ...props } = this.props;
|
||||||
|
|
||||||
// check for collapsed here and
|
// check for collapsed here and
|
||||||
// not at parent so we keep
|
// not at parent so we keep
|
||||||
// searchTerm in our state
|
// searchTerm in our state
|
||||||
// when collapsing and expanding
|
// when collapsing and expanding
|
||||||
if (this.props.collapsed) {
|
if (collapsed) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const clearButton = (!this.state.blurred || this.state.searchTerm) ?
|
const clearButton = (!this.state.blurred || this.state.searchTerm) ?
|
||||||
|
@ -153,13 +157,10 @@ export default class SearchBox extends React.Component<IProps, IState> {
|
||||||
// show a shorter placeholder when blurred, if requested
|
// show a shorter placeholder when blurred, if requested
|
||||||
// this is used for the room filter field that has
|
// this is used for the room filter field that has
|
||||||
// the explore button next to it when blurred
|
// the explore button next to it when blurred
|
||||||
const placeholder = this.state.blurred ?
|
|
||||||
(this.props.blurredPlaceholder || this.props.placeholder) :
|
|
||||||
this.props.placeholder;
|
|
||||||
const className = this.props.className || "";
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames("mx_SearchBox", "mx_textinput", { "mx_SearchBox_blurred": this.state.blurred })}>
|
<div className={classNames("mx_SearchBox", "mx_textinput", { "mx_SearchBox_blurred": this.state.blurred })}>
|
||||||
<input
|
<input
|
||||||
|
{...props}
|
||||||
key="searchfield"
|
key="searchfield"
|
||||||
type="text"
|
type="text"
|
||||||
ref={this.search}
|
ref={this.search}
|
||||||
|
@ -169,7 +170,7 @@ export default class SearchBox extends React.Component<IProps, IState> {
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
onBlur={this.onBlur}
|
onBlur={this.onBlur}
|
||||||
placeholder={placeholder}
|
placeholder={this.state.blurred ? (blurredPlaceholder || placeholder) : placeholder}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoFocus={this.props.autoFocus}
|
autoFocus={this.props.autoFocus}
|
||||||
/>
|
/>
|
||||||
|
|
540
src/components/views/dialogs/SpotlightDialog.tsx
Normal file
540
src/components/views/dialogs/SpotlightDialog.tsx
Normal file
|
@ -0,0 +1,540 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 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 React, {
|
||||||
|
ChangeEvent,
|
||||||
|
ComponentProps,
|
||||||
|
KeyboardEvent,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { normalize } from "matrix-js-sdk/src/utils";
|
||||||
|
import { IHierarchyRoom } from "matrix-js-sdk/src/@types/spaces";
|
||||||
|
import { RoomHierarchy } from "matrix-js-sdk/src/room-hierarchy";
|
||||||
|
import { RoomType } from "matrix-js-sdk/src/@types/event";
|
||||||
|
|
||||||
|
import { IDialogProps } from "./IDialogProps";
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
import BaseDialog from "./BaseDialog";
|
||||||
|
import { BreadcrumbsStore } from "../../../stores/BreadcrumbsStore";
|
||||||
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
|
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||||
|
import {
|
||||||
|
findSiblingElement,
|
||||||
|
RovingAccessibleButton,
|
||||||
|
RovingAccessibleTooltipButton,
|
||||||
|
RovingTabIndexContext,
|
||||||
|
RovingTabIndexProvider,
|
||||||
|
Type,
|
||||||
|
useRovingTabIndex,
|
||||||
|
} from "../../../accessibility/RovingTabIndex";
|
||||||
|
import { Key } from "../../../Keyboard";
|
||||||
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
|
import SpaceStore from "../../../stores/spaces/SpaceStore";
|
||||||
|
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||||
|
import { mediaFromMxc } from "../../../customisations/Media";
|
||||||
|
import BaseAvatar from "../avatars/BaseAvatar";
|
||||||
|
import Spinner from "../elements/Spinner";
|
||||||
|
import { roomContextDetailsText } from "../../../Rooms";
|
||||||
|
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||||
|
import { Action } from "../../../dispatcher/actions";
|
||||||
|
import Modal from "../../../Modal";
|
||||||
|
import GenericFeatureFeedbackDialog from "./GenericFeatureFeedbackDialog";
|
||||||
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
|
import RoomViewStore from "../../../stores/RoomViewStore";
|
||||||
|
import { showStartChatInviteDialog } from "../../../RoomInvite";
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||||
|
|
||||||
|
const MAX_RECENT_SEARCHES = 10;
|
||||||
|
const SECTION_LIMIT = 50; // only show 50 results per section for performance reasons
|
||||||
|
|
||||||
|
const Option: React.FC<ComponentProps<typeof RovingAccessibleButton>> = ({ inputRef, ...props }) => {
|
||||||
|
const [onFocus, isActive, ref] = useRovingTabIndex(inputRef);
|
||||||
|
return <AccessibleButton
|
||||||
|
{...props}
|
||||||
|
onFocus={onFocus}
|
||||||
|
inputRef={ref}
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-selected={isActive}
|
||||||
|
role="option"
|
||||||
|
/>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TooltipOption: React.FC<ComponentProps<typeof RovingAccessibleTooltipButton>> = ({ inputRef, ...props }) => {
|
||||||
|
const [onFocus, isActive, ref] = useRovingTabIndex(inputRef);
|
||||||
|
return <AccessibleTooltipButton
|
||||||
|
{...props}
|
||||||
|
onFocus={onFocus}
|
||||||
|
inputRef={ref}
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-selected={isActive}
|
||||||
|
role="option"
|
||||||
|
/>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useRecentSearches = (): [Room[], () => void] => {
|
||||||
|
const [rooms, setRooms] = useState(() => {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
const recents = SettingsStore.getValue("SpotlightSearch.recentSearches", null);
|
||||||
|
return recents.map(r => cli.getRoom(r)).filter(Boolean);
|
||||||
|
});
|
||||||
|
|
||||||
|
return [rooms, () => {
|
||||||
|
SettingsStore.setValue("SpotlightSearch.recentSearches", null, SettingLevel.ACCOUNT, []);
|
||||||
|
setRooms([]);
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
|
||||||
|
const ResultDetails = ({ room }: { room: Room }) => {
|
||||||
|
const roomContextDetails = roomContextDetailsText(room);
|
||||||
|
if (roomContextDetails) {
|
||||||
|
return <div className="mx_SpotlightDialog_result_details">
|
||||||
|
{ roomContextDetails }
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IProps extends IDialogProps {
|
||||||
|
initialText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useSpaceResults = (space?: Room, query?: string): [IHierarchyRoom[], boolean] => {
|
||||||
|
const [rooms, setRooms] = useState<IHierarchyRoom[]>([]);
|
||||||
|
const [hierarchy, setHierarchy] = useState<RoomHierarchy>();
|
||||||
|
|
||||||
|
const resetHierarchy = useCallback(() => {
|
||||||
|
const hierarchy = new RoomHierarchy(space, 50);
|
||||||
|
setHierarchy(hierarchy);
|
||||||
|
}, [space]);
|
||||||
|
useEffect(resetHierarchy, [resetHierarchy]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let unmounted = false;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
while (hierarchy?.canLoadMore && !unmounted && space === hierarchy.root) {
|
||||||
|
await hierarchy.load();
|
||||||
|
if (hierarchy.canLoadMore) hierarchy.load(); // start next load so that the loading attribute is right
|
||||||
|
setRooms(hierarchy.rooms);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unmounted = true;
|
||||||
|
};
|
||||||
|
}, [space, hierarchy]);
|
||||||
|
|
||||||
|
const results = useMemo(() => {
|
||||||
|
const trimmedQuery = query.trim();
|
||||||
|
const lcQuery = trimmedQuery.toLowerCase();
|
||||||
|
const normalizedQuery = normalize(trimmedQuery);
|
||||||
|
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
return rooms?.filter(r => {
|
||||||
|
return r.room_type !== RoomType.Space &&
|
||||||
|
cli.getRoom(r.room_id)?.getMyMembership() !== "join" &&
|
||||||
|
(
|
||||||
|
normalize(r.name || "").includes(normalizedQuery) ||
|
||||||
|
(r.canonical_alias || "").includes(lcQuery)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [rooms, query]);
|
||||||
|
|
||||||
|
return [results, hierarchy?.loading ?? false];
|
||||||
|
};
|
||||||
|
|
||||||
|
const SpotlightDialog: React.FC<IProps> = ({ initialText = "", onFinished }) => {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
const rovingContext = useContext(RovingTabIndexContext);
|
||||||
|
const [query, _setQuery] = useState(initialText);
|
||||||
|
const [recentSearches, clearRecentSearches] = useRecentSearches();
|
||||||
|
|
||||||
|
const results = useMemo<Room[] | null>(() => {
|
||||||
|
if (!query) return null;
|
||||||
|
|
||||||
|
const trimmedQuery = query.trim();
|
||||||
|
const lcQuery = trimmedQuery.toLowerCase();
|
||||||
|
const normalizedQuery = normalize(trimmedQuery);
|
||||||
|
|
||||||
|
return cli.getRooms().filter(r => {
|
||||||
|
return r.getCanonicalAlias()?.includes(lcQuery) || r.normalizedName.includes(normalizedQuery);
|
||||||
|
});
|
||||||
|
}, [cli, query]);
|
||||||
|
|
||||||
|
const activeSpace = SpaceStore.instance.activeSpaceRoom;
|
||||||
|
const [spaceResults, spaceResultsLoading] = useSpaceResults(activeSpace, query);
|
||||||
|
|
||||||
|
const setQuery = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
const newQuery = e.currentTarget.value;
|
||||||
|
_setQuery(newQuery);
|
||||||
|
if (!query !== !newQuery) {
|
||||||
|
setImmediate(() => {
|
||||||
|
// reset the activeRef when we start/stop querying as the view changes
|
||||||
|
const ref = rovingContext.state.refs[0];
|
||||||
|
if (ref) {
|
||||||
|
rovingContext.dispatch({
|
||||||
|
type: Type.SetFocus,
|
||||||
|
payload: { ref },
|
||||||
|
});
|
||||||
|
ref.current?.scrollIntoView({
|
||||||
|
block: "nearest",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const viewRoom = (roomId: string, persist = false) => {
|
||||||
|
if (persist) {
|
||||||
|
const recents = new Set(SettingsStore.getValue("SpotlightSearch.recentSearches", null).reverse());
|
||||||
|
// remove & add the room to put it at the end
|
||||||
|
recents.delete(roomId);
|
||||||
|
recents.add(roomId);
|
||||||
|
|
||||||
|
SettingsStore.setValue(
|
||||||
|
"SpotlightSearch.recentSearches",
|
||||||
|
null,
|
||||||
|
SettingLevel.ACCOUNT,
|
||||||
|
Array.from(recents).reverse().slice(0, MAX_RECENT_SEARCHES),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultDispatcher.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_id: roomId,
|
||||||
|
});
|
||||||
|
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 => (
|
||||||
|
<Option
|
||||||
|
id={`mx_SpotlightDialog_button_result_${room.roomId}`}
|
||||||
|
key={room.roomId}
|
||||||
|
onClick={() => {
|
||||||
|
viewRoom(room.roomId, true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RoomAvatar room={room} width={20} height={20} />
|
||||||
|
{ room.name }
|
||||||
|
<ResultDetails room={room} />
|
||||||
|
<div className="mx_SpotlightDialog_enterPrompt">↵</div>
|
||||||
|
</Option>
|
||||||
|
);
|
||||||
|
|
||||||
|
let peopleSection: JSX.Element;
|
||||||
|
if (people.length) {
|
||||||
|
peopleSection = <div className="mx_SpotlightDialog_section mx_SpotlightDialog_results" role="group">
|
||||||
|
<h4>{ _t("People") }</h4>
|
||||||
|
<div>
|
||||||
|
{ people.slice(0, SECTION_LIMIT).map(resultMapper) }
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let roomsSection: JSX.Element;
|
||||||
|
if (rooms.length) {
|
||||||
|
roomsSection = <div className="mx_SpotlightDialog_section mx_SpotlightDialog_results" role="group">
|
||||||
|
<h4>{ _t("Rooms") }</h4>
|
||||||
|
<div>
|
||||||
|
{ rooms.slice(0, SECTION_LIMIT).map(resultMapper) }
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let spacesSection: JSX.Element;
|
||||||
|
if (spaces.length) {
|
||||||
|
spacesSection = <div className="mx_SpotlightDialog_section mx_SpotlightDialog_results" role="group">
|
||||||
|
<h4>{ _t("Spaces you're in") }</h4>
|
||||||
|
<div>
|
||||||
|
{ spaces.slice(0, SECTION_LIMIT).map(resultMapper) }
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let spaceRoomsSection: JSX.Element;
|
||||||
|
if (spaceResults.length) {
|
||||||
|
spaceRoomsSection = <div className="mx_SpotlightDialog_section mx_SpotlightDialog_results" role="group">
|
||||||
|
<h4>{ _t("Other rooms in %(spaceName)s", { spaceName: activeSpace.name }) }</h4>
|
||||||
|
<div>
|
||||||
|
{ spaceResults.slice(0, SECTION_LIMIT).map((room: IHierarchyRoom): JSX.Element => (
|
||||||
|
<Option
|
||||||
|
id={`mx_SpotlightDialog_button_result_${room.room_id}`}
|
||||||
|
key={room.room_id}
|
||||||
|
onClick={() => {
|
||||||
|
viewRoom(room.room_id, true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BaseAvatar
|
||||||
|
name={room.name}
|
||||||
|
idName={room.room_id}
|
||||||
|
url={room.avatar_url ? mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(20) : null}
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>
|
||||||
|
{ room.name || room.canonical_alias }
|
||||||
|
{ room.name && room.canonical_alias && <div className="mx_SpotlightDialog_result_details">
|
||||||
|
{ room.canonical_alias }
|
||||||
|
</div> }
|
||||||
|
<div className="mx_SpotlightDialog_enterPrompt">↵</div>
|
||||||
|
</Option>
|
||||||
|
)) }
|
||||||
|
{ spaceResultsLoading && <Spinner /> }
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
content = <>
|
||||||
|
{ peopleSection }
|
||||||
|
{ roomsSection }
|
||||||
|
{ spacesSection }
|
||||||
|
{ spaceRoomsSection }
|
||||||
|
<div className="mx_SpotlightDialog_section mx_SpotlightDialog_otherSearches" role="group">
|
||||||
|
<h4>{ _t('Use "%(query)s" to search', { query }) }</h4>
|
||||||
|
<div>
|
||||||
|
<Option
|
||||||
|
id="mx_SpotlightDialog_button_explorePublicRooms"
|
||||||
|
className="mx_SpotlightDialog_explorePublicRooms"
|
||||||
|
onClick={() => {
|
||||||
|
defaultDispatcher.dispatch({
|
||||||
|
action: Action.ViewRoomDirectory,
|
||||||
|
initialText: query,
|
||||||
|
});
|
||||||
|
onFinished();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ _t("Public rooms") }
|
||||||
|
<div className="mx_SpotlightDialog_enterPrompt">↵</div>
|
||||||
|
</Option>
|
||||||
|
<Option
|
||||||
|
id="mx_SpotlightDialog_button_startChat"
|
||||||
|
className="mx_SpotlightDialog_startChat"
|
||||||
|
onClick={() => {
|
||||||
|
showStartChatInviteDialog(query);
|
||||||
|
onFinished();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ _t("People") }
|
||||||
|
<div className="mx_SpotlightDialog_enterPrompt">↵</div>
|
||||||
|
</Option>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mx_SpotlightDialog_section mx_SpotlightDialog_otherSearches" role="group">
|
||||||
|
<h4>{ _t("Other searches") }</h4>
|
||||||
|
<div className="mx_SpotlightDialog_otherSearches_messageSearchText">
|
||||||
|
{ _t("To search messages, look for this icon at the top of a room <icon/>", {}, {
|
||||||
|
icon: () => <div className="mx_SpotlightDialog_otherSearches_messageSearchIcon" />,
|
||||||
|
}) }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>;
|
||||||
|
} else {
|
||||||
|
let recentSearchesSection: JSX.Element;
|
||||||
|
if (recentSearches.length) {
|
||||||
|
recentSearchesSection = (
|
||||||
|
<div className="mx_SpotlightDialog_section mx_SpotlightDialog_recentSearches" role="group">
|
||||||
|
<h4>
|
||||||
|
{ _t("Recent searches") }
|
||||||
|
<AccessibleButton kind="link" onClick={clearRecentSearches}>
|
||||||
|
{ _t("Clear") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</h4>
|
||||||
|
<div>
|
||||||
|
{ recentSearches.map(room => (
|
||||||
|
<Option
|
||||||
|
id={`mx_SpotlightDialog_button_recentSearch_${room.roomId}`}
|
||||||
|
key={room.roomId}
|
||||||
|
onClick={() => {
|
||||||
|
viewRoom(room.roomId, true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RoomAvatar room={room} width={20} height={20} />
|
||||||
|
{ room.name }
|
||||||
|
<div className="mx_SpotlightDialog_enterPrompt">↵</div>
|
||||||
|
</Option>
|
||||||
|
)) }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
content = <>
|
||||||
|
<div className="mx_SpotlightDialog_section mx_SpotlightDialog_recentlyViewed" role="group">
|
||||||
|
<h4>{ _t("Recently viewed") }</h4>
|
||||||
|
<div>
|
||||||
|
{ BreadcrumbsStore.instance.rooms
|
||||||
|
.filter(r => r.roomId !== RoomViewStore.getRoomId())
|
||||||
|
.slice(0, 10)
|
||||||
|
.map(room => (
|
||||||
|
<TooltipOption
|
||||||
|
id={`mx_SpotlightDialog_button_recentlyViewed_${room.roomId}`}
|
||||||
|
title={room.name}
|
||||||
|
key={room.roomId}
|
||||||
|
onClick={() => {
|
||||||
|
viewRoom(room.roomId);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DecoratedRoomAvatar room={room} avatarSize={32} />
|
||||||
|
{ room.name }
|
||||||
|
</TooltipOption>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ recentSearchesSection }
|
||||||
|
|
||||||
|
<div className="mx_SpotlightDialog_section mx_SpotlightDialog_otherSearches" role="group">
|
||||||
|
<h4>{ _t("Other searches") }</h4>
|
||||||
|
<div>
|
||||||
|
<Option
|
||||||
|
id="mx_SpotlightDialog_button_explorePublicRooms"
|
||||||
|
className="mx_SpotlightDialog_explorePublicRooms"
|
||||||
|
onClick={() => {
|
||||||
|
defaultDispatcher.fire(Action.ViewRoomDirectory);
|
||||||
|
onFinished();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ _t("Explore public rooms") }
|
||||||
|
<div className="mx_SpotlightDialog_enterPrompt">↵</div>
|
||||||
|
</Option>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDialogKeyDown = (ev: KeyboardEvent) => {
|
||||||
|
if (ev.key === Key.ESCAPE) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
onFinished();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyDown = (ev: KeyboardEvent) => {
|
||||||
|
switch (ev.key) {
|
||||||
|
case Key.ARROW_UP:
|
||||||
|
case Key.ARROW_DOWN:
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
if (rovingContext.state.refs.length > 0) {
|
||||||
|
const idx = rovingContext.state.refs.indexOf(rovingContext.state.activeRef);
|
||||||
|
const ref = findSiblingElement(rovingContext.state.refs, idx + (ev.key === Key.ARROW_UP ? -1 : 1));
|
||||||
|
|
||||||
|
if (ref) {
|
||||||
|
rovingContext.dispatch({
|
||||||
|
type: Type.SetFocus,
|
||||||
|
payload: { ref },
|
||||||
|
});
|
||||||
|
ref.current?.scrollIntoView({
|
||||||
|
block: "nearest",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Key.ENTER:
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
rovingContext.state.activeRef?.current?.click();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const activeDescendant = rovingContext.state.activeRef?.current?.id;
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<div className="mx_SpotlightDialog_keyboardPrompt">
|
||||||
|
{ _t("Use <arrows/> to scroll results", {}, {
|
||||||
|
arrows: () => <>
|
||||||
|
<div>↓</div>
|
||||||
|
<div>↑</div>
|
||||||
|
</>,
|
||||||
|
}) }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BaseDialog
|
||||||
|
className="mx_SpotlightDialog"
|
||||||
|
onFinished={onFinished}
|
||||||
|
hasCancel={false}
|
||||||
|
onKeyDown={onDialogKeyDown}
|
||||||
|
>
|
||||||
|
<div className="mx_SpotlightDialog_searchBox mx_textinput">
|
||||||
|
<input
|
||||||
|
autoFocus
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
placeholder={_t("Search")}
|
||||||
|
value={query}
|
||||||
|
onChange={setQuery}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
aria-owns="mx_SpotlightDialog_content"
|
||||||
|
aria-activedescendant={activeDescendant}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="mx_SpotlightDialog_content" role="listbox" aria-activedescendant={activeDescendant}>
|
||||||
|
{ content }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx_SpotlightDialog_footer">
|
||||||
|
<span>
|
||||||
|
{ activeSpace
|
||||||
|
? _t("Searching rooms and chats you're in and %(spaceName)s", { spaceName: activeSpace.name })
|
||||||
|
: _t("Searching rooms and chats you're in") }
|
||||||
|
</span>
|
||||||
|
<AccessibleButton
|
||||||
|
kind="primary_outline"
|
||||||
|
onClick={() => {
|
||||||
|
Modal.createTrackedDialog("Spotlight Feedback", "", GenericFeatureFeedbackDialog, {
|
||||||
|
title: _t("Spotlight search feedback"),
|
||||||
|
subheading: _t("Thank you for trying Spotlight search. " +
|
||||||
|
"Your feedback will help inform the next versions."),
|
||||||
|
rageshakeLabel: "spotlight-feedback",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ _t("Feedback") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
</BaseDialog>
|
||||||
|
</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const RovingSpotlightDialog: React.FC<IProps> = (props) => {
|
||||||
|
return <RovingTabIndexProvider>
|
||||||
|
{ () => <SpotlightDialog {...props} /> }
|
||||||
|
</RovingTabIndexProvider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RovingSpotlightDialog;
|
|
@ -193,7 +193,7 @@ export enum Action {
|
||||||
SwitchSpace = "switch_space",
|
SwitchSpace = "switch_space",
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Signals to the visible space hierarchy that a change has occurred an that it should refresh.
|
* Signals to the visible space hierarchy that a change has occurred and that it should refresh.
|
||||||
*/
|
*/
|
||||||
UpdateSpaceHierarchy = "update_space_hierarchy",
|
UpdateSpaceHierarchy = "update_space_hierarchy",
|
||||||
|
|
||||||
|
@ -232,5 +232,5 @@ export enum Action {
|
||||||
* The user rejected anonymous analytics (i.e. matomo, pre-posthog) from the toast
|
* The user rejected anonymous analytics (i.e. matomo, pre-posthog) from the toast
|
||||||
* Payload: none
|
* Payload: none
|
||||||
*/
|
*/
|
||||||
AnonymousAnalyticsReject = "anonymous_analytics_reject"
|
AnonymousAnalyticsReject = "anonymous_analytics_reject",
|
||||||
}
|
}
|
||||||
|
|
|
@ -859,6 +859,7 @@
|
||||||
"New layout switcher (with message bubbles)": "New layout switcher (with message bubbles)",
|
"New layout switcher (with message bubbles)": "New layout switcher (with message bubbles)",
|
||||||
"Meta Spaces": "Meta Spaces",
|
"Meta Spaces": "Meta Spaces",
|
||||||
"Use new room breadcrumbs": "Use new room breadcrumbs",
|
"Use new room breadcrumbs": "Use new room breadcrumbs",
|
||||||
|
"New spotlight search experience": "New spotlight search experience",
|
||||||
"Don't send read receipts": "Don't send read receipts",
|
"Don't send read receipts": "Don't send read receipts",
|
||||||
"Font size": "Font size",
|
"Font size": "Font size",
|
||||||
"Use custom size": "Use custom size",
|
"Use custom size": "Use custom size",
|
||||||
|
@ -2706,6 +2707,19 @@
|
||||||
"Command Help": "Command Help",
|
"Command Help": "Command Help",
|
||||||
"Space settings": "Space settings",
|
"Space settings": "Space settings",
|
||||||
"Settings - %(spaceName)s": "Settings - %(spaceName)s",
|
"Settings - %(spaceName)s": "Settings - %(spaceName)s",
|
||||||
|
"Spaces you're in": "Spaces you're in",
|
||||||
|
"Other rooms in %(spaceName)s": "Other rooms in %(spaceName)s",
|
||||||
|
"Use \"%(query)s\" to search": "Use \"%(query)s\" to search",
|
||||||
|
"Public rooms": "Public rooms",
|
||||||
|
"Other searches": "Other searches",
|
||||||
|
"To search messages, look for this icon at the top of a room <icon/>": "To search messages, look for this icon at the top of a room <icon/>",
|
||||||
|
"Recent searches": "Recent searches",
|
||||||
|
"Clear": "Clear",
|
||||||
|
"Use <arrows/> to scroll results": "Use <arrows/> to scroll results",
|
||||||
|
"Searching rooms and chats you're in and %(spaceName)s": "Searching rooms and chats you're in and %(spaceName)s",
|
||||||
|
"Searching rooms and chats you're in": "Searching rooms and chats you're in",
|
||||||
|
"Spotlight search feedback": "Spotlight search feedback",
|
||||||
|
"Thank you for trying Spotlight search. Your feedback will help inform the next versions.": "Thank you for trying Spotlight search. Your feedback will help inform the next versions.",
|
||||||
"To help us prevent this in future, please <a>send us logs</a>.": "To help us prevent this in future, please <a>send us logs</a>.",
|
"To help us prevent this in future, please <a>send us logs</a>.": "To help us prevent this in future, please <a>send us logs</a>.",
|
||||||
"Missing session data": "Missing session data",
|
"Missing session data": "Missing session data",
|
||||||
"Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.",
|
"Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.",
|
||||||
|
@ -3104,7 +3118,6 @@
|
||||||
"Set status": "Set status",
|
"Set status": "Set status",
|
||||||
"Clear status": "Clear status",
|
"Clear status": "Clear status",
|
||||||
"Set a new status": "Set a new status",
|
"Set a new status": "Set a new status",
|
||||||
"Clear": "Clear",
|
|
||||||
"Got an account? <a>Sign in</a>": "Got an account? <a>Sign in</a>",
|
"Got an account? <a>Sign in</a>": "Got an account? <a>Sign in</a>",
|
||||||
"New here? <a>Create an account</a>": "New here? <a>Create an account</a>",
|
"New here? <a>Create an account</a>": "New here? <a>Create an account</a>",
|
||||||
"Do not disturb": "Do not disturb",
|
"Do not disturb": "Do not disturb",
|
||||||
|
|
|
@ -360,6 +360,13 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
displayName: _td("Use new room breadcrumbs"),
|
displayName: _td("Use new room breadcrumbs"),
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
"feature_spotlight": {
|
||||||
|
isFeature: true,
|
||||||
|
labsGroup: LabGroup.Rooms,
|
||||||
|
supportedLevels: LEVELS_FEATURE,
|
||||||
|
displayName: _td("New spotlight search experience"),
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
"RoomList.backgroundImage": {
|
"RoomList.backgroundImage": {
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
default: null,
|
default: null,
|
||||||
|
@ -597,6 +604,11 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
supportedLevels: [SettingLevel.ACCOUNT],
|
supportedLevels: [SettingLevel.ACCOUNT],
|
||||||
default: [],
|
default: [],
|
||||||
},
|
},
|
||||||
|
"SpotlightSearch.recentSearches": {
|
||||||
|
// not really a setting
|
||||||
|
supportedLevels: [SettingLevel.ACCOUNT],
|
||||||
|
default: [], // list of room IDs, most recent first
|
||||||
|
},
|
||||||
"room_directory_servers": {
|
"room_directory_servers": {
|
||||||
supportedLevels: [SettingLevel.ACCOUNT],
|
supportedLevels: [SettingLevel.ACCOUNT],
|
||||||
default: [],
|
default: [],
|
||||||
|
|
|
@ -226,17 +226,6 @@ describe("RovingTabIndex", () => {
|
||||||
refs: [ref1],
|
refs: [ref1],
|
||||||
});
|
});
|
||||||
|
|
||||||
state = reducer(state, {
|
|
||||||
type: Type.Register,
|
|
||||||
payload: {
|
|
||||||
ref: ref1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(state).toStrictEqual({
|
|
||||||
activeRef: ref1,
|
|
||||||
refs: [ref1],
|
|
||||||
});
|
|
||||||
|
|
||||||
state = reducer(state, {
|
state = reducer(state, {
|
||||||
type: Type.Register,
|
type: Type.Register,
|
||||||
payload: {
|
payload: {
|
||||||
|
|
Loading…
Reference in a new issue