Redesign room search interface (#12677)
* Extract SearchInfo interface and SearchScope enum Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix in-progress and update behaviour of RoomSearchView Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Remove search button from legacy header Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Move search from aux panel to room summary card Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Wire up Cmd/Ctrl F for moved search field Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Use cpd space tokens Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Remove stale props Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix ctrl/cmd f search shortcut Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update Compound Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Revert the back button for now Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * i18n Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Cancel search on escape Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix missing X Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Improve coverage Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Extract SearchScope and SearchInfo into Searching Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix test Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Switch to icon button for cancel search Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * yarn.lock Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * lint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update screenshots Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * i18n Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update screenshots Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update screenshots Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update locators Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Revert screenshots Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update screenshots Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update snapshots * Discard changes to package.json * i18n Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Snapshots Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Handle narrow viewports Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Improve coverage Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Improve coverage Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Revert copy Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
|
@ -781,10 +781,10 @@ test.describe("Timeline", () => {
|
|||
|
||||
await page.locator(".mx_LegacyRoomHeader").getByRole("button", { name: "Search" }).click();
|
||||
|
||||
await expect(page.locator(".mx_SearchBar")).toMatchScreenshot("search-bar-on-timeline.png");
|
||||
await page.locator(".mx_RoomSummaryCard_search").getByRole("searchbox").fill("Message");
|
||||
await page.locator(".mx_RoomSummaryCard_search").getByRole("searchbox").press("Enter");
|
||||
|
||||
await page.locator(".mx_SearchBar_input").getByRole("textbox").fill("Message");
|
||||
await page.locator(".mx_SearchBar_input").getByRole("textbox").press("Enter");
|
||||
await expect(page.locator(".mx_RoomSearchAuxPanel")).toMatchScreenshot("search-aux-panel.png");
|
||||
|
||||
for (const locator of await page
|
||||
.locator(".mx_EventTile:not(.mx_EventTile_contextual) .mx_EventTile_searchHighlight")
|
||||
|
@ -822,8 +822,8 @@ test.describe("Timeline", () => {
|
|||
await page.locator(".mx_LegacyRoomHeader").getByRole("button", { name: "Search" }).click();
|
||||
|
||||
// Search the string to display both the message and TextualEvent on search results panel
|
||||
await page.locator(".mx_SearchBar").getByRole("textbox").fill(stringToSearch);
|
||||
await page.locator(".mx_SearchBar").getByRole("textbox").press("Enter");
|
||||
await page.locator(".mx_RoomSummaryCard_search").getByRole("searchbox").fill(stringToSearch);
|
||||
await page.locator(".mx_RoomSummaryCard_search").getByRole("searchbox").press("Enter");
|
||||
|
||||
// On search results panel
|
||||
const resultsPanel = page.locator(".mx_RoomView_searchResultsPanel");
|
||||
|
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 5 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 19 KiB |
|
@ -177,9 +177,9 @@ a:visited {
|
|||
color: $accent-alt;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="search"],
|
||||
input[type="password"] {
|
||||
:not(.mx_no_textinput):not(.mx_textinput):not(.mx_Field) > input[type="text"],
|
||||
:not(.mx_no_textinput):not(.mx_textinput):not(.mx_Field) > input[type="search"],
|
||||
:not(.mx_no_textinput):not(.mx_textinput):not(.mx_Field) > input[type="password"] {
|
||||
padding: 9px;
|
||||
font: var(--cpd-font-body-md-semibold);
|
||||
font-weight: var(--cpd-font-weight-semibold);
|
||||
|
|
|
@ -306,10 +306,10 @@
|
|||
@import "./views/rooms/_RoomListHeader.pcss";
|
||||
@import "./views/rooms/_RoomPreviewBar.pcss";
|
||||
@import "./views/rooms/_RoomPreviewCard.pcss";
|
||||
@import "./views/rooms/_RoomSearchAuxPanel.pcss";
|
||||
@import "./views/rooms/_RoomSublist.pcss";
|
||||
@import "./views/rooms/_RoomTile.pcss";
|
||||
@import "./views/rooms/_RoomUpgradeWarningBar.pcss";
|
||||
@import "./views/rooms/_SearchBar.pcss";
|
||||
@import "./views/rooms/_SendMessageComposer.pcss";
|
||||
@import "./views/rooms/_SpaceScopeHeader.pcss";
|
||||
@import "./views/rooms/_Stickers.pcss";
|
||||
|
|
|
@ -238,25 +238,12 @@ limitations under the License.
|
|||
padding: 15px 12px;
|
||||
}
|
||||
|
||||
.mx_RoomSummaryCard_search input {
|
||||
/* Overriding very broad CSS rules */
|
||||
border: 0 !important;
|
||||
margin: 0 !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
.mx_RoomSummaryCard_search {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
|
||||
.mx_RoomSummaryCard_searchBtn {
|
||||
background: var(--cpd-color-bg-canvas-default);
|
||||
color: var(--cpd-color-icon-primary);
|
||||
border: 1px solid var(--cpd-color-gray-400);
|
||||
border-radius: 50%;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: var(--cpd-space-2x);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--cpd-color-bg-subtle-primary);
|
||||
input[type="search"]::-webkit-search-cancel-button {
|
||||
display: unset; /* override _common.pcss which inhibits this */
|
||||
}
|
||||
}
|
||||
|
||||
|
|
72
res/css/views/rooms/_RoomSearchAuxPanel.pcss
Normal file
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
Copyright 2024 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_RoomSearchAuxPanel {
|
||||
/* use `min-height` rather than height, to allow room for the text to wrap if the window is narrow */
|
||||
min-height: 84px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-color: var(--cpd-color-bg-canvas-default);
|
||||
border-style: solid;
|
||||
border-width: 1px 0;
|
||||
padding: var(--cpd-space-3x);
|
||||
box-sizing: border-box;
|
||||
gap: var(--cpd-space-2x);
|
||||
|
||||
.mx_RoomSearchAuxPanel_summary {
|
||||
flex-grow: 1;
|
||||
display: inherit; /* flex */
|
||||
gap: var(--cpd-space-2x);
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
|
||||
> svg {
|
||||
padding: var(--cpd-space-2x);
|
||||
border-radius: var(--cpd-space-2x);
|
||||
background-color: var(--cpd-color-bg-subtle-secondary);
|
||||
color: var(--cpd-color-icon-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mx_RoomSearchAuxPanel_summary_text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-22px;
|
||||
overflow: hidden;
|
||||
|
||||
span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SearchWarning {
|
||||
display: contents;
|
||||
font-size: $font-13px;
|
||||
line-height: $font-20px;
|
||||
color: var(--cpd-color-text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomSearchAuxPanel_buttons {
|
||||
display: inherit; /* flex */
|
||||
gap: var(--cpd-space-6x);
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
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_SearchBar {
|
||||
/* use `min-height` rather than height, to allow room for the text to wrap if the window is narrow */
|
||||
min-height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid $primary-hairline-color;
|
||||
|
||||
.mx_SearchBar_input {
|
||||
--size-button-search: 37px; /* size of the search button inside `input` element */
|
||||
|
||||
/* border: 1px solid $input-border-color; */
|
||||
/* font-size: $font-15px; */
|
||||
flex: 1 1 0;
|
||||
margin-left: 22px;
|
||||
|
||||
/* do not allow the input element to shrink below the width needed for the placeholder 'Search…'
|
||||
and the search button */
|
||||
min-width: calc(7em + var(--size-button-search));
|
||||
|
||||
input {
|
||||
box-sizing: border-box; /* include padding value into width calculation */
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SearchBar_searchButton {
|
||||
cursor: pointer;
|
||||
width: var(--size-button-search);
|
||||
height: var(--size-button-search);
|
||||
background-color: $accent;
|
||||
mask: url("$(res)/img/feather-customised/search-input.svg");
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
}
|
||||
|
||||
.mx_SearchBar_buttons {
|
||||
display: inherit; /* flex */
|
||||
min-width: 0; /* have the close button displayed even on a very narrow timeline */
|
||||
}
|
||||
|
||||
.mx_SearchBar_button {
|
||||
border: 0;
|
||||
margin: 0 0 0 22px;
|
||||
padding: 5px;
|
||||
font-size: $font-15px;
|
||||
cursor: pointer;
|
||||
color: $primary-content;
|
||||
border-bottom: 2px solid $accent;
|
||||
font-weight: var(--cpd-font-weight-semibold);
|
||||
word-break: break-all; /* prevent the input area and cancel button from being overlapped by BaseCard */
|
||||
}
|
||||
|
||||
.mx_SearchBar_unselected {
|
||||
color: $input-darker-fg-color;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.mx_SearchBar_cancel {
|
||||
background-color: $alert;
|
||||
mask: url("$(res)/img/cancel.svg");
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: 14px;
|
||||
padding: 9px;
|
||||
margin: 0 12px 0 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, { ChangeEvent } from "react";
|
||||
import { Room, RoomState, RoomStateEvent, RoomMember, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { throttle } from "lodash";
|
||||
|
||||
|
@ -57,7 +57,8 @@ interface RoomlessProps extends BaseProps {
|
|||
interface RoomProps extends BaseProps {
|
||||
room: Room;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
onSearchClick?: () => void;
|
||||
onSearchChange?: (e: ChangeEvent) => void;
|
||||
onSearchCancel?: () => void;
|
||||
}
|
||||
|
||||
type Props = XOR<RoomlessProps, RoomProps>;
|
||||
|
@ -296,7 +297,9 @@ export default class RightPanel extends React.Component<Props, IState> {
|
|||
onClose={this.onClose}
|
||||
// whenever RightPanel is passed a room it is passed a permalinkcreator
|
||||
permalinkCreator={this.props.permalinkCreator!}
|
||||
onSearchClick={this.props.onSearchClick}
|
||||
onSearchChange={this.props.onSearchChange}
|
||||
onSearchCancel={this.props.onSearchCancel}
|
||||
focusRoomSearch={cardState?.focusRoomSearch}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -48,6 +48,7 @@ if (DEBUG) {
|
|||
interface Props {
|
||||
term: string;
|
||||
scope: SearchScope;
|
||||
inProgress: boolean;
|
||||
promise: Promise<ISearchResults>;
|
||||
abortController?: AbortController;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
|
@ -58,10 +59,9 @@ interface Props {
|
|||
// XXX: todo: merge overlapping results somehow?
|
||||
// XXX: why doesn't searching on name work?
|
||||
export const RoomSearchView = forwardRef<ScrollPanel, Props>(
|
||||
({ term, scope, promise, abortController, resizeNotifier, className, onUpdate }: Props, ref) => {
|
||||
({ term, scope, promise, abortController, resizeNotifier, className, onUpdate, inProgress }: Props, ref) => {
|
||||
const client = useContext(MatrixClientContext);
|
||||
const roomContext = useContext(RoomContext);
|
||||
const [inProgress, setInProgress] = useState(true);
|
||||
const [highlights, setHighlights] = useState<string[] | null>(null);
|
||||
const [results, setResults] = useState<ISearchResults | null>(null);
|
||||
const aborted = useRef(false);
|
||||
|
@ -78,10 +78,9 @@ export const RoomSearchView = forwardRef<ScrollPanel, Props>(
|
|||
|
||||
const handleSearchResult = useCallback(
|
||||
(searchPromise: Promise<ISearchResults>): Promise<boolean> => {
|
||||
setInProgress(true);
|
||||
onUpdate(true, null);
|
||||
|
||||
return searchPromise
|
||||
.then(
|
||||
return searchPromise.then(
|
||||
async (results): Promise<boolean> => {
|
||||
debuglog("search complete");
|
||||
if (aborted.current) {
|
||||
|
@ -125,6 +124,7 @@ export const RoomSearchView = forwardRef<ScrollPanel, Props>(
|
|||
|
||||
setHighlights(highlights);
|
||||
setResults({ ...results }); // copy to force a refresh
|
||||
onUpdate(false, results);
|
||||
return false;
|
||||
},
|
||||
(error) => {
|
||||
|
@ -137,14 +137,12 @@ export const RoomSearchView = forwardRef<ScrollPanel, Props>(
|
|||
title: _t("error_dialog|search_failed|title"),
|
||||
description: error?.message ?? _t("error_dialog|search_failed|server_unavailable"),
|
||||
});
|
||||
onUpdate(false, null);
|
||||
return false;
|
||||
},
|
||||
)
|
||||
.finally(() => {
|
||||
setInProgress(false);
|
||||
});
|
||||
);
|
||||
},
|
||||
[client, term],
|
||||
[client, term, onUpdate],
|
||||
);
|
||||
|
||||
// Mount & unmount effect
|
||||
|
|
|
@ -17,7 +17,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef, ReactElement, ReactNode, RefObject, useContext } from "react";
|
||||
import React, { ChangeEvent, createRef, ReactElement, ReactNode, RefObject, useContext } from "react";
|
||||
import classNames from "classnames";
|
||||
import {
|
||||
IRecommendedVersion,
|
||||
|
@ -41,7 +41,7 @@ import {
|
|||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||
import { throttle } from "lodash";
|
||||
import { debounce, throttle } from "lodash";
|
||||
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
||||
import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
||||
|
||||
|
@ -70,7 +70,6 @@ import TimelinePanel from "./TimelinePanel";
|
|||
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
||||
import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
|
||||
import RoomPreviewCard from "../views/rooms/RoomPreviewCard";
|
||||
import SearchBar from "../views/rooms/SearchBar";
|
||||
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
||||
import AuxPanel from "../views/rooms/AuxPanel";
|
||||
import LegacyRoomHeader from "../views/rooms/LegacyRoomHeader";
|
||||
|
@ -133,6 +132,7 @@ import { CancelAskToJoinPayload } from "../../dispatcher/payloads/CancelAskToJoi
|
|||
import { SubmitAskToJoinPayload } from "../../dispatcher/payloads/SubmitAskToJoinPayload";
|
||||
import RightPanelStore from "../../stores/right-panel/RightPanelStore";
|
||||
import { onView3pidInvite } from "../../stores/right-panel/action-handlers";
|
||||
import RoomSearchAuxPanel from "../views/rooms/RoomSearchAuxPanel";
|
||||
|
||||
const DEBUG = false;
|
||||
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
|
||||
|
@ -1196,9 +1196,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
);
|
||||
}
|
||||
break;
|
||||
case Action.FocusMessageSearch:
|
||||
this.onSearchClick();
|
||||
break;
|
||||
|
||||
case "local_room_event":
|
||||
this.onLocalRoomEvent(payload.roomId);
|
||||
|
@ -1725,13 +1722,14 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
});
|
||||
}
|
||||
|
||||
private onSearch = (term: string, scope: SearchScope): void => {
|
||||
private onSearch = (term: string, scope = SearchScope.Room): void => {
|
||||
const roomId = scope === SearchScope.Room ? this.getRoomId() : undefined;
|
||||
debuglog("sending search request");
|
||||
const abortController = new AbortController();
|
||||
const promise = eventSearch(this.context.client!, term, roomId, abortController.signal);
|
||||
|
||||
this.setState({
|
||||
timelineRenderingType: TimelineRenderingType.Search,
|
||||
search: {
|
||||
// make sure that we don't end up showing results from
|
||||
// an aborted search by keeping a unique id.
|
||||
|
@ -1745,6 +1743,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
});
|
||||
};
|
||||
|
||||
private onSearchScopeChange = (scope: SearchScope): void => {
|
||||
this.onSearch(this.state.search?.term ?? "", scope);
|
||||
};
|
||||
|
||||
private onSearchUpdate = (inProgress: boolean, searchResults: ISearchResults | null): void => {
|
||||
this.setState({
|
||||
search: {
|
||||
|
@ -1839,15 +1841,14 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
};
|
||||
|
||||
private onSearchClick = (): void => {
|
||||
if (this.state.timelineRenderingType === TimelineRenderingType.Search) {
|
||||
this.onCancelSearchClick();
|
||||
} else {
|
||||
this.setState({
|
||||
timelineRenderingType: TimelineRenderingType.Search,
|
||||
});
|
||||
}
|
||||
dis.fire(Action.FocusMessageSearch);
|
||||
};
|
||||
|
||||
private onSearchChange = debounce((e: ChangeEvent): void => {
|
||||
const term = (e.target as HTMLInputElement).value;
|
||||
this.onSearch(term);
|
||||
}, 300);
|
||||
|
||||
private onCancelSearchClick = (): Promise<void> => {
|
||||
return new Promise<void>((resolve) => {
|
||||
this.setState(
|
||||
|
@ -2328,10 +2329,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
let previewBar;
|
||||
if (this.state.timelineRenderingType === TimelineRenderingType.Search) {
|
||||
aux = (
|
||||
<SearchBar
|
||||
searchInProgress={this.state.search?.inProgress}
|
||||
<RoomSearchAuxPanel
|
||||
searchInfo={this.state.search}
|
||||
onCancelClick={this.onCancelSearchClick}
|
||||
onSearch={this.onSearch}
|
||||
onSearchScopeChange={this.onSearchScopeChange}
|
||||
isRoomEncrypted={this.context.client.isRoomEncrypted(this.state.room.roomId)}
|
||||
/>
|
||||
);
|
||||
|
@ -2438,6 +2439,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
scope={this.state.search.scope}
|
||||
promise={this.state.search.promise}
|
||||
abortController={this.state.search.abortController}
|
||||
inProgress={!!this.state.search.inProgress}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
className={this.messagePanelClassNames}
|
||||
onUpdate={this.onSearchUpdate}
|
||||
|
@ -2507,7 +2509,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
resizeNotifier={this.props.resizeNotifier}
|
||||
permalinkCreator={this.permalinkCreator}
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
onSearchClick={this.onSearchClick}
|
||||
onSearchChange={this.onSearchChange}
|
||||
onSearchCancel={this.onCancelSearchClick}
|
||||
/>
|
||||
) : undefined;
|
||||
|
||||
|
|
|
@ -55,7 +55,6 @@ export const WaitingForThirdPartyRoomView: React.FC<Props> = ({ roomView, resize
|
|||
<LegacyRoomHeader
|
||||
room={context.room}
|
||||
inRoom={true}
|
||||
onSearchClick={null}
|
||||
onInviteClick={null}
|
||||
onForgetClick={null}
|
||||
e2eStatus={E2EStatus.Normal}
|
||||
|
|
|
@ -33,9 +33,10 @@ export enum WarningKind {
|
|||
interface IProps {
|
||||
isRoomEncrypted?: boolean;
|
||||
kind: WarningKind;
|
||||
showLogo?: boolean;
|
||||
}
|
||||
|
||||
export default function SearchWarning({ isRoomEncrypted, kind }: IProps): JSX.Element {
|
||||
export default function SearchWarning({ isRoomEncrypted, kind, showLogo = true }: IProps): JSX.Element {
|
||||
if (!isRoomEncrypted) return <></>;
|
||||
if (EventIndexPeg.get()) return <></>;
|
||||
|
||||
|
@ -121,7 +122,7 @@ export default function SearchWarning({ isRoomEncrypted, kind }: IProps): JSX.El
|
|||
|
||||
return (
|
||||
<div className="mx_SearchWarning">
|
||||
{logo}
|
||||
{showLogo ? logo : null}
|
||||
<span>{text}</span>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -14,11 +14,19 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { SyntheticEvent, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
SyntheticEvent,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import classNames from "classnames";
|
||||
import {
|
||||
MenuItem,
|
||||
Tooltip,
|
||||
Separator,
|
||||
ToggleMenuItem,
|
||||
Text,
|
||||
|
@ -26,8 +34,9 @@ import {
|
|||
Heading,
|
||||
IconButton,
|
||||
Link,
|
||||
Search,
|
||||
Form,
|
||||
} from "@vector-im/compound-web";
|
||||
import { Icon as SearchIcon } from "@vector-im/compound-design-tokens/icons/search.svg";
|
||||
import { Icon as FavouriteIcon } from "@vector-im/compound-design-tokens/icons/favourite.svg";
|
||||
import { Icon as UserAddIcon } from "@vector-im/compound-design-tokens/icons/user-add.svg";
|
||||
import { Icon as UserProfileSolidIcon } from "@vector-im/compound-design-tokens/icons/user-profile-solid.svg";
|
||||
|
@ -63,7 +72,7 @@ import WidgetAvatar from "../avatars/WidgetAvatar";
|
|||
import WidgetStore, { IApp } from "../../../stores/WidgetStore";
|
||||
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||
import { UIComponent, UIFeature } from "../../../settings/UIFeature";
|
||||
import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
|
||||
import { WidgetContextMenu } from "../context_menus/WidgetContextMenu";
|
||||
|
@ -89,12 +98,18 @@ import { useTopic } from "../../../hooks/room/useTopic";
|
|||
import { Linkify, topicToHtml } from "../../../HtmlUtils";
|
||||
import { Box } from "../../utils/Box";
|
||||
import { onRoomTopicLinkClick } from "../elements/RoomTopic";
|
||||
import { useDispatcher } from "../../../hooks/useDispatcher";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { Key } from "../../../Keyboard";
|
||||
import { useTransition } from "../../../hooks/useTransition";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
onClose(): void;
|
||||
onSearchClick?: () => void;
|
||||
onSearchChange?: (e: ChangeEvent) => void;
|
||||
onSearchCancel?: () => void;
|
||||
focusRoomSearch?: boolean;
|
||||
}
|
||||
|
||||
interface IAppsSectionProps {
|
||||
|
@ -364,7 +379,14 @@ const RoomTopic: React.FC<Pick<IProps, "room">> = ({ room }): JSX.Element | null
|
|||
);
|
||||
};
|
||||
|
||||
const RoomSummaryCard: React.FC<IProps> = ({ room, permalinkCreator, onClose, onSearchClick }) => {
|
||||
const RoomSummaryCard: React.FC<IProps> = ({
|
||||
room,
|
||||
permalinkCreator,
|
||||
onClose,
|
||||
onSearchChange,
|
||||
onSearchCancel,
|
||||
focusRoomSearch,
|
||||
}) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
const onShareRoomClick = (): void => {
|
||||
|
@ -419,6 +441,26 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, permalinkCreator, onClose, on
|
|||
}
|
||||
}, [room, directRoomsList]);
|
||||
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
useDispatcher(defaultDispatcher, (payload) => {
|
||||
if (payload.action === Action.FocusMessageSearch) {
|
||||
searchInputRef.current?.focus();
|
||||
}
|
||||
});
|
||||
// Clear the search field when the user leaves the search view
|
||||
useTransition(
|
||||
(prevTimelineRenderingType) => {
|
||||
if (
|
||||
prevTimelineRenderingType === TimelineRenderingType.Search &&
|
||||
roomContext.timelineRenderingType !== TimelineRenderingType.Search &&
|
||||
searchInputRef.current
|
||||
) {
|
||||
searchInputRef.current.value = "";
|
||||
}
|
||||
},
|
||||
[roomContext.timelineRenderingType],
|
||||
);
|
||||
|
||||
const alias = room.getCanonicalAlias() || room.getAltAliases()[0] || "";
|
||||
const header = (
|
||||
<header className="mx_RoomSummaryCard_container">
|
||||
|
@ -498,18 +540,24 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, permalinkCreator, onClose, on
|
|||
align="center"
|
||||
justify="space-between"
|
||||
>
|
||||
<Tooltip label={_t("action|search")} placement="right">
|
||||
<button
|
||||
className="mx_RoomSummaryCard_searchBtn"
|
||||
data-testid="summary-search"
|
||||
onClick={() => {
|
||||
onSearchClick?.();
|
||||
{onSearchChange && (
|
||||
<Form.Root className="mx_RoomSummaryCard_search" onSubmit={(e) => e.preventDefault()}>
|
||||
<Search
|
||||
placeholder={_t("room|search|placeholder")}
|
||||
name="room_message_search"
|
||||
onChange={onSearchChange}
|
||||
className="mx_no_textinput"
|
||||
ref={searchInputRef}
|
||||
autoFocus={focusRoomSearch}
|
||||
onKeyDown={(e) => {
|
||||
if (searchInputRef.current && e.key === Key.ESCAPE) {
|
||||
searchInputRef.current.value = "";
|
||||
onSearchCancel?.();
|
||||
}
|
||||
}}
|
||||
aria-label={_t("action|search")}
|
||||
>
|
||||
<SearchIcon width="100%" height="100%" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
/>
|
||||
</Form.Root>
|
||||
)}
|
||||
<AccessibleButton
|
||||
data-testid="base-card-close-button"
|
||||
className="mx_BaseCard_close"
|
||||
|
|
79
src/components/views/rooms/RoomSearchAuxPanel.tsx
Normal file
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
Copyright 2024 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 from "react";
|
||||
import { Icon as SearchIcon } from "@vector-im/compound-design-tokens/icons/search.svg";
|
||||
import { Icon as CloseIcon } from "@vector-im/compound-design-tokens/icons/close.svg";
|
||||
import { IconButton, Link } from "@vector-im/compound-web";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { PosthogScreenTracker } from "../../../PosthogTrackers";
|
||||
import SearchWarning, { WarningKind } from "../elements/SearchWarning";
|
||||
import { SearchInfo, SearchScope } from "../../../Searching";
|
||||
|
||||
interface Props {
|
||||
searchInfo?: SearchInfo;
|
||||
isRoomEncrypted: boolean;
|
||||
onSearchScopeChange(scope: SearchScope): void;
|
||||
onCancelClick(): void;
|
||||
}
|
||||
|
||||
const RoomSearchAuxPanel: React.FC<Props> = ({ searchInfo, isRoomEncrypted, onSearchScopeChange, onCancelClick }) => {
|
||||
const scope = searchInfo?.scope ?? SearchScope.Room;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PosthogScreenTracker screenName="RoomSearch" />
|
||||
<div className="mx_RoomSearchAuxPanel">
|
||||
<div className="mx_RoomSearchAuxPanel_summary">
|
||||
<SearchIcon width="24px" height="24px" />
|
||||
<div className="mx_RoomSearchAuxPanel_summary_text">
|
||||
{searchInfo
|
||||
? _t(
|
||||
"room|search|summary",
|
||||
{ count: searchInfo.count ?? 0 },
|
||||
{ query: () => <b>{searchInfo.term}</b> },
|
||||
)
|
||||
: undefined}
|
||||
<SearchWarning kind={WarningKind.Search} isRoomEncrypted={isRoomEncrypted} showLogo={false} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_RoomSearchAuxPanel_buttons">
|
||||
<Link
|
||||
onClick={() =>
|
||||
onSearchScopeChange(scope === SearchScope.Room ? SearchScope.All : SearchScope.Room)
|
||||
}
|
||||
kind="primary"
|
||||
>
|
||||
{scope === SearchScope.All
|
||||
? _t("room|search|this_room_button")
|
||||
: _t("room|search|all_rooms_button")}
|
||||
</Link>
|
||||
<IconButton
|
||||
onClick={onCancelClick}
|
||||
destructive
|
||||
tooltip={_t("action|cancel")}
|
||||
aria-label={_t("action|cancel")}
|
||||
>
|
||||
<CloseIcon width="20px" height="20px" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoomSearchAuxPanel;
|
|
@ -1,143 +0,0 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2020 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, { createRef, RefObject } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { PosthogScreenTracker } from "../../../PosthogTrackers";
|
||||
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||
import SearchWarning, { WarningKind } from "../elements/SearchWarning";
|
||||
import { SearchScope } from "../../../Searching";
|
||||
|
||||
interface IProps {
|
||||
onCancelClick: () => void;
|
||||
onSearch: (query: string, scope: SearchScope) => void;
|
||||
searchInProgress?: boolean;
|
||||
isRoomEncrypted?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
scope: SearchScope;
|
||||
}
|
||||
|
||||
export default class SearchBar extends React.Component<IProps, IState> {
|
||||
private searchTerm: RefObject<HTMLInputElement> = createRef();
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
scope: SearchScope.Room,
|
||||
};
|
||||
}
|
||||
|
||||
private onThisRoomClick = (): void => {
|
||||
this.setState({ scope: SearchScope.Room }, () => this.searchIfQuery());
|
||||
};
|
||||
|
||||
private onAllRoomsClick = (): void => {
|
||||
this.setState({ scope: SearchScope.All }, () => this.searchIfQuery());
|
||||
};
|
||||
|
||||
private onSearchChange = (e: React.KeyboardEvent): void => {
|
||||
const action = getKeyBindingsManager().getAccessibilityAction(e);
|
||||
switch (action) {
|
||||
case KeyBindingAction.Enter:
|
||||
this.onSearch();
|
||||
break;
|
||||
case KeyBindingAction.Escape:
|
||||
this.props.onCancelClick();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private searchIfQuery(): void {
|
||||
if (this.searchTerm.current?.value) {
|
||||
this.onSearch();
|
||||
}
|
||||
}
|
||||
|
||||
private onSearch = (): void => {
|
||||
if (!this.searchTerm.current?.value.trim()) return;
|
||||
this.props.onSearch(this.searchTerm.current.value, this.state.scope);
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const searchButtonClasses = classNames("mx_SearchBar_searchButton", {
|
||||
mx_SearchBar_searching: this.props.searchInProgress,
|
||||
});
|
||||
const thisRoomClasses = classNames("mx_SearchBar_button", {
|
||||
mx_SearchBar_unselected: this.state.scope !== SearchScope.Room,
|
||||
});
|
||||
const allRoomsClasses = classNames("mx_SearchBar_button", {
|
||||
mx_SearchBar_unselected: this.state.scope !== SearchScope.All,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<PosthogScreenTracker screenName="RoomSearch" />
|
||||
<div className="mx_SearchBar">
|
||||
<div className="mx_SearchBar_buttons" role="radiogroup">
|
||||
<AccessibleButton
|
||||
className={thisRoomClasses}
|
||||
onClick={this.onThisRoomClick}
|
||||
aria-checked={this.state.scope === SearchScope.Room}
|
||||
role="radio"
|
||||
>
|
||||
{_t("room|search|this_room")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
className={allRoomsClasses}
|
||||
onClick={this.onAllRoomsClick}
|
||||
aria-checked={this.state.scope === SearchScope.All}
|
||||
role="radio"
|
||||
>
|
||||
{_t("room|search|all_rooms")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div className="mx_SearchBar_input mx_textinput">
|
||||
<input
|
||||
ref={this.searchTerm}
|
||||
type="text"
|
||||
autoFocus={true}
|
||||
placeholder={_t("room|search|field_placeholder")}
|
||||
aria-label={
|
||||
this.state.scope === SearchScope.Room
|
||||
? _t("room|search|this_room_button")
|
||||
: _t("room|search|all_rooms_button")
|
||||
}
|
||||
onKeyDown={this.onSearchChange}
|
||||
/>
|
||||
<AccessibleButton
|
||||
className={searchButtonClasses}
|
||||
onClick={this.onSearch}
|
||||
aria-label={_t("action|search")}
|
||||
/>
|
||||
</div>
|
||||
<AccessibleButton
|
||||
className="mx_SearchBar_cancel"
|
||||
onClick={this.props.onCancelClick}
|
||||
aria-label={_t("action|cancel")}
|
||||
/>
|
||||
</div>
|
||||
<SearchWarning isRoomEncrypted={this.props.isRoomEncrypted} kind={WarningKind.Search} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
35
src/hooks/useTransition.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
Copyright 2024 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.
|
||||
*/
|
||||
|
||||
// Based on https://stackoverflow.com/a/61680184
|
||||
|
||||
import { DependencyList, useEffect, useRef } from "react";
|
||||
|
||||
export const useTransition = <D extends DependencyList>(callback: (...params: D) => void, deps: D): void => {
|
||||
const func = useRef<(...params: D) => void>(callback);
|
||||
|
||||
useEffect(() => {
|
||||
func.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
const args = useRef<D | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (args.current !== null) func.current(...args.current);
|
||||
args.current = deps;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, deps);
|
||||
};
|
|
@ -2030,14 +2030,16 @@
|
|||
"rejecting": "Rejecting invite…",
|
||||
"rejoin_button": "Re-join",
|
||||
"search": {
|
||||
"all_rooms": "All Rooms",
|
||||
"all_rooms_button": "Search all rooms",
|
||||
"field_placeholder": "Search…",
|
||||
"placeholder": "Search messages…",
|
||||
"result_count": {
|
||||
"one": "(~%(count)s result)",
|
||||
"other": "(~%(count)s results)"
|
||||
},
|
||||
"this_room": "This Room",
|
||||
"summary": {
|
||||
"one": "1 result found for “<query/>”",
|
||||
"other": "%(count)s results found for “<query/>”"
|
||||
},
|
||||
"this_room_button": "Search this room"
|
||||
},
|
||||
"show_labs_settings": "Show Labs settings",
|
||||
|
|
|
@ -77,10 +77,19 @@ export default class RightPanelStore extends ReadyWatchingStore {
|
|||
}
|
||||
|
||||
protected onDispatcherAction(payload: ActionPayload): void {
|
||||
if (payload.action !== Action.ActiveRoomChanged) return;
|
||||
|
||||
switch (payload.action) {
|
||||
case Action.ActiveRoomChanged: {
|
||||
const changePayload = <ActiveRoomChangedPayload>payload;
|
||||
this.handleViewedRoomChange(changePayload.oldRoomId, changePayload.newRoomId);
|
||||
break;
|
||||
}
|
||||
|
||||
case Action.FocusMessageSearch: {
|
||||
if (this.currentCard.phase !== RightPanelPhases.RoomSummary) {
|
||||
this.setCard({ phase: RightPanelPhases.RoomSummary, state: { focusRoomSearch: true } });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
|
|
@ -32,6 +32,8 @@ export interface IRightPanelCardState {
|
|||
initialEvent?: MatrixEvent;
|
||||
isInitialEventHighlighted?: boolean;
|
||||
initialEventScrollIntoView?: boolean;
|
||||
// room summary
|
||||
focusRoomSearch?: boolean;
|
||||
}
|
||||
|
||||
export interface IRightPanelCardStateStored {
|
||||
|
|
|
@ -65,6 +65,7 @@ describe("<RoomSearchView/>", () => {
|
|||
|
||||
render(
|
||||
<RoomSearchView
|
||||
inProgress={true}
|
||||
term="search term"
|
||||
scope={SearchScope.All}
|
||||
promise={deferred.promise}
|
||||
|
@ -81,6 +82,7 @@ describe("<RoomSearchView/>", () => {
|
|||
render(
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<RoomSearchView
|
||||
inProgress={false}
|
||||
term="search term"
|
||||
scope={SearchScope.All}
|
||||
promise={Promise.resolve<ISearchResults>({
|
||||
|
@ -142,6 +144,7 @@ describe("<RoomSearchView/>", () => {
|
|||
render(
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<RoomSearchView
|
||||
inProgress={false}
|
||||
term="search term"
|
||||
scope={SearchScope.Room}
|
||||
promise={Promise.resolve<ISearchResults>({
|
||||
|
@ -234,10 +237,30 @@ describe("<RoomSearchView/>", () => {
|
|||
],
|
||||
next_batch: undefined,
|
||||
});
|
||||
const onUpdate = jest.fn();
|
||||
|
||||
render(
|
||||
const { rerender } = render(
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<RoomSearchView
|
||||
inProgress={true}
|
||||
term="search term"
|
||||
scope={SearchScope.All}
|
||||
promise={Promise.resolve(searchResults)}
|
||||
resizeNotifier={resizeNotifier}
|
||||
className="someClass"
|
||||
onUpdate={onUpdate}
|
||||
/>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
await screen.findByRole("progressbar");
|
||||
await screen.findByText("Potato");
|
||||
expect(onUpdate).toHaveBeenCalledWith(false, expect.objectContaining({}));
|
||||
|
||||
rerender(
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<RoomSearchView
|
||||
inProgress={false}
|
||||
term="search term"
|
||||
scope={SearchScope.All}
|
||||
promise={Promise.resolve(searchResults)}
|
||||
|
@ -248,8 +271,6 @@ describe("<RoomSearchView/>", () => {
|
|||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
await screen.findByRole("progressbar");
|
||||
await screen.findByText("Potato");
|
||||
expect(screen.queryByRole("progressbar")).toBeFalsy();
|
||||
});
|
||||
|
||||
|
@ -259,6 +280,7 @@ describe("<RoomSearchView/>", () => {
|
|||
const { unmount } = render(
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<RoomSearchView
|
||||
inProgress={false}
|
||||
term="search term"
|
||||
scope={SearchScope.All}
|
||||
promise={deferred.promise}
|
||||
|
@ -282,6 +304,7 @@ describe("<RoomSearchView/>", () => {
|
|||
const { unmount } = render(
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<RoomSearchView
|
||||
inProgress={false}
|
||||
term="search term"
|
||||
scope={SearchScope.All}
|
||||
promise={deferred.promise}
|
||||
|
@ -305,6 +328,7 @@ describe("<RoomSearchView/>", () => {
|
|||
render(
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<RoomSearchView
|
||||
inProgress={false}
|
||||
term="search term"
|
||||
scope={SearchScope.All}
|
||||
promise={deferred.promise}
|
||||
|
@ -406,6 +430,7 @@ describe("<RoomSearchView/>", () => {
|
|||
render(
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<RoomSearchView
|
||||
inProgress={false}
|
||||
term="search term"
|
||||
scope={SearchScope.All}
|
||||
promise={Promise.resolve(searchResults)}
|
||||
|
@ -440,6 +465,7 @@ describe("<RoomSearchView/>", () => {
|
|||
render(
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<RoomSearchView
|
||||
inProgress={false}
|
||||
term="search term"
|
||||
scope={SearchScope.All}
|
||||
promise={Promise.resolve<ISearchResults>({
|
||||
|
|
51
test/components/views/elements/SearchWarning-test.tsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
Copyright 2024 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 { render } from "@testing-library/react";
|
||||
import React from "react";
|
||||
|
||||
import SdkConfig from "../../../../src/SdkConfig";
|
||||
import SearchWarning, { WarningKind } from "../../../../src/components/views/elements/SearchWarning";
|
||||
|
||||
describe("<SearchWarning />", () => {
|
||||
describe("with desktop builds available", () => {
|
||||
beforeEach(() => {
|
||||
SdkConfig.put({
|
||||
brand: "Element",
|
||||
desktop_builds: {
|
||||
available: true,
|
||||
logo: "https://logo",
|
||||
url: "https://url",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("renders with a logo by default", () => {
|
||||
const { asFragment, queryByRole } = render(
|
||||
<SearchWarning isRoomEncrypted={true} kind={WarningKind.Search} />,
|
||||
);
|
||||
expect(queryByRole("img")).toBeInTheDocument();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders without a logo when showLogo=false", () => {
|
||||
const { asFragment, queryByRole } = render(
|
||||
<SearchWarning isRoomEncrypted={true} kind={WarningKind.Search} showLogo={false} />,
|
||||
);
|
||||
|
||||
expect(queryByRole("img")).not.toBeInTheDocument();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<SearchWarning /> with desktop builds available renders with a logo by default 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_SearchWarning"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
src="https://logo"
|
||||
width="32px"
|
||||
/>
|
||||
<span>
|
||||
<span>
|
||||
Use the
|
||||
<a
|
||||
href="https://url"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Desktop app
|
||||
</a>
|
||||
to search encrypted messages
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<SearchWarning /> with desktop builds available renders without a logo when showLogo=false 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_SearchWarning"
|
||||
>
|
||||
<span>
|
||||
<span>
|
||||
Use the
|
||||
<a
|
||||
href="https://url"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Desktop app
|
||||
</a>
|
||||
to search encrypted messages
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -15,10 +15,11 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, fireEvent, screen } from "@testing-library/react";
|
||||
import { render, fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import { EventType, MatrixEvent, Room, MatrixClient, JoinRule } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { mocked, MockedObject } from "jest-mock";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||
import RoomSummaryCard from "../../../../src/components/views/right_panel/RoomSummaryCard";
|
||||
|
@ -37,6 +38,8 @@ import { _t } from "../../../../src/languageHandler";
|
|||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
import { tagRoom } from "../../../../src/utils/room/tagRoom";
|
||||
import { DefaultTagID } from "../../../../src/stores/room-list/models";
|
||||
import { Action } from "../../../../src/dispatcher/actions";
|
||||
import RoomContext, { TimelineRenderingType } from "../../../../src/contexts/RoomContext";
|
||||
|
||||
jest.mock("../../../../src/utils/room/tagRoom");
|
||||
|
||||
|
@ -141,15 +144,82 @@ describe("<RoomSummaryCard />", () => {
|
|||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("opens the search", async () => {
|
||||
const onSearchClick = jest.fn();
|
||||
const { getByLabelText } = getComponent({
|
||||
onSearchClick,
|
||||
describe("search", () => {
|
||||
it("has the search field", async () => {
|
||||
const onSearchChange = jest.fn();
|
||||
const { getByPlaceholderText } = getComponent({
|
||||
onSearchChange,
|
||||
});
|
||||
expect(getByPlaceholderText("Search messages…")).toBeVisible();
|
||||
});
|
||||
|
||||
const searchBtn = getByLabelText(_t("action|search"));
|
||||
fireEvent.click(searchBtn);
|
||||
expect(onSearchClick).toHaveBeenCalled();
|
||||
it("should focus the search field if Action.FocusMessageSearch is fired", async () => {
|
||||
const onSearchChange = jest.fn();
|
||||
const { getByPlaceholderText } = getComponent({
|
||||
onSearchChange,
|
||||
});
|
||||
expect(getByPlaceholderText("Search messages…")).not.toHaveFocus();
|
||||
defaultDispatcher.fire(Action.FocusMessageSearch);
|
||||
await waitFor(() => {
|
||||
expect(getByPlaceholderText("Search messages…")).toHaveFocus();
|
||||
});
|
||||
});
|
||||
|
||||
it("should focus the search field if focusRoomSearch=true", () => {
|
||||
const onSearchChange = jest.fn();
|
||||
const { getByPlaceholderText } = getComponent({
|
||||
onSearchChange,
|
||||
focusRoomSearch: true,
|
||||
});
|
||||
expect(getByPlaceholderText("Search messages…")).toHaveFocus();
|
||||
});
|
||||
|
||||
it("should cancel search on escape", () => {
|
||||
const onSearchChange = jest.fn();
|
||||
const onSearchCancel = jest.fn();
|
||||
const { getByPlaceholderText } = getComponent({
|
||||
onSearchChange,
|
||||
onSearchCancel,
|
||||
focusRoomSearch: true,
|
||||
});
|
||||
expect(getByPlaceholderText("Search messages…")).toHaveFocus();
|
||||
fireEvent.keyDown(getByPlaceholderText("Search messages…"), { key: "Escape" });
|
||||
expect(onSearchCancel).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should empty search field when the timeline rendering type changes away", async () => {
|
||||
const onSearchChange = jest.fn();
|
||||
const { rerender } = render(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<RoomContext.Provider value={{ timelineRenderingType: TimelineRenderingType.Search } as any}>
|
||||
<RoomSummaryCard
|
||||
room={room}
|
||||
permalinkCreator={new RoomPermalinkCreator(room)}
|
||||
onClose={jest.fn()}
|
||||
onSearchChange={onSearchChange}
|
||||
focusRoomSearch={true}
|
||||
/>
|
||||
</RoomContext.Provider>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
await userEvent.type(screen.getByPlaceholderText("Search messages…"), "test");
|
||||
expect(screen.getByPlaceholderText("Search messages…")).toHaveValue("test");
|
||||
|
||||
rerender(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<RoomContext.Provider value={{ timelineRenderingType: TimelineRenderingType.Room } as any}>
|
||||
<RoomSummaryCard
|
||||
room={room}
|
||||
permalinkCreator={new RoomPermalinkCreator(room)}
|
||||
onClose={jest.fn()}
|
||||
onSearchChange={onSearchChange}
|
||||
/>
|
||||
</RoomContext.Provider>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
expect(screen.getByPlaceholderText("Search messages…")).toHaveValue("");
|
||||
});
|
||||
});
|
||||
|
||||
it("opens room file panel on button click", () => {
|
||||
|
|
|
@ -13,16 +13,6 @@ exports[`<RoomSummaryCard /> has button to edit topic when expanded 1`] = `
|
|||
class="mx_Flex mx_RoomSummaryCard_header"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
>
|
||||
<button
|
||||
aria-label="Search"
|
||||
class="mx_RoomSummaryCard_searchBtn"
|
||||
data-testid="summary-search"
|
||||
>
|
||||
<div
|
||||
height="100%"
|
||||
width="100%"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
aria-label="Close"
|
||||
class="mx_AccessibleButton mx_BaseCard_close"
|
||||
|
@ -443,16 +433,6 @@ exports[`<RoomSummaryCard /> renders the room summary 1`] = `
|
|||
class="mx_Flex mx_RoomSummaryCard_header"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
>
|
||||
<button
|
||||
aria-label="Search"
|
||||
class="mx_RoomSummaryCard_searchBtn"
|
||||
data-testid="summary-search"
|
||||
>
|
||||
<div
|
||||
height="100%"
|
||||
width="100%"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
aria-label="Close"
|
||||
class="mx_AccessibleButton mx_BaseCard_close"
|
||||
|
@ -846,16 +826,6 @@ exports[`<RoomSummaryCard /> renders the room topic in the summary 1`] = `
|
|||
class="mx_Flex mx_RoomSummaryCard_header"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
>
|
||||
<button
|
||||
aria-label="Search"
|
||||
class="mx_RoomSummaryCard_searchBtn"
|
||||
data-testid="summary-search"
|
||||
>
|
||||
<div
|
||||
height="100%"
|
||||
width="100%"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
aria-label="Close"
|
||||
class="mx_AccessibleButton mx_BaseCard_close"
|
||||
|
|
95
test/components/views/rooms/RoomSearchAuxPanel-test.tsx
Normal file
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
Copyright 2024 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 from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
|
||||
import RoomSearchAuxPanel from "../../../../src/components/views/rooms/RoomSearchAuxPanel";
|
||||
import { SearchScope } from "../../../../src/Searching";
|
||||
|
||||
describe("RoomSearchAuxPanel", () => {
|
||||
it("should render the count of results", () => {
|
||||
render(
|
||||
<RoomSearchAuxPanel
|
||||
searchInfo={{
|
||||
searchId: 1234,
|
||||
count: 5,
|
||||
term: "abcd",
|
||||
scope: SearchScope.Room,
|
||||
promise: new Promise(() => {}),
|
||||
}}
|
||||
isRoomEncrypted={false}
|
||||
onSearchScopeChange={jest.fn()}
|
||||
onCancelClick={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("5 results found for", { exact: false })).toHaveTextContent(
|
||||
"5 results found for “abcd”",
|
||||
);
|
||||
});
|
||||
|
||||
it("should allow the user to toggle to all rooms search", async () => {
|
||||
const onSearchScopeChange = jest.fn();
|
||||
|
||||
render(
|
||||
<RoomSearchAuxPanel
|
||||
isRoomEncrypted={false}
|
||||
onSearchScopeChange={onSearchScopeChange}
|
||||
onCancelClick={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
screen.getByText("Search all rooms").click();
|
||||
expect(onSearchScopeChange).toHaveBeenCalledWith(SearchScope.All);
|
||||
});
|
||||
|
||||
it("should allow the user to toggle back to room-specific search", async () => {
|
||||
const onSearchScopeChange = jest.fn();
|
||||
|
||||
render(
|
||||
<RoomSearchAuxPanel
|
||||
searchInfo={{
|
||||
searchId: 1234,
|
||||
term: "abcd",
|
||||
scope: SearchScope.All,
|
||||
promise: new Promise(() => {}),
|
||||
}}
|
||||
isRoomEncrypted={false}
|
||||
onSearchScopeChange={onSearchScopeChange}
|
||||
onCancelClick={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
screen.getByText("Search this room").click();
|
||||
expect(onSearchScopeChange).toHaveBeenCalledWith(SearchScope.Room);
|
||||
});
|
||||
|
||||
it("should allow the user to cancel a search", async () => {
|
||||
const onCancelClick = jest.fn();
|
||||
|
||||
render(
|
||||
<RoomSearchAuxPanel
|
||||
isRoomEncrypted={false}
|
||||
onSearchScopeChange={jest.fn()}
|
||||
onCancelClick={onCancelClick}
|
||||
/>,
|
||||
);
|
||||
|
||||
screen.getByLabelText("Cancel").click();
|
||||
expect(onCancelClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -1,98 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 Emmanuel Ezeka <eec.studies@gmail.com>
|
||||
|
||||
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 from "react";
|
||||
import { fireEvent, render } from "@testing-library/react";
|
||||
|
||||
import SearchBar from "../../../../src/components/views/rooms/SearchBar";
|
||||
import { KeyBindingAction } from "../../../../src/accessibility/KeyboardShortcuts";
|
||||
import { SearchScope } from "../../../../src/Searching";
|
||||
|
||||
let mockCurrentEvent = KeyBindingAction.Enter;
|
||||
|
||||
const searchProps = {
|
||||
onCancelClick: jest.fn(),
|
||||
onSearch: jest.fn(),
|
||||
searchInProgress: false,
|
||||
isRoomEncrypted: false,
|
||||
};
|
||||
|
||||
jest.mock("../../../../src/KeyBindingsManager", () => ({
|
||||
__esModule: true,
|
||||
getKeyBindingsManager: jest.fn(() => ({ getAccessibilityAction: jest.fn(() => mockCurrentEvent) })),
|
||||
}));
|
||||
|
||||
describe("SearchBar", () => {
|
||||
afterEach(() => {
|
||||
searchProps.onCancelClick.mockClear();
|
||||
searchProps.onSearch.mockClear();
|
||||
});
|
||||
|
||||
it("must not search when input value is empty", () => {
|
||||
const { container } = render(<SearchBar {...searchProps} />);
|
||||
const roomButtons = container.querySelectorAll(".mx_SearchBar_button");
|
||||
const searchButton = container.querySelectorAll(".mx_SearchBar_searchButton");
|
||||
|
||||
expect(roomButtons.length).toEqual(2);
|
||||
|
||||
fireEvent.click(searchButton[0]);
|
||||
fireEvent.click(roomButtons[0]);
|
||||
fireEvent.click(roomButtons[1]);
|
||||
|
||||
expect(searchProps.onSearch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("must trigger onSearch when value is not empty", () => {
|
||||
const { container } = render(<SearchBar {...searchProps} />);
|
||||
const searchValue = "abcd";
|
||||
|
||||
const roomButtons = container.querySelectorAll(".mx_SearchBar_button");
|
||||
const searchButton = container.querySelectorAll(".mx_SearchBar_searchButton");
|
||||
const input = container.querySelector<HTMLInputElement>(".mx_SearchBar_input input");
|
||||
input!.value = searchValue;
|
||||
|
||||
expect(roomButtons.length).toEqual(2);
|
||||
|
||||
fireEvent.click(searchButton[0]);
|
||||
|
||||
expect(searchProps.onSearch).toHaveBeenCalledTimes(1);
|
||||
expect(searchProps.onSearch).toHaveBeenNthCalledWith(1, searchValue, SearchScope.Room);
|
||||
|
||||
fireEvent.click(roomButtons[0]);
|
||||
|
||||
expect(searchProps.onSearch).toHaveBeenCalledTimes(2);
|
||||
expect(searchProps.onSearch).toHaveBeenNthCalledWith(2, searchValue, SearchScope.Room);
|
||||
|
||||
fireEvent.click(roomButtons[1]);
|
||||
|
||||
expect(searchProps.onSearch).toHaveBeenCalledTimes(3);
|
||||
expect(searchProps.onSearch).toHaveBeenNthCalledWith(3, searchValue, SearchScope.All);
|
||||
});
|
||||
|
||||
it("cancel button and esc key should trigger onCancelClick", async () => {
|
||||
mockCurrentEvent = KeyBindingAction.Escape;
|
||||
const { container } = render(<SearchBar {...searchProps} />);
|
||||
const cancelButton = container.querySelector(".mx_SearchBar_cancel");
|
||||
const input = container.querySelector(".mx_SearchBar_input input");
|
||||
fireEvent.click(cancelButton!);
|
||||
expect(searchProps.onCancelClick).toHaveBeenCalledTimes(1);
|
||||
|
||||
fireEvent.focus(input!);
|
||||
fireEvent.keyDown(input!, { key: "Escape", code: "Escape", charCode: 27 });
|
||||
|
||||
expect(searchProps.onCancelClick).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|