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:
committed by
GitHub
parent
596ad38260
commit
2a26afe438
@@ -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,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
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user