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>
This commit is contained in:
Michael Telatynski
2024-07-08 10:57:41 +01:00
committed by GitHub
parent 596ad38260
commit 2a26afe438
33 changed files with 675 additions and 499 deletions

View File

@@ -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}
/>
);
}

View File

@@ -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,73 +78,71 @@ export const RoomSearchView = forwardRef<ScrollPanel, Props>(
const handleSearchResult = useCallback(
(searchPromise: Promise<ISearchResults>): Promise<boolean> => {
setInProgress(true);
onUpdate(true, null);
return searchPromise
.then(
async (results): Promise<boolean> => {
debuglog("search complete");
if (aborted.current) {
logger.error("Discarding stale search results");
return false;
}
return searchPromise.then(
async (results): Promise<boolean> => {
debuglog("search complete");
if (aborted.current) {
logger.error("Discarding stale search results");
return false;
}
// postgres on synapse returns us precise details of the strings
// which actually got matched for highlighting.
//
// In either case, we want to highlight the literal search term
// whether it was used by the search engine or not.
// postgres on synapse returns us precise details of the strings
// which actually got matched for highlighting.
//
// In either case, we want to highlight the literal search term
// whether it was used by the search engine or not.
let highlights = results.highlights;
if (!highlights.includes(term)) {
highlights = highlights.concat(term);
}
let highlights = results.highlights;
if (!highlights.includes(term)) {
highlights = highlights.concat(term);
}
// For overlapping highlights,
// favour longer (more specific) terms first
highlights = highlights.sort(function (a, b) {
return b.length - a.length;
});
// For overlapping highlights,
// favour longer (more specific) terms first
highlights = highlights.sort(function (a, b) {
return b.length - a.length;
});
for (const result of results.results) {
for (const event of result.context.getTimeline()) {
const bundledRelationship =
event.getServerAggregatedRelation<IThreadBundledRelationship>(
THREAD_RELATION_TYPE.name,
);
if (!bundledRelationship || event.getThread()) continue;
const room = client.getRoom(event.getRoomId());
const thread = room?.findThreadForEvent(event);
if (thread) {
event.setThread(thread);
} else {
room?.createThread(event.getId()!, event, [], true);
}
for (const result of results.results) {
for (const event of result.context.getTimeline()) {
const bundledRelationship =
event.getServerAggregatedRelation<IThreadBundledRelationship>(
THREAD_RELATION_TYPE.name,
);
if (!bundledRelationship || event.getThread()) continue;
const room = client.getRoom(event.getRoomId());
const thread = room?.findThreadForEvent(event);
if (thread) {
event.setThread(thread);
} else {
room?.createThread(event.getId()!, event, [], true);
}
}
}
setHighlights(highlights);
setResults({ ...results }); // copy to force a refresh
setHighlights(highlights);
setResults({ ...results }); // copy to force a refresh
onUpdate(false, results);
return false;
},
(error) => {
if (aborted.current) {
logger.error("Discarding stale search results");
return false;
},
(error) => {
if (aborted.current) {
logger.error("Discarding stale search results");
return false;
}
logger.error("Search failed", error);
Modal.createDialog(ErrorDialog, {
title: _t("error_dialog|search_failed|title"),
description: error?.message ?? _t("error_dialog|search_failed|server_unavailable"),
});
return false;
},
)
.finally(() => {
setInProgress(false);
});
}
logger.error("Search failed", error);
Modal.createDialog(ErrorDialog, {
title: _t("error_dialog|search_failed|title"),
description: error?.message ?? _t("error_dialog|search_failed|server_unavailable"),
});
onUpdate(false, null);
return false;
},
);
},
[client, term],
[client, term, onUpdate],
);
// Mount & unmount effect

View File

@@ -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;

View File

@@ -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}